nexus-dev 3.2.0__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 nexus-dev might be problematic. Click here for more details.
- nexus_dev/__init__.py +4 -0
- nexus_dev/agent_templates/__init__.py +26 -0
- nexus_dev/agent_templates/api_designer.yaml +26 -0
- nexus_dev/agent_templates/code_reviewer.yaml +26 -0
- nexus_dev/agent_templates/debug_detective.yaml +26 -0
- nexus_dev/agent_templates/doc_writer.yaml +26 -0
- nexus_dev/agent_templates/performance_optimizer.yaml +26 -0
- nexus_dev/agent_templates/refactor_architect.yaml +26 -0
- nexus_dev/agent_templates/security_auditor.yaml +26 -0
- nexus_dev/agent_templates/test_engineer.yaml +26 -0
- nexus_dev/agents/__init__.py +20 -0
- nexus_dev/agents/agent_config.py +97 -0
- nexus_dev/agents/agent_executor.py +197 -0
- nexus_dev/agents/agent_manager.py +104 -0
- nexus_dev/agents/prompt_factory.py +91 -0
- nexus_dev/chunkers/__init__.py +168 -0
- nexus_dev/chunkers/base.py +202 -0
- nexus_dev/chunkers/docs_chunker.py +291 -0
- nexus_dev/chunkers/java_chunker.py +343 -0
- nexus_dev/chunkers/javascript_chunker.py +312 -0
- nexus_dev/chunkers/python_chunker.py +308 -0
- nexus_dev/cli.py +1673 -0
- nexus_dev/config.py +253 -0
- nexus_dev/database.py +558 -0
- nexus_dev/embeddings.py +585 -0
- nexus_dev/gateway/__init__.py +10 -0
- nexus_dev/gateway/connection_manager.py +348 -0
- nexus_dev/github_importer.py +247 -0
- nexus_dev/mcp_client.py +281 -0
- nexus_dev/mcp_config.py +184 -0
- nexus_dev/schemas/mcp_config_schema.json +166 -0
- nexus_dev/server.py +1866 -0
- nexus_dev/templates/pre-commit-hook +33 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/__init__.py +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/api_designer.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/code_reviewer.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/debug_detective.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/doc_writer.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/performance_optimizer.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/refactor_architect.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/security_auditor.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/test_engineer.yaml +26 -0
- nexus_dev-3.2.0.data/data/nexus_dev/templates/pre-commit-hook +33 -0
- nexus_dev-3.2.0.dist-info/METADATA +636 -0
- nexus_dev-3.2.0.dist-info/RECORD +48 -0
- nexus_dev-3.2.0.dist-info/WHEEL +4 -0
- nexus_dev-3.2.0.dist-info/entry_points.txt +12 -0
- nexus_dev-3.2.0.dist-info/licenses/LICENSE +21 -0
nexus_dev/mcp_client.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""MCP Client for connecting to backend MCP servers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from mcp import ClientSession
|
|
13
|
+
from mcp.client.sse import sse_client
|
|
14
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class MCPToolSchema:
|
|
19
|
+
"""Schema for an MCP tool."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
description: str
|
|
23
|
+
input_schema: dict[str, Any]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MCPServerConnection:
|
|
28
|
+
"""Connection to an MCP server."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
command: str
|
|
32
|
+
args: list[str]
|
|
33
|
+
env: dict[str, str] | None = None
|
|
34
|
+
transport: str = "stdio"
|
|
35
|
+
url: str | None = None
|
|
36
|
+
headers: dict[str, str] | None = None
|
|
37
|
+
timeout: float = 30.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MCPClientManager:
|
|
41
|
+
"""Manages connections to multiple MCP servers."""
|
|
42
|
+
|
|
43
|
+
async def get_tools(self, server: MCPServerConnection) -> list[MCPToolSchema]:
|
|
44
|
+
"""Get all tools from an MCP server.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
server: Server connection config
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of tool schemas
|
|
51
|
+
"""
|
|
52
|
+
if server.transport == "http":
|
|
53
|
+
if not server.url:
|
|
54
|
+
raise ValueError(f"URL required for HTTP transport: {server.name}")
|
|
55
|
+
|
|
56
|
+
async with httpx.AsyncClient() as client:
|
|
57
|
+
response = await client.post(
|
|
58
|
+
server.url,
|
|
59
|
+
headers=server.headers,
|
|
60
|
+
json={
|
|
61
|
+
"jsonrpc": "2.0",
|
|
62
|
+
"id": 1,
|
|
63
|
+
"method": "tools/list",
|
|
64
|
+
"params": {},
|
|
65
|
+
},
|
|
66
|
+
timeout=server.timeout,
|
|
67
|
+
)
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
try:
|
|
70
|
+
data = response.json()
|
|
71
|
+
except Exception:
|
|
72
|
+
# Check for SSE-wrapped JSON (Github quirk)
|
|
73
|
+
text = response.text
|
|
74
|
+
if "data: " in text:
|
|
75
|
+
# Extract JSON from data lines
|
|
76
|
+
json_lines = []
|
|
77
|
+
for line in text.splitlines():
|
|
78
|
+
if line.startswith("data: "):
|
|
79
|
+
json_lines.append(line.replace("data: ", "", 1))
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
data = json.loads("".join(json_lines))
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"Failed to parse SSE-wrapped JSON from {server.url}. "
|
|
86
|
+
f"Status: {response.status_code}. Body: {response.text[:200]}..."
|
|
87
|
+
) from e
|
|
88
|
+
else:
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
f"Failed to decode JSON response from {server.url}. "
|
|
91
|
+
f"Status: {response.status_code}. Body: {response.text[:200]}..."
|
|
92
|
+
) from None
|
|
93
|
+
|
|
94
|
+
if "error" in data:
|
|
95
|
+
raise RuntimeError(f"JSON-RPC error: {data['error']}")
|
|
96
|
+
|
|
97
|
+
schemas = []
|
|
98
|
+
# Result structure: {"result": {"tools": [...]}}
|
|
99
|
+
tools_data = data.get("result", {}).get("tools", [])
|
|
100
|
+
for tool in tools_data:
|
|
101
|
+
schemas.append(
|
|
102
|
+
MCPToolSchema(
|
|
103
|
+
name=tool.get("name"),
|
|
104
|
+
description=tool.get("description", ""),
|
|
105
|
+
input_schema=tool.get("inputSchema", {}),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return schemas
|
|
109
|
+
|
|
110
|
+
elif server.transport == "sse":
|
|
111
|
+
if not server.url:
|
|
112
|
+
raise ValueError(f"URL required for SSE transport: {server.name}")
|
|
113
|
+
|
|
114
|
+
transport_cm = sse_client(
|
|
115
|
+
url=server.url,
|
|
116
|
+
headers=server.headers or {},
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
# Expand environment variables if needed
|
|
120
|
+
env = expand_env_vars(server.env) if server.env else None
|
|
121
|
+
|
|
122
|
+
# Create server parameters
|
|
123
|
+
server_params = StdioServerParameters(command=server.command, args=server.args, env=env)
|
|
124
|
+
transport_cm = stdio_client(server_params)
|
|
125
|
+
|
|
126
|
+
async with (
|
|
127
|
+
transport_cm as (read, write),
|
|
128
|
+
ClientSession(read, write) as session,
|
|
129
|
+
):
|
|
130
|
+
await session.initialize()
|
|
131
|
+
|
|
132
|
+
# List tools
|
|
133
|
+
tools_result = await session.list_tools()
|
|
134
|
+
|
|
135
|
+
schemas = []
|
|
136
|
+
for tool in tools_result.tools:
|
|
137
|
+
schemas.append(
|
|
138
|
+
MCPToolSchema(
|
|
139
|
+
name=tool.name,
|
|
140
|
+
description=tool.description or "",
|
|
141
|
+
input_schema=tool.inputSchema or {},
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return schemas
|
|
146
|
+
|
|
147
|
+
async def get_tool_schema(
|
|
148
|
+
self, server: MCPServerConnection, tool_name: str
|
|
149
|
+
) -> MCPToolSchema | None:
|
|
150
|
+
"""Get schema for a specific tool.
|
|
151
|
+
|
|
152
|
+
Note: The MCP protocol doesn't support fetching individual tool schemas,
|
|
153
|
+
so this method fetches all tools and filters locally. For servers with
|
|
154
|
+
many tools, consider calling get_tools() once and caching the results.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
server: Server connection config
|
|
158
|
+
tool_name: Name of the tool to get schema for
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Tool schema if found, None otherwise
|
|
162
|
+
"""
|
|
163
|
+
tools = await self.get_tools(server)
|
|
164
|
+
for tool in tools:
|
|
165
|
+
if tool.name == tool_name:
|
|
166
|
+
return tool
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
async def call_tool(
|
|
170
|
+
self,
|
|
171
|
+
server: MCPServerConnection,
|
|
172
|
+
tool_name: str,
|
|
173
|
+
arguments: dict[str, Any] | None = None,
|
|
174
|
+
) -> Any:
|
|
175
|
+
"""Call a tool on an MCP server.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
server: Server connection config
|
|
179
|
+
tool_name: Name of the tool to call
|
|
180
|
+
arguments: Tool arguments
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Tool execution result
|
|
184
|
+
"""
|
|
185
|
+
if server.transport == "http":
|
|
186
|
+
if not server.url:
|
|
187
|
+
raise ValueError(f"URL required for HTTP transport: {server.name}")
|
|
188
|
+
|
|
189
|
+
async with httpx.AsyncClient() as client:
|
|
190
|
+
response = await client.post(
|
|
191
|
+
server.url,
|
|
192
|
+
headers=server.headers,
|
|
193
|
+
json={
|
|
194
|
+
"jsonrpc": "2.0",
|
|
195
|
+
"id": 1,
|
|
196
|
+
"method": "tools/call",
|
|
197
|
+
"params": {
|
|
198
|
+
"name": tool_name,
|
|
199
|
+
"arguments": arguments or {},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
timeout=server.timeout,
|
|
203
|
+
)
|
|
204
|
+
response.raise_for_status()
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
data = response.json()
|
|
208
|
+
except Exception:
|
|
209
|
+
# Check for SSE-wrapped JSON (Github quirk)
|
|
210
|
+
text = response.text
|
|
211
|
+
if "data: " in text:
|
|
212
|
+
# Extract JSON from data lines
|
|
213
|
+
json_lines = []
|
|
214
|
+
for line in text.splitlines():
|
|
215
|
+
if line.startswith("data: "):
|
|
216
|
+
json_lines.append(line.replace("data: ", "", 1))
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
data = json.loads("".join(json_lines))
|
|
220
|
+
except Exception as e:
|
|
221
|
+
# print(f"ERROR: Failed to parse SSE JSON. Body: {response.text}")
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
f"Failed to parse SSE-wrapped JSON from {server.url}. "
|
|
224
|
+
f"Status: {response.status_code}. Body: {response.text[:200]}..."
|
|
225
|
+
) from e
|
|
226
|
+
else:
|
|
227
|
+
# print(f"ERROR: Failed to parse JSON. Body: {response.text}")
|
|
228
|
+
raise RuntimeError(
|
|
229
|
+
f"Failed to decode JSON response from {server.url}. "
|
|
230
|
+
f"Status: {response.status_code}. Body: {response.text[:200]}..."
|
|
231
|
+
) from None
|
|
232
|
+
|
|
233
|
+
if "error" in data:
|
|
234
|
+
raise RuntimeError(f"JSON-RPC error: {data['error']}")
|
|
235
|
+
|
|
236
|
+
return data.get("result", {})
|
|
237
|
+
|
|
238
|
+
elif server.transport == "sse":
|
|
239
|
+
if not server.url:
|
|
240
|
+
raise ValueError(f"URL required for SSE transport: {server.name}")
|
|
241
|
+
|
|
242
|
+
transport_cm = sse_client(
|
|
243
|
+
url=server.url,
|
|
244
|
+
headers=server.headers or {},
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
# Expand environment variables if needed
|
|
248
|
+
env = expand_env_vars(server.env) if server.env else None
|
|
249
|
+
|
|
250
|
+
# Create server parameters
|
|
251
|
+
server_params = StdioServerParameters(command=server.command, args=server.args, env=env)
|
|
252
|
+
transport_cm = stdio_client(server_params)
|
|
253
|
+
|
|
254
|
+
async with (
|
|
255
|
+
transport_cm as (read, write),
|
|
256
|
+
ClientSession(read, write) as session,
|
|
257
|
+
):
|
|
258
|
+
await session.initialize()
|
|
259
|
+
return await session.call_tool(tool_name, arguments or {})
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def expand_env_vars(env: dict[str, str]) -> dict[str, str]:
|
|
263
|
+
"""Expand ${VAR} patterns in environment dict.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
env: Dictionary of environment variables with potential ${VAR} patterns
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dictionary with expanded environment variables
|
|
270
|
+
"""
|
|
271
|
+
result = {}
|
|
272
|
+
pattern = re.compile(r"\$\{(\w+)\}")
|
|
273
|
+
|
|
274
|
+
def replacer(match: re.Match[str]) -> str:
|
|
275
|
+
var_name = match.group(1)
|
|
276
|
+
return os.environ.get(var_name, "")
|
|
277
|
+
|
|
278
|
+
for key, value in env.items():
|
|
279
|
+
result[key] = pattern.sub(replacer, value)
|
|
280
|
+
|
|
281
|
+
return result
|
nexus_dev/mcp_config.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""MCP Configuration management for Nexus-Dev."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
import jsonschema # type: ignore[import-untyped]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class MCPServerConfig:
|
|
15
|
+
"""Individual MCP server configuration."""
|
|
16
|
+
|
|
17
|
+
command: str | None = None
|
|
18
|
+
args: list[str] = field(default_factory=list)
|
|
19
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
20
|
+
enabled: bool = True
|
|
21
|
+
transport: Literal["stdio", "sse", "http"] = "stdio"
|
|
22
|
+
url: str | None = None
|
|
23
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
24
|
+
timeout: float = 30.0 # Tool execution timeout
|
|
25
|
+
connect_timeout: float = 10.0 # Connection timeout
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class GatewaySettings:
|
|
30
|
+
"""Global gateway configuration settings."""
|
|
31
|
+
|
|
32
|
+
default_timeout: float = 30.0
|
|
33
|
+
max_concurrent_connections: int = 5
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class MCPConfig:
|
|
38
|
+
"""Nexus-Dev MCP project configuration."""
|
|
39
|
+
|
|
40
|
+
version: str
|
|
41
|
+
servers: dict[str, MCPServerConfig]
|
|
42
|
+
profiles: dict[str, list[str]] = field(default_factory=dict)
|
|
43
|
+
active_profile: str = "default"
|
|
44
|
+
gateway: GatewaySettings = field(default_factory=GatewaySettings)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def load(cls, path: str | Path) -> MCPConfig:
|
|
48
|
+
"""Load and validate configuration from a JSON file.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path: Path to the configuration file.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Validated MCPConfig instance.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
FileNotFoundError: If configuration file doesn't exist.
|
|
58
|
+
ValueError: If configuration is invalid against the schema.
|
|
59
|
+
"""
|
|
60
|
+
path = Path(path)
|
|
61
|
+
if not path.exists():
|
|
62
|
+
raise FileNotFoundError(f"MCP configuration file not found: {path}")
|
|
63
|
+
|
|
64
|
+
with open(path, encoding="utf-8") as f:
|
|
65
|
+
data = json.load(f)
|
|
66
|
+
|
|
67
|
+
cls.validate(data)
|
|
68
|
+
|
|
69
|
+
servers = {
|
|
70
|
+
name: MCPServerConfig(
|
|
71
|
+
command=cfg.get("command"),
|
|
72
|
+
args=cfg.get("args", []),
|
|
73
|
+
env=cfg.get("env", {}),
|
|
74
|
+
enabled=cfg.get("enabled", True),
|
|
75
|
+
transport=cfg.get("transport", "stdio"),
|
|
76
|
+
url=cfg.get("url"),
|
|
77
|
+
headers=cfg.get("headers", {}),
|
|
78
|
+
timeout=cfg.get("timeout", 30.0),
|
|
79
|
+
connect_timeout=cfg.get("connect_timeout", 10.0),
|
|
80
|
+
)
|
|
81
|
+
for name, cfg in data["servers"].items()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
profiles = data.get("profiles", {})
|
|
85
|
+
active_profile = data.get("active_profile", "default")
|
|
86
|
+
|
|
87
|
+
gateway_data = data.get("gateway", {})
|
|
88
|
+
gateway = GatewaySettings(
|
|
89
|
+
default_timeout=gateway_data.get("default_timeout", 30.0),
|
|
90
|
+
max_concurrent_connections=gateway_data.get("max_concurrent_connections", 5),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return cls(
|
|
94
|
+
version=data["version"],
|
|
95
|
+
servers=servers,
|
|
96
|
+
profiles=profiles,
|
|
97
|
+
active_profile=active_profile,
|
|
98
|
+
gateway=gateway,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def get_active_servers(self) -> list[MCPServerConfig]:
|
|
102
|
+
"""Get a list of enabled MCP server configurations in the active profile.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of enabled MCPServerConfig instances from the active profile.
|
|
106
|
+
"""
|
|
107
|
+
# If active profile doesn't exist or is empty, return all enabled servers
|
|
108
|
+
if self.active_profile not in self.profiles:
|
|
109
|
+
return [s for s in self.servers.values() if s.enabled]
|
|
110
|
+
|
|
111
|
+
# Get servers in active profile
|
|
112
|
+
profile_server_names = self.profiles[self.active_profile]
|
|
113
|
+
active_servers = []
|
|
114
|
+
|
|
115
|
+
for name in profile_server_names:
|
|
116
|
+
if name in self.servers:
|
|
117
|
+
server = self.servers[name]
|
|
118
|
+
if server.enabled:
|
|
119
|
+
active_servers.append(server)
|
|
120
|
+
|
|
121
|
+
return active_servers
|
|
122
|
+
|
|
123
|
+
def save(self, path: str | Path) -> None:
|
|
124
|
+
"""Save configuration to a JSON file.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
path: Path to save the configuration file.
|
|
128
|
+
"""
|
|
129
|
+
path = Path(path)
|
|
130
|
+
|
|
131
|
+
# Convert to dictionary format
|
|
132
|
+
data = {
|
|
133
|
+
"version": self.version,
|
|
134
|
+
"servers": {
|
|
135
|
+
name: {
|
|
136
|
+
k: v
|
|
137
|
+
for k, v in {
|
|
138
|
+
"command": server.command,
|
|
139
|
+
"args": server.args,
|
|
140
|
+
"env": server.env,
|
|
141
|
+
"enabled": server.enabled,
|
|
142
|
+
"transport": server.transport,
|
|
143
|
+
"url": server.url,
|
|
144
|
+
"headers": server.headers,
|
|
145
|
+
"timeout": server.timeout,
|
|
146
|
+
"connect_timeout": server.connect_timeout,
|
|
147
|
+
}.items()
|
|
148
|
+
if v is not None
|
|
149
|
+
}
|
|
150
|
+
for name, server in self.servers.items()
|
|
151
|
+
},
|
|
152
|
+
"profiles": self.profiles,
|
|
153
|
+
"active_profile": self.active_profile,
|
|
154
|
+
"gateway": {
|
|
155
|
+
"default_timeout": self.gateway.default_timeout,
|
|
156
|
+
"max_concurrent_connections": self.gateway.max_concurrent_connections,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Validate before saving
|
|
161
|
+
self.validate(data)
|
|
162
|
+
|
|
163
|
+
# Write to file
|
|
164
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
165
|
+
json.dump(data, f, indent=2)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def validate(data: dict[str, Any]) -> None:
|
|
169
|
+
"""Validate configuration data against the JSON schema.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
data: Configuration data dictionary.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If configuration is invalid.
|
|
176
|
+
"""
|
|
177
|
+
schema_path = Path(__file__).parent / "schemas" / "mcp_config_schema.json"
|
|
178
|
+
with open(schema_path, encoding="utf-8") as f:
|
|
179
|
+
schema = json.load(f)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
jsonschema.validate(instance=data, schema=schema)
|
|
183
|
+
except jsonschema.ValidationError as e:
|
|
184
|
+
raise ValueError(f"Invalid MCP configuration: {e.message}") from e
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Nexus-Dev MCP Configuration",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"version": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"description": "Schema version",
|
|
9
|
+
"const": "1.0"
|
|
10
|
+
},
|
|
11
|
+
"servers": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"description": "MCP server configurations",
|
|
14
|
+
"additionalProperties": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"enabled": {
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": true
|
|
20
|
+
},
|
|
21
|
+
"transport": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": [
|
|
24
|
+
"stdio",
|
|
25
|
+
"sse",
|
|
26
|
+
"http"
|
|
27
|
+
],
|
|
28
|
+
"default": "stdio",
|
|
29
|
+
"description": "Transport type: stdio (local process) or sse (HTTP remote)"
|
|
30
|
+
},
|
|
31
|
+
"command": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Command to start the MCP server (required for stdio)"
|
|
34
|
+
},
|
|
35
|
+
"args": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"default": []
|
|
41
|
+
},
|
|
42
|
+
"env": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"default": {}
|
|
48
|
+
},
|
|
49
|
+
"url": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"format": "uri",
|
|
52
|
+
"description": "Server URL (required for sse)"
|
|
53
|
+
},
|
|
54
|
+
"headers": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": {
|
|
57
|
+
"type": "string"
|
|
58
|
+
},
|
|
59
|
+
"default": {},
|
|
60
|
+
"description": "HTTP headers for SSE connection"
|
|
61
|
+
},
|
|
62
|
+
"timeout": {
|
|
63
|
+
"type": "number",
|
|
64
|
+
"description": "Tool execution timeout in seconds",
|
|
65
|
+
"default": 30,
|
|
66
|
+
"minimum": 1
|
|
67
|
+
},
|
|
68
|
+
"connect_timeout": {
|
|
69
|
+
"type": "number",
|
|
70
|
+
"description": "Connection timeout in seconds",
|
|
71
|
+
"default": 10,
|
|
72
|
+
"minimum": 1
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"allOf": [
|
|
76
|
+
{
|
|
77
|
+
"if": {
|
|
78
|
+
"properties": {
|
|
79
|
+
"transport": {
|
|
80
|
+
"const": "stdio"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"then": {
|
|
85
|
+
"required": [
|
|
86
|
+
"command"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"if": {
|
|
92
|
+
"properties": {
|
|
93
|
+
"transport": {
|
|
94
|
+
"const": "sse"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"required": [
|
|
98
|
+
"transport"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
"then": {
|
|
102
|
+
"required": [
|
|
103
|
+
"url"
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"if": {
|
|
109
|
+
"properties": {
|
|
110
|
+
"transport": {
|
|
111
|
+
"const": "http"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"required": [
|
|
115
|
+
"transport"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"then": {
|
|
119
|
+
"required": [
|
|
120
|
+
"url"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"profiles": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"description": "Named profiles mapping to server lists",
|
|
130
|
+
"additionalProperties": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"items": {
|
|
133
|
+
"type": "string"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"default": {}
|
|
137
|
+
},
|
|
138
|
+
"gateway": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"description": "Global gateway settings",
|
|
141
|
+
"properties": {
|
|
142
|
+
"default_timeout": {
|
|
143
|
+
"type": "number",
|
|
144
|
+
"description": "Default tool execution timeout",
|
|
145
|
+
"default": 30,
|
|
146
|
+
"minimum": 1
|
|
147
|
+
},
|
|
148
|
+
"max_concurrent_connections": {
|
|
149
|
+
"type": "integer",
|
|
150
|
+
"description": "Maximum concurrent connections to backend servers",
|
|
151
|
+
"default": 5,
|
|
152
|
+
"minimum": 1
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"active_profile": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "The currently active profile",
|
|
159
|
+
"default": "default"
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"required": [
|
|
163
|
+
"version",
|
|
164
|
+
"servers"
|
|
165
|
+
]
|
|
166
|
+
}
|