Skip to content

Commit 6421f5d

Browse files
SummerOneTwoclaude
andcommitted
fix: 修复 MCP 协议兼容性问题
P1-1: 添加真实 MCP 端到端测试 - 新增 test_e2e_mcp.py,通过 stdio 启动 MCP Server 进程 - 测试 MCP 握手、工具列表、工具调用、JSON 格式、中文编码 P1-2: TextContent 输出改为标准 JSON - server.py 使用 json.dumps 替代 str() - 输出现在是标准 JSON(双引号),而非 Python repr(单引号) - 支持中文非转义输出(ensure_ascii=False) P2: 补充传输层/客户端兼容性说明 - README 添加 Transport & Compatibility 表格 - 明确当前仅支持 stdio 传输 - 列出 Claude Code/Cursor/OpenCode 验证状态 附带修复: - problem.py 类型错误:returncode → return_code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b273013 commit 6421f5d

5 files changed

Lines changed: 294 additions & 7 deletions

File tree

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ AutoCode MCP Server provides 15 atomic tools that enable AI assistants to create
1919
- **Multi-Strategy Generation** — Four generation strategies: tiny (exhaustive), random, extreme (edge cases), and TLE-inducing
2020
- **Stress Testing** — Automated comparison between optimal and brute-force solutions with configurable trial counts
2121
- **MCP Protocol** — Native support for Claude Code, Cursor, and other MCP-compatible AI tools
22-
- **Safe Execution** — Timeout control, memory limits (Linux), and temporary directory isolation
22+
- **Execution Control** — Timeout control, memory limits (Linux), and temporary directory isolation (local trusted environments only)
2323
- **Polygon Packaging** — Export problems in Polygon format for Codeforces-style platforms
2424

2525
## Installation
@@ -110,6 +110,18 @@ stress_test_run(problem_dir="problems/ab", trials=100)
110110

111111
## MCP Client Setup
112112

113+
### Transport & Compatibility
114+
115+
**Current Support**: Local stdio transport only. The server communicates via standard input/output streams and is designed for local trusted environments.
116+
117+
| Client | Status | Notes |
118+
|--------|--------|-------|
119+
| Claude Code | ✅ Verified | Primary development environment |
120+
| Cursor | ⚠️ Config provided | Not yet tested end-to-end |
121+
| OpenCode | ⚠️ Config provided | Not yet tested end-to-end |
122+
123+
**Not Supported**: HTTP/SSE transport, remote connections, or multi-tenant environments.
124+
113125
### Claude Code
114126

