
1. PHP 远程 MCP 服务端到底解决什么问题如果你在用 Cline 写代码大概率遇到过这种场景想让 AI 直接查一下本地数据库、调一下内部接口、或者读一份项目里的配置文件但 Cline 只能看到你手动贴进去的内容。MCPModel Context Protocol就是干这个的——它让 AI 客户端能主动调用你暴露出来的工具。问题是官方 TypeScript SDK 功能全PHP 这边一直缺一个能跑远程、走 streamHttp 的轻量实现而且很多老项目还卡在 PHP 7.x 上。我这次要讲的就是用 PHP 按 modelcontextprotocol 的思路手写一个无状态的远程 MCP 服务端传输层走 streamHttp也就是 HTTP POST SSE 响应然后让 Cline 通过streamableHttp类型直接连上来调用工具。整套东西不需要框架一个server.php文件加 PHP 内置服务器就能跑起来PHP 7.4 以上都能用。适合谁看一是手上是 PHP 技术栈、不想为了 MCP 单独起 Node 服务的后端二是想搞明白 MCP 的 JSON-RPC 报文到底长什么样、SSE 怎么回、Cline 那边怎么配的开发者三是已经在用 Cline、想给它加自定义工具但被官方 SDK 的复杂度劝退的人。读完你能拿到一份可复制的路由代码、一份 Cline 的mcpServers配置以及一次真实工具调用的验证过程。先说清楚 MCP 在这里的角色。Cline 是客户端Client你的 PHP 服务是服务端Server两者之间用 JSON-RPC 2.0 通信。客户端发initialize握手然后发tools/list问有哪些工具再发tools/call真正调用。streamHttp 的意思是请求用普通 POST 发过来响应用text/event-stream把结果以 SSE 事件推回去。无状态意味着服务端不保存 session每次请求自带上下文这对 PHP 这种请求即销毁的模型特别友好。2. TaoToken 前置准备与 Cline 侧接入参数在写 PHP 之前先把模型这一侧的事情理清楚。Cline 本身是个客户端它调用工具靠的是 MCP但它背后驱动对话和决策的大模型需要单独配置。我这边用的是 TaoToken 提供的接入能力它兼容 Anthropic 和 OpenAI 两种协议风格Cline 里选 Anthropic 类型就能直接填。你需要准备三样东西也就是常说的三件套Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 去控制台生成Model ID 按你实际要用的模型填。Cline 的配置入口在设置里的 API Provider 部分选 Anthropic然后把这三项填进去。这一步不做Cline 连模型都调不通更别说调你的 MCP 工具了。具体操作路径是这样打开 Cline 面板点右上角齿轮进设置Provider 选 AnthropicBase URL 填https://taotoken.net/apiAPI Key 粘贴你生成的 keyModel ID 填对应模型名。保存后 Cline 会做一次连通性检查通过就说明模型侧 OK 了。如果你还没生成 key去控制台的 API Keys 页面建一个注意 key 只在创建时显示一次记得存好。这里有个容易混的点模型接入和 MCP 接入是两条独立的链路。模型接入决定 Cline 用哪个大脑思考MCP 接入决定 Cline 能调用哪些工具。两条都要配缺一不可。很多人只配了模型然后发现 Cline 不会用工具其实是 MCP 那侧没配。反过来只配 MCP 不配模型Cline 连话都说不出来。关于 Coding Plan如果你打算长期用 Cline 做 Agent 式开发可以考虑订阅制的方式比按量计费更可控。这个看个人使用频率偶尔用按量就行天天用建议上套餐。模型对话页面可以用来单独验证某个模型是否正常返回排障时很有用——如果模型对话都不通那问题肯定在模型接入而不是 MCP。配好之后Cline 的 MCP 配置是写在一个 JSON 文件里的通常在 Cline 的设置界面点 Edit MCP Settings 会打开cline_mcp_settings.json。这个文件的结构下面会详细给。你要做的是在里面加一个mcpServers条目类型写streamableHttpURL 指向你 PHP 服务暴露的/mcp路径。注意type字段的值Cline 认的是streamableHttp写错了它不会去连。3. 可复制的 PHP 路由与 SSE 响应配置现在进入正题写 PHP 服务端。核心思路是一个入口文件server.php根据请求方法分发。GET 请求返回一个简单的健康检查或 SSE 流POST 请求解析 JSON-RPC 报文按method字段分发到initialize、tools/list、tools/call三个处理分支结果用 SSE 格式写回。先看目录结构就一个文件php-mcp-demo/ └── server.phpserver.php的完整代码如下我按功能拆开讲。首先是基础设置和路由?php // server.php - 无状态 streamHttp MCP 服务端 header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Accept, Mcp-Session-Id); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; } $path parse_url($_SERVER[REQUEST_URI], PHP_URL_PATH); if ($path ! /mcp) { http_response_code(404); echo json_encode([error not found]); exit; } if ($_SERVER[REQUEST_METHOD] GET) { // SSE 通道用于服务端主动推送这里保持连接即可 header(Content-Type: text/event-stream); header(Cache-Control: no-cache); echo event: endpoint\ndata: /mcp\n\n; exit; } // POST 处理 JSON-RPC $raw file_get_contents(php://input); $req json_decode($raw, true); if (!is_array($req)) { http_response_code(400); echo json_encode([jsonrpc 2.0, id null, error [code -32700, message Parse error]]); exit; }这段的关键点CORS 头必须放开因为 Cline 可能从不同来源发起请求Mcp-Session-Id虽然在无状态模式下不用但允许它出现在头里避免预检失败。GET 请求返回一个 SSE 端点事件这是 streamHttp 的惯例客户端会先 GET 建立流再 POST 发消息。接下来是工具定义和分发逻辑$method $req[method] ?? ; $id $req[id] ?? null; function sseReply($id, $result) { header(Content-Type: text/event-stream); header(Cache-Control: no-cache); $payload json_encode([ jsonrpc 2.0, id $id, result $result ], JSON_UNESCAPED_UNICODE); echo event: message\n; echo data: {$payload}\n\n; exit; } function sseError($id, $code, $msg) { header(Content-Type: text/event-stream); $payload json_encode([ jsonrpc 2.0, id $id, error [code $code, message $msg] ]); echo event: message\ndata: {$payload}\n\n; exit; } switch ($method) { case initialize: sseReply($id, [ protocolVersion 2024-11-05, capabilities [tools new stdClass()], serverInfo [name php-mcp-demo, version 1.0.0] ]); break; case notifications/initialized: // 通知类消息无需回复 http_response_code(202); exit; case tools/list: sseReply($id, [tools getTools()]); break; case tools/call: $name $req[params][name] ?? ; $args $req[params][arguments] ?? []; $out callTool($name, $args); sseReply($id, [ content [[type text, text $out]] ]); break; default: sseError($id, -32601, Method not found: {$method}); }initialize返回协议版本和 capabilitiestools/list返回工具数组tools/call执行具体工具。注意notifications/initialized是客户端握手完发的通知按 JSON-RPC 规范通知不需要回复返回 202 即可。工具定义和实现部分function getTools() { return [ [ name get_server_time, description 返回服务器当前时间用于验证 MCP 连通性, inputSchema [ type object, properties new stdClass(), required [] ] ], [ name echo_text, description 回显传入的文本, inputSchema [ type object, properties [ text [type string, description 要回显的文本] ], required [text] ] ] ]; } function callTool($name, $args) { switch ($name) { case get_server_time: return 当前服务器时间 . date(Y-m-d H:i:s); case echo_text: return 收到 . ($args[text] ?? (空)); default: return 未知工具{$name}; } }inputSchema用的是 JSON Schemaproperties为空对象时要用new stdClass()否则 PHP 会序列化成[]而不是{}某些客户端解析会报错。这是个很隐蔽的坑。启动服务php -S localhost:8000 server.php然后 Cline 的cline_mcp_settings.json里加{ mcpServers: { php-demo: { autoApprove: [], disabled: false, timeout: 60, type: streamableHttp, url: http://localhost:8000/mcp } } }三件套在这里体现为Base URL 是http://localhost:8000/mcptype 是streamableHttp工具名由服务端tools/list返回。Cline 侧不需要填 Key因为这是本地服务但模型侧的 Key 是另一回事别搞混。4. 验证请求与成功结果服务起来后先用 curl 手动验证一遍确认服务端逻辑没问题再去 Cline 里连。这样排障时能快速定位是服务端还是客户端的问题。第一步验证 initializecurl -N -X POST http://localhost:8000/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{},clientInfo:{name:curl,version:1.0}}}-N是关闭 curl 的缓冲这样能实时看到 SSE 输出。正常返回event: message data: {jsonrpc:2.0,id:1,result:{protocolVersion:2024-11-05,capabilities:{tools:{}},serverInfo:{name:php-mcp-demo,version:1.0.0}}}第二步验证 tools/listcurl -N -X POST http://localhost:8000/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,id:2,method:tools/list,params:{}}应该返回两个工具的定义。如果这里返回空数组检查getTools()的返回结构tools字段必须是数组。第三步真正调用工具curl -N -X POST http://localhost:8000/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,id:3,method:tools/call,params:{name:echo_text,arguments:{text:hello mcp}}}预期返回event: message data: {jsonrpc:2.0,id:3,result:{content:[{type:text,text:收到hello mcp}]}}三步都通了说明服务端没问题。这时候去 Cline 里打开 MCP 设置应该能看到php-demo这个 server 显示为已连接工具列表里出现get_server_time和echo_text。然后在对话里让 Cline 调用一下比如输入「用 echo_text 工具回显 hello」Cline 会发起tools/call你就能在 Cline 界面看到工具调用和返回结果。实测下来从 Cline 发起调用到拿到结果本地服务基本是毫秒级。如果 Cline 显示工具调用失败先看 PHP 服务的终端有没有收到请求日志收到了说明网络通问题在报文格式没收到说明 Cline 根本没连上检查 URL 和 type。5. 本篇常见错误排查这一节列几个真实会撞上的报错对照着改。报错一Cline 显示local proxy failed或连接超时。最常见的原因是 PHP 服务没起来或者端口被占。先确认php -S localhost:8000 server.php这个进程还在跑终端没报错。如果 8000 被占换 8001 并同步改 Cline 配置里的 URL。另一个原因是 Cline 配置里type写成了sse或http必须是streamableHttp大小写敏感。报错二返回 401 或鉴权失败。这个通常不是 MCP 服务端的问题而是模型侧的三件套没配对。检查 Cline 的 Provider 设置里 Base URL 是不是https://taotoken.net/apiAPI Key 有没有多余空格Model ID 是否拼错。如果模型对话页面单独测试都不通那肯定是模型接入的问题跟 MCP 无关。报错三reading choices或类似字段读取失败。这是模型返回格式和客户端预期不匹配多半是 Provider 类型选错了。Cline 里如果用的是 Anthropic 协议Provider 就得选 Anthropic如果模型是 OpenAI 风格的选 OpenAI 兼容。选错会导致 Cline 解析响应时找不到choices字段。改对 Provider 后重启 Cline。报错四OAuth相关报错或反复弹授权。本地 MCP 服务不需要 OAuth如果 Cline 提示授权说明它把你的服务当成了需要 OAuth 的远程服务。检查配置里有没有多余的auth字段删掉。另外url必须是完整的http://localhost:8000/mcp不能只写localhost:8000。报错五tools/list返回空或 Cline 看不到工具。检查getTools()返回的数组结构每个工具必须有name、description、inputSchema三个字段。inputSchema里properties为空时用new stdClass()用[]会导致序列化成数组部分客户端解析失败。还有capabilities里要有tools字段否则客户端不知道你支持工具。报错六SSE 响应被缓冲Cline 一直等不到结果。PHP 内置服务器有时会缓冲输出。在sseReply里加ob_end_flush()和flush()或者确认没有开启 output buffering。另外header(Content-Type: text/event-stream)必须在任何输出之前调用前面不能有 echo 或空行。报错七Codex 的auth.json相关配置。如果你同时用 Codex它的鉴权文件在~/.codex/auth.json里面存的是 token。这个跟 Cline 的 MCP 配置是两套东西别混。Codex 的 Base URL 和 Key 也是独立配的改 Codex 不影响 Cline。排查顺序建议先 curl 测服务端三步通了再测 Cline 模型对话最后测 Cline 调 MCP 工具。哪一步断问题就在哪一层。6. 继续往下走把工具接到真实业务上面这个 demo 只放了两个玩具工具实际用的时候你会想接数据库、接内部 API、接文件系统。思路是一样的在callTool里加分支就行。比如查数据库case query_user: $pdo new PDO(mysql:host127.0.0.1;dbnametest, user, pass); $stmt $pdo-prepare(SELECT id, name FROM users WHERE id ?); $stmt-execute([$args[id]]); return json_encode($stmt-fetch(PDO::FETCH_ASSOC), JSON_UNESCAPED_UNICODE);注意别把生产库直连暴露给 MCP工具里做好参数校验和权限控制只读查询优先。MCP 工具本质上是给 AI 开的 API安全边界要自己划清楚。如果你要长期跑建议把服务放到内网用 nginx 反代加个简单的 token 校验别直接暴露公网。Cline 配置里可以加自定义 header 传 token服务端在入口处校验。这样既保留了 streamHttp 的便利又不会让人随便调你的工具。工具多了之后tools/list的返回会变长Cline 每次对话都要拉一遍。可以在服务端加个缓存或者按需返回。不过本地服务这点开销可以忽略先跑起来再说。最后留个实用技巧调试 MCP 报文时在server.php开头加一行file_put_contents(/tmp/mcp.log, $raw . \n, FILE_APPEND);把每个请求的原始 JSON 记下来。Cline 那边报错但你看不到报文时这个日志能救命。我踩过的坑基本都靠它定位的。