- tpc_build/tpc_run_test/tpv_build/tpv_compile_project 的 asan 默认 true - tpv_build 新增 asan 参数(编译器自身 ASan 插桩,崩溃时可输出报告定位) - tpv_bootstrap Phase A 对齐 tpv_build 加 --asan - includes/coro.vp: 裸 null 改 t.CPtr(0)(TPV 兼容,M1 遗留未提交)
596 lines
23 KiB
Python
596 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""transpyc_mcp.server - TransPyC/TransPyV MCP 服务器
|
||
|
||
实现 MCP/ 协议(v2026-08):
|
||
- stdio transport,newline-delimited JSON-RPC 2.0
|
||
- 14 个工具(tpc_/tpv_/tp_),schema 从 MCP/tools/tools.json 加载
|
||
- 安全:路径白名单 / 命令白名单 / 工程串行锁 / 内存保护
|
||
- 输出:统一 {ok, exit_code, stdout, stderr, artifacts, elapsed_ms},日志末尾 50 行
|
||
|
||
启动:python -m transpyc_mcp.server(cwd = 项目根)
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import ctypes
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import shlex
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
from typing import Any, Callable
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 常量
|
||
# ---------------------------------------------------------------------------
|
||
def _find_root() -> str:
|
||
"""向上定位项目根(含 Projectrans.py 的目录),兼容服务器位于 MCP/transpyc_mcp 下。"""
|
||
d: str = os.path.dirname(os.path.abspath(__file__))
|
||
while True:
|
||
if os.path.isfile(os.path.join(d, "Projectrans.py")):
|
||
return d
|
||
parent: str = os.path.dirname(d)
|
||
if parent == d:
|
||
return d
|
||
d = parent
|
||
|
||
_ROOT: str = _find_root()
|
||
_TOOLS_JSON: str = os.path.join(_ROOT, 'MCP', 'tools', 'tools.json')
|
||
_COMPILE_TIMEOUT: int = 300
|
||
_RUN_TIMEOUT: int = 30
|
||
_REGRESSION_TIMEOUT: int = 1800
|
||
_LOG_TAIL: int = 50
|
||
_PROTOCOL_VERSION: str = '2024-11-05'
|
||
_SERVER_INFO: dict[str, str] = {'name': 'transpyc-tools', 'version': '1.0.0'}
|
||
_TPV_PROJECT: str = os.path.join('TransPyV', 'project.json')
|
||
_TPV_EXE: str = os.path.join('TransPyV', 'output', 'TransPyV.exe')
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 安全:路径白名单
|
||
# ---------------------------------------------------------------------------
|
||
class _PathError(ValueError):
|
||
pass
|
||
|
||
|
||
def _resolve(path: str) -> str:
|
||
"""规范化并校验路径必须位于项目根内(拒绝越界)。"""
|
||
p: str = path
|
||
if not os.path.isabs(p):
|
||
p = os.path.join(_ROOT, p)
|
||
p = os.path.normpath(p)
|
||
if not (p == _ROOT or p.startswith(_ROOT + os.sep)):
|
||
raise _PathError(f'path outside root: {path}')
|
||
return p
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 命令执行与输出截断
|
||
# ---------------------------------------------------------------------------
|
||
def _tail(text: str, n: int = _LOG_TAIL) -> str:
|
||
lines: list[str] = text.splitlines()
|
||
if len(lines) <= n:
|
||
return text
|
||
return '\n'.join(lines[-n:])
|
||
|
||
|
||
def _run(cmd: list[str], timeout: int = _COMPILE_TIMEOUT, cwd: str | None = None) -> dict[str, Any]:
|
||
"""执行白名单命令,返回统一结果结构。"""
|
||
t0: float = time.perf_counter()
|
||
try:
|
||
r: subprocess.CompletedProcess = subprocess.run(
|
||
cmd, cwd=cwd or _ROOT, capture_output=True, text=True,
|
||
encoding='utf-8', errors='replace', timeout=timeout,
|
||
)
|
||
return {
|
||
'ok': r.returncode == 0,
|
||
'exit_code': r.returncode,
|
||
'stdout': _tail(r.stdout),
|
||
'stderr': _tail(r.stderr),
|
||
'elapsed_ms': int((time.perf_counter() - t0) * 1000),
|
||
}
|
||
except subprocess.TimeoutExpired:
|
||
return {'ok': False, 'exit_code': 6, 'stdout': '', 'stderr': f'timeout after {timeout}s',
|
||
'elapsed_ms': timeout * 1000}
|
||
except Exception as e: # 捕获启动失败等
|
||
return {'ok': False, 'exit_code': 1, 'stdout': '', 'stderr': str(e),
|
||
'elapsed_ms': int((time.perf_counter() - t0) * 1000)}
|
||
|
||
|
||
def _result_ok(**kw: Any) -> dict[str, Any]:
|
||
base: dict[str, Any] = {'ok': True, 'exit_code': 0, 'stdout': '', 'stderr': '', 'elapsed_ms': 0}
|
||
base.update(kw)
|
||
return base
|
||
|
||
|
||
def _result_err(code: int, msg: str) -> dict[str, Any]:
|
||
return {'ok': False, 'exit_code': code, 'stdout': '', 'stderr': msg, 'elapsed_ms': 0}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工程串行锁
|
||
# ---------------------------------------------------------------------------
|
||
_BUSY: set[str] = set()
|
||
|
||
|
||
def _with_lock(key: str, fn: Callable[[], dict[str, Any]]) -> dict[str, Any]:
|
||
if key in _BUSY:
|
||
return _result_err(5, f'project busy: {key}')
|
||
_BUSY.add(key)
|
||
try:
|
||
return fn()
|
||
finally:
|
||
_BUSY.discard(key)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 内存保护(TPV 需 >= 1 GiB)
|
||
# ---------------------------------------------------------------------------
|
||
def _available_gb() -> float:
|
||
try:
|
||
class _M(ctypes.Structure):
|
||
_fields_ = [('length', ctypes.c_ulong), ('memory_load', ctypes.c_ulong),
|
||
('total_phys', ctypes.c_ulonglong), ('avail_phys', ctypes.c_ulonglong),
|
||
('total_page', ctypes.c_ulonglong), ('avail_page', ctypes.c_ulonglong),
|
||
('total_virtual', ctypes.c_ulonglong), ('avail_virtual', ctypes.c_ulonglong),
|
||
('avail_ext', ctypes.c_ulonglong)]
|
||
m: _M = _M()
|
||
m.length = ctypes.sizeof(_M)
|
||
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m))
|
||
return m.avail_phys / (1024.0 ** 3)
|
||
except Exception:
|
||
return 99.0
|
||
|
||
|
||
def _check_memory() -> str | None:
|
||
if _available_gb() < 1.0:
|
||
return 'insufficient memory (< 1 GiB) for TPV'
|
||
return None
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工具:参数构造辅助
|
||
# ---------------------------------------------------------------------------
|
||
def _build_project_cmd(args: dict[str, Any], project: str, exe: str | None = None) -> list[str]:
|
||
"""构造工程编译命令(exe=None 用 TPC Projectrans.py;否则用 TPV exe)。"""
|
||
cmd: list[str] = [sys.executable, 'Projectrans.py'] if exe is None else [exe]
|
||
cmd += ['--project', project]
|
||
if args.get('clean'):
|
||
cmd.append('--clean')
|
||
if args.get('run'):
|
||
cmd.append('--run')
|
||
phase: str = args.get('phase', 'all')
|
||
if phase in ('1', '2'):
|
||
cmd += ['--phase', phase]
|
||
if args.get('rebuild_includes'):
|
||
cmd.append('--rebuild-includes')
|
||
if args.get('clear_cache'):
|
||
cmd.append('--clear-cache')
|
||
if args.get('asan', True):
|
||
cmd.append('--asan')
|
||
if args.get('nowarn_register'):
|
||
cmd.append('--nowarn-register')
|
||
for opt in ('src', 'temp', 'output', 'cc'):
|
||
if args.get(opt):
|
||
cmd += [f'--{opt}', str(args[opt])]
|
||
return cmd
|
||
|
||
|
||
def _find_exe(project: str) -> dict[str, str]:
|
||
"""从 project.json 的 linker.output 定位产物 exe(相对根)。"""
|
||
proj_path: str = _resolve(project)
|
||
proj_dir: str = os.path.dirname(proj_path)
|
||
try:
|
||
with open(proj_path, 'r', encoding='utf-8') as f:
|
||
cfg: dict[str, Any] = json.load(f)
|
||
except Exception:
|
||
return {}
|
||
out_name: str = str(cfg.get('linker', {}).get('output', ''))
|
||
out_dir: str = str(cfg.get('output_dir', 'output'))
|
||
if out_name:
|
||
exe: str = os.path.join(proj_dir, out_dir, out_name)
|
||
if os.path.isfile(exe):
|
||
return {'exe': os.path.relpath(exe, _ROOT)}
|
||
# 兜底:output 目录下最新 .exe
|
||
out_abs: str = os.path.join(proj_dir, out_dir)
|
||
if os.path.isdir(out_abs):
|
||
cands: list[str] = [os.path.join(out_abs, f) for f in os.listdir(out_abs)
|
||
if f.lower().endswith('.exe')]
|
||
if cands:
|
||
latest: str = max(cands, key=os.path.getmtime)
|
||
return {'exe': os.path.relpath(latest, _ROOT)}
|
||
return {}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工具:tpc_ 系列
|
||
# ---------------------------------------------------------------------------
|
||
def _tpc_build(args: dict[str, Any]) -> dict[str, Any]:
|
||
project: str = str(args.get('project', ''))
|
||
if not project:
|
||
return _result_err(2, 'missing required argument: project')
|
||
proj_path: str = _resolve(project)
|
||
if not os.path.isfile(proj_path):
|
||
return _result_err(4, f'project not found: {project}')
|
||
|
||
def run() -> dict[str, Any]:
|
||
cmd: list[str] = _build_project_cmd(args, project)
|
||
res: dict[str, Any] = _run(cmd)
|
||
if res['ok']:
|
||
res['artifacts'] = _find_exe(project)
|
||
return res
|
||
|
||
return _with_lock(project, run)
|
||
|
||
|
||
def _tpc_build_file(args: dict[str, Any]) -> dict[str, Any]:
|
||
input_f: str = str(args.get('input', ''))
|
||
output_f: str = str(args.get('output', ''))
|
||
if not input_f or not output_f:
|
||
return _result_err(2, 'missing required arguments: input, output')
|
||
in_path: str = _resolve(input_f)
|
||
out_path: str = _resolve(output_f)
|
||
if not os.path.isfile(in_path):
|
||
return _result_err(4, f'input not found: {input_f}')
|
||
cmd: list[str] = [sys.executable, 'TransPyC.py', '-f', in_path, '-o', out_path]
|
||
if args.get('cc'):
|
||
cmd += ['-cc', str(args['cc'])]
|
||
if args.get('cflags'):
|
||
cmd += ['-cflags', str(args['cflags'])]
|
||
if args.get('run'):
|
||
cmd.append('-run')
|
||
if args.get('args'):
|
||
cmd += ['-args', str(args['args'])]
|
||
if args.get('level'):
|
||
cmd += ['-level', str(args['level'])]
|
||
cmd += ['-e', str(args.get('encoding', 'utf-8'))]
|
||
res: dict[str, Any] = _run(cmd)
|
||
if res['ok']:
|
||
res['artifacts'] = {'ir': output_f}
|
||
return res
|
||
|
||
|
||
def _tpc_run_test(args: dict[str, Any]) -> dict[str, Any]:
|
||
name: str = str(args.get('name', ''))
|
||
if not name:
|
||
return _result_err(2, 'missing required argument: name')
|
||
project: str = os.path.join('Test', name, 'project.json')
|
||
proj_path: str = _resolve(project)
|
||
if not os.path.isfile(proj_path):
|
||
return _result_err(4, f'test project not found: {project}')
|
||
cargs: dict[str, Any] = dict(args)
|
||
cargs['clean'] = True
|
||
cargs['run'] = True
|
||
|
||
def run() -> dict[str, Any]:
|
||
cmd: list[str] = _build_project_cmd(cargs, project)
|
||
res: dict[str, Any] = _run(cmd, timeout=_RUN_TIMEOUT + _COMPILE_TIMEOUT)
|
||
if res['ok']:
|
||
res['artifacts'] = _find_exe(project)
|
||
return res
|
||
|
||
return _with_lock(project, run)
|
||
|
||
|
||
def _tpc_run_regression(args: dict[str, Any]) -> dict[str, Any]:
|
||
timeout: int = int(args.get('timeout', _REGRESSION_TIMEOUT))
|
||
cmd: list[str] = [sys.executable, os.path.join('Test', 'regression_test.py')]
|
||
return _run(cmd, timeout=timeout)
|
||
|
||
|
||
def _tpc_flag_only(args: dict[str, Any], flag: str) -> dict[str, Any]:
|
||
project: str = str(args.get('project', ''))
|
||
if not project:
|
||
return _result_err(2, 'missing required argument: project')
|
||
proj_path: str = _resolve(project)
|
||
if not os.path.isfile(proj_path):
|
||
return _result_err(4, f'project not found: {project}')
|
||
|
||
def run() -> dict[str, Any]:
|
||
return _run([sys.executable, 'Projectrans.py', '--project', project, flag])
|
||
|
||
return _with_lock(project, run)
|
||
|
||
|
||
def _tpc_clean_project(args: dict[str, Any]) -> dict[str, Any]:
|
||
return _tpc_flag_only(args, '--clean')
|
||
|
||
|
||
def _tpc_clear_cache(args: dict[str, Any]) -> dict[str, Any]:
|
||
return _tpc_flag_only(args, '--clear-cache')
|
||
|
||
|
||
def _tpc_rebuild_includes(args: dict[str, Any]) -> dict[str, Any]:
|
||
return _tpc_flag_only(args, '--rebuild-includes')
|
||
# ---------------------------------------------------------------------------
|
||
# 工具:tpv_ 系列
|
||
# ---------------------------------------------------------------------------
|
||
def _tpv_build(args: dict[str, Any]) -> dict[str, Any]:
|
||
proj_path: str = _resolve(_TPV_PROJECT)
|
||
if not os.path.isfile(proj_path):
|
||
return _result_err(4, f'project not found: {_TPV_PROJECT}')
|
||
if args.get('clear_cache'):
|
||
return _result_err(2, 'clear_cache unsupported on tpv_build; use tpc_clear_cache on TPC side first')
|
||
|
||
def run() -> dict[str, Any]:
|
||
cmd: list[str] = [sys.executable, 'Projectrans.py', '--project', _TPV_PROJECT, '--clean']
|
||
if args.get('asan', True):
|
||
cmd.append('--asan')
|
||
if args.get('rebuild_includes'):
|
||
cmd.append('--rebuild-includes')
|
||
res: dict[str, Any] = _run(cmd)
|
||
if res['ok']:
|
||
exe: str = os.path.join(_ROOT, _TPV_EXE)
|
||
res['artifacts'] = {'exe': _TPV_EXE if os.path.isfile(exe) else ''}
|
||
return res
|
||
|
||
return _with_lock(_TPV_PROJECT, run)
|
||
|
||
|
||
def _sha1_file(path: str) -> str:
|
||
h: hashlib._Hash = hashlib.sha1()
|
||
with open(path, 'rb') as f:
|
||
for chunk in iter(lambda: f.read(65536), b''):
|
||
h.update(chunk)
|
||
return h.hexdigest()
|
||
|
||
|
||
def _tpv_bootstrap(args: dict[str, Any]) -> dict[str, Any]:
|
||
mem_err: str | None = _check_memory()
|
||
if mem_err:
|
||
return _result_err(7, mem_err)
|
||
phases: list[str] = list(args.get('phases') or ['A', 'B', 'C'])
|
||
compare: bool = bool(args.get('compare', True))
|
||
out_dirs: dict[str, str] = {'A': 'TransPyV/output', 'B': 'TransPyV/output_b', 'C': 'TransPyV/output_c'}
|
||
steps: list[dict[str, Any]] = []
|
||
|
||
# Phase A:TPC 编译 TransPyV → output/TransPyV.exe
|
||
if 'A' in phases:
|
||
res: dict[str, Any] = _run([sys.executable, 'Projectrans.py', '--project', _TPV_PROJECT, '--clean', '--asan'])
|
||
steps.append({'phase': 'A', 'exe': _TPV_EXE, **res})
|
||
if not res['ok']:
|
||
return {'ok': False, 'exit_code': res['exit_code'], 'stdout': res['stdout'],
|
||
'stderr': 'bootstrap Phase A failed\n' + res['stderr'],
|
||
'artifacts': {'steps': steps}, 'elapsed_ms': res['elapsed_ms']}
|
||
else:
|
||
steps.append({'phase': 'A', 'exe': _TPV_EXE, 'ok': True, 'exit_code': 0, 'stdout': '', 'stderr': '(skipped)'})
|
||
|
||
# Phase B/C:上一阶段 exe 编译自身到下一 output 目录
|
||
for idx, ph in enumerate(('B', 'C')):
|
||
if ph not in phases:
|
||
steps.append({'phase': ph, 'ok': True, 'exit_code': 0, 'stdout': '', 'stderr': '(skipped)'})
|
||
continue
|
||
prev_dir: str = out_dirs['A'] if idx == 0 else out_dirs['B']
|
||
prev_exe: str = os.path.join(_ROOT, prev_dir, 'TransPyV.exe')
|
||
if not os.path.isfile(prev_exe):
|
||
steps.append({'phase': ph, 'ok': False, 'exit_code': 4,
|
||
'stdout': '', 'stderr': f'previous exe missing: {prev_exe}'})
|
||
return {'ok': False, 'exit_code': 4, 'stdout': '', 'stderr': f'bootstrap Phase {ph} missing input',
|
||
'artifacts': {'steps': steps}, 'elapsed_ms': 0}
|
||
out_dir: str = out_dirs[ph]
|
||
r: dict[str, Any] = _run([prev_exe, '--project', _TPV_PROJECT, '--clean', '--output', out_dir])
|
||
steps.append({'phase': ph, 'exe': os.path.join(out_dir, 'TransPyV.exe'), **r})
|
||
if not r['ok']:
|
||
return {'ok': False, 'exit_code': r['exit_code'], 'stdout': r['stdout'],
|
||
'stderr': f'bootstrap Phase {ph} failed\n' + r['stderr'],
|
||
'artifacts': {'steps': steps}, 'elapsed_ms': r['elapsed_ms']}
|
||
|
||
# 对比各阶段产物 SHA1(自举一致性)
|
||
artifacts: dict[str, Any] = {'steps': steps}
|
||
if compare:
|
||
hashes: dict[str, str] = {}
|
||
for ph in phases:
|
||
exe_rel: str = os.path.join(out_dirs[ph], 'TransPyV.exe')
|
||
exe_abs: str = os.path.join(_ROOT, exe_rel)
|
||
hashes[ph] = _sha1_file(exe_abs) if os.path.isfile(exe_abs) else ''
|
||
artifacts['hashes'] = hashes
|
||
artifacts['identical'] = len(set(hashes.values()) - {''}) <= 1
|
||
return _result_ok(stdout=json.dumps(artifacts, ensure_ascii=False, indent=2),
|
||
artifacts=artifacts)
|
||
|
||
|
||
def _tpv_compile_project(args: dict[str, Any]) -> dict[str, Any]:
|
||
project: str = str(args.get('project', ''))
|
||
if not project:
|
||
return _result_err(2, 'missing required argument: project')
|
||
proj_path: str = _resolve(project)
|
||
if not os.path.isfile(proj_path):
|
||
return _result_err(4, f'project not found: {project}')
|
||
exe: str = os.path.join(_ROOT, _TPV_EXE)
|
||
if not os.path.isfile(exe):
|
||
return _result_err(4, f'TPV not built yet; run tpv_build first: {_TPV_EXE}')
|
||
mem_err: str | None = _check_memory()
|
||
if mem_err:
|
||
return _result_err(7, mem_err)
|
||
|
||
def run() -> dict[str, Any]:
|
||
cmd: list[str] = _build_project_cmd(args, project, exe=exe)
|
||
res: dict[str, Any] = _run(cmd)
|
||
if res['ok']:
|
||
res['artifacts'] = _find_exe(project)
|
||
return res
|
||
|
||
return _with_lock(project, run)
|
||
|
||
|
||
def _tpv_run_test(args: dict[str, Any]) -> dict[str, Any]:
|
||
exe: str = os.path.join(_ROOT, _TPV_EXE)
|
||
if not os.path.isfile(exe):
|
||
return _result_err(4, f'TPV not built yet; run tpv_build first: {_TPV_EXE}')
|
||
mem_err: str | None = _check_memory()
|
||
if mem_err:
|
||
return _result_err(7, mem_err)
|
||
suite: str = str(args.get('suite', 'all'))
|
||
proj: str = os.path.join('TransPyV', 'Test', 'project.vpj')
|
||
if not os.path.isfile(_resolve(proj)):
|
||
return _result_err(4, f'TPV test project not found: {proj}')
|
||
cmd: list[str] = [exe, '--project', proj, '--clean', '--run']
|
||
return _with_lock(proj, lambda: _run(cmd))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工具:tp_ 系列(只读诊断)
|
||
# ---------------------------------------------------------------------------
|
||
def _tp_read_ast(args: dict[str, Any]) -> dict[str, Any]:
|
||
file: str = str(args.get('file', ''))
|
||
if not file:
|
||
return _result_err(2, 'missing required argument: file')
|
||
fpath: str = _resolve(file)
|
||
if not os.path.isfile(fpath):
|
||
return _result_err(4, f'file not found: {file}')
|
||
if fpath.lower().endswith('.py') or fpath.lower().endswith('.vp'):
|
||
lib: str = os.path.join(_ROOT, 'lib')
|
||
if lib not in sys.path:
|
||
sys.path.insert(0, lib)
|
||
try:
|
||
import vast as ast # noqa: PLC0415
|
||
except Exception as e:
|
||
return _result_err(1, f'vast import failed: {e}')
|
||
with open(fpath, 'r', encoding='utf-8', errors='replace') as f:
|
||
src: str = f.read()
|
||
try:
|
||
tree: Any = ast.parse(src)
|
||
dump: str = args.get('dump', 'compact')
|
||
text: str = ast.dump(tree, indent=2) if dump == 'full' else ast.dump(tree)
|
||
except Exception as e:
|
||
return _result_err(1, f'parse failed: {e}')
|
||
return _result_ok(stdout=text)
|
||
return _result_err(2, f'unsupported file type: {file} (only .vp/.py)')
|
||
|
||
|
||
def _tp_read_log(args: dict[str, Any]) -> dict[str, Any]:
|
||
file: str = str(args.get('file', ''))
|
||
if not file:
|
||
return _result_err(2, 'missing required argument: file')
|
||
fpath: str = _resolve(file)
|
||
if not os.path.isfile(fpath):
|
||
return _result_err(4, f'file not found: {file}')
|
||
lines: int = int(args.get('lines', _LOG_TAIL))
|
||
try:
|
||
with open(fpath, 'r', encoding='utf-8', errors='replace') as f:
|
||
content: str = f.read()
|
||
except Exception as e:
|
||
return _result_err(1, f'read failed: {e}')
|
||
pattern: str = str(args.get('pattern', ''))
|
||
if pattern:
|
||
import re # noqa: PLC0415
|
||
try:
|
||
content = '\n'.join(l for l in content.splitlines() if re.search(pattern, l))
|
||
except Exception as e:
|
||
return _result_err(2, f'invalid pattern: {e}')
|
||
return _result_ok(stdout=_tail(content, max(1, lines)))
|
||
|
||
|
||
def _tp_sha1_map(args: dict[str, Any]) -> dict[str, Any]:
|
||
project: str = str(args.get('project', ''))
|
||
if not project:
|
||
return _result_err(2, 'missing required argument: project')
|
||
map_rel: str = os.path.join(project, 'temp', '_sha1_map.txt')
|
||
map_path: str = _resolve(map_rel)
|
||
if not os.path.isfile(map_path):
|
||
return _result_err(4, f'sha1 map not found: {map_rel}')
|
||
with open(map_path, 'r', encoding='utf-8', errors='replace') as f:
|
||
content: str = f.read()
|
||
filt: str = str(args.get('filter', ''))
|
||
if filt:
|
||
content = '\n'.join(l for l in content.splitlines() if filt in l)
|
||
return _result_ok(stdout=_tail(content, 200))
|
||
# ---------------------------------------------------------------------------
|
||
# 工具分派表
|
||
# ---------------------------------------------------------------------------
|
||
DISPATCH: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
||
'tpc_build': _tpc_build,
|
||
'tpc_build_file': _tpc_build_file,
|
||
'tpc_run_test': _tpc_run_test,
|
||
'tpc_run_regression': _tpc_run_regression,
|
||
'tpc_clean_project': _tpc_clean_project,
|
||
'tpc_clear_cache': _tpc_clear_cache,
|
||
'tpc_rebuild_includes': _tpc_rebuild_includes,
|
||
'tpv_build': _tpv_build,
|
||
'tpv_bootstrap': _tpv_bootstrap,
|
||
'tpv_compile_project': _tpv_compile_project,
|
||
'tpv_run_test': _tpv_run_test,
|
||
'tp_read_ast': _tp_read_ast,
|
||
'tp_read_log': _tp_read_log,
|
||
'tp_sha1_map': _tp_sha1_map,
|
||
}
|
||
|
||
|
||
def _load_tools() -> list[dict[str, Any]]:
|
||
try:
|
||
with open(_TOOLS_JSON, 'r', encoding='utf-8') as f:
|
||
tools: list[dict[str, Any]] = json.load(f)
|
||
if isinstance(tools, list):
|
||
return tools
|
||
except Exception:
|
||
pass
|
||
# 兜底:从分派表生成最小 schema
|
||
return [{'name': n, 'description': n, 'inputSchema': {'type': 'object', 'properties': {}, 'required': []}}
|
||
for n in DISPATCH]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# JSON-RPC 处理
|
||
# ---------------------------------------------------------------------------
|
||
def _rpc_result(msg: dict[str, Any], result: Any) -> dict[str, Any]:
|
||
return {'jsonrpc': '2.0', 'id': msg.get('id'), 'result': result}
|
||
|
||
|
||
def _rpc_error(msg: dict[str, Any], code: int, message: str) -> dict[str, Any]:
|
||
return {'jsonrpc': '2.0', 'id': msg.get('id'),
|
||
'error': {'code': code, 'message': message}}
|
||
|
||
|
||
def _handle(msg: dict[str, Any]) -> dict[str, Any] | None:
|
||
method: str = str(msg.get('method', ''))
|
||
if method == 'initialize':
|
||
return _rpc_result(msg, {
|
||
'protocolVersion': _PROTOCOL_VERSION,
|
||
'capabilities': {'tools': {}},
|
||
'serverInfo': _SERVER_INFO,
|
||
})
|
||
if method in ('notifications/initialized', 'notifications/cancelled'):
|
||
return None
|
||
if method == 'tools/list':
|
||
return _rpc_result(msg, {'tools': _load_tools()})
|
||
if method == 'tools/call':
|
||
params: dict[str, Any] = msg.get('params') or {}
|
||
name: str = str(params.get('name', ''))
|
||
fn: Callable[[dict[str, Any]], dict[str, Any]] | None = DISPATCH.get(name)
|
||
if fn is None:
|
||
return _rpc_error(msg, -32602, f'unknown tool: {name}')
|
||
args: dict[str, Any] = params.get('arguments') or {}
|
||
if not isinstance(args, dict):
|
||
return _rpc_error(msg, -32602, 'arguments must be an object')
|
||
try:
|
||
result: dict[str, Any] = fn(args)
|
||
except _PathError as e:
|
||
result = _result_err(3, str(e))
|
||
except Exception as e: # 工具内部未预期异常
|
||
result = _result_err(1, f'{type(e).__name__}: {e}')
|
||
text: str = json.dumps(result, ensure_ascii=False, indent=2)
|
||
return _rpc_result(msg, {'content': [{'type': 'text', 'text': text}],
|
||
'isError': not result.get('ok', True)})
|
||
if msg.get('id') is None:
|
||
return None # 其它 notification
|
||
return _rpc_error(msg, -32601, f'method not found: {method}')
|
||
|
||
|
||
def main() -> None:
|
||
"""stdio 循环:逐行读 JSON-RPC 请求,逐行写响应。"""
|
||
for line in sys.stdin:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
msg: dict[str, Any] = json.loads(line)
|
||
except Exception:
|
||
continue
|
||
resp: dict[str, Any] | None = _handle(msg)
|
||
if resp is not None:
|
||
sys.stdout.write(json.dumps(resp, ensure_ascii=False) + '\n')
|
||
sys.stdout.flush()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() |