pi-mcp-adapter 3.2.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +12 -2
  3. package/cli.js +1 -0
  4. package/direct-tools.ts +9 -1
  5. package/dist/elicitation-handler.d.ts +9 -1
  6. package/dist/elicitation-handler.js +65 -2
  7. package/dist/elicitation-handler.js.map +1 -1
  8. package/dist/jev-client.js +7 -5
  9. package/dist/jev-client.js.map +1 -1
  10. package/dist/jev-key-store.d.ts +1 -0
  11. package/dist/jev-key-store.js +11 -0
  12. package/dist/jev-key-store.js.map +1 -1
  13. package/dist/metadata-cache.js +5 -1
  14. package/dist/metadata-cache.js.map +1 -1
  15. package/dist/output-shape.d.ts +23 -0
  16. package/dist/output-shape.js +194 -0
  17. package/dist/output-shape.js.map +1 -0
  18. package/dist/state.d.ts +4 -0
  19. package/dist/ts-shape.d.ts +3 -0
  20. package/dist/ts-shape.js +153 -0
  21. package/dist/ts-shape.js.map +1 -0
  22. package/dist/types.d.ts +13 -2
  23. package/dist/types.js.map +1 -1
  24. package/dist/unix-socket-transport.js +1 -1
  25. package/dist/unix-socket-transport.js.map +1 -1
  26. package/dist/utils.d.ts +6 -1
  27. package/dist/utils.js +17 -0
  28. package/dist/utils.js.map +1 -1
  29. package/elicitation-handler.ts +78 -1
  30. package/jev-client.ts +7 -4
  31. package/jev-key-store.ts +7 -0
  32. package/mcp-code.ts +10 -0
  33. package/metadata-cache.ts +5 -0
  34. package/output-shape.ts +209 -0
  35. package/package.json +2 -2
  36. package/prompts.ts +68 -0
  37. package/proxy-modes.ts +28 -1
  38. package/skills/mcp-scripting/SKILL.md +2 -0
  39. package/state.ts +4 -0
  40. package/tool-approval.ts +17 -4
  41. package/tool-metadata.ts +3 -1
  42. package/ts-shape.ts +1 -1
  43. package/types.ts +14 -2
  44. package/ui-server.ts +5 -2
  45. package/unix-socket-transport.ts +1 -1
  46. 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.callTool({
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
@@ -8,6 +8,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;
@@ -54,6 +55,9 @@ export interface McpExtensionState {
54
55
  failureMessages: Map<string, string>;
55
56
  /** Session-only approvals keyed by server, tool definition, and arguments. */
56
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>>;
57
61
  /** Configured project servers disabled by trust or approval policy for this session. */
58
62
  blockedProjectServers?: Map<string, ProjectServerBlock>;
59
63
  /** Runtime-only server grants. Never persisted or restored from session entries. */
package/tool-approval.ts CHANGED
@@ -25,15 +25,23 @@ export type ToolCallApprovalResult =
25
25
  export function isToolCallApprovalRequired(
26
26
  config: McpConfig,
27
27
  serverName: string,
28
- toolMeta: Pick<ToolMetadata, "originalName">,
28
+ toolMeta: Pick<ToolMetadata, "originalName" | "annotations" | "resourceUri">,
29
29
  toolMetadata?: ReadonlyMap<string, readonly ToolMetadata[]>,
30
30
  ): boolean {
31
31
  const definition = config.mcpServers[serverName];
32
32
  const serverApproval = definition?.approveTools;
33
33
  const approval = serverApproval !== undefined ? serverApproval : config.settings?.approveTools;
34
34
 
35
- if (approval === true) return true;
36
- if (!Array.isArray(approval) || approval.length === 0) return false;
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;
37
45
 
38
46
  const prefix = resolveToolPrefix(definition, config.settings?.toolPrefix);
39
47
  const currentCandidates = getToolNameCandidates(toolMeta.originalName, serverName, prefix, false);
@@ -180,10 +188,15 @@ export async function ensureToolCallApproved(
180
188
  const sanitized = sanitizeTerminalText(json);
181
189
  const preview = sanitized.length > 500 ? `${sanitized.slice(0, 500)}...` : sanitized;
182
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
+ : "";
183
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.";
184
197
  const decision = await abortable(
185
198
  state.ui.select(
186
- `${title}\n\nArguments:\n${preview}\n\n${serverScope}`,
199
+ `${title}${hint}\n\nArguments:\n${preview}\n\n${serverScope}`,
187
200
  ["Allow once", "Allow for session", "Allow server for this session", "Deny"],
188
201
  ),
189
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
 
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.callTool(callArgs, requestOptions);
548
+ return callToolPausingForElicitation(conn.client, callArgs, requestOptions);
546
549
  };
547
550
  const result = options.config
548
551
  ? await withSessionRecovery(
@@ -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
+ }