agent-nuvira 3.3.19 → 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 (171) 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-aO3aG7ex.css +1 -0
  167. package/src/web-dashboard/public/assets/{index-Bz7QqQ_N.js → index-yky0dw5e.js} +115 -89
  168. package/src/web-dashboard/public/assets/index-yky0dw5e.js.map +1 -0
  169. package/src/web-dashboard/public/index.html +2 -2
  170. package/src/web-dashboard/public/assets/index-Bz7QqQ_N.js.map +0 -1
  171. package/src/web-dashboard/public/assets/index-cBWT2qxn.css +0 -1
package/dist/cli/mcp.js CHANGED
@@ -11,12 +11,26 @@
11
11
  * nuvira mcp info <name> — Show detailed info for an MCP server
12
12
  * nuvira mcp refresh — Re-discover and reconnect to MCP servers
13
13
  * nuvira mcp serve — Expose agent tools as an MCP server (stdio)
14
+ * nuvira mcp catalog — Capability slots, their variants, what each needs
15
+ * nuvira mcp install <name> — Install the best variant this machine can run
16
+ * nuvira mcp add <name> — Configure ANY server (not in the catalog) by hand
17
+ * nuvira mcp settings — Show the resolved `mcp.*` configuration
18
+ *
19
+ * 2026-10-12: `catalog`/`install` show and honour PREREQUISITES. A capability
20
+ * that cannot run here is reported with the missing dependency and how to get it,
21
+ * instead of writing a config that silently never starts (assessment §2).
14
22
  */
23
+ import { readFileSync } from 'node:fs';
15
24
  import { Command } from 'commander';
16
25
  import { BaseCommand, getCliName } from './commands.js';
17
26
  import { getMCPManager } from '../mcp/manager.js';
18
27
  import { MCP_CONFIG_DIR } from '../mcp/types.js';
19
- import { MCP_CATALOG, searchCatalog, getCatalogEntry, installCatalogServer, uninstallCatalogServer, isCatalogServerInstalled, catalogConfigPath, } from '../mcp/catalog.js';
28
+ import { MCP_CATALOG, searchCatalog, getCatalogEntry, installCatalogServer, uninstallCatalogServer, isCatalogServerInstalled, catalogConfigPath, chooseVariant, effectiveProbeEnv, evaluateVariants, npmSpecsForVariant, } from '../mcp/catalog.js';
29
+ import { commandExists, describePrereqProblems, resolveCommandPath, } from '../mcp/prereqs.js';
30
+ import { serverOrigin, validateServerSpec, writeServerConfig } from '../mcp/custom-server.js';
31
+ import { parseRuntimeSpec } from '../mcp/runtime.js';
32
+ import { verifyNpmSpec } from '../mcp/pin-verifier.js';
33
+ import { loadMcpSettings } from '../mcp/settings.js';
20
34
  import { logger } from '../utils/logger.js';
