agent-nuvira 3.4.0 → 3.5.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.
Files changed (169) hide show
  1. package/dist/agent-sdk/src/register.d.ts +23 -7
  2. package/dist/agent-sdk/src/register.d.ts.map +1 -1
  3. package/dist/agent-sdk/src/register.js +159 -5
  4. package/dist/agent-sdk/src/register.js.map +1 -1
  5. package/dist/agent-sdk/src/scaffold.d.ts +1 -1
  6. package/dist/agent-sdk/src/scaffold.js +1 -1
  7. package/dist/cli/agent.d.ts.map +1 -1
  8. package/dist/cli/agent.js +16 -3
  9. package/dist/cli/agent.js.map +1 -1
  10. package/dist/cli/chat.d.ts +85 -0
  11. package/dist/cli/chat.d.ts.map +1 -1
  12. package/dist/cli/chat.js +462 -15
  13. package/dist/cli/chat.js.map +1 -1
  14. package/dist/cli/config.d.ts +10 -0
  15. package/dist/cli/config.d.ts.map +1 -1
  16. package/dist/cli/config.js +39 -0
  17. package/dist/cli/config.js.map +1 -1
  18. package/dist/cli/doctor.d.ts.map +1 -1
  19. package/dist/cli/doctor.js +18 -3
  20. package/dist/cli/doctor.js.map +1 -1
  21. package/dist/cli/mcp.d.ts +50 -0
  22. package/dist/cli/mcp.d.ts.map +1 -1
  23. package/dist/cli/mcp.js +496 -49
  24. package/dist/cli/mcp.js.map +1 -1
  25. package/dist/cli/memory.d.ts.map +1 -1
  26. package/dist/cli/memory.js +3 -2
  27. package/dist/cli/memory.js.map +1 -1
  28. package/dist/cli/plugins.d.ts.map +1 -1
  29. package/dist/cli/plugins.js +26 -9
  30. package/dist/cli/plugins.js.map +1 -1
  31. package/dist/cli/sdk.d.ts.map +1 -1
  32. package/dist/cli/sdk.js +10 -3
  33. package/dist/cli/sdk.js.map +1 -1
  34. package/dist/cli/tool-install-prompt.d.ts.map +1 -1
  35. package/dist/cli/tool-install-prompt.js +23 -1
  36. package/dist/cli/tool-install-prompt.js.map +1 -1
  37. package/dist/cli/tools.d.ts +16 -0
  38. package/dist/cli/tools.d.ts.map +1 -1
  39. package/dist/cli/tools.js +101 -1
  40. package/dist/cli/tools.js.map +1 -1
  41. package/dist/config/manager.d.ts +6 -0
  42. package/dist/config/manager.d.ts.map +1 -1
  43. package/dist/config/manager.js +66 -0
  44. package/dist/config/manager.js.map +1 -1
  45. package/dist/config/types.d.ts +18 -0
  46. package/dist/config/types.d.ts.map +1 -1
  47. package/dist/index.d.ts +2 -2
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/learning/exhaustion-report.d.ts +12 -0
  52. package/dist/learning/exhaustion-report.d.ts.map +1 -1
  53. package/dist/learning/exhaustion-report.js +3 -1
  54. package/dist/learning/exhaustion-report.js.map +1 -1
  55. package/dist/learning/model-capability.d.ts +33 -0
  56. package/dist/learning/model-capability.d.ts.map +1 -1
  57. package/dist/learning/model-capability.js +51 -0
  58. package/dist/learning/model-capability.js.map +1 -1
  59. package/dist/learning/model-first-router.d.ts +7 -22
  60. package/dist/learning/model-first-router.d.ts.map +1 -1
  61. package/dist/learning/model-first-router.js +7 -37
  62. package/dist/learning/model-first-router.js.map +1 -1
  63. package/dist/learning/model-selection.d.ts +27 -0
  64. package/dist/learning/model-selection.d.ts.map +1 -1
  65. package/dist/learning/model-selection.js +46 -0
  66. package/dist/learning/model-selection.js.map +1 -1
  67. package/dist/learning/reasoning-trace.d.ts +1 -1
  68. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  69. package/dist/learning/reasoning-trace.js.map +1 -1
  70. package/dist/learning/resilient-call.d.ts +15 -1
  71. package/dist/learning/resilient-call.d.ts.map +1 -1
  72. package/dist/learning/resilient-call.js +38 -22
  73. package/dist/learning/resilient-call.js.map +1 -1
  74. package/dist/learning/working-state.d.ts +7 -0
  75. package/dist/learning/working-state.d.ts.map +1 -1
  76. package/dist/learning/working-state.js +23 -1
  77. package/dist/learning/working-state.js.map +1 -1
  78. package/dist/mcp/catalog.d.ts +206 -26
  79. package/dist/mcp/catalog.d.ts.map +1 -1
  80. package/dist/mcp/catalog.js +664 -52
  81. package/dist/mcp/catalog.js.map +1 -1
  82. package/dist/mcp/client.d.ts +6 -0
  83. package/dist/mcp/client.d.ts.map +1 -1
  84. package/dist/mcp/client.js +35 -5
  85. package/dist/mcp/client.js.map +1 -1
  86. package/dist/mcp/custom-server.d.ts +120 -0
  87. package/dist/mcp/custom-server.d.ts.map +1 -0
  88. package/dist/mcp/custom-server.js +330 -0
  89. package/dist/mcp/custom-server.js.map +1 -0
  90. package/dist/mcp/manager.d.ts +9 -1
  91. package/dist/mcp/manager.d.ts.map +1 -1
  92. package/dist/mcp/manager.js +15 -2
  93. package/dist/mcp/manager.js.map +1 -1
  94. package/dist/mcp/mcp-oauth.d.ts +28 -0
  95. package/dist/mcp/mcp-oauth.d.ts.map +1 -1
  96. package/dist/mcp/mcp-oauth.js +63 -8
  97. package/dist/mcp/mcp-oauth.js.map +1 -1
  98. package/dist/mcp/oauth-authorize.d.ts +51 -0
  99. package/dist/mcp/oauth-authorize.d.ts.map +1 -0
  100. package/dist/mcp/oauth-authorize.js +133 -0
  101. package/dist/mcp/oauth-authorize.js.map +1 -0
  102. package/dist/mcp/oauth-connect.d.ts +53 -0
  103. package/dist/mcp/oauth-connect.d.ts.map +1 -0
  104. package/dist/mcp/oauth-connect.js +103 -0
  105. package/dist/mcp/oauth-connect.js.map +1 -0
  106. package/dist/mcp/pin-verifier.d.ts +44 -0
  107. package/dist/mcp/pin-verifier.d.ts.map +1 -0
  108. package/dist/mcp/pin-verifier.js +102 -0
  109. package/dist/mcp/pin-verifier.js.map +1 -0
  110. package/dist/mcp/prereqs.d.ts +145 -0
  111. package/dist/mcp/prereqs.d.ts.map +1 -0
  112. package/dist/mcp/prereqs.js +245 -0
  113. package/dist/mcp/prereqs.js.map +1 -0
  114. package/dist/mcp/runtime.d.ts +59 -0
  115. package/dist/mcp/runtime.d.ts.map +1 -0
  116. package/dist/mcp/runtime.js +77 -0
  117. package/dist/mcp/runtime.js.map +1 -0
  118. package/dist/mcp/server.d.ts +21 -0
  119. package/dist/mcp/server.d.ts.map +1 -1
  120. package/dist/mcp/server.js +29 -1
  121. package/dist/mcp/server.js.map +1 -1
  122. package/dist/mcp/settings.d.ts +164 -0
  123. package/dist/mcp/settings.d.ts.map +1 -0
  124. package/dist/mcp/settings.js +358 -0
  125. package/dist/mcp/settings.js.map +1 -0
  126. package/dist/mcp/types.d.ts +62 -0
  127. package/dist/mcp/types.d.ts.map +1 -1
  128. package/dist/mcp/types.js.map +1 -1
  129. package/dist/memory/vector-store.d.ts +12 -0
  130. package/dist/memory/vector-store.d.ts.map +1 -1
  131. package/dist/memory/vector-store.js +23 -0
  132. package/dist/memory/vector-store.js.map +1 -1
  133. package/dist/plugins/agent-plugin.d.ts +35 -0
  134. package/dist/plugins/agent-plugin.d.ts.map +1 -1
  135. package/dist/plugins/agent-plugin.js +142 -24
  136. package/dist/plugins/agent-plugin.js.map +1 -1
  137. package/dist/sandbox/manager.d.ts +12 -0
  138. package/dist/sandbox/manager.d.ts.map +1 -1
  139. package/dist/sandbox/manager.js +43 -2
  140. package/dist/sandbox/manager.js.map +1 -1
  141. package/dist/tools/modality/browser.d.ts +39 -3
  142. package/dist/tools/modality/browser.d.ts.map +1 -1
  143. package/dist/tools/modality/browser.js +113 -16
  144. package/dist/tools/modality/browser.js.map +1 -1
  145. package/dist/tools/optional-deps.d.ts +48 -0
  146. package/dist/tools/optional-deps.d.ts.map +1 -0
  147. package/dist/tools/optional-deps.js +42 -0
  148. package/dist/tools/optional-deps.js.map +1 -0
  149. package/dist/tools/registry.d.ts.map +1 -1
  150. package/dist/tools/registry.js +68 -11
  151. package/dist/tools/registry.js.map +1 -1
  152. package/dist/tools/remediation-ladder.d.ts.map +1 -1
  153. package/dist/tools/remediation-ladder.js +9 -1
  154. package/dist/tools/remediation-ladder.js.map +1 -1
  155. package/dist/tools/tool-loop.d.ts.map +1 -1
  156. package/dist/tools/tool-loop.js +57 -5
  157. package/dist/tools/tool-loop.js.map +1 -1
  158. package/dist/web-dashboard/chat-retry.d.ts +11 -1
  159. package/dist/web-dashboard/chat-retry.d.ts.map +1 -1
  160. package/dist/web-dashboard/chat-retry.js +18 -4
  161. package/dist/web-dashboard/chat-retry.js.map +1 -1
  162. package/dist/web-dashboard/server.d.ts.map +1 -1
  163. package/dist/web-dashboard/server.js +349 -2
  164. package/dist/web-dashboard/server.js.map +1 -1
  165. package/package.json +3 -2
  166. package/src/web-dashboard/public/assets/{index-DeD3L0kG.js → index-yky0dw5e.js} +115 -89
  167. package/src/web-dashboard/public/assets/index-yky0dw5e.js.map +1 -0
  168. package/src/web-dashboard/public/index.html +1 -1
  169. package/src/web-dashboard/public/assets/index-DeD3L0kG.js.map +0 -1
