Skip to content

Commit 3d14ee6

Browse files
committed
fix: validate MCP tool names and async fetch concurrency
1 parent 639126d commit 3d14ee6

13 files changed

Lines changed: 82 additions & 1 deletion

File tree

‎course-solutions.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
{
6565
"slug": "mcp-server-in-python",
6666
"course_id": "course-mcp-tools",
67-
"solution_sha256": "e9a0c348e12bd1f04d83783689f299e3ece8884f3f8a5be2f8a40655e211b790"
67+
"solution_sha256": "3f58792fafaf0025f8766ca84ac8cae660e73868193fe55b61bb97548ac2aca7"
6868
},
6969
{
7070
"slug": "secure-ai-generated-code",

‎courses/mcp-server-in-python/TASK.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Change only `starter/mcp_server.py`.
1515
- Supported methods:
1616
- `"tools/list"`: returns `{"result": {"tools": [{"name": ..., "description": ..., "inputSchema": ...}, ...]}}`.
1717
- `"tools/call"`: accepts `params: {"name": ..., "arguments": ..., "inputResponses": {...}?}` and calls the handler as `handler(arguments, inputResponses)`.
18+
- If the tool name is missing or is not a string (including lists, objects, booleans, numbers, and null), return JSON-RPC error `-32602` (Invalid params), preserving the request ID.
1819
- If tool not registered: returns `{"result": {"isError": True, "content": [{"type": "text", "text": "Tool not found"}]}}`.
1920
- If required argument missing: returns `{"result": {"isError": True, "content": [{"type": "text", "text": "Missing required argument: <arg>"}]}}`.
2021
- If the handler raises `InputRequired(requests)`: returns `{"result": {"resultType": "input_required", "requests": <requests>, "content": [{"type": "text", "text": "Additional client input is required before this tool can finish."}]}}`. The client retries the same call with answers in `params.inputResponses`.

‎courses/mcp-server-in-python/TASK_cn.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- 支持的核心方法:
1616
- `"tools/list"`:返回 `{"result": {"tools": [{"name": ..., "description": ..., "inputSchema": ...}, ...]}}`。
1717
- `"tools/call"`:接收 `params: {"name": ..., "arguments": ..., "inputResponses": {...}?}`,并以 `handler(arguments, inputResponses)` 调用工具。
18+
- 工具名缺失或不是字符串(包括列表、对象、布尔值、数字和 null)时,返回 JSON-RPC 错误码 `-32602`(Invalid params),并保留请求 ID。
1819
- 工具未找到:返回 `{"result": {"isError": True, "content": [{"type": "text", "text": "Tool not found"}]}}`。
1920
- 缺少 schema 中声明的必填参数:返回 `{"result": {"isError": True, "content": [{"type": "text", "text": "Missing required argument: <arg>"}]}}`。
2021
- 工具抛出 `InputRequired(requests)`:返回 `{"result": {"resultType": "input_required", "requests": <requests>, "content": [{"type": "text", "text": "Additional client input is required before this tool can finish."}]}}`。客户端携带 `params.inputResponses` 重试同一请求。

‎courses/mcp-server-in-python/solution/mcp_server.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ def handle_request(self, request: Any) -> dict[str, Any]:
108108
}
109109

