pi-mcp-adapter 3.1.0 → 3.3.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 +54 -0
- package/OAUTH.md +7 -7
- package/README.md +64 -6
- package/cli.js +1 -0
- package/direct-tools.ts +9 -1
- package/dist/elicitation-handler.d.ts +9 -1
- package/dist/elicitation-handler.js +65 -2
- package/dist/elicitation-handler.js.map +1 -1
- package/dist/jev-client.js +7 -5
- package/dist/jev-client.js.map +1 -1
- package/dist/jev-key-store.d.ts +1 -0
- package/dist/jev-key-store.js +11 -0
- package/dist/jev-key-store.js.map +1 -1
- package/dist/mcp-callback-server.js +1 -2
- package/dist/mcp-callback-server.js.map +1 -1
- package/dist/mcp-oauth-provider.d.ts +2 -1
- package/dist/mcp-oauth-provider.js +5 -2
- package/dist/mcp-oauth-provider.js.map +1 -1
- package/dist/metadata-cache.js +5 -1
- package/dist/metadata-cache.js.map +1 -1
- package/dist/output-shape.d.ts +23 -0
- package/dist/output-shape.js +194 -0
- package/dist/output-shape.js.map +1 -0
- package/dist/state.d.ts +6 -1
- package/dist/ts-shape.d.ts +3 -0
- package/dist/ts-shape.js +153 -0
- package/dist/ts-shape.js.map +1 -0
- package/dist/types.d.ts +13 -2
- package/dist/types.js.map +1 -1
- package/dist/unix-socket-transport.js +1 -1
- package/dist/unix-socket-transport.js.map +1 -1
- package/dist/utils.d.ts +6 -1
- package/dist/utils.js +17 -0
- package/dist/utils.js.map +1 -1
- package/elicitation-handler.ts +78 -1
- package/host-managed.ts +547 -0
- package/index.ts +19 -22
- package/init.ts +15 -9
- package/jev-client.ts +7 -4
- package/jev-key-store.ts +7 -0
- package/mcp-callback-server.ts +1 -1
- package/mcp-code.ts +12 -2
- package/mcp-oauth-provider.ts +5 -2
- package/mcp-script-wasm.ts +18 -1
- package/mcp-script-worker.mjs +9 -2
- package/metadata-cache.ts +5 -0
- package/namespace-tools.ts +22 -2
- package/output-shape.ts +209 -0
- package/package.json +9 -2
- package/prompts.ts +68 -0
- package/proxy-modes.ts +28 -1
- package/skills/mcp-scripting/SKILL.md +2 -0
- package/state.ts +6 -1
- package/tool-approval.ts +28 -10
- package/tool-metadata.ts +3 -1
- package/tool-parameters.ts +11 -0
- package/ts-shape.ts +1 -1
- package/types.ts +14 -2
- package/ui-server.ts +5 -2
- package/unix-socket-transport.ts +1 -1
- package/utils.ts +16 -1
package/prompts.ts
CHANGED
|
@@ -122,6 +122,64 @@ function stripQuotes(value: string): string {
|
|
|
122
122
|
return value;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function findCurrentPromptTokenStart(input: string): number {
|
|
126
|
+
let quote: '"' | "'" | null = null;
|
|
127
|
+
let escaped = false;
|
|
128
|
+
let start = 0;
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < input.length; i++) {
|
|
131
|
+
const char = input.charAt(i);
|
|
132
|
+
if (escaped) {
|
|
133
|
+
escaped = false;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (char === "\\" && quote !== "'") {
|
|
137
|
+
escaped = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (quote) {
|
|
141
|
+
if (char === quote) quote = null;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (char === '"' || char === "'") {
|
|
145
|
+
quote = char;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (/\s/.test(char)) start = i + 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return start;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function completePromptArgs(
|
|
155
|
+
metadata: PromptMetadata,
|
|
156
|
+
input: string,
|
|
157
|
+
): Array<{ value: string; label: string; description?: string }> | null {
|
|
158
|
+
const tokenStart = findCurrentPromptTokenStart(input);
|
|
159
|
+
const current = input.slice(tokenStart);
|
|
160
|
+
if (current.includes("=") || current.startsWith('"') || current.startsWith("'")) return null;
|
|
161
|
+
|
|
162
|
+
const committed = input.slice(0, tokenStart);
|
|
163
|
+
const parsed = parsePromptArgs(committed);
|
|
164
|
+
const used = new Set(Object.keys(parsed.named));
|
|
165
|
+
let positionalIndex = 0;
|
|
166
|
+
|
|
167
|
+
for (const arg of metadata.arguments) {
|
|
168
|
+
if (Object.hasOwn(parsed.named, arg.name)) continue;
|
|
169
|
+
const positional = parsed.positional[positionalIndex++];
|
|
170
|
+
if (positional !== undefined && positional !== "") used.add(arg.name);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const matches = metadata.arguments.filter(arg => !used.has(arg.name) && arg.name.startsWith(current));
|
|
174
|
+
if (matches.length === 0) return null;
|
|
175
|
+
|
|
176
|
+
return matches.map(arg => ({
|
|
177
|
+
value: committed + arg.name + "=",
|
|
178
|
+
label: arg.name + "=",
|
|
179
|
+
...(arg.description ? { description: arg.description } : {}),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
125
183
|
export interface ResolvedPromptArgs {
|
|
126
184
|
ok: boolean;
|
|
127
185
|
/** Present when `ok === true`. */
|
|
@@ -249,6 +307,16 @@ export function createPromptCommand(
|
|
|
249
307
|
|
|
250
308
|
return {
|
|
251
309
|
description,
|
|
310
|
+
getArgumentCompletions: (prefix: string) => {
|
|
311
|
+
const state = getState();
|
|
312
|
+
if (!state) return completePromptArgs(metadata, prefix);
|
|
313
|
+
const definition = state.config.mcpServers[metadata.serverName];
|
|
314
|
+
if (!definition || isServerDisabled(definition)) return null;
|
|
315
|
+
|
|
316
|
+
const liveMetadata = findLivePromptMetadata(state, metadata.serverName, metadata.originalName);
|
|
317
|
+
if (state.promptMetadataLive?.has(metadata.serverName) && !liveMetadata) return null;
|
|
318
|
+
return completePromptArgs(liveMetadata ?? metadata, prefix);
|
|
319
|
+
},
|
|
252
320
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
253
321
|
let state = getState();
|
|
254
322
|
if (!state && runtime.ensureState) {
|
package/proxy-modes.ts
CHANGED
|
@@ -9,6 +9,7 @@ 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, recordObservedOutput, renderOutputShape } from "./output-shape.ts";
|
|
12
13
|
import { reconstructPromptMetadata } from "./metadata-cache.ts";
|
|
13
14
|
import { resolveMcpResultContent, transformMcpResourceContents } from "./tool-registrar.ts";
|
|
14
15
|
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
@@ -17,6 +18,7 @@ import { formatAuthRequiredMessage, formatMcpStatus, normalizeToolArguments, res
|
|
|
17
18
|
import { authenticate, completeAuthFromInput, getAuthStatus, startAuth, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
18
19
|
import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.ts";
|
|
19
20
|
import { callToolViaTaskSession } from "./mcp-tasks.ts";
|
|
21
|
+
import { callToolPausingForElicitation } from "./elicitation-handler.ts";
|
|
20
22
|
import { paginate, rankSuggestions, rankToolMatches, resolveSearchKeywords } from "./search-ranking.ts";
|
|
21
23
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.ts";
|
|
22
24
|
import { describeFailure, isServerInActiveFailureBackoff } from "./failure-backoff.ts";
|
|
@@ -704,6 +706,22 @@ export async function executeAuthComplete(state: McpExtensionState, serverName:
|
|
|
704
706
|
}
|
|
705
707
|
}
|
|
706
708
|
|
|
709
|
+
function formatToolHints(toolMeta: ToolMetadata): string {
|
|
710
|
+
const annotations = toolMeta.annotations;
|
|
711
|
+
if (!annotations) return "";
|
|
712
|
+
const hints: string[] = [];
|
|
713
|
+
const flag = (value: boolean | undefined, yes: string, no: string) => {
|
|
714
|
+
if (value !== undefined) hints.push(value ? yes : no);
|
|
715
|
+
};
|
|
716
|
+
flag(annotations.readOnlyHint, "read-only", "not read-only");
|
|
717
|
+
flag(annotations.destructiveHint, "destructive", "non-destructive");
|
|
718
|
+
flag(annotations.idempotentHint, "idempotent", "not idempotent");
|
|
719
|
+
flag(annotations.openWorldHint, "open-world", "closed-world");
|
|
720
|
+
const title = annotations.title?.trim();
|
|
721
|
+
if (title && title !== toolMeta.originalName && title !== toolMeta.name) hints.push(`title ${JSON.stringify(title)}`);
|
|
722
|
+
return hints.join(", ");
|
|
723
|
+
}
|
|
724
|
+
|
|
707
725
|
export function executeDescribe(state: McpExtensionState, toolName: string, serverOverride?: string): ProxyToolResult {
|
|
708
726
|
let serverName: string | undefined;
|
|
709
727
|
let toolMeta: ToolMetadata | undefined;
|
|
@@ -770,6 +788,8 @@ export function executeDescribe(state: McpExtensionState, toolName: string, serv
|
|
|
770
788
|
if (toolMeta.resourceUri) {
|
|
771
789
|
text += `Type: Resource (reads from ${toolMeta.resourceUri})\n`;
|
|
772
790
|
}
|
|
791
|
+
const hints = formatToolHints(toolMeta);
|
|
792
|
+
if (hints) text += `Hints: ${hints}\n`;
|
|
773
793
|
text += `\n${toolMeta.description || "(no description)"}\n`;
|
|
774
794
|
|
|
775
795
|
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
@@ -781,6 +801,12 @@ export function executeDescribe(state: McpExtensionState, toolName: string, serv
|
|
|
781
801
|
text += `\nNo parameters defined.`;
|
|
782
802
|
}
|
|
783
803
|
|
|
804
|
+
const observed = toolMeta.resourceUri ? undefined : getObservedOutput(state, serverName, toolMeta);
|
|
805
|
+
if (observed) {
|
|
806
|
+
const source = observed.source === "structuredContent" ? "structuredContent" : "JSON text result";
|
|
807
|
+
text += `\n\nObserved output (${source}, from ${observed.calls} call${observed.calls === 1 ? "" : "s"} this session, not a contract):\n${renderOutputShape(observed.shape)}`;
|
|
808
|
+
}
|
|
809
|
+
|
|
784
810
|
return {
|
|
785
811
|
content: [{ type: "text" as const, text: text.trim() }],
|
|
786
812
|
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
@@ -1629,13 +1655,14 @@ export async function executeCall(
|
|
|
1629
1655
|
requestTimeoutMs: requestOptions?.timeout,
|
|
1630
1656
|
}) as unknown as ClientCallToolResult;
|
|
1631
1657
|
}
|
|
1632
|
-
return abortable(conn.client
|
|
1658
|
+
return abortable(callToolPausingForElicitation(conn.client, {
|
|
1633
1659
|
name: toolMeta.originalName,
|
|
1634
1660
|
arguments: normalizedArgs,
|
|
1635
1661
|
_meta: requestMeta,
|
|
1636
1662
|
}, requestOptions), ownedSignal);
|
|
1637
1663
|
},
|
|
1638
1664
|
);
|
|
1665
|
+
if (!result.isError) recordObservedOutput(state, serverName, toolMeta.originalName, result as Record<string, unknown>);
|
|
1639
1666
|
|
|
1640
1667
|
if (toolMeta.uiResourceUri) {
|
|
1641
1668
|
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/client").CallToolResult);
|
|
@@ -34,6 +34,8 @@ Descriptors include `inputTypeScript` (a compact parameter shape, or formatted s
|
|
|
34
34
|
|
|
35
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
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, ...>`.
|
|
38
|
+
|
|
37
39
|
Calls resolve to `{ ok: true, data }` or `{ ok: false, error }`; handle failed calls instead of expecting them to stop the script. `emit(value)` adds user-visible output before the final `return` value. `console` output is captured too.
|
|
38
40
|
|
|
39
41
|
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
|
@@ -3,11 +3,12 @@ import type { ConsentManager } from "./consent-manager.ts";
|
|
|
3
3
|
import type { McpLifecycleManager } from "./lifecycle.ts";
|
|
4
4
|
import type { McpServerManager } from "./server-manager.ts";
|
|
5
5
|
import type { AuthStorageOptions } from "./mcp-auth.ts";
|
|
6
|
-
import type { ServerDefinition, ToolMetadata, PromptMetadata, McpConfig, UiSessionMessages, UiStreamSummary, McpStatusEventBus, UiServerHandle, ProjectServerBlock } from "./types.ts";
|
|
6
|
+
import type { ServerDefinition, ToolMetadata, PromptMetadata, ServerCacheEntry, McpConfig, UiSessionMessages, UiStreamSummary, McpStatusEventBus, UiServerHandle, ProjectServerBlock } from "./types.ts";
|
|
7
7
|
import type { UiResourceHandler } from "./ui-resource-handler.ts";
|
|
8
8
|
import type { McpRuntimeOwner } from "./runtime-owner.ts";
|
|
9
9
|
import type { McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
10
10
|
import type { SessionApprovalWriter } from "./session-approvals.ts";
|
|
11
|
+
import type { ObservedOutput } from "./output-shape.ts";
|
|
11
12
|
|
|
12
13
|
export interface CompletedUiSession {
|
|
13
14
|
serverName: string;
|
|
@@ -33,6 +34,7 @@ export interface McpExtensionState {
|
|
|
33
34
|
manager: McpServerManager;
|
|
34
35
|
lifecycle: McpLifecycleManager;
|
|
35
36
|
toolMetadata: Map<string, ToolMetadata[]>;
|
|
37
|
+
sessionMetadata?: Map<string, ServerCacheEntry>;
|
|
36
38
|
/** Number of tools currently registered directly with Pi, by server. */
|
|
37
39
|
directToolCounts: Map<string, number>;
|
|
38
40
|
/** Resource counts retained separately because tool metadata includes resource tools. */
|
|
@@ -53,6 +55,9 @@ export interface McpExtensionState {
|
|
|
53
55
|
failureMessages: Map<string, string>;
|
|
54
56
|
/** Session-only approvals keyed by server, tool definition, and arguments. */
|
|
55
57
|
approvedToolCalls: Map<string, true>;
|
|
58
|
+
/** Session-only output shapes learned from successful calls to tools without an outputSchema, by tool name.
|
|
59
|
+
* Keyed by the server's config entry so a replaced or reconfigured server starts over. Never persisted. */
|
|
60
|
+
observedOutputs?: WeakMap<ServerDefinition, Map<string, ObservedOutput>>;
|
|
56
61
|
/** Configured project servers disabled by trust or approval policy for this session. */
|
|
57
62
|
blockedProjectServers?: Map<string, ProjectServerBlock>;
|
|
58
63
|
/** Runtime-only server grants. Never persisted or restored from session entries. */
|
package/tool-approval.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { abortable } from "./abort.ts";
|
|
3
4
|
import { combineAbortSignals } from "./runtime-owner.ts";
|
|
4
5
|
import type { McpExtensionState } from "./state.ts";
|
|
@@ -24,15 +25,23 @@ export type ToolCallApprovalResult =
|
|
|
24
25
|
export function isToolCallApprovalRequired(
|
|
25
26
|
config: McpConfig,
|
|
26
27
|
serverName: string,
|
|
27
|
-
toolMeta: Pick<ToolMetadata, "originalName">,
|
|
28
|
+
toolMeta: Pick<ToolMetadata, "originalName" | "annotations" | "resourceUri">,
|
|
28
29
|
toolMetadata?: ReadonlyMap<string, readonly ToolMetadata[]>,
|
|
29
30
|
): boolean {
|
|
30
31
|
const definition = config.mcpServers[serverName];
|
|
31
32
|
const serverApproval = definition?.approveTools;
|
|
32
33
|
const approval = serverApproval !== undefined ? serverApproval : config.settings?.approveTools;
|
|
33
34
|
|
|
34
|
-
if (approval ===
|
|
35
|
-
|
|
35
|
+
if (approval === undefined || approval === false) return false;
|
|
36
|
+
// MCP treats a tool as possibly destructive unless it says otherwise; reading a resource never is.
|
|
37
|
+
if (approval === "destructive") {
|
|
38
|
+
return toolMeta.resourceUri === undefined
|
|
39
|
+
&& toolMeta.annotations?.readOnlyHint !== true
|
|
40
|
+
&& toolMeta.annotations?.destructiveHint !== false;
|
|
41
|
+
}
|
|
42
|
+
// Values from unvalidated sources (imports, runtime registration) fail closed.
|
|
43
|
+
if (!Array.isArray(approval)) return true;
|
|
44
|
+
if (approval.length === 0) return false;
|
|
36
45
|
|
|
37
46
|
const prefix = resolveToolPrefix(definition, config.settings?.toolPrefix);
|
|
38
47
|
const currentCandidates = getToolNameCandidates(toolMeta.originalName, serverName, prefix, false);
|
|
@@ -88,15 +97,19 @@ function isMcpToolApprovalDecision(value: unknown): value is McpToolApprovalDeci
|
|
|
88
97
|
|| value === "abstain";
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Returns "abstain" when no handler claims the request. A handler that throws
|
|
102
|
+
* or returns an unknown value counts as "deny". Rethrows only when `signal` aborted.
|
|
103
|
+
*/
|
|
104
|
+
export async function requestBrokerApproval(
|
|
105
|
+
events: ExtensionAPI["events"] | undefined,
|
|
93
106
|
serverName: string,
|
|
94
|
-
toolMeta: ToolMetadata,
|
|
107
|
+
toolMeta: Pick<ToolMetadata, "name" | "originalName">,
|
|
95
108
|
args: Record<string, unknown> | undefined,
|
|
96
109
|
origin: McpToolApprovalOrigin,
|
|
97
110
|
signal?: AbortSignal,
|
|
98
111
|
): Promise<McpToolApprovalDecision> {
|
|
99
|
-
if (!
|
|
112
|
+
if (!events) return "abstain";
|
|
100
113
|
|
|
101
114
|
let acceptingClaim = true;
|
|
102
115
|
let handler: McpToolApprovalHandler | undefined;
|
|
@@ -115,7 +128,7 @@ async function requestBrokerApproval(
|
|
|
115
128
|
},
|
|
116
129
|
};
|
|
117
130
|
|
|
118
|
-
|
|
131
|
+
events.emit(MCP_TOOL_APPROVAL_REQUEST_EVENT, request);
|
|
119
132
|
acceptingClaim = false;
|
|
120
133
|
if (!handler) return "abstain";
|
|
121
134
|
|
|
@@ -140,7 +153,7 @@ export async function ensureToolCallApproved(
|
|
|
140
153
|
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
141
154
|
const approvedServers = state.approvedServers ??= new Map();
|
|
142
155
|
const serverIdentity = getServerApprovalIdentity(state, serverName);
|
|
143
|
-
const brokerDecision = await requestBrokerApproval(state, serverName, toolMeta, args, origin, signal);
|
|
156
|
+
const brokerDecision = await requestBrokerApproval(state.approvalEvents, serverName, toolMeta, args, origin, signal);
|
|
144
157
|
if (brokerDecision === "allow_once") return { ok: true };
|
|
145
158
|
if (brokerDecision === "allow_for_session") {
|
|
146
159
|
rememberToolApproval(state, serverName, toolMeta, args);
|
|
@@ -175,10 +188,15 @@ export async function ensureToolCallApproved(
|
|
|
175
188
|
const sanitized = sanitizeTerminalText(json);
|
|
176
189
|
const preview = sanitized.length > 500 ? `${sanitized.slice(0, 500)}...` : sanitized;
|
|
177
190
|
const title = `MCP: ${sanitizeTerminalText(serverName)} wants to run ${sanitizeTerminalText(toolMeta.originalName)}`;
|
|
191
|
+
const hint = toolMeta.annotations?.destructiveHint === true
|
|
192
|
+
? "\n\nThe server marks this tool as destructive: it may delete or overwrite data."
|
|
193
|
+
: toolMeta.annotations?.readOnlyHint === true
|
|
194
|
+
? "\n\nThe server marks this tool as read-only."
|
|
195
|
+
: "";
|
|
178
196
|
const serverScope = "Allow server for this session permits all tools and arguments on this server until reload or session/branch change. Other security and UI consent checks still apply.";
|
|
179
197
|
const decision = await abortable(
|
|
180
198
|
state.ui.select(
|
|
181
|
-
`${title}\n\nArguments:\n${preview}\n\n${serverScope}`,
|
|
199
|
+
`${title}${hint}\n\nArguments:\n${preview}\n\n${serverScope}`,
|
|
182
200
|
["Allow once", "Allow for session", "Allow server for this session", "Deny"],
|
|
183
201
|
),
|
|
184
202
|
ownedSignal,
|
package/tool-metadata.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { McpExtensionState } from "./state.ts";
|
|
|
3
3
|
import type { ToolMetadata, McpTool, McpResource, ServerEntry, ToolPrefix } from "./types.ts";
|
|
4
4
|
import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isToolAllowed, resolveToolPrefix, resolveUniqueNameOwnership } from "./types.ts";
|
|
5
5
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
6
|
-
import { extractToolUiStreamMode } from "./utils.ts";
|
|
6
|
+
import { extractToolAnnotations, extractToolUiStreamMode } from "./utils.ts";
|
|
7
7
|
import { extractUiToolVisibility, isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
|
|
8
8
|
|
|
9
9
|
export function buildToolMetadata(
|
|
@@ -96,6 +96,7 @@ export function buildToolMetadata(
|
|
|
96
96
|
failedTools.push(tool.name);
|
|
97
97
|
}
|
|
98
98
|
const uiStreamMode = extractToolUiStreamMode(tool._meta);
|
|
99
|
+
const annotations = extractToolAnnotations(tool.annotations);
|
|
99
100
|
metadata.push({
|
|
100
101
|
name,
|
|
101
102
|
originalName: tool.name,
|
|
@@ -105,6 +106,7 @@ export function buildToolMetadata(
|
|
|
105
106
|
...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
|
|
106
107
|
...(uiVisibility !== undefined ? { uiVisibility } : {}),
|
|
107
108
|
...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
|
|
109
|
+
...(annotations !== undefined ? { annotations } : {}),
|
|
108
110
|
});
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
// OMP remaps `typebox` to a host shim that historically lacked Type.Unsafe.
|
|
4
|
+
// Prefer Unsafe when present (real TypeBox / fixed OMP shim); otherwise pass
|
|
5
|
+
// the normalized JSON Schema through as a plain object so toolWireSchema and
|
|
6
|
+
// validateToolArguments still treat it as JSON Schema.
|
|
7
|
+
export function toToolParameters(schema: Record<string, unknown>): unknown {
|
|
8
|
+
return typeof (Type as { Unsafe?: (value: never) => unknown }).Unsafe === "function"
|
|
9
|
+
? (Type as { Unsafe: (value: never) => unknown }).Unsafe(schema as never)
|
|
10
|
+
: schema;
|
|
11
|
+
}
|
package/ts-shape.ts
CHANGED
|
@@ -140,7 +140,7 @@ function renderLiteral(value: unknown): string | null {
|
|
|
140
140
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
function formatPropertyName(name: string): string {
|
|
143
|
+
export function formatPropertyName(name: string): string {
|
|
144
144
|
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
145
145
|
}
|
|
146
146
|
|
package/types.ts
CHANGED
|
@@ -97,6 +97,7 @@ export interface McpTool {
|
|
|
97
97
|
description?: SdkTool["description"];
|
|
98
98
|
inputSchema?: SdkTool["inputSchema"]; // JSON Schema
|
|
99
99
|
outputSchema?: SdkTool["outputSchema"]; // JSON Schema for structuredContent
|
|
100
|
+
annotations?: SdkTool["annotations"];
|
|
100
101
|
_meta?: SdkTool["_meta"];
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -488,7 +489,7 @@ export interface ServerEntry {
|
|
|
488
489
|
*/
|
|
489
490
|
searchKeywords?: Record<string, string[]>;
|
|
490
491
|
// Require interactive approval before calling matching MCP tools/resources.
|
|
491
|
-
approveTools?: boolean | string[];
|
|
492
|
+
approveTools?: boolean | "destructive" | string[];
|
|
492
493
|
// Debug
|
|
493
494
|
debug?: boolean; // Show server stderr (default: false)
|
|
494
495
|
/** Enable metadata-only JSONL protocol tracing for this server. */
|
|
@@ -661,7 +662,7 @@ export interface McpSettings {
|
|
|
661
662
|
/** Number of result text lines to show before expansion. Supports 1, 2, or 3. Defaults to 1 in compact mode and 3 in boxed mode. */
|
|
662
663
|
collapsedResultLines?: 1 | 2 | 3;
|
|
663
664
|
/** Default approval gate for matching tools/resources; per-server settings override it. */
|
|
664
|
-
approveTools?: boolean | string[];
|
|
665
|
+
approveTools?: boolean | "destructive" | string[];
|
|
665
666
|
disableProxyTool?: boolean;
|
|
666
667
|
/** Freeze direct-tool registration after the initial sync. Automatic metadata updates
|
|
667
668
|
* and explicit reconnects won't rebuild the system prompt, preserving the
|
|
@@ -739,6 +740,16 @@ export interface ToolMetadata {
|
|
|
739
740
|
inputSchema?: unknown; // JSON Schema for parameters (stored for describe/errors)
|
|
740
741
|
outputSchema?: unknown; // Server schema for structuredContent (stored for describe)
|
|
741
742
|
uiStreamMode?: UiStreamMode;
|
|
743
|
+
annotations?: McpToolAnnotations;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Behavior hints a server declared on a tool. Hints, not guarantees. */
|
|
747
|
+
export interface McpToolAnnotations {
|
|
748
|
+
title?: string;
|
|
749
|
+
readOnlyHint?: boolean;
|
|
750
|
+
destructiveHint?: boolean;
|
|
751
|
+
idempotentHint?: boolean;
|
|
752
|
+
openWorldHint?: boolean;
|
|
742
753
|
}
|
|
743
754
|
|
|
744
755
|
export interface PromptMetadata {
|
|
@@ -782,6 +793,7 @@ export interface CachedTool {
|
|
|
782
793
|
uiResourceUri?: string;
|
|
783
794
|
uiVisibility?: UiToolVisibility[];
|
|
784
795
|
uiStreamMode?: "eager" | "stream-first";
|
|
796
|
+
annotations?: McpToolAnnotations;
|
|
785
797
|
}
|
|
786
798
|
|
|
787
799
|
export interface CachedResource {
|
package/ui-server.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { ContentBlockSchema } from "@modelcontextprotocol/core";
|
|
11
11
|
import type { ConsentManager } from "./consent-manager.ts";
|
|
12
12
|
import { ServerError, wrapError } from "./errors.ts";
|
|
13
|
-
import { formatAuthRequiredMessage, normalizeToolArguments } from "./utils.ts";
|
|
13
|
+
import { extractToolAnnotations, formatAuthRequiredMessage, normalizeToolArguments } from "./utils.ts";
|
|
14
14
|
import { buildHostHtmlTemplate, buildSandboxResourceCsp } from "./host-html-template.ts";
|
|
15
15
|
import {
|
|
16
16
|
buildSandboxProxyCsp,
|
|
@@ -24,6 +24,7 @@ import type { McpExtensionState } from "./state.ts";
|
|
|
24
24
|
import { SessionRecoveryAuthRequiredError, withSessionRecovery, type SessionRecoveryDeps } from "./session-recovery.ts";
|
|
25
25
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.ts";
|
|
26
26
|
import { callToolViaTaskSession } from "./mcp-tasks.ts";
|
|
27
|
+
import { callToolPausingForElicitation } from "./elicitation-handler.ts";
|
|
27
28
|
import { extractUiToolVisibility, isUiToolCallableByApp, isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
|
|
28
29
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
29
30
|
import {
|
|
@@ -471,6 +472,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
471
472
|
} catch {
|
|
472
473
|
// Preserve the endpoint's existing behavior for malformed declarations.
|
|
473
474
|
}
|
|
475
|
+
const annotations = extractToolAnnotations(toolDefinition.annotations);
|
|
474
476
|
const toolMeta = {
|
|
475
477
|
name: callParams.name,
|
|
476
478
|
originalName: callParams.name,
|
|
@@ -478,6 +480,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
478
480
|
...(toolDefinition?.inputSchema !== undefined ? { inputSchema: toolDefinition.inputSchema } : {}),
|
|
479
481
|
...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
|
|
480
482
|
...(uiVisibility !== undefined ? { uiVisibility } : {}),
|
|
483
|
+
...(annotations !== undefined ? { annotations } : {}),
|
|
481
484
|
};
|
|
482
485
|
const approvalMetadata = new Map(options.state?.toolMetadata);
|
|
483
486
|
const definition = options.config?.mcpServers[options.serverName] ?? options.state?.config.mcpServers[options.serverName];
|
|
@@ -542,7 +545,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
542
545
|
signal: requestOptions?.signal,
|
|
543
546
|
});
|
|
544
547
|
}
|
|
545
|
-
return conn.client
|
|
548
|
+
return callToolPausingForElicitation(conn.client, callArgs, requestOptions);
|
|
546
549
|
};
|
|
547
550
|
const result = options.config
|
|
548
551
|
? await withSessionRecovery(
|
package/unix-socket-transport.ts
CHANGED
|
@@ -30,7 +30,7 @@ export class UnixSocketClientTransport implements Transport {
|
|
|
30
30
|
});
|
|
31
31
|
socket.on("data", chunk => {
|
|
32
32
|
try {
|
|
33
|
-
this.readBuffer.append(chunk);
|
|
33
|
+
this.readBuffer.append(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
34
34
|
while (true) {
|
|
35
35
|
const message = this.readBuffer.readMessage();
|
|
36
36
|
if (message === null) break;
|
package/utils.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import stripJsonComments from "strip-json-comments";
|
|
7
|
-
import type { McpConfig, ServerEntry } from "./types.ts";
|
|
7
|
+
import type { McpConfig, McpToolAnnotations, ServerEntry } from "./types.ts";
|
|
8
8
|
|
|
9
9
|
export function stripUtf8Bom(raw: string): string {
|
|
10
10
|
return raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
@@ -477,3 +477,18 @@ export function extractToolUiStreamMode(toolMeta: Record<string, unknown> | unde
|
|
|
477
477
|
}
|
|
478
478
|
return undefined;
|
|
479
479
|
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Keep only the spec tool annotations with the right types. Server and cache
|
|
483
|
+
* input is untrusted, so a malformed field is dropped instead of failing the tool list.
|
|
484
|
+
*/
|
|
485
|
+
export function extractToolAnnotations(annotations: unknown): McpToolAnnotations | undefined {
|
|
486
|
+
if (!annotations || typeof annotations !== "object") return undefined;
|
|
487
|
+
const source = annotations as Record<string, unknown>;
|
|
488
|
+
const kept: McpToolAnnotations = {};
|
|
489
|
+
if (typeof source.title === "string") kept.title = source.title;
|
|
490
|
+
for (const key of ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"] as const) {
|
|
491
|
+
if (typeof source[key] === "boolean") kept[key] = source[key];
|
|
492
|
+
}
|
|
493
|
+
return Object.keys(kept).length > 0 ? kept : undefined;
|
|
494
|
+
}
|