@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/OAUTH.md +1 -1
  3. package/README.md +27 -18
  4. package/dist/app-bridge.bundle.js +14 -14
  5. package/dist/config.js +41 -16
  6. package/dist/direct-tools.js +1 -1
  7. package/dist/gateway-arguments.d.ts +1 -1
  8. package/dist/index.js +38 -59
  9. package/dist/lifecycle.d.ts +2 -2
  10. package/dist/lifecycle.js +2 -2
  11. package/dist/mcp-auth-flow.js +2 -2
  12. package/dist/mcp-auth.d.ts +1 -1
  13. package/dist/mcp-auth.js +3 -3
  14. package/dist/mcp-code.d.ts +6 -6
  15. package/dist/mcp-code.js +11 -10
  16. package/dist/mcp-oauth-provider.d.ts +5 -5
  17. package/dist/mcp-oauth-provider.js +5 -5
  18. package/dist/mcp-output-guard.d.ts +1 -1
  19. package/dist/mcp-output-guard.js +1 -1
  20. package/dist/mcp-panel.d.ts +2 -2
  21. package/dist/mcp-panel.js +2 -2
  22. package/dist/mcp-setup-panel.d.ts +5 -4
  23. package/dist/mcp-setup-panel.js +15 -7
  24. package/dist/mcp-trace.d.ts +1 -1
  25. package/dist/mcp-trace.js +1 -1
  26. package/dist/npx-resolver.js +8 -4
  27. package/dist/proxy-modes.d.ts +2 -2
  28. package/dist/proxy-modes.js +20 -19
  29. package/dist/sampling-handler.js +1 -0
  30. package/dist/search-ranking.js +6 -2
  31. package/dist/server-manager.d.ts +2 -2
  32. package/dist/server-manager.js +44 -27
  33. package/dist/session-recovery.d.ts +7 -4
  34. package/dist/session-recovery.js +76 -10
  35. package/dist/tool-loader.d.ts +7 -2
  36. package/dist/tool-loader.js +56 -5
  37. package/dist/tool-metadata.d.ts +43 -43
  38. package/dist/tool-result-renderer.d.ts +3 -3
  39. package/dist/tool-result-renderer.js +3 -3
  40. package/dist/ui-resource-handler.d.ts +2 -2
  41. package/dist/ui-resource-handler.js +2 -2
  42. package/dist/ui-server.js +5 -4
  43. package/dist/ui-stream-types.d.ts +8 -8
  44. package/dist/unix-socket-transport.d.ts +1 -1
  45. package/dist/unix-socket-transport.js +1 -1
  46. package/dist/utils.d.ts +1 -0
  47. package/dist/utils.js +14 -0
  48. package/package.json +34 -98
  49. package/skills/mcp-scripting/SKILL.md +2 -0
  50. package/abort.ts +0 -34
  51. package/agent-dir.ts +0 -24
  52. package/app-bridge.bundle.js +0 -109
  53. package/banner.png +0 -0
  54. package/checkpoint.ts +0 -55
  55. package/commands.ts +0 -622
  56. package/config.ts +0 -1204
  57. package/consent-manager.ts +0 -25
  58. package/direct-tools.ts +0 -75
  59. package/elicitation-handler.ts +0 -349
  60. package/error-signal.ts +0 -21
  61. package/gateway-arguments.ts +0 -73
  62. package/glimpse-ui.ts +0 -80
  63. package/host-html-template.ts +0 -456
  64. package/index.ts +0 -767
  65. package/init.ts +0 -613
  66. package/legacy-migration.ts +0 -47
  67. package/lifecycle.ts +0 -165
  68. package/logger.ts +0 -127
  69. package/mcp-auth-flow.ts +0 -975
  70. package/mcp-auth.ts +0 -793
  71. package/mcp-callback-server.ts +0 -450
  72. package/mcp-code.ts +0 -412
  73. package/mcp-keyring-helper.cjs +0 -89
  74. package/mcp-oauth-provider.ts +0 -774
  75. package/mcp-output-guard.ts +0 -530
  76. package/mcp-panel.ts +0 -907
  77. package/mcp-script-worker.mjs +0 -131
  78. package/mcp-setup-panel.ts +0 -666
  79. package/mcp-status.ts +0 -110
  80. package/mcp-trace.ts +0 -295
  81. package/metadata-cache.ts +0 -271
  82. package/npx-resolver.ts +0 -488
  83. package/onboarding-state.ts +0 -58
  84. package/panel-keys.ts +0 -37
  85. package/prompts.ts +0 -337
  86. package/proxy-modes.ts +0 -1109
  87. package/resource-tools.ts +0 -52
  88. package/runtime-owner.ts +0 -123
  89. package/sampling-handler.ts +0 -276
  90. package/scripts/build-app-bridge.mjs +0 -15
  91. package/scripts/build.mjs +0 -132
  92. package/scripts/prepare.mjs +0 -103
  93. package/search-ranking.ts +0 -132
  94. package/server-manager.ts +0 -1320
  95. package/session-recovery.ts +0 -316
  96. package/state.ts +0 -64
  97. package/tool-approval.ts +0 -78
  98. package/tool-loader.ts +0 -153
  99. package/tool-metadata.ts +0 -240
  100. package/tool-registrar.ts +0 -74
  101. package/tool-result-renderer.ts +0 -294
  102. package/tsconfig.build.json +0 -13
  103. package/tsconfig.json +0 -16
  104. package/types.ts +0 -785
  105. package/ui-resource-handler.ts +0 -249
  106. package/ui-server.ts +0 -808
  107. package/ui-session.ts +0 -568
  108. package/ui-stream-types.ts +0 -89
  109. package/ui-tool-visibility.ts +0 -25
  110. package/unix-socket-transport.ts +0 -85
  111. 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
- }
@@ -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
- });