一:拓展安装
注意:php版本必须大于等于8.1
composer require mcp/sdk
二:具体实现
这里我以Yii框架为例实现
1:mcp配置
(1)mcp安全配置
api/config/params.php
'mcp.secret' => "sk-xxxxx",//验证密钥
'mcp.allowedHosts' => ['*'], //允许的 HTTP Host 头
'mcp.allowedOrigins' => ['*'],//跨域访问来源
(2)路由配置
api/config/main.php
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
'mcp' => 'mcp/index',
]
],
2:MCP工具类
真正暴露给 AI 客户端的能力,以后加业务工具,主要改这个类(或同目录再加带属性的类)
<?php
namespace api\mcp;
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Yii;
/**
* MCP tools and resources exposed by the API HTTP endpoint.
*/
final class AppTools
{
/**
* 报名系统管理时间
*/
#[McpTool(name: 'serverTime', description: '返回 报名系统管理 服务当前时间(Asia/Shanghai)')]
public function echoMessage(): string
{
return date('Y-m-d H:i:s');
}
/**
* 考生信息查询
*/
#[McpTool(name: 'query', description: '考生信息查询')]
public function query(string $name, string $studentNo): string
{
//具体业务逻辑
return "考生:" . $name . ",学号:" . $studentNo;
}
}
3:MCP server类
组装 MCP Server:名称版本、扫描 api/mcp 发现工具、会话落到 api/runtime/mcp-sessions,并加文件锁避免 PHP-FPM 并发写坏会话。
<?php
namespace api\mcp;
use Mcp\Server;
use Mcp\Server\Session\FileSessionLock;
use Mcp\Server\Session\FileSessionStore;
use Yii;
/**
* Builds the MCP Server bound to this API application.
*/
final class ServerFactory
{
public static function create(): Server
{
$sessionDir = Yii::getAlias('@api/runtime/mcp-sessions');
return Server::builder()
->setServerInfo('ubo-api', '2.0.0', 'API MCP Server')
->setInstructions('HTTP MCP endpoint for the API')
->setDiscovery(__DIR__, ['.'])
->setSession(new FileSessionStore($sessionDir))
->setSessionLock(new FileSessionLock($sessionDir))
->build();
}
}
4:HTTP 入口
关掉 Yii REST 的 JSON 包装和鉴权,把 Yii 请求转成 PSR-7,交给 SDK 的 Streamable HTTP,再把 PSR-7 响应写回 Yii。顺带做可选 MCP_SECRET 校验、CORS、Host 防护。
<?php
/**
* @link http://www.u-bo.com/
* @copyright 南京友博网络科技有限公司
* @license http://www.u-bo.com/license/
*/
namespace api\controllers;
use api\mcp\ServerFactory;
use Mcp\Schema\Wire\McpHeader;
use Mcp\Server\Transport\Http\Middleware\CorsMiddleware;
use Mcp\Server\Transport\Http\Middleware\DnsRebindingProtectionMiddleware;
use Mcp\Server\Transport\StreamableHttpTransport;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yii;
use yii\web\Response;
use yii\web\UnauthorizedHttpException;
/**
* Streamable HTTP MCP endpoint.
*
* Independent API domain: POST/GET/DELETE/OPTIONS /mcp
* Shared-domain pretty URL: POST/GET/DELETE/OPTIONS /api/mcp
*/
class McpController extends \yii\rest\Controller
{
/**
* @var string[] hop-by-hop headers that must not be copied onto the Yii response
*/
private const HOP_BY_HOP = [
'connection',
'keep-alive',
'proxy-authenticate',
'proxy-authorization',
'te',
'trailer',
'transfer-encoding',
'upgrade',
];
/**
* @inheritdoc
*/
public function behaviors()
{
$behaviors = parent::behaviors();
unset(
$behaviors['contentNegotiator'],
$behaviors['authenticator'],
$behaviors['rateLimiter'],
$behaviors['cors']
);
return $behaviors;
}
/**
* @inheritdoc
*/
protected function verbs()
{
return [
'index' => ['GET', 'POST', 'DELETE', 'OPTIONS', 'HEAD'],
];
}
/**
* Skip REST serialization so the PSR-7 body is emitted as-is.
*
* @inheritdoc
*/
public function afterAction($action, $result)
{
return \yii\web\Controller::afterAction($action, $result);
}
/**
* Handle every MCP HTTP method on a single path.
*/
public function actionIndex()
{
$this->authorize();
$psr17 = new Psr17Factory();
$psrRequest = $this->toPsrRequest($psr17);
$transport = new StreamableHttpTransport(
$psrRequest,
$psr17,
$psr17,
null,
$this->middleware($psr17),
);
return $this->toYiiResponse(ServerFactory::create()->run($transport));
}
/**
* @throws UnauthorizedHttpException
*/
private function authorize(): void
{
if (Yii::$app->request->getIsOptions()) {
return;
}
$secret = (string) (Yii::$app->params['mcp.secret'] ?? '');
if ($secret === '') {
return;
}
$provided = (string) Yii::$app->request->headers->get('X-MCP-Secret', '');
if ($provided === '') {
$authorization = (string) Yii::$app->request->headers->get('Authorization', '');
if (strncasecmp($authorization, 'Bearer ', 7) === 0) {
$provided = substr($authorization, 7);
}
}
if ($provided === '' || !hash_equals($secret, $provided)) {
throw new UnauthorizedHttpException('MCP secret is missing or invalid.');
}
}
private function toPsrRequest(Psr17Factory $psr17): ServerRequestInterface
{
$creator = new ServerRequestCreator($psr17, $psr17, $psr17, $psr17);
$psrRequest = $creator->fromGlobals();
$rawBody = Yii::$app->request->getRawBody();
if ($rawBody !== '') {
$psrRequest = $psrRequest->withBody($psr17->createStream($rawBody));
}
return $psrRequest;
}
/**
* @return list<\Psr\Http\Server\MiddlewareInterface>
*/
private function middleware(Psr17Factory $psr17): array
{
$hosts = $this->stringListParam('mcp.allowedHosts', ['localhost', '127.0.0.1', '[::1]']);
$origins = $this->stringListParam('mcp.allowedOrigins', ['*']);
$hostName = Yii::$app->request->hostName;
if (
$hostName !== ''
&& !in_array('*', $hosts, true)
&& !in_array(strtolower($hostName), array_map('strtolower', $hosts), true)
) {
$hosts[] = $hostName;
}
return [
new CorsMiddleware(
allowedOrigins: $origins,
allowedMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Accept',
'Authorization',
'Content-Type',
'Last-Event-ID',
'X-MCP-Secret',
StreamableHttpTransport::PROTOCOL_VERSION_HEADER,
StreamableHttpTransport::SESSION_HEADER,
McpHeader::METHOD,
McpHeader::NAME,
McpHeader::PARAM_PREFIX . '*',
],
),
new DnsRebindingProtectionMiddleware($hosts, $origins, $psr17, $psr17),
];
}
/**
* @param list<string> $default
* @return list<string>
*/
private function stringListParam(string $name, array $default): array
{
$value = Yii::$app->params[$name] ?? $default;
if (!is_array($value) || $value === []) {
return $default;
}
$list = [];
foreach ($value as $item) {
if (is_string($item) && $item !== '') {
$list[] = $item;
}
}
return $list === [] ? $default : array_values($list);
}
private function toYiiResponse(ResponseInterface $psrResponse): Response
{
$response = Yii::$app->response;
$response->format = Response::FORMAT_RAW;
$reason = $psrResponse->getReasonPhrase();
$response->setStatusCode($psrResponse->getStatusCode(), $reason !== '' ? $reason : null);
foreach ($psrResponse->getHeaders() as $name => $values) {
if (in_array(strtolower($name), self::HOP_BY_HOP, true)) {
continue;
}
$response->headers->remove($name);
foreach ($values as $value) {
$response->headers->add($name, $value);
}
}
$response->content = (string) $psrResponse->getBody();
$response->data = $response->content;
return $response;
}
}
5:测试
测试流程参考spring boot搭建MCP服务-CSDN博客 的测试流程