History

Serve artifacts by default; ?artifacts=false is the opt-out

Artifacts required a ?artifacts=true opt-in per MCP connection. Flip the default: a plain endpoint URL now gets the full artifact surface (tools, shell resource, skills), and a connection that does not want it says ?artifacts=false — or --no-artifacts on the stdio CLI. ?artifacts=true stays accepted as the explicit default. - readArtifactsEnabled: absent flag now reads true; any explicit value that is not a documented truthy form reads false (a user touching the flag was reaching for off). - tool-server config default and both Cloudflare DO restore defaults flip to true; sessions persisted without a value restore to enabled. - Install card: toggle defaults on, only the opt-out reaches the URL and the stdio command. - e2e: the comparative scenario now proves the opt-out (clean URL has the surface, artifacts=false loses it); artifact scenarios use plain sessions.

Rhys Sullivan committed 3 months ago

Commit eb83edc1 parent 9bd4a5b18 files · +138 −128Browse files

Unified · Split

18 files
  1. .changeset/eight-lions-argue.md +5 −0
  2. apps/cli/src/main.ts +8 −7
  3. apps/cloud/src/mcp/session-durable-object.ts +4 −4
  4. apps/host-cloudflare/src/mcp/session-durable-object.ts +4 −4
  5. e2e/cloud/mcp-oversize-response.test.ts +2 −2
  6. e2e/scenarios/artifact-loading-surface.test.ts +3 −3
  7. e2e/scenarios/artifact-preview-gallery.test.ts +3 −3
  8. e2e/scenarios/artifacts.test.ts +32 −33
  9. e2e/src/surfaces/mcp.ts +6 −5
  10. packages/core/execution/src/skills.ts +1 −1
  11. packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts +2 −2
  12. packages/hosts/mcp-apps-shell/src/shell/mcp-app.browser.test.ts +1 −1
  13. packages/hosts/mcp/src/artifacts-tools.test.ts +30 −27
  14. packages/hosts/mcp/src/browser-approval.ts +8 −7
  15. packages/hosts/mcp/src/in-memory-session-store.ts +3 −3
  16. packages/hosts/mcp/src/tool-server.ts +5 −5
  17. packages/react/src/components/mcp-install-card.test.ts +13 −13
  18. packages/react/src/components/mcp-install-card.tsx +8 −8
