fastmcp 2.1.0__py3-none-any.whl → 2.1.2__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/cli/cli.py +2 -0
- fastmcp/client/transports.py +22 -0
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/prompt.py +6 -24
- fastmcp/prompts/prompt_manager.py +5 -2
- fastmcp/resources/__init__.py +1 -1
- fastmcp/resources/resource.py +1 -9
- fastmcp/resources/resource_manager.py +94 -9
- fastmcp/resources/template.py +0 -8
- fastmcp/server/context.py +1 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +156 -73
- fastmcp/tools/tool.py +4 -9
- fastmcp/tools/tool_manager.py +4 -1
- fastmcp/utilities/decorators.py +101 -0
- fastmcp/utilities/func_metadata.py +4 -1
- fastmcp/utilities/logging.py +14 -6
- fastmcp/utilities/openapi.py +650 -358
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/METADATA +86 -54
- fastmcp-2.1.2.dist-info/RECORD +40 -0
- fastmcp-2.1.0.dist-info/RECORD +0 -39
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
2
|
|
|
3
|
-
import inspect
|
|
4
3
|
import json
|
|
5
|
-
import
|
|
6
|
-
from collections.abc import AsyncIterator, Callable
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
7
5
|
from contextlib import (
|
|
8
6
|
AbstractAsyncContextManager,
|
|
9
7
|
AsyncExitStack,
|
|
@@ -43,8 +41,12 @@ import fastmcp
|
|
|
43
41
|
import fastmcp.settings
|
|
44
42
|
from fastmcp.exceptions import ResourceError
|
|
45
43
|
from fastmcp.prompts import Prompt, PromptManager
|
|
46
|
-
from fastmcp.
|
|
44
|
+
from fastmcp.prompts.prompt import Message, PromptResult
|
|
45
|
+
from fastmcp.resources import Resource, ResourceManager
|
|
46
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
47
47
|
from fastmcp.tools import ToolManager
|
|
48
|
+
from fastmcp.tools.tool import Tool
|
|
49
|
+
from fastmcp.utilities.decorators import DecoratedFunction
|
|
48
50
|
from fastmcp.utilities.logging import configure_logging, get_logger
|
|
49
51
|
from fastmcp.utilities.types import Image
|
|
50
52
|
|
|
@@ -166,22 +168,32 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
166
168
|
Args:
|
|
167
169
|
transport: Transport protocol to use ("stdio" or "sse")
|
|
168
170
|
"""
|
|
171
|
+
logger.info(f'Starting server "{self.name}"...')
|
|
169
172
|
anyio.run(self.run_async, transport)
|
|
170
173
|
|
|
171
174
|
def _setup_handlers(self) -> None:
|
|
172
175
|
"""Set up core MCP protocol handlers."""
|
|
173
|
-
self._mcp_server.list_tools()(self.
|
|
176
|
+
self._mcp_server.list_tools()(self._mcp_list_tools)
|
|
174
177
|
self._mcp_server.call_tool()(self.call_tool)
|
|
175
|
-
self._mcp_server.list_resources()(self.
|
|
176
|
-
self._mcp_server.read_resource()(self.
|
|
177
|
-
self._mcp_server.list_prompts()(self.
|
|
178
|
-
self._mcp_server.get_prompt()(self.
|
|
179
|
-
self._mcp_server.list_resource_templates()(self.
|
|
178
|
+
self._mcp_server.list_resources()(self._mcp_list_resources)
|
|
179
|
+
self._mcp_server.read_resource()(self._mcp_read_resource)
|
|
180
|
+
self._mcp_server.list_prompts()(self._mcp_list_prompts)
|
|
181
|
+
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
182
|
+
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
def list_tools(self) -> list[Tool]:
|
|
185
|
+
return self._tool_manager.list_tools()
|
|
186
|
+
|
|
187
|
+
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
188
|
+
"""
|
|
189
|
+
List all available tools, in the format expected by the low-level MCP
|
|
190
|
+
server.
|
|
191
|
+
|
|
192
|
+
See `list_tools` for a more ergonomic way to list tools.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
tools = self.list_tools()
|
|
183
196
|
|
|
184
|
-
tools = self._tool_manager.list_tools()
|
|
185
197
|
return [
|
|
186
198
|
MCPTool(
|
|
187
199
|
name=info.name,
|
|
@@ -214,10 +226,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
214
226
|
converted_result = _convert_to_content(result)
|
|
215
227
|
return converted_result
|
|
216
228
|
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
def list_resources(self) -> list[Resource]:
|
|
230
|
+
return self._resource_manager.list_resources()
|
|
231
|
+
|
|
232
|
+
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
233
|
+
"""
|
|
234
|
+
List all available resources, in the format expected by the low-level MCP
|
|
235
|
+
server.
|
|
236
|
+
|
|
237
|
+
See `list_resources` for a more ergonomic way to list resources.
|
|
238
|
+
"""
|
|
219
239
|
|
|
220
|
-
resources = self.
|
|
240
|
+
resources = self.list_resources()
|
|
221
241
|
return [
|
|
222
242
|
MCPResource(
|
|
223
243
|
uri=resource.uri,
|
|
@@ -228,8 +248,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
228
248
|
for resource in resources
|
|
229
249
|
]
|
|
230
250
|
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
def list_resource_templates(self) -> list[ResourceTemplate]:
|
|
252
|
+
return self._resource_manager.list_templates()
|
|
253
|
+
|
|
254
|
+
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
255
|
+
"""
|
|
256
|
+
List all available resource templates, in the format expected by the low-level
|
|
257
|
+
MCP server.
|
|
258
|
+
|
|
259
|
+
See `list_resource_templates` for a more ergonomic way to list resource
|
|
260
|
+
templates.
|
|
261
|
+
"""
|
|
262
|
+
templates = self.list_resource_templates()
|
|
233
263
|
return [
|
|
234
264
|
MCPResourceTemplate(
|
|
235
265
|
uriTemplate=template.uri_template,
|
|
@@ -239,15 +269,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
239
269
|
for template in templates
|
|
240
270
|
]
|
|
241
271
|
|
|
242
|
-
async def read_resource(self, uri: AnyUrl | str) ->
|
|
272
|
+
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
|
|
243
273
|
"""Read a resource by URI."""
|
|
274
|
+
resource = await self._resource_manager.get_resource(uri)
|
|
275
|
+
if not resource:
|
|
276
|
+
raise ResourceError(f"Unknown resource: {uri}")
|
|
277
|
+
return await resource.read()
|
|
278
|
+
|
|
279
|
+
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
280
|
+
"""
|
|
281
|
+
Read a resource by URI, in the format expected by the low-level MCP
|
|
282
|
+
server.
|
|
283
|
+
|
|
284
|
+
See `read_resource` for a more ergonomic way to read resources.
|
|
285
|
+
"""
|
|
244
286
|
|
|
245
287
|
resource = await self._resource_manager.get_resource(uri)
|
|
246
288
|
if not resource:
|
|
247
289
|
raise ResourceError(f"Unknown resource: {uri}")
|
|
248
290
|
|
|
249
291
|
try:
|
|
250
|
-
content = await
|
|
292
|
+
content = await self.read_resource(uri)
|
|
251
293
|
return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
|
|
252
294
|
except Exception as e:
|
|
253
295
|
logger.error(f"Error reading resource {uri}: {e}")
|
|
@@ -307,6 +349,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
307
349
|
await context.report_progress(50, 100)
|
|
308
350
|
return str(x)
|
|
309
351
|
"""
|
|
352
|
+
|
|
310
353
|
# Check if user passed function directly instead of calling decorator
|
|
311
354
|
if callable(name):
|
|
312
355
|
raise TypeError(
|
|
@@ -316,7 +359,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
316
359
|
|
|
317
360
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
318
361
|
self.add_tool(fn, name=name, description=description, tags=tags)
|
|
319
|
-
return fn
|
|
362
|
+
return DecoratedFunction(fn)
|
|
320
363
|
|
|
321
364
|
return decorator
|
|
322
365
|
|
|
@@ -326,8 +369,40 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
326
369
|
Args:
|
|
327
370
|
resource: A Resource instance to add
|
|
328
371
|
"""
|
|
372
|
+
|
|
329
373
|
self._resource_manager.add_resource(resource)
|
|
330
374
|
|
|
375
|
+
def add_resource_fn(
|
|
376
|
+
self,
|
|
377
|
+
fn: AnyFunction,
|
|
378
|
+
uri: str,
|
|
379
|
+
name: str | None = None,
|
|
380
|
+
description: str | None = None,
|
|
381
|
+
mime_type: str | None = None,
|
|
382
|
+
tags: set[str] | None = None,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Add a resource or template to the server from a function.
|
|
385
|
+
|
|
386
|
+
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
387
|
+
has parameters, it will be registered as a template resource.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
fn: The function to register as a resource
|
|
391
|
+
uri: The URI for the resource
|
|
392
|
+
name: Optional name for the resource
|
|
393
|
+
description: Optional description of the resource
|
|
394
|
+
mime_type: Optional MIME type for the resource
|
|
395
|
+
tags: Optional set of tags for categorizing the resource
|
|
396
|
+
"""
|
|
397
|
+
self._resource_manager.add_resource_or_template_from_fn(
|
|
398
|
+
fn=fn,
|
|
399
|
+
uri=uri,
|
|
400
|
+
name=name,
|
|
401
|
+
description=description,
|
|
402
|
+
mime_type=mime_type,
|
|
403
|
+
tags=tags,
|
|
404
|
+
)
|
|
405
|
+
|
|
331
406
|
def resource(
|
|
332
407
|
self,
|
|
333
408
|
uri: str,
|
|
@@ -382,52 +457,36 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
382
457
|
)
|
|
383
458
|
|
|
384
459
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if uri_params != func_params:
|
|
395
|
-
raise ValueError(
|
|
396
|
-
f"Mismatch between URI parameters {uri_params} "
|
|
397
|
-
f"and function parameters {func_params}"
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
# Register as template
|
|
401
|
-
self._resource_manager.add_template_from_fn(
|
|
402
|
-
fn=fn,
|
|
403
|
-
uri_template=uri,
|
|
404
|
-
name=name,
|
|
405
|
-
description=description,
|
|
406
|
-
mime_type=mime_type or "text/plain",
|
|
407
|
-
tags=tags,
|
|
408
|
-
)
|
|
409
|
-
else:
|
|
410
|
-
# Register as regular resource
|
|
411
|
-
resource = FunctionResource(
|
|
412
|
-
uri=AnyUrl(uri),
|
|
413
|
-
name=name,
|
|
414
|
-
description=description,
|
|
415
|
-
mime_type=mime_type or "text/plain",
|
|
416
|
-
fn=fn,
|
|
417
|
-
tags=tags or set(), # Default to empty set if None
|
|
418
|
-
)
|
|
419
|
-
self.add_resource(resource)
|
|
420
|
-
return fn
|
|
460
|
+
self._resource_manager.add_resource_or_template_from_fn(
|
|
461
|
+
fn=fn,
|
|
462
|
+
uri=uri,
|
|
463
|
+
name=name,
|
|
464
|
+
description=description,
|
|
465
|
+
mime_type=mime_type,
|
|
466
|
+
tags=tags,
|
|
467
|
+
)
|
|
468
|
+
return DecoratedFunction(fn)
|
|
421
469
|
|
|
422
470
|
return decorator
|
|
423
471
|
|
|
424
|
-
def add_prompt(
|
|
472
|
+
def add_prompt(
|
|
473
|
+
self,
|
|
474
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
475
|
+
name: str | None = None,
|
|
476
|
+
description: str | None = None,
|
|
477
|
+
tags: set[str] | None = None,
|
|
478
|
+
) -> None:
|
|
425
479
|
"""Add a prompt to the server.
|
|
426
480
|
|
|
427
481
|
Args:
|
|
428
482
|
prompt: A Prompt instance to add
|
|
429
483
|
"""
|
|
430
|
-
self._prompt_manager.
|
|
484
|
+
self._prompt_manager.add_prompt_from_fn(
|
|
485
|
+
fn=fn,
|
|
486
|
+
name=name,
|
|
487
|
+
description=description,
|
|
488
|
+
tags=tags,
|
|
489
|
+
)
|
|
431
490
|
|
|
432
491
|
def prompt(
|
|
433
492
|
self,
|
|
@@ -477,11 +536,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
477
536
|
)
|
|
478
537
|
|
|
479
538
|
def decorator(func: AnyFunction) -> AnyFunction:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
)
|
|
483
|
-
self.add_prompt(prompt)
|
|
484
|
-
return func
|
|
539
|
+
self.add_prompt(func, name=name, description=description, tags=tags)
|
|
540
|
+
return DecoratedFunction(func)
|
|
485
541
|
|
|
486
542
|
return decorator
|
|
487
543
|
|
|
@@ -494,15 +550,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
494
550
|
self._mcp_server.create_initialization_options(),
|
|
495
551
|
)
|
|
496
552
|
|
|
497
|
-
async def run_sse_async(
|
|
553
|
+
async def run_sse_async(
|
|
554
|
+
self,
|
|
555
|
+
host: str | None = None,
|
|
556
|
+
port: int | None = None,
|
|
557
|
+
log_level: str | None = None,
|
|
558
|
+
) -> None:
|
|
498
559
|
"""Run the server using SSE transport."""
|
|
499
560
|
starlette_app = self.sse_app()
|
|
500
561
|
|
|
501
562
|
config = uvicorn.Config(
|
|
502
563
|
starlette_app,
|
|
503
|
-
host=self.settings.host,
|
|
504
|
-
port=self.settings.port,
|
|
505
|
-
log_level=self.settings.log_level.lower(),
|
|
564
|
+
host=host or self.settings.host,
|
|
565
|
+
port=port or self.settings.port,
|
|
566
|
+
log_level=log_level or self.settings.log_level.lower(),
|
|
506
567
|
)
|
|
507
568
|
server = uvicorn.Server(config)
|
|
508
569
|
await server.serve()
|
|
@@ -531,9 +592,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
531
592
|
],
|
|
532
593
|
)
|
|
533
594
|
|
|
534
|
-
|
|
535
|
-
"""
|
|
536
|
-
|
|
595
|
+
def list_prompts(self) -> list[Prompt]:
|
|
596
|
+
"""
|
|
597
|
+
List all available prompts.
|
|
598
|
+
"""
|
|
599
|
+
return self._prompt_manager.list_prompts()
|
|
600
|
+
|
|
601
|
+
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
602
|
+
"""
|
|
603
|
+
List all available prompts, in the format expected by the low-level MCP
|
|
604
|
+
server.
|
|
605
|
+
|
|
606
|
+
See `list_prompts` for a more ergonomic way to list prompts.
|
|
607
|
+
"""
|
|
608
|
+
prompts = self.list_prompts()
|
|
537
609
|
return [
|
|
538
610
|
MCPPrompt(
|
|
539
611
|
name=prompt.name,
|
|
@@ -552,10 +624,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
552
624
|
|
|
553
625
|
async def get_prompt(
|
|
554
626
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
555
|
-
) ->
|
|
627
|
+
) -> list[Message]:
|
|
556
628
|
"""Get a prompt by name with arguments."""
|
|
629
|
+
return await self._prompt_manager.render_prompt(name, arguments)
|
|
630
|
+
|
|
631
|
+
async def _mcp_get_prompt(
|
|
632
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
633
|
+
) -> GetPromptResult:
|
|
634
|
+
"""
|
|
635
|
+
Get a prompt by name with arguments, in the format expected by the low-level
|
|
636
|
+
MCP server.
|
|
637
|
+
|
|
638
|
+
See `get_prompt` for a more ergonomic way to get prompts.
|
|
639
|
+
"""
|
|
557
640
|
try:
|
|
558
|
-
messages = await self.
|
|
641
|
+
messages = await self.get_prompt(name, arguments)
|
|
559
642
|
|
|
560
643
|
return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
|
|
561
644
|
except Exception as e:
|
fastmcp/tools/tool.py
CHANGED
|
@@ -5,7 +5,6 @@ from collections.abc import Callable
|
|
|
5
5
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, BeforeValidator, Field
|
|
8
|
-
from typing_extensions import Self
|
|
9
8
|
|
|
10
9
|
from fastmcp.exceptions import ToolError
|
|
11
10
|
from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
|
@@ -58,7 +57,10 @@ class Tool(BaseModel):
|
|
|
58
57
|
is_async = inspect.iscoroutinefunction(fn)
|
|
59
58
|
|
|
60
59
|
if context_kwarg is None:
|
|
61
|
-
|
|
60
|
+
if isinstance(fn, classmethod):
|
|
61
|
+
sig = inspect.signature(fn.__func__)
|
|
62
|
+
else:
|
|
63
|
+
sig = inspect.signature(fn)
|
|
62
64
|
for param_name, param in sig.parameters.items():
|
|
63
65
|
if param.annotation is Context:
|
|
64
66
|
context_kwarg = param_name
|
|
@@ -99,13 +101,6 @@ class Tool(BaseModel):
|
|
|
99
101
|
except Exception as e:
|
|
100
102
|
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
|
101
103
|
|
|
102
|
-
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
103
|
-
"""Copy the tool with optional updates."""
|
|
104
|
-
data = self.model_dump()
|
|
105
|
-
if updates:
|
|
106
|
-
data.update(updates)
|
|
107
|
-
return type(self)(**data)
|
|
108
|
-
|
|
109
104
|
def __eq__(self, other: object) -> bool:
|
|
110
105
|
if not isinstance(other, Tool):
|
|
111
106
|
return False
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
from typing import TYPE_CHECKING, Any
|
|
5
6
|
|
|
@@ -90,7 +91,9 @@ class ToolManager:
|
|
|
90
91
|
for name, tool in tool_manager._tools.items():
|
|
91
92
|
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
92
93
|
|
|
93
|
-
new_tool =
|
|
94
|
+
new_tool = copy.copy(tool)
|
|
95
|
+
new_tool.name = prefixed_name
|
|
96
|
+
|
|
94
97
|
# Store the copied tool
|
|
95
98
|
self.add_tool(new_tool)
|
|
96
99
|
logger.debug(f'Imported tool "{name}" as "{prefixed_name}"')
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Generic, ParamSpec, TypeVar, cast, overload
|
|
4
|
+
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
R = TypeVar("R")
|
|
8
|
+
P = ParamSpec("P")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DecoratedFunction(Generic[P, R]):
|
|
12
|
+
"""Descriptor for decorated functions.
|
|
13
|
+
|
|
14
|
+
You can return this object from a decorator to ensure that it works across
|
|
15
|
+
all types of functions: vanilla, instance methods, class methods, and static
|
|
16
|
+
methods; both synchronous and asynchronous.
|
|
17
|
+
|
|
18
|
+
This class is used to store the original function and metadata about how to
|
|
19
|
+
register it as a tool.
|
|
20
|
+
|
|
21
|
+
Example usage:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
def my_decorator(fn: Callable[P, R]) -> DecoratedFunction[P, R]:
|
|
25
|
+
return DecoratedFunction(fn)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
On a function:
|
|
29
|
+
```python
|
|
30
|
+
@my_decorator
|
|
31
|
+
def my_function(a: int, b: int) -> int:
|
|
32
|
+
return a + b
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
On an instance method:
|
|
36
|
+
```python
|
|
37
|
+
class Test:
|
|
38
|
+
@my_decorator
|
|
39
|
+
def my_function(self, a: int, b: int) -> int:
|
|
40
|
+
return a + b
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
On a class method:
|
|
44
|
+
```python
|
|
45
|
+
class Test:
|
|
46
|
+
@classmethod
|
|
47
|
+
@my_decorator
|
|
48
|
+
def my_function(cls, a: int, b: int) -> int:
|
|
49
|
+
return a + b
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Note that for classmethods, the decorator must be applied first, then
|
|
53
|
+
`@classmethod` on top.
|
|
54
|
+
|
|
55
|
+
On a static method:
|
|
56
|
+
```python
|
|
57
|
+
class Test:
|
|
58
|
+
@staticmethod
|
|
59
|
+
@my_decorator
|
|
60
|
+
def my_function(a: int, b: int) -> int:
|
|
61
|
+
return a + b
|
|
62
|
+
```
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, fn: Callable[P, R]):
|
|
66
|
+
self.fn = fn
|
|
67
|
+
|
|
68
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
69
|
+
"""Call the original function."""
|
|
70
|
+
try:
|
|
71
|
+
return self.fn(*args, **kwargs)
|
|
72
|
+
except TypeError as e:
|
|
73
|
+
if "'classmethod' object is not callable" in str(e):
|
|
74
|
+
raise TypeError(
|
|
75
|
+
"To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top."
|
|
76
|
+
)
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
@overload
|
|
80
|
+
def __get__(self, instance: None, owner: type | None = None) -> Self: ...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def __get__(
|
|
84
|
+
self, instance: object, owner: type | None = None
|
|
85
|
+
) -> Callable[P, R]: ...
|
|
86
|
+
|
|
87
|
+
def __get__(
|
|
88
|
+
self, instance: object | None, owner: type | None = None
|
|
89
|
+
) -> Self | Callable[P, R]:
|
|
90
|
+
"""Return the original function when accessed from an instance, or self when accessed from the class."""
|
|
91
|
+
if instance is None:
|
|
92
|
+
return self
|
|
93
|
+
# Return the original function bound to the instance
|
|
94
|
+
return cast(Callable[P, R], self.fn.__get__(instance, owner))
|
|
95
|
+
|
|
96
|
+
def __repr__(self) -> str:
|
|
97
|
+
"""Return a representation that matches Python's function representation."""
|
|
98
|
+
module = getattr(self.fn, "__module__", "unknown")
|
|
99
|
+
qualname = getattr(self.fn, "__qualname__", str(self.fn))
|
|
100
|
+
sig_str = str(inspect.signature(self.fn))
|
|
101
|
+
return f"<function {module}.{qualname}{sig_str}>"
|
|
@@ -125,7 +125,10 @@ def func_metadata(
|
|
|
125
125
|
Returns:
|
|
126
126
|
A pydantic model representing the function's signature.
|
|
127
127
|
"""
|
|
128
|
-
|
|
128
|
+
if isinstance(func, classmethod):
|
|
129
|
+
sig = _get_typed_signature(func.__func__)
|
|
130
|
+
else:
|
|
131
|
+
sig = _get_typed_signature(func)
|
|
129
132
|
params = sig.parameters
|
|
130
133
|
dynamic_pydantic_model_params: dict[str, Any] = {}
|
|
131
134
|
globalns = getattr(func, "__globals__", {})
|
fastmcp/utilities/logging.py
CHANGED
|
@@ -20,15 +20,23 @@ def get_logger(name: str) -> logging.Logger:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def configure_logging(
|
|
23
|
-
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
|
|
23
|
+
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
|
|
24
24
|
) -> None:
|
|
25
25
|
"""Configure logging for FastMCP.
|
|
26
26
|
|
|
27
27
|
Args:
|
|
28
28
|
level: the log level to use
|
|
29
29
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
# Only configure the FastMCP logger namespace
|
|
31
|
+
handler = RichHandler(console=Console(stderr=True), rich_tracebacks=True)
|
|
32
|
+
formatter = logging.Formatter("%(message)s")
|
|
33
|
+
handler.setFormatter(formatter)
|
|
34
|
+
|
|
35
|
+
fastmcp_logger = logging.getLogger("FastMCP")
|
|
36
|
+
fastmcp_logger.setLevel(level)
|
|
37
|
+
|
|
38
|
+
# Remove any existing handlers to avoid duplicates on reconfiguration
|
|
39
|
+
for hdlr in fastmcp_logger.handlers[:]:
|
|
40
|
+
fastmcp_logger.removeHandler(hdlr)
|
|
41
|
+
|
|
42
|
+
fastmcp_logger.addHandler(handler)
|