@fitchmultz/pi-mcp-adapter 6.0.0 → 7.1.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 +33 -0
- package/OAUTH.md +1 -1
- package/README.md +27 -18
- package/dist/app-bridge.bundle.js +14 -14
- package/dist/config.js +41 -16
- package/dist/direct-tools.js +1 -1
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/index.js +38 -59
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +2 -2
- package/dist/mcp-auth-flow.js +2 -2
- package/dist/mcp-auth.d.ts +1 -1
- package/dist/mcp-auth.js +3 -3
- package/dist/mcp-code.d.ts +6 -6
- package/dist/mcp-code.js +11 -10
- package/dist/mcp-oauth-provider.d.ts +5 -5
- package/dist/mcp-oauth-provider.js +5 -5
- package/dist/mcp-output-guard.d.ts +1 -1
- package/dist/mcp-output-guard.js +1 -1
- package/dist/mcp-panel.d.ts +2 -2
- package/dist/mcp-panel.js +2 -2
- package/dist/mcp-setup-panel.d.ts +5 -4
- package/dist/mcp-setup-panel.js +15 -7
- package/dist/mcp-trace.d.ts +1 -1
- package/dist/mcp-trace.js +1 -1
- package/dist/npx-resolver.js +8 -4
- package/dist/proxy-modes.d.ts +2 -2
- package/dist/proxy-modes.js +20 -19
- package/dist/sampling-handler.js +1 -0
- package/dist/search-ranking.js +6 -2
- package/dist/server-manager.d.ts +2 -2
- package/dist/server-manager.js +44 -27
- package/dist/session-recovery.d.ts +7 -4
- package/dist/session-recovery.js +76 -10
- package/dist/tool-loader.d.ts +7 -2
- package/dist/tool-loader.js +56 -5
- package/dist/tool-metadata.d.ts +43 -43
- package/dist/tool-result-renderer.d.ts +3 -3
- package/dist/tool-result-renderer.js +3 -3
- package/dist/ui-resource-handler.d.ts +2 -2
- package/dist/ui-resource-handler.js +2 -2
- package/dist/ui-server.js +5 -4
- package/dist/ui-stream-types.d.ts +8 -8
- package/dist/unix-socket-transport.d.ts +1 -1
- package/dist/unix-socket-transport.js +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +14 -0
- package/package.json +34 -98
- package/skills/mcp-scripting/SKILL.md +2 -0
- package/abort.ts +0 -34
- package/agent-dir.ts +0 -24
- package/app-bridge.bundle.js +0 -109
- package/banner.png +0 -0
- package/checkpoint.ts +0 -55
- package/commands.ts +0 -622
- package/config.ts +0 -1204
- package/consent-manager.ts +0 -25
- package/direct-tools.ts +0 -75
- package/elicitation-handler.ts +0 -349
- package/error-signal.ts +0 -21
- package/gateway-arguments.ts +0 -73
- package/glimpse-ui.ts +0 -80
- package/host-html-template.ts +0 -456
- package/index.ts +0 -767
- package/init.ts +0 -613
- package/legacy-migration.ts +0 -47
- package/lifecycle.ts +0 -165
- package/logger.ts +0 -127
- package/mcp-auth-flow.ts +0 -975
- package/mcp-auth.ts +0 -793
- package/mcp-callback-server.ts +0 -450
- package/mcp-code.ts +0 -412
- package/mcp-keyring-helper.cjs +0 -89
- package/mcp-oauth-provider.ts +0 -774
- package/mcp-output-guard.ts +0 -530
- package/mcp-panel.ts +0 -907
- package/mcp-script-worker.mjs +0 -131
- package/mcp-setup-panel.ts +0 -666
- package/mcp-status.ts +0 -110
- package/mcp-trace.ts +0 -295
- package/metadata-cache.ts +0 -271
- package/npx-resolver.ts +0 -488
- package/onboarding-state.ts +0 -58
- package/panel-keys.ts +0 -37
- package/prompts.ts +0 -337
- package/proxy-modes.ts +0 -1109
- package/resource-tools.ts +0 -52
- package/runtime-owner.ts +0 -123
- package/sampling-handler.ts +0 -276
- package/scripts/build-app-bridge.mjs +0 -15
- package/scripts/build.mjs +0 -132
- package/scripts/prepare.mjs +0 -103
- package/search-ranking.ts +0 -132
- package/server-manager.ts +0 -1320
- package/session-recovery.ts +0 -316
- package/state.ts +0 -64
- package/tool-approval.ts +0 -78
- package/tool-loader.ts +0 -153
- package/tool-metadata.ts +0 -240
- package/tool-registrar.ts +0 -74
- package/tool-result-renderer.ts +0 -294
- package/tsconfig.build.json +0 -13
- package/tsconfig.json +0 -16
- package/types.ts +0 -785
- package/ui-resource-handler.ts +0 -249
- package/ui-server.ts +0 -808
- package/ui-session.ts +0 -568
- package/ui-stream-types.ts +0 -89
- package/ui-tool-visibility.ts +0 -25
- package/unix-socket-transport.ts +0 -85
- package/utils.ts +0 -307
package/resource-tools.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { McpExtensionState } from "./state.ts";
|
|
2
|
-
import { isServerDisabled, isToolAllowed, resolveToolPrefix, type McpOperationContext, type McpToolCallIdentity } from "./types.ts";
|
|
3
|
-
import { executeCall, prepareMcpConnection } from "./proxy-modes.ts";
|
|
4
|
-
import { paginate } from "./search-ranking.ts";
|
|
5
|
-
import { throwIfAborted } from "./abort.ts";
|
|
6
|
-
import { combineAbortSignals } from "./runtime-owner.ts";
|
|
7
|
-
|
|
8
|
-
export async function executeResourceList(state: McpExtensionState, server: string, limit = 12, offset = 0, signal?: AbortSignal) {
|
|
9
|
-
throwIfAborted(combineAbortSignals(state.owner?.signal, signal));
|
|
10
|
-
const definition = state.config.mcpServers[server];
|
|
11
|
-
if (!definition || isServerDisabled(definition)) {
|
|
12
|
-
return { content: [{ type: "text" as const, text: `Server "${server}" is ${definition ? "disabled" : "not configured"}.` }], details: { mode: "resources", server, error: definition ? "server_disabled" : "server_not_found" } };
|
|
13
|
-
}
|
|
14
|
-
if (!state.toolMetadata.has(server) || state.manager.getConnection(server)?.status === "connected") {
|
|
15
|
-
const prepared = await prepareMcpConnection(state, server, signal);
|
|
16
|
-
if (prepared.error) return prepared.error;
|
|
17
|
-
}
|
|
18
|
-
const prefix = resolveToolPrefix(definition, state.config.settings?.toolPrefix);
|
|
19
|
-
const page = paginate((state.toolMetadata.get(server) ?? [])
|
|
20
|
-
.filter(tool => tool.resourceUri !== undefined && definition.exposeResources !== false
|
|
21
|
-
&& isToolAllowed(tool.originalName, server, prefix, definition.includeTools, definition.excludeTools))
|
|
22
|
-
.map(tool => tool.resourceDescriptor ?? { uri: tool.resourceUri!, description: tool.description }), offset, limit);
|
|
23
|
-
return { content: [{ type: "text" as const, text: JSON.stringify({ server, ...page }, null, 2) }], details: { mode: "resources", server, ...page } };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function executeResourceRead(
|
|
27
|
-
state: McpExtensionState,
|
|
28
|
-
server: string,
|
|
29
|
-
uri: string,
|
|
30
|
-
signal?: AbortSignal,
|
|
31
|
-
identity: McpToolCallIdentity = {},
|
|
32
|
-
beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>,
|
|
33
|
-
raw = false,
|
|
34
|
-
) {
|
|
35
|
-
return executeCall(state, uri, {}, server, undefined, signal, identity, beforeDispatch, { resourceUri: uri, raw });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function resourceNameToToolName(name: string): string {
|
|
39
|
-
let result = name
|
|
40
|
-
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
41
|
-
.replace(/_+/g, "_")
|
|
42
|
-
.replace(/^_+/, "") // Remove leading underscores
|
|
43
|
-
.replace(/_+$/, "") // Remove trailing underscores
|
|
44
|
-
.toLowerCase();
|
|
45
|
-
|
|
46
|
-
// Ensure we have a valid name
|
|
47
|
-
if (!result || /^\d/.test(result)) {
|
|
48
|
-
result = "resource" + (result ? "_" + result : "");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return result;
|
|
52
|
-
}
|
package/runtime-owner.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import type { McpCheckpointEvent } from "./checkpoint.ts";
|
|
2
|
-
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { formatTerminalError } from "./utils.ts";
|
|
4
|
-
|
|
5
|
-
export interface McpRuntimeOwner {
|
|
6
|
-
readonly signal: AbortSignal;
|
|
7
|
-
isActive(): boolean;
|
|
8
|
-
addCleanup(cleanup: () => void | Promise<void>): void;
|
|
9
|
-
stop(reason?: string): Promise<void>;
|
|
10
|
-
throwIfInactive(): void;
|
|
11
|
-
beforeActivity(): void;
|
|
12
|
-
holdCheckpoint(event: McpCheckpointEvent): () => void;
|
|
13
|
-
runCallback<T>(kind: "beforeExecute" | "onToolCall", run: () => Promise<T>): Promise<T>;
|
|
14
|
-
getCheckpointBlocker(): string | undefined;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function createMcpRuntimeOwner(): McpRuntimeOwner {
|
|
18
|
-
const controller = new AbortController();
|
|
19
|
-
const cleanups: Array<() => void | Promise<void>> = [];
|
|
20
|
-
let stopPromise: Promise<void> | undefined;
|
|
21
|
-
let checkpoint: McpCheckpointEvent | undefined;
|
|
22
|
-
const beforeActivity = () => checkpoint?.invalidate();
|
|
23
|
-
// These two host hooks can outlive abortable() at their call sites. Count their underlying
|
|
24
|
-
// promises through settlement, without cancelling, serializing, or replaying callback work.
|
|
25
|
-
const callbacks = { beforeExecute: 0, onToolCall: 0 };
|
|
26
|
-
const getCheckpointBlocker = () => {
|
|
27
|
-
if (callbacks.beforeExecute) return "MCP beforeExecute callback is pending";
|
|
28
|
-
if (callbacks.onToolCall) return "MCP onToolCall callback is pending";
|
|
29
|
-
return undefined;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const reportCleanupFailure = (error: unknown, late: boolean) => {
|
|
33
|
-
console.error(`MCP: ${late ? "late " : ""}runtime cleanup failed: ${formatTerminalError(error)}`);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
signal: controller.signal,
|
|
38
|
-
beforeActivity,
|
|
39
|
-
getCheckpointBlocker,
|
|
40
|
-
runCallback: async (kind, run) => {
|
|
41
|
-
beforeActivity();
|
|
42
|
-
// Invocation/cancellation policy stays at the call site. In particular, an accepted
|
|
43
|
-
// tool's outcome callback may still need to persist its result during shutdown.
|
|
44
|
-
callbacks[kind]++;
|
|
45
|
-
try { return await run(); } finally { callbacks[kind]--; }
|
|
46
|
-
},
|
|
47
|
-
holdCheckpoint: event => {
|
|
48
|
-
if (checkpoint) throw new Error("MCP checkpoint is already held");
|
|
49
|
-
checkpoint = event;
|
|
50
|
-
return () => { if (checkpoint === event) checkpoint = undefined; };
|
|
51
|
-
},
|
|
52
|
-
isActive: () => !controller.signal.aborted,
|
|
53
|
-
addCleanup: cleanup => {
|
|
54
|
-
if (controller.signal.aborted) {
|
|
55
|
-
void Promise.resolve().then(cleanup).catch(error => reportCleanupFailure(error, true));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
cleanups.push(cleanup);
|
|
59
|
-
},
|
|
60
|
-
stop: (reason = "MCP extension runtime stopped") => {
|
|
61
|
-
if (stopPromise) return stopPromise;
|
|
62
|
-
beforeActivity();
|
|
63
|
-
controller.abort(new Error(reason));
|
|
64
|
-
const pendingCleanups = cleanups.splice(0).reverse().map(cleanup =>
|
|
65
|
-
Promise.resolve().then(cleanup),
|
|
66
|
-
);
|
|
67
|
-
stopPromise = Promise.allSettled(pendingCleanups).then(results => {
|
|
68
|
-
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
69
|
-
const pendingCallback = getCheckpointBlocker();
|
|
70
|
-
if (pendingCallback) failures.push(new Error(pendingCallback));
|
|
71
|
-
if (failures.length > 0) {
|
|
72
|
-
const aggregate = new AggregateError(failures, "MCP runtime cleanup failed");
|
|
73
|
-
console.error(`MCP: runtime cleanup failed: ${formatTerminalError(aggregate)}`);
|
|
74
|
-
throw aggregate;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
return stopPromise;
|
|
78
|
-
},
|
|
79
|
-
throwIfInactive: () => { beforeActivity(); controller.signal.throwIfAborted(); },
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
|
|
84
|
-
const active = signals.filter((signal): signal is AbortSignal => signal !== undefined);
|
|
85
|
-
if (active.length === 0) return undefined;
|
|
86
|
-
if (active.length === 1) return active[0];
|
|
87
|
-
return AbortSignal.any(active);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Fence session-bound UI calls after the owning extension runtime stops. */
|
|
91
|
-
export function createOwnedUi(ui: ExtensionUIContext, owner: McpRuntimeOwner): ExtensionUIContext {
|
|
92
|
-
const proxies = new WeakMap<object, object>();
|
|
93
|
-
const wrap = (value: unknown): unknown => {
|
|
94
|
-
if ((typeof value !== "object" || value === null) && typeof value !== "function") {
|
|
95
|
-
return value;
|
|
96
|
-
}
|
|
97
|
-
const object = value as object;
|
|
98
|
-
const existing = proxies.get(object);
|
|
99
|
-
if (existing) return existing;
|
|
100
|
-
|
|
101
|
-
const proxy = new Proxy(object, {
|
|
102
|
-
get(target, property, receiver) {
|
|
103
|
-
if (!owner.isActive()) return undefined;
|
|
104
|
-
const member = Reflect.get(target, property, receiver);
|
|
105
|
-
if (typeof member === "function") {
|
|
106
|
-
return (...args: unknown[]) => {
|
|
107
|
-
if (!owner.isActive()) return undefined;
|
|
108
|
-
return Reflect.apply(member, target, args);
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return owner.isActive() ? wrap(member) : undefined;
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
proxies.set(object, proxy);
|
|
115
|
-
return proxy;
|
|
116
|
-
};
|
|
117
|
-
return wrap(ui) as ExtensionUIContext;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function isAbortError(error: unknown, signal?: AbortSignal): boolean {
|
|
121
|
-
if (signal?.aborted) return true;
|
|
122
|
-
return error instanceof Error && (error.name === "AbortError" || error.message === "MCP extension runtime stopped");
|
|
123
|
-
}
|
package/sampling-handler.ts
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, Message, Model, TextContent } from "@earendil-works/pi-ai";
|
|
2
|
-
import { truncateAtWord } from "./utils.ts";
|
|
3
|
-
import { throwIfAborted } from "./abort.ts";
|
|
4
|
-
import type { ExtensionUIContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { Client } from "@modelcontextprotocol/client";
|
|
6
|
-
import {
|
|
7
|
-
type CreateMessageRequest,
|
|
8
|
-
type CreateMessageResult,
|
|
9
|
-
type ModelPreferences,
|
|
10
|
-
type SamplingMessage,
|
|
11
|
-
type SamplingMessageContentBlock,
|
|
12
|
-
} from "@modelcontextprotocol/client";
|
|
13
|
-
|
|
14
|
-
export interface SamplingHandlerOptions {
|
|
15
|
-
serverName: string;
|
|
16
|
-
autoApprove: boolean;
|
|
17
|
-
ui?: ExtensionUIContext;
|
|
18
|
-
modelRegistry: ModelRegistry;
|
|
19
|
-
getCurrentModel: () => Model<Api> | undefined;
|
|
20
|
-
getSignal: () => AbortSignal | undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type ServerSamplingConfig = Omit<SamplingHandlerOptions, "serverName">;
|
|
24
|
-
|
|
25
|
-
export function registerSamplingHandler(
|
|
26
|
-
client: Client,
|
|
27
|
-
options: SamplingHandlerOptions,
|
|
28
|
-
own: (run: () => Promise<CreateMessageResult>) => Promise<CreateMessageResult> = run => run(),
|
|
29
|
-
): void {
|
|
30
|
-
// Own the handler's real promise, not the SDK caller's cancellable response facade.
|
|
31
|
-
client.setRequestHandler("sampling/createMessage", request => own(() => handleSamplingRequest(options, request)));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function handleSamplingRequest(
|
|
35
|
-
options: SamplingHandlerOptions,
|
|
36
|
-
request: CreateMessageRequest,
|
|
37
|
-
): Promise<CreateMessageResult> {
|
|
38
|
-
const params = request.params;
|
|
39
|
-
const signal = options.getSignal();
|
|
40
|
-
throwIfAborted(signal);
|
|
41
|
-
|
|
42
|
-
if ("task" in params && params.task) {
|
|
43
|
-
throw new Error("MCP sampling tasks are not supported");
|
|
44
|
-
}
|
|
45
|
-
if (params.includeContext && params.includeContext !== "none") {
|
|
46
|
-
throw new Error("MCP sampling context inclusion is not supported");
|
|
47
|
-
}
|
|
48
|
-
if (params.tools?.length) {
|
|
49
|
-
throw new Error("MCP sampling tool use is not supported");
|
|
50
|
-
}
|
|
51
|
-
if (params.toolChoice) {
|
|
52
|
-
throw new Error("MCP sampling tool choice is not supported");
|
|
53
|
-
}
|
|
54
|
-
if (params.stopSequences?.length) {
|
|
55
|
-
throw new Error("MCP sampling stop sequences are not supported");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const messages = params.messages.map(convertSamplingMessage);
|
|
59
|
-
const model = await resolveSamplingModel(options, params.modelPreferences);
|
|
60
|
-
throwIfAborted(signal);
|
|
61
|
-
await confirmSampling(
|
|
62
|
-
options,
|
|
63
|
-
"Approve MCP sampling request",
|
|
64
|
-
formatRequestApproval(options.serverName, `${model.provider}/${model.id}`, params.systemPrompt, messages),
|
|
65
|
-
);
|
|
66
|
-
throwIfAborted(signal);
|
|
67
|
-
|
|
68
|
-
const supportsTemperature = model.compat && "supportsTemperature" in model.compat
|
|
69
|
-
? model.compat.supportsTemperature !== false
|
|
70
|
-
: !(model.id === "gpt-6-astra" && (model.api === "openai-responses" || model.api === "openai-codex-responses"));
|
|
71
|
-
const result = await options.modelRegistry.complete(
|
|
72
|
-
model,
|
|
73
|
-
{
|
|
74
|
-
...(params.systemPrompt !== undefined ? { systemPrompt: params.systemPrompt } : {}),
|
|
75
|
-
messages,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
maxTokens: params.maxTokens,
|
|
79
|
-
...(supportsTemperature && params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
80
|
-
...(params.metadata !== undefined ? { metadata: params.metadata as Record<string, unknown> } : {}),
|
|
81
|
-
...(signal ? { signal } : {}),
|
|
82
|
-
},
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const converted = convertAssistantResult(result);
|
|
86
|
-
throwIfAborted(signal);
|
|
87
|
-
await confirmSampling(
|
|
88
|
-
options,
|
|
89
|
-
"Return MCP sampling response",
|
|
90
|
-
formatResponseApproval(options.serverName, converted),
|
|
91
|
-
);
|
|
92
|
-
return converted;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function formatRequestApproval(
|
|
96
|
-
serverName: string,
|
|
97
|
-
modelName: string,
|
|
98
|
-
systemPrompt: string | undefined,
|
|
99
|
-
messages: Message[],
|
|
100
|
-
): string {
|
|
101
|
-
const lines = [`${serverName} wants to sample ${messages.length} message${messages.length === 1 ? "" : "s"} with ${modelName}.`];
|
|
102
|
-
if (systemPrompt) {
|
|
103
|
-
lines.push(`System: ${truncateAtWord(systemPrompt, 400)}`);
|
|
104
|
-
}
|
|
105
|
-
for (const [index, message] of messages.entries()) {
|
|
106
|
-
lines.push(`${index + 1}. ${message.role}: ${truncateAtWord(messageText(message), 400)}`);
|
|
107
|
-
}
|
|
108
|
-
return lines.join("\n\n");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatResponseApproval(serverName: string, response: CreateMessageResult): string {
|
|
112
|
-
const text = response.content.type === "text" ? response.content.text : `[${response.content.type} content]`;
|
|
113
|
-
return `${serverName} will receive this response from ${response.model}:\n\n${truncateAtWord(text, 1000)}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function messageText(message: Message): string {
|
|
117
|
-
if (typeof message.content === "string") return message.content;
|
|
118
|
-
return message.content.map((block) => {
|
|
119
|
-
if (block.type === "text") return block.text;
|
|
120
|
-
if (block.type === "image") return `[image: ${block.mimeType}]`;
|
|
121
|
-
if (block.type === "thinking") return "[thinking]";
|
|
122
|
-
if (block.type === "toolCall") return `[tool call: ${block.name}]`;
|
|
123
|
-
return "[content]";
|
|
124
|
-
}).join("\n");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function resolveSamplingModel(
|
|
128
|
-
options: SamplingHandlerOptions,
|
|
129
|
-
modelPreferences: ModelPreferences | undefined,
|
|
130
|
-
): Promise<Model<Api>> {
|
|
131
|
-
const candidates: Model<Api>[] = [];
|
|
132
|
-
const availableModels = options.modelRegistry.getAvailable();
|
|
133
|
-
|
|
134
|
-
for (const hint of modelPreferences?.hints ?? []) {
|
|
135
|
-
const normalizedHint = hint.name?.trim().toLowerCase();
|
|
136
|
-
if (!normalizedHint) continue;
|
|
137
|
-
for (const model of availableModels) {
|
|
138
|
-
const searchableNames = [`${model.provider}/${model.id}`, model.id, model.name];
|
|
139
|
-
if (searchableNames.some((name) => name.toLowerCase().includes(normalizedHint))) {
|
|
140
|
-
addSamplingCandidate(candidates, model);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const currentModel = options.getCurrentModel();
|
|
146
|
-
if (currentModel) addSamplingCandidate(candidates, currentModel);
|
|
147
|
-
|
|
148
|
-
for (const model of availableModels) {
|
|
149
|
-
addSamplingCandidate(candidates, model);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const errors: string[] = [];
|
|
153
|
-
const signal = options.getSignal();
|
|
154
|
-
for (const model of candidates) {
|
|
155
|
-
throwIfAborted(signal);
|
|
156
|
-
const auth = await options.modelRegistry.getApiKeyAndHeaders(model);
|
|
157
|
-
throwIfAborted(signal);
|
|
158
|
-
if (auth.ok === false) {
|
|
159
|
-
errors.push(`${model.provider}/${model.id}: ${auth.error}`);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
return model;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (errors.length > 0) {
|
|
166
|
-
throw new Error(`No configured auth for MCP sampling model. ${errors.join("; ")}`);
|
|
167
|
-
}
|
|
168
|
-
throw new Error("No Pi model is available for MCP sampling");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function addSamplingCandidate(candidates: Model<Api>[], model: Model<Api>): void {
|
|
172
|
-
if (!candidates.some((candidate) => candidate.provider === model.provider && candidate.id === model.id)) {
|
|
173
|
-
candidates.push(model);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function confirmSampling(options: SamplingHandlerOptions, title: string, message: string): Promise<void> {
|
|
178
|
-
if (options.autoApprove) return;
|
|
179
|
-
if (!options.ui) {
|
|
180
|
-
throw new Error("MCP sampling requires interactive approval. Set settings.samplingAutoApprove to true to allow it without UI.");
|
|
181
|
-
}
|
|
182
|
-
const approved = await options.ui.confirm(title, message);
|
|
183
|
-
if (!approved) {
|
|
184
|
-
throw new Error("MCP sampling request was declined");
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function convertSamplingMessage(message: SamplingMessage): Message {
|
|
189
|
-
const blocks = Array.isArray(message.content) ? message.content : [message.content];
|
|
190
|
-
if (message.role === "user") {
|
|
191
|
-
return {
|
|
192
|
-
role: "user",
|
|
193
|
-
content: blocks.map(convertUserContent),
|
|
194
|
-
timestamp: Date.now(),
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
role: "assistant",
|
|
200
|
-
content: blocks.map(convertAssistantContent),
|
|
201
|
-
api: "mcp-sampling",
|
|
202
|
-
provider: "mcp",
|
|
203
|
-
model: "sampling-request",
|
|
204
|
-
usage: zeroUsage(),
|
|
205
|
-
stopReason: "stop",
|
|
206
|
-
timestamp: Date.now(),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function convertUserContent(block: SamplingMessageContentBlock): TextContent {
|
|
211
|
-
if (block.type === "text") {
|
|
212
|
-
return { type: "text", text: block.text };
|
|
213
|
-
}
|
|
214
|
-
throw new Error(`MCP sampling ${block.type} content is not supported`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function convertAssistantContent(block: SamplingMessageContentBlock): TextContent {
|
|
218
|
-
if (block.type === "text") {
|
|
219
|
-
return { type: "text", text: block.text };
|
|
220
|
-
}
|
|
221
|
-
throw new Error(`MCP sampling assistant ${block.type} content is not supported`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function convertAssistantResult(message: AssistantMessage): CreateMessageResult {
|
|
225
|
-
if (message.stopReason === "error") {
|
|
226
|
-
throw new Error(message.errorMessage ?? "MCP sampling model call failed");
|
|
227
|
-
}
|
|
228
|
-
if (message.stopReason === "aborted") {
|
|
229
|
-
throw new Error(message.errorMessage ?? "MCP sampling model call was aborted");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const text = message.content
|
|
233
|
-
.map((block) => {
|
|
234
|
-
if (block.type === "text") return block.text;
|
|
235
|
-
if (block.type === "thinking") return undefined;
|
|
236
|
-
throw new Error(`MCP sampling result ${block.type} content is not supported`);
|
|
237
|
-
})
|
|
238
|
-
.filter((value): value is string => value !== undefined)
|
|
239
|
-
.join("\n\n")
|
|
240
|
-
.trim();
|
|
241
|
-
|
|
242
|
-
if (!text) {
|
|
243
|
-
throw new Error("MCP sampling result did not contain text content");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
role: "assistant",
|
|
248
|
-
content: { type: "text", text },
|
|
249
|
-
model: `${message.provider}/${message.model}`,
|
|
250
|
-
stopReason: mapStopReason(message.stopReason),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function mapStopReason(reason: AssistantMessage["stopReason"]): CreateMessageResult["stopReason"] {
|
|
255
|
-
if (reason === "stop") return "endTurn";
|
|
256
|
-
if (reason === "length") return "maxTokens";
|
|
257
|
-
if (reason === "toolUse") return "toolUse";
|
|
258
|
-
return reason;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function zeroUsage(): AssistantMessage["usage"] {
|
|
262
|
-
return {
|
|
263
|
-
input: 0,
|
|
264
|
-
output: 0,
|
|
265
|
-
cacheRead: 0,
|
|
266
|
-
cacheWrite: 0,
|
|
267
|
-
totalTokens: 0,
|
|
268
|
-
cost: {
|
|
269
|
-
input: 0,
|
|
270
|
-
output: 0,
|
|
271
|
-
cacheRead: 0,
|
|
272
|
-
cacheWrite: 0,
|
|
273
|
-
total: 0,
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { build } from "esbuild";
|
|
3
|
-
|
|
4
|
-
await build({
|
|
5
|
-
stdin: {
|
|
6
|
-
contents: 'export { AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge";',
|
|
7
|
-
resolveDir: process.cwd(),
|
|
8
|
-
},
|
|
9
|
-
bundle: true,
|
|
10
|
-
platform: "browser",
|
|
11
|
-
format: "esm",
|
|
12
|
-
target: "es2022",
|
|
13
|
-
minify: true,
|
|
14
|
-
outfile: process.argv[2] ?? "app-bridge.bundle.js",
|
|
15
|
-
});
|
package/scripts/build.mjs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Purpose: Produce the compiled runtime files that the Pi extension manifest loads.
|
|
4
|
-
* Responsibilities: Run TypeScript emit and bundle the browser bridge into staging, copy runtime assets
|
|
5
|
-
* that modules resolve as dist-relative siblings, then atomically swap it into dist/
|
|
6
|
-
* so a failed TypeScript emit never destroys a previously working dist.
|
|
7
|
-
* Usage: `npm run build`; also invoked by scripts/prepare.mjs during install lifecycles.
|
|
8
|
-
* Invariants/Assumptions: `node_modules` provides `typescript`; deleting `dist/` is safe generated output.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { execFile as execFileCallback } from "node:child_process";
|
|
12
|
-
import { existsSync } from "node:fs";
|
|
13
|
-
import { copyFile, readdir, rename, rm } from "node:fs/promises";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import process from "node:process";
|
|
16
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
17
|
-
import { promisify } from "node:util";
|
|
18
|
-
|
|
19
|
-
const execFile = promisify(execFileCallback);
|
|
20
|
-
const RM_OPTIONS = { force: true, maxRetries: 5, recursive: true, retryDelay: 100 };
|
|
21
|
-
const RENAME_RETRY_LIMIT = 50;
|
|
22
|
-
const RENAME_RETRY_MS = 50;
|
|
23
|
-
// Run tsc's JS entrypoint directly through the current node binary: no .cmd shim,
|
|
24
|
-
// no shell, safe for install paths containing spaces on every platform.
|
|
25
|
-
const tscPath = join(process.cwd(), "node_modules", "typescript", "bin", "tsc");
|
|
26
|
-
// Runtime siblings resolved relative to the compiled module directory
|
|
27
|
-
// (mcp-auth.ts, mcp-code.ts, ui-server.ts).
|
|
28
|
-
const RUNTIME_ASSETS = ["mcp-keyring-helper.cjs", "mcp-script-worker.mjs"];
|
|
29
|
-
|
|
30
|
-
async function discardStaging(path) {
|
|
31
|
-
try {
|
|
32
|
-
await rm(path, RM_OPTIONS);
|
|
33
|
-
} catch (error) {
|
|
34
|
-
// Cleanup is always best-effort: it must not turn a successful race loss
|
|
35
|
-
// into failure or replace the compiler/publish error that matters.
|
|
36
|
-
console.warn(`could not remove staging ${path}: ${error?.message ?? error}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isOwnerGone(pid) {
|
|
41
|
-
try {
|
|
42
|
-
process.kill(pid, 0);
|
|
43
|
-
return false;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
return error?.code === "ESRCH";
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function hasPublishedDist(path) {
|
|
50
|
-
try {
|
|
51
|
-
return (await readdir(path)).length > 0;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function reapStrandedStaging(cwd) {
|
|
58
|
-
// Signal 0 proves only that the owner was gone at probe time. Pid reuse
|
|
59
|
-
// between this probe and removal is inherent to pid-based reaping.
|
|
60
|
-
for (const entry of await readdir(cwd, { withFileTypes: true })) {
|
|
61
|
-
const match = /^dist\.staging\.(\d+)$/.exec(entry.name);
|
|
62
|
-
if (!match || !entry.isDirectory()) continue;
|
|
63
|
-
const ownerPid = Number(match[1]);
|
|
64
|
-
if (ownerPid === process.pid || !isOwnerGone(ownerPid)) continue;
|
|
65
|
-
await discardStaging(join(cwd, entry.name));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function compileToStaging(cwd, stagingDir) {
|
|
70
|
-
try {
|
|
71
|
-
const { stderr, stdout } = await execFile(
|
|
72
|
-
process.execPath,
|
|
73
|
-
[tscPath, "-p", "tsconfig.build.json", "--outDir", stagingDir],
|
|
74
|
-
{ cwd, maxBuffer: 10 * 1024 * 1024 },
|
|
75
|
-
);
|
|
76
|
-
if (stdout) process.stdout.write(stdout);
|
|
77
|
-
if (stderr) process.stderr.write(stderr);
|
|
78
|
-
await execFile(process.execPath, [join(cwd, "scripts", "build-app-bridge.mjs"), join(stagingDir, "app-bridge.bundle.js")], { cwd });
|
|
79
|
-
for (const asset of RUNTIME_ASSETS) await copyFile(join(cwd, asset), join(stagingDir, asset));
|
|
80
|
-
} catch (error) {
|
|
81
|
-
if (error?.stdout) process.stdout.write(error.stdout);
|
|
82
|
-
if (error?.stderr) process.stderr.write(error.stderr);
|
|
83
|
-
await discardStaging(stagingDir);
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function publishStaging(stagingDir, distDir) {
|
|
89
|
-
try {
|
|
90
|
-
// A failed dist removal must fail loudly; keep it outside the retry loop.
|
|
91
|
-
// Remove dist exactly once so retries never delete a concurrent winner.
|
|
92
|
-
await rm(distDir, RM_OPTIONS);
|
|
93
|
-
for (let retry = 0; ; retry++) {
|
|
94
|
-
try {
|
|
95
|
-
await rename(stagingDir, distDir);
|
|
96
|
-
return;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
// A completed staged build is non-empty; a bare directory is not proof
|
|
99
|
-
// that a concurrent publisher won the race.
|
|
100
|
-
if (await hasPublishedDist(distDir)) {
|
|
101
|
-
console.warn("dist/ was published by a concurrent build; discarding this build's staging tree.");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
// At most 50 retries (~2.5s) keep persistent filesystem errors bounded.
|
|
105
|
-
if (existsSync(stagingDir) && retry < RENAME_RETRY_LIMIT) {
|
|
106
|
-
await delay(RENAME_RETRY_MS);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} finally {
|
|
113
|
-
// Safe after success too: rename moved stagingDir, so force makes this a no-op.
|
|
114
|
-
await discardStaging(stagingDir);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function main() {
|
|
119
|
-
const cwd = process.cwd();
|
|
120
|
-
if (!existsSync(tscPath)) throw new Error(`typescript is not installed at ${tscPath}; run npm install first.`);
|
|
121
|
-
await reapStrandedStaging(cwd);
|
|
122
|
-
// Pid-scoped staging isolates concurrent emits; only a complete tree publishes.
|
|
123
|
-
const stagingDir = join(cwd, `dist.staging.${process.pid}`);
|
|
124
|
-
await rm(stagingDir, RM_OPTIONS);
|
|
125
|
-
await compileToStaging(cwd, stagingDir);
|
|
126
|
-
await publishStaging(stagingDir, join(cwd, "dist"));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
main().catch((error) => {
|
|
130
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
131
|
-
process.exitCode = 1;
|
|
132
|
-
});
|