pi-soly 1.9.3 → 1.11.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 (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Integration tests for MCP UI flow
3
+ *
4
+ * These tests exercise the full flow from tool call with UI resource
5
+ * through browser communication back to agent message retrieval.
6
+ */
7
+ import { describe, it, expect, vi, afterEach } from "vitest";
8
+ import http from "node:http";
9
+ import { startUiServer, type UiServerHandle } from "../ui-server.ts";
10
+ import { UiResourceHandler } from "../ui-resource-handler.ts";
11
+ import { ConsentManager } from "../consent-manager.ts";
12
+ import type { McpServerManager } from "../server-manager.ts";
13
+ import type { UiResourceContent, UiSessionMessages } from "../types.ts";
14
+
15
+ // Helper to make HTTP requests
16
+ async function request(
17
+ url: string,
18
+ options: { method?: string; body?: unknown } = {}
19
+ ): Promise<{ status: number; body: unknown }> {
20
+ return new Promise((resolve, reject) => {
21
+ const parsed = new URL(url);
22
+ const req = http.request(
23
+ {
24
+ hostname: parsed.hostname,
25
+ port: parsed.port,
26
+ path: parsed.pathname + parsed.search,
27
+ method: options.method ?? "GET",
28
+ headers: { "Content-Type": "application/json" },
29
+ },
30
+ (res) => {
31
+ const chunks: Buffer[] = [];
32
+ res.on("data", (chunk) => chunks.push(chunk));
33
+ res.on("end", () => {
34
+ const text = Buffer.concat(chunks).toString("utf-8");
35
+ let body: unknown;
36
+ try {
37
+ body = JSON.parse(text);
38
+ } catch {
39
+ body = text;
40
+ }
41
+ resolve({ status: res.statusCode ?? 0, body });
42
+ });
43
+ }
44
+ );
45
+ req.on("error", reject);
46
+ if (options.body) req.write(JSON.stringify(options.body));
47
+ req.end();
48
+ });
49
+ }
50
+
51
+ // Simulate browser behavior
52
+ class BrowserSimulator {
53
+ private serverUrl: string;
54
+ private sessionToken: string;
55
+
56
+ constructor(handle: UiServerHandle) {
57
+ this.serverUrl = `http://localhost:${handle.port}`;
58
+ this.sessionToken = handle.sessionToken;
59
+ }
60
+
61
+ async loadPage(): Promise<string> {
62
+ const res = await request(`${this.serverUrl}/?session=${this.sessionToken}`);
63
+ if (res.status !== 200) throw new Error(`Page load failed: ${res.status}`);
64
+ return res.body as string;
65
+ }
66
+
67
+ async sendPrompt(prompt: string): Promise<void> {
68
+ const res = await request(`${this.serverUrl}/proxy/ui/message`, {
69
+ method: "POST",
70
+ body: {
71
+ token: this.sessionToken,
72
+ params: { type: "prompt", prompt },
73
+ },
74
+ });
75
+ if (res.status !== 200) throw new Error(`Send prompt failed: ${res.status}`);
76
+ }
77
+
78
+ async sendIntent(intent: string, params?: Record<string, unknown>): Promise<void> {
79
+ const res = await request(`${this.serverUrl}/proxy/ui/message`, {
80
+ method: "POST",
81
+ body: {
82
+ token: this.sessionToken,
83
+ params: { type: "intent", intent, params },
84
+ },
85
+ });
86
+ if (res.status !== 200) throw new Error(`Send intent failed: ${res.status}`);
87
+ }
88
+
89
+ async sendNotification(message: string): Promise<void> {
90
+ const res = await request(`${this.serverUrl}/proxy/ui/message`, {
91
+ method: "POST",
92
+ body: {
93
+ token: this.sessionToken,
94
+ params: { type: "notify", message },
95
+ },
96
+ });
97
+ if (res.status !== 200) throw new Error(`Send notification failed: ${res.status}`);
98
+ }
99
+
100
+ async callTool(name: string, args: Record<string, unknown> = {}): Promise<unknown> {
101
+ const res = await request(`${this.serverUrl}/proxy/tools/call`, {
102
+ method: "POST",
103
+ body: {
104
+ token: this.sessionToken,
105
+ params: { name, arguments: args },
106
+ },
107
+ });
108
+ if (res.status !== 200) throw new Error(`Tool call failed: ${res.status}`);
109
+ return (res.body as { result: unknown }).result;
110
+ }
111
+
112
+ async grantConsent(): Promise<void> {
113
+ const res = await request(`${this.serverUrl}/proxy/ui/consent`, {
114
+ method: "POST",
115
+ body: {
116
+ token: this.sessionToken,
117
+ params: { approved: true },
118
+ },
119
+ });
120
+ if (res.status !== 200) throw new Error(`Consent failed: ${res.status}`);
121
+ }
122
+
123
+ async heartbeat(): Promise<void> {
124
+ const res = await request(`${this.serverUrl}/proxy/ui/heartbeat`, {
125
+ method: "POST",
126
+ body: { token: this.sessionToken, params: {} },
127
+ });
128
+ if (res.status !== 200) throw new Error(`Heartbeat failed: ${res.status}`);
129
+ }
130
+
131
+ async complete(reason = "done"): Promise<void> {
132
+ const res = await request(`${this.serverUrl}/proxy/ui/complete`, {
133
+ method: "POST",
134
+ body: {
135
+ token: this.sessionToken,
136
+ params: { reason },
137
+ },
138
+ });
139
+ if (res.status !== 200) throw new Error(`Complete failed: ${res.status}`);
140
+ }
141
+ }
142
+
143
+ // Mock manager that simulates real MCP server behavior
144
+ function createIntegrationManager(): McpServerManager {
145
+ const tools = new Map([
146
+ ["get_data", { result: { data: [1, 2, 3] } }],
147
+ ["save_file", { result: { saved: true, path: "/tmp/file.txt" } }],
148
+ ["slow_operation", { result: { completed: true }, delay: 100 }],
149
+ ]);
150
+
151
+ return {
152
+ getConnection: vi.fn().mockReturnValue({
153
+ status: "connected",
154
+ client: {
155
+ callTool: vi.fn().mockImplementation(async ({ name }) => {
156
+ const tool = tools.get(name);
157
+ if (!tool) throw new Error(`Unknown tool: ${name}`);
158
+ if ((tool as { delay?: number }).delay) {
159
+ await new Promise((r) => setTimeout(r, (tool as { delay: number }).delay));
160
+ }
161
+ return { content: [{ type: "text", text: JSON.stringify(tool.result) }] };
162
+ }),
163
+ },
164
+ }),
165
+ touch: vi.fn(),
166
+ incrementInFlight: vi.fn(),
167
+ decrementInFlight: vi.fn(),
168
+ readResource: vi.fn().mockResolvedValue({
169
+ contents: [
170
+ {
171
+ uri: "ui://test/app",
172
+ mimeType: "text/html",
173
+ text: `<!DOCTYPE html>
174
+ <html>
175
+ <head><title>Test App</title></head>
176
+ <body>
177
+ <h1>Test MCP App</h1>
178
+ <button id="getData">Get Data</button>
179
+ <button id="sendPrompt">Ask Agent</button>
180
+ </body>
181
+ </html>`,
182
+ },
183
+ ],
184
+ }),
185
+ } as unknown as McpServerManager;
186
+ }
187
+
188
+ describe("MCP UI Integration", () => {
189
+ let handle: UiServerHandle | null = null;
190
+
191
+ afterEach(() => {
192
+ if (handle) {
193
+ handle.close("test-cleanup");
194
+ handle = null;
195
+ }
196
+ });
197
+
198
+ describe("Full UI Session Flow", () => {
199
+ it("completes a full agent → browser → agent cycle", async () => {
200
+ // 1. Agent calls tool with UI
201
+ const manager = createIntegrationManager();
202
+ const consentManager = new ConsentManager("never"); // No consent prompts
203
+
204
+ const resource: UiResourceContent = {
205
+ uri: "ui://test/app",
206
+ html: "<h1>Test App</h1>",
207
+ mimeType: "text/html",
208
+ meta: { permissions: [] },
209
+ };
210
+
211
+ const receivedMessages: UiSessionMessages = { prompts: [], notifications: [], intents: [] };
212
+ const onMessage = vi.fn().mockImplementation((params) => {
213
+ if (params.type === "prompt") receivedMessages.prompts.push(params.prompt);
214
+ });
215
+
216
+ handle = await startUiServer({
217
+ serverName: "test-server",
218
+ toolName: "launch_app",
219
+ toolArgs: { mode: "interactive" },
220
+ resource,
221
+ manager,
222
+ consentManager,
223
+ onMessage,
224
+ });
225
+
226
+ // 2. Browser loads the page
227
+ const browser = new BrowserSimulator(handle);
228
+ const html = await browser.loadPage();
229
+ expect(html).toContain("test-server");
230
+ expect(html).toContain("launch_app");
231
+
232
+ // 3. Browser calls a tool through the proxy
233
+ const toolResult = await browser.callTool("get_data", { query: "test" });
234
+ expect(toolResult).toBeDefined();
235
+
236
+ // 4. Browser sends a prompt back to the agent
237
+ await browser.sendPrompt("Please analyze this data and summarize");
238
+
239
+ // 5. Agent retrieves the messages
240
+ const messages = handle.getSessionMessages();
241
+ expect(messages.prompts).toContain("Please analyze this data and summarize");
242
+
243
+ // 6. Browser completes the session
244
+ await browser.complete("user-done");
245
+ });
246
+
247
+ it("handles multiple messages in conversation", async () => {
248
+ const manager = createIntegrationManager();
249
+ const consentManager = new ConsentManager("never");
250
+
251
+ const resource: UiResourceContent = {
252
+ uri: "ui://test/chat",
253
+ html: "<div id='chat'></div>",
254
+ mimeType: "text/html",
255
+ meta: { permissions: [] },
256
+ };
257
+
258
+ handle = await startUiServer({
259
+ serverName: "chat-server",
260
+ toolName: "open_chat",
261
+ toolArgs: {},
262
+ resource,
263
+ manager,
264
+ consentManager,
265
+ });
266
+
267
+ const browser = new BrowserSimulator(handle);
268
+ await browser.loadPage();
269
+
270
+ // Simulate a multi-turn conversation
271
+ await browser.sendPrompt("What is the capital of France?");
272
+ await browser.sendNotification("User is typing...");
273
+ await browser.sendPrompt("And what about Germany?");
274
+ await browser.sendIntent("show_map", { countries: ["France", "Germany"] });
275
+
276
+ const messages = handle.getSessionMessages();
277
+ expect(messages.prompts).toHaveLength(2);
278
+ expect(messages.prompts[0]).toBe("What is the capital of France?");
279
+ expect(messages.prompts[1]).toBe("And what about Germany?");
280
+ expect(messages.notifications).toHaveLength(1);
281
+ expect(messages.intents).toHaveLength(1);
282
+ expect(messages.intents[0]).toEqual({
283
+ intent: "show_map",
284
+ params: { countries: ["France", "Germany"] },
285
+ });
286
+ });
287
+
288
+ it("handles consent flow for tool calls", async () => {
289
+ const manager = createIntegrationManager();
290
+ const consentManager = new ConsentManager("once-per-server");
291
+
292
+ const resource: UiResourceContent = {
293
+ uri: "ui://test/app",
294
+ html: "<h1>App</h1>",
295
+ mimeType: "text/html",
296
+ meta: { permissions: [] },
297
+ };
298
+
299
+ handle = await startUiServer({
300
+ serverName: "sensitive-server",
301
+ toolName: "admin_tool",
302
+ toolArgs: {},
303
+ resource,
304
+ manager,
305
+ consentManager,
306
+ });
307
+
308
+ const browser = new BrowserSimulator(handle);
309
+ await browser.loadPage();
310
+
311
+ // First tool call should fail - no consent yet
312
+ await expect(browser.callTool("get_data")).rejects.toThrow();
313
+
314
+ // Grant consent
315
+ await browser.grantConsent();
316
+
317
+ // Now tool call should succeed
318
+ const result = await browser.callTool("get_data");
319
+ expect(result).toBeDefined();
320
+ });
321
+
322
+ it("tracks in-flight requests correctly", async () => {
323
+ const manager = createIntegrationManager();
324
+ const consentManager = new ConsentManager("never");
325
+
326
+ const resource: UiResourceContent = {
327
+ uri: "ui://test/app",
328
+ html: "<h1>App</h1>",
329
+ mimeType: "text/html",
330
+ meta: { permissions: [] },
331
+ };
332
+
333
+ handle = await startUiServer({
334
+ serverName: "test-server",
335
+ toolName: "test_tool",
336
+ toolArgs: {},
337
+ resource,
338
+ manager,
339
+ consentManager,
340
+ });
341
+
342
+ const browser = new BrowserSimulator(handle);
343
+ await browser.loadPage();
344
+
345
+ // Call multiple tools
346
+ await Promise.all([
347
+ browser.callTool("get_data"),
348
+ browser.callTool("save_file"),
349
+ ]);
350
+
351
+ // Both should have incremented/decremented
352
+ expect(manager.incrementInFlight).toHaveBeenCalledTimes(2);
353
+ expect(manager.decrementInFlight).toHaveBeenCalledTimes(2);
354
+ });
355
+ });
356
+
357
+ describe("UiResourceHandler + UiServer Integration", () => {
358
+ it("reads resource and starts server with correct content", async () => {
359
+ const manager = createIntegrationManager();
360
+ const handler = new UiResourceHandler(manager);
361
+
362
+ // Read the UI resource
363
+ const resource = await handler.readUiResource("test-server", "ui://test/app");
364
+ expect(resource.html).toContain("Test MCP App");
365
+ expect(resource.mimeType).toBe("text/html");
366
+
367
+ // Start server with the resource
368
+ const consentManager = new ConsentManager("never");
369
+ handle = await startUiServer({
370
+ serverName: "test-server",
371
+ toolName: "launch_app",
372
+ toolArgs: {},
373
+ resource,
374
+ manager,
375
+ consentManager,
376
+ });
377
+
378
+ // Verify page contains the resource content
379
+ const browser = new BrowserSimulator(handle);
380
+ const html = await browser.loadPage();
381
+ // The host page wraps the resource in an iframe
382
+ expect(html).toContain("iframe");
383
+ });
384
+ });
385
+
386
+ describe("Session Lifecycle", () => {
387
+ it("calls onComplete when session ends", async () => {
388
+ const onComplete = vi.fn();
389
+ const manager = createIntegrationManager();
390
+ const consentManager = new ConsentManager("never");
391
+
392
+ handle = await startUiServer({
393
+ serverName: "test-server",
394
+ toolName: "test_tool",
395
+ toolArgs: {},
396
+ resource: {
397
+ uri: "ui://test/app",
398
+ html: "<h1>App</h1>",
399
+ mimeType: "text/html",
400
+ meta: { permissions: [] },
401
+ },
402
+ manager,
403
+ consentManager,
404
+ onComplete,
405
+ });
406
+
407
+ const browser = new BrowserSimulator(handle);
408
+ await browser.loadPage();
409
+ await browser.complete("user-finished");
410
+
411
+ // Wait for callback
412
+ await new Promise((r) => setTimeout(r, 50));
413
+ expect(onComplete).toHaveBeenCalledWith("user-finished");
414
+ });
415
+
416
+ it("maintains heartbeat to prevent timeout", async () => {
417
+ const manager = createIntegrationManager();
418
+ const consentManager = new ConsentManager("never");
419
+
420
+ handle = await startUiServer({
421
+ serverName: "test-server",
422
+ toolName: "test_tool",
423
+ toolArgs: {},
424
+ resource: {
425
+ uri: "ui://test/app",
426
+ html: "<h1>App</h1>",
427
+ mimeType: "text/html",
428
+ meta: { permissions: [] },
429
+ },
430
+ manager,
431
+ consentManager,
432
+ });
433
+
434
+ const browser = new BrowserSimulator(handle);
435
+ await browser.loadPage();
436
+
437
+ // Send heartbeats
438
+ await browser.heartbeat();
439
+ await browser.heartbeat();
440
+
441
+ // Session should still be active
442
+ const html = await browser.loadPage();
443
+ expect(html).toContain("test-server");
444
+ });
445
+ });
446
+
447
+ describe("Error Handling", () => {
448
+ it("handles MCP server errors gracefully", async () => {
449
+ const manager = {
450
+ ...createIntegrationManager(),
451
+ getConnection: vi.fn().mockReturnValue({
452
+ status: "connected",
453
+ client: {
454
+ callTool: vi.fn().mockRejectedValue(new Error("MCP server error")),
455
+ },
456
+ }),
457
+ } as unknown as McpServerManager;
458
+
459
+ const consentManager = new ConsentManager("never");
460
+
461
+ handle = await startUiServer({
462
+ serverName: "error-server",
463
+ toolName: "error_tool",
464
+ toolArgs: {},
465
+ resource: {
466
+ uri: "ui://test/app",
467
+ html: "<h1>App</h1>",
468
+ mimeType: "text/html",
469
+ meta: { permissions: [] },
470
+ },
471
+ manager,
472
+ consentManager,
473
+ });
474
+
475
+ const browser = new BrowserSimulator(handle);
476
+ await browser.loadPage();
477
+
478
+ // Tool call should fail but not crash the server
479
+ await expect(browser.callTool("failing_tool")).rejects.toThrow();
480
+
481
+ // Server should still be responsive
482
+ await browser.heartbeat();
483
+ });
484
+
485
+ it("handles disconnected server", async () => {
486
+ const manager = {
487
+ ...createIntegrationManager(),
488
+ getConnection: vi.fn().mockReturnValue(null),
489
+ } as unknown as McpServerManager;
490
+
491
+ const consentManager = new ConsentManager("never");
492
+
493
+ handle = await startUiServer({
494
+ serverName: "disconnected-server",
495
+ toolName: "test_tool",
496
+ toolArgs: {},
497
+ resource: {
498
+ uri: "ui://test/app",
499
+ html: "<h1>App</h1>",
500
+ mimeType: "text/html",
501
+ meta: { permissions: [] },
502
+ },
503
+ manager,
504
+ consentManager,
505
+ });
506
+
507
+ const browser = new BrowserSimulator(handle);
508
+ await browser.loadPage();
509
+
510
+ // Tool call should fail with 503
511
+ await expect(browser.callTool("any_tool")).rejects.toThrow("503");
512
+ });
513
+ });
514
+
515
+ describe("Display Mode Changes", () => {
516
+ it("allows switching display modes", async () => {
517
+ const manager = createIntegrationManager();
518
+ const consentManager = new ConsentManager("never");
519
+
520
+ handle = await startUiServer({
521
+ serverName: "test-server",
522
+ toolName: "test_tool",
523
+ toolArgs: {},
524
+ resource: {
525
+ uri: "ui://test/app",
526
+ html: "<h1>App</h1>",
527
+ mimeType: "text/html",
528
+ meta: { permissions: [] },
529
+ },
530
+ manager,
531
+ consentManager,
532
+ hostContext: {
533
+ displayMode: "inline",
534
+ availableDisplayModes: ["inline", "fullscreen", "pip"],
535
+ },
536
+ });
537
+
538
+ // Request fullscreen
539
+ const res = await request(`http://localhost:${handle.port}/proxy/ui/request-display-mode`, {
540
+ method: "POST",
541
+ body: {
542
+ token: handle.sessionToken,
543
+ params: { mode: "fullscreen" },
544
+ },
545
+ });
546
+
547
+ expect(res.status).toBe(200);
548
+ expect((res.body as { result: { mode: string } }).result.mode).toBe("fullscreen");
549
+ });
550
+ });
551
+ });