.changeset/eight-lions-argue.md+5−0
@@ -0,0 +1,5 @@
1---
2"executor": patch
3---
4
5**Artifacts are now on by default for MCP connections.** A plain endpoint URL serves the full artifact surface — the artifact tools, the app shell resource, and the artifact skills. Connections that don't want it opt out with `?artifacts=false` (or `--no-artifacts` on the stdio CLI); `?artifacts=true` remains accepted as the explicit default. Previously the surface required a `?artifacts=true` opt-in.
apps/cli/src/main.ts+8−7
@@ -1346,9 +1346,10 @@ const mcpUrlForActiveLocalServer = (input: { · 1345 unmodified lines
1346 if (input.elicitationMode === "browser") {1346 if (input.elicitationMode === "browser") {
1347 url.searchParams.set("elicitation_mode", "browser");1347 url.searchParams.set("elicitation_mode", "browser");
1348 }1348 }
1349 // Only the opt-in is spelled out; the default endpoint stays clean.1349 // Artifacts are on by default; only the opt-out is spelled out, so the
1350 if (input.artifacts) {1350 // default endpoint stays clean.
1351 url.searchParams.set("artifacts", "true");1351 if (!input.artifacts) {
1352 url.searchParams.set("artifacts", "false");
1352 }1353 }
1353 return url;1354 return url;
1354};1355};
@@ -2802,18 +2803,18 @@ const mcpCommand = Command.make( · 1447 unmodified lines
2802 "Choose the stdio approval flow: browser approval or a CLI resume tool exposed to the model.",2803 "Choose the stdio approval flow: browser approval or a CLI resume tool exposed to the model.",
2803 ),2804 ),
2804 ),2805 ),
2805 artifacts: Options.boolean("artifacts")2806 noArtifacts: Options.boolean("no-artifacts")
2806 .pipe(Options.withDefault(false))2807 .pipe(Options.withDefault(false))
2807 .pipe(2808 .pipe(
2808 Options.withDescription(2809 Options.withDescription(
2809 "Serve the artifact surface on this connection: the artifact tools, the app shell resource, and the artifact skills. Withheld by default.",2810 "Withhold the artifact surface from this connection: the artifact tools, the app shell resource, and the artifact skills. Served by default.",
2810 ),2811 ),
2811 ),2812 ),
2812 },2813 },
2813 ({ scope, elicitationMode, artifacts }) =>2814 ({ scope, elicitationMode, noArtifacts }) =>
2814 Effect.gen(function* () {2815 Effect.gen(function* () {
2815 applyScope(scope);2816 applyScope(scope);
2816 yield* runStdioMcpSession({ elicitationMode, artifacts });2817 yield* runStdioMcpSession({ elicitationMode, artifacts: !noArtifacts });
2817 }),2818 }),
2818).pipe(Command.withDescription("Start an MCP server over stdio"));2819).pipe(Command.withDescription("Start an MCP server over stdio"));
2819 2820
apps/cloud/src/mcp/session-durable-object.ts+4−4
@@ -276,10 +276,10 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 275 unmodified lines
276 description,276 description,
277 artifacts: executor.artifacts,277 artifacts: executor.artifacts,
278 connections: executor.connections,278 connections: executor.connections,
279 // Artifacts are opt-in per connection. Sessions persisted before the279 // Artifacts are on by default, opt-out per connection. A session
280 // flag existed carry no value; absent now means off, same as a fresh280 // persisted without a value restores to the default, same as a fresh
281 // connection that did not ask for `?artifacts=true`.281 // connection whose URL says nothing about `?artifacts=`.
282 artifactsEnabled: sessionMeta.artifactsEnabled ?? false,282 artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
283 // Cold restores rebuild this server with no `initialize` to replay, so283 // Cold restores rebuild this server with no `initialize` to replay, so
284 // the negotiated apps support comes back from storage instead.284 // the negotiated apps support comes back from storage instead.
285 restoredAppsEnabled: sessionMeta.appsEnabled ?? false,285 restoredAppsEnabled: sessionMeta.appsEnabled ?? false,
apps/host-cloudflare/src/mcp/session-durable-object.ts+4−4
@@ -145,10 +145,10 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession · 144 unmodified lines
145 engine,145 engine,
146 artifacts: executor.artifacts,146 artifacts: executor.artifacts,
147 connections: executor.connections,147 connections: executor.connections,
148 // Artifacts are opt-in per connection. Sessions persisted before the148 // Artifacts are on by default, opt-out per connection. A session
149 // flag existed carry no value; absent now means off, same as a fresh149 // persisted without a value restores to the default, same as a fresh
150 // connection that did not ask for `?artifacts=true`.150 // connection whose URL says nothing about `?artifacts=`.
151 artifactsEnabled: sessionMeta.artifactsEnabled ?? false,151 artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
152 // Cold restores rebuild this server with no `initialize` to replay, so152 // Cold restores rebuild this server with no `initialize` to replay, so
153 // the negotiated apps support comes back from storage instead.153 // the negotiated apps support comes back from storage instead.
154 restoredAppsEnabled: sessionMeta.appsEnabled ?? false,154 restoredAppsEnabled: sessionMeta.appsEnabled ?? false,
e2e/cloud/mcp-oversize-response.test.ts+2−2
@@ -92,8 +92,8 @@ scenario( · 91 unmodified lines
92 const mcp = yield* Mcp;92 const mcp = yield* Mcp;
93 const identity = yield* target.newIdentity();93 const identity = yield* target.newIdentity();
94 const bearer = yield* mcp.mintBearer(emailOf(identity));94 const bearer = yield* mcp.mintBearer(emailOf(identity));
95 // Artifacts opt-in is what registers the shell resource on the session.95 // Artifacts are on by default, which registers the shell resource.
96 const mcpUrl = `${target.mcpUrl}?artifacts=true`;96 const mcpUrl = target.mcpUrl;
97 97
98 const sessionId = yield* Effect.promise(() => openSession(mcpUrl, bearer));98 const sessionId = yield* Effect.promise(() => openSession(mcpUrl, bearer));
99 99
e2e/scenarios/artifact-loading-surface.test.ts+3−3
@@ -197,9 +197,9 @@ scenario( · 196 unmodified lines
197 197
198 const identity = yield* target.newIdentity();198 const identity = yield* target.newIdentity();
199 const client = yield* apiClient(api, identity);199 const client = yield* apiClient(api, identity);
200 // Artifacts are opt-in per MCP connection, so this session asks for the200 // Artifacts are on by default; the plain session carries the surface
201 // surface it is here to exercise (`?artifacts=true`).201 // this scenario exercises.
202 const session = mcp.session(identity, { artifacts: true });202 const session = mcp.session(identity);
203 203
204 const suffix = uniqueSuffix();204 const suffix = uniqueSuffix();
205 const title = `Loading Surface ${suffix}`;205 const title = `Loading Surface ${suffix}`;
e2e/scenarios/artifact-preview-gallery.test.ts+3−3
@@ -145,9 +145,9 @@ scenario( · 144 unmodified lines
145 const browser = yield* Browser;145 const browser = yield* Browser;
146 146
147 const identity = yield* target.newIdentity();147 const identity = yield* target.newIdentity();
148 // Artifacts are opt-in per MCP connection, so this session asks for the148 // Artifacts are on by default; the plain session carries the surface
149 // surface it is here to exercise (`?artifacts=true`).149 // this scenario exercises.
150 const session = mcp.session(identity, { artifacts: true });150 const session = mcp.session(identity);
151 const suffix = uniqueSuffix();151 const suffix = uniqueSuffix();
152 152
153 const seeded = [153 const seeded = [
e2e/scenarios/artifacts.test.ts+32−33
@@ -166,9 +166,9 @@ scenario( · 165 unmodified lines
166 166
167 const identity = yield* target.newIdentity();167 const identity = yield* target.newIdentity();
168 const client = yield* apiClient(api, identity);168 const client = yield* apiClient(api, identity);
169 // Artifacts are opt-in per MCP connection, so this session asks for the169 // Artifacts are on by default; the plain session carries the surface
170 // surface it is here to exercise (`?artifacts=true`).170 // this scenario exercises.
171 const session = mcp.session(identity, { artifacts: true });171 const session = mcp.session(identity);
172 172
173 const suffix = uniqueSuffix();173 const suffix = uniqueSuffix();
174 const title = `Release Readiness ${suffix}`;174 const title = `Release Readiness ${suffix}`;
@@ -556,9 +556,9 @@ scenario( · 381 unmodified lines
556 556
557 const identity = yield* target.newIdentity();557 const identity = yield* target.newIdentity();
558 const client = yield* apiClient(api, identity);558 const client = yield* apiClient(api, identity);
559 // Artifacts are opt-in per MCP connection, so this session asks for the559 // Artifacts are on by default; the plain session carries the surface
560 // surface it is here to exercise (`?artifacts=true`).560 // this scenario exercises.
561 const session = mcp.session(identity, { artifacts: true });561 const session = mcp.session(identity);
562 562
563 const suffix = uniqueSuffix();563 const suffix = uniqueSuffix();
564 const originalTitle = `Draft Dashboard ${suffix}`;564 const originalTitle = `Draft Dashboard ${suffix}`;
@@ -677,9 +677,9 @@ scenario( · 112 unmodified lines
677 677
678 const identity = yield* target.newIdentity();678 const identity = yield* target.newIdentity();
679 const client = yield* apiClient(api, identity);679 const client = yield* apiClient(api, identity);
680 // Artifacts are opt-in per MCP connection, so this session asks for the680 // Artifacts are on by default; the plain session carries the surface
681 // surface it is here to exercise (`?artifacts=true`).681 // this scenario exercises.
682 const session = mcp.session(identity, { artifacts: true });682 const session = mcp.session(identity);
683 683
684 const suffix = uniqueSuffix();684 const suffix = uniqueSuffix();
685 const title = `Iterated Dashboard ${suffix}`;685 const title = `Iterated Dashboard ${suffix}`;
@@ -773,14 +773,13 @@ function App() { · 87 unmodified lines
773 }),773 }),
774);774);
775 775
776// The opt-in. Artifacts are off by default: a plain endpoint URL gets no776// The opt-out. Artifacts are on by default: a plain endpoint URL gets the full
777// artifact surface at all — no artifact tools, and no artifact entries in the777// artifact surface. A connection that says `?artifacts=false` gets none of it —
778// `skills` inventory to point a model at tools it does not have. A user who778// no artifact tools, and no artifact entries in the `skills` inventory to point
779// wants generated UI says so on the URL (`?artifacts=true`) and that connection779// a model at tools it does not have. The proof is comparative: two sessions,
780// gets the whole surface. The proof is comparative: two sessions, same780// same identity, same server, differing only in that query.
781// identity, same server, differing only in that query.
782scenario(781scenario(
783 "Artifacts · only a session connected with artifacts=true sees the artifact surface",782 "Artifacts · a session connected with artifacts=false loses the artifact surface",
784 { timeout: 120_000 },783 { timeout: 120_000 },
785 Effect.gen(function* () {784 Effect.gen(function* () {
786 const target = yield* Target;785 const target = yield* Target;
@@ -788,7 +787,7 @@ scenario( · 1 unmodified line
788 787
789 const identity = yield* target.newIdentity();788 const identity = yield* target.newIdentity();
790 const defaultSession = mcp.session(identity);789 const defaultSession = mcp.session(identity);
791 const optedIn = mcp.session(identity, { artifacts: true });790 const optedOut = mcp.session(identity, { artifacts: false });
792 791
793 const ARTIFACT_TOOLS = [792 const ARTIFACT_TOOLS = [
794 "create-artifact",793 "create-artifact",
@@ -798,32 +797,32 @@ scenario( · 3 unmodified lines
798 "execute-action-resume",797 "execute-action-resume",
799 ] as const;798 ] as const;
800 799
801 const defaultTools = yield* defaultSession.listTools();800 const optedOutTools = yield* optedOut.listTools();
802 for (const tool of ARTIFACT_TOOLS) {801 for (const tool of ARTIFACT_TOOLS) {
803 expect(defaultTools, `${tool} is withheld from a default session`).not.toContain(tool);802 expect(optedOutTools, `${tool} is withheld from an opted-out session`).not.toContain(tool);
804 }803 }
805 // Only artifacts are withheld — the connection is otherwise a normal one.804 // Only artifacts are withheld — the connection is otherwise a normal one.
806 expect(defaultTools, "execute still works on a default session").toContain("execute");805 expect(optedOutTools, "execute still works on an opted-out session").toContain("execute");
807 expect(defaultTools, "skills still works on a default session").toContain("skills");806 expect(optedOutTools, "skills still works on an opted-out session").toContain("skills");
808 807
809 const defaultSkills = yield* defaultSession.call("skills", {});808 const optedOutSkills = yield* optedOut.call("skills", {});
810 expect(defaultSkills.ok, `the skills index came back: ${defaultSkills.text}`).toBe(true);809 expect(optedOutSkills.ok, `the skills index came back: ${optedOutSkills.text}`).toBe(true);
811 expect(defaultSkills.text, "the index still offers the execute skill").toContain("`execute`");810 expect(optedOutSkills.text, "the index still offers the execute skill").toContain("`execute`");
812 expect(811 expect(
813 defaultSkills.text,812 optedOutSkills.text,
814 "the index does not advertise a how-to for a tool this session lacks",813 "the index does not advertise a how-to for a tool this session lacks",
815 ).not.toContain("`create-artifact`");814 ).not.toContain("`create-artifact`");
816 expect(defaultSkills.text, "nor the artifact style guide").not.toContain("`artifact-style`");815 expect(optedOutSkills.text, "nor the artifact style guide").not.toContain("`artifact-style`");
817 816
818 // The control: the same identity against the same server, with the query,817 // The control: the same identity against the same server, with a clean
819 // gets everything. Without this the assertions above would also pass on a818 // URL, gets everything by default. Without this the assertions above would
820 // server that had simply lost artifacts.819 // also pass on a server that had simply lost artifacts.
821 const optedInTools = yield* optedIn.listTools();820 const defaultTools = yield* defaultSession.listTools();
822 for (const tool of ["create-artifact", "list-artifacts", "show-artifact"] as const) {821 for (const tool of ["create-artifact", "list-artifacts", "show-artifact"] as const) {
823 expect(optedInTools, `${tool} is present once the session opts in`).toContain(tool);822 expect(defaultTools, `${tool} is present on a default session`).toContain(tool);
824 }823 }
825 const optedInSkills = yield* optedIn.call("skills", {});824 const defaultSkills = yield* defaultSession.call("skills", {});
826 expect(optedInSkills.text, "the opted-in index offers the create-artifact skill").toContain(825 expect(defaultSkills.text, "the default index offers the create-artifact skill").toContain(
827 "`create-artifact`",826 "`create-artifact`",
828 );827 );
829 }),828 }),
e2e/src/surfaces/mcp.ts+6−5
@@ -164,8 +164,9 @@ export interface McpSurface { · 163 unmodified lines
164 identity: Identity,164 identity: Identity,
165 options?: {165 options?: {
166 readonly elicitationMode?: McpElicitationMode;166 readonly elicitationMode?: McpElicitationMode;
167 /** Opt this session in to artifacts (`?artifacts=true`). Omitted means167 /** Set `false` to opt this session out of artifacts
168 * the product default: no artifact surface at all. */168 * (`?artifacts=false`). Omitted means the product default: the full
169 * artifact surface. */
169 readonly artifacts?: boolean;170 readonly artifacts?: boolean;
170 readonly url?: string;171 readonly url?: string;
171 },172 },
@@ -302,12 +303,12 @@ export const makeMcpSurface = (target: Target, runDir?: string): McpSurface => ( · 130 unmodified lines
302 const serverName = `${target.name}-${randomUUID().slice(0, 8)}`;303 const serverName = `${target.name}-${randomUUID().slice(0, 8)}`;
303 // Per-connection settings ride the MCP endpoint query, the ecosystem304 // Per-connection settings ride the MCP endpoint query, the ecosystem
304 // convention: `elicitation_mode` so a paused execution yields an approvalUrl305 // convention: `elicitation_mode` so a paused execution yields an approvalUrl
305 // instead of letting the model resume inline, and `artifacts=true` to opt306 // instead of letting the model resume inline, and `artifacts=false` to opt
306 // the session in to the artifact surface. Both are non-defaults, so a307 // the session out of the artifact surface. Both are non-defaults, so a
307 // plain session's URL carries no query at all.308 // plain session's URL carries no query at all.
308 const sessionQuery = [309 const sessionQuery = [
309 ...(options?.elicitationMode ? [`elicitation_mode=${options.elicitationMode}`] : []),310 ...(options?.elicitationMode ? [`elicitation_mode=${options.elicitationMode}`] : []),
310 ...(options?.artifacts === true ? ["artifacts=true"] : []),311 ...(options?.artifacts === false ? ["artifacts=false"] : []),
311 ].join("&");312 ].join("&");
312 const sessionUrl = sessionQuery ? `${mcpUrl}?${sessionQuery}` : mcpUrl;313 const sessionUrl = sessionQuery ? `${mcpUrl}?${sessionQuery}` : mcpUrl;
313 314
packages/core/execution/src/skills.ts+1−1
@@ -609,7 +609,7 @@ export const SKILLS: readonly Skill[] = [ · 608 unmodified lines
609];609];
610 610
611/** The skills that only make sense when the session serves artifacts. A611/** The skills that only make sense when the session serves artifacts. A
612 * connection that did not opt in to artifacts (`?artifacts=true`) has no tool612 * connection that opted out of artifacts (`?artifacts=false`) has no tool
613 * to apply them to, so they are dropped from its catalog rather than served as613 * to apply them to, so they are dropped from its catalog rather than served as
614 * guidance for a surface it cannot reach. */614 * guidance for a surface it cannot reach. */
615const ARTIFACT_SKILLS: ReadonlySet<Skill> = new Set([CREATE_ARTIFACT_SKILL, ARTIFACT_STYLE_SKILL]);615const ARTIFACT_SKILLS: ReadonlySet<Skill> = new Set([CREATE_ARTIFACT_SKILL, ARTIFACT_STYLE_SKILL]);
packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts+2−2
@@ -41,7 +41,7 @@ export interface McpSessionInit { · 40 unmodified lines
41 readonly userId: string;41 readonly userId: string;
42 readonly elicitationMode: McpElicitationMode;42 readonly elicitationMode: McpElicitationMode;
43 /** Whether this session serves artifacts, read off `?artifacts=` at connect43 /** Whether this session serves artifacts, read off `?artifacts=` at connect
44 * time. Absent means the default (disabled). */44 * time. Absent means the default (enabled). */
45 readonly artifactsEnabled?: boolean;45 readonly artifactsEnabled?: boolean;
46 /** The MCP resource the session was minted against (`/mcp` default vs a46 /** The MCP resource the session was minted against (`/mcp` default vs a
47 * `/mcp/toolkits/<slug>` toolkit), so the tool catalog is scoped to it. */47 * `/mcp/toolkits/<slug>` toolkit), so the tool catalog is scoped to it. */
@@ -103,7 +103,7 @@ export interface SessionMeta { · 55 unmodified lines
103 readonly elicitationMode?: McpElicitationMode;103 readonly elicitationMode?: McpElicitationMode;
104 /** Whether the session serves artifacts (carried from {@link McpSessionInit}).104 /** Whether the session serves artifacts (carried from {@link McpSessionInit}).
105 * Absent — including for sessions persisted before the flag existed — means105 * Absent — including for sessions persisted before the flag existed — means
106 * disabled. */106 * the default (enabled). */
107 readonly artifactsEnabled?: boolean;107 readonly artifactsEnabled?: boolean;
108 /** The MCP resource the session serves (carried from {@link McpSessionInit});108 /** The MCP resource the session serves (carried from {@link McpSessionInit});
109 * `buildMcpServer` scopes the tool catalog to it. */109 * `buildMcpServer` scopes the tool catalog to it. */
packages/hosts/mcp-apps-shell/src/shell/mcp-app.browser.test.ts+1−1
@@ -1246,7 +1246,7 @@ const startMcpHarnessForEngine = async <E extends Cause.YieldableError>( · 1245 unmodified lines
1246 loadAppShellHtml: loadMcpAppsShellHtml,1246 loadAppShellHtml: loadMcpAppsShellHtml,
1247 artifacts: makeInMemoryArtifacts(),1247 artifacts: makeInMemoryArtifacts(),
1248 // Artifacts are opt-in per connection; this harness drives the artifact1248 // Artifacts are opt-in per connection; this harness drives the artifact
1249 // tools, so it connects the way a `?artifacts=true` client does.1249 // tools, the connection default.
1250 artifactsEnabled: true,1250 artifactsEnabled: true,
1251 }),1251 }),
1252 );1252 );
packages/hosts/mcp/src/artifacts-tools.test.ts+30−27
@@ -140,11 +140,11 @@ const withClient = async <E extends Cause.YieldableError>( · 139 unmodified lines
140 createExecutorMcpServer({140 createExecutorMcpServer({
141 engine,141 engine,
142 loadAppShellHtml: () => Promise.resolve(SHELL_HTML),142 loadAppShellHtml: () => Promise.resolve(SHELL_HTML),
143 // Artifacts are opt-in per connection, and nearly every test in this file143 // Artifacts are on by default and nearly every test in this file
144 // exercises the artifact surface — so the helper connects the way a real144 // exercises the artifact surface; spelled out here anyway so the cases
145 // `?artifacts=true` client does. The cases that assert the OFF surface145 // that assert the OFF surface (`artifactsEnabled: false`) read as the
146 // pass `artifactsEnabled: false`, and the default (no flag at all) is146 // deliberate contrast. The default (no flag at all) is proved directly
147 // proved by building the server directly below.147 // in the opt-out describe block.
148 artifactsEnabled: true,148 artifactsEnabled: true,
149 ...config,149 ...config,
150 } as ExecutorMcpServerConfig<E>),150 } as ExecutorMcpServerConfig<E>),
@@ -479,11 +479,11 @@ describe("MCP host — artifact tool visibility", () => { · 328 unmodified lines
479 await serverTransport.close();479 await serverTransport.close();
480 });480 });
481 481
482 // A session that did not opt in (`?artifacts=true`) gets no artifact surface482 // A session that opted out (`?artifacts=false`) gets no artifact surface at
483 // at all. The host here is fully artifact-capable — shell loader, store, the483 // all. The host here is fully artifact-capable — shell loader, store, the
484 // apps client capability — so what these assert is the connection default484 // apps client capability — so what these assert is the opt-out itself, not
485 // itself, not a host that never had artifacts.485 // a host that never had artifacts.
486 it("registers no artifact tools on a default connection", async () => {486 it("registers no artifact tools on an opted-out connection", async () => {
487 const store = makeArtifactStore();487 const store = makeArtifactStore();
488 await withClient(488 await withClient(
489 makeStubEngine({}),489 makeStubEngine({}),
@@ -500,9 +500,9 @@ describe("MCP host — artifact tool visibility", () => { · 10 unmodified lines
500 expect(names).toContain("skills");500 expect(names).toContain("skills");
501 expect(names).toContain("resume");501 expect(names).toContain("resume");
502 },502 },
503 // No `artifactsEnabled` at all: the real shape of a client that connected503 // The explicit opt-out: the shape of a client that connected with
504 // to a bare `/mcp` endpoint.504 // `?artifacts=false`.
505 { artifacts: store.port, artifactsEnabled: undefined },505 { artifacts: store.port, artifactsEnabled: false },
506 );506 );
507 });507 });
508 508
@@ -510,7 +510,7 @@ describe("MCP host — artifact tool visibility", () => { · 1 unmodified line
510 // The read is what proves it: with no resource registered at all, the SDK510 // The read is what proves it: with no resource registered at all, the SDK
511 // does not install a resources handler either, so `resources/list` itself is511 // does not install a resources handler either, so `resources/list` itself is
512 // unavailable — the same shape a host without a shell loader has always had.512 // unavailable — the same shape a host without a shell loader has always had.
513 it("serves no shell resource on a default connection", async () => {513 it("serves no shell resource on an opted-out connection", async () => {
514 const store = makeArtifactStore();514 const store = makeArtifactStore();
515 await withClient(515 await withClient(
516 makeStubEngine({}),516 makeStubEngine({}),
@@ -518,11 +518,11 @@ describe("MCP host — artifact tool visibility", () => { · 1 unmodified line
518 async (client) => {518 async (client) => {
519 await expect(client.readResource({ uri: MCP_APPS_SHELL_RESOURCE_URI })).rejects.toThrow();519 await expect(client.readResource({ uri: MCP_APPS_SHELL_RESOURCE_URI })).rejects.toThrow();
520 },520 },
521 { artifacts: store.port, artifactsEnabled: undefined },521 { artifacts: store.port, artifactsEnabled: false },
522 );522 );
523 });523 });
524 524
525 it("drops the artifact skills from a default session's inventory", async () => {525 it("drops the artifact skills from an opted-out session's inventory", async () => {
526 const store = makeArtifactStore();526 const store = makeArtifactStore();
527 await withClient(527 await withClient(
528 makeStubEngine({}),528 makeStubEngine({}),
@@ -542,11 +542,11 @@ describe("MCP host — artifact tool visibility", () => { · 13 unmodified lines
542 expect(fetched.isError).toBe(true);542 expect(fetched.isError).toBe(true);
543 expect(textOf(fetched)).toContain('No skill named "create-artifact"');543 expect(textOf(fetched)).toContain('No skill named "create-artifact"');
544 },544 },
545 { artifacts: store.port, artifactsEnabled: undefined },545 { artifacts: store.port, artifactsEnabled: false },
546 );546 );
547 });547 });
548 548
549 it("serves the full artifact surface to a session that opted in", async () => {549 it("serves the full artifact surface on the connection default (no flag at all)", async () => {
550 const store = makeArtifactStore();550 const store = makeArtifactStore();
551 await withClient(551 await withClient(
552 makeStubEngine({}),552 makeStubEngine({}),
@@ -566,7 +566,9 @@ describe("MCP host — artifact tool visibility", () => { · 13 unmodified lines
566 expect(index).toContain("`create-artifact`");566 expect(index).toContain("`create-artifact`");
567 expect(index).toContain("`artifact-style`");567 expect(index).toContain("`artifact-style`");
568 },568 },
569 { artifacts: store.port },569 // Overrides the helper's explicit `artifactsEnabled: true` with the real
570 // absent-flag shape, so this pins the DEFAULT, not an opt-in.
571 { artifacts: store.port, artifactsEnabled: undefined },
570 );572 );
571 });573 });
572 574
@@ -2235,24 +2237,25 @@ describe("artifactUrlFor", () => { · 1662 unmodified lines
2235// The `?artifacts=` endpoint query2237// The `?artifacts=` endpoint query
2236// ---------------------------------------------------------------------------2238// ---------------------------------------------------------------------------
2237 2239
2238describe("artifacts opt-in query", () => {2240describe("artifacts opt-out query", () => {
2239 const read = (query: string) =>2241 const read = (query: string) =>
2240 readArtifactsEnabled(new Request(`https://executor.example/mcp${query}`));2242 readArtifactsEnabled(new Request(`https://executor.example/mcp${query}`));
2241 2243
2242 it("keeps artifacts off for a clean endpoint", () => {2244 it("serves artifacts on a clean endpoint", () => {
2243 expect(read("")).toBe(false);2245 expect(read("")).toBe(true);
2244 expect(read("?elicitation_mode=browser")).toBe(false);2246 expect(read("?elicitation_mode=browser")).toBe(true);
2245 });2247 });
2246 2248
2247 it("turns artifacts on for the documented true values", () => {2249 it("keeps artifacts on for the explicit true values", () => {
2248 for (const value of ["true", "TRUE", "1", "yes", "on"]) {2250 for (const value of ["true", "TRUE", "1", "yes", "on"]) {
2249 expect(read(`?artifacts=${value}`)).toBe(true);2251 expect(read(`?artifacts=${value}`)).toBe(true);
2250 }2252 }
2251 });2253 });
2252 2254
2253 // Anything else reads as "not an opt-in", so a typo cannot silently hand a2255 // Once the flag is present, anything but a documented true value reads as
2254 // connection a surface the user never asked for.2256 // the opt-out — a user who touched the flag at all was reaching for "off",
2255 it("keeps artifacts off for any other value", () => {2257 // so a typo lands on the side they were steering toward.
2258 it("turns artifacts off for the opt-out and any other explicit value", () => {
2256 for (const value of ["false", "0", "no", "ture", ""]) {2259 for (const value of ["false", "0", "no", "ture", ""]) {
2257 expect(read(`?artifacts=${value}`)).toBe(false);2260 expect(read(`?artifacts=${value}`)).toBe(false);
2258 }2261 }
packages/hosts/mcp/src/browser-approval.ts+8−7
@@ -52,16 +52,17 @@ export const readElicitationMode = (request: Request): McpElicitationMode => { · 51 unmodified lines
52};52};
53 53
54/**54/**
55 * Read the artifacts opt-in off an MCP request's `?artifacts=` query.55 * Read the artifacts opt-OUT off an MCP request's `?artifacts=` query.
56 * Artifacts are OFF by default, so a clean endpoint URL serves no artifact56 * Artifacts are ON by default: a clean endpoint URL gets the full artifact
57 * surface; only an explicit true value (`?artifacts=true`, and the same truthy57 * surface, and only an explicit false value (`?artifacts=false`, or any
58 * spellings the rest of the query vocabulary accepts) turns them on. A session58 * spelling that isn't one of the accepted truthy forms) withholds it. A
59 * without the opt-in gets no artifact tools, no `ui://` shell resource, and no59 * session that opts out gets no artifact tools, no `ui://` shell resource,
60 * artifact skills — as if the host had never been configured for them.60 * and no artifact skills — as if the host had never been configured for them.
61 * `?artifacts=true` remains accepted and is simply the default made explicit.
61 */62 */
62export const readArtifactsEnabled = (request: Request): boolean => {63export const readArtifactsEnabled = (request: Request): boolean => {
63 const value = new URL(request.url).searchParams.get("artifacts");64 const value = new URL(request.url).searchParams.get("artifacts");
64 if (value === null) return false;65 if (value === null) return true;
65 return TRUE_QUERY_VALUES.has(value.toLowerCase());66 return TRUE_QUERY_VALUES.has(value.toLowerCase());
66};67};
67 68
packages/hosts/mcp/src/in-memory-session-store.ts+3−3
@@ -71,9 +71,9 @@ export interface McpBuildServerOptions { · 70 unmodified lines
71 | { readonly mode: "model" }71 | { readonly mode: "model" }
72 | { readonly mode: "native" };72 | { readonly mode: "native" };
73 readonly browserApprovalStore?: BrowserApprovalStore;73 readonly browserApprovalStore?: BrowserApprovalStore;
74 /** Whether this session serves artifacts. True only when the client connected74 /** Whether this session serves artifacts. True unless the client connected
75 * with `?artifacts=true`; otherwise the built server registers none of the75 * with `?artifacts=false`; opted out, the built server registers none of
76 * artifact tools, resource, or skills. */76 * the artifact tools, resource, or skills. */
77 readonly artifactsEnabled?: boolean;77 readonly artifactsEnabled?: boolean;
78}78}
79 79
packages/hosts/mcp/src/tool-server.ts+5−5
@@ -156,8 +156,8 @@ type SharedMcpServerConfig = { · 155 unmodified lines
156 */156 */
157 readonly loadAppShellHtml?: () => Promise<string>;157 readonly loadAppShellHtml?: () => Promise<string>;
158 /**158 /**
159 * Per-connection artifacts opt-in. Defaults to false. A client that connects159 * Per-connection artifacts opt-out. Defaults to true. A client that connects
160 * without `?artifacts=true` gets NO artifact surface at all: none of the five160 * with `?artifacts=false` gets NO artifact surface at all: none of the five
161 * artifact tools, no `ui://` shell resource, and no artifact entries in the161 * artifact tools, no `ui://` shell resource, and no artifact entries in the
162 * `skills` inventory — the same shape a host without `loadAppShellHtml`162 * `skills` inventory — the same shape a host without `loadAppShellHtml`
163 * serves. `execute`, `skills` and `resume` are untouched.163 * serves. `execute`, `skills` and `resume` are untouched.
@@ -1037,10 +1037,10 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 873 unmodified lines
1037 // The same live integration inventory the description carries, re-used by1037 // The same live integration inventory the description carries, re-used by
1038 // the `skills` tool so the `execute` guide lists what is connected too.1038 // the `skills` tool so the `execute` guide lists what is connected too.
1039 const executeInventory = extractInventory(description);1039 const executeInventory = extractInventory(description);
1040 // Artifacts are off unless this connection opted in (`?artifacts=true`).1040 // Artifacts are on unless this connection opted out (`?artifacts=false`).
1041 // One flag decides the whole surface: the tools, the shell resource, and1041 // One flag decides the whole surface: the tools, the shell resource, and
1042 // the skills catalog below.1042 // the skills catalog below.
1043 const artifactsEnabled = config.artifactsEnabled ?? false;1043 const artifactsEnabled = config.artifactsEnabled ?? true;
1044 const skillCatalog: readonly Skill[] = skillCatalogFor({ artifacts: artifactsEnabled });1044 const skillCatalog: readonly Skill[] = skillCatalogFor({ artifacts: artifactsEnabled });
1045 1045
1046 // Captured at construction time. SDK callbacks fire later (often1046 // Captured at construction time. SDK callbacks fire later (often
@@ -1812,7 +1812,7 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 765 unmodified lines
1812 );1812 );
1813 1813
1814 // Two independent reasons to serve no artifact surface: the host cannot1814 // Two independent reasons to serve no artifact surface: the host cannot
1815 // (no shell loader), or this connection did not ask for it (`?artifacts=true`).1815 // (no shell loader), or this connection opted out (`?artifacts=false`).
1816 // Either way nothing below registers, so a disabled session is byte-for-byte1816 // Either way nothing below registers, so a disabled session is byte-for-byte
1817 // a session on a host that never had artifacts.1817 // a session on a host that never had artifacts.
1818 const loadAppShellHtml = artifactsEnabled ? config.loadAppShellHtml : undefined;1818 const loadAppShellHtml = artifactsEnabled ? config.loadAppShellHtml : undefined;
packages/react/src/components/mcp-install-card.test.ts+13−13
@@ -74,15 +74,15 @@ describe("MCP install command rendering", () => { · 73 unmodified lines
74 );74 );
75 });75 });
76 76
77 // Artifacts are off by default, so only the opt-in is ever spelled out: a77 // Artifacts are on by default, so only the opt-out is ever spelled out: a
78 // card left alone must still produce the bare endpoint every doc and test78 // card left alone must still produce the bare endpoint every doc and test
79 // asserts.79 // asserts.
80 it("emits the artifacts opt-in only when artifacts are enabled", () => {80 it("emits the artifacts opt-out only when artifacts are disabled", () => {
81 expect(81 expect(
82 buildMcpHttpEndpoint({82 buildMcpHttpEndpoint({
83 origin: "https://executor.example",83 origin: "https://executor.example",
84 desktop: null,84 desktop: null,
85 artifacts: false,85 artifacts: true,
86 }),86 }),
87 ).toBe("https://executor.example/mcp");87 ).toBe("https://executor.example/mcp");
88 88
@@ -90,9 +90,9 @@ describe("MCP install command rendering", () => { · 1 unmodified line
90 buildMcpHttpEndpoint({90 buildMcpHttpEndpoint({
91 origin: "https://executor.example",91 origin: "https://executor.example",
92 desktop: null,92 desktop: null,
93 artifacts: true,93 artifacts: false,
94 }),94 }),
95 ).toBe("https://executor.example/mcp?artifacts=true");95 ).toBe("https://executor.example/mcp?artifacts=false");
96 96
97 // Both non-defaults together, in the order the card renders them.97 // Both non-defaults together, in the order the card renders them.
98 expect(98 expect(
@@ -100,25 +100,25 @@ describe("MCP install command rendering", () => { · 1 unmodified line
100 origin: "https://executor.example",100 origin: "https://executor.example",
101 desktop: null,101 desktop: null,
102 elicitationMode: "browser",102 elicitationMode: "browser",
103 artifacts: true,103 artifacts: false,
104 }),104 }),
105 ).toBe("https://executor.example/mcp?elicitation_mode=browser&artifacts=true");105 ).toBe("https://executor.example/mcp?elicitation_mode=browser&artifacts=false");
106 106
107 // The unknown-origin placeholder is not a parsable URL, so it is107 // The unknown-origin placeholder is not a parsable URL, so it is
108 // concatenated rather than round-tripped through `URL`.108 // concatenated rather than round-tripped through `URL`.
109 expect(buildMcpHttpEndpoint({ origin: null, desktop: null, artifacts: true })).toBe(109 expect(buildMcpHttpEndpoint({ origin: null, desktop: null, artifacts: false })).toBe(
110 "<this-server>/mcp?artifacts=true",110 "<this-server>/mcp?artifacts=false",
111 );111 );
112 });112 });
113 113
114 it("passes the artifacts opt-in to the stdio CLI as a flag", () => {114 it("passes the artifacts opt-out to the stdio CLI as a flag", () => {
115 expect(115 expect(
116 buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: false }),116 buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: true }),
117 ).toBe("npx add-mcp 'executor mcp' --name executor");117 ).toBe("npx add-mcp 'executor mcp' --name executor");
118 118
119 expect(119 expect(
120 buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: true }),120 buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: false }),
121 ).toBe("npx add-mcp 'executor mcp --artifacts' --name executor");121 ).toBe("npx add-mcp 'executor mcp --no-artifacts' --name executor");
122 });122 });
123 123
124 it("passes model-managed resume through stdio install commands", () => {124 it("passes model-managed resume through stdio install commands", () => {
packages/react/src/components/mcp-install-card.tsx+8−8
@@ -49,8 +49,8 @@ export const buildMcpHttpEndpoint = (input: { · 48 unmodified lines
49 readonly port: number;49 readonly port: number;
50 } | null;50 } | null;
51 readonly elicitationMode?: McpElicitationMode;51 readonly elicitationMode?: McpElicitationMode;
52 /** Artifacts are off by default, so only the opt-in is spelled out on the52 /** Artifacts are on by default, so only the opt-out is spelled out on the
53 * URL (`&artifacts=true`) and a default endpoint stays clean. */53 * URL (`&artifacts=false`) and a default endpoint stays clean. */
54 readonly artifacts?: boolean;54 readonly artifacts?: boolean;
55 // Cloud only: pins the URL to `/<org-slug>/mcp` (the server also accepts the55 // Cloud only: pins the URL to `/<org-slug>/mcp` (the server also accepts the
56 // legacy `/<org_id>/mcp` form). Desktop/local pass nothing and get the bare56 // legacy `/<org_id>/mcp` form). Desktop/local pass nothing and get the bare
@@ -67,12 +67,12 @@ export const buildMcpHttpEndpoint = (input: { · 10 unmodified lines
67 ? `${input.origin}${mcpPath}`67 ? `${input.origin}${mcpPath}`
68 : `<this-server>${mcpPath}`;68 : `<this-server>${mcpPath}`;
69 // Only non-default choices reach the URL, so the common endpoint has no query69 // Only non-default choices reach the URL, so the common endpoint has no query
70 // at all: `model` elicitation and artifacts-off are what the server assumes.70 // at all: `model` elicitation and artifacts-on are what the server assumes.
71 const params: Array<readonly [string, string]> = [];71 const params: Array<readonly [string, string]> = [];
72 if (input.elicitationMode && input.elicitationMode !== "model") {72 if (input.elicitationMode && input.elicitationMode !== "model") {
73 params.push(["elicitation_mode", input.elicitationMode]);73 params.push(["elicitation_mode", input.elicitationMode]);
74 }74 }
75 if (input.artifacts === true) params.push(["artifacts", "true"]);75 if (input.artifacts === false) params.push(["artifacts", "false"]);
76 if (params.length === 0) return endpoint;76 if (params.length === 0) return endpoint;
77 77
78 const query = params.map(([key, value]) => `${key}=${value}`).join("&");78 const query = params.map(([key, value]) => `${key}=${value}`).join("&");
@@ -127,8 +127,8 @@ export const buildMcpInstallCommand = (input: { · 48 unmodified lines
127 if (input.elicitationMode && input.elicitationMode !== "model") {127 if (input.elicitationMode && input.elicitationMode !== "model") {
128 innerArgs.push("--elicitation-mode", input.elicitationMode);128 innerArgs.push("--elicitation-mode", input.elicitationMode);
129 }129 }
130 if (input.artifacts === true) {130 if (input.artifacts === false) {
131 innerArgs.push("--artifacts");131 innerArgs.push("--no-artifacts");
132 }132 }
133 return `npx add-mcp ${shellQuoteWord(innerArgs.map(shellQuoteWord).join(" "))} --name executor`;133 return `npx add-mcp ${shellQuoteWord(innerArgs.map(shellQuoteWord).join(" "))} --name executor`;
134};134};
@@ -137,7 +137,7 @@ export function McpInstallCard(props: { className?: string }) { · 2 unmodified lines
137 const [mode, setMode] = useState<TransportMode>("http");137 const [mode, setMode] = useState<TransportMode>("http");
138 const [advancedOpen, setAdvancedOpen] = useState(false);138 const [advancedOpen, setAdvancedOpen] = useState(false);
139 const [httpElicitationMode, setHttpElicitationMode] = useState<McpElicitationMode>("model");139 const [httpElicitationMode, setHttpElicitationMode] = useState<McpElicitationMode>("model");
140 const [artifacts, setArtifacts] = useState(false);140 const [artifacts, setArtifacts] = useState(true);
141 const organizationSlug = useOrganizationSlug();141 const organizationSlug = useOrganizationSlug();
142 const serverConnection = useExecutorServerConnection();142 const serverConnection = useExecutorServerConnection();
143 // Desktop hosts ship Electron without putting an `executor` binary on143 // Desktop hosts ship Electron without putting an `executor` binary on
@@ -208,7 +208,7 @@ export function McpInstallCard(props: { className?: string }) { · 64 unmodified lines
208 <div className="mt-0.5 text-xs leading-5 text-muted-foreground">208 <div className="mt-0.5 text-xs leading-5 text-muted-foreground">
209 {artifacts209 {artifacts
210 ? "Generated UI components are saved to your workspace."210 ? "Generated UI components are saved to your workspace."
211 : "Enable to let agents create saved UI components."}211 : "Disabled: this connection serves no artifact tools."}
212 </div>212 </div>
213 </div>213 </div>
214 <Switch214 <Switch

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t