首页
下载
文档
社区
视频
捐赠
开源项目
typephp
swoole
code-galaxy
swoole-cli
phpx
phpy
赞助商
AOT 编译器
AI 助理
商业产品
Swoole-Compiler 代码加密器
CRMEB 新零售社交电商系统
新加坡独享 Mac mini AI 工作站
登录
注册
全部
提问
分享
讨论
建议
公告
开发框架
TypePHP
CodeGalaxy
发表新帖
typephp-mcp:一个用 TypePHP 自己编译出来的 MCP 开发助手
> 实测环境:Windows x64,产物 `typephp_mcp.exe`(713 KB),编译器包 `tpc_v0.9.5_windows_x64`。文中所有工具返回都是从 AOT 二进制的真实 stdio 会话里抓取的原文,不是示意图。 ## 问题:模型写不对 TypePHP 把 PHP 编译成原生二进制,PHP 语言里有一大批东西不能用。全局作用域不能有可执行语句,`$$var` 不支持,`strict_types=0` 被拒绝,局部变量被静态推断成 `int` 之后不能再赋成 `string`,`Closure::bind` 不能调用,`.stub.php` 的正文必须是空的 `{}`。 这些规则散落在 100 多篇文档、几百个 `tests/std` 用例和翻译器源码里。语言模型对 PHP 很熟,对"PHP 的一个子集"完全不熟——它写出看起来完全正常的 PHP,然后编译器在第 3 行报 `Unsupported statement: Stmt_Echo`。 `typephp-mcp` 就是为堵住这个落差做的:一个 MCP(Model Context Protocol)server,让 AI 客户端在写代码之前知道红线、写完立刻能验证。 ## 它是什么 一个 stdio server,以换行分隔的 JSON-RPC 2.0 对外服务,暴露 12 个工具: | 工具 | 什么时候用 | |---|---| | `check_code` | 提交前必调。跑 `tpc --dry`,返回结构化诊断 | | `list_restrictions` | 写码前必读。按 topic 查 TypePHP 裁剪规则 | | `search_docs` | 双语文档全文检索,带 `file#heading (L行号)` 出处 | | `get_symbol` | 精确查函数/类/接口/trait 声明,返回签名 + `file:line` | | `search_code` | grep `src/`、`examples/`、`tests/std`,返回所在声明 | | `list_stdlib` | 标准库用例目录(`array` 802 个、`strings` 734 个) | | `get_example` | 可运行示例(单文件全文,或目录清单 + 三段摘录) | | `get_stub_api` | `.stub.php` ABI 契约 | | `get_changelog` / `get_build_config_help` / `list_topics` | 版本变更、构建配置、文档目录 | | `get_index_info` | 元数据自证:repo 根、tpc 版本、git commit、文档数 | ## 第一件值得说的事:它是用 TypePHP 编译的 设计文档最初的计划是 Python + FastMCP。最后没这么做——改用被描述的那个编译器,把 PHP 源码编译成原生二进制。 这不是炫技,是一种自我验证:如果 TypePHP 连一个 MCP server 都编译不出来、跑不起来,那它就不该被推荐给别人用。注释里写得很直白:`main.php` 统一用 `fopen('php://stdin')` 而不是 `STDIN` 常量,因为 `STDIN/STDOUT/STDERR` 在优化器里会被折叠成字面量 `0`,运行时 `fwrite()` 直接抛 `TypeError`。这个坑就是编自己的时候踩出来的。 实测开销: ``` 产物大小 713 KB 进程启动 → initialize 应答 45 ms ``` 把 `PATH` 裁剪到只剩 `C:\Windows\System32`(完全找不到 php),`initialize` 照样应答,12 个工具照样可用。只有 `check_code` 需要一个能跑 `tpc --dry` 的运行时——那是编译器自己的需求,不是 server 的,缺了就以 `isError: true` 如实回报。 --- # 两条通道:正常结果 vs 工具失败 MCP 的 `tools/call` 返回 `result.content[0].text`,外加一个可选的 `isError`。这个字段是整个工具面设计的前提,先讲清楚。 **用户代码不合格**——正常结果,`isError` 不置位: ``` FAILED stage=generate exit=255 file=D:\git\php\aot-compiler\tmp-probe\bad.php diagnostics=1 Diagnostics: - [error] D:/git/php/aot-compiler/tmp-probe/bad.php:3 Unsupported statement: Stmt_Echo ``` **工具执行失败**——`isError: true`: ``` Tool error: File not found: no/such/file.php Pass an absolute path, or a path relative to the repo root. ``` 如果这两者混在一起,模型就会把"我的代码有语法问题"和"这个服务器坏了"当成同一件事,然后开始瞎改代码。 除了这个区分,返回格式还贯穿三条取舍: 1. **每条结果都有出处**(`file:line` 或 `docs/file.md#heading (L行号)`),模型可以顺着去读原文,不用猜。 2. **单次结果有字节预算**(4 KB 或 6 KB),超了显式打 `...[truncated]`,不静默截断。 3. **查不到就说查不到**,不编造一个看起来有用的结果。 # 12 个工具的真实返回 ## 1. `check_code` — 编译器 oracle **参数**:`path`(.php 文件,绝对路径或相对仓库根)。 **成功返回**(214 字节,309 ms,含真实编译): ``` OK stage=generate file=D:\git\php\aot-compiler\examples\hello.php diagnostics=0 Note: stage=generate runs `tpc --dry` (C++ generation only). The compile and link stages are not run, so their errors are not covered. ``` 熟悉领域的人会读出来:`stage=generate` 明确覆盖范围,`diagnostics=0` 是干净,第二行是**免责声明而不是噪音**——链接期错误它管不了。 **失败返回**(见上文 `Stmt_Echo`):`exit=255`、`diagnostics=N`,然后逐条 `- [error] 绝对路径:行 消息`。行号格式 `file:line[:col]`。 **拒绝返回**(30.8 ms,`isError: true`): ``` Tool error: check_code only accepts a PHP source file, got: project.yml Pass a .php file. Directory and project.yml inputs are rejected because tpc would parse the project configuration, which can start a real runtime build (php-builder) even under --dry. ``` 目录 `tools` 也走同一条。这个限制是 review 逼出来的,后文细说。 ## 2. `list_restrictions` — 写码前必读 **`topic=structure`**(668 字节,31 ms): ``` ## 程序结构 [structure] - 全局作用域不允许可执行语句;只允许声明、use、declare、常量定义。echo/赋值等语句必须放进函数或方法。 - 函数和方法内部不允许声明函数,也不允许声明具名类。 - bin 模式必须定义全局 main();签名仅允许 function main() 或 function main(int $argc, array $argv)。 - 文档要求 main() 返回 void;实际示例(examples/qt-taskboard)使用 int 退出码,以编译器行为为准。 - 所有源文件必须是 UTF-8。 Sources: docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md, docs/zh-cn/CLASS_INHERITANCE.md, docs/zh-cn/MIXED_CPP_PHP.md, docs/zh-cn/COMPILER_CLI.md ``` 第四条值得注意:文档和实际示例冲突时,**以编译器行为为准**并且明说。这种"承认内部不一致"的条目比统一口径的条目更有用。 **`topic=types`**(988 字节): ``` ## 类型与声明 [types] - 局部变量一旦被静态推断为具体 native 类型(int/float/bool/string/array),同一作用域内不能重新赋值为不兼容类型;需要 Zend 动态语义时用 std::any() 或文件级 use varint_types。 - 不支持可变变量 $$var。 - declare 只支持 strict_types 与 encoding=UTF-8;strict_types=0 被拒绝;不支持 ticks。 - 严格参数个数:非 variadic 函数不接受声明范围之外的额外参数,func_get_args() 不会放宽签名。 - __construct() 不允许返回值;参数默认值不允许出现在必填参数之前。 - 联合/交叉/nullable 类型以 mixed/any 作为 C++ 表示;静态阶段会提前拒绝确定不兼容的用法。 - 不支持 PHP 8.5 #[NoDiscard];全局/命名空间常量声明上不允许 attributes(class constant 不受限制)。 Sources: ... ``` **不传 topic**:全部 9 个 topic,6000 字节上限。 ## 3. `get_symbol` — 精确查声明 扫 `src/`、`examples/`、`tests/std/`,返回签名 + 位置。 **类**(60 字节,387 ms): ``` src/Translator.php:84 class Translator extends Preprocessor ``` **带类名的方法**(355 ms): ``` src/Translator.php:3914 public function genClassPropertyInit(): string (in class Translator) ``` **顶层类**(433 ms): ``` src/polyfills.php:125 final readonly class StdVector ``` **没找到**(341 ms): ``` No symbol found for: stdMap (searched src/, examples/, tests/std/) ``` 这里有个细节:`stdMap` 返回"没找到"是**诚实**的——`std::map` 是编译器内建容器,不是 PHP 源码里的类声明,源码扫描确实找不到。工具没有为了"看起来有用"去编结果。要查 std 容器该用 `list_restrictions topic=std` 或 `search_docs`。 400 KB 的 `src/Translator.php` 也在扫描范围内,超过旧版 256 KiB 静默上限的问题已经修掉。 ## 4. `list_stdlib` — 标准库用例目录 **不带参数**(373 字节,32 ms): ``` array (802 tests) core (12 tests) dir (2 tests) directory (1 tests) file (6 tests) filters (3 tests) general_functions (6 tests) hrtime (2 tests) math (12 tests) misc (3 tests) network (6 tests) serialize (1 tests) strings (734 tests) time (8 tests) std:: surface docs: docs/zh-cn/STD_CONTAINERS.md, HIGH_PRECISION_TYPES.md, COMPILE_TIME_FUNCTIONS.md, UNIVERSAL_METHODS.md ``` `array` 802 个、`strings` 734 个——这个规模说明为什么"猜 PHP 函数在 TypePHP 里行不行"不可靠,直接查用例更快。 **`category=array`**(4000 字节上限): ``` 001.phpt - array_diff: string-based comparison 002.phpt - array_intersect: string-based comparison 003.phpt - array_unique: sort flags 004.phpt - array_shift: non-packed re-indexing 005.phpt - array_unshift: preserves string keys 006.phpt - ksort: flag handling 007.phpt - array_sum / array_product: type handling 008.phpt - array_combine: non-int/non-string key handling 009.phpt - Test key(), current(), next() & reset() functions array_all_basic.phpt - Test array_all() function : basic functional ``` 标题本身就是可读的边界说明:`array_diff` 用字符串比较、`array_shift` 对非 packed 数组重排索引、`ksort` 处理 flags。模型读完就知道该找哪个 `.phpt` 看完整用例。 ## 5. `get_example` — 可运行示例 **单文件 `hello`**(220 字节,31 ms): ``` # examples/hello.php (187 bytes) <?php function main(): void { echo "Hello World!"; var_dump(PHP_VERSION); global $argv; var_dump($argv); $date = date('Y-m-d H:i:s', time()); var_dump($date); } ``` 整文件返回,因为才 187 字节——预算允许时就给全。 **目录 `qt-taskboard`**(2986 字节,31 ms): ``` # examples/qt-taskboard/ (5 files) examples/qt-taskboard/app/TaskBoardApplication.php examples/qt-taskboard/app/TaskStore.php examples/qt-taskboard/main.php examples/qt-taskboard/php-src/taskboard.stub.php examples/qt-taskboard/tests/task_store_test.php --- README.md --- # TypePHP Taskboard (Qt Widgets) This desktop project management demo uses PHP for task validation, status transitions, search, filtering, counts and PDO_SQLITE persistence. C++ creates Qt widgets, renders PHP-provided data and sends raw UI events back to PHP. ... ...[truncated] --- main.php --- <?php ... --- project.yml --- name: typephp_taskboard version: 0.1.0 mode: bin cxx-std: c++17 sources: - main.php - app - php-src - cpp-src ... ``` 目录走完全不同策略:**清单 + 三段摘录**(README / main.php / project.yml),各自独立截断。这正好是理解一个新项目需要的最少信息——不用把 5 个文件全塞进上下文。 ## 6. `get_stub_api` — C++/PHP ABI 契约 **不带 path**(1375 字节,279 ms):列出全部 28 个 stub 文件: ``` .github/smoke/linux-arm64/cpp-src/platform.stub.php .github/smoke/macos/cpp-src/platform.stub.php examples/android-native/cpp-src/android_ui.stub.php examples/apple-native/ios-src/uikit_bridge.stub.php examples/apple-native/objc-src/macos_bridge.stub.php examples/jni/php-src/jni.stub.php ... examples/qt-taskboard/php-src/taskboard.stub.php ... tests/windows/smoke/cpp-src/winapi.stub.php ``` **带 path**(586 字节,31 ms): ``` # D:\git\php\aot-compiler\examples\qt-taskboard\php-src\taskboard.stub.php (586 bytes) <?php /** Qt renders the view and returns user input; all task decisions live in PHP. */ function qt_board_create(string $title): mixed {} function qt_board_is_open(mixed $window): bool {} function qt_board_process_events(mixed $window): void {} function qt_board_poll_event(mixed $window): array {} function qt_board_set_view(mixed $window, array $rows, array $metrics, string $selectedId): void {} function qt_board_snapshot(mixed $window, string $path): bool {} function qt_board_show_error(mixed $window, string $message): void {} function qt_board_destroy(mixed $window): void {} ``` 这就是红线第 8 条的实物:函数体全空 `{}`、C++ 侧类型用 `mixed`/具体标量。模型照着这个写 stub 就不会踩"函数体非空"的错误。 ## 7. `search_docs` — 双语文档全文检索 **`query=bigInt, limit=2`**(1083 字节,46 ms): ``` [zh-cn] docs/zh-cn/NATIVE_TYPES.md#BigInt 方法 (L334) #### BigInt 方法 | 方法 | 参数 | 返回类型 | C++ 实现 | 说明 | |------|------|---------|---------|------| | `add($x)` | 1 | BigInt | `BigInt::add()` | 加法 | | `sub($x)` | 1 | BigInt | `BigInt::sub()` | 减法 | | `mul($x)` | 1 | BigInt | `BigInt::mul()` | 乘法 | ... ...[truncated] [zh-cn] docs/zh-cn/UNIVERSAL_METHODS.md#10.1 BigInt 方法 (L676) ### 10.1 BigInt 方法 ```php $a = std::bigInt("12345678901234567890"); // 算术(均返回新 BigInt,原值不变) $b = $a->add(1); // $a + 1 ... ``` 两个关键设计在这里:**chunk 按标题切分**(不是按行窗口),所以返回完整小节;**出处带行号** `(L334)`,模型可以精确回读。 **`query=stub, lang=en`**(46.6 ms): ``` [en] docs/en/MIXED_CPP_PHP.md#3. .stub.php File Requirements (L689) ### 3. .stub.php File Requirements In a library project, the `.stub.php` file is used to declare functions implemented in C++, and does not require a library name annotation: ```php <?php function vector_new(int $size, bool $init = false): mixed {} ``` `-m lib` aggregates the library project's `.php` and local `.stub.php` interfaces into `<target>.stub.php`... ## 8. `search_code` — 源码 grep **`query=orderedMap, limit=3`**(196 字节,155 ms): ``` src/NativeClass/NativeClassSupportTrait.php:1938 in trait NativeClassSupportTrait src/Parser/AssignOpTrait.php:619 in trait AssignOpTrait src/Parser/AssignOpTrait.php:635 in trait AssignOpTrait ``` **`query=taskboard, limit=2`**: ``` examples/qt-taskboard/app/TaskBoardApplication.php:4 in class TaskBoardApplication examples/qt-taskboard/app/TaskBoardApplication.php:15 in class TaskBoardApplication ``` 格式是 `file:line in <所在声明>`——`in` 后面是命中行所属的类/trait/函数。这让模型立刻知道"这个符号被谁实现",比裸 `grep -n` 多一条信息,且不用再读文件确认。 ## 9. `get_changelog` **不带参数**(4000 字节上限,15 ms): ``` ## 0.8.0 ### Breaking: native scalar storage is now the default `use native_types` has been removed. Inferred `int`, `float`, and `bool` locals now use `php::Int`, `php::Float`, and `php::Bool` by default. Their storage type is fixed and the compiler never promotes one of these locals to `php::Var` merely because a later operation needs dynamic PHP semantics. Two explicit escape hatches remain: - `use varint_types` is a file-level mode that stores inferred integer values in `php::Var`, preserving PHP's overflow-to-float, non-integral division, and other Zend integer arithmetic behavior. It does not box `float` or `bool` locals. - `std::any($value)` erases the static type of that in ...[truncated] ``` **`version=0.9.5`**——这里返回了一个真实缺口: ``` No changelog section for version: 0.9.5 Available: 0.8.0, 0.8.0(中文) ``` `CHANGELOG.md` 只写到 0.8.0,而发布包已经是 0.9.5/0.9.6。工具没有编造,而是列出实际可用版本。 ## 10. `get_build_config_help` — 构建配置 **不带参数**(1838 字节,31 ms)返回两节: ``` ## 构建配置(project.yml) [build] - mode:bin(默认,需全局 main())/ lib / ext;ext 模式用 info 提供 phpinfo 元数据。 - 常用字段:name/version/cxx-std/cxx-flags/c-flags/ld-flags/sources/ignore/objects/include-paths/defines/link-paths/link-libs/ext-deps/embedded-files/entry/sapi/build-dir/output。 - ext-deps 是 PHP 扩展依赖(生成 ZEND_MOD_REQUIRED);link-libs 是 C++ 链接库;两者不能互相替代。 - 命令行参数优先于 YAML 同名项;--dry 只生成 C++,不编译不链接(链接期错误覆盖不到)。 - bin 模式默认 embed SAPI;cli/fpm SAPI 强依赖 php-builder,从 php-src 构建私有静态运行时。 - embedded-files 把 Composer vendor/只读资源打包进可执行文件;内嵌文件系统不支持 glob()/目录迭代。 Sources: ... ``` ``` ## .stub.php 契约(混合 C++/PHP) [stub] - .stub.php 只声明由外部 C++ 提供的 Zend ABI 符号;函数体必须为空 {},写实现是编译错误。 - 禁止对 .stub.php 中的类使用 #[Native];Native Class 的对象布局必须由 TypePHP 编译器生成和持有。 - 外部 C++ 函数硬规则:参数类型必须全是 php:: 类型;返回类型必须是 php:: 类型;函数名必须以 php_ 前缀开头;必须有对应 .stub.php 声明。 - -m lib 会把项目的 .php 与本地 .stub.php 聚合为目标 <target>.stub.php 并自动加 @import-library;#[\NoExport] 可把声明排除出公共 ABI。 - 范式参考:examples/qt-taskboard/php-src/taskboard.stub.php、examples/prime/php-src/vector.stub.php。 ``` 一次调用给两节,因为**配置和 stub 契约是同一个问题**(怎么写对构建描述)。 ## 11. `list_topics` — 文档目录 **不带参数**(6000 字节上限,31 ms): ``` [zh-cn] ACKNOWLEDGEMENTS.md - 致谢 [zh-cn] AOT_BUILD_SPEED_RESEARCH.md - AOT 编译速度优化研究笔记 [zh-cn] BIG_NUMBER_INPLACE_OPTIMIZATION_PLAN.md - 高精度类型原地运算优化方案 [zh-cn] CLASS_INHERITANCE.md - AOT 编译器类继承规则 [zh-cn] COMPATIBILITY_POLICY.md - TypePHP 兼容性工程准则 [zh-cn] COMPILATION_MODES.md - PHP AOT 编译器编译模式详解 [zh-cn] COMPILER_CLI.md - TypePHP 编译器命令行 [zh-cn] COMPILE_TIME_FUNCTIONS.md - AOT 编译期函数与关键词方法 [zh-cn] CORE_OOA_OOD_OOP_REFACTORING_PLAN.md - TypePHP 核心类 OOA / OOD / OOP 重构计划 [zh-cn] CPP_SYMBOL_NAMING.md - C++ 命名空间、前缀与符号 ABI 规则 [zh-cn] EMBEDDED_FILES.md - 将 PHP 依赖嵌入可执行文件 [zh-cn] GMP_GAP.md - GMP 函数对照表(未实现) [zh-cn] HIGH_PRECISION_TYPES.md - AOT 编译器高精度类型使用教程 [zh-cn] INCOMPATIBLE_PHP_FEATURES.md - AOT 与 PHP 不兼容特性清单 ... ``` 每行 `[lang] 文件名 - 中文标题`。51 篇中文 + 50 篇英文,靠这个目录让模型知道"该去搜哪一篇"。 ## 12. `get_index_info` — 元数据自证 **完整环境**(692 字节,356 ms): ``` repo_root: D:\git\php\aot-compiler server: typephp-mcp 1.0.0 (TypePHP AOT native binary) protocol: 2024-11-05 tpc_version: v0.9.6 git_commit: 4cbb9c0d docs: 51 zh-cn + 50 en markdown files tools (12): check_code, list_restrictions, get_symbol, list_stdlib, get_example, get_stub_api, search_docs, search_code, get_changelog, get_build_config_help, list_topics, get_index_info server_cwd: D:/git/php/aot-compiler server_cwd_is_typephp_project: yes project_dirs: (none auto-detected; set TYPEPHP_MCP_PROJECT_ROOT or open a TypePHP project) staleness: no prebuilt index - docs/examples/src are read live on each call, so results always match this checkout. Rebuild the server after upgrading tpc. ``` 价值全在最后一行和中间两行: - `staleness:` 用一句话解释了为什么不存在"索引过期"——没有索引。 - `tpc_version` / `git_commit` 让模型能自己发现"我查的文档和编译器版本不匹配"。 **降级返回**(`PATH` 里没有 php): ``` tpc_version: unknown (no php on PATH; set TYPEPHP_MCP_PHP) git_commit: unknown ``` 不报错,不猜,明确告诉你缺什么、设哪个变量。 一个有意义的对照:同一份代码,`TYPEPHP_MCP_TPC` 指向发布包时 `tpc_version: v0.9.5`;只设 `TYPEPHP_MCP_PHP` 走 `php bin/tpc.php` 时变成 `v0.9.6`。**版本号反映的是实际使用的编译器后端**,不是写死的。 --- # 把它们串起来:一次真实会话 模型要写一个读 BigInt 的 TypePHP 程序,实际调用顺序: ``` 1. list_restrictions(topic=types) 31 ms ← 先知道 int 不能随便改类型 2. search_docs("bigInt", limit=2) 46 ms ← 拿到 BigInt 方法表和 PHP 示例 3. get_example("hello") 31 ms ← 拿到最小可运行骨架 4. 写代码 5. check_code("/tmp/my.php") 309 ms ← 验证,拿到 file:line 诊断 6. 失败则 get_symbol 或 search_code 定位,改,再 check ``` 总耗时约 420 ms(不含模型生成)。六个调用里五个是纯读、无副作用、可并行。 --- # 三个设计决定 **一、没有预建索引。** docs 2 MB、examples 14 MB、src 3.8 MB、tests/std 7.4 MB,加起来 20 MB,每次调用实时读。听起来很浪费,但它从结构上消除了"索引过期"这一整类 bug——不存在"索引和 checkout 不一致"的状态。代价用预算控制:单次扫描最多 3000 个文件、8 MB 单文件上限、100 MB 总扫描预算,结果 4000/6000 字节预算,UTF-8 边界安全截断,而且截断从不静默——`get_symbol` 扫到上限会显式报 Coverage 截断,绝不假装"没找到"。 **二、约束注入,而不是等模型来问。** 模型不会主动调 `list_restrictions`——它自信地开始写代码。所以约 1600 字节的红线摘要在三个地方同时出现:`initialize` 的 `instructions`、`check_code` 的 tool description、`list_restrictions` 的 description。就算一个工具都不调,头号失败模式也被压住了。 `initialize` 应答实测 1592 字节: ``` capabilities: {"tools":[],"roots":[]} protocolVersion: 2024-11-05 ``` **三、编译器 oracle 有完整生命周期。** `check_code` 是唯一有副作用的工具。这条路径被当成不可信子进程对待:超时(默认 120 s)、输出字节预算(64 KB,但持续排空管道避免子进程被写满阻塞)、POSIX 下整个进程树的内存上限、超时杀进程**树**并清理临时目录、构建目录放在源文件所在卷上(跨卷 `rename` 会失败)。 --- # 安全边界:四条评审逼出来的规则 这个工具最值得写的部分,是 review 抓出来的四个洞。它们都不是功能 bug,而是"工具在用户机器上做了不该做的事"。 ## 1. `--dry` 并不保证不构建 `tpc --dry project.yml` 看起来是纯检查。实际上 `ProjectBuildRunner::run()` 先 `prepare()` 再判断 `isRunDry`——两个动作在不同方法里。配置里只要有 `php-builder: {}`,`prepare()` 就会一路走到 `SapiPhpBuilder::prepare()`,缓存未命中时启动一个真实的 php-src 原生构建:没有构建锁、没有串行保证、没有整树内存限制。正好是 AGENTS.md 明令禁止的那种并发编译。 用拦截器替换 `SapiPhpBuilder` 复现确认: ``` SourcePipelineTrait(613): preparePhpBuilderEnvironment() SourcePipelineTrait(596): prepareRuntimeDependencies() ProjectBuildRunner(41): generateProjectSources() ``` 修复选的是**移除入口**而不是加锁:`check_code` 只接受 `.php` 文件,目录和 `project.yml` 一律返回说明原因的工具错误。单文件模式不解析 YAML,builder 路径从结构上不可达(同一个拦截器下 `builderReached=false`)。 ## 2. 符号链接能绕过后缀检查 第一版先看后缀再 `realpath()`。于是 `alias.php -> project.yml` 通过检查,而编译器打开的是 YAML——把上一个洞又开了回来。 修复是把 `realpath()` 移到类型检查**之前**,检查作用于最终交给 tpc 的那个路径。用真实 Windows 符号链接验证: ``` check_code(alias.php) -> isError=true "only accepts a PHP source file" check_code(good.php) -> OK stage=generate file=D:\...\real.php ``` 第二行才是关键:编译器报的是解析后的真实路径,alias 从未到过 tpc 手里。回归测试用记录型替身编译器,断言 `.php -> YAML` 时调用日志**根本不存在**。还做了变异测试——把 `pathinfo($file)` 放回去,测试会红。 ## 3. 看门狗创建失败后静默裸跑 Windows 上没有 `alarm()`,超时靠一个 `cmd.exe` 辅助进程。如果临时目录不可写,看门狗建不起来,函数返回 `null`,然后 `runProcess()` 继续往下走——进入一个没有任何东西能打断的阻塞读。设 1 秒超时,实际 4.2 秒后才返回,因为子进程自己跑完了。 修复:`null` 就立即 `terminateTree()`、关管道、回收进程、抛明确错误。同样场景现在 **0.58 秒**返回,且不留残留进程。 ## 4. disarm 遗留 ping 子进程 Windows 上 `proc_terminate()` 只杀直接子进程。看门狗是 `cmd.exe` 起的 `ping` 延时,杀掉 cmd 之后 ping 还在跑,跑完整个超时窗口——每个调用漏一个。 修复:先删哨兵文件(即使辅助进程活下来它也杀不了任何东西),再 `taskkill /PID <pid> /T /F` 杀整棵树,最后回收。实测 8 秒看门狗:arm 后出现 `PING.EXE pid=4024`,disarm 后 0.7 秒消失。 --- # CI:四个平台真的编译它 CI 在 Linux x64、Linux arm64、macOS arm64、Windows x64 上都会实际构建 MCP 产物并跑原生 stdio 冒烟测试。缺产物即判失败——不再靠"缺产物就 skip"来证明兼容。 跨平台构建的坑也很实在:Windows 有 tpc 发行包(phpx + PHP SDK 齐全),Unix 没有,`build.sh` 只能去找宿主 embed 运行时。macOS 走静态 `libphp.a` 时,tpc 不按运行时实际扩展集推导系统库,`-lonig -lxml2 -lsqlite3 -lz -lresolv -lcurl -lssl -lcrypto -liconv` 全要应用侧补,`build.sh` 用 `ar -t` 探测归档对象后补全。而 AGENTS.md 的资源安全约束在这里也生效:Linux 下先把清理和编译放进全用户构建锁的临界区,`-j1` 串行,整个进程树塞进 systemd user scope 并设 `MemoryMax`。 --- # 接入 ```json { "mcpServers": { "typephp-mcp": { "type": "stdio", "command": "D:/git/php/aot-compiler/tools/typephp-mcp/typephp_mcp.exe", "args": [], "env": { "TYPEPHP_MCP_ROOT": "D:/git/php/aot-compiler" } } } } ``` 一个坑值得单独说:**路径一律用正斜杠**。某些 MCP 启动器会剥掉配置里的反斜杠,`D:\\git\\...` 变成 `D:git...`,报 `not recognized` 然后连接关闭。 `check_code` 会调编译器,属"副作用"工具,建议在客户端对该 server 单独授权。 # 它坦诚做不到的事 | 缺口 | 表现 | |---|---| | 只覆盖 C++ 生成阶段 | 每次 `check_code` 都在返回里写明"链接期错误不在覆盖范围" | | 不构建项目 | 项目构建该走 `tpc` 本身,在构建锁下跑 | | Linux 单文件 `--dry` 仍可能构建 `libphpx.so` | tpc 既有 dry-run 语义,MCP 关不掉 | | `CHANGELOG.md` 落后于发布版本 | `get_changelog(version=0.9.5)` 如实回 `No changelog section` | | `std::map` 这类内建容器查不到 | `get_symbol("stdMap")` 返回 not found(它们不在 PHP 源码里) | | 硬链接绕过 `.php` 后缀检查 | 无解析目标;tpc 当作单文件解析失败,不进入 project 路径,无构建风险 | 最后一条是符号链接修复后的残留讨论点——`realpath()` 能处理的都处理了,硬链接需要内容嗅探,需要维护者拍板才做。 # 结语 12 个工具、7 个文件、5600 行 PHP,编译成 713 KB 原生二进制,启动 45 ms,PATH 里没有 PHP 也照样跑。 `typephp-mcp` 有意思的地方不在"又一个 MCP server",而在于它是被描述对象的**第一个用户**——如果 TypePHP 连一个 MCP server 都编不出来,它就不该被推荐。而它最有价值的那几行代码,是 review 逼出来的防御:一个"只做检查"的工具,也可能在用户机器上悄悄启动一个 `make -j`。 真正决定好不好用的不是工具数量,而是返回格式里那些不起眼的决定:`isError` 区分两类失败、每条结果带 `file:line` 出处、截断处显式标记、查不到时说 "No symbol found" 而不是编一个结果。 工具的能力边界,往往不是由它的功能定义的,而是由它**拒绝做什么**定义的。
发布于昨天 · 1 次浏览 · 来自
TypePHP
Rango-H
> 实测环境:Windows x64,产物 `typephp_mcp.exe`(713 KB),编译器包 `tpc_v0.9.5_windows_x64`。文中所有工具返回都是从 AOT 二进制的真实 stdio 会话里抓取的原文,不是示意图。 ## 问题:模型写不对 TypePHP 把 PHP 编译成原生二进制,PHP 语言里有一大批东西不能用。全局作用域不能有可执行语句,`$$var` 不支持,`strict_types=0` 被拒绝,局部变量被静态推断成 `int` 之后不能再赋成 `string`,`Closure::bind` 不能调用,`.stub.php` 的正文必须是空的 `{}`。 这些规则散落在 100 多篇文档、几百个 `tests/std` 用例和翻译器源码里。语言模型对 PHP 很熟,对"PHP 的一个子集"完全不熟——它写出看起来完全正常的 PHP,然后编译器在第 3 行报 `Unsupported statement: Stmt_Echo`。 `typephp-mcp` 就是为堵住这个落差做的:一个 MCP(Model Context Protocol)server,让 AI 客户端在写代码之前知道红线、写完立刻能验证。 ## 它是什么 一个 stdio server,以换行分隔的 JSON-RPC 2.0 对外服务,暴露 12 个工具: | 工具 | 什么时候用 | |---|---| | `check_code` | 提交前必调。跑 `tpc --dry`,返回结构化诊断 | | `list_restrictions` | 写码前必读。按 topic 查 TypePHP 裁剪规则 | | `search_docs` | 双语文档全文检索,带 `file#heading (L行号)` 出处 | | `get_symbol` | 精确查函数/类/接口/trait 声明,返回签名 + `file:line` | | `search_code` | grep `src/`、`examples/`、`tests/std`,返回所在声明 | | `list_stdlib` | 标准库用例目录(`array` 802 个、`strings` 734 个) | | `get_example` | 可运行示例(单文件全文,或目录清单 + 三段摘录) | | `get_stub_api` | `.stub.php` ABI 契约 | | `get_changelog` / `get_build_config_help` / `list_topics` | 版本变更、构建配置、文档目录 | | `get_index_info` | 元数据自证:repo 根、tpc 版本、git commit、文档数 | ## 第一件值得说的事:它是用 TypePHP 编译的 设计文档最初的计划是 Python + FastMCP。最后没这么做——改用被描述的那个编译器,把 PHP 源码编译成原生二进制。 这不是炫技,是一种自我验证:如果 TypePHP 连一个 MCP server 都编译不出来、跑不起来,那它就不该被推荐给别人用。注释里写得很直白:`main.php` 统一用 `fopen('php://stdin')` 而不是 `STDIN` 常量,因为 `STDIN/STDOUT/STDERR` 在优化器里会被折叠成字面量 `0`,运行时 `fwrite()` 直接抛 `TypeError`。这个坑就是编自己的时候踩出来的。 实测开销: ``` 产物大小 713 KB 进程启动 → initialize 应答 45 ms ``` 把 `PATH` 裁剪到只剩 `C:\Windows\System32`(完全找不到 php),`initialize` 照样应答,12 个工具照样可用。只有 `check_code` 需要一个能跑 `tpc --dry` 的运行时——那是编译器自己的需求,不是 server 的,缺了就以 `isError: true` 如实回报。 --- # 两条通道:正常结果 vs 工具失败 MCP 的 `tools/call` 返回 `result.content[0].text`,外加一个可选的 `isError`。这个字段是整个工具面设计的前提,先讲清楚。 **用户代码不合格**——正常结果,`isError` 不置位: ``` FAILED stage=generate exit=255 file=D:\git\php\aot-compiler\tmp-probe\bad.php diagnostics=1 Diagnostics: - [error] D:/git/php/aot-compiler/tmp-probe/bad.php:3 Unsupported statement: Stmt_Echo ``` **工具执行失败**——`isError: true`: ``` Tool error: File not found: no/such/file.php Pass an absolute path, or a path relative to the repo root. ``` 如果这两者混在一起,模型就会把"我的代码有语法问题"和"这个服务器坏了"当成同一件事,然后开始瞎改代码。 除了这个区分,返回格式还贯穿三条取舍: 1. **每条结果都有出处**(`file:line` 或 `docs/file.md#heading (L行号)`),模型可以顺着去读原文,不用猜。 2. **单次结果有字节预算**(4 KB 或 6 KB),超了显式打 `...[truncated]`,不静默截断。 3. **查不到就说查不到**,不编造一个看起来有用的结果。 # 12 个工具的真实返回 ## 1. `check_code` — 编译器 oracle **参数**:`path`(.php 文件,绝对路径或相对仓库根)。 **成功返回**(214 字节,309 ms,含真实编译): ``` OK stage=generate file=D:\git\php\aot-compiler\examples\hello.php diagnostics=0 Note: stage=generate runs `tpc --dry` (C++ generation only). The compile and link stages are not run, so their errors are not covered. ``` 熟悉领域的人会读出来:`stage=generate` 明确覆盖范围,`diagnostics=0` 是干净,第二行是**免责声明而不是噪音**——链接期错误它管不了。 **失败返回**(见上文 `Stmt_Echo`):`exit=255`、`diagnostics=N`,然后逐条 `- [error] 绝对路径:行 消息`。行号格式 `file:line[:col]`。 **拒绝返回**(30.8 ms,`isError: true`): ``` Tool error: check_code only accepts a PHP source file, got: project.yml Pass a .php file. Directory and project.yml inputs are rejected because tpc would parse the project configuration, which can start a real runtime build (php-builder) even under --dry. ``` 目录 `tools` 也走同一条。这个限制是 review 逼出来的,后文细说。 ## 2. `list_restrictions` — 写码前必读 **`topic=structure`**(668 字节,31 ms): ``` ## 程序结构 [structure] - 全局作用域不允许可执行语句;只允许声明、use、declare、常量定义。echo/赋值等语句必须放进函数或方法。 - 函数和方法内部不允许声明函数,也不允许声明具名类。 - bin 模式必须定义全局 main();签名仅允许 function main() 或 function main(int $argc, array $argv)。 - 文档要求 main() 返回 void;实际示例(examples/qt-taskboard)使用 int 退出码,以编译器行为为准。 - 所有源文件必须是 UTF-8。 Sources: docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md, docs/zh-cn/CLASS_INHERITANCE.md, docs/zh-cn/MIXED_CPP_PHP.md, docs/zh-cn/COMPILER_CLI.md ``` 第四条值得注意:文档和实际示例冲突时,**以编译器行为为准**并且明说。这种"承认内部不一致"的条目比统一口径的条目更有用。 **`topic=types`**(988 字节): ``` ## 类型与声明 [types] - 局部变量一旦被静态推断为具体 native 类型(int/float/bool/string/array),同一作用域内不能重新赋值为不兼容类型;需要 Zend 动态语义时用 std::any() 或文件级 use varint_types。 - 不支持可变变量 $$var。 - declare 只支持 strict_types 与 encoding=UTF-8;strict_types=0 被拒绝;不支持 ticks。 - 严格参数个数:非 variadic 函数不接受声明范围之外的额外参数,func_get_args() 不会放宽签名。 - __construct() 不允许返回值;参数默认值不允许出现在必填参数之前。 - 联合/交叉/nullable 类型以 mixed/any 作为 C++ 表示;静态阶段会提前拒绝确定不兼容的用法。 - 不支持 PHP 8.5 #[NoDiscard];全局/命名空间常量声明上不允许 attributes(class constant 不受限制)。 Sources: ... ``` **不传 topic**:全部 9 个 topic,6000 字节上限。 ## 3. `get_symbol` — 精确查声明 扫 `src/`、`examples/`、`tests/std/`,返回签名 + 位置。 **类**(60 字节,387 ms): ``` src/Translator.php:84 class Translator extends Preprocessor ``` **带类名的方法**(355 ms): ``` src/Translator.php:3914 public function genClassPropertyInit(): string (in class Translator) ``` **顶层类**(433 ms): ``` src/polyfills.php:125 final readonly class StdVector ``` **没找到**(341 ms): ``` No symbol found for: stdMap (searched src/, examples/, tests/std/) ``` 这里有个细节:`stdMap` 返回"没找到"是**诚实**的——`std::map` 是编译器内建容器,不是 PHP 源码里的类声明,源码扫描确实找不到。工具没有为了"看起来有用"去编结果。要查 std 容器该用 `list_restrictions topic=std` 或 `search_docs`。 400 KB 的 `src/Translator.php` 也在扫描范围内,超过旧版 256 KiB 静默上限的问题已经修掉。 ## 4. `list_stdlib` — 标准库用例目录 **不带参数**(373 字节,32 ms): ``` array (802 tests) core (12 tests) dir (2 tests) directory (1 tests) file (6 tests) filters (3 tests) general_functions (6 tests) hrtime (2 tests) math (12 tests) misc (3 tests) network (6 tests) serialize (1 tests) strings (734 tests) time (8 tests) std:: surface docs: docs/zh-cn/STD_CONTAINERS.md, HIGH_PRECISION_TYPES.md, COMPILE_TIME_FUNCTIONS.md, UNIVERSAL_METHODS.md ``` `array` 802 个、`strings` 734 个——这个规模说明为什么"猜 PHP 函数在 TypePHP 里行不行"不可靠,直接查用例更快。 **`category=array`**(4000 字节上限): ``` 001.phpt - array_diff: string-based comparison 002.phpt - array_intersect: string-based comparison 003.phpt - array_unique: sort flags 004.phpt - array_shift: non-packed re-indexing 005.phpt - array_unshift: preserves string keys 006.phpt - ksort: flag handling 007.phpt - array_sum / array_product: type handling 008.phpt - array_combine: non-int/non-string key handling 009.phpt - Test key(), current(), next() & reset() functions array_all_basic.phpt - Test array_all() function : basic functional ``` 标题本身就是可读的边界说明:`array_diff` 用字符串比较、`array_shift` 对非 packed 数组重排索引、`ksort` 处理 flags。模型读完就知道该找哪个 `.phpt` 看完整用例。 ## 5. `get_example` — 可运行示例 **单文件 `hello`**(220 字节,31 ms): ``` # examples/hello.php (187 bytes) <?php function main(): void { echo "Hello World!"; var_dump(PHP_VERSION); global $argv; var_dump($argv); $date = date('Y-m-d H:i:s', time()); var_dump($date); } ``` 整文件返回,因为才 187 字节——预算允许时就给全。 **目录 `qt-taskboard`**(2986 字节,31 ms): ``` # examples/qt-taskboard/ (5 files) examples/qt-taskboard/app/TaskBoardApplication.php examples/qt-taskboard/app/TaskStore.php examples/qt-taskboard/main.php examples/qt-taskboard/php-src/taskboard.stub.php examples/qt-taskboard/tests/task_store_test.php --- README.md --- # TypePHP Taskboard (Qt Widgets) This desktop project management demo uses PHP for task validation, status transitions, search, filtering, counts and PDO_SQLITE persistence. C++ creates Qt widgets, renders PHP-provided data and sends raw UI events back to PHP. ... ...[truncated] --- main.php --- <?php ... --- project.yml --- name: typephp_taskboard version: 0.1.0 mode: bin cxx-std: c++17 sources: - main.php - app - php-src - cpp-src ... ``` 目录走完全不同策略:**清单 + 三段摘录**(README / main.php / project.yml),各自独立截断。这正好是理解一个新项目需要的最少信息——不用把 5 个文件全塞进上下文。 ## 6. `get_stub_api` — C++/PHP ABI 契约 **不带 path**(1375 字节,279 ms):列出全部 28 个 stub 文件: ``` .github/smoke/linux-arm64/cpp-src/platform.stub.php .github/smoke/macos/cpp-src/platform.stub.php examples/android-native/cpp-src/android_ui.stub.php examples/apple-native/ios-src/uikit_bridge.stub.php examples/apple-native/objc-src/macos_bridge.stub.php examples/jni/php-src/jni.stub.php ... examples/qt-taskboard/php-src/taskboard.stub.php ... tests/windows/smoke/cpp-src/winapi.stub.php ``` **带 path**(586 字节,31 ms): ``` # D:\git\php\aot-compiler\examples\qt-taskboard\php-src\taskboard.stub.php (586 bytes) <?php /** Qt renders the view and returns user input; all task decisions live in PHP. */ function qt_board_create(string $title): mixed {} function qt_board_is_open(mixed $window): bool {} function qt_board_process_events(mixed $window): void {} function qt_board_poll_event(mixed $window): array {} function qt_board_set_view(mixed $window, array $rows, array $metrics, string $selectedId): void {} function qt_board_snapshot(mixed $window, string $path): bool {} function qt_board_show_error(mixed $window, string $message): void {} function qt_board_destroy(mixed $window): void {} ``` 这就是红线第 8 条的实物:函数体全空 `{}`、C++ 侧类型用 `mixed`/具体标量。模型照着这个写 stub 就不会踩"函数体非空"的错误。 ## 7. `search_docs` — 双语文档全文检索 **`query=bigInt, limit=2`**(1083 字节,46 ms): ``` [zh-cn] docs/zh-cn/NATIVE_TYPES.md#BigInt 方法 (L334) #### BigInt 方法 | 方法 | 参数 | 返回类型 | C++ 实现 | 说明 | |------|------|---------|---------|------| | `add($x)` | 1 | BigInt | `BigInt::add()` | 加法 | | `sub($x)` | 1 | BigInt | `BigInt::sub()` | 减法 | | `mul($x)` | 1 | BigInt | `BigInt::mul()` | 乘法 | ... ...[truncated] [zh-cn] docs/zh-cn/UNIVERSAL_METHODS.md#10.1 BigInt 方法 (L676) ### 10.1 BigInt 方法 ```php $a = std::bigInt("12345678901234567890"); // 算术(均返回新 BigInt,原值不变) $b = $a->add(1); // $a + 1 ... ``` 两个关键设计在这里:**chunk 按标题切分**(不是按行窗口),所以返回完整小节;**出处带行号** `(L334)`,模型可以精确回读。 **`query=stub, lang=en`**(46.6 ms): ``` [en] docs/en/MIXED_CPP_PHP.md#3. .stub.php File Requirements (L689) ### 3. .stub.php File Requirements In a library project, the `.stub.php` file is used to declare functions implemented in C++, and does not require a library name annotation: ```php <?php function vector_new(int $size, bool $init = false): mixed {} ``` `-m lib` aggregates the library project's `.php` and local `.stub.php` interfaces into `<target>.stub.php`... ## 8. `search_code` — 源码 grep **`query=orderedMap, limit=3`**(196 字节,155 ms): ``` src/NativeClass/NativeClassSupportTrait.php:1938 in trait NativeClassSupportTrait src/Parser/AssignOpTrait.php:619 in trait AssignOpTrait src/Parser/AssignOpTrait.php:635 in trait AssignOpTrait ``` **`query=taskboard, limit=2`**: ``` examples/qt-taskboard/app/TaskBoardApplication.php:4 in class TaskBoardApplication examples/qt-taskboard/app/TaskBoardApplication.php:15 in class TaskBoardApplication ``` 格式是 `file:line in <所在声明>`——`in` 后面是命中行所属的类/trait/函数。这让模型立刻知道"这个符号被谁实现",比裸 `grep -n` 多一条信息,且不用再读文件确认。 ## 9. `get_changelog` **不带参数**(4000 字节上限,15 ms): ``` ## 0.8.0 ### Breaking: native scalar storage is now the default `use native_types` has been removed. Inferred `int`, `float`, and `bool` locals now use `php::Int`, `php::Float`, and `php::Bool` by default. Their storage type is fixed and the compiler never promotes one of these locals to `php::Var` merely because a later operation needs dynamic PHP semantics. Two explicit escape hatches remain: - `use varint_types` is a file-level mode that stores inferred integer values in `php::Var`, preserving PHP's overflow-to-float, non-integral division, and other Zend integer arithmetic behavior. It does not box `float` or `bool` locals. - `std::any($value)` erases the static type of that in ...[truncated] ``` **`version=0.9.5`**——这里返回了一个真实缺口: ``` No changelog section for version: 0.9.5 Available: 0.8.0, 0.8.0(中文) ``` `CHANGELOG.md` 只写到 0.8.0,而发布包已经是 0.9.5/0.9.6。工具没有编造,而是列出实际可用版本。 ## 10. `get_build_config_help` — 构建配置 **不带参数**(1838 字节,31 ms)返回两节: ``` ## 构建配置(project.yml) [build] - mode:bin(默认,需全局 main())/ lib / ext;ext 模式用 info 提供 phpinfo 元数据。 - 常用字段:name/version/cxx-std/cxx-flags/c-flags/ld-flags/sources/ignore/objects/include-paths/defines/link-paths/link-libs/ext-deps/embedded-files/entry/sapi/build-dir/output。 - ext-deps 是 PHP 扩展依赖(生成 ZEND_MOD_REQUIRED);link-libs 是 C++ 链接库;两者不能互相替代。 - 命令行参数优先于 YAML 同名项;--dry 只生成 C++,不编译不链接(链接期错误覆盖不到)。 - bin 模式默认 embed SAPI;cli/fpm SAPI 强依赖 php-builder,从 php-src 构建私有静态运行时。 - embedded-files 把 Composer vendor/只读资源打包进可执行文件;内嵌文件系统不支持 glob()/目录迭代。 Sources: ... ``` ``` ## .stub.php 契约(混合 C++/PHP) [stub] - .stub.php 只声明由外部 C++ 提供的 Zend ABI 符号;函数体必须为空 {},写实现是编译错误。 - 禁止对 .stub.php 中的类使用 #[Native];Native Class 的对象布局必须由 TypePHP 编译器生成和持有。 - 外部 C++ 函数硬规则:参数类型必须全是 php:: 类型;返回类型必须是 php:: 类型;函数名必须以 php_ 前缀开头;必须有对应 .stub.php 声明。 - -m lib 会把项目的 .php 与本地 .stub.php 聚合为目标 <target>.stub.php 并自动加 @import-library;#[\NoExport] 可把声明排除出公共 ABI。 - 范式参考:examples/qt-taskboard/php-src/taskboard.stub.php、examples/prime/php-src/vector.stub.php。 ``` 一次调用给两节,因为**配置和 stub 契约是同一个问题**(怎么写对构建描述)。 ## 11. `list_topics` — 文档目录 **不带参数**(6000 字节上限,31 ms): ``` [zh-cn] ACKNOWLEDGEMENTS.md - 致谢 [zh-cn] AOT_BUILD_SPEED_RESEARCH.md - AOT 编译速度优化研究笔记 [zh-cn] BIG_NUMBER_INPLACE_OPTIMIZATION_PLAN.md - 高精度类型原地运算优化方案 [zh-cn] CLASS_INHERITANCE.md - AOT 编译器类继承规则 [zh-cn] COMPATIBILITY_POLICY.md - TypePHP 兼容性工程准则 [zh-cn] COMPILATION_MODES.md - PHP AOT 编译器编译模式详解 [zh-cn] COMPILER_CLI.md - TypePHP 编译器命令行 [zh-cn] COMPILE_TIME_FUNCTIONS.md - AOT 编译期函数与关键词方法 [zh-cn] CORE_OOA_OOD_OOP_REFACTORING_PLAN.md - TypePHP 核心类 OOA / OOD / OOP 重构计划 [zh-cn] CPP_SYMBOL_NAMING.md - C++ 命名空间、前缀与符号 ABI 规则 [zh-cn] EMBEDDED_FILES.md - 将 PHP 依赖嵌入可执行文件 [zh-cn] GMP_GAP.md - GMP 函数对照表(未实现) [zh-cn] HIGH_PRECISION_TYPES.md - AOT 编译器高精度类型使用教程 [zh-cn] INCOMPATIBLE_PHP_FEATURES.md - AOT 与 PHP 不兼容特性清单 ... ``` 每行 `[lang] 文件名 - 中文标题`。51 篇中文 + 50 篇英文,靠这个目录让模型知道"该去搜哪一篇"。 ## 12. `get_index_info` — 元数据自证 **完整环境**(692 字节,356 ms): ``` repo_root: D:\git\php\aot-compiler server: typephp-mcp 1.0.0 (TypePHP AOT native binary) protocol: 2024-11-05 tpc_version: v0.9.6 git_commit: 4cbb9c0d docs: 51 zh-cn + 50 en markdown files tools (12): check_code, list_restrictions, get_symbol, list_stdlib, get_example, get_stub_api, search_docs, search_code, get_changelog, get_build_config_help, list_topics, get_index_info server_cwd: D:/git/php/aot-compiler server_cwd_is_typephp_project: yes project_dirs: (none auto-detected; set TYPEPHP_MCP_PROJECT_ROOT or open a TypePHP project) staleness: no prebuilt index - docs/examples/src are read live on each call, so results always match this checkout. Rebuild the server after upgrading tpc. ``` 价值全在最后一行和中间两行: - `staleness:` 用一句话解释了为什么不存在"索引过期"——没有索引。 - `tpc_version` / `git_commit` 让模型能自己发现"我查的文档和编译器版本不匹配"。 **降级返回**(`PATH` 里没有 php): ``` tpc_version: unknown (no php on PATH; set TYPEPHP_MCP_PHP) git_commit: unknown ``` 不报错,不猜,明确告诉你缺什么、设哪个变量。 一个有意义的对照:同一份代码,`TYPEPHP_MCP_TPC` 指向发布包时 `tpc_version: v0.9.5`;只设 `TYPEPHP_MCP_PHP` 走 `php bin/tpc.php` 时变成 `v0.9.6`。**版本号反映的是实际使用的编译器后端**,不是写死的。 --- # 把它们串起来:一次真实会话 模型要写一个读 BigInt 的 TypePHP 程序,实际调用顺序: ``` 1. list_restrictions(topic=types) 31 ms ← 先知道 int 不能随便改类型 2. search_docs("bigInt", limit=2) 46 ms ← 拿到 BigInt 方法表和 PHP 示例 3. get_example("hello") 31 ms ← 拿到最小可运行骨架 4. 写代码 5. check_code("/tmp/my.php") 309 ms ← 验证,拿到 file:line 诊断 6. 失败则 get_symbol 或 search_code 定位,改,再 check ``` 总耗时约 420 ms(不含模型生成)。六个调用里五个是纯读、无副作用、可并行。 --- # 三个设计决定 **一、没有预建索引。** docs 2 MB、examples 14 MB、src 3.8 MB、tests/std 7.4 MB,加起来 20 MB,每次调用实时读。听起来很浪费,但它从结构上消除了"索引过期"这一整类 bug——不存在"索引和 checkout 不一致"的状态。代价用预算控制:单次扫描最多 3000 个文件、8 MB 单文件上限、100 MB 总扫描预算,结果 4000/6000 字节预算,UTF-8 边界安全截断,而且截断从不静默——`get_symbol` 扫到上限会显式报 Coverage 截断,绝不假装"没找到"。 **二、约束注入,而不是等模型来问。** 模型不会主动调 `list_restrictions`——它自信地开始写代码。所以约 1600 字节的红线摘要在三个地方同时出现:`initialize` 的 `instructions`、`check_code` 的 tool description、`list_restrictions` 的 description。就算一个工具都不调,头号失败模式也被压住了。 `initialize` 应答实测 1592 字节: ``` capabilities: {"tools":[],"roots":[]} protocolVersion: 2024-11-05 ``` **三、编译器 oracle 有完整生命周期。** `check_code` 是唯一有副作用的工具。这条路径被当成不可信子进程对待:超时(默认 120 s)、输出字节预算(64 KB,但持续排空管道避免子进程被写满阻塞)、POSIX 下整个进程树的内存上限、超时杀进程**树**并清理临时目录、构建目录放在源文件所在卷上(跨卷 `rename` 会失败)。 --- # 安全边界:四条评审逼出来的规则 这个工具最值得写的部分,是 review 抓出来的四个洞。它们都不是功能 bug,而是"工具在用户机器上做了不该做的事"。 ## 1. `--dry` 并不保证不构建 `tpc --dry project.yml` 看起来是纯检查。实际上 `ProjectBuildRunner::run()` 先 `prepare()` 再判断 `isRunDry`——两个动作在不同方法里。配置里只要有 `php-builder: {}`,`prepare()` 就会一路走到 `SapiPhpBuilder::prepare()`,缓存未命中时启动一个真实的 php-src 原生构建:没有构建锁、没有串行保证、没有整树内存限制。正好是 AGENTS.md 明令禁止的那种并发编译。 用拦截器替换 `SapiPhpBuilder` 复现确认: ``` SourcePipelineTrait(613): preparePhpBuilderEnvironment() SourcePipelineTrait(596): prepareRuntimeDependencies() ProjectBuildRunner(41): generateProjectSources() ``` 修复选的是**移除入口**而不是加锁:`check_code` 只接受 `.php` 文件,目录和 `project.yml` 一律返回说明原因的工具错误。单文件模式不解析 YAML,builder 路径从结构上不可达(同一个拦截器下 `builderReached=false`)。 ## 2. 符号链接能绕过后缀检查 第一版先看后缀再 `realpath()`。于是 `alias.php -> project.yml` 通过检查,而编译器打开的是 YAML——把上一个洞又开了回来。 修复是把 `realpath()` 移到类型检查**之前**,检查作用于最终交给 tpc 的那个路径。用真实 Windows 符号链接验证: ``` check_code(alias.php) -> isError=true "only accepts a PHP source file" check_code(good.php) -> OK stage=generate file=D:\...\real.php ``` 第二行才是关键:编译器报的是解析后的真实路径,alias 从未到过 tpc 手里。回归测试用记录型替身编译器,断言 `.php -> YAML` 时调用日志**根本不存在**。还做了变异测试——把 `pathinfo($file)` 放回去,测试会红。 ## 3. 看门狗创建失败后静默裸跑 Windows 上没有 `alarm()`,超时靠一个 `cmd.exe` 辅助进程。如果临时目录不可写,看门狗建不起来,函数返回 `null`,然后 `runProcess()` 继续往下走——进入一个没有任何东西能打断的阻塞读。设 1 秒超时,实际 4.2 秒后才返回,因为子进程自己跑完了。 修复:`null` 就立即 `terminateTree()`、关管道、回收进程、抛明确错误。同样场景现在 **0.58 秒**返回,且不留残留进程。 ## 4. disarm 遗留 ping 子进程 Windows 上 `proc_terminate()` 只杀直接子进程。看门狗是 `cmd.exe` 起的 `ping` 延时,杀掉 cmd 之后 ping 还在跑,跑完整个超时窗口——每个调用漏一个。 修复:先删哨兵文件(即使辅助进程活下来它也杀不了任何东西),再 `taskkill /PID <pid> /T /F` 杀整棵树,最后回收。实测 8 秒看门狗:arm 后出现 `PING.EXE pid=4024`,disarm 后 0.7 秒消失。 --- # CI:四个平台真的编译它 CI 在 Linux x64、Linux arm64、macOS arm64、Windows x64 上都会实际构建 MCP 产物并跑原生 stdio 冒烟测试。缺产物即判失败——不再靠"缺产物就 skip"来证明兼容。 跨平台构建的坑也很实在:Windows 有 tpc 发行包(phpx + PHP SDK 齐全),Unix 没有,`build.sh` 只能去找宿主 embed 运行时。macOS 走静态 `libphp.a` 时,tpc 不按运行时实际扩展集推导系统库,`-lonig -lxml2 -lsqlite3 -lz -lresolv -lcurl -lssl -lcrypto -liconv` 全要应用侧补,`build.sh` 用 `ar -t` 探测归档对象后补全。而 AGENTS.md 的资源安全约束在这里也生效:Linux 下先把清理和编译放进全用户构建锁的临界区,`-j1` 串行,整个进程树塞进 systemd user scope 并设 `MemoryMax`。 --- # 接入 ```json { "mcpServers": { "typephp-mcp": { "type": "stdio", "command": "D:/git/php/aot-compiler/tools/typephp-mcp/typephp_mcp.exe", "args": [], "env": { "TYPEPHP_MCP_ROOT": "D:/git/php/aot-compiler" } } } } ``` 一个坑值得单独说:**路径一律用正斜杠**。某些 MCP 启动器会剥掉配置里的反斜杠,`D:\\git\\...` 变成 `D:git...`,报 `not recognized` 然后连接关闭。 `check_code` 会调编译器,属"副作用"工具,建议在客户端对该 server 单独授权。 # 它坦诚做不到的事 | 缺口 | 表现 | |---|---| | 只覆盖 C++ 生成阶段 | 每次 `check_code` 都在返回里写明"链接期错误不在覆盖范围" | | 不构建项目 | 项目构建该走 `tpc` 本身,在构建锁下跑 | | Linux 单文件 `--dry` 仍可能构建 `libphpx.so` | tpc 既有 dry-run 语义,MCP 关不掉 | | `CHANGELOG.md` 落后于发布版本 | `get_changelog(version=0.9.5)` 如实回 `No changelog section` | | `std::map` 这类内建容器查不到 | `get_symbol("stdMap")` 返回 not found(它们不在 PHP 源码里) | | 硬链接绕过 `.php` 后缀检查 | 无解析目标;tpc 当作单文件解析失败,不进入 project 路径,无构建风险 | 最后一条是符号链接修复后的残留讨论点——`realpath()` 能处理的都处理了,硬链接需要内容嗅探,需要维护者拍板才做。 # 结语 12 个工具、7 个文件、5600 行 PHP,编译成 713 KB 原生二进制,启动 45 ms,PATH 里没有 PHP 也照样跑。 `typephp-mcp` 有意思的地方不在"又一个 MCP server",而在于它是被描述对象的**第一个用户**——如果 TypePHP 连一个 MCP server 都编不出来,它就不该被推荐。而它最有价值的那几行代码,是 review 逼出来的防御:一个"只做检查"的工具,也可能在用户机器上悄悄启动一个 `make -j`。 真正决定好不好用的不是工具数量,而是返回格式里那些不起眼的决定:`isError` 区分两类失败、每条结果带 `file:line` 出处、截断处显式标记、查不到时说 "No symbol found" 而不是编一个结果。 工具的能力边界,往往不是由它的功能定义的,而是由它**拒绝做什么**定义的。
赞
0
收藏
提问
分享
讨论
建议
公告
开发框架
TypePHP
CodeGalaxy
登录
后参与评论
评论
还没有评论!