21
35
  export class MCPCommand extends BaseCommand {
22
36
  create() {
@@ -76,48 +90,59 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
76
90
  .action(async () => {
77
91
  await this.refreshServers();
78
92
  });
79
- // ── catalog (I7 P2) ───────────────────────────────────────────────────
93
+ // ── catalog (I7 P2; slots + variants + prerequisites) ─────────────────
80
94
  command
81
95
  .command('catalog')
82
- .description('List the curated MCP server catalog (vetted, exact-version pins)')
96
+ .description('List MCP capability slots, their variants, and what each one needs on this machine')
83
97
  .option('--search <q>', 'Filter the catalog by keyword')
84
98
  .action((opts) => {
85
- const entries = opts?.search ? searchCatalog(opts.search) : MCP_CATALOG;
86
- if (entries.length === 0) {
87
- logger.info(`No catalog entries match '${opts?.search}'.`);
88
- return;
89
- }
90
- console.log(`\n📦 ${entries.length} vetted MCP server(s) (${entries.length === MCP_CATALOG.length ? 'full catalog' : 'filtered'}):\n`);
91
- for (const e of entries) {
92
- const installed = isCatalogServerInstalled(e.name) ? '✅ installed' : '—';
93
- console.log(` • ${e.name} [${installed}]`);
94
- console.log(` ${e.description}`);
95
- console.log(` ${e.transport}: ${e.command ? `${e.command} ${(e.args ?? []).join(' ')}` : e.url}`);
96
- console.log(` vetted by: ${e.vettedBy}`);
97
- console.log('');
98
- }
99
- console.log('Install one with: nuvira mcp install <name>');
99
+ this.showCatalog(opts?.search);
100
100
  });
101
- // ── install (I7 P2) ───────────────────────────────────────────────────
101
+ // ── install (I7 P2; prerequisite + resolvability gated) ───────────────
102
102
  command
103
103
  .command('install <name>')
104
- .description('Install a vetted MCP server from the catalog (writes ~/.nuvira/mcp/<name>.json)')
104
+ .description('Install a catalog capability, choosing the best variant this machine can actually run')
105
105
  .option('--env <key=value>', 'Provide a prompt-secret env var (repeatable: --env KEY=VAL)', collectEnv, {})
106
+ .option('--variant <id>', 'Force a variant id (see `mcp catalog`) — the only way to pick an opt-in one')
107
+ .option('--runtime <command>', 'Run this server under its own interpreter, e.g. --runtime "/opt/node20/bin/node /usr/local/bin/npx"')
108
+ .option('--command <path>', 'Use an explicit executable path (a binary installed outside PATH)')
109
+ .option('--force', 'Install even when a prerequisite is missing or a pin does not resolve')
110
+ .option('--no-verify', 'Skip the npm resolvability check (offline; the result is reported as unverified)')
106
111
  .action(async (name, opts) => {
107
- const entry = getCatalogEntry(name);
108
- if (!entry) {
109
- logger.error(`'${name}' is not in the catalog. Run \`${getCliName()} mcp catalog\` to see vetted servers.`);
110
- return;
111
- }
112
- const result = installCatalogServer(entry, opts?.env ?? {});
113
- if (!result.ok) {
114
- logger.error(`❌ ${result.reason}`);
115
- return;
116
- }
117
- logger.success(`✅ Installed '${name}' → ${result.path}`);
118
- logger.info(' Connect with: nuvira mcp connect ' + name);
112
+ await this.installServer(name, opts ?? {});
113
+ });
114
+ // ── add (custom server: configuration, not code) ──────────────────────
115
+ command
116
+ .command('add <name>')
117
+ .description('Add a server that is NOT in the catalog — any stdio command or remote URL, like a config file')
118
+ .option('--transport <kind>', "stdio | sse (inferred from --command / --url when omitted)")
119
+ .option('--command <path>', 'stdio: the executable to run (npx, node, an absolute path, …)')
120
+ .option('--arg <value>', 'stdio: one argument, in order (repeatable: --arg -y --arg @scope/pkg)', collectArg, [])
121
+ .option('--env <key=value>', 'environment variable for the server (repeatable: --env TOKEN=abc)', collectEnv, {})
122
+ .option('--url <url>', 'sse: the remote Streamable HTTP endpoint')
123
+ .option('--header <name:value>', 'sse: one HTTP header, e.g. --header "Authorization: Bearer …" (repeatable)', collectHeader, {})
124
+ .option('--json <spec>', 'a raw config object to start from — inline JSON, @file, or @- for stdin')
125
+ .option('--force', 'replace an existing config file for this name')
126
+ .option('--no-verify', 'skip the check that the stdio command exists on PATH')
127
+ .action(async (name, opts) => {
128
+ await this.addServer(name, opts ?? {});
129
+ });
130
+ // ── settings (the `mcp.*` config surface, resolved) ───────────────────
131
+ command
132
+ .command('settings')
133
+ .description('Show the resolved mcp.* configuration (caps, auto-connect, subagent inheritance)')
134
+ .action(() => {
135
+ this.showSettings();
119
136
  });
120
137
  // ── uninstall (I7 P2) ─────────────────────────────────────────────────
138
+ command
139
+ .command('authorize <name>')
140
+ .description('Authorize an OAuth MCP server (opens the consent page, captures the loopback redirect, stores the token)')
141
+ .option('--redirect-uri <uri>', 'Override the loopback redirect URI (must be registered on the OAuth client)')
142
+ .option('--no-browser', 'Do not try to open the consent page — print the URL instead')
143
+ .action(async (name, options) => {
144
+ await this.authorizeServer(name, options || {});
145
+ });
121
146
  command
122
147
  .command('uninstall <name>')
123
148
  .description('Remove an installed MCP server config')
@@ -134,12 +159,315 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
134
159
  .command('serve')
135
160
  .description('Expose the agent\'s H1 tools as an MCP server over stdio (MCP clients / IDEs / other agents can connect)')
136
161
  .option('--with <tools>', 'Comma-separated extra tools to expose beyond the safe surface (e.g. publish)', '')
162
+ .option('--memory', 'Also expose the MEMORY surface (search/add/replace/delete/list memories + stats) so the client can recall from the SAME store the agent writes to', false)
137
163
  .action(async (options) => {
138
164
  await this.serveTools(options || {});
139
165
  });
140
166
  return command;
141
167
  }
142
168
  // ── Action Handlers ──────────────────────────────────────────────────────
169
+ /**
170
+ * `nuvira mcp catalog` — every capability slot with its ranked variants and
171
+ * the prerequisite status of each. This is the "know what it offers and what
172
+ * you must have before you enable it" surface: nothing is installed, nothing
173
+ * is spawned, and a variant this machine cannot run says exactly why.
174
+ */
175
+ showCatalog(search) {
176
+ const entries = search ? searchCatalog(search) : MCP_CATALOG;
177
+ // A `service` prerequisite (docker's daemon) can only be established by
178
+ // actually asking it, so the CATALOG command runs those checks; unit tests
179
+ // and any non-interactive caller leave `deep` off.
180
+ const probe = { deep: true };
181
+ if (entries.length === 0) {
182
+ logger.info(`No catalog entries match '${search}'.`);
183
+ return;
184
+ }
185
+ const cli = getCliName();
186
+ console.log(`\n📦 ${entries.length} MCP capabilit${entries.length === 1 ? 'y' : 'ies'} (${entries.length === MCP_CATALOG.length ? 'full catalog' : 'filtered'}):\n`);
187
+ for (const entry of entries) {
188
+ const installed = isCatalogServerInstalled(entry.name) ? ' ✅ installed' : '';
189
+ console.log(` • ${entry.name}${installed}`);
190
+ console.log(` ${entry.description}`);
191
+ console.log(` offers:`);
192
+ for (const offer of entry.offers)
193
+ console.log(` - ${offer}`);
194
+ console.log(` ways to run it (${entry.variants.length}):`);
195
+ for (const choice of evaluateVariants(entry, { probe })) {
196
+ this.printVariantLine(choice);
197
+ }
198
+ if (entry.supersedes) {
199
+ console.log(` supersedes: ${entry.supersedes}`);
200
+ }
201
+ console.log(` install: ${cli} mcp install ${entry.name}`);
202
+ console.log('');
203
+ }
204
+ console.log('Every variant is configuration, not code — the install writes');
205
+ console.log('~/.nuvira/mcp/<name>.json and the caps/runtime come from `mcp.*` in');
206
+ console.log('buffconfig.json (`' + getCliName() + ' mcp settings`).');
207
+ }
208
+ /** One variant: runnable or not, what it needs, its trust level and any caveat. */
209
+ printVariantLine(choice) {
210
+ const { variant, status } = choice;
211
+ const mark = status.ok ? '✅' : '⛔';
212
+ const optIn = variant.optIn ? ' [opt-in: --variant ' + variant.id + ']' : '';
213
+ console.log(` ${mark} ${variant.id} — ${variant.label}${optIn} (${variant.trust})`);
214
+ console.log(` ${variant.command ? `${variant.command} ${(variant.args ?? []).join(' ')}` : variant.url}`);
215
+ // "needs" = absent, "blocked" = present but not working. They are different
216
+ // fixes, so they are different lines (the docker-daemon case).
217
+ for (const line of describePrereqProblems(status)) {
218
+ console.log(` ${status.blocked.some((b) => line.startsWith(b.reason)) ? 'blocked:' : 'needs:'} ${line}`);
219
+ }
220
+ // Where a satisfied prerequisite actually IS — so "it says not installed" is
221
+ // falsifiable in one `ls`, and a found binary is visibly found. Deduped: a
222
+ // `command` and a `service` on the same binary is one fact, not two lines.
223
+ const reported = new Set();
224
+ for (const prereq of variant.requires) {
225
+ if (prereq.kind !== 'command' && prereq.kind !== 'service')
226
+ continue;
227
+ if (status.missing.includes(prereq) || reported.has(prereq.name))
228
+ continue;
229
+ const path = resolveCommandPath(prereq.name, process.env);
230
+ if (path) {
231
+ reported.add(prereq.name);
232
+ console.log(` found: ${prereq.name} → ${path}`);
233
+ }
234
+ }
235
+ for (const note of status.unverified) {
236
+ console.log(` note: ${note}`);
237
+ }
238
+ if (variant.note)
239
+ console.log(` caveat: ${variant.note}`);
240
+ }
241
+ /**
242
+ * `nuvira mcp install` — probe, verify, then write.
243
+ *
244
+ * Order matters: choose a variant this machine can run, verify the pinned npm
245
+ * spec exists (unless offline), and only then write the config. Each refusal
246
+ * names the exact dependency, so a failed install teaches the user what to
247
+ * install rather than leaving a dead server behind.
248
+ */
249
+ async installServer(name, opts) {
250
+ const entry = getCatalogEntry(name);
251
+ if (!entry) {
252
+ logger.error(`'${name}' is not in the catalog. Run \`${getCliName()} mcp catalog\` to see the capabilities.`);
253
+ return;
254
+ }
255
+ const runtime = parseRuntimeSpec(opts.runtime);
256
+ if (opts.runtime && !runtime) {
257
+ logger.error(`--runtime '${opts.runtime}' is empty. Pass a command, e.g. --runtime "/opt/node20/bin/node /usr/local/bin/npx".`);
258
+ return;
259
+ }
260
+ // Identify the target variant (for the resolvability check). Whether it can
261
+ // actually run is decided in ONE place — `installCatalogServer` below — so a
262
+ // flag like `--command` can satisfy a prerequisite without the CLI having its
263
+ // own, differently-informed opinion.
264
+ const chosen = chooseVariant(entry, {
265
+ ...(opts.variant ? { variantId: opts.variant } : {}),
266
+ ...(runtime ? { runtime } : {}),
267
+ // Probe against the environment the install will actually use — including
268
+ // secrets being supplied on this command line.
269
+ probe: { env: effectiveProbeEnv(undefined, opts.env), deep: true },
270
+ });
271
+ const choice = chosen.ok
272
+ ? chosen.choice
273
+ : opts.variant
274
+ ? chosen.choices.find((c) => c.variant.id === opts.variant)
275
+ : undefined;
276
+ // npm resolvability (network). A miss refuses; unreachable proceeds unverified.
277
+ const pinSpecs = choice ? npmSpecsForVariant(choice.variant) : [];
278
+ let pinVerdict;
279
+ if (pinSpecs.length > 0 && opts.verify !== false) {
280
+ const verdicts = await Promise.all(pinSpecs.map((spec) => verifyNpmSpec(spec)));
281
+ const failed = verdicts.find((v) => v.checked && !v.ok);
282
+ const unreachable = verdicts.find((v) => !v.checked);
283
+ const deprecated = verdicts.find((v) => v.deprecated);
284
+ if (failed) {
285
+ pinVerdict = failed;
286
+ }
287
+ else if (unreachable) {
288
+ pinVerdict = unreachable;
289
+ logger.warn(` ⚠️ Could not verify ${pinSpecs.join(', ')} — ${unreachable.reason}. Saved unverified.`);
290
+ }
291
+ else {
292
+ pinVerdict = { checked: true, ok: true };
293
+ }
294
+ if (deprecated?.deprecated) {
295
+ logger.warn(` ⚠️ npm marks this package deprecated: ${deprecated.deprecated}`);
296
+ }
297
+ }
298
+ const result = installCatalogServer(entry, opts.env ?? {}, undefined, {
299
+ ...(opts.variant ? { variantId: opts.variant } : {}),
300
+ ...(opts.command ? { command: opts.command } : {}),
301
+ ...(runtime ? { runtime } : {}),
302
+ ...(pinVerdict ? { pinVerdict } : {}),
303
+ ...(opts.force ? { force: true } : {}),
304
+ probe: { env: effectiveProbeEnv(undefined, opts.env), deep: true },
305
+ });
306
+ if (!result.ok) {
307
+ logger.error(`❌ ${result.reason}`);
308
+ if (result.missing?.some((line) => line.includes('not found on PATH'))) {
309
+ logger.info(' Already installed but not on PATH? Point at it: --command /path/to/binary');
310
+ }
311
+ if (result.missing?.length) {
312
+ logger.info(` All variants: ${getCliName()} mcp catalog --search ${name}`);
313
+ }
314
+ return;
315
+ }
316
+ const variant = choice?.variant ?? entry.variants.find((v) => v.id === result.variantId);
317
+ logger.success(`✅ Installed '${name}' (${result.variantId}) → ${result.path}`);
318
+ console.log(` ${entry.description}`);
319
+ console.log(` what you get:`);
320
+ for (const offer of entry.offers)
321
+ console.log(` - ${offer}`);
322
+ if (opts.command)
323
+ console.log(` command: ${opts.command}`);
324
+ if (runtime || variant.runtime) {
325
+ const rt = runtime ?? variant.runtime;
326
+ console.log(` runtime override: ${[rt.command, ...(rt.args ?? [])].join(' ')}`);
327
+ }
328
+ if (variant.transport === 'sse') {
329
+ console.log(' remote server — nothing is installed locally.');
330
+ }
331
+ if (variant.note)
332
+ console.log(` caveat: ${variant.note}`);
333
+ if (result.unverifiedPins)
334
+ console.log(' note: the pinned npm version was NOT verified against the registry.');
335
+ console.log(` verify: ${getCliName()} mcp connect ${name}`);
336
+ console.log(` tune: ${getCliName()} mcp settings (caps, auto-connect, per-server overrides)`);
337
+ }
338
+ /** `nuvira mcp settings` — the resolved `mcp.*` section, and how to change it. */
339
+ showSettings() {
340
+ const settings = loadMcpSettings();
341
+ console.log('\n MCP settings (buffconfig.json → "mcp"):\n');
342
+ console.log(` enabled ${settings.enabled ? 'true' : 'false'} (master switch)`);
343
+ console.log(` autoConnect ${settings.autoConnect ? 'true' : 'false'} (connect at turn start)`);
344
+ console.log(` connectTimeoutMs ${settings.connectTimeoutMs} (wall-clock budget for the connect step)`);
345
+ // NOTE: these two are parsed, stored and reported, but NOT yet enforced —
346
+ // no code path truncates MCP tool exposure by them (`effectiveToolCap` in
347
+ // mcp/settings.ts has no callers). Saying "eager tool schemas per server"
348
+ // would describe a limit that does not bite.
349
+ console.log(` maxToolsPerServer ${settings.maxToolsPerServer} (per-server cap — stored, NOT enforced yet)`);
350
+ console.log(` maxToolsTotal ${settings.maxToolsTotal} (cross-server cap — stored, NOT enforced yet)`);
351
+ console.log(` subagent.inherit ${settings.subagentInheritance} (call by a subagent)`);
352
+ console.log('');
353
+ console.log(' subagent.inherit: "conservative" = a subagent only gets MCP when the parent had it');
354
+ console.log(' AND its own allowance covers it. "full" = the parent\'s whole MCP set is passed down');
355
+ console.log(' (still cap-bound and still governed by effect classes, hooks and grants).');
356
+ console.log('');
357
+ const servers = Object.entries(settings.servers);
358
+ if (servers.length > 0) {
359
+ console.log(' per-server overrides:');
360
+ for (const [name, override] of servers) {
361
+ const parts = [
362
+ override.enabled === false ? 'disabled' : undefined,
363
+ override.variant ? `variant=${override.variant}` : undefined,
364
+ override.command ? `command=${override.command}` : undefined,
365
+ override.runtime ? `runtime=${override.runtime.command}` : undefined,
366
+ override.args ? `args=${override.args.join(' ')}` : undefined,
367
+ override.maxTools !== undefined ? `maxTools=${override.maxTools}` : undefined,
368
+ ].filter(Boolean);
369
+ console.log(` ${name}: ${parts.join(', ') || '(no recognized keys)'}`);
370
+ }
371
+ console.log('');
372
+ }
373
+ for (const warning of settings.warnings)
374
+ logger.warn(` ⚠️ ${warning}`);
375
+ console.log(' Change one with, e.g.:');
376
+ console.log(` ${getCliName()} config set mcp.subagent.inherit full`);
377
+ console.log(` ${getCliName()} config set mcp.servers.context7.runtime.command /opt/node20/bin/node`);
378
+ console.log('');
379
+ }
380
+ /**
381
+ * `nuvira mcp add <name>` — configure a server we have never heard of.
382
+ *
383
+ * This is the parity path with Claude Desktop / Hermes: the CATALOG is a
384
+ * convenience (curated variants, prerequisites probed, pins verified), not a
385
+ * gate on what may be connected. Discovery has always read `*.json` from the
386
+ * config dir, so this command writes exactly that file — no code, no release,
387
+ * for anything the user names.
388
+ *
389
+ * Gates that DO apply, because a config that cannot connect is worse than a
390
+ * refusal: the name must be usable as a filename and a settings key, stdio
391
+ * and sse fields cannot be mixed, and the stdio command must exist on PATH
392
+ * (`--no-verify` overrides, for a command that only exists in the server's own
393
+ * environment).
394
+ */
395
+ async addServer(name, opts) {
396
+ const fail = (message) => {
397
+ logger.error(`❌ ${message}`);
398
+ process.exitCode = 1;
399
+ };
400
+ // A pasted vendor snippet is a first-class input (that is how every MCP
401
+ // README documents its server), so `--json` seeds the spec and explicit
402
+ // flags override individual fields.
403
+ let base = {};
404
+ if (opts.json) {
405
+ try {
406
+ let text = opts.json;
407
+ if (opts.json.startsWith('@')) {
408
+ const target = opts.json === '@-' ? 0 : opts.json.slice(1);
409
+ text = readFileSync(target, 'utf-8');
410
+ }
411
+ const parsed = JSON.parse(text);
412
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
413
+ return fail('--json must be a JSON object, e.g. { "command": "npx", "args": ["-y", "pkg"] }.');
414
+ }
415
+ base = parsed;
416
+ }
417
+ catch (err) {
418
+ return fail(`Could not read --json: ${err instanceof Error ? err.message : String(err)}`);
419
+ }
420
+ }
421
+ const baseEnv = (base.env ?? {});
422
+ const baseHeaders = (base.headers ?? {});
423
+ const spec = {
424
+ ...base,
425
+ name,
426
+ ...(opts.transport ? { transport: opts.transport } : {}),
427
+ ...(opts.command ? { command: opts.command } : {}),
428
+ ...(opts.arg && opts.arg.length > 0 ? { args: opts.arg } : {}),
429
+ ...(opts.url ? { url: opts.url } : {}),
430
+ ...(Object.keys(opts.env ?? {}).length > 0 ? { env: { ...baseEnv, ...opts.env } } : {}),
431
+ ...(Object.keys(opts.header ?? {}).length > 0 ? { headers: { ...baseHeaders, ...opts.header } } : {}),
432
+ };
433
+ if (base.name !== undefined && base.name !== name) {
434
+ logger.warn(` note: the JSON named this server '${String(base.name)}' — using '${name}'.`);
435
+ }
436
+ const validated = validateServerSpec(spec);
437
+ if (!validated.ok)
438
+ return fail(validated.error);
439
+ const { config, warnings } = validated;
440
+ // The `install` gate, applied to a hand-written command: name the missing
441
+ // dependency instead of writing a server that will fail at connect time.
442
+ if (config.transport === 'stdio' && config.command && opts.verify !== false) {
443
+ if (!commandExists(config.command)) {
444
+ return fail(`'${config.command}' was not found on PATH, so this server could not start.\n` +
445
+ ` Install it, point at it with --command /absolute/path, or pass --no-verify ` +
446
+ `to configure it anyway.`);
447
+ }
448
+ }
449
+ const result = writeServerConfig(config, {
450
+ ...(opts.force ? { force: true } : {}),
451
+ warnings,
452
+ });
453
+ if (!result.ok)
454
+ return fail(result.reason ?? 'Could not write the config.');
455
+ const cli = getCliName();
456
+ logger.success(`✅ ${result.overwrote ? 'Updated' : 'Added'} '${config.name}' → ${result.path}`);
457
+ console.log(` ${config.transport === 'stdio' ? `${config.command} ${(config.args ?? []).join(' ')}`.trim() : config.url}`);
458
+ const envKeys = Object.keys(config.env ?? {});
459
+ if (envKeys.length > 0) {
460
+ console.log(` credentials: ${envKeys.join(', ')} (written to the config file, never printed)`);
461
+ }
462
+ for (const warning of result.warnings ?? [])
463
+ console.log(` note: ${warning}`);
464
+ if (result.droppedEnvKeys && result.droppedEnvKeys.length > 0) {
465
+ logger.warn(` ⚠️ the replaced config had credentials this one does not: ${result.droppedEnvKeys.join(', ')}`);
466
+ }
467
+ console.log(` verify: ${cli} mcp connect ${config.name}`);
468
+ console.log(` call: ${cli} mcp call <tool-name> --server ${config.name}`);
469
+ console.log(` manage: ${cli} mcp settings (enable/disable, runtime override, caps)`);
470
+ }
143
471
  async listServers() {
144
472
  const manager = getMCPManager();
145
473
  const states = manager.getAllStates();
@@ -211,6 +539,8 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
211
539
  console.log(` 🟢 ${client.name} — ${serverVersion} (${toolCount} tools)`);
212
540
  }
