Skip to content
Closed
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
1 change: 0 additions & 1 deletion apps/host-selfhost/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"@executor-js/sdk": "workspace:*",
"@libsql/client": "catalog:",
"@libsql/kysely-libsql": "catalog:",
"@modelcontextprotocol/sdk": "^1.29.0",
"@tanstack/react-router": "catalog:",
"better-auth": "^1.6.11",
"drizzle-orm": "catalog:",
Expand Down
4 changes: 2 additions & 2 deletions apps/host-selfhost/src/mcp/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { SelfHostExecutionStackLayer } from "../execution";
// ALL shared (`@executor-js/host-mcp/in-memory-session-store` + `makeMcpBuildServer`
// / `makeConsoleMcpErrorReporter` in `@executor-js/api/server`). Self-host
// supplies only its fully-provided execution-stack layer (QuickJS over the
// long-lived `SelfHostDb`) and its `ErrorCapture`. The Cloudflare host wires the
// identical seam with its own stack layer.
// long-lived `SelfHostDb`) and its `ErrorCapture`; the builder creates the
// connection-lifetime SDK v2 assembly used by the shared store.
// ---------------------------------------------------------------------------

import { loadMcpAppsShellHtml } from "@executor-js/mcp-apps-shell";
Expand Down
2 changes: 1 addition & 1 deletion apps/local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"@executor-js/sdk": "workspace:*",
"@executor-js/vite-plugin": "workspace:*",
"@libsql/client": "catalog:",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/server": "2.0.0",
"@tanstack/react-router": "catalog:",
"drizzle-orm": "catalog:",
Expand All @@ -57,6 +56,7 @@
},
"devDependencies": {
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@rhyssul/portless": "^0.13.0",
"@tailwindcss/vite": "catalog:",
"@tanstack/router-plugin": "^1.167.12",
Expand Down
44 changes: 44 additions & 0 deletions apps/local/src/mcp-stdio-test-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Effect } from "effect";

import type { ExecutionEngine, ExecutionResult } from "@executor-js/execution";
import { FormElicitation, ToolAddress } from "@executor-js/sdk";

import { runMcpStdioServer } from "./mcp";

const TOOL_ADDRESS = ToolAddress.make("tools.test.org.main.approve");
const paused: Extract<ExecutionResult, { status: "paused" }> = {
status: "paused",
execution: {
id: "stdio-execution",
elicitationContext: {
address: TOOL_ADDRESS,
args: {},
request: FormElicitation.make({
message: "Approve the stdio action?",
requestedSchema: {
type: "object",
properties: { value: { type: "string" } },
required: ["value"],
},
}),
},
},
};

const engine: ExecutionEngine = {
execute: () => Effect.succeed({ result: "unused" }),
executeWithPause: (code) =>
code === "needs approval"
? Effect.succeed(paused)
: Effect.succeed({ status: "completed", result: { result: 4 } }),
resume: (_executionId, response) =>
Effect.succeed({ status: "completed", result: { result: response.content?.value } }),
isExecutionSettled: () => Effect.succeed(false),
getPausedExecution: (executionId) =>
Effect.succeed(executionId === paused.execution.id ? paused.execution : null),
pausedExecutionCount: () => Effect.succeed(1),
hasPausedExecutions: () => Effect.succeed(true),
getDescription: Effect.succeed("stdio integration test executor"),
};

await runMcpStdioServer({ engine, elicitationMode: { mode: "native" } });
36 changes: 22 additions & 14 deletions apps/local/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,24 @@ import { Effect, type Cause } from "effect";
import {
createMcpHandler,
isLegacyRequest,
McpServer,
WebStandardStreamableHTTPServerTransport,
type McpHttpHandler,
} from "@modelcontextprotocol/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { serveStdio } from "@modelcontextprotocol/server/stdio";

