@akon-labs/nexus-cli 0.1.10 → 0.1.11
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/cli/require-login.d.ts.map +1 -1
- package/dist/cli/require-login.js +2 -2
- package/dist/cli/require-login.js.map +1 -1
- package/dist/cli.js +2492 -1088
- package/dist/core/agent-session.d.ts +3 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +7 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +14 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +18 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/graph-rail-store.d.ts +10 -0
- package/dist/core/graph-rail-store.d.ts.map +1 -1
- package/dist/core/graph-rail-store.js +9 -0
- package/dist/core/graph-rail-store.js.map +1 -1
- package/dist/core/keybindings.d.ts +2 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +4 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/extensions/gitnexus/client.d.ts.map +1 -1
- package/dist/extensions/gitnexus/client.js +45 -14
- package/dist/extensions/gitnexus/client.js.map +1 -1
- package/dist/extensions/gitnexus/config.d.ts +15 -1
- package/dist/extensions/gitnexus/config.d.ts.map +1 -1
- package/dist/extensions/gitnexus/config.js +86 -2
- package/dist/extensions/gitnexus/config.js.map +1 -1
- package/dist/extensions/gitnexus/hub-api.d.ts +56 -0
- package/dist/extensions/gitnexus/hub-api.d.ts.map +1 -0
- package/dist/extensions/gitnexus/hub-api.js +105 -0
- package/dist/extensions/gitnexus/hub-api.js.map +1 -0
- package/dist/extensions/gitnexus/index-repo.d.ts +50 -0
- package/dist/extensions/gitnexus/index-repo.d.ts.map +1 -0
- package/dist/extensions/gitnexus/index-repo.js +162 -0
- package/dist/extensions/gitnexus/index-repo.js.map +1 -0
- package/dist/extensions/gitnexus/index.d.ts.map +1 -1
- package/dist/extensions/gitnexus/index.js +137 -17
- package/dist/extensions/gitnexus/index.js.map +1 -1
- package/dist/extensions/gitnexus/observatory.d.ts.map +1 -1
- package/dist/extensions/gitnexus/observatory.js +2 -2
- package/dist/extensions/gitnexus/observatory.js.map +1 -1
- package/dist/extensions/gitnexus/resolve.d.ts +9 -0
- package/dist/extensions/gitnexus/resolve.d.ts.map +1 -1
- package/dist/extensions/gitnexus/resolve.js +52 -2
- package/dist/extensions/gitnexus/resolve.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +3 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/mcp/commands.d.ts +22 -2
- package/dist/extensions/mcp/commands.d.ts.map +1 -1
- package/dist/extensions/mcp/commands.js +201 -9
- package/dist/extensions/mcp/commands.js.map +1 -1
- package/dist/extensions/mcp/config.d.ts +2 -0
- package/dist/extensions/mcp/config.d.ts.map +1 -1
- package/dist/extensions/mcp/config.js +6 -1
- package/dist/extensions/mcp/config.js.map +1 -1
- package/dist/extensions/mcp/index.d.ts.map +1 -1
- package/dist/extensions/mcp/index.js +54 -5
- package/dist/extensions/mcp/index.js.map +1 -1
- package/dist/extensions/mcp/mcp-setup-panel.d.ts +63 -2
- package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -1
- package/dist/extensions/mcp/mcp-setup-panel.js +563 -77
- package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts +2 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +19 -6
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent-render.d.ts +3 -0
- package/dist/extensions/subagent-render.d.ts.map +1 -0
- package/dist/extensions/subagent-render.js +39 -0
- package/dist/extensions/subagent-render.js.map +1 -0
- package/dist/index.js +2524 -1120
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/command-palette.d.ts +2 -1
- package/dist/modes/interactive/components/command-palette.d.ts.map +1 -1
- package/dist/modes/interactive/components/command-palette.js +11 -4
- package/dist/modes/interactive/components/command-palette.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +9 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/graph-rail.d.ts.map +1 -1
- package/dist/modes/interactive/components/graph-rail.js +18 -3
- package/dist/modes/interactive/components/graph-rail.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +7 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/landing-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/landing-panel.js +15 -2
- package/dist/modes/interactive/components/landing-panel.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +3 -2
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +42 -8
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/rpc-entry.js +2492 -1088
- package/dist/utils/hidden-spawn.d.ts +29 -0
- package/dist/utils/hidden-spawn.d.ts.map +1 -0
- package/dist/utils/hidden-spawn.js +101 -0
- package/dist/utils/hidden-spawn.js.map +1 -0
- package/dist/vendor/ai.js +233 -233
- package/dist/vendor/tui.js +1 -1
- package/native/win32/build.mjs +95 -0
- package/native/win32/prebuilds/win32-x64/hidden-spawn.exe +0 -0
- package/native/win32/src/hidden-spawn.c +157 -0
- package/npm-shrinkwrap.json +2 -37
- package/package.json +7 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\n\n/** One indexed repo as reported by the hub's list_repos tool. */\nexport interface IndexedRepo {\n\tname: string;\n\tsymbols: string;\n\trelationships: string;\n\tindexedAt: string;\n}\n\nexport interface IndexedGroup {\n\tname: string;\n\trepoCount: string;\n}\n\nexport interface RepoInventory {\n\trepos: IndexedRepo[];\n\tgroups: IndexedGroup[];\n}\n\nexport interface GitHubRemote {\n\tname: string;\n\tfullName: string;\n\tcloneUrl: string;\n}\n\n/** Parse the markdown returned by the hub list_repos tool. */\nexport function parseRepoInventory(markdown: string): RepoInventory {\n\tconst repos: IndexedRepo[] = [];\n\tconst groups: IndexedGroup[] = [];\n\tconst repoRe = /^- \\*\\*([^*]+)\\*\\* \\(([\\d,]+) symbols, ([\\d,]+) relationships\\) — indexed (.+)$/;\n\tconst groupRe = /^- \\*\\*(@[^*]+)\\*\\* \\((\\d+) repos?/;\n\tfor (const line of markdown.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tconst repoMatch = repoRe.exec(trimmed);\n\t\tif (repoMatch?.[1] && repoMatch[2] && repoMatch[3] && repoMatch[4]) {\n\t\t\trepos.push({\n\t\t\t\tname: repoMatch[1],\n\t\t\t\tsymbols: repoMatch[2],\n\t\t\t\trelationships: repoMatch[3],\n\t\t\t\tindexedAt: repoMatch[4],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst groupMatch = groupRe.exec(trimmed);\n\t\tif (groupMatch?.[1] && groupMatch[2]) {\n\t\t\tgroups.push({ name: groupMatch[1], repoCount: groupMatch[2] });\n\t\t}\n\t}\n\treturn { repos, groups };\n}\n\nfunction gitOriginUrl(cwd: string): string | undefined {\n\tconst result = spawnSync(\"git\", [\"remote\", \"get-url\", \"origin\"], {\n\t\tcwd,\n\t\tencoding: \"utf8\",\n\t\ttimeout: 3000,\n\t});\n\tif (result.status !== 0) return undefined;\n\tconst url = (result.stdout ?? \"\").trim();\n\treturn url || undefined;\n}\n\n/** Parse a supported GitHub origin without preserving credentials or URL parameters. */\nexport function parseGitHubRemote(remoteUrl: string): GitHubRemote | undefined {\n\tif (!remoteUrl || /\\s/.test(remoteUrl)) return undefined;\n\n\tconst scpMatch = /^git@github\\.com:([^/]+)\\/([^/]+)$/.exec(remoteUrl);\n\tif (scpMatch?.[1] && scpMatch[2]) return normalizeGitHubRemote(scpMatch[1], scpMatch[2]);\n\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(remoteUrl);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed.hostname !== \"github.com\" || parsed.port || parsed.search || parsed.hash) return undefined;\n\tif (parsed.protocol === \"https:\") {\n\t\tif (parsed.username || parsed.password) return undefined;\n\t} else if (parsed.protocol === \"ssh:\") {\n\t\tif (parsed.username !== \"git\" || parsed.password) return undefined;\n\t} else {\n\t\treturn undefined;\n\t}\n\n\tconst pathMatch = /^\\/([^/]+)\\/([^/]+)$/.exec(parsed.pathname);\n\tif (!pathMatch?.[1] || !pathMatch[2]) return undefined;\n\treturn normalizeGitHubRemote(pathMatch[1], pathMatch[2]);\n}\n\nfunction normalizeGitHubRemote(owner: string, rawName: string): GitHubRemote | undefined {\n\tconst name = rawName.endsWith(\".git\") ? rawName.slice(0, -4) : rawName;\n\tif (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner)) return undefined;\n\tif (name.length > 100 || !/^[A-Za-z0-9._-]+$/.test(name) || name === \".\" || name === \"..\") return undefined;\n\tconst fullName = `${owner}/${name}`;\n\treturn { name, fullName, cloneUrl: `https://github.com/${fullName}.git` };\n}\n\n/** Resolve the current repository's origin as a canonical GitHub remote. */\nexport function currentGitHubRemote(cwd: string): GitHubRemote | undefined {\n\tconst remoteUrl = gitOriginUrl(cwd);\n\treturn remoteUrl ? parseGitHubRemote(remoteUrl) : undefined;\n}\n\n/** Read the git remote of cwd and normalize to \"owner/name\" (undefined outside a repo). */\nexport function gitRemoteSlug(cwd: string): string | undefined {\n\tconst url = gitOriginUrl(cwd);\n\tif (!url) return undefined;\n\t// https://github.com/owner/name(.git) | git@github.com:owner/name(.git)\n\tconst match = /[/:]([^/:]+\\/[^/:]+?)(?:\\.git)?$/.exec(url);\n\treturn match?.[1];\n}\n\n/** Match the cwd's remote against the hub inventory. Exact slug first, then name suffix. */\nexport function matchCurrentRepo(inventory: RepoInventory, slug: string | undefined): IndexedRepo | undefined {\n\tif (!slug) return undefined;\n\tconst lower = slug.toLowerCase();\n\tconst exact = inventory.repos.find((r) => r.name.toLowerCase() === lower);\n\tif (exact) return exact;\n\tconst name = lower.split(\"/\")[1] ?? lower;\n\treturn inventory.repos.find((r) => r.name.toLowerCase().endsWith(`/${name}`));\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AASnE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAQ9C,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport subagentRenderExtension from \"./subagent-render.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t// Display-only override for bundled pi-subagents tools; harmless no-op if that package isn't loaded.\n\t{ name: \"subagent-render\", factory: subagentRenderExtension, hidden: true },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
package/dist/extensions/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import gitnexusExtension from "./gitnexus/index.js";
|
|
2
2
|
import mcpExtension from "./mcp/index.js";
|
|
3
3
|
import subagentExtension from "./subagent/index.js";
|
|
4
|
+
import subagentRenderExtension from "./subagent-render.js";
|
|
4
5
|
import webSearchConcurrencyExtension from "./web-search-concurrency.js";
|
|
5
6
|
// llama.cpp support lives in ./llama and is kept in tree but not registered.
|
|
6
7
|
// Re-add `{ name: "llama.cpp", factory: llamaExtension, hidden: true }` to turn it back on.
|
|
@@ -9,6 +10,8 @@ export const builtInExtensions = [
|
|
|
9
10
|
{ name: "delegated-worker-runtime", factory: subagentExtension, hidden: true },
|
|
10
11
|
// No-op until the user writes an mcp.json; connecting nothing costs nothing.
|
|
11
12
|
{ name: "mcp", factory: mcpExtension },
|
|
13
|
+
// Display-only override for bundled pi-subagents tools; harmless no-op if that package isn't loaded.
|
|
14
|
+
{ name: "subagent-render", factory: subagentRenderExtension, hidden: true },
|
|
12
15
|
{ name: "web-search-concurrency", factory: webSearchConcurrencyExtension },
|
|
13
16
|
];
|
|
14
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,6BAA6B,MAAM,6BAA6B,CAAC;AAExE,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC9E,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;CAC1E,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,uBAAuB,MAAM,sBAAsB,CAAC;AAC3D,OAAO,6BAA6B,MAAM,6BAA6B,CAAC;AAExE,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC9E,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,qGAAqG;IACrG,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC3E,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;CAC1E,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport subagentRenderExtension from \"./subagent-render.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t// Display-only override for bundled pi-subagents tools; harmless no-op if that package isn't loaded.\n\t{ name: \"subagent-render\", factory: subagentRenderExtension, hidden: true },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "../../core/extensions/types.ts";
|
|
2
2
|
import { type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
3
3
|
import type { McpExtensionState } from "./state.ts";
|
|
4
|
-
import { type McpAuthResult, type McpConfig } from "./types.ts";
|
|
4
|
+
import { type McpAuthResult, type McpConfig, type ServerEntry } from "./types.ts";
|
|
5
5
|
export declare function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void>;
|
|
6
6
|
export declare function showPrompts(state: McpExtensionState, ctx: ExtensionContext): Promise<void>;
|
|
7
7
|
export declare function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void>;
|
|
8
8
|
export declare function reconnectServer(state: McpExtensionState, ctx: ExtensionContext, name: string): Promise<boolean>;
|
|
9
9
|
export declare function reconnectServers(state: McpExtensionState, ctx: ExtensionContext, targetServer?: string): Promise<void>;
|
|
10
|
-
export declare function authenticateServer(serverName: string, config: McpConfig, ctx: ExtensionContext, signal?: AbortSignal, runtime?: McpOAuthRuntime): Promise<McpAuthResult>;
|
|
10
|
+
export declare function authenticateServer(serverName: string, config: McpConfig, ctx: ExtensionContext, signal?: AbortSignal, runtime?: McpOAuthRuntime, nexus?: ExtensionAPI): Promise<McpAuthResult>;
|
|
11
11
|
export declare function logoutServer(serverName: string, state: McpExtensionState, ctx: ExtensionContext): Promise<{
|
|
12
12
|
ok: boolean;
|
|
13
13
|
message: string;
|
|
@@ -16,6 +16,25 @@ export declare function manageBearerToken(action: "set" | "remove" | "status", s
|
|
|
16
16
|
ok: boolean;
|
|
17
17
|
message: string;
|
|
18
18
|
}>;
|
|
19
|
+
type ParsedAddServer = {
|
|
20
|
+
ok: true;
|
|
21
|
+
name: string;
|
|
22
|
+
entry: ServerEntry;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parses `/mcp add <name> ...` arguments. Mirrors the rest of /mcp's naive
|
|
29
|
+
* whitespace-split argument handling (no quoting support) rather than
|
|
30
|
+
* inventing a shell-quote parser just for this subcommand.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseAddServerArgs(argv: string[]): ParsedAddServer;
|
|
33
|
+
export declare const ADD_SERVER_USAGE: string;
|
|
34
|
+
/** Adds a server to the project's shared MCP config. Reused by both `/mcp add` and any UI entry point. */
|
|
35
|
+
export declare function addCustomServer(cwd: string, name: string, entry: ServerEntry): {
|
|
36
|
+
path: string;
|
|
37
|
+
};
|
|
19
38
|
export interface PanelFlowResult {
|
|
20
39
|
configChanged: boolean;
|
|
21
40
|
}
|
|
@@ -24,4 +43,5 @@ export declare function openMcpSetup(state: McpExtensionState, nexus: ExtensionA
|
|
|
24
43
|
}): Promise<PanelFlowResult>;
|
|
25
44
|
export declare function openMcpPanel(state: McpExtensionState, nexus: ExtensionAPI, ctx: ExtensionContext, configOverridePath?: string, onDirectToolsConfigChanged?: (changes: Map<string, true | string[] | false>) => void | Promise<void>): Promise<PanelFlowResult>;
|
|
26
45
|
export declare function openMcpAuthPanel(state: McpExtensionState, nexus: ExtensionAPI, ctx: ExtensionContext, configOverridePath?: string): Promise<PanelFlowResult>;
|
|
46
|
+
export {};
|
|
27
47
|
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AA0BrF,OAAO,EAAgB,KAAK,eAAe,EAA6B,MAAM,oBAAoB,CAAC;AAWnG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EAGd,MAAM,YAAY,CAAC;AAoBpB,wBAAsB,UAAU,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4C/F;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyChG;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe9F;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA4ErH;AAED,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,EACrB,YAAY,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,kBAAkB,CACvC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,gBAAgB,EACrB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,aAAa,CAAC,CAyExB;AAED,wBAAsB,YAAY,CACjC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,GACnB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA8C3C;AA8BD,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,EACnC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,GACnB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAwC3C;AAED,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,OAAO,CAAC;CACvB;AAuBD,wBAAsB,YAAY,CACjC,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,gBAAgB,EACrB,kBAAkB,CAAC,EAAE,MAAM,EAC3B,IAAI,GAAE,OAAO,GAAG,OAAiB,EACjC,OAAO,GAAE;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5C,OAAO,CAAC,eAAe,CAAC,CAuE1B;AAqED,wBAAsB,YAAY,CACjC,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,gBAAgB,EACrB,kBAAkB,CAAC,EAAE,MAAM,EAC3B,0BAA0B,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAClG,OAAO,CAAC,eAAe,CAAC,CA4E1B;AAED,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,gBAAgB,EACrB,kBAAkB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,eAAe,CAAC,CA2D1B","sourcesContent":["import type { OverlayHandle } from \"@earendil-works/pi-tui\";\nimport type { ExtensionAPI, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport {\n\tensureCompatibilityImports,\n\tgetMcpDiscoverySummary,\n\tgetMcpStandardConfigSummary,\n\tgetProjectConfigPath,\n\tgetServerProvenance,\n\ttype KnownServerPreset,\n\tpreviewCompatibilityImports,\n\tpreviewSharedServerEntry,\n\tpreviewStarterProjectConfig,\n\twriteDirectToolsConfig,\n\twriteSharedServerEntry,\n\twriteStarterProjectConfig,\n} from \"./config.ts\";\nimport {\n\tclearFailure,\n\tgetFailureAgeSeconds,\n\tgetFailureMessage,\n\tmarkKeepAliveAfterConnect,\n\tnotifyToolMetadataUpdated,\n\trecordFailure,\n\tupdateMetadataCache,\n\tupdateStatusBar,\n} from \"./init.ts\";\nimport { getAuthStorageOptions, inspectAuthForUrl } from \"./mcp-auth.ts\";\nimport { authenticate, type McpOAuthRuntime, removeAuth, supportsOAuth } from \"./mcp-auth-flow.ts\";\nimport { inspectBearerTokenForUrl, removeBearerToken } from \"./mcp-bearer-store.ts\";\nimport { createMcpPanel } from \"./mcp-panel.ts\";\nimport { createMcpSetupPanel } from \"./mcp-setup-panel.ts\";\nimport { loadMetadataCache, reconstructPromptMetadata } from \"./metadata-cache.ts\";\nimport {\n\tloadOnboardingState,\n\tmarkSharedConfigHintShown,\n\tmarkSetupCompleted as persistSetupCompleted,\n} from \"./onboarding-state.ts\";\nimport { isAbortError } from \"./runtime-owner.ts\";\nimport type { McpExtensionState } from \"./state.ts\";\nimport { buildToolMetadata } from \"./tool-metadata.ts\";\nimport {\n\ttype ImportKind,\n\tisServerDisabled,\n\ttype McpAuthResult,\n\ttype McpConfig,\n\ttype McpPanelCallbacks,\n\ttype McpPanelResult,\n} from \"./types.ts\";\nimport { openPath, resolveServerUrl, sanitizeTerminalText } from \"./utils.ts\";\n\nfunction terminalHyperlink(label: string, url: string): string {\n\treturn `\\u001B]8;;${sanitizeTerminalText(url)}\\u001B\\\\${sanitizeTerminalText(label)}\\u001B]8;;\\u001B\\\\`;\n}\n\n/**\n * True when this run mode can display a `ctx.ui.custom()` overlay.\n *\n * `ctx.hasUI` only reports that *some* UI context is bound. In rpc/print mode\n * that context is a headless stub whose `custom()` returns immediately without\n * ever invoking the factory or the `done` callback, so a panel awaited through\n * `new Promise(resolve => ctx.ui.custom(...))` never settles and the command\n * hangs. Overlay panels therefore additionally require `ctx.mode === \"tui\"`.\n */\nfunction canRenderPanel(ctx: ExtensionContext): boolean {\n\treturn ctx.hasUI && ctx.mode === \"tui\";\n}\n\nexport async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst lines: string[] = [\"MCP Server Status:\", \"\"];\n\n\tfor (const name of Object.keys(state.config.mcpServers)) {\n\t\tconst definition = state.config.mcpServers[name];\n\t\tif (isServerDisabled(definition)) {\n\t\t\tlines.push(`⊘ ${name}: disabled (run /mcp enable ${name}, then /reload)`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst connection = state.manager.getConnection(name);\n\t\tconst metadata = state.toolMetadata.get(name);\n\t\tconst toolCount = metadata?.length ?? 0;\n\t\tconst failedAgo = getFailureAgeSeconds(state, name);\n\t\tlet status = \"not connected\";\n\t\tlet statusIcon = \"○\";\n\t\tlet failed = false;\n\n\t\tif (connection?.status === \"connected\") {\n\t\t\tstatus = \"connected\";\n\t\t\tstatusIcon = \"✓\";\n\t\t} else if (connection?.status === \"needs-auth\") {\n\t\t\tstatus = \"needs auth\";\n\t\t\tstatusIcon = \"⚠\";\n\t\t} else if (failedAgo !== null) {\n\t\t\tconst reason = sanitizeTerminalText(getFailureMessage(state, name) ?? \"\");\n\t\t\tstatus = reason ? `failed ${failedAgo}s ago — ${reason}` : `failed ${failedAgo}s ago`;\n\t\t\tstatusIcon = \"✗\";\n\t\t\tfailed = true;\n\t\t} else if (metadata !== undefined) {\n\t\t\tstatus = \"cached\";\n\t\t}\n\n\t\tconst toolSuffix = failed ? \"\" : ` (${toolCount} tools${status === \"cached\" ? \", cached\" : \"\"})`;\n\t\tlines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);\n\t}\n\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\tlines.push(\"No MCP servers configured\");\n\t\tlines.push(\"Run /mcp setup to adopt imports or scaffold .nexus/mcp.json\");\n\t}\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showPrompts(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\tconst allPrompts = [...(state.promptMetadata?.values() ?? [])].flat();\n\tconst failedPromptServers = [...(state.manager.getAllConnections?.() ?? [])]\n\t\t.filter(([, connection]) => connection.status === \"connected\" && connection.promptDiscoveryFailed)\n\t\t.map(([serverName]) => serverName)\n\t\t.sort();\n\tif (allPrompts.length === 0) {\n\t\tconst failureNote =\n\t\t\tfailedPromptServers.length > 0 ? ` Prompt discovery failed for: ${failedPromptServers.join(\", \")}.` : \"\";\n\t\tctx.ui.notify(\n\t\t\t`No MCP prompts available. Prompts are discovered when servers with the \\`prompts\\` capability connect.${failureNote}`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn;\n\t}\n\tconst lines = [\"MCP Prompts:\", \"\"];\n\tconst grouped = new Map<string, typeof allPrompts>();\n\tfor (const prompt of allPrompts) {\n\t\tconst list = grouped.get(prompt.serverName) ?? [];\n\t\tlist.push(prompt);\n\t\tgrouped.set(prompt.serverName, list);\n\t}\n\tfor (const [serverName, prompts] of [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n\t\tlines.push(`${serverName}:`);\n\t\tfor (const prompt of prompts.sort((a, b) => a.commandName.localeCompare(b.commandName))) {\n\t\t\tconst args = prompt.arguments\n\t\t\t\t.map((argument) => (argument.required ? `<${argument.name}>` : `[${argument.name}]`))\n\t\t\t\t.join(\" \");\n\t\t\tlines.push(` /${prompt.commandName}${args ? ` ${args}` : \"\"}`);\n\t\t\tif (prompt.description) lines.push(` ${prompt.description}`);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\tlines.push(`Total: ${allPrompts.length} prompt${allPrompts.length === 1 ? \"\" : \"s\"}`);\n\tif (failedPromptServers.length > 0) {\n\t\tlines.push(\n\t\t\t`Prompt discovery failed for: ${failedPromptServers.join(\", \")}. Cached prompt metadata may be stale.`,\n\t\t);\n\t}\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst allTools = [...state.toolMetadata.entries()]\n\t\t.filter(([serverName]) => !isServerDisabled(state.config.mcpServers[serverName]))\n\t\t.flatMap(([, metadata]) => metadata.map((m) => m.name));\n\n\tif (allTools.length === 0) {\n\t\tctx.ui.notify(\"No MCP tools available\", \"info\");\n\t\treturn;\n\t}\n\n\tconst lines = [\"MCP Tools:\", \"\", ...allTools.map((t) => ` ${t}`), \"\", `Total: ${allTools.length} tools`];\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function reconnectServer(state: McpExtensionState, ctx: ExtensionContext, name: string): Promise<boolean> {\n\tconst definition = state.config.mcpServers[name];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst signal = state.owner?.signal;\n\tif (!definition) {\n\t\tif (ui) {\n\t\t\tui.notify(`Server \"${name}\" not found in config`, \"error\");\n\t\t}\n\t\treturn false;\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tif (ui) ui.notify(`MCP: ${name} is disabled. Run /mcp enable ${name}, then /reload.`, \"warning\");\n\t\treturn false;\n\t}\n\n\ttry {\n\t\tawait state.manager.close(name);\n\t\tstate.owner?.throwIfInactive();\n\t\tconst connection = signal\n\t\t\t? await state.manager.connect(name, definition, signal)\n\t\t\t: await state.manager.connect(name, definition);\n\t\tstate.owner?.throwIfInactive();\n\t\tif (connection.status === \"needs-auth\") {\n\t\t\tif (ui) {\n\t\t\t\tui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, \"warning\");\n\t\t\t}\n\t\t\tupdateStatusBar(state);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst prefix = state.config.settings?.toolPrefix ?? \"server\";\n\t\tconst { metadata, failedTools } = buildToolMetadata(\n\t\t\tconnection.tools,\n\t\t\tconnection.resources,\n\t\t\tdefinition,\n\t\t\tname,\n\t\t\tprefix,\n\t\t\tstate.config.mcpServers,\n\t\t\tstate.toolMetadata,\n\t\t);\n\t\tstate.toolMetadata.set(name, metadata);\n\t\tif (!connection.promptDiscoveryFailed) {\n\t\t\tstate.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));\n\t\t\tstate.promptMetadataLive?.add(name);\n\t\t}\n\t\tif (connection.instructions) {\n\t\t\tstate.serverInstructions.set(name, connection.instructions);\n\t\t} else {\n\t\t\tstate.serverInstructions.delete(name);\n\t\t}\n\t\tupdateMetadataCache(state, name);\n\t\tnotifyToolMetadataUpdated(state, name, \"command-reconnect\");\n\t\tmarkKeepAliveAfterConnect(state, name);\n\t\tclearFailure(state, name);\n\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tif (failedTools.length > 0) {\n\t\t\t\tui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, \"warning\");\n\t\t\t}\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (isAbortError(error, signal)) throw error;\n\t\trecordFailure(state, name, message);\n\t\tif (ui) {\n\t\t\tui.notify(`MCP: Failed to reconnect to ${name}: ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn false;\n\t}\n}\n\nexport async function reconnectServers(\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n\ttargetServer?: string,\n): Promise<void> {\n\tif (targetServer && !state.config.mcpServers[targetServer]) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(`Server \"${targetServer}\" not found in config`, \"error\");\n\t\t}\n\t\treturn;\n\t}\n\n\tconst names = targetServer ? [targetServer] : Object.keys(state.config.mcpServers);\n\tfor (const name of names) {\n\t\tawait reconnectServer(state, ctx, name);\n\t}\n\n\tupdateStatusBar(state);\n}\n\nexport async function authenticateServer(\n\tserverName: string,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tsignal?: AbortSignal,\n\truntime?: McpOAuthRuntime,\n): Promise<McpAuthResult> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst cwd = ctx.cwd;\n\tsignal ??= ctx.signal;\n\tif (!ui) return { ok: false, message: \"OAuth authentication requires an interactive session.\" };\n\n\tconst definition = config.mcpServers[serverName];\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tconst message = `Server \"${serverName}\" is disabled. Run /mcp enable ${serverName}, then /reload.`;\n\t\tui.notify(message, \"warning\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (!supportsOAuth(definition)) {\n\t\tconst message = `Server \"${serverName}\" does not use OAuth authentication. Set \"auth\": \"oauth\" or omit auth for auto-detection.`;\n\t\tui.notify(\n\t\t\t`Server \"${serverName}\" does not use OAuth authentication.\\n` +\n\t\t\t\t`Set \"auth\": \"oauth\" or omit auth for auto-detection.`,\n\t\t\t\"error\",\n\t\t);\n\t\treturn { ok: false, message };\n\t}\n\n\ttry {\n\t\tconst serverUrl = resolveServerUrl(definition);\n\t\tif (!serverUrl) {\n\t\t\tconst message = `Server \"${serverName}\" has no URL configured (OAuth requires HTTP transport)`;\n\t\t\tui.notify(message, \"error\");\n\t\t\treturn { ok: false, message };\n\t\t}\n\n\t\tui.setStatus(\"mcp-auth\", `Authenticating ${serverName}...`);\n\t\tconst authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);\n\t\tconst status = await authenticate(serverName, serverUrl, definition, {\n\t\t\t...(authStorageOptions.baseDir ? { authStorageOptions } : {}),\n\t\t\tonAuthorizationUrl: () => {},\n\t\t\tonAuthorizationInput: async (authorizationUrl, inputSignal) => {\n\t\t\t\tif (inputSignal.aborted) return undefined;\n\t\t\t\treturn ui.input(\n\t\t\t\t\t`Complete ${serverName} OAuth\\n\\n` +\n\t\t\t\t\t\t`${terminalHyperlink(\"Open authorization page\", authorizationUrl)}\\n${authorizationUrl}\\n\\n` +\n\t\t\t\t\t\t\"Approve access, then paste the full localhost callback URL below.\",\n\t\t\t\t\tundefined,\n\t\t\t\t\t{ signal: inputSignal },\n\t\t\t\t);\n\t\t\t},\n\t\t\t...(signal ? { signal } : {}),\n\t\t\t...(runtime ? { runtime } : {}),\n\t\t});\n\t\tif (signal?.aborted) signal.throwIfAborted();\n\n\t\tif (status === \"authenticated\") {\n\t\t\tconst message = `OAuth authentication successful for \"${serverName}\".`;\n\t\t\tui.notify(message, \"info\");\n\t\t\treturn { ok: true, message };\n\t\t}\n\n\t\tconst message = `OAuth authentication failed for \"${serverName}\".`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t} catch (error) {\n\t\tif (signal?.aborted) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tui.notify(`Failed to authenticate \"${serverName}\": ${message}`, \"error\");\n\t\treturn { ok: false, message };\n\t} finally {\n\t\tif (!signal?.aborted) ui.setStatus(\"mcp-auth\", undefined);\n\t}\n}\n\nexport async function logoutServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst definition = state.config.mcpServers[serverName];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst signal = state.owner?.signal;\n\ttry {\n\t\tawait removeAuth(serverName, {\n\t\t\tauthStorageOptions: state.authStorageOptions,\n\t\t\tsignal,\n\t\t\truntime: state.oauthRuntime,\n\t\t});\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(`Failed to clear OAuth credentials for \"${serverName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\ttry {\n\t\tawait state.manager.close(serverName);\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`OAuth credentials were cleared for \"${serverName}\", but its connection could not be closed: ${sanitizeTerminalText(message)}`,\n\t\t\t\t\"error\",\n\t\t\t);\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\tupdateStatusBar(state);\n\n\tconst message = `OAuth credentials cleared for \"${serverName}\". Run /mcp-auth ${serverName} to authenticate again.`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\nfunction validateBearerTokenStoreServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n): { ok: true; serverUrl: string } | { ok: false; message: string; type: \"error\" | \"warning\" } {\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst definition = state.config.mcpServers[serverName];\n\tif (!definition) return { ok: false, message: `Server \"${safeName}\" not found in config`, type: \"error\" };\n\tif (isServerDisabled(definition))\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Server \"${safeName}\" is disabled. Run /mcp enable ${safeName}, then /reload.`,\n\t\t\ttype: \"warning\",\n\t\t};\n\tif (definition.auth !== \"bearer\" || definition.bearerTokenStore !== true) {\n\t\treturn { ok: false, message: `Server \"${safeName}\" is not configured for bearerTokenStore.`, type: \"error\" };\n\t}\n\t// resolveServerUrl embeds the interpolated URL in its exceptions; redact it\n\t// because the URL can carry userinfo or interpolated secrets.\n\tlet serverUrl: string | undefined;\n\ttry {\n\t\tserverUrl = resolveServerUrl(definition);\n\t} catch {\n\t\treturn { ok: false, message: `Server \"${safeName}\" has an invalid or unresolvable URL.`, type: \"error\" };\n\t}\n\tif (!serverUrl) return { ok: false, message: `Server \"${safeName}\" has no URL configured.`, type: \"error\" };\n\treturn { ok: true, serverUrl };\n}\n\nexport async function manageBearerToken(\n\taction: \"set\" | \"remove\" | \"status\",\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst validation = validateBearerTokenStoreServer(serverName, state);\n\tif (!validation.ok) {\n\t\tif (ui) ui.notify(validation.message, validation.type);\n\t\treturn { ok: false, message: validation.message };\n\t}\n\n\tif (action === \"set\") {\n\t\tconst message = `Nexus will not collect bearer tokens through an unmasked prompt. Configure bearerTokenEnv for \"${safeName}\" in MCP config, then reload.`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (action === \"status\") {\n\t\tconst status = inspectBearerTokenForUrl(serverName, validation.serverUrl);\n\t\tconst message =\n\t\t\tstatus.status === \"present\"\n\t\t\t\t? `Bearer token is stored for \"${safeName}\".`\n\t\t\t\t: status.status === \"url-mismatch\"\n\t\t\t\t\t? `Bearer token is stored for \"${safeName}\", but its URL does not match the current server URL.`\n\t\t\t\t\t: status.status === \"unavailable\"\n\t\t\t\t\t\t? status.message\n\t\t\t\t\t\t: `No bearer token is stored for \"${safeName}\".`;\n\t\tif (ui) ui.notify(message, status.status === \"unavailable\" ? \"error\" : \"info\");\n\t\treturn { ok: status.status !== \"unavailable\", message };\n\t}\n\n\ttry {\n\t\tremoveBearerToken(serverName);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) ui.notify(`Failed to remove bearer token for \"${safeName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst message = `Bearer token removed for \"${safeName}\".`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\nexport interface PanelFlowResult {\n\tconfigChanged: boolean;\n}\n\nfunction buildSharedConfigNoticeLines(\n\tconfigOverridePath: string | undefined,\n\tcwd: string,\n): { lines: string[]; fingerprint: string | null } {\n\tconst discovery = getMcpStandardConfigSummary(configOverridePath, cwd);\n\tconst onboardingState = loadOnboardingState();\n\tif (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {\n\t\treturn { lines: [], fingerprint: null };\n\t}\n\n\tconst sharedSources = discovery.sources.filter((source) => source.kind === \"shared\" && source.serverCount > 0);\n\tconst sourceList = sharedSources.map((source) => source.path).join(\", \");\n\treturn {\n\t\tlines: [\n\t\t\t`Using MCP configuration from ${sourceList}.`,\n\t\t\t\"Nexus writes compatibility imports and MCP-specific overrides only to Nexus configuration files.\",\n\t\t],\n\t\tfingerprint: discovery.fingerprint,\n\t};\n}\n\nexport async function openMcpSetup(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tmode: \"empty\" | \"setup\" = \"setup\",\n\toptions: { includeHostConfigs?: boolean } = {},\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP setup panel is only available in the terminal UI (current mode: ${ctx.mode}). Edit .nexus/mcp.json directly, or run /mcp status to review servers.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"MCP setup is unavailable when config is supplied by createMcpAdapter().\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options);\n\tconst onboardingState = loadOnboardingState();\n\tlet configChanged = false;\n\n\tconst callbacks = {\n\t\tpreviewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),\n\t\tpreviewStarterProject: () => previewStarterProjectConfig(ctx.cwd),\n\t\tpreviewRepoPrompt: () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;\n\t\t\treturn previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t},\n\t\tpreviewKnownServer: (preset: KnownServerPreset) =>\n\t\t\tpreviewSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry),\n\t\tadoptImports: async (imports: ImportKind[]) => {\n\t\t\tconst result = ensureCompatibilityImports(imports, configOverridePath);\n\t\t\tif (result.added.length > 0) configChanged = true;\n\t\t\treturn result;\n\t\t},\n\t\tscaffoldProjectConfig: async () => {\n\t\t\tconst path = writeStarterProjectConfig(ctx.cwd);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path };\n\t\t},\n\t\taddRepoPrompt: async () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {\n\t\t\t\tthrow new Error(\"RepoPrompt is not available to add from this setup screen.\");\n\t\t\t}\n\t\t\tconst path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: repoPrompt.serverName };\n\t\t},\n\t\taddKnownServer: async (preset: KnownServerPreset) => {\n\t\t\tconst path = writeSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: preset.name };\n\t\t},\n\t\topenPath: async (targetPath: string) => {\n\t\t\tawait openPath(nexus, targetPath);\n\t\t},\n\t\tmarkSetupCompleted: () => {\n\t\t\tpersistSetupCompleted(discovery.fingerprint);\n\t\t},\n\t};\n\n\treturn new Promise<PanelFlowResult>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {\n\t\t\t\t\tdone(undefined);\n\t\t\t\t\tresolve({ configChanged });\n\t\t\t\t});\n\t\t\t},\n\t\t\t{ overlay: true, overlayOptions: { anchor: \"center\", width: 92 } },\n\t\t);\n\t});\n}\n\nfunction buildMcpPanelCallbacks(\n\tstate: McpExtensionState,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tgetOverlayHandle?: () => OverlayHandle | undefined,\n): McpPanelCallbacks {\n\t// Panel-only diagnostics keep status inspection from mutating connection\n\t// failure state while allowing the existing panel failure UI to show why the\n\t// credential store could not be inspected.\n\tconst authStatusFailures = new Map<string, string>();\n\n\treturn {\n\t\treconnect: (serverName: string) => reconnectServer(state, ctx, serverName),\n\t\tcanAuthenticate: (serverName: string) => {\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\treturn definition ? !isServerDisabled(definition) && supportsOAuth(definition) : false;\n\t\t},\n\t\tauthenticate: async (serverName: string) => {\n\t\t\tconst overlay = getOverlayHandle?.();\n\t\t\toverlay?.setHidden(true);\n\t\t\ttry {\n\t\t\t\treturn await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime);\n\t\t\t} finally {\n\t\t\t\toverlay?.setHidden(false);\n\t\t\t\toverlay?.focus();\n\t\t\t}\n\t\t},\n\t\tgetConnectionStatus: (serverName: string) => {\n\t\t\tauthStatusFailures.delete(serverName);\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\tif (isServerDisabled(definition)) return \"disabled\";\n\t\t\tconst connection = state.manager.getConnection(serverName);\n\t\t\tlet serverUrl: string | undefined;\n\t\t\ttry {\n\t\t\t\tserverUrl = definition ? resolveServerUrl(definition) : undefined;\n\t\t\t} catch {\n\t\t\t\treturn \"failed\";\n\t\t\t}\n\t\t\tif (\n\t\t\t\tdefinition?.auth === \"oauth\" &&\n\t\t\t\tserverUrl &&\n\t\t\t\tdefinition.oauth !== false &&\n\t\t\t\tdefinition.oauth?.grantType !== \"client_credentials\"\n\t\t\t) {\n\t\t\t\tconst authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);\n\t\t\t\tif (authStatus.status === \"unavailable\") {\n\t\t\t\t\tauthStatusFailures.set(serverName, authStatus.message);\n\t\t\t\t\treturn \"failed\";\n\t\t\t\t}\n\t\t\t\tif (authStatus.status === \"absent\" || !authStatus.entry.tokens) {\n\t\t\t\t\treturn \"needs-auth\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (connection?.status === \"needs-auth\") return \"needs-auth\";\n\t\t\tif (connection?.status === \"connected\") return \"connected\";\n\t\t\tif (getFailureAgeSeconds(state, serverName) !== null) return \"failed\";\n\t\t\treturn \"idle\";\n\t\t},\n\t\tgetFailureMessage: (serverName: string) =>\n\t\t\tauthStatusFailures.get(serverName) ?? getFailureMessage(state, serverName),\n\t\trefreshCacheAfterReconnect: (serverName: string) => {\n\t\t\tconst freshCache = loadMetadataCache();\n\t\t\treturn freshCache?.servers?.[serverName] ?? null;\n\t\t},\n\t};\n}\n\nexport async function openMcpPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tonDirectToolsConfigChanged?: (changes: Map<string, true | string[] | false>) => void | Promise<void>,\n): Promise<PanelFlowResult> {\n\tif (state.programmaticConfig) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(\n\t\t\t\t\"MCP status is shown from the in-memory SDK config; configuration discovery is unavailable.\",\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tawait showStatus(state, ctx);\n\t\t}\n\t\treturn { configChanged: false };\n\t}\n\tif (!canRenderPanel(ctx)) {\n\t\t// No overlay here, but the same information is available as text.\n\t\tawait showStatus(state, ctx);\n\t\treturn { configChanged: false };\n\t}\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\treturn openMcpSetup(state, nexus, ctx, configOverridePath, \"empty\", { includeHostConfigs: false });\n\t}\n\n\tconst config = state.config;\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tconst { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);\n\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);\n\n\tlet configChanged = false;\n\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t(result: McpPanelResult) => {\n\t\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\t\tif (!result.cancelled && result.changes.size > 0) {\n\t\t\t\t\t\t\t\twriteDirectToolsConfig(result.changes, provenanceMap);\n\t\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\t\tawait onDirectToolsConfigChanged?.(result.changes);\n\t\t\t\t\t\t\t\tctx.ui.notify(\"Direct tools updated for this session.\", \"info\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t})().catch((error) => {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tctx.ui.notify(`Direct tools updated, but live refresh failed: ${message}`, \"error\");\n\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{ noticeLines, keybindings },\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\tif (noticeLines.length > 0 && fingerprint) {\n\t\tmarkSharedConfigHintShown(fingerprint);\n\t}\n\n\treturn { configChanged };\n}\n\nexport async function openMcpAuthPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP auth panel is only available in the terminal UI (current mode: ${ctx.mode}). Use /mcp-auth <server> to authenticate a specific server.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst config = state.config;\n\tconst oauthServers = Object.entries(config.mcpServers).filter(\n\t\t([, definition]) => !isServerDisabled(definition) && supportsOAuth(definition),\n\t);\n\tif (oauthServers.length === 0) {\n\t\tctx.ui.notify(\"No OAuth-capable MCP servers are configured.\", \"warning\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t() => {\n\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tauthOnly: true,\n\t\t\t\t\t\tkeybindings,\n\t\t\t\t\t\tnoticeLines: [\"Select an OAuth MCP server and press Enter or ctrl+a to authenticate.\"],\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\treturn { configChanged: false };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/commands.ts"],"names":[],"mappings":"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","sourcesContent":["import type { OverlayHandle } from \"@earendil-works/pi-tui\";\nimport type { ExtensionAPI, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport {\n\tensureCompatibilityImports,\n\tgetMcpDiscoverySummary,\n\tgetMcpStandardConfigSummary,\n\tgetProjectConfigPath,\n\tgetServerProvenance,\n\ttype KnownServerPreset,\n\tpreviewCompatibilityImports,\n\tpreviewSharedServerEntry,\n\tpreviewStarterProjectConfig,\n\twriteDirectToolsConfig,\n\twriteProjectServerDisabledOverride,\n\twriteSharedServerEntry,\n\twriteStarterProjectConfig,\n} from \"./config.ts\";\nimport {\n\tclearFailure,\n\tgetFailureAgeSeconds,\n\tgetFailureMessage,\n\tmarkKeepAliveAfterConnect,\n\tnotifyToolMetadataUpdated,\n\trecordFailure,\n\tupdateMetadataCache,\n\tupdateStatusBar,\n} from \"./init.ts\";\nimport { getAuthStorageOptions, inspectAuthForUrl } from \"./mcp-auth.ts\";\nimport { authenticate, type McpOAuthRuntime, removeAuth, supportsOAuth } from \"./mcp-auth-flow.ts\";\nimport { inspectBearerTokenForUrl, removeBearerToken } from \"./mcp-bearer-store.ts\";\nimport { createMcpPanel } from \"./mcp-panel.ts\";\nimport { createMcpSetupPanel } from \"./mcp-setup-panel.ts\";\nimport { loadMetadataCache, reconstructPromptMetadata } from \"./metadata-cache.ts\";\nimport {\n\tloadOnboardingState,\n\tmarkSharedConfigHintShown,\n\tmarkSetupCompleted as persistSetupCompleted,\n} from \"./onboarding-state.ts\";\nimport { isAbortError } from \"./runtime-owner.ts\";\nimport type { McpExtensionState } from \"./state.ts\";\nimport { buildToolMetadata } from \"./tool-metadata.ts\";\nimport {\n\ttype ImportKind,\n\tisServerDisabled,\n\ttype McpAuthResult,\n\ttype McpConfig,\n\ttype McpPanelCallbacks,\n\ttype McpPanelResult,\n\ttype ServerEntry,\n} from \"./types.ts\";\nimport { openPath, resolveServerUrl, sanitizeTerminalText } from \"./utils.ts\";\n\nfunction terminalHyperlink(label: string, url: string): string {\n\treturn `\\u001B]8;;${sanitizeTerminalText(url)}\\u001B\\\\${sanitizeTerminalText(label)}\\u001B]8;;\\u001B\\\\`;\n}\n\n/**\n * True when this run mode can display a `ctx.ui.custom()` overlay.\n *\n * `ctx.hasUI` only reports that *some* UI context is bound. In rpc/print mode\n * that context is a headless stub whose `custom()` returns immediately without\n * ever invoking the factory or the `done` callback, so a panel awaited through\n * `new Promise(resolve => ctx.ui.custom(...))` never settles and the command\n * hangs. Overlay panels therefore additionally require `ctx.mode === \"tui\"`.\n */\nfunction canRenderPanel(ctx: ExtensionContext): boolean {\n\treturn ctx.hasUI && ctx.mode === \"tui\";\n}\n\nexport async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst lines: string[] = [\"MCP Server Status:\", \"\"];\n\n\tfor (const name of Object.keys(state.config.mcpServers)) {\n\t\tconst definition = state.config.mcpServers[name];\n\t\tif (isServerDisabled(definition)) {\n\t\t\tlines.push(`⊘ ${name}: disabled (run /mcp enable ${name}, then /reload)`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst connection = state.manager.getConnection(name);\n\t\tconst metadata = state.toolMetadata.get(name);\n\t\tconst toolCount = metadata?.length ?? 0;\n\t\tconst failedAgo = getFailureAgeSeconds(state, name);\n\t\tlet status = \"not connected\";\n\t\tlet statusIcon = \"○\";\n\t\tlet failed = false;\n\n\t\tif (connection?.status === \"connected\") {\n\t\t\tstatus = \"connected\";\n\t\t\tstatusIcon = \"✓\";\n\t\t} else if (connection?.status === \"needs-auth\") {\n\t\t\tstatus = \"needs auth\";\n\t\t\tstatusIcon = \"⚠\";\n\t\t} else if (failedAgo !== null) {\n\t\t\tconst reason = sanitizeTerminalText(getFailureMessage(state, name) ?? \"\");\n\t\t\tstatus = reason ? `failed ${failedAgo}s ago — ${reason}` : `failed ${failedAgo}s ago`;\n\t\t\tstatusIcon = \"✗\";\n\t\t\tfailed = true;\n\t\t} else if (metadata !== undefined) {\n\t\t\tstatus = \"cached\";\n\t\t}\n\n\t\tconst toolSuffix = failed ? \"\" : ` (${toolCount} tools${status === \"cached\" ? \", cached\" : \"\"})`;\n\t\tlines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);\n\t}\n\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\tlines.push(\"No MCP servers configured\");\n\t\tlines.push(\"Run /mcp setup to adopt imports or scaffold .nexus/mcp.json\");\n\t}\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showPrompts(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\tconst allPrompts = [...(state.promptMetadata?.values() ?? [])].flat();\n\tconst failedPromptServers = [...(state.manager.getAllConnections?.() ?? [])]\n\t\t.filter(([, connection]) => connection.status === \"connected\" && connection.promptDiscoveryFailed)\n\t\t.map(([serverName]) => serverName)\n\t\t.sort();\n\tif (allPrompts.length === 0) {\n\t\tconst failureNote =\n\t\t\tfailedPromptServers.length > 0 ? ` Prompt discovery failed for: ${failedPromptServers.join(\", \")}.` : \"\";\n\t\tctx.ui.notify(\n\t\t\t`No MCP prompts available. Prompts are discovered when servers with the \\`prompts\\` capability connect.${failureNote}`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn;\n\t}\n\tconst lines = [\"MCP Prompts:\", \"\"];\n\tconst grouped = new Map<string, typeof allPrompts>();\n\tfor (const prompt of allPrompts) {\n\t\tconst list = grouped.get(prompt.serverName) ?? [];\n\t\tlist.push(prompt);\n\t\tgrouped.set(prompt.serverName, list);\n\t}\n\tfor (const [serverName, prompts] of [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n\t\tlines.push(`${serverName}:`);\n\t\tfor (const prompt of prompts.sort((a, b) => a.commandName.localeCompare(b.commandName))) {\n\t\t\tconst args = prompt.arguments\n\t\t\t\t.map((argument) => (argument.required ? `<${argument.name}>` : `[${argument.name}]`))\n\t\t\t\t.join(\" \");\n\t\t\tlines.push(` /${prompt.commandName}${args ? ` ${args}` : \"\"}`);\n\t\t\tif (prompt.description) lines.push(` ${prompt.description}`);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\tlines.push(`Total: ${allPrompts.length} prompt${allPrompts.length === 1 ? \"\" : \"s\"}`);\n\tif (failedPromptServers.length > 0) {\n\t\tlines.push(\n\t\t\t`Prompt discovery failed for: ${failedPromptServers.join(\", \")}. Cached prompt metadata may be stale.`,\n\t\t);\n\t}\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst allTools = [...state.toolMetadata.entries()]\n\t\t.filter(([serverName]) => !isServerDisabled(state.config.mcpServers[serverName]))\n\t\t.flatMap(([, metadata]) => metadata.map((m) => m.name));\n\n\tif (allTools.length === 0) {\n\t\tctx.ui.notify(\"No MCP tools available\", \"info\");\n\t\treturn;\n\t}\n\n\tconst lines = [\"MCP Tools:\", \"\", ...allTools.map((t) => ` ${t}`), \"\", `Total: ${allTools.length} tools`];\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function reconnectServer(state: McpExtensionState, ctx: ExtensionContext, name: string): Promise<boolean> {\n\tconst definition = state.config.mcpServers[name];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst signal = state.owner?.signal;\n\tif (!definition) {\n\t\tif (ui) {\n\t\t\tui.notify(`Server \"${name}\" not found in config`, \"error\");\n\t\t}\n\t\treturn false;\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tif (ui) ui.notify(`MCP: ${name} is disabled. Run /mcp enable ${name}, then /reload.`, \"warning\");\n\t\treturn false;\n\t}\n\n\ttry {\n\t\tawait state.manager.close(name);\n\t\tstate.owner?.throwIfInactive();\n\t\tconst connection = signal\n\t\t\t? await state.manager.connect(name, definition, signal)\n\t\t\t: await state.manager.connect(name, definition);\n\t\tstate.owner?.throwIfInactive();\n\t\tif (connection.status === \"needs-auth\") {\n\t\t\tif (ui) {\n\t\t\t\tui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, \"warning\");\n\t\t\t}\n\t\t\tupdateStatusBar(state);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst prefix = state.config.settings?.toolPrefix ?? \"server\";\n\t\tconst { metadata, failedTools } = buildToolMetadata(\n\t\t\tconnection.tools,\n\t\t\tconnection.resources,\n\t\t\tdefinition,\n\t\t\tname,\n\t\t\tprefix,\n\t\t\tstate.config.mcpServers,\n\t\t\tstate.toolMetadata,\n\t\t);\n\t\tstate.toolMetadata.set(name, metadata);\n\t\tif (!connection.promptDiscoveryFailed) {\n\t\t\tstate.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));\n\t\t\tstate.promptMetadataLive?.add(name);\n\t\t}\n\t\tif (connection.instructions) {\n\t\t\tstate.serverInstructions.set(name, connection.instructions);\n\t\t} else {\n\t\t\tstate.serverInstructions.delete(name);\n\t\t}\n\t\tupdateMetadataCache(state, name);\n\t\tnotifyToolMetadataUpdated(state, name, \"command-reconnect\");\n\t\tmarkKeepAliveAfterConnect(state, name);\n\t\tclearFailure(state, name);\n\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tif (failedTools.length > 0) {\n\t\t\t\tui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, \"warning\");\n\t\t\t}\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (isAbortError(error, signal)) throw error;\n\t\trecordFailure(state, name, message);\n\t\tif (ui) {\n\t\t\tui.notify(`MCP: Failed to reconnect to ${name}: ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn false;\n\t}\n}\n\nexport async function reconnectServers(\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n\ttargetServer?: string,\n): Promise<void> {\n\tif (targetServer && !state.config.mcpServers[targetServer]) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(`Server \"${targetServer}\" not found in config`, \"error\");\n\t\t}\n\t\treturn;\n\t}\n\n\tconst names = targetServer ? [targetServer] : Object.keys(state.config.mcpServers);\n\tfor (const name of names) {\n\t\tawait reconnectServer(state, ctx, name);\n\t}\n\n\tupdateStatusBar(state);\n}\n\n/** Whether stored OAuth tokens actually exist for a server — independent of whether it's currently connected. */\nfunction isSignedInForOAuth(serverName: string, definition: ServerEntry, state: McpExtensionState): boolean {\n\ttry {\n\t\tconst serverUrl = resolveServerUrl(definition);\n\t\tif (!serverUrl) return false;\n\t\tconst authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);\n\t\treturn authStatus.status !== \"absent\" && authStatus.status !== \"unavailable\" && !!authStatus.entry.tokens;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport async function authenticateServer(\n\tserverName: string,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tsignal?: AbortSignal,\n\truntime?: McpOAuthRuntime,\n\tnexus?: ExtensionAPI,\n): Promise<McpAuthResult> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst cwd = ctx.cwd;\n\tsignal ??= ctx.signal;\n\tif (!ui) return { ok: false, message: \"OAuth authentication requires an interactive session.\" };\n\n\tconst definition = config.mcpServers[serverName];\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tconst message = `Server \"${serverName}\" is disabled. Run /mcp enable ${serverName}, then /reload.`;\n\t\tui.notify(message, \"warning\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (!supportsOAuth(definition)) {\n\t\tconst message = `Server \"${serverName}\" does not use OAuth authentication. Set \"auth\": \"oauth\" or omit auth for auto-detection.`;\n\t\tui.notify(\n\t\t\t`Server \"${serverName}\" does not use OAuth authentication.\\n` +\n\t\t\t\t`Set \"auth\": \"oauth\" or omit auth for auto-detection.`,\n\t\t\t\"error\",\n\t\t);\n\t\treturn { ok: false, message };\n\t}\n\n\ttry {\n\t\tconst serverUrl = resolveServerUrl(definition);\n\t\tif (!serverUrl) {\n\t\t\tconst message = `Server \"${serverName}\" has no URL configured (OAuth requires HTTP transport)`;\n\t\t\tui.notify(message, \"error\");\n\t\t\treturn { ok: false, message };\n\t\t}\n\n\t\tui.setStatus(\"mcp-auth\", `Authenticating ${serverName}...`);\n\t\tconst authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);\n\t\tconst status = await authenticate(serverName, serverUrl, definition, {\n\t\t\t...(authStorageOptions.baseDir ? { authStorageOptions } : {}),\n\t\t\tonAuthorizationUrl: async (authorizationUrl) => {\n\t\t\t\t// Best-effort: launch the user's browser straight to the authorization page\n\t\t\t\t// instead of relying on them to click (or copy) the link printed below.\n\t\t\t\tif (!nexus) return;\n\t\t\t\ttry {\n\t\t\t\t\tawait openPath(nexus, authorizationUrl);\n\t\t\t\t} catch {\n\t\t\t\t\t// The link is still shown in the prompt text below; nothing else to do here.\n\t\t\t\t}\n\t\t\t},\n\t\t\tonAuthorizationInput: async (authorizationUrl, inputSignal) => {\n\t\t\t\tif (inputSignal.aborted) return undefined;\n\t\t\t\treturn ui.input(\n\t\t\t\t\t`Complete ${serverName} OAuth\\n\\n` +\n\t\t\t\t\t\t`${terminalHyperlink(\"Open authorization page\", authorizationUrl)}\\n${authorizationUrl}\\n\\n` +\n\t\t\t\t\t\t\"Approve access, then paste the full localhost callback URL below.\",\n\t\t\t\t\tundefined,\n\t\t\t\t\t{ signal: inputSignal },\n\t\t\t\t);\n\t\t\t},\n\t\t\t...(signal ? { signal } : {}),\n\t\t\t...(runtime ? { runtime } : {}),\n\t\t});\n\t\tif (signal?.aborted) signal.throwIfAborted();\n\n\t\tif (status === \"authenticated\") {\n\t\t\tconst message = `OAuth authentication successful for \"${serverName}\".`;\n\t\t\tui.notify(message, \"info\");\n\t\t\treturn { ok: true, message };\n\t\t}\n\n\t\tconst message = `OAuth authentication failed for \"${serverName}\".`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t} catch (error) {\n\t\tif (signal?.aborted) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tui.notify(`Failed to authenticate \"${serverName}\": ${message}`, \"error\");\n\t\treturn { ok: false, message };\n\t} finally {\n\t\tif (!signal?.aborted) ui.setStatus(\"mcp-auth\", undefined);\n\t}\n}\n\nexport async function logoutServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst definition = state.config.mcpServers[serverName];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst signal = state.owner?.signal;\n\ttry {\n\t\tawait removeAuth(serverName, {\n\t\t\tauthStorageOptions: state.authStorageOptions,\n\t\t\tsignal,\n\t\t\truntime: state.oauthRuntime,\n\t\t});\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(`Failed to clear OAuth credentials for \"${serverName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\ttry {\n\t\tawait state.manager.close(serverName);\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`OAuth credentials were cleared for \"${serverName}\", but its connection could not be closed: ${sanitizeTerminalText(message)}`,\n\t\t\t\t\"error\",\n\t\t\t);\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\tupdateStatusBar(state);\n\n\tconst message = `OAuth credentials cleared for \"${serverName}\". Run /mcp-auth ${serverName} to authenticate again.`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\nfunction validateBearerTokenStoreServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n): { ok: true; serverUrl: string } | { ok: false; message: string; type: \"error\" | \"warning\" } {\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst definition = state.config.mcpServers[serverName];\n\tif (!definition) return { ok: false, message: `Server \"${safeName}\" not found in config`, type: \"error\" };\n\tif (isServerDisabled(definition))\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Server \"${safeName}\" is disabled. Run /mcp enable ${safeName}, then /reload.`,\n\t\t\ttype: \"warning\",\n\t\t};\n\tif (definition.auth !== \"bearer\" || definition.bearerTokenStore !== true) {\n\t\treturn { ok: false, message: `Server \"${safeName}\" is not configured for bearerTokenStore.`, type: \"error\" };\n\t}\n\t// resolveServerUrl embeds the interpolated URL in its exceptions; redact it\n\t// because the URL can carry userinfo or interpolated secrets.\n\tlet serverUrl: string | undefined;\n\ttry {\n\t\tserverUrl = resolveServerUrl(definition);\n\t} catch {\n\t\treturn { ok: false, message: `Server \"${safeName}\" has an invalid or unresolvable URL.`, type: \"error\" };\n\t}\n\tif (!serverUrl) return { ok: false, message: `Server \"${safeName}\" has no URL configured.`, type: \"error\" };\n\treturn { ok: true, serverUrl };\n}\n\nexport async function manageBearerToken(\n\taction: \"set\" | \"remove\" | \"status\",\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst validation = validateBearerTokenStoreServer(serverName, state);\n\tif (!validation.ok) {\n\t\tif (ui) ui.notify(validation.message, validation.type);\n\t\treturn { ok: false, message: validation.message };\n\t}\n\n\tif (action === \"set\") {\n\t\tconst message = `Nexus will not collect bearer tokens through an unmasked prompt. Configure bearerTokenEnv for \"${safeName}\" in MCP config, then reload.`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (action === \"status\") {\n\t\tconst status = inspectBearerTokenForUrl(serverName, validation.serverUrl);\n\t\tconst message =\n\t\t\tstatus.status === \"present\"\n\t\t\t\t? `Bearer token is stored for \"${safeName}\".`\n\t\t\t\t: status.status === \"url-mismatch\"\n\t\t\t\t\t? `Bearer token is stored for \"${safeName}\", but its URL does not match the current server URL.`\n\t\t\t\t\t: status.status === \"unavailable\"\n\t\t\t\t\t\t? status.message\n\t\t\t\t\t\t: `No bearer token is stored for \"${safeName}\".`;\n\t\tif (ui) ui.notify(message, status.status === \"unavailable\" ? \"error\" : \"info\");\n\t\treturn { ok: status.status !== \"unavailable\", message };\n\t}\n\n\ttry {\n\t\tremoveBearerToken(serverName);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) ui.notify(`Failed to remove bearer token for \"${safeName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst message = `Bearer token removed for \"${safeName}\".`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\ntype ParsedAddServer = { ok: true; name: string; entry: ServerEntry } | { ok: false; error: string };\n\nconst ADD_SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nfunction parseKeyValueFlag(\n\tflag: string,\n\traw: string | undefined,\n): { ok: true; key: string; value: string } | { ok: false; error: string } {\n\tconst eq = raw?.indexOf(\"=\") ?? -1;\n\tif (!raw || eq <= 0) return { ok: false, error: `${flag} expects KEY=VALUE, got \"${raw ?? \"\"}\".` };\n\treturn { ok: true, key: raw.slice(0, eq), value: raw.slice(eq + 1) };\n}\n\n/**\n * Parses `/mcp add <name> ...` arguments. Mirrors the rest of /mcp's naive\n * whitespace-split argument handling (no quoting support) rather than\n * inventing a shell-quote parser just for this subcommand.\n */\nexport function parseAddServerArgs(argv: string[]): ParsedAddServer {\n\tconst name = argv[0];\n\tif (!name) return { ok: false, error: \"Missing server name.\" };\n\tif (!ADD_SERVER_NAME_PATTERN.test(name)) {\n\t\treturn { ok: false, error: `Invalid server name \"${name}\" — use letters, digits, \".\", \"_\", \"-\".` };\n\t}\n\n\tconst rest = argv.slice(1);\n\tconst env: Record<string, string> = {};\n\tconst headers: Record<string, string> = {};\n\tlet cwd: string | undefined;\n\tlet url: string | undefined;\n\tlet auth: \"oauth\" | \"bearer\" | undefined;\n\tlet bearerTokenEnv: string | undefined;\n\tlet commandArgv: string[] | undefined;\n\n\tlet i = 0;\n\twhile (i < rest.length) {\n\t\tconst token = rest[i];\n\t\tif (token === \"--\") {\n\t\t\tcommandArgv = rest.slice(i + 1);\n\t\t\tbreak;\n\t\t}\n\t\tif (token === \"--url\") {\n\t\t\turl = rest[i + 1];\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--env\" || token === \"--header\") {\n\t\t\tconst parsed = parseKeyValueFlag(token, rest[i + 1]);\n\t\t\tif (!parsed.ok) return parsed;\n\t\t\t(token === \"--env\" ? env : headers)[parsed.key] = parsed.value;\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--cwd\") {\n\t\t\tcwd = rest[i + 1];\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--auth\") {\n\t\t\tconst value = rest[i + 1];\n\t\t\tif (value !== \"oauth\" && value !== \"bearer\") {\n\t\t\t\treturn { ok: false, error: `--auth expects \"oauth\" or \"bearer\", got \"${value ?? \"\"}\".` };\n\t\t\t}\n\t\t\tauth = value;\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--bearer-env\") {\n\t\t\tbearerTokenEnv = rest[i + 1];\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\treturn { ok: false, error: `Unrecognized argument \"${token}\".` };\n\t}\n\n\tif (url && commandArgv) {\n\t\treturn { ok: false, error: \"Use either --url (remote server) or -- <command> (local server), not both.\" };\n\t}\n\tif (!url && !commandArgv) {\n\t\treturn { ok: false, error: \"Specify a server type: --url <url> (remote), or -- <command> [args...] (local).\" };\n\t}\n\n\tif (commandArgv) {\n\t\tconst [command, ...args] = commandArgv;\n\t\tif (!command) return { ok: false, error: \"Missing command after --.\" };\n\t\tconst entry: ServerEntry = { command, ...(args.length ? { args } : {}) };\n\t\tif (Object.keys(env).length) entry.env = env;\n\t\tif (cwd) entry.cwd = cwd;\n\t\treturn { ok: true, name, entry };\n\t}\n\n\tconst entry: ServerEntry = { url: url as string };\n\tif (Object.keys(headers).length) entry.headers = headers;\n\tif (bearerTokenEnv) {\n\t\tentry.bearerTokenEnv = bearerTokenEnv;\n\t\tentry.auth = auth ?? \"bearer\";\n\t} else if (auth) {\n\t\tentry.auth = auth;\n\t}\n\treturn { ok: true, name, entry };\n}\n\nexport const ADD_SERVER_USAGE = [\n\t\"Usage:\",\n\t\" /mcp add <name> -- <command> [args...] local server, runs over stdio\",\n\t\" /mcp add <name> --url <url> remote server over HTTP\",\n\t\"\",\n\t\"Flags:\",\n\t\" --env KEY=VALUE extra environment variable (local, repeatable)\",\n\t\" --cwd <path> working directory (local)\",\n\t\" --header KEY=VALUE extra HTTP header (remote, repeatable)\",\n\t\" --auth oauth|bearer auth mode (remote; default is OAuth auto-detection)\",\n\t\" --bearer-env <VAR> read a bearer token from this env var (remote)\",\n\t\"\",\n\t\"For fields not covered here (lifecycle, includeTools, ...), edit the config\",\n\t'file directly — /mcp setup, then \"Open detected config paths\".',\n].join(\"\\n\");\n\n/** Adds a server to the project's shared MCP config. Reused by both `/mcp add` and any UI entry point. */\nexport function addCustomServer(cwd: string, name: string, entry: ServerEntry): { path: string } {\n\tconst path = writeSharedServerEntry(getProjectConfigPath(cwd), name, entry);\n\treturn { path };\n}\n\nexport interface PanelFlowResult {\n\tconfigChanged: boolean;\n}\n\nfunction buildSharedConfigNoticeLines(\n\tconfigOverridePath: string | undefined,\n\tcwd: string,\n): { lines: string[]; fingerprint: string | null } {\n\tconst discovery = getMcpStandardConfigSummary(configOverridePath, cwd);\n\tconst onboardingState = loadOnboardingState();\n\tif (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {\n\t\treturn { lines: [], fingerprint: null };\n\t}\n\n\tconst sharedSources = discovery.sources.filter((source) => source.kind === \"shared\" && source.serverCount > 0);\n\tconst sourceList = sharedSources.map((source) => source.path).join(\", \");\n\treturn {\n\t\tlines: [\n\t\t\t`Using MCP configuration from ${sourceList}.`,\n\t\t\t\"Nexus writes compatibility imports and MCP-specific overrides only to Nexus configuration files.\",\n\t\t],\n\t\tfingerprint: discovery.fingerprint,\n\t};\n}\n\nexport async function openMcpSetup(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tmode: \"empty\" | \"setup\" = \"setup\",\n\toptions: { includeHostConfigs?: boolean } = {},\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP setup panel is only available in the terminal UI (current mode: ${ctx.mode}). Edit .nexus/mcp.json directly, or run /mcp status to review servers.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"MCP setup is unavailable when config is supplied by createMcpAdapter().\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options);\n\tconst onboardingState = loadOnboardingState();\n\tconst installedServers = Object.entries(state.config.mcpServers).map(([name, definition]) => {\n\t\tconst authCapable = supportsOAuth(definition);\n\t\treturn {\n\t\t\tname,\n\t\t\tdisabled: isServerDisabled(definition),\n\t\t\tkind: (definition.command ? \"stdio\" : \"http\") as \"stdio\" | \"http\",\n\t\t\tauthCapable,\n\t\t\tsignedIn: authCapable ? isSignedInForOAuth(name, definition, state) : false,\n\t\t};\n\t});\n\tlet configChanged = false;\n\tlet overlayHandle: OverlayHandle | undefined;\n\n\tconst callbacks = {\n\t\tpreviewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),\n\t\tpreviewStarterProject: () => previewStarterProjectConfig(ctx.cwd),\n\t\tpreviewRepoPrompt: () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;\n\t\t\treturn previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t},\n\t\tpreviewKnownServer: (preset: KnownServerPreset) =>\n\t\t\tpreviewSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry),\n\t\tadoptImports: async (imports: ImportKind[]) => {\n\t\t\tconst result = ensureCompatibilityImports(imports, configOverridePath);\n\t\t\tif (result.added.length > 0) configChanged = true;\n\t\t\treturn result;\n\t\t},\n\t\tscaffoldProjectConfig: async () => {\n\t\t\tconst path = writeStarterProjectConfig(ctx.cwd);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path };\n\t\t},\n\t\taddRepoPrompt: async () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {\n\t\t\t\tthrow new Error(\"RepoPrompt is not available to add from this setup screen.\");\n\t\t\t}\n\t\t\tconst path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: repoPrompt.serverName };\n\t\t},\n\t\taddKnownServer: async (preset: KnownServerPreset) => {\n\t\t\tconst path = writeSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: preset.name };\n\t\t},\n\t\topenPath: async (targetPath: string) => {\n\t\t\tawait openPath(nexus, targetPath);\n\t\t},\n\t\tmarkSetupCompleted: () => {\n\t\t\tpersistSetupCompleted(discovery.fingerprint);\n\t\t},\n\t\tseedEditor: (text: string) => {\n\t\t\tif (ctx.hasUI) ctx.ui.setEditorText(text);\n\t\t},\n\t\ttoggleServerDisabled: async (serverName: string, disabled: boolean) => {\n\t\t\tconst result = writeProjectServerDisabledOverride(configOverridePath, ctx.cwd, serverName, disabled);\n\t\t\tif (result.changed) configChanged = true;\n\t\t\treturn { path: result.path, changed: result.changed };\n\t\t},\n\t\tauthenticateServer: async (serverName: string, fallbackEntry?: ServerEntry) => {\n\t\t\t// A server added this session isn't in the live config until /reload —\n\t\t\t// hand authenticateServer a synthetic config with just that one entry\n\t\t\t// merged in, rather than making the user reload first.\n\t\t\tconst effectiveConfig: McpConfig =\n\t\t\t\tstate.config.mcpServers[serverName] || !fallbackEntry\n\t\t\t\t\t? state.config\n\t\t\t\t\t: { ...state.config, mcpServers: { ...state.config.mcpServers, [serverName]: fallbackEntry } };\n\t\t\t// The auth prompt (the actual link, and the \"paste the callback URL\"\n\t\t\t// input) renders as a *separate* overlay. Without hiding this one first,\n\t\t\t// it stays drawn on top and the auth prompt is invisible underneath it.\n\t\t\toverlayHandle?.setHidden(true);\n\t\t\ttry {\n\t\t\t\tconst result = await authenticateServer(\n\t\t\t\t\tserverName,\n\t\t\t\t\teffectiveConfig,\n\t\t\t\t\tctx,\n\t\t\t\t\tctx.signal,\n\t\t\t\t\tstate.oauthRuntime,\n\t\t\t\t\tnexus,\n\t\t\t\t);\n\t\t\t\treturn {\n\t\t\t\t\tok: result.ok,\n\t\t\t\t\tmessage:\n\t\t\t\t\t\tresult.message ??\n\t\t\t\t\t\t(result.ok ? `Signed in to \"${serverName}\".` : `Sign-in failed for \"${serverName}\".`),\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\toverlayHandle?.setHidden(false);\n\t\t\t\toverlayHandle?.focus();\n\t\t\t}\n\t\t},\n\t};\n\n\treturn new Promise<PanelFlowResult>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpSetupPanel(\n\t\t\t\t\tdiscovery,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\t{ mode, onboardingState, keybindings, installedServers },\n\t\t\t\t\ttui,\n\t\t\t\t\t() => {\n\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\tresolve({ configChanged });\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 92, maxHeight: \"90%\" },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n}\n\nfunction buildMcpPanelCallbacks(\n\tstate: McpExtensionState,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tnexus: ExtensionAPI,\n\tgetOverlayHandle?: () => OverlayHandle | undefined,\n): McpPanelCallbacks {\n\t// Panel-only diagnostics keep status inspection from mutating connection\n\t// failure state while allowing the existing panel failure UI to show why the\n\t// credential store could not be inspected.\n\tconst authStatusFailures = new Map<string, string>();\n\n\treturn {\n\t\treconnect: (serverName: string) => reconnectServer(state, ctx, serverName),\n\t\tcanAuthenticate: (serverName: string) => {\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\treturn definition ? !isServerDisabled(definition) && supportsOAuth(definition) : false;\n\t\t},\n\t\tauthenticate: async (serverName: string) => {\n\t\t\tconst overlay = getOverlayHandle?.();\n\t\t\toverlay?.setHidden(true);\n\t\t\ttry {\n\t\t\t\treturn await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime, nexus);\n\t\t\t} finally {\n\t\t\t\toverlay?.setHidden(false);\n\t\t\t\toverlay?.focus();\n\t\t\t}\n\t\t},\n\t\tgetConnectionStatus: (serverName: string) => {\n\t\t\tauthStatusFailures.delete(serverName);\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\tif (isServerDisabled(definition)) return \"disabled\";\n\t\t\tconst connection = state.manager.getConnection(serverName);\n\t\t\tlet serverUrl: string | undefined;\n\t\t\ttry {\n\t\t\t\tserverUrl = definition ? resolveServerUrl(definition) : undefined;\n\t\t\t} catch {\n\t\t\t\treturn \"failed\";\n\t\t\t}\n\t\t\tif (\n\t\t\t\tdefinition?.auth === \"oauth\" &&\n\t\t\t\tserverUrl &&\n\t\t\t\tdefinition.oauth !== false &&\n\t\t\t\tdefinition.oauth?.grantType !== \"client_credentials\"\n\t\t\t) {\n\t\t\t\tconst authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);\n\t\t\t\tif (authStatus.status === \"unavailable\") {\n\t\t\t\t\tauthStatusFailures.set(serverName, authStatus.message);\n\t\t\t\t\treturn \"failed\";\n\t\t\t\t}\n\t\t\t\tif (authStatus.status === \"absent\" || !authStatus.entry.tokens) {\n\t\t\t\t\treturn \"needs-auth\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (connection?.status === \"needs-auth\") return \"needs-auth\";\n\t\t\tif (connection?.status === \"connected\") return \"connected\";\n\t\t\tif (getFailureAgeSeconds(state, serverName) !== null) return \"failed\";\n\t\t\treturn \"idle\";\n\t\t},\n\t\tgetFailureMessage: (serverName: string) =>\n\t\t\tauthStatusFailures.get(serverName) ?? getFailureMessage(state, serverName),\n\t\trefreshCacheAfterReconnect: (serverName: string) => {\n\t\t\tconst freshCache = loadMetadataCache();\n\t\t\treturn freshCache?.servers?.[serverName] ?? null;\n\t\t},\n\t};\n}\n\nexport async function openMcpPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tonDirectToolsConfigChanged?: (changes: Map<string, true | string[] | false>) => void | Promise<void>,\n): Promise<PanelFlowResult> {\n\tif (state.programmaticConfig) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(\n\t\t\t\t\"MCP status is shown from the in-memory SDK config; configuration discovery is unavailable.\",\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tawait showStatus(state, ctx);\n\t\t}\n\t\treturn { configChanged: false };\n\t}\n\tif (!canRenderPanel(ctx)) {\n\t\t// No overlay here, but the same information is available as text.\n\t\tawait showStatus(state, ctx);\n\t\treturn { configChanged: false };\n\t}\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\treturn openMcpSetup(state, nexus, ctx, configOverridePath, \"empty\", { includeHostConfigs: false });\n\t}\n\n\tconst config = state.config;\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tconst { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);\n\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);\n\n\tlet configChanged = false;\n\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t(result: McpPanelResult) => {\n\t\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\t\tif (!result.cancelled && result.changes.size > 0) {\n\t\t\t\t\t\t\t\twriteDirectToolsConfig(result.changes, provenanceMap);\n\t\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\t\tawait onDirectToolsConfigChanged?.(result.changes);\n\t\t\t\t\t\t\t\tctx.ui.notify(\"Direct tools updated for this session.\", \"info\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t})().catch((error) => {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tctx.ui.notify(`Direct tools updated, but live refresh failed: ${message}`, \"error\");\n\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{ noticeLines, keybindings },\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\tif (noticeLines.length > 0 && fingerprint) {\n\t\tmarkSharedConfigHintShown(fingerprint);\n\t}\n\n\treturn { configChanged };\n}\n\nexport async function openMcpAuthPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP auth panel is only available in the terminal UI (current mode: ${ctx.mode}). Use /mcp-auth <server> to authenticate a specific server.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst config = state.config;\n\tconst oauthServers = Object.entries(config.mcpServers).filter(\n\t\t([, definition]) => !isServerDisabled(definition) && supportsOAuth(definition),\n\t);\n\tif (oauthServers.length === 0) {\n\t\tctx.ui.notify(\"No OAuth-capable MCP servers are configured.\", \"warning\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t() => {\n\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tauthOnly: true,\n\t\t\t\t\t\tkeybindings,\n\t\t\t\t\t\tnoticeLines: [\"Select an OAuth MCP server and press Enter or ctrl+a to authenticate.\"],\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\treturn { configChanged: false };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ensureCompatibilityImports, getMcpDiscoverySummary, getMcpStandardConfigSummary, getProjectConfigPath, getServerProvenance, previewCompatibilityImports, previewSharedServerEntry, previewStarterProjectConfig, writeDirectToolsConfig, writeSharedServerEntry, writeStarterProjectConfig, } from "./config.js";
|
|
1
|
+
import { ensureCompatibilityImports, getMcpDiscoverySummary, getMcpStandardConfigSummary, getProjectConfigPath, getServerProvenance, previewCompatibilityImports, previewSharedServerEntry, previewStarterProjectConfig, writeDirectToolsConfig, writeProjectServerDisabledOverride, writeSharedServerEntry, writeStarterProjectConfig, } from "./config.js";
|
|
2
2
|
import { clearFailure, getFailureAgeSeconds, getFailureMessage, markKeepAliveAfterConnect, notifyToolMetadataUpdated, recordFailure, updateMetadataCache, updateStatusBar, } from "./init.js";
|
|
3
3
|
import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.js";
|
|
4
4
|
import { authenticate, removeAuth, supportsOAuth } from "./mcp-auth-flow.js";
|
|
@@ -200,7 +200,20 @@ export async function reconnectServers(state, ctx, targetServer) {
|
|
|
200
200
|
}
|
|
201
201
|
updateStatusBar(state);
|
|
202
202
|
}
|
|
203
|
-
|
|
203
|
+
/** Whether stored OAuth tokens actually exist for a server — independent of whether it's currently connected. */
|
|
204
|
+
function isSignedInForOAuth(serverName, definition, state) {
|
|
205
|
+
try {
|
|
206
|
+
const serverUrl = resolveServerUrl(definition);
|
|
207
|
+
if (!serverUrl)
|
|
208
|
+
return false;
|
|
209
|
+
const authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);
|
|
210
|
+
return authStatus.status !== "absent" && authStatus.status !== "unavailable" && !!authStatus.entry.tokens;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function authenticateServer(serverName, config, ctx, signal, runtime, nexus) {
|
|
204
217
|
const ui = ctx.hasUI ? ctx.ui : undefined;
|
|
205
218
|
const cwd = ctx.cwd;
|
|
206
219
|
signal ??= ctx.signal;
|
|
@@ -234,7 +247,18 @@ export async function authenticateServer(serverName, config, ctx, signal, runtim
|
|
|
234
247
|
const authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);
|
|
235
248
|
const status = await authenticate(serverName, serverUrl, definition, {
|
|
236
249
|
...(authStorageOptions.baseDir ? { authStorageOptions } : {}),
|
|
237
|
-
onAuthorizationUrl: () => {
|
|
250
|
+
onAuthorizationUrl: async (authorizationUrl) => {
|
|
251
|
+
// Best-effort: launch the user's browser straight to the authorization page
|
|
252
|
+
// instead of relying on them to click (or copy) the link printed below.
|
|
253
|
+
if (!nexus)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
await openPath(nexus, authorizationUrl);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// The link is still shown in the prompt text below; nothing else to do here.
|
|
260
|
+
}
|
|
261
|
+
},
|
|
238
262
|
onAuthorizationInput: async (authorizationUrl, inputSignal) => {
|
|
239
263
|
if (inputSignal.aborted)
|
|
240
264
|
return undefined;
|
|
@@ -383,6 +407,123 @@ export async function manageBearerToken(action, serverName, state, ctx) {
|
|
|
383
407
|
ui.notify(message, "info");
|
|
384
408
|
return { ok: true, message };
|
|
385
409
|
}
|
|
410
|
+
const ADD_SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
411
|
+
function parseKeyValueFlag(flag, raw) {
|
|
412
|
+
const eq = raw?.indexOf("=") ?? -1;
|
|
413
|
+
if (!raw || eq <= 0)
|
|
414
|
+
return { ok: false, error: `${flag} expects KEY=VALUE, got "${raw ?? ""}".` };
|
|
415
|
+
return { ok: true, key: raw.slice(0, eq), value: raw.slice(eq + 1) };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Parses `/mcp add <name> ...` arguments. Mirrors the rest of /mcp's naive
|
|
419
|
+
* whitespace-split argument handling (no quoting support) rather than
|
|
420
|
+
* inventing a shell-quote parser just for this subcommand.
|
|
421
|
+
*/
|
|
422
|
+
export function parseAddServerArgs(argv) {
|
|
423
|
+
const name = argv[0];
|
|
424
|
+
if (!name)
|
|
425
|
+
return { ok: false, error: "Missing server name." };
|
|
426
|
+
if (!ADD_SERVER_NAME_PATTERN.test(name)) {
|
|
427
|
+
return { ok: false, error: `Invalid server name "${name}" — use letters, digits, ".", "_", "-".` };
|
|
428
|
+
}
|
|
429
|
+
const rest = argv.slice(1);
|
|
430
|
+
const env = {};
|
|
431
|
+
const headers = {};
|
|
432
|
+
let cwd;
|
|
433
|
+
let url;
|
|
434
|
+
let auth;
|
|
435
|
+
let bearerTokenEnv;
|
|
436
|
+
let commandArgv;
|
|
437
|
+
let i = 0;
|
|
438
|
+
while (i < rest.length) {
|
|
439
|
+
const token = rest[i];
|
|
440
|
+
if (token === "--") {
|
|
441
|
+
commandArgv = rest.slice(i + 1);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
if (token === "--url") {
|
|
445
|
+
url = rest[i + 1];
|
|
446
|
+
i += 2;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (token === "--env" || token === "--header") {
|
|
450
|
+
const parsed = parseKeyValueFlag(token, rest[i + 1]);
|
|
451
|
+
if (!parsed.ok)
|
|
452
|
+
return parsed;
|
|
453
|
+
(token === "--env" ? env : headers)[parsed.key] = parsed.value;
|
|
454
|
+
i += 2;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (token === "--cwd") {
|
|
458
|
+
cwd = rest[i + 1];
|
|
459
|
+
i += 2;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (token === "--auth") {
|
|
463
|
+
const value = rest[i + 1];
|
|
464
|
+
if (value !== "oauth" && value !== "bearer") {
|
|
465
|
+
return { ok: false, error: `--auth expects "oauth" or "bearer", got "${value ?? ""}".` };
|
|
466
|
+
}
|
|
467
|
+
auth = value;
|
|
468
|
+
i += 2;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (token === "--bearer-env") {
|
|
472
|
+
bearerTokenEnv = rest[i + 1];
|
|
473
|
+
i += 2;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
return { ok: false, error: `Unrecognized argument "${token}".` };
|
|
477
|
+
}
|
|
478
|
+
if (url && commandArgv) {
|
|
479
|
+
return { ok: false, error: "Use either --url (remote server) or -- <command> (local server), not both." };
|
|
480
|
+
}
|
|
481
|
+
if (!url && !commandArgv) {
|
|
482
|
+
return { ok: false, error: "Specify a server type: --url <url> (remote), or -- <command> [args...] (local)." };
|
|
483
|
+
}
|
|
484
|
+
if (commandArgv) {
|
|
485
|
+
const [command, ...args] = commandArgv;
|
|
486
|
+
if (!command)
|
|
487
|
+
return { ok: false, error: "Missing command after --." };
|
|
488
|
+
const entry = { command, ...(args.length ? { args } : {}) };
|
|
489
|
+
if (Object.keys(env).length)
|
|
490
|
+
entry.env = env;
|
|
491
|
+
if (cwd)
|
|
492
|
+
entry.cwd = cwd;
|
|
493
|
+
return { ok: true, name, entry };
|
|
494
|
+
}
|
|
495
|
+
const entry = { url: url };
|
|
496
|
+
if (Object.keys(headers).length)
|
|
497
|
+
entry.headers = headers;
|
|
498
|
+
if (bearerTokenEnv) {
|
|
499
|
+
entry.bearerTokenEnv = bearerTokenEnv;
|
|
500
|
+
entry.auth = auth ?? "bearer";
|
|
501
|
+
}
|
|
502
|
+
else if (auth) {
|
|
503
|
+
entry.auth = auth;
|
|
504
|
+
}
|
|
505
|
+
return { ok: true, name, entry };
|
|
506
|
+
}
|
|
507
|
+
export const ADD_SERVER_USAGE = [
|
|
508
|
+
"Usage:",
|
|
509
|
+
" /mcp add <name> -- <command> [args...] local server, runs over stdio",
|
|
510
|
+
" /mcp add <name> --url <url> remote server over HTTP",
|
|
511
|
+
"",
|
|
512
|
+
"Flags:",
|
|
513
|
+
" --env KEY=VALUE extra environment variable (local, repeatable)",
|
|
514
|
+
" --cwd <path> working directory (local)",
|
|
515
|
+
" --header KEY=VALUE extra HTTP header (remote, repeatable)",
|
|
516
|
+
" --auth oauth|bearer auth mode (remote; default is OAuth auto-detection)",
|
|
517
|
+
" --bearer-env <VAR> read a bearer token from this env var (remote)",
|
|
518
|
+
"",
|
|
519
|
+
"For fields not covered here (lifecycle, includeTools, ...), edit the config",
|
|
520
|
+
'file directly — /mcp setup, then "Open detected config paths".',
|
|
521
|
+
].join("\n");
|
|
522
|
+
/** Adds a server to the project's shared MCP config. Reused by both `/mcp add` and any UI entry point. */
|
|
523
|
+
export function addCustomServer(cwd, name, entry) {
|
|
524
|
+
const path = writeSharedServerEntry(getProjectConfigPath(cwd), name, entry);
|
|
525
|
+
return { path };
|
|
526
|
+
}
|
|
386
527
|
function buildSharedConfigNoticeLines(configOverridePath, cwd) {
|
|
387
528
|
const discovery = getMcpStandardConfigSummary(configOverridePath, cwd);
|
|
388
529
|
const onboardingState = loadOnboardingState();
|
|
@@ -412,7 +553,18 @@ export async function openMcpSetup(state, nexus, ctx, configOverridePath, mode =
|
|
|
412
553
|
}
|
|
413
554
|
const discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options);
|
|
414
555
|
const onboardingState = loadOnboardingState();
|
|
556
|
+
const installedServers = Object.entries(state.config.mcpServers).map(([name, definition]) => {
|
|
557
|
+
const authCapable = supportsOAuth(definition);
|
|
558
|
+
return {
|
|
559
|
+
name,
|
|
560
|
+
disabled: isServerDisabled(definition),
|
|
561
|
+
kind: (definition.command ? "stdio" : "http"),
|
|
562
|
+
authCapable,
|
|
563
|
+
signedIn: authCapable ? isSignedInForOAuth(name, definition, state) : false,
|
|
564
|
+
};
|
|
565
|
+
});
|
|
415
566
|
let configChanged = false;
|
|
567
|
+
let overlayHandle;
|
|
416
568
|
const callbacks = {
|
|
417
569
|
previewImports: (imports) => previewCompatibilityImports(imports, configOverridePath),
|
|
418
570
|
previewStarterProject: () => previewStarterProjectConfig(ctx.cwd),
|
|
@@ -454,17 +606,57 @@ export async function openMcpSetup(state, nexus, ctx, configOverridePath, mode =
|
|
|
454
606
|
markSetupCompleted: () => {
|
|
455
607
|
persistSetupCompleted(discovery.fingerprint);
|
|
456
608
|
},
|
|
609
|
+
seedEditor: (text) => {
|
|
610
|
+
if (ctx.hasUI)
|
|
611
|
+
ctx.ui.setEditorText(text);
|
|
612
|
+
},
|
|
613
|
+
toggleServerDisabled: async (serverName, disabled) => {
|
|
614
|
+
const result = writeProjectServerDisabledOverride(configOverridePath, ctx.cwd, serverName, disabled);
|
|
615
|
+
if (result.changed)
|
|
616
|
+
configChanged = true;
|
|
617
|
+
return { path: result.path, changed: result.changed };
|
|
618
|
+
},
|
|
619
|
+
authenticateServer: async (serverName, fallbackEntry) => {
|
|
620
|
+
// A server added this session isn't in the live config until /reload —
|
|
621
|
+
// hand authenticateServer a synthetic config with just that one entry
|
|
622
|
+
// merged in, rather than making the user reload first.
|
|
623
|
+
const effectiveConfig = state.config.mcpServers[serverName] || !fallbackEntry
|
|
624
|
+
? state.config
|
|
625
|
+
: { ...state.config, mcpServers: { ...state.config.mcpServers, [serverName]: fallbackEntry } };
|
|
626
|
+
// The auth prompt (the actual link, and the "paste the callback URL"
|
|
627
|
+
// input) renders as a *separate* overlay. Without hiding this one first,
|
|
628
|
+
// it stays drawn on top and the auth prompt is invisible underneath it.
|
|
629
|
+
overlayHandle?.setHidden(true);
|
|
630
|
+
try {
|
|
631
|
+
const result = await authenticateServer(serverName, effectiveConfig, ctx, ctx.signal, state.oauthRuntime, nexus);
|
|
632
|
+
return {
|
|
633
|
+
ok: result.ok,
|
|
634
|
+
message: result.message ??
|
|
635
|
+
(result.ok ? `Signed in to "${serverName}".` : `Sign-in failed for "${serverName}".`),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
overlayHandle?.setHidden(false);
|
|
640
|
+
overlayHandle?.focus();
|
|
641
|
+
}
|
|
642
|
+
},
|
|
457
643
|
};
|
|
458
644
|
return new Promise((resolve) => {
|
|
459
645
|
ctx.ui.custom((tui, _theme, keybindings, done) => {
|
|
460
|
-
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {
|
|
646
|
+
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings, installedServers }, tui, () => {
|
|
461
647
|
done(undefined);
|
|
462
648
|
resolve({ configChanged });
|
|
463
649
|
});
|
|
464
|
-
}, {
|
|
650
|
+
}, {
|
|
651
|
+
overlay: true,
|
|
652
|
+
overlayOptions: { anchor: "center", width: 92, maxHeight: "90%" },
|
|
653
|
+
onHandle: (handle) => {
|
|
654
|
+
overlayHandle = handle;
|
|
655
|
+
},
|
|
656
|
+
});
|
|
465
657
|
});
|
|
466
658
|
}
|
|
467
|
-
function buildMcpPanelCallbacks(state, config, ctx, getOverlayHandle) {
|
|
659
|
+
function buildMcpPanelCallbacks(state, config, ctx, nexus, getOverlayHandle) {
|
|
468
660
|
// Panel-only diagnostics keep status inspection from mutating connection
|
|
469
661
|
// failure state while allowing the existing panel failure UI to show why the
|
|
470
662
|
// credential store could not be inspected.
|
|
@@ -479,7 +671,7 @@ function buildMcpPanelCallbacks(state, config, ctx, getOverlayHandle) {
|
|
|
479
671
|
const overlay = getOverlayHandle?.();
|
|
480
672
|
overlay?.setHidden(true);
|
|
481
673
|
try {
|
|
482
|
-
return await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime);
|
|
674
|
+
return await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime, nexus);
|
|
483
675
|
}
|
|
484
676
|
finally {
|
|
485
677
|
overlay?.setHidden(false);
|
|
@@ -549,7 +741,7 @@ export async function openMcpPanel(state, nexus, ctx, configOverridePath, onDire
|
|
|
549
741
|
const provenanceMap = getServerProvenance(configPath, ctx.cwd);
|
|
550
742
|
const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);
|
|
551
743
|
let overlayHandle;
|
|
552
|
-
const callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);
|
|
744
|
+
const callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);
|
|
553
745
|
let configChanged = false;
|
|
554
746
|
await new Promise((resolve) => {
|
|
555
747
|
ctx.ui.custom((tui, _theme, keybindings, done) => {
|
|
@@ -605,7 +797,7 @@ export async function openMcpAuthPanel(state, nexus, ctx, configOverridePath) {
|
|
|
605
797
|
const configPath = nexus.getFlag("mcp-config") ?? configOverridePath;
|
|
606
798
|
const provenanceMap = getServerProvenance(configPath, ctx.cwd);
|
|
607
799
|
let overlayHandle;
|
|
608
|
-
const callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);
|
|
800
|
+
const callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);
|
|
609
801
|
await new Promise((resolve) => {
|
|
610
802
|
ctx.ui.custom((tui, _theme, keybindings, done) => {
|
|
611
803
|
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {
|