PD03 commited on
Commit
ab1679c
·
verified ·
1 Parent(s): 9ec6b48

Create model_context_protocol/fastapi.py

Browse files
Files changed (1) hide show
  1. model_context_protocol/fastapi.py +152 -0
model_context_protocol/fastapi.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lightweight reimplementation of FastAPI helpers for MCP tools.
2
+
3
+ This module provides a small subset of the functionality exposed by the
4
+ ``model-context-protocol`` package so the project can run in environments where
5
+ that package is not available on PyPI (e.g. Hugging Face Spaces build images).
6
+
7
+ The implementation focuses on two concerns:
8
+
9
+ * registering tool handlers that are exposed over HTTP to AgentKit-compatible
10
+ clients; and
11
+ * generating a JSON schema for those tools so that clients can understand the
12
+ available parameters.
13
+
14
+ Only the behaviour required by this project is implemented. The API mirrors the
15
+ real helper closely enough that swapping back to the official dependency simply
16
+ requires reinstalling the package and removing this module.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import inspect
21
+ from dataclasses import dataclass
22
+ from typing import Any, Awaitable, Callable, Dict
23
+
24
+ from fastapi import APIRouter, FastAPI, HTTPException
25
+ from fastapi import Body
26
+ from fastapi.responses import JSONResponse
27
+ from pydantic import BaseModel, create_model
28
+
29
+
30
+ ToolCallable = Callable[..., Awaitable[Any] | Any]
31
+
32
+
33
+ @dataclass
34
+ class _ToolDefinition:
35
+ """Stores metadata required to expose a tool over HTTP."""
36
+
37
+ name: str
38
+ description: str
39
+ endpoint: str
40
+ handler: ToolCallable
41
+ model: type[BaseModel]
42
+
43
+ @property
44
+ def schema(self) -> Dict[str, Any]:
45
+ """Return the JSON schema for the tool's request body."""
46
+ schema = self.model.model_json_schema()
47
+ # The schema contains ``title`` and ``definitions`` keys that are not
48
+ # strictly required for clients. Returning only ``properties`` and
49
+ # ``required`` keeps the payload compact while still conveying the
50
+ # necessary structure.
51
+ return {
52
+ "type": "object",
53
+ "properties": schema.get("properties", {}),
54
+ "required": schema.get("required", []),
55
+ }
56
+
57
+
58
+ class FastAPIMCPServer:
59
+ """Registers MCP tools on a FastAPI application.
60
+
61
+ Parameters
62
+ ----------
63
+ app:
64
+ The FastAPI application to attach routes to.
65
+ base_path:
66
+ The URL prefix for MCP routes. Defaults to ``"/mcp"``.
67
+ """
68
+
69
+ def __init__(self, app: FastAPI, *, base_path: str = "/mcp") -> None:
70
+ self._app = app
71
+ self._base_path = base_path.rstrip("/")
72
+ self._router = APIRouter()
73
+ self._tools: Dict[str, _ToolDefinition] = {}
74
+
75
+ # A discovery endpoint so clients can learn the available tools.
76
+ @self._router.get("/tools", response_class=JSONResponse)
77
+ async def list_tools() -> Dict[str, Any]:
78
+ return {
79
+ "tools": [
80
+ {
81
+ "name": tool.name,
82
+ "description": tool.description,
83
+ "input_schema": tool.schema,
84
+ "endpoint": f"{self._base_path}{tool.endpoint}",
85
+ }
86
+ for tool in self._tools.values()
87
+ ]
88
+ }
89
+
90
+ self._app.include_router(self._router, prefix=self._base_path)
91
+
92
+ # ------------------------------------------------------------------
93
+ # Public API
94
+ # ------------------------------------------------------------------
95
+ def tool(self, *, name: str, description: str) -> Callable[[ToolCallable], ToolCallable]:
96
+ """Decorator used to register a tool with the MCP server."""
97
+
98
+ def decorator(func: ToolCallable) -> ToolCallable:
99
+ if name in self._tools:
100
+ raise ValueError(f"A tool named '{name}' is already registered")
101
+
102
+ model = self._build_model_for_callable(name, func)
103
+ endpoint_path = f"/tools/{name}"
104
+
105
+ async def call_tool(payload: model = Body(...)) -> Any: # type: ignore[misc]
106
+ data = payload.model_dump(exclude_unset=True)
107
+ try:
108
+ result = func(**data)
109
+ if inspect.isawaitable(result):
110
+ result = await result # type: ignore[assignment]
111
+ except TypeError as exc: # pragma: no cover - defensive programming
112
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
113
+ return result
114
+
115
+ self._router.post(endpoint_path, response_class=JSONResponse)(call_tool)
116
+ self._tools[name] = _ToolDefinition(
117
+ name=name,
118
+ description=description,
119
+ endpoint=endpoint_path,
120
+ handler=func,
121
+ model=model,
122
+ )
123
+ return func
124
+
125
+ return decorator
126
+
127
+ # ------------------------------------------------------------------
128
+ # Helpers
129
+ # ------------------------------------------------------------------
130
+ @staticmethod
131
+ def _build_model_for_callable(name: str, func: ToolCallable) -> type[BaseModel]:
132
+ signature = inspect.signature(func)
133
+ annotations = inspect.get_annotations(func)
134
+ fields: Dict[str, tuple[Any, Any]] = {}
135
+
136
+ for parameter_name, parameter in signature.parameters.items():
137
+ if parameter.kind in {parameter.VAR_POSITIONAL, parameter.VAR_KEYWORD}:
138
+ raise TypeError(
139
+ "Var-args are not supported for MCP tool registration"
140
+ )
141
+
142
+ annotation = annotations.get(parameter_name, Any)
143
+ default: Any
144
+ if parameter.default is inspect.Signature.empty:
145
+ default = ...
146
+ else:
147
+ default = parameter.default
148
+
149
+ fields[parameter_name] = (annotation, default)
150
+
151
+ model_name = f"{name.title().replace('_', '')}ToolInput"
152
+ return create_model(model_name, **fields) # type: ignore[call-arg]