213
541
  console.log('');
542
+ // One-shot: release every stdio child so the command exits (see above).
543
+ manager.disconnectAll();
214
544
  return;
215
545
  }
216
546
  if (!name) {
@@ -226,10 +556,18 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
226
556
  console.log(` Resources: ${client.resources.length}`);
227
557
  console.log(` Prompts: ${client.prompts.length}`);
228
558
  console.log('');
559
+ // A one-shot command must not hold the session open: a stdio server keeps
560
+ // its child alive, so without this `nuvira mcp connect <name>` (the step
561
+ // `mcp install` prints as "verify") never returned to the shell.
562
+ manager.disconnect(name);
229
563
  }
230
564
  catch (err) {
231
565
  logger.error(`Failed to connect: ${err instanceof Error ? err.message : String(err)}`);
232
566
  console.log('');
567
+ // `mcp install` prints this command as the "verify" step, so a failed
568
+ // verification must be a non-zero exit — otherwise a script (or CI) reads
569
+ // a dead server as a working one.
570
+ process.exitCode = 1;
233
571
  }
234
572
  }
235
573
  async callTool(toolName, options) {
@@ -245,13 +583,26 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
245
583
  return;
246
584
  }
247
585
  }
586
+ // WHY THE CLI CONNECTS FIRST. A `call` used to require a connection that
587
+ // only existed inside a long-lived process, so `nuvira mcp call <tool>`
588
+ // could never work from a shell — the one thing a user wants right after
589
+ // adding a server. Connecting here (and releasing it again) makes `call`
590
+ // the shortest honest way to prove a config works.
248
591
  if (options.server) {
249
592
  // Call on a specific server
250
- const client = manager.getClient(options.server);
593
+ let client = manager.getClient(options.server);
594
+ const openedHere = !client;
251
595
  if (!client) {
252
- logger.error(`Server '${options.server}' is not connected. Connect first: nuvira mcp connect ${options.server}`);
253
- console.log('');
254
- return;
596
+ try {
597
+ client = await manager.connect(options.server);
598
+ }
599
+ catch (err) {
600
+ logger.error(`Server '${options.server}' is not connected and could not be reached: ${err instanceof Error ? err.message : String(err)}`);
601
+ logger.info(`Check the config with \`${getCliName()} mcp info ${options.server}\`.`);
602
+ console.log('');
603
+ process.exitCode = 1;
604
+ return;
605
+ }
255
606
  }
256
607
  logger.info(`Calling ${options.server}.${toolName}...`);
257
608
  console.log('');
@@ -262,22 +613,41 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
262
613
  catch (err) {
263
614
  logger.error(`Tool call failed: ${err instanceof Error ? err.message : String(err)}`);
264
615
  console.log('');
616
+ process.exitCode = 1;
617
+ }
618
+ finally {
619
+ // One-shot: release the child we opened, or the process never exits.
620
+ if (openedHere)
621
+ manager.disconnect(options.server);
265
622
  }
266
623
  }
