php搭建mcp服务

一:拓展安装

注意: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博客 的测试流程

相关推荐
张小姐的猫1 小时前
【MySQL】数据类型
android·linux·数据库·c++·mysql·adb·php
估值探索者1 小时前
【Python量化技术指标实战 02】MACD金叉也分强弱?手工算一次 DIF/DEA 就懂了
java·开发语言·python·股票数据·股票数据api接口
讳疾忌医丶1 小时前
编译器眼中的 C++ 循环:从指针别名、IEEE 754 结合律到 SIMD 语义证明
开发语言·c++
ZzT1 小时前
嫌推理慢就把 effort 调高?effort 管的是想多少,不是想多快
openai·ai编程·claude
重生六道1 小时前
B站视频时长统计:一段JS脚本搞定分P/合集总时长(附倍速计算)
开发语言·javascript·音视频
孔令飞2 小时前
OneX Harness AI Coding 工具介绍
云原生·go·ai编程
XiYang-DING2 小时前
P5528.带过期时间的 LRU 缓存
java·开发语言·缓存
小徐不徐说2 小时前
C++ 机器视觉入门:从零搭建 AOI 开发环境(OpenCV + CMake)
开发语言·c++·opencv