@@ -1,72 +1,444 @@
1
1
  /**
2
2
  * I7 P2 — Curated MCP catalog (`src/mcp/catalog.ts`).
3
3
  *
4
- * A vetted list
5
- * of MCP servers with EXACT-VERSION pins (no floating tags), a "vetted by"
6
- * stamp, and `prompt-secret` env placeholders that are resolved at install
7
- * time (from the environment or an explicit value) and written into the
8
- * standard MCP config file (~/.nuvira/mcp/<name>.json) — never into
9
- * buffconfig.json, and never echoed to the terminal.
4
+ * A catalog entry is a CAPABILITY SLOT (`github`, `fetch`, `docs`), not one
5
+ * hardcoded package. A slot offers ranked VARIANTS — ways to run that
6
+ * capability on this machine — and every variant declares what it requires
7
+ * (`requires`) and what the user gets (`offers`).
8
+ *
9
+ * WHY THE SHAPE CHANGED (2026-10-12). The first version was `name → one exact
10
+ * npm pin`. Four of its six pins did not resolve on npm at all
11
+ * (`docs/mcp-assessment.md` §2), so `nuvira mcp install context7` wrote a config
12
+ * whose stdio command could never spawn — a two-command, no-code install that
13
+ * could not work as advertised. A single pin per capability can only ever be as
14
+ * reachable as that one pin.
15
+ *
16
+ * The slot/variant shape fixes the class of defect rather than the instances:
17
+ * • `github` — the hosted remote endpoint by default (OAuth/PAT, no install),
18
+ * with the official local binary kept behind an explicit opt-in, and the
19
+ * npm package REMOVED (npm marks it "no longer supported").
20
+ * • `context7` — the current 4.3.0 pin, which needs Node ≥ 20.18.1 while the
21
+ * product runs on ≥ 18.18, so the floor is met by a per-server RUNTIME
22
+ * OVERRIDE instead of raising the global floor; a Node-18-safe release is
23
+ * offered as a second variant rather than leaving those machines stranded.
24
+ * • `fetch` — the package the old catalog pinned does not exist on npm.
25
+ * Three real ways to run it (`npx`, `uvx`, docker), each with its own
26
+ * prerequisite, so a machine that has any one of them has the capability.
27
+ *
28
+ * Install rules this module enforces (see `installCatalogServer`):
29
+ * 1. Prerequisites are probed BEFORE anything is written; a variant that
30
+ * cannot run is refused loudly, naming what is missing and how to get it.
31
+ * 2. An exact npm pin is only written after a resolvability check the CALLER
32
+ * performed (async, network); when it could not run, the result says so
33
+ * (`unverifiedPins`) instead of implying a verification that never happened.
34
+ * 3. Secrets are resolved from the environment or an explicit value, never
35
+ * echoed, never written to `buffconfig.json`, and the config file is
36
+ * written `0600` when it carries one.
10
37
  *
11
38
  * CLI surface (src/cli/mcp.ts):
12
- * nuvira mcp catalog [--search <q>] — list vetted servers (+ installed badge)
13
- * nuvira mcp install <name> — resolve pins → write config → ready
14
- * nuvira mcp uninstall <name> — remove the config file
39
+ * nuvira mcp catalog [--search <q>] — slots, variants, what they need
40
+ * nuvira mcp install <name> [--variant id] [--runtime <cmd>]
41
+ * nuvira mcp uninstall <name>
15
42
  */