267
624
  else {
268
- // Search across all connected servers
625
+ // Search across all connected servers — connecting them first when this
626
+ // process has nothing, so a bare `mcp call <tool>` is a real lookup.
627
+ const openedHere = manager.getAllTools().length === 0;
628
+ if (openedHere) {
629
+ logger.info('Discovering MCP servers...');
630
+ await manager.connectAll();
631
+ }
269
632
  logger.info(`Searching for tool '${toolName}' across all connected servers...`);
270
633
  console.log('');
271
- const result = await manager.callTool(toolName, args);
272
- if (!result) {
273
- logger.error(`Tool '${toolName}' not found on any connected server.`);
274
- logger.info('Use `${getCliName()} mcp list` to see available tools.');
634
+ try {
635
+ const result = await manager.callTool(toolName, args);
636
+ if (!result) {
637
+ logger.error(`Tool '${toolName}' not found on any connected server.`);
638
+ logger.info(`Use \`${getCliName()} mcp list\` to see available tools.`);
639
+ console.log('');
640
+ process.exitCode = 1;
641
+ return;
642
+ }
643
+ logger.success(`Found tool on server '${result.server}'`);
275
644
  console.log('');
276
- return;
645
+ this.renderToolResult(result.result);
646
+ }
647
+ finally {
648
+ if (openedHere)
649
+ manager.disconnectAll();
277
650
  }
278
- logger.success(`Found tool on server '${result.server}'`);
279
- console.log('');
280
- this.renderToolResult(result.result);
281
651
  }
282
652
  }
