Skip to content

feat(sdk-python): enable mcp_servers support via initialize control request - #6463

Closed
juhuan wants to merge 1 commit into
QwenLM:mainfrom
juhuan:feat/sdk-python-mcp-servers
Closed

feat(sdk-python): enable mcp_servers support via initialize control request#6463
juhuan wants to merge 1 commit into
QwenLM:mainfrom
juhuan:feat/sdk-python-mcp-servers

Conversation

@juhuan

@juhuan juhuan commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Removes the validation that explicitly rejected mcp_servers in the Python SDK. CLI MCP server configs are now sent to the CLI during the initialize control request, matching the TypeScript SDK's pattern.

The Python SDK already had mcp_servers: dict[str, dict[str, Any]] in QueryOptions and QueryOptionsDict, and from_mapping already parsed it correctly. The only blocker was:

  1. validation.py raised a ValidationError when mcp_servers was set
  2. query.py's _initialize() method did not include mcpServers in the initialize payload

Both are now fixed. The CLI's SystemController.handleInitialize() already handles external MCP server configs received through the mcpServers field in the initialize request.

What's supported

CLI MCP servers (external servers managed by the CLI process): stdio, SSE, streamable HTTP, WebSocket. These are the same configs the TypeScript SDK supports via CLIMcpServerConfigSchema.

What's not supported

In-process SDK MCP servers (type: 'sdk' with a server instance). The TypeScript SDK supports these via SdkMcpServerConfig, but this requires a Python MCP server runtime that doesn't exist yet. This is tracked separately in #4889.

Changes

Python SDK (packages/sdk-python):

  • validation.py — removed the mcp_servers rejection block
  • query.py_initialize() now includes mcpServers in the initialize payload when configured
  • test_validation.py — replaced test_rejects_mcp_servers with test_accepts_mcp_servers

Why it's needed

The Python SDK is the primary SDK for programmatic access to qwen-code, but it couldn't configure MCP servers — a core feature for extending the CLI with custom tools. Users had to use the TypeScript SDK or configure MCP servers in settings files instead of programmatically. This unblocks the most common use case: defining MCP servers in code when creating a query.

Reviewer Test Plan

How to verify

  1. cd packages/sdk-python && PYTHONPATH=src python3 -m pytest tests/unit/test_validation.py tests/unit/test_transport.py -v — 34 tests pass, including test_accepts_mcp_servers

Evidence (Before & After)

N/A — SDK option change, no user-visible TUI change

Tested on

OS Status
🍏 macOS N/A
🪟 Windows N/A
🐧 Linux

Environment

Unit tests only: Python 3.11 with pytest 9.1

Risk & Scope

Linked Issues

Closes #4889 (partially — CLI MCP servers only, not in-process SDK MCP servers)

中文说明

本 PR 做了什么

移除了 Python SDK 中明确拒绝 mcp_servers 的验证逻辑。CLI MCP 服务器配置现在通过 initialize 控制请求发送给 CLI,与 TypeScript SDK 的模式一致。

Python SDK 此前已有 mcp_servers 字段在 QueryOptionsQueryOptionsDict 中,from_mapping 也能正确解析。唯一的阻塞点是:

  1. validation.py 在设置 mcp_servers 时抛出 ValidationError
  2. query.py_initialize() 方法未在初始化载荷中包含 mcpServers

两者均已修复。CLI 的 SystemController.handleInitialize() 已经能处理通过 mcpServers 字段接收的外部 MCP 服务器配置。

支持范围

CLI MCP 服务器(由 CLI 进程管理的外部服务器):stdio、SSE、streamable HTTP、WebSocket。

不支持

进程内 SDK MCP 服务器(type: 'sdk' 带服务器实例),需要 Python MCP 服务器运行时,跟踪于 #4889

风险与范围

  • 主要风险:移除了硬错误,此前捕获 mcp_servers ValidationError 的代码不再触发。这是预期的行为变更。
  • 破坏性变更:无(正常使用下)。依赖 mcp_servers 被拒绝的代码需更新以处理新行为。

