From 13a7cb75f6dc4f7cd2026ebc0c40687b85fa2d07 Mon Sep 17 00:00:00 2001 From: XiaoQing235 <60169803+XiaoQing235@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:44:50 +0800 Subject: [PATCH] fix: restore MCP v2 integration (#133) --- domain/agent/mcp.py | 49 ++++++++---------- domain/agent/service.py | 4 +- pyproject.toml | 4 +- uv.lock | 111 +++++++++++++++++++++++++++++----------- 4 files changed, 107 insertions(+), 61 deletions(-) diff --git a/domain/agent/mcp.py b/domain/agent/mcp.py index 6fed6d1..7cbd7fc 100644 --- a/domain/agent/mcp.py +++ b/domain/agent/mcp.py @@ -5,12 +5,12 @@ from datetime import timedelta from typing import Annotated, Any, Literal from urllib.parse import quote, unquote -import httpx -from mcp import ClientSession -from mcp.client.streamable_http import streamablehttp_client +import httpx2 +from mcp.client import Client +from mcp.client.streamable_http import streamable_http_client from mcp.server.auth.provider import AccessToken -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.fastmcp.server import AuthSettings +from mcp.server.auth.settings import AuthSettings +from mcp.server.mcpserver import Context, MCPServer from mcp.types import ToolAnnotations from pydantic import Field @@ -119,10 +119,9 @@ class FoxelMcpTokenVerifier: return AccessToken(token=token, client_id=user.username, scopes=[]) -MCP_SERVER = FastMCP( +MCP_SERVER = MCPServer( name="Foxel MCP", instructions="Foxel 内置 MCP 服务,提供文件系统、网页抓取、时间与处理器相关能力。", - streamable_http_path="/", token_verifier=FoxelMcpTokenVerifier(), auth=AuthSettings( issuer_url="http://127.0.0.1:8000", @@ -268,21 +267,7 @@ def fetch_web_page_prompt(url: Annotated[str, Field(description="目标网址")] return [{"role": "user", "content": f"请抓取网页 `{url}`,并总结标题、正文与关键链接。必要时调用 web_fetch。"}] -MCP_HTTP_APP = MCP_SERVER.streamable_http_app() - - -def loopback_httpx_client_factory(app): - def factory(headers: dict[str, str] | None = None, timeout=None, auth=None) -> httpx.AsyncClient: - return httpx.AsyncClient( - transport=httpx.ASGITransport(app=app), - base_url=INTERNAL_MCP_BASE_URL.rstrip("/"), - headers=headers, - timeout=timeout, - auth=auth, - follow_redirects=True, - ) - - return factory +MCP_HTTP_APP = MCP_SERVER.streamable_http_app(streamable_http_path="/") async def create_loopback_mcp_headers(user: User | None, current_path: str | None = None) -> dict[str, str]: @@ -301,14 +286,20 @@ async def create_loopback_mcp_headers(user: User | None, current_path: str | Non @asynccontextmanager async def mcp_client_session(user: User | None, current_path: str | None = None): headers = await create_loopback_mcp_headers(user, current_path) - async with streamablehttp_client( - INTERNAL_MCP_BASE_URL, + timeout = httpx2.Timeout(30.0, read=300.0) + async with httpx2.AsyncClient( + transport=httpx2.ASGITransport(app=MCP_HTTP_APP), + base_url=INTERNAL_MCP_BASE_URL.rstrip("/"), headers=headers, - httpx_client_factory=loopback_httpx_client_factory(MCP_HTTP_APP), - ) as (read_stream, write_stream, _): - async with ClientSession(read_stream, write_stream) as session: - await session.initialize() - yield session + timeout=timeout, + follow_redirects=True, + ) as http_client: + transport = streamable_http_client( + INTERNAL_MCP_BASE_URL, + http_client=http_client, + ) + async with Client(transport, mode="2026-07-28") as client: + yield client def mcp_content_to_text(content: list[Any], structured_content: dict[str, Any] | None = None) -> str: diff --git a/domain/agent/service.py b/domain/agent/service.py index bcb513f..dfd8353 100644 --- a/domain/agent/service.py +++ b/domain/agent/service.py @@ -149,7 +149,7 @@ async def _list_mcp_tools(session) -> List[Dict[str, Any]]: { "name": str(getattr(item, "name", "") or ""), "description": str(getattr(item, "description", "") or ""), - "input_schema": getattr(item, "inputSchema", None) or {}, + "input_schema": getattr(item, "input_schema", None) or {}, "annotations": annotations.model_dump(exclude_none=True) if annotations is not None else {}, "meta": meta if isinstance(meta, dict) else {}, } @@ -159,7 +159,7 @@ async def _list_mcp_tools(session) -> List[Dict[str, Any]]: async def _execute_mcp_call(session, name: str, arguments: Dict[str, Any]) -> str: result = await session.call_tool(name, arguments) - return mcp_content_to_text(result.content, result.structuredContent) + return mcp_content_to_text(result.content, result.structured_content) class AgentService: diff --git a/pyproject.toml b/pyproject.toml index f266eac..b17eebe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,9 @@ dependencies = [ "bcrypt>=5.0.0", "croniter>=6.0.0", "fastapi>=0.127.0", - "mcp>=1.28.1", + "mcp>=2.1.1,<3", + "httpx>=0.28.1,<1", + "httpx2>=2.5,<3", "paramiko>=5.0.0", "pillow>=12.3.0", "pydantic[email]>=2.12.5", diff --git a/uv.lock b/uv.lock index 342e6d4..6b15bea 100644 --- a/uv.lock +++ b/uv.lock @@ -452,6 +452,8 @@ dependencies = [ { name = "bcrypt" }, { name = "croniter" }, { name = "fastapi" }, + { name = "httpx" }, + { name = "httpx2" }, { name = "mcp" }, { name = "paramiko" }, { name = "pillow" }, @@ -475,7 +477,9 @@ requires-dist = [ { name = "bcrypt", specifier = ">=5.0.0" }, { name = "croniter", specifier = ">=6.0.0" }, { name = "fastapi", specifier = ">=0.127.0" }, - { name = "mcp", specifier = ">=1.28.1" }, + { name = "httpx", specifier = ">=0.28.1,<1" }, + { name = "httpx2", specifier = ">=2.5,<3" }, + { name = "mcp", specifier = ">=2.1.1,<3" }, { name = "paramiko", specifier = ">=5.0.0" }, { name = "pillow", specifier = ">=12.3.0" }, { name = "pydantic", extras = ["email"], specifier = ">=2.12.5" }, @@ -598,6 +602,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -619,12 +636,28 @@ http2 = [ ] [[package]] -name = "httpx-sse" -version = "0.4.3" +name = "httpx2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] @@ -638,11 +671,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -701,15 +734,15 @@ wheels = [ [[package]] name = "mcp" -version = "1.28.1" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, + { name = "httpx2" }, { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, - { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, @@ -719,9 +752,22 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/6e/21fb8e5d579dbe21d96ea4d5034200d46d8bdf2261053b5bd041f3c2f612/mcp-2.1.1.tar.gz", hash = "sha256:50b7ba1ebbe117008ea7bdd288234043e69c20b403d6851d19661e6d431a75ef", size = 3984589, upload-time = "2026-08-25T16:14:02.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/50/af/8644cc5fa26a59afd2df2e98eeb19e72926887fa4b7441aba4ff661140db/mcp-2.1.1-py3-none-any.whl", hash = "sha256:1c6c31c5d6471c58db76af3af8af67f46d11d01f0a59077d0a308cbdb3d3e915", size = 357912, upload-time = "2026-08-25T16:13:59.024Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/dd/1c4417dc0b722c23a1669032d5f044e41170fe5d4773b488a50fcce98c32/mcp_types-2.1.1.tar.gz", hash = "sha256:77dcbe48fba73cca71a673f2646a5f037a017b7a0a07ac89cec1113028890eda", size = 66674, upload-time = "2026-08-25T16:14:03.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/d0/242e63c510f4a17381f55b1549a3f94f5687a0595984febd2b6f87a687a0/mcp_types-2.1.1-py3-none-any.whl", hash = "sha256:26f9f7f03f2a5730717a5b98e2ab7eb640ac352d05a00cdc725c311864778295", size = 69656, upload-time = "2026-08-25T16:14:00.667Z" }, ] [[package]] @@ -729,7 +775,7 @@ name = "milvus-lite" version = "2.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "tqdm", marker = "sys_platform != 'win32'" }, + { name = "tqdm" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a9/b2/acc5024c8e8b6a0b034670b8e8af306ebd633ede777dcbf557eac4785937/milvus_lite-2.5.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6b014453200ba977be37ba660cb2d021030375fa6a35bc53c2e1d92980a0c512", size = 27934713, upload-time = "2025-06-30T04:23:37.028Z" }, @@ -812,6 +858,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + [[package]] name = "orjson" version = "3.11.7" @@ -1078,20 +1136,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.14.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, -] - [[package]] name = "pyjwt" version = "2.13.0" @@ -1403,6 +1447,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"