16
- import { existsSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
43
+ import { chmodSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
17
44
  import { join } from 'node:path';
18
45
  import { resolveMcpConfigDir } from '../config/paths.js';
19
46
  import { logger } from '../utils/logger.js';
20
- /** Vetted catalog (exact pins; 2-week-old minimum for freshness on review). */
47
+ import { commandExists, describePrereqProblems, probePrereqs, } from './prereqs.js';
48
+ /** Vetted catalog. Every npm spec is exact-version pinned so a stale pin is visible. */
21
49
  export const MCP_CATALOG = [
22
50
  {
23
51
  name: 'filesystem',
24
52
  description: 'Filesystem access (read/write/search within allowed roots)',
25
- transport: 'stdio',
26
- command: 'npx',
27
- args: ['-y', '@modelcontextprotocol/server-filesystem@0.6.2'],
28
- vettedBy: 'nous-catalog',
53
+ offers: [
54
+ 'Read, write and edit files under the directories you allow',
55
+ 'Directory listing, globbing and content search, locally',
56
+ ],
57
+ variants: [
58
+ {
59
+ id: 'npx',
60
+ label: 'npx (Node ≥18.18)',
61
+ transport: 'stdio',
62
+ command: 'npx',
63
+ args: ['-y', '@modelcontextprotocol/server-filesystem@1.0.0'],
64
+ requires: [
65
+ { kind: 'command', name: 'npx', hint: 'Install Node.js ≥18.18 — npx ships with it' },
66
+ { kind: 'node', name: 'node', minVersion: '18.18.0', hint: 'Install Node.js ≥18.18' },
67
+ ],
68
+ rank: 1,
69
+ trust: 'official',
70
+ vettedBy: 'nous-catalog',
71
+ note: 'Pass allowed roots as extra args (see mcp.servers.filesystem.args) — without one the server exposes nothing.',
72
+ },
73
+ ],
29
74
  },
30
75
  {
31
76
  name: 'github',
32
77
  description: 'GitHub API via MCP (repos, issues, PRs, search)',
33
- transport: 'stdio',
34
- command: 'npx',
35
- args: ['-y', '@modelcontextprotocol/server-github@0.6.2'],
36
- env: { GITHUB_PERSONAL_ACCESS_TOKEN: 'prompt-secret' },
37
- vettedBy: 'nous-catalog',
78
+ offers: [
79
+ 'Read and search repositories, issues, pull requests and code',
80
+ 'Comment, open issues/PRs and update branches with your token’s permissions',
81
+ ],
82
+ supersedes: '@modelcontextprotocol/server-github@0.6.2 / @2025.4.8 — npm marks the package "no longer supported"; install can never succeed from it reliably',
83
+ variants: [
84
+ {
85
+ id: 'remote',
86
+ label: 'Hosted remote endpoint (no install)',
87
+ transport: 'sse',
88
+ url: 'https://api.githubcopilot.com/mcp/',
89
+ headers: { Authorization: 'Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}' },
90
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: 'prompt-secret' },
91
+ requires: [
92
+ {
93
+ kind: 'env',
94
+ name: 'GITHUB_PERSONAL_ACCESS_TOKEN',
95
+ hint: 'Create a token at github.com/settings/tokens (repo + read:org for most work)',
96
+ },
97
+ ],
98
+ rank: 1,
99
+ trust: 'official',
100
+ vettedBy: 'nous-catalog',
101
+ note: 'GitHub’s own remote MCP endpoint — nothing is installed locally.',
102
+ },
103
+ {
104
+ id: 'local-binary',
105
+ label: 'Official local binary (github-mcp-server stdio)',
106
+ transport: 'stdio',
107
+ command: 'github-mcp-server',
108
+ args: ['stdio'],
109
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: 'prompt-secret' },
110
+ requires: [
111
+ {
112
+ kind: 'command',
113
+ name: 'github-mcp-server',
114
+ hint: 'Install GitHub’s Go binary: brew install github-mcp-server, or a release from github/github-mcp-server',
115
+ },
116
+ {
117
+ kind: 'env',
118
+ name: 'GITHUB_PERSONAL_ACCESS_TOKEN',
119
+ hint: 'Create a token at github.com/settings/tokens',
120
+ },
121
+ ],
122
+ rank: 2,
123
+ // Q1 decision: a local stdio server stays an explicit opt-in; the
124
+ // remote endpoint is the default.
125
+ optIn: true,
126
+ trust: 'official',
127
+ vettedBy: 'nous-catalog',
128
+ note: 'Runs entirely locally; requires the binary on PATH and the token in the config file.',
129
+ },
130
+ ],
38
131
  },
39
132
  {
40
133
  name: 'fetch',
41
134
  description: 'Fetch a URL and convert it to readable markdown',
42
- transport: 'stdio',
43
- command: 'npx',
44
- args: ['-y', '@modelcontextprotocol/server-fetch@0.1.7'],
45
- vettedBy: 'nous-catalog',
135
+ offers: [
136
+ 'Turn a web page into clean markdown the model can read',
137
+ 'Pull documentation, changelogs and articles into a turn',
138
+ ],
139
+ supersedes: '@modelcontextprotocol/server-fetch@0.1.7 — the package does not exist on npm (E404); the capability is offered through three runnable variants instead',
140
+ variants: [
141
+ {
142
+ id: 'npx',
143
+ label: 'npx (Node ≥18)',
144
+ transport: 'stdio',
145
+ command: 'npx',
146
+ args: ['-y', '@tokenizin/mcp-npx-fetch@1.0.0'],
147
+ requires: [
148
+ { kind: 'command', name: 'npx', hint: 'Install Node.js ≥18 — npx ships with it' },
149
+ { kind: 'node', name: 'node', minVersion: '18.0.0', hint: 'Install Node.js ≥18' },
150
+ ],
151
+ rank: 1,
152
+ trust: 'community',
153
+ vettedBy: 'community-listed',
154
+ note: 'Third-party npm package (last published 2024-12) — pinned exactly; prefer the uvx variant if you already have uv.',
155
+ },
156
+ {
157
+ id: 'uvx',
158
+ label: 'uvx (Python ≥3.10, uv)',
159
+ transport: 'stdio',
160
+ command: 'uvx',
161
+ args: ['mcp-server-fetch==2026.10.10'],
162
+ requires: [
163
+ {
164
+ kind: 'command',
165
+ name: 'uvx',
166
+ hint: 'Install uv (which provides uvx): brew install uv, or see docs.astral.sh/uv',
167
+ },
168
+ ],
169
+ rank: 2,
170
+ trust: 'official',
171
+ vettedBy: 'nous-catalog',
172
+ note: 'The maintained upstream fetch server; Python ≥3.10 is provided by uv, not your system Python.',
173
+ },
174
+ {
175
+ id: 'docker',
176
+ label: 'docker (mcp/fetch image)',
177
+ transport: 'stdio',
178
+ command: 'docker',
179
+ args: ['run', '-i', '--rm', 'mcp/fetch'],
180
+ requires: [
181
+ { kind: 'command', name: 'docker', hint: 'Install Docker Desktop, or the docker engine' },
182
+ {
183
+ kind: 'service',
184
+ name: 'docker',
185
+ check: { args: ['info'] },
186
+ hint: 'START the Docker daemon (open Docker Desktop, or `colima start`) — `docker run` needs it, and the CLI being on PATH is not enough',
187
+ },
188
+ ],
189
+ rank: 3,
190
+ trust: 'official',
191
+ vettedBy: 'nous-catalog',
192
+ note: 'The publisher tags this image `latest` only, so the image itself cannot be pinned to a version — the one variant where that is true (it is a caveat about version drift, NOT about Docker being missing).',
193
+ },
194
+ ],
46
195
  },
47
196
  {
48
197
  name: 'memory',
49
198
  description: 'Knowledge-graph based persistent memory (entities, relations, observations)',
50
- transport: 'stdio',
51
- command: 'npx',
52
- args: ['-y', '@modelcontextprotocol/server-memory@0.3.6'],
53
- vettedBy: 'nous-catalog',
199
+ offers: [
200
+ 'A knowledge graph the model can read and write across sessions',
201
+ 'Entities, relations and observations as first-class tools',
202
+ ],
203
+ variants: [
204
+ {
205
+ id: 'npx',
206
+ label: 'npx (Node ≥18.18)',
207
+ transport: 'stdio',
208
+ command: 'npx',
209
+ args: ['-y', '@modelcontextprotocol/server-memory@1.0.0'],
210
+ requires: [
211
+ { kind: 'command', name: 'npx', hint: 'Install Node.js ≥18.18 — npx ships with it' },
212
+ { kind: 'node', name: 'node', minVersion: '18.18.0', hint: 'Install Node.js ≥18.18' },
213
+ ],
214
+ rank: 1,
215
+ trust: 'official',
216
+ vettedBy: 'nous-catalog',
217
+ note: 'A second memory system beside Nuvira’s own memory tools — enable it only if you want the graph form.',
218
+ },
219
+ ],
54
220
  },
55
221
  {
56
222
  name: 'sequential-thinking',
57
223
  description: 'Structured, explicit reasoning over a problem space',
58
- transport: 'stdio',
59
- command: 'npx',
60
- args: ['-y', '@modelcontextprotocol/server-sequential-thinking@0.4.2'],
61
- vettedBy: 'nous-catalog',
224
+ offers: [
225
+ 'A step-by-step reasoning scratchpad the model can revise',
226
+ 'Branch, backtrack and revise a line of thought explicitly',
227
+ ],
228
+ variants: [
229
+ {
230
+ id: 'npx',
231
+ label: 'npx (Node ≥18.18)',
232
+ transport: 'stdio',
233
+ command: 'npx',
234
+ args: ['-y', '@modelcontextprotocol/server-sequential-thinking@1.0.0'],
235
+ requires: [
236
+ { kind: 'command', name: 'npx', hint: 'Install Node.js ≥18.18 — npx ships with it' },
237
+ { kind: 'node', name: 'node', minVersion: '18.18.0', hint: 'Install Node.js ≥18.18' },
238
+ ],
239
+ rank: 1,
240
+ trust: 'official',
241
+ vettedBy: 'nous-catalog',
242
+ },
243
+ ],
62
244
  },
63
245
  {
64
246
  name: 'context7',
65
247
  description: 'Up-to-date library/framework documentation retrieval',
66
- transport: 'stdio',
67
- command: 'npx',
68
- args: ['-y', '@upstash/context7-mcp@1.5.2'],
69
- vettedBy: 'nous-catalog',
248
+ offers: [
249
+ 'Current (not training-cutoff) docs and code examples for a library you name',
250
+ 'Version-specific usage snippets while implementing',
251
+ ],
252
+ supersedes: '@upstash/context7-mcp@1.5.2 — that version never existed on npm (line jumps 1.0.34 → 2.0.0)',
253
+ variants: [
254
+ {
255
+ id: 'npx',
256
+ label: 'npx 4.3.0 (needs Node ≥20.18.1)',
257
+ transport: 'stdio',
258
+ command: 'npx',
259
+ args: ['-y', '@upstash/context7-mcp@4.3.0'],
260
+ envOptional: { CONTEXT7_API_KEY: 'prompt-secret' },
261
+ requires: [
262
+ { kind: 'command', name: 'npx', hint: 'Install Node.js ≥20.18.1 — npx ships with it' },
263
+ {
264
+ kind: 'node',
265
+ name: 'node',
266
+ minVersion: '20.18.1',
267
+ hint: 'context7 4.x declares engines.node ≥20.18.1 while Nuvira runs on ≥18.18 — install Node 20+ and pass ' +
268
+ '`--runtime /path/to/node20` (or set mcp.servers.context7.runtime.command), or install the npx-node18 variant',
269
+ },
270
+ ],
271
+ rank: 1,
272
+ trust: 'official',
273
+ vettedBy: 'nous-catalog',
274
+ note: 'CONTEXT7_API_KEY is optional (higher rate limits with a key), so install never blocks on it.',
275
+ },
276
+ {
277
+ id: 'npx-node18',
278
+ label: 'npx 2.0.0 (no engines floor — Node 18 ok)',
279
+ transport: 'stdio',
280
+ command: 'npx',
281
+ args: ['-y', '@upstash/context7-mcp@2.0.0'],
282
+ envOptional: { CONTEXT7_API_KEY: 'prompt-secret' },
283
+ requires: [
284
+ { kind: 'command', name: 'npx', hint: 'Install Node.js ≥18.18 — npx ships with it' },
285
+ { kind: 'node', name: 'node', minVersion: '18.18.0', hint: 'Install Node.js ≥18.18' },
286
+ ],
287
+ rank: 2,
288
+ trust: 'official',
289
+ vettedBy: 'nous-catalog',
290
+ note: 'Kept for reach on Node 18: 2.0.0 declares no engines floor, but it is an older release — its docs coverage may lag 4.x.',
291
+ },
292
+ ],
293
+ },
294
+ // ─── Google Workspace (official remote MCP servers, OAuth 2.0) ────────────
295
+ //
296
+ // These are the first entries whose credential is a TOKEN rather than a static
297
+ // API key, which is why the `oauth` block exists on a variant: install writes
298
+ // HOW to get a token (endpoints, client, scopes) and the 0600 config file holds
299
+ // the client secret; the token itself is obtained once with
300
+ // `nuvira mcp authorize <name>` and refreshed silently from then on.
301
+ //
302
+ // Every claim below is from Google's own configuration guide
303
+ // (developers.google.com/workspace/guides/configure-mcp-servers): one remote
304
+ // server per product, OAuth 2.0, and — stated plainly rather than discovered
305
+ // later — Developer Preview membership plus a Cloud project with the product
306
+ // API and its MCP API enabled.
307
+ {
308
+ name: 'gmail',
309
+ description: 'Official Google Workspace Gmail MCP server (remote, OAuth 2.0)',
310
+ offers: [
311
+ 'Search and read mail, and create drafts, with your own Gmail permissions',
312
+ 'Inbox context inside a turn without handing the mailbox to a third party',
313
+ ],
314
+ variants: [
315
+ {
316
+ id: 'remote',
317
+ label: 'Google Workspace MCP — Gmail (remote, OAuth 2.0)',
318
+ transport: 'sse',
319
+ url: 'https://gmailmcp.googleapis.com/mcp/v1',
320
+ oauth: {
321
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
322
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
323
+ clientId: '${GOOGLE_MCP_CLIENT_ID}',
324
+ clientSecret: '${GOOGLE_MCP_CLIENT_SECRET}',
325
+ scopes: [
326
+ 'https://www.googleapis.com/auth/gmail.readonly',
327
+ 'https://www.googleapis.com/auth/gmail.compose',
328
+ ],
329
+ usePKCE: true,
330
+ grant: 'authorization_code',
331
+ },
332
+ env: { GOOGLE_MCP_CLIENT_ID: 'prompt-secret', GOOGLE_MCP_CLIENT_SECRET: 'prompt-secret' },
333
+ requires: [
334
+ {
335
+ kind: 'env',
336
+ name: 'GOOGLE_MCP_CLIENT_ID',
337
+ hint: 'OAuth client ID from your Google Cloud project (see developers.google.com/workspace/guides/configure-mcp-servers)',
338
+ },
339
+ {
340
+ kind: 'env',
341
+ name: 'GOOGLE_MCP_CLIENT_SECRET',
342
+ hint: 'The matching OAuth client secret — a Desktop-app client, since the flow uses a loopback redirect and PKCE',
343
+ },
344
+ ],
345
+ rank: 1,
346
+ trust: 'official',
347
+ vettedBy: 'google-workspace-docs',
348
+ note: 'Google’s own remote Gmail MCP server — nothing is installed locally. Needs Google Workspace Developer Preview membership and the Gmail API + Gmail MCP API enabled in the Cloud project. Install, then run `nuvira mcp authorize gmail` once.',
349
+ },
350
+ ],
351
+ },
352
+ {
353
+ name: 'gdrive',
354
+ description: 'Official Google Workspace Drive MCP server (remote, OAuth 2.0)',
355
+ offers: [
356
+ 'Search, list and read files you already have access to',
357
+ 'Upload and organise files with `drive.file` (only what this app creates or opens)',
358
+ ],
359
+ variants: [
360
+ {
361
+ id: 'remote',
362
+ label: 'Google Workspace MCP — Drive (remote, OAuth 2.0)',
363
+ transport: 'sse',
364
+ url: 'https://drivemcp.googleapis.com/mcp/v1',
365
+ oauth: {
366
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
367
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
368
+ clientId: '${GOOGLE_MCP_CLIENT_ID}',
369
+ clientSecret: '${GOOGLE_MCP_CLIENT_SECRET}',
370
+ scopes: [
371
+ 'https://www.googleapis.com/auth/drive.readonly',
372
+ 'https://www.googleapis.com/auth/drive.file',
373
+ ],
374
+ usePKCE: true,
375
+ grant: 'authorization_code',
376
+ },
377
+ env: { GOOGLE_MCP_CLIENT_ID: 'prompt-secret', GOOGLE_MCP_CLIENT_SECRET: 'prompt-secret' },
378
+ requires: [
379
+ {
380
+ kind: 'env',
381
+ name: 'GOOGLE_MCP_CLIENT_ID',
382
+ hint: 'OAuth client ID from your Google Cloud project (the SAME client can serve all three Workspace servers)',
383
+ },
384
+ {
385
+ kind: 'env',
386
+ name: 'GOOGLE_MCP_CLIENT_SECRET',
387
+ hint: 'The matching OAuth client secret — a Desktop-app client, since the flow uses a loopback redirect and PKCE',
388
+ },
389
+ ],
390
+ rank: 1,
391
+ trust: 'official',
392
+ vettedBy: 'google-workspace-docs',
393
+ note: 'Google’s own remote Drive MCP server. Needs the Drive API + Drive MCP API enabled, and Developer Preview membership. `drive.file` deliberately does NOT grant blanket access to your Drive — only to files this app creates or that you open with it.',
394
+ },
395
+ ],
396
+ },
397
+ {
398
+ name: 'gcalendar',
399
+ description: 'Official Google Workspace Calendar MCP server (remote, OAuth 2.0)',
400
+ offers: [
401
+ 'List calendars and read events, and check free/busy before proposing a time',
402
+ 'Give the agent a real schedule view instead of guessing availability',
403
+ ],
404
+ variants: [
405
+ {
406
+ id: 'remote',
407
+ label: 'Google Workspace MCP — Calendar (remote, OAuth 2.0)',
408
+ transport: 'sse',
409
+ url: 'https://calendarmcp.googleapis.com/mcp/v1',
410
+ oauth: {
411
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
412
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
413
+ clientId: '${GOOGLE_MCP_CLIENT_ID}',
414
+ clientSecret: '${GOOGLE_MCP_CLIENT_SECRET}',
415
+ scopes: [
416
+ 'https://www.googleapis.com/auth/calendar.calendarlist.readonly',
417
+ 'https://www.googleapis.com/auth/calendar.events.freebusy',
418
+ 'https://www.googleapis.com/auth/calendar.events.readonly',
419
+ ],
420
+ usePKCE: true,
421
+ grant: 'authorization_code',
422
+ },
423
+ env: { GOOGLE_MCP_CLIENT_ID: 'prompt-secret', GOOGLE_MCP_CLIENT_SECRET: 'prompt-secret' },
424
+ requires: [
425
+ {
426
+ kind: 'env',
427
+ name: 'GOOGLE_MCP_CLIENT_ID',
428
+ hint: 'OAuth client ID from your Google Cloud project (the SAME client can serve all three Workspace servers)',
429
+ },
430
+ {
431
+ kind: 'env',
432
+ name: 'GOOGLE_MCP_CLIENT_SECRET',
433
+ hint: 'The matching OAuth client secret — a Desktop-app client, since the flow uses a loopback redirect and PKCE',
434
+ },
435
+ ],
436
+ rank: 1,
437
+ trust: 'official',
438
+ vettedBy: 'google-workspace-docs',
439
+ note: 'Google’s own remote Calendar MCP server. Needs the Google Calendar API + Calendar MCP API enabled, and Developer Preview membership. The scopes here are READ + free/busy only, so the agent can propose a time but cannot silently book one.',
440
+ },
441
+ ],
70
442
  },
71
443
  ];
