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.
Unified · Split
| @@ -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. | |
| @@ -1346,9 +1346,10 @@ const mcpUrlForActiveLocalServer = (input: { · 1345 unmodified lines | ||
| 1346 | 1346 | if (input.elicitationMode === "browser") { |
| 1347 | 1347 | url.searchParams.set("elicitation_mode", "browser"); |
| 1348 | 1348 | } |
| 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"); | |
| 1352 | 1353 | } |
| 1353 | 1354 | return url; |
| 1354 | 1355 | }; |
| @@ -2802,18 +2803,18 @@ const mcpCommand = Command.make( · 1447 unmodified lines | ||
| 2802 | 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 | 2807 | .pipe(Options.withDefault(false)) |
| 2807 | 2808 | .pipe( |
| 2808 | 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 | 2815 | Effect.gen(function* () { |
| 2815 | 2816 | applyScope(scope); |
| 2816 | − yield* runStdioMcpSession({ elicitationMode, artifacts }); | |
| 2817 | + yield* runStdioMcpSession({ elicitationMode, artifacts: !noArtifacts }); | |
| 2817 | 2818 | }), |
| 2818 | 2819 | ).pipe(Command.withDescription("Start an MCP server over stdio")); |
| 2819 | 2820 | |
| @@ -276,10 +276,10 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 275 unmodified lines | ||
| 276 | 276 | description, |
| 277 | 277 | artifacts: executor.artifacts, |
| 278 | 278 | 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, | |
| 283 | 283 | // Cold restores rebuild this server with no `initialize` to replay, so |
| 284 | 284 | // the negotiated apps support comes back from storage instead. |
| 285 | 285 | restoredAppsEnabled: sessionMeta.appsEnabled ?? false, |
| @@ -145,10 +145,10 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession · 144 unmodified lines | ||
| 145 | 145 | engine, |
| 146 | 146 | artifacts: executor.artifacts, |
| 147 | 147 | 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, | |
| 152 | 152 | // Cold restores rebuild this server with no `initialize` to replay, so |
| 153 | 153 | // the negotiated apps support comes back from storage instead. |
| 154 | 154 | restoredAppsEnabled: sessionMeta.appsEnabled ?? false, |
| @@ -92,8 +92,8 @@ scenario( · 91 unmodified lines | ||
| 92 | 92 | const mcp = yield* Mcp; |
| 93 | 93 | const identity = yield* target.newIdentity(); |
| 94 | 94 | 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; | |
| 97 | 97 | |
| 98 | 98 | const sessionId = yield* Effect.promise(() => openSession(mcpUrl, bearer)); |
| 99 | 99 | |
| @@ -197,9 +197,9 @@ scenario( · 196 unmodified lines | ||
| 197 | 197 | |
| 198 | 198 | const identity = yield* target.newIdentity(); |
| 199 | 199 | 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); | |
| 203 | 203 | |
| 204 | 204 | const suffix = uniqueSuffix(); |
| 205 | 205 | const title = `Loading Surface ${suffix}`; |
| @@ -145,9 +145,9 @@ scenario( · 144 unmodified lines | ||
| 145 | 145 | const browser = yield* Browser; |
| 146 | 146 | |
| 147 | 147 | 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); | |
| 151 | 151 | const suffix = uniqueSuffix(); |
| 152 | 152 | |
| 153 | 153 | const seeded = [ |
| @@ -166,9 +166,9 @@ scenario( · 165 unmodified lines | ||
| 166 | 166 | |
| 167 | 167 | const identity = yield* target.newIdentity(); |
| 168 | 168 | 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); | |
| 172 | 172 | |
| 173 | 173 | const suffix = uniqueSuffix(); |
| 174 | 174 | const title = `Release Readiness ${suffix}`; |
| @@ -556,9 +556,9 @@ scenario( · 381 unmodified lines | ||
| 556 | 556 | |
| 557 | 557 | const identity = yield* target.newIdentity(); |
| 558 | 558 | 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); | |
| 562 | 562 | |
| 563 | 563 | const suffix = uniqueSuffix(); |
| 564 | 564 | const originalTitle = `Draft Dashboard ${suffix}`; |
| @@ -677,9 +677,9 @@ scenario( · 112 unmodified lines | ||
| 677 | 677 | |
| 678 | 678 | const identity = yield* target.newIdentity(); |
| 679 | 679 | 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); | |
| 683 | 683 | |
| 684 | 684 | const suffix = uniqueSuffix(); |
| 685 | 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 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. | |
| 782 | 781 | 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", | |
| 784 | 783 | { timeout: 120_000 }, |
| 785 | 784 | Effect.gen(function* () { |
| 786 | 785 | const target = yield* Target; |
| @@ -788,7 +787,7 @@ scenario( · 1 unmodified line | ||
| 788 | 787 | |
| 789 | 788 | const identity = yield* target.newIdentity(); |
| 790 | 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 | 792 | const ARTIFACT_TOOLS = [ |
| 794 | 793 | "create-artifact", |
| @@ -798,32 +797,32 @@ scenario( · 3 unmodified lines | ||
| 798 | 797 | "execute-action-resume", |
| 799 | 798 | ] as const; |
| 800 | 799 | |
| 801 | − const defaultTools = yield* defaultSession.listTools(); | |
| 800 | + const optedOutTools = yield* optedOut.listTools(); | |
| 802 | 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 | 804 | // 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"); | |
| 808 | 807 | |
| 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`"); | |
| 812 | 811 | expect( |
| 813 | − defaultSkills.text, | |
| 812 | + optedOutSkills.text, | |
| 814 | 813 | "the index does not advertise a how-to for a tool this session lacks", |
| 815 | 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, | |
| 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(); | |
| 822 | 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", {}); | |
| 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( | |
| 827 | 826 | "`create-artifact`", |
| 828 | 827 | ); |
| 829 | 828 | }), |
| @@ -164,8 +164,9 @@ export interface McpSurface { · 163 unmodified lines | ||
| 164 | 164 | identity: Identity, |
| 165 | 165 | options?: { |
| 166 | 166 | 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. */ | |
| 169 | 170 | readonly artifacts?: boolean; |
| 170 | 171 | readonly url?: string; |
| 171 | 172 | }, |
| @@ -302,12 +303,12 @@ export const makeMcpSurface = (target: Target, runDir?: string): McpSurface => ( · 130 unmodified lines | ||
| 302 | 303 | const serverName = `${target.name}-${randomUUID().slice(0, 8)}`; |
| 303 | 304 | // Per-connection settings ride the MCP endpoint query, the ecosystem |
| 304 | 305 | // 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 | |
| 307 | 308 | // plain session's URL carries no query at all. |
| 308 | 309 | const sessionQuery = [ |
| 309 | 310 | ...(options?.elicitationMode ? [`elicitation_mode=${options.elicitationMode}`] : []), |
| 310 | − ...(options?.artifacts === true ? ["artifacts=true"] : []), | |
| 311 | + ...(options?.artifacts === false ? ["artifacts=false"] : []), | |
| 311 | 312 | ].join("&"); |
| 312 | 313 | const sessionUrl = sessionQuery ? `${mcpUrl}?${sessionQuery}` : mcpUrl; |
| 313 | 314 | |
| @@ -609,7 +609,7 @@ export const SKILLS: readonly Skill[] = [ · 608 unmodified lines | ||
| 609 | 609 | ]; |
| 610 | 610 | |
| 611 | 611 | /** 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 | |
| 613 | 613 | * to apply them to, so they are dropped from its catalog rather than served as |
| 614 | 614 | * guidance for a surface it cannot reach. */ |
| 615 | 615 | const ARTIFACT_SKILLS: ReadonlySet<Skill> = new Set([CREATE_ARTIFACT_SKILL, ARTIFACT_STYLE_SKILL]); |
| @@ -41,7 +41,7 @@ export interface McpSessionInit { · 40 unmodified lines | ||
| 41 | 41 | readonly userId: string; |
| 42 | 42 | readonly elicitationMode: McpElicitationMode; |
| 43 | 43 | /** 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 | 45 | readonly artifactsEnabled?: boolean; |
| 46 | 46 | /** The MCP resource the session was minted against (`/mcp` default vs a |
| 47 | 47 | * `/mcp/toolkits/<slug>` toolkit), so the tool catalog is scoped to it. */ |
| @@ -103,7 +103,7 @@ export interface SessionMeta { · 55 unmodified lines | ||
| 103 | 103 | readonly elicitationMode?: McpElicitationMode; |
| 104 | 104 | /** Whether the session serves artifacts (carried from {@link McpSessionInit}). |
| 105 | 105 | * Absent — including for sessions persisted before the flag existed — means |
| 106 | − * disabled. */ | |
| 106 | + * the default (enabled). */ | |
| 107 | 107 | readonly artifactsEnabled?: boolean; |
| 108 | 108 | /** The MCP resource the session serves (carried from {@link McpSessionInit}); |
| 109 | 109 | * `buildMcpServer` scopes the tool catalog to it. */ |
| @@ -1246,7 +1246,7 @@ const startMcpHarnessForEngine = async <E extends Cause.YieldableError>( · 1245 unmodified lines | ||
| 1246 | 1246 | loadAppShellHtml: loadMcpAppsShellHtml, |
| 1247 | 1247 | artifacts: makeInMemoryArtifacts(), |
| 1248 | 1248 | // 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 | 1250 | artifactsEnabled: true, |
| 1251 | 1251 | }), |
| 1252 | 1252 | ); |
| @@ -140,11 +140,11 @@ const withClient = async <E extends Cause.YieldableError>( · 139 unmodified lines | ||
| 140 | 140 | createExecutorMcpServer({ |
| 141 | 141 | engine, |
| 142 | 142 | 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. | |
| 148 | 148 | artifactsEnabled: true, |
| 149 | 149 | ...config, |
| 150 | 150 | } as ExecutorMcpServerConfig<E>), |
| @@ -479,11 +479,11 @@ describe("MCP host — artifact tool visibility", () => { · 328 unmodified lines | ||
| 479 | 479 | await serverTransport.close(); |
| 480 | 480 | }); |
| 481 | 481 | |
| 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 () => { | |
| 487 | 487 | const store = makeArtifactStore(); |
| 488 | 488 | await withClient( |
| 489 | 489 | makeStubEngine({}), |
| @@ -500,9 +500,9 @@ describe("MCP host — artifact tool visibility", () => { · 10 unmodified lines | ||
| 500 | 500 | expect(names).toContain("skills"); |
| 501 | 501 | expect(names).toContain("resume"); |
| 502 | 502 | }, |
| 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 }, | |
| 506 | 506 | ); |
| 507 | 507 | }); |
| 508 | 508 | |
| @@ -510,7 +510,7 @@ describe("MCP host — artifact tool visibility", () => { · 1 unmodified line | ||
| 510 | 510 | // The read is what proves it: with no resource registered at all, the SDK |
| 511 | 511 | // does not install a resources handler either, so `resources/list` itself is |
| 512 | 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 | 514 | const store = makeArtifactStore(); |
| 515 | 515 | await withClient( |
| 516 | 516 | makeStubEngine({}), |
| @@ -518,11 +518,11 @@ describe("MCP host — artifact tool visibility", () => { · 1 unmodified line | ||
| 518 | 518 | async (client) => { |
| 519 | 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 | 526 | const store = makeArtifactStore(); |
| 527 | 527 | await withClient( |
| 528 | 528 | makeStubEngine({}), |
| @@ -542,11 +542,11 @@ describe("MCP host — artifact tool visibility", () => { · 13 unmodified lines | ||
| 542 | 542 | expect(fetched.isError).toBe(true); |
| 543 | 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 | 550 | const store = makeArtifactStore(); |
| 551 | 551 | await withClient( |
| 552 | 552 | makeStubEngine({}), |
| @@ -566,7 +566,9 @@ describe("MCP host — artifact tool visibility", () => { · 13 unmodified lines | ||
| 566 | 566 | expect(index).toContain("`create-artifact`"); |
| 567 | 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 | 2237 | // The `?artifacts=` endpoint query |
| 2236 | 2238 | // --------------------------------------------------------------------------- |
| 2237 | 2239 | |
| 2238 | −describe("artifacts opt-in query", () => { | |
| 2240 | +describe("artifacts opt-out query", () => { | |
| 2239 | 2241 | const read = (query: string) => |
| 2240 | 2242 | readArtifactsEnabled(new Request(`https://executor.example/mcp${query}`)); |
| 2241 | 2243 | |
| 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); | |
| 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 | 2250 | for (const value of ["true", "TRUE", "1", "yes", "on"]) { |
| 2249 | 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 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", () => { | |
| 2256 | 2259 | for (const value of ["false", "0", "no", "ture", ""]) { |
| 2257 | 2260 | expect(read(`?artifacts=${value}`)).toBe(false); |
| 2258 | 2261 | } |
| @@ -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. | |
| 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. | |
| 61 | 62 | */ |
| 62 | 63 | export const readArtifactsEnabled = (request: Request): boolean => { |
| 63 | 64 | const value = new URL(request.url).searchParams.get("artifacts"); |
| 64 | − if (value === null) return false; | |
| 65 | + if (value === null) return true; | |
| 65 | 66 | return TRUE_QUERY_VALUES.has(value.toLowerCase()); |
| 66 | 67 | }; |
| 67 | 68 | |
| @@ -71,9 +71,9 @@ export interface McpBuildServerOptions { · 70 unmodified lines | ||
| 71 | 71 | | { readonly mode: "model" } |
| 72 | 72 | | { readonly mode: "native" }; |
| 73 | 73 | 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. */ | |
| 77 | 77 | readonly artifactsEnabled?: boolean; |
| 78 | 78 | } |
| 79 | 79 | |
| @@ -156,8 +156,8 @@ type SharedMcpServerConfig = { · 155 unmodified lines | ||
| 156 | 156 | */ |
| 157 | 157 | readonly loadAppShellHtml?: () => Promise<string>; |
| 158 | 158 | /** |
| 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 | |
| 161 | 161 | * artifact tools, no `ui://` shell resource, and no artifact entries in the |
| 162 | 162 | * `skills` inventory — the same shape a host without `loadAppShellHtml` |
| 163 | 163 | * serves. `execute`, `skills` and `resume` are untouched. |
| @@ -1037,10 +1037,10 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 873 unmodified lines | ||
| 1037 | 1037 | // The same live integration inventory the description carries, re-used by |
| 1038 | 1038 | // the `skills` tool so the `execute` guide lists what is connected too. |
| 1039 | 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 | 1041 | // One flag decides the whole surface: the tools, the shell resource, and |
| 1042 | 1042 | // the skills catalog below. |
| 1043 | − const artifactsEnabled = config.artifactsEnabled ?? false; | |
| 1043 | + const artifactsEnabled = config.artifactsEnabled ?? true; | |
| 1044 | 1044 | const skillCatalog: readonly Skill[] = skillCatalogFor({ artifacts: artifactsEnabled }); |
| 1045 | 1045 | |
| 1046 | 1046 | // 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 | 1814 | // 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 | 1816 | // Either way nothing below registers, so a disabled session is byte-for-byte |
| 1817 | 1817 | // a session on a host that never had artifacts. |
| 1818 | 1818 | const loadAppShellHtml = artifactsEnabled ? config.loadAppShellHtml : undefined; |
| @@ -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: a | |
| 77 | + // Artifacts are on by default, so only the opt-out is ever spelled out: a | |
| 78 | 78 | // card left alone must still produce the bare endpoint every doc and test |
| 79 | 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 | 81 | expect( |
| 82 | 82 | buildMcpHttpEndpoint({ |
| 83 | 83 | origin: "https://executor.example", |
| 84 | 84 | desktop: null, |
| 85 | − artifacts: false, | |
| 85 | + artifacts: true, | |
| 86 | 86 | }), |
| 87 | 87 | ).toBe("https://executor.example/mcp"); |
| 88 | 88 | |
| @@ -90,9 +90,9 @@ describe("MCP install command rendering", () => { · 1 unmodified line | ||
| 90 | 90 | buildMcpHttpEndpoint({ |
| 91 | 91 | origin: "https://executor.example", |
| 92 | 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 | 97 | // Both non-defaults together, in the order the card renders them. |
| 98 | 98 | expect( |
| @@ -100,25 +100,25 @@ describe("MCP install command rendering", () => { · 1 unmodified line | ||
| 100 | 100 | origin: "https://executor.example", |
| 101 | 101 | desktop: null, |
| 102 | 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 | 107 | // The unknown-origin placeholder is not a parsable URL, so it is |
| 108 | 108 | // 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", | |
| 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 | 115 | expect( |
| 116 | − buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: false }), | |
| 116 | + buildMcpInstallCommand({ mode: "stdio", isDev: false, origin: null, artifacts: true }), | |
| 117 | 117 | ).toBe("npx add-mcp 'executor mcp' --name executor"); |
| 118 | 118 | |
| 119 | 119 | 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"); | |
| 122 | 122 | }); |
| 123 | 123 | |
| 124 | 124 | it("passes model-managed resume through stdio install commands", () => { |
| @@ -49,8 +49,8 @@ export const buildMcpHttpEndpoint = (input: { · 48 unmodified lines | ||
| 49 | 49 | readonly port: number; |
| 50 | 50 | } | null; |
| 51 | 51 | 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. */ | |
| 54 | 54 | readonly artifacts?: boolean; |
| 55 | 55 | // Cloud only: pins the URL to `/<org-slug>/mcp` (the server also accepts the |
| 56 | 56 | // 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 | 67 | ? `${input.origin}${mcpPath}` |
| 68 | 68 | : `<this-server>${mcpPath}`; |
| 69 | 69 | // 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 | 71 | const params: Array<readonly [string, string]> = []; |
| 72 | 72 | if (input.elicitationMode && input.elicitationMode !== "model") { |
| 73 | 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 | 76 | if (params.length === 0) return endpoint; |
| 77 | 77 | |
| 78 | 78 | const query = params.map(([key, value]) => `${key}=${value}`).join("&"); |
| @@ -127,8 +127,8 @@ export const buildMcpInstallCommand = (input: { · 48 unmodified lines | ||
| 127 | 127 | if (input.elicitationMode && input.elicitationMode !== "model") { |
| 128 | 128 | innerArgs.push("--elicitation-mode", input.elicitationMode); |
| 129 | 129 | } |
| 130 | − if (input.artifacts === true) { | |
| 131 | − innerArgs.push("--artifacts"); | |
| 130 | + if (input.artifacts === false) { | |
| 131 | + innerArgs.push("--no-artifacts"); | |
| 132 | 132 | } |
| 133 | 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 | 137 | const [mode, setMode] = useState<TransportMode>("http"); |
| 138 | 138 | const [advancedOpen, setAdvancedOpen] = useState(false); |
| 139 | 139 | const [httpElicitationMode, setHttpElicitationMode] = useState<McpElicitationMode>("model"); |
| 140 | − const [artifacts, setArtifacts] = useState(false); | |
| 140 | + const [artifacts, setArtifacts] = useState(true); | |
| 141 | 141 | const organizationSlug = useOrganizationSlug(); |
| 142 | 142 | const serverConnection = useExecutorServerConnection(); |
| 143 | 143 | // Desktop hosts ship Electron without putting an `executor` binary on |
| @@ -208,7 +208,7 @@ export function McpInstallCard(props: { className?: string }) { · 64 unmodified lines | ||
| 208 | 208 | <div className="mt-0.5 text-xs leading-5 text-muted-foreground"> |
| 209 | 209 | {artifacts |
| 210 | 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 | 212 | </div> |
| 213 | 213 | </div> |
| 214 | 214 | <Switch |