emdash-core 0.1.7__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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""MCP Server Manager for lifecycle and tool registry.
|
|
2
|
+
|
|
3
|
+
This module provides the MCPServerManager class that handles:
|
|
4
|
+
- Loading MCP config from .emdash/mcp.json
|
|
5
|
+
- Starting/stopping MCP servers
|
|
6
|
+
- Registering tools from all servers
|
|
7
|
+
- Handling tool name collisions
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import atexit
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from .client import GenericMCPClient, MCPResponse, MCPToolInfo, MCPError
|
|
15
|
+
from .config import MCPConfigFile, MCPServerConfig, get_default_mcp_config_path, ensure_mcp_config
|
|
16
|
+
from ...utils.logger import log
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MCPServerManager:
|
|
20
|
+
"""Manages lifecycle of multiple MCP servers.
|
|
21
|
+
|
|
22
|
+
This class is responsible for:
|
|
23
|
+
- Loading MCP server configurations from file
|
|
24
|
+
- Starting servers on demand (lazy initialization)
|
|
25
|
+
- Maintaining a unified tool registry from all servers
|
|
26
|
+
- Handling tool name collisions with prefixing
|
|
27
|
+
- Graceful shutdown of all servers
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
manager = MCPServerManager(config_path=Path(".emdash/mcp.json"))
|
|
31
|
+
|
|
32
|
+
# List all available tools from all servers
|
|
33
|
+
tools = manager.get_all_tools()
|
|
34
|
+
|
|
35
|
+
# Call a tool (server starts automatically if needed)
|
|
36
|
+
result = manager.call_tool("read_file", {"path": "/tmp/test.txt"})
|
|
37
|
+
|
|
38
|
+
# Cleanup
|
|
39
|
+
manager.shutdown_all()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config_path: Optional[Path] = None, repo_root: Optional[Path] = None):
|
|
43
|
+
"""Initialize the MCP server manager.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config_path: Path to mcp.json config file. If None, uses default.
|
|
47
|
+
repo_root: Repository root for default config path resolution.
|
|
48
|
+
"""
|
|
49
|
+
self.repo_root = repo_root or Path.cwd()
|
|
50
|
+
self.config_path = config_path or get_default_mcp_config_path(self.repo_root)
|
|
51
|
+
|
|
52
|
+
self._config: Optional[MCPConfigFile] = None
|
|
53
|
+
self._clients: dict[str, GenericMCPClient] = {}
|
|
54
|
+
self._tool_registry: dict[str, tuple[str, MCPToolInfo]] = {} # tool_name -> (server_name, tool_info)
|
|
55
|
+
self._started = False
|
|
56
|
+
|
|
57
|
+
# Register atexit handler for cleanup
|
|
58
|
+
atexit.register(self._cleanup)
|
|
59
|
+
|
|
60
|
+
def _cleanup(self) -> None:
|
|
61
|
+
"""Cleanup handler for atexit."""
|
|
62
|
+
try:
|
|
63
|
+
self.shutdown_all()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
log.warning(f"Error during MCP cleanup: {e}")
|
|
66
|
+
|
|
67
|
+
def load_config(self) -> MCPConfigFile:
|
|
68
|
+
"""Load MCP configuration from file, creating default if needed.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
MCPConfigFile with loaded server configurations
|
|
72
|
+
"""
|
|
73
|
+
if self._config is None:
|
|
74
|
+
self._config = ensure_mcp_config(self.config_path)
|
|
75
|
+
return self._config
|
|
76
|
+
|
|
77
|
+
def reload_config(self) -> MCPConfigFile:
|
|
78
|
+
"""Reload configuration from file.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
MCPConfigFile with fresh configuration
|
|
82
|
+
"""
|
|
83
|
+
self._config = MCPConfigFile.load(self.config_path)
|
|
84
|
+
return self._config
|
|
85
|
+
|
|
86
|
+
def save_config(self) -> None:
|
|
87
|
+
"""Save current configuration to file."""
|
|
88
|
+
if self._config:
|
|
89
|
+
self._config.save(self.config_path)
|
|
90
|
+
|
|
91
|
+
def get_enabled_servers(self) -> list[MCPServerConfig]:
|
|
92
|
+
"""Get list of enabled server configurations.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of enabled MCPServerConfig instances
|
|
96
|
+
"""
|
|
97
|
+
config = self.load_config()
|
|
98
|
+
return config.get_enabled_servers()
|
|
99
|
+
|
|
100
|
+
def _resolve_args(self, args: list[str], resolved_env: dict[str, str]) -> list[str]:
|
|
101
|
+
"""Resolve ${VAR} placeholders in args using resolved env.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
args: List of argument strings
|
|
105
|
+
resolved_env: Already-resolved environment variables
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of args with placeholders resolved
|
|
109
|
+
"""
|
|
110
|
+
import re
|
|
111
|
+
pattern = re.compile(r"\$\{([^}]+)\}")
|
|
112
|
+
resolved = []
|
|
113
|
+
for arg in args:
|
|
114
|
+
def replace_var(m):
|
|
115
|
+
var_name = m.group(1)
|
|
116
|
+
return resolved_env.get(var_name, "")
|
|
117
|
+
resolved.append(pattern.sub(replace_var, arg))
|
|
118
|
+
return resolved
|
|
119
|
+
|
|
120
|
+
def start_server(self, name: str) -> GenericMCPClient:
|
|
121
|
+
"""Start an MCP server by name.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Server name from configuration
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Started GenericMCPClient instance
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
MCPError: If server not found in config or fails to start
|
|
131
|
+
"""
|
|
132
|
+
# Check if already running
|
|
133
|
+
if name in self._clients and self._clients[name].is_running:
|
|
134
|
+
return self._clients[name]
|
|
135
|
+
|
|
136
|
+
config = self.load_config()
|
|
137
|
+
server_config = config.get_server(name)
|
|
138
|
+
|
|
139
|
+
if not server_config:
|
|
140
|
+
raise MCPError(f"MCP server '{name}' not found in configuration")
|
|
141
|
+
|
|
142
|
+
if not server_config.enabled:
|
|
143
|
+
raise MCPError(f"MCP server '{name}' is disabled")
|
|
144
|
+
|
|
145
|
+
# Resolve env vars first, then args
|
|
146
|
+
resolved_env = server_config.get_resolved_env()
|
|
147
|
+
resolved_args = self._resolve_args(server_config.args, resolved_env)
|
|
148
|
+
|
|
149
|
+
# Create and start client
|
|
150
|
+
client = GenericMCPClient(
|
|
151
|
+
name=server_config.name,
|
|
152
|
+
command=server_config.command,
|
|
153
|
+
args=resolved_args,
|
|
154
|
+
env=resolved_env,
|
|
155
|
+
timeout=server_config.timeout,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
client.start()
|
|
160
|
+
self._clients[name] = client
|
|
161
|
+
|
|
162
|
+
# Register tools from this server
|
|
163
|
+
self._register_server_tools(name, client)
|
|
164
|
+
|
|
165
|
+
return client
|
|
166
|
+
except Exception as e:
|
|
167
|
+
log.error(f"Failed to start MCP server '{name}': {e}")
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
def stop_server(self, name: str) -> bool:
|
|
171
|
+
"""Stop an MCP server by name.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
name: Server name
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
True if server was stopped, False if not running
|
|
178
|
+
"""
|
|
179
|
+
if name not in self._clients:
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
client = self._clients[name]
|
|
183
|
+
client.stop()
|
|
184
|
+
del self._clients[name]
|
|
185
|
+
|
|
186
|
+
# Unregister tools from this server
|
|
187
|
+
self._unregister_server_tools(name)
|
|
188
|
+
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
def start_all_enabled(self) -> list[str]:
|
|
192
|
+
"""Start all enabled MCP servers.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
List of server names that were started successfully
|
|
196
|
+
"""
|
|
197
|
+
started = []
|
|
198
|
+
for server_config in self.get_enabled_servers():
|
|
199
|
+
try:
|
|
200
|
+
self.start_server(server_config.name)
|
|
201
|
+
started.append(server_config.name)
|
|
202
|
+
except MCPError as e:
|
|
203
|
+
log.warning(f"Failed to start MCP server '{server_config.name}': {e}")
|
|
204
|
+
|
|
205
|
+
self._started = True
|
|
206
|
+
return started
|
|
207
|
+
|
|
208
|
+
def shutdown_all(self) -> None:
|
|
209
|
+
"""Stop all running MCP servers."""
|
|
210
|
+
for name in list(self._clients.keys()):
|
|
211
|
+
try:
|
|
212
|
+
self.stop_server(name)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
log.warning(f"Error stopping MCP server '{name}': {e}")
|
|
215
|
+
|
|
216
|
+
self._clients.clear()
|
|
217
|
+
self._tool_registry.clear()
|
|
218
|
+
self._started = False
|
|
219
|
+
|
|
220
|
+
def _register_server_tools(self, server_name: str, client: GenericMCPClient) -> None:
|
|
221
|
+
"""Register tools from a server into the unified registry.
|
|
222
|
+
|
|
223
|
+
Handles name collisions by prefixing with server name.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
server_name: Name of the server
|
|
227
|
+
client: Started client to get tools from
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
tools = client.list_tools()
|
|
231
|
+
except MCPError as e:
|
|
232
|
+
log.warning(f"Failed to list tools from '{server_name}': {e}")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
for tool in tools:
|
|
236
|
+
# Check for name collision
|
|
237
|
+
if tool.name in self._tool_registry:
|
|
238
|
+
existing_server, _ = self._tool_registry[tool.name]
|
|
239
|
+
# Use prefixed name for collision
|
|
240
|
+
prefixed_name = f"{server_name}_{tool.name}"
|
|
241
|
+
log.warning(
|
|
242
|
+
f"Tool name collision: '{tool.name}' exists in '{existing_server}'. "
|
|
243
|
+
f"Registering as '{prefixed_name}' for server '{server_name}'."
|
|
244
|
+
)
|
|
245
|
+
self._tool_registry[prefixed_name] = (server_name, tool)
|
|
246
|
+
else:
|
|
247
|
+
self._tool_registry[tool.name] = (server_name, tool)
|
|
248
|
+
|
|
249
|
+
log.info(f"Registered {len(tools)} tools from MCP server '{server_name}'")
|
|
250
|
+
|
|
251
|
+
def _unregister_server_tools(self, server_name: str) -> None:
|
|
252
|
+
"""Remove tools from a server from the registry.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
server_name: Name of the server
|
|
256
|
+
"""
|
|
257
|
+
to_remove = [
|
|
258
|
+
name for name, (srv, _) in self._tool_registry.items()
|
|
259
|
+
if srv == server_name
|
|
260
|
+
]
|
|
261
|
+
for name in to_remove:
|
|
262
|
+
del self._tool_registry[name]
|
|
263
|
+
|
|
264
|
+
def get_all_tools(self) -> list[tuple[str, str, MCPToolInfo]]:
|
|
265
|
+
"""Get all registered tools from all servers.
|
|
266
|
+
|
|
267
|
+
Starts all enabled servers if not already started.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of (tool_name, server_name, MCPToolInfo) tuples
|
|
271
|
+
"""
|
|
272
|
+
if not self._started:
|
|
273
|
+
self.start_all_enabled()
|
|
274
|
+
|
|
275
|
+
return [
|
|
276
|
+
(name, server_name, tool_info)
|
|
277
|
+
for name, (server_name, tool_info) in self._tool_registry.items()
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
def get_tool(self, name: str) -> Optional[tuple[str, MCPToolInfo]]:
|
|
281
|
+
"""Get a tool by name.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
name: Tool name (may be prefixed for collisions)
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
(server_name, MCPToolInfo) tuple or None if not found
|
|
288
|
+
"""
|
|
289
|
+
if not self._started:
|
|
290
|
+
self.start_all_enabled()
|
|
291
|
+
|
|
292
|
+
return self._tool_registry.get(name)
|
|
293
|
+
|
|
294
|
+
def call_tool(self, tool_name: str, arguments: dict) -> MCPResponse:
|
|
295
|
+
"""Call a tool by name.
|
|
296
|
+
|
|
297
|
+
Automatically routes to the correct server.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
tool_name: Tool name (may be prefixed)
|
|
301
|
+
arguments: Tool arguments
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
MCPResponse from the tool call
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
MCPError: If tool not found or call fails
|
|
308
|
+
"""
|
|
309
|
+
tool_info = self.get_tool(tool_name)
|
|
310
|
+
if not tool_info:
|
|
311
|
+
raise MCPError(f"Tool '{tool_name}' not found in any MCP server")
|
|
312
|
+
|
|
313
|
+
server_name, mcp_tool = tool_info
|
|
314
|
+
|
|
315
|
+
# Get the client, start if needed
|
|
316
|
+
if server_name not in self._clients:
|
|
317
|
+
self.start_server(server_name)
|
|
318
|
+
|
|
319
|
+
client = self._clients[server_name]
|
|
320
|
+
|
|
321
|
+
# Use original tool name (strip prefix if present)
|
|
322
|
+
original_name = mcp_tool.name
|
|
323
|
+
|
|
324
|
+
return client.call_tool(original_name, arguments)
|
|
325
|
+
|
|
326
|
+
def add_server(self, config: MCPServerConfig) -> None:
|
|
327
|
+
"""Add a new server to configuration.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
config: Server configuration to add
|
|
331
|
+
"""
|
|
332
|
+
self.load_config()
|
|
333
|
+
self._config.add_server(config)
|
|
334
|
+
self.save_config()
|
|
335
|
+
log.info(f"Added MCP server '{config.name}' to configuration")
|
|
336
|
+
|
|
337
|
+
def remove_server(self, name: str) -> bool:
|
|
338
|
+
"""Remove a server from configuration.
|
|
339
|
+
|
|
340
|
+
Also stops the server if running.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
name: Server name to remove
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
True if removed, False if not found
|
|
347
|
+
"""
|
|
348
|
+
# Stop if running
|
|
349
|
+
if name in self._clients:
|
|
350
|
+
self.stop_server(name)
|
|
351
|
+
|
|
352
|
+
self.load_config()
|
|
353
|
+
if self._config.remove_server(name):
|
|
354
|
+
self.save_config()
|
|
355
|
+
log.info(f"Removed MCP server '{name}' from configuration")
|
|
356
|
+
return True
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
def enable_server(self, name: str) -> bool:
|
|
360
|
+
"""Enable a server.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
name: Server name
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
True if enabled, False if not found
|
|
367
|
+
"""
|
|
368
|
+
self.load_config()
|
|
369
|
+
server = self._config.get_server(name)
|
|
370
|
+
if server:
|
|
371
|
+
server.enabled = True
|
|
372
|
+
self.save_config()
|
|
373
|
+
return True
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
def disable_server(self, name: str) -> bool:
|
|
377
|
+
"""Disable a server.
|
|
378
|
+
|
|
379
|
+
Also stops the server if running.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
name: Server name
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
True if disabled, False if not found
|
|
386
|
+
"""
|
|
387
|
+
# Stop if running
|
|
388
|
+
if name in self._clients:
|
|
389
|
+
self.stop_server(name)
|
|
390
|
+
|
|
391
|
+
self.load_config()
|
|
392
|
+
server = self._config.get_server(name)
|
|
393
|
+
if server:
|
|
394
|
+
server.enabled = False
|
|
395
|
+
self.save_config()
|
|
396
|
+
return True
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
def list_servers(self) -> list[dict]:
|
|
400
|
+
"""List all configured servers with their status.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
List of server info dicts with keys:
|
|
404
|
+
- name: Server name
|
|
405
|
+
- command: Server command
|
|
406
|
+
- enabled: Whether enabled
|
|
407
|
+
- running: Whether currently running
|
|
408
|
+
- tool_count: Number of tools (if running)
|
|
409
|
+
"""
|
|
410
|
+
self.load_config()
|
|
411
|
+
servers = []
|
|
412
|
+
|
|
413
|
+
for name, server_config in self._config.servers.items():
|
|
414
|
+
is_running = name in self._clients and self._clients[name].is_running
|
|
415
|
+
tool_count = len([
|
|
416
|
+
1 for _, (srv, _) in self._tool_registry.items()
|
|
417
|
+
if srv == name
|
|
418
|
+
]) if is_running else 0
|
|
419
|
+
|
|
420
|
+
servers.append({
|
|
421
|
+
"name": name,
|
|
422
|
+
"command": server_config.command,
|
|
423
|
+
"args": server_config.args,
|
|
424
|
+
"enabled": server_config.enabled,
|
|
425
|
+
"running": is_running,
|
|
426
|
+
"tool_count": tool_count,
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
return servers
|
|
430
|
+
|
|
431
|
+
def describe_server(self, name: str) -> Optional[dict]:
|
|
432
|
+
"""Get detailed information about a server including its tools.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
name: Server name
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Dict with server details and tools, or None if not found
|
|
439
|
+
"""
|
|
440
|
+
self.load_config()
|
|
441
|
+
server_config = self._config.get_server(name)
|
|
442
|
+
|
|
443
|
+
if not server_config:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
is_running = name in self._clients and self._clients[name].is_running
|
|
447
|
+
|
|
448
|
+
# Get tools for this server
|
|
449
|
+
tools = []
|
|
450
|
+
if is_running:
|
|
451
|
+
for tool_name, (srv, tool_info) in self._tool_registry.items():
|
|
452
|
+
if srv == name:
|
|
453
|
+
tools.append({
|
|
454
|
+
"name": tool_name,
|
|
455
|
+
"original_name": tool_info.name,
|
|
456
|
+
"description": tool_info.description,
|
|
457
|
+
"input_schema": tool_info.input_schema,
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
"name": name,
|
|
462
|
+
"command": server_config.command,
|
|
463
|
+
"args": server_config.args,
|
|
464
|
+
"env": server_config.env,
|
|
465
|
+
"enabled": server_config.enabled,
|
|
466
|
+
"timeout": server_config.timeout,
|
|
467
|
+
"running": is_running,
|
|
468
|
+
"tools": tools,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# Global manager instance (lazy initialization)
|
|
473
|
+
_manager: Optional[MCPServerManager] = None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def get_mcp_manager(config_path: Optional[Path] = None) -> MCPServerManager:
|
|
477
|
+
"""Get or create the global MCP manager instance.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
config_path: Optional path to override default config location
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
MCPServerManager instance
|
|
484
|
+
"""
|
|
485
|
+
global _manager
|
|
486
|
+
if _manager is None:
|
|
487
|
+
_manager = MCPServerManager(config_path=config_path)
|
|
488
|
+
return _manager
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def reset_mcp_manager() -> None:
|
|
492
|
+
"""Reset the global MCP manager (for testing)."""
|
|
493
|
+
global _manager
|
|
494
|
+
if _manager:
|
|
495
|
+
_manager.shutdown_all()
|
|
496
|
+
_manager = None
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Tool factory for creating BaseTool instances from MCP servers.
|
|
2
|
+
|
|
3
|
+
This module provides functions to dynamically create agent tools
|
|
4
|
+
from MCP server tool definitions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from ..tools.base import BaseTool, ToolResult, ToolCategory
|
|
11
|
+
from .manager import MCPServerManager
|
|
12
|
+
from .client import MCPToolInfo, MCPError
|
|
13
|
+
from ...utils.logger import log
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MCPDynamicTool(BaseTool):
|
|
17
|
+
"""A tool dynamically created from an MCP server tool definition.
|
|
18
|
+
|
|
19
|
+
This allows MCP tools to be used seamlessly alongside native tools
|
|
20
|
+
in the agent toolkit.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
mcp_manager: MCPServerManager,
|
|
26
|
+
tool_name: str,
|
|
27
|
+
tool_info: MCPToolInfo,
|
|
28
|
+
server_name: str,
|
|
29
|
+
connection: Any = None,
|
|
30
|
+
):
|
|
31
|
+
"""Initialize the dynamic tool.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
mcp_manager: MCP server manager for calling tools
|
|
35
|
+
tool_name: Tool name (may be prefixed)
|
|
36
|
+
tool_info: Tool info from MCP server
|
|
37
|
+
server_name: Name of the source server
|
|
38
|
+
connection: Optional Kuzu connection (for compatibility)
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(connection)
|
|
41
|
+
self._mcp_manager = mcp_manager
|
|
42
|
+
self._tool_name = tool_name
|
|
43
|
+
self._tool_info = tool_info
|
|
44
|
+
self._server_name = server_name
|
|
45
|
+
|
|
46
|
+
# Set BaseTool attributes
|
|
47
|
+
self.name = tool_name
|
|
48
|
+
self.description = tool_info.description or f"MCP tool: {tool_name}"
|
|
49
|
+
self.category = self._infer_category(tool_name, tool_info)
|
|
50
|
+
|
|
51
|
+
def _infer_category(self, name: str, info: MCPToolInfo) -> ToolCategory:
|
|
52
|
+
"""Infer tool category from name and description.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
name: Tool name
|
|
56
|
+
info: Tool info
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Inferred ToolCategory
|
|
60
|
+
"""
|
|
61
|
+
name_lower = name.lower()
|
|
62
|
+
desc_lower = (info.description or "").lower()
|
|
63
|
+
|
|
64
|
+
# Search-related
|
|
65
|
+
if any(kw in name_lower for kw in ["search", "find", "query", "grep"]):
|
|
66
|
+
return ToolCategory.SEARCH
|
|
67
|
+
|
|
68
|
+
# Traversal-related
|
|
69
|
+
if any(kw in name_lower for kw in ["expand", "caller", "callee", "dependency", "neighbor"]):
|
|
70
|
+
return ToolCategory.TRAVERSAL
|
|
71
|
+
|
|
72
|
+
# Analytics-related
|
|
73
|
+
if any(kw in name_lower for kw in ["pagerank", "community", "importance", "metric"]):
|
|
74
|
+
return ToolCategory.ANALYTICS
|
|
75
|
+
|
|
76
|
+
# History-related (PRs, commits)
|
|
77
|
+
if any(kw in name_lower for kw in ["pr", "commit", "history", "github"]):
|
|
78
|
+
return ToolCategory.HISTORY
|
|
79
|
+
|
|
80
|
+
# Default to search
|
|
81
|
+
return ToolCategory.SEARCH
|
|
82
|
+
|
|
83
|
+
def execute(self, **kwargs) -> ToolResult:
|
|
84
|
+
"""Execute the MCP tool.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
**kwargs: Tool arguments
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
ToolResult with tool output
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
response = self._mcp_manager.call_tool(self._tool_name, kwargs)
|
|
94
|
+
|
|
95
|
+
if response.is_error:
|
|
96
|
+
return ToolResult.error_result(
|
|
97
|
+
f"MCP tool error: {response.get_text()}",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Parse response content
|
|
101
|
+
result_data = self._parse_response(response)
|
|
102
|
+
|
|
103
|
+
return ToolResult.success_result(
|
|
104
|
+
data=result_data,
|
|
105
|
+
metadata={
|
|
106
|
+
"server": self._server_name,
|
|
107
|
+
"tool": self._tool_name,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
except MCPError as e:
|
|
112
|
+
return ToolResult.error_result(
|
|
113
|
+
f"MCP error: {str(e)}",
|
|
114
|
+
suggestions=["Check if the MCP server is running"],
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
log.exception(f"MCP tool execution error: {self._tool_name}")
|
|
118
|
+
return ToolResult.error_result(
|
|
119
|
+
f"Tool execution failed: {str(e)}",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _parse_response(self, response) -> dict:
|
|
123
|
+
"""Parse MCP response into result data.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
response: MCPResponse
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Parsed result dict
|
|
130
|
+
"""
|
|
131
|
+
result = {"content": [], "text": ""}
|
|
132
|
+
|
|
133
|
+
for item in response.content:
|
|
134
|
+
if item.get("type") == "text":
|
|
135
|
+
text = item.get("text", "")
|
|
136
|
+
result["text"] += text + "\n"
|
|
137
|
+
|
|
138
|
+
# Try to parse as JSON
|
|
139
|
+
try:
|
|
140
|
+
parsed = json.loads(text)
|
|
141
|
+
if isinstance(parsed, dict):
|
|
142
|
+
result.update(parsed)
|
|
143
|
+
elif isinstance(parsed, list):
|
|
144
|
+
result["results"] = parsed
|
|
145
|
+
except json.JSONDecodeError:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
result["content"].append(item)
|
|
149
|
+
|
|
150
|
+
result["text"] = result["text"].strip()
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
def get_schema(self) -> dict:
|
|
154
|
+
"""Get OpenAI function calling schema.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
OpenAI function schema dict
|
|
158
|
+
"""
|
|
159
|
+
input_schema = self._tool_info.input_schema or {}
|
|
160
|
+
|
|
161
|
+
# Convert MCP schema to OpenAI format
|
|
162
|
+
parameters = {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": input_schema.get("properties", {}),
|
|
165
|
+
"required": input_schema.get("required", []),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"type": "function",
|
|
170
|
+
"function": {
|
|
171
|
+
"name": self.name,
|
|
172
|
+
"description": self.description,
|
|
173
|
+
"parameters": parameters,
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def create_tools_from_mcp(
|
|
179
|
+
manager: MCPServerManager,
|
|
180
|
+
connection: Any = None,
|
|
181
|
+
) -> list[BaseTool]:
|
|
182
|
+
"""Create BaseTool instances from all MCP server tools.
|
|
183
|
+
|
|
184
|
+
This function queries all enabled MCP servers and creates
|
|
185
|
+
dynamic tool wrappers for each tool they provide.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
manager: MCP server manager
|
|
189
|
+
connection: Optional Kuzu connection
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of MCPDynamicTool instances
|
|
193
|
+
"""
|
|
194
|
+
tools = []
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
all_tools = manager.get_all_tools()
|
|
198
|
+
|
|
199
|
+
for tool_name, server_name, tool_info in all_tools:
|
|
200
|
+
tool = MCPDynamicTool(
|
|
201
|
+
mcp_manager=manager,
|
|
202
|
+
tool_name=tool_name,
|
|
203
|
+
tool_info=tool_info,
|
|
204
|
+
server_name=server_name,
|
|
205
|
+
connection=connection,
|
|
206
|
+
)
|
|
207
|
+
tools.append(tool)
|
|
208
|
+
log.debug(f"Created MCP tool: {tool_name} from {server_name}")
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
log.warning(f"Failed to create tools from MCP: {e}")
|
|
212
|
+
|
|
213
|
+
return tools
|