Skip to content

Commit 3c263d7

Browse files
authored
Add search and invoke MCP mode (#1942)
* Add search and invoke MCP mode * Preserve full annotations in MCP scenario decoding
1 parent 0e9d800 commit 3c263d7

29 files changed

Lines changed: 2064 additions & 111 deletions

‎.changeset/mcp-passthrough-mode.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@executor-js/sdk": minor
3+
"@executor-js/execution": minor
4+
"executor": minor
5+
---
6+
7+
Add a search and invoke MCP mode (`?mode=passthrough`, `executor mcp --mode passthrough`). Search returns bounded pages of matching tool IDs and input schemas. Invoke validates arguments and runs the selected tool, with native client approval and workspace blocks enforced. The MCP catalog stays at two tools regardless of integration count.

‎apps/cli/src/main.ts‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,7 @@ const mcpUrlForActiveLocalServer = (input: {
13631363
readonly elicitationMode: "browser" | "model";
13641364
readonly artifacts: boolean;
13651365
readonly searchTools: boolean;
1366+
readonly toolMode: "codemode" | "passthrough";
13661367
}): URL => {
13671368
const url = new URL("/mcp", input.connection.origin);
13681369
if (input.elicitationMode === "browser") {
@@ -1378,6 +1379,10 @@ const mcpUrlForActiveLocalServer = (input: {
13781379
if (input.searchTools) {
13791380
url.searchParams.set("search_tools", "true");
13801381
}
1382+
// Passthrough is the non-default surface; only it is spelled out.
1383+
if (input.toolMode === "passthrough") {
1384+
url.searchParams.set("mode", "passthrough");
1385+
}
13811386
return url;
13821387
};
13831388

@@ -1394,6 +1399,7 @@ const runMcpHttpBridge = async (input: {
13941399
readonly elicitationMode: "browser" | "model";
13951400
readonly artifacts: boolean;
13961401
readonly searchTools: boolean;
1402+
readonly toolMode: "codemode" | "passthrough";
13971403
}): Promise<void> => {
13981404
const stdio = new StdioServerTransport();
13991405
const authorization = getExecutorServerAuthorizationHeader(input.manifest.connection);
@@ -1403,6 +1409,7 @@ const runMcpHttpBridge = async (input: {
14031409
elicitationMode: input.elicitationMode,
14041410
artifacts: input.artifacts,
14051411
searchTools: input.searchTools,
1412+
toolMode: input.toolMode,
14061413
}),
14071414
authorization ? { requestInit: { headers: { Authorization: authorization } } } : undefined,
14081415
);
@@ -1482,6 +1489,7 @@ const runStdioMcpSession = (input: {
14821489
readonly elicitationMode: "browser" | "model";
14831490
readonly artifacts: boolean;
14841491
readonly searchTools: boolean;
1492+
readonly toolMode: "codemode" | "passthrough";
14851493
}) =>
14861494
Effect.gen(function* () {
14871495
// `executor mcp` never owns the local database. If a local server is already
@@ -1499,6 +1507,7 @@ const runStdioMcpSession = (input: {
14991507
elicitationMode: input.elicitationMode,
15001508
artifacts: input.artifacts,
15011509
searchTools: input.searchTools,
1510+
toolMode: input.toolMode,
15021511
}),
15031512
);
15041513
return;
@@ -1526,6 +1535,7 @@ const runStdioMcpSession = (input: {
15261535
elicitationMode: input.elicitationMode,
15271536
artifacts: input.artifacts,
15281537
searchTools: input.searchTools,
1538+
toolMode: input.toolMode,
15291539
}),
15301540
);
15311541
});
@@ -2898,11 +2908,30 @@ const mcpCommand = Command.make(
28982908
"Serve one search_<integration> tool per connected integration. Off by default; each routes through the same flow as tools.search inside execute.",
28992909
),
29002910
),
2911+
toolMode: Options.choice("mode", ["codemode", "passthrough"] as const)
2912+
.pipe(Options.withDefault("codemode"))
2913+
.pipe(
2914+
Options.withDescription(
2915+
"codemode (default) serves the execute tool; passthrough serves search and invoke, with input schemas in search results and client approval for invoke.",
2916+
),
2917+
),
29012918
},
2902-
({ scope, elicitationMode, noArtifacts, searchTools }) =>
2919+
({ scope, elicitationMode, noArtifacts, searchTools, toolMode }) =>
29032920
Effect.gen(function* () {
29042921
applyScope(scope);
2905-
yield* runStdioMcpSession({ elicitationMode, artifacts: !noArtifacts, searchTools });
2922+
if (toolMode === "passthrough" && searchTools) {
2923+
return yield* Effect.fail(
2924+
new Error(
2925+
"--search-tools is a codemode option; passthrough already provides search. Drop --search-tools or --mode passthrough.",
2926+
),
2927+
);
2928+
}
2929+
yield* runStdioMcpSession({
2930+
elicitationMode,
2931+
artifacts: !noArtifacts,
2932+
searchTools,
2933+
toolMode,
2934+
});
29062935
}),
29072936
).pipe(Command.withDescription("Start an MCP server over stdio"));
29082937

