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.
Files changed (77) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/logging.py +25 -1
  13. fastmcp/client/oauth_callback.py +91 -91
  14. fastmcp/client/sampling.py +12 -4
  15. fastmcp/client/transports.py +143 -67
  16. fastmcp/experimental/sampling/__init__.py +0 -0
  17. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  18. fastmcp/experimental/sampling/handlers/base.py +21 -0
  19. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  20. fastmcp/experimental/server/openapi/routing.py +1 -3
  21. fastmcp/experimental/server/openapi/server.py +10 -25
  22. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  23. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  24. fastmcp/experimental/utilities/openapi/models.py +5 -2
  25. fastmcp/experimental/utilities/openapi/parser.py +252 -70
  26. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  27. fastmcp/mcp_config.py +40 -20
  28. fastmcp/prompts/prompt_manager.py +4 -2
  29. fastmcp/resources/resource_manager.py +16 -6
  30. fastmcp/server/auth/__init__.py +11 -1
  31. fastmcp/server/auth/auth.py +19 -2
  32. fastmcp/server/auth/oauth_proxy.py +1047 -0
  33. fastmcp/server/auth/providers/azure.py +270 -0
  34. fastmcp/server/auth/providers/github.py +287 -0
  35. fastmcp/server/auth/providers/google.py +305 -0
  36. fastmcp/server/auth/providers/jwt.py +27 -16
  37. fastmcp/server/auth/providers/workos.py +256 -2
  38. fastmcp/server/auth/redirect_validation.py +65 -0
  39. fastmcp/server/auth/registry.py +1 -1
  40. fastmcp/server/context.py +91 -41
  41. fastmcp/server/dependencies.py +32 -2
  42. fastmcp/server/elicitation.py +60 -1
  43. fastmcp/server/http.py +44 -37
  44. fastmcp/server/middleware/logging.py +66 -28
  45. fastmcp/server/proxy.py +2 -0
  46. fastmcp/server/sampling/handler.py +19 -0
  47. fastmcp/server/server.py +85 -20
  48. fastmcp/settings.py +18 -3
  49. fastmcp/tools/tool.py +23 -10
  50. fastmcp/tools/tool_manager.py +5 -1
  51. fastmcp/tools/tool_transform.py +75 -32
  52. fastmcp/utilities/auth.py +34 -0
  53. fastmcp/utilities/cli.py +148 -15
  54. fastmcp/utilities/components.py +21 -5
  55. fastmcp/utilities/inspect.py +166 -37
  56. fastmcp/utilities/json_schema_type.py +4 -2
  57. fastmcp/utilities/logging.py +4 -1
  58. fastmcp/utilities/mcp_config.py +47 -18
  59. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  60. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  61. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  62. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  63. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  64. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  65. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  66. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  67. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  68. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  69. fastmcp/utilities/openapi.py +4 -4
  70. fastmcp/utilities/tests.py +7 -2
  71. fastmcp/utilities/types.py +15 -2
  72. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/METADATA +3 -2
  73. fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
  74. fastmcp-2.11.2.dist-info/RECORD +0 -108
  75. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
  76. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
  77. {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
+ }
@@ -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
@@ -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.info(
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.info(
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.info(
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.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
737
+ logger.debug(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
738
738
  return routes
739
739
 
740
740
 
@@ -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
- host = "127.0.0.1"
96
- port = find_available_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}