devduck 0.1.0__py3-none-any.whl → 0.1.1766644714__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.
Potentially problematic release.
This version of devduck might be problematic. Click here for more details.
- devduck/__init__.py +1439 -483
- devduck/__main__.py +7 -0
- devduck/_version.py +34 -0
- devduck/agentcore_handler.py +76 -0
- devduck/test_redduck.py +0 -1
- devduck/tools/__init__.py +47 -0
- devduck/tools/_ambient_input.py +423 -0
- devduck/tools/_tray_app.py +530 -0
- devduck/tools/agentcore_agents.py +197 -0
- devduck/tools/agentcore_config.py +441 -0
- devduck/tools/agentcore_invoke.py +423 -0
- devduck/tools/agentcore_logs.py +320 -0
- devduck/tools/ambient.py +157 -0
- devduck/tools/create_subagent.py +659 -0
- devduck/tools/fetch_github_tool.py +201 -0
- devduck/tools/install_tools.py +409 -0
- devduck/tools/ipc.py +546 -0
- devduck/tools/mcp_server.py +600 -0
- devduck/tools/scraper.py +935 -0
- devduck/tools/speech_to_speech.py +850 -0
- devduck/tools/state_manager.py +292 -0
- devduck/tools/store_in_kb.py +187 -0
- devduck/tools/system_prompt.py +608 -0
- devduck/tools/tcp.py +263 -94
- devduck/tools/tray.py +247 -0
- devduck/tools/use_github.py +438 -0
- devduck/tools/websocket.py +498 -0
- devduck-0.1.1766644714.dist-info/METADATA +717 -0
- devduck-0.1.1766644714.dist-info/RECORD +33 -0
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/entry_points.txt +1 -0
- devduck-0.1.1766644714.dist-info/licenses/LICENSE +201 -0
- devduck/install.sh +0 -42
- devduck-0.1.0.dist-info/METADATA +0 -106
- devduck-0.1.0.dist-info/RECORD +0 -11
- devduck-0.1.0.dist-info/licenses/LICENSE +0 -21
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/WHEEL +0 -0
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""MCP Server Tool for DevDuck.
|
|
2
|
+
|
|
3
|
+
Transforms DevDuck into an MCP (Model Context Protocol) server, exposing devduck
|
|
4
|
+
tools and capabilities to any MCP-compatible client (Claude Desktop, other agents, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from strands import tool
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# MCP imports with error handling
|
|
20
|
+
MCP_IMPORT_ERROR = ""
|
|
21
|
+
try:
|
|
22
|
+
import uvicorn
|
|
23
|
+
from mcp import types
|
|
24
|
+
from mcp.server.lowlevel import Server
|
|
25
|
+
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
26
|
+
from starlette.applications import Starlette
|
|
27
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
28
|
+
from starlette.routing import Mount
|
|
29
|
+
from starlette.types import Receive, Scope, Send
|
|
30
|
+
|
|
31
|
+
HAS_MCP = True
|
|
32
|
+
except ImportError as e:
|
|
33
|
+
HAS_MCP = False
|
|
34
|
+
MCP_IMPORT_ERROR = str(e)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Global state to track MCP servers
|
|
38
|
+
_server_state = {
|
|
39
|
+
"servers": {}, # Map of server_id -> server_info
|
|
40
|
+
"default_server": None, # ID of the default server
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@tool
|
|
45
|
+
def mcp_server(
|
|
46
|
+
action: str,
|
|
47
|
+
server_id: str = "default",
|
|
48
|
+
transport: str = "http",
|
|
49
|
+
port: int = 8000,
|
|
50
|
+
tools: Optional[list[str]] = None,
|
|
51
|
+
expose_agent: bool = True,
|
|
52
|
+
stateless: bool = False,
|
|
53
|
+
agent: Any = None,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
"""Turn devduck into an MCP server, exposing tools as MCP tools.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
action: Action to perform - "start", "stop", "status", "list"
|
|
59
|
+
server_id: Unique identifier for this server instance (default: "default")
|
|
60
|
+
transport: Transport type - "http" (StreamableHTTP, background) or "stdio" (foreground, blocking)
|
|
61
|
+
port: Port for HTTP server (only used when transport="http", default: 8000)
|
|
62
|
+
tools: Optional list of tool names to expose. If None, exposes all tools except mcp_server itself
|
|
63
|
+
expose_agent: Whether to expose "devduck" tool for full agent conversations (default: True)
|
|
64
|
+
stateless: If True, creates fresh transport per request with no session state (default: False)
|
|
65
|
+
agent: Parent agent instance (auto-injected by Strands framework)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Result dictionary with status and content
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# Check if MCP is installed
|
|
72
|
+
if not HAS_MCP:
|
|
73
|
+
return {
|
|
74
|
+
"status": "error",
|
|
75
|
+
"content": [
|
|
76
|
+
{
|
|
77
|
+
"text": f"❌ MCP not installed: {MCP_IMPORT_ERROR}\n\n"
|
|
78
|
+
f"Install with: pip install strands-mcp-server"
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Route to appropriate handler
|
|
84
|
+
if action == "start":
|
|
85
|
+
return _start_mcp_server(
|
|
86
|
+
server_id, transport, port, tools, expose_agent, stateless, agent
|
|
87
|
+
)
|
|
88
|
+
elif action == "stop":
|
|
89
|
+
return _stop_mcp_server(server_id)
|
|
90
|
+
elif action == "status":
|
|
91
|
+
return _get_mcp_status()
|
|
92
|
+
elif action == "list":
|
|
93
|
+
return _list_mcp_servers()
|
|
94
|
+
else:
|
|
95
|
+
return {
|
|
96
|
+
"status": "error",
|
|
97
|
+
"content": [
|
|
98
|
+
{
|
|
99
|
+
"text": f"❌ Unknown action: {action}\n\nValid actions: start, stop, status, list"
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.exception("MCP server tool error")
|
|
106
|
+
return {
|
|
107
|
+
"status": "error",
|
|
108
|
+
"content": [{"text": f"❌ Error: {str(e)}\n\n{traceback.format_exc()}"}],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _start_mcp_server(
|
|
113
|
+
server_id: str,
|
|
114
|
+
transport: str,
|
|
115
|
+
port: int,
|
|
116
|
+
tools_filter: Optional[list[str]],
|
|
117
|
+
expose_agent: bool,
|
|
118
|
+
stateless: bool,
|
|
119
|
+
agent: Any,
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
"""Start an MCP server exposing devduck tools."""
|
|
122
|
+
if server_id in _server_state["servers"]:
|
|
123
|
+
return {
|
|
124
|
+
"status": "error",
|
|
125
|
+
"content": [{"text": f"❌ Server '{server_id}' is already running"}],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if not agent:
|
|
129
|
+
return {
|
|
130
|
+
"status": "error",
|
|
131
|
+
"content": [{"text": "❌ Tool context not available"}],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Get all agent tools
|
|
135
|
+
all_tools = agent.tool_registry.get_all_tools_config()
|
|
136
|
+
if not all_tools:
|
|
137
|
+
return {"status": "error", "content": [{"text": "❌ No tools found in agent"}]}
|
|
138
|
+
|
|
139
|
+
# Filter tools based on tools_filter parameter
|
|
140
|
+
if tools_filter:
|
|
141
|
+
agent_tools = {
|
|
142
|
+
name: spec
|
|
143
|
+
for name, spec in all_tools.items()
|
|
144
|
+
if name in tools_filter and name != "mcp_server"
|
|
145
|
+
}
|
|
146
|
+
if not agent_tools and not expose_agent:
|
|
147
|
+
return {
|
|
148
|
+
"status": "error",
|
|
149
|
+
"content": [
|
|
150
|
+
{
|
|
151
|
+
"text": f"❌ No matching tools found. Available: {list(all_tools.keys())}"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
}
|
|
155
|
+
else:
|
|
156
|
+
agent_tools = {
|
|
157
|
+
name: spec for name, spec in all_tools.items() if name != "mcp_server"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Creating MCP server with {len(agent_tools)} tools: {list(agent_tools.keys())}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
# Create low-level MCP server
|
|
166
|
+
server = Server(f"devduck-{server_id}")
|
|
167
|
+
|
|
168
|
+
# Create MCP Tool objects from agent tools
|
|
169
|
+
mcp_tools = []
|
|
170
|
+
for tool_name, tool_spec in agent_tools.items():
|
|
171
|
+
description = tool_spec.get("description", f"DevDuck tool: {tool_name}")
|
|
172
|
+
input_schema = {}
|
|
173
|
+
|
|
174
|
+
if "inputSchema" in tool_spec:
|
|
175
|
+
if "json" in tool_spec["inputSchema"]:
|
|
176
|
+
input_schema = tool_spec["inputSchema"]["json"]
|
|
177
|
+
else:
|
|
178
|
+
input_schema = tool_spec["inputSchema"]
|
|
179
|
+
|
|
180
|
+
mcp_tools.append(
|
|
181
|
+
types.Tool(
|
|
182
|
+
name=tool_name,
|
|
183
|
+
description=description,
|
|
184
|
+
inputSchema=input_schema,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Add agent invocation tool if requested
|
|
189
|
+
if expose_agent:
|
|
190
|
+
agent_invoke_tool = types.Tool(
|
|
191
|
+
name="devduck",
|
|
192
|
+
description=(
|
|
193
|
+
"Invoke a FULL DevDuck instance with complete capabilities. "
|
|
194
|
+
"Each invocation creates a fresh DevDuck agent with self-healing, "
|
|
195
|
+
"hot-reload, all tools, knowledge base integration, and system prompt building. "
|
|
196
|
+
"Use this for complex queries requiring reasoning, multi-tool orchestration, "
|
|
197
|
+
"or when you need the complete DevDuck experience via MCP."
|
|
198
|
+
),
|
|
199
|
+
inputSchema={
|
|
200
|
+
"type": "object",
|
|
201
|
+
"properties": {
|
|
202
|
+
"prompt": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"description": "The prompt or query to send to DevDuck",
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"required": ["prompt"],
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
mcp_tools.append(agent_invoke_tool)
|
|
211
|
+
|
|
212
|
+
logger.debug(
|
|
213
|
+
f"Created {len(mcp_tools)} MCP tools (agent invocation: {expose_agent})"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Capture transport in closure
|
|
217
|
+
_transport = transport
|
|
218
|
+
|
|
219
|
+
# Register list_tools handler
|
|
220
|
+
@server.list_tools()
|
|
221
|
+
async def list_tools() -> list[types.Tool]:
|
|
222
|
+
"""Return list of available MCP tools."""
|
|
223
|
+
logger.debug(f"list_tools called, returning {len(mcp_tools)} tools")
|
|
224
|
+
return mcp_tools
|
|
225
|
+
|
|
226
|
+
# Register call_tool handler
|
|
227
|
+
@server.call_tool()
|
|
228
|
+
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
229
|
+
"""Handle tool calls from MCP clients."""
|
|
230
|
+
try:
|
|
231
|
+
logger.debug(f"call_tool: name={name}, arguments={arguments}")
|
|
232
|
+
|
|
233
|
+
# Handle agent invocation tool - create a full DevDuck instance
|
|
234
|
+
if name == "devduck" and expose_agent:
|
|
235
|
+
prompt = arguments.get("prompt")
|
|
236
|
+
if not prompt:
|
|
237
|
+
return [
|
|
238
|
+
types.TextContent(
|
|
239
|
+
type="text",
|
|
240
|
+
text="❌ Error: 'prompt' parameter is required",
|
|
241
|
+
)
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
logger.debug(f"Invoking devduck with prompt: {prompt[:100]}...")
|
|
245
|
+
|
|
246
|
+
# Create a NEW DevDuck instance for this MCP invocation
|
|
247
|
+
# This gives full DevDuck power: self-healing, hot-reload, all tools, etc.
|
|
248
|
+
try:
|
|
249
|
+
from devduck import DevDuck
|
|
250
|
+
|
|
251
|
+
# Create fresh DevDuck instance (no auto-start to avoid recursion)
|
|
252
|
+
mcp_devduck = DevDuck(auto_start_servers=False)
|
|
253
|
+
mcp_agent = mcp_devduck.agent
|
|
254
|
+
|
|
255
|
+
if not mcp_agent:
|
|
256
|
+
return [
|
|
257
|
+
types.TextContent(
|
|
258
|
+
type="text",
|
|
259
|
+
text="❌ Error: Failed to create DevDuck instance",
|
|
260
|
+
)
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
# Execute with full DevDuck capabilities
|
|
264
|
+
result = mcp_agent(prompt)
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.error(f"DevDuck creation failed: {e}", exc_info=True)
|
|
268
|
+
return [
|
|
269
|
+
types.TextContent(
|
|
270
|
+
type="text",
|
|
271
|
+
text=f"❌ Error creating DevDuck instance: {str(e)}",
|
|
272
|
+
)
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
# Extract text response from agent result
|
|
276
|
+
response_text = str(result)
|
|
277
|
+
|
|
278
|
+
logger.debug(
|
|
279
|
+
f"DevDuck invocation complete, response length: {len(response_text)}"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return [types.TextContent(type="text", text=response_text)]
|
|
283
|
+
|
|
284
|
+
# Check if tool exists in agent
|
|
285
|
+
if name not in agent_tools:
|
|
286
|
+
return [
|
|
287
|
+
types.TextContent(type="text", text=f"❌ Unknown tool: {name}")
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
# Call the agent tool
|
|
291
|
+
tool_caller = getattr(agent.tool, name.replace("-", "_"))
|
|
292
|
+
|
|
293
|
+
# For stdio transport, try to pass non_interactive=True if the tool supports it
|
|
294
|
+
if _transport == "stdio":
|
|
295
|
+
try:
|
|
296
|
+
result = tool_caller(**arguments, non_interactive=True)
|
|
297
|
+
except TypeError:
|
|
298
|
+
logger.debug(
|
|
299
|
+
f"Tool '{name}' doesn't support non_interactive parameter"
|
|
300
|
+
)
|
|
301
|
+
result = tool_caller(**arguments)
|
|
302
|
+
else:
|
|
303
|
+
result = tool_caller(**arguments)
|
|
304
|
+
|
|
305
|
+
logger.debug(f"Tool '{name}' execution complete")
|
|
306
|
+
|
|
307
|
+
# Convert result to MCP TextContent format
|
|
308
|
+
mcp_content = []
|
|
309
|
+
if isinstance(result, dict) and "content" in result:
|
|
310
|
+
for item in result.get("content", []):
|
|
311
|
+
if isinstance(item, dict) and "text" in item:
|
|
312
|
+
mcp_content.append(
|
|
313
|
+
types.TextContent(type="text", text=item["text"])
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
mcp_content.append(types.TextContent(type="text", text=str(result)))
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
mcp_content
|
|
320
|
+
if mcp_content
|
|
321
|
+
else [
|
|
322
|
+
types.TextContent(
|
|
323
|
+
type="text", text="✅ Tool executed successfully"
|
|
324
|
+
)
|
|
325
|
+
]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.exception(f"Error calling tool '{name}'")
|
|
330
|
+
return [types.TextContent(type="text", text=f"❌ Error: {str(e)}")]
|
|
331
|
+
|
|
332
|
+
# Record server state
|
|
333
|
+
_server_state["servers"][server_id] = {
|
|
334
|
+
"server": server,
|
|
335
|
+
"transport": transport,
|
|
336
|
+
"port": port,
|
|
337
|
+
"stateless": stateless,
|
|
338
|
+
"tools": list(agent_tools.keys()),
|
|
339
|
+
"start_time": time.time(),
|
|
340
|
+
"status": "starting",
|
|
341
|
+
"expose_agent": expose_agent,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if _server_state["default_server"] is None:
|
|
345
|
+
_server_state["default_server"] = server_id
|
|
346
|
+
|
|
347
|
+
# For stdio transport: Run in FOREGROUND
|
|
348
|
+
if transport == "stdio":
|
|
349
|
+
logger.info(
|
|
350
|
+
f"Starting MCP server '{server_id}' in stdio mode (foreground, blocking)"
|
|
351
|
+
)
|
|
352
|
+
_server_state["servers"][server_id]["status"] = "running"
|
|
353
|
+
|
|
354
|
+
import asyncio
|
|
355
|
+
from mcp.server.stdio import stdio_server
|
|
356
|
+
|
|
357
|
+
async def run_stdio() -> None:
|
|
358
|
+
"""Run stdio server in foreground."""
|
|
359
|
+
async with stdio_server() as streams:
|
|
360
|
+
await server.run(
|
|
361
|
+
streams[0], streams[1], server.create_initialization_options()
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
asyncio.run(run_stdio())
|
|
365
|
+
|
|
366
|
+
if server_id in _server_state["servers"]:
|
|
367
|
+
del _server_state["servers"][server_id]
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
"status": "success",
|
|
371
|
+
"content": [{"text": f"✅ MCP server '{server_id}' stopped"}],
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# For http transport: Run in BACKGROUND
|
|
375
|
+
server_thread = threading.Thread(
|
|
376
|
+
target=_run_mcp_server,
|
|
377
|
+
args=(server, transport, port, stateless, server_id, len(mcp_tools)),
|
|
378
|
+
daemon=True,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
_server_state["servers"][server_id]["thread"] = server_thread
|
|
382
|
+
server_thread.start()
|
|
383
|
+
|
|
384
|
+
# Give server time to start
|
|
385
|
+
time.sleep(2)
|
|
386
|
+
|
|
387
|
+
# Check status
|
|
388
|
+
if server_id not in _server_state["servers"]:
|
|
389
|
+
return {
|
|
390
|
+
"status": "error",
|
|
391
|
+
"content": [{"text": f"❌ Server '{server_id}' failed to start"}],
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
server_info = _server_state["servers"][server_id]
|
|
395
|
+
if server_info["status"] == "error":
|
|
396
|
+
error_msg = server_info.get("error", "Unknown error")
|
|
397
|
+
return {
|
|
398
|
+
"status": "error",
|
|
399
|
+
"content": [{"text": f"❌ Server '{server_id}' failed: {error_msg}"}],
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
# Update to running
|
|
403
|
+
_server_state["servers"][server_id]["status"] = "running"
|
|
404
|
+
|
|
405
|
+
# Build status message
|
|
406
|
+
tool_list = "\n".join(f" • {tool.name}" for tool in mcp_tools[:10])
|
|
407
|
+
if len(mcp_tools) > 10:
|
|
408
|
+
tool_list += f"\n ... and {len(mcp_tools) - 10} more"
|
|
409
|
+
|
|
410
|
+
if expose_agent:
|
|
411
|
+
tool_list += "\n • devduck (full devduck invocation) ✨"
|
|
412
|
+
|
|
413
|
+
mode_desc = (
|
|
414
|
+
"stateless (multi-node ready)"
|
|
415
|
+
if stateless
|
|
416
|
+
else "stateful (session persistence)"
|
|
417
|
+
)
|
|
418
|
+
message = (
|
|
419
|
+
f"✅ MCP server '{server_id}' started on port {port}\n\n"
|
|
420
|
+
f"📊 Mode: {mode_desc}\n"
|
|
421
|
+
f"🔧 Exposed {len(mcp_tools)} tools:\n"
|
|
422
|
+
f"{tool_list}\n\n"
|
|
423
|
+
f"🔗 Connect at: http://localhost:{port}/mcp"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return {"status": "success", "content": [{"text": message}]}
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.exception("Error starting MCP server")
|
|
430
|
+
|
|
431
|
+
if server_id in _server_state["servers"]:
|
|
432
|
+
_server_state["servers"][server_id]["status"] = "error"
|
|
433
|
+
_server_state["servers"][server_id]["error"] = str(e)
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
"status": "error",
|
|
437
|
+
"content": [{"text": f"❌ Failed to start MCP server: {str(e)}"}],
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _run_mcp_server(
|
|
442
|
+
server: "Server",
|
|
443
|
+
transport: str,
|
|
444
|
+
port: int,
|
|
445
|
+
stateless: bool,
|
|
446
|
+
server_id: str,
|
|
447
|
+
tool_count: int,
|
|
448
|
+
) -> None:
|
|
449
|
+
"""Run MCP server in background thread."""
|
|
450
|
+
try:
|
|
451
|
+
logger.debug(
|
|
452
|
+
f"Starting MCP server: server_id={server_id}, transport={transport}, port={port}, stateless={stateless}"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if transport == "http":
|
|
456
|
+
session_manager = StreamableHTTPSessionManager(
|
|
457
|
+
app=server,
|
|
458
|
+
event_store=None,
|
|
459
|
+
json_response=False,
|
|
460
|
+
stateless=stateless,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
async def handle_streamable_http(
|
|
464
|
+
scope: Scope, receive: Receive, send: Send
|
|
465
|
+
) -> None:
|
|
466
|
+
await session_manager.handle_request(scope, receive, send)
|
|
467
|
+
|
|
468
|
+
@contextlib.asynccontextmanager
|
|
469
|
+
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
|
470
|
+
async with session_manager.run():
|
|
471
|
+
logger.info(
|
|
472
|
+
f"MCP server '{server_id}' running (stateless={stateless})"
|
|
473
|
+
)
|
|
474
|
+
try:
|
|
475
|
+
yield
|
|
476
|
+
finally:
|
|
477
|
+
logger.info(f"MCP server '{server_id}' shutting down...")
|
|
478
|
+
|
|
479
|
+
starlette_app = Starlette(
|
|
480
|
+
debug=True,
|
|
481
|
+
routes=[
|
|
482
|
+
Mount("/mcp", app=handle_streamable_http),
|
|
483
|
+
],
|
|
484
|
+
lifespan=lifespan,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
starlette_app = CORSMiddleware(
|
|
488
|
+
starlette_app,
|
|
489
|
+
allow_origins=["*"],
|
|
490
|
+
allow_methods=["GET", "POST", "DELETE"],
|
|
491
|
+
expose_headers=["Mcp-Session-Id"],
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
logger.debug(f"Starting Uvicorn server on 0.0.0.0:{port}")
|
|
495
|
+
uvicorn.run(starlette_app, host="0.0.0.0", port=port, log_level="info")
|
|
496
|
+
else:
|
|
497
|
+
logger.error(f"Unsupported transport: {transport}")
|
|
498
|
+
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.exception("Error in _run_mcp_server")
|
|
501
|
+
|
|
502
|
+
if server_id in _server_state["servers"]:
|
|
503
|
+
_server_state["servers"][server_id]["status"] = "error"
|
|
504
|
+
_server_state["servers"][server_id]["error"] = str(e)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _stop_mcp_server(server_id: str) -> dict[str, Any]:
|
|
508
|
+
"""Stop a running MCP server."""
|
|
509
|
+
if server_id not in _server_state["servers"]:
|
|
510
|
+
return {
|
|
511
|
+
"status": "error",
|
|
512
|
+
"content": [{"text": f"❌ Server '{server_id}' is not running"}],
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
server_info = _server_state["servers"][server_id]
|
|
516
|
+
server_info["status"] = "stopping"
|
|
517
|
+
|
|
518
|
+
del _server_state["servers"][server_id]
|
|
519
|
+
|
|
520
|
+
if _server_state["default_server"] == server_id:
|
|
521
|
+
_server_state["default_server"] = (
|
|
522
|
+
next(iter(_server_state["servers"])) if _server_state["servers"] else None
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
"status": "success",
|
|
527
|
+
"content": [{"text": f"✅ MCP server '{server_id}' stopped"}],
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _get_mcp_status() -> dict[str, Any]:
|
|
532
|
+
"""Get status of all MCP servers."""
|
|
533
|
+
if not _server_state["servers"]:
|
|
534
|
+
return {"status": "success", "content": [{"text": "ℹ️ No MCP servers running"}]}
|
|
535
|
+
|
|
536
|
+
lines = ["📡 **MCP Server Status**\n"]
|
|
537
|
+
|
|
538
|
+
for server_id, server_info in _server_state["servers"].items():
|
|
539
|
+
uptime = time.time() - server_info["start_time"]
|
|
540
|
+
uptime_str = f"{int(uptime // 60)}m {int(uptime % 60)}s"
|
|
541
|
+
|
|
542
|
+
default_marker = (
|
|
543
|
+
" (default)" if server_id == _server_state["default_server"] else ""
|
|
544
|
+
)
|
|
545
|
+
status_emoji = {
|
|
546
|
+
"running": "✅",
|
|
547
|
+
"starting": "🔄",
|
|
548
|
+
"stopping": "⏸️",
|
|
549
|
+
"error": "❌",
|
|
550
|
+
}.get(server_info["status"], "❓")
|
|
551
|
+
|
|
552
|
+
lines.append(f"\n**{server_id}{default_marker}**")
|
|
553
|
+
lines.append(f" • Status: {status_emoji} {server_info['status']}")
|
|
554
|
+
lines.append(f" • Transport: {server_info['transport']}")
|
|
555
|
+
|
|
556
|
+
if server_info["transport"] == "http":
|
|
557
|
+
lines.append(f" • Port: {server_info['port']}")
|
|
558
|
+
lines.append(f" • Connect: http://localhost:{server_info['port']}/mcp")
|
|
559
|
+
mode_type = (
|
|
560
|
+
"stateless (multi-node)"
|
|
561
|
+
if server_info.get("stateless", False)
|
|
562
|
+
else "stateful (single-node)"
|
|
563
|
+
)
|
|
564
|
+
lines.append(f" • Type: {mode_type}")
|
|
565
|
+
|
|
566
|
+
lines.append(f" • Uptime: {uptime_str}")
|
|
567
|
+
lines.append(f" • Tools: {len(server_info['tools'])} exposed")
|
|
568
|
+
|
|
569
|
+
if server_info.get("expose_agent"):
|
|
570
|
+
lines.append(f" • Agent Invocation: ✅ Enabled")
|
|
571
|
+
|
|
572
|
+
if server_info["status"] == "error" and "error" in server_info:
|
|
573
|
+
lines.append(f" • Error: {server_info['error']}")
|
|
574
|
+
|
|
575
|
+
return {"status": "success", "content": [{"text": "\n".join(lines)}]}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _list_mcp_servers() -> dict[str, Any]:
|
|
579
|
+
"""List running MCP servers."""
|
|
580
|
+
if not _server_state["servers"]:
|
|
581
|
+
return {"status": "success", "content": [{"text": "ℹ️ No MCP servers running"}]}
|
|
582
|
+
|
|
583
|
+
lines = ["📋 **Running MCP Servers**\n"]
|
|
584
|
+
|
|
585
|
+
for server_id, server_info in _server_state["servers"].items():
|
|
586
|
+
default_marker = (
|
|
587
|
+
" (default)" if server_id == _server_state["default_server"] else ""
|
|
588
|
+
)
|
|
589
|
+
mode_info = (
|
|
590
|
+
f"port {server_info['port']}"
|
|
591
|
+
if server_info["transport"] == "http"
|
|
592
|
+
else "stdio"
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
lines.append(
|
|
596
|
+
f"• {server_id}{default_marker}: {server_info['status']}, "
|
|
597
|
+
f"{server_info['transport']} ({mode_info})"
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return {"status": "success", "content": [{"text": "\n".join(lines)}]}
|