115127
Edit `~/.config/claude-code/config.json`:
@@ -436,7 +448,18 @@ problem_pack_polygon(
436448

437449
3. **Unified Return Format** — All tools return `{success, error, data}` for consistent error handling.
438450

439-
4. **Safe Execution** — Timeout control, memory limits (Linux via prlimit), and temporary directory isolation.
451+
4. **Execution Control** — Timeout control, memory limits (Linux via prlimit), and temporary directory isolation.
452+
453+
### Security Boundaries
454+
455+
⚠️ **Important: This tool is designed for local trusted environments only**
456+
457+
- **File Operations**: `file_read` and `file_save` can read/write arbitrary paths (use `problem_dir` parameter to limit scope)
458+
- **Code Execution**: Compiles and executes AI-generated C++ code with only time/memory limits, no sandbox isolation
459+
- **Use Cases**: Local development, competitive programming problem creation, AI-assisted coding in trusted environments
460+
- **Not Suitable For**: Multi-tenant environments, untrusted code execution, production-grade code execution platforms
461+
462+
For stronger isolation, run inside a container or virtual machine.
440463

441464
### Generation Strategies
442465

README_CN.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ AutoCode MCP Server 提供 15 个原子工具,让 AI 助手能够创建、验
1919
- **多策略生成** — 四种生成策略:tiny(穷举)、random(随机)、extreme(边界情况)、tle(诱导超时)
2020
- **压力测试** — 自动比较最优解和暴力解,可配置测试轮数
2121
- **MCP 协议** — 原生支持 Claude Code、Cursor 等 MCP 兼容的 AI 工具
22-
- **安全执行** — 超时控制、内存限制(Linux)、临时目录隔离
22+
- **执行控制** — 超时控制、内存限制(Linux)、临时目录隔离(仅限本地可信环境)
2323
- **Polygon 打包** — 导出为 Polygon 格式,适用于 Codeforces 等平台
2424

2525
## 安装
@@ -110,6 +110,18 @@ stress_test_run(problem_dir="problems/ab", trials=100)
110110

111111
## MCP 客户端配置
112112

113+
### 传输层与兼容性
114+
115+
**当前支持**:仅支持本地 stdio 传输。服务器通过标准输入/输出流通信,适用于本地可信环境。
116+
117+
| 客户端 | 状态 | 说明 |
118+
|--------|------|------|
119+
| Claude Code | ✅ 已验证 | 主要开发环境 |
120+
| Cursor | ⚠️ 配置已提供 | 尚未端到端测试 |
121+
| OpenCode | ⚠️ 配置已提供 | 尚未端到端测试 |
122+
123+
**不支持**:HTTP/SSE 传输、远程连接或多租户环境。
124+
113125
### Claude Code
114126

115127
编辑 `~/.config/claude-code/config.json`
@@ -189,7 +201,7 @@ stress_test_run(problem_dir="problems/ab", trials=100)
189201

190202
### 验证安装
191203

192-
配置完成后,重启 MCP 客户端并检查工具是否可用。你应该能看到 15 个以 `autocode_` 为前缀的工具
204+
配置完成后,重启 MCP 客户端并检查工具是否可用。你应该能看到 15 个工具,包括 `solution_build``validator_build``generator_build`
193205

194206
## 工具参考
195207

@@ -438,6 +450,17 @@ problem_pack_polygon(
438450

439451
4. **安全执行** — 超时控制、内存限制(Linux 通过 prlimit)、临时目录隔离。
440452

453+
### 安全边界
454+
455+
⚠️ **重要提示:本工具仅适用于本地可信环境**
456+
457+
- **文件操作**`file_read``file_save` 可读写任意路径(需显式指定 `problem_dir` 参数限制范围)
458+
- **代码执行**:编译并执行 AI 生成的 C++ 代码,仅提供时间/内存限制,无沙箱隔离
459+
- **适用场景**:本地开发、竞赛编程出题、可信环境下的 AI 辅助编程
460+
- **不适用场景**:多租户环境、不可信代码执行、生产级代码运行平台
461+
462+
如需更强的安全隔离,建议在容器或虚拟机中运行。
463+
441464
### 生成策略
442465

443466
| 策略 | 类型码 | 用途 |

src/autocode_mcp/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import asyncio
10+
import json
1011
from typing import Any
1112

1213
from mcp.server import Server
@@ -109,15 +110,15 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> CallToolResult:
109110
result = await tool.execute(**arguments)
110111
result_dict = result.to_dict()
111112
return CallToolResult(
112-
content=[TextContent(type="text", text=str(result_dict))],
113+
content=[TextContent(type="text", text=json.dumps(result_dict, ensure_ascii=False))],
113114
structuredContent=result_dict,
114115
isError=not result.success,
115116
)
116117
except Exception as e:
117118
error_result = ToolResult.fail(str(e))
118119
error_dict = error_result.to_dict()
119120
return CallToolResult(
120-
content=[TextContent(type="text", text=str(error_dict))],
121+
content=[TextContent(type="text", text=json.dumps(error_dict, ensure_ascii=False))],
121122
structuredContent=error_dict,
122123
isError=True,
123124
)

src/autocode_mcp/tools/problem.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ async def execute(
387387
# Validator 过滤
388388
if validator_available:
389389
val_result = await run_binary(val_exe, input_data, timeout=timeout)
390-
if val_result.returncode != 0:
390+
if val_result.return_code != 0:
391391
# 输入无效,跳过
392392
seed += 1
393393
continue

tests/test_e2e_mcp.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
"""真实 MCP 端到端兼容性测试。
2+
3+
通过 stdio 启动 MCP Server 进程,进行完整的协议握手和工具调用验证。
4+
"""
5+
6+
import asyncio
7+
import json
8+
import os
9+
import sys
10+
import tempfile
11+
12+
import pytest
13+
14+
15+
class MCPClient:
16+
"""简单的 MCP 客户端,用于端到端测试。"""
17+
18+
def __init__(self, process: asyncio.subprocess.Process):
19+
self.process = process
20+
self.request_id = 0
21+
22+
async def send_request(self, method: str, params: dict | None = None) -> dict:
23+
"""发送 JSON-RPC 请求并等待响应。"""
24+
self.request_id += 1
25+
request = {
26+
"jsonrpc": "2.0",
27+
"id": self.request_id,
28+
"method": method,
29+
"params": params or {},
30+
}
31+
32+
# 发送请求
33+
message = json.dumps(request) + "\n"
34+
self.process.stdin.write(message.encode())
35+
await self.process.stdin.drain()
36+
37+
# 读取响应
38+
response_line = await self.process.stdout.readline()
39+
if not response_line:
40+
raise RuntimeError("MCP server closed connection")
41+
42+
response = json.loads(response_line.decode())
43+
44+
if "error" in response:
45+
raise RuntimeError(f"MCP error: {response['error']}")
46+
47+
return response.get("result", {})
48+
49+
async def initialize(self) -> dict:
50+
"""执行 MCP 初始化握手。"""
51+
# 发送 initialize 请求
52+
result = await self.send_request(
53+
"initialize",
54+
{
55+
"protocolVersion": "2024-11-05",
56+
"capabilities": {},
57+
"clientInfo": {"name": "test-client", "version": "1.0.0"},
58+
},
59+
)
60+
61+
# 发送 initialized 通知
62+
notification = {"jsonrpc": "2.0", "method": "notifications/initialized"}
63+
message = json.dumps(notification) + "\n"
64+
self.process.stdin.write(message.encode())
65+
await self.process.stdin.drain()
66+
67+
return result
68+
69+
async def list_tools(self) -> list[dict]:
70+
"""获取工具列表。"""
71+
result = await self.send_request("tools/list")
72+
return result.get("tools", [])
73+
74+
async def call_tool(self, name: str, arguments: dict) -> dict:
75+
"""调用工具。"""
76+
return await self.send_request("tools/call", {"name": name, "arguments": arguments})
77+
78+
async def close(self) -> None:
79+
"""关闭连接。"""
80+
if self.process.stdin:
81+
self.process.stdin.close()
82+
try:
83+
self.process.kill()
84+
except ProcessLookupError:
85+
pass
86+
87+
88+
@pytest.fixture
89+
async def mcp_client():
90+
"""启动 MCP Server 并返回客户端实例。"""
91+
# 使用 uv run 启动 autocode-mcp
92+
process = await asyncio.create_subprocess_exec(
93+
sys.executable,
94+
"-m",
95+
"autocode_mcp.server",
96+
stdin=asyncio.subprocess.PIPE,
97+
stdout=asyncio.subprocess.PIPE,
98+
stderr=asyncio.subprocess.PIPE,
99+
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
100+
)
101+
102+
client = MCPClient(process)
103+
104+
try:
105+
yield client
106+
finally:
107+
await client.close()
108+
109+
110+
@pytest.mark.asyncio
111+
async def test_mcp_handshake(mcp_client: MCPClient):
112+
"""测试 MCP 协议握手。"""
113+
result = await mcp_client.initialize()
114+
115+
assert "protocolVersion" in result
116+
assert "serverInfo" in result
117+
assert result["serverInfo"]["name"] == "autocode-mcp"
118+
119+
120+
@pytest.mark.asyncio
121+
async def test_mcp_list_tools(mcp_client: MCPClient):
122+
"""测试获取工具列表。"""
123+
await mcp_client.initialize()
124+
125+
tools = await mcp_client.list_tools()
126+
127+
# 验证有 15 个工具
128+
assert len(tools) == 15
129+
130+
# 验证关键工具存在
131+
tool_names = {t["name"] for t in tools}
132+
expected_tools = {
133+
"file_read",
134+
"file_save",
135+
"solution_build",
136+
"solution_run",
137+
"validator_build",
138+
"generator_build",
139+
"checker_build",
140+
"stress_test_run",
141+
"problem_create",
142+
"problem_generate_tests",
143+
}
144+
assert expected_tools.issubset(tool_names)
145+
146+
147+
@pytest.mark.asyncio
148+
async def test_mcp_call_file_read(mcp_client: MCPClient):
149+
"""测试通过 MCP 调用 file_read 工具。"""
150+
await mcp_client.initialize()
151+
152+
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
153+
f.write("hello world")
154+
temp_path = f.name
155+
156+
try:
157+
result = await mcp_client.call_tool("file_read", {"path": temp_path})
158+
159+
# 验证返回结构
160+
assert "content" in result
161+
assert not result.get("isError", True)
162+
163+
# 验证 content 是列表且包含 TextContent
164+
content = result["content"]
165+
assert isinstance(content, list)
166+
assert len(content) == 1
167+
assert content[0]["type"] == "text"
168+
169+
# 验证文本内容是有效 JSON
170+
text = content[0]["text"]
171+
parsed = json.loads(text)
172+
assert parsed["success"] is True
173+
assert "data" in parsed
174+
assert parsed["data"]["content"] == "hello world"
175+
176+
# 验证 structuredContent 存在
177+
assert "structuredContent" in result
178+
assert result["structuredContent"]["success"] is True
179+
finally:
180+
os.unlink(temp_path)
181+
182+
183+
@pytest.mark.asyncio
184+
async def test_mcp_call_unknown_tool(mcp_client: MCPClient):
185+
"""测试调用不存在的工具返回错误。"""
186+
await mcp_client.initialize()
187+
188+
result = await mcp_client.call_tool("nonexistent_tool", {})
189+
190+
assert result.get("isError") is True
191+
assert "Unknown tool" in result["content"][0]["text"]
192+
193+
194+
@pytest.mark.asyncio
195+
async def test_mcp_text_content_is_valid_json(mcp_client: MCPClient):
196+
"""测试 TextContent 的文本是有效 JSON(不是 Python repr)。"""
197+
await mcp_client.initialize()
198+
199+
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
200+
f.write("test")
201+
temp_path = f.name
202+
203+
try:
204+
result = await mcp_client.call_tool("file_read", {"path": temp_path})
205+
206+
text = result["content"][0]["text"]
207+
208+
# 必须是有效 JSON
209+
parsed = json.loads(text)
210+
211+
# 不能是 Python repr 格式(如 {'success': True})
212+
# Python repr 使用单引号,JSON 使用双引号
213+
assert "'" not in text # JSON 不使用单引号
214+
assert parsed["success"] is True
215+
finally:
216+
os.unlink(temp_path)
217+
218+
219+
@pytest.mark.asyncio
220+
async def test_mcp_chinese_text_encoding(mcp_client: MCPClient):
221+
"""测试中文文本编码正确处理。"""
222+
await mcp_client.initialize()
223+
224+
chinese_content = "你好世界"
225+
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
226+
f.write(chinese_content)
227+
temp_path = f.name
228+
229+
try:
230+
result = await mcp_client.call_tool("file_read", {"path": temp_path})
231+
232+
text = result["content"][0]["text"]
233+
parsed = json.loads(text)
234+
235+
# 验证中文正确编码(ensure_ascii=False)
236+
assert parsed["data"]["content"] == chinese_content
237+
# 原始文本应该包含中文字符,不是 \uXXXX 转义
238+
assert chinese_content in text
239+
finally:
240+
os.unlink(temp_path)

0 commit comments

Comments
 (0)