pi-mcp-adapter 3.3.0 → 4.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 +51 -0
- package/OAUTH.md +4 -2
- package/README.md +61 -9
- package/commands.ts +20 -2
- package/config.ts +19 -1
- package/direct-tool-surface.ts +3 -2
- package/direct-tools.ts +5 -3
- package/dist/config.d.ts +6 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/mcp-auth-flow.d.ts +2 -0
- package/dist/mcp-auth-flow.js +12 -3
- package/dist/mcp-auth-flow.js.map +1 -1
- package/dist/metadata-cache.d.ts +12 -0
- package/dist/metadata-cache.js +45 -5
- package/dist/metadata-cache.js.map +1 -1
- package/dist/output-shape.d.ts +18 -5
- package/dist/output-shape.js +162 -18
- package/dist/output-shape.js.map +1 -1
- package/dist/package-mcp-loader.js +9 -5
- package/dist/package-mcp-loader.js.map +1 -1
- package/dist/server-manager.js +27 -16
- package/dist/server-manager.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/types.d.ts +11 -1
- package/dist/types.js.map +1 -1
- package/index.ts +72 -12
- package/init.ts +20 -5
- package/mcp-auth-flow.ts +13 -2
- package/mcp-code.ts +125 -77
- package/mcp-output-guard.ts +24 -5
- package/mcp-script-worker.mjs +6 -2
- package/mcp-setup-panel.ts +583 -302
- package/metadata-cache.ts +55 -5
- package/output-shape.ts +161 -22
- package/package-mcp-loader.ts +8 -5
- package/package.json +5 -5
- package/proxy-modes.ts +125 -109
- package/server-manager.ts +28 -15
- package/skills/mcp-scripting/SKILL.md +24 -30
- package/skills/mcp-scripting/references/jev.md +3 -0
- package/state.ts +2 -0
- package/types.ts +8 -1
package/proxy-modes.ts
CHANGED
|
@@ -9,17 +9,17 @@ import { abortable, throwIfAborted } from "./abort.ts";
|
|
|
9
9
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
|
|
10
10
|
import { buildToolMetadata, getToolNames, formatSchema } from "./tool-metadata.ts";
|
|
11
11
|
import { renderTsShape } from "./ts-shape.ts";
|
|
12
|
-
import { getObservedOutput,
|
|
12
|
+
import { getObservedOutput, observedOutputRecorder, renderOutputShape } from "./output-shape.ts";
|
|
13
13
|
import { reconstructPromptMetadata } from "./metadata-cache.ts";
|
|
14
14
|
import { resolveMcpResultContent, transformMcpResourceContents } from "./tool-registrar.ts";
|
|
15
|
-
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
15
|
+
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions, scriptPipeHint } from "./mcp-output-guard.ts";
|
|
16
16
|
import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime } from "./ui-session.ts";
|
|
17
17
|
import { formatAuthRequiredMessage, formatMcpStatus, normalizeToolArguments, resolveServerUrl, truncateAtWord, withToolCallIdMeta } from "./utils.ts";
|
|
18
18
|
import { authenticate, completeAuthFromInput, getAuthStatus, startAuth, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
19
19
|
import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.ts";
|
|
20
20
|
import { callToolViaTaskSession } from "./mcp-tasks.ts";
|
|
21
21
|
import { callToolPausingForElicitation } from "./elicitation-handler.ts";
|
|
22
|
-
import { paginate, rankSuggestions, rankToolMatches, resolveSearchKeywords } from "./search-ranking.ts";
|
|
22
|
+
import { paginate, rankSuggestions, rankToolMatches, resolveSearchKeywords, type RankedToolMatch } from "./search-ranking.ts";
|
|
23
23
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.ts";
|
|
24
24
|
import { describeFailure, isServerInActiveFailureBackoff } from "./failure-backoff.ts";
|
|
25
25
|
import { semanticSearch, type SemanticSearchBackend, type SemanticSearchEvaluator } from "./semantic-search.ts";
|
|
@@ -722,26 +722,28 @@ function formatToolHints(toolMeta: ToolMetadata): string {
|
|
|
722
722
|
return hints.join(", ");
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
-
export function
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
725
|
+
export function resolveDescribeTarget(
|
|
726
|
+
state: McpExtensionState,
|
|
727
|
+
toolName: string,
|
|
728
|
+
serverOverride?: string,
|
|
729
|
+
): { server: string; tool: ToolMetadata } | { error: ProxyToolResult } {
|
|
731
730
|
if (serverOverride) {
|
|
732
731
|
if (!state.config.mcpServers[serverOverride]) {
|
|
733
732
|
return {
|
|
734
|
-
|
|
735
|
-
|
|
733
|
+
error: {
|
|
734
|
+
content: [{ type: "text" as const, text: `Server "${serverOverride}" not found. Use mcp({}) to see available servers.` }],
|
|
735
|
+
details: { mode: "describe", error: "server_not_found", server: serverOverride, requestedTool: toolName },
|
|
736
|
+
},
|
|
736
737
|
};
|
|
737
738
|
}
|
|
738
739
|
const match = getServerScopedToolMatch(state.toolMetadata.get(serverOverride), toolName);
|
|
739
|
-
if (match === "ambiguous") return ambiguousServerToolResult("describe", toolName, serverOverride);
|
|
740
|
-
if (isServerDisabled(state.config.mcpServers[serverOverride])) return disabledResult(state, "describe", serverOverride);
|
|
741
|
-
if (isServerInActiveFailureBackoff(state, serverOverride)) return serverBackoffResult(state, "describe", serverOverride);
|
|
742
|
-
|
|
743
|
-
toolMeta = match?.tool;
|
|
740
|
+
if (match === "ambiguous") return { error: ambiguousServerToolResult("describe", toolName, serverOverride) };
|
|
741
|
+
if (isServerDisabled(state.config.mcpServers[serverOverride])) return { error: disabledResult(state, "describe", serverOverride) };
|
|
742
|
+
if (isServerInActiveFailureBackoff(state, serverOverride)) return { error: serverBackoffResult(state, "describe", serverOverride) };
|
|
743
|
+
if (match) return { server: serverOverride, tool: match.tool };
|
|
744
744
|
} else {
|
|
745
|
+
let disabledMatch: string | undefined;
|
|
746
|
+
let failedMatch: string | undefined;
|
|
745
747
|
const matches: Array<{ server: string; tool: ToolMetadata; precedence: number }> = [];
|
|
746
748
|
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
747
749
|
const candidates = getServerScopedToolCandidates(metadata, toolName);
|
|
@@ -759,27 +761,31 @@ export function executeDescribe(state: McpExtensionState, toolName: string, serv
|
|
|
759
761
|
if (matches.length > 0) {
|
|
760
762
|
const precedence = Math.min(...matches.map(match => match.precedence));
|
|
761
763
|
const bestMatches = matches.filter(match => match.precedence === precedence);
|
|
762
|
-
if (bestMatches.length > 1) return ambiguousToolResult("describe", toolName);
|
|
763
|
-
|
|
764
|
-
toolMeta = bestMatches[0]!.tool;
|
|
764
|
+
if (bestMatches.length > 1) return { error: ambiguousToolResult("describe", toolName) };
|
|
765
|
+
return { server: bestMatches[0]!.server, tool: bestMatches[0]!.tool };
|
|
765
766
|
}
|
|
767
|
+
if (disabledMatch) return { error: disabledResult(state, "describe", disabledMatch) };
|
|
768
|
+
if (failedMatch) return { error: serverBackoffResult(state, "describe", failedMatch) };
|
|
766
769
|
}
|
|
767
770
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
: `mcp({ search: "..." })`;
|
|
777
|
-
return {
|
|
771
|
+
const suggestions = rankSuggestions(state, toolName, 5, serverOverride);
|
|
772
|
+
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
773
|
+
const scopeText = serverOverride ? ` on server "${serverOverride}"` : "";
|
|
774
|
+
const searchHint = serverOverride
|
|
775
|
+
? `mcp({ search: "...", server: "${serverOverride}" })`
|
|
776
|
+
: `mcp({ search: "..." })`;
|
|
777
|
+
return {
|
|
778
|
+
error: {
|
|
778
779
|
content: [{ type: "text" as const, text: `Tool "${toolName}" not found${scopeText}. Use ${searchHint} to search.${suggestionText}` }],
|
|
779
780
|
details: { mode: "describe", error: "tool_not_found", server: serverOverride, requestedTool: toolName, suggestions },
|
|
780
|
-
}
|
|
781
|
-
}
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
782
784
|
|
|
785
|
+
export function executeDescribe(state: McpExtensionState, toolName: string, serverOverride?: string): ProxyToolResult {
|
|
786
|
+
const target = resolveDescribeTarget(state, toolName, serverOverride);
|
|
787
|
+
if ("error" in target) return target.error;
|
|
788
|
+
const { server: serverName, tool: toolMeta } = target;
|
|
783
789
|
const approvalMarker = isToolCallApprovalRequired(state.config, serverName, toolMeta, state.toolMetadata)
|
|
784
790
|
? " (requires approval)"
|
|
785
791
|
: "";
|
|
@@ -804,7 +810,7 @@ export function executeDescribe(state: McpExtensionState, toolName: string, serv
|
|
|
804
810
|
const observed = toolMeta.resourceUri ? undefined : getObservedOutput(state, serverName, toolMeta);
|
|
805
811
|
if (observed) {
|
|
806
812
|
const source = observed.source === "structuredContent" ? "structuredContent" : "JSON text result";
|
|
807
|
-
text += `\n\nObserved output (${source}, from
|
|
813
|
+
text += `\n\nObserved output (${source}, from earlier calls, not a contract):\n${renderOutputShape(observed.shape)}`;
|
|
808
814
|
}
|
|
809
815
|
|
|
810
816
|
return {
|
|
@@ -885,81 +891,66 @@ function renderSearchResults(
|
|
|
885
891
|
};
|
|
886
892
|
}
|
|
887
893
|
|
|
888
|
-
export
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
894
|
+
export interface ToolSearchInput {
|
|
895
|
+
query: string;
|
|
896
|
+
regex?: boolean | undefined;
|
|
897
|
+
server?: string | undefined;
|
|
898
|
+
searchMode?: unknown;
|
|
899
|
+
signal?: AbortSignal | undefined;
|
|
900
|
+
semanticEvaluator?: SemanticSearchEvaluator | undefined;
|
|
901
|
+
observedSources?: readonly string[];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Ranked matches, unpaginated; each surface pages and renders them its own way. */
|
|
905
|
+
export type ToolSearchOutcome =
|
|
906
|
+
| { matches: RankedToolMatch[]; backend?: SemanticSearchBackend }
|
|
907
|
+
| { error: ProxyToolResult };
|
|
908
|
+
|
|
909
|
+
export function findTools(state: McpExtensionState, input: ToolSearchInput): ToolSearchOutcome | Promise<ToolSearchOutcome> {
|
|
910
|
+
const { query, regex, server, searchMode = "lexical" } = input;
|
|
911
|
+
const failure = (text: string, details: Record<string, unknown>): ToolSearchOutcome => ({
|
|
912
|
+
error: { content: [{ type: "text" as const, text }], details: { mode: "search", ...details } },
|
|
913
|
+
});
|
|
914
|
+
if (searchMode !== "lexical" && searchMode !== "semantic") {
|
|
915
|
+
return failure("Search mode must be lexical or semantic.", { error: "invalid_search_mode", query });
|
|
906
916
|
}
|
|
907
|
-
if (server && isServerDisabled(state.config.mcpServers[server])) return disabledResult(state, "search", server);
|
|
908
|
-
if (server && isServerInActiveFailureBackoff(state, server)) return serverBackoffResult(state, "search", server);
|
|
917
|
+
if (server && isServerDisabled(state.config.mcpServers[server])) return { error: disabledResult(state, "search", server) };
|
|
918
|
+
if (server && isServerInActiveFailureBackoff(state, server)) return { error: serverBackoffResult(state, "search", server) };
|
|
909
919
|
if (searchMode === "semantic" && regex) {
|
|
910
|
-
return {
|
|
911
|
-
content: [{ type: "text" as const, text: "Semantic search cannot be combined with regex search." }],
|
|
912
|
-
details: { mode: "search", error: "invalid_search_mode", query },
|
|
913
|
-
};
|
|
920
|
+
return failure("Semantic search cannot be combined with regex search.", { error: "invalid_search_mode", query });
|
|
914
921
|
}
|
|
915
922
|
if (searchMode === "semantic") {
|
|
916
|
-
return semanticSearch(state, query, server, signal, semanticEvaluator).then(result =>
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
content: [{ type: "text" as const, text: `Semantic search failed: ${result.error.message}` }],
|
|
920
|
-
details: { mode: "search", error: result.error.code, message: result.error.message, query },
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
return renderSearchResults(state, query, server, showSchemas, limit, offset, result.matches, result.backend);
|
|
924
|
-
});
|
|
923
|
+
return semanticSearch(state, query, server, input.signal, input.semanticEvaluator, input.observedSources).then(result => result.ok
|
|
924
|
+
? { matches: result.matches, backend: result.backend }
|
|
925
|
+
: failure(`Semantic search failed: ${result.error.message}`, { error: result.error.code, message: result.error.message, query }));
|
|
925
926
|
}
|
|
926
927
|
|
|
927
|
-
let matches: Array<{ server: string; tool: ToolMetadata; score: number }>;
|
|
928
928
|
if (regex) {
|
|
929
|
+
if (query.length > MAX_REGEX_SEARCH_QUERY_LENGTH) {
|
|
930
|
+
return failure(
|
|
931
|
+
`Regex query is too long; maximum length is ${MAX_REGEX_SEARCH_QUERY_LENGTH} characters.`,
|
|
932
|
+
{ error: "query_too_long", query, maxLength: MAX_REGEX_SEARCH_QUERY_LENGTH },
|
|
933
|
+
);
|
|
934
|
+
}
|
|
929
935
|
let pattern: RegExp;
|
|
930
936
|
try {
|
|
931
|
-
if (query.length > MAX_REGEX_SEARCH_QUERY_LENGTH) {
|
|
932
|
-
return {
|
|
933
|
-
content: [{ type: "text" as const, text: `Regex query is too long; maximum length is ${MAX_REGEX_SEARCH_QUERY_LENGTH} characters.` }],
|
|
934
|
-
details: { mode: "search", error: "query_too_long", query, maxLength: MAX_REGEX_SEARCH_QUERY_LENGTH },
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
937
|
pattern = new RegExp(query, "i");
|
|
938
|
-
let safety;
|
|
939
|
-
try {
|
|
940
|
-
const { checkSync } = require("recheck") as typeof import("recheck");
|
|
941
|
-
safety = checkSync(query, "i", REGEX_SAFETY_CHECK_PARAMS);
|
|
942
|
-
} catch (error) {
|
|
943
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
944
|
-
return {
|
|
945
|
-
content: [{ type: "text" as const, text: "Regex query rejected because safety analysis failed." }],
|
|
946
|
-
details: { mode: "search", error: "unsafe_pattern", query, reason },
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
if (safety.status !== "safe") {
|
|
950
|
-
return {
|
|
951
|
-
content: [{ type: "text" as const, text: `Regex query rejected as unsafe (${safety.status}).` }],
|
|
952
|
-
details: { mode: "search", error: "unsafe_pattern", query, safetyStatus: safety.status },
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
938
|
} catch {
|
|
956
|
-
return {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
939
|
+
return failure(`Invalid regex: ${query}`, { error: "invalid_pattern", query });
|
|
940
|
+
}
|
|
941
|
+
let safety;
|
|
942
|
+
try {
|
|
943
|
+
const { checkSync } = require("recheck") as typeof import("recheck");
|
|
944
|
+
safety = checkSync(query, "i", REGEX_SAFETY_CHECK_PARAMS);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
947
|
+
return failure("Regex query rejected because safety analysis failed.", { error: "unsafe_pattern", query, reason });
|
|
948
|
+
}
|
|
949
|
+
if (safety.status !== "safe") {
|
|
950
|
+
return failure(`Regex query rejected as unsafe (${safety.status}).`, { error: "unsafe_pattern", query, safetyStatus: safety.status });
|
|
960
951
|
}
|
|
961
952
|
|
|
962
|
-
matches = [];
|
|
953
|
+
const matches: RankedToolMatch[] = [];
|
|
963
954
|
const globalPrefix = state.config.settings?.toolPrefix ?? "server";
|
|
964
955
|
for (const [serverName, metadata] of state.toolMetadata.entries()) {
|
|
965
956
|
const definition = state.config.mcpServers[serverName];
|
|
@@ -972,21 +963,37 @@ export function executeSearch(
|
|
|
972
963
|
if (matched) matches.push({ server: serverName, tool, score: 0 });
|
|
973
964
|
}
|
|
974
965
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
} else {
|
|
986
|
-
matches = rankToolMatches(state, query, server);
|
|
966
|
+
return { matches };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (query.trim().length === 0) {
|
|
970
|
+
if (!server) return failure("Search query cannot be empty", { error: "empty_query" });
|
|
971
|
+
return {
|
|
972
|
+
matches: (state.toolMetadata.get(server) ?? [])
|
|
973
|
+
.map(tool => ({ server, tool, score: 0 }))
|
|
974
|
+
.sort((a, b) => a.tool.name.localeCompare(b.tool.name)),
|
|
975
|
+
};
|
|
987
976
|
}
|
|
977
|
+
return { matches: rankToolMatches(state, query, server) };
|
|
978
|
+
}
|
|
988
979
|
|
|
989
|
-
|
|
980
|
+
export function executeSearch(
|
|
981
|
+
state: McpExtensionState,
|
|
982
|
+
query: string,
|
|
983
|
+
regex?: boolean,
|
|
984
|
+
server?: string,
|
|
985
|
+
includeSchemas?: boolean,
|
|
986
|
+
limit = 12,
|
|
987
|
+
offset = 0,
|
|
988
|
+
searchMode: "lexical" | "semantic" = "lexical",
|
|
989
|
+
signal?: AbortSignal,
|
|
990
|
+
semanticEvaluator?: SemanticSearchEvaluator,
|
|
991
|
+
): ProxyToolResult | Promise<ProxyToolResult> {
|
|
992
|
+
const render = (outcome: ToolSearchOutcome): ProxyToolResult => "error" in outcome
|
|
993
|
+
? outcome.error
|
|
994
|
+
: renderSearchResults(state, query, server, includeSchemas !== false, limit, offset, outcome.matches, outcome.backend);
|
|
995
|
+
const outcome = findTools(state, { query, regex, server, searchMode, signal, semanticEvaluator });
|
|
996
|
+
return outcome instanceof Promise ? outcome.then(render) : render(outcome);
|
|
990
997
|
}
|
|
991
998
|
|
|
992
999
|
export function executeList(state: McpExtensionState, server: string): ProxyToolResult {
|
|
@@ -1178,6 +1185,12 @@ export async function executeConnect(state: McpExtensionState, serverName: strin
|
|
|
1178
1185
|
}
|
|
1179
1186
|
}
|
|
1180
1187
|
|
|
1188
|
+
/** Whether executeCall without a server resolves this exact displayed tool name to serverName (its first, exact-name lookup). */
|
|
1189
|
+
export function unscopedCallReachesServer(state: McpExtensionState, toolName: string, serverName: string): boolean {
|
|
1190
|
+
const matches = getEnabledToolMatches(state, toolName, true);
|
|
1191
|
+
return matches.length === 1 && matches[0]!.server === serverName;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1181
1194
|
export async function executeCall(
|
|
1182
1195
|
state: McpExtensionState,
|
|
1183
1196
|
toolName: string,
|
|
@@ -1636,6 +1649,7 @@ export async function executeCall(
|
|
|
1636
1649
|
: null;
|
|
1637
1650
|
|
|
1638
1651
|
const requestMeta = withToolCallIdMeta(uiSession?.requestMeta, toolCallId);
|
|
1652
|
+
const recordOutput = observedOutputRecorder(state, serverName, toolMeta.originalName);
|
|
1639
1653
|
const result = await withSessionRecovery<ClientCallToolResult>(
|
|
1640
1654
|
{
|
|
1641
1655
|
manager: state.manager,
|
|
@@ -1662,7 +1676,7 @@ export async function executeCall(
|
|
|
1662
1676
|
}, requestOptions), ownedSignal);
|
|
1663
1677
|
},
|
|
1664
1678
|
);
|
|
1665
|
-
if (!result.isError)
|
|
1679
|
+
if (!result.isError) recordOutput(result as Record<string, unknown>);
|
|
1666
1680
|
|
|
1667
1681
|
if (toolMeta.uiResourceUri) {
|
|
1668
1682
|
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/client").CallToolResult);
|
|
@@ -1714,7 +1728,9 @@ export async function executeCall(
|
|
|
1714
1728
|
|
|
1715
1729
|
const content = resolveMcpResultContent(result as Record<string, unknown>, state.owner?.signal);
|
|
1716
1730
|
const outputContent = content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }];
|
|
1717
|
-
|
|
1731
|
+
// Runtime event-bus callers ("script" origin) are extensions, not the model, so they get no hint.
|
|
1732
|
+
const pipeHint = scriptPipeHint(origin !== "script" && state.scriptTool, outputContent);
|
|
1733
|
+
const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, ...pipeHint, rawMcpResult: result });
|
|
1718
1734
|
return {
|
|
1719
1735
|
content: guarded.content,
|
|
1720
1736
|
details: { mode: "call", ...guardedMcpDetails(guarded), ...callIdentity },
|
package/server-manager.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type SubscriptionFilter,
|
|
19
19
|
type CacheableRequestOptions,
|
|
20
20
|
type RequestOptions,
|
|
21
|
+
RegistrationRejectedError,
|
|
21
22
|
type UrlElicitationRequiredError,
|
|
22
23
|
type VersionNegotiationOptions,
|
|
23
24
|
} from "@modelcontextprotocol/client";
|
|
@@ -43,7 +44,7 @@ import { logger } from "./logger.ts";
|
|
|
43
44
|
import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
|
|
44
45
|
import { isBuiltInAgentPlugin } from "./agent-plugin-provenance.ts";
|
|
45
46
|
import { McpOAuthProvider } from "./mcp-oauth-provider.ts";
|
|
46
|
-
import { extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
47
|
+
import { explainRegistrationRejection, extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
47
48
|
import {
|
|
48
49
|
captureOAuthAuthority,
|
|
49
50
|
inspectAuthForUrl,
|
|
@@ -110,17 +111,22 @@ function isLiteralLocalAddress(url: string): boolean {
|
|
|
110
111
|
return local.check(hostname, family === 6 ? "ipv6" : "ipv4");
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
function
|
|
114
|
+
function isLoopbackUrl(url: string): boolean {
|
|
115
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const LOCAL_NETWORK_FAILURE_CODES = ["EHOSTUNREACH", "ENETUNREACH", "EACCES"];
|
|
119
|
+
|
|
120
|
+
function networkFailureCodes(error: unknown, wanted: readonly string[], seen = new Set<object>()): string[] {
|
|
114
121
|
if (typeof error !== "object" || error === null || seen.has(error)) return [];
|
|
115
122
|
seen.add(error);
|
|
116
123
|
const codes: string[] = [];
|
|
117
|
-
if ("code" in error && typeof error.code === "string"
|
|
118
|
-
&& ["EHOSTUNREACH", "ENETUNREACH", "EACCES"].includes(error.code)) {
|
|
124
|
+
if ("code" in error && typeof error.code === "string" && wanted.includes(error.code)) {
|
|
119
125
|
codes.push(error.code);
|
|
120
126
|
}
|
|
121
|
-
if ("cause" in error) codes.push(...
|
|
127
|
+
if ("cause" in error) codes.push(...networkFailureCodes(error.cause, wanted, seen));
|
|
122
128
|
if (error instanceof AggregateError) {
|
|
123
|
-
for (const nested of error.errors) codes.push(...
|
|
129
|
+
for (const nested of error.errors) codes.push(...networkFailureCodes(nested, wanted, seen));
|
|
124
130
|
}
|
|
125
131
|
return [...new Set(codes)];
|
|
126
132
|
}
|
|
@@ -1287,9 +1293,15 @@ export class McpServerManager {
|
|
|
1287
1293
|
}
|
|
1288
1294
|
|
|
1289
1295
|
private async enrichHttpConnectionError(definition: ServerDefinition, error: unknown): Promise<Error> {
|
|
1296
|
+
// The transport's OAuth provider can register a client during connect, bypassing startAuth.
|
|
1297
|
+
if (error instanceof RegistrationRejectedError) return explainRegistrationRejection(error, resolveServerUrl(definition)!);
|
|
1290
1298
|
const originalMessage = error instanceof Error ? error.message : String(error);
|
|
1299
|
+
if (networkFailureCodes(error, ["ECONNREFUSED"]).length > 0 && isLoopbackUrl(resolveServerUrl(definition)!)) {
|
|
1300
|
+
const url = new URL(resolveServerUrl(definition)!);
|
|
1301
|
+
return new Error(`${originalMessage} — Nothing is listening at ${url.origin}${url.pathname}. Start the app or local process that serves this MCP server.`, { cause: error });
|
|
1302
|
+
}
|
|
1291
1303
|
if (process.platform === "darwin") {
|
|
1292
|
-
const codes =
|
|
1304
|
+
const codes = networkFailureCodes(error, LOCAL_NETWORK_FAILURE_CODES);
|
|
1293
1305
|
if (codes.length > 0 && isLiteralLocalAddress(resolveServerUrl(definition)!)) {
|
|
1294
1306
|
return new Error(`${originalMessage} — ${codes.join(", ")} — macOS Local Network Privacy may be blocking access. Check System Settings > Privacy & Security > Local Network for the app hosting Pi; enable access if listed and restart it. Try launching Pi from Terminal.app or SSH. Routing or firewall problems can also cause this error.`, { cause: error });
|
|
1295
1307
|
}
|
|
@@ -1422,8 +1434,7 @@ export class McpServerManager {
|
|
|
1422
1434
|
if (!connection || connection.client !== client || connection.status !== "connected") return;
|
|
1423
1435
|
connection.tools = tools;
|
|
1424
1436
|
connection.toolsRevision = (connection.toolsRevision ?? 0) + 1;
|
|
1425
|
-
this.
|
|
1426
|
-
this.pendingMetadataPublications.delete(serverName);
|
|
1437
|
+
this.publishMetadataChanged(serverName, connection, "tools-list-changed");
|
|
1427
1438
|
}
|
|
1428
1439
|
|
|
1429
1440
|
private handlePromptsListChanged(
|
|
@@ -1441,8 +1452,7 @@ export class McpServerManager {
|
|
|
1441
1452
|
if (!connection || connection.client !== client || connection.status !== "connected") return;
|
|
1442
1453
|
connection.prompts = prompts;
|
|
1443
1454
|
connection.promptDiscoveryFailed = false;
|
|
1444
|
-
this.
|
|
1445
|
-
this.pendingMetadataPublications.delete(serverName);
|
|
1455
|
+
this.publishMetadataChanged(serverName, connection, "prompts-list-changed");
|
|
1446
1456
|
}
|
|
1447
1457
|
|
|
1448
1458
|
private handleResourcesListChanged(
|
|
@@ -1460,8 +1470,7 @@ export class McpServerManager {
|
|
|
1460
1470
|
if (!connection || connection.client !== client || connection.status !== "connected") return;
|
|
1461
1471
|
connection.resources = resources;
|
|
1462
1472
|
connection.resourceDiscoveryFailed = false;
|
|
1463
|
-
this.
|
|
1464
|
-
this.pendingMetadataPublications.delete(serverName);
|
|
1473
|
+
this.publishMetadataChanged(serverName, connection, "resources-list-changed");
|
|
1465
1474
|
}
|
|
1466
1475
|
|
|
1467
1476
|
async handleUrlElicitationRequired(
|
|
@@ -1631,13 +1640,17 @@ export class McpServerManager {
|
|
|
1631
1640
|
> => {
|
|
1632
1641
|
const authProvider = "provider" in authState ? authState.provider : undefined;
|
|
1633
1642
|
let sseFetchFailure: unknown;
|
|
1634
|
-
const
|
|
1643
|
+
const sseFailureCodes = kind !== "sse" ? []
|
|
1644
|
+
: isLoopbackUrl(serverUrl) ? ["ECONNREFUSED"]
|
|
1645
|
+
: process.platform === "darwin" && isLiteralLocalAddress(serverUrl) ? LOCAL_NETWORK_FAILURE_CODES
|
|
1646
|
+
: [];
|
|
1647
|
+
const transportFetch: FetchLike | undefined = sseFailureCodes.length > 0
|
|
1635
1648
|
? async (input, init) => {
|
|
1636
1649
|
try {
|
|
1637
1650
|
return await (requestFetch ?? globalThis.fetch)(input, init);
|
|
1638
1651
|
} catch (error) {
|
|
1639
1652
|
// EventSource discards the fetch cause before the SDK creates SseError.
|
|
1640
|
-
if (
|
|
1653
|
+
if (networkFailureCodes(error, sseFailureCodes).length > 0) sseFetchFailure = error;
|
|
1641
1654
|
throw error;
|
|
1642
1655
|
}
|
|
1643
1656
|
}
|
|
@@ -1,51 +1,45 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp-scripting
|
|
3
|
-
description:
|
|
3
|
+
description: Read before writing mcpScript code. Covers tool discovery, the shape of call results (including JSON returned as text), and script limits.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# MCP scripting
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Use `mcpScript` when a request needs several MCP calls with logic between them, or when one call's output should go straight into another call. For a single search, describe, status check, auth action, or call, use `mcp`.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Only what the script emits, logs, or returns enters the conversation, so keep intermediate data in variables and return a small result:
|
|
12
12
|
|
|
13
13
|
```js
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
if (!
|
|
14
|
+
const found = await tools.search({ query: "list issues", server: "github" });
|
|
15
|
+
const path = found.items[0]?.path;
|
|
16
|
+
if (!path) return { error: "No matching tool" };
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
if (details.error) return details;
|
|
20
|
-
|
|
21
|
-
const result = await tools.call(details.path, { query: "is:open label:bug" });
|
|
18
|
+
const result = await tools.call(path, { state: "open" });
|
|
22
19
|
if (!result.ok) return result;
|
|
23
|
-
|
|
24
|
-
return
|
|
20
|
+
const issues = result.data.structuredContent ?? JSON.parse(result.data.content[0].text);
|
|
21
|
+
return issues.filter((issue) => issue.comments === 0).map((issue) => issue.number);
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
1. Find candidate tools with `await tools.search({ query, server?, limit?, offset? })`.
|
|
30
|
-
2. Inspect the exact returned path with `await tools.describe({ path })`.
|
|
31
|
-
3. Call it with `tools.call(path, args)`.
|
|
32
|
-
|
|
33
|
-
Descriptors include `inputTypeScript` (a compact parameter shape, or formatted schema fallback). When a compact shape would omit documented fields, `inputGuidance` preserves their descriptions, including formats and units. Undocumented inputs stay compact.
|
|
34
|
-
|
|
35
|
-
When advertised by the server, `outputSchema` is the original JSON Schema and `outputSchemaTarget` is `"data.structuredContent"`: it describes structured output inside the successful `{ ok: true, data }` call envelope, not the envelope itself. Inspect this schema for result fields and constraints; unsupported constructs remain intact rather than being presented as an approximate TypeScript type. Both output fields are absent when no output schema is advertised. Discovery and cache refresh preserve these optional schemas; old cache entries gain them on the next server metadata refresh. Ordinary search results do not include schemas.
|
|
36
|
-
|
|
37
|
-
When no output schema is advertised but the tool already returned structured content or a JSON text result earlier in this session, `observedOutput` gives `{ target, typeScript, calls }`: the path inside the call envelope (`"data.structuredContent"` or `"JSON.parse(data.content[0].text)"`), the field names and types seen so far, and how many calls they came from. It is a hint, not a contract: fields missing from some results are optional, and wide objects appear as `Record<string, ...>`.
|
|
24
|
+
## API
|
|
38
25
|
|
|
39
|
-
|
|
26
|
+
- `await tools.search({ query, server?, regex?, searchMode?, limit?, offset? })` returns `{ items: [{ path, name, server, description? }], total, hasMore, nextOffset }`, plus `error: { code, message }` when the search cannot run. It is the same search as `mcp({ search })`; an empty `query` with `server` lists that server's tools. Filter the items in code; follow `nextOffset` for more than one page.
|
|
27
|
+
- `await tools.describe({ path, server? })` returns `inputTypeScript` (plus `inputGuidance` when documented fields would otherwise be lost) and optional `annotations`. When the server declares an `outputSchema`, it describes `data.structuredContent`. Otherwise, once the tool has returned JSON (in this or an earlier `mcpScript` session), `observedOutput` gives `{ target, typeScript }`: where the JSON is, and the field names and types seen so far. It is a hint, not a contract.
|
|
28
|
+
- `await tools.call(path, args, { server }?)` returns `{ ok: true, data }` or `{ ok: false, error: { code, message } }`. A failed call does not stop the script. When two servers share a tool name, pass the hit's `server` to `describe` and `call`.
|
|
29
|
+
- Known paths can be called directly and resolve the same `{ ok, data }` way: `tools.github_search_issues(args)`, or `tools["server_tool-name"](args)` for hyphenated names. `tools` cannot be enumerated. `search`, `call`, `describe`, `then`, `catch`, `finally`, `toJSON`, `toString`, and `valueOf` are reserved; call a colliding path with `tools.call`.
|
|
30
|
+
- `emit(value)` adds output before the final `return` value; `console` output is captured too.
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
## Reading results
|
|
42
33
|
|
|
43
|
-
|
|
34
|
+
A tool call's `data` is the raw MCP `CallToolResult` (`{ content, structuredContent?, isError? }`), not the domain payload; resource reads return text. Use `data.structuredContent` when present. Otherwise most JSON APIs return their payload as text, so parse `data.content[0].text` (some servers emit newline-delimited JSON). If neither shape is understood, emit the envelope for inspection instead of coercing it to `[]` or `{}`.
|
|
44
35
|
|
|
45
|
-
|
|
36
|
+
Write the real script first instead of spending a turn looking at a result. Use `observedOutput` when describe has it; otherwise use the tool description and the field names the API most likely uses. Before a loop that writes (comments, closes, creates), check that the first item has the fields you filter on and throw if it does not, so a wrong guess stops before it changes anything. When a script throws, times out, or returns `[]`, `{}`, `null`, or `""`, its result lists the fields seen from the tools it called; fix the script from that. Never emit a whole list to inspect it.
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
## Limits
|
|
48
39
|
|
|
49
|
-
|
|
40
|
+
- Scripts time out after 30 seconds by default (`timeoutMs` changes it). The worker is stopped at the deadline, including infinite loops.
|
|
41
|
+
- Intermediate results share a fixed 16 MiB transfer budget per script. A result that does not fit returns `{ ok: false, error: { code: "intermediate_result_too_large" } }`, and the upstream call may still have had side effects.
|
|
42
|
+
- Every call goes through the normal connection, auth, and approval gates. The result details list each search, describe, and call with its outcome and duration.
|
|
43
|
+
- There are no fluent helpers such as `tools.find`, `tools.parallel`, or `tools.retry`; use loops and `Promise.all`.
|
|
50
44
|
|
|
51
|
-
|
|
45
|
+
When `settings.jev.scriptEvaluation` is enabled, read [references/jev.md](references/jev.md) before calling `jev.evaluate`.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# jev.evaluate in mcpScript
|
|
2
|
+
|
|
3
|
+
With `settings.jev.scriptEvaluation` enabled, call `jev.evaluate({ state, questions, sources })`; declare every MCP server represented in state. The host conservatively taints the whole script with every server-attributed call result or error, so every declared or observed source must remain enabled and allowed for later direct evaluations and semantic searches. Semantic search requires `searchMode: "semantic"`. Direct and semantic attempts share count, UTF-8 request-byte, deadline, and provider-reported token budgets. Scores do not bypass `tools.call` approval. Action loops must use fresh observations, positive step/time/evaluation budgets, validated operations and targets, and stop on uncertainty, staleness, no progress, no match, or missing information. Do not retry a possibly side-effectful action.
|
package/state.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface McpExtensionState {
|
|
|
60
60
|
observedOutputs?: WeakMap<ServerDefinition, Map<string, ObservedOutput>>;
|
|
61
61
|
/** Configured project servers disabled by trust or approval policy for this session. */
|
|
62
62
|
blockedProjectServers?: Map<string, ProjectServerBlock>;
|
|
63
|
+
/** mcpScript is registered. Fixed when Pi loads the adapter, so a session's own config cannot change it. */
|
|
64
|
+
scriptTool?: boolean;
|
|
63
65
|
/** Runtime-only server grants. Never persisted or restored from session entries. */
|
|
64
66
|
approvedServers?: Map<string, { definition: ServerDefinition; hash: string }>;
|
|
65
67
|
/** Optional active-session sink for approval decisions. */
|
package/types.ts
CHANGED
|
@@ -634,8 +634,10 @@ export interface McpSettings {
|
|
|
634
634
|
directToolResultDetails?: "lean" | "bounded";
|
|
635
635
|
/** Show the advisory when 75 or more direct tools resolve. Defaults to true. */
|
|
636
636
|
warnOnLargeDirectTools?: boolean;
|
|
637
|
-
/** Register the
|
|
637
|
+
/** Register the MCP-only JavaScript scripting tool and its manual skill. Defaults to false. */
|
|
638
638
|
scriptMode?: boolean;
|
|
639
|
+
/** `"model"` points the model at the mcp-scripting skill from the mcpScript description. Defaults to `"manual"`: `/skill:mcp-scripting` only. */
|
|
640
|
+
scriptSkill?: "manual" | "model";
|
|
639
641
|
/** Expose MCP resources as tools (default: true). Set to false to disable globally across all servers. */
|
|
640
642
|
exposeResources?: boolean;
|
|
641
643
|
/** Optional Jev (System One) integrations. A valid key enables semantic search; script evaluation remains disabled by default. */
|
|
@@ -818,6 +820,11 @@ export interface ServerCacheEntry {
|
|
|
818
820
|
/** Server-level hints from the aggregated tools/list result. */
|
|
819
821
|
ttlMs?: ListToolsResult["ttlMs"];
|
|
820
822
|
cacheScope?: ListToolsResult["cacheScope"];
|
|
823
|
+
/**
|
|
824
|
+
* Result shapes (field names and types, never values) seen from tools without an outputSchema,
|
|
825
|
+
* keyed by original tool name. Dropped when the server config or that tool's description or input schema changes.
|
|
826
|
+
*/
|
|
827
|
+
outputShapes?: Record<string, { source: "structuredContent" | "jsonText"; shape: unknown }>;
|
|
821
828
|
cachedAt: number;
|
|
822
829
|
}
|
|
823
830
|
|