UNPKG

pi-mcp-extension

Version:

Full-featured, lightweight MCP extension for the Pi coding agent: stdio, SSE & Streamable HTTP transports, OAuth 2.1 (PKCE + DCR), lazy/eager lifecycles — minimal dependencies, zero bloat.

209 lines (190 loc) • 8.96 kB
/** * Pure deterministic tool-ordering policy and provider-payload transforms (#278). * * Keeps MCP tool positions in Pi's active list and in outgoing provider requests a pure * function of the tool set — independent of eager-connect timing, `tools/list` order, * `/tree` transcript restoration, mid-turn `setActiveTools` calls, and first-appearance * transcript replay — while leaving non-owned tools and provider-specific markers in place. */ function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); } /** True when two tool-name lists have the same length and the same entries in order. */ export function sameToolList(left: readonly string[], right: readonly string[]): boolean { if (left.length !== right.length) return false; return left.every((name, index) => name === right[index]); } /** Compare strings by Unicode code point, not by UTF-16 code unit. */ export function compareCodePoints(a: string, b: string): number { const left = Array.from(a, (ch) => ch.codePointAt(0) ?? 0); const right = Array.from(b, (ch) => ch.codePointAt(0) ?? 0); const shared = Math.min(left.length, right.length); for (let i = 0; i < shared; i++) { const diff = (left[i] ?? 0) - (right[i] ?? 0); if (diff !== 0) return diff; } return left.length - right.length; } /** * Sort the entries `isOwned` selects into name order inside the slots they already occupy, and * return `undefined` when the result is identical to `entries`. * * WHY: this bridge cannot move what it does not own — another extension's tools keep the position * they were given, and so does Pi's trailing Anthropic `__pi_deferred_placeholder__`, which Pi * requires last — while the bridge's own tools, whose insertion order depends on which server * connected first and on `tools/list` order, end up in one deterministic order (#278). */ export function orderOwnedEntries<T>( entries: readonly T[], nameOf: (entry: T) => string | undefined, isOwned: (name: string) => boolean, ): T[] | undefined { const owned: Array<{ slot: number; name: string; entry: T }> = []; entries.forEach((entry, slot) => { const name = nameOf(entry); if (name !== undefined && isOwned(name)) owned.push({ slot, name, entry }); }); if (owned.length < 2) return undefined; const slots = owned.map((candidate) => candidate.slot); owned.sort((left, right) => compareCodePoints(left.name, right.name)); const ordered = [...entries]; owned.forEach((candidate, index) => { const slot = slots[index]; if (slot === undefined) return; ordered[slot] = candidate.entry; }); const unchanged = ordered.every((entry, index) => entry === entries[index]); return unchanged ? undefined : ordered; } function nestedStringField( record: Record<string, unknown>, key: string, field: string, ): string | undefined { const nested = record[key]; if (!isRecord(nested)) return undefined; const value = nested[field]; return typeof value === "string" ? value : undefined; } /** * Name of a provider tool definition: `{ name }` (Anthropic, OpenAI/Azure/Codex Responses, * Gemini `functionDeclarations`, `pi-messages` `toolsAdded`), `{ function: { name } }` * (OpenAI Chat Completions, Mistral), or `{ toolSpec: { name } }` (AWS Bedrock Converse). */ function payloadToolName(definition: unknown): string | undefined { if (!isRecord(definition)) return undefined; const name = definition["name"]; if (typeof name === "string") return name; return ( nestedStringField(definition, "function", "name") ?? nestedStringField(definition, "toolSpec", "name") ); } /** * Keep a slot's `cache_control` breakpoint on that slot when another tool definition moves into * it: Pi's Anthropic and OpenAI-completions adapters attach `cache_control` to the last tool * slot before `before_provider_request` runs, so moving that object earlier would strand later * tools outside the cached prefix and diverge from a session that started in sorted order. */ function withSlotCacheControl(entry: unknown, slotEntry: unknown): unknown { const entryRecord = entry as Record<string, unknown>; const slotCache = (slotEntry as { cache_control?: unknown }).cache_control; if (entryRecord["cache_control"] === slotCache) return entry; if (slotCache === undefined) { const { cache_control: _dropped, ...rest } = entryRecord; return rest; } return { ...entryRecord, cache_control: slotCache }; } /** Order one tool-definition array in place of its owned slots, keeping slot `cache_control`. */ function orderDefinitionList( definitions: unknown, isOwned: (name: string) => boolean, ): unknown[] | undefined { if (!Array.isArray(definitions) || definitions.length === 0) return undefined; const ordered = orderOwnedEntries(definitions, payloadToolName, isOwned); if (ordered === undefined) return undefined; return ordered.map((entry, index) => entry === definitions[index] ? entry : withSlotCacheControl(entry, definitions[index]), ); } /** Order one Gemini `Tool` entry's `functionDeclarations` array when present. */ function orderGoogleToolGroup( group: unknown, isOwned: (name: string) => boolean, ): unknown { if (!isRecord(group)) return group; const ordered = orderDefinitionList(group["functionDeclarations"], isOwned); return ordered === undefined ? group : { ...group, functionDeclarations: ordered }; } /** Order Gemini / Vertex `config.tools[].functionDeclarations` without mutating untouched groups. */ function orderGoogleConfigTools( config: unknown, isOwned: (name: string) => boolean, ): Record<string, unknown> | undefined { if (!isRecord(config) || !Array.isArray(config["tools"])) return undefined; const tools = config["tools"]; const nextTools = tools.map((group) => orderGoogleToolGroup(group, isOwned)); const changed = nextTools.some((group, index) => group !== tools[index]); return changed ? { ...config, tools: nextTools } : undefined; } /** Order Bedrock Converse `toolConfig.tools` (`{ toolSpec: { name } }`) when present. */ function orderBedrockToolConfig( toolConfig: unknown, isOwned: (name: string) => boolean, ): Record<string, unknown> | undefined { if (!isRecord(toolConfig)) return undefined; const ordered = orderDefinitionList(toolConfig["tools"], isOwned); return ordered === undefined ? undefined : { ...toolConfig, tools: ordered }; } /** Order `toolsAdded` on one `pi-messages` `system` message while leaving other messages as is. */ function orderSystemMessageTools( message: unknown, isOwned: (name: string) => boolean, ): unknown { if (!isRecord(message) || message["role"] !== "system") return message; const ordered = orderDefinitionList(message["toolsAdded"], isOwned); return ordered === undefined ? message : { ...message, toolsAdded: ordered }; } /** * Order `pi-messages` `context.messages[].toolsAdded` within each `system` message without * moving declarations across turns or mutating untouched messages. */ function orderPiMessagesContext( context: unknown, isOwned: (name: string) => boolean, ): Record<string, unknown> | undefined { if (!isRecord(context) || !Array.isArray(context["messages"])) return undefined; const messages = context["messages"]; const nextMessages = messages.map((message) => orderSystemMessageTools(message, isOwned)); const changed = nextMessages.some((message, index) => message !== messages[index]); return changed ? { ...context, messages: nextMessages } : undefined; } /** * Order nested provider tool containers: Bedrock `toolConfig.tools`, Google `config.tools`, * or `pi-messages` `context.messages[].toolsAdded`. */ function orderNestedPayloadTools( payload: Record<string, unknown>, isOwned: (name: string) => boolean, ): Record<string, unknown> | undefined { const orderedToolConfig = orderBedrockToolConfig(payload["toolConfig"], isOwned); if (orderedToolConfig !== undefined) return { ...payload, toolConfig: orderedToolConfig }; const orderedGoogleConfig = orderGoogleConfigTools(payload["config"], isOwned); if (orderedGoogleConfig !== undefined) return { ...payload, config: orderedGoogleConfig }; const orderedContext = orderPiMessagesContext(payload["context"], isOwned); return orderedContext === undefined ? undefined : { ...payload, context: orderedContext }; } /** * Order owned tool definitions in an outgoing provider payload across all 10 built-in Pi API * payload shapes, returning `payload` untouched when no owned slots move. */ export function orderPayloadToolEntries( payload: unknown, isOwned: (name: string) => boolean, ): unknown { if (!isRecord(payload)) return payload; const orderedTopLevel = orderDefinitionList(payload["tools"], isOwned); if (orderedTopLevel !== undefined) return { ...payload, tools: orderedTopLevel }; return orderNestedPayloadTools(payload, isOwned) ?? payload; }