72
444
  // ─── Lookup ─────────────────────────────────────────────────────────────────
@@ -79,6 +451,84 @@ export function searchCatalog(query) {
79
451
  const q = query.toLowerCase();
80
452
  return MCP_CATALOG.filter((e) => `${e.name} ${e.description}`.toLowerCase().includes(q));
81
453
  }
454
+ // ─── Variant selection ──────────────────────────────────────────────────────
455
+ /**
456
+ * The environment a variant is probed against: the process env, then anything
457
+ * the caller injected (tests describe a machine this way), then the secrets being
458
+ * supplied right now.
459
+ *
460
+ * WHY the secrets belong here: `nuvira mcp install github --env
461
+ * GITHUB_PERSONAL_ACCESS_TOKEN=…` must satisfy the variant's env prerequisite
462
+ * even when the shell variable is not exported — otherwise the documented
463
+ * one-command install refuses on the flag designed to make it work. Both the
464
+ * probe and the config write use this one function, so a value can never be
465
+ * checked in one environment and written from another.
466
+ */
467
+ export function effectiveProbeEnv(injected, secrets) {
468
+ const env = { ...process.env, ...(injected ?? {}) };
469
+ for (const [key, value] of Object.entries(secrets ?? {}))
470
+ env[key] = value;
471
+ return env;
472
+ }
473
+ /** A variant's runtime override, if it declares one (or the entry's explicit one). */
474
+ function variantProbeOptions(variant, runtime, probe) {
475
+ const runtimeCommand = runtime?.command ?? variant.runtime?.command;
476
+ return { ...(probe ?? {}), ...(runtimeCommand ? { runtimeCommand } : {}) };
477
+ }
478
+ /**
479
+ * Every variant with its probed status, in preference order — what `nuvira mcp
480
+ * catalog` prints, and the basis for choosing one.
481
+ */
482
+ export function evaluateVariants(entry, options = {}) {
483
+ return [...entry.variants]
484
+ .sort((a, b) => a.rank - b.rank)
485
+ .map((variant) => ({
486
+ variant,
487
+ status: probePrereqs(variant.requires, variantProbeOptions(variant, options.runtime, options.probe)),
488
+ }));
489
+ }
490
+ /**
491
+ * Pick the variant to install: an explicitly named one, else the highest-ranked
492
+ * variant that is not `optIn` and whose prerequisites are satisfied.
493
+ *
494
+ * Returns `{ ok: false }` with the full candidate list when nothing can run, so
495
+ * the caller can name every way the capability could be reached AND what each
496
+ * one is missing. That is the difference between "install failed" and "here is
497
+ * the dependency you need".
498
+ */
499
+ export function chooseVariant(entry, options = {}) {
500
+ const choices = evaluateVariants(entry, options);
501
+ if (options.variantId) {
502
+ const named = choices.find((c) => c.variant.id === options.variantId);
503
+ if (!named) {
504
+ return {
505
+ ok: false,
506
+ reason: `'${options.variantId}' is not a variant of '${entry.name}'. Available: ${choices
507
+ .map((c) => c.variant.id)
508
+ .join(', ')}`,
509
+ choices,
510
+ };
511
+ }
512
+ return { ok: true, choice: named };
513
+ }
514
+ const auto = choices.filter((c) => !c.variant.optIn);
515
+ const runnable = auto.find((c) => c.status.ok);
516
+ if (runnable)
517
+ return { ok: true, choice: runnable };
518
+ const lines = (auto.length > 0 ? auto : choices).flatMap((c) => {
519
+ const problems = describePrereqProblems(c.status);
520
+ return [` • ${c.variant.id}: ${problems.length > 0 ? problems.join('; ') : '(available)'}`];
521
+ });
522
+ const optIn = choices.filter((c) => c.variant.optIn);
523
+ if (optIn.length > 0) {
524
+ lines.push(` • opt-in variants (name them with --variant): ${optIn.map((c) => c.variant.id).join(', ')}`);
525
+ }
526
+ return {
527
+ ok: false,
528
+ reason: `no variant of '${entry.name}' can run on this machine yet:\n${lines.join('\n')}`,
529
+ choices,
530
+ };
531
+ }
82
532
  // ─── Install path ───────────────────────────────────────────────────────────
