build: complete upgrade to fastapi 0.141, mcp sdk v2, fastmcp 4 and httpx2

This commit is contained in:
lda
2026-09-06 12:17:15 +07:00 Verified
parent 5b901557bd
commit 6d5b6741fb
42 changed files with 946 additions and 737 deletions
+1 -1
View File
@@ -215,7 +215,7 @@ def rpc_client_from_target(
) -> RpcWorkflowApiClient:
"""Build the remote workflow surface for a resolved RPC target.
CLI tests patch this project-owned seam instead of monkeypatching `httpx`
CLI tests patch this project-owned seam instead of monkeypatching `httpx2`
internals. The production path still keeps HTTP construction inside the
transport package.
"""
+6 -4
View File
@@ -9,7 +9,7 @@ import typer
from wf_cli.context import CliContext
if TYPE_CHECKING:
import httpx
import httpx2
T = TypeVar("T")
@@ -24,11 +24,11 @@ def run_cli_operation(context: CliContext, operation: Coroutine[Any, Any, T]) ->
# HTTP exceptions only matter after a CLI operation starts. Keep the HTTP
# client stack out of command registration and the `wf --help` path.
import httpx
import httpx2
try:
return asyncio.run(operation)
except (RuntimeError, httpx.HTTPError) as exc:
except (RuntimeError, httpx2.HTTPError) as exc:
if context.verbose:
raise
# Typer 0.26 vendors Click, so external ClickException classes bypass
@@ -37,7 +37,9 @@ def run_cli_operation(context: CliContext, operation: Coroutine[Any, Any, T]) ->
raise typer.Exit(code=1) from exc
def _operation_error_message(exc: RuntimeError | httpx.HTTPError) -> str:
def _operation_error_message(
exc: RuntimeError | httpx2.HTTPError,
) -> str:
"""Return a stable non-empty message for compact CLI error output."""
message = str(exc)
if message:
+2 -2
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
from wf_api.models import (
DeleteDeploymentResult,
@@ -20,7 +20,7 @@ from ..workflow_surface.models import RunDeploymentResult
from .service import WfMcpService
def register_artifact_tools(server: FastMCP, service: WfMcpService) -> None:
def register_artifact_tools(server: MCPServer, service: WfMcpService) -> None:
"""Register stable MCP tools for saved workflow artifact inspection."""
handlers = WorkflowSurfaceHandlers(service)
+2 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
from .service import WfMcpService
@@ -18,7 +18,7 @@ smallest reusable piece before saving a larger workflow artifact.
"""
def register_broker_prompts(server: FastMCP, service: WfMcpService) -> None:
def register_broker_prompts(server: MCPServer, service: WfMcpService) -> None:
"""Register broker prompt handlers on a FastMCP server."""
@server.prompt(
+2 -2
View File
@@ -3,12 +3,12 @@ from __future__ import annotations
import json
from dataclasses import asdict
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
from .service import WfMcpService
def register_broker_resources(server: FastMCP, service: WfMcpService) -> None:
def register_broker_resources(server: MCPServer, service: WfMcpService) -> None:
"""Register broker resource handlers on a FastMCP server."""
@server.resource("wf-mcp://catalog", name="catalog.all")
+3 -3
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
from wf_api import (
WorkflowAdminApi,
@@ -28,8 +28,8 @@ from .service.workflow_operation_context import context_from_service
from .tools import register_broker_tools
def create_broker_server(service: WfMcpService) -> FastMCP:
server = FastMCP(
def create_broker_server(service: WfMcpService) -> MCPServer:
server = MCPServer(
"wf-mcp-broker",
instructions=(
"A broker MCP server over one or more upstream MCP connections. "
@@ -7,9 +7,9 @@ from dataclasses import dataclass, field
from typing import Any
import anyio
import httpx
import httpx2
from mcp.client.streamable_http import StreamableHTTPError
from mcp.shared.exceptions import McpError
from mcp.shared.exceptions import MCPError
from wf_artifacts import (
DependencyDiagnostic,
@@ -399,8 +399,8 @@ _LIVE_SOURCE_CHECK_FAILURES = (
anyio.ClosedResourceError,
anyio.EndOfStream,
anyio.BrokenResourceError,
httpx.HTTPError,
McpError,
httpx2.HTTPError,
MCPError,
StreamableHTTPError,
)
+2 -2
View File
@@ -1,12 +1,12 @@
from __future__ import annotations
from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer
from ..admin_surface import register_service_admin_tools
from .service import WfMcpService
def register_broker_tools(server: FastMCP, service: WfMcpService) -> None:
def register_broker_tools(server: MCPServer, service: WfMcpService) -> None:
"""Register legacy bare-name broker tools from the shared admin registrar."""
register_service_admin_tools(
server,
+1 -1
View File
@@ -15,7 +15,7 @@ def map_event_to_notifications(event: McpEvent) -> list[mcp_types.ServerNotifica
notification = _list_changed_notification(event)
if notification is None:
return []
return [mcp_types.ServerNotification(notification)]
return [notification]
def _list_changed_notification(
+2 -2
View File
@@ -34,7 +34,7 @@ class FastMcpNotificationContext(Protocol):
async def send_notification(
self,
notification: mcp_types.ServerNotificationType,
notification: mcp_types.ServerNotification,
) -> None: ...
@@ -46,4 +46,4 @@ class FastMcpContextNotificationSink:
async def send_event(self, event: McpEvent) -> None:
for notification in map_event_to_notifications(event):
await self._context.send_notification(notification.root)
await self._context.send_notification(notification)
+4 -4
View File
@@ -9,12 +9,12 @@ from pathlib import Path
from typing import Any, Generic, TypeVar
import anyio
import httpx
import httpx2
from fastmcp import FastMCP
from fastmcp.client.transports.config import MCPConfigTransport
from fastmcp.server.providers.proxy import FastMCPProxy, StatefulProxyClient
from mcp.client.streamable_http import StreamableHTTPError
from mcp.shared.exceptions import McpError
from mcp.shared.exceptions import MCPError
from ..models import BrokerConfig, ConnectionConfig
from ..proxy_config import broker_config_to_fastmcp_config
@@ -33,8 +33,8 @@ _PROXY_LIST_FAILURES = (
anyio.ClosedResourceError,
anyio.EndOfStream,
anyio.BrokenResourceError,
httpx.HTTPError,
McpError,
httpx2.HTTPError,
MCPError,
StreamableHTTPError,
)
logger = logging.getLogger(__name__)
+1 -1
View File
@@ -10,7 +10,7 @@ from .models import OpenApiOperation
@dataclass(frozen=True, slots=True)
class HttpRequestParts:
"""OpenAPI-shaped request parts ready for `httpx` execution."""
"""OpenAPI-shaped request parts ready for `httpx2` execution."""
method: str
url: str
+4 -4
View File
@@ -4,8 +4,8 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any, TypeVar
import httpx
from mcp import McpError
import httpx2
from mcp import MCPError
from mcp.types import METHOD_NOT_FOUND
from wf_authoring import NodeSpec
@@ -63,9 +63,9 @@ async def _list_optional_capabilities(
return await load()
except Exception as exc:
root = _root_exception(exc)
if isinstance(root, McpError) and root.error.code == METHOD_NOT_FOUND:
if isinstance(root, MCPError) and root.error.code == METHOD_NOT_FOUND:
return []
if isinstance(root, httpx.HTTPStatusError) and root.response.status_code in {
if isinstance(root, httpx2.HTTPStatusError) and root.response.status_code in {
400,
404,
}:
+6 -6
View File
@@ -14,7 +14,7 @@ from .protocols import ToolCallResult
def tool_to_discovered(tool: McpTool) -> DiscoveredTool:
"""Convert an MCP SDK tool into the source discovery model."""
output_schema = workflow_output_schema_from_mcp_tool_schema(tool.outputSchema)
output_schema = workflow_output_schema_from_mcp_tool_schema(tool.output_schema)
display_name = (
tool.annotations.title
if tool.annotations is not None and tool.annotations.title
@@ -24,7 +24,7 @@ def tool_to_discovered(tool: McpTool) -> DiscoveredTool:
name=tool.name,
title=display_name,
description=tool.description,
input_schema=tool.inputSchema,
input_schema=tool.input_schema,
output_schema=output_schema,
outcomes=("ok", "error"),
metadata=tool.model_dump(by_alias=True, mode="json"),
@@ -55,7 +55,7 @@ def resource_to_discovered(resource: McpResource) -> DiscoveredResource:
name=local_name,
title=resource.title,
description=resource.description,
mime_type=resource.mimeType,
mime_type=resource.mime_type,
metadata=resource.model_dump(by_alias=True, mode="json"),
)
@@ -77,14 +77,14 @@ def prompt_to_discovered(prompt: McpPrompt) -> DiscoveredPrompt:
def tool_result_to_call_result(result: McpCallToolResult) -> ToolCallResult:
"""Convert an MCP SDK tool call result into the adapter result model."""
if result.structuredContent is not None:
output = result.structuredContent
if result.structured_content is not None:
output = result.structured_content
else:
output: dict[str, Any] = {
"content": [item.model_dump(by_alias=True) for item in result.content]
}
return ToolCallResult(
outcome="error" if result.isError else "ok",
outcome="error" if result.is_error else "ok",
output=output,
meta=result.meta or {},
)
+3 -3
View File
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from typing import Any, Protocol
from uuid import uuid4
import httpx
import httpx2
@dataclass(slots=True)
@@ -48,7 +48,7 @@ class RpcClientTransport:
url: str
timeout_seconds: float = 30.0
http_client: httpx.AsyncClient | None = None
http_client: httpx2.AsyncClient | None = None
async def _call(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
request_id = uuid4().hex
@@ -59,7 +59,7 @@ class RpcClientTransport:
"params": params,
}
if self.http_client is None:
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
async with httpx2.AsyncClient(timeout=self.timeout_seconds) as client:
response = await client.post(self.url, json=request)
else:
response = await self.http_client.post(self.url, json=request)