Skip to content

Commit 7f6bed6

Browse files
committed
Add account discovery and guides to search and invoke
1 parent 387d0b3 commit 7f6bed6

12 files changed

Lines changed: 562 additions & 65 deletions

File tree

apps/cloud/src/mcp/session-durable-object.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD
385385
artifacts: executor.artifacts,
386386
connections: executor.connections,
387387
tools: executor.tools,
388+
integrations: executor.integrations,
388389
// Artifacts are on by default, opt-out per connection. A session
389390
// persisted without a value restores to the default, same as a fresh
390391
// connection whose URL says nothing about `?artifacts=`.

apps/docs/mcp-proxy.mdx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,32 @@ Executor:
5656

5757
Once a client is connected, every integration you add to Executor appears in that
5858
agent automatically.
59+
60+
## Search and invoke mode
61+
62+
Add `?mode=passthrough` to your MCP endpoint, or enable **Search and invoke**
63+
in the Connect card. This mode exposes four tools:
64+
65+
- `integrations`: list connected accounts with integration descriptions, account
66+
labels, and their last recorded health. Results are paginated, with one item per
67+
account. Health is not checked again by this call.
68+
- `skills`: list this server's guides, or read `skills({ name: "search-invoke" })`.
69+
- `search`: find tools and read their JSON input schemas. Use the exact
70+
`integration`, `owner`, and `connection` fields from `integrations` to select an
71+
account.
72+
- `invoke`: call the exact tool ID from search with JSON arguments.
73+
74+
For example, call `integrations({})`, then
75+
`search({ query: "create issue", integration: "github", owner: "org", connection: "main" })`
76+
using the account names returned by your server. Call `invoke` with the matching
77+
result's `id` and arguments that follow its `inputSchema`.
78+
79+
Both `integrations` and `search` return `items`, `total`, `hasMore`, and
80+
`nextOffset`. To get another page, repeat the same query and filters with
81+
`offset: nextOffset`. This pagination finds tools and accounts; each upstream API
82+
has its own pagination for records.
83+
84+
The tool list stays small as you add integrations. Input schemas are loaded only
85+
for matching search results. Your client handles approval for `invoke`, and
86+
workspace block policies still apply. This mode does not expose code execution or
87+
artifact tools.

apps/host-cloudflare/src/mcp/session-durable-object.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
167167
artifacts: executor.artifacts,
168168
connections: executor.connections,
169169
tools: executor.tools,
170+
integrations: executor.integrations,
170171
// Artifacts are on by default, opt-out per connection. A session
171172
// persisted without a value restores to the default, same as a fresh
172173
// connection whose URL says nothing about `?artifacts=`.

apps/local/src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
124124
artifacts: executor.artifacts,
125125
connections: executor.connections,
126126
tools: executor.tools,
127+
integrations: executor.integrations,
127128
...appsConfig,
128129
},
129130
webBaseUrl: process.env.EXECUTOR_WEB_BASE_URL || undefined,
@@ -135,6 +136,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
135136
artifacts: executor.artifacts,
136137
connections: executor.connections,
137138
tools: executor.tools,
139+
integrations: executor.integrations,
138140
...appsConfig,
139141
},
140142
};
@@ -161,6 +163,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
161163
artifacts: handle.executor.artifacts,
162164
connections: handle.executor.connections,
163165
tools: handle.executor.tools,
166+
integrations: handle.executor.integrations,
164167
...appsConfig,
165168
},
166169
close: handle.dispose,

e2e/cloud/passthrough-opencode-codemode.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Real OpenCode codemode discovers and invokes tools through a two-tool MCP surface.
1+
// Real OpenCode codemode discovers and invokes tools through a fixed MCP tool surface.
22
// The server retains 10,200 tools; the replay model scripts discovery and a real upstream call.
33
import { randomBytes } from "node:crypto";
44
import { createServer } from "node:http";

e2e/cloud/passthrough-scale.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ scenario(
4545
`a ${visible.length}-tool passthrough connect stays bounded (took ${elapsedMs}ms)`,
4646
).toBeLessThan(MAX_PASSTHROUGH_CONNECT_MS);
4747

48-
expect(served.map((tool) => tool.name).sort()).toEqual(["invoke", "search"]);
48+
expect(served.map((tool) => tool.name).sort()).toEqual([
49+
"integrations",
50+
"invoke",
51+
"search",
52+
"skills",
53+
]);
4954
const first = decodeToolSearch(
5055
(yield* session.call("search", { query: "org", limit: 20 })).raw,
5156
).structuredContent;

e2e/scenarios/mcp-passthrough.test.ts

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { randomBytes } from "node:crypto";
33
import { createServer, type IncomingMessage } from "node:http";
44

