fastmcp 2.10.6__py3-none-any.whl → 2.11.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/cli/cli.py +128 -33
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/mcp_json.py +41 -0
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +17 -2
- fastmcp/server/auth/auth.py +144 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +170 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +107 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +62 -30
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +1 -1
- fastmcp/server/proxy.py +50 -11
- fastmcp/server/server.py +168 -59
- fastmcp/settings.py +73 -6
- fastmcp/tools/tool.py +36 -3
- fastmcp/tools/tool_manager.py +38 -2
- fastmcp/tools/tool_transform.py +112 -3
- fastmcp/utilities/components.py +35 -2
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +240 -50
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +89 -11
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
- fastmcp-2.11.0.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.6.dist-info/RECORD +0 -93
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/utilities/json_schema.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import copy
|
|
4
3
|
from collections import defaultdict
|
|
5
4
|
|
|
6
5
|
|
|
@@ -25,116 +24,159 @@ def _prune_param(schema: dict, param: str) -> dict:
|
|
|
25
24
|
return schema
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
def
|
|
29
|
-
|
|
27
|
+
def _single_pass_optimize(
|
|
28
|
+
schema: dict,
|
|
29
|
+
prune_titles: bool = False,
|
|
30
|
+
prune_additional_properties: bool = False,
|
|
31
|
+
prune_defs: bool = True,
|
|
32
|
+
) -> dict:
|
|
33
|
+
"""
|
|
34
|
+
Optimize JSON schemas in a single traversal for better performance.
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
This function combines three schema cleanup operations that would normally require
|
|
37
|
+
separate tree traversals:
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return schema
|
|
39
|
+
1. **Remove unused definitions** (prune_defs): Finds and removes `$defs` entries
|
|
40
|
+
that aren't referenced anywhere in the schema, reducing schema size.
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
) -> None:
|
|
41
|
-
if isinstance(node, dict):
|
|
42
|
-
# Process $ref for definition tracking
|
|
43
|
-
ref = node.get("$ref")
|
|
44
|
-
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
|
45
|
-
def_name = ref.split("/")[-1]
|
|
46
|
-
if current_def:
|
|
47
|
-
referenced_by[def_name].append(current_def)
|
|
48
|
-
else:
|
|
49
|
-
root_defs.add(def_name)
|
|
42
|
+
2. **Remove titles** (prune_titles): Strips `title` fields throughout the schema
|
|
43
|
+
to reduce verbosity while preserving functional information.
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if skip_defs and k == "$defs":
|
|
54
|
-
continue
|
|
45
|
+
3. **Remove restrictive additionalProperties** (prune_additional_properties):
|
|
46
|
+
Removes `"additionalProperties": false` constraints to make schemas more flexible.
|
|
55
47
|
|
|
56
|
-
|
|
48
|
+
**Performance Benefits:**
|
|
49
|
+
- Single tree traversal instead of multiple passes (2-3x faster)
|
|
50
|
+
- Immutable design prevents shared reference bugs
|
|
51
|
+
- Early termination prevents runaway recursion on deeply nested schemas
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# Traverse the schema once, skipping the $defs
|
|
63
|
-
walk(schema, skip_defs=True)
|
|
64
|
-
|
|
65
|
-
# Now figure out what defs reference other defs
|
|
66
|
-
for def_name, value in defs.items():
|
|
67
|
-
walk(value, current_def=def_name)
|
|
68
|
-
|
|
69
|
-
# Figure out what defs were referenced directly or recursively
|
|
70
|
-
def def_is_referenced(def_name, parent_def_names: set[str] | None = None):
|
|
71
|
-
if def_name in root_defs:
|
|
72
|
-
return True
|
|
73
|
-
references = referenced_by.get(def_name)
|
|
74
|
-
if references:
|
|
75
|
-
if parent_def_names is None:
|
|
76
|
-
parent_def_names = set()
|
|
77
|
-
|
|
78
|
-
# Handle recursion by excluding references already present in parent references
|
|
79
|
-
parent_def_names = parent_def_names | {def_name}
|
|
80
|
-
valid_references = [
|
|
81
|
-
reference
|
|
82
|
-
for reference in references
|
|
83
|
-
if reference not in parent_def_names
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
for reference in valid_references:
|
|
87
|
-
if def_is_referenced(reference, parent_def_names):
|
|
88
|
-
return True
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
# Remove orphaned definitions if requested
|
|
92
|
-
for def_name in list(defs):
|
|
93
|
-
if not def_is_referenced(def_name):
|
|
94
|
-
defs.pop(def_name)
|
|
95
|
-
if not defs:
|
|
96
|
-
schema.pop("$defs", None)
|
|
97
|
-
|
|
98
|
-
return schema
|
|
53
|
+
**Algorithm Overview:**
|
|
54
|
+
1. Traverse main schema, collecting $ref references and applying cleanups
|
|
55
|
+
2. Traverse $defs section to map inter-definition dependencies
|
|
56
|
+
3. Remove unused definitions based on reference analysis
|
|
99
57
|
|
|
58
|
+
Args:
|
|
59
|
+
schema: JSON schema dict to optimize (not modified)
|
|
60
|
+
prune_titles: Remove title fields for cleaner output
|
|
61
|
+
prune_additional_properties: Remove "additionalProperties": false constraints
|
|
62
|
+
prune_defs: Remove unused $defs entries to reduce size
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A new optimized schema dict
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> schema = {
|
|
69
|
+
... "type": "object",
|
|
70
|
+
... "title": "MySchema",
|
|
71
|
+
... "additionalProperties": False,
|
|
72
|
+
... "$defs": {"UnusedDef": {"type": "string"}}
|
|
73
|
+
... }
|
|
74
|
+
>>> result = _single_pass_optimize(schema, prune_titles=True, prune_defs=True)
|
|
75
|
+
>>> # Result: {"type": "object", "additionalProperties": False}
|
|
76
|
+
"""
|
|
77
|
+
if not (prune_defs or prune_titles or prune_additional_properties):
|
|
78
|
+
return schema # Nothing to do
|
|
79
|
+
|
|
80
|
+
# Phase 1: Collect references and apply simple cleanups
|
|
81
|
+
# Track which $defs are referenced from the main schema and from other $defs
|
|
82
|
+
root_refs: set[str] = set() # $defs referenced directly from main schema
|
|
83
|
+
def_dependencies: defaultdict[str, list[str]] = defaultdict(
|
|
84
|
+
list
|
|
85
|
+
) # def A references def B
|
|
86
|
+
defs = schema.get("$defs")
|
|
100
87
|
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
88
|
+
def traverse_and_clean(
|
|
89
|
+
node: object,
|
|
90
|
+
current_def_name: str | None = None,
|
|
91
|
+
skip_defs_section: bool = False,
|
|
92
|
+
depth: int = 0,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Traverse schema tree, collecting $ref info and applying cleanups."""
|
|
95
|
+
if depth > 50: # Prevent infinite recursion
|
|
96
|
+
return
|
|
107
97
|
|
|
108
|
-
def walk(node: object) -> None:
|
|
109
98
|
if isinstance(node, dict):
|
|
110
|
-
#
|
|
99
|
+
# Collect $ref references for unused definition removal
|
|
100
|
+
if prune_defs:
|
|
101
|
+
ref = node.get("$ref")
|
|
102
|
+
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
|
103
|
+
referenced_def = ref.split("/")[-1]
|
|
104
|
+
if current_def_name:
|
|
105
|
+
# We're inside a $def, so this is a def->def reference
|
|
106
|
+
def_dependencies[referenced_def].append(current_def_name)
|
|
107
|
+
else:
|
|
108
|
+
# We're in the main schema, so this is a root reference
|
|
109
|
+
root_refs.add(referenced_def)
|
|
110
|
+
|
|
111
|
+
# Apply cleanups
|
|
111
112
|
if prune_titles and "title" in node:
|
|
112
113
|
node.pop("title")
|
|
113
114
|
|
|
114
|
-
# Remove additionalProperties: false at any level if requested
|
|
115
115
|
if (
|
|
116
116
|
prune_additional_properties
|
|
117
|
-
and node.get("additionalProperties"
|
|
117
|
+
and node.get("additionalProperties") is False
|
|
118
118
|
):
|
|
119
119
|
node.pop("additionalProperties")
|
|
120
120
|
|
|
121
|
-
#
|
|
122
|
-
for
|
|
123
|
-
|
|
121
|
+
# Recursive traversal
|
|
122
|
+
for key, value in node.items():
|
|
123
|
+
if skip_defs_section and key == "$defs":
|
|
124
|
+
continue # Skip $defs during main schema traversal
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return schema
|
|
126
|
+
# Handle schema composition keywords with special traversal
|
|
127
|
+
if key in ["allOf", "oneOf", "anyOf"] and isinstance(value, list):
|
|
128
|
+
for item in value:
|
|
129
|
+
traverse_and_clean(item, current_def_name, depth=depth + 1)
|
|
130
|
+
else:
|
|
131
|
+
traverse_and_clean(value, current_def_name, depth=depth + 1)
|
|
132
132
|
|
|
133
|
+
elif isinstance(node, list):
|
|
134
|
+
for item in node:
|
|
135
|
+
traverse_and_clean(item, current_def_name, depth=depth + 1)
|
|
136
|
+
|
|
137
|
+
# Phase 2: Traverse main schema (excluding $defs section)
|
|
138
|
+
traverse_and_clean(schema, skip_defs_section=True)
|
|
139
|
+
|
|
140
|
+
# Phase 3: Traverse $defs to find inter-definition references
|
|
141
|
+
if prune_defs and defs:
|
|
142
|
+
for def_name, def_schema in defs.items():
|
|
143
|
+
traverse_and_clean(def_schema, current_def_name=def_name)
|
|
144
|
+
|
|
145
|
+
# Phase 4: Remove unused definitions
|
|
146
|
+
def is_def_used(def_name: str, visiting: set[str] | None = None) -> bool:
|
|
147
|
+
"""Check if a definition is used, handling circular references."""
|
|
148
|
+
if def_name in root_refs:
|
|
149
|
+
return True # Used directly from main schema
|
|
150
|
+
|
|
151
|
+
# Check if any definition that references this one is itself used
|
|
152
|
+
referencing_defs = def_dependencies.get(def_name, [])
|
|
153
|
+
if referencing_defs:
|
|
154
|
+
if visiting is None:
|
|
155
|
+
visiting = set()
|
|
156
|
+
|
|
157
|
+
# Avoid infinite recursion on circular references
|
|
158
|
+
if def_name in visiting:
|
|
159
|
+
return False
|
|
160
|
+
visiting = visiting | {def_name}
|
|
161
|
+
|
|
162
|
+
# If any referencing def is used, then this def is used
|
|
163
|
+
for referencing_def in referencing_defs:
|
|
164
|
+
if referencing_def not in visiting and is_def_used(
|
|
165
|
+
referencing_def, visiting
|
|
166
|
+
):
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
# Remove unused definitions
|
|
172
|
+
for def_name in list(defs.keys()):
|
|
173
|
+
if not is_def_used(def_name):
|
|
174
|
+
defs.pop(def_name)
|
|
175
|
+
|
|
176
|
+
# Clean up empty $defs section
|
|
177
|
+
if not defs:
|
|
178
|
+
schema.pop("$defs", None)
|
|
133
179
|
|
|
134
|
-
def _prune_additional_properties(schema: dict) -> dict:
|
|
135
|
-
"""Remove additionalProperties from the schema if it is False."""
|
|
136
|
-
if schema.get("additionalProperties", None) is False:
|
|
137
|
-
schema.pop("additionalProperties")
|
|
138
180
|
return schema
|
|
139
181
|
|
|
140
182
|
|
|
@@ -155,21 +197,17 @@ def compress_schema(
|
|
|
155
197
|
prune_additional_properties: Whether to remove additionalProperties: false
|
|
156
198
|
prune_titles: Whether to remove title fields from the schema
|
|
157
199
|
"""
|
|
158
|
-
# Make a copy so we don't modify the original
|
|
159
|
-
schema = copy.deepcopy(schema)
|
|
160
|
-
|
|
161
200
|
# Remove specific parameters if requested
|
|
162
201
|
for param in prune_params or []:
|
|
163
202
|
schema = _prune_param(schema, param=param)
|
|
164
203
|
|
|
165
|
-
#
|
|
166
|
-
if prune_titles or prune_additional_properties:
|
|
167
|
-
schema =
|
|
204
|
+
# Apply combined optimizations in a single tree traversal
|
|
205
|
+
if prune_titles or prune_additional_properties or prune_defs:
|
|
206
|
+
schema = _single_pass_optimize(
|
|
168
207
|
schema,
|
|
169
208
|
prune_titles=prune_titles,
|
|
170
209
|
prune_additional_properties=prune_additional_properties,
|
|
210
|
+
prune_defs=prune_defs,
|
|
171
211
|
)
|
|
172
|
-
if prune_defs:
|
|
173
|
-
schema = _prune_unused_defs(schema)
|
|
174
212
|
|
|
175
213
|
return schema
|
|
@@ -561,9 +561,7 @@ def _create_dataclass(
|
|
|
561
561
|
else:
|
|
562
562
|
field_def = field(default=None, metadata=meta)
|
|
563
563
|
|
|
564
|
-
if is_required
|
|
565
|
-
fields.append((field_name, field_type, field_def))
|
|
566
|
-
elif is_required:
|
|
564
|
+
if is_required or default_val is not MISSING:
|
|
567
565
|
fields.append((field_name, field_type, field_def))
|
|
568
566
|
else:
|
|
569
567
|
fields.append((field_name, Union[field_type, type(None)], field_def)) # type: ignore[misc] # noqa: UP007
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastmcp.mcp_config import MCPConfig
|
|
4
|
+
from fastmcp.server.server import FastMCP
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def composite_server_from_mcp_config(
|
|
8
|
+
config: MCPConfig, name_as_prefix: bool = True
|
|
9
|
+
) -> FastMCP[None]:
|
|
10
|
+
"""A utility function to create a composite server from an MCPConfig."""
|
|
11
|
+
composite_server = FastMCP[None]()
|
|
12
|
+
|
|
13
|
+
mount_mcp_config_into_server(config, composite_server, name_as_prefix)
|
|
14
|
+
|
|
15
|
+
return composite_server
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def mount_mcp_config_into_server(
|
|
19
|
+
config: MCPConfig,
|
|
20
|
+
server: FastMCP[Any],
|
|
21
|
+
name_as_prefix: bool = True,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""A utility function to mount the servers from an MCPConfig into a FastMCP server."""
|
|
24
|
+
for name, mcp_server in config.mcpServers.items():
|
|
25
|
+
server.mount(
|
|
26
|
+
prefix=name if name_as_prefix else None,
|
|
27
|
+
server=FastMCP.as_proxy(backend=mcp_server.to_transport()),
|
|
28
|
+
)
|