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
8 changes: 8 additions & 0 deletions apps/cloud/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"executor": {
"type": "http",
"url": "https://executor.sh/mcp"
}
}
}
4 changes: 4 additions & 0 deletions apps/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@executor/api": "workspace:*",
"@executor/env": "workspace:*",
"@executor/execution": "workspace:*",
"@executor/host-mcp": "workspace:*",
"@executor/plugin-google-discovery": "workspace:*",
"@executor/plugin-graphql": "workspace:*",
"@executor/plugin-mcp": "workspace:*",
Expand All @@ -19,11 +20,14 @@
"@executor/runtime-dynamic-worker": "workspace:*",
"@executor/sdk": "workspace:*",
"@executor/storage-postgres": "workspace:*",
"@modelcontextprotocol/sdk": "^1.29.0",
"@tanstack/react-router": "catalog:",
"@tanstack/react-start": "catalog:",
"@workos-inc/node": "^7.0.0",
"agents": "^0.10.0",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"jose": "^5.6.3",
"pg": "^8.16.0",
"react": "catalog:",
"react-dom": "catalog:"
Expand Down
7 changes: 4 additions & 3 deletions apps/cloud/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { CloudAuthHandlers, CloudAuthPublicHandlers } from "./auth/handlers";
import { WorkOSAuth } from "./auth/workos";
import { DbService } from "./services/db";
import { createTeamExecutor } from "./services/executor";
import { cf, server } from "./env";
import { server } from "./env";

const ProtectedCloudApi = addGroup(OpenApiGroup)
.add(McpGroup)
Expand Down Expand Up @@ -233,8 +233,9 @@ export const handleApiRequest = async (request: Request): Promise<Response> => {
);

const handler = yield* Effect.acquireRelease(
Effect.sync(() => {
const codeExecutor = makeDynamicWorkerExecutor({ loader: cf.loader });
Effect.tryPromise(async () => {
const { env } = await import("cloudflare:workers");
const codeExecutor = makeDynamicWorkerExecutor({ loader: (env as any).LOADER });
return createProtectedHandler(auth, teamId, executor, codeExecutor);
}),
disposeProtectedHandler,
Expand Down
10 changes: 0 additions & 10 deletions apps/cloud/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { env as cfEnv } from "cloudflare:workers";
import { createEnv, Env } from "@executor/env";

const sharedShape = {
Expand Down Expand Up @@ -53,12 +52,3 @@ export const server = createEnv(serverShape, {
emptyStringAsUndefined: true,
}) as ServerEnv;

// ---------------------------------------------------------------------------
// Cloudflare bindings — single boundary for all platform-specific access
// ---------------------------------------------------------------------------

export const cf = {
get hyperdrive() { return cfEnv.HYPERDRIVE; },
get loader() { return cfEnv.LOADER; },
get marketing() { return cfEnv.MARKETING; },
};
155 changes: 155 additions & 0 deletions apps/cloud/src/mcp-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// ---------------------------------------------------------------------------
// MCP Session Durable Object — holds MCP server + engine per session
// ---------------------------------------------------------------------------

import { DurableObject, env } from "cloudflare:workers";
import { Effect, Layer } from "effect";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WorkerTransport, type TransportState } from "agents/mcp";

import { createExecutorMcpServer } from "@executor/host-mcp";
import { makeDynamicWorkerExecutor } from "@executor/runtime-dynamic-worker";

import { UserStoreService } from "./auth/context";
import { server } from "./env";
import { createTeamExecutor } from "./services/executor";
import { DbService } from "./services/db";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type McpSessionInit = {
userId: string;
};

// Heartbeat interval — keeps the DO alive by re-scheduling an alarm before
// Cloudflare's ~60s idle eviction kicks in.
const HEARTBEAT_MS = 30 * 1000;

// Session timeout — clean up after no requests for this long.
// TODO: Make tier-based — free users get 60s, paid users get 5 minutes.
const SESSION_TIMEOUT_MS = 5 * 60 * 1000;

// ---------------------------------------------------------------------------
// Session initialization effect
// ---------------------------------------------------------------------------

const DbLive = DbService.Unscoped;
const UserStoreLive = UserStoreService.Live.pipe(Layer.provide(DbLive));
const Services = Layer.mergeAll(DbLive, UserStoreLive);

const initSession = (userId: string) =>
Effect.gen(function* () {
const users = yield* UserStoreService;
const teams = yield* users.use((store) => store.getTeamsForUser(userId));

if (teams.length === 0) {
return yield* Effect.fail(
new Error("No team found for user — account may not be set up"),
);
}

const { teamId, teamName } = {
teamId: teams[0]!.teamId,
teamName: teams[0]!.teamName ?? "Team",
};

const executor = yield* createTeamExecutor(
teamId,
teamName,
server.ENCRYPTION_KEY,
);

const codeExecutor = makeDynamicWorkerExecutor({ loader: env.LOADER });
const mcpServer = yield* Effect.promise(() =>
createExecutorMcpServer({ executor, codeExecutor }),
);

return mcpServer;
}).pipe(Effect.provide(Services));

// ---------------------------------------------------------------------------
// JSON-RPC error response helper
// ---------------------------------------------------------------------------

const jsonRpcError = (status: number, code: number, message: string) =>
new Response(
JSON.stringify({ jsonrpc: "2.0", error: { code, message }, id: null }),
{ status, headers: { "content-type": "application/json" } },
);

// ---------------------------------------------------------------------------
// Durable Object
// ---------------------------------------------------------------------------

export class McpSessionDO extends DurableObject {
private mcpServer: McpServer | null = null;
private transport: WorkerTransport | null = null;
private initialized = false;
private lastActivityMs = 0;

private makeStorage() {
return {
get: async (): Promise<TransportState | undefined> => {
return await this.ctx.storage.get<TransportState>("transport");
},
set: async (state: TransportState): Promise<void> => {
await this.ctx.storage.put("transport", state);
},
};
}

async init(token: McpSessionInit): Promise<void> {
if (this.initialized) return;

this.mcpServer = await Effect.runPromise(initSession(token.userId));

this.transport = new WorkerTransport({
sessionIdGenerator: () => this.ctx.id.toString(),
storage: this.makeStorage(),
});

await this.mcpServer.connect(this.transport);
this.initialized = true;
this.lastActivityMs = Date.now();

await this.ctx.storage.setAlarm(Date.now() + HEARTBEAT_MS);
}

async handleRequest(request: Request): Promise<Response> {
if (!this.initialized || !this.transport) {
return jsonRpcError(404, -32001, "Session timed out due to inactivity — please reconnect");
}

this.lastActivityMs = Date.now();

try {
return await this.transport.handleRequest(request);
} catch (err) {
console.error("[mcp-session] handleRequest error:", err instanceof Error ? err.stack : err);
return jsonRpcError(500, -32603, err instanceof Error ? err.message : "Internal error");
}
}

async alarm(): Promise<void> {
const idleMs = Date.now() - this.lastActivityMs;
if (idleMs >= SESSION_TIMEOUT_MS) {
await this.cleanup();
return;
}
await this.ctx.storage.setAlarm(Date.now() + HEARTBEAT_MS);
}

private async cleanup(): Promise<void> {
if (this.transport) {
await this.transport.close().catch(() => undefined);
this.transport = null;
}
if (this.mcpServer) {
await this.mcpServer.close().catch(() => undefined);
this.mcpServer = null;
}
this.initialized = false;
}
}
Loading