Skip to content

feat(tools): add typephp-mcp, a TypePHP-built MCP dev assistant (Windows + macOS) - #174

Open
yangweijie wants to merge 3 commits into
swoole:masterfrom
yangweijie:feature/mcp
Open

yangweijie wants to merge 3 commits into
swoole:masterfrom
yangweijie:feature/mcp

Conversation

@yangweijie

Copy link
Copy Markdown
Contributor

结论

新增 tools/typephp-mcp:用 TypePHP 自己(AOT)实现的 MCP server,通过 stdio 以
换行分隔的 JSON-RPC 2.0 对外提供 12 个工具,替代设计文档里原计划的 Python/FastMCP
实现——协议层、检索层、编译器 oracle 全是编译成原生二进制的 PHP 代码。

22 files changed, +3482/-1。除 tools/typephp-mcp/** 外只有 3 处顺带改动(见文末)。

它是什么

工具 作用
check_code 跑 tpc --dry 验证 TypePHP 子集合法性,返回 file:line 诊断或 OK
list_restrictions 裁剪规则(结构/类型/引用/对象/控制流/std/stub/构建),可按 topic 查
get_symbol / search_code 声明与全文检索,返回 file:line + 所在声明
list_stdlib / get_example / get_stub_api tests/std 用例、examples/、.stub.php ABI
search_docs / get_changelog / list_topics / get_build_config_help 双语 docs 全文检索带出处、CHANGELOG、project.yml 说明
get_index_info repo 根 / tpc 版本 / git commit / 文档数 / 工具清单(时效自证)

设计取舍(详见 tools/typephp-mcp/README.md):

  • 无预建索引:docs/examples/src 每次实时读取(约 2 MB),结构上消除索引过期。
  • 约束注入:initialize 的 instructions 与 check_code/list_restrictions 的 tool
    description 内置约 800 token 红线摘要,不调工具也能压住头号失败模式。
  • 返回摘录 + 出处,不返整文件;单次结果约 4 KB 字节预算,UTF-8 边界安全。
  • 也实现了 MCP roots 能力:初始化后向客户端要工作区,自动识别 TypePHP 项目
    (vendor/swoole/typephp / composer 依赖 / project.yml)并入默认检索集;客户端不发
    roots 时用 TYPEPHP_MCP_PROJECT_ROOT 兜底。

构建

  • Windows:tools/typephp-mcp/build.bat(vswhere 定位 vcvars64.bat,tpc 发行包路径相对
    脚本解析,产物 typephp_mcp.exe + 同目录运行时 DLL)。
  • macOS / Linux:tools/typephp-mcp/build.sh(本 PR 新增)。Unix 侧没有 tpc 发行包目录可
    默认,所以脚本改为定位宿主 embed 运行时:显式 PHP_HOME/PHPX_HOME →
    ~/.typephp/php-builder/php-*/install 里最新的私有运行时 → 仓库 vendor/swoole/phpx,
    都找不到时给出可执行的下一步提示,而不是让人对着 tpc 那句
    The host PHP embed library is missing 猜。

macOS 实测(Apple Silicon,PHP 8.5.7 + 私有 8.5.11 embed 运行时 + phpx 2.9.4)

三道只有换平台才暴露的墙,逐个实测定位:

  1. Homebrew php 没有 embed 库:只建 cli SAPI,libphp.{a,dylib} 都不存在,
    tpc --dry 在 prepareRuntimeDependencies() 直接 Fatal——check_code 在 mac 上因此全红
    (报的是编译器,不是工具逻辑)。
  2. phpx 版本闸:编译器现在生成 php::empty_string,而 composer.lock 已不入库,本地
    vendor/swoole/phpx 停在 v2.7.0(头文件里没有该符号)→ 16 个翻译单元全在编译期死。CI 的
    macos-arm64 作业躲过了是因为它 checkout phpx master。build.sh:62 现在提前校验并直接报
    「太老,去 composer update swoole/phpx」,不再让人对着 C++ 报错猜。
  3. 链接期扩展依赖:走静态 libphp.a 时 tpc 不按运行时实际扩展集推导系统库,需应用侧
    补 -lonig -lxml2 -lsqlite3 -lz -lresolv -lcurl -lssl -lcrypto -liconv(iconv/openssl 还是
    brew keg-only,要带 -L)。build.sh 用 ar -t 探测归档对象后补全,不靠手抄清单。