83
533
  /** The per-server config file for an installed catalog server. */
84
534
  export function catalogConfigPath(name, configDir) {
@@ -99,40 +549,173 @@ export function resolveSecret(key, provided) {
99
549
  const explicit = provided?.[key];
100
550
  return explicit ?? null;
101
551
  }
552
+ /** Expand `${VAR}` references in a string from a resolved value map. */
553
+ export function expandRefs(text, values) {
554
+ return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (whole, name) => values[name] !== undefined ? values[name] : whole);
555
+ }
102
556
  /**
103
- * Install a catalog server: resolve secrets, write the standard MCP config
104
- * (never the buffconfig), return the config written. Skips when already
105
- * installed. Secrets are resolved from the environment or `providedSecrets`;
106
- * missing required secrets → ok:false with the key named (never logged).
557
+ * Install a catalog server: choose a runnable variant, resolve secrets, write
558
+ * the standard MCP config (never the buffconfig), return what was written.
559
+ *
560
+ * Refuses — loudly, before writing anything — when no variant can run on this
561
+ * machine (unless `force`), or when the caller's npm resolvability check
562
+ * reported that a pinned spec does not exist. Secrets come from the environment
563
+ * or `providedSecrets`; a missing REQUIRED secret names the key and never logs a
564
+ * value. The file is written `0600` whenever it carries a secret.
107
565
  */
