nexo-brain 7.30.19 → 7.30.21
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/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -2
- package/bin/nexo-brain.js +1 -1
- package/bin/nexo-managed-mcp.js +90 -0
- package/package.json +4 -2
- package/src/auto_update.py +16 -1
- package/src/client_sync.py +47 -0
- package/src/closure_plane.py +720 -0
- package/src/db/_schema.py +93 -0
- package/src/managed_mcp/__init__.py +31 -0
- package/src/managed_mcp/catalog.json +77 -0
- package/src/managed_mcp/catalog.py +263 -0
- package/src/managed_mcp/client_config.py +76 -0
- package/src/managed_mcp/lock.json +52 -0
- package/src/managed_mcp/reconcile.py +122 -0
- package/src/plugins/update.py +1 -0
- package/src/server.py +70 -0
- package/tool-enforcement-map.json +90 -0
package/src/db/_schema.py
CHANGED
|
@@ -2775,6 +2775,98 @@ def _m77_morning_briefing_presentation(conn):
|
|
|
2775
2775
|
)
|
|
2776
2776
|
|
|
2777
2777
|
|
|
2778
|
+
def _m78_operational_closure_plane(conn):
|
|
2779
|
+
"""Operational Closure Plane MVP: canonical read-only closure items."""
|
|
2780
|
+
conn.execute(
|
|
2781
|
+
"""
|
|
2782
|
+
CREATE TABLE IF NOT EXISTS closure_items (
|
|
2783
|
+
id TEXT PRIMARY KEY,
|
|
2784
|
+
title TEXT NOT NULL,
|
|
2785
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
2786
|
+
kind TEXT NOT NULL,
|
|
2787
|
+
state TEXT NOT NULL DEFAULT 'open',
|
|
2788
|
+
source_primary TEXT NOT NULL,
|
|
2789
|
+
source_key TEXT NOT NULL,
|
|
2790
|
+
dedupe_key TEXT NOT NULL,
|
|
2791
|
+
impact_score REAL NOT NULL DEFAULT 0,
|
|
2792
|
+
urgency_score REAL NOT NULL DEFAULT 0,
|
|
2793
|
+
risk_score REAL NOT NULL DEFAULT 0,
|
|
2794
|
+
confidence_score REAL NOT NULL DEFAULT 0,
|
|
2795
|
+
priority_score REAL NOT NULL DEFAULT 0,
|
|
2796
|
+
safety_class TEXT NOT NULL DEFAULT 'normal',
|
|
2797
|
+
capability_required TEXT NOT NULL DEFAULT '',
|
|
2798
|
+
capability_status TEXT NOT NULL DEFAULT 'unknown',
|
|
2799
|
+
owner TEXT NOT NULL DEFAULT 'nero',
|
|
2800
|
+
next_action TEXT NOT NULL DEFAULT '',
|
|
2801
|
+
blocker_reason TEXT NOT NULL DEFAULT '',
|
|
2802
|
+
evidence_required TEXT NOT NULL DEFAULT '',
|
|
2803
|
+
evidence_observed TEXT NOT NULL DEFAULT '',
|
|
2804
|
+
deadline_at TEXT NOT NULL DEFAULT '',
|
|
2805
|
+
first_seen_at TEXT NOT NULL,
|
|
2806
|
+
last_seen_at TEXT NOT NULL,
|
|
2807
|
+
last_progress_at TEXT NOT NULL DEFAULT '',
|
|
2808
|
+
closed_at TEXT NOT NULL DEFAULT '',
|
|
2809
|
+
close_reason TEXT NOT NULL DEFAULT '',
|
|
2810
|
+
source_payload_json TEXT NOT NULL DEFAULT '{}',
|
|
2811
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2812
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2813
|
+
UNIQUE(dedupe_key)
|
|
2814
|
+
)
|
|
2815
|
+
"""
|
|
2816
|
+
)
|
|
2817
|
+
conn.execute(
|
|
2818
|
+
"""
|
|
2819
|
+
CREATE TABLE IF NOT EXISTS closure_item_sources (
|
|
2820
|
+
id TEXT PRIMARY KEY,
|
|
2821
|
+
closure_item_id TEXT NOT NULL,
|
|
2822
|
+
source_type TEXT NOT NULL,
|
|
2823
|
+
source_id TEXT NOT NULL,
|
|
2824
|
+
source_status TEXT NOT NULL DEFAULT '',
|
|
2825
|
+
source_payload_json TEXT NOT NULL DEFAULT '{}',
|
|
2826
|
+
observed_at TEXT NOT NULL,
|
|
2827
|
+
FOREIGN KEY(closure_item_id) REFERENCES closure_items(id) ON DELETE CASCADE,
|
|
2828
|
+
UNIQUE(closure_item_id, source_type, source_id)
|
|
2829
|
+
)
|
|
2830
|
+
"""
|
|
2831
|
+
)
|
|
2832
|
+
conn.execute(
|
|
2833
|
+
"""
|
|
2834
|
+
CREATE TABLE IF NOT EXISTS closure_item_events (
|
|
2835
|
+
id TEXT PRIMARY KEY,
|
|
2836
|
+
closure_item_id TEXT NOT NULL,
|
|
2837
|
+
event_type TEXT NOT NULL,
|
|
2838
|
+
from_state TEXT NOT NULL DEFAULT '',
|
|
2839
|
+
to_state TEXT NOT NULL DEFAULT '',
|
|
2840
|
+
note TEXT NOT NULL DEFAULT '',
|
|
2841
|
+
evidence TEXT NOT NULL DEFAULT '',
|
|
2842
|
+
actor TEXT NOT NULL DEFAULT 'nexo',
|
|
2843
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2844
|
+
FOREIGN KEY(closure_item_id) REFERENCES closure_items(id) ON DELETE CASCADE
|
|
2845
|
+
)
|
|
2846
|
+
"""
|
|
2847
|
+
)
|
|
2848
|
+
conn.execute(
|
|
2849
|
+
"""
|
|
2850
|
+
CREATE TABLE IF NOT EXISTS closure_daily_snapshots (
|
|
2851
|
+
snapshot_date TEXT PRIMARY KEY,
|
|
2852
|
+
total_open INTEGER NOT NULL DEFAULT 0,
|
|
2853
|
+
total_verified INTEGER NOT NULL DEFAULT 0,
|
|
2854
|
+
total_waiting INTEGER NOT NULL DEFAULT 0,
|
|
2855
|
+
total_closed INTEGER NOT NULL DEFAULT 0,
|
|
2856
|
+
top_items_json TEXT NOT NULL DEFAULT '[]',
|
|
2857
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2858
|
+
)
|
|
2859
|
+
"""
|
|
2860
|
+
)
|
|
2861
|
+
_migrate_add_index(conn, "idx_closure_items_state_priority", "closure_items", "state, priority_score DESC, updated_at")
|
|
2862
|
+
_migrate_add_index(conn, "idx_closure_items_source", "closure_items", "source_primary, source_key")
|
|
2863
|
+
_migrate_add_index(conn, "idx_closure_items_kind", "closure_items", "kind, state")
|
|
2864
|
+
_migrate_add_index(conn, "idx_closure_items_deadline", "closure_items", "deadline_at")
|
|
2865
|
+
_migrate_add_index(conn, "idx_closure_sources_item", "closure_item_sources", "closure_item_id")
|
|
2866
|
+
_migrate_add_index(conn, "idx_closure_sources_source", "closure_item_sources", "source_type, source_id")
|
|
2867
|
+
_migrate_add_index(conn, "idx_closure_events_item", "closure_item_events", "closure_item_id, created_at")
|
|
2868
|
+
|
|
2869
|
+
|
|
2778
2870
|
MIGRATIONS = [
|
|
2779
2871
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
2780
2872
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -2853,6 +2945,7 @@ MIGRATIONS = [
|
|
|
2853
2945
|
(75, "failure_prevention_ledger", _m75_failure_prevention_ledger),
|
|
2854
2946
|
(76, "semantic_layers", _m76_semantic_layers),
|
|
2855
2947
|
(77, "morning_briefing_presentation", _m77_morning_briefing_presentation),
|
|
2948
|
+
(78, "operational_closure_plane", _m78_operational_closure_plane),
|
|
2856
2949
|
]
|
|
2857
2950
|
|
|
2858
2951
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Managed MCP capability catalog and client-config helpers."""
|
|
2
|
+
|
|
3
|
+
from .catalog import (
|
|
4
|
+
CATALOG_PATH,
|
|
5
|
+
LOCK_PATH,
|
|
6
|
+
ManagedCapability,
|
|
7
|
+
ManagedProvider,
|
|
8
|
+
build_managed_server_entries,
|
|
9
|
+
load_catalog,
|
|
10
|
+
load_lock,
|
|
11
|
+
provider_for_capability,
|
|
12
|
+
validate_catalog_lock,
|
|
13
|
+
)
|
|
14
|
+
from .client_config import merge_json_mcp_servers, merge_toml_mcp_servers
|
|
15
|
+
from .reconcile import managed_mcp_status, reconcile_managed_mcp
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CATALOG_PATH",
|
|
19
|
+
"LOCK_PATH",
|
|
20
|
+
"ManagedCapability",
|
|
21
|
+
"ManagedProvider",
|
|
22
|
+
"build_managed_server_entries",
|
|
23
|
+
"load_catalog",
|
|
24
|
+
"load_lock",
|
|
25
|
+
"provider_for_capability",
|
|
26
|
+
"validate_catalog_lock",
|
|
27
|
+
"merge_json_mcp_servers",
|
|
28
|
+
"merge_toml_mcp_servers",
|
|
29
|
+
"managed_mcp_status",
|
|
30
|
+
"reconcile_managed_mcp",
|
|
31
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "nexo.managed_mcp.catalog.v1",
|
|
3
|
+
"catalog_version": "2026.06.06",
|
|
4
|
+
"defaults_profile": "normal_user",
|
|
5
|
+
"capabilities": [
|
|
6
|
+
{
|
|
7
|
+
"id": "chrome_control",
|
|
8
|
+
"display_name": "Chrome control",
|
|
9
|
+
"enabled_by_default": true,
|
|
10
|
+
"risk": "high",
|
|
11
|
+
"clients": ["claude_code", "claude_desktop", "codex"],
|
|
12
|
+
"providers": [
|
|
13
|
+
{
|
|
14
|
+
"id": "chrome-devtools-mcp",
|
|
15
|
+
"platforms": ["darwin", "win32"],
|
|
16
|
+
"source": {"type": "npm", "package": "chrome-devtools-mcp"},
|
|
17
|
+
"version_policy": "locked",
|
|
18
|
+
"transport": "stdio",
|
|
19
|
+
"preflight": {
|
|
20
|
+
"node": "^20.19.0 || ^22.12.0 || >=23",
|
|
21
|
+
"binaries": ["chrome"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "desktop_control",
|
|
28
|
+
"display_name": "Desktop control",
|
|
29
|
+
"enabled_by_default": true,
|
|
30
|
+
"risk": "high",
|
|
31
|
+
"clients": ["claude_code", "claude_desktop", "codex"],
|
|
32
|
+
"providers": [
|
|
33
|
+
{
|
|
34
|
+
"id": "mac-use-mcp",
|
|
35
|
+
"platforms": ["darwin"],
|
|
36
|
+
"source": {"type": "npm", "package": "mac-use-mcp"},
|
|
37
|
+
"version_policy": "locked",
|
|
38
|
+
"transport": "stdio",
|
|
39
|
+
"preflight": {
|
|
40
|
+
"os_permissions": ["accessibility", "screen_recording"]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "native-devtools-mcp",
|
|
45
|
+
"platforms": ["win32"],
|
|
46
|
+
"source": {"type": "npm", "package": "native-devtools-mcp"},
|
|
47
|
+
"version_policy": "locked",
|
|
48
|
+
"transport": "stdio"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "open-computer-use",
|
|
52
|
+
"platforms": ["win32"],
|
|
53
|
+
"source": {"type": "npm", "package": "open-computer-use"},
|
|
54
|
+
"version_policy": "locked",
|
|
55
|
+
"transport": "stdio",
|
|
56
|
+
"fallback": true
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "power_control",
|
|
62
|
+
"display_name": "Power control",
|
|
63
|
+
"enabled_by_default": true,
|
|
64
|
+
"risk": "critical",
|
|
65
|
+
"clients": ["claude_code", "claude_desktop", "codex"],
|
|
66
|
+
"providers": [
|
|
67
|
+
{
|
|
68
|
+
"id": "desktop-commander",
|
|
69
|
+
"platforms": ["darwin", "win32"],
|
|
70
|
+
"source": {"type": "npm", "package": "@wonderwhy-er/desktop-commander"},
|
|
71
|
+
"version_policy": "locked",
|
|
72
|
+
"transport": "stdio"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
CATALOG_PATH = Path(__file__).with_name("catalog.json")
|
|
12
|
+
LOCK_PATH = Path(__file__).with_name("lock.json")
|
|
13
|
+
CLIENT_KEYS = {"claude_code", "claude_desktop", "codex"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ManagedProvider:
|
|
18
|
+
id: str
|
|
19
|
+
package: str
|
|
20
|
+
source_type: str
|
|
21
|
+
platforms: tuple[str, ...]
|
|
22
|
+
fallback: bool = False
|
|
23
|
+
risk: str = ""
|
|
24
|
+
version: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ManagedCapability:
|
|
29
|
+
id: str
|
|
30
|
+
display_name: str
|
|
31
|
+
enabled_by_default: bool
|
|
32
|
+
risk: str
|
|
33
|
+
clients: tuple[str, ...]
|
|
34
|
+
providers: tuple[ManagedProvider, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_json(path: Path) -> dict[str, Any]:
|
|
38
|
+
payload = json.loads(path.read_text())
|
|
39
|
+
if not isinstance(payload, dict):
|
|
40
|
+
raise ValueError(f"{path} must contain a JSON object")
|
|
41
|
+
return payload
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_catalog(path: Path | None = None) -> dict[str, Any]:
|
|
45
|
+
return _load_json(path or CATALOG_PATH)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_lock(path: Path | None = None) -> dict[str, Any]:
|
|
49
|
+
return _load_json(path or LOCK_PATH)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _platform_key(platform: str | None = None) -> str:
|
|
53
|
+
value = (platform or sys.platform or "").lower()
|
|
54
|
+
if value.startswith("darwin"):
|
|
55
|
+
return "darwin"
|
|
56
|
+
if value.startswith(("win32", "cygwin", "msys")) or os.name == "nt":
|
|
57
|
+
return "win32"
|
|
58
|
+
if value.startswith("linux"):
|
|
59
|
+
return "linux"
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_catalog_lock(
|
|
64
|
+
catalog: dict[str, Any] | None = None,
|
|
65
|
+
lock: dict[str, Any] | None = None,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
catalog = catalog or load_catalog()
|
|
68
|
+
lock = lock or load_lock()
|
|
69
|
+
errors: list[str] = []
|
|
70
|
+
warnings: list[str] = []
|
|
71
|
+
|
|
72
|
+
if catalog.get("schema") != "nexo.managed_mcp.catalog.v1":
|
|
73
|
+
errors.append("catalog schema mismatch")
|
|
74
|
+
if lock.get("schema") != "nexo.managed_mcp.lock.v1":
|
|
75
|
+
errors.append("lock schema mismatch")
|
|
76
|
+
if catalog.get("catalog_version") != lock.get("catalog_version"):
|
|
77
|
+
errors.append("catalog_version mismatch")
|
|
78
|
+
|
|
79
|
+
lock_providers = lock.get("providers")
|
|
80
|
+
if not isinstance(lock_providers, dict):
|
|
81
|
+
errors.append("lock providers must be an object")
|
|
82
|
+
lock_providers = {}
|
|
83
|
+
|
|
84
|
+
capabilities = catalog.get("capabilities")
|
|
85
|
+
if not isinstance(capabilities, list) or not capabilities:
|
|
86
|
+
errors.append("catalog capabilities must be a non-empty list")
|
|
87
|
+
capabilities = []
|
|
88
|
+
|
|
89
|
+
capability_ids: set[str] = set()
|
|
90
|
+
required_provider_ids: set[str] = set()
|
|
91
|
+
for capability in capabilities:
|
|
92
|
+
if not isinstance(capability, dict):
|
|
93
|
+
errors.append("capability entries must be objects")
|
|
94
|
+
continue
|
|
95
|
+
capability_id = str(capability.get("id") or "").strip()
|
|
96
|
+
if not capability_id:
|
|
97
|
+
errors.append("capability without id")
|
|
98
|
+
continue
|
|
99
|
+
if capability_id in capability_ids:
|
|
100
|
+
errors.append(f"duplicate capability id: {capability_id}")
|
|
101
|
+
capability_ids.add(capability_id)
|
|
102
|
+
clients = capability.get("clients")
|
|
103
|
+
if not isinstance(clients, list) or not clients:
|
|
104
|
+
errors.append(f"{capability_id}: clients must be non-empty")
|
|
105
|
+
elif any(str(client) not in CLIENT_KEYS for client in clients):
|
|
106
|
+
errors.append(f"{capability_id}: unknown client in clients")
|
|
107
|
+
providers = capability.get("providers")
|
|
108
|
+
if not isinstance(providers, list) or not providers:
|
|
109
|
+
errors.append(f"{capability_id}: providers must be non-empty")
|
|
110
|
+
continue
|
|
111
|
+
platforms_by_provider: set[str] = set()
|
|
112
|
+
for provider in providers:
|
|
113
|
+
if not isinstance(provider, dict):
|
|
114
|
+
errors.append(f"{capability_id}: provider entries must be objects")
|
|
115
|
+
continue
|
|
116
|
+
provider_id = str(provider.get("id") or "").strip()
|
|
117
|
+
source = provider.get("source") if isinstance(provider.get("source"), dict) else {}
|
|
118
|
+
package = str(source.get("package") or "").strip()
|
|
119
|
+
platforms = provider.get("platforms")
|
|
120
|
+
if not provider_id:
|
|
121
|
+
errors.append(f"{capability_id}: provider without id")
|
|
122
|
+
continue
|
|
123
|
+
required_provider_ids.add(provider_id)
|
|
124
|
+
if not package:
|
|
125
|
+
errors.append(f"{provider_id}: source.package missing")
|
|
126
|
+
if not isinstance(platforms, list) or not platforms:
|
|
127
|
+
errors.append(f"{provider_id}: platforms missing")
|
|
128
|
+
else:
|
|
129
|
+
platforms_by_provider.update(str(item) for item in platforms)
|
|
130
|
+
if provider.get("version_policy") not in {"locked", "latest_on_release"}:
|
|
131
|
+
errors.append(f"{provider_id}: unsupported version_policy")
|
|
132
|
+
locked = lock_providers.get(provider_id)
|
|
133
|
+
if not isinstance(locked, dict):
|
|
134
|
+
errors.append(f"{provider_id}: missing from lockfile")
|
|
135
|
+
elif locked.get("package") != package:
|
|
136
|
+
errors.append(f"{provider_id}: lock package mismatch")
|
|
137
|
+
elif "@latest" in str(locked.get("version") or ""):
|
|
138
|
+
errors.append(f"{provider_id}: lock version must not use @latest")
|
|
139
|
+
elif str(locked.get("version") or "").strip() in {"", "0.0.0-managed"}:
|
|
140
|
+
errors.append(f"{provider_id}: lock version must be an exact package version")
|
|
141
|
+
elif str(locked.get("source_type") or "") == "npm":
|
|
142
|
+
if not str(locked.get("integrity") or "").strip():
|
|
143
|
+
errors.append(f"{provider_id}: npm lock integrity missing")
|
|
144
|
+
if not str(locked.get("tarball") or "").strip():
|
|
145
|
+
errors.append(f"{provider_id}: npm lock tarball missing")
|
|
146
|
+
if not str(locked.get("bin") or "").strip():
|
|
147
|
+
errors.append(f"{provider_id}: npm lock bin missing")
|
|
148
|
+
if capability.get("enabled_by_default") is True:
|
|
149
|
+
for required_platform in ("darwin", "win32"):
|
|
150
|
+
if required_platform not in platforms_by_provider:
|
|
151
|
+
errors.append(f"{capability_id}: missing {required_platform} provider")
|
|
152
|
+
|
|
153
|
+
extra = set(lock_providers) - required_provider_ids
|
|
154
|
+
if extra:
|
|
155
|
+
warnings.append("lock contains unused providers: " + ", ".join(sorted(extra)))
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"ok": not errors,
|
|
159
|
+
"errors": errors,
|
|
160
|
+
"warnings": warnings,
|
|
161
|
+
"catalog_version": str(catalog.get("catalog_version") or ""),
|
|
162
|
+
"capabilities": sorted(capability_ids),
|
|
163
|
+
"providers": sorted(required_provider_ids),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def provider_for_capability(
|
|
168
|
+
capability: dict[str, Any],
|
|
169
|
+
*,
|
|
170
|
+
platform: str | None = None,
|
|
171
|
+
) -> dict[str, Any] | None:
|
|
172
|
+
platform_key = _platform_key(platform)
|
|
173
|
+
providers = capability.get("providers")
|
|
174
|
+
if not isinstance(providers, list):
|
|
175
|
+
return None
|
|
176
|
+
exact: list[dict[str, Any]] = []
|
|
177
|
+
fallbacks: list[dict[str, Any]] = []
|
|
178
|
+
for provider in providers:
|
|
179
|
+
if not isinstance(provider, dict):
|
|
180
|
+
continue
|
|
181
|
+
platforms = provider.get("platforms")
|
|
182
|
+
if not isinstance(platforms, list) or platform_key not in {str(p) for p in platforms}:
|
|
183
|
+
continue
|
|
184
|
+
if provider.get("fallback"):
|
|
185
|
+
fallbacks.append(provider)
|
|
186
|
+
else:
|
|
187
|
+
exact.append(provider)
|
|
188
|
+
return (exact or fallbacks or [None])[0]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _runner_path(nexo_home: Path, runtime_root: Path | None = None) -> Path:
|
|
192
|
+
runtime_bin = nexo_home / "runtime" / "bin" / "nexo-managed-mcp"
|
|
193
|
+
if runtime_bin.exists():
|
|
194
|
+
return runtime_bin
|
|
195
|
+
if runtime_root:
|
|
196
|
+
candidate = runtime_root / "bin" / "nexo-managed-mcp.js"
|
|
197
|
+
if candidate.exists():
|
|
198
|
+
return candidate
|
|
199
|
+
sibling = runtime_root.parent / "bin" / "nexo-managed-mcp.js"
|
|
200
|
+
if sibling.exists():
|
|
201
|
+
return sibling
|
|
202
|
+
return runtime_bin
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _entry_digest(entry: dict[str, Any]) -> str:
|
|
206
|
+
body = json.dumps(entry, sort_keys=True, separators=(",", ":"))
|
|
207
|
+
return "sha256:" + hashlib.sha256(body.encode("utf-8")).hexdigest()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_managed_server_entries(
|
|
211
|
+
*,
|
|
212
|
+
client: str,
|
|
213
|
+
nexo_home: str | os.PathLike[str] | Path,
|
|
214
|
+
runtime_root: str | os.PathLike[str] | Path | None = None,
|
|
215
|
+
catalog: dict[str, Any] | None = None,
|
|
216
|
+
lock: dict[str, Any] | None = None,
|
|
217
|
+
platform: str | None = None,
|
|
218
|
+
) -> dict[str, dict[str, Any]]:
|
|
219
|
+
catalog = catalog or load_catalog()
|
|
220
|
+
lock = lock or load_lock()
|
|
221
|
+
validation = validate_catalog_lock(catalog, lock)
|
|
222
|
+
if not validation["ok"]:
|
|
223
|
+
raise ValueError("; ".join(validation["errors"]))
|
|
224
|
+
nexo_home_path = Path(nexo_home).expanduser()
|
|
225
|
+
runtime_root_path = Path(runtime_root).expanduser() if runtime_root else None
|
|
226
|
+
runner = _runner_path(nexo_home_path, runtime_root_path)
|
|
227
|
+
lock_providers = lock.get("providers") if isinstance(lock.get("providers"), dict) else {}
|
|
228
|
+
entries: dict[str, dict[str, Any]] = {}
|
|
229
|
+
for capability in catalog.get("capabilities") or []:
|
|
230
|
+
if not isinstance(capability, dict):
|
|
231
|
+
continue
|
|
232
|
+
if not capability.get("enabled_by_default"):
|
|
233
|
+
continue
|
|
234
|
+
clients = {str(item) for item in capability.get("clients") or []}
|
|
235
|
+
if client not in clients:
|
|
236
|
+
continue
|
|
237
|
+
capability_id = str(capability.get("id") or "").strip()
|
|
238
|
+
provider = provider_for_capability(capability, platform=platform)
|
|
239
|
+
if not capability_id or not provider:
|
|
240
|
+
continue
|
|
241
|
+
provider_id = str(provider.get("id") or "").strip()
|
|
242
|
+
locked = lock_providers.get(provider_id) if isinstance(lock_providers, dict) else {}
|
|
243
|
+
name = f"nexo_{capability_id}"
|
|
244
|
+
entry = {
|
|
245
|
+
"command": str(runner),
|
|
246
|
+
"args": ["run", capability_id],
|
|
247
|
+
"env": {"NEXO_HOME": str(nexo_home_path)},
|
|
248
|
+
"nexo": {
|
|
249
|
+
"owner": "nexo",
|
|
250
|
+
"schema": "nexo.managed_mcp.client.v1",
|
|
251
|
+
"capability_id": capability_id,
|
|
252
|
+
"provider_id": provider_id,
|
|
253
|
+
"provider_package": str((locked or {}).get("package") or ""),
|
|
254
|
+
"provider_version": str((locked or {}).get("version") or ""),
|
|
255
|
+
"provider_bin": str((locked or {}).get("bin") or ""),
|
|
256
|
+
"risk": str(capability.get("risk") or ""),
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
if runtime_root_path:
|
|
260
|
+
entry["env"]["NEXO_CODE"] = str(runtime_root_path)
|
|
261
|
+
entry["nexo"]["config_digest"] = _entry_digest(entry)
|
|
262
|
+
entries[name] = entry
|
|
263
|
+
return entries
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _is_nexo_owned(entry: Any) -> bool:
|
|
8
|
+
if not isinstance(entry, dict):
|
|
9
|
+
return False
|
|
10
|
+
meta = entry.get("nexo")
|
|
11
|
+
return isinstance(meta, dict) and meta.get("owner") == "nexo"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def merge_json_mcp_servers(payload: dict[str, Any], entries: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
15
|
+
result = deepcopy(payload) if isinstance(payload, dict) else {}
|
|
16
|
+
servers = result.setdefault("mcpServers", {})
|
|
17
|
+
if not isinstance(servers, dict):
|
|
18
|
+
servers = {}
|
|
19
|
+
result["mcpServers"] = servers
|
|
20
|
+
metadata = result.setdefault("nexo", {})
|
|
21
|
+
if not isinstance(metadata, dict):
|
|
22
|
+
metadata = {}
|
|
23
|
+
result["nexo"] = metadata
|
|
24
|
+
managed = metadata.setdefault("managed_mcp", {})
|
|
25
|
+
if not isinstance(managed, dict):
|
|
26
|
+
managed = {}
|
|
27
|
+
metadata["managed_mcp"] = managed
|
|
28
|
+
managed_servers = managed.setdefault("servers", {})
|
|
29
|
+
if not isinstance(managed_servers, dict):
|
|
30
|
+
managed_servers = {}
|
|
31
|
+
managed["servers"] = managed_servers
|
|
32
|
+
|
|
33
|
+
for name, entry in entries.items():
|
|
34
|
+
current = servers.get(name)
|
|
35
|
+
if current is not None and not _is_nexo_owned(current):
|
|
36
|
+
continue
|
|
37
|
+
servers[name] = deepcopy(entry)
|
|
38
|
+
managed_servers[name] = deepcopy(entry.get("nexo") or {})
|
|
39
|
+
managed["schema"] = "nexo.managed_mcp.client.v1"
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def merge_toml_mcp_servers(payload: dict[str, Any], entries: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
44
|
+
result = deepcopy(payload) if isinstance(payload, dict) else {}
|
|
45
|
+
servers = result.setdefault("mcp_servers", {})
|
|
46
|
+
if not isinstance(servers, dict):
|
|
47
|
+
servers = {}
|
|
48
|
+
result["mcp_servers"] = servers
|
|
49
|
+
nexo_table = result.setdefault("nexo", {})
|
|
50
|
+
if not isinstance(nexo_table, dict):
|
|
51
|
+
nexo_table = {}
|
|
52
|
+
result["nexo"] = nexo_table
|
|
53
|
+
managed = nexo_table.setdefault("managed_mcp", {})
|
|
54
|
+
if not isinstance(managed, dict):
|
|
55
|
+
managed = {}
|
|
56
|
+
nexo_table["managed_mcp"] = managed
|
|
57
|
+
managed_servers = managed.setdefault("servers", {})
|
|
58
|
+
if not isinstance(managed_servers, dict):
|
|
59
|
+
managed_servers = {}
|
|
60
|
+
managed["servers"] = managed_servers
|
|
61
|
+
|
|
62
|
+
for name, entry in entries.items():
|
|
63
|
+
current = servers.get(name)
|
|
64
|
+
current_meta = managed_servers.get(name)
|
|
65
|
+
if current is not None and not (
|
|
66
|
+
_is_nexo_owned(current) or (isinstance(current_meta, dict) and current_meta.get("owner") == "nexo")
|
|
67
|
+
):
|
|
68
|
+
continue
|
|
69
|
+
servers[name] = {
|
|
70
|
+
"command": entry.get("command", ""),
|
|
71
|
+
"args": list(entry.get("args", []) or []),
|
|
72
|
+
"env": dict(entry.get("env", {}) or {}),
|
|
73
|
+
}
|
|
74
|
+
managed_servers[name] = deepcopy(entry.get("nexo") or {})
|
|
75
|
+
managed["schema"] = "nexo.managed_mcp.client.v1"
|
|
76
|
+
return result
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "nexo.managed_mcp.lock.v1",
|
|
3
|
+
"catalog_version": "2026.06.06",
|
|
4
|
+
"generated_at": "2026-06-06T00:00:00Z",
|
|
5
|
+
"providers": {
|
|
6
|
+
"chrome-devtools-mcp": {
|
|
7
|
+
"source_type": "npm",
|
|
8
|
+
"package": "chrome-devtools-mcp",
|
|
9
|
+
"version": "1.1.1",
|
|
10
|
+
"integrity": "sha512-Fs/ASXAkQqvYCbJjHIx/pnShjyIoZoPxdg4J3wjaA9FLkRb2ngGnisu2AGcBIXdw5qrPkOuV/cOlGOonpsE1qw==",
|
|
11
|
+
"tarball": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-1.1.1.tgz",
|
|
12
|
+
"bin": "chrome-devtools-mcp",
|
|
13
|
+
"engines": {"node": "^20.19.0 || ^22.12.0 || >=23"}
|
|
14
|
+
},
|
|
15
|
+
"mac-use-mcp": {
|
|
16
|
+
"source_type": "npm",
|
|
17
|
+
"package": "mac-use-mcp",
|
|
18
|
+
"version": "1.1.1",
|
|
19
|
+
"integrity": "sha512-UcVkzvHuw+f21nEZwb3MwdqWGxLK/nYlhN55SRD6FZ2yn2t3ji0zKLD2XvQLp0ugeYyS92TqVgnw6E4P5VB+bg==",
|
|
20
|
+
"tarball": "https://registry.npmjs.org/mac-use-mcp/-/mac-use-mcp-1.1.1.tgz",
|
|
21
|
+
"bin": "mac-use-mcp",
|
|
22
|
+
"engines": {"node": ">=22"}
|
|
23
|
+
},
|
|
24
|
+
"native-devtools-mcp": {
|
|
25
|
+
"source_type": "npm",
|
|
26
|
+
"package": "native-devtools-mcp",
|
|
27
|
+
"version": "0.10.1",
|
|
28
|
+
"integrity": "sha512-TIR8QCKzYCaHY+N1IWB7OM6pZH49HJxRj1dZjT4RNkviA1QpgINm8H95ohMCbf4ZC5jdFssMlb9KmWNZnCeCSw==",
|
|
29
|
+
"tarball": "https://registry.npmjs.org/native-devtools-mcp/-/native-devtools-mcp-0.10.1.tgz",
|
|
30
|
+
"bin": "native-devtools-mcp",
|
|
31
|
+
"engines": {"node": ">=18"}
|
|
32
|
+
},
|
|
33
|
+
"open-computer-use": {
|
|
34
|
+
"source_type": "npm",
|
|
35
|
+
"package": "open-computer-use",
|
|
36
|
+
"version": "0.1.52",
|
|
37
|
+
"integrity": "sha512-KlOHmFvXHe2IEMGE/O+zMN5ASo+FQ42copj4j1xEOnyeLq4oxUxhtHqEdPUACCUcMZaHzKXfZboL1dk5a2GjLA==",
|
|
38
|
+
"tarball": "https://registry.npmjs.org/open-computer-use/-/open-computer-use-0.1.52.tgz",
|
|
39
|
+
"bin": "open-computer-use-mcp",
|
|
40
|
+
"engines": {"node": "^20.19.0 || ^22.12.0 || >=23"}
|
|
41
|
+
},
|
|
42
|
+
"desktop-commander": {
|
|
43
|
+
"source_type": "npm",
|
|
44
|
+
"package": "@wonderwhy-er/desktop-commander",
|
|
45
|
+
"version": "0.2.42",
|
|
46
|
+
"integrity": "sha512-ZgdBDihpaLfrzQQQGQCPmElYMx91oUXeVEWbxbygeUfq2aOZvHrcVMeuTGy9oMDp9vxjq6d/+ZGE0mQLJnAWkw==",
|
|
47
|
+
"tarball": "https://registry.npmjs.org/@wonderwhy-er/desktop-commander/-/desktop-commander-0.2.42.tgz",
|
|
48
|
+
"bin": "desktop-commander",
|
|
49
|
+
"engines": {"node": ">=18.0.0"}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|