hammad-python 0.0.30__py3-none-any.whl → 0.0.31__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.
- ham/__init__.py +10 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/METADATA +6 -32
- hammad_python-0.0.31.dist-info/RECORD +6 -0
- hammad/__init__.py +0 -84
- hammad/_internal.py +0 -256
- hammad/_main.py +0 -226
- hammad/cache/__init__.py +0 -40
- hammad/cache/base_cache.py +0 -181
- hammad/cache/cache.py +0 -169
- hammad/cache/decorators.py +0 -261
- hammad/cache/file_cache.py +0 -80
- hammad/cache/ttl_cache.py +0 -74
- hammad/cli/__init__.py +0 -33
- hammad/cli/animations.py +0 -573
- hammad/cli/plugins.py +0 -867
- hammad/cli/styles/__init__.py +0 -55
- hammad/cli/styles/settings.py +0 -139
- hammad/cli/styles/types.py +0 -358
- hammad/cli/styles/utils.py +0 -634
- hammad/data/__init__.py +0 -90
- hammad/data/collections/__init__.py +0 -49
- hammad/data/collections/collection.py +0 -326
- hammad/data/collections/indexes/__init__.py +0 -37
- hammad/data/collections/indexes/qdrant/__init__.py +0 -1
- hammad/data/collections/indexes/qdrant/index.py +0 -723
- hammad/data/collections/indexes/qdrant/settings.py +0 -94
- hammad/data/collections/indexes/qdrant/utils.py +0 -210
- hammad/data/collections/indexes/tantivy/__init__.py +0 -1
- hammad/data/collections/indexes/tantivy/index.py +0 -426
- hammad/data/collections/indexes/tantivy/settings.py +0 -40
- hammad/data/collections/indexes/tantivy/utils.py +0 -176
- hammad/data/configurations/__init__.py +0 -35
- hammad/data/configurations/configuration.py +0 -564
- hammad/data/models/__init__.py +0 -50
- hammad/data/models/extensions/__init__.py +0 -4
- hammad/data/models/extensions/pydantic/__init__.py +0 -42
- hammad/data/models/extensions/pydantic/converters.py +0 -759
- hammad/data/models/fields.py +0 -546
- hammad/data/models/model.py +0 -1078
- hammad/data/models/utils.py +0 -280
- hammad/data/sql/__init__.py +0 -24
- hammad/data/sql/database.py +0 -576
- hammad/data/sql/types.py +0 -127
- hammad/data/types/__init__.py +0 -75
- hammad/data/types/file.py +0 -431
- hammad/data/types/multimodal/__init__.py +0 -36
- hammad/data/types/multimodal/audio.py +0 -200
- hammad/data/types/multimodal/image.py +0 -182
- hammad/data/types/text.py +0 -1308
- hammad/formatting/__init__.py +0 -33
- hammad/formatting/json/__init__.py +0 -27
- hammad/formatting/json/converters.py +0 -158
- hammad/formatting/text/__init__.py +0 -63
- hammad/formatting/text/converters.py +0 -723
- hammad/formatting/text/markdown.py +0 -131
- hammad/formatting/yaml/__init__.py +0 -26
- hammad/formatting/yaml/converters.py +0 -5
- hammad/genai/__init__.py +0 -217
- hammad/genai/a2a/__init__.py +0 -32
- hammad/genai/a2a/workers.py +0 -552
- hammad/genai/agents/__init__.py +0 -59
- hammad/genai/agents/agent.py +0 -1973
- hammad/genai/agents/run.py +0 -1024
- hammad/genai/agents/types/__init__.py +0 -42
- hammad/genai/agents/types/agent_context.py +0 -13
- hammad/genai/agents/types/agent_event.py +0 -128
- hammad/genai/agents/types/agent_hooks.py +0 -220
- hammad/genai/agents/types/agent_messages.py +0 -31
- hammad/genai/agents/types/agent_response.py +0 -125
- hammad/genai/agents/types/agent_stream.py +0 -327
- hammad/genai/graphs/__init__.py +0 -125
- hammad/genai/graphs/_utils.py +0 -190
- hammad/genai/graphs/base.py +0 -1828
- hammad/genai/graphs/plugins.py +0 -316
- hammad/genai/graphs/types.py +0 -638
- hammad/genai/models/__init__.py +0 -1
- hammad/genai/models/embeddings/__init__.py +0 -43
- hammad/genai/models/embeddings/model.py +0 -226
- hammad/genai/models/embeddings/run.py +0 -163
- hammad/genai/models/embeddings/types/__init__.py +0 -37
- hammad/genai/models/embeddings/types/embedding_model_name.py +0 -75
- hammad/genai/models/embeddings/types/embedding_model_response.py +0 -76
- hammad/genai/models/embeddings/types/embedding_model_run_params.py +0 -66
- hammad/genai/models/embeddings/types/embedding_model_settings.py +0 -47
- hammad/genai/models/language/__init__.py +0 -57
- hammad/genai/models/language/model.py +0 -1098
- hammad/genai/models/language/run.py +0 -878
- hammad/genai/models/language/types/__init__.py +0 -40
- hammad/genai/models/language/types/language_model_instructor_mode.py +0 -47
- hammad/genai/models/language/types/language_model_messages.py +0 -28
- hammad/genai/models/language/types/language_model_name.py +0 -239
- hammad/genai/models/language/types/language_model_request.py +0 -127
- hammad/genai/models/language/types/language_model_response.py +0 -217
- hammad/genai/models/language/types/language_model_response_chunk.py +0 -56
- hammad/genai/models/language/types/language_model_settings.py +0 -89
- hammad/genai/models/language/types/language_model_stream.py +0 -600
- hammad/genai/models/language/utils/__init__.py +0 -28
- hammad/genai/models/language/utils/requests.py +0 -421
- hammad/genai/models/language/utils/structured_outputs.py +0 -135
- hammad/genai/models/model_provider.py +0 -4
- hammad/genai/models/multimodal.py +0 -47
- hammad/genai/models/reranking.py +0 -26
- hammad/genai/types/__init__.py +0 -1
- hammad/genai/types/base.py +0 -215
- hammad/genai/types/history.py +0 -290
- hammad/genai/types/tools.py +0 -507
- hammad/logging/__init__.py +0 -35
- hammad/logging/decorators.py +0 -834
- hammad/logging/logger.py +0 -1018
- hammad/mcp/__init__.py +0 -53
- hammad/mcp/client/__init__.py +0 -35
- hammad/mcp/client/client.py +0 -624
- hammad/mcp/client/client_service.py +0 -400
- hammad/mcp/client/settings.py +0 -178
- hammad/mcp/servers/__init__.py +0 -26
- hammad/mcp/servers/launcher.py +0 -1161
- hammad/runtime/__init__.py +0 -32
- hammad/runtime/decorators.py +0 -142
- hammad/runtime/run.py +0 -299
- hammad/service/__init__.py +0 -49
- hammad/service/create.py +0 -527
- hammad/service/decorators.py +0 -283
- hammad/types.py +0 -288
- hammad/typing/__init__.py +0 -435
- hammad/web/__init__.py +0 -43
- hammad/web/http/__init__.py +0 -1
- hammad/web/http/client.py +0 -944
- hammad/web/models.py +0 -275
- hammad/web/openapi/__init__.py +0 -1
- hammad/web/openapi/client.py +0 -740
- hammad/web/search/__init__.py +0 -1
- hammad/web/search/client.py +0 -1023
- hammad/web/utils.py +0 -472
- hammad_python-0.0.30.dist-info/RECORD +0 -135
- {hammad → ham}/py.typed +0 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/WHEEL +0 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/licenses/LICENSE +0 -0
hammad/mcp/servers/launcher.py
DELETED
@@ -1,1161 +0,0 @@
|
|
1
|
-
"""hammad.mcp.servers.launcher
|
2
|
-
|
3
|
-
Contains quick launcher methods that allow running mcp servers
|
4
|
-
based on a set of functions and descriptions. This process uses
|
5
|
-
"subprocess" and ensures that each server is correctly terminated
|
6
|
-
after the session is complete. This also allows for multiple
|
7
|
-
servers to be ran in parallel within the same process, even if they
|
8
|
-
are serving over HTTP ports.
|
9
|
-
"""
|
10
|
-
|
11
|
-
import sys
|
12
|
-
import socket
|
13
|
-
import subprocess
|
14
|
-
import inspect
|
15
|
-
import signal
|
16
|
-
import time
|
17
|
-
import atexit
|
18
|
-
import threading
|
19
|
-
from dataclasses import dataclass, field
|
20
|
-
from typing import Callable, List, Literal, Dict, Any, Optional, Tuple, Union
|
21
|
-
from queue import Queue
|
22
|
-
import os
|
23
|
-
|
24
|
-
import logging
|
25
|
-
|
26
|
-
logger = logging.getLogger(__name__)
|
27
|
-
|
28
|
-
__all__ = [
|
29
|
-
# Main launch function
|
30
|
-
"launch_mcp_servers",
|
31
|
-
# Server configuration classes
|
32
|
-
"MCPServerStdioSettings",
|
33
|
-
"MCPServerSseSettings",
|
34
|
-
"MCPServerStreamableHttpSettings",
|
35
|
-
"ServerSettings",
|
36
|
-
# Server management
|
37
|
-
"MCPServerService",
|
38
|
-
"ServerInfo",
|
39
|
-
"get_server_service",
|
40
|
-
"shutdown_all_servers",
|
41
|
-
# Utility functions
|
42
|
-
"find_next_free_port",
|
43
|
-
# Legacy functions (deprecated - will be removed in future)
|
44
|
-
"launch_stdio_mcp_server",
|
45
|
-
"launch_sse_mcp_server",
|
46
|
-
"launch_streamable_http_mcp_server",
|
47
|
-
"keep_servers_running",
|
48
|
-
]
|
49
|
-
|
50
|
-
|
51
|
-
@dataclass
|
52
|
-
class ServerInfo:
|
53
|
-
"""Information about a launched server."""
|
54
|
-
|
55
|
-
name: str
|
56
|
-
transport: str
|
57
|
-
process: subprocess.Popen
|
58
|
-
host: Optional[str] = None
|
59
|
-
port: Optional[int] = None
|
60
|
-
|
61
|
-
@property
|
62
|
-
def url(self) -> Optional[str]:
|
63
|
-
"""Get the server URL if applicable."""
|
64
|
-
if self.host and self.port:
|
65
|
-
return f"http://{self.host}:{self.port}"
|
66
|
-
return None
|
67
|
-
|
68
|
-
|
69
|
-
@dataclass
|
70
|
-
class BaseServerSettings:
|
71
|
-
"""Base configuration for all MCP servers."""
|
72
|
-
|
73
|
-
name: str
|
74
|
-
instructions: Optional[str] = None
|
75
|
-
tools: List[Callable] = field(default_factory=list)
|
76
|
-
dependencies: List[str] = field(default_factory=list)
|
77
|
-
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
78
|
-
debug_mode: bool = False
|
79
|
-
cwd: Optional[str] = None
|
80
|
-
|
81
|
-
|
82
|
-
@dataclass
|
83
|
-
class MCPServerStdioSettings(BaseServerSettings):
|
84
|
-
"""Configuration for stdio transport MCP servers."""
|
85
|
-
|
86
|
-
transport: Literal["stdio"] = field(default="stdio", init=False)
|
87
|
-
|
88
|
-
|
89
|
-
@dataclass
|
90
|
-
class MCPServerSseSettings(BaseServerSettings):
|
91
|
-
"""Configuration for SSE transport MCP servers."""
|
92
|
-
|
93
|
-
transport: Literal["sse"] = field(default="sse", init=False)
|
94
|
-
host: str = "127.0.0.1"
|
95
|
-
start_port: int = 8000
|
96
|
-
mount_path: str = "/"
|
97
|
-
sse_path: str = "/sse"
|
98
|
-
message_path: str = "/messages/"
|
99
|
-
json_response: bool = False
|
100
|
-
stateless_http: bool = False
|
101
|
-
warn_on_duplicate_resources: bool = True
|
102
|
-
warn_on_duplicate_tools: bool = True
|
103
|
-
|
104
|
-
|
105
|
-
@dataclass
|
106
|
-
class MCPServerStreamableHttpSettings(BaseServerSettings):
|
107
|
-
"""Configuration for StreamableHTTP transport MCP servers."""
|
108
|
-
|
109
|
-
transport: Literal["streamable-http"] = field(default="streamable-http", init=False)
|
110
|
-
host: str = "127.0.0.1"
|
111
|
-
start_port: int = 8000
|
112
|
-
mount_path: str = "/"
|
113
|
-
streamable_http_path: str = "/mcp"
|
114
|
-
json_response: bool = False
|
115
|
-
stateless_http: bool = False
|
116
|
-
warn_on_duplicate_resources: bool = True
|
117
|
-
warn_on_duplicate_tools: bool = True
|
118
|
-
|
119
|
-
|
120
|
-
# Type alias for any server settings
|
121
|
-
ServerSettings = Union[
|
122
|
-
MCPServerStdioSettings, MCPServerSseSettings, MCPServerStreamableHttpSettings
|
123
|
-
]
|
124
|
-
|
125
|
-
|
126
|
-
def _monitor_process_output(process: subprocess.Popen, name: str, stream_name: str):
|
127
|
-
"""Monitor and log process output in a separate thread."""
|
128
|
-
stream = process.stdout if stream_name == "stdout" else process.stderr
|
129
|
-
if not stream:
|
130
|
-
return
|
131
|
-
|
132
|
-
try:
|
133
|
-
for line in stream:
|
134
|
-
if line:
|
135
|
-
# Add server name prefix to make multi-server logs clearer
|
136
|
-
logger.info(f"[{name}] {line.rstrip()}")
|
137
|
-
except Exception as e:
|
138
|
-
logger.error(f"Error reading {stream_name} from {name}: {e}")
|
139
|
-
|
140
|
-
|
141
|
-
def find_next_free_port(start_port: int = 8000, host: str = "127.0.0.1") -> int:
|
142
|
-
"""
|
143
|
-
Finds the next free port starting from the given start_port.
|
144
|
-
"""
|
145
|
-
port = start_port
|
146
|
-
while True:
|
147
|
-
try:
|
148
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
149
|
-
s.bind((host, port))
|
150
|
-
logger.debug(f"Found free port {port} on host {host}.")
|
151
|
-
return port
|
152
|
-
except OSError:
|
153
|
-
port += 1
|
154
|
-
if port > 65535:
|
155
|
-
logger.error(f"No free ports found above {start_port} on host {host}.")
|
156
|
-
raise IOError("No free ports found")
|
157
|
-
|
158
|
-
|
159
|
-
# Global variables for signal handling
|
160
|
-
_signal_handlers_registered = False
|
161
|
-
|
162
|
-
|
163
|
-
def _register_signal_handlers():
|
164
|
-
"""Register signal handlers for graceful shutdown."""
|
165
|
-
global _signal_handlers_registered
|
166
|
-
if _signal_handlers_registered:
|
167
|
-
return
|
168
|
-
|
169
|
-
def signal_handler(signum, frame):
|
170
|
-
logger.info(
|
171
|
-
f"Received signal {signum}. Shutting down all MCP server services..."
|
172
|
-
)
|
173
|
-
global _singleton_service
|
174
|
-
if _singleton_service is not None:
|
175
|
-
try:
|
176
|
-
_singleton_service.shutdown_all()
|
177
|
-
except Exception as e:
|
178
|
-
logger.error(f"Error during signal-triggered shutdown: {e}")
|
179
|
-
logger.info("Signal-triggered shutdown complete.")
|
180
|
-
|
181
|
-
# Register handlers for common termination signals
|
182
|
-
signal.signal(signal.SIGTERM, signal_handler)
|
183
|
-
signal.signal(signal.SIGINT, signal_handler)
|
184
|
-
|
185
|
-
# Also register atexit handler as a fallback
|
186
|
-
def atexit_handler():
|
187
|
-
global _singleton_service
|
188
|
-
if _singleton_service is not None:
|
189
|
-
_singleton_service.shutdown_all()
|
190
|
-
|
191
|
-
atexit.register(atexit_handler)
|
192
|
-
_signal_handlers_registered = True
|
193
|
-
|
194
|
-
|
195
|
-
@dataclass
|
196
|
-
class MCPServerService:
|
197
|
-
"""
|
198
|
-
Class that manages the lifecycles and startup/shutdown of
|
199
|
-
configured MCP servers by running them as subprocesses.
|
200
|
-
"""
|
201
|
-
|
202
|
-
active_servers: List[subprocess.Popen] = field(default_factory=list)
|
203
|
-
server_info: List[ServerInfo] = field(default_factory=list)
|
204
|
-
output_threads: List[threading.Thread] = field(default_factory=list)
|
205
|
-
python_executable: str = sys.executable
|
206
|
-
process_startup_timeout: float = (
|
207
|
-
10.0 # seconds to wait for process startup verification
|
208
|
-
)
|
209
|
-
_keep_running: bool = field(default=True, init=False)
|
210
|
-
|
211
|
-
def __post_init__(self):
|
212
|
-
"""Register signal handlers when service is created."""
|
213
|
-
_register_signal_handlers()
|
214
|
-
|
215
|
-
def _generate_runner_script(
|
216
|
-
self,
|
217
|
-
name: str,
|
218
|
-
instructions: str | None,
|
219
|
-
tools_source_code: List[str],
|
220
|
-
tool_function_names: List[str],
|
221
|
-
dependencies: List[str],
|
222
|
-
log_level: str,
|
223
|
-
debug_mode: bool,
|
224
|
-
transport: Literal["stdio", "sse", "streamable-http"],
|
225
|
-
server_settings: Dict[str, Any],
|
226
|
-
) -> str:
|
227
|
-
"""
|
228
|
-
Generates the Python script content that will be run by the subprocess.
|
229
|
-
"""
|
230
|
-
import textwrap
|
231
|
-
|
232
|
-
# Properly dedent and format tool definitions
|
233
|
-
formatted_tools = []
|
234
|
-
for source in tools_source_code:
|
235
|
-
# Remove common leading whitespace to fix indentation
|
236
|
-
dedented_source = textwrap.dedent(source)
|
237
|
-
formatted_tools.append(dedented_source)
|
238
|
-
|
239
|
-
tool_definitions_str = "\n\n".join(formatted_tools)
|
240
|
-
|
241
|
-
tool_registrations_str = ""
|
242
|
-
for func_name in tool_function_names:
|
243
|
-
tool_registrations_str += f" server.add_tool({func_name})\n"
|
244
|
-
|
245
|
-
# Safely represent instructions string, handling None and quotes
|
246
|
-
if instructions is None:
|
247
|
-
instructions_repr = "None"
|
248
|
-
else:
|
249
|
-
escaped_instructions = instructions.replace("'''", "'''")
|
250
|
-
instructions_repr = f"'''{escaped_instructions}'''"
|
251
|
-
|
252
|
-
script_lines = [
|
253
|
-
"import sys",
|
254
|
-
"from mcp.server.fastmcp import FastMCP",
|
255
|
-
"from typing import Literal, List, Callable, Any, Dict",
|
256
|
-
"import anyio", # FastMCP.run might use it implicitly or explicitly
|
257
|
-
"import logging", # For basic logging within the script if needed
|
258
|
-
"",
|
259
|
-
"# Set up logger for tool functions",
|
260
|
-
"logger = logging.getLogger(__name__)",
|
261
|
-
"",
|
262
|
-
"# Import dependencies for UCP tool functions",
|
263
|
-
"try:",
|
264
|
-
" from eval_interface.ucp.search_client import UCPSearchClient",
|
265
|
-
" from eval_interface.ucp.types import SearchContentItem, KnowledgeBit",
|
266
|
-
"except ImportError as e:",
|
267
|
-
" logger.warning(f'Could not import UCP dependencies: {e}')",
|
268
|
-
" UCPSearchClient = None",
|
269
|
-
" SearchContentItem = None",
|
270
|
-
" KnowledgeBit = None",
|
271
|
-
"",
|
272
|
-
"# --- Tool Function Definitions ---",
|
273
|
-
tool_definitions_str,
|
274
|
-
"# --- End Tool Function Definitions ---",
|
275
|
-
"",
|
276
|
-
"def main():",
|
277
|
-
" # Configure basic logging for the runner script itself",
|
278
|
-
" log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'",
|
279
|
-
f" logging.basicConfig(level='{log_level.upper()}', format=log_format)",
|
280
|
-
" script_logger = logging.getLogger('mcp_runner_script')",
|
281
|
-
f' script_logger.info(f"MCP Runner script for {name!r} starting with transport {transport!r}")',
|
282
|
-
f" server_specific_settings = {server_settings!r}",
|
283
|
-
f" server = FastMCP(",
|
284
|
-
f" name={name!r},",
|
285
|
-
f" instructions={instructions_repr},",
|
286
|
-
f" dependencies={dependencies!r},",
|
287
|
-
f" log_level='{log_level.upper()}',",
|
288
|
-
f" debug={debug_mode},",
|
289
|
-
" **server_specific_settings",
|
290
|
-
" )",
|
291
|
-
"",
|
292
|
-
" script_logger.info('Registering tools...')",
|
293
|
-
tool_registrations_str,
|
294
|
-
" script_logger.info('Tools registered.')",
|
295
|
-
"",
|
296
|
-
f' script_logger.info(f"Starting FastMCP server {name!r} with transport {transport!r}...")',
|
297
|
-
f" server.run(transport={transport!r})", # mount_path for SSE is handled by FastMCP settings
|
298
|
-
f' script_logger.info(f"FastMCP server {name!r} has shut down.")',
|
299
|
-
"",
|
300
|
-
"if __name__ == '__main__':",
|
301
|
-
" main()",
|
302
|
-
]
|
303
|
-
return "\n".join(script_lines)
|
304
|
-
|
305
|
-
def _verify_process_started(self, process: subprocess.Popen, name: str) -> bool:
|
306
|
-
"""
|
307
|
-
Verify that the process started successfully and is running.
|
308
|
-
|
309
|
-
Args:
|
310
|
-
process: The subprocess to verify
|
311
|
-
name: Name of the server for logging
|
312
|
-
|
313
|
-
Returns:
|
314
|
-
True if process started successfully, False otherwise
|
315
|
-
"""
|
316
|
-
start_time = time.time()
|
317
|
-
|
318
|
-
while time.time() - start_time < self.process_startup_timeout:
|
319
|
-
poll_result = process.poll()
|
320
|
-
|
321
|
-
if poll_result is None:
|
322
|
-
# Process is still running - this is good
|
323
|
-
logger.debug(f"Server '{name}' (PID {process.pid}) is running")
|
324
|
-
return True
|
325
|
-
elif poll_result != 0:
|
326
|
-
# Process exited with error
|
327
|
-
stderr_output = ""
|
328
|
-
if process.stderr:
|
329
|
-
try:
|
330
|
-
stderr_output = process.stderr.read()
|
331
|
-
except Exception as e:
|
332
|
-
logger.warning(
|
333
|
-
f"Could not read stderr from failed process: {e}"
|
334
|
-
)
|
335
|
-
logger.error(
|
336
|
-
f"Server '{name}' (PID {process.pid}) failed to start. Exit code: {poll_result}. Stderr: {stderr_output}"
|
337
|
-
)
|
338
|
-
return False
|
339
|
-
|
340
|
-
# Give it a moment before checking again
|
341
|
-
time.sleep(0.1)
|
342
|
-
|
343
|
-
# Timeout reached
|
344
|
-
if process.poll() is None:
|
345
|
-
logger.warning(
|
346
|
-
f"Server '{name}' (PID {process.pid}) startup verification timed out after {self.process_startup_timeout}s, but process is still running"
|
347
|
-
)
|
348
|
-
return True
|
349
|
-
else:
|
350
|
-
logger.error(
|
351
|
-
f"Server '{name}' (PID {process.pid}) failed to start within {self.process_startup_timeout}s"
|
352
|
-
)
|
353
|
-
return False
|
354
|
-
|
355
|
-
def launch_server_process(
|
356
|
-
self,
|
357
|
-
name: str,
|
358
|
-
instructions: str | None,
|
359
|
-
tools: List[Callable],
|
360
|
-
dependencies: List[str],
|
361
|
-
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
362
|
-
debug_mode: bool,
|
363
|
-
transport: Literal["stdio", "sse", "streamable-http"],
|
364
|
-
server_settings: Dict[str, Any], # host, port etc. for sse/http
|
365
|
-
cwd: str | None = None,
|
366
|
-
) -> subprocess.Popen:
|
367
|
-
"""
|
368
|
-
Prepares and launches an MCP server in a subprocess.
|
369
|
-
"""
|
370
|
-
tools_source_code = []
|
371
|
-
tool_function_names = []
|
372
|
-
for tool_func in tools:
|
373
|
-
try:
|
374
|
-
# Attempt to get source. This is fragile for complex tools or non-file-based functions.
|
375
|
-
source = inspect.getsource(tool_func)
|
376
|
-
tools_source_code.append(source)
|
377
|
-
tool_function_names.append(tool_func.__name__)
|
378
|
-
except (TypeError, OSError) as e:
|
379
|
-
logger.error(
|
380
|
-
f"Could not get source for tool '{tool_func.__name__}': {e}. This tool will be SKIPPED."
|
381
|
-
)
|
382
|
-
continue
|
383
|
-
|
384
|
-
script_content = self._generate_runner_script(
|
385
|
-
name=name,
|
386
|
-
instructions=instructions,
|
387
|
-
tools_source_code=tools_source_code,
|
388
|
-
tool_function_names=tool_function_names,
|
389
|
-
dependencies=dependencies,
|
390
|
-
log_level=log_level,
|
391
|
-
debug_mode=debug_mode,
|
392
|
-
transport=transport,
|
393
|
-
server_settings=server_settings,
|
394
|
-
)
|
395
|
-
logger.debug(
|
396
|
-
f"Generated runner script for server '{name}' ({transport}):\n{script_content}"
|
397
|
-
)
|
398
|
-
|
399
|
-
process = None
|
400
|
-
try:
|
401
|
-
process = subprocess.Popen(
|
402
|
-
[self.python_executable, "-c", script_content],
|
403
|
-
cwd=cwd,
|
404
|
-
stdout=subprocess.PIPE,
|
405
|
-
stderr=subprocess.PIPE,
|
406
|
-
text=True, # Decode stdout/stderr as text
|
407
|
-
)
|
408
|
-
|
409
|
-
logger.info(f"Launched {transport} server '{name}' with PID {process.pid}.")
|
410
|
-
|
411
|
-
# Verify the process started successfully before adding to active_servers
|
412
|
-
if self._verify_process_started(process, name):
|
413
|
-
self.active_servers.append(process)
|
414
|
-
|
415
|
-
# Create server info
|
416
|
-
info = ServerInfo(
|
417
|
-
name=name,
|
418
|
-
transport=transport,
|
419
|
-
process=process,
|
420
|
-
host=server_settings.get("host"),
|
421
|
-
port=server_settings.get("port"),
|
422
|
-
)
|
423
|
-
self.server_info.append(info)
|
424
|
-
|
425
|
-
# Start output monitoring threads
|
426
|
-
stdout_thread = threading.Thread(
|
427
|
-
target=_monitor_process_output,
|
428
|
-
args=(process, name, "stdout"),
|
429
|
-
daemon=True,
|
430
|
-
)
|
431
|
-
stderr_thread = threading.Thread(
|
432
|
-
target=_monitor_process_output,
|
433
|
-
args=(process, name, "stderr"),
|
434
|
-
daemon=True,
|
435
|
-
)
|
436
|
-
stdout_thread.start()
|
437
|
-
stderr_thread.start()
|
438
|
-
self.output_threads.extend([stdout_thread, stderr_thread])
|
439
|
-
|
440
|
-
logger.info(
|
441
|
-
f"Server '{name}' (PID {process.pid}) verified as started successfully."
|
442
|
-
)
|
443
|
-
return process
|
444
|
-
else:
|
445
|
-
# Process failed to start properly, clean it up
|
446
|
-
logger.error(
|
447
|
-
f"Server '{name}' failed startup verification, cleaning up..."
|
448
|
-
)
|
449
|
-
self._cleanup_single_process(process, name, force_kill_timeout=2.0)
|
450
|
-
raise RuntimeError(f"Server '{name}' failed to start properly")
|
451
|
-
|
452
|
-
except Exception as e:
|
453
|
-
logger.critical(f"Failed to launch subprocess for server '{name}': {e}")
|
454
|
-
if process and process.poll() is None:
|
455
|
-
# If we created a process but had an error, clean it up
|
456
|
-
self._cleanup_single_process(process, name, force_kill_timeout=2.0)
|
457
|
-
raise
|
458
|
-
|
459
|
-
def _cleanup_single_process(
|
460
|
-
self, process: subprocess.Popen, name: str, force_kill_timeout: float = 5.0
|
461
|
-
):
|
462
|
-
"""
|
463
|
-
Clean up a single process with proper error handling.
|
464
|
-
|
465
|
-
Args:
|
466
|
-
process: The process to clean up
|
467
|
-
name: Name for logging
|
468
|
-
force_kill_timeout: How long to wait before force killing
|
469
|
-
"""
|
470
|
-
if process.poll() is not None:
|
471
|
-
logger.debug(f"Process '{name}' (PID {process.pid}) already terminated.")
|
472
|
-
return
|
473
|
-
|
474
|
-
logger.info(f"Terminating server process '{name}' (PID {process.pid})...")
|
475
|
-
try:
|
476
|
-
process.terminate() # Ask nicely first
|
477
|
-
try:
|
478
|
-
process.wait(
|
479
|
-
timeout=force_kill_timeout
|
480
|
-
) # Wait for graceful termination
|
481
|
-
logger.info(
|
482
|
-
f"Server process '{name}' (PID {process.pid}) terminated gracefully."
|
483
|
-
)
|
484
|
-
except subprocess.TimeoutExpired:
|
485
|
-
logger.warning(
|
486
|
-
f"Server process '{name}' (PID {process.pid}) did not terminate gracefully after {force_kill_timeout}s, killing."
|
487
|
-
)
|
488
|
-
process.kill() # Force kill
|
489
|
-
process.wait() # Ensure it's reaped
|
490
|
-
logger.info(f"Server process '{name}' (PID {process.pid}) killed.")
|
491
|
-
except Exception as e:
|
492
|
-
logger.error(
|
493
|
-
f"Error during cleanup of process '{name}' (PID {process.pid}): {e}"
|
494
|
-
)
|
495
|
-
|
496
|
-
def get_running_servers(self) -> List[subprocess.Popen]:
|
497
|
-
"""
|
498
|
-
Get a list of currently running server processes.
|
499
|
-
|
500
|
-
Returns:
|
501
|
-
List of running Popen objects
|
502
|
-
"""
|
503
|
-
running = []
|
504
|
-
for server_process in self.active_servers:
|
505
|
-
if server_process.poll() is None:
|
506
|
-
running.append(server_process)
|
507
|
-
return running
|
508
|
-
|
509
|
-
def cleanup_dead_servers(self):
|
510
|
-
"""
|
511
|
-
Remove dead processes from the active_servers list.
|
512
|
-
"""
|
513
|
-
original_count = len(self.active_servers)
|
514
|
-
self.active_servers = [
|
515
|
-
server for server in self.active_servers if server.poll() is None
|
516
|
-
]
|
517
|
-
self.server_info = [
|
518
|
-
info for info in self.server_info if info.process.poll() is None
|
519
|
-
]
|
520
|
-
cleaned_count = original_count - len(self.active_servers)
|
521
|
-
if cleaned_count > 0:
|
522
|
-
logger.info(
|
523
|
-
f"Cleaned up {cleaned_count} dead server process(es) from active list."
|
524
|
-
)
|
525
|
-
|
526
|
-
def shutdown_all(self, force_kill_timeout: float = 5.0):
|
527
|
-
"""
|
528
|
-
Shutdown all managed server processes.
|
529
|
-
|
530
|
-
Args:
|
531
|
-
force_kill_timeout: How long to wait before force killing each process
|
532
|
-
"""
|
533
|
-
if not self.active_servers:
|
534
|
-
logger.info("No active MCP server services to shut down.")
|
535
|
-
return
|
536
|
-
|
537
|
-
logger.info(
|
538
|
-
f"Shutting down {len(self.active_servers)} MCP server service(s)..."
|
539
|
-
)
|
540
|
-
|
541
|
-
# Create a copy of the list to iterate over, in case of concurrent modifications
|
542
|
-
servers_to_shutdown = self.active_servers.copy()
|
543
|
-
|
544
|
-
for server_process in servers_to_shutdown:
|
545
|
-
try:
|
546
|
-
# Try to extract server name from the process args for better logging
|
547
|
-
server_name = "unknown"
|
548
|
-
if hasattr(server_process, "args") and len(server_process.args) > 2:
|
549
|
-
# Try to extract name from the script content
|
550
|
-
script_snippet = (
|
551
|
-
server_process.args[2][:50]
|
552
|
-
if server_process.args[2]
|
553
|
-
else "unknown"
|
554
|
-
)
|
555
|
-
server_name = f"script:{script_snippet}..."
|
556
|
-
|
557
|
-
self._cleanup_single_process(
|
558
|
-
server_process, server_name, force_kill_timeout
|
559
|
-
)
|
560
|
-
except Exception as e:
|
561
|
-
logger.error(
|
562
|
-
f"Error shutting down server process (PID {server_process.pid}): {e}"
|
563
|
-
)
|
564
|
-
|
565
|
-
self.active_servers = []
|
566
|
-
self.server_info = []
|
567
|
-
self._keep_running = False
|
568
|
-
logger.info("All managed MCP server services shut down.")
|
569
|
-
|
570
|
-
def __enter__(self):
|
571
|
-
"""Context manager entry."""
|
572
|
-
return self
|
573
|
-
|
574
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
575
|
-
"""Context manager exit with cleanup."""
|
576
|
-
self.shutdown_all()
|
577
|
-
|
578
|
-
|
579
|
-
# Global singleton service instance - declared after class definition
|
580
|
-
_singleton_service: "MCPServerService | None" = None
|
581
|
-
|
582
|
-
|
583
|
-
# ------------------------------------------------------------------------------
|
584
|
-
# Singleton Server Service Management
|
585
|
-
# ------------------------------------------------------------------------------
|
586
|
-
|
587
|
-
|
588
|
-
def get_server_service() -> MCPServerService:
|
589
|
-
"""
|
590
|
-
Get the singleton MCPServerService instance.
|
591
|
-
Creates one if it doesn't exist.
|
592
|
-
|
593
|
-
Returns:
|
594
|
-
The singleton MCPServerService instance
|
595
|
-
"""
|
596
|
-
global _singleton_service
|
597
|
-
if _singleton_service is None:
|
598
|
-
_singleton_service = MCPServerService()
|
599
|
-
logger.debug("Created singleton MCPServerService instance")
|
600
|
-
return _singleton_service
|
601
|
-
|
602
|
-
|
603
|
-
def shutdown_all_servers(force_kill_timeout: float = 5.0):
|
604
|
-
"""
|
605
|
-
Shutdown all servers managed by the singleton service.
|
606
|
-
|
607
|
-
Args:
|
608
|
-
force_kill_timeout: How long to wait before force killing each process
|
609
|
-
"""
|
610
|
-
global _singleton_service
|
611
|
-
if _singleton_service is not None:
|
612
|
-
_singleton_service.shutdown_all(force_kill_timeout)
|
613
|
-
|
614
|
-
|
615
|
-
# ------------------------------------------------------------------------------
|
616
|
-
# Server Management Functions
|
617
|
-
# ------------------------------------------------------------------------------
|
618
|
-
|
619
|
-
|
620
|
-
def launch_mcp_servers(
|
621
|
-
servers: List[ServerSettings],
|
622
|
-
check_interval: float = 1.0,
|
623
|
-
auto_restart: bool = False,
|
624
|
-
) -> List[subprocess.Popen]:
|
625
|
-
"""
|
626
|
-
Launch multiple MCP servers with different configurations.
|
627
|
-
|
628
|
-
This provides a unified interface for launching and managing multiple MCP servers
|
629
|
-
with different transport types. It automatically provides a uvicorn-like experience
|
630
|
-
with proper startup messages and graceful shutdown.
|
631
|
-
|
632
|
-
Args:
|
633
|
-
servers: List of server configurations (MCPServerStdioSettings, MCPServerSseSettings,
|
634
|
-
or MCPServerStreamableHttpSettings)
|
635
|
-
check_interval: How often to check server health (seconds)
|
636
|
-
auto_restart: Whether to automatically restart failed servers
|
637
|
-
|
638
|
-
Returns:
|
639
|
-
List of subprocess.Popen objects for the launched servers
|
640
|
-
|
641
|
-
Example:
|
642
|
-
servers = [
|
643
|
-
MCPServerStdioSettings(
|
644
|
-
name="stdio_server",
|
645
|
-
instructions="A stdio server",
|
646
|
-
tools=[my_tool]
|
647
|
-
),
|
648
|
-
MCPServerSseSettings(
|
649
|
-
name="sse_server",
|
650
|
-
instructions="An SSE server",
|
651
|
-
tools=[another_tool],
|
652
|
-
start_port=8080
|
653
|
-
)
|
654
|
-
]
|
655
|
-
|
656
|
-
launch_mcp_servers(servers)
|
657
|
-
"""
|
658
|
-
if not servers:
|
659
|
-
logger.warning("No server configurations provided.")
|
660
|
-
return []
|
661
|
-
|
662
|
-
service = get_server_service()
|
663
|
-
processes = []
|
664
|
-
|
665
|
-
# Launch all servers
|
666
|
-
for config in servers:
|
667
|
-
try:
|
668
|
-
# Prepare server settings based on transport type
|
669
|
-
if isinstance(config, MCPServerSseSettings):
|
670
|
-
actual_port = find_next_free_port(config.start_port, config.host)
|
671
|
-
server_settings = {
|
672
|
-
"host": config.host,
|
673
|
-
"port": actual_port,
|
674
|
-
"mount_path": config.mount_path,
|
675
|
-
"sse_path": config.sse_path,
|
676
|
-
"message_path": config.message_path,
|
677
|
-
"json_response": config.json_response,
|
678
|
-
"stateless_http": config.stateless_http,
|
679
|
-
"warn_on_duplicate_resources": config.warn_on_duplicate_resources,
|
680
|
-
"warn_on_duplicate_tools": config.warn_on_duplicate_tools,
|
681
|
-
}
|
682
|
-
transport = "sse"
|
683
|
-
elif isinstance(config, MCPServerStreamableHttpSettings):
|
684
|
-
actual_port = find_next_free_port(config.start_port, config.host)
|
685
|
-
server_settings = {
|
686
|
-
"host": config.host,
|
687
|
-
"port": actual_port,
|
688
|
-
"mount_path": config.mount_path,
|
689
|
-
"streamable_http_path": config.streamable_http_path,
|
690
|
-
"json_response": config.json_response,
|
691
|
-
"stateless_http": config.stateless_http,
|
692
|
-
"warn_on_duplicate_resources": config.warn_on_duplicate_resources,
|
693
|
-
"warn_on_duplicate_tools": config.warn_on_duplicate_tools,
|
694
|
-
}
|
695
|
-
transport = "streamable-http"
|
696
|
-
else: # MCPServerStdioSettings
|
697
|
-
server_settings = {}
|
698
|
-
transport = "stdio"
|
699
|
-
|
700
|
-
# Launch the server
|
701
|
-
process = service.launch_server_process(
|
702
|
-
name=config.name,
|
703
|
-
instructions=config.instructions,
|
704
|
-
tools=config.tools,
|
705
|
-
dependencies=config.dependencies,
|
706
|
-
log_level=config.log_level,
|
707
|
-
debug_mode=config.debug_mode,
|
708
|
-
transport=transport,
|
709
|
-
server_settings=server_settings,
|
710
|
-
cwd=config.cwd,
|
711
|
-
)
|
712
|
-
processes.append(process)
|
713
|
-
|
714
|
-
except Exception as e:
|
715
|
-
logger.error(f"Failed to launch server '{config.name}': {e}")
|
716
|
-
|
717
|
-
# Display startup information
|
718
|
-
if service.server_info:
|
719
|
-
print("\n" + "=" * 60)
|
720
|
-
print("MCP Server Manager")
|
721
|
-
print("=" * 60)
|
722
|
-
|
723
|
-
for info in service.server_info:
|
724
|
-
print(f"\n✓ {info.name} ({info.transport})")
|
725
|
-
print(f" PID: {info.process.pid}")
|
726
|
-
if info.url:
|
727
|
-
print(f" URL: {info.url}")
|
728
|
-
|
729
|
-
print("\n" + "=" * 60)
|
730
|
-
print("Press CTRL+C to shutdown all servers")
|
731
|
-
print("=" * 60 + "\n")
|
732
|
-
|
733
|
-
# Keep servers running
|
734
|
-
try:
|
735
|
-
while service._keep_running:
|
736
|
-
# Check for dead servers
|
737
|
-
dead_servers = []
|
738
|
-
for info in service.server_info:
|
739
|
-
if info.process.poll() is not None:
|
740
|
-
dead_servers.append(info)
|
741
|
-
logger.error(
|
742
|
-
f"Server '{info.name}' (PID {info.process.pid}) "
|
743
|
-
f"exited with code {info.process.poll()}"
|
744
|
-
)
|
745
|
-
|
746
|
-
# Clean up dead servers
|
747
|
-
if dead_servers:
|
748
|
-
service.cleanup_dead_servers()
|
749
|
-
|
750
|
-
# If auto_restart is enabled, restart dead servers
|
751
|
-
if auto_restart:
|
752
|
-
for dead_info in dead_servers:
|
753
|
-
# Find the original config
|
754
|
-
for config in servers:
|
755
|
-
if config.name == dead_info.name:
|
756
|
-
logger.info(f"Restarting server '{config.name}'...")
|
757
|
-
try:
|
758
|
-
# Re-launch the server with same config
|
759
|
-
launch_mcp_servers(
|
760
|
-
[config],
|
761
|
-
check_interval=check_interval,
|
762
|
-
auto_restart=False,
|
763
|
-
)
|
764
|
-
except Exception as e:
|
765
|
-
logger.error(
|
766
|
-
f"Failed to restart server '{config.name}': {e}"
|
767
|
-
)
|
768
|
-
break
|
769
|
-
|
770
|
-
# If all servers are dead and no auto-restart, exit
|
771
|
-
if not service.server_info and not auto_restart:
|
772
|
-
logger.error("All servers have exited. Shutting down.")
|
773
|
-
break
|
774
|
-
|
775
|
-
time.sleep(check_interval)
|
776
|
-
|
777
|
-
except KeyboardInterrupt:
|
778
|
-
print("\n\nShutting down servers...")
|
779
|
-
finally:
|
780
|
-
# Ensure cleanup happens
|
781
|
-
shutdown_all_servers()
|
782
|
-
print("All servers stopped.")
|
783
|
-
|
784
|
-
return processes
|
785
|
-
|
786
|
-
|
787
|
-
def _is_main_module() -> bool:
|
788
|
-
"""Check if we're running as the main module."""
|
789
|
-
import __main__
|
790
|
-
import sys
|
791
|
-
|
792
|
-
# Check if we're in an interactive session
|
793
|
-
if hasattr(__main__, "__file__"):
|
794
|
-
# We have a file, check if it's being run directly
|
795
|
-
try:
|
796
|
-
# Get the calling frame
|
797
|
-
import inspect
|
798
|
-
|
799
|
-
frame = inspect.currentframe()
|
800
|
-
if frame and frame.f_back and frame.f_back.f_back:
|
801
|
-
caller_frame = frame.f_back.f_back
|
802
|
-
return caller_frame.f_globals.get("__name__") == "__main__"
|
803
|
-
except:
|
804
|
-
pass
|
805
|
-
return False
|
806
|
-
|
807
|
-
|
808
|
-
def _auto_keep_running_if_main():
|
809
|
-
"""Automatically keep servers running if this is the main module."""
|
810
|
-
if _is_main_module():
|
811
|
-
service = get_server_service()
|
812
|
-
if service.server_info:
|
813
|
-
# Schedule keep_servers_running to run after a brief delay
|
814
|
-
# This allows all launch calls to complete first
|
815
|
-
import threading
|
816
|
-
|
817
|
-
timer = threading.Timer(0.1, keep_servers_running)
|
818
|
-
timer.daemon = False
|
819
|
-
timer.start()
|
820
|
-
|
821
|
-
|
822
|
-
def keep_servers_running(check_interval: float = 1.0) -> None:
|
823
|
-
"""
|
824
|
-
Keep the main process running and monitor all launched servers.
|
825
|
-
|
826
|
-
This provides a uvicorn-like experience where the script keeps running
|
827
|
-
until interrupted with Ctrl+C. It displays startup information and
|
828
|
-
monitors server health.
|
829
|
-
|
830
|
-
Args:
|
831
|
-
check_interval: How often to check server health (seconds)
|
832
|
-
"""
|
833
|
-
service = get_server_service()
|
834
|
-
|
835
|
-
if not service.server_info:
|
836
|
-
logger.warning("No servers have been launched. Call launch_* functions first.")
|
837
|
-
return
|
838
|
-
|
839
|
-
# Display startup information
|
840
|
-
print("\n" + "=" * 60)
|
841
|
-
print("MCP Server Manager")
|
842
|
-
print("=" * 60)
|
843
|
-
|
844
|
-
for info in service.server_info:
|
845
|
-
print(f"\n✓ {info.name} ({info.transport})")
|
846
|
-
print(f" PID: {info.process.pid}")
|
847
|
-
if info.url:
|
848
|
-
print(f" URL: {info.url}")
|
849
|
-
|
850
|
-
print("\n" + "=" * 60)
|
851
|
-
print("Press CTRL+C to shutdown all servers")
|
852
|
-
print("=" * 60 + "\n")
|
853
|
-
|
854
|
-
try:
|
855
|
-
while service._keep_running:
|
856
|
-
# Check for dead servers
|
857
|
-
dead_servers = []
|
858
|
-
for info in service.server_info:
|
859
|
-
if info.process.poll() is not None:
|
860
|
-
dead_servers.append(info)
|
861
|
-
logger.error(
|
862
|
-
f"Server '{info.name}' (PID {info.process.pid}) "
|
863
|
-
f"exited with code {info.process.poll()}"
|
864
|
-
)
|
865
|
-
|
866
|
-
# Clean up dead servers
|
867
|
-
if dead_servers:
|
868
|
-
service.cleanup_dead_servers()
|
869
|
-
|
870
|
-
# If all servers are dead, exit
|
871
|
-
if not service.server_info:
|
872
|
-
logger.error("All servers have exited. Shutting down.")
|
873
|
-
break
|
874
|
-
|
875
|
-
time.sleep(check_interval)
|
876
|
-
|
877
|
-
except KeyboardInterrupt:
|
878
|
-
print("\n\nShutting down servers...")
|
879
|
-
finally:
|
880
|
-
# Ensure cleanup happens
|
881
|
-
shutdown_all_servers()
|
882
|
-
print("All servers stopped.")
|
883
|
-
|
884
|
-
|
885
|
-
# ------------------------------------------------------------------------------
|
886
|
-
# Simplified Launch Functions (No Service Manager Required)
|
887
|
-
# ------------------------------------------------------------------------------
|
888
|
-
|
889
|
-
|
890
|
-
def launch_stdio_mcp_server(
|
891
|
-
name: str,
|
892
|
-
instructions: str | None = None,
|
893
|
-
tools: List[Callable] = None,
|
894
|
-
dependencies: List[str] = None,
|
895
|
-
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
|
896
|
-
debug_mode: bool = False,
|
897
|
-
cwd: str | None = None,
|
898
|
-
auto_keep_running: bool = True,
|
899
|
-
) -> subprocess.Popen:
|
900
|
-
"""
|
901
|
-
Quickly launches an MCP server using FastMCP with stdio transport in a subprocess.
|
902
|
-
|
903
|
-
The server is automatically managed by a singleton service instance.
|
904
|
-
|
905
|
-
Args:
|
906
|
-
name: Name of the MCP server.
|
907
|
-
instructions: Optional instructions for the MCP server.
|
908
|
-
tools: List of tool functions to register. (Caveats apply, see MCPServerService).
|
909
|
-
dependencies: List of dependencies for FastMCP.
|
910
|
-
log_level: Logging level for the server.
|
911
|
-
debug_mode: Whether to run FastMCP in debug mode.
|
912
|
-
cwd: Optional current working directory for the subprocess.
|
913
|
-
|
914
|
-
Returns:
|
915
|
-
The Popen object for the launched subprocess.
|
916
|
-
"""
|
917
|
-
if tools is None:
|
918
|
-
tools = []
|
919
|
-
if dependencies is None:
|
920
|
-
dependencies = []
|
921
|
-
|
922
|
-
logger.info(f"Preparing to launch STDIN/OUT MCP Server: {name}")
|
923
|
-
service = get_server_service()
|
924
|
-
process = service.launch_server_process(
|
925
|
-
name=name,
|
926
|
-
instructions=instructions,
|
927
|
-
tools=tools,
|
928
|
-
dependencies=dependencies,
|
929
|
-
log_level=log_level,
|
930
|
-
debug_mode=debug_mode,
|
931
|
-
transport="stdio",
|
932
|
-
server_settings={},
|
933
|
-
cwd=cwd,
|
934
|
-
)
|
935
|
-
|
936
|
-
# Auto-start keep_servers_running if this is the main module
|
937
|
-
_auto_keep_running_if_main()
|
938
|
-
|
939
|
-
return process
|
940
|
-
|
941
|
-
|
942
|
-
def launch_sse_mcp_server(
|
943
|
-
name: str,
|
944
|
-
instructions: str | None = None,
|
945
|
-
host: str = "127.0.0.1",
|
946
|
-
start_port: int = 8000,
|
947
|
-
mount_path: str = "/",
|
948
|
-
sse_path: str = "/sse",
|
949
|
-
message_path: str = "/messages/",
|
950
|
-
json_response: bool = False,
|
951
|
-
stateless_http: bool = False,
|
952
|
-
warn_on_duplicate_resources: bool = True,
|
953
|
-
warn_on_duplicate_tools: bool = True,
|
954
|
-
tools: List[Callable] = None,
|
955
|
-
dependencies: List[str] = None,
|
956
|
-
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
|
957
|
-
debug_mode: bool = False,
|
958
|
-
cwd: str | None = None,
|
959
|
-
) -> subprocess.Popen:
|
960
|
-
"""
|
961
|
-
Quickly launches an MCP server using FastMCP with SSE transport in a subprocess.
|
962
|
-
It will find the next available port starting from `start_port`.
|
963
|
-
|
964
|
-
The server is automatically managed by a singleton service instance.
|
965
|
-
|
966
|
-
Args:
|
967
|
-
name: Name of the MCP server.
|
968
|
-
instructions: Optional instructions for the MCP server.
|
969
|
-
host: Host for the SSE server.
|
970
|
-
start_port: Port number to start searching for a free port.
|
971
|
-
mount_path: Mount path for the Starlette application in FastMCP.
|
972
|
-
sse_path: SSE endpoint path within FastMCP settings.
|
973
|
-
message_path: Message endpoint path for SSE, within FastMCP settings.
|
974
|
-
warn_on_duplicate_resources: FastMCP setting.
|
975
|
-
warn_on_duplicate_tools: FastMCP setting.
|
976
|
-
tools: List of tool functions to register. (Caveats apply, see MCPServerService).
|
977
|
-
dependencies: List of dependencies for FastMCP.
|
978
|
-
log_level: Logging level for the server.
|
979
|
-
debug_mode: Whether to run FastMCP in debug mode.
|
980
|
-
cwd: Optional current working directory for the subprocess.
|
981
|
-
|
982
|
-
Returns:
|
983
|
-
The Popen object for the launched subprocess.
|
984
|
-
"""
|
985
|
-
if tools is None:
|
986
|
-
tools = []
|
987
|
-
if dependencies is None:
|
988
|
-
dependencies = []
|
989
|
-
|
990
|
-
actual_port = find_next_free_port(start_port, host)
|
991
|
-
logger.info(f"Preparing to launch SSE MCP Server: {name} on {host}:{actual_port}")
|
992
|
-
|
993
|
-
server_http_settings = {
|
994
|
-
"host": host,
|
995
|
-
"port": actual_port,
|
996
|
-
"mount_path": mount_path,
|
997
|
-
"sse_path": sse_path,
|
998
|
-
"message_path": message_path,
|
999
|
-
"json_response": json_response,
|
1000
|
-
"stateless_http": stateless_http,
|
1001
|
-
"warn_on_duplicate_resources": warn_on_duplicate_resources,
|
1002
|
-
"warn_on_duplicate_tools": warn_on_duplicate_tools,
|
1003
|
-
}
|
1004
|
-
|
1005
|
-
service = get_server_service()
|
1006
|
-
process = service.launch_server_process(
|
1007
|
-
name=name,
|
1008
|
-
instructions=instructions,
|
1009
|
-
tools=tools,
|
1010
|
-
dependencies=dependencies,
|
1011
|
-
log_level=log_level,
|
1012
|
-
debug_mode=debug_mode,
|
1013
|
-
transport="sse",
|
1014
|
-
server_settings=server_http_settings,
|
1015
|
-
cwd=cwd,
|
1016
|
-
)
|
1017
|
-
|
1018
|
-
# Auto-start keep_servers_running if this is the main module
|
1019
|
-
_auto_keep_running_if_main()
|
1020
|
-
|
1021
|
-
return process
|
1022
|
-
|
1023
|
-
|
1024
|
-
def launch_streamable_http_mcp_server(
|
1025
|
-
name: str,
|
1026
|
-
instructions: str | None = None,
|
1027
|
-
host: str = "127.0.0.1",
|
1028
|
-
start_port: int = 8000,
|
1029
|
-
mount_path: str = "/",
|
1030
|
-
streamable_http_path: str = "/mcp",
|
1031
|
-
json_response: bool = False,
|
1032
|
-
stateless_http: bool = False,
|
1033
|
-
warn_on_duplicate_resources: bool = True,
|
1034
|
-
warn_on_duplicate_tools: bool = True,
|
1035
|
-
tools: List[Callable] = None,
|
1036
|
-
dependencies: List[str] = None,
|
1037
|
-
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
|
1038
|
-
debug_mode: bool = False,
|
1039
|
-
cwd: str | None = None,
|
1040
|
-
) -> subprocess.Popen:
|
1041
|
-
"""
|
1042
|
-
Quickly launches an MCP server using FastMCP with StreamableHTTP transport in a subprocess.
|
1043
|
-
It will find the next available port starting from `start_port`.
|
1044
|
-
|
1045
|
-
The server is automatically managed by a singleton service instance.
|
1046
|
-
|
1047
|
-
Args:
|
1048
|
-
name: Name of the MCP server.
|
1049
|
-
instructions: Optional instructions for the MCP server.
|
1050
|
-
host: Host for the StreamableHTTP server.
|
1051
|
-
start_port: Port number to start searching for a free port.
|
1052
|
-
mount_path: Mount path for the root Starlette application in FastMCP.
|
1053
|
-
The StreamableHTTP endpoint will be relative to this.
|
1054
|
-
streamable_http_path: The specific path for the StreamableHTTP MCP endpoint.
|
1055
|
-
json_response: FastMCP setting for StreamableHTTP to send JSON responses.
|
1056
|
-
stateless_http: FastMCP setting for StreamableHTTP true stateless mode.
|
1057
|
-
warn_on_duplicate_resources: FastMCP setting.
|
1058
|
-
warn_on_duplicate_tools: FastMCP setting.
|
1059
|
-
tools: List of tool functions to register. (Caveats apply, see MCPServerService).
|
1060
|
-
dependencies: List of dependencies for FastMCP.
|
1061
|
-
log_level: Logging level for the server.
|
1062
|
-
debug_mode: Whether to run FastMCP in debug mode.
|
1063
|
-
cwd: Optional current working directory for the subprocess.
|
1064
|
-
|
1065
|
-
Returns:
|
1066
|
-
The Popen object for the launched subprocess.
|
1067
|
-
"""
|
1068
|
-
if tools is None:
|
1069
|
-
tools = []
|
1070
|
-
if dependencies is None:
|
1071
|
-
dependencies = []
|
1072
|
-
|
1073
|
-
actual_port = find_next_free_port(start_port, host)
|
1074
|
-
logger.info(
|
1075
|
-
f"Preparing to launch StreamableHTTP MCP Server: {name} on {host}:{actual_port}"
|
1076
|
-
)
|
1077
|
-
|
1078
|
-
server_http_settings = {
|
1079
|
-
"host": host,
|
1080
|
-
"port": actual_port,
|
1081
|
-
"mount_path": mount_path,
|
1082
|
-
"streamable_http_path": streamable_http_path,
|
1083
|
-
"json_response": json_response,
|
1084
|
-
"stateless_http": stateless_http,
|
1085
|
-
"warn_on_duplicate_resources": warn_on_duplicate_resources,
|
1086
|
-
"warn_on_duplicate_tools": warn_on_duplicate_tools,
|
1087
|
-
}
|
1088
|
-
|
1089
|
-
service = get_server_service()
|
1090
|
-
process = service.launch_server_process(
|
1091
|
-
name=name,
|
1092
|
-
instructions=instructions,
|
1093
|
-
tools=tools,
|
1094
|
-
dependencies=dependencies,
|
1095
|
-
log_level=log_level,
|
1096
|
-
debug_mode=debug_mode,
|
1097
|
-
transport="streamable-http",
|
1098
|
-
server_settings=server_http_settings,
|
1099
|
-
cwd=cwd,
|
1100
|
-
)
|
1101
|
-
|
1102
|
-
# Auto-start keep_servers_running if this is the main module
|
1103
|
-
_auto_keep_running_if_main()
|
1104
|
-
|
1105
|
-
return process
|
1106
|
-
|
1107
|
-
|
1108
|
-
# Example Usage (optional, for testing this module directly):
|
1109
|
-
if __name__ == "__main__":
|
1110
|
-
# Configure logging
|
1111
|
-
logging.basicConfig(
|
1112
|
-
level=logging.INFO,
|
1113
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
1114
|
-
)
|
1115
|
-
|
1116
|
-
logger.info("MCP Launcher Example - New Configuration-Based API")
|
1117
|
-
|
1118
|
-
# Dummy tool functions for testing
|
1119
|
-
def example_tool_one(param: str) -> str:
|
1120
|
-
"""Example tool that echoes input."""
|
1121
|
-
return f"Example tool one received: {param}"
|
1122
|
-
|
1123
|
-
import math
|
1124
|
-
|
1125
|
-
def example_tool_two(num: float) -> str:
|
1126
|
-
"""Example tool that calculates square root."""
|
1127
|
-
return f"Square root of {num} is {math.sqrt(num)}"
|
1128
|
-
|
1129
|
-
# Configure servers using the new settings classes
|
1130
|
-
servers = [
|
1131
|
-
# A stdio server
|
1132
|
-
MCPServerStdioSettings(
|
1133
|
-
name="MyStdioServer",
|
1134
|
-
instructions="This is a test stdio server.",
|
1135
|
-
tools=[example_tool_one, example_tool_two],
|
1136
|
-
log_level="DEBUG",
|
1137
|
-
debug_mode=True,
|
1138
|
-
),
|
1139
|
-
# An SSE server
|
1140
|
-
MCPServerSseSettings(
|
1141
|
-
name="MySSEServer",
|
1142
|
-
instructions="This is a test SSE server.",
|
1143
|
-
tools=[example_tool_one],
|
1144
|
-
start_port=8080,
|
1145
|
-
log_level="DEBUG",
|
1146
|
-
debug_mode=True,
|
1147
|
-
),
|
1148
|
-
# A StreamableHTTP server
|
1149
|
-
MCPServerStreamableHttpSettings(
|
1150
|
-
name="MyStreamableHTTPServer",
|
1151
|
-
instructions="This is a test StreamableHTTP server.",
|
1152
|
-
tools=[example_tool_two],
|
1153
|
-
start_port=9090,
|
1154
|
-
log_level="DEBUG",
|
1155
|
-
debug_mode=True,
|
1156
|
-
json_response=True,
|
1157
|
-
),
|
1158
|
-
]
|
1159
|
-
|
1160
|
-
# Launch all servers with unified interface
|
1161
|
-
launch_mcp_servers(servers, auto_restart=True)
|