From a32050206a08521d2d240614a86a9561e995c6ab Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Mon, 14 Sep 2026 21:44:41 -0700
Subject: [PATCH] Keep artifacts available in search and invoke mode
---
apps/docs/mcp-proxy.mdx | 8 +-
e2e/cloud/passthrough-scale.test.ts | 2 +-
e2e/scenarios/mcp-passthrough.test.ts | 66 ++++++++++++++-
packages/core/execution/src/skills.test.ts | 14 ++++
packages/core/execution/src/skills.ts | 80 ++++++++++++++-----
.../hosts/mcp/src/passthrough-tools.test.ts | 62 +++++++++-----
packages/hosts/mcp/src/passthrough-tools.ts | 4 +-
packages/hosts/mcp/src/tool-server.ts | 16 ++--
.../react/src/components/mcp-install-card.tsx | 11 +--
9 files changed, 200 insertions(+), 63 deletions(-)
diff --git a/apps/docs/mcp-proxy.mdx b/apps/docs/mcp-proxy.mdx
index b053205f91..e0dfb27939 100644
--- a/apps/docs/mcp-proxy.mdx
+++ b/apps/docs/mcp-proxy.mdx
@@ -60,7 +60,7 @@ agent automatically.
## Search and invoke mode
Add `?mode=passthrough` to your MCP endpoint, or enable **Search and invoke**
-in the Connect card. This mode exposes four tools:
+in the Connect card. This mode exposes four discovery and invocation tools:
- `integrations`: list connected accounts with integration descriptions, account
labels, and their last recorded health. Results are paginated, with one item per
@@ -83,5 +83,7 @@ has its own pagination for records.
The tool list stays small as you add integrations. Input schemas are loaded only
for matching search results. Your client handles approval for `invoke`, and
-workspace block policies still apply. This mode does not expose code execution or
-artifact tools.
+workspace block policies still apply. This mode does not expose a general code execution tool. Artifacts remain
+available unless you disable them with `artifacts=false` or the Artifacts toggle.
+Use `skills({ name: "create-artifact" })` for the guide to building artifacts
+after discovering data with search and invoke.
diff --git a/e2e/cloud/passthrough-scale.test.ts b/e2e/cloud/passthrough-scale.test.ts
index f5513ecf5b..a5230560d2 100644
--- a/e2e/cloud/passthrough-scale.test.ts
+++ b/e2e/cloud/passthrough-scale.test.ts
@@ -35,7 +35,7 @@ scenario(
);
expect(visible.length, "the seeded catalog is large").toBeGreaterThan(3000);
- const session = mcp.session(identity, { mode: "passthrough" });
+ const session = mcp.session(identity, { mode: "passthrough", artifacts: false });
const startedAt = Date.now();
const served = yield* session.describeTools();
const elapsedMs = Date.now() - startedAt;
diff --git a/e2e/scenarios/mcp-passthrough.test.ts b/e2e/scenarios/mcp-passthrough.test.ts
index 827982f316..05b0ce5ae4 100644
--- a/e2e/scenarios/mcp-passthrough.test.ts
+++ b/e2e/scenarios/mcp-passthrough.test.ts
@@ -7,6 +7,7 @@ import { Effect, Schema } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
+ ArtifactId,
AuthTemplateSlug,
ConnectionName,
IntegrationSlug,
@@ -34,6 +35,12 @@ const decodeInventory = Schema.decodeUnknownSync(
}),
);
+const decodeArtifact = Schema.decodeUnknownSync(
+ Schema.Struct({
+ structuredContent: Schema.Struct({ artifactId: ArtifactId, url: Schema.String }),
+ }),
+);
+
const api = composePluginApi([openApiHttpPlugin()] as const);
const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`;
@@ -192,7 +199,14 @@ scenario(
await visit(page, "/");
await page.getByRole("button", { name: "Advanced" }).click();
await page.getByRole("switch", { name: "Search and invoke" }).check();
+ await page.getByRole("switch", { name: "Artifacts", exact: true }).check();
await settle(page);
+ expect(
+ await page.getByRole("switch", { name: "Artifacts", exact: true }).isEnabled(),
+ ).toBe(true);
+ expect(await page.locator("code").first().innerText()).not.toContain(
+ "artifacts=false",
+ );
expect(await page.locator("code").first().innerText()).toContain("mode=passthrough");
expect(
await page
@@ -205,10 +219,58 @@ scenario(
});
});
+ const withArtifacts = mcp.session(identity, { mode: "passthrough" });
+ expect(yield* withArtifacts.listTools()).toEqual(
+ expect.arrayContaining([
+ "search",
+ "invoke",
+ "create-artifact",
+ "edit-artifact",
+ "list-artifacts",
+ "show-artifact",
+ ]),
+ );
+ const artifactGuide = yield* withArtifacts.call("skills", { name: "create-artifact" });
+ expect(artifactGuide.ok).toBe(true);
+ expect(artifactGuide.text).toContain("invoke");
+ expect(artifactGuide.text).not.toContain("`execute`");
+ expect((yield* withArtifacts.call("list-artifacts", {})).ok).toBe(true);
+ const artifact = yield* withArtifacts.call("create-artifact", {
+ title: "Search and invoke artifact",
+ code: "function App() { return
Artifact available
; }",
+ });
+ expect(artifact.ok, artifact.text).toBe(true);
+ const saved = decodeArtifact(artifact.raw).structuredContent;
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const edited = yield* withArtifacts.call("edit-artifact", {
+ artifactId: saved.artifactId,
+ edits: [{ oldText: "Artifact available", newText: "Artifact restored" }],
+ });
+ expect(edited.ok, edited.text).toBe(true);
+ const shown = yield* withArtifacts.call("show-artifact", { id: saved.artifactId });
+ expect(shown.ok, shown.text).toBe(true);
+ expect(shown.text).toContain("Artifact restored");
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Open the artifact created through Search and invoke", async () => {
+ await visit(page, saved.url);
+ await page
+ .frameLocator('[data-testid="artifact-shell-frame"]')
+ .frameLocator("iframe")
+ .getByText("Artifact restored", { exact: true })
+ .waitFor({ timeout: 30_000 });
+ });
+ });
+ }),
+ client.artifacts
+ .remove({ params: { artifactId: saved.artifactId } })
+ .pipe(Effect.orDie),
+ );
+
const codemode = mcp.session(identity);
expect(yield* codemode.listTools()).toContain("execute");
- const passthrough = mcp.session(identity, { mode: "passthrough" });
+ const passthrough = mcp.session(identity, { mode: "passthrough", artifacts: false });
const described = yield* passthrough.describeTools();
expect(described.map((tool) => tool.name).sort()).toEqual([
"integrations",
@@ -328,7 +390,7 @@ scenario(
});
yield* Effect.ensuring(
Effect.gen(function* () {
- const afterBlock = mcp.session(identity, { mode: "passthrough" });
+ const afterBlock = mcp.session(identity, { mode: "passthrough", artifacts: false });
const afterNames = decodeToolSearch(
(yield* afterBlock.call("search", { query: slug })).raw,
).structuredContent.items.map((tool) => tool.id);
diff --git a/packages/core/execution/src/skills.test.ts b/packages/core/execution/src/skills.test.ts
index 846acb6860..49a1229cef 100644
--- a/packages/core/execution/src/skills.test.ts
+++ b/packages/core/execution/src/skills.test.ts
@@ -56,3 +56,17 @@ describe("skills registry", () => {
expect(skillCatalogFor({ artifacts: true })).toEqual(SKILLS);
});
});
+
+describe("artifact discovery guides", () => {
+ it("uses the search/invoke workflow without advertising execute", () => {
+ const catalog = skillCatalogFor({ artifacts: true, discovery: "search-invoke" });
+ expect(catalog.map((skill) => skill.name)).toEqual(["create-artifact", "artifact-style"]);
+ const body = findSkill("create-artifact", catalog)?.body;
+ expect(body).toContain("integrations");
+ expect(body).toContain("invoke");
+ expect(body).toContain("queryOptions");
+ expect(body).not.toContain("`execute`");
+ expect(body).not.toContain("connections.list");
+ expect(skillCatalogFor({ artifacts: false, discovery: "search-invoke" })).toEqual([]);
+ });
+});
diff --git a/packages/core/execution/src/skills.ts b/packages/core/execution/src/skills.ts
index b4803449ae..9bf8f43ef0 100644
--- a/packages/core/execution/src/skills.ts
+++ b/packages/core/execution/src/skills.ts
@@ -86,7 +86,7 @@ const LUCIDE_ICONS =
"Plus, Minus, Check, X, Search, Loader2, AlertCircle, ExternalLink, Copy, Trash2, Edit, Settings, User, Globe, Star, TrendingUp, Activity, Database, Shield, Package, and more";
const CREATE_ARTIFACT_SKILL_BODY = [
- "# create-artifact",
+ "## Build the artifact",
"",
"Render an interactive React UI component as an MCP app, and save it as an artifact.",
"",
@@ -103,7 +103,7 @@ const CREATE_ARTIFACT_SKILL_BODY = [
"",
"## Workflow",
"",
- "1. If you need to understand tool names, query syntax, required arguments, response shapes, IDs, mutation inputs, or a list tool's cursor field, first use the regular `execute` tool to inspect them.",
+ "1. Follow the discovery workflow above to inspect tool names, input schemas, response shapes, and pagination before writing the artifact.",
"2. Then call `create-artifact` with a component named `App` in the `code` parameter.",
"3. Recreate every read from the discovery step inside `App` with `useQuery(tools...queryOptions(args))` so the UI stays live.",
"4. Use `useMutation(tools...mutationOptions({ onSuccess }))` for user-triggered writes or actions.",
@@ -146,14 +146,14 @@ const CREATE_ARTIFACT_SKILL_BODY = [
"",
"## Addressing: Integrations, Not Connections",
"",
- "This is the one place artifact code differs from `execute` code, and getting it",
+ "Artifact code differs from the full tool IDs used during discovery. Getting it",
"wrong is rejected outright.",
"",
- "`execute` addresses a tool by its full five-segment address, because discovery has",
+ "A tool ID includes its full five-segment address, because discovery has",
"to say exactly which saved connection it means:",
"",
"```",
- "return await tools.linear.org.linearProd.issues.list({ first: 20 })",
+ "tools.linear.org.linearProd.issues.list",
"```",
"",
"Artifact code drops the middle two segments and names only the INTEGRATION:",
@@ -174,8 +174,8 @@ const CREATE_ARTIFACT_SKILL_BODY = [
'connections: { "linear": "linear.org.linearProd" }',
"```",
"",
- "The value is the `..` triple — exactly the",
- "`address` field from `tools.executor.coreTools.connections.list({})`, minus the",
+ "The value is the `..` triple.",
+ "Use the integration, owner, and connection from discovery. Omit the",
"leading `tools.`. The key is the ROLE, which for a single-account artifact is just",
"the integration slug.",
"",
@@ -188,12 +188,11 @@ const CREATE_ARTIFACT_SKILL_BODY = [
'// connections: { "prod": "linear.org.linearProd", "staging": "linear.user.myLinear" }',
"```",
"",
- "Worked example, end to end. Discovery through `execute` shows the full address:",
+ "For example, discovery identifies the account and its operation:",
"",
"```",
- "await tools.executor.coreTools.connections.list({})",
- '// -> [{ address: "tools.linear.org.linearProd", integration: "linear", ... }]',
- "return await tools.linear.org.linearProd.issues.list({ first: 5 })",
+ "Integration: linear; owner: org; connection: linearProd",
+ "Tool ID: tools.linear.org.linearProd.issues.list; input: { first: 5 }",
"```",
"",
"The artifact you then create says:",
@@ -203,8 +202,7 @@ const CREATE_ARTIFACT_SKILL_BODY = [
'connections: { "linear": "linear.org.linearProd" } // optional if linearProd is your only linear connection',
"```",
"",
- "System tools keep their usual paths and need no binding: `tools.search(...)`,",
- "`tools.describe.tool(...)`, `tools.executor.coreTools.*`.",
+ "Keep tool discovery outside the component. The component uses integration operations.",
"",
"## The Contract: tools.* Only",
"",
@@ -215,12 +213,12 @@ const CREATE_ARTIFACT_SKILL_BODY = [
"- **Never hand-roll `useQuery({ queryKey, queryFn })`.** Always pass the proxy's options object: `useQuery(tools...queryOptions(args))`. A hand-written `queryKey` is invisible to `queryFilter`/`pathFilter`, so mutations silently stop refreshing the UI, and it hides which tool the artifact uses from artifact analysis.",
"- **Never fetch in a loop by hand.** Cursor pagination is declarative — see below.",
"",
- "## Using Execute For Discovery",
+ "## Keep Discovered Data Live",
"",
- "- `execute` is for exploration: list datasets, inspect schemas, test a query, fetch one small sample row, or learn the exact mutation input shape.",
+ "- Use the discovery workflow above to inspect schemas, test a query, fetch a small sample, or learn mutation inputs.",
"- `create-artifact` is for the final interactive surface. Do not paste discovery results into JSX as literal rows, cards, summaries, metrics, or chart series.",
- "- After discovering an API call with `execute`, put the same call in TanStack Query options inside the generated component.",
- "- Example discovery: call `execute` with `return await tools.axiom_mcp.querydataset({ ... })` to confirm columns, then call `create-artifact` with `useQuery(tools.axiom_mcp.querydataset.queryOptions({ ... }))`.",
+ "- After discovering an API call, put that integration operation in TanStack Query options inside the generated component.",
+ "- For example, inspect a dataset query to confirm columns, then use its integration operation with `.queryOptions(...)` in the component.",
"- Use discovered result shapes exactly. If a sample or schema returns `{ renew, expiresAt }`, read `data?.renew`, not `data?.domain?.renew`.",
"- Keep discovery small. Use limits, narrow time ranges, or schema/list tools when possible.",
"",
@@ -243,7 +241,7 @@ const CREATE_ARTIFACT_SKILL_BODY = [
"",
"- `getNextPageParam(lastPage, allPages)` reads the cursor out of the tool's own response. Return `undefined` (or `null`) when there are no more pages — that is what stops the paging.",
"- `initialPageParam` defaults to `null`, which means the FIRST request carries no cursor at all. Set it only when a tool requires an explicit starting value (e.g. `initialPageParam: 1` for page numbers).",
- '- `cursorKey` says where the page param lands in the tool input. It defaults to `"cursor"`. Use a dotted path for nested inputs — `cursorKey: "query.since"` writes `{ query: { since: } }`. Read the tool\'s input shape with `execute` first; do not guess the field name.',
+ '- `cursorKey` says where the page param lands in the tool input. It defaults to `"cursor"`. Use a dotted path for nested inputs — `cursorKey: "query.since"` writes `{ query: { since: } }`. Read the tool\'s input schema during discovery first; do not guess the field name.',
"- Render `data.pages` (an array of tool results, newest page last) and drive further loading from `hasNextPage` / `fetchNextPage` / `isFetchingNextPage`. Do not call `fetchNextPage` in a loop on mount — let the user pull more, or paginate deliberately with a bounded `useEffect`.",
"",
"**Never chain `useQuery` calls in a loop to page through a cursor.** This is",
@@ -326,7 +324,7 @@ const CREATE_ARTIFACT_SKILL_BODY = [
"",
"## Rules",
"",
- "- Use this tool instead of `execute` whenever the output should be an interactive UI.",
+ "- Use this tool whenever the output should be an interactive UI.",
"- Export a component named `App`. A top-level `const config = { maxHeight }` caps the frame height where the artifact is embedded in a scrolling page; it is ignored where the artifact has been given the whole viewport.",
'- Lay the artifact out as an APP, not a document: root `flex h-full flex-col`, headers and filters as ordinary children, and the one long list or table as `flex-1 min-h-0 overflow-auto` so it scrolls under a header that stays put. See `skills({ name: "artifact-style" })`.',
"- Do not call API tools first and paste returned data into JSX.",
@@ -347,11 +345,32 @@ const CREATE_ARTIFACT_SKILL_BODY = [
"- Clients that cannot display MCP apps get a link to the artifact in the web app instead; pass that URL on to the user verbatim.",
].join("\n");
+const createArtifactSkillBody = (searchAndInvoke: boolean): string =>
+ [
+ "# create-artifact",
+ "",
+ "## Discover accounts and data",
+ "",
+ ...(searchAndInvoke
+ ? [
+ "1. Call `integrations({})` to choose an account. Its `integration`, `owner`, and `connection` fields form the artifact binding, joined with dots.",
+ '2. Call `search({ query: "list issues", integration: "linear", owner: "org", connection: "linearProd" })` using that account. Read the returned inputSchema.',
+ "3. Call `invoke({ tool: , arguments: })` to inspect a small sample response. Use search and invoke, not general code execution, to learn the data shape.",
+ ]
+ : [
+ '1. Use `execute` to call `tools.search({ query: "list issues" })`, then `tools.describe.tool({ path })` to inspect the matched tool.',
+ "2. Use `tools.executor.coreTools.connections.list({})` for saved accounts. The connection address without its leading `tools.` is the artifact binding.",
+ "3. Call the discovered tool in `execute` to inspect a small sample response.",
+ ]),
+ "",
+ CREATE_ARTIFACT_SKILL_BODY,
+ ].join("\n");
+
export const CREATE_ARTIFACT_SKILL: Skill = {
name: "create-artifact",
summary:
"How to write a React component for the create-artifact tool: discover data with execute, keep it live with TanStack Query (including cursor pagination), and what is already in scope.",
- body: CREATE_ARTIFACT_SKILL_BODY,
+ body: createArtifactSkillBody(false),
};
// The design system, kept SEPARATE from the capability manifest above.
@@ -631,8 +650,25 @@ const ARTIFACT_SKILLS: ReadonlySet = new Set([CREATE_ARTIFACT_SKILL, ARTI
* artifact skills so the index never advertises a doc for tools this connection
* does not have; a session that opted in gets the full {@link SKILLS} list.
*/
-export const skillCatalogFor = (options: { readonly artifacts: boolean }): readonly Skill[] =>
- options.artifacts ? SKILLS : SKILLS.filter((skill) => !ARTIFACT_SKILLS.has(skill));
+export const skillCatalogFor = (options: {
+ readonly artifacts: boolean;
+ readonly discovery?: "execute" | "search-invoke";
+}): readonly Skill[] => {
+ if (options.discovery === "search-invoke") {
+ return options.artifacts
+ ? [
+ {
+ ...CREATE_ARTIFACT_SKILL,
+ summary:
+ "Build a live React artifact after discovering accounts and data with integrations, search, and invoke.",
+ body: createArtifactSkillBody(true),
+ },
+ ARTIFACT_STYLE_SKILL,
+ ]
+ : [];
+ }
+ return options.artifacts ? SKILLS : SKILLS.filter((skill) => !ARTIFACT_SKILLS.has(skill));
+};
/** Look up a skill by its exact name within a session's catalog. */
export const findSkill = (name: string, catalog: readonly Skill[] = SKILLS): Skill | undefined =>
diff --git a/packages/hosts/mcp/src/passthrough-tools.test.ts b/packages/hosts/mcp/src/passthrough-tools.test.ts
index 9d09425b72..491a623740 100644
--- a/packages/hosts/mcp/src/passthrough-tools.test.ts
+++ b/packages/hosts/mcp/src/passthrough-tools.test.ts
@@ -776,27 +776,49 @@ describe("passthrough mode server", () => {
);
});
- it("serves no artifact tools in passthrough even when artifacts are requested", async () => {
- const { engine } = makeRecordingEngine();
- await withClient(
- {
- engine,
- mode: "passthrough",
- artifactsEnabled: true,
- loadAppShellHtml: async () => "",
- artifacts: {
- list: () => Effect.succeed([]),
- get: () => Effect.die("unused"),
- save: () => Effect.die("unused"),
+ it.each([true, false])(
+ "honors artifacts=%s independently of passthrough",
+ async (artifactsEnabled) => {
+ const { engine } = makeRecordingEngine();
+ await withClient(
+ {
+ engine,
+ mode: "passthrough",
+ artifactsEnabled,
+ loadAppShellHtml: async () => "",
+ artifacts: {
+ list: () => Effect.succeed([]),
+ get: () => Effect.die("unused"),
+ save: () => Effect.die("unused"),
+ },
+ tools: toolPort(CATALOG),
},
- tools: toolPort(CATALOG),
- },
- async (client) => {
- const names = (await client.listTools()).tools.map((tool) => tool.name);
- expect(names.sort()).toEqual(["integrations", "invoke", "search", "skills"]);
- },
- );
- });
+ async (client) => {
+ const names = (await client.listTools()).tools.map((tool) => tool.name);
+ expect(names).toEqual(
+ expect.arrayContaining(["integrations", "invoke", "search", "skills"]),
+ );
+ expect(names).not.toContain("execute");
+ expect(names).not.toContain("resume");
+ for (const name of [
+ "create-artifact",
+ "edit-artifact",
+ "list-artifacts",
+ "show-artifact",
+ ]) {
+ expect(names.includes(name)).toBe(artifactsEnabled);
+ }
+ const guide = await client.callTool({
+ name: "skills",
+ arguments: { name: "create-artifact" },
+ });
+ expect(guide.isError === true).toBe(!artifactsEnabled);
+ expect(JSON.stringify(guide.content).includes("queryOptions")).toBe(artifactsEnabled);
+ expect(JSON.stringify(guide.content)).not.toContain("`execute`");
+ },
+ );
+ },
+ );
it("rejects arguments that fail the advertised schema before running anything", async () => {
const recording = makeRecordingEngine();
diff --git a/packages/hosts/mcp/src/passthrough-tools.ts b/packages/hosts/mcp/src/passthrough-tools.ts
index 44e3f3705d..5ad32f1f6e 100644
--- a/packages/hosts/mcp/src/passthrough-tools.ts
+++ b/packages/hosts/mcp/src/passthrough-tools.ts
@@ -29,7 +29,7 @@ export const passthroughInstructions = (): string =>
"Find connected integration tools with search, then call invoke with the returned tool ID and JSON arguments. " +
"Search returns input schemas and account details. Use its nextOffset to get more matches. " +
"Invoke can change external state; your client handles approval for each call. Workspace block policies remain enforced. " +
- "No JavaScript, execute, resume, or artifact tools are exposed in this mode.";
+ "No general execute or resume tools are exposed. When artifacts are enabled, use skills to read their guides.";
/** On-demand guidance for the JSON tool surface; no sandbox or artifact instructions. */
export const SEARCH_INVOKE_SKILL: Skill = {
@@ -49,6 +49,6 @@ export const SEARCH_INVOKE_SKILL: Skill = {
"## Results and approval",
"Invoke forwards the tool's result, including supported MCP content. Check `isError` and any returned error before treating a call as successful. Your client handles approval for invoke; workspace block policies still apply. An upstream request for user input needs a client that supports native elicitation.",
"If a tool is no longer available, search again. If an account needs authentication, ask the user to reconnect it in Executor. Never ask for credentials in chat.",
- "This mode accepts JSON tool arguments. It does not expose execute, resume, or artifact tools. The skills tool serves only this server's guides, not files or skills from your harness or project.",
+ "This mode accepts JSON tool arguments. It does not expose general execute or resume tools. When artifacts are enabled, use create-artifact, edit-artifact, list-artifacts, and show-artifact; read the create-artifact and artifact-style guides through skills first. The skills tool serves only this server's guides, not files or skills from your harness or project.",
].join("\n"),
};
diff --git a/packages/hosts/mcp/src/tool-server.ts b/packages/hosts/mcp/src/tool-server.ts
index 0255ff582e..f7c1a349f7 100644
--- a/packages/hosts/mcp/src/tool-server.ts
+++ b/packages/hosts/mcp/src/tool-server.ts
@@ -1533,12 +1533,16 @@ export const createExecutorMcpServer = (
// Artifacts are on unless this connection opted out (`?artifacts=false`).
// One flag decides the whole surface: the tools, the shell resource, and
// the skills catalog below.
- // Search/invoke serves no artifact tools: artifacts run sandboxed code.
- const artifactsEnabled =
- config.mode === "passthrough" ? false : (config.artifactsEnabled ?? true);
+ const artifactsEnabled = config.artifactsEnabled ?? true;
const skillCatalog: readonly Skill[] =
config.mode === "passthrough"
- ? [SEARCH_INVOKE_SKILL]
+ ? [
+ SEARCH_INVOKE_SKILL,
+ ...skillCatalogFor({
+ artifacts: artifactsEnabled && config.loadAppShellHtml !== undefined,
+ discovery: "search-invoke",
+ }),
+ ]
: skillCatalogFor({ artifacts: artifactsEnabled });
// Per-integration search tools are off unless this connection opted in
// (`?search_tools=true`).
@@ -2619,7 +2623,7 @@ export const createExecutorMcpServer = (
'Call `skills({ name: "create-artifact" })` for the full guide: the discovery-then-render protocol, TanStack Query rules, and every component already in scope. Call `skills({ name: "artifact-style" })` for how it must look — artifacts render inside the Executor console and must match its design system.',
"Write a component named `App` in `code`. Do not import anything and do not paste fetched data into JSX — read it live with `useQuery(tools...queryOptions(args))`.",
"Lay it out as an app, not a document: an artifact may be given the whole viewport, so make the root `flex h-full flex-col`, keep headers and filters as ordinary children, and give the one long table or list `flex-1 min-h-0 overflow-auto` — its header then stays put while the rows scroll under it.",
- "Artifact code addresses an INTEGRATION, never a connection: write `tools.vercel.domains.getDomains`, not the full `tools.vercel.user.personalVercel.domains.getDomains` address `execute` uses for discovery. The connection is bound when the artifact is saved, so it stays portable. Code containing a `.user.` or `.org.` segment is rejected.",
+ "Artifact code addresses an INTEGRATION, never a connection: write `tools.vercel.domains.getDomains`, not the full `tools.vercel.user.personalVercel.domains.getDomains` address used during discovery. The connection is bound when the artifact is saved, so it stays portable. Code containing a `.user.` or `.org.` segment is rejected.",
'To use two accounts of the same integration, tag each call site with a role — `tools.linear("prod").issues.list` and `tools.linear("staging").issues.list` — and map every role in `connections`.',
"All data access is declarative `tools.*`: `.queryOptions()` to read, `.infiniteQueryOptions()` to page through a cursor, `.mutationOptions()` to write. There is no `run()` and no arbitrary code — never hand-roll `useQuery({ queryKey, queryFn })`, or invalidation breaks.",
"To read every page of a paginated tool, call `useInfiniteQuery(tools...infiniteQueryOptions(args, { cursorKey, getNextPageParam }))` once and render `data.pages`. Never call hooks inside a loop — a `useQuery` per page is rejected.",
@@ -2640,7 +2644,7 @@ export const createExecutorMcpServer = (
.record(z.string(), z.string())
.optional()
.describe(
- 'Which connection each integration role in `code` uses, as `..` (the address `connections.list` reports, minus the leading `tools.`). Keys are roles: the integration slug for an untagged `tools.linear.…`, or the tag for `tools.linear("prod").…`. Optional when you have exactly one connection per integration used — that one binds automatically. Required when you have several, and the error lists them.',
+ 'Which connection each integration role in `code` uses, as `..` (use the integration, owner, and connection from discovery). Keys are roles: the integration slug for an untagged `tools.linear.…`, or the tag for `tools.linear("prod").…`. Optional when you have exactly one connection per integration used — that one binds automatically. Required when you have several, and the error lists them.',
),
title: z
.string()
diff --git a/packages/react/src/components/mcp-install-card.tsx b/packages/react/src/components/mcp-install-card.tsx
index e61e713d9e..eac93d9b1f 100644
--- a/packages/react/src/components/mcp-install-card.tsx
+++ b/packages/react/src/components/mcp-install-card.tsx
@@ -331,16 +331,13 @@ export function McpInstallCard(props: { className?: string }) {
Artifacts
- {toolMode === "passthrough"
- ? "Not available in search and invoke mode."
- : artifacts
- ? "Generated UI components are saved to your workspace."
- : "Disabled: this connection serves no artifact tools."}
+ {artifacts
+ ? "Generated UI components are saved to your workspace."
+ : "Disabled: this connection serves no artifact tools."}
{
setPreferences((current) => ({ ...current, artifacts: next }));
trackEvent("mcp_install_artifacts_toggled", { artifacts: next });