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.
- package/dist/agent-sdk/src/register.d.ts +23 -7
- package/dist/agent-sdk/src/register.d.ts.map +1 -1
- package/dist/agent-sdk/src/register.js +159 -5
- package/dist/agent-sdk/src/register.js.map +1 -1
- package/dist/agent-sdk/src/scaffold.d.ts +1 -1
- package/dist/agent-sdk/src/scaffold.js +1 -1
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/agent.js +16 -3
- package/dist/cli/agent.js.map +1 -1
- package/dist/cli/chat.d.ts +85 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +462 -15
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +39 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +18 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/mcp.d.ts +50 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +496 -49
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/memory.d.ts.map +1 -1
- package/dist/cli/memory.js +3 -2
- package/dist/cli/memory.js.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/plugins.js +26 -9
- package/dist/cli/plugins.js.map +1 -1
- package/dist/cli/sdk.d.ts.map +1 -1
- package/dist/cli/sdk.js +10 -3
- package/dist/cli/sdk.js.map +1 -1
- package/dist/cli/tool-install-prompt.d.ts.map +1 -1
- package/dist/cli/tool-install-prompt.js +23 -1
- package/dist/cli/tool-install-prompt.js.map +1 -1
- package/dist/cli/tools.d.ts +16 -0
- package/dist/cli/tools.d.ts.map +1 -1
- package/dist/cli/tools.js +101 -1
- package/dist/cli/tools.js.map +1 -1
- package/dist/config/manager.d.ts +6 -0
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +66 -0
- package/dist/config/manager.js.map +1 -1
- package/dist/config/types.d.ts +18 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/learning/exhaustion-report.d.ts +12 -0
- package/dist/learning/exhaustion-report.d.ts.map +1 -1
- package/dist/learning/exhaustion-report.js +3 -1
- package/dist/learning/exhaustion-report.js.map +1 -1
- package/dist/learning/model-capability.d.ts +33 -0
- package/dist/learning/model-capability.d.ts.map +1 -1
- package/dist/learning/model-capability.js +51 -0
- package/dist/learning/model-capability.js.map +1 -1
- package/dist/learning/model-first-router.d.ts +7 -22
- package/dist/learning/model-first-router.d.ts.map +1 -1
- package/dist/learning/model-first-router.js +7 -37
- package/dist/learning/model-first-router.js.map +1 -1
- package/dist/learning/model-selection.d.ts +27 -0
- package/dist/learning/model-selection.d.ts.map +1 -1
- package/dist/learning/model-selection.js +46 -0
- package/dist/learning/model-selection.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +1 -1
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/learning/resilient-call.d.ts +15 -1
- package/dist/learning/resilient-call.d.ts.map +1 -1
- package/dist/learning/resilient-call.js +38 -22
- package/dist/learning/resilient-call.js.map +1 -1
- package/dist/learning/working-state.d.ts +7 -0
- package/dist/learning/working-state.d.ts.map +1 -1
- package/dist/learning/working-state.js +23 -1
- package/dist/learning/working-state.js.map +1 -1
- package/dist/mcp/catalog.d.ts +206 -26
- package/dist/mcp/catalog.d.ts.map +1 -1
- package/dist/mcp/catalog.js +664 -52
- package/dist/mcp/catalog.js.map +1 -1
- package/dist/mcp/client.d.ts +6 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +35 -5
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/custom-server.d.ts +120 -0
- package/dist/mcp/custom-server.d.ts.map +1 -0
- package/dist/mcp/custom-server.js +330 -0
- package/dist/mcp/custom-server.js.map +1 -0
- package/dist/mcp/manager.d.ts +9 -1
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +15 -2
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/mcp-oauth.d.ts +28 -0
- package/dist/mcp/mcp-oauth.d.ts.map +1 -1
- package/dist/mcp/mcp-oauth.js +63 -8
- package/dist/mcp/mcp-oauth.js.map +1 -1
- package/dist/mcp/oauth-authorize.d.ts +51 -0
- package/dist/mcp/oauth-authorize.d.ts.map +1 -0
- package/dist/mcp/oauth-authorize.js +133 -0
- package/dist/mcp/oauth-authorize.js.map +1 -0
- package/dist/mcp/oauth-connect.d.ts +53 -0
- package/dist/mcp/oauth-connect.d.ts.map +1 -0
- package/dist/mcp/oauth-connect.js +103 -0
- package/dist/mcp/oauth-connect.js.map +1 -0
- package/dist/mcp/pin-verifier.d.ts +44 -0
- package/dist/mcp/pin-verifier.d.ts.map +1 -0
- package/dist/mcp/pin-verifier.js +102 -0
- package/dist/mcp/pin-verifier.js.map +1 -0
- package/dist/mcp/prereqs.d.ts +145 -0
- package/dist/mcp/prereqs.d.ts.map +1 -0
- package/dist/mcp/prereqs.js +245 -0
- package/dist/mcp/prereqs.js.map +1 -0
- package/dist/mcp/runtime.d.ts +59 -0
- package/dist/mcp/runtime.d.ts.map +1 -0
- package/dist/mcp/runtime.js +77 -0
- package/dist/mcp/runtime.js.map +1 -0
- package/dist/mcp/server.d.ts +21 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +29 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/settings.d.ts +164 -0
- package/dist/mcp/settings.d.ts.map +1 -0
- package/dist/mcp/settings.js +358 -0
- package/dist/mcp/settings.js.map +1 -0
- package/dist/mcp/types.d.ts +62 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/mcp/types.js.map +1 -1
- package/dist/memory/vector-store.d.ts +12 -0
- package/dist/memory/vector-store.d.ts.map +1 -1
- package/dist/memory/vector-store.js +23 -0
- package/dist/memory/vector-store.js.map +1 -1
- package/dist/plugins/agent-plugin.d.ts +35 -0
- package/dist/plugins/agent-plugin.d.ts.map +1 -1
- package/dist/plugins/agent-plugin.js +142 -24
- package/dist/plugins/agent-plugin.js.map +1 -1
- package/dist/sandbox/manager.d.ts +12 -0
- package/dist/sandbox/manager.d.ts.map +1 -1
- package/dist/sandbox/manager.js +43 -2
- package/dist/sandbox/manager.js.map +1 -1
- package/dist/tools/modality/browser.d.ts +39 -3
- package/dist/tools/modality/browser.d.ts.map +1 -1
- package/dist/tools/modality/browser.js +113 -16
- package/dist/tools/modality/browser.js.map +1 -1
- package/dist/tools/optional-deps.d.ts +48 -0
- package/dist/tools/optional-deps.d.ts.map +1 -0
- package/dist/tools/optional-deps.js +42 -0
- package/dist/tools/optional-deps.js.map +1 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +68 -11
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/remediation-ladder.d.ts.map +1 -1
- package/dist/tools/remediation-ladder.js +9 -1
- package/dist/tools/remediation-ladder.js.map +1 -1
- package/dist/tools/tool-loop.d.ts.map +1 -1
- package/dist/tools/tool-loop.js +57 -5
- package/dist/tools/tool-loop.js.map +1 -1
- package/dist/web-dashboard/chat-retry.d.ts +11 -1
- package/dist/web-dashboard/chat-retry.d.ts.map +1 -1
- package/dist/web-dashboard/chat-retry.js +18 -4
- package/dist/web-dashboard/chat-retry.js.map +1 -1
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +349 -2
- package/dist/web-dashboard/server.js.map +1 -1
- package/package.json +3 -2
- package/src/web-dashboard/public/assets/{index-DeD3L0kG.js → index-yky0dw5e.js} +115 -89
- package/src/web-dashboard/public/assets/index-yky0dw5e.js.map +1 -0
- package/src/web-dashboard/public/index.html +1 -1
- package/src/web-dashboard/public/assets/index-DeD3L0kG.js.map +0 -1
package/dist/mcp/catalog.js
CHANGED
|
@@ -1,72 +1,444 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* I7 P2 — Curated MCP catalog (`src/mcp/catalog.ts`).
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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>]
|
|
13
|
-
* nuvira mcp install <name>
|
|
14
|
-
* nuvira mcp uninstall <name>
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
104
|
-
* (never the buffconfig), return
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
-
//
|
|
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(
|
|
116
|
-
const resolved = value === 'prompt-secret' ?
|
|
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 {
|
|
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:
|
|
686
|
+
transport: variant.transport,
|
|
125
687
|
enabled: true,
|
|
126
|
-
...(
|
|
127
|
-
...(
|
|
128
|
-
...(
|
|
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
|
-
|
|
135
|
-
|
|
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.
|
|
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
|