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,346 @@
|
|
|
1
|
+
"""Generic MCP client for communicating with MCP servers.
|
|
2
|
+
|
|
3
|
+
This module provides a generic client that can communicate with any
|
|
4
|
+
MCP-compliant server over stdio.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
from queue import Queue, Empty
|
|
13
|
+
|
|
14
|
+
from ...utils.logger import log
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCPError(Exception):
|
|
18
|
+
"""Error from MCP server communication."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class MCPToolInfo:
|
|
24
|
+
"""Information about a tool provided by an MCP server.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
name: Tool name
|
|
28
|
+
description: Tool description
|
|
29
|
+
input_schema: JSON Schema for tool input
|
|
30
|
+
"""
|
|
31
|
+
name: str
|
|
32
|
+
description: str
|
|
33
|
+
input_schema: dict = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class MCPResponse:
|
|
38
|
+
"""Response from an MCP tool call.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
content: Response content (list of content items)
|
|
42
|
+
is_error: Whether this is an error response
|
|
43
|
+
"""
|
|
44
|
+
content: list[dict] = field(default_factory=list)
|
|
45
|
+
is_error: bool = False
|
|
46
|
+
|
|
47
|
+
def get_text(self) -> str:
|
|
48
|
+
"""Extract text content from response.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Concatenated text content
|
|
52
|
+
"""
|
|
53
|
+
texts = []
|
|
54
|
+
for item in self.content:
|
|
55
|
+
if item.get("type") == "text":
|
|
56
|
+
texts.append(item.get("text", ""))
|
|
57
|
+
return "\n".join(texts)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GenericMCPClient:
|
|
61
|
+
"""Generic client for MCP servers.
|
|
62
|
+
|
|
63
|
+
Communicates with MCP servers over stdio using JSON-RPC.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
client = GenericMCPClient(
|
|
67
|
+
name="github",
|
|
68
|
+
command="github-mcp-server",
|
|
69
|
+
args=["stdio"],
|
|
70
|
+
env={"GITHUB_TOKEN": "..."},
|
|
71
|
+
)
|
|
72
|
+
client.start()
|
|
73
|
+
|
|
74
|
+
tools = client.list_tools()
|
|
75
|
+
result = client.call_tool("search_code", {"query": "auth"})
|
|
76
|
+
|
|
77
|
+
client.stop()
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
name: str,
|
|
83
|
+
command: str,
|
|
84
|
+
args: list[str] = None,
|
|
85
|
+
env: dict[str, str] = None,
|
|
86
|
+
timeout: int = 30,
|
|
87
|
+
):
|
|
88
|
+
"""Initialize the MCP client.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
name: Name for this client (for logging)
|
|
92
|
+
command: Command to run the server
|
|
93
|
+
args: Command arguments
|
|
94
|
+
env: Environment variables
|
|
95
|
+
timeout: Timeout for operations in seconds
|
|
96
|
+
"""
|
|
97
|
+
self.name = name
|
|
98
|
+
self.command = command
|
|
99
|
+
self.args = args or []
|
|
100
|
+
self.env = env or {}
|
|
101
|
+
self.timeout = timeout
|
|
102
|
+
|
|
103
|
+
self._process: Optional[subprocess.Popen] = None
|
|
104
|
+
self._request_id = 0
|
|
105
|
+
self._pending: dict[int, Queue] = {}
|
|
106
|
+
self._reader_thread: Optional[threading.Thread] = None
|
|
107
|
+
self._running = False
|
|
108
|
+
self._tools: list[MCPToolInfo] = []
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def is_running(self) -> bool:
|
|
112
|
+
"""Check if the client is running."""
|
|
113
|
+
return self._running and self._process is not None
|
|
114
|
+
|
|
115
|
+
def start(self) -> None:
|
|
116
|
+
"""Start the MCP server process."""
|
|
117
|
+
if self._running:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
import os
|
|
121
|
+
|
|
122
|
+
# Build environment
|
|
123
|
+
full_env = os.environ.copy()
|
|
124
|
+
full_env.update(self.env)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
self._process = subprocess.Popen(
|
|
128
|
+
[self.command] + self.args,
|
|
129
|
+
stdin=subprocess.PIPE,
|
|
130
|
+
stdout=subprocess.PIPE,
|
|
131
|
+
stderr=subprocess.PIPE,
|
|
132
|
+
env=full_env,
|
|
133
|
+
text=True,
|
|
134
|
+
bufsize=1,
|
|
135
|
+
)
|
|
136
|
+
except FileNotFoundError:
|
|
137
|
+
raise MCPError(f"MCP server command not found: {self.command}")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
raise MCPError(f"Failed to start MCP server: {e}")
|
|
140
|
+
|
|
141
|
+
self._running = True
|
|
142
|
+
|
|
143
|
+
# Start reader thread
|
|
144
|
+
self._reader_thread = threading.Thread(target=self._read_responses, daemon=True)
|
|
145
|
+
self._reader_thread.start()
|
|
146
|
+
|
|
147
|
+
# Initialize the connection
|
|
148
|
+
self._initialize()
|
|
149
|
+
|
|
150
|
+
log.info(f"Started MCP server: {self.name}")
|
|
151
|
+
|
|
152
|
+
def stop(self) -> None:
|
|
153
|
+
"""Stop the MCP server process."""
|
|
154
|
+
self._running = False
|
|
155
|
+
|
|
156
|
+
if self._process:
|
|
157
|
+
try:
|
|
158
|
+
self._process.terminate()
|
|
159
|
+
self._process.wait(timeout=5)
|
|
160
|
+
except subprocess.TimeoutExpired:
|
|
161
|
+
self._process.kill()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
finally:
|
|
165
|
+
self._process = None
|
|
166
|
+
|
|
167
|
+
self._tools = []
|
|
168
|
+
log.info(f"Stopped MCP server: {self.name}")
|
|
169
|
+
|
|
170
|
+
def _initialize(self) -> None:
|
|
171
|
+
"""Initialize the MCP connection."""
|
|
172
|
+
# Send initialize request
|
|
173
|
+
response = self._send_request("initialize", {
|
|
174
|
+
"protocolVersion": "2024-11-05",
|
|
175
|
+
"capabilities": {},
|
|
176
|
+
"clientInfo": {
|
|
177
|
+
"name": "emdash",
|
|
178
|
+
"version": "1.0.0",
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
# Send initialized notification
|
|
183
|
+
self._send_notification("notifications/initialized", {})
|
|
184
|
+
|
|
185
|
+
def _send_request(self, method: str, params: dict) -> dict:
|
|
186
|
+
"""Send a JSON-RPC request and wait for response.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
method: RPC method name
|
|
190
|
+
params: Method parameters
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Response result
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
MCPError: On communication error or error response
|
|
197
|
+
"""
|
|
198
|
+
if not self._process or not self._running:
|
|
199
|
+
raise MCPError("MCP client not running")
|
|
200
|
+
|
|
201
|
+
self._request_id += 1
|
|
202
|
+
request_id = self._request_id
|
|
203
|
+
|
|
204
|
+
request = {
|
|
205
|
+
"jsonrpc": "2.0",
|
|
206
|
+
"id": request_id,
|
|
207
|
+
"method": method,
|
|
208
|
+
"params": params,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Create response queue
|
|
212
|
+
response_queue: Queue = Queue()
|
|
213
|
+
self._pending[request_id] = response_queue
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Send request
|
|
217
|
+
request_line = json.dumps(request) + "\n"
|
|
218
|
+
self._process.stdin.write(request_line)
|
|
219
|
+
self._process.stdin.flush()
|
|
220
|
+
|
|
221
|
+
# Wait for response
|
|
222
|
+
try:
|
|
223
|
+
response = response_queue.get(timeout=self.timeout)
|
|
224
|
+
except Empty:
|
|
225
|
+
raise MCPError(f"Timeout waiting for response to {method}")
|
|
226
|
+
|
|
227
|
+
if "error" in response:
|
|
228
|
+
error = response["error"]
|
|
229
|
+
raise MCPError(f"MCP error: {error.get('message', 'Unknown error')}")
|
|
230
|
+
|
|
231
|
+
return response.get("result", {})
|
|
232
|
+
|
|
233
|
+
finally:
|
|
234
|
+
del self._pending[request_id]
|
|
235
|
+
|
|
236
|
+
def _send_notification(self, method: str, params: dict) -> None:
|
|
237
|
+
"""Send a JSON-RPC notification (no response expected).
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
method: Notification method
|
|
241
|
+
params: Method parameters
|
|
242
|
+
"""
|
|
243
|
+
if not self._process or not self._running:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
notification = {
|
|
247
|
+
"jsonrpc": "2.0",
|
|
248
|
+
"method": method,
|
|
249
|
+
"params": params,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
notification_line = json.dumps(notification) + "\n"
|
|
253
|
+
self._process.stdin.write(notification_line)
|
|
254
|
+
self._process.stdin.flush()
|
|
255
|
+
|
|
256
|
+
def _read_responses(self) -> None:
|
|
257
|
+
"""Background thread to read responses from the server."""
|
|
258
|
+
while self._running and self._process:
|
|
259
|
+
try:
|
|
260
|
+
line = self._process.stdout.readline()
|
|
261
|
+
if not line:
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
response = json.loads(line.strip())
|
|
266
|
+
except json.JSONDecodeError:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# Route to pending request
|
|
270
|
+
request_id = response.get("id")
|
|
271
|
+
if request_id and request_id in self._pending:
|
|
272
|
+
self._pending[request_id].put(response)
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
if self._running:
|
|
276
|
+
log.warning(f"Error reading MCP response: {e}")
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
def list_tools(self) -> list[MCPToolInfo]:
|
|
280
|
+
"""List available tools from the server.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List of MCPToolInfo
|
|
284
|
+
"""
|
|
285
|
+
if self._tools:
|
|
286
|
+
return self._tools
|
|
287
|
+
|
|
288
|
+
result = self._send_request("tools/list", {})
|
|
289
|
+
|
|
290
|
+
tools = []
|
|
291
|
+
for tool_data in result.get("tools", []):
|
|
292
|
+
tools.append(MCPToolInfo(
|
|
293
|
+
name=tool_data.get("name", ""),
|
|
294
|
+
description=tool_data.get("description", ""),
|
|
295
|
+
input_schema=tool_data.get("inputSchema", {}),
|
|
296
|
+
))
|
|
297
|
+
|
|
298
|
+
self._tools = tools
|
|
299
|
+
return tools
|
|
300
|
+
|
|
301
|
+
def call_tool(self, name: str, arguments: dict) -> MCPResponse:
|
|
302
|
+
"""Call a tool on the server.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
name: Tool name
|
|
306
|
+
arguments: Tool arguments
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
MCPResponse with tool result
|
|
310
|
+
"""
|
|
311
|
+
result = self._send_request("tools/call", {
|
|
312
|
+
"name": name,
|
|
313
|
+
"arguments": arguments,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return MCPResponse(
|
|
317
|
+
content=result.get("content", []),
|
|
318
|
+
is_error=result.get("isError", False),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# Legacy client for backward compatibility
|
|
323
|
+
class GitHubMCPClient(GenericMCPClient):
|
|
324
|
+
"""GitHub-specific MCP client.
|
|
325
|
+
|
|
326
|
+
Deprecated: Use GenericMCPClient directly with appropriate config.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def __init__(self, token: Optional[str] = None, timeout: int = 30):
|
|
330
|
+
"""Initialize GitHub MCP client.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
token: GitHub token. If None, reads from environment.
|
|
334
|
+
timeout: Operation timeout in seconds.
|
|
335
|
+
"""
|
|
336
|
+
import os
|
|
337
|
+
|
|
338
|
+
github_token = token or os.getenv("GITHUB_TOKEN") or os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
|
|
339
|
+
|
|
340
|
+
super().__init__(
|
|
341
|
+
name="github",
|
|
342
|
+
command="github-mcp-server",
|
|
343
|
+
args=["stdio"],
|
|
344
|
+
env={"GITHUB_PERSONAL_ACCESS_TOKEN": github_token or ""},
|
|
345
|
+
timeout=timeout,
|
|
346
|
+
)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""MCP configuration schema and loading.
|
|
2
|
+
|
|
3
|
+
This module provides configuration classes for managing MCP servers,
|
|
4
|
+
compatible with Claude Desktop's mcp_config.json format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from ...utils.logger import log
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class MCPServerConfig:
|
|
19
|
+
"""Configuration for a single MCP server.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name: Unique identifier for this server
|
|
23
|
+
command: Command to run (e.g., "npx", "github-mcp-server")
|
|
24
|
+
args: Arguments to pass to the command
|
|
25
|
+
env: Environment variables (supports ${VAR} syntax)
|
|
26
|
+
enabled: Whether this server is enabled
|
|
27
|
+
timeout: Timeout in seconds for tool calls
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
command: str
|
|
32
|
+
args: list[str] = field(default_factory=list)
|
|
33
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
34
|
+
enabled: bool = True
|
|
35
|
+
timeout: int = 30
|
|
36
|
+
|
|
37
|
+
def get_resolved_env(self) -> dict[str, str]:
|
|
38
|
+
"""Resolve environment variable references like ${VAR_NAME}.
|
|
39
|
+
|
|
40
|
+
For GitHub tokens, also checks Rove auth config first.
|
|
41
|
+
For graph DB path, uses emdash config default.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary with resolved environment values
|
|
45
|
+
"""
|
|
46
|
+
resolved = {}
|
|
47
|
+
pattern = re.compile(r"\$\{([^}]+)\}")
|
|
48
|
+
|
|
49
|
+
def get_env_value(var_name: str) -> str:
|
|
50
|
+
"""Get environment value, checking emdash config for special vars."""
|
|
51
|
+
# Check for GitHub token - use emdash auth if available
|
|
52
|
+
if var_name in ("GITHUB_TOKEN", "GITHUB_PERSONAL_ACCESS_TOKEN"):
|
|
53
|
+
try:
|
|
54
|
+
from ...auth import get_github_token
|
|
55
|
+
token = get_github_token()
|
|
56
|
+
if token:
|
|
57
|
+
return token
|
|
58
|
+
except ImportError:
|
|
59
|
+
pass
|
|
60
|
+
# Check for graph DB path - use emdash config default
|
|
61
|
+
if var_name == "EMDASH_GRAPH_DB_PATH":
|
|
62
|
+
env_val = os.getenv(var_name)
|
|
63
|
+
if env_val:
|
|
64
|
+
return env_val
|
|
65
|
+
# Default to .emdash/index/kuzu_db in cwd
|
|
66
|
+
default_path = Path.cwd() / ".emdash" / "index" / "kuzu_db"
|
|
67
|
+
return str(default_path)
|
|
68
|
+
# Fall back to environment variable
|
|
69
|
+
return os.getenv(var_name, "")
|
|
70
|
+
|
|
71
|
+
for key, value in self.env.items():
|
|
72
|
+
match = pattern.fullmatch(value)
|
|
73
|
+
if match:
|
|
74
|
+
env_var = match.group(1)
|
|
75
|
+
env_value = get_env_value(env_var)
|
|
76
|
+
if not env_value:
|
|
77
|
+
log.warning(f"Environment variable {env_var} not set for MCP server {self.name}")
|
|
78
|
+
resolved[key] = env_value
|
|
79
|
+
else:
|
|
80
|
+
# Check for partial substitution like "prefix_${VAR}_suffix"
|
|
81
|
+
def replace_var(m):
|
|
82
|
+
return get_env_value(m.group(1))
|
|
83
|
+
|
|
84
|
+
resolved[key] = pattern.sub(replace_var, value)
|
|
85
|
+
|
|
86
|
+
return resolved
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict:
|
|
89
|
+
"""Convert to dictionary for JSON serialization."""
|
|
90
|
+
return {
|
|
91
|
+
"command": self.command,
|
|
92
|
+
"args": self.args,
|
|
93
|
+
"env": self.env,
|
|
94
|
+
"enabled": self.enabled,
|
|
95
|
+
"timeout": self.timeout,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, name: str, data: dict) -> "MCPServerConfig":
|
|
100
|
+
"""Create from dictionary."""
|
|
101
|
+
return cls(
|
|
102
|
+
name=name,
|
|
103
|
+
command=data.get("command", ""),
|
|
104
|
+
args=data.get("args", []),
|
|
105
|
+
env=data.get("env", {}),
|
|
106
|
+
enabled=data.get("enabled", True),
|
|
107
|
+
timeout=data.get("timeout", 30),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class MCPConfigFile:
|
|
113
|
+
"""Root configuration for all MCP servers.
|
|
114
|
+
|
|
115
|
+
This class handles loading and saving the MCP configuration file,
|
|
116
|
+
which uses Claude Desktop's format:
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"server-name": {
|
|
121
|
+
"command": "...",
|
|
122
|
+
"args": [...],
|
|
123
|
+
"env": {...}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
servers: dict[str, MCPServerConfig] = field(default_factory=dict)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def load(cls, path: Path) -> "MCPConfigFile":
|
|
133
|
+
"""Load MCP configuration from file.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
path: Path to the configuration file
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
MCPConfigFile instance (empty if file doesn't exist)
|
|
140
|
+
"""
|
|
141
|
+
if not path.exists():
|
|
142
|
+
log.debug(f"MCP config file not found: {path}")
|
|
143
|
+
return cls()
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with open(path) as f:
|
|
147
|
+
data = json.load(f)
|
|
148
|
+
except json.JSONDecodeError as e:
|
|
149
|
+
log.error(f"Invalid JSON in MCP config file {path}: {e}")
|
|
150
|
+
return cls()
|
|
151
|
+
except Exception as e:
|
|
152
|
+
log.error(f"Failed to read MCP config file {path}: {e}")
|
|
153
|
+
return cls()
|
|
154
|
+
|
|
155
|
+
servers = {}
|
|
156
|
+
for name, config in data.get("mcpServers", {}).items():
|
|
157
|
+
try:
|
|
158
|
+
servers[name] = MCPServerConfig.from_dict(name, config)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
log.warning(f"Invalid MCP server config '{name}': {e}")
|
|
161
|
+
|
|
162
|
+
log.info(f"Loaded MCP config with {len(servers)} servers from {path}")
|
|
163
|
+
return cls(servers=servers)
|
|
164
|
+
|
|
165
|
+
def save(self, path: Path) -> None:
|
|
166
|
+
"""Save configuration to file.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
path: Path to save the configuration file
|
|
170
|
+
"""
|
|
171
|
+
data = {"mcpServers": {}}
|
|
172
|
+
for name, server in self.servers.items():
|
|
173
|
+
data["mcpServers"][name] = server.to_dict()
|
|
174
|
+
|
|
175
|
+
# Ensure directory exists
|
|
176
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
|
|
178
|
+
with open(path, "w") as f:
|
|
179
|
+
json.dump(data, f, indent=2)
|
|
180
|
+
|
|
181
|
+
log.info(f"Saved MCP config to {path}")
|
|
182
|
+
|
|
183
|
+
def get_enabled_servers(self) -> list[MCPServerConfig]:
|
|
184
|
+
"""Get list of enabled server configurations.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of enabled MCPServerConfig instances
|
|
188
|
+
"""
|
|
189
|
+
return [s for s in self.servers.values() if s.enabled]
|
|
190
|
+
|
|
191
|
+
def add_server(self, config: MCPServerConfig) -> None:
|
|
192
|
+
"""Add or update a server configuration.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
config: Server configuration to add
|
|
196
|
+
"""
|
|
197
|
+
self.servers[config.name] = config
|
|
198
|
+
|
|
199
|
+
def remove_server(self, name: str) -> bool:
|
|
200
|
+
"""Remove a server configuration.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
name: Name of the server to remove
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if server was removed, False if not found
|
|
207
|
+
"""
|
|
208
|
+
if name in self.servers:
|
|
209
|
+
del self.servers[name]
|
|
210
|
+
return True
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def get_server(self, name: str) -> Optional[MCPServerConfig]:
|
|
214
|
+
"""Get a server configuration by name.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
name: Name of the server
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
MCPServerConfig or None if not found
|
|
221
|
+
"""
|
|
222
|
+
return self.servers.get(name)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_default_mcp_config_path(repo_root: Optional[Path] = None) -> Path:
|
|
226
|
+
"""Get the default path for the MCP configuration file.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
repo_root: Repository root directory (uses cwd if not provided)
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Path to .emdash/mcp.json
|
|
233
|
+
"""
|
|
234
|
+
if repo_root is None:
|
|
235
|
+
repo_root = Path.cwd()
|
|
236
|
+
return repo_root / ".emdash" / "mcp.json"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_default_mcp_servers() -> dict[str, MCPServerConfig]:
|
|
240
|
+
"""Get the default MCP servers that ship with Rove.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Dictionary of default server configurations
|
|
244
|
+
"""
|
|
245
|
+
# Check if graph MCP is enabled via env flag
|
|
246
|
+
enable_graph_mcp = os.getenv("ENABLE_GRAPH_MCP", "false").lower() == "true"
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"github": MCPServerConfig(
|
|
250
|
+
name="github",
|
|
251
|
+
command="github-mcp-server",
|
|
252
|
+
args=["stdio"],
|
|
253
|
+
env={
|
|
254
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}",
|
|
255
|
+
},
|
|
256
|
+
enabled=False, # Disabled - use local tools for codebase exploration
|
|
257
|
+
timeout=30,
|
|
258
|
+
),
|
|
259
|
+
"emdash-graph": MCPServerConfig(
|
|
260
|
+
name="emdash-graph",
|
|
261
|
+
command="emdash-graph-mcp",
|
|
262
|
+
args=["--db-path", "${EMDASH_GRAPH_DB_PATH}"],
|
|
263
|
+
env={
|
|
264
|
+
"EMDASH_GRAPH_DB_PATH": "${EMDASH_GRAPH_DB_PATH}",
|
|
265
|
+
},
|
|
266
|
+
enabled=enable_graph_mcp,
|
|
267
|
+
timeout=30,
|
|
268
|
+
),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def create_default_mcp_config(path: Path) -> MCPConfigFile:
|
|
273
|
+
"""Create a default MCP config file with pre-configured servers.
|
|
274
|
+
|
|
275
|
+
This creates the .emdash/mcp.json file with GitHub MCP enabled
|
|
276
|
+
by default for all new Rove installations.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
path: Path to save the config file
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The created MCPConfigFile
|
|
283
|
+
"""
|
|
284
|
+
config = MCPConfigFile(servers=get_default_mcp_servers())
|
|
285
|
+
config.save(path)
|
|
286
|
+
log.info(f"Created default MCP config with GitHub MCP at {path}")
|
|
287
|
+
return config
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def ensure_mcp_config(path: Path) -> MCPConfigFile:
|
|
291
|
+
"""Ensure MCP config exists, creating default if needed.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
path: Path to the config file
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
MCPConfigFile (loaded or newly created)
|
|
298
|
+
"""
|
|
299
|
+
if path.exists():
|
|
300
|
+
return MCPConfigFile.load(path)
|
|
301
|
+
else:
|
|
302
|
+
return create_default_mcp_config(path)
|