‎apps/cloud/src/mcp/agent-handler.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
readArtifactsEnabled,
1717
readElicitationMode,
1818
readSearchToolsEnabled,
19+
readToolMode,
1920
withVerifiedIdentityHeaders,
2021
} from "@executor-js/cloudflare/mcp/do-headers";
2122
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
@@ -189,6 +190,7 @@ const propsForPrincipal = (
189190
elicitationMode: readElicitationMode(request),
190191
artifactsEnabled: readArtifactsEnabled(request),
191192
searchToolsEnabled: readSearchToolsEnabled(request),
193+
toolMode: readToolMode(request),
192194
resource,
193195
webOrigin: new URL(request.url).origin,
194196
},

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,17 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD
384384
description,
385385
artifacts: executor.artifacts,
386386
connections: executor.connections,
387+
tools: executor.tools,
387388
// Artifacts are on by default, opt-out per connection. A session
388389
// persisted without a value restores to the default, same as a fresh
389390
// connection whose URL says nothing about `?artifacts=`.
390391
artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
391392
// Per-integration search tools are off by default, opt-in per
392393
// connection (`?search_tools=true`). Same restore rule as artifacts.
393394
searchToolsEnabled: sessionMeta.searchToolsEnabled ?? false,
395+
// The tool surface must survive a cold restore unchanged: the client
396+
// cached the names it saw at `initialize`.
397+
mode: sessionMeta.toolMode ?? "codemode",
394398
// Cold restores rebuild this server with no `initialize` to replay, so
395399
// the negotiated apps support comes back from storage instead.
396400
restoredAppsEnabled: sessionMeta.appsEnabled ?? false,

‎apps/cloud/src/mcp/session-meta.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const metaFromIdentity = (
127127
elicitationMode: token.elicitationMode,
128128
artifactsEnabled: token.artifactsEnabled,
129129
searchToolsEnabled: token.searchToolsEnabled,
130+
toolMode: token.toolMode,
130131
};
131132
};
132133

‎apps/host-cloudflare/src/mcp/agent-handler.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
readArtifactsEnabled,
1515
readElicitationMode,
1616
readSearchToolsEnabled,
17+
readToolMode,
1718
withVerifiedIdentityHeaders,
1819
} from "@executor-js/cloudflare/mcp/do-headers";
1920
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
@@ -86,6 +87,7 @@ const propsForPrincipal = (
8687
elicitationMode: readElicitationMode(request),
8788
artifactsEnabled: readArtifactsEnabled(request),
8889
searchToolsEnabled: readSearchToolsEnabled(request),
90+
toolMode: readToolMode(request),
8991
// host-cloudflare only routes the bare `/mcp` endpoint to the Agent
9092
// bridge (see worker.ts), so the session always serves the default
9193
// resource.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
133133
elicitationMode: token.elicitationMode,
134134
artifactsEnabled: token.artifactsEnabled,
135135
searchToolsEnabled: token.searchToolsEnabled,
136+
toolMode: token.toolMode,
136137
} satisfies SessionMeta);
137138
}
138139

@@ -165,13 +166,15 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
165166
engine,
166167
artifacts: executor.artifacts,
167168
connections: executor.connections,
169+
tools: executor.tools,
168170
// Artifacts are on by default, opt-out per connection. A session
169171
// persisted without a value restores to the default, same as a fresh
170172
// connection whose URL says nothing about `?artifacts=`.
171173
artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
172174
// Per-integration search tools are off by default, opt-in per
173175
// connection (`?search_tools=true`). Same restore rule as artifacts.
174176
searchToolsEnabled: sessionMeta.searchToolsEnabled ?? false,
177+
mode: sessionMeta.toolMode ?? "codemode",
175178
// Cold restores rebuild this server with no `initialize` to replay, so
176179
// the negotiated apps support comes back from storage instead.
177180
restoredAppsEnabled: sessionMeta.appsEnabled ?? false,

‎apps/local/src/main.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
123123
engine,
124124
artifacts: executor.artifacts,
125125
connections: executor.connections,
126+
tools: executor.tools,
126127
...appsConfig,
127128
},
128129
webBaseUrl: process.env.EXECUTOR_WEB_BASE_URL || undefined,
@@ -133,6 +134,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
133134
engine,
134135
artifacts: executor.artifacts,
135136
connections: executor.connections,
137+
tools: executor.tools,
136138
...appsConfig,
137139
},
138140
};
@@ -158,6 +160,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
158160
engine: toolkitEngine,
159161
artifacts: handle.executor.artifacts,
160162
connections: handle.executor.connections,
163+
tools: handle.executor.tools,
161164
...appsConfig,
162165
},
163166
close: handle.dispose,

‎apps/local/src/mcp.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
readArtifactsEnabled,
2222
readElicitationMode,
2323
readSearchToolsEnabled,
24+
readToolMode,
2425
} from "@executor-js/host-mcp/browser-approval";
2526
import { makeInProcessBrowserApprovalStore } from "@executor-js/host-mcp/browser-approval-store";
2627
import {
@@ -246,6 +247,7 @@ export const createMcpRequestHandler = (
246247
browserApprovalStore: approvals.store,
247248
artifactsEnabled: readArtifactsEnabled(request),
248249
searchToolsEnabled: readSearchToolsEnabled(request),
250+
mode: readToolMode(request),
249251
elicitationMode:
250252
elicitationMode === "browser"
251253
? {

0 commit comments

Comments
 (0)