Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d5b7338
Add passthrough MCP mode
RhysSullivan Sep 4, 2026
d64685c
Add OpenCode codemode passthrough scenario at 10,200 tools
RhysSullivan Sep 4, 2026
db18de5
Harden passthrough: provenance-gated approvals, injection-safe call c…
RhysSullivan Sep 4, 2026
2082c84
Passthrough: generation-checked catalog read, single-key tool access
RhysSullivan Sep 4, 2026
7eb987e
Stamp catalog builds with a generation id; describeAll refuses a mixe…
RhysSullivan Sep 4, 2026
ded81de
Catalog manifest per connection; passthrough refuses tightened policy…
RhysSullivan Sep 4, 2026
a215897
Catalog without a manifest is refused and rescanned; rebuilds clear t…
RhysSullivan Sep 4, 2026
0c827d8
Rebuilds claim an ownership token; a build that lost its claim never …
RhysSullivan Sep 4, 2026
c12285b
Catalog replacement is one fenced atomic unit; recovery stale-marks e…
RhysSullivan Sep 4, 2026
829b193
replaceMany guard reads every driver's row count or refuses to fence;…
RhysSullivan Sep 4, 2026
0dfffd9
Approval dominates read-only; replaceMany reads mysql2 counts; honest…
RhysSullivan Sep 4, 2026
01e2943
Passthrough integration filter narrows the catalog read; refresh repo…
RhysSullivan Sep 4, 2026
d2dcf24
Scope the stale catalog sync to the requested integration; harden the…
RhysSullivan Sep 4, 2026
5e0fd2a
A lost rebuild claim reports the persisted catalog only when it is whole
RhysSullivan Sep 4, 2026
f2e9b94
Every rebuild reports only a whole persisted catalog and stale-marks …
RhysSullivan Sep 4, 2026
cbd416b
A refused torn catalog is proven recovered on the next read; drop a d…
RhysSullivan Sep 4, 2026
4a0f571
Kysely replaceMany; describeAll ignores torn catalogs the projection …
RhysSullivan Sep 4, 2026
6bab5b2
Toolkit connection scope from real grant overlap; Kysely inserts are …
RhysSullivan Sep 4, 2026
75651f9
Policy resolver and connection scope come from one prepared snapshot;…
RhysSullivan Sep 4, 2026
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
10 changes: 10 additions & 0 deletions .changeset/mcp-passthrough-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@executor-js/sdk": minor
"@executor-js/fumadb": minor
"@executor-js/plugin-openapi": patch
"@executor-js/plugin-graphql": patch
"@executor-js/plugin-mcp": patch
"executor": minor
---

Add a passthrough MCP mode (`?mode=passthrough`, `executor mcp --mode passthrough`) that serves every connected integration tool as its own MCP tool, with workspace policy folded into each tool's annotations so the client's native approval flow applies. Adds `executor.tools.describeAll()` and a `readOnly` tool annotation.
58 changes: 56 additions & 2 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,8 @@ const mcpUrlForActiveLocalServer = (input: {
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
readonly searchTools: boolean;
readonly toolMode: "codemode" | "passthrough";
readonly integrations: readonly string[];
}): URL => {
const url = new URL("/mcp", input.connection.origin);
if (input.elicitationMode === "browser") {
Expand All @@ -1378,6 +1380,14 @@ const mcpUrlForActiveLocalServer = (input: {
if (input.searchTools) {
url.searchParams.set("search_tools", "true");
}
// Passthrough is the non-default surface; only it is spelled out, and the
// integration filter only means anything alongside it.
if (input.toolMode === "passthrough") {
url.searchParams.set("mode", "passthrough");
if (input.integrations.length > 0) {
url.searchParams.set("integrations", input.integrations.join(","));
}
}
return url;
};

Expand All @@ -1394,6 +1404,8 @@ const runMcpHttpBridge = async (input: {
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
readonly searchTools: boolean;
readonly toolMode: "codemode" | "passthrough";
readonly integrations: readonly string[];
}): Promise<void> => {
const stdio = new StdioServerTransport();
const authorization = getExecutorServerAuthorizationHeader(input.manifest.connection);
Expand All @@ -1403,6 +1415,8 @@ const runMcpHttpBridge = async (input: {
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
searchTools: input.searchTools,
toolMode: input.toolMode,
integrations: input.integrations,
}),
authorization ? { requestInit: { headers: { Authorization: authorization } } } : undefined,
);
Expand Down Expand Up @@ -1482,6 +1496,8 @@ const runStdioMcpSession = (input: {
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
readonly searchTools: boolean;
readonly toolMode: "codemode" | "passthrough";
readonly integrations: readonly string[];
}) =>
Effect.gen(function* () {
// `executor mcp` never owns the local database. If a local server is already
Expand All @@ -1499,6 +1515,8 @@ const runStdioMcpSession = (input: {
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
searchTools: input.searchTools,
toolMode: input.toolMode,
integrations: input.integrations,
}),
);
return;
Expand Down Expand Up @@ -1526,6 +1544,8 @@ const runStdioMcpSession = (input: {
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
searchTools: input.searchTools,
toolMode: input.toolMode,
integrations: input.integrations,
}),
);
});
Expand Down Expand Up @@ -2898,11 +2918,45 @@ 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 every connected integration tool directly, with policy folded into each tool's annotations and no execute, skills, or resume.",
),
),
integrations: Options.string("integrations")
.pipe(Options.optional)
.pipe(
Options.withDescription(
"Passthrough only: comma-separated integration slugs to serve. Omit for every connected integration.",
),
),
},
({ scope, elicitationMode, noArtifacts, searchTools }) =>
({ scope, elicitationMode, noArtifacts, searchTools, toolMode, integrations }) =>
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 lists every tool. Drop --search-tools or --mode passthrough.",
),
);
}
yield* runStdioMcpSession({
elicitationMode,
artifacts: !noArtifacts,
searchTools,
toolMode,
integrations: Option.match(integrations, {
onNone: () => [],
onSome: (value) =>
value
.split(",")
.map((slug) => slug.trim())
.filter((slug) => slug.length > 0),
}),
});
}),
).pipe(Command.withDescription("Start an MCP server over stdio"));

Expand Down
2 changes: 2 additions & 0 deletions apps/cloud/drizzle/0018_tool_generation.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "definition" ADD COLUMN "generation" text;--> statement-breakpoint
ALTER TABLE "tool" ADD COLUMN "generation" text;
1 change: 1 addition & 0 deletions apps/cloud/drizzle/0019_connection_tools_manifest.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "connection" ADD COLUMN "tools_manifest" json;
1 change: 1 addition & 0 deletions apps/cloud/drizzle/0020_connection_tools_rebuild.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "connection" ADD COLUMN "tools_rebuild" text;
Loading
Loading