@agimon-ai/po-mcp 0.0.0-alpha.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/apiContracts.cjs +269 -0
  4. package/dist/apiContracts.cjs.map +1 -0
  5. package/dist/apiContracts.d.mts +137 -0
  6. package/dist/apiContracts.d.mts.map +1 -0
  7. package/dist/apiContracts.mjs +258 -0
  8. package/dist/apiContracts.mjs.map +1 -0
  9. package/dist/extensions/mcp-ui.mjs +31 -0
  10. package/dist/extensions/mcp-ui.mjs.map +1 -0
  11. package/dist/extensions/mcp.cjs +104 -0
  12. package/dist/extensions/mcp.cjs.map +1 -0
  13. package/dist/extensions/mcp.d.mts +5 -0
  14. package/dist/extensions/mcp.d.mts.map +1 -0
  15. package/dist/extensions/mcp.mjs +99 -0
  16. package/dist/extensions/mcp.mjs.map +1 -0
  17. package/dist/extensions/server.cjs +2852 -0
  18. package/dist/extensions/server.cjs.map +1 -0
  19. package/dist/extensions/server.d.mts +5 -0
  20. package/dist/extensions/server.d.mts.map +1 -0
  21. package/dist/extensions/server.mjs +2839 -0
  22. package/dist/extensions/server.mjs.map +1 -0
  23. package/dist/extensions/web.mjs +2200 -0
  24. package/dist/extensions/web.mjs.map +1 -0
  25. package/dist/index.cjs +18 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.d.mts +89 -0
  28. package/dist/index.d.mts.map +1 -0
  29. package/dist/index.mjs +9 -0
  30. package/dist/index.mjs.map +1 -0
  31. package/dist/mcpCatalog-6Tpt6Gqn.cjs +900 -0
  32. package/dist/mcpCatalog-6Tpt6Gqn.cjs.map +1 -0
  33. package/dist/mcpCatalog-SlqWS-sb.mjs +806 -0
  34. package/dist/mcpCatalog-SlqWS-sb.mjs.map +1 -0
  35. package/dist/mcpConfig-BYESy-zP.d.mts +57 -0
  36. package/dist/mcpConfig-BYESy-zP.d.mts.map +1 -0
  37. package/dist/projection-gyAA2jRB.mjs +43 -0
  38. package/dist/projection-gyAA2jRB.mjs.map +1 -0
  39. package/dist/projection-w3XxXmVi.cjs +51 -0
  40. package/dist/projection-w3XxXmVi.cjs.map +1 -0
  41. package/dist/projection.cjs +17 -0
  42. package/dist/projection.d.mts +12 -0
  43. package/dist/projection.d.mts.map +1 -0
  44. package/dist/projection.mjs +4 -0
  45. package/dist/rolldown-runtime-C0BPl7ul.cjs +43 -0
  46. package/llms.txt +9 -0
  47. package/package.json +157 -0
  48. package/src/prompts/po-use-mcp/SKILL.md +79 -0
