Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/mcp-passthrough-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@executor-js/sdk": minor
"@executor-js/execution": minor
"executor": minor
---

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.
33 changes: 31 additions & 2 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,7 @@ const mcpUrlForActiveLocalServer = (input: {
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
readonly searchTools: boolean;
readonly toolMode: "codemode" | "passthrough";
}): URL => {
const url = new URL("/mcp", input.connection.origin);
if (input.elicitationMode === "browser") {
Expand All @@ -1378,6 +1379,10 @@ const mcpUrlForActiveLocalServer = (input: {
if (input.searchTools) {
url.searchParams.set("search_tools", "true");
}
// Passthrough is the non-default surface; only it is spelled out.
if (input.toolMode === "passthrough") {
url.searchParams.set("mode", "passthrough");
}
return url;
};

Expand All @@ -1394,6 +1399,7 @@ const runMcpHttpBridge = async (input: {
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
readonly searchTools: boolean;
readonly toolMode: "codemode" | "passthrough";
}): Promise<void> => {
const stdio = new StdioServerTransport();
const authorization = getExecutorServerAuthorizationHeader(input.manifest.connection);
Expand All @@ -1403,6 +1409,7 @@ const runMcpHttpBridge = async (input: {
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
searchTools: input.searchTools,
toolMode: input.toolMode,
}),
authorization ? { requestInit: { headers: { Authorization: authorization } } } : undefined,
);
Expand Down Expand Up @@ -1482,6 +1489,7 @@ const runStdioMcpSession = (input: {
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
readonly searchTools: boolean;
readonly toolMode: "codemode" | "passthrough";
}) =>
Effect.gen(function* () {
// `executor mcp` never owns the local database. If a local server is already
Expand All @@ -1499,6 +1507,7 @@ const runStdioMcpSession = (input: {
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
searchTools: input.searchTools,
toolMode: input.toolMode,
}),
);
return;
Expand Down Expand Up @@ -1526,6 +1535,7 @@ const runStdioMcpSession = (input: {
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
searchTools: input.searchTools,
toolMode: input.toolMode,
}),
);
});
Expand Down Expand Up @@ -2898,11 +2908,30 @@ const mcpCommand = Command.make(
"Serve one search_<integration> tool per connected integration. Off by default; each routes through the same flow as tools.search inside execute.",
),
),
toolMode: Options.choice("mode", ["codemode", "passthrough"] as const)
.pipe(Options.withDefault("codemode"))
.pipe(
Options.withDescription(
"codemode (default) serves the execute tool; passthrough serves search and invoke, with input schemas in search results and client approval for invoke.",
),
),
},
({ scope, elicitationMode, noArtifacts, searchTools }) =>
({ scope, elicitationMode, noArtifacts, searchTools, toolMode }) =>
Effect.gen(function* () {
applyScope(scope);
yield* runStdioMcpSession({ elicitationMode, artifacts: !noArtifacts, searchTools });
if (toolMode === "passthrough" && searchTools) {
return yield* Effect.fail(
new Error(
"--search-tools is a codemode option; passthrough already provides search. Drop --search-tools or --mode passthrough.",
),
);
}
yield* runStdioMcpSession({
elicitationMode,
artifacts: !noArtifacts,
searchTools,
toolMode,
});
}),
).pipe(Command.withDescription("Start an MCP server over stdio"));

