fastmcp 2.11.2__py3-none-any.whl → 2.12.0rc1__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 +5 -4
- fastmcp/cli/claude.py +22 -18
- fastmcp/cli/cli.py +472 -136
- fastmcp/cli/install/claude_code.py +37 -40
- fastmcp/cli/install/claude_desktop.py +37 -42
- fastmcp/cli/install/cursor.py +148 -38
- fastmcp/cli/install/mcp_json.py +38 -43
- fastmcp/cli/install/shared.py +64 -7
- fastmcp/cli/run.py +122 -215
- fastmcp/client/auth/oauth.py +69 -13
- fastmcp/client/client.py +46 -9
- fastmcp/client/logging.py +25 -1
- fastmcp/client/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +143 -67
- fastmcp/experimental/sampling/__init__.py +0 -0
- fastmcp/experimental/sampling/handlers/__init__.py +3 -0
- fastmcp/experimental/sampling/handlers/base.py +21 -0
- fastmcp/experimental/sampling/handlers/openai.py +163 -0
- fastmcp/experimental/server/openapi/routing.py +1 -3
- fastmcp/experimental/server/openapi/server.py +10 -25
- fastmcp/experimental/utilities/openapi/__init__.py +2 -2
- fastmcp/experimental/utilities/openapi/formatters.py +34 -0
- fastmcp/experimental/utilities/openapi/models.py +5 -2
- fastmcp/experimental/utilities/openapi/parser.py +252 -70
- fastmcp/experimental/utilities/openapi/schemas.py +135 -106
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +4 -2
- fastmcp/resources/resource_manager.py +16 -6
- fastmcp/server/auth/__init__.py +11 -1
- fastmcp/server/auth/auth.py +19 -2
- fastmcp/server/auth/oauth_proxy.py +1047 -0
- fastmcp/server/auth/providers/azure.py +270 -0
- fastmcp/server/auth/providers/github.py +287 -0
- fastmcp/server/auth/providers/google.py +305 -0
- fastmcp/server/auth/providers/jwt.py +27 -16
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/auth/registry.py +1 -1
- fastmcp/server/context.py +91 -41
- fastmcp/server/dependencies.py +32 -2
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +44 -37
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +85 -20
- fastmcp/settings.py +18 -3
- fastmcp/tools/tool.py +23 -10
- fastmcp/tools/tool_manager.py +5 -1
- fastmcp/tools/tool_transform.py +75 -32
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +21 -5
- fastmcp/utilities/inspect.py +166 -37
- fastmcp/utilities/json_schema_type.py +4 -2
- fastmcp/utilities/logging.py +4 -1
- fastmcp/utilities/mcp_config.py +47 -18
- fastmcp/utilities/mcp_server_config/__init__.py +25 -0
- fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
- fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
- fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
- fastmcp/utilities/openapi.py +4 -4
- fastmcp/utilities/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/METADATA +3 -2
- fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$defs": {
|
|
3
|
+
"Deployment": {
|
|
4
|
+
"description": "Configuration for server deployment and runtime settings.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"transport": {
|
|
7
|
+
"anyOf": [
|
|
8
|
+
{
|
|
9
|
+
"enum": [
|
|
10
|
+
"stdio",
|
|
11
|
+
"http",
|
|
12
|
+
"sse"
|
|
13
|
+
],
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "null"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"default": null,
|
|
21
|
+
"description": "Transport protocol to use",
|
|
22
|
+
"title": "Transport"
|
|
23
|
+
},
|
|
24
|
+
"host": {
|
|
25
|
+
"anyOf": [
|
|
26
|
+
{
|
|
27
|
+
"type": "string"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "null"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"default": null,
|
|
34
|
+
"description": "Host to bind to when using HTTP transport",
|
|
35
|
+
"examples": [
|
|
36
|
+
"127.0.0.1",
|
|
37
|
+
"0.0.0.0",
|
|
38
|
+
"localhost"
|
|
39
|
+
],
|
|
40
|
+
"title": "Host"
|
|
41
|
+
},
|
|
42
|
+
"port": {
|
|
43
|
+
"anyOf": [
|
|
44
|
+
{
|
|
45
|
+
"type": "integer"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "null"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"default": null,
|
|
52
|
+
"description": "Port to bind to when using HTTP transport",
|
|
53
|
+
"examples": [
|
|
54
|
+
8000,
|
|
55
|
+
3000,
|
|
56
|
+
5000
|
|
57
|
+
],
|
|
58
|
+
"title": "Port"
|
|
59
|
+
},
|
|
60
|
+
"path": {
|
|
61
|
+
"anyOf": [
|
|
62
|
+
{
|
|
63
|
+
"type": "string"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"type": "null"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"default": null,
|
|
70
|
+
"description": "URL path for the server endpoint",
|
|
71
|
+
"examples": [
|
|
72
|
+
"/mcp/",
|
|
73
|
+
"/api/mcp/",
|
|
74
|
+
"/sse/"
|
|
75
|
+
],
|
|
76
|
+
"title": "Path"
|
|
77
|
+
},
|
|
78
|
+
"log_level": {
|
|
79
|
+
"anyOf": [
|
|
80
|
+
{
|
|
81
|
+
"enum": [
|
|
82
|
+
"DEBUG",
|
|
83
|
+
"INFO",
|
|
84
|
+
"WARNING",
|
|
85
|
+
"ERROR",
|
|
86
|
+
"CRITICAL"
|
|
87
|
+
],
|
|
88
|
+
"type": "string"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"type": "null"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"default": null,
|
|
95
|
+
"description": "Log level for the server",
|
|
96
|
+
"title": "Log Level"
|
|
97
|
+
},
|
|
98
|
+
"cwd": {
|
|
99
|
+
"anyOf": [
|
|
100
|
+
{
|
|
101
|
+
"type": "string"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "null"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"default": null,
|
|
108
|
+
"description": "Working directory for the server process",
|
|
109
|
+
"examples": [
|
|
110
|
+
".",
|
|
111
|
+
"./src",
|
|
112
|
+
"/app"
|
|
113
|
+
],
|
|
114
|
+
"title": "Cwd"
|
|
115
|
+
},
|
|
116
|
+
"env": {
|
|
117
|
+
"anyOf": [
|
|
118
|
+
{
|
|
119
|
+
"additionalProperties": {
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
"type": "object"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"type": "null"
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"default": null,
|
|
129
|
+
"description": "Environment variables to set when running the server",
|
|
130
|
+
"examples": [
|
|
131
|
+
{
|
|
132
|
+
"API_KEY": "secret",
|
|
133
|
+
"DEBUG": "true"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"title": "Env"
|
|
137
|
+
},
|
|
138
|
+
"args": {
|
|
139
|
+
"anyOf": [
|
|
140
|
+
{
|
|
141
|
+
"items": {
|
|
142
|
+
"type": "string"
|
|
143
|
+
},
|
|
144
|
+
"type": "array"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"type": "null"
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"default": null,
|
|
151
|
+
"description": "Arguments to pass to the server (after --)",
|
|
152
|
+
"examples": [
|
|
153
|
+
[
|
|
154
|
+
"--config",
|
|
155
|
+
"config.json",
|
|
156
|
+
"--debug"
|
|
157
|
+
]
|
|
158
|
+
],
|
|
159
|
+
"title": "Args"
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"title": "Deployment",
|
|
163
|
+
"type": "object"
|
|
164
|
+
},
|
|
165
|
+
"FileSystemSource": {
|
|
166
|
+
"description": "Source for local Python files.",
|
|
167
|
+
"properties": {
|
|
168
|
+
"type": {
|
|
169
|
+
"const": "filesystem",
|
|
170
|
+
"default": "filesystem",
|
|
171
|
+
"title": "Type",
|
|
172
|
+
"type": "string"
|
|
173
|
+
},
|
|
174
|
+
"path": {
|
|
175
|
+
"description": "Path to Python file containing the server",
|
|
176
|
+
"title": "Path",
|
|
177
|
+
"type": "string"
|
|
178
|
+
},
|
|
179
|
+
"entrypoint": {
|
|
180
|
+
"anyOf": [
|
|
181
|
+
{
|
|
182
|
+
"type": "string"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"type": "null"
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
"default": null,
|
|
189
|
+
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
|
190
|
+
"title": "Entrypoint"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"required": [
|
|
194
|
+
"path"
|
|
195
|
+
],
|
|
196
|
+
"title": "FileSystemSource",
|
|
197
|
+
"type": "object"
|
|
198
|
+
},
|
|
199
|
+
"UVEnvironment": {
|
|
200
|
+
"description": "Configuration for Python environment setup.",
|
|
201
|
+
"properties": {
|
|
202
|
+
"type": {
|
|
203
|
+
"const": "uv",
|
|
204
|
+
"default": "uv",
|
|
205
|
+
"title": "Type",
|
|
206
|
+
"type": "string"
|
|
207
|
+
},
|
|
208
|
+
"python": {
|
|
209
|
+
"anyOf": [
|
|
210
|
+
{
|
|
211
|
+
"type": "string"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"type": "null"
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
"default": null,
|
|
218
|
+
"description": "Python version constraint",
|
|
219
|
+
"examples": [
|
|
220
|
+
"3.10",
|
|
221
|
+
"3.11",
|
|
222
|
+
"3.12"
|
|
223
|
+
],
|
|
224
|
+
"title": "Python"
|
|
225
|
+
},
|
|
226
|
+
"dependencies": {
|
|
227
|
+
"anyOf": [
|
|
228
|
+
{
|
|
229
|
+
"items": {
|
|
230
|
+
"type": "string"
|
|
231
|
+
},
|
|
232
|
+
"type": "array"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"type": "null"
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
"default": null,
|
|
239
|
+
"description": "Python packages to install with PEP 508 specifiers",
|
|
240
|
+
"examples": [
|
|
241
|
+
[
|
|
242
|
+
"fastmcp>=2.0,<3",
|
|
243
|
+
"httpx",
|
|
244
|
+
"pandas>=2.0"
|
|
245
|
+
]
|
|
246
|
+
],
|
|
247
|
+
"title": "Dependencies"
|
|
248
|
+
},
|
|
249
|
+
"requirements": {
|
|
250
|
+
"anyOf": [
|
|
251
|
+
{
|
|
252
|
+
"type": "string"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"type": "null"
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
"default": null,
|
|
259
|
+
"description": "Path to requirements.txt file",
|
|
260
|
+
"examples": [
|
|
261
|
+
"requirements.txt",
|
|
262
|
+
"../requirements/prod.txt"
|
|
263
|
+
],
|
|
264
|
+
"title": "Requirements"
|
|
265
|
+
},
|
|
266
|
+
"project": {
|
|
267
|
+
"anyOf": [
|
|
268
|
+
{
|
|
269
|
+
"type": "string"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"type": "null"
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
"default": null,
|
|
276
|
+
"description": "Path to project directory containing pyproject.toml",
|
|
277
|
+
"examples": [
|
|
278
|
+
".",
|
|
279
|
+
"../my-project"
|
|
280
|
+
],
|
|
281
|
+
"title": "Project"
|
|
282
|
+
},
|
|
283
|
+
"editable": {
|
|
284
|
+
"anyOf": [
|
|
285
|
+
{
|
|
286
|
+
"items": {
|
|
287
|
+
"type": "string"
|
|
288
|
+
},
|
|
289
|
+
"type": "array"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"type": "null"
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
"default": null,
|
|
296
|
+
"description": "Directories to install in editable mode",
|
|
297
|
+
"examples": [
|
|
298
|
+
[
|
|
299
|
+
".",
|
|
300
|
+
"../my-package"
|
|
301
|
+
],
|
|
302
|
+
[
|
|
303
|
+
"/path/to/package"
|
|
304
|
+
]
|
|
305
|
+
],
|
|
306
|
+
"title": "Editable"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"title": "UVEnvironment",
|
|
310
|
+
"type": "object"
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"description": "Configuration file for FastMCP servers",
|
|
314
|
+
"properties": {
|
|
315
|
+
"$schema": {
|
|
316
|
+
"anyOf": [
|
|
317
|
+
{
|
|
318
|
+
"type": "string"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"type": "null"
|
|
322
|
+
}
|
|
323
|
+
],
|
|
324
|
+
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
|
325
|
+
"description": "JSON schema for IDE support and validation",
|
|
326
|
+
"title": "$Schema"
|
|
327
|
+
},
|
|
328
|
+
"source": {
|
|
329
|
+
"$ref": "#/$defs/FileSystemSource",
|
|
330
|
+
"description": "Source configuration for the server",
|
|
331
|
+
"examples": [
|
|
332
|
+
{
|
|
333
|
+
"path": "server.py"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"entrypoint": "app",
|
|
337
|
+
"path": "server.py"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"entrypoint": "mcp",
|
|
341
|
+
"path": "src/server.py",
|
|
342
|
+
"type": "filesystem"
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
},
|
|
346
|
+
"environment": {
|
|
347
|
+
"$ref": "#/$defs/UVEnvironment",
|
|
348
|
+
"description": "Python environment setup configuration"
|
|
349
|
+
},
|
|
350
|
+
"deployment": {
|
|
351
|
+
"$ref": "#/$defs/Deployment",
|
|
352
|
+
"description": "Server deployment and runtime settings"
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
"required": [
|
|
356
|
+
"source"
|
|
357
|
+
],
|
|
358
|
+
"title": "FastMCP Configuration",
|
|
359
|
+
"type": "object",
|
|
360
|
+
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
|
|
361
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Source(BaseModel, ABC):
|
|
8
|
+
"""Abstract base class for all source types."""
|
|
9
|
+
|
|
10
|
+
type: str = Field(description="Source type identifier")
|
|
11
|
+
|
|
12
|
+
async def prepare(self) -> None:
|
|
13
|
+
"""Prepare the source (download, clone, install, etc).
|
|
14
|
+
|
|
15
|
+
For sources that need preparation (e.g., git clone, download),
|
|
16
|
+
this method performs that preparation. For sources that don't
|
|
17
|
+
need preparation (e.g., local files), this is a no-op.
|
|
18
|
+
"""
|
|
19
|
+
# Default implementation for sources that don't need preparation
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def load_server(self) -> Any:
|
|
24
|
+
"""Load and return the FastMCP server instance.
|
|
25
|
+
|
|
26
|
+
Must be called after prepare() if the source requires preparation.
|
|
27
|
+
All information needed to load the server should be available
|
|
28
|
+
as attributes on the source instance.
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import inspect
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, field_validator
|
|
8
|
+
|
|
9
|
+
from fastmcp.utilities.logging import get_logger
|
|
10
|
+
from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileSystemSource(Source):
|
|
16
|
+
"""Source for local Python files."""
|
|
17
|
+
|
|
18
|
+
type: Literal["filesystem"] = "filesystem"
|
|
19
|
+
|
|
20
|
+
path: str = Field(description="Path to Python file containing the server")
|
|
21
|
+
entrypoint: str | None = Field(
|
|
22
|
+
default=None,
|
|
23
|
+
description="Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@field_validator("path", mode="before")
|
|
27
|
+
@classmethod
|
|
28
|
+
def parse_path_with_object(cls, v: str) -> str:
|
|
29
|
+
"""Parse path:object syntax and extract the object name.
|
|
30
|
+
|
|
31
|
+
This validator runs before the model is created, allowing us to
|
|
32
|
+
handle the "file.py:object" syntax at the model boundary.
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(v, str) and ":" in v:
|
|
35
|
+
# Check if it's a Windows path (e.g., C:\...)
|
|
36
|
+
has_windows_drive = len(v) > 1 and v[1] == ":"
|
|
37
|
+
|
|
38
|
+
# Only split if colon is not part of Windows drive
|
|
39
|
+
if ":" in (v[2:] if has_windows_drive else v):
|
|
40
|
+
# This path has an object specification
|
|
41
|
+
# We'll handle it in __init__ by setting entrypoint
|
|
42
|
+
return v
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
def __init__(self, **data: Any) -> None:
|
|
46
|
+
"""Initialize FileSystemSource, handling path:object syntax."""
|
|
47
|
+
# Check if path contains an object specification
|
|
48
|
+
if "path" in data and isinstance(data["path"], str) and ":" in data["path"]:
|
|
49
|
+
path_str = data["path"]
|
|
50
|
+
# Check if it's a Windows path (e.g., C:\...)
|
|
51
|
+
has_windows_drive = len(path_str) > 1 and path_str[1] == ":"
|
|
52
|
+
|
|
53
|
+
# Only split if colon is not part of Windows drive
|
|
54
|
+
if ":" in (path_str[2:] if has_windows_drive else path_str):
|
|
55
|
+
file_str, obj = path_str.rsplit(":", 1)
|
|
56
|
+
data["path"] = file_str
|
|
57
|
+
# Only set entrypoint if not already provided
|
|
58
|
+
if "entrypoint" not in data or data["entrypoint"] is None:
|
|
59
|
+
data["entrypoint"] = obj
|
|
60
|
+
|
|
61
|
+
super().__init__(**data)
|
|
62
|
+
|
|
63
|
+
async def load_server(self) -> Any:
|
|
64
|
+
"""Load server from filesystem."""
|
|
65
|
+
# Resolve the file path
|
|
66
|
+
file_path = Path(self.path).expanduser().resolve()
|
|
67
|
+
if not file_path.exists():
|
|
68
|
+
logger.error(f"File not found: {file_path}")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
if not file_path.is_file():
|
|
71
|
+
logger.error(f"Not a file: {file_path}")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
# Import the module
|
|
75
|
+
module = self._import_module(file_path)
|
|
76
|
+
|
|
77
|
+
# Find the server object
|
|
78
|
+
server = await self._find_server_object(module, file_path)
|
|
79
|
+
|
|
80
|
+
return server
|
|
81
|
+
|
|
82
|
+
def _import_module(self, file_path: Path) -> Any:
|
|
83
|
+
"""Import a Python module from a file path.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
file_path: Path to the Python file
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The imported module
|
|
90
|
+
"""
|
|
91
|
+
# Add parent directory to Python path so imports can be resolved
|
|
92
|
+
file_dir = str(file_path.parent)
|
|
93
|
+
if file_dir not in sys.path:
|
|
94
|
+
sys.path.insert(0, file_dir)
|
|
95
|
+
|
|
96
|
+
# Import the module
|
|
97
|
+
spec = importlib.util.spec_from_file_location("server_module", file_path)
|
|
98
|
+
if not spec or not spec.loader:
|
|
99
|
+
logger.error("Could not load module", extra={"file": str(file_path)})
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
module = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
|
|
103
|
+
sys.modules["server_module"] = module # Register in sys.modules
|
|
104
|
+
spec.loader.exec_module(module) # type: ignore[union-attr]
|
|
105
|
+
|
|
106
|
+
return module
|
|
107
|
+
|
|
108
|
+
async def _find_server_object(self, module: Any, file_path: Path) -> Any:
|
|
109
|
+
"""Find the server object in the module.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
module: The imported Python module
|
|
113
|
+
file_path: Path to the file (for error messages)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The server object (or result of calling a factory function)
|
|
117
|
+
"""
|
|
118
|
+
# Avoid circular import by importing here
|
|
119
|
+
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
|
120
|
+
|
|
121
|
+
from fastmcp.server.server import FastMCP
|
|
122
|
+
|
|
123
|
+
# If entrypoint is specified, use it
|
|
124
|
+
if self.entrypoint:
|
|
125
|
+
# Handle module:object syntax (though this is legacy)
|
|
126
|
+
if ":" in self.entrypoint:
|
|
127
|
+
module_name, object_name = self.entrypoint.split(":", 1)
|
|
128
|
+
try:
|
|
129
|
+
import importlib
|
|
130
|
+
|
|
131
|
+
server_module = importlib.import_module(module_name)
|
|
132
|
+
obj = getattr(server_module, object_name, None)
|
|
133
|
+
except ImportError:
|
|
134
|
+
logger.error(
|
|
135
|
+
f"Could not import module '{module_name}'",
|
|
136
|
+
extra={"file": str(file_path)},
|
|
137
|
+
)
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
else:
|
|
140
|
+
# Just object name
|
|
141
|
+
obj = getattr(module, self.entrypoint, None)
|
|
142
|
+
|
|
143
|
+
if obj is None:
|
|
144
|
+
logger.error(
|
|
145
|
+
f"Server object '{self.entrypoint}' not found",
|
|
146
|
+
extra={"file": str(file_path)},
|
|
147
|
+
)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
return await self._resolve_factory(obj, file_path, self.entrypoint)
|
|
151
|
+
|
|
152
|
+
# No entrypoint specified, try common server names
|
|
153
|
+
for name in ["mcp", "server", "app"]:
|
|
154
|
+
if hasattr(module, name):
|
|
155
|
+
obj = getattr(module, name)
|
|
156
|
+
if isinstance(obj, FastMCP | FastMCP1x):
|
|
157
|
+
return await self._resolve_factory(obj, file_path, name)
|
|
158
|
+
|
|
159
|
+
# No server found
|
|
160
|
+
logger.error(
|
|
161
|
+
f"No server object found in {file_path}. Please either:\n"
|
|
162
|
+
"1. Use a standard variable name (mcp, server, or app)\n"
|
|
163
|
+
"2. Specify the entrypoint name in fastmcp.json or use `file.py:object` syntax as your path.",
|
|
164
|
+
extra={"file": str(file_path)},
|
|
165
|
+
)
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
|
|
168
|
+
async def _resolve_factory(self, obj: Any, file_path: Path, name: str) -> Any:
|
|
169
|
+
"""Resolve a server object or factory function to a server instance.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
obj: The object that might be a server or factory function
|
|
173
|
+
file_path: Path to the file for error messages
|
|
174
|
+
name: Name of the object for error messages
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A server instance
|
|
178
|
+
"""
|
|
179
|
+
# Avoid circular import by importing here
|
|
180
|
+
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
|
181
|
+
|
|
182
|
+
from fastmcp.server.server import FastMCP
|
|
183
|
+
|
|
184
|
+
# Check if it's a function or coroutine function
|
|
185
|
+
if inspect.isfunction(obj) or inspect.iscoroutinefunction(obj):
|
|
186
|
+
logger.debug(f"Found factory function '{name}' in {file_path}")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
if inspect.iscoroutinefunction(obj):
|
|
190
|
+
# Async factory function
|
|
191
|
+
server = await obj()
|
|
192
|
+
else:
|
|
193
|
+
# Sync factory function
|
|
194
|
+
server = obj()
|
|
195
|
+
|
|
196
|
+
# Validate the result is a FastMCP server
|
|
197
|
+
if not isinstance(server, FastMCP | FastMCP1x):
|
|
198
|
+
logger.error(
|
|
199
|
+
f"Factory function '{name}' must return a FastMCP server instance, "
|
|
200
|
+
f"got {type(server).__name__}",
|
|
201
|
+
extra={"file": str(file_path)},
|
|
202
|
+
)
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
|
|
205
|
+
logger.debug(f"Factory function '{name}' created server: {server.name}")
|
|
206
|
+
return server
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(
|
|
210
|
+
f"Failed to call factory function '{name}': {e}",
|
|
211
|
+
extra={"file": str(file_path)},
|
|
212
|
+
)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
# Not a function, return as-is (should be a server instance)
|
|
216
|
+
return obj
|
fastmcp/utilities/openapi.py
CHANGED
|
@@ -212,7 +212,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
|
|
|
212
212
|
if openapi_version.startswith("3.0"):
|
|
213
213
|
# Use OpenAPI 3.0 models
|
|
214
214
|
openapi_30 = OpenAPI_30.model_validate(openapi_dict)
|
|
215
|
-
logger.
|
|
215
|
+
logger.debug(
|
|
216
216
|
f"Successfully parsed OpenAPI 3.0 schema version: {openapi_30.openapi}"
|
|
217
217
|
)
|
|
218
218
|
parser = OpenAPIParser(
|
|
@@ -230,7 +230,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
|
|
|
230
230
|
else:
|
|
231
231
|
# Default to OpenAPI 3.1 models
|
|
232
232
|
openapi_31 = OpenAPI.model_validate(openapi_dict)
|
|
233
|
-
logger.
|
|
233
|
+
logger.debug(
|
|
234
234
|
f"Successfully parsed OpenAPI 3.1 schema version: {openapi_31.openapi}"
|
|
235
235
|
)
|
|
236
236
|
parser = OpenAPIParser(
|
|
@@ -713,7 +713,7 @@ class OpenAPIParser(
|
|
|
713
713
|
openapi_version=self.openapi_version,
|
|
714
714
|
)
|
|
715
715
|
routes.append(route)
|
|
716
|
-
logger.
|
|
716
|
+
logger.debug(
|
|
717
717
|
f"Successfully extracted route: {method_upper} {path_str}"
|
|
718
718
|
)
|
|
719
719
|
except ValueError as op_error:
|
|
@@ -734,7 +734,7 @@ class OpenAPIParser(
|
|
|
734
734
|
exc_info=True,
|
|
735
735
|
)
|
|
736
736
|
|
|
737
|
-
logger.
|
|
737
|
+
logger.debug(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
|
|
738
738
|
return routes
|
|
739
739
|
|
|
740
740
|
|
fastmcp/utilities/tests.py
CHANGED
|
@@ -76,6 +76,8 @@ def run_server_in_process(
|
|
|
76
76
|
server_fn: Callable[..., None],
|
|
77
77
|
*args,
|
|
78
78
|
provide_host_and_port: bool = True,
|
|
79
|
+
host: str = "127.0.0.1",
|
|
80
|
+
port: int | None = None,
|
|
79
81
|
**kwargs,
|
|
80
82
|
) -> Generator[str, None, None]:
|
|
81
83
|
"""
|
|
@@ -87,13 +89,16 @@ def run_server_in_process(
|
|
|
87
89
|
not pickleable, so we need a function that creates and runs one.
|
|
88
90
|
*args: Arguments to pass to the server function.
|
|
89
91
|
provide_host_and_port: Whether to provide the host and port to the server function as kwargs.
|
|
92
|
+
host: Host to bind the server to (default: "127.0.0.1").
|
|
93
|
+
port: Port to bind the server to (default: find available port).
|
|
90
94
|
**kwargs: Keyword arguments to pass to the server function.
|
|
91
95
|
|
|
92
96
|
Returns:
|
|
93
97
|
The server URL.
|
|
94
98
|
"""
|
|
95
|
-
|
|
96
|
-
port
|
|
99
|
+
# Use provided port or find an available one
|
|
100
|
+
if port is None:
|
|
101
|
+
port = find_available_port()
|
|
97
102
|
|
|
98
103
|
if provide_host_and_port:
|
|
99
104
|
kwargs |= {"host": host, "port": port}
|