import { describe, expect, it, vi } from "@effect/vitest";import { Data, Effect } from "effect";import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js";import { EXTENSION_ID, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";import type * as Cause from "effect/Cause"; import { ArtifactId, FormElicitation, type Artifact, type ArtifactBindings, type ArtifactSummary,} from "@executor-js/sdk";import type { ExecutionEngine, ExecutionResult } from "@executor-js/execution"; import type { BindableConnection } from "./artifact-bindings";import { readArtifactsEnabled } from "./browser-approval";import { MCP_APPS_SHELL_RESOURCE_URI, artifactUrlFor } from "./create-artifact";import { createExecutorMcpServer, type ExecutorMcpServerConfig } from "./tool-server"; /** The caller's connection inventory, as `create-artifact` sees it when it * binds an artifact's integration roles. */const connectionsPort = (available: readonly BindableConnection[]) => ({ list: (): Effect.Effect<readonly BindableConnection[]> => Effect.succeed(available),}); const conn = (integration: string, owner: "user" | "org", name: string): BindableConnection => ({ integration, owner, name,}); // ---------------------------------------------------------------------------// Harness// --------------------------------------------------------------------------- class TestArtifactError extends Data.TaggedError("TestArtifactError")<{ readonly message: string;}> {} const makeStubEngine = <E extends Cause.YieldableError = never>(overrides: { executeWithPause?: ExecutionEngine<E>["executeWithPause"]; resume?: ExecutionEngine<E>["resume"];}): ExecutionEngine<E> => ({ execute: () => Effect.succeed({ result: "default" }), executeWithPause: overrides.executeWithPause ?? (() => Effect.succeed({ status: "completed", result: { result: "default" } })), resume: overrides.resume ?? (() => Effect.succeed(null)), isExecutionSettled: undefined, getPausedExecution: () => Effect.succeed(null), pausedExecutionCount: () => Effect.succeed(0), hasPausedExecutions: () => Effect.succeed(false), getDescription: Effect.succeed("test executor"),}); /** * An in-memory stand-in for `executor.artifacts` with the same observable * behavior the real owner-scoped store has: save mints an id (or overwrites in * place), get fails when nothing matches, list is newest-first without code. */const makeArtifactStore = () => { const rows = new Map<string, Artifact>(); let seq = 0; const calls: Array<{ title: string; description: string | null; code: string; bindings: ArtifactBindings | null; }> = []; return { calls, rows, port: { list: (): Effect.Effect<readonly ArtifactSummary[]> => Effect.sync(() => [...rows.values()] .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) .map(({ code: _code, bindings: _bindings, ...summary }) => summary), ), get: (id: string): Effect.Effect<Artifact, TestArtifactError> => Effect.suspend(() => { const row = rows.get(id); return row ? Effect.succeed(row) : Effect.fail(new TestArtifactError({ message: `no artifact ${id}` })); }), save: (input: { readonly id?: string; readonly title: string; readonly description?: string | null; readonly code: string; readonly bindings?: ArtifactBindings | null; readonly preview?: string | null; }): Effect.Effect<Artifact, TestArtifactError> => Effect.suspend(() => { calls.push({ title: input.title, description: input.description ?? null, code: input.code, bindings: input.bindings ?? null, }); const existing = input.id === undefined ? undefined : rows.get(input.id); if (input.id !== undefined && !existing) { return Effect.fail(new TestArtifactError({ message: `no artifact ${input.id}` })); } seq += 1; const artifact: Artifact = { id: ArtifactId.make(existing?.id ?? `art_${seq}`), owner: "user", title: input.title, description: input.description ?? null, // Mirrors the real save: the preview is written on every overwrite, // including back to null, because it interprets `code`. preview: input.preview === undefined || input.preview === null ? null : { kind: "layout", markup: input.preview }, code: input.code, bindings: input.bindings ?? null, createdAt: existing?.createdAt ?? new Date(seq * 1000), updatedAt: new Date(seq * 1000), }; rows.set(artifact.id, artifact); return Effect.succeed(artifact); }), }, };}; const withClient = async <E extends Cause.YieldableError>( engine: ExecutionEngine<E>, capabilities: ClientCapabilities, fn: (client: Client) => Promise<void>, config?: Partial<ExecutorMcpServerConfig<E>>,) => { const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine, loadAppShellHtml: () => Promise.resolve(SHELL_HTML), // Artifacts are on by default and nearly every test in this file // exercises the artifact surface; spelled out here anyway so the cases // that assert the OFF surface (`artifactsEnabled: false`) read as the // deliberate contrast. The default (no flag at all) is proved directly // in the opt-out describe block. artifactsEnabled: true, ...config, } as ExecutorMcpServerConfig<E>), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities }); await mcpServer.connect(serverTransport); await client.connect(clientTransport); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test helper must close MCP transports after async client assertions try { await fn(client); } finally { await clientTransport.close(); await serverTransport.close(); }}; const SHELL_HTML = "<!doctype html><html><body><div id='root'></div></body></html>"; // What a client that renders MCP Apps advertises at `initialize`. The SDK's// `ClientCapabilities` has no `extensions` field yet (pending SEP-1724), which// is exactly why ext-apps ships `getUiCapability` to read it.// oxlint-disable-next-line executor/no-double-cast -- boundary: MCP SDK ClientCapabilities predates the ext-apps `extensions` fieldconst APPS_CAPS = { extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } },} as unknown as ClientCapabilities; const NO_APPS_CAPS: ClientCapabilities = {}; const structuredOf = (result: Awaited<ReturnType<Client["callTool"]>>): Record<string, unknown> => (result.structuredContent ?? {}) as Record<string, unknown>; const textOf = (result: Awaited<ReturnType<Client["callTool"]>>): string => (result.content as Array<{ type: string; text: string }>)[0].text; const toolNames = async (client: Client): Promise<string[]> => (await client.listTools()).tools.map((tool) => tool.name); const COUNTER_CODE = "function App() { return <div>hi</div>; }";const UPDATED_CODE = "function App() { return <div>hi again</div>; }"; const makePausedResult = (id: string, message: string): ExecutionResult => ({ status: "paused", execution: { id, elicitationContext: { request: FormElicitation.make({ message, requestedSchema: {} }), }, } as ExecutionResult extends { status: "paused"; execution: infer P } ? P : never,}); // ---------------------------------------------------------------------------// Capability-gated visibility// --------------------------------------------------------------------------- describe("MCP host — artifact tool visibility", () => { it("hides the app-only tools from clients that cannot render MCP Apps", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), NO_APPS_CAPS, async (client) => { const names = await toolNames(client); // The model-facing tools are always advertised — they degrade to a // deep link rather than disappearing. expect(names).toContain("create-artifact"); expect(names).toContain("edit-artifact"); expect(names).toContain("list-artifacts"); expect(names).toContain("show-artifact"); // `execute-action` is only callable from inside a rendered app. expect(names).not.toContain("execute-action"); expect(names).not.toContain("execute-action-resume"); }, { artifacts: store.port }, ); }); it("exposes the app-only tools to clients that advertise the shell mime type", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { const names = await toolNames(client); expect(names).toContain("create-artifact"); expect(names).toContain("execute-action"); expect(names).toContain("execute-action-resume"); }, { artifacts: store.port }, ); }); // A cold restore (deploy, DO eviction) rebuilds the server mid-conversation // on the same session id, and the client — already past `initialize` — never // sends another one. Everything the rebuilt server knows about the client's // capabilities has to come from what the host persisted. it("keeps the app-only tools visible on a cold restore that replays no initialize", async () => { const store = makeArtifactStore(); const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine: makeStubEngine({}), artifacts: store.port, artifactsEnabled: true, loadAppShellHtml: () => Promise.resolve(SHELL_HTML), // What the host read back out of DO storage for this session. restoredAppsEnabled: true, }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await mcpServer.connect(serverTransport); // Deliberately NOT an SDK `Client`: `client.connect()` always sends // `initialize`, which is the one thing a cold restore never replays. The // client is mid-conversation and just keeps issuing calls against the same // mcp-session-id, so this drives raw JSON-RPC over the transport. const responses = new Map<number, Record<string, unknown>>(); clientTransport.onmessage = (message) => { const envelope = message as { id?: number; result?: Record<string, unknown> }; if (typeof envelope.id === "number" && envelope.result) { responses.set(envelope.id, envelope.result); } }; await clientTransport.start(); const call = async (id: number, method: string, params: Record<string, unknown>) => { await clientTransport.send({ jsonrpc: "2.0", id, method, params }); await vi.waitFor(() => expect(responses.has(id)).toBe(true)); return responses.get(id) ?? {}; }; const listed = (await call(1, "tools/list", {})) as { tools: Array<{ name: string }>; }; const names = listed.tools.map((tool) => tool.name); expect(names).toContain("execute-action"); expect(names).toContain("execute-action-resume"); // The render path is the symptom that took this to production: the same // session rendered inline before the restore and a deep link after. const created = (await call(2, "tools/call", { name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, })) as { structuredContent?: Record<string, unknown> }; expect(created.structuredContent).toEqual({ code: COUNTER_CODE, artifactId: "art_1" }); expect(created.structuredContent).not.toHaveProperty("status", "fallback_url"); await clientTransport.close(); await serverTransport.close(); }); // The restore path the `agents` runtime actually takes, and the one that // reached production. On cold restore it replays the PERSISTED `initialize` // REQUEST (`reinitializeServer`), which does re-establish the server's client // capabilities — but it never replays the `notifications/initialized` // NOTIFICATION, and `oninitialized` (the only hook that re-runs // `syncToolAvailability`) fires solely on that notification. So the rebuilt // server holds full apps capabilities while nothing re-reads them, and the // replay is dispatched un-awaited, so a tool call can also land ahead of it. // // Both orderings are covered here: capabilities present but no hook, and a // tool call dispatched before the replay arrives at all. it("renders inline on a restore that replays initialize without the initialized notification", async () => { const store = makeArtifactStore(); const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine: makeStubEngine({}), artifacts: store.port, artifactsEnabled: true, loadAppShellHtml: () => Promise.resolve(SHELL_HTML), // Nothing persisted: the capability must come from the replay alone, so // a passing assertion cannot be coming from the seed. restoredAppsEnabled: false, artifactUrl: artifactUrlFor("https://executor.test"), }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await mcpServer.connect(serverTransport); const responses = new Map<string | number, Record<string, unknown>>(); clientTransport.onmessage = (message) => { const envelope = message as { id?: string | number; result?: Record<string, unknown> }; if (envelope.id !== undefined && envelope.result) responses.set(envelope.id, envelope.result); }; await clientTransport.start(); // Exactly what `agents/mcp` replays: the initialize request under its // restore sentinel id, and no `notifications/initialized` after it. await clientTransport.send({ jsonrpc: "2.0", id: "__worker_transport_restore__", method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: APPS_CAPS, clientInfo: { name: "ClaudeAI", version: "1.0.0" }, }, } as never); await vi.waitFor(() => expect(mcpServer.server.getClientCapabilities()).toMatchObject(APPS_CAPS), ); const call = async (id: number, method: string, params: Record<string, unknown>) => { await clientTransport.send({ jsonrpc: "2.0", id, method, params }); await vi.waitFor(() => expect(responses.has(id)).toBe(true)); return responses.get(id) ?? {}; }; const created = (await call(1, "tools/call", { name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, })) as { structuredContent?: Record<string, unknown> }; expect(created.structuredContent).toEqual({ code: COUNTER_CODE, artifactId: "art_1", url: "https://executor.test/artifacts/art_1", }); expect(created.structuredContent).not.toHaveProperty("status", "fallback_url"); // The widget is useless if it cannot call back in, so visibility has to // reconcile on the same restore. const listed = (await call(2, "tools/list", {})) as { tools: Array<{ name: string }> }; const names = listed.tools.map((tool) => tool.name); expect(names).toContain("execute-action"); expect(names).toContain("execute-action-resume"); await clientTransport.close(); await serverTransport.close(); }); it("still falls back to a deep link when the restored session had no apps support", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), NO_APPS_CAPS, async (client) => { const result = await client.callTool({ name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, }); expect(structuredOf(result)).toEqual({ status: "fallback_url", url: "https://executor.test/artifacts/art_1", artifactId: "art_1", }); }, { artifacts: store.port, restoredAppsEnabled: false, artifactUrl: artifactUrlFor("https://executor.test"), }, ); }); // The restored value seeds the session; it must never outrank the client in // front of you. A client that reconnects without apps support gets the // fallback, restored bit or not. it("lets a real initialize overrule the restored value in both directions", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), NO_APPS_CAPS, async (client) => { const names = await toolNames(client); expect(names).not.toContain("execute-action"); const result = await client.callTool({ name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, }); expect(structuredOf(result)).toMatchObject({ status: "fallback_url" }); }, { artifacts: store.port, restoredAppsEnabled: true, artifactUrl: artifactUrlFor("https://executor.test"), }, ); }); it("reports the negotiated value to the host so the next restore can seed itself", async () => { const store = makeArtifactStore(); const seen: boolean[] = []; await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { await toolNames(client); }, { artifacts: store.port, restoredAppsEnabled: false, onAppsEnabledChange: (appsEnabled) => Effect.sync(() => void seen.push(appsEnabled)), }, ); expect(seen).toEqual([true]); }); it("does not re-persist when initialize confirms the restored value", async () => { const store = makeArtifactStore(); const seen: boolean[] = []; await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { await toolNames(client); }, { artifacts: store.port, restoredAppsEnabled: true, onAppsEnabledChange: (appsEnabled) => Effect.sync(() => void seen.push(appsEnabled)), }, ); expect(seen).toEqual([]); }); it("registers no ui tools at all when no shell loader is configured", async () => { const store = makeArtifactStore(); const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine: makeStubEngine({}), artifacts: store.port, // Opted in, so the only thing withholding the surface is the missing // shell loader — otherwise this would pass on the connection default. artifactsEnabled: true, }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} }); await mcpServer.connect(serverTransport); await client.connect(clientTransport); const names = (await client.listTools()).tools.map((tool) => tool.name); expect(names).toContain("execute"); expect(names).not.toContain("create-artifact"); expect(names).not.toContain("list-artifacts"); await clientTransport.close(); await serverTransport.close(); }); // A session that opted out (`?artifacts=false`) gets no artifact surface at // all. The host here is fully artifact-capable — shell loader, store, the // apps client capability — so what these assert is the opt-out itself, not // a host that never had artifacts. it("registers no artifact tools on an opted-out connection", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { const names = await toolNames(client); expect(names).not.toContain("create-artifact"); expect(names).not.toContain("edit-artifact"); expect(names).not.toContain("list-artifacts"); expect(names).not.toContain("show-artifact"); expect(names).not.toContain("execute-action"); expect(names).not.toContain("execute-action-resume"); // The rest of the surface is untouched.Lines 1–500 of 2555. Load more
import { describe, expect, it, vi } from "@effect/vitest"; import { Data, Effect } from "effect"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js"; import { EXTENSION_ID, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; import type * as Cause from "effect/Cause"; import { ArtifactId, FormElicitation, type Artifact, type ArtifactBindings, type ArtifactSummary, } from "@executor-js/sdk"; import type { ExecutionEngine, ExecutionResult } from "@executor-js/execution"; import type { BindableConnection } from "./artifact-bindings"; import { readArtifactsEnabled } from "./browser-approval"; import { MCP_APPS_SHELL_RESOURCE_URI, artifactUrlFor } from "./create-artifact"; import { createExecutorMcpServer, type ExecutorMcpServerConfig } from "./tool-server"; /** The caller's connection inventory, as `create-artifact` sees it when it * binds an artifact's integration roles. */ const connectionsPort = (available: readonly BindableConnection[]) => ({ list: (): Effect.Effect<readonly BindableConnection[]> => Effect.succeed(available), }); const conn = (integration: string, owner: "user" | "org", name: string): BindableConnection => ({ integration, owner, name, }); // --------------------------------------------------------------------------- // Harness // --------------------------------------------------------------------------- class TestArtifactError extends Data.TaggedError("TestArtifactError")<{ readonly message: string; }> {} const makeStubEngine = <E extends Cause.YieldableError = never>(overrides: { executeWithPause?: ExecutionEngine<E>["executeWithPause"]; resume?: ExecutionEngine<E>["resume"]; }): ExecutionEngine<E> => ({ execute: () => Effect.succeed({ result: "default" }), executeWithPause: overrides.executeWithPause ?? (() => Effect.succeed({ status: "completed", result: { result: "default" } })), resume: overrides.resume ?? (() => Effect.succeed(null)), isExecutionSettled: undefined, getPausedExecution: () => Effect.succeed(null), pausedExecutionCount: () => Effect.succeed(0), hasPausedExecutions: () => Effect.succeed(false), getDescription: Effect.succeed("test executor"), }); /** * An in-memory stand-in for `executor.artifacts` with the same observable * behavior the real owner-scoped store has: save mints an id (or overwrites in * place), get fails when nothing matches, list is newest-first without code. */ const makeArtifactStore = () => { const rows = new Map<string, Artifact>(); let seq = 0; const calls: Array<{ title: string; description: string | null; code: string; bindings: ArtifactBindings | null; }> = []; return { calls, rows, port: { list: (): Effect.Effect<readonly ArtifactSummary[]> => Effect.sync(() => [...rows.values()] .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) .map(({ code: _code, bindings: _bindings, ...summary }) => summary), ), get: (id: string): Effect.Effect<Artifact, TestArtifactError> => Effect.suspend(() => { const row = rows.get(id); return row ? Effect.succeed(row) : Effect.fail(new TestArtifactError({ message: `no artifact ${id}` })); }), save: (input: { readonly id?: string; readonly title: string; readonly description?: string | null; readonly code: string; readonly bindings?: ArtifactBindings | null; readonly preview?: string | null; }): Effect.Effect<Artifact, TestArtifactError> => Effect.suspend(() => { calls.push({ title: input.title, description: input.description ?? null, code: input.code, bindings: input.bindings ?? null, }); const existing = input.id === undefined ? undefined : rows.get(input.id); if (input.id !== undefined && !existing) { return Effect.fail(new TestArtifactError({ message: `no artifact ${input.id}` })); } seq += 1; const artifact: Artifact = { id: ArtifactId.make(existing?.id ?? `art_${seq}`), owner: "user", title: input.title, description: input.description ?? null, // Mirrors the real save: the preview is written on every overwrite, // including back to null, because it interprets `code`. preview: input.preview === undefined || input.preview === null ? null : { kind: "layout", markup: input.preview }, code: input.code, bindings: input.bindings ?? null, createdAt: existing?.createdAt ?? new Date(seq * 1000), updatedAt: new Date(seq * 1000), }; rows.set(artifact.id, artifact); return Effect.succeed(artifact); }), }, }; }; const withClient = async <E extends Cause.YieldableError>( engine: ExecutionEngine<E>, capabilities: ClientCapabilities, fn: (client: Client) => Promise<void>, config?: Partial<ExecutorMcpServerConfig<E>>, ) => { const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine, loadAppShellHtml: () => Promise.resolve(SHELL_HTML), // Artifacts are on by default and nearly every test in this file // exercises the artifact surface; spelled out here anyway so the cases // that assert the OFF surface (`artifactsEnabled: false`) read as the // deliberate contrast. The default (no flag at all) is proved directly // in the opt-out describe block. artifactsEnabled: true, ...config, } as ExecutorMcpServerConfig<E>), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities }); await mcpServer.connect(serverTransport); await client.connect(clientTransport); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test helper must close MCP transports after async client assertions try { await fn(client); } finally { await clientTransport.close(); await serverTransport.close(); } }; const SHELL_HTML = "<!doctype html><html><body><div id='root'></div></body></html>"; // What a client that renders MCP Apps advertises at `initialize`. The SDK's // `ClientCapabilities` has no `extensions` field yet (pending SEP-1724), which // is exactly why ext-apps ships `getUiCapability` to read it. // oxlint-disable-next-line executor/no-double-cast -- boundary: MCP SDK ClientCapabilities predates the ext-apps `extensions` field const APPS_CAPS = { extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } }, } as unknown as ClientCapabilities; const NO_APPS_CAPS: ClientCapabilities = {}; const structuredOf = (result: Awaited<ReturnType<Client["callTool"]>>): Record<string, unknown> => (result.structuredContent ?? {}) as Record<string, unknown>; const textOf = (result: Awaited<ReturnType<Client["callTool"]>>): string => (result.content as Array<{ type: string; text: string }>)[0].text; const toolNames = async (client: Client): Promise<string[]> => (await client.listTools()).tools.map((tool) => tool.name); const COUNTER_CODE = "function App() { return <div>hi</div>; }"; const UPDATED_CODE = "function App() { return <div>hi again</div>; }"; const makePausedResult = (id: string, message: string): ExecutionResult => ({ status: "paused", execution: { id, elicitationContext: { request: FormElicitation.make({ message, requestedSchema: {} }), }, } as ExecutionResult extends { status: "paused"; execution: infer P } ? P : never, }); // --------------------------------------------------------------------------- // Capability-gated visibility // --------------------------------------------------------------------------- describe("MCP host — artifact tool visibility", () => { it("hides the app-only tools from clients that cannot render MCP Apps", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), NO_APPS_CAPS, async (client) => { const names = await toolNames(client); // The model-facing tools are always advertised — they degrade to a // deep link rather than disappearing. expect(names).toContain("create-artifact"); expect(names).toContain("edit-artifact"); expect(names).toContain("list-artifacts"); expect(names).toContain("show-artifact"); // `execute-action` is only callable from inside a rendered app. expect(names).not.toContain("execute-action"); expect(names).not.toContain("execute-action-resume"); }, { artifacts: store.port }, ); }); it("exposes the app-only tools to clients that advertise the shell mime type", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { const names = await toolNames(client); expect(names).toContain("create-artifact"); expect(names).toContain("execute-action"); expect(names).toContain("execute-action-resume"); }, { artifacts: store.port }, ); }); // A cold restore (deploy, DO eviction) rebuilds the server mid-conversation // on the same session id, and the client — already past `initialize` — never // sends another one. Everything the rebuilt server knows about the client's // capabilities has to come from what the host persisted. it("keeps the app-only tools visible on a cold restore that replays no initialize", async () => { const store = makeArtifactStore(); const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine: makeStubEngine({}), artifacts: store.port, artifactsEnabled: true, loadAppShellHtml: () => Promise.resolve(SHELL_HTML), // What the host read back out of DO storage for this session. restoredAppsEnabled: true, }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await mcpServer.connect(serverTransport); // Deliberately NOT an SDK `Client`: `client.connect()` always sends // `initialize`, which is the one thing a cold restore never replays. The // client is mid-conversation and just keeps issuing calls against the same // mcp-session-id, so this drives raw JSON-RPC over the transport. const responses = new Map<number, Record<string, unknown>>(); clientTransport.onmessage = (message) => { const envelope = message as { id?: number; result?: Record<string, unknown> }; if (typeof envelope.id === "number" && envelope.result) { responses.set(envelope.id, envelope.result); } }; await clientTransport.start(); const call = async (id: number, method: string, params: Record<string, unknown>) => { await clientTransport.send({ jsonrpc: "2.0", id, method, params }); await vi.waitFor(() => expect(responses.has(id)).toBe(true)); return responses.get(id) ?? {}; }; const listed = (await call(1, "tools/list", {})) as { tools: Array<{ name: string }>; }; const names = listed.tools.map((tool) => tool.name); expect(names).toContain("execute-action"); expect(names).toContain("execute-action-resume"); // The render path is the symptom that took this to production: the same // session rendered inline before the restore and a deep link after. const created = (await call(2, "tools/call", { name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, })) as { structuredContent?: Record<string, unknown> }; expect(created.structuredContent).toEqual({ code: COUNTER_CODE, artifactId: "art_1" }); expect(created.structuredContent).not.toHaveProperty("status", "fallback_url"); await clientTransport.close(); await serverTransport.close(); }); // The restore path the `agents` runtime actually takes, and the one that // reached production. On cold restore it replays the PERSISTED `initialize` // REQUEST (`reinitializeServer`), which does re-establish the server's client // capabilities — but it never replays the `notifications/initialized` // NOTIFICATION, and `oninitialized` (the only hook that re-runs // `syncToolAvailability`) fires solely on that notification. So the rebuilt // server holds full apps capabilities while nothing re-reads them, and the // replay is dispatched un-awaited, so a tool call can also land ahead of it. // // Both orderings are covered here: capabilities present but no hook, and a // tool call dispatched before the replay arrives at all. it("renders inline on a restore that replays initialize without the initialized notification", async () => { const store = makeArtifactStore(); const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine: makeStubEngine({}), artifacts: store.port, artifactsEnabled: true, loadAppShellHtml: () => Promise.resolve(SHELL_HTML), // Nothing persisted: the capability must come from the replay alone, so // a passing assertion cannot be coming from the seed. restoredAppsEnabled: false, artifactUrl: artifactUrlFor("https://executor.test"), }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await mcpServer.connect(serverTransport); const responses = new Map<string | number, Record<string, unknown>>(); clientTransport.onmessage = (message) => { const envelope = message as { id?: string | number; result?: Record<string, unknown> }; if (envelope.id !== undefined && envelope.result) responses.set(envelope.id, envelope.result); }; await clientTransport.start(); // Exactly what `agents/mcp` replays: the initialize request under its // restore sentinel id, and no `notifications/initialized` after it. await clientTransport.send({ jsonrpc: "2.0", id: "__worker_transport_restore__", method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: APPS_CAPS, clientInfo: { name: "ClaudeAI", version: "1.0.0" }, }, } as never); await vi.waitFor(() => expect(mcpServer.server.getClientCapabilities()).toMatchObject(APPS_CAPS), ); const call = async (id: number, method: string, params: Record<string, unknown>) => { await clientTransport.send({ jsonrpc: "2.0", id, method, params }); await vi.waitFor(() => expect(responses.has(id)).toBe(true)); return responses.get(id) ?? {}; }; const created = (await call(1, "tools/call", { name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, })) as { structuredContent?: Record<string, unknown> }; expect(created.structuredContent).toEqual({ code: COUNTER_CODE, artifactId: "art_1", url: "https://executor.test/artifacts/art_1", }); expect(created.structuredContent).not.toHaveProperty("status", "fallback_url"); // The widget is useless if it cannot call back in, so visibility has to // reconcile on the same restore. const listed = (await call(2, "tools/list", {})) as { tools: Array<{ name: string }> }; const names = listed.tools.map((tool) => tool.name); expect(names).toContain("execute-action"); expect(names).toContain("execute-action-resume"); await clientTransport.close(); await serverTransport.close(); }); it("still falls back to a deep link when the restored session had no apps support", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), NO_APPS_CAPS, async (client) => { const result = await client.callTool({ name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, }); expect(structuredOf(result)).toEqual({ status: "fallback_url", url: "https://executor.test/artifacts/art_1", artifactId: "art_1", }); }, { artifacts: store.port, restoredAppsEnabled: false, artifactUrl: artifactUrlFor("https://executor.test"), }, ); }); // The restored value seeds the session; it must never outrank the client in // front of you. A client that reconnects without apps support gets the // fallback, restored bit or not. it("lets a real initialize overrule the restored value in both directions", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), NO_APPS_CAPS, async (client) => { const names = await toolNames(client); expect(names).not.toContain("execute-action"); const result = await client.callTool({ name: "create-artifact", arguments: { code: COUNTER_CODE, title: "Active users dashboard" }, }); expect(structuredOf(result)).toMatchObject({ status: "fallback_url" }); }, { artifacts: store.port, restoredAppsEnabled: true, artifactUrl: artifactUrlFor("https://executor.test"), }, ); }); it("reports the negotiated value to the host so the next restore can seed itself", async () => { const store = makeArtifactStore(); const seen: boolean[] = []; await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { await toolNames(client); }, { artifacts: store.port, restoredAppsEnabled: false, onAppsEnabledChange: (appsEnabled) => Effect.sync(() => void seen.push(appsEnabled)), }, ); expect(seen).toEqual([true]); }); it("does not re-persist when initialize confirms the restored value", async () => { const store = makeArtifactStore(); const seen: boolean[] = []; await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { await toolNames(client); }, { artifacts: store.port, restoredAppsEnabled: true, onAppsEnabledChange: (appsEnabled) => Effect.sync(() => void seen.push(appsEnabled)), }, ); expect(seen).toEqual([]); }); it("registers no ui tools at all when no shell loader is configured", async () => { const store = makeArtifactStore(); const mcpServer = await Effect.runPromise( createExecutorMcpServer({ engine: makeStubEngine({}), artifacts: store.port, // Opted in, so the only thing withholding the surface is the missing // shell loader — otherwise this would pass on the connection default. artifactsEnabled: true, }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} }); await mcpServer.connect(serverTransport); await client.connect(clientTransport); const names = (await client.listTools()).tools.map((tool) => tool.name); expect(names).toContain("execute"); expect(names).not.toContain("create-artifact"); expect(names).not.toContain("list-artifacts"); await clientTransport.close(); await serverTransport.close(); }); // A session that opted out (`?artifacts=false`) gets no artifact surface at // all. The host here is fully artifact-capable — shell loader, store, the // apps client capability — so what these assert is the opt-out itself, not // a host that never had artifacts. it("registers no artifact tools on an opted-out connection", async () => { const store = makeArtifactStore(); await withClient( makeStubEngine({}), APPS_CAPS, async (client) => { const names = await toolNames(client); expect(names).not.toContain("create-artifact"); expect(names).not.toContain("edit-artifact"); expect(names).not.toContain("list-artifacts"); expect(names).not.toContain("show-artifact"); expect(names).not.toContain("execute-action"); expect(names).not.toContain("execute-action-resume"); // The rest of the surface is untouched.