@@ -0,0 +1,2200 @@
1
+ import { apiResponse, createApiClient, defineApiRoutes, defineFill, defineRepositorySettingsPanel, defineSessionStore, defineToolRenderer, defineWebLifecycle, defineWebPlugin, sessionApiAddress } from "@agimon-ai/po-core/web";
2
+ import { Button, ChevronRightIcon, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, ExternalLinkIcon, Input, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, SettingsActions, SettingsCard, SettingsNote, SettingsRow, SettingsRowDescription, SettingsRowText, SettingsRowTitle, SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SyntaxText, toolTone } from "@agimon-ai/po-web-components";
3
+ import { useStore } from "@tanstack/react-store";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { Store } from "@tanstack/store";
6
+ import { sealedTransport } from "@agimon-ai/po-web-security/browser";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge";
9
+ const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "gu");
10
+ const MAX_SESSION_AUTH_SERVERS = 128;
11
+ const MAX_AUTHORIZATION_URL_LENGTH = 8192;
12
+ const MAX_SERVER_TOOLS = 512;
13
+ const MAX_TOOL_NAME_LENGTH = 256;
14
+ const SESSION_AUTH_STATES = [
15
+ "not-connected",
16
+ "connecting",
17
+ "connected",
18
+ "degraded",
19
+ "needs-auth",
20
+ "failed",
21
+ "closed",
22
+ "disabled"
23
+ ];
24
+ function isSessionAuthState(value) {
25
+ return SESSION_AUTH_STATES.some((state) => state === value);
26
+ }
27
+ /** Session route for one reachable tool's description and schema. */
28
+ const MCP_SESSION_TOOL_API_PATH = "/tool";
29
+ const MCP_SESSION_TOOL_SERVER_QUERY = "server";
30
+ const MCP_SESSION_TOOL_NAME_QUERY = "tool";
31
+ /** Reads the live-session status defensively. Empty is the browser's active-session clear value. */
32
+ function parseMcpSessionAuthStatus(raw) {
33
+ if (raw === void 0 || raw.trim() === "") return void 0;
34
+ try {
35
+ const value = JSON.parse(raw.replace(ANSI_ESCAPE_PATTERN, ""));
36
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_SESSION_AUTH_SERVERS) return void 0;
37
+ const names = /* @__PURE__ */ new Set();
38
+ for (const item of value) {
39
+ if (!isSessionAuthStatusItem(item) || names.has(item.name)) return void 0;
40
+ names.add(item.name);
41
+ }
42
+ return value;
43
+ } catch {
44
+ return;
45
+ }
46
+ }
47
+ function hasControlCharacter(value) {
48
+ for (const character of value) {
49
+ const codePoint = character.codePointAt(0);
50
+ if (codePoint !== void 0 && (codePoint < 32 || codePoint >= 127 && codePoint <= 159)) return true;
51
+ }
52
+ return false;
53
+ }
54
+ function isAuthorizationUrl(value) {
55
+ if (typeof value !== "string" || value.length > MAX_AUTHORIZATION_URL_LENGTH || hasControlCharacter(value)) return false;
56
+ try {
57
+ const url = new URL(value);
58
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ function isToolName(value) {
64
+ return typeof value === "string" && value.trim() !== "" && value.length <= MAX_TOOL_NAME_LENGTH && !hasControlCharacter(value);
65
+ }
66
+ function isTokenCount(value) {
67
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
68
+ }
69
+ function isReachableTool(value) {
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
71
+ const record = value;
72
+ return Object.keys(record).every((key) => key === "name" || key === "piName" || key === "tokens") && isToolName(record.name) && (!("piName" in record) || isToolName(record.piName)) && (!("tokens" in record) || isTokenCount(record.tokens));
73
+ }
74
+ function isSessionAuthStatusItem(value) {
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
76
+ const record = value;
77
+ return Object.keys(record).every((key) => key === "name" || key === "state" || key === "authorizationUrl" || key === "tools") && (!("authorizationUrl" in record) || isAuthorizationUrl(record.authorizationUrl)) && (!("tools" in record) || Array.isArray(record.tools) && record.tools.length <= MAX_SERVER_TOOLS && record.tools.every(isReachableTool)) && typeof record.name === "string" && record.name.trim() !== "" && !hasControlCharacter(record.name) && isSessionAuthState(record.state);
78
+ }
79
+ /** Hub API paths for repository-scoped MCP management. */
80
+ const MCP_REPOSITORY_API_PATH = "/repository";
81
+ const MCP_DISCOVERY_API_PATH = "/repository/discover";
82
+ const MCP_AUTHORIZATION_API_PATH = "/repository/authorize";
83
+ /** The repository every route is asked about, and the flow segment the two flow routes carry. */
84
+ const MCP_REPOSITORY_ID_QUERY = "repositoryId";
85
+ const MCP_FLOW_ID_PARAM = "flowId";
86
+ //#endregion
87
+ //#region src/types/apiRoutes.ts
88
+ /** One authorization flow, addressed by id below the flow collection. */
89
+ const AUTHORIZATION_FLOW_PATH = `${MCP_AUTHORIZATION_API_PATH}/:${MCP_FLOW_ID_PARAM}`;
90
+ /**
91
+ * This package's routes, as data.
92
+ *
93
+ * No scope and no base path. The build reads both off
94
+ * `src/extensions/(backend)/api/mcp/`, so the mount is stated once, by the
95
+ * folder that creates it. The sub-paths come from `webMcp`, which already owned
96
+ * them, so this table names where a route sits and nothing else.
97
+ *
98
+ * This is the one file the hub handler and the settings panel both read, so a
99
+ * path cannot move on one side alone. The handler matches on these strings
100
+ * rather than parsing its own copies of them.
101
+ *
102
+ * `:flowId` is the client's parameter syntax; the handler reads the segment off
103
+ * the same path, so the two cannot disagree about where the id sits.
104
+ */
105
+ var apiRoutes_default = defineApiRoutes({
106
+ catalog: {
107
+ method: "GET",
108
+ path: MCP_REPOSITORY_API_PATH,
109
+ query: [MCP_REPOSITORY_ID_QUERY],
110
+ response: apiResponse()
111
+ },
112
+ discover: {
113
+ method: "POST",
114
+ path: MCP_DISCOVERY_API_PATH,
115
+ response: apiResponse()
116
+ },
117
+ authorize: {
118
+ method: "POST",
119
+ path: MCP_AUTHORIZATION_API_PATH,
120
+ response: apiResponse()
121
+ },
122
+ readAuthorization: {
123
+ method: "GET",
124
+ path: AUTHORIZATION_FLOW_PATH,
125
+ query: [MCP_REPOSITORY_ID_QUERY],
126
+ response: apiResponse()
127
+ },
128
+ /** The same path as `readAuthorization`; the method is the whole of the difference. */
129
+ cancelAuthorization: {
130
+ method: "DELETE",
131
+ path: AUTHORIZATION_FLOW_PATH,
132
+ query: [MCP_REPOSITORY_ID_QUERY],
133
+ response: apiResponse()
134
+ },
135
+ /** Session scope: one available tool's registered name, description and schema. */
136
+ sessionTool: {
137
+ method: "GET",
138
+ path: MCP_SESSION_TOOL_API_PATH,
139
+ query: [MCP_SESSION_TOOL_SERVER_QUERY, MCP_SESSION_TOOL_NAME_QUERY],
140
+ response: apiResponse()
141
+ }
142
+ });
143
+ const mcp = createApiClient(apiRoutes_default, {
144
+ scopes: [
145
+ "global",
146
+ "workspace",
147
+ "session"
148
+ ],
149
+ basePath: "mcp",
150
+ transport: (input, init) => sealedTransport.fetch(input, init),
151
+ sessionAddress: sessionApiAddress
152
+ });
153
+ const mcpSession = createApiClient(apiRoutes_default, {
154
+ scopes: ["session"],
155
+ basePath: "mcp-session",
156
+ transport: (input, init) => sealedTransport.fetch(input, init),
157
+ sessionAddress: sessionApiAddress
158
+ });
159
+ //#endregion
160
+ //#region src/extensions/workspaces/(frontend)/repository-settings-panel/_lib/mcpSettingsApi.ts
161
+ /**
162
+ * The panel's half of this package's repository API.
163
+ *
164
+ * The generated client owns every URL, addressed at the workspace scope the
165
+ * repository names, so nothing here spells a mount. The workspace prefix, the
166
+ * plugins segment and the base path used to be concatenated at the top of this
167
+ * file, the only copy of that shape outside the host, and it agreed with the
168
+ * route by inspection alone.
169
+ *
170
+ * The transport stays injected, which is the one way this package differs from
171
+ * its siblings. Every write here is step-up gated: the panel passes the
172
+ * elevated request for discovery, authorization and cancellation and the plain
173
+ * one for reads, and that choice belongs to the caller. So the client is asked
174
+ * for the URL and the method, and the caller's own request issues them.
175
+ */
176
+ const JSON_HEADERS = { "content-type": "application/json" };
177
+ function isRecord$1(value) {
178
+ return typeof value === "object" && value !== null && !Array.isArray(value);
179
+ }
180
+ async function resultOf(response, fallback) {
181
+ let body;
182
+ try {
183
+ body = await response.json();
184
+ } catch {
185
+ body = void 0;
186
+ }
187
+ if (!response.ok) return { error: isRecord$1(body) && typeof body.error === "string" ? body.error : fallback };
188
+ return { value: body };
189
+ }
190
+ async function readMcpCatalog(request, repositoryId) {
191
+ const route = mcp.workspace(repositoryId).catalog;
192
+ return await resultOf(await request(route.url({ query: { [MCP_REPOSITORY_ID_QUERY]: repositoryId } })), "The synced MCP catalog could not be read.");
193
+ }
194
+ async function discoverMcpCatalog(requestWithStepUp, repositoryId) {
195
+ const route = mcp.workspace(repositoryId).discover;
196
+ return await resultOf(await requestWithStepUp(route.url(), {
197
+ method: route.spec.method,
198
+ headers: JSON_HEADERS,
199
+ body: JSON.stringify({ repositoryId })
200
+ }), "MCP discovery could not be completed.");
201
+ }
202
+ async function startMcpAuthorization(requestWithStepUp, repositoryId, serverName) {
203
+ const route = mcp.workspace(repositoryId).authorize;
204
+ return await resultOf(await requestWithStepUp(route.url(), {
205
+ method: route.spec.method,
206
+ headers: JSON_HEADERS,
207
+ body: JSON.stringify({
208
+ repositoryId,
209
+ serverName
210
+ })
211
+ }), "MCP authorization could not be started.");
212
+ }
213
+ async function readMcpAuthorization(request, repositoryId, flowId) {
214
+ const route = mcp.workspace(repositoryId).readAuthorization;
215
+ return await resultOf(await request(route.url({
216
+ params: { [MCP_FLOW_ID_PARAM]: flowId },
217
+ query: { [MCP_REPOSITORY_ID_QUERY]: repositoryId }
218
+ })), "The authorization state could not be read.");
219
+ }
220
+ async function cancelMcpAuthorization(requestWithStepUp, repositoryId, flowId) {
221
+ const route = mcp.workspace(repositoryId).cancelAuthorization;
222
+ return await resultOf(await requestWithStepUp(route.url({
223
+ params: { [MCP_FLOW_ID_PARAM]: flowId },
224
+ query: { [MCP_REPOSITORY_ID_QUERY]: repositoryId }
225
+ }), { method: route.spec.method }), "The authorization flow could not be cancelled.");
226
+ }
227
+ //#endregion
228
+ //#region src/extensions/workspaces/(frontend)/repository-settings-panel/_lib/mcpSettingsStore.ts
229
+ const mcpSettings = new Store({
230
+ repositoryId: void 0,
231
+ catalog: void 0,
232
+ authorization: void 0,
233
+ busy: void 0,
234
+ error: void 0
235
+ });
236
+ function selectRepository(repositoryId) {
237
+ if (mcpSettings.state.repositoryId === repositoryId) return;
238
+ mcpSettings.setState(() => ({
239
+ repositoryId,
240
+ catalog: void 0,
241
+ authorization: void 0,
242
+ busy: void 0,
243
+ error: void 0
244
+ }));
245
+ }
246
+ function applyFor(repositoryId, update) {
247
+ if (mcpSettings.state.repositoryId !== repositoryId) return;
248
+ mcpSettings.setState(update);
249
+ }
250
+ async function loadMcpSettings(request, repositoryId) {
251
+ selectRepository(repositoryId);
252
+ mcpSettings.setState((current) => ({
253
+ ...current,
254
+ busy: "loading",
255
+ error: void 0
256
+ }));
257
+ try {
258
+ const result = await readMcpCatalog(request, repositoryId);
259
+ applyFor(repositoryId, (current) => "error" in result ? {
260
+ ...current,
261
+ busy: void 0,
262
+ error: result.error
263
+ } : {
264
+ ...current,
265
+ busy: void 0,
266
+ catalog: result.value,
267
+ error: void 0
268
+ });
269
+ } catch {
270
+ applyFor(repositoryId, (current) => ({
271
+ ...current,
272
+ busy: void 0,
273
+ error: "The cockpit hub is unreachable."
274
+ }));
275
+ }
276
+ }
277
+ async function discoverMcpSettings(requestWithStepUp, repositoryId) {
278
+ selectRepository(repositoryId);
279
+ mcpSettings.setState((current) => ({
280
+ ...current,
281
+ busy: "discovering",
282
+ error: void 0
283
+ }));
284
+ try {
285
+ const result = await discoverMcpCatalog(requestWithStepUp, repositoryId);
286
+ applyFor(repositoryId, (current) => "error" in result ? {
287
+ ...current,
288
+ busy: void 0,
289
+ error: result.error
290
+ } : {
291
+ ...current,
292
+ busy: void 0,
293
+ catalog: result.value,
294
+ error: void 0
295
+ });
296
+ } catch {
297
+ applyFor(repositoryId, (current) => ({
298
+ ...current,
299
+ busy: void 0,
300
+ error: "The cockpit hub is unreachable."
301
+ }));
302
+ }
303
+ }
304
+ async function authorizeMcpServer(requestWithStepUp, repositoryId, serverName) {
305
+ selectRepository(repositoryId);
306
+ mcpSettings.setState((current) => ({
307
+ ...current,
308
+ busy: "authorizing",
309
+ error: void 0
310
+ }));
311
+ try {
312
+ const result = await startMcpAuthorization(requestWithStepUp, repositoryId, serverName);
313
+ applyFor(repositoryId, (current) => "error" in result ? {
314
+ ...current,
315
+ busy: void 0,
316
+ error: result.error
317
+ } : {
318
+ ...current,
319
+ busy: void 0,
320
+ authorization: result.value,
321
+ error: void 0
322
+ });
323
+ } catch {
324
+ applyFor(repositoryId, (current) => ({
325
+ ...current,
326
+ busy: void 0,
327
+ error: "The cockpit hub is unreachable."
328
+ }));
329
+ }
330
+ }
331
+ function followMcpAuthorization(request, repositoryId, flowId, onCompleted) {
332
+ let stopped = false;
333
+ let timer;
334
+ const poll = async () => {
335
+ if (stopped) return;
336
+ try {
337
+ const result = await readMcpAuthorization(request, repositoryId, flowId);
338
+ if (stopped) return;
339
+ if ("error" in result) {
340
+ applyFor(repositoryId, (current) => ({
341
+ ...current,
342
+ error: result.error
343
+ }));
344
+ return;
345
+ }
346
+ applyFor(repositoryId, (current) => ({
347
+ ...current,
348
+ authorization: result.value,
349
+ error: void 0
350
+ }));
351
+ if (result.value.status === "completed") {
352
+ onCompleted();
353
+ return;
354
+ }
355
+ if ([
356
+ "failed",
357
+ "cancelled",
358
+ "expired"
359
+ ].includes(result.value.status)) return;
360
+ timer = setTimeout(() => void poll(), 1e3);
361
+ } catch {
362
+ applyFor(repositoryId, (current) => ({
363
+ ...current,
364
+ error: "The authorization status could not be refreshed."
365
+ }));
366
+ }
367
+ };
368
+ poll();
369
+ return () => {
370
+ stopped = true;
371
+ if (timer) clearTimeout(timer);
372
+ };
373
+ }
374
+ async function cancelMcpFlow(requestWithStepUp, repositoryId, flowId) {
375
+ mcpSettings.setState((current) => ({
376
+ ...current,
377
+ busy: "cancelling",
378
+ error: void 0
379
+ }));
380
+ try {
381
+ const result = await cancelMcpAuthorization(requestWithStepUp, repositoryId, flowId);
382
+ applyFor(repositoryId, (current) => "error" in result ? {
383
+ ...current,
384
+ busy: void 0,
385
+ error: result.error
386
+ } : {
387
+ ...current,
388
+ busy: void 0,
389
+ authorization: result.value,
390
+ error: void 0
391
+ });
392
+ } catch {
393
+ applyFor(repositoryId, (current) => ({
394
+ ...current,
395
+ busy: void 0,
396
+ error: "The cockpit hub is unreachable."
397
+ }));
398
+ }
399
+ }
400
+ //#endregion
401
+ //#region src/extensions/workspaces/(frontend)/repository-settings-panel/_components/McpRepositorySettingsPanel.tsx
402
+ const TERMINAL_AUTHORIZATION = /* @__PURE__ */ new Set([
403
+ "completed",
404
+ "failed",
405
+ "cancelled",
406
+ "expired"
407
+ ]);
408
+ const SYNC_REASON = {
409
+ "never-synced": "this repository has never been synced",
410
+ "configuration-changed": "the configuration changed since the last sync",
411
+ "cockpit-bundle-missing": "the cockpit bundle is missing",
412
+ "package-apis-missing": "package APIs are missing"
413
+ };
414
+ const SERVER_STATE = {
415
+ "not-connected": "Not connected",
416
+ connecting: "Connecting",
417
+ connected: "Connected",
418
+ degraded: "Degraded",
419
+ "needs-auth": "Needs sign-in",
420
+ failed: "Failed",
421
+ closed: "Closed"
422
+ };
423
+ const AUTHORIZATION_STATUS = {
424
+ starting: "Starting sign-in.",
425
+ waiting: "Waiting for you to finish signing in.",
426
+ completed: "Signed in.",
427
+ failed: "Sign-in didn't finish.",
428
+ cancelled: "Sign-in was cancelled.",
429
+ expired: "Sign-in expired before it finished."
430
+ };
431
+ function syncReasons(reasons) {
432
+ return `${reasons.map((reason) => SYNC_REASON[reason] ?? reason).join(", ") || "the last sync is out of date"}. Run po sync, then refresh.`;
433
+ }
434
+ /** One server: its state and credential on the left, Authorize on the right, cached tools folded underneath. */
435
+ function ServerRow({ server, busy, canAct, confirming, onAuthorize, onConfirm, onCancel }) {
436
+ const toolCount = server.tools.length;
437
+ return /* @__PURE__ */ jsx(SettingsRow, {
438
+ asChild: true,
439
+ className: "items-start",
440
+ children: /* @__PURE__ */ jsxs("li", {
441
+ "data-testid": `mcp-server-${server.name}`,
442
+ children: [
443
+ /* @__PURE__ */ jsxs(SettingsRowText, { children: [
444
+ /* @__PURE__ */ jsx(SettingsRowTitle, {
445
+ className: "font-mono first-letter:normal-case",
446
+ children: server.name
447
+ }),
448
+ /* @__PURE__ */ jsxs(SettingsRowDescription, { children: [
449
+ SERVER_STATE[server.state],
450
+ ". ",
451
+ server.credentialPresent ? "Credential saved." : "No credential saved."
452
+ ] }),
453
+ server.error === void 0 ? null : /* @__PURE__ */ jsx(SettingsRowDescription, {
454
+ className: "text-doom-red",
455
+ children: server.error
456
+ })
457
+ ] }),
458
+ confirming ? null : /* @__PURE__ */ jsx(Button, {
459
+ variant: "outline",
460
+ size: "sm",
461
+ disabled: busy || !canAct,
462
+ onClick: onAuthorize,
463
+ children: "Authorize"
464
+ }),
465
+ confirming ? /* @__PURE__ */ jsxs("div", {
466
+ className: "flex basis-full flex-wrap items-center gap-x-4 gap-y-2 rounded-md bg-doom-bg px-4 py-3",
467
+ children: [/* @__PURE__ */ jsxs("span", {
468
+ className: "min-w-0 flex-1 text-base leading-relaxed text-doom-text",
469
+ children: [
470
+ "Start a short OAuth sign-in for ",
471
+ server.name,
472
+ "? A passkey may be required."
473
+ ]
474
+ }), /* @__PURE__ */ jsxs(SettingsActions, { children: [/* @__PURE__ */ jsx(Button, {
475
+ variant: "primary",
476
+ size: "sm",
477
+ disabled: busy,
478
+ onClick: onConfirm,
479
+ children: "Continue"
480
+ }), /* @__PURE__ */ jsx(Button, {
481
+ variant: "ghost",
482
+ size: "sm",
483
+ onClick: onCancel,
484
+ children: "Cancel"
485
+ })] })]
486
+ }) : null,
487
+ /* @__PURE__ */ jsxs("details", {
488
+ className: "group basis-full",
489
+ children: [/* @__PURE__ */ jsxs("summary", {
490
+ className: "flex w-fit cursor-pointer list-none items-center gap-1 text-base font-medium text-doom-text hover:text-doom-hi [&::-webkit-details-marker]:hidden",
491
+ children: [
492
+ /* @__PURE__ */ jsx(ChevronRightIcon, {
493
+ "aria-hidden": true,
494
+ className: "size-3.5 transition-transform group-open:rotate-90"
495
+ }),
496
+ toolCount === 1 ? "1 tool" : `${String(toolCount)} tools`,
497
+ ", ",
498
+ server.source
499
+ ]
500
+ }), /* @__PURE__ */ jsx("ul", {
501
+ className: "mt-3 flex flex-col gap-1.5 border-l border-doom-border-soft pl-4",
502
+ children: toolCount === 0 ? /* @__PURE__ */ jsx("li", {
503
+ className: "text-sm text-doom-dim",
504
+ children: "Run discovery to see what this server offers."
505
+ }) : server.tools.map((tool) => /* @__PURE__ */ jsxs("li", {
506
+ className: "flex min-w-0 items-baseline gap-3 text-base",
507
+ children: [/* @__PURE__ */ jsx("code", {
508
+ title: tool.piName,
509
+ className: "min-w-0 shrink-0 truncate font-mono text-doom-hi",
510
+ children: tool.piName
511
+ }), /* @__PURE__ */ jsx("span", {
512
+ className: "min-w-0 truncate text-doom-dim",
513
+ children: tool.description ?? ""
514
+ })]
515
+ }, tool.piName))
516
+ })]
517
+ })
518
+ ]
519
+ })
520
+ });
521
+ }
522
+ function McpRepositorySettingsPanel({ repository, request, requestWithStepUp }) {
523
+ const state = useStore(mcpSettings);
524
+ const [confirmDiscovery, setConfirmDiscovery] = useState(false);
525
+ const [confirmAuthorization, setConfirmAuthorization] = useState();
526
+ const repositoryId = repository?.id;
527
+ useEffect(() => {
528
+ setConfirmDiscovery(false);
529
+ setConfirmAuthorization(void 0);
530
+ if (repositoryId) loadMcpSettings(request, repositoryId);
531
+ }, [repositoryId, request]);
532
+ const authorization = state.repositoryId === repositoryId ? state.authorization : void 0;
533
+ const authorizationId = authorization?.id;
534
+ const authorizationStatus = authorization?.status;
535
+ useEffect(() => {
536
+ if (!repositoryId || authorizationId === void 0) return;
537
+ if (authorizationStatus !== void 0 && TERMINAL_AUTHORIZATION.has(authorizationStatus)) return;
538
+ return followMcpAuthorization(request, repositoryId, authorizationId, () => {
539
+ loadMcpSettings(request, repositoryId);
540
+ });
541
+ }, [
542
+ authorizationId,
543
+ authorizationStatus,
544
+ repositoryId,
545
+ request
546
+ ]);
547
+ if (!repository) return /* @__PURE__ */ jsx(SettingsCard, {
548
+ "data-testid": "mcp-repository-settings",
549
+ children: /* @__PURE__ */ jsx(SettingsRow, { children: /* @__PURE__ */ jsxs(SettingsRowText, { children: [/* @__PURE__ */ jsx(SettingsRowTitle, { children: "No repository picked" }), /* @__PURE__ */ jsx(SettingsRowDescription, { children: "Pick a repository above to see its MCP servers." })] }) })
550
+ });
551
+ const catalog = state.repositoryId === repository.id ? state.catalog : void 0;
552
+ const busy = state.repositoryId === repository.id ? state.busy : void 0;
553
+ const error = state.repositoryId === repository.id ? state.error : void 0;
554
+ const fresh = catalog?.sync.fresh === true;
555
+ const servers = catalog?.servers ?? [];
556
+ const dropped = catalog?.droppedServers ?? [];
557
+ return /* @__PURE__ */ jsxs("div", {
558
+ className: "flex min-w-0 flex-col gap-10",
559
+ "data-testid": "mcp-repository-settings",
560
+ children: [
561
+ /* @__PURE__ */ jsxs(SettingsSection, { children: [/* @__PURE__ */ jsx(SettingsSectionHeader, { children: /* @__PURE__ */ jsx(SettingsSectionTitle, { children: "Catalog" }) }), /* @__PURE__ */ jsxs(SettingsCard, { children: [
562
+ /* @__PURE__ */ jsxs(SettingsRow, { children: [/* @__PURE__ */ jsxs(SettingsRowText, { children: [/* @__PURE__ */ jsx(SettingsRowTitle, { children: fresh ? "Up to date" : catalog ? "Needs a sync" : "Reading the catalog" }), /* @__PURE__ */ jsx(SettingsRowDescription, { children: fresh ? "Po reads the cached catalog on this machine. Discovery contacts each configured server." : catalog ? syncReasons(catalog.sync.reasons) : "Reading what the last sync recorded." })] }), /* @__PURE__ */ jsxs(SettingsActions, { children: [/* @__PURE__ */ jsx(Button, {
563
+ variant: "outline",
564
+ size: "sm",
565
+ loading: busy === "loading",
566
+ disabled: busy !== void 0,
567
+ onClick: () => void loadMcpSettings(request, repository.id),
568
+ children: "Refresh"
569
+ }), /* @__PURE__ */ jsx(Button, {
570
+ variant: "outline",
571
+ size: "sm",
572
+ loading: busy === "discovering",
573
+ disabled: busy !== void 0 || !fresh || confirmDiscovery,
574
+ onClick: () => setConfirmDiscovery(true),
575
+ children: "Discover servers"
576
+ })] })] }),
577
+ confirmDiscovery ? /* @__PURE__ */ jsxs(SettingsRow, { children: [/* @__PURE__ */ jsxs(SettingsRowText, { children: [/* @__PURE__ */ jsx(SettingsRowTitle, { children: "Discover servers now?" }), /* @__PURE__ */ jsx(SettingsRowDescription, { children: "Discovery starts the configured processes and contacts remote MCP servers." })] }), /* @__PURE__ */ jsxs(SettingsActions, { children: [/* @__PURE__ */ jsx(Button, {
578
+ variant: "primary",
579
+ size: "sm",
580
+ disabled: busy !== void 0,
581
+ onClick: () => {
582
+ setConfirmDiscovery(false);
583
+ discoverMcpSettings(requestWithStepUp, repository.id);
584
+ },
585
+ children: "Discover"
586
+ }), /* @__PURE__ */ jsx(Button, {
587
+ variant: "ghost",
588
+ size: "sm",
589
+ onClick: () => setConfirmDiscovery(false),
590
+ children: "Cancel"
591
+ })] })] }) : null,
592
+ error === void 0 ? null : /* @__PURE__ */ jsxs(SettingsRow, {
593
+ role: "alert",
594
+ children: [/* @__PURE__ */ jsxs(SettingsRowText, { children: [/* @__PURE__ */ jsx(SettingsRowTitle, { children: "Po couldn't finish that just now" }), /* @__PURE__ */ jsx(SettingsRowDescription, { children: error })] }), /* @__PURE__ */ jsx(Button, {
595
+ variant: "outline",
596
+ size: "sm",
597
+ disabled: busy !== void 0,
598
+ onClick: () => void loadMcpSettings(request, repository.id),
599
+ children: "Try again"
600
+ })]
601
+ })
602
+ ] })] }),
603
+ authorization ? /* @__PURE__ */ jsxs(SettingsSection, { children: [/* @__PURE__ */ jsx(SettingsSectionHeader, { children: /* @__PURE__ */ jsx(SettingsSectionTitle, { children: "Sign-in" }) }), /* @__PURE__ */ jsx(SettingsCard, { children: /* @__PURE__ */ jsxs(SettingsRow, { children: [/* @__PURE__ */ jsxs(SettingsRowText, { children: [
604
+ /* @__PURE__ */ jsx(SettingsRowTitle, {
605
+ className: "font-mono first-letter:normal-case",
606
+ children: authorization.serverName
607
+ }),
608
+ /* @__PURE__ */ jsx(SettingsRowDescription, { children: AUTHORIZATION_STATUS[authorization.status] }),
609
+ authorization.error ? /* @__PURE__ */ jsx(SettingsRowDescription, {
610
+ className: "text-doom-red",
611
+ children: authorization.error
612
+ }) : null
613
+ ] }), /* @__PURE__ */ jsxs(SettingsActions, { children: [authorization.authorizationUrl ? /* @__PURE__ */ jsx(Button, {
614
+ variant: "primary",
615
+ size: "sm",
616
+ asChild: true,
617
+ children: /* @__PURE__ */ jsxs("a", {
618
+ href: authorization.authorizationUrl,
619
+ target: "_blank",
620
+ rel: "noreferrer noopener",
621
+ children: ["Open sign-in page", /* @__PURE__ */ jsx(ExternalLinkIcon, {
622
+ "aria-hidden": true,
623
+ className: "size-3.5"
624
+ })]
625
+ })
626
+ }) : null, TERMINAL_AUTHORIZATION.has(authorization.status) ? null : /* @__PURE__ */ jsx(Button, {
627
+ variant: "ghost",
628
+ size: "sm",
629
+ loading: busy === "cancelling",
630
+ disabled: busy !== void 0,
631
+ onClick: () => void cancelMcpFlow(requestWithStepUp, repository.id, authorization.id),
632
+ children: "Cancel"
633
+ })] })] }) })] }) : null,
634
+ /* @__PURE__ */ jsxs(SettingsSection, { children: [
635
+ /* @__PURE__ */ jsxs(SettingsSectionHeader, { children: [/* @__PURE__ */ jsx(SettingsSectionTitle, { children: "Servers" }), servers.length === 0 ? null : /* @__PURE__ */ jsx("span", {
636
+ className: "text-sm text-doom-dim",
637
+ children: servers.length === 1 ? "1 server" : `${String(servers.length)} servers`
638
+ })] }),
639
+ /* @__PURE__ */ jsx(SettingsCard, {
640
+ asChild: true,
641
+ children: /* @__PURE__ */ jsx("ul", { children: servers.length === 0 ? /* @__PURE__ */ jsx(SettingsRow, {
642
+ asChild: true,
643
+ children: /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(SettingsRowText, { children: [/* @__PURE__ */ jsx(SettingsRowTitle, { children: busy === "loading" ? "Reading the catalog" : "No servers yet" }), /* @__PURE__ */ jsx(SettingsRowDescription, { children: busy === "loading" ? "This takes a moment." : "None are enabled in this repository's last sync." })] }) })
644
+ }) : servers.map((server) => /* @__PURE__ */ jsx(ServerRow, {
645
+ server,
646
+ busy: busy !== void 0,
647
+ canAct: fresh,
648
+ confirming: confirmAuthorization === server.name,
649
+ onAuthorize: () => setConfirmAuthorization(server.name),
650
+ onConfirm: () => {
651
+ setConfirmAuthorization(void 0);
652
+ authorizeMcpServer(requestWithStepUp, repository.id, server.name);
653
+ },
654
+ onCancel: () => setConfirmAuthorization(void 0)
655
+ }, server.name)) })
656
+ }),
657
+ dropped.length === 0 ? null : /* @__PURE__ */ jsx(SettingsNote, { children: `Left out by policy: ${dropped.join(", ")}.` }),
658
+ catalog?.diagnostics.map((diagnostic) => /* @__PURE__ */ jsx(SettingsNote, {
659
+ className: "text-doom-red",
660
+ children: diagnostic
661
+ }, diagnostic))
662
+ ] })
663
+ ]
664
+ });
665
+ }
666
+ //#endregion
667
+ //#region src/extensions/workspaces/(frontend)/repository-settings-panel/mcp.web.tsx
668
+ var mcp_web_default$1 = defineRepositorySettingsPanel({
669
+ label: "MCP servers",
670
+ detail: "The MCP servers this repository's sessions can use, what each one offers, and where to sign in.",
671
+ order: 100,
672
+ component: McpRepositorySettingsPanel
673
+ });
674
+ //#endregion
675
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_lib/mcpSessionToolApi.ts
676
+ /** One available session tool: its registered name, description, schema and token estimate. */
677
+ async function fetchSessionTool(sessionId, server, tool) {
678
+ const result = await mcpSession.session(sessionId).sessionTool({ query: {
679
+ [MCP_SESSION_TOOL_SERVER_QUERY]: server,
680
+ [MCP_SESSION_TOOL_NAME_QUERY]: tool
681
+ } });
682
+ if (!result.ok) {
683
+ if (result.status === 0) return {
684
+ ok: false,
685
+ error: "The session is unreachable."
686
+ };
687
+ return {
688
+ ok: false,
689
+ error: result.error === "" ? `The session answered ${result.status}.` : result.error
690
+ };
691
+ }
692
+ return {
693
+ ok: true,
694
+ detail: result.data
695
+ };
696
+ }
697
+ //#endregion
698
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/McpToolDetailDialog.tsx
699
+ function keyOf(target) {
700
+ return `${target.sessionId}\u0000${target.server}\u0000${target.tool}`;
701
+ }
702
+ /** One MCP declaration's description, complete input schema and estimated context cost. */
703
+ function McpToolDetailDialog({ target, onClose }) {
704
+ const [loaded, setLoaded] = useState(null);
705
+ useEffect(() => {
706
+ if (target === null) return;
707
+ let live = true;
708
+ const key = keyOf(target);
709
+ fetchSessionTool(target.sessionId, target.server, target.tool).then((result) => {
710
+ if (!live) return;
711
+ setLoaded(result.ok ? {
712
+ key,
713
+ detail: result.detail
714
+ } : {
715
+ key,
716
+ error: result.error
717
+ });
718
+ });
719
+ return () => {
720
+ live = false;
721
+ };
722
+ }, [target]);
723
+ const current = target !== null && loaded?.key === keyOf(target) ? loaded : null;
724
+ const detail = current?.detail;
725
+ return /* @__PURE__ */ jsx(Dialog, {
726
+ open: target !== null,
727
+ onOpenChange: (open) => {
728
+ if (!open) onClose();
729
+ },
730
+ children: /* @__PURE__ */ jsxs(DialogContent, {
731
+ "data-testid": "mcp-tool-detail-dialog",
732
+ children: [/* @__PURE__ */ jsx(DialogHeader, {
733
+ className: "items-start px-4 py-4 sm:px-5",
734
+ children: /* @__PURE__ */ jsxs("div", {
735
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
736
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
737
+ className: "break-words leading-snug",
738
+ children: detail?.piName ?? target?.piName ?? (target === null ? "" : `${target.server} · ${target.tool}`)
739
+ }), /* @__PURE__ */ jsxs(DialogDescription, { children: [
740
+ target === null ? "" : `${target.server} / ${target.tool}. `,
741
+ target?.inContext === false ? "Not in the model context. " : "",
742
+ detail === void 0 ? "Description and complete input schema." : `Estimated schema cost: ~${detail.tokens.toLocaleString()} tokens.`
743
+ ] })]
744
+ })
745
+ }), /* @__PURE__ */ jsxs(DialogBody, {
746
+ className: "flex flex-col gap-3 px-4 py-4 sm:px-5 sm:py-5",
747
+ children: [
748
+ current === null ? /* @__PURE__ */ jsx("output", {
749
+ className: "text-sm text-doom-dim",
750
+ children: "Loading the tool..."
751
+ }) : null,
752
+ current?.error === void 0 ? null : /* @__PURE__ */ jsx("output", {
753
+ className: "text-sm text-doom-dim",
754
+ children: current.error
755
+ }),
756
+ detail === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
757
+ /* @__PURE__ */ jsxs("section", {
758
+ className: "flex flex-col gap-1",
759
+ children: [/* @__PURE__ */ jsx("p", {
760
+ className: "text-sm font-semibold text-doom-dim",
761
+ children: "description"
762
+ }), /* @__PURE__ */ jsx("p", {
763
+ className: "whitespace-pre-wrap text-base text-doom-text",
764
+ children: detail.description ?? "No description."
765
+ })]
766
+ }),
767
+ detail.warnings?.length ? /* @__PURE__ */ jsx("section", {
768
+ "aria-label": "Tool warnings",
769
+ className: "flex flex-col gap-1 text-base text-doom-yellow",
770
+ children: detail.warnings.map((warning) => /* @__PURE__ */ jsxs("p", { children: [
771
+ warning.source,
772
+ " ",
773
+ warning.path,
774
+ ": ",
775
+ warning.message
776
+ ] }, `${warning.source}:${warning.path}:${warning.message}`))
777
+ }) : null,
778
+ /* @__PURE__ */ jsxs("section", {
779
+ className: "flex min-h-0 flex-col gap-1",
780
+ children: [/* @__PURE__ */ jsx("p", {
781
+ className: "text-sm font-semibold text-doom-dim",
782
+ children: "input schema"
783
+ }), /* @__PURE__ */ jsx("pre", {
784
+ "data-testid": "mcp-tool-detail-schema",
785
+ className: "max-h-96 overflow-auto rounded border border-doom-border-soft p-2 font-mono text-sm text-doom-text",
786
+ children: JSON.stringify(detail.inputSchema, null, 2)
787
+ })]
788
+ })
789
+ ] })
790
+ ]
791
+ })]
792
+ })
793
+ });
794
+ }
795
+ //#endregion
796
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/McpSessionAuthSection.tsx
797
+ /** Requests authorization through Pi's command frame, never through a shell. */
798
+ function requestMcpSessionAuthorization(sendSessionFrame, sessionId, serverName) {
799
+ sendSessionFrame(sessionId, {
800
+ type: "prompt",
801
+ message: `/mcp auth ${serverName}`
802
+ });
803
+ }
804
+ function tokenEstimate(value) {
805
+ return value === null ? "-" : `~${value.toLocaleString()}`;
806
+ }
807
+ /** Keep configured servers actionable even before they have discovered any tools. */
808
+ function McpSessionAuthSection({ sessionId, statuses, sendSessionFrame, contextInventory = [] }) {
809
+ const servers = parseMcpSessionAuthStatus(statuses["doom-mcp-session-auth"]) ?? [];
810
+ const [target, setTarget] = useState(null);
811
+ const [copyFeedback, setCopyFeedback] = useState("");
812
+ const [popupBlocked, setPopupBlocked] = useState(false);
813
+ const [toolTarget, setToolTarget] = useState(null);
814
+ const pendingTab = useRef(null);
815
+ const selected = target?.sessionId === sessionId ? target : null;
816
+ const server = servers.find((item) => item.name === selected?.name);
817
+ const authorizationUrl = server?.authorizationUrl;
818
+ const closePendingTab = () => {
819
+ pendingTab.current?.close();
820
+ pendingTab.current = null;
821
+ };
822
+ useEffect(() => {
823
+ const tab = pendingTab.current;
824
+ if (!selected || !authorizationUrl || !tab || tab.closed) return;
825
+ tab.location.replace(authorizationUrl);
826
+ pendingTab.current = null;
827
+ }, [selected, authorizationUrl]);
828
+ useEffect(() => () => {
829
+ pendingTab.current?.close();
830
+ pendingTab.current = null;
831
+ }, [sessionId]);
832
+ const authorize = (name) => {
833
+ if (sessionId === null) return;
834
+ const current = servers.find((item) => item.name === name);
835
+ closePendingTab();
836
+ setTarget({
837
+ sessionId,
838
+ name
839
+ });
840
+ setCopyFeedback("");
841
+ const tab = window.open("about:blank", "_blank");
842
+ setPopupBlocked(tab === null);
843
+ if (tab) {
844
+ tab.opener = null;
845
+ if (current?.authorizationUrl) tab.location.replace(current.authorizationUrl);
846
+ else {
847
+ tab.document.title = "MCP authorization";
848
+ tab.document.body.textContent = "Preparing authorization. Return to Po for progress or manual sign-in.";
849
+ pendingTab.current = tab;
850
+ }
851
+ }
852
+ if (!current?.authorizationUrl && current?.state !== "connecting") requestMcpSessionAuthorization(sendSessionFrame, sessionId, name);
853
+ };
854
+ const copyLink = async () => {
855
+ if (!authorizationUrl) return;
856
+ try {
857
+ await navigator.clipboard.writeText(authorizationUrl);
858
+ setCopyFeedback("Link copied.");
859
+ } catch {
860
+ setCopyFeedback("Clipboard unavailable. Select the link above and copy it manually.");
861
+ }
862
+ };
863
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
864
+ servers.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
865
+ "data-testid": "context-mcp-auth",
866
+ className: "flex flex-col gap-2 border-b border-doom-border px-3 py-3",
867
+ children: [/* @__PURE__ */ jsxs("div", {
868
+ className: "flex flex-col gap-0.5",
869
+ children: [/* @__PURE__ */ jsx("p", {
870
+ className: "text-sm font-semibold text-doom-dim",
871
+ children: "MCP servers"
872
+ }), /* @__PURE__ */ jsx("p", {
873
+ className: "text-sm leading-relaxed text-doom-dim",
874
+ children: "Open sign-in in a new tab, or copy the link from the authorization dialog."
875
+ })]
876
+ }), /* @__PURE__ */ jsx("ul", {
877
+ "aria-label": "MCP servers",
878
+ className: "flex flex-col gap-2",
879
+ children: servers.map((item) => {
880
+ const tools = contextInventory.filter((inventoryItem) => inventoryItem.source === "mcp" && inventoryItem.owner === item.name);
881
+ const reachable = item.tools ?? [];
882
+ return /* @__PURE__ */ jsxs("li", {
883
+ className: "flex min-w-0 flex-col gap-1 px-1 py-0.5",
884
+ children: [/* @__PURE__ */ jsxs("div", {
885
+ className: "flex min-w-0 items-center gap-2",
886
+ children: [
887
+ /* @__PURE__ */ jsx("span", {
888
+ className: "min-w-0 flex-1 truncate text-base font-semibold text-doom-hi",
889
+ children: item.name
890
+ }),
891
+ /* @__PURE__ */ jsx("span", {
892
+ className: "text-sm font-semibold text-doom-dim",
893
+ children: item.state.replace(/-/g, " ")
894
+ }),
895
+ /* @__PURE__ */ jsx(Button, {
896
+ variant: "subtle",
897
+ size: "xs",
898
+ "data-testid": `context-mcp-auth-${item.name}`,
899
+ "aria-label": `${item.state === "connected" ? "Manage" : "Authorize"} ${item.name}`,
900
+ disabled: sessionId === null || item.state === "disabled",
901
+ onClick: () => {
902
+ if (item.state !== "connected") authorize(item.name);
903
+ else if (sessionId !== null) {
904
+ closePendingTab();
905
+ setCopyFeedback("");
906
+ setPopupBlocked(false);
907
+ setTarget({
908
+ sessionId,
909
+ name: item.name
910
+ });
911
+ }
912
+ },
913
+ className: "shrink-0 ",
914
+ children: item.state === "connected" ? "manage" : "authorize"
915
+ })
916
+ ]
917
+ }), tools.length > 0 ? /* @__PURE__ */ jsx("ul", {
918
+ "aria-label": `${item.name} tools and skills`,
919
+ className: "flex flex-col",
920
+ children: tools.map((tool) => {
921
+ const identity = reachable.find((candidate) => candidate.piName === tool.name);
922
+ return /* @__PURE__ */ jsxs("li", {
923
+ "data-active": tool.active,
924
+ className: "flex min-w-0 items-center gap-2 py-px pl-3",
925
+ children: [
926
+ /* @__PURE__ */ jsx("abbr", {
927
+ title: tool.itemKind,
928
+ className: `w-3 shrink-0 text-2xs font-semibold no-underline ${tool.active ? "text-doom-violet" : "text-doom-dim"}`,
929
+ children: tool.itemKind === "skill" ? "S" : "T"
930
+ }),
931
+ tool.itemKind === "tool" ? /* @__PURE__ */ jsx(Button, {
932
+ variant: "ghost",
933
+ size: "card",
934
+ "aria-label": `Open schema for ${tool.name}`,
935
+ "data-testid": `context-mcp-tool-${tool.name}`,
936
+ disabled: sessionId === null || identity === void 0,
937
+ onClick: () => {
938
+ if (sessionId !== null && identity !== void 0) setToolTarget({
939
+ sessionId,
940
+ server: item.name,
941
+ tool: identity.name,
942
+ piName: identity.piName,
943
+ inContext: tool.active
944
+ });
945
+ },
946
+ className: `min-w-0 flex-1 truncate rounded-none py-0 text-left text-base ${tool.active ? "text-doom-hi" : "text-doom-dim"}`,
947
+ children: tool.name
948
+ }) : /* @__PURE__ */ jsx("span", {
949
+ className: "min-w-0 flex-1 truncate text-base text-doom-dim",
950
+ children: tool.name
951
+ }),
952
+ /* @__PURE__ */ jsx("span", {
953
+ className: `w-14 shrink-0 text-right text-sm ${tool.active ? "text-doom-text" : "text-doom-dim"}`,
954
+ children: tool.active ? tokenEstimate(tool.tokens) : `(${tokenEstimate(tool.tokens)})`
955
+ })
956
+ ]
957
+ }, `${tool.itemKind}:${tool.name}`);
958
+ })
959
+ }) : reachable.length > 0 ? /* @__PURE__ */ jsxs("div", {
960
+ className: "flex flex-col",
961
+ children: [/* @__PURE__ */ jsx("p", {
962
+ className: "pl-3 text-sm text-doom-dim",
963
+ children: `${reachable.length} available ${reachable.length === 1 ? "tool" : "tools"}, not in context`
964
+ }), /* @__PURE__ */ jsx("ul", {
965
+ "aria-label": `${item.name} reachable tools`,
966
+ className: "flex flex-col",
967
+ children: reachable.map((tool) => /* @__PURE__ */ jsx("li", {
968
+ className: "flex min-w-0",
969
+ children: /* @__PURE__ */ jsxs(Button, {
970
+ variant: "ghost",
971
+ size: "card",
972
+ "data-testid": `context-mcp-reachable-${item.name}-${tool.name}`,
973
+ title: "open the tool's description and schema",
974
+ disabled: sessionId === null,
975
+ onClick: () => {
976
+ if (sessionId !== null) setToolTarget({
977
+ sessionId,
978
+ server: item.name,
979
+ tool: tool.name,
980
+ piName: tool.piName,
981
+ inContext: false
982
+ });
983
+ },
984
+ className: "min-w-0 flex-1 flex-row items-center gap-2 rounded-none py-px pr-0 pl-3",
985
+ children: [
986
+ /* @__PURE__ */ jsx("abbr", {
987
+ title: "tool",
988
+ className: "w-3 shrink-0 text-2xs font-semibold text-doom-violet no-underline",
989
+ children: "T"
990
+ }),
991
+ /* @__PURE__ */ jsx("span", {
992
+ className: "min-w-0 flex-1 truncate text-left text-base text-doom-text",
993
+ children: tool.piName ?? tool.name
994
+ }),
995
+ /* @__PURE__ */ jsx("span", {
996
+ title: "what the schema would cost if added to context directly",
997
+ className: "w-14 shrink-0 text-right text-sm text-doom-dim",
998
+ children: tool.tokens === void 0 ? "(-)" : `(~${tool.tokens.toLocaleString()})`
999
+ })
1000
+ ]
1001
+ })
1002
+ }, tool.name))
1003
+ })]
1004
+ }) : /* @__PURE__ */ jsx("p", {
1005
+ className: "pl-3 text-sm text-doom-dim",
1006
+ children: "no tools or skills reported"
1007
+ })]
1008
+ }, item.name);
1009
+ })
1010
+ })]
1011
+ }),
1012
+ /* @__PURE__ */ jsx(McpToolDetailDialog, {
1013
+ target: toolTarget,
1014
+ onClose: () => setToolTarget(null)
1015
+ }),
1016
+ /* @__PURE__ */ jsx(Dialog, {
1017
+ open: selected !== null,
1018
+ onOpenChange: (open) => {
1019
+ if (!open) {
1020
+ closePendingTab();
1021
+ setTarget(null);
1022
+ }
1023
+ },
1024
+ children: /* @__PURE__ */ jsxs(DialogContent, {
1025
+ "data-testid": "mcp-authorization-dialog",
1026
+ onInteractOutside: (event) => event.preventDefault(),
1027
+ children: [
1028
+ /* @__PURE__ */ jsx(DialogHeader, {
1029
+ className: "items-start px-4 py-4 sm:px-5",
1030
+ children: /* @__PURE__ */ jsxs("div", {
1031
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
1032
+ children: [/* @__PURE__ */ jsxs(DialogTitle, {
1033
+ className: "break-words leading-snug",
1034
+ children: [
1035
+ server?.state === "connected" ? "Manage" : "Authorize",
1036
+ " ",
1037
+ selected?.name
1038
+ ]
1039
+ }), /* @__PURE__ */ jsx(DialogDescription, {
1040
+ className: "max-w-2xl",
1041
+ children: server?.state === "connected" ? "Disconnect this session without deleting saved credentials, or reauthorize to sign in again." : "Complete sign-in in the provider's tab. You can also open or copy the link below."
1042
+ })]
1043
+ })
1044
+ }),
1045
+ /* @__PURE__ */ jsxs(DialogBody, {
1046
+ className: "px-4 py-4 sm:px-5 sm:py-5",
1047
+ children: [
1048
+ /* @__PURE__ */ jsx("output", {
1049
+ className: "text-sm text-doom-dim",
1050
+ children: server?.state === "connected" ? "Authorization complete. This server is connected." : server?.state === "closed" ? "Disconnected from this session. Saved credentials were kept." : authorizationUrl ? "Waiting for you to complete authorization." : server?.state === "failed" ? "Connection failed. Retry authorization; details are in the session transcript." : server ? "Preparing the authorization link..." : "This server is no longer available in the session."
1051
+ }),
1052
+ popupBlocked ? /* @__PURE__ */ jsx("p", {
1053
+ className: "text-sm text-doom-dim",
1054
+ children: "The browser blocked the new tab. Open the link below when it is ready."
1055
+ }) : null,
1056
+ authorizationUrl ? /* @__PURE__ */ jsxs("div", {
1057
+ className: "flex flex-col gap-2",
1058
+ children: [
1059
+ /* @__PURE__ */ jsx(Input, {
1060
+ "aria-label": "OAuth authorization URL",
1061
+ readOnly: true,
1062
+ value: authorizationUrl,
1063
+ onFocus: (event) => event.currentTarget.select()
1064
+ }),
1065
+ /* @__PURE__ */ jsx("a", {
1066
+ href: authorizationUrl,
1067
+ target: "_blank",
1068
+ rel: "noopener noreferrer",
1069
+ className: "text-base font-medium text-doom-blue underline underline-offset-2",
1070
+ children: "open authorization page"
1071
+ }),
1072
+ /* @__PURE__ */ jsx(Button, {
1073
+ variant: "outline",
1074
+ size: "xs",
1075
+ onClick: () => void copyLink(),
1076
+ children: "copy link"
1077
+ })
1078
+ ]
1079
+ }) : null,
1080
+ copyFeedback ? /* @__PURE__ */ jsx("output", {
1081
+ className: "text-sm text-doom-dim",
1082
+ children: copyFeedback
1083
+ }) : null
1084
+ ]
1085
+ }),
1086
+ /* @__PURE__ */ jsxs(DialogFooter, {
1087
+ className: "flex-wrap justify-end border-t border-doom-border-soft px-4 py-3 sm:px-5",
1088
+ children: [
1089
+ server?.state === "connected" && selected ? /* @__PURE__ */ jsx(Button, {
1090
+ variant: "outline",
1091
+ size: "md",
1092
+ onClick: () => {
1093
+ closePendingTab();
1094
+ setCopyFeedback("");
1095
+ setPopupBlocked(false);
1096
+ sendSessionFrame(selected.sessionId, {
1097
+ type: "prompt",
1098
+ message: `/mcp disconnect ${selected.name}`
1099
+ });
1100
+ },
1101
+ children: "disconnect"
1102
+ }) : null,
1103
+ (server?.state === "failed" || server?.state === "connected" || server?.state === "closed") && selected ? /* @__PURE__ */ jsx(Button, {
1104
+ size: "md",
1105
+ onClick: () => authorize(selected.name),
1106
+ children: server.state === "failed" ? "retry authorization" : "reauthorize"
1107
+ }) : null,
1108
+ /* @__PURE__ */ jsx(Button, {
1109
+ variant: "ghost",
1110
+ size: "md",
1111
+ onClick: () => {
1112
+ closePendingTab();
1113
+ setTarget(null);
1114
+ },
1115
+ children: "close"
1116
+ })
1117
+ ]
1118
+ })
1119
+ ]
1120
+ })
1121
+ })
1122
+ ] });
1123
+ }
1124
+ //#endregion
1125
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/McpAuth.context.web.tsx
1126
+ var McpAuth_context_web_default = defineFill({ component: McpSessionAuthSection });
1127
+ //#endregion
1128
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/mcpAppRuntime.ts
1129
+ const mcpAppRuntime = defineSessionStore({ runtime: null });
1130
+ /** Session mount lifecycle owns the authenticated RPC capability and drops it on unload. */
1131
+ function startMcpAppRuntime(runtime) {
1132
+ const mount = runtime.mount;
1133
+ if (mount?.scope !== "session") throw new Error("MCP Apps require a session runtime mount");
1134
+ mcpAppRuntime.update(mount.sessionId, () => ({ runtime }));
1135
+ return () => {
1136
+ if (mcpAppRuntime.select(mcpAppRuntime.store.state, mount.sessionId).runtime === runtime) mcpAppRuntime.drop(mount.sessionId);
1137
+ };
1138
+ }
1139
+ //#endregion
1140
+ //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/start.web.ts
1141
+ var start_web_default = defineWebLifecycle(startMcpAppRuntime);
1142
+ //#endregion
1143
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/mcpToolMatch.ts
1144
+ /**
1145
+ * Pure view logic for MCP tool cards. MCP tools are registered under `<server>_<tool>`,
1146
+ * so the plugin recognises a call by the server names the session publishes.
1147
+ */
1148
+ /**
1149
+ * The footer status the session half publishes (src/constants/piMcp.ts
1150
+ * MCP_STATUS_KEY). Duplicated because the browser half may import only
1151
+ * src/types and src/constants; a test keeps the two literals equal.
1152
+ */
1153
+ const MCP_STATUS_KEY = "doom-mcp";
1154
+ const SERVER_SEPARATOR = ",";
1155
+ const PI_NAME_SEPARATOR = "_";
1156
+ const MAX_ARGUMENTS = 3;
1157
+ /** Text lines kept in the collapsed result, before the card is expanded. */
1158
+ const COLLAPSED_RESULT_LINES = 12;
1159
+ /** The server names the session reports, in the order it listed them. */
1160
+ function mcpServers(statuses) {
1161
+ const raw = statuses[MCP_STATUS_KEY];
1162
+ if (raw === void 0) return [];
1163
+ return raw.split(SERVER_SEPARATOR).map((name) => name.trim()).filter((name) => name.length > 0);
1164
+ }
1165
+ /**
1166
+ * Splits a Pi tool name into its MCP server and tool. The longest server
1167
+ * prefix wins so `github_enterprise_search` belongs to `github_enterprise`
1168
+ * when both it and `github` are configured.
1169
+ */
1170
+ function matchMcpTool(toolName, statuses) {
1171
+ let best = null;
1172
+ for (const server of mcpServers(statuses)) {
1173
+ const prefix = `${server}${PI_NAME_SEPARATOR}`;
1174
+ if (!toolName.startsWith(prefix) || toolName.length === prefix.length) continue;
1175
+ if (best === null || server.length > best.server.length) best = {
1176
+ server,
1177
+ tool: toolName.slice(prefix.length)
1178
+ };
1179
+ }
1180
+ return best;
1181
+ }
1182
+ /** The identity the result details carry, which the tool wrote itself and is exact. */
1183
+ function mcpIdentityFromDetails(details) {
1184
+ if (typeof details !== "object" || details === null) return null;
1185
+ const record = details;
1186
+ if (typeof record.server !== "string" || typeof record.tool !== "string") return null;
1187
+ return {
1188
+ server: record.server,
1189
+ tool: record.tool
1190
+ };
1191
+ }
1192
+ function argumentValue(value) {
1193
+ if (typeof value === "string") return value;
1194
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1195
+ if (value === null) return "null";
1196
+ if (Array.isArray(value)) return `[${value.length}]`;
1197
+ return "{…}";
1198
+ }
1199
+ /** The first few arguments as `key=value`, with a count for the rest. */
1200
+ function mcpArgumentSummary(params) {
1201
+ const entries = Object.entries(params);
1202
+ const shown = entries.slice(0, MAX_ARGUMENTS).map(([key, value]) => `${key}=${argumentValue(value)}`);
1203
+ if (entries.length > shown.length) shown.push(`+${entries.length - shown.length}`);
1204
+ return shown.join(" · ");
1205
+ }
1206
+ /** The result body as renderMcpResult lays it out: text lines, then one status line. */
1207
+ function mcpResultView(input) {
1208
+ const all = input.output.split("\n");
1209
+ while (all.length > 0 && all.at(-1)?.trim() === "") all.pop();
1210
+ const lines = input.expanded ? all : all.slice(0, COLLAPSED_RESULT_LINES);
1211
+ if (input.isPartial) return {
1212
+ lines,
1213
+ status: {
1214
+ glyph: "◐",
1215
+ tone: "running",
1216
+ text: "running"
1217
+ }
1218
+ };
1219
+ if (input.isError) return {
1220
+ lines,
1221
+ status: {
1222
+ glyph: "✗",
1223
+ tone: "error",
1224
+ text: "failed"
1225
+ }
1226
+ };
1227
+ if (!input.expanded && all.length > lines.length) return {
1228
+ lines,
1229
+ status: {
1230
+ glyph: "…",
1231
+ tone: "hint",
1232
+ text: `${all.length - lines.length} more line(s)`
1233
+ }
1234
+ };
1235
+ if (lines.length === 0) return {
1236
+ lines,
1237
+ status: {
1238
+ glyph: "✓",
1239
+ tone: "ok",
1240
+ text: "done"
1241
+ }
1242
+ };
1243
+ return {
1244
+ lines,
1245
+ status: null
1246
+ };
1247
+ }
1248
+ function isRecord(value) {
1249
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1250
+ }
1251
+ /** One block of the result's details, kept only when its required fields are the strings the adapter wrote. */
1252
+ function mcpResultBlock(entry) {
1253
+ if (!isRecord(entry)) return null;
1254
+ const text = (key) => typeof entry[key] === "string" ? entry[key] : void 0;
1255
+ switch (entry.type) {
1256
+ case "audio": {
1257
+ const data = text("data");
1258
+ const mimeType = text("mimeType");
1259
+ return data !== void 0 && mimeType !== void 0 ? {
1260
+ type: "audio",
1261
+ data,
1262
+ mimeType
1263
+ } : null;
1264
+ }
1265
+ case "resource_link": {
1266
+ const uri = text("uri");
1267
+ const name = text("name");
1268
+ if (uri === void 0 || name === void 0) return null;
1269
+ const title = text("title");
1270
+ const description = text("description");
1271
+ const mimeType = text("mimeType");
1272
+ return {
1273
+ type: "resource_link",
1274
+ uri,
1275
+ name,
1276
+ ...title === void 0 ? {} : { title },
1277
+ ...description === void 0 ? {} : { description },
1278
+ ...mimeType === void 0 ? {} : { mimeType }
1279
+ };
1280
+ }
1281
+ case "resource": {
1282
+ const uri = text("uri");
1283
+ if (uri === void 0) return null;
1284
+ const mimeType = text("mimeType");
1285
+ const content = text("text");
1286
+ const blob = text("blob");
1287
+ return {
1288
+ type: "resource",
1289
+ uri,
1290
+ ...mimeType === void 0 ? {} : { mimeType },
1291
+ ...content === void 0 ? {} : { text: content },
1292
+ ...blob === void 0 ? {} : { blob }
1293
+ };
1294
+ }
1295
+ case "structured": return isRecord(entry.value) ? {
1296
+ type: "structured",
1297
+ value: entry.value
1298
+ } : null;
1299
+ default: return null;
1300
+ }
1301
+ }
1302
+ /** The blocks beyond text the adapter attached to the result's details; malformed entries are dropped. */
1303
+ function mcpResultBlocks(details) {
1304
+ if (!isRecord(details) || !Array.isArray(details.blocks)) return [];
1305
+ return details.blocks.flatMap((entry) => {
1306
+ const block = mcpResultBlock(entry);
1307
+ return block === null ? [] : [block];
1308
+ });
1309
+ }
1310
+ /** The image blocks of a result's content, which Pi carries beside the text. */
1311
+ function mcpImageBlocks(content) {
1312
+ return content.flatMap((block) => {
1313
+ if (!isRecord(block) || block.type !== "image") return [];
1314
+ return typeof block.data === "string" && typeof block.mimeType === "string" ? [{
1315
+ data: block.data,
1316
+ mimeType: block.mimeType
1317
+ }] : [];
1318
+ });
1319
+ }
1320
+ //#endregion
1321
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/mcpAppSandbox.ts
1322
+ const MAX_CSP_DOMAINS = 64;
1323
+ const MAX_DOMAIN_LENGTH = 2048;
1324
+ function record(value) {
1325
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1326
+ }
1327
+ /** Accept origins only, never CSP tokens, URL paths, credentials, or the host origin. */
1328
+ function origins(value, hostOrigin) {
1329
+ if (value === void 0) return [];
1330
+ if (!Array.isArray(value) || value.length > MAX_CSP_DOMAINS) throw new Error("Invalid widget CSP domains");
1331
+ return value.map((domain) => {
1332
+ if (typeof domain !== "string" || domain.length > MAX_DOMAIN_LENGTH) throw new Error("Invalid widget CSP domain");
1333
+ const parsed = new URL(domain);
1334
+ const wildcard = parsed.hostname.startsWith("*.");
1335
+ const host = new URL(hostOrigin);
1336
+ const includesHost = parsed.hostname === host.hostname || wildcard && host.hostname.endsWith(parsed.hostname.slice(1));
1337
+ if (!["https:", "wss:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || includesHost || parsed.hostname === "localhost" || /^(127\.|0\.|\[?::1\]?)/u.test(parsed.hostname) || /[\s;'"<>]/u.test(domain)) throw new Error("Unsafe widget CSP domain");
1338
+ return parsed.origin;
1339
+ });
1340
+ }
1341
+ function mcpAppCsp(metadata, hostOrigin) {
1342
+ const meta = record(metadata);
1343
+ const ui = record(meta?.ui);
1344
+ if (meta?.ui !== void 0 && ui === void 0) throw new Error("Invalid widget UI metadata");
1345
+ const standard = ui !== void 0 && "csp" in ui;
1346
+ const raw = standard ? ui.csp : meta?.["openai/widgetCSP"];
1347
+ const csp = raw === void 0 ? {} : record(raw);
1348
+ if (csp === void 0) throw new Error("Invalid widget CSP");
1349
+ const resource = origins(csp[standard ? "resourceDomains" : "resource_domains"], hostOrigin).join(" ");
1350
+ const connect = origins(csp[standard ? "connectDomains" : "connect_domains"], hostOrigin).join(" ");
1351
+ return [
1352
+ "default-src 'none'",
1353
+ `script-src 'unsafe-inline' ${resource}`,
1354
+ `style-src 'unsafe-inline' ${resource}`,
1355
+ `img-src data: blob: ${resource}`,
1356
+ `font-src data: ${resource}`,
1357
+ `media-src blob: ${resource}`,
1358
+ `connect-src ${connect || "'none'"}`,
1359
+ "frame-src 'none'",
1360
+ "child-src 'none'",
1361
+ "worker-src 'none'",
1362
+ "object-src 'none'",
1363
+ "base-uri 'none'",
1364
+ "form-action 'none'"
1365
+ ].join("; ");
1366
+ }
1367
+ function escapeAttribute(value) {
1368
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;");
1369
+ }
1370
+ /** The theme a host message carries, from either bridge. Serialized into frames, so it stays self-contained. */
1371
+ function mcpAppHostTheme(data) {
1372
+ const message = data;
1373
+ const theme = message?.channel === "po.openai" ? message.globals?.theme : message?.jsonrpc === "2.0" && message.method === "ui/notifications/host-context-changed" ? message.params?.theme : void 0;
1374
+ return theme === "light" || theme === "dark" ? theme : void 0;
1375
+ }
1376
+ /**
1377
+ * Bundled trusted code, runs inside the App document. Waits for the App's own
1378
+ * <html> attributes, which the parser merges only where the root has none, so
1379
+ * an App-chosen class or data-theme wins until the host theme changes.
1380
+ */
1381
+ function widgetThemeBootstrap(hostTheme, initial) {
1382
+ const root = document.documentElement;
1383
+ let theme = initial;
1384
+ let changed = false;
1385
+ const settle = () => {
1386
+ if (!root.classList.contains("light") && !root.classList.contains("dark")) root.classList.add(theme);
1387
+ if (root.getAttribute("data-theme") === null) root.setAttribute("data-theme", theme);
1388
+ };
1389
+ const follow = () => {
1390
+ root.classList.remove("light", "dark");
1391
+ root.classList.add(theme);
1392
+ const current = root.getAttribute("data-theme");
1393
+ if (current === null || current === "light" || current === "dark") root.setAttribute("data-theme", theme);
1394
+ root.style.colorScheme = theme;
1395
+ };
1396
+ window.addEventListener("message", (event) => {
1397
+ const next = event.source === parent ? hostTheme(event.data) : void 0;
1398
+ if (next === void 0) return;
1399
+ theme = next;
1400
+ changed = true;
1401
+ if (document.readyState !== "loading") follow();
1402
+ });
1403
+ if (document.readyState !== "loading") settle();
1404
+ else document.addEventListener("readystatechange", () => {
1405
+ if (document.readyState === "interactive") {
1406
+ if (changed) follow();
1407
+ else settle();
1408
+ }
1409
+ });
1410
+ }
1411
+ /** Bundled trusted code, runs in an opaque relay separate from the untrusted App. */
1412
+ function relayBootstrap(hostTheme) {
1413
+ let configured = false;
1414
+ const follow = (data) => {
1415
+ const theme = hostTheme(data);
1416
+ if (theme !== void 0) document.documentElement.style.colorScheme = theme;
1417
+ };
1418
+ window.addEventListener("message", (event) => {
1419
+ if (event.source !== parent || configured || event.ports.length !== 1) return;
1420
+ const config = event.data;
1421
+ if (typeof config?.html !== "string" || typeof config.policy !== "string") return;
1422
+ configured = true;
1423
+ const port = event.ports[0];
1424
+ const inner = document.createElement("iframe");
1425
+ inner.title = "MCP App content";
1426
+ inner.setAttribute("sandbox", "allow-scripts");
1427
+ inner.setAttribute("csp", config.policy);
1428
+ inner.setAttribute("allow", "camera 'none'; microphone 'none'; geolocation 'none'; clipboard-read 'none'; clipboard-write 'none'");
1429
+ inner.style.cssText = "width:100%;height:100%;border:0;display:block";
1430
+ let loaded = false;
1431
+ let revoked = false;
1432
+ const revoke = () => {
1433
+ if (revoked) return;
1434
+ revoked = true;
1435
+ inner.remove();
1436
+ port.postMessage({ type: "revoked" });
1437
+ port.close();
1438
+ };
1439
+ inner.addEventListener("load", () => {
1440
+ if (loaded) revoke();
1441
+ loaded = true;
1442
+ });
1443
+ let messageWindow = Date.now();
1444
+ let messageCount = 0;
1445
+ window.addEventListener("message", (childEvent) => {
1446
+ if (revoked || childEvent.source !== inner.contentWindow) return;
1447
+ try {
1448
+ const encoded = JSON.stringify(childEvent.data);
1449
+ if (encoded === void 0 || new TextEncoder().encode(encoded).byteLength > 65536) return revoke();
1450
+ const now = Date.now();
1451
+ if (now - messageWindow >= 1e3) {
1452
+ messageWindow = now;
1453
+ messageCount = 0;
1454
+ }
1455
+ if (++messageCount > 128) return revoke();
1456
+ } catch {
1457
+ return revoke();
1458
+ }
1459
+ const openai = childEvent.data?.channel === "po.openai";
1460
+ if (config.protocol === "mcp" && !openai) parent.postMessage(childEvent.data, "*");
1461
+ else port.postMessage({
1462
+ type: "legacy",
1463
+ data: childEvent.data
1464
+ });
1465
+ });
1466
+ port.onmessage = (hostEvent) => {
1467
+ if (revoked) return;
1468
+ follow(hostEvent.data);
1469
+ inner.contentWindow?.postMessage(hostEvent.data, "*");
1470
+ };
1471
+ window.addEventListener("message", (hostEvent) => {
1472
+ if (!revoked && hostEvent.source === parent && config.protocol === "mcp") {
1473
+ const data = hostEvent.data;
1474
+ if (data?.jsonrpc !== "2.0") return;
1475
+ follow(data);
1476
+ inner.contentWindow?.postMessage(data, "*");
1477
+ }
1478
+ });
1479
+ port.start();
1480
+ inner.srcdoc = config.html;
1481
+ document.body.append(inner);
1482
+ port.postMessage({ type: "ready" });
1483
+ });
1484
+ }
1485
+ function mcpAppRelayHtml(policy, theme = "light") {
1486
+ return `<!doctype html><html style="color-scheme:${theme}"><meta http-equiv="Content-Security-Policy" content="${escapeAttribute(policy.replace("frame-src 'none'", "frame-src about:").replace("child-src 'none'", "child-src about:"))}"><style>html,body{margin:0;height:100%;overflow:hidden}</style><script>(${relayBootstrap.toString()})(${mcpAppHostTheme.toString()})<\/script>`;
1487
+ }
1488
+ /** No host <html> is injected, so the App's own root attributes survive parsing. */
1489
+ function mcpAppDocument(html, policy, bootstrap = "", theme = "light") {
1490
+ const themeScript = `<script>(${widgetThemeBootstrap.toString()})(${mcpAppHostTheme.toString()}, ${JSON.stringify(theme)})<\/script>`;
1491
+ return `<!doctype html><meta http-equiv="Content-Security-Policy" content="${escapeAttribute(policy)}"><meta name="color-scheme" content="${theme}">${themeScript}${bootstrap}${html}`;
1492
+ }
1493
+ /** Required CSP enforcement must survive self-navigation, not only initial srcdoc. */
1494
+ function supportsMcpAppSandbox() {
1495
+ return typeof HTMLIFrameElement !== "undefined" && "csp" in HTMLIFrameElement.prototype;
1496
+ }
1497
+ //#endregion
1498
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/mcpOpenAiBootstrap.ts
1499
+ function bootstrap(initial) {
1500
+ const channel = "po.openai";
1501
+ const requestTimeout = 6e4;
1502
+ const pending = /* @__PURE__ */ new Map();
1503
+ let sequence = 0;
1504
+ let globals = initial;
1505
+ const update = (patch) => {
1506
+ globals = {
1507
+ ...globals,
1508
+ ...patch
1509
+ };
1510
+ window.dispatchEvent(new CustomEvent("openai:set_globals", { detail: { globals: patch } }));
1511
+ };
1512
+ const request = (method, input) => {
1513
+ if (pending.size >= 32) return Promise.reject(/* @__PURE__ */ new Error("Too many pending widget requests"));
1514
+ const id = ++sequence;
1515
+ return new Promise((resolve, reject) => {
1516
+ const timer = window.setTimeout(() => {
1517
+ pending.delete(id);
1518
+ reject(/* @__PURE__ */ new Error("Widget request timed out"));
1519
+ }, requestTimeout);
1520
+ pending.set(id, {
1521
+ resolve,
1522
+ reject,
1523
+ timer
1524
+ });
1525
+ parent.postMessage({
1526
+ channel,
1527
+ id,
1528
+ method,
1529
+ input
1530
+ }, "*");
1531
+ });
1532
+ };
1533
+ Object.defineProperty(window, "openai", {
1534
+ value: Object.freeze({
1535
+ get toolInput() {
1536
+ return globals.toolInput;
1537
+ },
1538
+ get toolOutput() {
1539
+ return globals.toolOutput;
1540
+ },
1541
+ get toolResponseMetadata() {
1542
+ return globals.toolResponseMetadata;
1543
+ },
1544
+ get widgetState() {
1545
+ return globals.widgetState;
1546
+ },
1547
+ get theme() {
1548
+ return globals.theme;
1549
+ },
1550
+ get locale() {
1551
+ return globals.locale;
1552
+ },
1553
+ get displayMode() {
1554
+ return globals.displayMode;
1555
+ },
1556
+ get maxHeight() {
1557
+ return globals.maxHeight;
1558
+ },
1559
+ get safeArea() {
1560
+ return globals.safeArea;
1561
+ },
1562
+ get userAgent() {
1563
+ return globals.userAgent;
1564
+ },
1565
+ callTool: (name, args = {}) => request("callTool", {
1566
+ name,
1567
+ arguments: args
1568
+ }),
1569
+ setWidgetState: (state) => {
1570
+ update({ widgetState: state });
1571
+ return request("setState", { state });
1572
+ },
1573
+ sendFollowUpMessage: (input) => request("followUp", input),
1574
+ requestDisplayMode: () => Promise.resolve({ mode: "inline" }),
1575
+ openExternal: (input) => request("openLink", { url: input.href }),
1576
+ requestClose: () => request("close", {}),
1577
+ notifyIntrinsicHeight: (height) => request("height", { height })
1578
+ }),
1579
+ configurable: false,
1580
+ writable: false
1581
+ });
1582
+ window.addEventListener("message", (event) => {
1583
+ if (event.source !== parent) return;
1584
+ const message = event.data;
1585
+ if (message?.channel !== channel) return;
1586
+ if (message.globals !== void 0) update(message.globals);
1587
+ if (message.id === void 0) return;
1588
+ const callback = pending.get(message.id);
1589
+ if (callback === void 0) return;
1590
+ pending.delete(message.id);
1591
+ clearTimeout(callback.timer);
1592
+ if (typeof message.error === "string") callback.reject(new Error(message.error));
1593
+ else callback.resolve(message.result);
1594
+ });
1595
+ parent.postMessage({
1596
+ channel,
1597
+ method: "initialized",
1598
+ input: {}
1599
+ }, "*");
1600
+ document.addEventListener("DOMContentLoaded", () => {
1601
+ update(globals);
1602
+ const observer = new ResizeObserver(() => {
1603
+ const height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
1604
+ parent.postMessage({
1605
+ channel,
1606
+ method: "height",
1607
+ input: { height }
1608
+ }, "*");
1609
+ });
1610
+ observer.observe(document.body);
1611
+ window.addEventListener("pagehide", () => observer.disconnect(), { once: true });
1612
+ }, { once: true });
1613
+ }
1614
+ function mcpOpenAiScript(globals) {
1615
+ const encoded = JSON.stringify(globals).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
1616
+ return `<script>(${bootstrap.toString()})(${encoded})<\/script>`;
1617
+ }
1618
+ //#endregion
1619
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/McpInlineApp.tsx
1620
+ const DEFAULT_HEIGHT = 320;
1621
+ const MAX_HEIGHT = 1200;
1622
+ const MIN_HEIGHT = 80;
1623
+ const OPEN_TIMEOUT = 15e3;
1624
+ const LEGACY_CHANNEL = "po.openai";
1625
+ function currentTheme() {
1626
+ const scheme = document.documentElement.style.colorScheme;
1627
+ if (scheme === "light" || scheme === "dark") return scheme;
1628
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1629
+ }
1630
+ function errorText(error) {
1631
+ return error instanceof Error ? error.message : "The MCP App could not complete this request";
1632
+ }
1633
+ function object(value) {
1634
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid App request");
1635
+ return value;
1636
+ }
1637
+ function invoke(runtime, sessionId, method, input) {
1638
+ return runtime.invokeServerMethod({
1639
+ mount: {
1640
+ scope: "session",
1641
+ sessionId
1642
+ },
1643
+ service: "mcp.apps",
1644
+ method,
1645
+ input
1646
+ });
1647
+ }
1648
+ async function openLink(value) {
1649
+ if (typeof value !== "string") throw new Error("Invalid external link");
1650
+ const url = new URL(value);
1651
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("Only HTTPS external links are supported");
1652
+ if (!window.confirm(`Open this external link?\n${url.href}`)) throw new Error("Opening the link was declined");
1653
+ const opened = window.open(url.href, "_blank", "noopener,noreferrer");
1654
+ if (opened !== null) opened.opener = null;
1655
+ return {};
1656
+ }
1657
+ function AppFrame({ runtime, sessionId, toolCallId, data, savedState, saveState, onClose }) {
1658
+ const frameRef = useRef(null);
1659
+ const [height, setHeight] = useState(DEFAULT_HEIGHT);
1660
+ const [failure, setFailure] = useState(null);
1661
+ const [ready, setReady] = useState(false);
1662
+ const [initialState] = useState(() => savedState);
1663
+ useEffect(() => {
1664
+ const frame = frameRef.current;
1665
+ if (frame === null || frame.contentWindow === null) return;
1666
+ let live = true;
1667
+ let bridge;
1668
+ const channels = new MessageChannel();
1669
+ const request = (method, input) => {
1670
+ if (!live) return Promise.reject(/* @__PURE__ */ new Error("MCP App is closed"));
1671
+ return invoke(runtime, sessionId, method, {
1672
+ ...input,
1673
+ leaseId: data.leaseId
1674
+ });
1675
+ };
1676
+ const resize = (value) => {
1677
+ if (typeof value === "number" && Number.isFinite(value)) setHeight(Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, Math.ceil(value))));
1678
+ };
1679
+ const fail = (error) => {
1680
+ if (!live) return;
1681
+ setFailure(errorText(error));
1682
+ live = false;
1683
+ frame.srcdoc = "";
1684
+ requestCloseLease().catch((closeError) => {
1685
+ setFailure(`${errorText(error)}. Closing the App lease failed: ${errorText(closeError)}`);
1686
+ });
1687
+ };
1688
+ const requestCloseLease = () => invoke(runtime, sessionId, "close", { leaseId: data.leaseId });
1689
+ const timeout = window.setTimeout(() => fail(/* @__PURE__ */ new Error("MCP App initialization timed out")), OPEN_TIMEOUT);
1690
+ const markReady = () => {
1691
+ if (!live) return;
1692
+ clearTimeout(timeout);
1693
+ setReady(true);
1694
+ };
1695
+ const callTool = async (name, args) => {
1696
+ if (data.readOnly) throw new Error("Enable interactions in the conversation before calling tools");
1697
+ if (typeof name !== "string") throw new Error("Invalid App tool name");
1698
+ return await request("callTool", {
1699
+ name,
1700
+ arguments: object(args ?? {})
1701
+ });
1702
+ };
1703
+ const persist = async (state) => {
1704
+ await request("setState", { state });
1705
+ saveState(state);
1706
+ return {};
1707
+ };
1708
+ const followUp = async (prompt) => {
1709
+ if (data.readOnly) throw new Error("Enable interactions in the conversation before sending a follow-up");
1710
+ if (typeof prompt !== "string") throw new Error("Invalid App follow-up");
1711
+ await request("followUp", { prompt });
1712
+ return {};
1713
+ };
1714
+ const close = async () => {
1715
+ await request("close", {});
1716
+ onClose();
1717
+ return {};
1718
+ };
1719
+ const theme = currentTheme();
1720
+ const globals = {
1721
+ toolInput: data.args,
1722
+ toolOutput: data.result.structuredContent ?? null,
1723
+ toolResponseMetadata: data.result._meta ?? {},
1724
+ widgetState: initialState,
1725
+ theme,
1726
+ locale: navigator.language,
1727
+ displayMode: "inline",
1728
+ maxHeight: MAX_HEIGHT,
1729
+ safeArea: { insets: {
1730
+ top: 0,
1731
+ right: 0,
1732
+ bottom: 0,
1733
+ left: 0
1734
+ } },
1735
+ userAgent: {
1736
+ device: { type: window.matchMedia("(pointer: coarse)").matches ? "mobile" : "desktop" },
1737
+ capabilities: {
1738
+ hover: window.matchMedia("(hover: hover)").matches,
1739
+ touch: navigator.maxTouchPoints > 0
1740
+ }
1741
+ }
1742
+ };
1743
+ try {
1744
+ const policy = mcpAppCsp(data.resourceMeta, location.origin);
1745
+ if (data.protocol === "mcp") {
1746
+ bridge = new AppBridge(null, {
1747
+ name: "Po",
1748
+ version: "1"
1749
+ }, {
1750
+ ...data.readOnly ? {} : {
1751
+ serverTools: {},
1752
+ message: { text: {} }
1753
+ },
1754
+ openLinks: {}
1755
+ }, { hostContext: {
1756
+ theme,
1757
+ locale: navigator.language,
1758
+ displayMode: "inline",
1759
+ availableDisplayModes: ["inline"],
1760
+ toolInfo: {
1761
+ id: toolCallId,
1762
+ tool: data.tool
1763
+ }
1764
+ } });
1765
+ bridge.oncalltool = ({ name, arguments: args }) => callTool(name, args);
1766
+ bridge.onmessage = async ({ content }) => {
1767
+ if (content.some((block) => block.type !== "text")) throw new Error("Only text follow-ups are supported");
1768
+ return followUp(content.map((block) => block.type === "text" ? block.text : "").join("\n"));
1769
+ };
1770
+ bridge.onopenlink = ({ url }) => openLink(url);
1771
+ bridge.onsizechange = ({ height: next }) => resize(next);
1772
+ bridge.onrequestdisplaymode = async () => ({ mode: "inline" });
1773
+ bridge.onrequestteardown = () => {
1774
+ close().catch(fail);
1775
+ };
1776
+ bridge.oninitialized = () => {
1777
+ (async () => {
1778
+ await bridge.sendToolInput({ arguments: data.args });
1779
+ await bridge.sendToolResult(data.result);
1780
+ markReady();
1781
+ })().catch(fail);
1782
+ };
1783
+ bridge.connect(new PostMessageTransport(frame.contentWindow, frame.contentWindow)).catch(fail);
1784
+ }
1785
+ channels.port1.onmessage = (event) => {
1786
+ if (!live) return;
1787
+ const message = event.data;
1788
+ if (message?.type === "revoked") {
1789
+ fail(/* @__PURE__ */ new Error("MCP App navigation was blocked; reopen it to continue"));
1790
+ return;
1791
+ }
1792
+ if (message?.type !== "legacy") return;
1793
+ const legacy = message.data;
1794
+ if (legacy?.channel !== LEGACY_CHANNEL) return;
1795
+ (async () => {
1796
+ const input = object(legacy.input ?? {});
1797
+ switch (legacy.method) {
1798
+ case "initialized": {
1799
+ markReady();
1800
+ const loadedTheme = currentTheme();
1801
+ if (loadedTheme !== theme) channels.port1.postMessage({
1802
+ channel: LEGACY_CHANNEL,
1803
+ globals: { theme: loadedTheme }
1804
+ });
1805
+ return {};
1806
+ }
1807
+ case "height":
1808
+ resize(input.height);
1809
+ return {};
1810
+ case "callTool": return callTool(input.name, input.arguments);
1811
+ case "setState": return persist(input.state);
1812
+ case "followUp": return followUp(input.prompt);
1813
+ case "openLink": return openLink(input.url);
1814
+ case "close": return close();
1815
+ default: throw new Error("Unsupported ChatGPT widget method");
1816
+ }
1817
+ })().then((result) => {
1818
+ if (!live || legacy.id === void 0) return;
1819
+ const returned = legacy.method === "callTool" && data.protocol === "openai" ? result : void 0;
1820
+ channels.port1.postMessage({
1821
+ channel: LEGACY_CHANNEL,
1822
+ id: legacy.id,
1823
+ result,
1824
+ ...returned === void 0 ? {} : { globals: {
1825
+ toolOutput: returned.structuredContent ?? null,
1826
+ toolResponseMetadata: returned._meta ?? {}
1827
+ } }
1828
+ });
1829
+ }, (error) => {
1830
+ if (!live || legacy.id === void 0) return;
1831
+ channels.port1.postMessage({
1832
+ channel: LEGACY_CHANNEL,
1833
+ id: legacy.id,
1834
+ error: errorText(error)
1835
+ });
1836
+ });
1837
+ };
1838
+ channels.port1.start();
1839
+ const load = () => {
1840
+ frame.contentWindow?.postMessage({
1841
+ html: mcpAppDocument(data.html, policy, mcpOpenAiScript(globals), theme),
1842
+ policy,
1843
+ protocol: data.protocol
1844
+ }, "*", [channels.port2]);
1845
+ };
1846
+ frame.addEventListener("load", load, { once: true });
1847
+ frame.srcdoc = mcpAppRelayHtml(policy, theme);
1848
+ const observer = new MutationObserver(() => {
1849
+ const nextTheme = currentTheme();
1850
+ bridge?.setHostContext({ theme: nextTheme });
1851
+ channels.port1.postMessage({
1852
+ channel: LEGACY_CHANNEL,
1853
+ globals: { theme: nextTheme }
1854
+ });
1855
+ });
1856
+ observer.observe(document.documentElement, {
1857
+ attributes: true,
1858
+ attributeFilter: [
1859
+ "class",
1860
+ "data-theme",
1861
+ "style"
1862
+ ]
1863
+ });
1864
+ return () => {
1865
+ live = false;
1866
+ clearTimeout(timeout);
1867
+ observer.disconnect();
1868
+ frame.removeEventListener("load", load);
1869
+ channels.port1.close();
1870
+ channels.port2.close();
1871
+ if (bridge !== void 0) bridge.close().catch((error) => setFailure(errorText(error)));
1872
+ frame.srcdoc = "";
1873
+ };
1874
+ } catch (error) {
1875
+ fail(error);
1876
+ return () => {
1877
+ clearTimeout(timeout);
1878
+ channels.port1.close();
1879
+ channels.port2.close();
1880
+ };
1881
+ }
1882
+ }, [
1883
+ runtime,
1884
+ sessionId,
1885
+ toolCallId,
1886
+ data,
1887
+ initialState,
1888
+ saveState,
1889
+ onClose
1890
+ ]);
1891
+ return /* @__PURE__ */ jsxs("div", {
1892
+ className: "overflow-hidden rounded border border-doom-border-soft",
1893
+ "data-testid": "mcp-inline-app",
1894
+ children: [
1895
+ failure !== null ? /* @__PURE__ */ jsx("p", {
1896
+ role: "alert",
1897
+ className: "p-3 text-doom-dim",
1898
+ children: failure
1899
+ }) : null,
1900
+ !ready && failure === null ? /* @__PURE__ */ jsx("output", {
1901
+ className: "p-3 text-doom-faint",
1902
+ children: "Loading MCP App…"
1903
+ }) : null,
1904
+ /* @__PURE__ */ jsx("iframe", {
1905
+ ref: frameRef,
1906
+ title: `${data.tool.name} MCP App`,
1907
+ sandbox: "allow-scripts",
1908
+ referrerPolicy: "no-referrer",
1909
+ allow: "camera 'none'; microphone 'none'; geolocation 'none'; clipboard-read 'none'; clipboard-write 'none'",
1910
+ className: failure !== null ? "hidden" : "block w-full border-0",
1911
+ style: { height }
1912
+ })
1913
+ ]
1914
+ });
1915
+ }
1916
+ function McpInlineApp({ sessionId, toolCallId }) {
1917
+ const runtime = useStore(mcpAppRuntime.store, (state) => mcpAppRuntime.select(state, sessionId).runtime);
1918
+ const [data, setData] = useState(null);
1919
+ const [failure, setFailure] = useState(null);
1920
+ const [activating, setActivating] = useState(false);
1921
+ const [actionFailure, setActionFailure] = useState(null);
1922
+ const [closed, setClosed] = useState(false);
1923
+ const [widgetState, setWidgetState] = useState(null);
1924
+ const saveState = useCallback((value) => setWidgetState(value), []);
1925
+ const close = useCallback(() => setClosed(true), []);
1926
+ useEffect(() => {
1927
+ if (runtime === null || sessionId === null || !supportsMcpAppSandbox()) return;
1928
+ let mounted = true;
1929
+ let leaseId;
1930
+ invoke(runtime, sessionId, "open", { toolCallId }).then((response) => {
1931
+ const opened = response;
1932
+ leaseId = opened.leaseId;
1933
+ if (!mounted) return invoke(runtime, sessionId, "close", { leaseId });
1934
+ setWidgetState(opened.state);
1935
+ setData(opened);
1936
+ }).catch((error) => {
1937
+ if (mounted) setFailure(errorText(error));
1938
+ });
1939
+ return () => {
1940
+ mounted = false;
1941
+ if (leaseId !== void 0) invoke(runtime, sessionId, "close", { leaseId }).catch((error) => setFailure(errorText(error)));
1942
+ };
1943
+ }, [
1944
+ runtime,
1945
+ sessionId,
1946
+ toolCallId
1947
+ ]);
1948
+ if (closed) return /* @__PURE__ */ jsx("p", {
1949
+ className: "text-doom-faint",
1950
+ children: "MCP App closed. Expand the card to view the tool result."
1951
+ });
1952
+ if (!supportsMcpAppSandbox()) return /* @__PURE__ */ jsx("p", {
1953
+ className: "text-doom-faint",
1954
+ children: "This browser cannot securely display MCP Apps. Expand the card to view the tool result."
1955
+ });
1956
+ if (failure !== null) return /* @__PURE__ */ jsxs("p", {
1957
+ role: "alert",
1958
+ className: "text-doom-dim",
1959
+ children: ["MCP App unavailable: ", failure]
1960
+ });
1961
+ if (runtime === null || sessionId === null) return /* @__PURE__ */ jsx("p", {
1962
+ className: "text-doom-faint",
1963
+ children: "The interactive MCP App is unavailable in this view. Expand the card to view the tool result."
1964
+ });
1965
+ if (data === null) return /* @__PURE__ */ jsx("output", {
1966
+ className: "text-doom-faint",
1967
+ children: "Loading MCP App…"
1968
+ });
1969
+ const activate = () => {
1970
+ setActivating(true);
1971
+ setActionFailure(null);
1972
+ invoke(runtime, sessionId, "activate", { leaseId: data.leaseId }).then(() => {
1973
+ setData({
1974
+ ...data,
1975
+ readOnly: false
1976
+ });
1977
+ }).catch((error) => setActionFailure(errorText(error))).finally(() => setActivating(false));
1978
+ };
1979
+ return /* @__PURE__ */ jsxs("div", {
1980
+ className: "flex flex-col gap-2",
1981
+ children: [
1982
+ /* @__PURE__ */ jsxs("div", {
1983
+ className: "flex items-center justify-between gap-2 text-doom-faint",
1984
+ children: [/* @__PURE__ */ jsx("span", { children: data.readOnly ? "Read-only MCP App" : "MCP App interactions enabled" }), data.readOnly ? /* @__PURE__ */ jsx("button", {
1985
+ type: "button",
1986
+ disabled: activating,
1987
+ onClick: activate,
1988
+ className: "rounded border border-doom-border-soft px-2 py-1 text-doom-blue disabled:opacity-50",
1989
+ children: activating ? "Enabling…" : "Enable interactions"
1990
+ }) : null]
1991
+ }),
1992
+ actionFailure !== null ? /* @__PURE__ */ jsx("p", {
1993
+ role: "alert",
1994
+ className: "text-doom-dim",
1995
+ children: actionFailure
1996
+ }) : null,
1997
+ /* @__PURE__ */ jsx(AppFrame, {
1998
+ runtime,
1999
+ sessionId,
2000
+ toolCallId,
2001
+ data,
2002
+ savedState: widgetState,
2003
+ saveState,
2004
+ onClose: close
2005
+ }, `${data.leaseId}-${String(data.readOnly)}`)
2006
+ ]
2007
+ });
2008
+ }
2009
+ //#endregion
2010
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/McpToolMessage.tsx
2011
+ const STATUS_TONE = {
2012
+ running: "running",
2013
+ ok: "ok",
2014
+ error: "error",
2015
+ hint: "neutral"
2016
+ };
2017
+ /** The Pi name split on its first underscore, for a call whose server the session has not named. */
2018
+ function fallbackIdentity(toolName) {
2019
+ const separator = toolName.indexOf("_");
2020
+ if (separator <= 0) return {
2021
+ server: toolName,
2022
+ tool: ""
2023
+ };
2024
+ return {
2025
+ server: toolName.slice(0, separator),
2026
+ tool: toolName.slice(separator + 1)
2027
+ };
2028
+ }
2029
+ /** One block beyond text the result carried: a link, an embedded resource, structured content, or audio. */
2030
+ function McpBlock({ block }) {
2031
+ switch (block.type) {
2032
+ case "resource_link": return /* @__PURE__ */ jsx("a", {
2033
+ href: block.uri,
2034
+ target: "_blank",
2035
+ rel: "noreferrer",
2036
+ "data-testid": "tool-result-mcp-link",
2037
+ className: "truncate text-doom-blue hover:underline",
2038
+ title: block.description ?? block.uri,
2039
+ children: block.title ?? block.name
2040
+ });
2041
+ case "resource": return block.text !== void 0 ? /* @__PURE__ */ jsx(SyntaxText, {
2042
+ "data-testid": "tool-result-mcp-resource",
2043
+ className: "text-doom-dim",
2044
+ text: block.text
2045
+ }) : /* @__PURE__ */ jsxs("span", {
2046
+ "data-testid": "tool-result-mcp-resource",
2047
+ className: "truncate text-doom-dim",
2048
+ children: [
2049
+ "binary ",
2050
+ block.mimeType ?? "resource",
2051
+ " · ",
2052
+ block.uri
2053
+ ]
2054
+ });
2055
+ case "structured": return /* @__PURE__ */ jsx(SyntaxText, {
2056
+ "data-testid": "tool-result-mcp-structured",
2057
+ className: "text-doom-dim",
2058
+ grammar: "json",
2059
+ text: JSON.stringify(block.value, null, 2)
2060
+ });
2061
+ case "audio": return /* @__PURE__ */ jsxs("span", {
2062
+ "data-testid": "tool-result-mcp-audio",
2063
+ className: "text-doom-dim",
2064
+ children: ["audio · ", block.mimeType]
2065
+ });
2066
+ }
2067
+ }
2068
+ /**
2069
+ * The MCP tool's timeline item, the web half of renderMcpCall and
2070
+ * renderMcpResult: `mcp | server / tool · k=v` in the header; the text lines,
2071
+ * collapsed to twelve, then the images Pi carries beside the text, the
2072
+ * blocks the adapter put in the details, and one status line in the body.
2073
+ * A call that rendered an MCP App keeps its result behind the toggle, unless it failed.
2074
+ */
2075
+ function McpToolMessage({ sessionId, toolCallId, toolName, args, result, output, running, isError, statuses }) {
2076
+ const identity = mcpIdentityFromDetails(result?.details) ?? matchMcpTool(toolName, statuses) ?? fallbackIdentity(toolName);
2077
+ const summary = mcpArgumentSummary(args);
2078
+ const collapsed = mcpResultView({
2079
+ output,
2080
+ expanded: false,
2081
+ isPartial: running,
2082
+ isError
2083
+ });
2084
+ const images = mcpImageBlocks(result?.content ?? []);
2085
+ const blocks = mcpResultBlocks(result?.details);
2086
+ const details = result?.details;
2087
+ const app = !running && details !== null && typeof details === "object" && "app" in details ? details.app : void 0;
2088
+ const hasApp = app !== null && typeof app === "object" && "version" in app && app.version === 1 && "resourceUri" in app && typeof app.resourceUri === "string";
2089
+ const resultFirst = !hasApp || isError;
2090
+ return /* @__PURE__ */ jsx(MessageItem, {
2091
+ tone: toolTone({
2092
+ running,
2093
+ isError
2094
+ }),
2095
+ expandable: !resultFirst || collapsed.status?.tone === "hint",
2096
+ children: ({ expanded }) => {
2097
+ const view = expanded ? mcpResultView({
2098
+ output,
2099
+ expanded: true,
2100
+ isPartial: running,
2101
+ isError
2102
+ }) : collapsed;
2103
+ const hint = view.status?.tone === "hint";
2104
+ const showResult = resultFirst || expanded;
2105
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2106
+ /* @__PURE__ */ jsx(MessageItemHeader, {
2107
+ title: "mcp",
2108
+ children: /* @__PURE__ */ jsxs("span", {
2109
+ "data-testid": "tool-call-mcp",
2110
+ className: "flex min-w-0 flex-1 items-center gap-1.5",
2111
+ children: [
2112
+ /* @__PURE__ */ jsx("span", {
2113
+ className: "shrink-0 text-doom-dim",
2114
+ children: identity.server
2115
+ }),
2116
+ /* @__PURE__ */ jsx("span", {
2117
+ className: "shrink-0 text-doom-faint",
2118
+ children: "/"
2119
+ }),
2120
+ /* @__PURE__ */ jsx("span", {
2121
+ className: "shrink-0 text-doom-blue",
2122
+ children: identity.tool
2123
+ }),
2124
+ summary ? /* @__PURE__ */ jsxs("span", {
2125
+ className: "min-w-0 flex-1 truncate text-doom-dim",
2126
+ children: ["· ", summary]
2127
+ }) : null
2128
+ ]
2129
+ })
2130
+ }),
2131
+ hasApp ? /* @__PURE__ */ jsx(MessageItemBody, { children: /* @__PURE__ */ jsx(McpInlineApp, {
2132
+ sessionId,
2133
+ toolCallId
2134
+ }, `${sessionId}:${toolCallId}`) }) : null,
2135
+ showResult && (view.lines.length > 0 || view.status !== null || images.length > 0 || blocks.length > 0) ? /* @__PURE__ */ jsxs(MessageItemBody, {
2136
+ "data-testid": "tool-result-mcp",
2137
+ "data-mcp-server": identity.server,
2138
+ className: "flex flex-col gap-1",
2139
+ children: [
2140
+ view.lines.length > 0 ? /* @__PURE__ */ jsx("pre", {
2141
+ className: "whitespace-pre-wrap break-words font-mono text-doom-dim",
2142
+ children: view.lines.join("\n")
2143
+ }) : null,
2144
+ images.map((image, index) => /* @__PURE__ */ jsx("img", {
2145
+ "data-testid": "tool-result-mcp-image",
2146
+ src: `data:${image.mimeType};base64,${image.data}`,
2147
+ alt: `${identity.tool} result ${String(index + 1)}`,
2148
+ className: "max-h-60 max-w-full self-start rounded border border-doom-border-soft"
2149
+ }, `${String(index)}-${image.mimeType}`)),
2150
+ blocks.map((block, index) => /* @__PURE__ */ jsx(McpBlock, { block }, `${String(index)}-${block.type}`)),
2151
+ view.status ? /* @__PURE__ */ jsx(MessageItemStatus, {
2152
+ "data-testid": "tool-result-mcp-status",
2153
+ tone: STATUS_TONE[view.status.tone],
2154
+ glyph: view.status.glyph,
2155
+ expands: hint,
2156
+ children: view.status.text
2157
+ }) : null
2158
+ ]
2159
+ }) : null
2160
+ ] });
2161
+ }
2162
+ });
2163
+ }
2164
+ //#endregion
2165
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/mcp.web.tsx
2166
+ var mcp_web_default = defineToolRenderer({
2167
+ tools: [],
2168
+ matches: (toolName, statuses, details) => mcpIdentityFromDetails(details) !== null || matchMcpTool(toolName, statuses) !== null,
2169
+ message: McpToolMessage
2170
+ });
2171
+ //#endregion
2172
+ //#region generated/web.ts
2173
+ const via = (identity, value) => value === void 0 ? void 0 : {
2174
+ ...identity,
2175
+ ...value
2176
+ };
2177
+ const webPlugin = defineWebPlugin({
2178
+ id: "mcp",
2179
+ workspace: { get repositorySettingsPanel() {
2180
+ return mcp_web_default$1;
2181
+ } },
2182
+ session: {
2183
+ get fills() {
2184
+ return [via({
2185
+ slot: "context",
2186
+ id: "mcp-auth"
2187
+ }, McpAuth_context_web_default)];
2188
+ },
2189
+ get start() {
2190
+ return start_web_default;
2191
+ },
2192
+ get toolRenderers() {
2193
+ return [via({ tools: ["mcp"] }, mcp_web_default)];
2194
+ }
2195
+ }
2196
+ });
2197
+ //#endregion
2198
+ export { webPlugin };
2199
+
2200
+ //# sourceMappingURL=web.mjs.map