diff --git a/.github/actions/unix-arm64-build/action.yml b/.github/actions/unix-arm64-build/action.yml
index e993449a..bdba46f1 100644
--- a/.github/actions/unix-arm64-build/action.yml
+++ b/.github/actions/unix-arm64-build/action.yml
@@ -195,6 +195,36 @@ runs:
output="$("${smoke_binary}")"
test "${output}" = '${{ inputs.smoke-output }}'
+ - name: Build typephp-mcp
+ shell: bash
+ run: |
+ export PHP_HOME="$(php-config --prefix)"
+ bash tools/typephp-mcp/build.sh
+ test -x tools/typephp-mcp/typephp_mcp
+
+ - name: Run typephp-mcp native smoke
+ shell: bash
+ run: |
+ # Point check_code at the compiler this job just built, so it never
+ # depends on the auto-detection order.
+ export TYPEPHP_MCP_TPC="${PWD}/tpc"
+ # 10 minute wall-clock limit: a wedged process must fail the stage
+ # rather than hold the job open. GNU timeout is missing on macOS, and
+ # a plain fallback there let a non-replying server hang the job
+ # forever, so use perl (shipped with macOS) as the watchdog.
+ run_smoke() {
+ if command -v timeout >/dev/null 2>&1; then
+ timeout 600 "$@"
+ elif command -v perl >/dev/null 2>&1; then
+ perl -e 'alarm shift; exec @ARGV' 600 "$@"
+ else
+ echo "[smoke] ERROR: neither timeout nor perl is available for the watchdog" >&2
+ return 1
+ fi
+ }
+ run_smoke php tools/typephp-mcp/tests/mcp-binary-smoke.php tools/typephp-mcp/typephp_mcp "${{ github.workspace }}"
+ run_smoke php tools/typephp-mcp/tests/mcp-binary-smoke.php --interpreted "${{ github.workspace }}"
+
- name: Show native dependencies
shell: bash
run: |
diff --git a/.github/workflows/linux-x64.yml b/.github/workflows/linux-x64.yml
index 278ff326..9fddf0e7 100644
--- a/.github/workflows/linux-x64.yml
+++ b/.github/workflows/linux-x64.yml
@@ -195,6 +195,7 @@ jobs:
php: ["8.4", "8.5"]
env:
PHPX_HOME: ${{ github.workspace }}/third_party/phpx
+ TYPEPHP_MCP_REQUIRE_BINARY: '1'
steps:
- name: Checkout TypePHP
@@ -274,6 +275,19 @@ jobs:
shell: bash
run: echo "LD_LIBRARY_PATH=${PHPX_HOME}/lib:$(php-config --prefix)/lib" >> "${GITHUB_ENV}"
+ - name: Build typephp-mcp
+ shell: bash
+ run: |
+ php_home="$(php-config --prefix)"
+ export PHP_HOME="${php_home}"
+ # Lock critical section first: fails fast and needs no toolchain.
+ bash tools/typephp-mcp/tests/build-lock-test.sh
+ bash tools/typephp-mcp/build.sh
+ test -x tools/typephp-mcp/typephp_mcp
+ # Pin check_code to the compiler built above rather than relying on
+ # the auto-detection order (the repository also ships bin/tpc.php).
+ echo "TYPEPHP_MCP_TPC=${PWD}/tpc" >> "${GITHUB_ENV}"
+
- name: Run PHPUnit
run: vendor/bin/phpunit
diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml
index 0d991fc3..4a126ce7 100644
--- a/.github/workflows/windows-build.yml
+++ b/.github/workflows/windows-build.yml
@@ -348,6 +348,65 @@ jobs:
throw 'The compiler did not produce tpc.exe'
}
+ - name: Build typephp-mcp
+ shell: pwsh
+ # Build from the tool directory like build.bat does: tpc writes the
+ # executable next to project.yml, not next to the caller's cwd.
+ working-directory: tools/typephp-mcp
+ run: |
+ $ErrorActionPreference = 'Stop'
+ # The compiled compiler declares ZEND_MOD_REQUIRED for mbstring and
+ # links phpx.dll from the PHPX build directory, so it needs both the
+ # packaged php.ini and that directory on PATH - the same setup every
+ # other step that runs tpc.exe uses.
+ $env:PHPRC = Join-Path $env:PHP_HOME 'php.ini'
+ $env:PATH = "$env:PHPX_HOME\build;$env:PATH"
+ & ..\..\tpc.exe project.yml --job $env:BUILD_JOBS --no-progress
+ if ($LASTEXITCODE -ne 0) {
+ throw "typephp-mcp build failed with exit code $LASTEXITCODE"
+ }
+ $binary = Join-Path '${{ github.workspace }}' 'tools\typephp-mcp\typephp_mcp.exe'
+ if (-not (Test-Path $binary)) {
+ throw "typephp_mcp.exe was not generated: $binary"
+ }
+
+ - name: Run typephp-mcp native smoke
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $env:PHPRC = Join-Path $env:PHP_HOME 'php.ini'
+ # The AOT binary links PHPX and the PHP runtime; the dev-machine
+ # build script copies their DLLs next to the exe, CI uses PATH.
+ $env:PATH = "$env:PHPX_HOME\build;$env:PATH"
+ # Point check_code at the compiler this job just built. Relying on
+ # auto-detection would fall back to `php bin/tpc.php`, which needs
+ # the phpy extension this runner does not install.
+ $env:TYPEPHP_MCP_TPC = Join-Path '${{ github.workspace }}' 'tpc.exe'
+ $binary = Join-Path '${{ github.workspace }}' 'tools\typephp-mcp\typephp_mcp.exe'
+ # Hard wall-clock limit. Do NOT redirect the child's output: the MCP
+ # server spawns compiler children that inherit the handles, so a
+ # redirected stream never reaches EOF and reading it hangs the step
+ # forever (that is what kept this job open for 30+ minutes). Let the
+ # output go straight to the job log and only bound the wait.
+ $psi = [Diagnostics.ProcessStartInfo]::new()
+ $psi.FileName = (Get-Command php).Source
+ $psi.ArgumentList.Add('tools\typephp-mcp\tests\mcp-binary-smoke.php')
+ $psi.ArgumentList.Add($binary)
+ $psi.ArgumentList.Add('${{ github.workspace }}')
+ $psi.UseShellExecute = $false
+ $psi.WorkingDirectory = '${{ github.workspace }}'
+ $process = [Diagnostics.Process]::new()
+ $process.StartInfo = $psi
+ if (-not $process.Start()) { throw 'Unable to start the typephp-mcp smoke driver' }
+ if (-not $process.WaitForExit(600000)) {
+ $process.Kill($true)
+ $process.WaitForExit()
+ throw 'typephp-mcp native smoke exceeded its 10 minute limit'
+ }
+ if ($process.ExitCode -ne 0) {
+ throw "typephp-mcp native smoke failed with exit code $($process.ExitCode)"
+ }
+
- name: Test bootstrap signatures
shell: pwsh
run: |
diff --git a/.gitignore b/.gitignore
index 9d7090d9..d1418d90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@ tests/**/*.sh
/tests/wasm/harness/node_modules/
/ide-helper/
/debug/
+/tools/typephp-mcp/build/
diff --git a/docs/en/CLASS_INHERITANCE.md b/docs/en/CLASS_INHERITANCE.md
index 0b47bfff..19faf546 100644
--- a/docs/en/CLASS_INHERITANCE.md
+++ b/docs/en/CLASS_INHERITANCE.md
@@ -35,21 +35,18 @@ interface BaseInterface {}
class MyClass implements BaseInterface {}
```
-### Rule 3: Cannot Inherit Internal Classes
+### Rule 3: Native Classes and Plain Zend Classes Cannot Inherit from Each Other
```php
-// ❌ Wrong: inheriting a PHP internal class
+// ✅ Correct: inheriting a Zend internal class (detected via
+// Reflection::isInternalClass at compile time)
class MyArray extends ArrayObject {}
-// Compile error: Class `MyArray` Cannot inherit built-in class `ArrayObject`
+class MyDb extends PDO {}
-// ✅ Correct: use the composition pattern
-class MyContainer {
- private ArrayObject $storage;
-
- public function __construct() {
- $this->storage = new ArrayObject();
- }
-}
+// ❌ Wrong: a Native class inheriting from a Zend class (or vice versa)
+// class MyNative extends SomeZendClass {} // Native cannot extend Zend
+// class MyZend extends SomeNativeClass {} // Zend cannot extend Native
+// Compile error: Native and ZendVM-backed classes cannot inherit from each other
```
---
@@ -73,27 +70,31 @@ class MyContainer {
### Checking Code Location
-The relevant implementation is in `src/Php/Translator.php`:
+The relevant implementation is in `src/Translator.php` (inheritance checks) and
+`src/Preprocessor.php` (internal-class detection):
```php
-if ($extends) {
- $parentClass = $this->getParentClass($class->extends);
- if ($this->hasNativeClass($parentClass)) {
- // The parent class is a defined native class
- $parent = $this->getClassDef($parentClass);
+// src/Translator.php — compile-time inheritance checks (v0.9.5)
+if ($this->classDef->extends and !$this->classDef->inheritedFromInternalClass) {
+ $parentClass = $this->getNamespacedClassName($this->parseIdentifier($class->extends));
+ if ($this->hasClass($parentClass)) {
+ $parent = $this->getClass($parentClass);
+ if ($this->classDef->nativeObject !== $parent->nativeObject) {
+ $this->fatalError($class,
+ 'Native and ZendVM-backed classes cannot inherit from each other');
+ }
if ($parent->flags & Modifiers::FINAL) {
$this->fatalError($class, "Class `{$this->class}` cannot extend final class `{$parentClass}`");
}
- $this->classDef->extends = $parentClass;
+ $this->assertReadonlyInheritanceContract($class, $parentClass, ...);
} else {
- // Check whether it is a PHP internal class
- if (Reflection::isInternalClass($parentClass)) {
- $this->fatalError($class, "Class `{$this->class}` Cannot inherit built-in class `$parentClass`");
- } else {
- $this->fatalError($class, "Class `{$this->class}` inherits from a non-existent class `$parentClass`");
- }
+ $this->fatalError($class, "Class `{$this->class}` inherits from a non-existent class `{$parentClass}`");
}
}
+// src/Preprocessor.php — internal-class detection: when the parent is a
+// built-in class, inheritedFromInternalClass is set and only the readonly
+// contract is checked against the host runtime's reflection. Inheriting
+// built-in classes is allowed.
```
---
@@ -312,31 +313,32 @@ class MyService {
}
```
-### Example 3: Inheriting a PHP Internal Class
+### Example 3: A Native Class Inheriting from a Zend Class
```php
```
**Compile error**:
```
-Fatal error: Class `MyDatabase` Cannot inherit built-in class `PDO`
+Fatal error: Native and ZendVM-backed classes cannot inherit from each other
in /path/to/file.php on line X
```
-**Solution**:
+**Note**: inheriting Zend internal classes (such as `PDO` or `ArrayObject`) is
+**allowed**; composition remains a recommended style but is not enforced by the
+compiler.
+
```php
storage = new ArrayObject();
- }
-}
+// ❌ 错误:Native 类与普通 Zend 类互继
+// class MyNative extends SomeZendClass {} // Native 类不能继承 Zend 类
+// class MyZend extends SomeNativeClass {} // Zend 类不能继承 Native 类
+// 编译错误:Native and ZendVM-backed classes cannot inherit from each other
```
---
@@ -73,27 +69,28 @@ class MyContainer {
### 检查代码位置
-相关实现在 `src/Php/Translator.php`:
+相关实现在 `src/Translator.php`(类继承检查)与 `src/Preprocessor.php`(内部类判定):
```php
-if ($extends) {
- $parentClass = $this->getParentClass($class->extends);
- if ($this->hasNativeClass($parentClass)) {
- // 父类是已定义的 native 类
- $parent = $this->getClassDef($parentClass);
+// src/Translator.php — 编译期继承检查(v0.9.5)
+if ($this->classDef->extends and !$this->classDef->inheritedFromInternalClass) {
+ $parentClass = $this->getNamespacedClassName($this->parseIdentifier($class->extends));
+ if ($this->hasClass($parentClass)) {
+ $parent = $this->getClass($parentClass);
+ if ($this->classDef->nativeObject !== $parent->nativeObject) {
+ $this->fatalError($class,
+ 'Native and ZendVM-backed classes cannot inherit from each other');
+ }
if ($parent->flags & Modifiers::FINAL) {
$this->fatalError($class, "Class `{$this->class}` cannot extend final class `{$parentClass}`");
}
- $this->classDef->extends = $parentClass;
+ $this->assertReadonlyInheritanceContract($class, $parentClass, ...);
} else {
- // 检查是否为 PHP 内部类
- if (Reflection::isInternalClass($parentClass)) {
- $this->fatalError($class, "Class `{$this->class}` Cannot inherit built-in class `$parentClass`");
- } else {
- $this->fatalError($class, "Class `{$this->class}` inherits from a non-existent class `$parentClass`");
- }
+ $this->fatalError($class, "Class `{$this->class}` inherits from a non-existent class `{$parentClass}`");
}
}
+// src/Preprocessor.php — 内部类判定:父类是内置类时标记 inheritedFromInternalClass,
+// 后续只按宿主运行时反射校验 readonly 契约,允许继承。
```
---
@@ -312,31 +309,31 @@ class MyService {
}
```
-### 示例三:继承 PHP 内部类
+### 示例三:Native 类与 Zend 类互继
```php
```
**编译错误**:
```
-Fatal error: Class `MyDatabase` Cannot inherit built-in class `PDO`
+Fatal error: Native and ZendVM-backed classes cannot inherit from each other
in /path/to/file.php on line X
```
-**解决方案**:
+**说明**:继承 Zend 内部类(如 `PDO`、`ArrayObject`)是**允许**的;组合模式仍然是
+值得推荐的风格,但不是编译器强制要求。
+
```php
./phpunit
+
+ ./tools/typephp-mcp/tests
+
diff --git a/tests/windows/clang/README.md b/tests/windows/clang/README.md
index d7018550..46647c5a 100644
--- a/tests/windows/clang/README.md
+++ b/tests/windows/clang/README.md
@@ -16,7 +16,7 @@ session in the compiler repository:
```powershell
. .\tests\windows\clang\set-llvm-env.ps1 `
-LlvmHome D:\workspace\tools\llvm-23.1.2 `
- -VcToolsRoot 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207' `
+ -VcToolsRoot 'D:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207' `
-WindowsSdkRoot 'C:\Program Files (x86)\Windows Kits\10' `
-WindowsSdkVersion 10.0.26100.0
diff --git a/tools/typephp-mcp/.gitignore b/tools/typephp-mcp/.gitignore
new file mode 100644
index 00000000..8d84beb3
--- /dev/null
+++ b/tools/typephp-mcp/.gitignore
@@ -0,0 +1,2 @@
+/build.log
+/typephp_mcp
diff --git a/tools/typephp-mcp/README.md b/tools/typephp-mcp/README.md
new file mode 100644
index 00000000..5b300ceb
--- /dev/null
+++ b/tools/typephp-mcp/README.md
@@ -0,0 +1,206 @@
+# typephp-mcp — TypePHP 辅助开发 MCP Server(TypePHP 自己实现)
+
+用 TypePHP AOT 编译器把 PHP 源码编译成原生二进制的 MCP(Model Context Protocol)server,
+通过 stdio 以换行分隔的 JSON-RPC 2.0 对外提供服务。它替代了设计文档
+(《TypePHP辅助开发MCP设计.md》)中原计划的 Python/FastMCP 实现:**协议层、检索层、
+编译器 oracle 全部由 TypePHP 编译后的 PHP 代码完成**,运行时只依赖 PHP 运行时 DLL。
+
+## 工具面(12 个)
+
+| 工具 | 作用 |
+|---|---|
+| `check_code` | 第一工具。跑 `php bin/tpc.php --dry` 验证 TypePHP 子集合法性,返回结构化结果 `OK/FAILED stage=generate file=… diagnostics=N` + 逐条 `file:line[:col] 严重级别 消息`。只覆盖 C++ 生成阶段;编译/链接阶段不跑、不覆盖。工具自身执行失败(文件不存在 / PHP 或 SDK 缺失 / 编译器超时)以 `isError: true` 返回,与"用户代码不合格"的正常结果区分开 |
+| `list_restrictions` | 写码前必读。TypePHP 裁剪规则(程序结构/类型/引用/对象/控制流/std 表面/stub 契约/构建配置),可按 topic 查询 |
+| `get_symbol` | 按精确名字查函数/类/接口/trait/enum/const 声明,返回签名 + `file:line`(扫 src/、examples/、tests/std/)。支持完整命名空间(`TypePhp\Translator`)与 `Class::method`;大文件分块扫描(超过扫描上限会显式报告 Coverage 截断,绝不静默跳过) |
+| `list_stdlib` | 列出 tests/std 各分类及测试标题;指定分类返回该分类全部用例 |
+| `get_example` | 返回 examples/ 下可运行示例(单文件内容,或目录的文件清单 + README/main.php/project.yml 摘录) |
+| `get_stub_api` | 解析 `.stub.php` ABI 契约:列出全部 stub 文件,或读取指定文件的声明 |
+| `search_docs` | docs/zh-cn + docs/en 全文检索,返回带 `docs//.md# (L)` 出处的 chunk |
+| `search_code` | grep examples/、tests/std/、src/,返回 `file:line` + 所在声明 |
+| `get_changelog` | 读 CHANGELOG.md:最新小节或指定版本 |
+| `get_build_config_help` | project.yml 字段、bin/lib/ext 三种模式、CLI 选项、stub 契约 |
+| `list_topics` | 列出全部文档标题(双语) |
+| `get_index_info` | 元数据:repo 根、tpc 版本、git commit、文档数、工具清单(staleness 自证) |
+
+设计取舍:
+
+- **无预建索引**。docs/examples/src 每次实时读取(约 2 MB),从结构上消除索引过期;
+ `get_index_info` 用 tpc 版本 + git commit 自证时效。
+- **约束注入**。`initialize` 的 instructions 与 `check_code`/`list_restrictions` 的
+ tool description 中内置约 800 token 的 TypePHP 红线摘要,不调工具也能压住头号失败模式。
+- **返回摘录 + 出处,不返整文件**;单次结果有字节预算(约 4 KB),UTF-8 边界安全。
+- **协议版本来真的**。`initialize` 按 MCP 版本规范应答:客户端请求的版本属于已实现集合才回显,
+ 否则回本 server 支持的最新版(不假装支持任意版本)。`roots` 只在存在该能力的版本里声明,
+ 工作区发现另有 `TYPEPHP_MCP_PROJECT_ROOT` 与 cwd 自动探测兜底。无 id 的通知(含 `tools/list`)一律不回复。
+- **编译器调用有生命周期控制**。`check_code` 通过受控子进程跑 tpc:超时、输出预算、进程树内存上限,
+ 超时杀树并清理临时目录;服务在等待期间不会永久卡死。
+
+## 构建
+
+### Windows
+
+前置:VS 2022 BuildTools(`vcvars64.bat`,经 vswhere 自动定位)与一个 tpc 包。
+
+```bat
+tools\typephp-mcp\build.bat
+```
+
+脚本所有路径都相对自身位置解析,**可从任意工作目录执行**:
+
+| 路径 | 解析方式 |
+|---|---|
+| `project.yml` / `build/` / 输出 exe | 脚本所在目录 |
+| `bin\tpc.php` | 脚本目录上两级(仓库根) |
+| tpc 包(phpx/PHP SDK/运行时 DLL) | 脚本目录上三级 + `tpc_v0.9.5_windows_x64`(即仓库同级目录) |
+
+换 tpc 版本只需改 `build.bat` 里 `TPC_PACKAGE` 一行中的版本目录名;若 tpc 包不在
+默认位置,先 `set PHP_HOME=<包目录>` 再运行(脚本找不到包时会给出该提示并以退出码 1 失败)。
+
+产物 `typephp_mcp.exe` 及运行时 DLL(phpx/php8ts/gmp/mpfr/mpdec)都在本目录,
+**不需要改 PATH**。
+
+### macOS / Linux
+
+```bash
+tools/typephp-mcp/build.sh
+```
+
+Windows 有一个 tpc 发行包目录装着 phpx/PHP SDK,Unix 侧没有,所以 `build.sh` 改为定位
+**宿主 embed 运行时**(顺序:显式 `PHP_HOME`/`PHPX_HOME` → `~/.typephp/php-builder/php-*/install`
+里最新的私有运行时 → 仓库 `vendor/swoole/phpx`):
+
+| 前置 | 要求 | 实测踩坑 |
+|---|---|---|
+| clang | `xcode-select --install` | tpc 启动即打印 `Initialized platform/backend: macOS + Clang` |
+| `PHP_HOME` | `lib/libphp.{a,dylib}` | Homebrew 的 php 只建 cli SAPI,**没有 libphp**;`--dry` 也会直接 Fatal |
+| `PHPX_HOME` | `include/phpx.h` + `lib/libphpx.{a,dylib}` | 编译器现在生成 `php::empty_string`,phpx 需 ≥ 该 API(`composer update swoole/phpx`),`build.sh` 会先校验再报错 |
+| 链接库 | — | 私有运行时的扩展集通常比本项目声明的多,`libphp.a` 于是引用 tpc 不认识的 `-lonig/-lxml2/-lsqlite3/-lz/-lresolv/-lcurl/-lssl -lcrypto/-liconv`;`build.sh` 用 `ar -t` 探测实际对象后补齐,iconv/openssl 是 brew keg-only 还要带 `-L` |
+
+产物是本目录的 `typephp_mcp`(无 `.exe` 后缀,Mach-O arm64,约 25 MB),依赖 gmp/mpfr/oniguruma
+等 brew dylib 的绝对路径,换机需重装同样的 brew 包。
+
+以上 macOS 路径为实测;`build.sh` 同时按 Linux 写(`uname` 分支只影响 libiconv 那条)。
+
+`build.sh` 在用户自管机器上遵守资源安全约束(AGENTS.md):Linux 下先把清理和编译放进全用户构
+建锁 `${XDG_RUNTIME_DIR:-/tmp}/codex-build-$(id -u).lock` 的临界区(抢不到锁就不触碰 `build/`、不调用编译器),`-j1` 串行,并把整个构建进程树放进 systemd user scope
+(`MemoryMax` 为 8 GiB 或启动时可用内存的一半取小者,`MemorySwapMax=0` 禁 swap)——这才是整树
+内存总预算,`ulimit -v` 只是每进程第二道防线;scope 建不起来时拒绝构建并给出可用的受控入口
+(root scope / 容器);CI 跑在专用 runner 上,改用可用逻辑 CPU 数。
+
+CI(本 PR 新增)在 **Linux x64 / Linux arm64 / macOS arm64 / Windows x64** 上都会实际构建 MCP
+产物并跑原生 stdio 冒烟测试;缺产物即判失败(不再靠"缺产物就 skip"证明兼容)。
+
+## 运行 / 接入 MCP 客户端
+
+```bat
+set TYPEPHP_MCP_ROOT=D:\git\php\aot-compiler
+tools\typephp-mcp\typephp_mcp.exe
+```
+
+macOS / Linux(`check_code` 要调编译器,所以 `PHP_HOME`/`PHPX_HOME` 与构建时同一套):
+
+```bash
+export TYPEPHP_MCP_ROOT=/Volumes/data/git/php/typephp-compiler
+export PHP_HOME=$HOME/.typephp/php-builder/php-8.5.11-/install
+export PHPX_HOME=
+tools/typephp-mcp/typephp_mcp
+```
+
+环境变量:
+
+| 变量 | 默认 | 说明 |
+|---|---|---|
+| `TYPEPHP_MCP_ROOT` | 从 cwd 向上找 `bin/tpc.php` + `docs/`,再退到源码相对默认值(`tools/typephp-mcp/src` 上三级) | **工具资源目录**(编译器 checkout),不是用户项目目录。默认值随源码位置解析,不再是写死的开发机路径;但 **AOT 产物内嵌的是构建机路径**,发布二进制从别处拉起时必须显式设置(否则以可操作的错误信息失败) |
+| `TYPEPHP_MCP_TPC` | 仓库根的 `tpc` / `tpc.exe` | `check_code` 调用的**编译好的 tpc 二进制**:不需要 PHP 解释器、也不需要 phpy 扩展(Windows 上尤其重要,因为解释执行 `php bin/tpc.php` 必须加载 phpy)。会先做一次有界的 `--version` 探测,跑不起来(缺运行时 DLL 或缺 mbstring)就退回解释执行 |
+| `TYPEPHP_MCP_PHP` | `php`(PATH 查找) | 只有走解释执行(`php bin/tpc.php`)时才需要;`get_index_info` 探测 tpc 版本同理 |
+| `PHP_HOME` / `PHPX_HOME` | 继承调用方环境 | 这两个只被 `check_code`/`get_index_info` 间接使用(tpc 自己读);mac 上不设就是 `The host PHP embed library is missing`,工具会如实回报成 `isError: true` |
+| `TYPEPHP_MCP_PROJECT_ROOT` | 未设 | 客户端不发 MCP roots 时,手动把 TypePHP 项目路径并入默认检索集。分隔符:`;` 通用,POSIX 下 `:` 也认;**Windows 只认 `;`**(盘符里的冒号不能当分隔符) |
+| `TYPEPHP_MCP_COMPILE_TIMEOUT` | `120` | `check_code` 调编译器的超时秒数;超时杀掉整个子进程树并清理临时目录,服务保持可响应 |
+| `TYPEPHP_MCP_COMPILE_OUTPUT_LIMIT` | `65536` | 编译器输出捕获字节上限(仍持续排空管道,子进程不会被写满阻塞) |
+| `TYPEPHP_MCP_COMPILE_MEMORY_LIMIT_MB` | `4096` | POSIX 下编译器进程树的虚拟内存上限(MiB) |
+
+Finch(`~/.finch/mcp/servers.json`,对象数组):
+
+```json
+{
+ "name": "typephp-dev",
+ "enabled": true,
+ "command": "D:\\git\\php\\aot-compiler\\tools\\typephp-mcp\\typephp_mcp.exe",
+ "args": []
+}
+```
+
+Claude Code:
+
+```bash
+claude mcp add typephp-dev -- D:\git\php\aot-compiler\tools\typephp-mcp\typephp_mcp.exe
+```
+
+CodeBuddy(用户级配置 `%USERPROFILE%\.codebuddy\mcp.json`,在 MCP 标签页手动添加等价):
+
+```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",
+ "TYPEPHP_MCP_PHP": "D:/git/php/tpc_v0.9.5_windows_x64/php.exe",
+ "PHPX_HOME": "D:/git/php/tpc_v0.9.5_windows_x64/phpx",
+ "PHP_HOME": "D:/git/php/tpc_v0.9.5_windows_x64"
+ }
+ }
+ }
+}
+```
+
+注意:**路径一律用正斜杠**。CodeBuddy 的 MCP 启动器会剥掉配置里的反斜杠
+(`D:\\git\\...` 会变成 `D:git...`,报 `not recognized ... Connection closed`),
+正斜杠在 Windows 下全链路可用(与同配置中正常工作的 ripwire 条目一致)。
+
+`env` 四个变量都建议显式设置:客户端的工作目录不确定,`TYPEPHP_MCP_ROOT` 保证仓库定位;
+后三个把 `check_code` 调用的编译器钉在 tpc 包上,避免继承到过期的 `PHPX_HOME`/`PHP_HOME`
+或 PATH 里没有 php。修改配置后需重启 CodeBuddy 生效。
+
+`check_code` 会调编译器,属"副作用"工具,建议在客户端对该 server 单独授权。
+
+## 测试
+
+```bash
+php vendor/bin/phpunit tools/typephp-mcp/tests/
+```
+
+- `McpServerTest`:协议分发 + 12 个工具的逻辑单测(普通 PHP 直接跑;
+ 源码刻意保持在 stock PHP 与 TypePHP 的交集内,解释执行与 AOT 编译跑同一份代码)。
+- `McpBinaryTest`:用共享的 `McpBinaryDriver` 驱动编译产物跑真实 stdio 会话(逐条 flush 超时校验、
+ 大文件符号、工具失败 `isError`、通知不回复、仓库外 cwd 启动、解释执行与 AOT 一致性)。二进制按平台取
+ `typephp_mcp.exe` / `typephp_mcp`,也可用 `TYPEPHP_MCP_BINARY=` 指定;未构建时默认 skip,
+ 设 `TYPEPHP_MCP_REQUIRE_BINARY=1`(CI 会设)则改为失败。
+- `tools/typephp-mcp/tests/mcp-binary-smoke.php`:独立 CI 驱动,落地同样的 wire 检查;`--interpreted`
+ 跑解释执行入口 `run-interpreted.php`,用于和 AOT 产物对比。缺产物直接以退出码 1 失败。
+
+`check_code` 相关断言要求调用方环境里有一个能跑 `tpc --dry` 的 embed 运行时;Windows 靠 tpc 包,
+macOS/Linux 靠 `PHP_HOME`/`PHPX_HOME`(见"构建")。没设这些变量时它们会红,报的是编译器那句
+`The host PHP embed library is missing`,不是本项目的问题。
+
+本 PR 起 CI 在 Linux x64/arm64、macOS arm64、Windows x64 上实际构建产物并跑原生冒烟,
+不再依赖"缺产物就 skip"来证明兼容。本机(Windows,PHP 8.5.11 + tpc_v0.9.5 包)实测:
+`php vendor/bin/phpunit --testsuite TypePhpMcpTests` → 45 用例 / 456 断言全绿;AOT 产物真实 stdio
+会话里 12 个工具与两条编译器通路(`get_index_info` 的 tpc+git、`check_code` 的 tpc --dry)均正常;
+反向对照:把 `PHP_HOME` 指向没有 embed 库的目录,`check_code` 断言一起变红,说明它们真的在管编译器通路。
+
+## 已知问题(编译器侧,非本项目 bug)
+
+`STDIN/STDOUT/STDERR` 常量在某些项目形态下被 -O2 优化器折叠为字面量 `0`
+(见 `src/Translator.php::loadInternalConstants()` 与
+`src/Parser/ConstantExpressionTrait.php::parseConstFetch()` 的常量折叠路径),
+运行时 `fwrite()/fgets()` 会抛 `TypeError`。规避:`main.php` 统一用
+`fopen('php://stdin'/'php://stdout'/'php://stderr')` 打开标准流。
+
+macOS 链接期缺扩展依赖:走私有 embed **静态** `libphp.a` 时,tpc 不按运行时的实际扩展集推导
+系统库,除 gmp/mpfr/c++ 外的 `-lonig -lxml2 -lsqlite3 -lz -lresolv -lcurl -lssl -lcrypto -liconv`
+都要应用侧补上,`build.sh` 用 `ar -t` 探测归档对象后补全(iconv/openssl 还是 brew keg-only,
+要带 `-L`)。CI 的 macos-arm64 作业没踩到,因为它用 setup-php 的**共享** `libphp.dylib`
+(依赖由 dyld 解析),只有静态 embed 运行时才需要这一串 `-l`。
diff --git a/tools/typephp-mcp/build.bat b/tools/typephp-mcp/build.bat
new file mode 100644
index 00000000..b0285be3
--- /dev/null
+++ b/tools/typephp-mcp/build.bat
@@ -0,0 +1,60 @@
+@echo off
+REM Build typephp-mcp.exe with TypePHP (AOT).
+REM
+REM All paths are resolved relative to this script's location:
+REM