History

mcp server

Rhys Sullivan committed 8 months ago

Commit 439cbf21 parent 515030516 files · +318 −96Browse files

Unified · Split

16 files
  1. apps/executor/src/cli/main.ts +10 −12
  2. apps/executor/src/distribution/harness.ts +2 −1
  3. apps/executor/src/server/server.test.ts +9 −8
  4. apps/web/src/components/local-mcp-install-card.tsx +37 −0
  5. apps/web/src/dev.ts +48 −20
  6. apps/web/src/views/add-source.tsx +9 −0
  7. apps/web/src/views/home.tsx +3 −0
  8. apps/web/vite.config.ts +2 −2
  9. packages/codemode-mcp/src/mcp-tools.ts +18 −6
  10. packages/control-plane/src/persistence/index.ts +4 −2
  11. packages/control-plane/src/persistence/sql-runtime.ts +43 −2
  12. packages/control-plane/src/runtime/executor-tools.ts +14 −5
  13. packages/control-plane/src/runtime/index.ts +1 −1
  14. packages/executor-mcp/src/index.ts +83 −23
  15. packages/react/src/index.ts +26 −10
  16. packages/server/src/index.ts +9 −4
apps/executor/src/cli/main.ts+10−12
@@ -652,18 +652,16 @@ const stopServer = (baseUrl: string) => · 651 unmodified lines
652 const pid = typeof pidRecord?.pid === "number" ? pidRecord.pid : null;652 const pid = typeof pidRecord?.pid === "number" ? pidRecord.pid : null;
653 653
654 if (pid === null) {654 if (pid === null) {
655 yield* Effect.tryPromise({655 yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
656 try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }),656 Effect.ignore,
657 catch: () => undefined,657 );
658 });
659 return false;658 return false;
660 }659 }
661 660
662 if (!isPidRunning(pid)) {661 if (!isPidRunning(pid)) {
663 yield* Effect.tryPromise({662 yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
664 try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }),663 Effect.ignore,
665 catch: () => undefined,664 );
666 });
667 return false;665 return false;
668 }666 }
669 667
@@ -674,10 +672,10 @@ const stopServer = (baseUrl: string) => · 4 unmodified lines
674 672
675 yield* waitForReachability(baseUrl, false).pipe(673 yield* waitForReachability(baseUrl, false).pipe(
676 Effect.catchAll(() =>674 Effect.catchAll(() =>
677 Effect.tryPromise({675 Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
678 try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }),676 Effect.ignore,
679 catch: () => undefined,677 Effect.zipRight(Effect.fail(new Error(`Timed out stopping local executor server pid ${pid}`))),
680 }).pipe(Effect.zipRight(Effect.fail(new Error(`Timed out stopping local executor server pid ${pid}`)))),678 ),
681 ),679 ),
682 );680 );
683 681
apps/executor/src/distribution/harness.ts+2−1
@@ -176,7 +176,8 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 175 unmodified lines
176 }),176 }),
177 (path) => Effect.tryPromise({177 (path) => Effect.tryPromise({
178 try: () => rm(path, { recursive: true, force: true }),178 try: () => rm(path, { recursive: true, force: true }),
179 catch: () => undefined,179 catch: (cause) =>
180 cause instanceof Error ? cause : new Error(String(cause ?? "temp dir cleanup failed")),
180 }).pipe(Effect.orDie),181 }).pipe(Effect.orDie),
181 );182 );
182 183
apps/executor/src/server/server.test.ts+9−8
@@ -647,12 +647,13 @@ describe("local-executor-server", () => { · 646 unmodified lines
647 });647 });
648 648
649 const listed = yield* Effect.promise(649 const listed = yield* Effect.promise(
650 () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string }> }>,650 () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string; description?: string }> }>,
651 );651 );
652 expect(listed.tools.map((tool) => tool.name)).toEqual(["execute"]);652 expect(listed.tools.map((tool) => tool.name)).toEqual(["execute"]);
653 expect(mcp.client.getInstructions()).toContain(653 expect(listed.tools[0]?.description).toContain("Workflow:");
654 `npx add-mcp \"${server.baseUrl}/mcp\" --transport http --name \"executor\"`,654 expect(listed.tools[0]?.description).toContain("tools.discover");
655 );655 expect(listed.tools[0]?.description).toContain("tools.executor.sources.add");
656
656 657
657 const executed = yield* Effect.promise(658 const executed = yield* Effect.promise(
658 () =>659 () =>
@@ -686,12 +687,12 @@ describe("local-executor-server", () => { · 27 unmodified lines
686 });687 });
687 688
688 const listed = yield* Effect.promise(689 const listed = yield* Effect.promise(
689 () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string }> }>,690 () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string; description?: string }> }>,
690 );691 );
691 expect(listed.tools.map((tool) => tool.name).sort()).toEqual(["execute", "resume"]);692 expect(listed.tools.map((tool) => tool.name).sort()).toEqual(["execute", "resume"]);
692 expect(mcp.client.getInstructions()).toContain(693 expect(listed.tools.find((tool) => tool.name === "execute")?.description).toContain("Workflow:");
693 `npx add-mcp \"${server.baseUrl}/mcp\" --transport http --name \"executor\"`,694 expect(listed.tools.find((tool) => tool.name === "execute")?.description).toContain("tools.discover");
694 );695
695 696
696 const executed = yield* Effect.promise(697 const executed = yield* Effect.promise(
697 () =>698 () =>
apps/web/src/components/local-mcp-install-card.tsx+37−0
@@ -0,0 +1,37 @@
1import { useEffect, useMemo, useState } from "react";
2import { CodeBlock } from "./code-block";
3
4export function LocalMcpInstallCard(props: {
5 title?: string;
6 description?: string;
7 className?: string;
8}) {
9 const [origin, setOrigin] = useState<string | null>(null);
10
11 useEffect(() => {
12 setOrigin(window.location.origin);
13 }, []);
14
15 const command = useMemo(
16 () =>
17 origin
18 ? `npx add-mcp "${origin}/mcp" --transport http --name "executor"`
19 : 'npx add-mcp "<this-server>/mcp" --transport http --name "executor"',
20 [origin],
21 );
22
23 return (
24 <section className={props.className ?? "rounded-2xl border border-border bg-card/80 p-5"}>
25 <div className="mb-3 space-y-1">
26 <h2 className="text-sm font-semibold text-foreground">
27 {props.title ?? "Install local MCP"}
28 </h2>
29 <p className="text-[13px] text-muted-foreground">
30 {props.description
31 ?? "Add this local executor server to an MCP client with one command. The URL uses the same origin as this web app."}
32 </p>
33 </div>
34 <CodeBlock code={command} lang="bash" className="rounded-xl border border-border bg-background/70" />
35 </section>
36 );
37}
apps/web/src/dev.ts+48−20
@@ -5,7 +5,10 @@ · 4 unmodified lines
5 * control-plane handler. Everything else (frontend assets, HMR) is5 * control-plane handler. Everything else (frontend assets, HMR) is
6 * handled by Vite itself.6 * handled by Vite itself.
7 */7 */
8import * as Cause from "effect/Cause";
8import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
10import * as Exit from "effect/Exit";
11import * as Runtime from "effect/Runtime";
9import * as Scope from "effect/Scope";12import * as Scope from "effect/Scope";
10import { createLocalExecutorRequestHandler } from "@executor-v3/server";13import { createLocalExecutorRequestHandler } from "@executor-v3/server";
11 14
@@ -16,28 +19,53 @@ const truncateForLog = (value: string): string => · 4 unmodified lines
16 ? `${value.slice(0, MAX_LOGGED_ERROR_BODY_LENGTH)}... [truncated]`19 ? `${value.slice(0, MAX_LOGGED_ERROR_BODY_LENGTH)}... [truncated]`
17 : value;20 : value;
18 21
19const formatErrorForLog = (error: unknown) =>22/**
20 error instanceof Error23 * Extract a detailed, human-readable description from an error.
21 ? {24 *
22 name: error.name,25 * When Effect rejects via `runPromise`, the thrown value is a
23 message: error.message,26 * `FiberFailure` wrapping a `Cause`. `Cause.pretty` renders the full
24 stack: error.stack,27 * failure tree (defects, interrupts, tagged errors, etc.) rather than the
25 }28 * opaque fallback message ("An error has occurred").
26 : error;29 */
30const formatErrorForLog = (error: unknown): string => {
31 if (Runtime.isFiberFailure(error)) {
32 const cause = error[Runtime.FiberFailureCauseId];
33 return Cause.pretty(cause);
34 }
35
36 if (error instanceof Error) {
37 return error.stack ?? error.message;
38 }
39
40 return String(error);
41};
27 42
28// Create a long-lived scope that stays open for the lifetime of the process.43// Create a long-lived scope that stays open for the lifetime of the process.
29const handlerPromise = Effect.runPromise(44const handlerPromise = (async () => {
30 Effect.gen(function* () {45 const exit = await Effect.runPromiseExit(
31 const scope = yield* Scope.make();46 Effect.gen(function* () {
32 const handler = yield* createLocalExecutorRequestHandler().pipe(47 const scope = yield* Scope.make();
33 Effect.provideService(Scope.Scope, scope),48 const handler = yield* createLocalExecutorRequestHandler().pipe(
34 );49 Effect.provideService(Scope.Scope, scope),
35 return handler;50 );
36 }),51 return handler;
37).catch((error) => {52 }),
38 console.error("[executor dev api] failed to initialize request handler", formatErrorForLog(error));53 );
39 throw error;54
40});55 if (Exit.isSuccess(exit)) {
56 return exit.value;
57 }
58
59 // Log the full Cause tree — this captures typed errors, defects (uncaught
60 // throws), and interrupts with their original stack traces.
61 console.error(
62 "[executor dev api] failed to initialize request handler\n\n" +
63 Cause.pretty(exit.cause),
64 );
65
66 // Re-throw so every subsequent request sees the init failure.
67 throw Cause.squash(exit.cause);
68})();
41 69
42export default {70export default {
43 async fetch(request: Request) {71 async fetch(request: Request) {
apps/web/src/views/add-source.tsx+9−0
@@ -18,6 +18,7 @@ import { · 17 unmodified lines
18} from "@executor-v3/react";18} from "@executor-v3/react";
19import { Badge } from "../components/ui/badge";19import { Badge } from "../components/ui/badge";
20import { Button } from "../components/ui/button";20import { Button } from "../components/ui/button";
21import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
21import { SourceFavicon } from "../components/source-favicon";22import { SourceFavicon } from "../components/source-favicon";
22import {23import {
23 IconArrowLeft,24 IconArrowLeft,
@@ -737,6 +738,14 @@ export function AddSourcePage() { · 713 unmodified lines
737 738
738 {statusBanner && <StatusBanner state={statusBanner} className="mb-6" />}739 {statusBanner && <StatusBanner state={statusBanner} className="mb-6" />}
739 740
741
742 {(phase === "idle" || phase === "discovering") && (
743 <LocalMcpInstallCard
744 className="mb-6 rounded-xl border border-border bg-card/80 p-5"
745 title="Install this executor as MCP"
746 description="Prefer a one-command setup? Install this local executor server into your MCP client, or add an external MCP source below."
747 />
748 )}
740 {/* Step 1: Discovery */}749 {/* Step 1: Discovery */}
741 {(phase === "idle" || phase === "discovering") && (750 {(phase === "idle" || phase === "discovering") && (
742 <div className="space-y-6">751 <div className="space-y-6">
apps/web/src/views/home.tsx+3−0
@@ -1,6 +1,7 @@
1import { Link } from "@tanstack/react-router";1import { Link } from "@tanstack/react-router";
2import { useSources, type Source } from "@executor-v3/react";2import { useSources, type Source } from "@executor-v3/react";
3import { LoadableBlock } from "../components/loadable";3import { LoadableBlock } from "../components/loadable";
4import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
4import { SourceFavicon } from "../components/source-favicon";5import { SourceFavicon } from "../components/source-favicon";
5import { Badge } from "../components/ui/badge";6import { Badge } from "../components/ui/badge";
6import { Button } from "../components/ui/button";7import { Button } from "../components/ui/button";
@@ -30,6 +31,8 @@ export function HomePage() { · 23 unmodified lines
30 </Link>31 </Link>
31 </div>32 </div>
32 33
34 <LocalMcpInstallCard className="mb-8" />
35
33 {/* Source list */}36 {/* Source list */}
34 <LoadableBlock loadable={sources} loading="Loading sources...">37 <LoadableBlock loadable={sources} loading="Loading sources...">
35 {(items) =>38 {(items) =>
apps/web/vite.config.ts+2−2
@@ -11,8 +11,8 @@ export default defineConfig({ · 10 unmodified lines
11 devServer({11 devServer({
12 entry: "src/dev.ts",12 entry: "src/dev.ts",
13 exclude: [13 exclude: [
14 // Only let /v1 requests reach the API handler14 // Only let /v1 and /mcp requests reach the API handler
15 /^\/(?!v1(\/|$))/,15 /^\/(?!(v1|mcp)(\/|$))/,
16 /^\/(src|node_modules|@vite|@id|@react-refresh)/,16 /^\/(src|node_modules|@vite|@id|@react-refresh)/,
17 /\.(css|ts|tsx|js|jsx|svg|png|jpg|gif|ico|woff2?|json|map)(\?.*)?$/,17 /\.(css|ts|tsx|js|jsx|svg|png|jpg|gif|ico|woff2?|json|map)(\?.*)?$/,
18 ],18 ],
packages/codemode-mcp/src/mcp-tools.ts+18−6
@@ -2,6 +2,8 @@ import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js"; · 1 unmodified line
2import * as Data from "effect/Data";2import * as Data from "effect/Data";
3import * as Either from "effect/Either";3import * as Either from "effect/Either";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Cause from "effect/Cause";
6import * as Exit from "effect/Exit";
5import * as PartitionedSemaphore from "effect/PartitionedSemaphore";7import * as PartitionedSemaphore from "effect/PartitionedSemaphore";
6 8
7import {9import {
@@ -88,10 +90,11 @@ const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => { · 80 unmodified lines
88const closeConnection = (connection: McpConnection): Effect.Effect<void> =>90const closeConnection = (connection: McpConnection): Effect.Effect<void> =>
89 Effect.tryPromise({91 Effect.tryPromise({
90 try: () => connection.close?.() ?? Promise.resolve(),92 try: () => connection.close?.() ?? Promise.resolve(),
91 catch: () => undefined,93 catch: (cause) =>
94 cause instanceof Error ? cause : new Error(String(cause ?? "mcp connection close failed")),
92 }).pipe(95 }).pipe(
93 Effect.asVoid,96 Effect.asVoid,
94 Effect.catchAll(() => Effect.succeed(undefined)),97 Effect.catchAll(() => Effect.void),
95 );98 );
96 99
97const withConnectionEffect = <A, E>(input: {100const withConnectionEffect = <A, E>(input: {
@@ -192,7 +195,13 @@ const installMcpElicitationHandler = (input: { · 94 unmodified lines
192 }),195 }),
193 ),196 ),
194 Effect.map(toMcpElicitationResponse),197 Effect.map(toMcpElicitationResponse),
195 Effect.catchAll(() => Effect.succeed({ action: "cancel" as const })),198 Effect.catchAll((error) => {
199 console.error(
200 `[mcp-tools] elicitation failed for ${input.toolName}, treating as cancel:`,
201 error instanceof Error ? error.message : String(error),
202 );
203 return Effect.succeed({ action: "cancel" as const });
204 }),
196 ),205 ),
197 );206 );
198 });207 });
@@ -427,8 +436,8 @@ export const createMcpToolsFromManifest = (input: { · 228 unmodified lines
427 tool: {436 tool: {
428 description: entry.description ?? `MCP tool: ${entry.toolName}`,437 description: entry.description ?? `MCP tool: ${entry.toolName}`,
429 inputSchema: inputSchemaFromManifest(entry.inputSchemaJson),438 inputSchema: inputSchemaFromManifest(entry.inputSchemaJson),
430 execute: (args: unknown, executionContext?: ToolExecutionContext) =>439 execute: async (args: unknown, executionContext?: ToolExecutionContext) => {
431 Effect.runPromise(440 const exit = await Effect.runPromiseExit(
432 withConnectionEffect({441 withConnectionEffect({
433 connect: input.connect,442 connect: input.connect,
434 onConnectError: (cause) =>443 onConnectError: (cause) =>
@@ -453,7 +462,10 @@ export const createMcpToolsFromManifest = (input: { · 18 unmodified lines
453 : callEffect;462 : callEffect;
454 },463 },
455 }),464 }),
456 ),465 );
466 if (Exit.isSuccess(exit)) return exit.value;
467 throw Cause.squash(exit.cause);
468 },
457 },469 },
458 metadata: {470 metadata: {
459 sourceKey,471 sourceKey,
packages/control-plane/src/persistence/index.ts+4−2
@@ -113,7 +113,8 @@ const runMigrationsEffect = ( · 112 unmodified lines
113const closeRuntimeEffect = (runtime: SqlRuntime) =>113const closeRuntimeEffect = (runtime: SqlRuntime) =>
114 Effect.tryPromise({114 Effect.tryPromise({
115 try: () => runtime.close(),115 try: () => runtime.close(),
116 catch: () => undefined,116 catch: (cause) =>
117 cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")),
117 }).pipe(Effect.orDie);118 }).pipe(Effect.orDie);
118 119
119export const createSqlControlPlanePersistence = (120export const createSqlControlPlanePersistence = (
@@ -150,7 +151,8 @@ export const SqlControlPlanePersistenceLive = ( · 30 unmodified lines
150 (persistence) =>151 (persistence) =>
151 Effect.tryPromise({152 Effect.tryPromise({
152 try: () => persistence.close(),153 try: () => persistence.close(),
153 catch: () => undefined,154 catch: (cause) =>
155 cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")),
154 }).pipe(Effect.orDie),156 }).pipe(Effect.orDie),
155 ),157 ),
156 );158 );
packages/control-plane/src/persistence/sql-runtime.ts+43−2
@@ -3,8 +3,8 @@ import { drizzle as drizzlePGlite } from "drizzle-orm/pglite"; · 2 unmodified lines
3import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator";3import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator";
4import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";4import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
5import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";5import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";
6import { existsSync, readdirSync } from "node:fs";6import { existsSync, readdirSync, readFileSync } from "node:fs";
7import { mkdir } from "node:fs/promises";7import { mkdir, unlink } from "node:fs/promises";
8import * as path from "node:path";8import * as path from "node:path";
9import { fileURLToPath } from "node:url";9import { fileURLToPath } from "node:url";
10import postgres from "postgres";10import postgres from "postgres";
@@ -44,6 +44,46 @@ const trim = (value: string | undefined): string | undefined => { · 33 unmodified lines
44const isPostgresUrl = (value: string): boolean =>44const isPostgresUrl = (value: string): boolean =>
45 value.startsWith("postgres://") || value.startsWith("postgresql://");45 value.startsWith("postgres://") || value.startsWith("postgresql://");
46 46
47/**
48 * Remove a stale PGlite `postmaster.pid` lock file if present.
49 *
50 * PGlite writes this file on open and removes it on close. When a process
51 * exits without calling `PGlite.close()` (e.g. the Vite dev server is
52 * killed with SIGKILL, or the terminal is closed), the lock file is left
53 * behind and prevents subsequent PGlite instances from opening the
54 * database. PGlite uses a synthetic PID (e.g. `-42`) that is never a real
55 * OS process, so we cannot use it to detect a live owner. Instead we rely
56 * on the fact that in the local single-user context only one process
57 * should ever own this database at a time.
58 */
59const cleanupStalePGliteLock = async (dataDir: string): Promise<void> => {
60 const lockPath = path.join(dataDir, "postmaster.pid");
61 if (!existsSync(lockPath)) {
62 return;
63 }
64
65 // PGlite's synthetic PID is always negative (e.g. -42). A real Postgres
66 // postmaster would write a positive PID. If we ever see a positive PID
67 // we leave the file alone.
68 try {
69 const content = readFileSync(lockPath, "utf-8");
70 const firstLine = content.split("\n")[0]?.trim();
71 const pid = Number(firstLine);
72 if (!Number.isNaN(pid) && pid > 0) {
73 // Looks like a real Postgres PID — don't touch it.
74 return;
75 }
76 } catch {
77 // If we can't read the file, try to remove it anyway.
78 }
79
80 try {
81 await unlink(lockPath);
82 } catch {
83 // Best-effort cleanup; PGlite will report the real error if this fails.
84 }
85};
86
47const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => {87const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => {
48 const normalized = trim(localDataDir) ?? ".executor-v3/control-plane-pgdata";88 const normalized = trim(localDataDir) ?? ".executor-v3/control-plane-pgdata";
49 89
@@ -55,6 +95,7 @@ const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => · 5 unmodified lines
55 if (!existsSync(resolvedDataDir)) {95 if (!existsSync(resolvedDataDir)) {
56 await mkdir(resolvedDataDir, { recursive: true });96 await mkdir(resolvedDataDir, { recursive: true });
57 }97 }
98 await cleanupStalePGliteLock(resolvedDataDir);
58 client = new PGlite(resolvedDataDir);99 client = new PGlite(resolvedDataDir);
59 }100 }
60 101
packages/control-plane/src/runtime/executor-tools.ts+14−5
@@ -15,8 +15,17 @@ import { · 14 unmodified lines
15 type WorkspaceId,15 type WorkspaceId,
16} from "#schema";16} from "#schema";
17import * as Effect from "effect/Effect";17import * as Effect from "effect/Effect";
18import * as Cause from "effect/Cause";
19import * as Exit from "effect/Exit";
18import * as Schema from "effect/Schema";20import * as Schema from "effect/Schema";
19 21
22/** Run an Effect as a Promise, preserving the original error (not FiberFailure). */
23const runEffect = async <A>(effect: Effect.Effect<A, unknown, never>): Promise<A> => {
24 const exit = await Effect.runPromiseExit(effect);
25 if (Exit.isSuccess(exit)) return exit.value;
26 throw Cause.squash(exit.cause);
27};
28
20import {29import {
21 type ExecutorAddSourceInput,30 type ExecutorAddSourceInput,
22 type ExecutorHttpSourceAuthInput,31 type ExecutorHttpSourceAuthInput,
@@ -277,7 +286,7 @@ export const createExecutorToolMap = (input: { · 254 unmodified lines
277 executionId,286 executionId,
278 interactionId,287 interactionId,
279 };288 };
280 const result = await Effect.runPromise(289 const result = await runEffect(
281 input.sourceAuthService.addExecutorSource(290 input.sourceAuthService.addExecutorSource(
282 preparedArgs,291 preparedArgs,
283 context?.onElicitation292 context?.onElicitation
@@ -304,7 +313,7 @@ export const createExecutorToolMap = (input: { · 20 unmodified lines
304 ExecutorAddSourceInput,313 ExecutorAddSourceInput,
305 { kind: "openapi" | "graphql" }314 { kind: "openapi" | "graphql" }
306 >;315 >;
307 const selectedAuth = await Effect.runPromise(316 const selectedAuth = await runEffect(
308 promptForSourceCredentialSelection({317 promptForSourceCredentialSelection({
309 args: {318 args: {
310 ...preparedHttpArgs,319 ...preparedHttpArgs,
@@ -323,7 +332,7 @@ export const createExecutorToolMap = (input: { · 12 unmodified lines
323 }),332 }),
324 );333 );
325 334
326 const completed = await Effect.runPromise(335 const completed = await runEffect(
327 input.sourceAuthService.addExecutorSource(336 input.sourceAuthService.addExecutorSource(
328 {337 {
329 ...preparedHttpArgs,338 ...preparedHttpArgs,
@@ -355,7 +364,7 @@ export const createExecutorToolMap = (input: { · 25 unmodified lines
355 throw new Error("executor.sources.add requires an elicitation-capable host");364 throw new Error("executor.sources.add requires an elicitation-capable host");
356 }365 }
357 366
358 const response: ElicitationResponse = await Effect.runPromise(367 const response: ElicitationResponse = await runEffect(
359 context.onElicitation({368 context.onElicitation({
360 interactionId,369 interactionId,
361 path: context.path ?? asToolPath("executor.sources.add"),370 path: context.path ?? asToolPath("executor.sources.add"),
@@ -376,7 +385,7 @@ export const createExecutorToolMap = (input: { · 14 unmodified lines
376 throw new Error(`Source add was not completed for ${result.source.id}`);385 throw new Error(`Source add was not completed for ${result.source.id}`);
377 }386 }
378 387
379 return await Effect.runPromise(388 return await runEffect(
380 input.sourceAuthService.getSourceById({389 input.sourceAuthService.getSourceById({
381 workspaceId: input.workspaceId,390 workspaceId: input.workspaceId,
382 sourceId: result.source.id,391 sourceId: result.source.id,
packages/control-plane/src/runtime/index.ts+1−1
@@ -192,6 +192,6 @@ export const createSqlControlPlaneRuntime = ( · 191 unmodified lines
192 persistence,192 persistence,
193 localInstallation,193 localInstallation,
194 runtimeLayer: concreteRuntimeLayer,194 runtimeLayer: concreteRuntimeLayer,
195 close: () => Effect.runPromise(Scope.close(scope, Exit.void)),195 close: () => Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}),
196 };196 };
197 });197 });
packages/executor-mcp/src/index.ts+83−23
@@ -4,11 +4,16 @@ import type { · 3 unmodified lines
4 SqlControlPlaneRuntime,4 SqlControlPlaneRuntime,
5} from "@executor-v3/control-plane";5} from "@executor-v3/control-plane";
6import {6import {
7 EXECUTOR_SOURCES_ADD_HELP_LINES,
8 ExecutionIdSchema,
9 RuntimeExecutionResolverService,
7 createExecution,10 createExecution,
8 getExecution,11 getExecution,
9 resumeExecution,12 resumeExecution,
10} from "@executor-v3/control-plane";13} from "@executor-v3/control-plane";
11import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
15import * as Cause from "effect/Cause";
16import * as Exit from "effect/Exit";
12import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";17import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";18import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
14import { z } from "zod/v4";19import { z } from "zod/v4";
@@ -110,14 +115,27 @@ const parseInteractionPayload = ( · 95 unmodified lines
110 }115 }
111};116};
112 117
113const runControlPlane = <A, E, R>(118const runControlPlane = async <A, E, R>(
114 runtime: SqlControlPlaneRuntime,119 runtime: SqlControlPlaneRuntime,
115 effect: Effect.Effect<A, E, R>,120 effect: Effect.Effect<A, E, R>,
116): Promise<A> =>121): Promise<A> => {
117 Effect.runPromise(122 const exit = await Effect.runPromiseExit(
118 effect.pipe(Effect.provide(runtime.runtimeLayer)) as Effect.Effect<A, E, never>,123 effect.pipe(Effect.provide(runtime.runtimeLayer)) as Effect.Effect<A, E, never>,
119 );124 );
120 125
126 if (Exit.isSuccess(exit)) {
127 return exit.value;
128 }
129
130 const error = Cause.squash(exit.cause);
131 if (error instanceof Error) {
132 // Preserve the original error with its stack trace and message rather
133 // than wrapping it in an opaque FiberFailure.
134 throw error;
135 }
136 throw error;
137};
138
121const sleep = (ms: number) =>139const sleep = (ms: number) =>
122 new Promise<void>((resolve) => {140 new Promise<void>((resolve) => {
123 setTimeout(resolve, ms);141 setTimeout(resolve, ms);
@@ -128,6 +146,63 @@ const supportsManagedElicitation = (server: McpServer): boolean => { · 4 unmodified lines
128 return Boolean(capabilities?.elicitation?.form) && Boolean(capabilities?.elicitation?.url);146 return Boolean(capabilities?.elicitation?.form) && Boolean(capabilities?.elicitation?.url);
129};147};
130 148
149type CatalogLike = {
150 listNamespaces: (input: { limit: number }) => Effect.Effect<
151 ReadonlyArray<{ namespace: string; displayName?: string }>,
152 unknown
153 >;
154};
155
156const buildExecuteWorkflowText = (namespaces: readonly string[] = []): string =>
157 [
158 "Execute TypeScript in sandbox; call tools via discovery workflow.",
159 ...(namespaces.length > 0
160 ? [
161 "Available namespaces:",
162 ...namespaces.map((namespace) => `- ${namespace}`),
163 ]
164 : []),
165 "Workflow:",
166 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
167 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
168 "3) Call selected tools.<path>(input).",
169 '4) To connect a source, call tools.executor.sources.add(...) for MCP, OpenAPI, or GraphQL APIs.',
170 ...EXECUTOR_SOURCES_ADD_HELP_LINES,
171 "5) If execution pauses for interaction, resume it with the returned resumePayload or the available resume flow.",
172 "Do not use fetch; use tools.* only.",
173 ].join("\n");
174
175const defaultExecuteDescription = buildExecuteWorkflowText();
176
177const loadExecuteDescription = (runtime: SqlControlPlaneRuntime): Promise<string> =>
178 runControlPlane(
179 runtime,
180 Effect.gen(function* () {
181 const resolveExecutionEnvironment = yield* RuntimeExecutionResolverService;
182 const environment = yield* resolveExecutionEnvironment({
183 workspaceId: runtime.localInstallation.workspaceId,
184 accountId: runtime.localInstallation.accountId,
185 executionId: ExecutionIdSchema.make("exec_mcp_help"),
186 });
187
188 const catalog = environment.catalog as CatalogLike | undefined;
189 if (!catalog) {
190 return defaultExecuteDescription;
191 }
192
193 const namespaces = yield* catalog.listNamespaces({ limit: 200 }).pipe(
194 Effect.map((items) =>
195 items.length > 0
196 ? items.map((item) => item.displayName ?? item.namespace)
197 : ["none discovered yet"],
198 ),
199 Effect.catchAll(() => Effect.succeed(["none discovered yet"])),
200 );
201
202 return buildExecuteWorkflowText(namespaces);
203 }).pipe(Effect.catchAll(() => Effect.succeed(defaultExecuteDescription))),
204 );
205
131const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {206const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {
132 switch (execution.status) {207 switch (execution.status) {
133 case "completed":208 case "completed":
@@ -348,26 +423,16 @@ const driveExecutionWithoutElicitation = async (input: { · 214 unmodified lines
348 return current;423 return current;
349};424};
350 425
351const buildInstallInstructions = (baseUrl: string): string => {426const createExecutorMcpServer = async (config: {
352 const mcpUrl = new URL("/mcp", baseUrl).toString();
353
354 return [
355 "Run once to install for supported MCP clients (via add-mcp):",
356 `npx add-mcp \"${mcpUrl}\" --transport http --name \"executor\"`,
357 ].join("\n");
358};
359
360const createExecutorMcpServer = (config: {
361 runtime: SqlControlPlaneRuntime;427 runtime: SqlControlPlaneRuntime;
362 baseUrl: string;428}): Promise<McpServer> => {
363}): McpServer => {429 const executeDescription = await loadExecuteDescription(config.runtime);
364 const server = new McpServer(430 const server = new McpServer(
365 { name: "executor", version: "1.0.0" },431 { name: "executor", version: "1.0.0" },
366 {432 {
367 capabilities: {433 capabilities: {
368 tools: {},434 tools: {},
369 },435 },
370 instructions: buildInstallInstructions(config.baseUrl),
371 },436 },
372 );437 );
373 438
@@ -377,10 +442,7 @@ const createExecutorMcpServer = (config: { · 3 unmodified lines
377 const executeTool = server.registerTool(442 const executeTool = server.registerTool(
378 "execute",443 "execute",
379 {444 {
380 description: [445 description: executeDescription,
381 "Execute TypeScript against the local executor runtime.",
382 "Use the in-sandbox tool catalog via tools.* rather than direct fetch calls.",
383 ].join("\n"),
384 inputSchema: executeInputSchema,446 inputSchema: executeInputSchema,
385 },447 },
386 async ({ code }: { code: string }) => {448 async ({ code }: { code: string }) => {
@@ -431,7 +493,6 @@ const createExecutorMcpServer = (config: { · 44 unmodified lines
431 },493 },
432 );494 );
433 495
434
435 const syncToolAvailability = () => {496 const syncToolAvailability = () => {
436 if (supportsManagedElicitation(server)) {497 if (supportsManagedElicitation(server)) {
437 executeTool.enable();498 executeTool.enable();
@@ -524,9 +585,8 @@ export const createExecutorMcpRequestHandler = ( · 86 unmodified lines
524 };585 };
525 586
526 try {587 try {
527 createdServer = createExecutorMcpServer({588 createdServer = await createExecutorMcpServer({
528 runtime,589 runtime,
529 baseUrl: new URL(request.url).origin,
530 });590 });
531 await createdServer.connect(transport);591 await createdServer.connect(transport);
532 const response = await transport.handleRequest(request);592 const response = await transport.handleRequest(request);
packages/react/src/index.ts+26−10
@@ -28,6 +28,8 @@ import type { · 27 unmodified lines
28} from "@executor-v3/control-plane";28} from "@executor-v3/control-plane";
29import * as Cause from "effect/Cause";29import * as Cause from "effect/Cause";
30import * as Effect from "effect/Effect";30import * as Effect from "effect/Effect";
31import * as Exit from "effect/Exit";
32import * as Runtime from "effect/Runtime";
31import * as React from "react";33import * as React from "react";
32 34
33const DEFAULT_EXECUTOR_API_BASE_URL = "http://127.0.0.1:8788";35const DEFAULT_EXECUTOR_API_BASE_URL = "http://127.0.0.1:8788";
@@ -177,6 +179,15 @@ const shouldLogExecutorDevErrors = (): boolean => { · 143 unmodified lines
177};179};
178 180
179const describeExecutorDevError = (cause: unknown): Record<string, unknown> => {181const describeExecutorDevError = (cause: unknown): Record<string, unknown> => {
182 if (Runtime.isFiberFailure(cause)) {
183 const inner = cause[Runtime.FiberFailureCauseId];
184 return {
185 name: cause.name,
186 message: cause.message,
187 cause: Cause.pretty(inner),
188 };
189 }
190
180 if (cause instanceof Error) {191 if (cause instanceof Error) {
181 return {192 return {
182 name: cause.name,193 name: cause.name,
@@ -198,26 +209,31 @@ const logExecutorDevError = (label: string, details: Record<string, unknown>): v · 15 unmodified lines
198 console.error(`[executor react] ${label}`, details);209 console.error(`[executor react] ${label}`, details);
199};210};
200 211
201const runControlPlane = <A>(input: {212const runControlPlane = async <A>(input: {
202 accountId?: string;213 accountId?: string;
203 execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>;214 execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>;
204}): Promise<A> => {215}): Promise<A> => {
205 const accountId = input.accountId;216 const accountId = input.accountId;
206 217
207 return Effect.runPromise(218 const exit = await Effect.runPromiseExit(
208 createControlPlaneClient({219 createControlPlaneClient({
209 baseUrl: apiBaseUrl,220 baseUrl: apiBaseUrl,
210 ...(accountId !== undefined ? { accountId } : {}),221 ...(accountId !== undefined ? { accountId } : {}),
211 }).pipe(Effect.flatMap(input.execute)),222 }).pipe(Effect.flatMap(input.execute)),
212 ).catch((cause) => {223 );
213 logExecutorDevError("control-plane request failed", {224
214 baseUrl: apiBaseUrl,225 if (Exit.isSuccess(exit)) {
215 accountId,226 return exit.value;
216 error: describeExecutorDevError(cause),227 }
217 cause,228
218 });229 const error = Cause.squash(exit.cause);
219 throw cause;230 logExecutorDevError("control-plane request failed", {
231 baseUrl: apiBaseUrl,
232 accountId,
233 error: describeExecutorDevError(error),
234 cause: Cause.pretty(exit.cause),
220 });235 });
236 throw error;
221};237};
222 238
223const controlPlaneRequest = <A>(input: {239const controlPlaneRequest = <A>(input: {
packages/server/src/index.ts+9−4
@@ -80,7 +80,11 @@ export type LocalExecutorRequestHandler = { · 79 unmodified lines
80};80};
81 81
82const disposeRuntime = (runtime: SqlControlPlaneRuntime) =>82const disposeRuntime = (runtime: SqlControlPlaneRuntime) =>
83 Effect.promise(() => runtime.close()).pipe(Effect.orDie);83 Effect.tryPromise({
84 try: () => runtime.close(),
85 catch: (cause) =>
86 cause instanceof Error ? cause : new Error(String(cause ?? "runtime close failed")),
87 }).pipe(Effect.orDie);
84 88
85const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) =>89const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) =>
86 Effect.acquireRelease(90 Effect.acquireRelease(
@@ -92,7 +96,7 @@ const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => · 5 unmodified lines
92 ),96 ),
93 ),97 ),
94 ),98 ),
95 (handler) => Effect.promise(() => handler.dispose()).pipe(Effect.orDie),99 (handler) => Effect.tryPromise({ try: () => handler.dispose(), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause ?? "web handler dispose failed")) }).pipe(Effect.orDie),
96 );100 );
97 101
98const safeFilePath = (assetsDir: string, pathname: string): string | null => {102const safeFilePath = (assetsDir: string, pathname: string): string | null => {
@@ -280,7 +284,7 @@ export const createLocalExecutorRequestHandler = ( · 181 unmodified lines
280 const apiHandler = yield* createControlPlaneWebHandler(runtime);284 const apiHandler = yield* createControlPlaneWebHandler(runtime);
281 const mcpHandler = yield* Effect.acquireRelease(285 const mcpHandler = yield* Effect.acquireRelease(
282 Effect.sync(() => createExecutorMcpRequestHandler(runtime)),286 Effect.sync(() => createExecutorMcpRequestHandler(runtime)),
283 (handler) => Effect.promise(() => handler.close()).pipe(Effect.orDie),287 (handler) => Effect.tryPromise({ try: () => handler.close(), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause ?? "mcp handler close failed")) }).pipe(Effect.orDie),
284 );288 );
285 289
286 return {290 return {
@@ -389,7 +393,8 @@ export const runLocalExecutorServer = ( · 102 unmodified lines
389 }),393 }),
390 () => Effect.tryPromise({394 () => Effect.tryPromise({
391 try: () => rm(pidFile, { force: true }),395 try: () => rm(pidFile, { force: true }),
392 catch: () => undefined,396 catch: (cause) =>
397 cause instanceof Error ? cause : new Error(String(cause ?? "pid file cleanup failed")),
393 }).pipe(Effect.orDie),398 }).pipe(Effect.orDie),
394 );399 );
395 400