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
13461346 if (input.elicitationMode === "browser") {
13471347 url.searchParams.set("elicitation_mode", "browser");
13481348 }
1349− // Only the opt-in is spelled out; the default endpoint stays clean.
1350− if (input.artifacts) {
1351− url.searchParams.set("artifacts", "true");
1349+ // Artifacts are on by default; only the opt-out is spelled out, so the
1350+ // default endpoint stays clean.
1351+ if (!input.artifacts) {
1352+ url.searchParams.set("artifacts", "false");
13521353 }
13531354 return url;
13541355 };
@@ -2802,18 +2803,18 @@ const mcpCommand = Command.make( · 1447 unmodified lines
28022803 "Choose the stdio approval flow: browser approval or a CLI resume tool exposed to the model.",
28032804 ),
28042805 ),
2805− artifacts: Options.boolean("artifacts")
2806+ noArtifacts: Options.boolean("no-artifacts")
28062807 .pipe(Options.withDefault(false))
28072808 .pipe(
28082809 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.",
28102811 ),
28112812 ),
28122813 },
2813− ({ scope, elicitationMode, artifacts }) =>
2814+ ({ scope, elicitationMode, noArtifacts }) =>
28142815 Effect.gen(function* () {
28152816 applyScope(scope);
2816− yield* runStdioMcpSession({ elicitationMode, artifacts });
2817+ yield* runStdioMcpSession({ elicitationMode, artifacts: !noArtifacts });
28172818 }),
28182819 ).pipe(Command.withDescription("Start an MCP server over stdio"));
28192820
apps/cloud/src/mcp/session-durable-object.ts+4−4
@@ -276,10 +276,10 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 275 unmodified lines
276276 description,
277277 artifacts: executor.artifacts,
278278 connections: executor.connections,
279− // Artifacts are opt-in per connection. Sessions persisted before the
280− // flag existed carry no value; absent now means off, same as a fresh
281− // connection that did not ask for `?artifacts=true`.
282− artifactsEnabled: sessionMeta.artifactsEnabled ?? false,
279+ // Artifacts are on by default, opt-out per connection. A session
280+ // persisted without a value restores to the default, same as a fresh
281+ // connection whose URL says nothing about `?artifacts=`.
282+ artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
283283 // Cold restores rebuild this server with no `initialize` to replay, so
284284 // the negotiated apps support comes back from storage instead.
285285 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
145145 engine,
146146 artifacts: executor.artifacts,
147147 connections: executor.connections,
148− // Artifacts are opt-in per connection. Sessions persisted before the
149− // flag existed carry no value; absent now means off, same as a fresh
150− // connection that did not ask for `?artifacts=true`.
151− artifactsEnabled: sessionMeta.artifactsEnabled ?? false,
148+ // Artifacts are on by default, opt-out per connection. A session
149+ // persisted without a value restores to the default, same as a fresh
150+ // connection whose URL says nothing about `?artifacts=`.
151+ artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
152152 // Cold restores rebuild this server with no `initialize` to replay, so
153153 // the negotiated apps support comes back from storage instead.
154154 restoredAppsEnabled: sessionMeta.appsEnabled ?? false,
e2e/cloud/mcp-oversize-response.test.ts+2−2
@@ -92,8 +92,8 @@ scenario( · 91 unmodified lines
9292 const mcp = yield* Mcp;
9393 const identity = yield* target.newIdentity();
9494 const bearer = yield* mcp.mintBearer(emailOf(identity));
95− // Artifacts opt-in is what registers the shell resource on the session.
96− const mcpUrl = `${target.mcpUrl}?artifacts=true`;
95+ // Artifacts are on by default, which registers the shell resource.
96+ const mcpUrl = target.mcpUrl;
9797
9898 const sessionId = yield* Effect.promise(() => openSession(mcpUrl, bearer));
9999
e2e/scenarios/artifact-loading-surface.test.ts+3−3
@@ -197,9 +197,9 @@ scenario( · 196 unmodified lines
197197
198198 const identity = yield* target.newIdentity();
199199 const client = yield* apiClient(api, identity);
200− // Artifacts are opt-in per MCP connection, so this session asks for the
201− // surface it is here to exercise (`?artifacts=true`).
202− const session = mcp.session(identity, { artifacts: true });
200+ // Artifacts are on by default; the plain session carries the surface
201+ // this scenario exercises.
202+ const session = mcp.session(identity);
203203
204204 const suffix = uniqueSuffix();
205205 const title = `Loading Surface ${suffix}`;
e2e/scenarios/artifact-preview-gallery.test.ts+3−3
@@ -145,9 +145,9 @@ scenario( · 144 unmodified lines
145145 const browser = yield* Browser;
146146
147147 const identity = yield* target.newIdentity();
148− // Artifacts are opt-in per MCP connection, so this session asks for the
149− // surface it is here to exercise (`?artifacts=true`).
150− const session = mcp.session(identity, { artifacts: true });
148+ // Artifacts are on by default; the plain session carries the surface
149+ // this scenario exercises.
150+ const session = mcp.session(identity);
151151 const suffix = uniqueSuffix();
152152
153153 const seeded = [
e2e/scenarios/artifacts.test.ts+32−33
@@ -166,9 +166,9 @@ scenario( · 165 unmodified lines
166166
167167 const identity = yield* target.newIdentity();
168168 const client = yield* apiClient(api, identity);
169− // Artifacts are opt-in per MCP connection, so this session asks for the
170− // surface it is here to exercise (`?artifacts=true`).
171− const session = mcp.session(identity, { artifacts: true });
169+ // Artifacts are on by default; the plain session carries the surface
170+ // this scenario exercises.
171+ const session = mcp.session(identity);
172172
173173 const suffix = uniqueSuffix();
174174 const title = `Release Readiness ${suffix}`;
@@ -556,9 +556,9 @@ scenario( · 381 unmodified lines
556556
557557 const identity = yield* target.newIdentity();
558558 const client = yield* apiClient(api, identity);
559− // Artifacts are opt-in per MCP connection, so this session asks for the
560− // surface it is here to exercise (`?artifacts=true`).
561− const session = mcp.session(identity, { artifacts: true });
559+ // Artifacts are on by default; the plain session carries the surface
560+ // this scenario exercises.
561+ const session = mcp.session(identity);
562562
563563 const suffix = uniqueSuffix();
564564 const originalTitle = `Draft Dashboard ${suffix}`;
@@ -677,9 +677,9 @@ scenario( · 112 unmodified lines
677677
678678 const identity = yield* target.newIdentity();
679679 const client = yield* apiClient(api, identity);
680− // Artifacts are opt-in per MCP connection, so this session asks for the
681− // surface it is here to exercise (`?artifacts=true`).
682− const session = mcp.session(identity, { artifacts: true });
680+ // Artifacts are on by default; the plain session carries the surface
681+ // this scenario exercises.
682+ const session = mcp.session(identity);
683683
684684 const suffix = uniqueSuffix();
685685 const title = `Iterated Dashboard ${suffix}`;
@@ -773,14 +773,13 @@ function App() { · 87 unmodified lines
773773 }),
774774 );
775775
776−// The opt-in. Artifacts are off by default: a plain endpoint URL gets no
777−// artifact surface at all — no artifact tools, and no artifact entries in the
778−// `skills` inventory to point a model at tools it does not have. A user who
779−// wants generated UI says so on the URL (`?artifacts=true`) and that connection
780−// gets the whole surface. The proof is comparative: two sessions, same
781−// identity, same server, differing only in that query.
776+// The opt-out. Artifacts are on by default: a plain endpoint URL gets the full
777+// artifact surface. A connection that says `?artifacts=false` gets none of it —
778+// no artifact tools, and no artifact entries in the `skills` inventory to point
779+// a model at tools it does not have. The proof is comparative: two sessions,
780+// same identity, same server, differing only in that query.
782781 scenario(
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",
784783 { timeout: 120_000 },
785784 Effect.gen(function* () {
786785 const target = yield* Target;
@@ -788,7 +787,7 @@ scenario( · 1 unmodified line
788787
789788 const identity = yield* target.newIdentity();
790789 const defaultSession = mcp.session(identity);
791− const optedIn = mcp.session(identity, { artifacts: true });
790+ const optedOut = mcp.session(identity, { artifacts: false });
792791
793792 const ARTIFACT_TOOLS = [
794793 "create-artifact",
@@ -798,32 +797,32 @@ scenario( · 3 unmodified lines
798797 "execute-action-resume",
799798 ] as const;
800799
801− const defaultTools = yield* defaultSession.listTools();
800+ const optedOutTools = yield* optedOut.listTools();
802801 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);
804803 }
805804 // Only artifacts are withheld — the connection is otherwise a normal one.
806− expect(defaultTools, "execute still works on a default session").toContain("execute");
807− expect(defaultTools, "skills still works on a default session").toContain("skills");
805+ expect(optedOutTools, "execute still works on an opted-out session").toContain("execute");
806+ expect(optedOutTools, "skills still works on an opted-out session").toContain("skills");
808807
809− const defaultSkills = yield* defaultSession.call("skills", {});
810− expect(defaultSkills.ok, `the skills index came back: ${defaultSkills.text}`).toBe(true);
811− expect(defaultSkills.text, "the index still offers the execute skill").toContain("`execute`");
808+ const optedOutSkills = yield* optedOut.call("skills", {});
809+ expect(optedOutSkills.ok, `the skills index came back: ${optedOutSkills.text}`).toBe(true);
810+ expect(optedOutSkills.text, "the index still offers the execute skill").toContain("`execute`");
812811 expect(
813− defaultSkills.text,
812+ optedOutSkills.text,
814813 "the index does not advertise a how-to for a tool this session lacks",
815814 ).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`");
817816
818− // The control: the same identity against the same server, with the query,
819− // gets everything. Without this the assertions above would also pass on a
820− // server that had simply lost artifacts.
821− const optedInTools = yield* optedIn.listTools();
817+ // The control: the same identity against the same server, with a clean
818+ // URL, gets everything by default. Without this the assertions above would
819+ // also pass on a server that had simply lost artifacts.
820+ const defaultTools = yield* defaultSession.listTools();
822821 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);
824823 }
825− const optedInSkills = yield* optedIn.call("skills", {});
826− expect(optedInSkills.text, "the opted-in index offers the create-artifact skill").toContain(
824+ const defaultSkills = yield* defaultSession.call("skills", {});
825+ expect(defaultSkills.text, "the default index offers the create-artifact skill").toContain(
827826 "`create-artifact`",
828827 );
829828 }),
e2e/src/surfaces/mcp.ts+6−5
@@ -164,8 +164,9 @@ export interface McpSurface { · 163 unmodified lines
164164 identity: Identity,
165165 options?: {
166166 readonly elicitationMode?: McpElicitationMode;
167− /** Opt this session in to artifacts (`?artifacts=true`). Omitted means
168− * the product default: no artifact surface at all. */
167+ /** Set `false` to opt this session out of artifacts
168+ * (`?artifacts=false`). Omitted means the product default: the full
169+ * artifact surface. */
169170 readonly artifacts?: boolean;
170171 readonly url?: string;
171172 },
@@ -302,12 +303,12 @@ export const makeMcpSurface = (target: Target, runDir?: string): McpSurface => ( · 130 unmodified lines
302303 const serverName = `${target.name}-${randomUUID().slice(0, 8)}`;
303304 // Per-connection settings ride the MCP endpoint query, the ecosystem
304305 // convention: `elicitation_mode` so a paused execution yields an approvalUrl
305− // instead of letting the model resume inline, and `artifacts=true` to opt
306− // the session in to the artifact surface. Both are non-defaults, so a
306+ // instead of letting the model resume inline, and `artifacts=false` to opt
307+ // the session out of the artifact surface. Both are non-defaults, so a
307308 // plain session's URL carries no query at all.
308309 const sessionQuery = [
309310 ...(options?.elicitationMode ? [`elicitation_mode=${options.elicitationMode}`] : []),
310− ...(options?.artifacts === true ? ["artifacts=true"] : []),
311+ ...(options?.artifacts === false ? ["artifacts=false"] : []),
311312 ].join("&");
312313 const sessionUrl = sessionQuery ? `${mcpUrl}?${sessionQuery}` : mcpUrl;
313314
packages/core/execution/src/skills.ts+1−1
@@ -609,7 +609,7 @@ export const SKILLS: readonly Skill[] = [ · 608 unmodified lines
609609 ];
610610
611611 /** 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 tool
612+ * connection that opted out of artifacts (`?artifacts=false`) has no tool
613613 * to apply them to, so they are dropped from its catalog rather than served as
614614 * guidance for a surface it cannot reach. */
615615 const 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
4141 readonly userId: string;
4242 readonly elicitationMode: McpElicitationMode;
4343 /** Whether this session serves artifacts, read off `?artifacts=` at connect
44− * time. Absent means the default (disabled). */
44+ * time. Absent means the default (enabled). */
4545 readonly artifactsEnabled?: boolean;
4646 /** The MCP resource the session was minted against (`/mcp` default vs a
4747 * `/mcp/toolkits/<slug>` toolkit), so the tool catalog is scoped to it. */
@@ -103,7 +103,7 @@ export interface SessionMeta { · 55 unmodified lines
103103 readonly elicitationMode?: McpElicitationMode;
104104 /** Whether the session serves artifacts (carried from {@link McpSessionInit}).
105105 * Absent — including for sessions persisted before the flag existed — means
106− * disabled. */
106+ * the default (enabled). */
107107 readonly artifactsEnabled?: boolean;
108108 /** The MCP resource the session serves (carried from {@link McpSessionInit});
109109 * `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
12461246 loadAppShellHtml: loadMcpAppsShellHtml,
12471247 artifacts: makeInMemoryArtifacts(),
12481248 // 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.
12501250 artifactsEnabled: true,
12511251 }),
12521252 );
packages/hosts/mcp/src/artifacts-tools.test.ts+30−27
@@ -140,11 +140,11 @@ const withClient = async <E extends Cause.YieldableError>( · 139 unmodified lines
140140 createExecutorMcpServer({
141141 engine,
142142 loadAppShellHtml: () => Promise.resolve(SHELL_HTML),
143− // Artifacts are opt-in per connection, and nearly every test in this file
144− // exercises the artifact surface — so the helper connects the way a real
145− // `?artifacts=true` client does. The cases that assert the OFF surface
146− // pass `artifactsEnabled: false`, and the default (no flag at all) is
147− // proved by building the server directly below.
143+ // Artifacts are on by default and nearly every test in this file
144+ // exercises the artifact surface; spelled out here anyway so the cases
145+ // that assert the OFF surface (`artifactsEnabled: false`) read as the
146+ // deliberate contrast. The default (no flag at all) is proved directly
147+ // in the opt-out describe block.
148148 artifactsEnabled: true,
149149 ...config,
150150 } as ExecutorMcpServerConfig<E>),
@@ -479,11 +479,11 @@ describe("MCP host — artifact tool visibility", () => { · 328 unmodified lines
479479 await serverTransport.close();
480480 });
481481
482− // A session that did not opt in (`?artifacts=true`) gets no artifact surface
483− // at all. The host here is fully artifact-capable — shell loader, store, the
484− // apps client capability — so what these assert is the connection default
485− // itself, not a host that never had artifacts.
486− it("registers no artifact tools on a default connection", async () => {
482+ // A session that opted out (`?artifacts=false`) gets no artifact surface at
483+ // all. The host here is fully artifact-capable — shell loader, store, the
484+ // apps client capability — so what these assert is the opt-out itself, not
485+ // a host that never had artifacts.
486+ it("registers no artifact tools on an opted-out connection", async () => {
487487 const store = makeArtifactStore();
488488 await withClient(
489489 makeStubEngine({}),
@@ -500,9 +500,9 @@ describe("MCP host — artifact tool visibility", () => { · 10 unmodified lines
500500 expect(names).toContain("skills");
501501 expect(names).toContain("resume");
502502 },
503− // No `artifactsEnabled` at all: the real shape of a client that connected
504− // to a bare `/mcp` endpoint.
505− { artifacts: store.port, artifactsEnabled: undefined },
503+ // The explicit opt-out: the shape of a client that connected with
504+ // `?artifacts=false`.
505+ { artifacts: store.port, artifactsEnabled: false },
506506 );
507507 });
508508
@@ -510,7 +510,7 @@ describe("MCP host — artifact tool visibility", () => { · 1 unmodified line
510510 // The read is what proves it: with no resource registered at all, the SDK
511511 // does not install a resources handler either, so `resources/list` itself is
512512 // 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 () => {
514514 const store = makeArtifactStore();
515515 await withClient(
516516 makeStubEngine({}),
@@ -518,11 +518,11 @@ describe("MCP host — artifact tool visibility", () => { · 1 unmodified line
518518 async (client) => {
519519 await expect(client.readResource({ uri: MCP_APPS_SHELL_RESOURCE_URI })).rejects.toThrow();
520520 },
521− { artifacts: store.port, artifactsEnabled: undefined },
521+ { artifacts: store.port, artifactsEnabled: false },
522522 );
523523 });
524524
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 () => {
526526 const store = makeArtifactStore();
527527 await withClient(
528528 makeStubEngine({}),
@@ -542,11 +542,11 @@ describe("MCP host — artifact tool visibility", () => { · 13 unmodified lines
542542 expect(fetched.isError).toBe(true);
543543 expect(textOf(fetched)).toContain('No skill named "create-artifact"');
544544 },
545− { artifacts: store.port, artifactsEnabled: undefined },
545+ { artifacts: store.port, artifactsEnabled: false },
546546 );
547547 });
548548
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 () => {
550550 const store = makeArtifactStore();
551551 await withClient(
552552 makeStubEngine({}),
@@ -566,7 +566,9 @@ describe("MCP host — artifact tool visibility", () => { · 13 unmodified lines
566566 expect(index).toContain("`create-artifact`");
567567 expect(index).toContain("`artifact-style`");
568568 },
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 },
570572 );
571573 });
572574
@@ -2235,24 +2237,25 @@ describe("artifactUrlFor", () => { · 1662 unmodified lines
22352237 // The `?artifacts=` endpoint query
22362238 // ---------------------------------------------------------------------------
22372239
2238−describe("artifacts opt-in query", () => {
2240+describe("artifacts opt-out query", () => {
22392241 const read = (query: string) =>
22402242 readArtifactsEnabled(new Request(`https://executor.example/mcp${query}`));
22412243
2242− it("keeps artifacts off for a clean endpoint", () => {
2243− expect(read("")).toBe(false);
2244− expect(read("?elicitation_mode=browser")).toBe(false);
2244+ it("serves artifacts on a clean endpoint", () => {
2245+ expect(read("")).toBe(true);
2246+ expect(read("?elicitation_mode=browser")).toBe(true);
22452247 });
22462248
2247− it("turns artifacts on for the documented true values", () => {
2249+ it("keeps artifacts on for the explicit true values", () => {
22482250 for (const value of ["true", "TRUE", "1", "yes", "on"]) {
22492251 expect(read(`?artifacts=${value}`)).toBe(true);
22502252 }
22512253 });
22522254
2253− // Anything else reads as "not an opt-in", so a typo cannot silently hand a
2254− // connection a surface the user never asked for.
2255− it("keeps artifacts off for any other value", () => {
2255+ // Once the flag is present, anything but a documented true value reads as
2256+ // the opt-out — a user who touched the flag at all was reaching for "off",
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", () => {
22562259 for (const value of ["false", "0", "no", "ture", ""]) {
22572260 expect(read(`?artifacts=${value}`)).toBe(false);
22582261 }
packages/hosts/mcp/src/browser-approval.ts+8−7
@@ -52,16 +52,17 @@ export const readElicitationMode = (request: Request): McpElicitationMode => { · 51 unmodified lines
5252 };
5353
5454 /**
55− * Read the artifacts opt-in off an MCP request's `?artifacts=` query.
56− * Artifacts are OFF by default, so a clean endpoint URL serves no artifact
57− * surface; only an explicit true value (`?artifacts=true`, and the same truthy
58− * spellings the rest of the query vocabulary accepts) turns them on. A session
59− * without the opt-in gets no artifact tools, no `ui://` shell resource, and no
60− * artifact skills — as if the host had never been configured for them.
55+ * Read the artifacts opt-OUT off an MCP request's `?artifacts=` query.
56+ * Artifacts are ON by default: a clean endpoint URL gets the full artifact
57+ * surface, and only an explicit false value (`?artifacts=false`, or any
58+ * spelling that isn't one of the accepted truthy forms) withholds it. A
59+ * session that opts out gets no artifact tools, no `ui://` shell resource,
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.
6162 */
6263 export const readArtifactsEnabled = (request: Request): boolean => {
6364 const value = new URL(request.url).searchParams.get("artifacts");
64− if (value === null) return false;
65+ if (value === null) return true;
6566 return TRUE_QUERY_VALUES.has(value.toLowerCase());
6667 };
6768
packages/hosts/mcp/src/in-memory-session-store.ts+3−3
@@ -71,9 +71,9 @@ export interface McpBuildServerOptions { · 70 unmodified lines
7171 | { readonly mode: "model" }
7272 | { readonly mode: "native" };
7373 readonly browserApprovalStore?: BrowserApprovalStore;
74− /** Whether this session serves artifacts. True only when the client connected
75− * with `?artifacts=true`; otherwise the built server registers none of the
76− * artifact tools, resource, or skills. */
74+ /** Whether this session serves artifacts. True unless the client connected
75+ * with `?artifacts=false`; opted out, the built server registers none of
76+ * the artifact tools, resource, or skills. */
7777 readonly artifactsEnabled?: boolean;
7878 }
7979
packages/hosts/mcp/src/tool-server.ts+5−5
@@ -156,8 +156,8 @@ type SharedMcpServerConfig = { · 155 unmodified lines
156156 */
157157 readonly loadAppShellHtml?: () => Promise<string>;
158158 /**
159− * Per-connection artifacts opt-in. Defaults to false. A client that connects
160− * without `?artifacts=true` gets NO artifact surface at all: none of the five
159+ * Per-connection artifacts opt-out. Defaults to true. A client that connects
160+ * with `?artifacts=false` gets NO artifact surface at all: none of the five
161161 * artifact tools, no `ui://` shell resource, and no artifact entries in the
162162 * `skills` inventory — the same shape a host without `loadAppShellHtml`
163163 * serves. `execute`, `skills` and `resume` are untouched.
@@ -1037,10 +1037,10 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 873 unmodified lines
10371037 // The same live integration inventory the description carries, re-used by
10381038 // the `skills` tool so the `execute` guide lists what is connected too.
10391039 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`).
10411041 // One flag decides the whole surface: the tools, the shell resource, and
10421042 // the skills catalog below.
1043− const artifactsEnabled = config.artifactsEnabled ?? false;
1043+ const artifactsEnabled = config.artifactsEnabled ?? true;
10441044 const skillCatalog: readonly Skill[] = skillCatalogFor({ artifacts: artifactsEnabled });
10451045
10461046 // Captured at construction time. SDK callbacks fire later (often
@@ -1812,7 +1812,7 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 765 unmodified lines
18121812 );
18131813
18141814 // 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`).
18161816 // Either way nothing below registers, so a disabled session is byte-for-byte
18171817 // a session on a host that never had artifacts.
18181818 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
7474 );
7575 });
7676
77− // Artifacts are off by default, so only the opt-in is ever spelled out: a
77+ // Artifacts are on by default, so only the opt-out is ever spelled out: a
7878 // card left alone must still produce the bare endpoint every doc and test
7979 // 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", () => {
8181 expect(
8282 buildMcpHttpEndpoint({
8383 origin: "https://executor.example",
8484 desktop: null,
85− artifacts: false,
85+ artifacts: true,
8686 }),
8787 ).toBe("https://executor.example/mcp");
8888
@@ -90,9 +90,9 @@ describe("MCP install command rendering", () => { · 1 unmodified line
9090 buildMcpHttpEndpoint({
9191 origin: "https://executor.example",
9292 desktop: null,
93− artifacts: true,
93+ artifacts: false,
9494 }),
95− ).toBe("https://executor.example/mcp?artifacts=true");
95+ ).toBe("https://executor.example/mcp?artifacts=false");
9696
9797 // Both non-defaults together, in the order the card renders them.
9898 expect(
@@ -100,25 +100,25 @@ describe("MCP install command rendering", () => { · 1 unmodified line
100100 origin: "https://executor.example",
101101 desktop: null,
102102 elicitationMode: "browser",
103− artifacts: true,
103+ artifacts: false,
104104 }),
105− ).toBe("https://executor.example/mcp?elicitation_mode=browser&artifacts=true");
105+ ).toBe("https://executor.example/mcp?elicitation_mode=browser&artifacts=false");
106106
107107 // The unknown-origin placeholder is not a parsable URL, so it is
108108 // concatenated rather than round-tripped through `URL`.
109− expect(buildMcpHttpEndpoint({ origin: null, desktop: null, artifacts: true })).toBe(
110− "<this-server>/mcp?artifacts=true",
109+ expect(buildMcpHttpEndpoint({ origin: null, desktop: null, artifacts: false })).toBe(
110+ "<this-server>/mcp?artifacts=false",
111111 );
112112 });
113113
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", () => {
115115 expect(
116− buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: false }),
116+ buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: true }),
117117 ).toBe("npx add-mcp 'executor mcp' --name executor");
118118
119119 expect(
120− buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: true }),
121− ).toBe("npx add-mcp 'executor mcp --artifacts' --name executor");
120+ buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: false }),
121+ ).toBe("npx add-mcp 'executor mcp --no-artifacts' --name executor");
122122 });
123123
124124 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
4949 readonly port: number;
5050 } | null;
5151 readonly elicitationMode?: McpElicitationMode;
52− /** Artifacts are off by default, so only the opt-in is spelled out on the
53− * URL (`&artifacts=true`) and a default endpoint stays clean. */
52+ /** Artifacts are on by default, so only the opt-out is spelled out on the
53+ * URL (`&artifacts=false`) and a default endpoint stays clean. */
5454 readonly artifacts?: boolean;
5555 // Cloud only: pins the URL to `/<org-slug>/mcp` (the server also accepts the
5656 // legacy `/<org_id>/mcp` form). Desktop/local pass nothing and get the bare
@@ -67,12 +67,12 @@ export const buildMcpHttpEndpoint = (input: { · 10 unmodified lines
6767 ? `${input.origin}${mcpPath}`
6868 : `<this-server>${mcpPath}`;
6969 // 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.
7171 const params: Array<readonly [string, string]> = [];
7272 if (input.elicitationMode && input.elicitationMode !== "model") {
7373 params.push(["elicitation_mode", input.elicitationMode]);
7474 }
75− if (input.artifacts === true) params.push(["artifacts", "true"]);
75+ if (input.artifacts === false) params.push(["artifacts", "false"]);
7676 if (params.length === 0) return endpoint;
7777
7878 const query = params.map(([key, value]) => `${key}=${value}`).join("&");
@@ -127,8 +127,8 @@ export const buildMcpInstallCommand = (input: { · 48 unmodified lines
127127 if (input.elicitationMode && input.elicitationMode !== "model") {
128128 innerArgs.push("--elicitation-mode", input.elicitationMode);
129129 }
130− if (input.artifacts === true) {
131− innerArgs.push("--artifacts");
130+ if (input.artifacts === false) {
131+ innerArgs.push("--no-artifacts");
132132 }
133133 return `npx add-mcp ${shellQuoteWord(innerArgs.map(shellQuoteWord).join(" "))} --name executor`;
134134 };
@@ -137,7 +137,7 @@ export function McpInstallCard(props: { className?: string }) { · 2 unmodified lines
137137 const [mode, setMode] = useState<TransportMode>("http");
138138 const [advancedOpen, setAdvancedOpen] = useState(false);
139139 const [httpElicitationMode, setHttpElicitationMode] = useState<McpElicitationMode>("model");
140− const [artifacts, setArtifacts] = useState(false);
140+ const [artifacts, setArtifacts] = useState(true);
141141 const organizationSlug = useOrganizationSlug();
142142 const serverConnection = useExecutorServerConnection();
143143 // Desktop hosts ship Electron without putting an `executor` binary on
@@ -208,7 +208,7 @@ export function McpInstallCard(props: { className?: string }) { · 64 unmodified lines
208208 <div className="mt-0.5 text-xs leading-5 text-muted-foreground">
209209 {artifacts
210210 ? "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."}
212212 </div>
213213 </div>
214214 <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