283
653
  async showInfo(name) {
@@ -294,6 +664,13 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
294
664
  logger.highlight(` ${statusIcon} ${state.name}`);
295
665
  console.log('');
296
666
  console.log(` Transport: ${state.transport}`);
667
+ // Where the config came from — a curated install and a hand-written file are
668
+ // both fine, and knowing which one is in play is what makes debugging one of
669
+ // them possible.
670
+ const origin = serverOrigin(name);
671
+ console.log(` Source: ${origin.origin === 'catalog'
672
+ ? `catalog${origin.variant ? ` (variant ${origin.variant})` : ' (config written by `mcp install`)'}`
673
+ : 'custom (hand-written, or written by `mcp add`)'}`);
297
674
  console.log(` Status: ${state.status}`);
298
675
  if (state.serverInfo) {
299
676
  console.log(` Server: ${state.serverInfo.name} v${state.serverInfo.version}`);
@@ -350,10 +727,57 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
350
727
  console.log('');
351
728
  }
352
729
  // ── MCP server (F2 follow-up) ─────────────────────────────────────────
730
+ /**
731
+ * `nuvira mcp authorize <name>` — complete an OAuth authorization-code flow
732
+ * for a configured remote server and store the token. The token is then
733
+ * refreshed silently on every connect (see `oauth-connect.ts`).
734
+ */
735
+ async authorizeServer(name, options) {
736
+ const { getMCPManager } = await import('../mcp/manager.js');
737
+ const { runAuthorizeFlow } = await import('../mcp/oauth-authorize.js');
738
+ const config = getMCPManager().discoverConfigs().find((c) => c.name === name);
739
+ if (!config) {
740
+ logger.error(`No configured MCP server named '${name}'.`);
741
+ const oauthServers = getMCPManager().discoverConfigs().filter((c) => c.oauth).map((c) => c.name);
742
+ if (oauthServers.length > 0)
743
+ logger.info(`OAuth servers you can authorize: ${oauthServers.join(', ')}`);
744
+ else
745
+ logger.info('Install one first, e.g. `nuvira mcp install gmail --env GOOGLE_MCP_CLIENT_ID=… --env GOOGLE_MCP_CLIENT_SECRET=…`.');
746
+ process.exitCode = 1;
747
+ return;
748
+ }
749
+ if (!config.oauth) {
750
+ logger.error(`'${name}' is not an OAuth server — it authenticates with ${config.headers ? 'a static header' : 'its own transport credentials'}.`);
751
+ logger.info('Nothing to authorize. OAuth servers are the ones with an `oauth` block (gmail, gdrive, gcalendar).');
752
+ process.exitCode = 1;
753
+ return;
754
+ }
755
+ const clientId = config.oauth.clientId;
756
+ if (!clientId) {
757
+ logger.error(`'${name}' has no oauth.clientId — reinstall with --env GOOGLE_MCP_CLIENT_ID=… .`);
758
+ process.exitCode = 1;
759
+ return;
760
+ }
761
+ const { toOAuthConfig } = await import('../mcp/oauth-connect.js');
762
+ const result = await runAuthorizeFlow(name, toOAuthConfig(config.oauth), {
763
+ ...(options.redirectUri ? { redirectUri: options.redirectUri } : {}),
764
+ ...(options.browser === false ? { openBrowser: false } : {}),
765
+ });
766
+ if (!result.ok) {
767
+ logger.error(result.reason);
768
+ process.exitCode = 1;
769
+ return;
770
+ }
771
+ const expiresIn = Math.max(0, Math.round((result.tokens.expiresAt - Date.now()) / 1000));
772
+ logger.success(`Authorized '${name}' — access token valid ~${expiresIn}s, refresh token ${result.tokens.refreshToken ? 'stored' : 'NOT issued (a new authorization will be needed when it expires)'}.`);
773
+ logger.info('Connect with `nuvira mcp list`; the token is refreshed automatically from now on.');
774
+ }
353
775
  async serveTools(options) {
354
776
  try {
355
- const { serveStdio, AGENT_MCP_TOOL_NAMES } = await import('../mcp/server.js');
777
+ const { serveStdio, AGENT_MCP_TOOL_NAMES, MEMORY_MCP_TOOL_NAMES } = await import('../mcp/server.js');
356
778
  const extraTools = (options.with || '').split(',').map((t) => t.trim()).filter(Boolean);
779
+ const includeMemory = options.memory === true;
780
+ const exposed = [...AGENT_MCP_TOOL_NAMES, ...(includeMemory ? MEMORY_MCP_TOOL_NAMES : []), ...extraTools];
357
781
  // CRITICAL: the MCP stdio protocol owns stdout — every byte written
358
782
  // there must be a protocol frame, or clients (MCP hosts, IDEs)
359
783
  // fail their initialize handshake. Route ALL human output to stderr.
@@ -362,14 +786,14 @@ Configured via JSON files in: ~/${MCP_CONFIG_DIR}/
362
786
  banner(' 🖥️ Agent-Nuvira MCP Server (stdio)');
363
787
  banner('═'.repeat(60));
364
788
  banner('');
365
- banner(` Exposing ${AGENT_MCP_TOOL_NAMES.length + extraTools.length} tool(s):`);
366
- for (const name of [...AGENT_MCP_TOOL_NAMES, ...extraTools]) {
789
+ banner(` Exposing ${exposed.length} tool(s)${includeMemory ? ' (memory surface ON — the client can read and write this agent\'s memory)' : ''}:`);
790
+ for (const name of exposed) {
367
791
  banner(` • ${name}`);
368
792
  }
369
793
  banner('');
370
794
  banner(' Waiting for MCP client on stdin/stdout... (Ctrl+C to stop)');
371
795
  banner('');
372
- await serveStdio({ extraTools });
796
+ await serveStdio({ extraTools, ...(includeMemory ? { includeMemory: true } : {}) });
373
797
  }
374
798
  catch (err) {
375
799
  // logger.error goes to stderr — safe here.
@@ -411,4 +835,27 @@ function collectEnv(value, previous) {
411
835
  }
412
836
  return { ...previous, [value.slice(0, idx)]: value.slice(idx + 1) };
413
837
  }
838
+ /** Collect repeatable `--arg` options in the order they were written. */
839
+ function collectArg(value, previous) {
840
+ return [...previous, value];
841
+ }
842
+ /**
843
+ * Collect repeatable `--header "Name: value"` options into a map.
844
+ *
845
+ * The first colon splits name from value (a URL value contains colons of its
846
+ * own), and a value may be empty — `--header "X-Empty:"` is a valid header.
847
+ */
848
+ function collectHeader(value, previous) {
849
+ const idx = value.indexOf(':');
850
+ if (idx <= 0) {
851
+ logger.warn(`Ignoring malformed --header '${value}' (expected "Name: value").`);
852
+ return previous;
853
+ }
854
+ const name = value.slice(0, idx).trim();
855
+ if (!name) {
856
+ logger.warn(`Ignoring malformed --header '${value}' (empty header name).`);
857
+ return previous;
858
+ }
859
+ return { ...previous, [name]: value.slice(idx + 1).trim() };
860
+ }
414
861
  //# sourceMappingURL=mcp.js.map