110110
tool_name = params["name"]
111+
if not isinstance(tool_name, str):
112+
return {
113+
"jsonrpc": "2.0",
114+
"id": req_id,
115+
"error": {
116+
"code": -32602,
117+
"message": "Invalid params: tool name must be a string",
118+
},
119+
}
111120
if tool_name not in self.tools:
112121
return {
113122
"jsonrpc": "2.0",

‎courses/mcp-server-in-python/tests/test_mcp_server.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ def test_tools_list_returns_schemas(self) -> None:
5555
names = {t["name"] for t in tools}
5656
self.assertEqual(names, {"calculate_sum", "risky_operation"})
5757

58+
def test_tools_call_rejects_non_string_tool_name(self) -> None:
59+
for invalid_name in ([], {}, None, True, False, 1, 1.5):
60+
with self.subTest(invalid_name=invalid_name):
61+
resp = self.server.handle_request({
62+
"jsonrpc": "2.0",
63+
"id": 99,
64+
"method": "tools/call",
65+
"params": {"name": invalid_name, "arguments": {}},
66+
})
67+
self.assertEqual(resp["jsonrpc"], "2.0")
68+
self.assertEqual(resp["id"], 99)
69+
self.assertEqual(resp["error"]["code"], -32602)
70+
self.assertIn("tool name must be a string", resp["error"]["message"])
71+
5872
def test_tools_call_success(self) -> None:
5973
resp = self.server.handle_request({
6074
"jsonrpc": "2.0",

‎examples/async-fetcher/TASK.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Change only `starter/fetcher.py`.
44

55
- Implement `async_batch_fetch(items: list[dict], fetch_fn: Callable[[dict], Awaitable[dict]], max_concurrency: int = 3, max_retries: int = 3) -> list[dict]`.
66
- Concurrency bounding:
7+
- `max_concurrency` must be a positive integer; reject zero, negative values, booleans, and non-integers with `ValueError` before starting any tasks, even for empty input.
78
- At most `max_concurrency` calls to `fetch_fn` may be executing concurrently at any moment.
89
- Retry & Backoff contract:
910
- `fetch_fn` returns `{"status_code": int, "data": ...}` or raises an exception.

‎examples/async-fetcher/TASK_cn.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- 实现 `async_batch_fetch(items: list[dict], fetch_fn: Callable[[dict], Awaitable[dict]], max_concurrency: int = 3, max_retries: int = 3) -> list[dict]`。
66
- 并发控制要求:
7+
- `max_concurrency` 必须是正整数;零、负数、布尔值及非整数必须在启动任何任务之前抛出 `ValueError`,即使输入为空也不例外。
78
- 任何时刻并发执行 `fetch_fn` 的协程数量不得超过 `max_concurrency`。
89
- 重试与退避策略:
910
- `fetch_fn` 返回 `{"status_code": int, "data": ...}` 或抛出异常。

‎examples/async-fetcher/solution/fetcher.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ async def async_batch_fetch(
7474
max_concurrency: int = 3,
7575
max_retries: int = 3,
7676
) -> list[dict[str, Any]]:
77+
if isinstance(max_concurrency, bool) or not isinstance(max_concurrency, int) or max_concurrency < 1:
78+
raise ValueError("max_concurrency must be a positive integer")
7779
semaphore = asyncio.Semaphore(max_concurrency)
7880
async with asyncio.TaskGroup() as group:
7981
tasks = [

‎examples/async-fetcher/tests/test_fetcher.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@
77

88

99
class TestAsyncBatchFetcher(unittest.TestCase):
10+
def test_rejects_invalid_concurrency(self) -> None:
11+
async def mock_fetch(item: dict) -> dict:
12+
return {"status_code": 200, "data": item}
13+
14+
async def run_invalid(value: object) -> None:
15+
# Bound the regression test itself: Semaphore(0) used to hang.
16+
await asyncio.wait_for(
17+
async_batch_fetch([{"id": 1}], mock_fetch, max_concurrency=value),
18+
timeout=1,
19+
)
20+
21+
for invalid in (0, -1, True, False, 1.5, "2", None, float("nan"), float("inf")):
22+
with self.subTest(max_concurrency=invalid):
23+
with self.assertRaisesRegex(ValueError, "max_concurrency must be a positive integer"):
24+
asyncio.run(run_invalid(invalid))
25+
with self.assertRaisesRegex(ValueError, "max_concurrency must be a positive integer"):
26+
asyncio.run(async_batch_fetch([], mock_fetch, max_concurrency=invalid))
27+
28+
def test_single_concurrency_and_empty_input(self) -> None:
29+
async def mock_fetch(item: dict) -> dict:
30+
return {"status_code": 200, "data": item["id"]}
31+
32+
self.assertEqual(asyncio.run(async_batch_fetch([], mock_fetch, max_concurrency=1)), [])
33+
results = asyncio.run(async_batch_fetch([{"id": 1}], mock_fetch, max_concurrency=1))
34+
self.assertTrue(results[0]["success"])
35+
self.assertEqual(results[0]["data"], 1)
36+
1037
def test_bounded_concurrency(self) -> None:
1138
active_concurrency = 0
1239
peak_concurrency = 0

‎examples/mcp-server/TASK.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Change only `starter/mcp_server.py`.
1515
- Supported methods:
1616
- `"tools/list"`: returns `{"result": {"tools": [{"name": ..., "description": ..., "inputSchema": ...}, ...]}}`.
1717
- `"tools/call"`: accepts `params: {"name": ..., "arguments": ..., "inputResponses": {...}?}` and calls the handler as `handler(arguments, inputResponses)`.
18+
- If the tool name is missing or is not a string (including lists, objects, booleans, numbers, and null), return JSON-RPC error `-32602` (Invalid params), preserving the request ID.
1819
- If tool not registered: returns `{"result": {"isError": True, "content": [{"type": "text", "text": "Tool not found"}]}}`.
1920
- If required argument missing: returns `{"result": {"isError": True, "content": [{"type": "text", "text": "Missing required argument: <arg>"}]}}`.
2021
- If the handler raises `InputRequired(requests)`: returns `{"result": {"resultType": "input_required", "requests": <requests>, "content": [{"type": "text", "text": "Additional client input is required before this tool can finish."}]}}`. The client retries the same call with answers in `params.inputResponses`.

0 commit comments

Comments
 (0)