读数:

  • php vendor/bin/phpunit tools/typephp-mcp/tests/ → 36 用例 / 383 断言全绿,唯一 skip
    是 testMsvcBinDirDiscovery(vswhere 只在 Windows 存在)。
  • AOT 产物 typephp_mcp(Mach-O arm64,25 MB),真实 stdio 会话跑通 initialize /
    tools/list(12) / tools/call;两条 exec() 通路(get_index_info 的 tpc+git、
    check_code 的 tpc --dry)在编译产物里正常。
  • AOT 与解释执行逐工具比对:12 条回复逐字节相同,唯一差异是 check_code 里那个
    uniqid() 临时构建目录名。
  • 反向对照:PHP_HOME 指向无 embed 库的目录时,3 条 check_code 断言(两个单测 + 产物会话
    各一)一起变红——这几条断言确实在管编译器通路。

McpBinaryTest 原来硬编码 typephp_mcp.exe,在 mac 上恒 skip;现在按平台取名并支持
TYPEPHP_MCP_BINARY=<path> 覆盖,另补了上面两条 exec 断言。

复现

# 解释执行 + 产物会话(需要 embed 运行时)
export PHP_HOME=$HOME/.typephp/php-builder/php-<ver>-<hash>/install
export PHPX_HOME=<phpx 树:include/phpx.h + lib/libphpx.{a,dylib}>
tools/typephp-mcp/build.sh
php vendor/bin/phpunit tools/typephp-mcp/tests/

顺带的 3 处改动

  • phpunit.xml:注册 TypePhpMcpTests 套件(./tools/typephp-mcp/tests)。
  • .gitignore:忽略 /tools/typephp-mcp/build/;工具目录内另忽略产物名 typephp_mcp(.exe)。
  • tests/windows/clang/README.md:一处 BuildTools 路径笔误(C: → 该机实际的 D:),与本
    PR 主题无关,介意可以拆走。

已知边界(想请维护者定夺)

  1. check_code 只跑 --dry,覆盖 C++ 生成、不覆盖链接期,输出里已明确写这句。要不要再加
    一条链接期档位?
  2. STDIN/STDOUT/STDERR 常量在某些项目形态下被 -O2 折叠成字面量 0
    (src/Parser/ConstantExpressionTrait.php 的常量折叠路径),运行时 fwrite()/fgets() 抛
    TypeError。本工具用 fopen('php://stdin') 绕开,规避写在 main.php 注释里,但根因在
    编译器侧,另开 issue 更合适。
  3. 上面第 3 条 mac 链接期缺依赖同理是编译器侧问题:CI 用 setup-php 的共享 libphp.dylib
    所以躲过了,只有静态 embed 运行时才需要那一串 -l。本 PR 只在 build.sh 里绕过,没动
    src/Build/NativeBuildConfigurationTrait.php。
  4. build.sh 同时按 Linux 写(uname 分支只影响 libiconv 那条),Linux 未实跑验证。
  5. Windows 路径本次未复跑(本 PR 主体在 Windows 上开发,README 里那套客户端配置说明即来自那时)。
  6. TYPEPHP_MCP_DEFAULT_ROOT(src/Config.php:10)是开发机绝对路径,在别的机器上永远命中
    不到,只在「客户端从仓库外拉起且没设 TYPEPHP_MCP_ROOT」时才暴露;是否改成按产物同目录
    推导,听各位意见。

Windows was the only platform with an entry point, so the tool could neither be
compiled nor tested here: build.bat is cmd-only, McpBinaryTest hard-coded
typephp_mcp.exe and therefore always skipped, and the README documented only
Windows paths and clients.