Expand Down
2 changes: 2 additions & 0 deletions apps/cloud/src/mcp/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
readArtifactsEnabled,
readElicitationMode,
readSearchToolsEnabled,
readToolMode,
withVerifiedIdentityHeaders,
} from "@executor-js/cloudflare/mcp/do-headers";
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
Expand Down Expand Up @@ -189,6 +190,7 @@ const propsForPrincipal = (
elicitationMode: readElicitationMode(request),
artifactsEnabled: readArtifactsEnabled(request),
searchToolsEnabled: readSearchToolsEnabled(request),
toolMode: readToolMode(request),
resource,
webOrigin: new URL(request.url).origin,
},
Expand Down
4 changes: 4 additions & 0 deletions apps/cloud/src/mcp/session-durable-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,13 +384,17 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD
description,
artifacts: executor.artifacts,
connections: executor.connections,
tools: executor.tools,
// Artifacts are on by default, opt-out per connection. A session
// persisted without a value restores to the default, same as a fresh
// connection whose URL says nothing about `?artifacts=`.
artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
// Per-integration search tools are off by default, opt-in per
// connection (`?search_tools=true`). Same restore rule as artifacts.
searchToolsEnabled: sessionMeta.searchToolsEnabled ?? false,
// The tool surface must survive a cold restore unchanged: the client
// cached the names it saw at `initialize`.
mode: sessionMeta.toolMode ?? "codemode",
// Cold restores rebuild this server with no `initialize` to replay, so
// the negotiated apps support comes back from storage instead.
restoredAppsEnabled: sessionMeta.appsEnabled ?? false,
Expand Down
1 change: 1 addition & 0 deletions apps/cloud/src/mcp/session-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const metaFromIdentity = (
elicitationMode: token.elicitationMode,
artifactsEnabled: token.artifactsEnabled,
searchToolsEnabled: token.searchToolsEnabled,
toolMode: token.toolMode,
};
};

Expand Down
2 changes: 2 additions & 0 deletions apps/host-cloudflare/src/mcp/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
readArtifactsEnabled,
readElicitationMode,
readSearchToolsEnabled,
readToolMode,
withVerifiedIdentityHeaders,
} from "@executor-js/cloudflare/mcp/do-headers";
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
Expand Down Expand Up @@ -86,6 +87,7 @@ const propsForPrincipal = (
elicitationMode: readElicitationMode(request),
artifactsEnabled: readArtifactsEnabled(request),
searchToolsEnabled: readSearchToolsEnabled(request),
toolMode: readToolMode(request),
// host-cloudflare only routes the bare `/mcp` endpoint to the Agent
// bridge (see worker.ts), so the session always serves the default
// resource.
Expand Down
3 changes: 3 additions & 0 deletions apps/host-cloudflare/src/mcp/session-durable-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
elicitationMode: token.elicitationMode,
artifactsEnabled: token.artifactsEnabled,
searchToolsEnabled: token.searchToolsEnabled,
toolMode: token.toolMode,
} satisfies SessionMeta);
}

Expand Down Expand Up @@ -165,13 +166,15 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
engine,
artifacts: executor.artifacts,
connections: executor.connections,
tools: executor.tools,
// Artifacts are on by default, opt-out per connection. A session
// persisted without a value restores to the default, same as a fresh
// connection whose URL says nothing about `?artifacts=`.
artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
// Per-integration search tools are off by default, opt-in per
// connection (`?search_tools=true`). Same restore rule as artifacts.
searchToolsEnabled: sessionMeta.searchToolsEnabled ?? false,
mode: sessionMeta.toolMode ?? "codemode",
// Cold restores rebuild this server with no `initialize` to replay, so
// the negotiated apps support comes back from storage instead.
restoredAppsEnabled: sessionMeta.appsEnabled ?? false,
Expand Down
3 changes: 3 additions & 0 deletions apps/local/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
engine,
artifacts: executor.artifacts,
connections: executor.connections,
tools: executor.tools,
...appsConfig,
},
webBaseUrl: process.env.EXECUTOR_WEB_BASE_URL || undefined,
Expand All @@ -133,6 +134,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
engine,
artifacts: executor.artifacts,
connections: executor.connections,
tools: executor.tools,
...appsConfig,
},
};
Expand All @@ -158,6 +160,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
engine: toolkitEngine,
artifacts: handle.executor.artifacts,
connections: handle.executor.connections,
tools: handle.executor.tools,
...appsConfig,
},
close: handle.dispose,
Expand Down
2 changes: 2 additions & 0 deletions apps/local/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
readArtifactsEnabled,
readElicitationMode,
readSearchToolsEnabled,
readToolMode,
} from "@executor-js/host-mcp/browser-approval";
import { makeInProcessBrowserApprovalStore } from "@executor-js/host-mcp/browser-approval-store";
import {
Expand Down Expand Up @@ -246,6 +247,7 @@ export const createMcpRequestHandler = (
browserApprovalStore: approvals.store,
artifactsEnabled: readArtifactsEnabled(request),
searchToolsEnabled: readSearchToolsEnabled(request),
mode: readToolMode(request),
elicitationMode:
elicitationMode === "browser"
? {
Expand Down
Loading
Loading