nucleus-mcp 1.0.7 → 1.2.0
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.
- package/package.json +2 -4
- package/.github/workflows/ci.yml +0 -72
- package/.smithery/shttp/module.js +0 -3
- package/.smithery/shttp/module.js.map +0 -7
- package/.smithery/stdio/index.cjs +0 -74
- package/.smithery/stdio/index.cjs.map +0 -7
- package/CHANGELOG.md +0 -65
- package/CODE_OF_CONDUCT.md +0 -34
- package/COMPARISON.md +0 -20
- package/CONTRIBUTING.md +0 -137
- package/DEVELOPMENT.md +0 -109
- package/LICENSE +0 -21
- package/README.md +0 -319
- package/ROADMAP.md +0 -24
- package/SECURITY.md +0 -88
- package/docs/COMMUNITY_FAQ.md +0 -48
- package/docs/COMPETITIVE_ANALYSIS.md +0 -90
- package/docs/DEMO_SCRIPT.md +0 -86
- package/docs/ENTERPRISE.md +0 -169
- package/docs/FAQ.md +0 -228
- package/docs/QUICK_START.md +0 -155
- package/docs/README.md +0 -67
- package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
- package/docs/social-preview-hq.png +0 -0
- package/docs/social-preview.png +0 -0
- package/examples/basic_usage.py +0 -84
- package/package.json.real +0 -24
- package/pyproject.toml +0 -50
- package/registry-server.json +0 -20
- package/scripts/demo_governance.py +0 -109
- package/scripts/sse_bridge.py +0 -87
- package/scripts/verify_windows_paths.py +0 -48
- package/server.json +0 -20
- package/src/mcp_server_nucleus/__init__.py +0 -8583
- package/src/mcp_server_nucleus/__main__.py +0 -9
- package/src/mcp_server_nucleus/cli.py +0 -1225
- package/src/mcp_server_nucleus/commitment_core.py +0 -146
- package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
- package/src/mcp_server_nucleus/core/__init__.py +0 -6
- package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
- package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
- package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
- package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
- package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
- package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
- package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
- package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
- package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
- package/src/mcp_server_nucleus/runtime/agent.py +0 -517
- package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
- package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
- package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
- package/src/mcp_server_nucleus/runtime/auth.py +0 -98
- package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
- package/src/mcp_server_nucleus/runtime/broker.py +0 -126
- package/src/mcp_server_nucleus/runtime/budget.py +0 -153
- package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
- package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
- package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
- package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
- package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
- package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
- package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
- package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
- package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
- package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
- package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
- package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
- package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
- package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
- package/src/mcp_server_nucleus/runtime/common.py +0 -134
- package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
- package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
- package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
- package/src/mcp_server_nucleus/runtime/critic.py +0 -150
- package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
- package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
- package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
- package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
- package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
- package/src/mcp_server_nucleus/runtime/export.py +0 -64
- package/src/mcp_server_nucleus/runtime/factory.py +0 -594
- package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
- package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
- package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
- package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
- package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
- package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
- package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
- package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
- package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
- package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
- package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
- package/src/mcp_server_nucleus/runtime/installer.py +0 -81
- package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
- package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
- package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
- package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
- package/src/mcp_server_nucleus/runtime/locking.py +0 -228
- package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
- package/src/mcp_server_nucleus/runtime/memory.py +0 -158
- package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
- package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
- package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
- package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
- package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
- package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
- package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
- package/src/mcp_server_nucleus/runtime/policy.py +0 -117
- package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
- package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
- package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
- package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
- package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
- package/src/mcp_server_nucleus/runtime/registry.py +0 -82
- package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
- package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
- package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
- package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
- package/src/mcp_server_nucleus/runtime/storage.py +0 -83
- package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
- package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
- package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
- package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
- package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
- package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
- package/src/mcp_server_nucleus/runtime/team.py +0 -59
- package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
- package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
- package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
- package/src/mcp_server_nucleus/tool_tiers.py +0 -243
- package/tests/__init__.py +0 -1
- package/tests/conftest.py +0 -42
- package/tests/test_core.py +0 -235
- package/tests/test_search.py +0 -69
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Recursive Mounter Prototype (AG-021).
|
|
3
|
-
Enables Nucleus to act as a Host-Runtime for external MCP servers.
|
|
4
|
-
Supported Transports: stdio (MVP).
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
import json
|
|
9
|
-
import logging
|
|
10
|
-
import uuid
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Dict, Any, List, Optional
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger("MOUNTER")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class MountedServer:
|
|
18
|
-
def __init__(self, name: str, command: str, args: List[str]):
|
|
19
|
-
self.name = name
|
|
20
|
-
self.command = command
|
|
21
|
-
self.args = args
|
|
22
|
-
self.process: Optional[asyncio.subprocess.Process] = None
|
|
23
|
-
self.id = f"mnt-{uuid.uuid4().hex[:6]}"
|
|
24
|
-
self.mounted_at = asyncio.get_event_loop().time()
|
|
25
|
-
self.tools: List[Dict] = []
|
|
26
|
-
|
|
27
|
-
async def start(self):
|
|
28
|
-
"""Starts the external MCP server via stdio."""
|
|
29
|
-
logger.info(f"🚀 Starting Mounted Server: {self.name} ({self.command} {' '.join(self.args)})")
|
|
30
|
-
self.process = await asyncio.create_subprocess_exec(
|
|
31
|
-
self.command, *self.args,
|
|
32
|
-
stdin=asyncio.subprocess.PIPE,
|
|
33
|
-
stdout=asyncio.subprocess.PIPE,
|
|
34
|
-
stderr=asyncio.subprocess.PIPE
|
|
35
|
-
)
|
|
36
|
-
# In a real implementation, we would now negotiate list_tools
|
|
37
|
-
# For the prototype, we assume it's running.
|
|
38
|
-
return True
|
|
39
|
-
|
|
40
|
-
async def stop(self):
|
|
41
|
-
if self.process:
|
|
42
|
-
self.process.terminate()
|
|
43
|
-
await self.process.wait()
|
|
44
|
-
logger.info(f"🛑 Stopped Mounted Server: {self.name}")
|
|
45
|
-
|
|
46
|
-
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict:
|
|
47
|
-
"""Proxies a tool call to the mounted server via MCP (stdio)."""
|
|
48
|
-
if not self.process:
|
|
49
|
-
raise RuntimeError(f"Server {self.name} is not running")
|
|
50
|
-
|
|
51
|
-
request = {
|
|
52
|
-
"jsonrpc": "2.0",
|
|
53
|
-
"method": "tools/call",
|
|
54
|
-
"params": {
|
|
55
|
-
"name": tool_name,
|
|
56
|
-
"arguments": arguments
|
|
57
|
-
},
|
|
58
|
-
"id": str(uuid.uuid4())
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# Send request
|
|
62
|
-
self.process.stdin.write((json.dumps(request) + "\n").encode())
|
|
63
|
-
await self.process.stdin.drain()
|
|
64
|
-
|
|
65
|
-
# Read response (one line)
|
|
66
|
-
line = await self.process.stdout.readline()
|
|
67
|
-
if not line:
|
|
68
|
-
return {"success": False, "error": "No response from mounted server"}
|
|
69
|
-
|
|
70
|
-
return json.loads(line.decode())
|
|
71
|
-
|
|
72
|
-
async def list_tools(self) -> List[Dict]:
|
|
73
|
-
"""Discover tools from the mounted server via MCP protocol."""
|
|
74
|
-
if not self.process:
|
|
75
|
-
return []
|
|
76
|
-
|
|
77
|
-
request = {
|
|
78
|
-
"jsonrpc": "2.0",
|
|
79
|
-
"method": "tools/list",
|
|
80
|
-
"params": {},
|
|
81
|
-
"id": str(uuid.uuid4())
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
self.process.stdin.write((json.dumps(request) + "\n").encode())
|
|
86
|
-
await self.process.stdin.drain()
|
|
87
|
-
|
|
88
|
-
# Read response with timeout
|
|
89
|
-
line = await asyncio.wait_for(
|
|
90
|
-
self.process.stdout.readline(),
|
|
91
|
-
timeout=5.0
|
|
92
|
-
)
|
|
93
|
-
if not line:
|
|
94
|
-
return []
|
|
95
|
-
|
|
96
|
-
response = json.loads(line.decode())
|
|
97
|
-
if "result" in response and "tools" in response["result"]:
|
|
98
|
-
self.tools = response["result"]["tools"]
|
|
99
|
-
return self.tools
|
|
100
|
-
return []
|
|
101
|
-
except asyncio.TimeoutError:
|
|
102
|
-
logger.warning(f"Timeout discovering tools from {self.name}")
|
|
103
|
-
return []
|
|
104
|
-
except Exception as e:
|
|
105
|
-
logger.error(f"Error discovering tools from {self.name}: {e}")
|
|
106
|
-
return []
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class RecursiveMounter:
|
|
111
|
-
def __init__(self, brain_path: Path):
|
|
112
|
-
self.brain_path = brain_path
|
|
113
|
-
self.mounted_servers: Dict[str, MountedServer] = {}
|
|
114
|
-
self.mounts_file = brain_path / "ledger" / "mounts.json"
|
|
115
|
-
self.mounts_file.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
-
self._load_mounts()
|
|
117
|
-
|
|
118
|
-
def _load_mounts(self):
|
|
119
|
-
"""Load persistent mounts from disk."""
|
|
120
|
-
if not self.mounts_file.exists():
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
try:
|
|
124
|
-
data = json.loads(self.mounts_file.read_text())
|
|
125
|
-
for mount_data in data:
|
|
126
|
-
# We don't auto-start on load in this MVP, but we restore the config
|
|
127
|
-
# In a real OS, we would have a startup supervisor.
|
|
128
|
-
# For now, we just track them.
|
|
129
|
-
server = MountedServer(
|
|
130
|
-
name=mount_data["name"],
|
|
131
|
-
command=mount_data["command"],
|
|
132
|
-
args=mount_data["args"]
|
|
133
|
-
)
|
|
134
|
-
server.id = mount_data["id"]
|
|
135
|
-
self.mounted_servers[server.id] = server
|
|
136
|
-
except Exception as e:
|
|
137
|
-
logger.error(f"Failed to load mounts: {e}")
|
|
138
|
-
|
|
139
|
-
def _save_mounts(self):
|
|
140
|
-
"""Save persistent mounts to disk."""
|
|
141
|
-
data = [
|
|
142
|
-
{
|
|
143
|
-
"id": s.id,
|
|
144
|
-
"name": s.name,
|
|
145
|
-
"command": s.command,
|
|
146
|
-
"args": s.args
|
|
147
|
-
}
|
|
148
|
-
for s in self.mounted_servers.values()
|
|
149
|
-
]
|
|
150
|
-
self.mounts_file.write_text(json.dumps(data, indent=2))
|
|
151
|
-
|
|
152
|
-
async def mount(self, name: str, command: str, args: List[str]) -> str:
|
|
153
|
-
"""Mounts an external MCP server."""
|
|
154
|
-
# Check for existing name
|
|
155
|
-
if not name or len(name.strip()) < 2:
|
|
156
|
-
return "Error: Server name must be at least 2 characters"
|
|
157
|
-
|
|
158
|
-
# V9.1 Security: Recursive Self-Mount Detection
|
|
159
|
-
# Check if the command or args try to launch the nucleus server itself
|
|
160
|
-
forbidden_patterns = ["mcp_server_nucleus", "mcp-server-nucleus"]
|
|
161
|
-
cmd_str = f"{command} {' '.join(args)}".lower()
|
|
162
|
-
for pattern in forbidden_patterns:
|
|
163
|
-
if pattern in cmd_str:
|
|
164
|
-
import sys
|
|
165
|
-
print("[NUCLEUS] SECURITY VIOLATION: Recursive mount detected", file=sys.stderr)
|
|
166
|
-
return f"Error: Recursive mounting of {pattern} is forbidden for stability."
|
|
167
|
-
|
|
168
|
-
for s in self.mounted_servers.values():
|
|
169
|
-
if s.name == name:
|
|
170
|
-
return f"Server {name} is already mounted as {s.id}"
|
|
171
|
-
|
|
172
|
-
server = MountedServer(name, command, args)
|
|
173
|
-
try:
|
|
174
|
-
await server.start()
|
|
175
|
-
self.mounted_servers[server.id] = server
|
|
176
|
-
self._save_mounts()
|
|
177
|
-
return f"Successfully mounted {name} as {server.id}"
|
|
178
|
-
except Exception as e:
|
|
179
|
-
return f"Failed to mount {name}: {e}"
|
|
180
|
-
|
|
181
|
-
async def unmount(self, server_id: str) -> str:
|
|
182
|
-
"""Unmounts and stops a server."""
|
|
183
|
-
if server_id not in self.mounted_servers:
|
|
184
|
-
return f"Server {server_id} not found"
|
|
185
|
-
|
|
186
|
-
server = self.mounted_servers[server_id]
|
|
187
|
-
await server.stop()
|
|
188
|
-
del self.mounted_servers[server_id]
|
|
189
|
-
self._save_mounts()
|
|
190
|
-
return f"Unmounted {server.name} ({server_id})"
|
|
191
|
-
|
|
192
|
-
def list_mounted(self) -> List[Dict]:
|
|
193
|
-
return [
|
|
194
|
-
{
|
|
195
|
-
"id": s.id,
|
|
196
|
-
"name": s.name,
|
|
197
|
-
"command": s.command,
|
|
198
|
-
"status": "Running" if s.process and s.process.returncode is None else "Stopped"
|
|
199
|
-
}
|
|
200
|
-
for s in self.mounted_servers.values()
|
|
201
|
-
]
|
|
202
|
-
|
|
203
|
-
# Global singleton for prototype
|
|
204
|
-
_mounter: Optional[RecursiveMounter] = None
|
|
205
|
-
|
|
206
|
-
def get_mounter(brain_path: Path) -> RecursiveMounter:
|
|
207
|
-
global _mounter
|
|
208
|
-
if _mounter is None:
|
|
209
|
-
_mounter = RecursiveMounter(brain_path)
|
|
210
|
-
return _mounter
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import logging
|
|
5
|
-
from typing import Dict, List, Any
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
# Try importing mcp, handle failure gracefully for the "no dependency" case
|
|
9
|
-
try:
|
|
10
|
-
from mcp import ClientSession, StdioServerParameters
|
|
11
|
-
from mcp.client.stdio import stdio_client
|
|
12
|
-
from mcp.client.sse import sse_client
|
|
13
|
-
MCP_AVAILABLE = True
|
|
14
|
-
except ImportError:
|
|
15
|
-
MCP_AVAILABLE = False
|
|
16
|
-
# Mock classes for type hinting if missing
|
|
17
|
-
ClientSession = Any
|
|
18
|
-
StdioServerParameters = Any
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger("nucleus_mounter")
|
|
21
|
-
|
|
22
|
-
class Mounter:
|
|
23
|
-
"""
|
|
24
|
-
Manages connections to external MCP servers (Recursive Mounting).
|
|
25
|
-
Supports Stdio and SSE transports.
|
|
26
|
-
"""
|
|
27
|
-
def __init__(self, brain_path: Path):
|
|
28
|
-
self.brain_path = brain_path
|
|
29
|
-
self.mounts_file = brain_path / "mounts.json"
|
|
30
|
-
|
|
31
|
-
# In-memory session store: mount_id -> ClientSession
|
|
32
|
-
self.sessions: Dict[str, ClientSession] = {}
|
|
33
|
-
# Keep track of exit stack context managers to close them later
|
|
34
|
-
self.exit_stacks: Dict[str, Any] = {}
|
|
35
|
-
|
|
36
|
-
self.mount_configs: Dict[str, Dict] = {}
|
|
37
|
-
self._load_mounts()
|
|
38
|
-
|
|
39
|
-
def _load_mounts(self):
|
|
40
|
-
"""Load persisted mount configurations."""
|
|
41
|
-
if self.mounts_file.exists():
|
|
42
|
-
try:
|
|
43
|
-
with open(self.mounts_file, "r") as f:
|
|
44
|
-
self.mount_configs = json.load(f)
|
|
45
|
-
except Exception as e:
|
|
46
|
-
logger.error(f"Failed to load mounts: {e}")
|
|
47
|
-
async def restore_mounts(self):
|
|
48
|
-
"""Re-establish connections for all persisted mounts."""
|
|
49
|
-
print(f"🔄 Restoring {len(self.mount_configs)} mounts...")
|
|
50
|
-
for mount_id, config in self.mount_configs.items():
|
|
51
|
-
try:
|
|
52
|
-
if config.get("transport") == "stdio":
|
|
53
|
-
await self.mount_server(
|
|
54
|
-
mount_id=mount_id,
|
|
55
|
-
transport="stdio",
|
|
56
|
-
command=config.get("command"),
|
|
57
|
-
args=config.get("args"),
|
|
58
|
-
env=config.get("env")
|
|
59
|
-
)
|
|
60
|
-
print(f" ✅ Re-mounted: {mount_id}")
|
|
61
|
-
elif config.get("transport") == "sse":
|
|
62
|
-
await self.mount_server(
|
|
63
|
-
mount_id=mount_id,
|
|
64
|
-
transport="sse",
|
|
65
|
-
url=config.get("url")
|
|
66
|
-
)
|
|
67
|
-
print(f" ✅ Re-mounted: {mount_id}")
|
|
68
|
-
except Exception as e:
|
|
69
|
-
logger.error(f"Failed to restore mount {mount_id}: {e}")
|
|
70
|
-
print(f" ❌ Failed to restore {mount_id}: {e}")
|
|
71
|
-
def _save_mounts(self):
|
|
72
|
-
"""Persist mount configurations."""
|
|
73
|
-
try:
|
|
74
|
-
with open(self.mounts_file, "w") as f:
|
|
75
|
-
json.dump(self.mount_configs, f, indent=2)
|
|
76
|
-
except Exception as e:
|
|
77
|
-
logger.error(f"Failed to save mounts: {e}")
|
|
78
|
-
|
|
79
|
-
async def mount_server(self,
|
|
80
|
-
mount_id: str,
|
|
81
|
-
transport: str,
|
|
82
|
-
command: str = None,
|
|
83
|
-
args: List[str] = None,
|
|
84
|
-
env: Dict[str, str] = None,
|
|
85
|
-
url: str = None) -> Dict[str, Any]:
|
|
86
|
-
"""
|
|
87
|
-
Mount an MCP server.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
mount_id: Unique identifier for this mount.
|
|
91
|
-
transport: "stdio" or "sse".
|
|
92
|
-
command: (stdio) Executable command.
|
|
93
|
-
args: (stdio) List of arguments.
|
|
94
|
-
env: (stdio) Environment variables.
|
|
95
|
-
url: (sse) Server URL.
|
|
96
|
-
"""
|
|
97
|
-
if not MCP_AVAILABLE:
|
|
98
|
-
raise ImportError("The 'mcp' package is not installed. Please install it to use mounting features.")
|
|
99
|
-
|
|
100
|
-
if mount_id in self.sessions:
|
|
101
|
-
raise ValueError(f"Mount ID '{mount_id}' is already active.")
|
|
102
|
-
|
|
103
|
-
if transport == "stdio":
|
|
104
|
-
if not command:
|
|
105
|
-
raise ValueError("Command is required for stdio transport")
|
|
106
|
-
|
|
107
|
-
server_params = StdioServerParameters(
|
|
108
|
-
command=command,
|
|
109
|
-
args=args or [],
|
|
110
|
-
env={**os.environ, **(env or {})}
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# We need to maintain the context manager lifecycle
|
|
114
|
-
# This is a bit tricky in an async methods that keeps the session open.
|
|
115
|
-
# We'll use an AsyncExitStack stored in class state.
|
|
116
|
-
from contextlib import AsyncExitStack
|
|
117
|
-
|
|
118
|
-
stack = AsyncExitStack()
|
|
119
|
-
stdio_transport = await stack.enter_async_context(stdio_client(server_params))
|
|
120
|
-
self.exit_stacks[mount_id] = stack
|
|
121
|
-
|
|
122
|
-
read, write = stdio_transport
|
|
123
|
-
session = await stack.enter_async_context(ClientSession(read, write))
|
|
124
|
-
await session.initialize()
|
|
125
|
-
|
|
126
|
-
self.sessions[mount_id] = session
|
|
127
|
-
|
|
128
|
-
# Persist config
|
|
129
|
-
self.mount_configs[mount_id] = {
|
|
130
|
-
"transport": "stdio",
|
|
131
|
-
"command": command,
|
|
132
|
-
"args": args,
|
|
133
|
-
"env": env,
|
|
134
|
-
"status": "connected"
|
|
135
|
-
}
|
|
136
|
-
self._save_mounts()
|
|
137
|
-
|
|
138
|
-
return {"mount_id": mount_id, "status": "connected", "tools": len((await session.list_tools()).tools)}
|
|
139
|
-
|
|
140
|
-
elif transport == "sse":
|
|
141
|
-
if not url:
|
|
142
|
-
raise ValueError("URL is required for SSE transport")
|
|
143
|
-
|
|
144
|
-
from contextlib import AsyncExitStack
|
|
145
|
-
stack = AsyncExitStack()
|
|
146
|
-
sse_transport = await stack.enter_async_context(sse_client(url))
|
|
147
|
-
self.exit_stacks[mount_id] = stack
|
|
148
|
-
|
|
149
|
-
read, write = sse_transport
|
|
150
|
-
session = await stack.enter_async_context(ClientSession(read, write))
|
|
151
|
-
await session.initialize()
|
|
152
|
-
|
|
153
|
-
self.sessions[mount_id] = session
|
|
154
|
-
|
|
155
|
-
self.mount_configs[mount_id] = {
|
|
156
|
-
"transport": "sse",
|
|
157
|
-
"url": url,
|
|
158
|
-
"status": "connected"
|
|
159
|
-
}
|
|
160
|
-
self._save_mounts()
|
|
161
|
-
|
|
162
|
-
return {"mount_id": mount_id, "status": "connected", "tools": len((await session.list_tools()).tools)}
|
|
163
|
-
|
|
164
|
-
else:
|
|
165
|
-
raise ValueError(f"Unknown transport: {transport}")
|
|
166
|
-
|
|
167
|
-
async def unmount_server(self, mount_id: str):
|
|
168
|
-
"""Unmount a server and close connection."""
|
|
169
|
-
if mount_id in self.exit_stacks:
|
|
170
|
-
await self.exit_stacks[mount_id].aclose()
|
|
171
|
-
del self.exit_stacks[mount_id]
|
|
172
|
-
|
|
173
|
-
if mount_id in self.sessions:
|
|
174
|
-
del self.sessions[mount_id]
|
|
175
|
-
|
|
176
|
-
if mount_id in self.mount_configs:
|
|
177
|
-
del self.mount_configs[mount_id]
|
|
178
|
-
self._save_mounts()
|
|
179
|
-
|
|
180
|
-
async def list_mounts(self) -> List[Dict]:
|
|
181
|
-
"""List active mounts."""
|
|
182
|
-
return [
|
|
183
|
-
{"id": k, **v} for k, v in self.mount_configs.items()
|
|
184
|
-
]
|
|
185
|
-
|
|
186
|
-
async def list_tools(self) -> List[Dict]:
|
|
187
|
-
"""
|
|
188
|
-
Aggregate tools from all mounted servers.
|
|
189
|
-
Tools are namespaced as 'mount_id:tool_name'.
|
|
190
|
-
"""
|
|
191
|
-
aggregated_tools = []
|
|
192
|
-
|
|
193
|
-
for mount_id, session in self.sessions.items():
|
|
194
|
-
try:
|
|
195
|
-
result = await session.list_tools()
|
|
196
|
-
for tool in result.tools:
|
|
197
|
-
# Create a namespaced copy of the tool definition
|
|
198
|
-
tool_dict = tool.model_dump() if hasattr(tool, "model_dump") else tool.__dict__
|
|
199
|
-
tool_dict["name"] = f"{mount_id}:{tool.name}"
|
|
200
|
-
tool_dict["description"] = f"[{mount_id}] {tool_dict.get('description', '')}"
|
|
201
|
-
aggregated_tools.append(tool_dict)
|
|
202
|
-
except Exception as e:
|
|
203
|
-
logger.error(f"Error listing tools for {mount_id}: {e}")
|
|
204
|
-
|
|
205
|
-
return aggregated_tools
|
|
206
|
-
|
|
207
|
-
async def call_tool(self, name: str, args: Dict) -> Any:
|
|
208
|
-
"""
|
|
209
|
-
Call a namespaced tool.
|
|
210
|
-
Name format: 'mount_id:tool_name'
|
|
211
|
-
"""
|
|
212
|
-
if ":" not in name:
|
|
213
|
-
raise ValueError("Invalid namespaced tool name. Format must be 'mount_id:tool_name'")
|
|
214
|
-
|
|
215
|
-
mount_id, tool_name = name.split(":", 1)
|
|
216
|
-
|
|
217
|
-
if mount_id not in self.sessions:
|
|
218
|
-
raise ValueError(f"Mount ID '{mount_id}' not found")
|
|
219
|
-
|
|
220
|
-
session = self.sessions[mount_id]
|
|
221
|
-
result = await session.call_tool(tool_name, arguments=args)
|
|
222
|
-
return result
|
|
223
|
-
|
|
224
|
-
async def traverse_and_mount(self, root_mount_id: str) -> Dict:
|
|
225
|
-
"""
|
|
226
|
-
Recursive discovery logic.
|
|
227
|
-
Inspects tools of a mounted server to find other 'mount_server' compatible capabilities.
|
|
228
|
-
This is a heuristic implementation for the demo.
|
|
229
|
-
"""
|
|
230
|
-
if root_mount_id not in self.sessions:
|
|
231
|
-
raise ValueError(f"Root mount '{root_mount_id}' not found")
|
|
232
|
-
|
|
233
|
-
session = self.sessions[root_mount_id]
|
|
234
|
-
tools = await session.list_tools()
|
|
235
|
-
|
|
236
|
-
found_servers = []
|
|
237
|
-
|
|
238
|
-
# Heuristic: Look for tools that return 'mcp_server' or have 'mount' in description
|
|
239
|
-
# In a real scenario, this would use a standardized 'list_servers' tool or resource.
|
|
240
|
-
# For the demo, we'll just report what we see.
|
|
241
|
-
|
|
242
|
-
for tool in tools.tools:
|
|
243
|
-
if "server" in tool.name.lower() or "mount" in tool.name.lower():
|
|
244
|
-
found_servers.append(tool.name)
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
"root": root_mount_id,
|
|
248
|
-
"potential_sub_servers": found_servers,
|
|
249
|
-
"status": "traversal_complete"
|
|
250
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
The .nuke Protocol (Nucleus Universal Knowledge Exchange).
|
|
3
|
-
Phase 57 Implementation.
|
|
4
|
-
|
|
5
|
-
Format: ZIP Archive
|
|
6
|
-
Structure:
|
|
7
|
-
/manifest.json (AgentManifest Schema)
|
|
8
|
-
/signature.sig (Ed25519 Signature of manifest.json)
|
|
9
|
-
/tools/ (Python Code)
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import logging
|
|
14
|
-
import zipfile
|
|
15
|
-
import shutil
|
|
16
|
-
import tempfile
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import List, Dict
|
|
19
|
-
|
|
20
|
-
from .identity.keygen import KeyManager
|
|
21
|
-
from .identity.manifest import AgentManifest, ManifestValidator
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger("NUKE_PROTOCOL")
|
|
24
|
-
|
|
25
|
-
class NukePacker:
|
|
26
|
-
"""
|
|
27
|
-
Creates signed .nuke artifacts.
|
|
28
|
-
"""
|
|
29
|
-
def __init__(self, staging_root: Path):
|
|
30
|
-
self.staging_root = staging_root
|
|
31
|
-
self.staging_root.mkdir(parents=True, exist_ok=True)
|
|
32
|
-
self.key_manager = KeyManager()
|
|
33
|
-
|
|
34
|
-
def pack(self, manifest: AgentManifest, tool_paths: List[Path], private_key_pem: str, output_path: Path) -> Path:
|
|
35
|
-
"""
|
|
36
|
-
Bundles an agent into a .nuke file.
|
|
37
|
-
"""
|
|
38
|
-
logger.info(f"📦 Packing Agent: {manifest.agent.id} -> {output_path}")
|
|
39
|
-
|
|
40
|
-
# 1. Create Staging Area
|
|
41
|
-
with tempfile.TemporaryDirectory(dir=self.staging_root) as tmp_dir:
|
|
42
|
-
staging_dir = Path(tmp_dir)
|
|
43
|
-
tools_dir = staging_dir / "tools"
|
|
44
|
-
tools_dir.mkdir()
|
|
45
|
-
|
|
46
|
-
# 2. Add Tools
|
|
47
|
-
for tool_path in tool_paths:
|
|
48
|
-
if not tool_path.exists():
|
|
49
|
-
raise FileNotFoundError(f"Tool not found: {tool_path}")
|
|
50
|
-
shutil.copy2(tool_path, tools_dir / tool_path.name)
|
|
51
|
-
|
|
52
|
-
# 3. Write Manifest
|
|
53
|
-
manifest_json = manifest.model_dump_json(indent=2)
|
|
54
|
-
manifest_path = staging_dir / "manifest.json"
|
|
55
|
-
manifest_path.write_text(manifest_json)
|
|
56
|
-
|
|
57
|
-
# 4. Sign Manifest
|
|
58
|
-
# We sign the BYTES of the manifest.json file to ensure exact integrity
|
|
59
|
-
manifest_bytes = manifest_path.read_bytes()
|
|
60
|
-
signature = self.key_manager.sign(private_key_pem, manifest_bytes)
|
|
61
|
-
|
|
62
|
-
# Save signature as hex string
|
|
63
|
-
(staging_dir / "signature.sig").write_text(signature.hex())
|
|
64
|
-
|
|
65
|
-
# 5. Zip it
|
|
66
|
-
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
67
|
-
for file in staging_dir.rglob("*"):
|
|
68
|
-
if file.is_file():
|
|
69
|
-
zf.write(file, file.relative_to(staging_dir))
|
|
70
|
-
|
|
71
|
-
logger.info(f"✅ Created {output_path} ({output_path.stat().st_size} bytes)")
|
|
72
|
-
return output_path
|
|
73
|
-
|
|
74
|
-
class NukeLoader:
|
|
75
|
-
"""
|
|
76
|
-
Loads and Verifies .nuke artifacts.
|
|
77
|
-
"""
|
|
78
|
-
def __init__(self, install_root: Path):
|
|
79
|
-
self.install_root = install_root
|
|
80
|
-
self.key_manager = KeyManager()
|
|
81
|
-
self.installed_tools_dir = install_root / "tools" / "installed"
|
|
82
|
-
self.installed_tools_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
-
|
|
84
|
-
def load(self, nuke_path: Path, trusted_keys: Dict[str, str]) -> AgentManifest:
|
|
85
|
-
"""
|
|
86
|
-
Verifies and Unpacks a .nuke file.
|
|
87
|
-
trusted_keys: Dict[KeyID, PublicKeyPEM]
|
|
88
|
-
"""
|
|
89
|
-
logger.info(f"🔓 Loading .nuke: {nuke_path}")
|
|
90
|
-
|
|
91
|
-
with zipfile.ZipFile(nuke_path, 'r') as zf:
|
|
92
|
-
# 1. Extract Manifest & Sig to memory
|
|
93
|
-
try:
|
|
94
|
-
manifest_bytes = zf.read("manifest.json")
|
|
95
|
-
signature_hex = zf.read("signature.sig").decode().strip()
|
|
96
|
-
signature_bytes = bytes.fromhex(signature_hex)
|
|
97
|
-
except KeyError:
|
|
98
|
-
raise ValueError("❌ Invalid .nuke: Missing manifest or signature")
|
|
99
|
-
|
|
100
|
-
# 2. Parse Manifest
|
|
101
|
-
try:
|
|
102
|
-
manifest_dict = json.loads(manifest_bytes)
|
|
103
|
-
manifest = ManifestValidator.validate(manifest_dict)
|
|
104
|
-
except Exception as e:
|
|
105
|
-
raise ValueError(f"❌ Invalid Manifest: {e}")
|
|
106
|
-
|
|
107
|
-
# 3. Lookup Public Key
|
|
108
|
-
# In Phase 57, we assume the publisher ID is embedded in the manifest or we trust the signer
|
|
109
|
-
# For this MVP, we verify using the TRUSTED_KEYS passed in.
|
|
110
|
-
# We treat the Key ID as the signer.
|
|
111
|
-
|
|
112
|
-
# Note: The Manifest doesn't strictly have a "signer_id" field in our schema yet.
|
|
113
|
-
# We rely on the KeyManager logic: We need the Public Key corresponding to the Private Key that signed it.
|
|
114
|
-
# In a real PKI, the Manifest would contain the KeyID, or the Repository would provide it.
|
|
115
|
-
# Let's try every trusted key. If ANY valid key verifies it, we are good.
|
|
116
|
-
|
|
117
|
-
verified = False
|
|
118
|
-
signer_key_id = None
|
|
119
|
-
|
|
120
|
-
for key_id, pub_pem in trusted_keys.items():
|
|
121
|
-
if self.key_manager.verify(pub_pem, signature_bytes, manifest_bytes):
|
|
122
|
-
verified = True
|
|
123
|
-
signer_key_id = key_id
|
|
124
|
-
break
|
|
125
|
-
|
|
126
|
-
if not verified:
|
|
127
|
-
raise PermissionError("❌ SIGNATURE VERIFICATION FAILED: No trusted key matches signature")
|
|
128
|
-
|
|
129
|
-
logger.info(f"✅ Verified by KeyID: {signer_key_id}")
|
|
130
|
-
|
|
131
|
-
# 4. Install Tools
|
|
132
|
-
# We namespace them by Agent ID to avoid collisions
|
|
133
|
-
agent_dir = self.installed_tools_dir / manifest.agent.id
|
|
134
|
-
if agent_dir.exists():
|
|
135
|
-
shutil.rmtree(agent_dir)
|
|
136
|
-
agent_dir.mkdir()
|
|
137
|
-
|
|
138
|
-
for file in zf.namelist():
|
|
139
|
-
if file.startswith("tools/") and file.endswith(".py"):
|
|
140
|
-
# Extract tool code
|
|
141
|
-
tool_content = zf.read(file)
|
|
142
|
-
tool_name = Path(file).name
|
|
143
|
-
(agent_dir / tool_name).write_bytes(tool_content)
|
|
144
|
-
|
|
145
|
-
logger.info(f" -> Installed {len(list(agent_dir.glob('*.py')))} tools to {agent_dir}")
|
|
146
|
-
|
|
147
|
-
return manifest
|