Add build.sh. There is no tpc package directory on Unix, so it locates a host
embed runtime instead (PHP_HOME/PHPX_HOME, then the newest php-builder private
runtime, then vendor/swoole/phpx) and derives the link libraries from the
archive: a cached private runtime carries extensions this project does not
declare, and tpc does not add their system libraries for a static libphp.a
(-lonig -lxml2 -lsqlite3 -lz -lresolv -lcurl -lssl -lcrypto -liconv, the last
three behind keg-only brew prefixes). It also rejects a phpx older than the
php::empty_string API the compiler now emits, which otherwise fails in every
translation unit with a C++ error that does not point at the version gap.

McpBinaryTest picks the binary by platform, accepts TYPEPHP_MCP_BINARY, and now
drives the compiled artifact through both exec() paths: get_index_info's
tpc+git and check_code's tpc --dry.

Validation: 36 tests / 383 assertions pass with one platform-scoped skip
(vswhere); all 12 tools answer byte-identically from the AOT binary and from
interpreted PHP apart from check_code's uniqid() build directory; pointing
PHP_HOME at a runtime without an embed library turns all three check_code
assertions red together, which is the control that they own the compiler path.
Linux shares the script but is not yet measured.

@matyhtf matyhtf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请求修改后再合并。

这个方向有价值,尤其是 TypePHP 约束、API 检索和编译诊断。但目前还存在错误知识注入、检索漏报、协议兼容声明不实、子进程缺少资源保护,以及原生产物验证不足的问题,具体要求见行内意见。

部署部分也请一并补齐:区分编译器资源目录与用户项目目录;修复 Windows 项目路径被冒号拆开的情况;Linux 构建支持共享库 .so,并按平台选择依赖检查命令,不能无条件调用 otool。构建脚本应遵循仓库约定:专用 CI 按实际 CPU 数量构建,本机保留单任务及资源保护。

验证要求:三平台实际构建 MCP 二进制,用官方 SDK 跑 stdio 会话,覆盖正常调用、错误调用、超时以及从仓库外启动。CI 必须明确区分“通过”和“因缺少产物而跳过”。

本次已阅读实现,并用 PHP 解释执行探针验证了任意协议版本被接受、核心符号检索漏报、文件不存在未标记工具错误,以及通知被回复等情况;未构建本 PR 的原生 MCP 二进制。

. '2) bin mode requires a global main() with signature main() or main(int $argc, array $argv). '
. '3) once a local is inferred as a concrete native type (int/float/bool/string/array) it cannot be reassigned to an incompatible type in the same scope; use std::any() for dynamic semantics. '
. '4) reference arguments of dynamic/Closure calls need explicit std::ref()/toRef(); closures cannot return by reference; Closure::bind/bindTo/call are unsupported. '
. '5) built-in classes cannot be extended (e.g. PDO) - use composition; Native Class objects are not plain Zend objects/arrays/mixed. '

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 请按真实继承边界修正规则,避免初始化时持续误导 AI。

TypePHP 允许继承内置类,也允许继承参与 AOT 编译的用户类;不允许继承仅在运行时定义或加载、未参与编译的用户类。这里的“built-in classes cannot be extended”以及中文规则中“不能继承内置类”均不正确。仓库已有 tests/compiler/class/class_extends.phpt 覆盖继承 ArrayObject。

请同时修正中文规则、英文摘要和相应测试,并将普通 Zend 类与 Native 类各自的约束区分清楚。规则应关联适用的编译器版本及权威出处,避免文档、规则表和初始化摘要分别维护后发生漂移。