55
import { expect } from "@effect/vitest";
6-
import { Effect } from "effect";
6+
import { Effect, Schema } from "effect";
77
import { composePluginApi } from "@executor-js/api/server";
88
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
99
import {
@@ -16,7 +16,23 @@ import {
1616
import { decodeToolSearch } from "./support/search-invoke";
1717

1818
import { scenario } from "../src/scenario";
19-
import { Api, Mcp, Target } from "../src/services";
19+
import { Api, Browser, Mcp, Target } from "../src/services";
20+
import { visit, settle } from "../src/surfaces/browser";
21+
22+
const decodeInventory = Schema.decodeUnknownSync(
23+
Schema.Struct({
24+
structuredContent: Schema.Struct({
25+
items: Schema.Array(
26+
Schema.Struct({
27+
integration: Schema.String,
28+
owner: Schema.String,
29+
connection: Schema.String,
30+
}),
31+
),
32+
total: Schema.Number,
33+
}),
34+
}),
35+
);
2036

2137
const api = composePluginApi([openApiHttpPlugin()] as const);
2238

@@ -133,6 +149,7 @@ scenario(
133149
Effect.gen(function* () {
134150
const target = yield* Target;
135151
const mcp = yield* Mcp;
152+
const browser = yield* Browser;
136153
const { client: makeClient } = yield* Api;
137154

138155
const identity = yield* target.newIdentity();
@@ -170,12 +187,35 @@ scenario(
170187
});
171188
}
172189

190+
yield* browser.session(identity, async ({ page, step }) => {
191+
await step("Choose Search and invoke in the Connect card", async () => {
192+
await visit(page, "/");
193+
await page.getByRole("button", { name: "Advanced" }).click();
194+
await page.getByRole("switch", { name: "Search and invoke" }).check();
195+
await settle(page);
196+
expect(await page.locator("code").first().innerText()).toContain("mode=passthrough");
197+
expect(
198+
await page
199+
.getByText(
200+
"Discover connected accounts with integrations and read the guide with skills.",
201+
{ exact: false },
202+
)
203+
.isVisible(),
204+
).toBe(true);
205+
});
206+
});
207+
173208
const codemode = mcp.session(identity);
174209
expect(yield* codemode.listTools()).toContain("execute");
175210

176211
const passthrough = mcp.session(identity, { mode: "passthrough" });
177212
const described = yield* passthrough.describeTools();
178-
expect(described.map((tool) => tool.name).sort()).toEqual(["invoke", "search"]);
213+
expect(described.map((tool) => tool.name).sort()).toEqual([
214+
"integrations",
215+
"invoke",
216+
"search",
217+
"skills",
218+
]);
179219
expect(described.find((tool) => tool.name === "search")?.annotations).toMatchObject({
180220
readOnlyHint: true,
181221
destructiveHint: false,
@@ -184,9 +224,42 @@ scenario(
184224
readOnlyHint: false,
185225
destructiveHint: true,
186226
});
227+
const inventory = yield* passthrough.call("integrations", {
228+
integration: slug,
229+
owner: "org",
230+
});
231+
expect(inventory.ok).toBe(true);
232+
const decodedInventory = decodeInventory(inventory.raw).structuredContent;
233+
expect(decodedInventory).toEqual({
234+
items: [{ integration: slug, owner: "org", connection: "main" }],
235+
total: 1,
236+
});
237+
expect(inventory.text).not.toContain(`tok_${slug}`);
238+
const skills = yield* passthrough.call("skills", {});
239+
expect(skills.ok).toBe(true);
240+
expect(skills.text).toContain("search-invoke");
241+
const guide = yield* passthrough.call("skills", { name: "search-invoke" });
242+
expect(guide.ok).toBe(true);
243+
expect(guide.text).toContain("integrations({})");
244+
expect((yield* passthrough.call("skills", { name: "execute" })).ok).toBe(false);
187245
const found = decodeToolSearch(
188-
(yield* passthrough.call("search", { query: slug })).raw,
246+
(yield* passthrough.call("search", {
247+
query: "notes",
248+
integration: slug,
249+
owner: "org",
250+
connection: "main",
251+
})).raw,
252+
).structuredContent;
253+
expect(found.items.every((tool) => tool.integration === slug)).toBe(true);
254+
const missingAccount = decodeToolSearch(
255+
(yield* passthrough.call("search", {
256+
query: "notes",
257+
integration: slug,
258+
owner: "user",
259+
connection: "main",
260+
})).raw,
189261
).structuredContent;
262+
expect(missingAccount.items).toEqual([]);
190263
const listDef = found.items.find((tool) => tool.id.endsWith(".listNotes"));
191264
const createDef = found.items.find((tool) => tool.id.endsWith(".createNote"));
192265
if (!listDef || !createDef) return yield* Effect.die("Search omitted notes operations");

packages/core/api/src/server/mcp-build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const makeMcpBuildServer =
7272
artifacts: executor.artifacts,
7373
connections: executor.connections,
7474
tools: executor.tools,
75+
integrations: executor.integrations,
7576
...(hostOptions?.loadAppShellHtml
7677
? { loadAppShellHtml: hostOptions.loadAppShellHtml }
7778
: {}),

0 commit comments

Comments
 (0)