import {
defaultMcpResource,
jsonRpcErrorBody,
mcpResourceKey,
type McpResource,
} from "@executor-js/host-mcp";
import {
createExecutorMcpServer,
type ExecutorMcpServerConfig,
} from "@executor-js/host-mcp/tool-server";
import {
appsEnabledForClientCapabilities,
buildMcpServerV2,
clientCapabilitiesFromRequest,
requestBodyFromRequest,
type ExecutorMcpServerConfig,
} from "@executor-js/host-mcp/tool-server-v2";
import {
approvalUrlForRequest,
Expand Down Expand Up @@ -280,10 +277,14 @@ export const createMcpRequestHandler = (
const elicitationMode = readElicitationMode(request);
resourceConfig = await configForResource(resource);
created = await Effect.runPromise(
createExecutorMcpServer({
buildMcpServerV2({
...resourceConfig.config,
browserApprovalStore: approvals.store,
artifactsEnabled: readArtifactsEnabled(request),
appsEnabled: resourceConfig.config.restoredAppsEnabled ?? false,
requestStateSigningKey: signingKey(),
requestStatePrincipal: "local",
sessionful: true,
elicitationMode:
elicitationMode === "browser"
? {
Expand Down Expand Up @@ -362,9 +363,18 @@ export const createMcpRequestHandler = (
export const runMcpStdioServer = async (config: ExecutorMcpServerConfig): Promise<void> => {
startIntegrationsRefresh();

// Deliberately v1-only in this release; modern stdio clients use their probe fallback policy.
const server = await Effect.runPromise(createExecutorMcpServer(config));
const transport = new StdioServerTransport();
const requestStateSigningKey = crypto.getRandomValues(new Uint8Array(32));
const stdio = serveStdio(() =>
Effect.runPromise(
buildMcpServerV2({
...config,
appsEnabled: config.restoredAppsEnabled ?? false,
requestStateSigningKey,
requestStatePrincipal: "local",
sessionful: true,
}),
),
);

const waitForExit = () =>
new Promise<void>((resolve) => {
Expand All @@ -383,10 +393,8 @@ export const runMcpStdioServer = async (config: ExecutorMcpServerConfig): Promis

// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: stdio server lifetime uses Promise-based SDK/process APIs and always closes resources
try {
await server.connect(transport);
await waitForExit();
} finally {
await ignoreClose(() => transport.close());
await ignoreClose(() => server.close());
await ignoreClose(() => stdio.close());
}
};
3 changes: 1 addition & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 11 additions & 16 deletions packages/core/api/src/server/mcp-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import {
import {
McpEngineBuildError,
type McpBuildServer,
type McpBuildServerOptions,
} from "@executor-js/host-mcp/in-memory-session-store";
import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server";
import { buildMcpServerV2 } from "@executor-js/host-mcp/tool-server-v2";
import {
artifactUrlFor,
Expand All @@ -25,14 +23,9 @@ import { HostConfig, PluginsProvider, RequestOrgSlug } from "./scoped-executor";
// ---------------------------------------------------------------------------
// Shared in-process MCP host helpers.
//
// Every host that serves MCP from one isolate (self-host, the Cloudflare QuickJS
// host) builds its per-session McpServer the same way — assemble the scoped
// engine via `makeExecutionStack`, wrap it with `createExecutorMcpServer` — and
// reports orchestration defects through the same console `ErrorCapture` seam.
// These two factories are the single home for that logic; a host supplies ONLY
// its fully-provided execution-stack layer and its `ErrorCapture` layer. The
// cross-isolate variant (cloud's Durable Object store) is the exception that
// builds its engine inside the DO.
// Neutral hosts build both sessionful legacy connections and stateless modern
// requests from the same SDK v2 assembly over a scoped execution stack. The
// Cloudflare Durable Object path remains a separate v1-backed composition root.
// ---------------------------------------------------------------------------

/** The five execution-stack seams a host fully provides (no residual). */
Expand All @@ -42,18 +35,19 @@ export type McpExecutionStackLayer = Layer.Layer<

/**
* Build the per-session MCP server factory over a host's execution stack:
* `makeExecutionStack` → engine → `createExecutorMcpServer`. Hosts differ only
* `makeExecutionStack` → engine → `buildMcpServerV2`. Hosts differ only
* in the injected stack layer (libSQL vs D1, etc.).
*/
export const makeMcpBuildServer =
(executionStack: McpExecutionStackLayer, hostOptions?: McpBuildHostOptions): McpBuildServer =>
(principal: Principal, options?: McpBuildServerOptions) =>
Effect.gen(function* () {
(principal: Principal, options) => {
const { resource, ...serverOptions } = options;
return Effect.gen(function* () {
const { engine, executor } = yield* makeExecutionStack(
principal.accountId,
principal.organizationId,
principal.organizationName,
{ mcpResource: options?.resource },
{ mcpResource: resource },
).pipe(Effect.withSpan("mcp.execution_stack.build"));
// Read inside the provided boundary: `webBaseUrl` is a host seam, and
// hosts that can't know their public URL at boot leave it unset — in
Expand All @@ -69,7 +63,7 @@ export const makeMcpBuildServer =
Effect.provide(executionStack),
Effect.mapError((cause) => new McpEngineBuildError({ cause })),
Effect.flatMap(({ engine, executor, webBaseUrl }) =>
createExecutorMcpServer({
buildMcpServerV2({
engine,
artifacts: executor.artifacts,
connections: executor.connections,
Expand All @@ -87,13 +81,14 @@ export const makeMcpBuildServer =
...(webBaseUrl
? { artifactUrl: artifactUrlFor(webBaseUrl, principal.organizationSlug) }
: {}),
...(options ?? {}),
...serverOptions,
}).pipe(
Effect.withSpan("mcp.server.create"),
Effect.map((mcpServer) => ({ mcpServer, engine })),
),
),
);
};

/** Build function consumed by the neutral envelope's modern-server seam. */
export type McpBuildServerV2 = McpModernServerBuilder["Service"]["build"];
Expand Down
Loading
Loading