@fitchmultz/pi-mcp-adapter 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +750 -0
- package/LICENSE +21 -0
- package/OAUTH.md +462 -0
- package/README.md +820 -0
- package/abort.ts +34 -0
- package/agent-dir.ts +24 -0
- package/app-bridge.bundle.js +67 -0
- package/banner.png +0 -0
- package/checkpoint.ts +55 -0
- package/cli.js +222 -0
- package/commands.ts +619 -0
- package/config.ts +1204 -0
- package/consent-manager.ts +25 -0
- package/direct-tools.ts +408 -0
- package/dist/abort.d.ts +2 -0
- package/dist/abort.js +35 -0
- package/dist/agent-dir.d.ts +3 -0
- package/dist/agent-dir.js +21 -0
- package/dist/app-bridge.bundle.js +67 -0
- package/dist/checkpoint.d.ts +18 -0
- package/dist/checkpoint.js +53 -0
- package/dist/commands.d.ts +20 -0
- package/dist/commands.js +521 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +1013 -0
- package/dist/consent-manager.d.ts +8 -0
- package/dist/consent-manager.js +21 -0
- package/dist/direct-tools.d.ts +10 -0
- package/dist/direct-tools.js +350 -0
- package/dist/docs/client-metadata.json +14 -0
- package/dist/elicitation-handler.d.ts +17 -0
- package/dist/elicitation-handler.js +308 -0
- package/dist/error-signal.d.ts +15 -0
- package/dist/error-signal.js +21 -0
- package/dist/glimpse-ui.d.ts +9 -0
- package/dist/glimpse-ui.js +75 -0
- package/dist/host-html-template.d.ts +15 -0
- package/dist/host-html-template.js +429 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +840 -0
- package/dist/init.d.ts +25 -0
- package/dist/init.js +562 -0
- package/dist/legacy-migration.d.ts +18 -0
- package/dist/legacy-migration.js +45 -0
- package/dist/lifecycle.d.ts +40 -0
- package/dist/lifecycle.js +167 -0
- package/dist/logger.d.ts +38 -0
- package/dist/logger.js +102 -0
- package/dist/mcp-auth-flow.d.ts +113 -0
- package/dist/mcp-auth-flow.js +834 -0
- package/dist/mcp-auth.d.ts +127 -0
- package/dist/mcp-auth.js +658 -0
- package/dist/mcp-callback-server.d.ts +56 -0
- package/dist/mcp-callback-server.js +371 -0
- package/dist/mcp-code.d.ts +51 -0
- package/dist/mcp-code.js +366 -0
- package/dist/mcp-keyring-helper.cjs +89 -0
- package/dist/mcp-oauth-provider.d.ts +154 -0
- package/dist/mcp-oauth-provider.js +672 -0
- package/dist/mcp-output-guard.d.ts +59 -0
- package/dist/mcp-output-guard.js +332 -0
- package/dist/mcp-panel.d.ts +66 -0
- package/dist/mcp-panel.js +889 -0
- package/dist/mcp-script-worker.mjs +128 -0
- package/dist/mcp-setup-panel.d.ts +84 -0
- package/dist/mcp-setup-panel.js +561 -0
- package/dist/mcp-status.d.ts +11 -0
- package/dist/mcp-status.js +96 -0
- package/dist/mcp-trace.d.ts +95 -0
- package/dist/mcp-trace.js +233 -0
- package/dist/metadata-cache.d.ts +18 -0
- package/dist/metadata-cache.js +277 -0
- package/dist/npx-resolver.d.ts +6 -0
- package/dist/npx-resolver.js +448 -0
- package/dist/onboarding-state.d.ts +11 -0
- package/dist/onboarding-state.js +47 -0
- package/dist/panel-keys.d.ts +16 -0
- package/dist/panel-keys.js +15 -0
- package/dist/prompts.d.ts +56 -0
- package/dist/prompts.js +284 -0
- package/dist/proxy-modes.d.ts +38 -0
- package/dist/proxy-modes.js +1235 -0
- package/dist/resource-tools.d.ts +1 -0
- package/dist/resource-tools.js +14 -0
- package/dist/runtime-owner.d.ts +18 -0
- package/dist/runtime-owner.js +117 -0
- package/dist/sampling-handler.d.ts +15 -0
- package/dist/sampling-handler.js +211 -0
- package/dist/search-ranking.d.ts +20 -0
- package/dist/search-ranking.js +180 -0
- package/dist/server-manager.d.ts +114 -0
- package/dist/server-manager.js +1175 -0
- package/dist/session-recovery.d.ts +31 -0
- package/dist/session-recovery.js +310 -0
- package/dist/state.d.ts +63 -0
- package/dist/state.js +1 -0
- package/dist/tool-approval.d.ts +10 -0
- package/dist/tool-approval.js +42 -0
- package/dist/tool-metadata.d.ts +9 -0
- package/dist/tool-metadata.js +193 -0
- package/dist/tool-registrar.d.ts +10 -0
- package/dist/tool-registrar.js +64 -0
- package/dist/tool-result-renderer.d.ts +45 -0
- package/dist/tool-result-renderer.js +224 -0
- package/dist/ts-shape.d.ts +2 -0
- package/dist/ts-shape.js +147 -0
- package/dist/types.d.ts +516 -0
- package/dist/types.js +168 -0
- package/dist/ui-resource-handler.d.ts +17 -0
- package/dist/ui-resource-handler.js +209 -0
- package/dist/ui-server.d.ts +51 -0
- package/dist/ui-server.js +669 -0
- package/dist/ui-session.d.ts +39 -0
- package/dist/ui-session.js +463 -0
- package/dist/ui-stream-types.d.ts +104 -0
- package/dist/ui-stream-types.js +58 -0
- package/dist/ui-tool-visibility.d.ts +4 -0
- package/dist/ui-tool-visibility.js +26 -0
- package/dist/unix-socket-transport.d.ts +15 -0
- package/dist/unix-socket-transport.js +84 -0
- package/dist/utils.d.ts +30 -0
- package/dist/utils.js +276 -0
- package/docs/client-metadata.json +14 -0
- package/elicitation-handler.ts +349 -0
- package/error-signal.ts +21 -0
- package/glimpse-ui.ts +80 -0
- package/host-html-template.ts +456 -0
- package/index.ts +900 -0
- package/init.ts +627 -0
- package/legacy-migration.ts +47 -0
- package/lifecycle.ts +165 -0
- package/logger.ts +127 -0
- package/mcp-auth-flow.ts +975 -0
- package/mcp-auth.ts +793 -0
- package/mcp-callback-server.ts +450 -0
- package/mcp-code.ts +386 -0
- package/mcp-keyring-helper.cjs +89 -0
- package/mcp-oauth-provider.ts +774 -0
- package/mcp-output-guard.ts +412 -0
- package/mcp-panel.ts +978 -0
- package/mcp-script-worker.mjs +128 -0
- package/mcp-setup-panel.ts +666 -0
- package/mcp-status.ts +109 -0
- package/mcp-trace.ts +295 -0
- package/metadata-cache.ts +333 -0
- package/npx-resolver.ts +488 -0
- package/onboarding-state.ts +58 -0
- package/package.json +182 -0
- package/panel-keys.ts +37 -0
- package/prompts.ts +337 -0
- package/proxy-modes.ts +1361 -0
- package/resource-tools.ts +17 -0
- package/runtime-owner.ts +123 -0
- package/sampling-handler.ts +273 -0
- package/scripts/build.mjs +131 -0
- package/scripts/prepare.mjs +102 -0
- package/search-ranking.ts +196 -0
- package/server-manager.ts +1320 -0
- package/session-recovery.ts +316 -0
- package/skills/mcp-scripting/SKILL.md +38 -0
- package/state.ts +64 -0
- package/tool-approval.ts +78 -0
- package/tool-metadata.ts +235 -0
- package/tool-registrar.ts +69 -0
- package/tool-result-renderer.ts +286 -0
- package/ts-shape.ts +145 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +16 -0
- package/types.ts +810 -0
- package/ui-resource-handler.ts +249 -0
- package/ui-server.ts +808 -0
- package/ui-session.ts +568 -0
- package/ui-stream-types.ts +89 -0
- package/ui-tool-visibility.ts +25 -0
- package/unix-socket-transport.ts +85 -0
- package/utils.ts +309 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CallToolResult } from "@modelcontextprotocol/client";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
|
+
import { type UiStreamMode } from "./types.ts";
|
|
4
|
+
import type { SessionRecoveryDeps } from "./session-recovery.ts";
|
|
5
|
+
export interface UiSessionRequest {
|
|
6
|
+
serverName: string;
|
|
7
|
+
toolName: string;
|
|
8
|
+
toolArgs: Record<string, unknown>;
|
|
9
|
+
uiResourceUri: string;
|
|
10
|
+
streamMode?: UiStreamMode;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
onNeedsAuth?: SessionRecoveryDeps["onNeedsAuth"];
|
|
13
|
+
}
|
|
14
|
+
export type UiSessionViewer = "browser" | "glimpse" | "suppressed";
|
|
15
|
+
export interface UiSessionRuntime {
|
|
16
|
+
serverName: string;
|
|
17
|
+
toolName: string;
|
|
18
|
+
reused: boolean;
|
|
19
|
+
streamId?: string;
|
|
20
|
+
streamToken?: string;
|
|
21
|
+
streamMode?: UiStreamMode;
|
|
22
|
+
requestMeta?: Record<string, unknown>;
|
|
23
|
+
url: string;
|
|
24
|
+
viewer: UiSessionViewer;
|
|
25
|
+
windowOpen: boolean;
|
|
26
|
+
isActive: () => boolean;
|
|
27
|
+
sendToolResult: (result: CallToolResult) => void;
|
|
28
|
+
sendResultPatch: (result: CallToolResult) => void;
|
|
29
|
+
sendToolCancelled: (reason: string) => void;
|
|
30
|
+
close: (reason?: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface UiSessionResultSummary {
|
|
33
|
+
message: string;
|
|
34
|
+
uiOpen: boolean;
|
|
35
|
+
uiViewer?: UiSessionViewer;
|
|
36
|
+
uiUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function summarizeUiSessionResult(uiSession: UiSessionRuntime | null): UiSessionResultSummary;
|
|
39
|
+
export declare function maybeStartUiSession(state: McpExtensionState, request: UiSessionRequest): Promise<UiSessionRuntime | null>;
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
5
|
+
import { createUiModelContextUpdate, extractUiPromptText, UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_STRUCTURED_CONTENT_KEY, } from "./types.js";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
import { startUiServer } from "./ui-server.js";
|
|
8
|
+
import { isGlimpseAvailable, openGlimpseWindow } from "./glimpse-ui.js";
|
|
9
|
+
import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
|
|
10
|
+
import { throwIfAborted } from "./abort.js";
|
|
11
|
+
let activeGlimpseWindow = null;
|
|
12
|
+
export function summarizeUiSessionResult(uiSession) {
|
|
13
|
+
if (!uiSession) {
|
|
14
|
+
return {
|
|
15
|
+
message: "Interactive UI was unavailable; returning the tool result inline.",
|
|
16
|
+
uiOpen: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (!uiSession.windowOpen) {
|
|
20
|
+
const action = uiSession.reused ? "Updated the suppressed MCP UI session." : "MCP UI window was suppressed.";
|
|
21
|
+
return {
|
|
22
|
+
message: `${action} Open manually: ${uiSession.url}`,
|
|
23
|
+
uiOpen: false,
|
|
24
|
+
uiViewer: uiSession.viewer,
|
|
25
|
+
uiUrl: uiSession.url,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
message: uiSession.reused
|
|
30
|
+
? "Updated the open UI."
|
|
31
|
+
: "Interactive UI is open. I'll respond to your prompts and intents as you interact with it.",
|
|
32
|
+
uiOpen: true,
|
|
33
|
+
uiViewer: uiSession.viewer,
|
|
34
|
+
uiUrl: uiSession.url,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const MAX_COMPLETED_SESSIONS = 10;
|
|
38
|
+
function withStreamEnvelope(result, streamId, sequence) {
|
|
39
|
+
if (!streamId) {
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
const structuredContent = result.structuredContent && typeof result.structuredContent === "object" && !Array.isArray(result.structuredContent)
|
|
43
|
+
? { ...result.structuredContent }
|
|
44
|
+
: {};
|
|
45
|
+
const rawEnvelope = structuredContent[UI_STREAM_STRUCTURED_CONTENT_KEY];
|
|
46
|
+
const envelope = rawEnvelope && typeof rawEnvelope === "object" && !Array.isArray(rawEnvelope)
|
|
47
|
+
? { ...rawEnvelope }
|
|
48
|
+
: {
|
|
49
|
+
frameType: "final",
|
|
50
|
+
phase: "settled",
|
|
51
|
+
status: result.isError ? "error" : "ok",
|
|
52
|
+
};
|
|
53
|
+
structuredContent[UI_STREAM_STRUCTURED_CONTENT_KEY] = {
|
|
54
|
+
...envelope,
|
|
55
|
+
streamId,
|
|
56
|
+
sequence,
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
...result,
|
|
60
|
+
structuredContent,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function openInBrowser(state, url, signal) {
|
|
64
|
+
throwIfAborted(signal);
|
|
65
|
+
try {
|
|
66
|
+
await state.openBrowser(url);
|
|
67
|
+
throwIfAborted(signal);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (isAbortError(error, signal))
|
|
72
|
+
throw error;
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
if (state.owner?.isActive() === false)
|
|
75
|
+
return null;
|
|
76
|
+
state.ui?.notify(`MCP UI browser open failed: ${message}`, "warning");
|
|
77
|
+
return message;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isRemoteSession() {
|
|
81
|
+
return Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY);
|
|
82
|
+
}
|
|
83
|
+
function hasActiveRemoteLogin() {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
execFile("who", { encoding: "utf8", timeout: 2000 }, (error, out) => {
|
|
86
|
+
if (error) {
|
|
87
|
+
resolve(false);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
resolve(/\(.+\)\s*$/m.test(out));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function probeMoshiGateway() {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const socket = net.connect({ host: "127.0.0.1", port: 24543 });
|
|
97
|
+
const done = (ok) => {
|
|
98
|
+
socket.destroy();
|
|
99
|
+
resolve(ok);
|
|
100
|
+
};
|
|
101
|
+
socket.setTimeout(300);
|
|
102
|
+
socket.once("connect", () => done(true));
|
|
103
|
+
socket.once("timeout", () => done(false));
|
|
104
|
+
socket.once("error", () => done(false));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function remoteAccessHint(opts) {
|
|
108
|
+
const lines = [
|
|
109
|
+
opts.openError !== null
|
|
110
|
+
? "Couldn't open MCP UI here. Open it from your own device:"
|
|
111
|
+
: opts.openedOnHost
|
|
112
|
+
? "MCP UI opened on this host. If you're controlling this session remotely, open it from your own device:"
|
|
113
|
+
: "This looks like a remote session - if no MCP UI appeared, open it from your own device:",
|
|
114
|
+
` ${opts.url}`,
|
|
115
|
+
];
|
|
116
|
+
if (opts.openError !== null) {
|
|
117
|
+
lines.push(`Browser launch failed: ${opts.openError}`);
|
|
118
|
+
}
|
|
119
|
+
if (opts.moshi) {
|
|
120
|
+
lines.push("Moshi: tap the preview button in the terminal title bar and pick this MCP UI server.");
|
|
121
|
+
}
|
|
122
|
+
lines.push(`SSH: run \`ssh -L ${opts.port}:127.0.0.1:${opts.port} <this-host>\` on your local machine, then open the URL above.`, "mosh can't forward ports - run that ssh command in a separate terminal.");
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
export async function maybeStartUiSession(state, request) {
|
|
126
|
+
const log = logger.child({
|
|
127
|
+
component: "UiSession",
|
|
128
|
+
server: request.serverName,
|
|
129
|
+
tool: request.toolName,
|
|
130
|
+
});
|
|
131
|
+
const runtimeSignal = combineAbortSignals(state.owner?.signal, request.signal) ?? new AbortController().signal;
|
|
132
|
+
try {
|
|
133
|
+
throwIfAborted(runtimeSignal);
|
|
134
|
+
if (state.uiServer &&
|
|
135
|
+
state.uiServer.serverName === request.serverName &&
|
|
136
|
+
state.uiServer.toolName === request.toolName) {
|
|
137
|
+
const existingHandle = state.uiServer;
|
|
138
|
+
const streamMode = request.streamMode;
|
|
139
|
+
const streamId = streamMode ? randomUUID() : undefined;
|
|
140
|
+
const streamToken = streamMode ? randomUUID() : undefined;
|
|
141
|
+
let active = true;
|
|
142
|
+
let nextStreamSequence = 0;
|
|
143
|
+
const cleanupStreamListener = () => {
|
|
144
|
+
if (streamToken) {
|
|
145
|
+
state.manager.removeUiStreamListener(streamToken);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
existingHandle.sendToolInput(request.toolArgs);
|
|
149
|
+
if (streamToken) {
|
|
150
|
+
state.manager.registerUiStreamListener(streamToken, (serverName, notification) => {
|
|
151
|
+
if (!active || state.uiServer !== existingHandle)
|
|
152
|
+
return;
|
|
153
|
+
if (serverName !== request.serverName)
|
|
154
|
+
return;
|
|
155
|
+
nextStreamSequence += 1;
|
|
156
|
+
existingHandle.sendResultPatch(withStreamEnvelope(notification.result, streamId, nextStreamSequence));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
serverName: request.serverName,
|
|
161
|
+
toolName: request.toolName,
|
|
162
|
+
reused: true,
|
|
163
|
+
...(streamId !== undefined ? { streamId } : {}),
|
|
164
|
+
...(streamToken !== undefined ? { streamToken } : {}),
|
|
165
|
+
...(streamMode !== undefined ? { streamMode } : {}),
|
|
166
|
+
...(streamToken ? { requestMeta: { [UI_STREAM_REQUEST_META_KEY]: streamToken } } : {}),
|
|
167
|
+
url: existingHandle.url,
|
|
168
|
+
viewer: existingHandle.viewer ?? "browser",
|
|
169
|
+
windowOpen: existingHandle.windowOpen ?? true,
|
|
170
|
+
isActive: () => active && state.uiServer === existingHandle,
|
|
171
|
+
sendToolResult: (result) => {
|
|
172
|
+
if (!active || state.uiServer !== existingHandle)
|
|
173
|
+
return;
|
|
174
|
+
nextStreamSequence += 1;
|
|
175
|
+
existingHandle.sendToolResult(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
176
|
+
},
|
|
177
|
+
sendResultPatch: (result) => {
|
|
178
|
+
if (!active || state.uiServer !== existingHandle)
|
|
179
|
+
return;
|
|
180
|
+
nextStreamSequence += 1;
|
|
181
|
+
existingHandle.sendResultPatch(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
182
|
+
},
|
|
183
|
+
sendToolCancelled: (reason) => {
|
|
184
|
+
if (!active || state.uiServer !== existingHandle)
|
|
185
|
+
return;
|
|
186
|
+
nextStreamSequence += 1;
|
|
187
|
+
existingHandle.sendToolResult(withStreamEnvelope({
|
|
188
|
+
isError: true,
|
|
189
|
+
content: [{ type: "text", text: reason }],
|
|
190
|
+
}, streamId, nextStreamSequence));
|
|
191
|
+
},
|
|
192
|
+
close: () => {
|
|
193
|
+
active = false;
|
|
194
|
+
cleanupStreamListener();
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const resource = await state.uiResourceHandler.readUiResource(request.serverName, request.uiResourceUri, {
|
|
199
|
+
config: state.config,
|
|
200
|
+
signal: runtimeSignal,
|
|
201
|
+
onNeedsAuth: request.onNeedsAuth,
|
|
202
|
+
});
|
|
203
|
+
throwIfAborted(runtimeSignal);
|
|
204
|
+
if (state.uiServer) {
|
|
205
|
+
state.uiServer.close("replaced");
|
|
206
|
+
state.uiServer = null;
|
|
207
|
+
}
|
|
208
|
+
if (activeGlimpseWindow) {
|
|
209
|
+
activeGlimpseWindow.close();
|
|
210
|
+
activeGlimpseWindow = null;
|
|
211
|
+
}
|
|
212
|
+
const streamMode = request.streamMode;
|
|
213
|
+
const streamId = streamMode ? randomUUID() : undefined;
|
|
214
|
+
const streamToken = streamMode ? randomUUID() : undefined;
|
|
215
|
+
const hostContext = streamMode && streamId
|
|
216
|
+
? {
|
|
217
|
+
[UI_STREAM_HOST_CONTEXT_KEY]: {
|
|
218
|
+
mode: streamMode,
|
|
219
|
+
streamId,
|
|
220
|
+
intermediateResultPatches: streamMode === "stream-first",
|
|
221
|
+
partialInput: false,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
: undefined;
|
|
225
|
+
let active = true;
|
|
226
|
+
let nextStreamSequence = 0;
|
|
227
|
+
let handle;
|
|
228
|
+
const cleanupStreamListener = () => {
|
|
229
|
+
if (streamToken) {
|
|
230
|
+
state.manager.removeUiStreamListener(streamToken);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
handle = await startUiServer({
|
|
234
|
+
serverName: request.serverName,
|
|
235
|
+
toolName: request.toolName,
|
|
236
|
+
toolArgs: streamMode === "stream-first" ? {} : request.toolArgs,
|
|
237
|
+
resource,
|
|
238
|
+
manager: state.manager,
|
|
239
|
+
config: state.config,
|
|
240
|
+
state,
|
|
241
|
+
...(request.onNeedsAuth ? { onNeedsAuth: request.onNeedsAuth } : {}),
|
|
242
|
+
consentManager: state.consentManager,
|
|
243
|
+
...(hostContext !== undefined ? { hostContext } : {}),
|
|
244
|
+
onMessage: (params) => {
|
|
245
|
+
const prompt = extractUiPromptText(params);
|
|
246
|
+
if (prompt) {
|
|
247
|
+
if (state.sendMessage) {
|
|
248
|
+
state.sendMessage({
|
|
249
|
+
customType: "mcp-ui-prompt",
|
|
250
|
+
content: [{ type: "text", text: `User sent prompt from ${request.serverName} UI: "${prompt}"` }],
|
|
251
|
+
display: `💬 UI Prompt: ${prompt}`,
|
|
252
|
+
details: { server: request.serverName, tool: request.toolName, prompt },
|
|
253
|
+
}, { triggerTurn: true });
|
|
254
|
+
log.debug("Triggered agent turn for UI prompt", { prompt: prompt.slice(0, 50) });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (params.type === "intent" || params.intent) {
|
|
258
|
+
const intent = params.intent ?? "";
|
|
259
|
+
const intentParams = params.params;
|
|
260
|
+
if (intent && state.sendMessage) {
|
|
261
|
+
const paramsStr = intentParams ? ` ${JSON.stringify(intentParams)}` : "";
|
|
262
|
+
state.sendMessage({
|
|
263
|
+
customType: "mcp-ui-intent",
|
|
264
|
+
content: [{ type: "text", text: `User triggered intent from ${request.serverName} UI: ${intent}${paramsStr}` }],
|
|
265
|
+
display: `🎯 UI Intent: ${intent}`,
|
|
266
|
+
details: { server: request.serverName, tool: request.toolName, intent, params: intentParams },
|
|
267
|
+
}, { triggerTurn: true });
|
|
268
|
+
log.debug("Triggered agent turn for UI intent", { intent });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (params.type === "notify" || params.message) {
|
|
272
|
+
const text = params.message ?? "";
|
|
273
|
+
if (text && state.ui) {
|
|
274
|
+
state.ui.notify(`[${request.serverName}] ${text}`, "info");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
onContextUpdate: (params) => {
|
|
279
|
+
const update = createUiModelContextUpdate(params);
|
|
280
|
+
log.debug("Model context update from UI", {
|
|
281
|
+
hasContent: !!params.content,
|
|
282
|
+
hasStructured: !!params.structuredContent,
|
|
283
|
+
hasUpdate: !!update,
|
|
284
|
+
});
|
|
285
|
+
if (update && state.sendMessage) {
|
|
286
|
+
state.sendMessage({
|
|
287
|
+
customType: "mcp-ui-context",
|
|
288
|
+
content: [{ type: "text", text: `User submitted model context from ${request.serverName} UI:\n${update.summary}` }],
|
|
289
|
+
display: "UI Context submitted",
|
|
290
|
+
details: { server: request.serverName, tool: request.toolName, context: update },
|
|
291
|
+
}, { triggerTurn: true });
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
onComplete: (reason) => {
|
|
295
|
+
active = false;
|
|
296
|
+
cleanupStreamListener();
|
|
297
|
+
if (state.uiServer === handle) {
|
|
298
|
+
const messages = handle.getSessionMessages();
|
|
299
|
+
const stream = handle.getStreamSummary();
|
|
300
|
+
const hasContent = messages.prompts.length > 0 ||
|
|
301
|
+
messages.intents.length > 0 ||
|
|
302
|
+
messages.notifications.length > 0 ||
|
|
303
|
+
messages.contexts.length > 0 ||
|
|
304
|
+
!!stream;
|
|
305
|
+
if (hasContent) {
|
|
306
|
+
state.completedUiSessions.push({
|
|
307
|
+
serverName: handle.serverName,
|
|
308
|
+
toolName: handle.toolName,
|
|
309
|
+
completedAt: new Date(),
|
|
310
|
+
reason,
|
|
311
|
+
messages,
|
|
312
|
+
...(stream !== undefined ? { stream } : {}),
|
|
313
|
+
});
|
|
314
|
+
while (state.completedUiSessions.length > MAX_COMPLETED_SESSIONS) {
|
|
315
|
+
state.completedUiSessions.shift();
|
|
316
|
+
}
|
|
317
|
+
log.debug("Session completed", {
|
|
318
|
+
reason,
|
|
319
|
+
prompts: messages.prompts.length,
|
|
320
|
+
intents: messages.intents.length,
|
|
321
|
+
notifications: messages.notifications.length,
|
|
322
|
+
contexts: messages.contexts.length,
|
|
323
|
+
streamFrames: stream?.frames ?? 0,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
state.uiServer = null;
|
|
327
|
+
if (activeGlimpseWindow) {
|
|
328
|
+
activeGlimpseWindow.close();
|
|
329
|
+
activeGlimpseWindow = null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
if (state.owner?.isActive() === false || runtimeSignal.aborted) {
|
|
335
|
+
handle.close("runtime_owner_stopped");
|
|
336
|
+
throwIfAborted(runtimeSignal);
|
|
337
|
+
throw new Error("MCP UI session became stale before registration");
|
|
338
|
+
}
|
|
339
|
+
if (streamToken) {
|
|
340
|
+
state.manager.registerUiStreamListener(streamToken, (serverName, notification) => {
|
|
341
|
+
if (!active || state.uiServer !== handle)
|
|
342
|
+
return;
|
|
343
|
+
if (serverName !== request.serverName)
|
|
344
|
+
return;
|
|
345
|
+
nextStreamSequence += 1;
|
|
346
|
+
handle.sendResultPatch(withStreamEnvelope(notification.result, streamId, nextStreamSequence));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
state.uiServer = handle;
|
|
350
|
+
const viewerPref = process.env.MCP_UI_VIEWER?.toLowerCase();
|
|
351
|
+
const uiSuppressed = viewerPref === "none" || viewerPref === "off" || viewerPref === "disabled";
|
|
352
|
+
let viewer = "browser";
|
|
353
|
+
let windowOpen = true;
|
|
354
|
+
const remoteByEnv = isRemoteSession();
|
|
355
|
+
if (uiSuppressed) {
|
|
356
|
+
viewer = "suppressed";
|
|
357
|
+
windowOpen = false;
|
|
358
|
+
state.ui?.notify(`MCP UI window suppressed (MCP_UI_VIEWER=${viewerPref}). Open manually: ${handle.url}`, "info");
|
|
359
|
+
log.info("Suppressing MCP UI window (MCP_UI_VIEWER=" + viewerPref + ")", { url: handle.url });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
const remoteLikely = remoteByEnv || await hasActiveRemoteLogin();
|
|
363
|
+
const emitRemoteHint = async (openError, openedOnHost = false) => {
|
|
364
|
+
state.ui?.notify(remoteAccessHint({
|
|
365
|
+
url: handle.url,
|
|
366
|
+
port: handle.port,
|
|
367
|
+
moshi: await probeMoshiGateway(),
|
|
368
|
+
openError,
|
|
369
|
+
openedOnHost,
|
|
370
|
+
}), openError === null ? "info" : "warning");
|
|
371
|
+
};
|
|
372
|
+
const glimpseDetected = !remoteByEnv && isGlimpseAvailable();
|
|
373
|
+
const useGlimpse = !remoteByEnv && (viewerPref === "glimpse" ||
|
|
374
|
+
(viewerPref !== "browser" && glimpseDetected));
|
|
375
|
+
if (useGlimpse) {
|
|
376
|
+
try {
|
|
377
|
+
const glimpseHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;padding:0;width:100vw;height:100vh;overflow:hidden}iframe{width:100%;height:100%;border:none}</style></head><body><iframe src="${handle.url}"></iframe></body></html>`;
|
|
378
|
+
const glimpseWindow = await openGlimpseWindow(glimpseHtml, {
|
|
379
|
+
title: `MCP · ${request.serverName} · ${request.toolName}`,
|
|
380
|
+
width: 1000,
|
|
381
|
+
height: 800,
|
|
382
|
+
onClosed: () => {
|
|
383
|
+
if (active)
|
|
384
|
+
handle.close("glimpse-closed");
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
if (state.owner?.isActive() === false || runtimeSignal.aborted) {
|
|
388
|
+
glimpseWindow.close();
|
|
389
|
+
throwIfAborted(runtimeSignal);
|
|
390
|
+
throw new Error("MCP Glimpse window became stale before registration");
|
|
391
|
+
}
|
|
392
|
+
activeGlimpseWindow = glimpseWindow;
|
|
393
|
+
viewer = "glimpse";
|
|
394
|
+
if (remoteLikely) {
|
|
395
|
+
await emitRemoteHint(null, true);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
log.debug("Glimpse unavailable, using browser", {
|
|
400
|
+
error: error instanceof Error ? error.message : String(error),
|
|
401
|
+
});
|
|
402
|
+
const openError = await openInBrowser(state, handle.url, runtimeSignal);
|
|
403
|
+
if (openError !== null || remoteLikely) {
|
|
404
|
+
await emitRemoteHint(openError);
|
|
405
|
+
}
|
|
406
|
+
viewer = "browser";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const openError = await openInBrowser(state, handle.url, runtimeSignal);
|
|
411
|
+
if (openError !== null || remoteLikely) {
|
|
412
|
+
await emitRemoteHint(openError);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
throwIfAborted(runtimeSignal);
|
|
417
|
+
handle.viewer = viewer;
|
|
418
|
+
handle.windowOpen = windowOpen;
|
|
419
|
+
return {
|
|
420
|
+
serverName: request.serverName,
|
|
421
|
+
toolName: request.toolName,
|
|
422
|
+
reused: false,
|
|
423
|
+
...(streamId !== undefined ? { streamId } : {}),
|
|
424
|
+
...(streamToken !== undefined ? { streamToken } : {}),
|
|
425
|
+
...(streamMode !== undefined ? { streamMode } : {}),
|
|
426
|
+
...(streamToken ? { requestMeta: { [UI_STREAM_REQUEST_META_KEY]: streamToken } } : {}),
|
|
427
|
+
url: handle.url,
|
|
428
|
+
viewer,
|
|
429
|
+
windowOpen,
|
|
430
|
+
isActive: () => active && state.uiServer === handle,
|
|
431
|
+
sendToolResult: (result) => {
|
|
432
|
+
if (!active || state.uiServer !== handle)
|
|
433
|
+
return;
|
|
434
|
+
nextStreamSequence += 1;
|
|
435
|
+
handle.sendToolResult(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
436
|
+
},
|
|
437
|
+
sendResultPatch: (result) => {
|
|
438
|
+
if (!active || state.uiServer !== handle)
|
|
439
|
+
return;
|
|
440
|
+
nextStreamSequence += 1;
|
|
441
|
+
handle.sendResultPatch(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
442
|
+
},
|
|
443
|
+
sendToolCancelled: (reason) => {
|
|
444
|
+
if (!active || state.uiServer !== handle)
|
|
445
|
+
return;
|
|
446
|
+
handle.sendToolCancelled(reason);
|
|
447
|
+
},
|
|
448
|
+
close: (reason) => {
|
|
449
|
+
active = false;
|
|
450
|
+
cleanupStreamListener();
|
|
451
|
+
handle.close(reason);
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (error instanceof UrlElicitationRequiredError || isAbortError(error, runtimeSignal))
|
|
457
|
+
throw error;
|
|
458
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
459
|
+
log.error("Failed to start UI session", error instanceof Error ? error : undefined);
|
|
460
|
+
state.ui?.notify(`MCP UI unavailable for ${request.toolName} (${request.serverName}): ${message}`, "warning");
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const UI_STREAM_HOST_CONTEXT_KEY = "pi-mcp-adapter/stream";
|
|
3
|
+
export declare const UI_STREAM_REQUEST_META_KEY = "pi-mcp-adapter/stream-token";
|
|
4
|
+
export declare const UI_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/ui-result-patch";
|
|
5
|
+
export declare const SERVER_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/result-patch";
|
|
6
|
+
export declare const UI_STREAM_STRUCTURED_CONTENT_KEY = "pi-mcp-adapter/stream";
|
|
7
|
+
export declare const uiStreamModeSchema: z.ZodEnum<{
|
|
8
|
+
eager: "eager";
|
|
9
|
+
"stream-first": "stream-first";
|
|
10
|
+
}>;
|
|
11
|
+
export type UiStreamMode = z.infer<typeof uiStreamModeSchema>;
|
|
12
|
+
export declare const visualizationStreamPhaseSchema: z.ZodEnum<{
|
|
13
|
+
shell: "shell";
|
|
14
|
+
narrative: "narrative";
|
|
15
|
+
structure: "structure";
|
|
16
|
+
detail: "detail";
|
|
17
|
+
settled: "settled";
|
|
18
|
+
}>;
|
|
19
|
+
export type VisualizationStreamPhase = z.infer<typeof visualizationStreamPhaseSchema>;
|
|
20
|
+
export declare const visualizationStreamFrameTypeSchema: z.ZodEnum<{
|
|
21
|
+
patch: "patch";
|
|
22
|
+
checkpoint: "checkpoint";
|
|
23
|
+
final: "final";
|
|
24
|
+
}>;
|
|
25
|
+
export type VisualizationStreamFrameType = z.infer<typeof visualizationStreamFrameTypeSchema>;
|
|
26
|
+
export declare const visualizationStreamStatusSchema: z.ZodEnum<{
|
|
27
|
+
error: "error";
|
|
28
|
+
ok: "ok";
|
|
29
|
+
}>;
|
|
30
|
+
export type VisualizationStreamStatus = z.infer<typeof visualizationStreamStatusSchema>;
|
|
31
|
+
export declare const uiStreamHostContextSchema: z.ZodObject<{
|
|
32
|
+
mode: z.ZodEnum<{
|
|
33
|
+
eager: "eager";
|
|
34
|
+
"stream-first": "stream-first";
|
|
35
|
+
}>;
|
|
36
|
+
streamId: z.ZodString;
|
|
37
|
+
intermediateResultPatches: z.ZodBoolean;
|
|
38
|
+
partialInput: z.ZodBoolean;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
export type UiStreamHostContext = z.infer<typeof uiStreamHostContextSchema>;
|
|
41
|
+
export declare const visualizationStreamEnvelopeSchema: z.ZodObject<{
|
|
42
|
+
streamId: z.ZodString;
|
|
43
|
+
sequence: z.ZodNumber;
|
|
44
|
+
frameType: z.ZodEnum<{
|
|
45
|
+
patch: "patch";
|
|
46
|
+
checkpoint: "checkpoint";
|
|
47
|
+
final: "final";
|
|
48
|
+
}>;
|
|
49
|
+
phase: z.ZodEnum<{
|
|
50
|
+
shell: "shell";
|
|
51
|
+
narrative: "narrative";
|
|
52
|
+
structure: "structure";
|
|
53
|
+
detail: "detail";
|
|
54
|
+
settled: "settled";
|
|
55
|
+
}>;
|
|
56
|
+
status: z.ZodEnum<{
|
|
57
|
+
error: "error";
|
|
58
|
+
ok: "ok";
|
|
59
|
+
}>;
|
|
60
|
+
message: z.ZodOptional<z.ZodString>;
|
|
61
|
+
spec: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
62
|
+
checkpoint: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
export type VisualizationStreamEnvelope = z.infer<typeof visualizationStreamEnvelopeSchema>;
|
|
65
|
+
export declare const uiStreamCallToolResultSchema: z.ZodObject<{
|
|
66
|
+
content: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
67
|
+
structuredContent: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
68
|
+
isError: z.ZodOptional<z.ZodBoolean>;
|
|
69
|
+
_meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
70
|
+
}, z.core.$loose>;
|
|
71
|
+
export type UiStreamCallToolResult = z.infer<typeof uiStreamCallToolResultSchema>;
|
|
72
|
+
export declare const uiStreamResultPatchNotificationSchema: z.ZodObject<{
|
|
73
|
+
method: z.ZodLiteral<"notifications/pi-mcp-adapter/ui-result-patch">;
|
|
74
|
+
params: z.ZodObject<{
|
|
75
|
+
content: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
76
|
+
structuredContent: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
77
|
+
isError: z.ZodOptional<z.ZodBoolean>;
|
|
78
|
+
_meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
79
|
+
}, z.core.$loose>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
export type UiStreamResultPatchNotification = z.infer<typeof uiStreamResultPatchNotificationSchema>;
|
|
82
|
+
export declare const serverStreamResultPatchNotificationSchema: z.ZodObject<{
|
|
83
|
+
method: z.ZodLiteral<"notifications/pi-mcp-adapter/result-patch">;
|
|
84
|
+
params: z.ZodObject<{
|
|
85
|
+
streamToken: z.ZodString;
|
|
86
|
+
result: z.ZodObject<{
|
|
87
|
+
content: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
88
|
+
structuredContent: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
89
|
+
isError: z.ZodOptional<z.ZodBoolean>;
|
|
90
|
+
_meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
91
|
+
}, z.core.$loose>;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
}, z.core.$strip>;
|
|
94
|
+
export type ServerStreamResultPatchNotification = z.infer<typeof serverStreamResultPatchNotificationSchema>;
|
|
95
|
+
export interface UiStreamSummary {
|
|
96
|
+
streamId: string;
|
|
97
|
+
mode: UiStreamMode;
|
|
98
|
+
frames: number;
|
|
99
|
+
phases: VisualizationStreamPhase[];
|
|
100
|
+
finalStatus?: VisualizationStreamStatus;
|
|
101
|
+
lastMessage?: string;
|
|
102
|
+
}
|
|
103
|
+
export declare function getUiStreamHostContext(hostContext: Record<string, unknown> | undefined): UiStreamHostContext | undefined;
|
|
104
|
+
export declare function getVisualizationStreamEnvelope(structuredContent: unknown): VisualizationStreamEnvelope | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const UI_STREAM_HOST_CONTEXT_KEY = "pi-mcp-adapter/stream";
|
|
3
|
+
export const UI_STREAM_REQUEST_META_KEY = "pi-mcp-adapter/stream-token";
|
|
4
|
+
export const UI_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/ui-result-patch";
|
|
5
|
+
export const SERVER_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/result-patch";
|
|
6
|
+
export const UI_STREAM_STRUCTURED_CONTENT_KEY = "pi-mcp-adapter/stream";
|
|
7
|
+
export const uiStreamModeSchema = z.enum(["eager", "stream-first"]);
|
|
8
|
+
export const visualizationStreamPhaseSchema = z.enum(["shell", "narrative", "structure", "detail", "settled"]);
|
|
9
|
+
export const visualizationStreamFrameTypeSchema = z.enum(["patch", "checkpoint", "final"]);
|
|
10
|
+
export const visualizationStreamStatusSchema = z.enum(["ok", "error"]);
|
|
11
|
+
const looseRecordSchema = z.record(z.string(), z.unknown());
|
|
12
|
+
const looseArraySchema = z.array(z.unknown());
|
|
13
|
+
export const uiStreamHostContextSchema = z.object({
|
|
14
|
+
mode: uiStreamModeSchema,
|
|
15
|
+
streamId: z.string().min(1),
|
|
16
|
+
intermediateResultPatches: z.boolean(),
|
|
17
|
+
partialInput: z.boolean(),
|
|
18
|
+
});
|
|
19
|
+
export const visualizationStreamEnvelopeSchema = z.object({
|
|
20
|
+
streamId: z.string().min(1),
|
|
21
|
+
sequence: z.number().int().nonnegative(),
|
|
22
|
+
frameType: visualizationStreamFrameTypeSchema,
|
|
23
|
+
phase: visualizationStreamPhaseSchema,
|
|
24
|
+
status: visualizationStreamStatusSchema,
|
|
25
|
+
message: z.string().optional(),
|
|
26
|
+
spec: looseRecordSchema.optional(),
|
|
27
|
+
checkpoint: looseRecordSchema.optional(),
|
|
28
|
+
});
|
|
29
|
+
export const uiStreamCallToolResultSchema = z.object({
|
|
30
|
+
content: looseArraySchema.optional(),
|
|
31
|
+
structuredContent: looseRecordSchema.optional(),
|
|
32
|
+
isError: z.boolean().optional(),
|
|
33
|
+
_meta: looseRecordSchema.optional(),
|
|
34
|
+
}).passthrough();
|
|
35
|
+
export const uiStreamResultPatchNotificationSchema = z.object({
|
|
36
|
+
method: z.literal(UI_STREAM_RESULT_PATCH_METHOD),
|
|
37
|
+
params: uiStreamCallToolResultSchema,
|
|
38
|
+
});
|
|
39
|
+
export const serverStreamResultPatchNotificationSchema = z.object({
|
|
40
|
+
method: z.literal(SERVER_STREAM_RESULT_PATCH_METHOD),
|
|
41
|
+
params: z.object({
|
|
42
|
+
streamToken: z.string().min(1),
|
|
43
|
+
result: uiStreamCallToolResultSchema,
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
export function getUiStreamHostContext(hostContext) {
|
|
47
|
+
const candidate = hostContext?.[UI_STREAM_HOST_CONTEXT_KEY];
|
|
48
|
+
const parsed = uiStreamHostContextSchema.safeParse(candidate);
|
|
49
|
+
return parsed.success ? parsed.data : undefined;
|
|
50
|
+
}
|
|
51
|
+
export function getVisualizationStreamEnvelope(structuredContent) {
|
|
52
|
+
if (!structuredContent || typeof structuredContent !== "object" || Array.isArray(structuredContent)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const candidate = structuredContent[UI_STREAM_STRUCTURED_CONTENT_KEY];
|
|
56
|
+
const parsed = visualizationStreamEnvelopeSchema.safeParse(candidate);
|
|
57
|
+
return parsed.success ? parsed.data : undefined;
|
|
58
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type UiToolVisibility = "model" | "app";
|
|
2
|
+
export declare function extractUiToolVisibility(meta: Record<string, unknown> | undefined): UiToolVisibility[] | undefined;
|
|
3
|
+
export declare function isUiToolVisibleToModel(visibility: readonly UiToolVisibility[] | undefined): boolean;
|
|
4
|
+
export declare function isUiToolCallableByApp(visibility: readonly UiToolVisibility[] | undefined): boolean;
|