nexus-dev 3.3.1__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.
Files changed (48) hide show
  1. nexus_dev/__init__.py +4 -0
  2. nexus_dev/agent_templates/__init__.py +26 -0
  3. nexus_dev/agent_templates/api_designer.yaml +26 -0
  4. nexus_dev/agent_templates/code_reviewer.yaml +26 -0
  5. nexus_dev/agent_templates/debug_detective.yaml +26 -0
  6. nexus_dev/agent_templates/doc_writer.yaml +26 -0
  7. nexus_dev/agent_templates/performance_optimizer.yaml +26 -0
  8. nexus_dev/agent_templates/refactor_architect.yaml +26 -0
  9. nexus_dev/agent_templates/security_auditor.yaml +26 -0
  10. nexus_dev/agent_templates/test_engineer.yaml +26 -0
  11. nexus_dev/agents/__init__.py +20 -0
  12. nexus_dev/agents/agent_config.py +97 -0
  13. nexus_dev/agents/agent_executor.py +197 -0
  14. nexus_dev/agents/agent_manager.py +104 -0
  15. nexus_dev/agents/prompt_factory.py +91 -0
  16. nexus_dev/chunkers/__init__.py +168 -0
  17. nexus_dev/chunkers/base.py +202 -0
  18. nexus_dev/chunkers/docs_chunker.py +291 -0
  19. nexus_dev/chunkers/java_chunker.py +343 -0
  20. nexus_dev/chunkers/javascript_chunker.py +312 -0
  21. nexus_dev/chunkers/python_chunker.py +308 -0
  22. nexus_dev/cli.py +2017 -0
  23. nexus_dev/config.py +261 -0
  24. nexus_dev/database.py +569 -0
  25. nexus_dev/embeddings.py +703 -0
  26. nexus_dev/gateway/__init__.py +10 -0
  27. nexus_dev/gateway/connection_manager.py +348 -0
  28. nexus_dev/github_importer.py +247 -0
  29. nexus_dev/mcp_client.py +281 -0
  30. nexus_dev/mcp_config.py +184 -0
  31. nexus_dev/schemas/mcp_config_schema.json +166 -0
  32. nexus_dev/server.py +1866 -0
  33. nexus_dev/templates/pre-commit-hook +56 -0
  34. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/__init__.py +26 -0
  35. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/api_designer.yaml +26 -0
  36. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/code_reviewer.yaml +26 -0
  37. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/debug_detective.yaml +26 -0
  38. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/doc_writer.yaml +26 -0
  39. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/performance_optimizer.yaml +26 -0
  40. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/refactor_architect.yaml +26 -0
  41. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/security_auditor.yaml +26 -0
  42. nexus_dev-3.3.1.data/data/nexus_dev/agent_templates/test_engineer.yaml +26 -0
  43. nexus_dev-3.3.1.data/data/nexus_dev/templates/pre-commit-hook +56 -0
  44. nexus_dev-3.3.1.dist-info/METADATA +668 -0
  45. nexus_dev-3.3.1.dist-info/RECORD +48 -0
  46. nexus_dev-3.3.1.dist-info/WHEEL +4 -0
  47. nexus_dev-3.3.1.dist-info/entry_points.txt +14 -0
  48. nexus_dev-3.3.1.dist-info/licenses/LICENSE +21 -0
@@ -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
@@ -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
+ }