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/init.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { McpExtensionState } from "./state.ts";
|
|
|
3
3
|
import { formatToolName, isServerDisabled, resolveToolPrefix, type McpAdapterOptions, type PromptMetadata, type ToolMetadata, type ToolSelectorCandidateIndex } from "./types.ts";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { cloneMcpConfig, loadMcpConfig, resolveConfiguredClaudePluginMcp } from "./config.ts";
|
|
6
|
-
import { applyProjectServerTrustToConfig, describeProjectServerBlock } from "./project-server-trust.ts";
|
|
6
|
+
import { applyProjectServerTrustToConfig, describeProjectServerBlock, excludeProjectServersAtLoadTime } from "./project-server-trust.ts";
|
|
7
7
|
import { ConsentManager } from "./consent-manager.ts";
|
|
8
8
|
import { McpLifecycleManager } from "./lifecycle.ts";
|
|
9
9
|
import {
|
|
@@ -104,6 +104,8 @@ type McpInitializationOptions = McpAdapterOptions & {
|
|
|
104
104
|
oauthRuntime?: McpOAuthRuntime;
|
|
105
105
|
statusEvents?: McpExtensionState["statusEvents"];
|
|
106
106
|
onProjectTrustResolved?: () => void;
|
|
107
|
+
/** Load-time runs have no Pi context, so project trust is unknown; session_start decides later. */
|
|
108
|
+
excludeProjectServers?: boolean;
|
|
107
109
|
};
|
|
108
110
|
|
|
109
111
|
export async function initializeMcp(
|
|
@@ -142,7 +144,9 @@ export async function initializeMcp(
|
|
|
142
144
|
const runtimeSignal = combineAbortSignals(owner.signal, initialSignal);
|
|
143
145
|
const trustResult = options.config !== undefined
|
|
144
146
|
? { config: resolveConfiguredClaudePluginMcp(cloneMcpConfig(options.config), cwd), blockedServers: new Map() }
|
|
145
|
-
:
|
|
147
|
+
: options.excludeProjectServers
|
|
148
|
+
? { config: excludeProjectServersAtLoadTime(loadMcpConfig(configPath, cwd)), blockedServers: new Map() }
|
|
149
|
+
: await applyProjectServerTrustToConfig(loadMcpConfig(configPath, cwd), ctx);
|
|
146
150
|
options.onProjectTrustResolved?.();
|
|
147
151
|
const config = trustResult.config;
|
|
148
152
|
const authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd, config.settings?.oauthCredentialStore);
|
|
@@ -593,6 +597,7 @@ export function updateMetadataCache(
|
|
|
593
597
|
if (!definition || isServerDisabled(definition)) return;
|
|
594
598
|
|
|
595
599
|
const configHash = computeServerHash(definition);
|
|
600
|
+
if (connection.definition && computeServerHash(connection.definition) !== configHash) return;
|
|
596
601
|
const existing = loadMetadataCache();
|
|
597
602
|
const existingEntry = existing?.servers?.[serverName];
|
|
598
603
|
|
|
@@ -602,13 +607,13 @@ export function updateMetadataCache(
|
|
|
602
607
|
? existingEntry?.configHash === configHash ? existingEntry.prompts : undefined
|
|
603
608
|
: serializePrompts(connection.prompts ?? []);
|
|
604
609
|
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
isServerCacheValid(existingEntry, definition)
|
|
610
|
-
|
|
611
|
-
|
|
610
|
+
if (definition.exposeResources !== false && connection.resourceDiscoveryFailed === true) {
|
|
611
|
+
const sessionEntry = state.sessionMetadata?.get(serverName);
|
|
612
|
+
if (sessionEntry?.configHash === configHash) {
|
|
613
|
+
resources = sessionEntry.resources ?? [];
|
|
614
|
+
} else if (existingEntry?.resources?.length && isServerCacheValid(existingEntry, definition)) {
|
|
615
|
+
resources = existingEntry.resources;
|
|
616
|
+
}
|
|
612
617
|
}
|
|
613
618
|
|
|
614
619
|
const entry: ServerCacheEntry = {
|
|
@@ -622,6 +627,7 @@ export function updateMetadataCache(
|
|
|
622
627
|
cachedAt: Date.now(),
|
|
623
628
|
};
|
|
624
629
|
|
|
630
|
+
(state.sessionMetadata ??= new Map()).set(serverName, entry);
|
|
625
631
|
saveMetadataCache({ version: 1, servers: { [serverName]: entry } });
|
|
626
632
|
}
|
|
627
633
|
|
package/jev-client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
|
2
2
|
import type { McpExtensionState } from "./state.ts";
|
|
3
3
|
import { isServerDisabled } from "./types.ts";
|
|
4
4
|
import { combineAbortSignals } from "./runtime-owner.ts";
|
|
5
|
-
import { JEV_SDK_PATH, resolveJevCredential, resolveJevEndpoint, type JevCredentialResolution, type ResolvedJevEndpoint } from "./jev-key-store.ts";
|
|
5
|
+
import { JEV_SDK_PATH, OPENROUTER_ORIGIN, resolveJevCredential, resolveJevEndpoint, type JevCredentialResolution, type ResolvedJevEndpoint } from "./jev-key-store.ts";
|
|
6
6
|
import type { JevAnswer, JevBudget, JevEvaluateInput, JevEvaluationData, JevEvaluationEnvelope, JevJson, JevQuestion, ResolvedJevSettings } from "./jev-contracts.ts";
|
|
7
7
|
|
|
8
8
|
export type { ResolvedJevSettings } from "./jev-contracts.ts";
|
|
@@ -15,6 +15,8 @@ const DEFAULTS: ResolvedJevSettings = {
|
|
|
15
15
|
semanticMinProbability: 0.2,
|
|
16
16
|
};
|
|
17
17
|
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
18
|
+
/** OpenRouter's name for Jev, used there when `settings.jev.model` is not configured. */
|
|
19
|
+
const OPENROUTER_DEFAULT_MODEL = "typesafe/jev-1.13";
|
|
18
20
|
|
|
19
21
|
type JevSettingsInput = Partial<ResolvedJevSettings>;
|
|
20
22
|
type Host = { endpoint: ResolvedJevEndpoint; client: TypeSafeClient };
|
|
@@ -192,8 +194,8 @@ function sameKeys(actual: Record<string, unknown>, expected: string[]): boolean
|
|
|
192
194
|
return keys.length === expected.length && expected.every(key => Object.hasOwn(actual, key));
|
|
193
195
|
}
|
|
194
196
|
function validateResponse(value: unknown, input: JevEvaluateInput): JevEvaluationData {
|
|
197
|
+
// Providers may add metadata such as `id`, `provider`, or `usage.cost`; only the fields validated here are kept.
|
|
195
198
|
const response = record(value, "response");
|
|
196
|
-
exactKeys(response, ["model", "answers", "usage"], "response");
|
|
197
199
|
const answers = record(response.answers, "response answers");
|
|
198
200
|
const questionNames = Object.keys(input.questions);
|
|
199
201
|
if (!sameKeys(answers, questionNames)) throw new Error("Response answer keys do not match questions");
|
|
@@ -224,7 +226,6 @@ function validateResponse(value: unknown, input: JevEvaluateInput): JevEvaluatio
|
|
|
224
226
|
}
|
|
225
227
|
if (typeof response.model !== "string" || response.model.length === 0 || response.model.length > 128) throw new Error("Invalid response model");
|
|
226
228
|
const usage = record(response.usage, "response usage");
|
|
227
|
-
if (!sameKeys(usage, ["input_tokens", "output_tokens"])) throw new Error("Invalid response usage");
|
|
228
229
|
return { answers: validated, model: response.model, usage: { inputTokens: safeNumber(usage.input_tokens, "input tokens", true), outputTokens: safeNumber(usage.output_tokens, "output tokens", true) } };
|
|
229
230
|
}
|
|
230
231
|
|
|
@@ -291,7 +292,9 @@ export async function evaluateJev(state: McpExtensionState, value: JevEvaluateIn
|
|
|
291
292
|
const timer = setTimeout(() => { timedOut = true; deadline.abort(new Error("Jev evaluation deadline exceeded")); }, settings.requestTimeoutMs);
|
|
292
293
|
const signal = combineAbortSignals(options.signal, state.owner.signal, deadline.signal);
|
|
293
294
|
try {
|
|
294
|
-
const
|
|
295
|
+
const configured = state.config.settings?.jev;
|
|
296
|
+
const model = resolved.endpoint.origin === OPENROUTER_ORIGIN && (!configured || configured.model === undefined) ? OPENROUTER_DEFAULT_MODEL : settings.model;
|
|
297
|
+
const request = { state: input.state, questions: input.questions, model };
|
|
295
298
|
const raw = await resolved.client.systemOne(request as Parameters<TypeSafeClient["systemOne"]>[0], { ...(signal ? { signal } : {}), timeout: settings.requestTimeoutMs, retry: { maxRetries: settings.maxRetries } });
|
|
296
299
|
try { return { ok: true, data: validateResponse(raw, input) }; }
|
|
297
300
|
catch { return { ok: false, error: { code: "invalid_response", message: "Jev returned an invalid response." } }; }
|
package/jev-key-store.ts
CHANGED
|
@@ -6,6 +6,9 @@ const SYSTEMONE_ENDPOINT_ENV = "SYSTEMONE_ENDPOINT";
|
|
|
6
6
|
const SYSTEMONE_API_KEY_ENV = "SYSTEMONE_API_KEY";
|
|
7
7
|
/** TypeSafe-issued key: honored only for the default endpoint and never sent anywhere else. */
|
|
8
8
|
const LEGACY_TYPESAFE_API_KEY_ENV = "TYPESAFE_API_KEY";
|
|
9
|
+
/** OpenRouter's general-purpose key: honored only for an OpenRouter endpoint and never sent anywhere else. */
|
|
10
|
+
const OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
|
|
11
|
+
export const OPENROUTER_ORIGIN = "https://openrouter.ai";
|
|
9
12
|
/** Origin recorded in version 1 credential records. */
|
|
10
13
|
export const TYPESAFE_API_ORIGIN = "https://api.typesafe.ai";
|
|
11
14
|
/** Version 1 credential account, still read for the default endpoint. */
|
|
@@ -158,6 +161,10 @@ export function resolveJevCredential(
|
|
|
158
161
|
try { return { status: "present", source: "environment", apiKey: validateApiKey(env[LEGACY_TYPESAFE_API_KEY_ENV]) }; }
|
|
159
162
|
catch { return { status: "unavailable", message: `${LEGACY_TYPESAFE_API_KEY_ENV} is present but invalid.` }; }
|
|
160
163
|
}
|
|
164
|
+
if (target.origin === OPENROUTER_ORIGIN && Object.hasOwn(env, OPENROUTER_API_KEY_ENV)) {
|
|
165
|
+
try { return { status: "present", source: "environment", apiKey: validateApiKey(env[OPENROUTER_API_KEY_ENV]) }; }
|
|
166
|
+
catch { return { status: "unavailable", message: `${OPENROUTER_API_KEY_ENV} is present but invalid.` }; }
|
|
167
|
+
}
|
|
161
168
|
let stored: string | undefined;
|
|
162
169
|
try { stored = readStoredKey(secretStore, target); }
|
|
163
170
|
catch (error) {
|
package/mcp-callback-server.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http"
|
|
9
9
|
import { getAppName } from "./agent-dir.ts"
|
|
10
10
|
import {
|
|
11
|
+
DEFAULT_OAUTH_CALLBACK_HOST,
|
|
11
12
|
DEFAULT_OAUTH_CALLBACK_PATH,
|
|
12
13
|
getConfiguredOAuthCallbackPort,
|
|
13
14
|
getOAuthCallbackPath,
|
|
@@ -197,7 +198,6 @@ interface EnsureCallbackServerOptions {
|
|
|
197
198
|
reserveState?: boolean
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
const DEFAULT_OAUTH_CALLBACK_HOST = "localhost"
|
|
201
201
|
let callbackServerHost = DEFAULT_OAUTH_CALLBACK_HOST
|
|
202
202
|
|
|
203
203
|
/**
|
package/mcp-code.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { formatWithOptions } from "node:util";
|
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { throwIfAborted } from "./abort.ts";
|
|
5
5
|
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
6
|
-
import { loadMcpScriptWasm } from "./mcp-script-wasm.ts";
|
|
6
|
+
import { loadMcpScriptWasm, resolveMcpScriptQuickJsUrl } from "./mcp-script-wasm.ts";
|
|
7
7
|
import { evaluateJev, validateJevSettings } from "./jev-client.ts";
|
|
8
8
|
import type { JevErrorCode, JevEvaluateInput, JevEvaluationEnvelope } from "./jev-contracts.ts";
|
|
9
9
|
import { executeCall } from "./proxy-modes.ts";
|
|
@@ -14,6 +14,7 @@ import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
|
|
|
14
14
|
import type { McpExtensionState } from "./state.ts";
|
|
15
15
|
import { findToolByName, formatSchema, hasSchemaDescriptions } from "./tool-metadata.ts";
|
|
16
16
|
import { renderTsShape } from "./ts-shape.ts";
|
|
17
|
+
import { getObservedOutput, renderOutputShape } from "./output-shape.ts";
|
|
17
18
|
import type { ContentBlock } from "./types.ts";
|
|
18
19
|
|
|
19
20
|
export const DEFAULT_MCP_SCRIPT_TIMEOUT_MS = 30_000;
|
|
@@ -338,6 +339,7 @@ export async function runMcpScript(
|
|
|
338
339
|
if (!tool) continue;
|
|
339
340
|
const inputShape = tool.inputSchema ? renderTsShape(tool.inputSchema) : null;
|
|
340
341
|
const inputTypeScript = inputShape ?? (tool.inputSchema ? formatSchema(tool.inputSchema) : null);
|
|
342
|
+
const observed = tool.resourceUri ? undefined : getObservedOutput(state, server, tool);
|
|
341
343
|
return {
|
|
342
344
|
path: tool.name,
|
|
343
345
|
name: tool.originalName,
|
|
@@ -350,6 +352,14 @@ export async function runMcpScript(
|
|
|
350
352
|
outputSchemaTarget: "data.structuredContent",
|
|
351
353
|
outputSchema: tool.outputSchema,
|
|
352
354
|
} : {}),
|
|
355
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
356
|
+
...(observed ? {
|
|
357
|
+
observedOutput: {
|
|
358
|
+
target: observed.source === "structuredContent" ? "data.structuredContent" : "JSON.parse(data.content[0].text)",
|
|
359
|
+
typeScript: renderOutputShape(observed.shape),
|
|
360
|
+
calls: observed.calls,
|
|
361
|
+
},
|
|
362
|
+
} : {}),
|
|
353
363
|
};
|
|
354
364
|
}
|
|
355
365
|
const suggestions = path ? rankSuggestions(state, path, 5) : [];
|
|
@@ -408,7 +418,7 @@ export async function runMcpScript(
|
|
|
408
418
|
try {
|
|
409
419
|
const wasm = await Promise.race([loadMcpScriptWasm(), timeout, aborted]);
|
|
410
420
|
worker = new Worker(new URL("./mcp-script-worker.mjs", import.meta.url), {
|
|
411
|
-
workerData: { code, wasm, interrupt, outputMaxBytes: MCP_SCRIPT_OUTPUT_MAX_BYTES },
|
|
421
|
+
workerData: { code, wasm, quickjsUrl: resolveMcpScriptQuickJsUrl(), interrupt, outputMaxBytes: MCP_SCRIPT_OUTPUT_MAX_BYTES },
|
|
412
422
|
env: {},
|
|
413
423
|
// The sandbox cannot open files, and the host always terminates this worker.
|
|
414
424
|
// Disable Node's unmanaged FD bookkeeping, which emits false warnings when
|
package/mcp-oauth-provider.ts
CHANGED
|
@@ -89,6 +89,9 @@ function toOAuthTokens(tokens: StoredTokens): IssuerBoundTokens {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Callback server configuration
|
|
92
|
+
// RFC 8252 section 7.3: use the loopback IP literal, not "localhost". The
|
|
93
|
+
// callback listener binds this same host so the advertised redirect reaches it.
|
|
94
|
+
const DEFAULT_OAUTH_CALLBACK_HOST = "127.0.0.1"
|
|
92
95
|
const DEFAULT_OAUTH_CALLBACK_PORT = 19876
|
|
93
96
|
const DEFAULT_OAUTH_CALLBACK_PATH = "/callback"
|
|
94
97
|
|
|
@@ -287,7 +290,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
287
290
|
this.flowState = initialState
|
|
288
291
|
this.redirectUrlSnapshot = config.grantType === "client_credentials"
|
|
289
292
|
? undefined
|
|
290
|
-
: config.redirectUri ?? `http
|
|
293
|
+
: config.redirectUri ?? `http://${DEFAULT_OAUTH_CALLBACK_HOST}:${getOAuthCallbackPort()}${getOAuthCallbackPath()}`
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
setAuthFetch(fetchFn: OAuthFetch): void {
|
|
@@ -816,4 +819,4 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
816
819
|
}
|
|
817
820
|
}
|
|
818
821
|
|
|
819
|
-
export { DEFAULT_OAUTH_CALLBACK_PORT, DEFAULT_OAUTH_CALLBACK_PATH }
|
|
822
|
+
export { DEFAULT_OAUTH_CALLBACK_HOST, DEFAULT_OAUTH_CALLBACK_PORT, DEFAULT_OAUTH_CALLBACK_PATH }
|
package/mcp-script-wasm.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
3
4
|
|
|
4
5
|
export type McpScriptWasmModule = object;
|
|
5
6
|
|
|
6
7
|
const cached = new Map<string, Promise<McpScriptWasmModule>>();
|
|
7
8
|
|
|
9
|
+
function resolveQuickJsWasmPath(): string {
|
|
10
|
+
return createRequire(import.meta.url).resolve("quickjs-wasi/quickjs.wasm");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the quickjs-wasi entry on the host thread. The worker imports this
|
|
15
|
+
* file URL because Bun-compiled executables cannot resolve bare package names
|
|
16
|
+
* from a worker file loaded from disk.
|
|
17
|
+
* Those executables also fail to resolve the bare `quickjs-wasi` root on the
|
|
18
|
+
* host thread, while package subpaths resolve. So the entry is located next to
|
|
19
|
+
* the wasm file, which is how quickjs-wasi's `"."` export lays it out.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveMcpScriptQuickJsUrl(): string {
|
|
22
|
+
return new URL("./dist/index.js", pathToFileURL(resolveQuickJsWasmPath())).href;
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
/** Load and compile the packaged QuickJS runtime once per host process. */
|
|
9
|
-
export function loadMcpScriptWasm(path =
|
|
26
|
+
export function loadMcpScriptWasm(path = resolveQuickJsWasmPath()): Promise<McpScriptWasmModule> {
|
|
10
27
|
let module = cached.get(path);
|
|
11
28
|
if (!module) {
|
|
12
29
|
const webAssembly = (globalThis as unknown as {
|
package/mcp-script-worker.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
-
import { JSException, MAX_STACK_SIZE, QuickJS } from "quickjs-wasi";
|
|
3
2
|
|
|
4
3
|
const MEMORY_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
5
4
|
const ERROR_MAX_BYTES = 64 * 1024;
|
|
6
5
|
const ERROR_TRUNCATION_MARKER = "\n...[mcpScript error truncated]";
|
|
7
6
|
|
|
7
|
+
// Set once main() imports quickjs-wasi from the host-resolved file URL.
|
|
8
|
+
let JSException;
|
|
9
|
+
|
|
8
10
|
function post(message) {
|
|
9
11
|
parentPort?.postMessage(message);
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
function errorText(error) {
|
|
13
|
-
if (error instanceof JSException) {
|
|
15
|
+
if (JSException && error instanceof JSException) {
|
|
14
16
|
const head = error.message ? `${error.name}: ${error.message}` : error.name;
|
|
15
17
|
return error.stack ? `${head}\n${error.stack}` : head;
|
|
16
18
|
}
|
|
@@ -172,6 +174,11 @@ const PRELUDE_SOURCE = `(function (bridge) {
|
|
|
172
174
|
})`;
|
|
173
175
|
|
|
174
176
|
async function main() {
|
|
177
|
+
// Bun-compiled executables cannot resolve bare package names from this
|
|
178
|
+
// worker file, so import quickjs-wasi from the URL the host resolved.
|
|
179
|
+
const quickjs = await import(workerData.quickjsUrl);
|
|
180
|
+
JSException = quickjs.JSException;
|
|
181
|
+
const { MAX_STACK_SIZE, QuickJS } = quickjs;
|
|
175
182
|
const interrupt = new Int32Array(workerData.interrupt);
|
|
176
183
|
let outputBytes = 0;
|
|
177
184
|
let outputExceeded = false;
|
package/metadata-cache.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import { createToolSelectorCandidateIndex, formatPromptCommandName, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix, resolveUniqueNameOwnership, type ToolPrefix, type ToolSelectorCandidateIndex } from "./types.ts";
|
|
24
24
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
25
25
|
import {
|
|
26
|
+
extractToolAnnotations,
|
|
26
27
|
extractToolUiStreamMode,
|
|
27
28
|
interpolateEnvRecord,
|
|
28
29
|
interpolateEnvVars,
|
|
@@ -225,6 +226,7 @@ export function reconstructToolMetadata(
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
const name = formatToolName(tool.name, serverName, effectivePrefix);
|
|
229
|
+
const annotations = extractToolAnnotations(tool.annotations);
|
|
228
230
|
metadata.push({
|
|
229
231
|
name,
|
|
230
232
|
originalName: tool.name,
|
|
@@ -234,6 +236,7 @@ export function reconstructToolMetadata(
|
|
|
234
236
|
...(tool.uiResourceUri !== undefined ? { uiResourceUri: tool.uiResourceUri } : {}),
|
|
235
237
|
...(tool.uiVisibility !== undefined ? { uiVisibility: tool.uiVisibility } : {}),
|
|
236
238
|
...(tool.uiStreamMode !== undefined ? { uiStreamMode: tool.uiStreamMode } : {}),
|
|
239
|
+
...(annotations !== undefined ? { annotations } : {}),
|
|
237
240
|
});
|
|
238
241
|
}
|
|
239
242
|
|
|
@@ -289,6 +292,7 @@ export function serializeTools(tools: McpTool[]): CachedTool[] {
|
|
|
289
292
|
const uiResourceUri = tryGetToolUiResourceUri(t);
|
|
290
293
|
const uiVisibility = extractUiToolVisibility(t._meta);
|
|
291
294
|
const uiStreamMode = extractToolUiStreamMode(t._meta);
|
|
295
|
+
const annotations = extractToolAnnotations(t.annotations);
|
|
292
296
|
return {
|
|
293
297
|
name: t.name,
|
|
294
298
|
...(t.description !== undefined ? { description: t.description } : {}),
|
|
@@ -297,6 +301,7 @@ export function serializeTools(tools: McpTool[]): CachedTool[] {
|
|
|
297
301
|
...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
|
|
298
302
|
...(uiVisibility !== undefined ? { uiVisibility } : {}),
|
|
299
303
|
...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
|
|
304
|
+
...(annotations !== undefined ? { annotations } : {}),
|
|
300
305
|
};
|
|
301
306
|
});
|
|
302
307
|
}
|
package/namespace-tools.ts
CHANGED
|
@@ -190,6 +190,10 @@ export interface SyncNamespaceProxyToolsInput {
|
|
|
190
190
|
activeDirectNames?: ReadonlySet<string>;
|
|
191
191
|
existingNamespaceNames: Set<string>;
|
|
192
192
|
unavailableServers?: ReadonlySet<string>;
|
|
193
|
+
// Names the adapter removed from Pi's active tools because Pi could not
|
|
194
|
+
// unregister them. Pi does not re-activate a name it already knows, so a
|
|
195
|
+
// later re-registration must add the name back.
|
|
196
|
+
fallbackDeactivatedNames?: Set<string>;
|
|
193
197
|
pi: ExtensionAPI;
|
|
194
198
|
getState: GetState;
|
|
195
199
|
getInitPromise: GetInitPromise;
|
|
@@ -265,10 +269,19 @@ function registerNamespaceProxyTool(
|
|
|
265
269
|
),
|
|
266
270
|
});
|
|
267
271
|
input.guardReentrant?.();
|
|
272
|
+
if (input.fallbackDeactivatedNames?.delete(spec.toolName)) {
|
|
273
|
+
// Only undo a fallback deactivation the adapter still owns.
|
|
274
|
+
input.guardReentrant?.();
|
|
275
|
+
const activeTools = getActiveToolsIfReady(input.pi);
|
|
276
|
+
input.guardReentrant?.();
|
|
277
|
+
if (activeTools && !activeTools.includes(spec.toolName)) {
|
|
278
|
+
input.pi.setActiveTools([...activeTools, spec.toolName]);
|
|
279
|
+
input.guardReentrant?.();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
268
282
|
}
|
|
269
283
|
|
|
270
|
-
function
|
|
271
|
-
if (staleNames.length === 0) return undefined;
|
|
284
|
+
function getActiveToolsIfReady(pi: ExtensionAPI): string[] | undefined {
|
|
272
285
|
try {
|
|
273
286
|
return pi.getActiveTools?.();
|
|
274
287
|
} catch (error) {
|
|
@@ -277,6 +290,11 @@ function getActiveToolsForStaleCleanup(pi: ExtensionAPI, staleNames: string[]):
|
|
|
277
290
|
}
|
|
278
291
|
}
|
|
279
292
|
|
|
293
|
+
function getActiveToolsForStaleCleanup(pi: ExtensionAPI, staleNames: string[]): string[] | undefined {
|
|
294
|
+
if (staleNames.length === 0) return undefined;
|
|
295
|
+
return getActiveToolsIfReady(pi);
|
|
296
|
+
}
|
|
297
|
+
|
|
280
298
|
function deactivateStaleNamespaceTools(input: SyncNamespaceProxyToolsInput, nextNames: Set<string>): string[] {
|
|
281
299
|
const activeDirectNames = input.activeDirectNames ?? new Set<string>();
|
|
282
300
|
const staleNames = [...input.existingNamespaceNames].filter(
|
|
@@ -298,12 +316,14 @@ function deactivateStaleNamespaceTools(input: SyncNamespaceProxyToolsInput, next
|
|
|
298
316
|
if (!activeTools) return deactivated;
|
|
299
317
|
|
|
300
318
|
const stale = new Set(staleNames);
|
|
319
|
+
const fallbackRemoved = staleNames.filter((name) => !deactivated.includes(name) && activeTools.includes(name));
|
|
301
320
|
const nextActiveTools = activeTools.filter((name) => !stale.has(name));
|
|
302
321
|
if (nextActiveTools.length === activeTools.length) return deactivated;
|
|
303
322
|
|
|
304
323
|
input.guardReentrant?.();
|
|
305
324
|
input.pi.setActiveTools(nextActiveTools);
|
|
306
325
|
input.guardReentrant?.();
|
|
326
|
+
for (const name of fallbackRemoved) input.fallbackDeactivatedNames?.add(name);
|
|
307
327
|
for (const name of staleNames) {
|
|
308
328
|
if (!deactivated.includes(name)) deactivated.push(name);
|
|
309
329
|
}
|
package/output-shape.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { McpExtensionState } from "./state.ts";
|
|
2
|
+
import { formatPropertyName } from "./ts-shape.ts";
|
|
3
|
+
import type { ToolMetadata } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Session-only output shapes for tools that declare no outputSchema.
|
|
7
|
+
* Field names and broad JSON types only, never values; every dimension is
|
|
8
|
+
* bounded because inference runs on arbitrary upstream payloads after each call.
|
|
9
|
+
*/
|
|
10
|
+
export interface OutputShape {
|
|
11
|
+
type?: "null" | "boolean" | "number" | "string" | "object" | "array";
|
|
12
|
+
properties?: Record<string, OutputShape>;
|
|
13
|
+
required?: string[];
|
|
14
|
+
items?: OutputShape;
|
|
15
|
+
additionalProperties?: OutputShape;
|
|
16
|
+
anyOf?: OutputShape[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ObservedOutput {
|
|
20
|
+
source: "structuredContent" | "jsonText";
|
|
21
|
+
shape: OutputShape;
|
|
22
|
+
calls: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAX_DEPTH = 6;
|
|
26
|
+
const MAX_ARRAY_SAMPLE = 5;
|
|
27
|
+
// Objects wider than this, or with any key that does not look like a field name, are treated as maps
|
|
28
|
+
// keyed by data (ids, emails, dates), so their keys are not kept.
|
|
29
|
+
const MAX_OBJECT_KEYS = 40;
|
|
30
|
+
const FIELD_NAME = /^[A-Za-z_$][A-Za-z0-9_$]{0,39}$/;
|
|
31
|
+
const MAX_UNION = 4;
|
|
32
|
+
const MAX_NODES_PER_CALL = 1000;
|
|
33
|
+
// Sizes count UTF-16 code units (string length), not bytes.
|
|
34
|
+
const MAX_SHAPE_CHARS = 4 * 1024;
|
|
35
|
+
const MAX_JSON_TEXT_CHARS = 256 * 1024;
|
|
36
|
+
|
|
37
|
+
export function recordObservedOutput(
|
|
38
|
+
state: McpExtensionState,
|
|
39
|
+
serverName: string,
|
|
40
|
+
toolName: string,
|
|
41
|
+
result: Record<string, unknown>,
|
|
42
|
+
): void {
|
|
43
|
+
if (findTool(state, serverName, toolName)?.outputSchema !== undefined) return;
|
|
44
|
+
const observed = readResultValue(result);
|
|
45
|
+
if (!observed) return;
|
|
46
|
+
const definition = state.config.mcpServers[serverName];
|
|
47
|
+
if (!definition) return;
|
|
48
|
+
const shape = inferShape(observed.value, 0, { nodes: MAX_NODES_PER_CALL });
|
|
49
|
+
const observedOutputs = state.observedOutputs ??= new WeakMap();
|
|
50
|
+
let byTool = observedOutputs.get(definition);
|
|
51
|
+
if (!byTool) observedOutputs.set(definition, byTool = new Map());
|
|
52
|
+
const previous = byTool.get(toolName);
|
|
53
|
+
byTool.set(toolName, previous?.source === observed.source
|
|
54
|
+
? { source: observed.source, shape: fitShape(mergeShapes(previous.shape, shape)), calls: previous.calls + 1 }
|
|
55
|
+
: { source: observed.source, shape: fitShape(shape), calls: 1 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getObservedOutput(
|
|
59
|
+
state: McpExtensionState,
|
|
60
|
+
serverName: string,
|
|
61
|
+
tool: Pick<ToolMetadata, "originalName" | "outputSchema">,
|
|
62
|
+
): ObservedOutput | undefined {
|
|
63
|
+
if (tool.outputSchema !== undefined) return undefined;
|
|
64
|
+
const definition = state.config.mcpServers[serverName];
|
|
65
|
+
const observed = definition && state.observedOutputs?.get(definition)?.get(tool.originalName);
|
|
66
|
+
return observed && !isUnknown(observed.shape) ? observed : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function renderOutputShape(shape: OutputShape): string {
|
|
70
|
+
if (shape.anyOf) return shape.anyOf.map(renderOutputShape).join(" | ");
|
|
71
|
+
switch (shape.type) {
|
|
72
|
+
case undefined:
|
|
73
|
+
return "unknown";
|
|
74
|
+
case "object": {
|
|
75
|
+
if (shape.additionalProperties) return `Record<string, ${renderOutputShape(shape.additionalProperties)}>`;
|
|
76
|
+
const required = new Set(shape.required);
|
|
77
|
+
const properties = Object.entries(shape.properties ?? {})
|
|
78
|
+
.map(([name, property]) => `${formatPropertyName(name)}${required.has(name) ? "" : "?"}: ${renderOutputShape(property)};`);
|
|
79
|
+
return properties.length === 0 ? "{}" : `{ ${properties.join(" ")} }`;
|
|
80
|
+
}
|
|
81
|
+
case "array": {
|
|
82
|
+
if (!shape.items) return "unknown[]";
|
|
83
|
+
const item = renderOutputShape(shape.items);
|
|
84
|
+
return item.includes(" | ") ? `(${item})[]` : `${item}[]`;
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return shape.type;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findTool(state: McpExtensionState, serverName: string, toolName: string): ToolMetadata | undefined {
|
|
92
|
+
return state.toolMetadata.get(serverName)?.find(tool => tool.originalName === toolName && !tool.resourceUri);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readResultValue(result: Record<string, unknown>): { source: ObservedOutput["source"]; value: unknown } | undefined {
|
|
96
|
+
const structured = result.structuredContent;
|
|
97
|
+
if (typeof structured === "object" && structured !== null) return { source: "structuredContent", value: structured };
|
|
98
|
+
const content = result.content;
|
|
99
|
+
if (!Array.isArray(content) || content.length !== 1) return undefined;
|
|
100
|
+
const block = content[0] as { type?: unknown; text?: unknown };
|
|
101
|
+
if (block.type !== "text" || typeof block.text !== "string" || block.text.length > MAX_JSON_TEXT_CHARS) return undefined;
|
|
102
|
+
const text = block.text.trim();
|
|
103
|
+
if (!text.startsWith("{") && !text.startsWith("[")) return undefined;
|
|
104
|
+
try {
|
|
105
|
+
return { source: "jsonText", value: JSON.parse(text) };
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function inferShape(value: unknown, depth: number, budget: { nodes: number }): OutputShape {
|
|
112
|
+
if (--budget.nodes < 0) return {};
|
|
113
|
+
if (value === null) return { type: "null" };
|
|
114
|
+
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return { type: typeof value as "boolean" | "number" | "string" };
|
|
115
|
+
if (typeof value !== "object" || depth >= MAX_DEPTH) return {};
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
const items = value.slice(0, MAX_ARRAY_SAMPLE).map(item => inferShape(item, depth + 1, budget));
|
|
118
|
+
return items.length === 0 ? { type: "array" } : { type: "array", items: items.reduce(mergeShapes) };
|
|
119
|
+
}
|
|
120
|
+
const record = value as Record<string, unknown>;
|
|
121
|
+
const keys: string[] = [];
|
|
122
|
+
let isMap = false;
|
|
123
|
+
// Stops at the first key that makes this a map, so a huge object costs O(MAX_OBJECT_KEYS) here.
|
|
124
|
+
for (const key in record) {
|
|
125
|
+
if (!Object.hasOwn(record, key)) continue;
|
|
126
|
+
keys.push(key);
|
|
127
|
+
// An own "__proto__" key would set the prototype of the shape's properties object instead of adding a field.
|
|
128
|
+
if (keys.length > MAX_OBJECT_KEYS || !FIELD_NAME.test(key) || /\d{4}/.test(key) || key === "__proto__") {
|
|
129
|
+
isMap = true;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (isMap) {
|
|
134
|
+
const values = keys.slice(0, MAX_ARRAY_SAMPLE).map(key => inferShape(record[key], depth + 1, budget));
|
|
135
|
+
return { type: "object", additionalProperties: values.reduce(mergeShapes) };
|
|
136
|
+
}
|
|
137
|
+
const properties: Record<string, OutputShape> = {};
|
|
138
|
+
for (const key of keys) properties[key] = inferShape(record[key], depth + 1, budget);
|
|
139
|
+
return { type: "object", properties, required: keys };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isUnknown(shape: OutputShape): boolean {
|
|
143
|
+
return shape.type === undefined && shape.anyOf === undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function mergeShapes(left: OutputShape, right: OutputShape): OutputShape {
|
|
147
|
+
if (isUnknown(left) || isUnknown(right)) return {};
|
|
148
|
+
const variants = [...(left.anyOf ?? [left])];
|
|
149
|
+
for (const variant of right.anyOf ?? [right]) {
|
|
150
|
+
const index = variants.findIndex(existing => existing.type === variant.type);
|
|
151
|
+
if (index === -1) variants.push(variant);
|
|
152
|
+
else variants[index] = mergeSameType(variants[index]!, variant);
|
|
153
|
+
}
|
|
154
|
+
if (variants.length > MAX_UNION) return {};
|
|
155
|
+
return variants.length === 1 ? variants[0]! : { anyOf: variants };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mergeSameType(left: OutputShape, right: OutputShape): OutputShape {
|
|
159
|
+
if (left.type === "array") {
|
|
160
|
+
const items = left.items && right.items ? mergeShapes(left.items, right.items) : left.items ?? right.items;
|
|
161
|
+
return items ? { type: "array", items } : { type: "array" };
|
|
162
|
+
}
|
|
163
|
+
if (left.type !== "object") return left;
|
|
164
|
+
const keys = new Set([...Object.keys(left.properties ?? {}), ...Object.keys(right.properties ?? {})]);
|
|
165
|
+
if (left.additionalProperties || right.additionalProperties || keys.size > MAX_OBJECT_KEYS) {
|
|
166
|
+
const values = [
|
|
167
|
+
left.additionalProperties,
|
|
168
|
+
right.additionalProperties,
|
|
169
|
+
...Object.values(left.properties ?? {}),
|
|
170
|
+
...Object.values(right.properties ?? {}),
|
|
171
|
+
].filter((shape): shape is OutputShape => shape !== undefined);
|
|
172
|
+
return { type: "object", additionalProperties: values.reduce(mergeShapes) };
|
|
173
|
+
}
|
|
174
|
+
const properties: Record<string, OutputShape> = {};
|
|
175
|
+
for (const key of keys) {
|
|
176
|
+
const leftProperty = left.properties?.[key];
|
|
177
|
+
const rightProperty = right.properties?.[key];
|
|
178
|
+
properties[key] = leftProperty && rightProperty ? mergeShapes(leftProperty, rightProperty) : (leftProperty ?? rightProperty)!;
|
|
179
|
+
}
|
|
180
|
+
const rightRequired = new Set(right.required);
|
|
181
|
+
return { type: "object", properties, required: (left.required ?? []).filter(key => rightRequired.has(key)) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Drops nesting from the deepest level up until the stored shape fits the size bound. */
|
|
185
|
+
function fitShape(shape: OutputShape): OutputShape {
|
|
186
|
+
for (let depth = MAX_DEPTH; depth > 0; depth--) {
|
|
187
|
+
const pruned = pruneShape(shape, depth);
|
|
188
|
+
if (JSON.stringify(pruned).length <= MAX_SHAPE_CHARS) return pruned;
|
|
189
|
+
}
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function pruneShape(shape: OutputShape, depth: number): OutputShape {
|
|
194
|
+
if (shape.anyOf) {
|
|
195
|
+
const variants = shape.anyOf.map(variant => pruneShape(variant, depth));
|
|
196
|
+
return variants.some(isUnknown) ? {} : { anyOf: variants };
|
|
197
|
+
}
|
|
198
|
+
if (shape.type !== "object" && shape.type !== "array") return shape;
|
|
199
|
+
if (depth === 0) return {};
|
|
200
|
+
const prune = (child: OutputShape) => pruneShape(child, depth - 1);
|
|
201
|
+
return {
|
|
202
|
+
...shape,
|
|
203
|
+
...(shape.properties
|
|
204
|
+
? { properties: Object.fromEntries(Object.entries(shape.properties).map(([key, child]) => [key, prune(child)])) }
|
|
205
|
+
: {}),
|
|
206
|
+
...(shape.items ? { items: prune(shape.items) } : {}),
|
|
207
|
+
...(shape.additionalProperties ? { additionalProperties: prune(shape.additionalProperties) } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.ts",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"import": "./oauth.ts",
|
|
21
21
|
"default": "./oauth.ts"
|
|
22
22
|
},
|
|
23
|
+
"./host-managed": {
|
|
24
|
+
"types": "./host-managed.ts",
|
|
25
|
+
"import": "./host-managed.ts",
|
|
26
|
+
"default": "./host-managed.ts"
|
|
27
|
+
},
|
|
23
28
|
"./config": {
|
|
24
29
|
"types": "./dist/config.d.ts",
|
|
25
30
|
"import": "./dist/config.js",
|
|
@@ -84,8 +89,11 @@
|
|
|
84
89
|
"runtime-owner.ts",
|
|
85
90
|
"tool-metadata.ts",
|
|
86
91
|
"ts-shape.ts",
|
|
92
|
+
"output-shape.ts",
|
|
87
93
|
"search-ranking.ts",
|
|
88
94
|
"tool-approval.ts",
|
|
95
|
+
"tool-parameters.ts",
|
|
96
|
+
"host-managed.ts",
|
|
89
97
|
"session-approvals.ts",
|
|
90
98
|
"project-server-trust.ts",
|
|
91
99
|
"init.ts",
|
|
@@ -173,7 +181,6 @@
|
|
|
173
181
|
"dependencies": {
|
|
174
182
|
"@modelcontextprotocol/client": "2.0.0",
|
|
175
183
|
"@modelcontextprotocol/core": "2.0.0",
|
|
176
|
-
"@modelcontextprotocol/ext-apps": "^1.2.2",
|
|
177
184
|
"@modelcontextprotocol/ext-tasks": "^0.1.0",
|
|
178
185
|
"@napi-rs/keyring": "^2.1.0",
|
|
179
186
|
"@typesafe-ai/sdk": "0.6.0",
|