@fitchmultz/pi-mcp-adapter 5.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/OAUTH.md +3 -3
- package/README.md +160 -88
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/config.js +41 -16
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +2 -2
- package/dist/mcp-auth-flow.js +2 -2
- package/dist/mcp-auth.d.ts +1 -1
- package/dist/mcp-auth.js +3 -3
- package/dist/mcp-code.d.ts +7 -6
- package/dist/mcp-code.js +98 -51
- package/dist/mcp-oauth-provider.d.ts +5 -5
- package/dist/mcp-oauth-provider.js +5 -5
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +3 -3
- package/dist/mcp-panel.js +36 -99
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-setup-panel.d.ts +5 -4
- package/dist/mcp-setup-panel.js +15 -7
- package/dist/mcp-status.js +2 -1
- package/dist/mcp-trace.d.ts +1 -1
- package/dist/mcp-trace.js +1 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/npx-resolver.js +8 -4
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +41 -7
- package/dist/proxy-modes.js +275 -490
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +5 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +70 -137
- package/dist/server-manager.d.ts +2 -2
- package/dist/server-manager.js +44 -27
- package/dist/session-recovery.d.ts +7 -4
- package/dist/session-recovery.js +76 -10
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +6 -3
- package/dist/tool-result-renderer.js +11 -6
- package/dist/types.d.ts +11 -36
- package/dist/ui-resource-handler.d.ts +2 -2
- package/dist/ui-resource-handler.js +2 -2
- package/dist/ui-server.js +4 -3
- package/dist/ui-stream-types.d.ts +8 -8
- package/dist/unix-socket-transport.d.ts +1 -1
- package/dist/unix-socket-transport.js +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +16 -4
- package/package.json +37 -96
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/abort.ts +0 -34
- package/agent-dir.ts +0 -24
- package/app-bridge.bundle.js +0 -67
- package/banner.png +0 -0
- package/checkpoint.ts +0 -55
- package/commands.ts +0 -619
- package/config.ts +0 -1204
- package/consent-manager.ts +0 -25
- package/direct-tools.ts +0 -408
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/elicitation-handler.ts +0 -349
- package/error-signal.ts +0 -21
- package/glimpse-ui.ts +0 -80
- package/host-html-template.ts +0 -456
- package/index.ts +0 -900
- package/init.ts +0 -627
- package/legacy-migration.ts +0 -47
- package/lifecycle.ts +0 -165
- package/logger.ts +0 -127
- package/mcp-auth-flow.ts +0 -975
- package/mcp-auth.ts +0 -793
- package/mcp-callback-server.ts +0 -450
- package/mcp-code.ts +0 -386
- package/mcp-keyring-helper.cjs +0 -89
- package/mcp-oauth-provider.ts +0 -774
- package/mcp-output-guard.ts +0 -412
- package/mcp-panel.ts +0 -978
- package/mcp-script-worker.mjs +0 -128
- package/mcp-setup-panel.ts +0 -666
- package/mcp-status.ts +0 -109
- package/mcp-trace.ts +0 -295
- package/metadata-cache.ts +0 -333
- package/npx-resolver.ts +0 -488
- package/onboarding-state.ts +0 -58
- package/panel-keys.ts +0 -37
- package/prompts.ts +0 -337
- package/proxy-modes.ts +0 -1361
- package/resource-tools.ts +0 -17
- package/runtime-owner.ts +0 -123
- package/sampling-handler.ts +0 -273
- package/scripts/build.mjs +0 -131
- package/scripts/prepare.mjs +0 -102
- package/search-ranking.ts +0 -196
- package/server-manager.ts +0 -1320
- package/session-recovery.ts +0 -316
- package/state.ts +0 -64
- package/tool-approval.ts +0 -78
- package/tool-metadata.ts +0 -235
- package/tool-registrar.ts +0 -69
- package/tool-result-renderer.ts +0 -286
- package/ts-shape.ts +0 -145
- package/tsconfig.build.json +0 -13
- package/tsconfig.json +0 -16
- package/types.ts +0 -810
- package/ui-resource-handler.ts +0 -249
- package/ui-server.ts +0 -808
- package/ui-session.ts +0 -568
- package/ui-stream-types.ts +0 -89
- package/ui-tool-visibility.ts +0 -25
- package/unix-socket-transport.ts +0 -85
- package/utils.ts +0 -309
package/dist/commands.js
CHANGED
|
@@ -22,7 +22,8 @@ export async function showStatus(state, ctx) {
|
|
|
22
22
|
}
|
|
23
23
|
const connection = state.manager.getConnection(name);
|
|
24
24
|
const metadata = state.toolMetadata.get(name);
|
|
25
|
-
const toolCount = metadata?.length ?? 0;
|
|
25
|
+
const toolCount = metadata?.filter(tool => tool.resourceUri === undefined).length ?? 0;
|
|
26
|
+
const resourceCount = metadata?.filter(tool => tool.resourceUri !== undefined).length ?? 0;
|
|
26
27
|
const failedAgo = getFailureAgeSeconds(state, name);
|
|
27
28
|
let status = "not connected";
|
|
28
29
|
let statusIcon = "○";
|
|
@@ -44,7 +45,9 @@ export async function showStatus(state, ctx) {
|
|
|
44
45
|
else if (metadata !== undefined) {
|
|
45
46
|
status = "cached";
|
|
46
47
|
}
|
|
47
|
-
const toolSuffix = failed ? "" :
|
|
48
|
+
const toolSuffix = failed ? "" : metadata === undefined
|
|
49
|
+
? " (undiscovered)"
|
|
50
|
+
: ` (${toolCount} tool${toolCount === 1 ? "" : "s"}, ${resourceCount} resource${resourceCount === 1 ? "" : "s"}${status === "cached" ? ", cached" : ""})`;
|
|
48
51
|
lines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);
|
|
49
52
|
}
|
|
50
53
|
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
@@ -96,17 +99,17 @@ export async function showTools(state, ctx) {
|
|
|
96
99
|
return;
|
|
97
100
|
const allTools = [...state.toolMetadata.entries()]
|
|
98
101
|
.filter(([serverName]) => !isServerDisabled(state.config.mcpServers[serverName]))
|
|
99
|
-
.flatMap(([, metadata]) => metadata.map(
|
|
102
|
+
.flatMap(([, metadata]) => metadata.filter(tool => tool.resourceUri === undefined).map(tool => tool.name));
|
|
100
103
|
if (allTools.length === 0) {
|
|
101
|
-
ctx.ui.notify("No MCP tools
|
|
104
|
+
ctx.ui.notify("No MCP tools discovered. Use mcp_search to discover tools.", "info");
|
|
102
105
|
return;
|
|
103
106
|
}
|
|
104
107
|
const lines = [
|
|
105
|
-
"MCP Tools:",
|
|
108
|
+
"Discovered MCP Tools:",
|
|
106
109
|
"",
|
|
107
110
|
...allTools.map(t => ` ${t}`),
|
|
108
111
|
"",
|
|
109
|
-
`Total: ${allTools.length}
|
|
112
|
+
`Total: ${allTools.length} tool${allTools.length === 1 ? "" : "s"}`,
|
|
110
113
|
];
|
|
111
114
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
112
115
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// config.ts - Config loading with import support
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync, renameSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync, renameSync, statSync, chmodSync } from "node:fs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
7
|
import { parse as parseToml } from "smol-toml";
|
|
6
8
|
import stripJsonComments from "strip-json-comments";
|
|
7
9
|
import { getAdapterPath } from "./agent-dir.js";
|
|
@@ -367,6 +369,14 @@ function mergeServerMaps(base, next) {
|
|
|
367
369
|
delete baseEntry.oauth;
|
|
368
370
|
}
|
|
369
371
|
}
|
|
372
|
+
// Stdio env belongs to the program that received it, not just the server name.
|
|
373
|
+
if (existing?.command && ((definition.command !== undefined && definition.command !== existing.command)
|
|
374
|
+
|| (definition.args !== undefined && !isDeepStrictEqual(definition.args, existing.args ?? []))
|
|
375
|
+
|| (definition.cwd !== undefined && definition.cwd !== existing.cwd))) {
|
|
376
|
+
if (baseEntry === existing)
|
|
377
|
+
baseEntry = { ...existing };
|
|
378
|
+
delete baseEntry.env;
|
|
379
|
+
}
|
|
370
380
|
merged[name] = { ...baseEntry, ...definition };
|
|
371
381
|
}
|
|
372
382
|
return merged;
|
|
@@ -533,8 +543,10 @@ function mergeOpenCodeConfigs(base, next) {
|
|
|
533
543
|
delete safeBase.cwd;
|
|
534
544
|
}
|
|
535
545
|
}
|
|
546
|
+
if (Object.hasOwn(override, "cwd") && override.cwd !== safeBase.cwd)
|
|
547
|
+
delete safeBase.environment;
|
|
536
548
|
const mergedEntry = { ...safeBase, ...override };
|
|
537
|
-
for (const field of ["
|
|
549
|
+
for (const field of ["headers", "oauth"]) {
|
|
538
550
|
const baseField = safeBase[field];
|
|
539
551
|
const nextField = override[field];
|
|
540
552
|
if (baseField && typeof baseField === "object" && !Array.isArray(baseField)
|
|
@@ -714,24 +726,37 @@ function buildConfigWritePreview(filePath, nextRaw) {
|
|
|
714
726
|
function readRawConfigObject(filePath) {
|
|
715
727
|
if (!existsSync(filePath))
|
|
716
728
|
return {};
|
|
729
|
+
let raw;
|
|
717
730
|
try {
|
|
718
|
-
|
|
719
|
-
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
731
|
+
raw = parseJsonConfig(readFileSync(filePath, "utf-8"));
|
|
720
732
|
}
|
|
721
|
-
catch {
|
|
722
|
-
|
|
733
|
+
catch (error) {
|
|
734
|
+
throw new Error(`Cannot read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
735
|
+
}
|
|
736
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
737
|
+
throw new Error(`Invalid MCP config at ${filePath}: expected an object`);
|
|
723
738
|
}
|
|
739
|
+
return raw;
|
|
724
740
|
}
|
|
725
741
|
function writeRawConfigObject(filePath, raw) {
|
|
726
742
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
727
|
-
const
|
|
728
|
-
|
|
743
|
+
const existingMode = existsSync(filePath) ? statSync(filePath).mode & 0o7777 : undefined;
|
|
744
|
+
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
745
|
+
writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, {
|
|
746
|
+
encoding: "utf-8",
|
|
747
|
+
mode: existingMode ?? 0o666,
|
|
748
|
+
flag: "wx",
|
|
749
|
+
});
|
|
750
|
+
if (existingMode !== undefined)
|
|
751
|
+
chmodSync(tmpPath, existingMode);
|
|
729
752
|
renameSync(tmpPath, filePath);
|
|
730
753
|
}
|
|
731
|
-
function getServersObject(raw) {
|
|
732
|
-
const existing = raw.mcpServers
|
|
754
|
+
function getServersObject(raw, filePath) {
|
|
755
|
+
const existing = raw.mcpServers !== undefined ? raw.mcpServers
|
|
756
|
+
: raw["mcp-servers"] !== undefined ? raw["mcp-servers"]
|
|
757
|
+
: {};
|
|
733
758
|
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
734
|
-
|
|
759
|
+
throw new Error(`Invalid MCP config at ${filePath}: mcpServers must be an object`);
|
|
735
760
|
}
|
|
736
761
|
return existing;
|
|
737
762
|
}
|
|
@@ -886,7 +911,7 @@ export function previewCompatibilityImports(importKinds, overridePath) {
|
|
|
886
911
|
const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : [];
|
|
887
912
|
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
888
913
|
const nextRaw = { ...raw, imports: merged };
|
|
889
|
-
setServersObject(nextRaw, getServersObject(nextRaw));
|
|
914
|
+
setServersObject(nextRaw, getServersObject(nextRaw, targetPath));
|
|
890
915
|
return buildConfigWritePreview(targetPath, nextRaw);
|
|
891
916
|
}
|
|
892
917
|
export function ensureCompatibilityImports(importKinds, overridePath) {
|
|
@@ -899,7 +924,7 @@ export function ensureCompatibilityImports(importKinds, overridePath) {
|
|
|
899
924
|
return { path: targetPath, added: [] };
|
|
900
925
|
}
|
|
901
926
|
raw.imports = merged;
|
|
902
|
-
const servers = getServersObject(raw);
|
|
927
|
+
const servers = getServersObject(raw, targetPath);
|
|
903
928
|
setServersObject(raw, servers);
|
|
904
929
|
writeRawConfigObject(targetPath, raw);
|
|
905
930
|
return { path: targetPath, added };
|
|
@@ -923,14 +948,14 @@ export function writeStarterProjectConfig(cwd = process.cwd()) {
|
|
|
923
948
|
export function previewSharedServerEntry(filePath, serverName, entry) {
|
|
924
949
|
const raw = readRawConfigObject(filePath);
|
|
925
950
|
const nextRaw = { ...raw };
|
|
926
|
-
const servers = getServersObject(nextRaw);
|
|
951
|
+
const servers = getServersObject(nextRaw, filePath);
|
|
927
952
|
servers[serverName] = entry;
|
|
928
953
|
setServersObject(nextRaw, servers);
|
|
929
954
|
return buildConfigWritePreview(filePath, nextRaw);
|
|
930
955
|
}
|
|
931
956
|
export function writeSharedServerEntry(filePath, serverName, entry) {
|
|
932
957
|
const raw = readRawConfigObject(filePath);
|
|
933
|
-
const servers = getServersObject(raw);
|
|
958
|
+
const servers = getServersObject(raw, filePath);
|
|
934
959
|
servers[serverName] = entry;
|
|
935
960
|
setServersObject(raw, servers);
|
|
936
961
|
writeRawConfigObject(filePath, raw);
|
|
@@ -991,7 +1016,7 @@ export function writeDirectToolsConfig(changes, provenance) {
|
|
|
991
1016
|
}
|
|
992
1017
|
for (const [filePath, entries] of byPath) {
|
|
993
1018
|
const raw = readRawConfigObject(filePath);
|
|
994
|
-
const servers = getServersObject(raw);
|
|
1019
|
+
const servers = getServersObject(raw, filePath);
|
|
995
1020
|
for (const { name, value } of entries) {
|
|
996
1021
|
// Keep inherited connection details in their source file.
|
|
997
1022
|
servers[name] = { ...servers[name], directTools: value };
|
package/dist/direct-tools.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { McpExtensionState } from "./state.ts";
|
|
3
|
-
import type { DirectToolSpec, McpAdapterOptions, McpConfig
|
|
4
|
-
import type { MetadataCache } from "./metadata-cache.ts";
|
|
3
|
+
import type { DirectToolSpec, McpAdapterOptions, McpConfig } from "./types.ts";
|
|
5
4
|
export { getMissingConfiguredDirectToolServers } from "./metadata-cache.ts";
|
|
6
|
-
export declare
|
|
7
|
-
export declare function resolveDirectTools(config: McpConfig, cache: MetadataCache | null, prefix: ToolPrefix, envOverride?: string[]): DirectToolSpec[];
|
|
5
|
+
export declare function directToolSelection(config: McpConfig, serverName: string, envOverride?: string[]): true | string[] | false;
|
|
8
6
|
export declare function buildProxyDescription(config: McpConfig): string;
|
|
9
7
|
type DirectToolExecute = (toolCallId: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Record<string, unknown>>>;
|
|
10
8
|
export declare function createDirectToolExecutor(getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, spec: DirectToolSpec, beforeExecute?: McpAdapterOptions["beforeExecute"]): DirectToolExecute;
|
package/dist/direct-tools.js
CHANGED
|
@@ -1,204 +1,25 @@
|
|
|
1
|
-
import { lazyConnect, getFailureAgeSeconds, clearFailure, recordFailure, updateStatusBar } from "./init.js";
|
|
2
1
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
3
|
-
import {
|
|
2
|
+
import { parseDirectToolSelectors } from "./metadata-cache.js";
|
|
4
3
|
export { getMissingConfiguredDirectToolServers } from "./metadata-cache.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const advisedDirectToolSets = new Set();
|
|
16
|
-
function getDirectAuthRequiredMessage(state, serverName, defaultMessage = `MCP server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`) {
|
|
17
|
-
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
18
|
-
if (oauth && oauth.crossAppAccess)
|
|
19
|
-
defaultMessage = `MCP server "${serverName}" requires enterprise authorization. Check oauth.crossAppAccess IdP configuration and ID-token source, then retry.`;
|
|
20
|
-
return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
|
|
21
|
-
}
|
|
22
|
-
function getDirectAuthFailedMessage(state, serverName, message) {
|
|
23
|
-
const customGuidance = state.config.settings?.authRequiredMessage;
|
|
24
|
-
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
25
|
-
if (customGuidance || (oauth && oauth.crossAppAccess)) {
|
|
26
|
-
return `OAuth authentication failed for "${serverName}": ${message}. ${getDirectAuthRequiredMessage(state, serverName)}`;
|
|
27
|
-
}
|
|
28
|
-
return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
29
|
-
}
|
|
30
|
-
async function attemptDirectAutoAuth(state, serverName, signal) {
|
|
31
|
-
if (state.config.settings?.autoAuth !== true) {
|
|
32
|
-
return { status: "skipped" };
|
|
33
|
-
}
|
|
34
|
-
const definition = state.config.mcpServers[serverName];
|
|
35
|
-
if (!definition || isServerDisabled(definition) || !supportsOAuth(definition)) {
|
|
36
|
-
return { status: "skipped" };
|
|
37
|
-
}
|
|
38
|
-
let serverUrl;
|
|
39
|
-
try {
|
|
40
|
-
serverUrl = resolveServerUrl(definition);
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
-
return { status: "failed", message: getDirectAuthFailedMessage(state, serverName, message) };
|
|
45
|
-
}
|
|
46
|
-
if (!serverUrl) {
|
|
47
|
-
return { status: "skipped" };
|
|
48
|
-
}
|
|
49
|
-
if (!state.ui && !isNonInteractiveOAuth(definition.oauth)) {
|
|
50
|
-
return {
|
|
51
|
-
status: "failed",
|
|
52
|
-
message: getDirectAuthRequiredMessage(state, serverName, `MCP server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
if (state.authStorageOptions) {
|
|
57
|
-
await authenticate(serverName, serverUrl, definition, signal
|
|
58
|
-
? { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime }
|
|
59
|
-
: { authStorageOptions: state.authStorageOptions, runtime: state.oauthRuntime });
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
await authenticate(serverName, serverUrl, definition, {
|
|
63
|
-
...(signal ? { signal } : {}),
|
|
64
|
-
runtime: state.oauthRuntime,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
return { status: "success" };
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
if (isAbortError(error, signal))
|
|
71
|
-
throw error;
|
|
72
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
-
return {
|
|
74
|
-
status: "failed",
|
|
75
|
-
message: getDirectAuthFailedMessage(state, serverName, message),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
export function resolveDirectTools(config, cache, prefix, envOverride) {
|
|
80
|
-
const specs = [];
|
|
81
|
-
if (!cache)
|
|
82
|
-
return specs;
|
|
83
|
-
const seenNames = new Set();
|
|
84
|
-
const envSelection = envOverride ? parseDirectToolSelectors(envOverride) : null;
|
|
85
|
-
const globalDirect = config.settings?.directTools;
|
|
86
|
-
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
87
|
-
if (isServerDisabled(definition))
|
|
88
|
-
continue;
|
|
89
|
-
const serverCache = cache.servers[serverName];
|
|
90
|
-
if (!serverCache || !isServerCacheValid(serverCache, definition))
|
|
91
|
-
continue;
|
|
92
|
-
let toolFilter = false;
|
|
93
|
-
if (envSelection) {
|
|
94
|
-
if (envSelection.servers.has(serverName)) {
|
|
95
|
-
toolFilter = true;
|
|
96
|
-
}
|
|
97
|
-
else if (envSelection.tools.has(serverName)) {
|
|
98
|
-
toolFilter = [...envSelection.tools.get(serverName)];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
if (definition.directTools !== undefined) {
|
|
103
|
-
toolFilter = definition.directTools;
|
|
104
|
-
}
|
|
105
|
-
else if (globalDirect) {
|
|
106
|
-
toolFilter = globalDirect;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (!toolFilter)
|
|
110
|
-
continue;
|
|
111
|
-
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
112
|
-
for (const tool of serverCache.tools ?? []) {
|
|
113
|
-
if (toolFilter !== true && !toolFilter.includes(tool.name))
|
|
114
|
-
continue;
|
|
115
|
-
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools))
|
|
116
|
-
continue;
|
|
117
|
-
const prefixedName = formatToolName(tool.name, serverName, effectivePrefix);
|
|
118
|
-
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
119
|
-
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (seenNames.has(prefixedName)) {
|
|
123
|
-
console.warn(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
seenNames.add(prefixedName);
|
|
127
|
-
specs.push({
|
|
128
|
-
serverName,
|
|
129
|
-
originalName: tool.name,
|
|
130
|
-
prefixedName,
|
|
131
|
-
description: tool.description ?? "",
|
|
132
|
-
...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
|
|
133
|
-
...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
|
|
134
|
-
...(tool.uiResourceUri !== undefined ? { uiResourceUri: tool.uiResourceUri } : {}),
|
|
135
|
-
...(tool.uiStreamMode !== undefined ? { uiStreamMode: tool.uiStreamMode } : {}),
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
if (definition.exposeResources !== false) {
|
|
139
|
-
for (const resource of serverCache.resources ?? []) {
|
|
140
|
-
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
141
|
-
if (toolFilter !== true && !toolFilter.includes(baseName))
|
|
142
|
-
continue;
|
|
143
|
-
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools))
|
|
144
|
-
continue;
|
|
145
|
-
const prefixedName = formatToolName(baseName, serverName, effectivePrefix);
|
|
146
|
-
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
147
|
-
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (seenNames.has(prefixedName)) {
|
|
151
|
-
console.warn(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`);
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
seenNames.add(prefixedName);
|
|
155
|
-
specs.push({
|
|
156
|
-
serverName,
|
|
157
|
-
originalName: baseName,
|
|
158
|
-
prefixedName,
|
|
159
|
-
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
160
|
-
resourceUri: resource.uri,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const advisoryKey = specs.map(spec => spec.prefixedName).sort().join("\0");
|
|
166
|
-
if (specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD && !advisedDirectToolSets.has(advisoryKey)) {
|
|
167
|
-
advisedDirectToolSets.add(advisoryKey);
|
|
168
|
-
console.warn(`MCP: ${specs.length} direct tools resolved. Each direct tool adds prompt context; README guidance recommends targeted sets of 5-20 tools and using the proxy or an explicit string[] when 75+ direct tools would be registered.`);
|
|
169
|
-
}
|
|
170
|
-
return specs;
|
|
4
|
+
import { executeCall } from "./proxy-modes.js";
|
|
5
|
+
import { isServerDisabled } from "./types.js";
|
|
6
|
+
export function directToolSelection(config, serverName, envOverride) {
|
|
7
|
+
if (envOverride) {
|
|
8
|
+
const selection = parseDirectToolSelectors(envOverride);
|
|
9
|
+
if (selection.servers.has(serverName))
|
|
10
|
+
return true;
|
|
11
|
+
return selection.tools.has(serverName) ? [...selection.tools.get(serverName)] : false;
|
|
12
|
+
}
|
|
13
|
+
return config.mcpServers[serverName]?.directTools ?? config.settings?.directTools ?? false;
|
|
171
14
|
}
|
|
172
15
|
export function buildProxyDescription(config) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
if (enabledServers.length > 0) {
|
|
181
|
-
desc += `\nConfigured servers (call mcp({}) for live status): ${enabledServers.join(", ")}\n`;
|
|
182
|
-
}
|
|
183
|
-
const disabledServers = Object.entries(config.mcpServers)
|
|
184
|
-
.filter(([, definition]) => isServerDisabled(definition))
|
|
185
|
-
.map(([serverName]) => serverName);
|
|
186
|
-
if (disabledServers.length > 0) {
|
|
187
|
-
desc += `\nDisabled servers (enable with /mcp enable <server> and /reload): ${disabledServers.join(", ")}\n`;
|
|
188
|
-
}
|
|
189
|
-
desc += `\nUsage:\n`;
|
|
190
|
-
desc += ` mcp({ }) → Show server status\n`;
|
|
191
|
-
desc += ` mcp({ server: "name", limit: 12 }) → Browse a server's tools (use offset for more)\n`;
|
|
192
|
-
desc += ` mcp({ search: "query" }) → Search MCP tools by name/description\n`;
|
|
193
|
-
desc += ` mcp({ describe: "tool_name" }) → Show tool details and parameters\n`;
|
|
194
|
-
desc += ` mcp({ instructions: "name" }) → Show full server usage instructions\n`;
|
|
195
|
-
desc += ` mcp({ connect: "server-name" }) → Connect to a server and refresh metadata\n`;
|
|
196
|
-
desc += ` mcp({ tool: "name", args: { key: "value" } }) → Call a tool (object args; JSON string also accepted)\n`;
|
|
197
|
-
desc += ` mcp({ action: "ui-messages" }) → Retrieve accumulated messages from completed UI sessions\n`;
|
|
198
|
-
desc += ` mcp({ action: "auth-start", server: "name" }) → Start manual OAuth and get a browser URL\n`;
|
|
199
|
-
desc += ` mcp({ action: "auth-complete", server: "name", args: { redirectUrl: "..." } }) → Complete manual OAuth\n`;
|
|
200
|
-
desc += `\nMode: action > tool (call) > connect > describe > instructions > search > server (list) > nothing (status)`;
|
|
201
|
-
return desc;
|
|
16
|
+
const servers = Object.entries(config.mcpServers).filter(([, definition]) => !isServerDisabled(definition)).map(([name]) => name).sort();
|
|
17
|
+
return [
|
|
18
|
+
"MCP gateway. Use mcp_search to discover and load typed tools; mcp_script for multi-call composition. Native Pi tools are called directly.",
|
|
19
|
+
`Configured servers: ${servers.join(", ") || "none"}. Catalog discovery is lazy.`,
|
|
20
|
+
"Actions: status; list/search/describe; call (tool,args,server?); connect (server); instructions (server); resources (server); read-resource (server,uri); read-result (ref,path?,fields?,offset?,limit?); auth-start/auth-complete (server,args?); ui-messages.",
|
|
21
|
+
"Use call when a discovered direct tool is unavailable in this host. read-result reads a retained response without repeating the remote operation.",
|
|
22
|
+
].join("\n");
|
|
202
23
|
}
|
|
203
24
|
export function createDirectToolExecutor(getState, getInitPromise, spec, beforeExecute) {
|
|
204
25
|
return async function execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -224,127 +45,8 @@ export function createDirectToolExecutor(getState, getInitPromise, spec, beforeE
|
|
|
224
45
|
details: { error: "not_initialized" },
|
|
225
46
|
};
|
|
226
47
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
content: [{ type: "text", text: message }],
|
|
232
|
-
details: { error: "server_disabled", server: spec.serverName, message },
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
236
|
-
throwIfAborted(ownedSignal);
|
|
237
|
-
let connected = await lazyConnect(state, spec.serverName, ownedSignal);
|
|
238
|
-
let autoAuthAttempted = false;
|
|
239
|
-
const needsAuthConnection = state.manager.getConnection(spec.serverName);
|
|
240
|
-
if (!connected && needsAuthConnection?.status === "needs-auth") {
|
|
241
|
-
autoAuthAttempted = true;
|
|
242
|
-
const autoAuth = await attemptDirectAutoAuth(state, spec.serverName, ownedSignal);
|
|
243
|
-
if (autoAuth.status === "failed") {
|
|
244
|
-
return {
|
|
245
|
-
content: [{ type: "text", text: autoAuth.message }],
|
|
246
|
-
details: { error: "auth_required", server: spec.serverName, message: autoAuth.message },
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
if (autoAuth.status === "success") {
|
|
250
|
-
clearFailure(state, spec.serverName);
|
|
251
|
-
try {
|
|
252
|
-
const liveDefinition = state.config.mcpServers[spec.serverName];
|
|
253
|
-
if (liveDefinition) {
|
|
254
|
-
await state.manager.reconnect(spec.serverName, liveDefinition, needsAuthConnection, ownedSignal);
|
|
255
|
-
connected = true;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
if (isAbortError(error, ownedSignal))
|
|
260
|
-
throwIfAborted(ownedSignal);
|
|
261
|
-
recordFailure(state, spec.serverName, error instanceof Error ? error.message : String(error));
|
|
262
|
-
updateStatusBar(state);
|
|
263
|
-
}
|
|
264
|
-
if (connected)
|
|
265
|
-
connected = await lazyConnect(state, spec.serverName, ownedSignal);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (!connected) {
|
|
269
|
-
const authConnection = state.manager.getConnection(spec.serverName);
|
|
270
|
-
if (authConnection?.status === "needs-auth") {
|
|
271
|
-
const message = getDirectAuthRequiredMessage(state, spec.serverName);
|
|
272
|
-
return {
|
|
273
|
-
content: [{ type: "text", text: message }],
|
|
274
|
-
details: { error: "auth_required", server: spec.serverName, message, autoAuthAttempted },
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
const failedAgo = getFailureAgeSeconds(state, spec.serverName);
|
|
278
|
-
return {
|
|
279
|
-
content: [{ type: "text", text: `MCP server "${spec.serverName}" not available${failedAgo !== null ? ` (failed ${failedAgo}s ago)` : ""}` }],
|
|
280
|
-
details: { error: "server_unavailable", server: spec.serverName },
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
const connection = state.manager.getConnection(spec.serverName);
|
|
284
|
-
if (!connection || connection.status !== "connected") {
|
|
285
|
-
return {
|
|
286
|
-
content: [{ type: "text", text: `MCP server "${spec.serverName}" not connected` }],
|
|
287
|
-
details: { error: "not_connected", server: spec.serverName },
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
const approval = await ensureToolCallApproved(state, spec.serverName, {
|
|
291
|
-
name: spec.prefixedName,
|
|
292
|
-
originalName: spec.originalName,
|
|
293
|
-
description: spec.description,
|
|
294
|
-
...(spec.inputSchema !== undefined ? { inputSchema: spec.inputSchema } : {}),
|
|
295
|
-
...(spec.resourceUri !== undefined ? { resourceUri: spec.resourceUri } : {}),
|
|
296
|
-
...(spec.uiResourceUri !== undefined ? { uiResourceUri: spec.uiResourceUri } : {}),
|
|
297
|
-
...(spec.uiStreamMode !== undefined ? { uiStreamMode: spec.uiStreamMode } : {}),
|
|
298
|
-
}, params, ownedSignal);
|
|
299
|
-
if (approval.ok === false) {
|
|
300
|
-
const denied = approval.reason === "denied";
|
|
301
|
-
const message = denied
|
|
302
|
-
? `The user declined approval to run MCP tool "${spec.originalName}" on server "${spec.serverName}".`
|
|
303
|
-
: `MCP tool "${spec.originalName}" on server "${spec.serverName}" is approval-gated and requires an interactive session.`;
|
|
304
|
-
return {
|
|
305
|
-
content: [{ type: "text", text: message }],
|
|
306
|
-
details: {
|
|
307
|
-
error: denied ? "approval_denied" : "approval_required",
|
|
308
|
-
server: spec.serverName,
|
|
309
|
-
tool: spec.originalName,
|
|
310
|
-
},
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
const recoverAuthConnection = async (_serverName, recoverySignal = ownedSignal, challenge) => {
|
|
314
|
-
throwIfAborted(recoverySignal);
|
|
315
|
-
const current = state.manager.getConnection(spec.serverName);
|
|
316
|
-
if (current?.status === "connected" && current !== challenge?.connection)
|
|
317
|
-
return current;
|
|
318
|
-
if (!autoAuthAttempted) {
|
|
319
|
-
autoAuthAttempted = true;
|
|
320
|
-
const autoAuth = await attemptDirectAutoAuth(state, spec.serverName, recoverySignal);
|
|
321
|
-
throwIfAborted(recoverySignal);
|
|
322
|
-
if (autoAuth.status === "failed") {
|
|
323
|
-
throw new SessionRecoveryAuthRequiredError(spec.serverName, autoAuth.message);
|
|
324
|
-
}
|
|
325
|
-
if (autoAuth.status === "success") {
|
|
326
|
-
const liveDefinition = state.config.mcpServers[spec.serverName];
|
|
327
|
-
if (!current || !liveDefinition)
|
|
328
|
-
return undefined;
|
|
329
|
-
await state.manager.reconnect(spec.serverName, liveDefinition, current, recoverySignal);
|
|
330
|
-
clearFailure(state, spec.serverName);
|
|
331
|
-
const reconnected = await lazyConnect(state, spec.serverName, recoverySignal);
|
|
332
|
-
return reconnected ? state.manager.getConnection(spec.serverName) : undefined;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return challenge ? undefined : state.manager.getConnection(spec.serverName);
|
|
336
|
-
};
|
|
337
|
-
return runToolCall(state, spec.serverName, spec, params, {
|
|
338
|
-
toolCallId,
|
|
339
|
-
...(beforeExecute ? { beforeDispatch: (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) } : {}),
|
|
340
|
-
detailsBase: spec.resourceUri
|
|
341
|
-
? { server: spec.serverName, resourceUri: spec.resourceUri }
|
|
342
|
-
: { server: spec.serverName, tool: spec.originalName },
|
|
343
|
-
ownedSignal,
|
|
344
|
-
signal,
|
|
345
|
-
recoverAuthConnection,
|
|
346
|
-
authRequiredMessage: () => getDirectAuthRequiredMessage(state, spec.serverName),
|
|
347
|
-
autoAuthAttempted: () => autoAuthAttempted,
|
|
348
|
-
});
|
|
48
|
+
const result = await executeCall(state, spec.originalName, params, spec.serverName, undefined, signal, { toolCallId }, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined, { exactOriginalName: true, ...(spec.resourceUri ? { resourceUri: spec.resourceUri } : {}) });
|
|
49
|
+
const { mode: _mode, ...details } = result.details;
|
|
50
|
+
return { ...result, details };
|
|
349
51
|
};
|
|
350
52
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
declare const actions: readonly ["status", "search", "list", "describe", "call", "connect", "instructions", "resources", "read-resource", "read-result", "auth-start", "auth-complete", "ui-messages"];
|
|
3
|
+
export type GatewayArguments = {
|
|
4
|
+
action: typeof actions[number];
|
|
5
|
+
server?: string;
|
|
6
|
+
tool?: string;
|
|
7
|
+
args?: Record<string, unknown>;
|
|
8
|
+
query?: string;
|
|
9
|
+
includeSchemas?: boolean;
|
|
10
|
+
limit?: number;
|
|
11
|
+
offset?: number;
|
|
12
|
+
uri?: string;
|
|
13
|
+
ref?: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
fields?: string[];
|
|
16
|
+
};
|
|
17
|
+
export declare const gatewayParameters: Type.TObject<{
|
|
18
|
+
action: Type.TUnsafe<"auth-complete" | "auth-start" | "call" | "connect" | "describe" | "instructions" | "list" | "read-resource" | "read-result" | "resources" | "search" | "status" | "ui-messages">;
|
|
19
|
+
server: Type.TOptional<Type.TString>;
|
|
20
|
+
tool: Type.TOptional<Type.TString>;
|
|
21
|
+
args: Type.TOptional<Type.TObject<{}>>;
|
|
22
|
+
query: Type.TOptional<Type.TString>;
|
|
23
|
+
includeSchemas: Type.TOptional<Type.TBoolean>;
|
|
24
|
+
limit: Type.TOptional<Type.TInteger>;
|
|
25
|
+
offset: Type.TOptional<Type.TInteger>;
|
|
26
|
+
uri: Type.TOptional<Type.TString>;
|
|
27
|
+
ref: Type.TOptional<Type.TString>;
|
|
28
|
+
path: Type.TOptional<Type.TString>;
|
|
29
|
+
fields: Type.TOptional<Type.TArray<Type.TString>>;
|
|
30
|
+
}>;
|
|
31
|
+
/** Migrate stored v5 calls before host validation; the advertised schema stays explicit. */
|
|
32
|
+
export declare function prepareGatewayArguments(input: unknown): GatewayArguments;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { MAX_PAGE_SIZE, MAX_TOOL_NAME_LENGTH } from "./search-ranking.js";
|
|
3
|
+
const actions = ["status", "search", "list", "describe", "call", "connect", "instructions", "resources", "read-resource", "read-result", "auth-start", "auth-complete", "ui-messages"];
|
|
4
|
+
const requirements = {
|
|
5
|
+
search: ["query"], list: ["server"], describe: ["tool"], call: ["tool"],
|
|
6
|
+
connect: ["server"], instructions: ["server"], resources: ["server"],
|
|
7
|
+
"read-resource": ["server", "uri"], "read-result": ["ref"], "auth-start": ["server"], "auth-complete": ["server"],
|
|
8
|
+
};
|
|
9
|
+
export const gatewayParameters = Type.Object({
|
|
10
|
+
action: Type.Unsafe({ type: "string", enum: [...actions] }),
|
|
11
|
+
server: Type.Optional(Type.String({ description: "MCP server; required for server and resource actions." })),
|
|
12
|
+
tool: Type.Optional(Type.String({ maxLength: MAX_TOOL_NAME_LENGTH, description: "Exact tool name for call or describe. Use server to disambiguate." })),
|
|
13
|
+
args: Type.Optional(Type.Object({}, { additionalProperties: true, description: "Tool arguments, or {redirectUrl} for auth-complete." })),
|
|
14
|
+
query: Type.Optional(Type.String({ description: "Search terms. Prefer mcp_search to discover and load tools." })),
|
|
15
|
+
includeSchemas: Type.Optional(Type.Boolean({ description: "Include complete schemas in search results (default true)." })),
|
|
16
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, description: "Page size (default 12, max 100); for read-result, characters of selected JSON." })),
|
|
17
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based item offset; for read-result, character offset in selected JSON." })),
|
|
18
|
+
uri: Type.Optional(Type.String({ description: "Exact MCP resource URI for read-resource." })),
|
|
19
|
+
ref: Type.Optional(Type.String({ description: "Saved result reference returned by an earlier MCP call; read-result never calls the server again." })),
|
|
20
|
+
path: Type.Optional(Type.String({ description: "JSON Pointer into a saved result, e.g. /structuredContent/rows." })),
|
|
21
|
+
fields: Type.Optional(Type.Array(Type.String(), { description: "Immediate object fields to retain when reading a saved result." })),
|
|
22
|
+
}, {
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
allOf: [
|
|
25
|
+
...Object.entries(requirements).map(([action, required]) => ({ if: { properties: { action: { const: action } } }, then: { required } })),
|
|
26
|
+
{ if: { properties: { action: { enum: ["search", "list", "connect", "resources"] } } }, then: { properties: { limit: { maximum: MAX_PAGE_SIZE } } } },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
/** Migrate stored v5 calls before host validation; the advertised schema stays explicit. */
|
|
30
|
+
export function prepareGatewayArguments(input) {
|
|
31
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
32
|
+
throw new Error("MCP arguments must be an object");
|
|
33
|
+
const value = input;
|
|
34
|
+
let action = value.action;
|
|
35
|
+
const result = { ...value };
|
|
36
|
+
if (action === undefined) {
|
|
37
|
+
action = value.tool ? "call" : value.connect ? "connect" : value.describe ? "describe" : value.instructions ? "instructions" : value.search !== undefined ? "search" : value.server ? "list" : "status";
|
|
38
|
+
if (action === "connect")
|
|
39
|
+
result.server = value.connect;
|
|
40
|
+
if (action === "describe")
|
|
41
|
+
result.tool = value.describe;
|
|
42
|
+
if (action === "instructions")
|
|
43
|
+
result.server = value.instructions;
|
|
44
|
+
if (action === "search")
|
|
45
|
+
result.query = value.search;
|
|
46
|
+
}
|
|
47
|
+
for (const key of ["connect", "describe", "instructions", "search"])
|
|
48
|
+
delete result[key];
|
|
49
|
+
if (!actions.includes(action))
|
|
50
|
+
throw new Error(`Unknown MCP action: ${String(action)}`);
|
|
51
|
+
result.action = action;
|
|
52
|
+
if (typeof result.args === "string") {
|
|
53
|
+
result.args = result.args === "" ? undefined : JSON.parse(result.args);
|
|
54
|
+
}
|
|
55
|
+
if (result.args !== undefined && (!result.args || typeof result.args !== "object" || Array.isArray(result.args))) {
|
|
56
|
+
throw new Error("Invalid args: expected a JSON object");
|
|
57
|
+
}
|
|
58
|
+
for (const field of requirements[action] ?? []) {
|
|
59
|
+
if (typeof result[field] !== "string" || (field !== "query" && result[field] === ""))
|
|
60
|
+
throw new Error(`${action} requires ${field}`);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|