108
- export function installCatalogServer(entry, providedSecrets, configDir) {
566
+ export function installCatalogServer(entry, providedSecrets, configDir, options = {}) {
109
567
  const target = catalogConfigPath(entry.name, configDir);
110
568
  if (existsSync(target)) {
111
569
  return { ok: false, reason: `already installed at ${target}` };
112
570
  }
113
- // Resolve env (secrets + static values).
571
+ // The probe sees the SAME environment the install will use (see
572
+ // `effectiveProbeEnv`), so what is verified and what is written cannot
573
+ // disagree.
574
+ const probeEnv = effectiveProbeEnv(options.probe?.env, providedSecrets);
575
+ const probe = { ...(options.probe ?? {}), env: probeEnv };
576
+ // An explicit --command names an executable that may deliberately sit outside
577
+ // PATH. It is verified HERE (exists + executable); when it checks out it
578
+ // satisfies the variant's command prerequisite, so a local binary is reachable
579
+ // without installing it into PATH.
580
+ const explicitCommand = options.command?.trim();
581
+ const explicitCommandOk = explicitCommand ? commandExists(explicitCommand, probeEnv) : false;
582
+ const chosen = chooseVariant(entry, {
583
+ ...(options.variantId ? { variantId: options.variantId } : {}),
584
+ probe,
585
+ ...(options.runtime ? { runtime: options.runtime } : {}),
586
+ });
587
+ // Resolve which variant to write. A named variant that exists but cannot run
588
+ // is still selected — the caller may be about to fix it — and the prerequisite
589
+ // gate below decides; an unknown name, or nothing runnable with no --force, is
590
+ // a refusal that names every way the capability could work and what is missing.
591
+ let choice;
592
+ if (chosen.ok) {
593
+ choice = chosen.choice;
594
+ }
595
+ else if (options.variantId) {
596
+ const named = chosen.choices.find((c) => c.variant.id === options.variantId);
597
+ if (!named)
598
+ return { ok: false, reason: chosen.reason };
599
+ choice = named;
600
+ }
601
+ else if (options.force) {
602
+ // Nothing probed clean; the user asked to override the probe, so write the
603
+ // preferred variant (lowest rank, never an opt-in one) anyway.
604
+ choice = chosen.choices.find((c) => !c.variant.optIn) ?? chosen.choices[0];
605
+ }
606
+ else {
607
+ return {
608
+ ok: false,
609
+ reason: chosen.reason,
610
+ missing: chosen.choices
611
+ .filter((c) => !c.variant.optIn)
612
+ .flatMap((c) => describePrereqProblems(c.status)),
613
+ };
614
+ }
615
+ const variant = choice.variant;
616
+ // Prerequisite gate — the "call out the dependency before enabling" rule.
617
+ const missingAfterCommand = explicitCommandOk
618
+ ? choice.status.missing.filter((p) => p.kind !== 'command')
619
+ : choice.status.missing;
620
+ // NOTE: `--command` satisfies a MISSING binary only. A `service` that is
621
+ // present but down stays blocked — pointing at a path does not start a daemon.
622
+ if ((missingAfterCommand.length > 0 || choice.status.blocked.length > 0) && !options.force) {
623
+ const missing = describePrereqProblems({ ...choice.status, missing: missingAfterCommand });
624
+ return {
625
+ ok: false,
626
+ reason: `'${entry.name}' (variant '${variant.id}') cannot run on this machine yet:\n${missing
627
+ .map((m) => ` • ${m}`)
628
+ .join('\n')}\nInstall the dependency above, pass a runtime override, or re-run with --force.`,
629
+ missing,
630
+ variantId: variant.id,
631
+ };
632
+ }
633
+ // Resolvability gate — a pin that does not exist must fail HERE, not at
634
+ // connect time in a chat turn. An UNCHECKED verdict (offline) is not a
635
+ // refusal: it installs and reports `unverifiedPins` instead.
636
+ if (options.pinVerdict?.checked && !options.pinVerdict.ok && !options.force) {
637
+ return {
638
+ ok: false,
639
+ reason: `pinned package does not resolve: ${options.pinVerdict.reason ?? 'not found in the registry'}`,
640
+ variantId: variant.id,
641
+ };
642
+ }
643
+ // Resolve env (required + optional) against the EFFECTIVE environment — the
644
+ // same one the probe saw — so what was verified and what is written cannot
645
+ // disagree (`--env KEY=VALUE` counts for both).
646
+ const resolveFrom = (key) => {
647
+ const fromEnv = probeEnv[key];
648
+ if (fromEnv && fromEnv.trim())
649
+ return fromEnv;
650
+ const explicit = providedSecrets?.[key];
651
+ return explicit ?? null;
652
+ };
114
653
  const env = {};
115
- for (const [key, value] of Object.entries(entry.env ?? {})) {
116
- const resolved = value === 'prompt-secret' ? resolveSecret(key, providedSecrets) : value;
654
+ for (const [key, value] of Object.entries(variant.env ?? {})) {
655
+ const resolved = value === 'prompt-secret' ? resolveFrom(key) : value;
117
656
  if (resolved === null) {
118
- return { ok: false, reason: `missing required env var ${key} — set it or pass a value` };
657
+ return {
658
+ ok: false,
659
+ reason: `missing required env var ${key} — set it or pass a value`,
660
+ variantId: variant.id,
661
+ };
119
662
  }
120
663
  env[key] = resolved;
121
664
  }
665
+ for (const [key, value] of Object.entries(variant.envOptional ?? {})) {
666
+ const resolved = value === 'prompt-secret' ? resolveFrom(key) : value;
667
+ if (resolved !== null)
668
+ env[key] = resolved;
669
+ }
670
+ const headers = variant.headers
671
+ ? Object.fromEntries(Object.entries(variant.headers).map(([k, v]) => [k, expandRefs(v, env)]))
672
+ : undefined;
673
+ // OAuth endpoints are literal; the CLIENT CREDENTIALS may be `${VAR}`
674
+ // references, expanded from the same resolved env as the headers so the
675
+ // secret is never duplicated into a second place.
676
+ const oauth = variant.oauth
677
+ ? {
678
+ ...variant.oauth,
679
+ ...(variant.oauth.clientId ? { clientId: expandRefs(variant.oauth.clientId, env) } : {}),
680
+ ...(variant.oauth.clientSecret ? { clientSecret: expandRefs(variant.oauth.clientSecret, env) } : {}),
681
+ }
682
+ : undefined;
683
+ const runtime = options.runtime ?? variant.runtime;
122
684
  const config = {
123
685
  name: entry.name,
124
- transport: entry.transport,
686
+ transport: variant.transport,
125
687
  enabled: true,
126
- ...(entry.command ? { command: entry.command } : {}),
127
- ...(entry.args ? { args: entry.args } : {}),
128
- ...(entry.url ? { url: entry.url } : {}),
688
+ variant: variant.id, ...((explicitCommand ?? variant.command) ? { command: explicitCommand ?? variant.command } : {}),
689
+ ...(variant.args ? { args: variant.args } : {}),
690
+ ...(variant.url ? { url: variant.url } : {}),
691
+ ...(headers ? { headers } : {}),
692
+ ...(oauth ? { oauth } : {}),
129
693
  ...(Object.keys(env).length > 0 ? { env } : {}),
694
+ ...(runtime ? { runtime } : {}),
130
695
  };
131
696
  try {
132
697
  mkdirSync(join(target, '..'), { recursive: true });
133
698
  writeFileSync(target, JSON.stringify(config, null, 2), 'utf-8');
134
- logger.success(`Installed MCP server '${entry.name}' → ${target}`);
135
- return { ok: true, path: target };
699
+ // §7.8 hygiene: a config that carries a credential is not world-readable.
700
+ if (Object.keys(env).length > 0) {
701
+ try {
702
+ chmodSync(target, 0o600);
703
+ }
704
+ catch {
705
+ // Windows / exotic filesystems: best-effort, never fail an install.
706
+ }
707
+ }
708
+ // The CALLER reports success (one fact, one owner) — the CLI prints the
709
+ // variant, what it offers and the next step; a second success line here
710
+ // would just say the same thing twice.
711
+ logger.debug(`MCP install: wrote '${entry.name}' (variant ${variant.id}) → ${target}`);
712
+ return {
713
+ ok: true,
714
+ path: target,
715
+ variantId: variant.id,
716
+ ...(options.pinVerdict && !options.pinVerdict.checked ? { unverifiedPins: true } : {}),
717
+ ...(options.force && options.pinVerdict && !options.pinVerdict.ok ? { forced: true } : {}),
718
+ };
136
719
  }
137
720
  catch (err) {
138
721
  return { ok: false, reason: `write failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -146,11 +729,40 @@ export function uninstallCatalogServer(name, configDir) {
146
729
  }
147
730
  try {
148
731
  unlinkSync(target);
149
- logger.success(`Uninstalled MCP server '${name}'`);
732
+ logger.debug(`MCP uninstall: removed '${name}' (${target})`);
150
733
  return { ok: true };
151
734
  }
152
735
  catch (err) {
153
736
  return { ok: false, reason: `unlink failed: ${err instanceof Error ? err.message : String(err)}` };
154
737
  }
155
738
  }
739
+ /**
740
+ * The exact npm specs ONE variant pins (`@scope/pkg@1.2.3`) — what the
741
+ * registry-existence check should verify before this variant is written. Empty
742
+ * for remote/native variants. Shared by the CLI and the dashboard.
743
+ */
744
+ export function npmSpecsForVariant(variant) {
745
+ if (variant.command !== 'npx')
746
+ return [];
747
+ return (variant.args ?? []).filter((arg) => /^(@[^/]+\/)?[^@]+@\d/.test(arg));
748
+ }
749
+ /**
750
+ * Every exact npm spec in the catalog (`@scope/pkg@1.2.3`), for the
751
+ * registry-existence check. Kept here so the catalog stays the single source of
752
+ * truth for what is pinned.
753
+ */
754
+ export function catalogNpmSpecs(entries = MCP_CATALOG) {
755
+ const specs = new Set();
756
+ for (const entry of entries) {
757
+ for (const variant of entry.variants) {
758
+ if (variant.command !== 'npx')
759
+ continue;
760
+ for (const arg of variant.args ?? []) {
761
+ if (arg.startsWith('@') || /^[a-z0-9][a-z0-9-]*@\d/.test(arg))
762
+ specs.add(arg);
763
+ }
764
+ }
765
+ }
766
+ return [...specs];
767
+ }
156
768
  //# sourceMappingURL=catalog.js.map