…equest

Remove the validation that rejected mcp_servers in the Python SDK.
CLI MCP server configs are now sent to the CLI during the initialize
control request, matching the TypeScript SDK's pattern. The CLI's
SystemController already handles external MCP server configs received
through this channel.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This is a real feature gap, not theoretical hardening. The Python SDK currently raises ValidationError("mcp_servers is not supported in Python SDK v1") when users try to configure MCP servers programmatically — a feature the TypeScript SDK already supports. Users are forced to use the TS SDK or configure MCP servers in settings files instead.

Direction: Aligned. MCP server configuration is a core SDK capability, and the Python SDK is the primary SDK for programmatic access. Bringing it to parity with the TypeScript SDK on CLI-managed MCP servers is clearly within scope. The PR correctly draws the line at in-process SDK MCP servers (tracked in #4889).

Size: 8 production lines (2 added in query.py, 6 removed from validation.py), 9 test lines. Not core infrastructure — scoped entirely to packages/sdk-python. Tiny.

Approach: Minimal and focused. Two surgical edits: remove the validation block, wire mcpServers into the initialize payload. The change mirrors the TypeScript SDK's pattern (Query.ts:304-306) exactly. No scope creep, no drive-by refactors.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是真实的功能缺口,不是理论性加固。Python SDK 目前在用户尝试配置 MCP 服务器时抛出 ValidationError,而 TypeScript SDK 已支持此功能。用户被迫使用 TS SDK 或在配置文件中设置 MCP 服务器。

方向:对齐。MCP 服务器配置是 SDK 的核心能力,Python SDK 是编程访问的主要 SDK。在 CLI 管理的 MCP 服务器方面与 TS SDK 对齐,完全在范围内。PR 正确地将进程内 SDK MCP 服务器排除在外(跟踪于 #4889)。

规模:8 行生产代码(query.py 新增 2 行,validation.py 移除 6 行),9 行测试代码。不涉及核心基础设施——全部在 packages/sdk-python 内。非常小。

方案:最小且聚焦。两处精确修改:移除验证块,将 mcpServers 接入初始化载荷。与 TypeScript SDK 的模式(Query.ts:304-306)完全一致。无范围蔓延,无顺手重构。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: To enable MCP server support in the Python SDK, I would: (1) remove the ValidationError block in validation.py, (2) add mcpServers to the _initialize() payload in query.py matching the TS SDK's sendControlRequest(INITIALIZE, { mcpServers: ... }) pattern, and (3) flip the test from "rejects" to "accepts". That's exactly what this PR does — no simpler path exists.

Diff comparison: The implementation is correct and matches the TypeScript SDK pattern (Query.ts:304-306). The Python SDK sends self._options.mcp_servers directly as payload["mcpServers"], which is the right format — dict[str, dict[str, Any]] maps to Record<string, CLIMcpServerConfig>. The TS SDK has additional logic to filter out SDK MCP servers (getMcpServersForCli()), but since the Python SDK doesn't support SDK MCP servers (in-process), all entries are CLI servers and no filtering is needed. Clean.

Reuse check: No new code added — just removing a guard and wiring an existing field. Nothing to reuse.

Issues found: None. No correctness bugs, no security holes, no convention violations.

Before/After

This is an SDK behavior change (no TUI impact). Verified with a Python script that exercises the validation path:

Before (main)

$ PYTHONPATH=src python3 -c "
from qwen_code_sdk.types import QueryOptions
from qwen_code_sdk.validation import validate_query_options
opts = QueryOptions(mcp_servers={'my-server': {'command': 'node', 'args': ['server.js']}})
try:
    validate_query_options(opts)
    print('UNEXPECTED: no error raised')
except Exception as e:
    print(f'ERROR: {type(e).__name__}: {e}')
"
ERROR: ValidationError: mcp_servers is not supported in Python SDK v1. Remove the mcp_servers option or use the TypeScript SDK.

After (this PR)

$ PYTHONPATH=src python3 -c "
from qwen_code_sdk.types import QueryOptions
from qwen_code_sdk.validation import validate_query_options
opts = QueryOptions(mcp_servers={'my-server': {'command': 'node', 'args': ['server.js']}})
validate_query_options(opts)
print('SUCCESS: mcp_servers accepted without error')
print(f'mcp_servers value: {opts.mcp_servers}')
"
SUCCESS: mcp_servers accepted without error
mcp_servers value: {'my-server': {'command': 'node', 'args': ['server.js']}}

Unit Tests

$ PYTHONPATH=src python3 -m pytest tests/ -v
...
tests/unit/test_validation.py::test_accepts_mcp_servers PASSED
...
============================== 58 passed in 0.86s ==============================

All 58 tests pass (including the renamed test_accepts_mcp_servers).

中文说明

代码审查

独立方案: 要在 Python SDK 中启用 MCP 服务器支持,我会:(1) 移除 validation.py 中的 ValidationError 块,(2) 在 query.py_initialize() 载荷中添加 mcpServers,匹配 TS SDK 的 sendControlRequest(INITIALIZE, { mcpServers: ... }) 模式,(3) 将测试从"拒绝"翻转为"接受"。这正是 PR 所做的——没有更简单的路径。

Diff 对比: 实现正确,与 TypeScript SDK 的模式一致。Python SDK 直接将 self._options.mcp_servers 作为 payload["mcpServers"] 发送。TS SDK 有额外逻辑过滤 SDK MCP 服务器(getMcpServersForCli()),但 Python SDK 不支持进程内 SDK MCP 服务器,所以所有条目都是 CLI 服务器,无需过滤。干净。

问题: 无。无正确性 bug、无安全漏洞、无规范违反。

Before/After

这是 SDK 行为变更(无 TUI 影响)。通过 Python 脚本验证了验证路径:

Before:ValidationError: mcp_servers is not supported in Python SDK v1
After:SUCCESS: mcp_servers accepted without error

全部 58 个测试通过。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is one of the cleanest fork PRs I've reviewed. Eight production lines that do exactly what they say: remove a guard, wire a field. The before/after confirms the behavior change, all 58 tests pass, and the implementation mirrors the TypeScript SDK's established pattern.

The problem is real — I verified it by running the validation on main and getting the ValidationError the PR describes. MCP server support is a core SDK capability, and the Python SDK was the odd one out. This fixes that gap for CLI-managed servers, which covers the common case.

No reservations. The scope is tight, the diff is minimal, the test is updated. Ships the feature cleanly.

Approving. ✅

中文说明

这是我审查过的最干净的 fork PR 之一。8 行生产代码精确地完成了声明的功能:移除守卫,接入字段。Before/after 确认了行为变更,全部 58 个测试通过,实现与 TypeScript SDK 已建立的模式一致。

问题是真实的——我在 main 上运行验证,确认了 PR 描述的 ValidationError。MCP 服务器支持是 SDK 的核心能力,Python SDK 是唯一不支持的。此 PR 为 CLI 管理的服务器修复了这一缺口,覆盖了常见场景。

没有顾虑。范围紧凑,diff 最小,测试已更新。干净地交付了功能。

批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

try:
payload: dict[str, Any] = {"hooks": None}
if self._options.mcp_servers:
payload["mcpServers"] = self._options.mcp_servers

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The 2 new production lines that wire mcpServers into the initialize payload have no corresponding unit test. test_accepts_mcp_servers only verifies that validation passes — it does not verify the field actually reaches the CLI process.

A regression here (typo in key name, wrong variable reference) would silently break MCP server support with no test catching it.

Consider adding a test in test_query_core.py:

@pytest.mark.asyncio
async def test_initialize_includes_mcp_servers_in_payload() -> None:
    transport = FakeTransport()
    servers = {"my-server": {"command": "node", "args": []}}
    query = Query(
        transport=transport,
        options=QueryOptions(mcp_servers=servers, timeout=TimeoutOptions(...)),
        prompt="hello",
        session_id=VALID_UUID,
    )
    await query._ensure_started()
    init_request = await _wait_for_request(transport, "initialize")
    assert init_request["request"]["mcpServers"] == servers
    await query.close()

Also worth testing the negative case: when mcp_servers is None, mcpServers should not appear in the payload.

— qwen3.7-max via Qwen Code /review

@@ -64,12 +64,6 @@ def validate_query_options(options: QueryOptions) -> None:
):
raise ValidationError("path_to_qwen_executable cannot be empty")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The PR removes the reject-all validation for mcp_servers without adding any positive validation on inner dict values. The TypeScript SDK validates server configs against CLIMcpServerConfigSchema (Zod); the Python SDK now accepts any dict[str, dict[str, Any]] — including {"server": {}} (empty config, no transport type) or {"server": {"command": 123}} (wrong type).

Malformed configs will pass through silently and be dropped by the CLI's normalizeMcpServerConfig with only a debug-level log — the user gets no error, their MCP server simply doesn't appear.

Consider adding minimal validation:

if options.mcp_servers:
    for name, cfg in options.mcp_servers.items():
        if not isinstance(cfg, dict):
            raise ValidationError(f"mcp_servers['{name}'] must be a dict")
        if not any(k in cfg for k in ("command", "url", "httpUrl", "tcp")):
            raise ValidationError(
                f"mcp_servers['{name}'] must include at least one of: "
                "command, url, httpUrl, tcp"
            )

— qwen3.7-max via Qwen Code /review

async def _initialize(self) -> None:
try:
payload: dict[str, Any] = {"hooks": None}
if self._options.mcp_servers:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The 2 new lines adding mcpServers to the initialize payload have no test coverage. The existing test_accepts_mcp_servers only verifies that validation doesn't raise — it doesn't assert the wire payload. A regression that drops or renames the key would go undetected.

Add a unit test in test_query_core.py that constructs a Query with mcp_servers set and asserts the captured initialize request payload contains "mcpServers":

options = QueryOptions(mcp_servers={"my-server": {"command": "node", "args": []}})
# ... start query, capture initialize request via FakeTransport ...
assert init_request["mcpServers"] == {"my-server": {"command": "node", "args": []}}

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Suggestions — commit 5755ab3

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

💡 Suggestion: Consolidate SDK PRs

Hi @juhuan, thanks for the comprehensive SDK work! We noticed you have 15 open PRs that all modify the same core files (transport.py, types.py, queryOptionsSchema.ts, types.ts, ProcessTransport.ts, createQuery.ts) and were created on the same day.

The problem

  • Merge conflicts: Since all 15 PRs touch the same files, whichever merges first will cause conflicts in the remaining 14.
  • Review overhead: Reviewing 15 near-identical PRs separately is inefficient and risks fatigue.
  • CI cost: 15 separate CI runs for the same lint/typecheck passes.

Suggestion: regroup into 2 PRs

We recommend closing the current 15 PRs and reopening them as 2 consolidated PRs:

PR 1 — feat(sdk): expose transport and query options in both SDKs

Covers pure SDK-side option additions (~9 current PRs):

PR 2 — feat(sdk): add control request methods to both SDKs

Covers features that also involve CLI-side ControlDispatcher changes (~4 current PRs):

This keeps a reasonable separation of concerns while eliminating the merge-conflict chain and making review much more manageable.

/cc @juhuan

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Closing in favor of consolidated PRs (see suggestion comment above). Please reopen as 2 grouped PRs.

@wenshao wenshao closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] In-process MCP server support for Python SDK (like create_sdk_mcp_server in Claude Code SDK)

3 participants