rasuvaeff/yii3-mcp
Composer 安装命令:
composer require rasuvaeff/yii3-mcp
包简介
MCP server integration for Yii3: PSR-15 Streamable HTTP endpoint, DI tool registry, and stdio transport over the official mcp/sdk
README 文档
README
Model Context Protocol server integration
for Yii3 over the official mcp/sdk
(PHP Foundation + Symfony): expose your application's domain operations as MCP
tools/resources for AI agents (Claude Code, Claude Desktop, …) through a
PSR-15 Streamable HTTP endpoint, with tools resolved through the Yii3 DI
container.
Using an AI coding assistant? llms.txt contains a compact API reference you can share with the model. Contributors: see AGENTS.md.
Requirements
| Requirement | Version |
|---|---|
| PHP | 8.3 – 8.5 |
mcp/sdk |
~0.6.0 (experimental until 1.0 — hence the tilde pin) |
| MCP protocol | 2025-06-18 (via SDK) |
ext-fileinfo |
required by the SDK |
Installation
composer require rasuvaeff/yii3-mcp
Usage
1. Declare a tool
Tools are ordinary Yii3 services. Capability methods are annotated with the SDK's own attributes — this package invents no protocol structures:
use Mcp\Capability\Attribute\McpTool; final readonly class OrderTools { public function __construct(private OrderRepository $orders) {} /** * Returns the current status of an order. */ #[McpTool(name: 'order.status')] public function status(string $orderId): string { return $this->orders->get($orderId)->status->value; } }
Input schemas are generated by the SDK from the method signature and DocBlock.
#[McpResource], #[McpResourceTemplate] and #[McpPrompt] methods work the
same way — all four SDK capability attributes are recognized.
To gate a capability class (feature flag, environment check), implement
ConditionalToolInterface — the instance is resolved through the container
at build time and skipped when shouldRegister() returns false:
final readonly class BetaTools implements ConditionalToolInterface { public function __construct(private FeatureFlags $flags) {} public function shouldRegister(): bool { return $this->flags->isEnabled('mcp-beta-tools'); } #[McpTool(name: 'beta.op')] public function betaOp(): string { ... } }
2. Register it
// config/params.php return [ 'rasuvaeff/yii3-mcp' => [ 'server_name' => 'my-app', 'server_version' => '1.0.0', 'tools' => [OrderTools::class], 'endpoint_secret' => getenv('MCP_SECRET'), ], ];
Handlers are registered as [class, method] references — the SDK resolves the
instance through the Yii3 container on call, so constructor dependencies are
injected the normal way.
3. Route the endpoint
// config/routes.php Route::methods(['POST', 'GET', 'DELETE', 'OPTIONS'], '/mcp') ->middleware(SharedSecretMiddleware::class) ->action(McpAction::class),
An MCP client connects with the secret header:
{
"mcpServers": {
"my-app": {
"type": "http",
"url": "https://example.com/mcp",
"headers": { "X-Mcp-Secret": "..." }
}
}
}
stdio for local development
// add McpServeCommand to your console commands ./yii mcp:serve
Claude Code config: claude mcp add my-app -- ./yii mcp:serve.
Sessions (important for PHP-FPM)
The MCP Streamable HTTP session spans several HTTP requests (initialize
first, then tools/call with the returned Mcp-Session-Id). The SDK's
default in-memory store would lose the session between FPM workers, so this
package defaults to a file-based store (sys_get_temp_dir(), override via
session.dir param). For multi-host setups rebind the interface:
// config/common/di/mcp.php use Mcp\Server\Session\Psr16SessionStore; use Mcp\Server\Session\SessionStoreInterface; return [ SessionStoreInterface::class => static fn (CacheInterface $cache) => new Psr16SessionStore($cache), ];
Prompts from Markdown files
Prompts are content, not code — keep them in a directory and every *.md
file becomes an MCP prompt (edited without a deployment, versioned like any
other file):
'rasuvaeff/yii3-mcp' => [ 'prompts_path' => __DIR__ . '/../resources/prompts', ],
--- name: code-review # defaults to the file name title: Code review assistant description: Reviews a diff with a given focus arguments: - name: diff description: The diff to review required: true - focus # simple form: optional argument --- Review the following diff focusing on {{focus}}: {{diff}}
Declared {{argument}} placeholders are substituted from the request
(missing ones become empty strings); undeclared placeholders are left
intact. Malformed frontmatter, an unreadable file or a duplicate prompt
name fail the server build with Prompts\Exception\InvalidPromptFileException
— never a silently missing prompt.
The file format is intentionally compatible with — and inspired by — vjik/my-prompts-mcp by Sergei Predvoditelev: the same prompt file works in a personal stdio prompt manager and on an application server.
OpenAPI bridge: expose an existing REST API
If the application already maintains an OpenAPI document, allow-listed
operations can be bridged as MCP tools with zero duplication — names come
from operationId, descriptions from summary/description, input schemas
from parameters/request body. Calls are executed as real HTTP requests
against the API, passing its full middleware stack (validation, rate
limiting, auth) — unlike hand-written tools that invoke handlers directly.
// config/params.php 'rasuvaeff/yii3-mcp' => [ 'openapi' => [ // file path OR http(s) URL — e.g. the app's own spec endpoint, // always current; fetched with the same `headers` (auth included) 'spec_path' => 'https://api.example.com/rest/json-url', 'base_url' => 'https://api.example.com', 'operations' => ['getBlogTags', 'getPage'], // allow-list, empty = nothing 'headers' => ['Authorization' => 'Bearer ' . getenv('MCP_API_TOKEN')], 'safe_methods_only' => true, // read-only bridge: non-GET in the list => build error ], ],
The DI wiring requires PSR-18/PSR-17 services (ClientInterface,
RequestFactoryInterface, StreamFactoryInterface) in the container.
Request bodies are passed as a single body tool argument; an operationId
missing from the document throws at server build time (fail-fast).
For custom scenarios use the pieces directly: SpecIndex +
HttpOperationExecutor + OpenApiServerConfigurator (a
ServerConfiguratorInterface — the generic extension point accepted by
McpServerFactory::create(tools, configurators)).
Components
| Class | Role |
|---|---|
McpServerFactory |
list of tool FQCNs → configured SDK Server (reads #[McpTool]/#[McpResource] attributes, wires the DI container and session store) |
McpAction |
PSR-15 handler running the SDK StreamableHttpTransport for the current request |
SharedSecretMiddleware |
fail-closed hash_equals() guard; an empty secret rejects every request with an explanatory 503 — an unprotected endpoint must be an explicit decision |
McpServeCommand |
mcp:serve — stdio transport for local MCP clients |
Exception\InvalidToolClassException |
configured tool class missing or without capability attributes (fail-fast) |
ConditionalToolInterface |
capability class opts out of registration at build time (shouldRegister()) |
Testing\McpTester |
in-process test client: initialize/listTools/callTool/readResource |
Prompts\MarkdownPromptsConfigurator |
a directory of *.md files as MCP prompts (vjik/my-prompts-mcp-compatible format) |
ServerConfiguratorInterface |
generic extension point for contributing capabilities to the builder |
OpenApi\OpenApiServerConfigurator |
bridges allow-listed OpenAPI operations as tools (HTTP execution) |
OpenApi\Exception\* |
InvalidSpecException, UnknownOperationException, OperationFailedException |
Security
- The endpoint is trusted-only. MCP tools execute application code;
treat the endpoint like an admin API. Ship it behind
SharedSecretMiddleware(an empty secret rejects every request with an explanatory 503) or an explicit network ACL. - Tool errors are returned as MCP error envelopes by the SDK — internals are not leaked as 500 traces.
- The core registers no tools by default; every exposed operation is an
explicit entry in
params['rasuvaeff/yii3-mcp']['tools']. - OAuth from the MCP authorization spec is deliberately out of scope until it stabilizes; shared-secret/ACL only.
Examples
See examples/ for a runnable script.
| Script | Shows | Needs server? |
|---|---|---|
http-handshake.php |
Full in-process MCP cycle: initialize + tools/call | no |
Testing your tools
Testing\McpTester drives the real Streamable HTTP code path in-process —
no HTTP server, no stdio process:
$tester = new McpTester($server, $psr17, $psr17, $psr17); $result = $tester->callTool('order.status', ['orderId' => '42']); $this->assertSame('paid', $result['content'][0]['text']); $tester->listTools(); // tool definitions $tester->readResource('app://x'); // resource contents $tester->request('prompts/list'); // any raw JSON-RPC method
For interactive debugging use the official MCP Inspector:
npx @modelcontextprotocol/inspector # transport: Streamable HTTP, URL: https://your-app/rest/mcp, # header: X-Mcp-Secret: <secret>
Development
No PHP/Composer on the host — run in Docker via the composer:2 image:
docker run --rm -v "$PWD":/app -w /app composer:2 composer build
Or with Make: make build, make cs-fix, make psalm, make test.
License
BSD-3-Clause. See LICENSE.md.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 5
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: BSD-3-Clause
- 更新时间: 2026-07-04