fastmcp 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +8 -2
- fastmcp/cli/__init__.py +3 -1
- fastmcp/cli/claude.py +20 -18
- fastmcp/cli/cli.py +16 -12
- fastmcp/prompts/__init__.py +4 -0
- fastmcp/prompts/base.py +149 -0
- fastmcp/prompts/manager.py +50 -0
- fastmcp/prompts/prompt_manager.py +36 -0
- fastmcp/resources/__init__.py +23 -0
- fastmcp/resources/base.py +63 -0
- fastmcp/resources/resource_manager.py +94 -0
- fastmcp/resources/templates.py +80 -0
- fastmcp/resources/types.py +171 -0
- fastmcp/server.py +446 -112
- fastmcp/tools/__init__.py +4 -0
- fastmcp/tools/base.py +79 -0
- fastmcp/tools/tool_manager.py +55 -0
- fastmcp/utilities/types.py +53 -0
- fastmcp-0.3.0.dist-info/METADATA +385 -0
- fastmcp-0.3.0.dist-info/RECORD +26 -0
- {fastmcp-0.2.0.dist-info → fastmcp-0.3.0.dist-info}/WHEEL +1 -2
- fastmcp-0.3.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/_version.py +0 -16
- fastmcp/app.py +0 -6
- fastmcp/resources.py +0 -232
- fastmcp/tools.py +0 -150
- fastmcp-0.2.0.dist-info/METADATA +0 -143
- fastmcp-0.2.0.dist-info/RECORD +0 -17
- fastmcp-0.2.0.dist-info/top_level.txt +0 -1
- {fastmcp-0.2.0.dist-info → fastmcp-0.3.0.dist-info}/entry_points.txt +0 -0
fastmcp/server.py
CHANGED
|
@@ -2,27 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
+
import inspect
|
|
5
6
|
import json
|
|
6
|
-
import
|
|
7
|
-
from
|
|
7
|
+
import re
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from typing import Any, Callable, Dict, Literal, Sequence
|
|
8
10
|
|
|
9
|
-
import
|
|
11
|
+
import pydantic_core
|
|
12
|
+
import uvicorn
|
|
10
13
|
from mcp.server import Server as MCPServer
|
|
11
|
-
from mcp.server.stdio import stdio_server
|
|
12
14
|
from mcp.server.sse import SseServerTransport
|
|
15
|
+
from mcp.server.stdio import stdio_server
|
|
16
|
+
from mcp.shared.context import RequestContext
|
|
13
17
|
from mcp.types import (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
TextContent,
|
|
18
|
+
EmbeddedResource,
|
|
19
|
+
GetPromptResult,
|
|
17
20
|
ImageContent,
|
|
21
|
+
TextContent,
|
|
22
|
+
)
|
|
23
|
+
from mcp.types import (
|
|
24
|
+
Prompt as MCPPrompt,
|
|
25
|
+
)
|
|
26
|
+
from mcp.types import (
|
|
27
|
+
Resource as MCPResource,
|
|
28
|
+
)
|
|
29
|
+
from mcp.types import (
|
|
30
|
+
ResourceTemplate as MCPResourceTemplate,
|
|
31
|
+
)
|
|
32
|
+
from mcp.types import (
|
|
33
|
+
Tool as MCPTool,
|
|
18
34
|
)
|
|
19
|
-
from
|
|
20
|
-
from pydantic.networks import
|
|
35
|
+
from pydantic import BaseModel
|
|
36
|
+
from pydantic.networks import AnyUrl
|
|
37
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
21
38
|
|
|
22
|
-
from .exceptions import ResourceError
|
|
23
|
-
from .
|
|
24
|
-
from .
|
|
25
|
-
from .
|
|
39
|
+
from fastmcp.exceptions import ResourceError
|
|
40
|
+
from fastmcp.prompts import Prompt, PromptManager
|
|
41
|
+
from fastmcp.resources import FunctionResource, Resource, ResourceManager
|
|
42
|
+
from fastmcp.tools import ToolManager
|
|
43
|
+
from fastmcp.utilities.logging import configure_logging, get_logger
|
|
44
|
+
from fastmcp.utilities.types import Image
|
|
26
45
|
|
|
27
46
|
logger = get_logger(__name__)
|
|
28
47
|
|
|
@@ -34,13 +53,11 @@ class Settings(BaseSettings):
|
|
|
34
53
|
For example, FASTMCP_DEBUG=true will set debug=True.
|
|
35
54
|
"""
|
|
36
55
|
|
|
37
|
-
model_config:
|
|
56
|
+
model_config: SettingsConfigDict = SettingsConfigDict(env_prefix="FASTMCP_")
|
|
38
57
|
|
|
39
58
|
# Server settings
|
|
40
59
|
debug: bool = False
|
|
41
|
-
log_level: Literal[
|
|
42
|
-
logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL
|
|
43
|
-
] = logging.INFO
|
|
60
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
44
61
|
|
|
45
62
|
# HTTP settings
|
|
46
63
|
host: str = "0.0.0.0"
|
|
@@ -52,9 +69,12 @@ class Settings(BaseSettings):
|
|
|
52
69
|
# tool settings
|
|
53
70
|
warn_on_duplicate_tools: bool = True
|
|
54
71
|
|
|
72
|
+
# prompt settings
|
|
73
|
+
warn_on_duplicate_prompts: bool = True
|
|
74
|
+
|
|
55
75
|
|
|
56
76
|
class FastMCP:
|
|
57
|
-
def __init__(self, name=None, **settings:
|
|
77
|
+
def __init__(self, name: str | None = None, **settings: Any):
|
|
58
78
|
self.settings = Settings(**settings)
|
|
59
79
|
self._mcp_server = MCPServer(name=name or "FastMCP")
|
|
60
80
|
self._tool_manager = ToolManager(
|
|
@@ -63,6 +83,9 @@ class FastMCP:
|
|
|
63
83
|
self._resource_manager = ResourceManager(
|
|
64
84
|
warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
|
|
65
85
|
)
|
|
86
|
+
self._prompt_manager = PromptManager(
|
|
87
|
+
warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
|
|
88
|
+
)
|
|
66
89
|
|
|
67
90
|
# Set up MCP protocol handlers
|
|
68
91
|
self._setup_handlers()
|
|
@@ -95,12 +118,16 @@ class FastMCP:
|
|
|
95
118
|
self._mcp_server.call_tool()(self.call_tool)
|
|
96
119
|
self._mcp_server.list_resources()(self.list_resources)
|
|
97
120
|
self._mcp_server.read_resource()(self.read_resource)
|
|
121
|
+
self._mcp_server.list_prompts()(self.list_prompts)
|
|
122
|
+
self._mcp_server.get_prompt()(self.get_prompt)
|
|
123
|
+
# TODO: This has not been added to MCP yet, see https://github.com/jlowin/fastmcp/issues/10
|
|
124
|
+
# self._mcp_server.list_resource_templates()(self.list_resource_templates)
|
|
98
125
|
|
|
99
|
-
async def list_tools(self) -> list[
|
|
126
|
+
async def list_tools(self) -> list[MCPTool]:
|
|
100
127
|
"""List all available tools."""
|
|
101
128
|
tools = self._tool_manager.list_tools()
|
|
102
129
|
return [
|
|
103
|
-
|
|
130
|
+
MCPTool(
|
|
104
131
|
name=info.name,
|
|
105
132
|
description=info.description,
|
|
106
133
|
inputSchema=info.parameters,
|
|
@@ -108,22 +135,25 @@ class FastMCP:
|
|
|
108
135
|
for info in tools
|
|
109
136
|
]
|
|
110
137
|
|
|
138
|
+
def get_context(self) -> "Context":
|
|
139
|
+
"""
|
|
140
|
+
Returns a Context object. Note that the context will only be valid
|
|
141
|
+
during a request; outside a request, most methods will error.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
request_context = self._mcp_server.request_context
|
|
145
|
+
except LookupError:
|
|
146
|
+
request_context = None
|
|
147
|
+
return Context(request_context=request_context, fastmcp=self)
|
|
148
|
+
|
|
111
149
|
async def call_tool(
|
|
112
150
|
self, name: str, arguments: dict
|
|
113
|
-
) -> Sequence[
|
|
151
|
+
) -> Sequence[TextContent | ImageContent]:
|
|
114
152
|
"""Call a tool by name with arguments."""
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
logger.error(f"Error calling tool {name}: {e}")
|
|
120
|
-
return [
|
|
121
|
-
TextContent(
|
|
122
|
-
type="text",
|
|
123
|
-
text=str(e),
|
|
124
|
-
is_error=True,
|
|
125
|
-
)
|
|
126
|
-
]
|
|
153
|
+
context = self.get_context()
|
|
154
|
+
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
155
|
+
converted_result = _convert_to_content(result)
|
|
156
|
+
return converted_result
|
|
127
157
|
|
|
128
158
|
async def list_resources(self) -> list[MCPResource]:
|
|
129
159
|
"""List all available resources."""
|
|
@@ -132,16 +162,27 @@ class FastMCP:
|
|
|
132
162
|
return [
|
|
133
163
|
MCPResource(
|
|
134
164
|
uri=resource.uri,
|
|
135
|
-
name=resource.name,
|
|
165
|
+
name=resource.name or "",
|
|
136
166
|
description=resource.description,
|
|
137
167
|
mimeType=resource.mime_type,
|
|
138
168
|
)
|
|
139
169
|
for resource in resources
|
|
140
170
|
]
|
|
141
171
|
|
|
142
|
-
async def
|
|
172
|
+
async def list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
173
|
+
templates = self._resource_manager.list_templates()
|
|
174
|
+
return [
|
|
175
|
+
MCPResourceTemplate(
|
|
176
|
+
uriTemplate=template.uri_template,
|
|
177
|
+
name=template.name,
|
|
178
|
+
description=template.description,
|
|
179
|
+
)
|
|
180
|
+
for template in templates
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
|
|
143
184
|
"""Read a resource by URI."""
|
|
144
|
-
resource = self._resource_manager.get_resource(uri)
|
|
185
|
+
resource = await self._resource_manager.get_resource(uri)
|
|
145
186
|
if not resource:
|
|
146
187
|
raise ResourceError(f"Unknown resource: {uri}")
|
|
147
188
|
|
|
@@ -151,64 +192,49 @@ class FastMCP:
|
|
|
151
192
|
logger.error(f"Error reading resource {uri}: {e}")
|
|
152
193
|
raise ResourceError(str(e))
|
|
153
194
|
|
|
154
|
-
def
|
|
155
|
-
self,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if all(isinstance(x, (TextContent, ImageContent)) for x in value):
|
|
162
|
-
return value
|
|
163
|
-
# Handle mixed content including Image objects
|
|
164
|
-
result = []
|
|
165
|
-
for item in value:
|
|
166
|
-
if isinstance(item, (TextContent, ImageContent)):
|
|
167
|
-
result.append(item)
|
|
168
|
-
elif isinstance(item, Image):
|
|
169
|
-
result.append(item.to_image_content())
|
|
170
|
-
else:
|
|
171
|
-
result.append(
|
|
172
|
-
TextContent(
|
|
173
|
-
type="text",
|
|
174
|
-
text=json.dumps(
|
|
175
|
-
item, indent=2, default=pydantic.json.pydantic_encoder
|
|
176
|
-
),
|
|
177
|
-
)
|
|
178
|
-
)
|
|
179
|
-
return result
|
|
195
|
+
def add_tool(
|
|
196
|
+
self,
|
|
197
|
+
fn: Callable,
|
|
198
|
+
name: str | None = None,
|
|
199
|
+
description: str | None = None,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Add a tool to the server.
|
|
180
202
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return [value]
|
|
203
|
+
The tool function can optionally request a Context object by adding a parameter
|
|
204
|
+
with the Context type annotation. See the @tool decorator for examples.
|
|
184
205
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
206
|
+
Args:
|
|
207
|
+
fn: The function to register as a tool
|
|
208
|
+
name: Optional name for the tool (defaults to function name)
|
|
209
|
+
description: Optional description of what the tool does
|
|
210
|
+
"""
|
|
211
|
+
self._tool_manager.add_tool(fn, name=name, description=description)
|
|
188
212
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
TextContent(
|
|
192
|
-
type="text",
|
|
193
|
-
text=json.dumps(
|
|
194
|
-
value, indent=2, default=pydantic.json.pydantic_encoder
|
|
195
|
-
),
|
|
196
|
-
)
|
|
197
|
-
]
|
|
213
|
+
def tool(self, name: str | None = None, description: str | None = None) -> Callable:
|
|
214
|
+
"""Decorator to register a tool.
|
|
198
215
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
func: Callable,
|
|
202
|
-
name: Optional[str] = None,
|
|
203
|
-
description: Optional[str] = None,
|
|
204
|
-
) -> None:
|
|
205
|
-
"""Add a tool to the server."""
|
|
206
|
-
self._tool_manager.add_tool(func, name=name, description=description)
|
|
216
|
+
Tools can optionally request a Context object by adding a parameter with the Context type annotation.
|
|
217
|
+
The context provides access to MCP capabilities like logging, progress reporting, and resource access.
|
|
207
218
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
Args:
|
|
220
|
+
name: Optional name for the tool (defaults to function name)
|
|
221
|
+
description: Optional description of what the tool does
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
@server.tool()
|
|
225
|
+
def my_tool(x: int) -> str:
|
|
226
|
+
return str(x)
|
|
227
|
+
|
|
228
|
+
@server.tool()
|
|
229
|
+
def tool_with_context(x: int, ctx: Context) -> str:
|
|
230
|
+
ctx.info(f"Processing {x}")
|
|
231
|
+
return str(x)
|
|
232
|
+
|
|
233
|
+
@server.tool()
|
|
234
|
+
async def async_tool(x: int, context: Context) -> str:
|
|
235
|
+
await context.report_progress(50, 100)
|
|
236
|
+
return str(x)
|
|
237
|
+
"""
|
|
212
238
|
# Check if user passed function directly instead of calling decorator
|
|
213
239
|
if callable(name):
|
|
214
240
|
raise TypeError(
|
|
@@ -216,9 +242,9 @@ class FastMCP:
|
|
|
216
242
|
"Did you forget to call it? Use @tool() instead of @tool"
|
|
217
243
|
)
|
|
218
244
|
|
|
219
|
-
def decorator(
|
|
220
|
-
self.add_tool(
|
|
221
|
-
return
|
|
245
|
+
def decorator(fn: Callable) -> Callable:
|
|
246
|
+
self.add_tool(fn, name=name, description=description)
|
|
247
|
+
return fn
|
|
222
248
|
|
|
223
249
|
return decorator
|
|
224
250
|
|
|
@@ -234,16 +260,24 @@ class FastMCP:
|
|
|
234
260
|
self,
|
|
235
261
|
uri: str,
|
|
236
262
|
*,
|
|
237
|
-
name:
|
|
238
|
-
description:
|
|
239
|
-
mime_type:
|
|
263
|
+
name: str | None = None,
|
|
264
|
+
description: str | None = None,
|
|
265
|
+
mime_type: str | None = None,
|
|
240
266
|
) -> Callable:
|
|
241
267
|
"""Decorator to register a function as a resource.
|
|
242
268
|
|
|
243
269
|
The function will be called when the resource is read to generate its content.
|
|
270
|
+
The function can return:
|
|
271
|
+
- str for text content
|
|
272
|
+
- bytes for binary content
|
|
273
|
+
- other types will be converted to JSON
|
|
274
|
+
|
|
275
|
+
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
276
|
+
has parameters, it will be registered as a template resource.
|
|
244
277
|
|
|
245
278
|
Args:
|
|
246
|
-
uri: URI for the resource (e.g. "resource://my-resource")
|
|
279
|
+
uri: URI for the resource (e.g. "resource://my-resource" or "resource://{param}")
|
|
280
|
+
name: Optional name for the resource
|
|
247
281
|
description: Optional description of the resource
|
|
248
282
|
mime_type: Optional MIME type for the resource
|
|
249
283
|
|
|
@@ -251,6 +285,10 @@ class FastMCP:
|
|
|
251
285
|
@server.resource("resource://my-resource")
|
|
252
286
|
def get_data() -> str:
|
|
253
287
|
return "Hello, world!"
|
|
288
|
+
|
|
289
|
+
@server.resource("resource://{city}/weather")
|
|
290
|
+
def get_weather(city: str) -> str:
|
|
291
|
+
return f"Weather for {city}"
|
|
254
292
|
"""
|
|
255
293
|
# Check if user passed function directly instead of calling decorator
|
|
256
294
|
if callable(uri):
|
|
@@ -259,26 +297,110 @@ class FastMCP:
|
|
|
259
297
|
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
|
260
298
|
)
|
|
261
299
|
|
|
262
|
-
def decorator(
|
|
263
|
-
@functools.wraps(
|
|
264
|
-
def wrapper() -> Any:
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
300
|
+
def decorator(fn: Callable) -> Callable:
|
|
301
|
+
@functools.wraps(fn)
|
|
302
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
303
|
+
return fn(*args, **kwargs)
|
|
304
|
+
|
|
305
|
+
# Check if this should be a template
|
|
306
|
+
has_uri_params = "{" in uri and "}" in uri
|
|
307
|
+
has_func_params = bool(inspect.signature(fn).parameters)
|
|
308
|
+
|
|
309
|
+
if has_uri_params or has_func_params:
|
|
310
|
+
# Validate that URI params match function params
|
|
311
|
+
uri_params = set(re.findall(r"{(\w+)}", uri))
|
|
312
|
+
func_params = set(inspect.signature(fn).parameters.keys())
|
|
313
|
+
|
|
314
|
+
if uri_params != func_params:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"Mismatch between URI parameters {uri_params} "
|
|
317
|
+
f"and function parameters {func_params}"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Register as template
|
|
321
|
+
self._resource_manager.add_template(
|
|
322
|
+
wrapper,
|
|
323
|
+
uri_template=uri,
|
|
324
|
+
name=name,
|
|
325
|
+
description=description,
|
|
326
|
+
mime_type=mime_type or "text/plain",
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
# Register as regular resource
|
|
330
|
+
resource = FunctionResource(
|
|
331
|
+
uri=AnyUrl(uri),
|
|
332
|
+
name=name,
|
|
333
|
+
description=description,
|
|
334
|
+
mime_type=mime_type or "text/plain",
|
|
335
|
+
fn=wrapper,
|
|
336
|
+
)
|
|
337
|
+
self.add_resource(resource)
|
|
275
338
|
return wrapper
|
|
276
339
|
|
|
277
340
|
return decorator
|
|
278
341
|
|
|
342
|
+
def add_prompt(self, prompt: Prompt) -> None:
|
|
343
|
+
"""Add a prompt to the server.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
prompt: A Prompt instance to add
|
|
347
|
+
"""
|
|
348
|
+
self._prompt_manager.add_prompt(prompt)
|
|
349
|
+
|
|
350
|
+
def prompt(
|
|
351
|
+
self, name: str | None = None, description: str | None = None
|
|
352
|
+
) -> Callable:
|
|
353
|
+
"""Decorator to register a prompt.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
name: Optional name for the prompt (defaults to function name)
|
|
357
|
+
description: Optional description of what the prompt does
|
|
358
|
+
|
|
359
|
+
Example:
|
|
360
|
+
@server.prompt()
|
|
361
|
+
def analyze_table(table_name: str) -> list[Message]:
|
|
362
|
+
schema = read_table_schema(table_name)
|
|
363
|
+
return [
|
|
364
|
+
{
|
|
365
|
+
"role": "user",
|
|
366
|
+
"content": f"Analyze this schema:\n{schema}"
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
@server.prompt()
|
|
371
|
+
async def analyze_file(path: str) -> list[Message]:
|
|
372
|
+
content = await read_file(path)
|
|
373
|
+
return [
|
|
374
|
+
{
|
|
375
|
+
"role": "user",
|
|
376
|
+
"content": {
|
|
377
|
+
"type": "resource",
|
|
378
|
+
"resource": {
|
|
379
|
+
"uri": f"file://{path}",
|
|
380
|
+
"text": content
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
"""
|
|
386
|
+
# Check if user passed function directly instead of calling decorator
|
|
387
|
+
if callable(name):
|
|
388
|
+
raise TypeError(
|
|
389
|
+
"The @prompt decorator was used incorrectly. "
|
|
390
|
+
"Did you forget to call it? Use @prompt() instead of @prompt"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def decorator(func: Callable) -> Callable:
|
|
394
|
+
prompt = Prompt.from_function(func, name=name, description=description)
|
|
395
|
+
self.add_prompt(prompt)
|
|
396
|
+
return func
|
|
397
|
+
|
|
398
|
+
return decorator
|
|
399
|
+
|
|
279
400
|
async def run_stdio_async(self) -> None:
|
|
280
401
|
"""Run the server using stdio transport."""
|
|
281
402
|
async with stdio_server() as (read_stream, write_stream):
|
|
403
|
+
logger.info(f'Starting "{self.name}"...')
|
|
282
404
|
await self._mcp_server.run(
|
|
283
405
|
read_stream,
|
|
284
406
|
write_stream,
|
|
@@ -289,7 +411,6 @@ class FastMCP:
|
|
|
289
411
|
"""Run the server using SSE transport."""
|
|
290
412
|
from starlette.applications import Starlette
|
|
291
413
|
from starlette.routing import Route
|
|
292
|
-
import uvicorn
|
|
293
414
|
|
|
294
415
|
sse = SseServerTransport("/messages")
|
|
295
416
|
|
|
@@ -314,9 +435,222 @@ class FastMCP:
|
|
|
314
435
|
],
|
|
315
436
|
)
|
|
316
437
|
|
|
317
|
-
uvicorn.
|
|
438
|
+
config = uvicorn.Config(
|
|
318
439
|
starlette_app,
|
|
319
440
|
host=self.settings.host,
|
|
320
441
|
port=self.settings.port,
|
|
321
|
-
log_level=self.settings.log_level,
|
|
442
|
+
log_level=self.settings.log_level.lower(),
|
|
443
|
+
)
|
|
444
|
+
server = uvicorn.Server(config)
|
|
445
|
+
await server.serve()
|
|
446
|
+
|
|
447
|
+
async def list_prompts(self) -> list[MCPPrompt]:
|
|
448
|
+
"""List all available prompts."""
|
|
449
|
+
prompts = self._prompt_manager.list_prompts()
|
|
450
|
+
return [
|
|
451
|
+
MCPPrompt(
|
|
452
|
+
name=prompt.name,
|
|
453
|
+
description=prompt.description,
|
|
454
|
+
arguments=[
|
|
455
|
+
{
|
|
456
|
+
"name": arg.name,
|
|
457
|
+
"description": arg.description,
|
|
458
|
+
"required": arg.required,
|
|
459
|
+
}
|
|
460
|
+
for arg in (prompt.arguments or [])
|
|
461
|
+
],
|
|
462
|
+
)
|
|
463
|
+
for prompt in prompts
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
async def get_prompt(
|
|
467
|
+
self, name: str, arguments: Dict[str, Any] | None = None
|
|
468
|
+
) -> GetPromptResult:
|
|
469
|
+
"""Get a prompt by name with arguments."""
|
|
470
|
+
try:
|
|
471
|
+
messages = await self._prompt_manager.render_prompt(name, arguments)
|
|
472
|
+
|
|
473
|
+
return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
|
|
474
|
+
except Exception as e:
|
|
475
|
+
logger.error(f"Error getting prompt {name}: {e}")
|
|
476
|
+
raise ValueError(str(e))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _convert_to_content(
|
|
480
|
+
result: Any,
|
|
481
|
+
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
|
482
|
+
"""Convert a result to a sequence of content objects."""
|
|
483
|
+
if result is None:
|
|
484
|
+
return []
|
|
485
|
+
|
|
486
|
+
if isinstance(result, (TextContent, ImageContent, EmbeddedResource)):
|
|
487
|
+
return [result]
|
|
488
|
+
|
|
489
|
+
if isinstance(result, Image):
|
|
490
|
+
return [result.to_image_content()]
|
|
491
|
+
|
|
492
|
+
if isinstance(result, (list, tuple)):
|
|
493
|
+
return list(chain.from_iterable(_convert_to_content(item) for item in result))
|
|
494
|
+
|
|
495
|
+
if not isinstance(result, str):
|
|
496
|
+
try:
|
|
497
|
+
result = json.dumps(pydantic_core.to_jsonable_python(result))
|
|
498
|
+
except Exception:
|
|
499
|
+
result = str(result)
|
|
500
|
+
|
|
501
|
+
return [TextContent(type="text", text=result)]
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class Context(BaseModel):
|
|
505
|
+
"""Context object providing access to MCP capabilities.
|
|
506
|
+
|
|
507
|
+
This provides a cleaner interface to MCP's RequestContext functionality.
|
|
508
|
+
It gets injected into tool and resource functions that request it via type hints.
|
|
509
|
+
|
|
510
|
+
To use context in a tool function, add a parameter with the Context type annotation:
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
@server.tool()
|
|
514
|
+
def my_tool(x: int, ctx: Context) -> str:
|
|
515
|
+
# Log messages to the client
|
|
516
|
+
ctx.info(f"Processing {x}")
|
|
517
|
+
ctx.debug("Debug info")
|
|
518
|
+
ctx.warning("Warning message")
|
|
519
|
+
ctx.error("Error message")
|
|
520
|
+
|
|
521
|
+
# Report progress
|
|
522
|
+
ctx.report_progress(50, 100)
|
|
523
|
+
|
|
524
|
+
# Access resources
|
|
525
|
+
data = ctx.read_resource("resource://data")
|
|
526
|
+
|
|
527
|
+
# Get request info
|
|
528
|
+
request_id = ctx.request_id
|
|
529
|
+
client_id = ctx.client_id
|
|
530
|
+
|
|
531
|
+
return str(x)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
The context parameter name can be anything as long as it's annotated with Context.
|
|
535
|
+
The context is optional - tools that don't need it can omit the parameter.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
_request_context: RequestContext | None
|
|
539
|
+
_fastmcp: FastMCP | None
|
|
540
|
+
|
|
541
|
+
def __init__(
|
|
542
|
+
self,
|
|
543
|
+
*,
|
|
544
|
+
request_context: RequestContext | None = None,
|
|
545
|
+
fastmcp: FastMCP | None = None,
|
|
546
|
+
**kwargs: Any,
|
|
547
|
+
):
|
|
548
|
+
super().__init__(**kwargs)
|
|
549
|
+
self._request_context = request_context
|
|
550
|
+
self._fastmcp = fastmcp
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def fastmcp(self) -> FastMCP:
|
|
554
|
+
"""Access to the FastMCP server."""
|
|
555
|
+
if self._fastmcp is None:
|
|
556
|
+
raise ValueError("Context is not available outside of a request")
|
|
557
|
+
return self._fastmcp
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def request_context(self) -> RequestContext:
|
|
561
|
+
"""Access to the underlying request context."""
|
|
562
|
+
if self._request_context is None:
|
|
563
|
+
raise ValueError("Context is not available outside of a request")
|
|
564
|
+
return self._request_context
|
|
565
|
+
|
|
566
|
+
async def report_progress(
|
|
567
|
+
self, progress: float, total: float | None = None
|
|
568
|
+
) -> None:
|
|
569
|
+
"""Report progress for the current operation.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
progress: Current progress value e.g. 24
|
|
573
|
+
total: Optional total value e.g. 100
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
progress_token = (
|
|
577
|
+
self.request_context.meta.progressToken
|
|
578
|
+
if self.request_context.meta
|
|
579
|
+
else None
|
|
322
580
|
)
|
|
581
|
+
|
|
582
|
+
if not progress_token:
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
await self.request_context.session.send_progress_notification(
|
|
586
|
+
progress_token=progress_token, progress=progress, total=total
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
async def read_resource(self, uri: str | AnyUrl) -> str | bytes:
|
|
590
|
+
"""Read a resource by URI.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
uri: Resource URI to read
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
The resource content as either text or bytes
|
|
597
|
+
"""
|
|
598
|
+
assert (
|
|
599
|
+
self._fastmcp is not None
|
|
600
|
+
), "Context is not available outside of a request"
|
|
601
|
+
return await self._fastmcp.read_resource(uri)
|
|
602
|
+
|
|
603
|
+
def log(
|
|
604
|
+
self,
|
|
605
|
+
level: Literal["debug", "info", "warning", "error"],
|
|
606
|
+
message: str,
|
|
607
|
+
*,
|
|
608
|
+
logger_name: str | None = None,
|
|
609
|
+
) -> None:
|
|
610
|
+
"""Send a log message to the client.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
level: Log level (debug, info, warning, error)
|
|
614
|
+
message: Log message
|
|
615
|
+
logger_name: Optional logger name
|
|
616
|
+
**extra: Additional structured data to include
|
|
617
|
+
"""
|
|
618
|
+
self.request_context.session.send_log_message(
|
|
619
|
+
level=level, data=message, logger=logger_name
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
@property
|
|
623
|
+
def client_id(self) -> str | None:
|
|
624
|
+
"""Get the client ID if available."""
|
|
625
|
+
return (
|
|
626
|
+
getattr(self.request_context.meta, "client_id", None)
|
|
627
|
+
if self.request_context.meta
|
|
628
|
+
else None
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def request_id(self) -> str:
|
|
633
|
+
"""Get the unique ID for this request."""
|
|
634
|
+
return str(self.request_context.request_id)
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def session(self):
|
|
638
|
+
"""Access to the underlying session for advanced usage."""
|
|
639
|
+
return self.request_context.session
|
|
640
|
+
|
|
641
|
+
# Convenience methods for common log levels
|
|
642
|
+
def debug(self, message: str, **extra: Any) -> None:
|
|
643
|
+
"""Send a debug log message."""
|
|
644
|
+
self.log("debug", message, **extra)
|
|
645
|
+
|
|
646
|
+
def info(self, message: str, **extra: Any) -> None:
|
|
647
|
+
"""Send an info log message."""
|
|
648
|
+
self.log("info", message, **extra)
|
|
649
|
+
|
|
650
|
+
def warning(self, message: str, **extra: Any) -> None:
|
|
651
|
+
"""Send a warning log message."""
|
|
652
|
+
self.log("warning", message, **extra)
|
|
653
|
+
|
|
654
|
+
def error(self, message: str, **extra: Any) -> None:
|
|
655
|
+
"""Send an error log message."""
|
|
656
|
+
self.log("error", message, **extra)
|