private function readLines(string $path): array
{
$size = filesize($path);
if (!is_int($size) || $size <= 0 || $size > self::MAX_FILE_BYTES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 不要把超过大小限制的核心文件静默当成没有匹配。

当前 PR 的 src/Translator.php 为 409,520 字节,超过 256 KiB,因此会被这里直接跳过。我调用 get_symbol("genClassPropertyInit"),得到 No symbol found,但该方法实际就在这个文件中。这会给 AI 提供错误的否定结论。

请改为流式或分段扫描,或明确返回未扫描文件、截断与覆盖范围。还应支持完整命名空间和 Class::method,并补充大文件及重名符号测试。目录递归需要真实路径去重和全局扫描预算,避免符号链接环或大型项目拖住服务;返回内容限制为 4 KB 不等于扫描成本受限。

. ' --no-color --no-progress 2>&1';
$output = [];
$code = -1;
exec($command, $output, $code);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 编译器调用需要超时、取消和整个子进程树的资源限制。

这里同步 exec(),没有超时、输出上限或可取消的进程控制。服务主循环在等待期间无法处理其他消息;检查大型项目或编译器卡住时,整个 MCP 服务都会阻塞。

请使用可控制生命周期的子进程执行方式,设置超时、输出预算和进程树内存限制,异常或超时后清理临时目录并保持服务可继续响应。如果提供真正的编译/链接档位,本机还必须落实公共构建锁、--job 1 以及相应的 Make/CMake 并发限制,不能只在工具说明里约定。

exec($command, $output, $code);
self::removeDir($buildDir);

return self::format($resolved, $code, $output);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 请区分检查阶段与工具执行失败,避免笼统的 OK/FAILED 文本。

--dry 只生成 C++,既不验证 C++ 是否可编译,也不验证链接。#172 中的未声明临时变量就是这种检查发现不了的错误。目前提示只突出“不检查链接”,容易让使用方误以为已经通过 C++ 编译。

请明确返回实际检查阶段与已验证阶段,建议提供结构化的 ok、stage、diagnostics,诊断包含文件、行列、严重程度和可用的诊断编号。生成、编译、链接应分别标识,后两者显式启用。

路径不存在、PHP/SDK 缺失、编译器执行异常等属于工具执行失败,应设置 isError: true。目前不存在的文件也返回普通成功结果中的文本;我已通过 tools/call 复现。用户代码被诊断为不合法则应以清晰的结构化检查结果表示,与工具自身执行失败区分。

$clientCaps = isset($params['capabilities']) && is_array($params['capabilities']) ? $params['capabilities'] : [];
$this->clientSupportsRoots = isset($clientCaps['roots']);
return $this->result($id, [
'protocolVersion' => $requested !== '' ? $requested : self::PROTOCOL_VERSION,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 不能通过原样回显来声称支持客户端请求的任意协议版本。

我用 protocolVersion: "1900-01-01" 初始化,服务仍成功返回同一个版本。工具名称相同不代表协议版本之间没有差异。请明确实际支持的版本列表,并按所选版本的规范协商;暂时只支持旧版也可以,但不能假装支持任意新版。

请同时修正能力声明:旧版 roots 是客户端能力;新版 MCP 已弃用 roots,新接口应优先使用显式工作区参数或配置,roots 保留兼容即可。

还需要补协议负向测试:未支持版本、无效请求、错误参数和通知不回复。当前不带 id 的 tools/list 通知仍会得到回复,我已复现。

参考:官方版本规范、roots 规范。

* from the current working directory, then this constant. The constant only
* matters when the binary is started outside the repository.
*/
const TYPEPHP_MCP_DEFAULT_ROOT = 'D:\\git\\php\\aot-compiler';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 发布产物不能依赖开发机的绝对路径。

这个默认值在其他机器上无效。客户端从用户项目或仓库外启动时,上级目录通常也找不到编译器仓库,服务会直接启动失败。

请通过明确配置、安装布局或产物位置定位与当前编译器版本匹配的资源,并区分“工具/文档资源目录”和“用户项目目录”。缺失依赖时给出可操作的错误信息。请补一个从仓库外启动的解释执行及原生产物测试,验证不依赖开发机路径。

}

public function testStdioMcpSession(): void
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] CI 需要实际构建 MCP 产物,不能依靠缺产物时跳过的测试证明原生兼容。

本 PR 将测试加入 PHPUnit,但没有增加 MCP 二进制的 CI 构建步骤;这里缺少产物就跳过。因此现有绿色 CI 无法证明这批新增代码已经在各平台 AOT 编译、链接并跑通 stdio 会话。

请在 Linux、macOS、Windows 上实际构建 MCP 产物,再运行原生会话测试;该 CI 阶段缺产物应判失败。至少覆盖官方 SDK 的握手与工具调用、持续打开 stdin 时及时刷新响应、错误返回、超时恢复和仓库外启动。解释执行与 AOT 的关键结果也应保持一致。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants