Skip to content

Commit ea2df9f

Browse files
committed
fix: clarify preset and resume guidance
1 parent 337fe3d commit ea2df9f

8 files changed

Lines changed: 84 additions & 7 deletions

File tree

notes/source-setup-sdk-flow.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Source setup orchestration
2+
3+
The web source-add flow currently owns a lot of business logic that agents have
4+
to rediscover through low-level tools: preset lookup, URL-to-endpoint mapping,
5+
endpoint probing, OAuth strategy choice, connection id generation, browser
6+
handoff, credential binding, and final source registration.
7+
8+
Longer term, consider moving this into an SDK-level source setup service so the
9+
frontend and agent tools share the same state machine. The frontend would render
10+
steps from the service, while agent tools would return the same state with
11+
model-facing `instructions` fields. Keep low-level plugin tools as escape
12+
hatches, but make common preset flows first-class.

packages/core/execution/src/engine.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,31 @@ export const formatPausedExecution = (
107107
const lines: string[] = [`Execution paused: ${req.message}`];
108108
const isUrlElicitation = Predicate.isTagged(req, "UrlElicitation");
109109
const isFormElicitation = Predicate.isTagged(req, "FormElicitation");
110+
const requestedSchema = isFormElicitation ? req.requestedSchema : undefined;
111+
const hasRequestedSchema =
112+
requestedSchema !== undefined && Object.keys(requestedSchema).length > 0;
113+
const instructions = isUrlElicitation
114+
? `The user needs to open this URL in a browser and complete the flow. After the user finishes, call the resume tool with executionId "${paused.id}" and action "accept".`
115+
: hasRequestedSchema
116+
? `Ask the user for values matching requestedSchema. Then call the resume tool with executionId "${paused.id}", action "accept", and content matching requestedSchema. If the user declines, call resume with action "decline" or "cancel".`
117+
: `This is a model-side confirmation gate; there is no browser form to open. Ask the user whether to approve the paused tool call. If the user approves, call the resume tool with executionId "${paused.id}" and action "accept". If the user declines, call resume with action "decline" or "cancel".`;
110118

111119
if (isUrlElicitation) {
112120
lines.push(`\nOpen this URL in a browser:\n${req.url}`);
113-
lines.push("\nAfter the browser flow, resume with the executionId below:");
121+
lines.push('\nAfter the browser flow, call the resume tool with action "accept".');
122+
} else if (hasRequestedSchema) {
123+
lines.push(
124+
"\nAsk the user for a response matching the requested schema, then call the resume tool.",
125+
);
126+
lines.push(`\nRequested schema:\n${JSON.stringify(requestedSchema, null, 2)}`);
114127
} else {
115-
lines.push("\nResume with the executionId below and a response matching the requested schema:");
116-
const schema = req.requestedSchema;
117-
if (schema && Object.keys(schema).length > 0) {
118-
lines.push(`\nRequested schema:\n${JSON.stringify(schema, null, 2)}`);
119-
}
128+
lines.push(
129+
'\nThis is a model-side confirmation gate; no browser form is waiting. Ask the user whether to approve, then call the resume tool with action "accept", "decline", or "cancel".',
130+
);
120131
}
121132

122133
lines.push(`\nexecutionId: ${paused.id}`);
134+
lines.push(`\ninstructions: ${instructions}`);
123135

124136
return {
125137
text: lines.join("\n"),
@@ -129,6 +141,7 @@ export const formatPausedExecution = (
129141
interaction: {
130142
kind: isUrlElicitation ? "url" : "form",
131143
message: req.message,
144+
instructions,
132145
toolId: String(paused.elicitationContext.toolId),
133146
args: paused.elicitationContext.args,
134147
...(isUrlElicitation ? { url: req.url } : {}),

packages/core/sdk/src/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ export interface SourcePreset {
9696
/** URL passed as `initialUrl` to the add form. Omit for presets that
9797
* don't use a URL (e.g. stdio MCP presets). */
9898
readonly url?: string;
99+
/** Endpoint passed to agent-facing probe/add tools when their schema
100+
* uses `endpoint` instead of `url`. */
101+
readonly endpoint?: string;
99102
/** Optional icon URL (favicon, logo). */
100103
readonly icon?: string;
101104
/** Shown in the top-level grid on the sources page when true. */

packages/core/sdk/src/core-tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ const SourcePresetOutput = Schema.Struct({
122122
name: Schema.String,
123123
summary: Schema.String,
124124
url: Schema.optional(Schema.String),
125+
endpoint: Schema.optional(Schema.String),
125126
icon: Schema.optional(Schema.String),
126127
featured: Schema.optional(Schema.Boolean),
127128
transport: Schema.optional(Schema.Literals(["remote", "stdio"])),
@@ -642,7 +643,7 @@ export const coreToolsPlugin = definePlugin((options: CoreToolsPluginOptions = {
642643
tool({
643644
name: "sources.presets",
644645
description:
645-
"List the same popular source presets shown in Executor web's Connect dialog. Use this before asking the user what to connect; filter with `query` for names like GitHub, Stripe, Axiom, Google Calendar, Linear, or OpenAI. Use the returned `pluginId`, `url`, and optional stdio command to choose the matching add flow (`openapi.previewSpec`/`openapi.addSource`, `graphql.addSource`, `mcp.probeEndpoint`/`mcp.addSource`, or `googleDiscovery.probeDiscovery`/`googleDiscovery.addSource`).",
646+
"List the same popular source presets shown in Executor web's Connect dialog. Use this before asking the user what to connect; filter with `query` for names like GitHub, Stripe, Axiom, Google Calendar, Linear, or OpenAI. For MCP and GraphQL presets, pass `endpoint` to the probe/add tools. For OpenAPI and Google Discovery presets, pass `url` to the preview/probe and add tools. For stdio MCP presets, use the returned command/args/env.",
646647
inputSchema: SourcesPresetsInputStd,
647648
outputSchema: SourcesPresetsOutputStd,
648649
execute: (input, { ctx }) =>

packages/core/sdk/src/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ export interface SourcePreset {
448448
readonly name: string;
449449
readonly summary: string;
450450
readonly url?: string;
451+
readonly endpoint?: string;
451452
readonly icon?: string;
452453
readonly featured?: boolean;
453454
readonly transport?: "remote" | "stdio";

packages/hosts/mcp/src/server.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,35 @@ describe("MCP host server — client without elicitation (pause/resume)", () =>
559559
const structured = result.structuredContent as Record<string, unknown>;
560560
expect(structured?.executionId).toBe("exec_42");
561561
expect(structured?.status).toBe("waiting_for_interaction");
562+
const interaction = structured.interaction as Record<string, unknown>;
563+
expect(interaction.instructions).toContain(
564+
"Ask the user for values matching requestedSchema",
565+
);
566+
});
567+
});
568+
569+
it("default model resume mode explains empty form schemas as model-side confirmation", async () => {
570+
const engine = makeStubEngine({
571+
executeWithPause: () =>
572+
Effect.succeed(
573+
makePausedResult(
574+
"exec_confirm",
575+
FormElicitation.make({ message: "Confirm source add", requestedSchema: {} }),
576+
),
577+
),
578+
});
579+
580+
await withClient(engine, NO_CAPS, async (client) => {
581+
const result = await client.callTool({
582+
name: "execute",
583+
arguments: { code: "confirm-me" },
584+
});
585+
586+
expect(textOf(result)).toContain("no browser form is waiting");
587+
const structured = result.structuredContent as Record<string, unknown>;
588+
const interaction = structured.interaction as Record<string, unknown>;
589+
expect(interaction.instructions).toContain("model-side confirmation gate");
590+
expect(interaction.instructions).toContain('action "accept"');
562591
});
563592
});
564593

packages/plugins/graphql/src/sdk/presets.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface GraphqlPreset {
33
readonly name: string;
44
readonly summary: string;
55
readonly url: string;
6+
readonly endpoint: string;
67
readonly icon?: string;
78
readonly featured?: boolean;
89
}
@@ -13,6 +14,7 @@ export const graphqlPresets: readonly GraphqlPreset[] = [
1314
name: "GitHub GraphQL",
1415
summary: "Repos, issues, PRs, and users via GitHub's GraphQL API.",
1516
url: "https://api.github.com/graphql",
17+
endpoint: "https://api.github.com/graphql",
1618
icon: "https://github.com/favicon.ico",
1719
featured: true,
1820
},
@@ -21,6 +23,7 @@ export const graphqlPresets: readonly GraphqlPreset[] = [
2123
name: "GitLab",
2224
summary: "Projects, merge requests, pipelines, and users.",
2325
url: "https://gitlab.com/api/graphql",
26+
endpoint: "https://gitlab.com/api/graphql",
2427
icon: "https://gitlab.com/favicon.ico",
2528
featured: true,
2629
},
@@ -29,6 +32,7 @@ export const graphqlPresets: readonly GraphqlPreset[] = [
2932
name: "Linear",
3033
summary: "Issues, projects, teams, and cycles.",
3134
url: "https://api.linear.app/graphql",
35+
endpoint: "https://api.linear.app/graphql",
3236
icon: "https://linear.app/favicon.ico",
3337
featured: true,
3438
},
@@ -37,13 +41,15 @@ export const graphqlPresets: readonly GraphqlPreset[] = [
3741
name: "Monday.com",
3842
summary: "Boards, items, columns, and workspace automation.",
3943
url: "https://api.monday.com/v2",
44+
endpoint: "https://api.monday.com/v2",
4045
icon: "https://monday.com/favicon.ico",
4146
},
4247
{
4348
id: "anilist",
4449
name: "AniList",
4550
summary: "Anime and manga database — no auth required.",
4651
url: "https://graphql.anilist.co",
52+
endpoint: "https://graphql.anilist.co",
4753
icon: "https://anilist.co/img/icons/favicon-32x32.png",
4854
},
4955
];

packages/plugins/mcp/src/sdk/presets.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface McpRemotePreset {
33
readonly name: string;
44
readonly summary: string;
55
readonly url: string;
6+
readonly endpoint: string;
67
readonly icon?: string;
78
readonly featured?: boolean;
89
readonly transport?: undefined;
@@ -28,6 +29,7 @@ export const mcpPresets: readonly McpPreset[] = [
2829
name: "DeepWiki",
2930
summary: "Search and read documentation from any GitHub repo.",
3031
url: "https://mcp.deepwiki.com/mcp",
32+
endpoint: "https://mcp.deepwiki.com/mcp",
3133
icon: "https://deepwiki.com/favicon.ico",
3234
featured: true,
3335
},
@@ -36,6 +38,7 @@ export const mcpPresets: readonly McpPreset[] = [
3638
name: "Context7",
3739
summary: "Up-to-date docs and code examples for any library.",
3840
url: "https://mcp.context7.com/mcp",
41+
endpoint: "https://mcp.context7.com/mcp",
3942
icon: "https://context7.com/favicon.ico",
4043
featured: true,
4144
},
@@ -44,6 +47,7 @@ export const mcpPresets: readonly McpPreset[] = [
4447
name: "Browserbase",
4548
summary: "Cloud browser sessions for web scraping and automation.",
4649
url: "https://mcp.browserbase.com/mcp",
50+
endpoint: "https://mcp.browserbase.com/mcp",
4751
icon: "https://www.browserbase.com/favicon.ico",
4852
featured: true,
4953
},
@@ -52,6 +56,7 @@ export const mcpPresets: readonly McpPreset[] = [
5256
name: "Firecrawl",
5357
summary: "Crawl and scrape websites into structured data.",
5458
url: "https://mcp.firecrawl.dev/mcp",
59+
endpoint: "https://mcp.firecrawl.dev/mcp",
5560
icon: "https://www.firecrawl.dev/favicon.ico",
5661
featured: true,
5762
},
@@ -60,6 +65,7 @@ export const mcpPresets: readonly McpPreset[] = [
6065
name: "Neon",
6166
summary: "Serverless Postgres — branches, queries, and management.",
6267
url: "https://mcp.neon.tech/mcp",
68+
endpoint: "https://mcp.neon.tech/mcp",
6369
icon: "https://neon.tech/favicon/favicon.ico",
6470
featured: true,
6571
},
@@ -68,6 +74,7 @@ export const mcpPresets: readonly McpPreset[] = [
6874
name: "Axiom",
6975
summary: "Query, analyze, and monitor your logs and event data.",
7076
url: "https://mcp.axiom.co/mcp",
77+
endpoint: "https://mcp.axiom.co/mcp",
7178
icon: "https://axiom.co/favicon.ico",
7279
featured: true,
7380
},
@@ -76,6 +83,7 @@ export const mcpPresets: readonly McpPreset[] = [
7683
name: "Stripe",
7784
summary: "Manage payments, subscriptions, and billing via MCP.",
7885
url: "https://mcp.stripe.com",
86+
endpoint: "https://mcp.stripe.com",
7987
icon: "https://stripe.com/favicon.ico",
8088
featured: true,
8189
},
@@ -84,6 +92,7 @@ export const mcpPresets: readonly McpPreset[] = [
8492
name: "Linear",
8593
summary: "Issues, projects, teams, and cycles via MCP.",
8694
url: "https://mcp.linear.app/mcp",
95+
endpoint: "https://mcp.linear.app/mcp",
8796
icon: "https://linear.app/favicon.ico",
8897
featured: true,
8998
},
@@ -92,6 +101,7 @@ export const mcpPresets: readonly McpPreset[] = [
92101
name: "Notion",
93102
summary: "Databases, pages, blocks, and search via MCP.",
94103
url: "https://mcp.notion.com/mcp",
104+
endpoint: "https://mcp.notion.com/mcp",
95105
icon: "https://www.notion.com/front-static/favicon.ico",
96106
featured: true,
97107
},
@@ -100,13 +110,15 @@ export const mcpPresets: readonly McpPreset[] = [
100110
name: "Sentry",
101111
summary: "Error monitoring, issues, and performance data.",
102112
url: "https://mcp.sentry.dev/mcp",
113+
endpoint: "https://mcp.sentry.dev/mcp",
103114
icon: "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png",
104115
},
105116
{
106117
id: "cloudflare",
107118
name: "Cloudflare",
108119
summary: "Workers, KV, D1, R2, and DNS management via MCP.",
109120
url: "https://mcp.cloudflare.com/mcp",
121+
endpoint: "https://mcp.cloudflare.com/mcp",
110122
icon: "https://cloudflare.com/favicon.ico",
111123
},
112124
{

0 commit comments

Comments
 (0)