fastmcp 0.1.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 -1
- fastmcp/cli/__init__.py +3 -1
- fastmcp/cli/claude.py +42 -19
- fastmcp/cli/cli.py +87 -35
- 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 +455 -74
- 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.1.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/cli.py +0 -6
- fastmcp/resources.py +0 -219
- fastmcp/tools.py +0 -101
- fastmcp-0.1.0.dist-info/METADATA +0 -121
- fastmcp-0.1.0.dist-info/RECORD +0 -17
- fastmcp-0.1.0.dist-info/top_level.txt +0 -1
- {fastmcp-0.1.0.dist-info → fastmcp-0.3.0.dist-info}/entry_points.txt +0 -0
fastmcp/server.py
CHANGED
|
@@ -1,23 +1,47 @@
|
|
|
1
1
|
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import base64
|
|
5
4
|
import functools
|
|
5
|
+
import inspect
|
|
6
6
|
import json
|
|
7
|
-
|
|
7
|
+
import re
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from typing import Any, Callable, Dict, Literal, Sequence
|
|
8
10
|
|
|
11
|
+
import pydantic_core
|
|
12
|
+
import uvicorn
|
|
9
13
|
from mcp.server import Server as MCPServer
|
|
10
|
-
from mcp.server.stdio import stdio_server
|
|
11
14
|
from mcp.server.sse import SseServerTransport
|
|
12
|
-
from mcp.
|
|
13
|
-
from mcp.
|
|
15
|
+
from mcp.server.stdio import stdio_server
|
|
16
|
+
from mcp.shared.context import RequestContext
|
|
17
|
+
from mcp.types import (
|
|
18
|
+
EmbeddedResource,
|
|
19
|
+
GetPromptResult,
|
|
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,
|
|
34
|
+
)
|
|
14
35
|
from pydantic import BaseModel
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
from .
|
|
36
|
+
from pydantic.networks import AnyUrl
|
|
37
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
38
|
+
|
|
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
|
|
21
45
|
|
|
22
46
|
logger = get_logger(__name__)
|
|
23
47
|
|
|
@@ -29,7 +53,7 @@ class Settings(BaseSettings):
|
|
|
29
53
|
For example, FASTMCP_DEBUG=true will set debug=True.
|
|
30
54
|
"""
|
|
31
55
|
|
|
32
|
-
model_config:
|
|
56
|
+
model_config: SettingsConfigDict = SettingsConfigDict(env_prefix="FASTMCP_")
|
|
33
57
|
|
|
34
58
|
# Server settings
|
|
35
59
|
debug: bool = False
|
|
@@ -45,9 +69,12 @@ class Settings(BaseSettings):
|
|
|
45
69
|
# tool settings
|
|
46
70
|
warn_on_duplicate_tools: bool = True
|
|
47
71
|
|
|
72
|
+
# prompt settings
|
|
73
|
+
warn_on_duplicate_prompts: bool = True
|
|
74
|
+
|
|
48
75
|
|
|
49
76
|
class FastMCP:
|
|
50
|
-
def __init__(self, name=None, **settings:
|
|
77
|
+
def __init__(self, name: str | None = None, **settings: Any):
|
|
51
78
|
self.settings = Settings(**settings)
|
|
52
79
|
self._mcp_server = MCPServer(name=name or "FastMCP")
|
|
53
80
|
self._tool_manager = ToolManager(
|
|
@@ -56,6 +83,9 @@ class FastMCP:
|
|
|
56
83
|
self._resource_manager = ResourceManager(
|
|
57
84
|
warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
|
|
58
85
|
)
|
|
86
|
+
self._prompt_manager = PromptManager(
|
|
87
|
+
warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
|
|
88
|
+
)
|
|
59
89
|
|
|
60
90
|
# Set up MCP protocol handlers
|
|
61
91
|
self._setup_handlers()
|
|
@@ -73,12 +103,14 @@ class FastMCP:
|
|
|
73
103
|
Args:
|
|
74
104
|
transport: Transport protocol to use ("stdio" or "sse")
|
|
75
105
|
"""
|
|
106
|
+
TRANSPORTS = Literal["stdio", "sse"]
|
|
107
|
+
if transport not in TRANSPORTS.__args__: # type: ignore
|
|
108
|
+
raise ValueError(f"Unknown transport: {transport}")
|
|
109
|
+
|
|
76
110
|
if transport == "stdio":
|
|
77
111
|
asyncio.run(self.run_stdio_async())
|
|
78
|
-
|
|
112
|
+
else: # transport == "sse"
|
|
79
113
|
asyncio.run(self.run_sse_async())
|
|
80
|
-
else:
|
|
81
|
-
raise ValueError(f"Unknown transport: {transport}")
|
|
82
114
|
|
|
83
115
|
def _setup_handlers(self) -> None:
|
|
84
116
|
"""Set up core MCP protocol handlers."""
|
|
@@ -86,12 +118,16 @@ class FastMCP:
|
|
|
86
118
|
self._mcp_server.call_tool()(self.call_tool)
|
|
87
119
|
self._mcp_server.list_resources()(self.list_resources)
|
|
88
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)
|
|
89
125
|
|
|
90
|
-
async def list_tools(self) -> list[
|
|
126
|
+
async def list_tools(self) -> list[MCPTool]:
|
|
91
127
|
"""List all available tools."""
|
|
92
128
|
tools = self._tool_manager.list_tools()
|
|
93
129
|
return [
|
|
94
|
-
|
|
130
|
+
MCPTool(
|
|
95
131
|
name=info.name,
|
|
96
132
|
description=info.description,
|
|
97
133
|
inputSchema=info.parameters,
|
|
@@ -99,29 +135,54 @@ class FastMCP:
|
|
|
99
135
|
for info in tools
|
|
100
136
|
]
|
|
101
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
|
+
|
|
102
149
|
async def call_tool(
|
|
103
150
|
self, name: str, arguments: dict
|
|
104
|
-
) -> Sequence[
|
|
151
|
+
) -> Sequence[TextContent | ImageContent]:
|
|
105
152
|
"""Call a tool by name with arguments."""
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
108
157
|
|
|
109
158
|
async def list_resources(self) -> list[MCPResource]:
|
|
110
159
|
"""List all available resources."""
|
|
160
|
+
|
|
111
161
|
resources = self._resource_manager.list_resources()
|
|
112
162
|
return [
|
|
113
163
|
MCPResource(
|
|
114
164
|
uri=resource.uri,
|
|
115
|
-
name=resource.name,
|
|
165
|
+
name=resource.name or "",
|
|
116
166
|
description=resource.description,
|
|
117
167
|
mimeType=resource.mime_type,
|
|
118
168
|
)
|
|
119
169
|
for resource in resources
|
|
120
170
|
]
|
|
121
171
|
|
|
122
|
-
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:
|
|
123
184
|
"""Read a resource by URI."""
|
|
124
|
-
resource = self._resource_manager.get_resource(uri)
|
|
185
|
+
resource = await self._resource_manager.get_resource(uri)
|
|
125
186
|
if not resource:
|
|
126
187
|
raise ResourceError(f"Unknown resource: {uri}")
|
|
127
188
|
|
|
@@ -131,37 +192,49 @@ class FastMCP:
|
|
|
131
192
|
logger.error(f"Error reading resource {uri}: {e}")
|
|
132
193
|
raise ResourceError(str(e))
|
|
133
194
|
|
|
134
|
-
def _convert_to_content(
|
|
135
|
-
self, value: Any
|
|
136
|
-
) -> Union[TextContent, ImageContent, EmbeddedResource]:
|
|
137
|
-
"""Convert Python values to MCP content types."""
|
|
138
|
-
if isinstance(value, (dict, list)):
|
|
139
|
-
return TextContent(type="text", text=json.dumps(value, indent=2))
|
|
140
|
-
if isinstance(value, str):
|
|
141
|
-
return TextContent(type="text", text=value)
|
|
142
|
-
if isinstance(value, bytes):
|
|
143
|
-
return ImageContent(
|
|
144
|
-
type="image",
|
|
145
|
-
data=base64.b64encode(value).decode(),
|
|
146
|
-
mimeType="application/octet-stream",
|
|
147
|
-
)
|
|
148
|
-
if isinstance(value, BaseModel):
|
|
149
|
-
return TextContent(type="text", text=value.model_dump_json(indent=2))
|
|
150
|
-
return TextContent(type="text", text=str(value))
|
|
151
|
-
|
|
152
195
|
def add_tool(
|
|
153
196
|
self,
|
|
154
|
-
|
|
155
|
-
name:
|
|
156
|
-
description:
|
|
197
|
+
fn: Callable,
|
|
198
|
+
name: str | None = None,
|
|
199
|
+
description: str | None = None,
|
|
157
200
|
) -> None:
|
|
158
|
-
"""Add a tool to the server.
|
|
159
|
-
self._tool_manager.add_tool(func, name=name, description=description)
|
|
201
|
+
"""Add a tool to the server.
|
|
160
202
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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.
|
|
205
|
+
|
|
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)
|
|
212
|
+
|
|
213
|
+
def tool(self, name: str | None = None, description: str | None = None) -> Callable:
|
|
214
|
+
"""Decorator to register a tool.
|
|
215
|
+
|
|
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.
|
|
218
|
+
|
|
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
|
+
"""
|
|
165
238
|
# Check if user passed function directly instead of calling decorator
|
|
166
239
|
if callable(name):
|
|
167
240
|
raise TypeError(
|
|
@@ -169,9 +242,9 @@ class FastMCP:
|
|
|
169
242
|
"Did you forget to call it? Use @tool() instead of @tool"
|
|
170
243
|
)
|
|
171
244
|
|
|
172
|
-
def decorator(
|
|
173
|
-
self.add_tool(
|
|
174
|
-
return
|
|
245
|
+
def decorator(fn: Callable) -> Callable:
|
|
246
|
+
self.add_tool(fn, name=name, description=description)
|
|
247
|
+
return fn
|
|
175
248
|
|
|
176
249
|
return decorator
|
|
177
250
|
|
|
@@ -187,16 +260,24 @@ class FastMCP:
|
|
|
187
260
|
self,
|
|
188
261
|
uri: str,
|
|
189
262
|
*,
|
|
190
|
-
name:
|
|
191
|
-
description:
|
|
192
|
-
mime_type:
|
|
263
|
+
name: str | None = None,
|
|
264
|
+
description: str | None = None,
|
|
265
|
+
mime_type: str | None = None,
|
|
193
266
|
) -> Callable:
|
|
194
267
|
"""Decorator to register a function as a resource.
|
|
195
268
|
|
|
196
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.
|
|
197
277
|
|
|
198
278
|
Args:
|
|
199
|
-
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
|
|
200
281
|
description: Optional description of the resource
|
|
201
282
|
mime_type: Optional MIME type for the resource
|
|
202
283
|
|
|
@@ -204,6 +285,10 @@ class FastMCP:
|
|
|
204
285
|
@server.resource("resource://my-resource")
|
|
205
286
|
def get_data() -> str:
|
|
206
287
|
return "Hello, world!"
|
|
288
|
+
|
|
289
|
+
@server.resource("resource://{city}/weather")
|
|
290
|
+
def get_weather(city: str) -> str:
|
|
291
|
+
return f"Weather for {city}"
|
|
207
292
|
"""
|
|
208
293
|
# Check if user passed function directly instead of calling decorator
|
|
209
294
|
if callable(uri):
|
|
@@ -212,26 +297,110 @@ class FastMCP:
|
|
|
212
297
|
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
|
213
298
|
)
|
|
214
299
|
|
|
215
|
-
def decorator(
|
|
216
|
-
@functools.wraps(
|
|
217
|
-
def wrapper() -> Any:
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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)
|
|
228
338
|
return wrapper
|
|
229
339
|
|
|
230
340
|
return decorator
|
|
231
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
|
+
|
|
232
400
|
async def run_stdio_async(self) -> None:
|
|
233
401
|
"""Run the server using stdio transport."""
|
|
234
402
|
async with stdio_server() as (read_stream, write_stream):
|
|
403
|
+
logger.info(f'Starting "{self.name}"...')
|
|
235
404
|
await self._mcp_server.run(
|
|
236
405
|
read_stream,
|
|
237
406
|
write_stream,
|
|
@@ -242,7 +411,6 @@ class FastMCP:
|
|
|
242
411
|
"""Run the server using SSE transport."""
|
|
243
412
|
from starlette.applications import Starlette
|
|
244
413
|
from starlette.routing import Route
|
|
245
|
-
import uvicorn
|
|
246
414
|
|
|
247
415
|
sse = SseServerTransport("/messages")
|
|
248
416
|
|
|
@@ -267,9 +435,222 @@ class FastMCP:
|
|
|
267
435
|
],
|
|
268
436
|
)
|
|
269
437
|
|
|
270
|
-
uvicorn.
|
|
438
|
+
config = uvicorn.Config(
|
|
271
439
|
starlette_app,
|
|
272
440
|
host=self.settings.host,
|
|
273
441
|
port=self.settings.port,
|
|
274
|
-
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
|
|
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
|
|
275
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)
|