History

mcp server

Rhys Sullivan committed 8 months ago

Commit 8db6d7a1 parent 8cd05d616 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
652652 const pid = typeof pidRecord?.pid === "number" ? pidRecord.pid : null;
653653
654654 if (pid === null) {
655− yield* Effect.tryPromise({
656− try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }),
657− catch: () => undefined,
658− });
655+ yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
656+ Effect.ignore,
657+ );
659658 return false;
660659 }
661660
662661 if (!isPidRunning(pid)) {
663− yield* Effect.tryPromise({
664− try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }),
665− catch: () => undefined,
666− });
662+ yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
663+ Effect.ignore,
664+ );
667665 return false;
668666 }
669667
@@ -674,10 +672,10 @@ const stopServer = (baseUrl: string) => · 4 unmodified lines
674672
675673 yield* waitForReachability(baseUrl, false).pipe(
676674 Effect.catchAll(() =>
677− Effect.tryPromise({
678− try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }),
679− catch: () => undefined,
680− }).pipe(Effect.zipRight(Effect.fail(new Error(`Timed out stopping local executor server pid ${pid}`)))),
675+ Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe(
676+ Effect.ignore,
677+ Effect.zipRight(Effect.fail(new Error(`Timed out stopping local executor server pid ${pid}`))),
678+ ),
681679 ),
682680 );
683681
apps/executor/src/distribution/harness.ts+2−1
@@ -176,7 +176,8 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 175 unmodified lines
176176 }),
177177 (path) => Effect.tryPromise({
178178 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")),
180181 }).pipe(Effect.orDie),
181182 );
182183
apps/executor/src/server/server.test.ts+9−8
@@ -647,12 +647,13 @@ describe("local-executor-server", () => { · 646 unmodified lines
647647 });
648648
649649 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 }> }>,
651651 );
652652 expect(listed.tools.map((tool) => tool.name)).toEqual(["execute"]);
653− expect(mcp.client.getInstructions()).toContain(
654− `npx add-mcp \"${server.baseUrl}/mcp\" --transport http --name \"executor\"`,
655− );
653+ expect(listed.tools[0]?.description).toContain("Workflow:");
654+ expect(listed.tools[0]?.description).toContain("tools.discover");
655+ expect(listed.tools[0]?.description).toContain("tools.executor.sources.add");
656+
656657
657658 const executed = yield* Effect.promise(
658659 () =>
@@ -686,12 +687,12 @@ describe("local-executor-server", () => { · 27 unmodified lines
686687 });
687688
688689 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 }> }>,
690691 );
691692 expect(listed.tools.map((tool) => tool.name).sort()).toEqual(["execute", "resume"]);
692− expect(mcp.client.getInstructions()).toContain(
693− `npx add-mcp \"${server.baseUrl}/mcp\" --transport http --name \"executor\"`,
694− );
693+ expect(listed.tools.find((tool) => tool.name === "execute")?.description).toContain("Workflow:");
694+ expect(listed.tools.find((tool) => tool.name === "execute")?.description).toContain("tools.discover");
695+
695696
696697 const executed = yield* Effect.promise(
697698 () =>
apps/web/src/components/local-mcp-install-card.tsx+37−0
@@ -0,0 +1,37 @@
1+import { useEffect, useMemo, useState } from "react";
2+import { CodeBlock } from "./code-block";
3+
4+export 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
55 * control-plane handler. Everything else (frontend assets, HMR) is
66 * handled by Vite itself.
77 */
8+import * as Cause from "effect/Cause";
89 import * as Effect from "effect/Effect";
10+import * as Exit from "effect/Exit";
11+import * as Runtime from "effect/Runtime";
912 import * as Scope from "effect/Scope";
1013 import { createLocalExecutorRequestHandler } from "@executor-v3/server";
1114
@@ -16,28 +19,53 @@ const truncateForLog = (value: string): string => · 4 unmodified lines
1619 ? `${value.slice(0, MAX_LOGGED_ERROR_BODY_LENGTH)}... [truncated]`
1720 : value;
1821
19−const formatErrorForLog = (error: unknown) =>
20− error instanceof Error
21− ? {
22− name: error.name,
23− message: error.message,
24− stack: error.stack,
25− }
26− : error;
22+/**
23+ * Extract a detailed, human-readable description from an error.
24+ *
25+ * When Effect rejects via `runPromise`, the thrown value is a
26+ * `FiberFailure` wrapping a `Cause`. `Cause.pretty` renders the full
27+ * failure tree (defects, interrupts, tagged errors, etc.) rather than the
28+ * opaque fallback message ("An error has occurred").
29+ */
30+const 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+};
2742
2843 // Create a long-lived scope that stays open for the lifetime of the process.
29−const handlerPromise = Effect.runPromise(
30− Effect.gen(function* () {
31− const scope = yield* Scope.make();
32− const handler = yield* createLocalExecutorRequestHandler().pipe(
33− Effect.provideService(Scope.Scope, scope),
34− );
35− return handler;
36− }),
37−).catch((error) => {
38− console.error("[executor dev api] failed to initialize request handler", formatErrorForLog(error));
39− throw error;
40−});
44+const handlerPromise = (async () => {
45+ const exit = await Effect.runPromiseExit(
46+ Effect.gen(function* () {
47+ const scope = yield* Scope.make();
48+ const handler = yield* createLocalExecutorRequestHandler().pipe(
49+ Effect.provideService(Scope.Scope, scope),
50+ );
51+ return handler;
52+ }),
53+ );
54+
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+})();
4169
4270 export default {
4371 async fetch(request: Request) {
apps/web/src/views/add-source.tsx+9−0
@@ -18,6 +18,7 @@ import { · 17 unmodified lines
1818 } from "@executor-v3/react";
1919 import { Badge } from "../components/ui/badge";
2020 import { Button } from "../components/ui/button";
21+import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
2122 import { SourceFavicon } from "../components/source-favicon";
2223 import {
2324 IconArrowLeft,
@@ -737,6 +738,14 @@ export function AddSourcePage() { · 713 unmodified lines
737738
738739 {statusBanner && <StatusBanner state={statusBanner} className="mb-6" />}
739740
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+ )}
740749 {/* Step 1: Discovery */}
741750 {(phase === "idle" || phase === "discovering") && (
742751 <div className="space-y-6">
apps/web/src/views/home.tsx+3−0
@@ -1,6 +1,7 @@
11 import { Link } from "@tanstack/react-router";
22 import { useSources, type Source } from "@executor-v3/react";
33 import { LoadableBlock } from "../components/loadable";
4+import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
45 import { SourceFavicon } from "../components/source-favicon";
56 import { Badge } from "../components/ui/badge";
67 import { Button } from "../components/ui/button";
@@ -30,6 +31,8 @@ export function HomePage() { · 23 unmodified lines
3031 </Link>
3132 </div>
3233
34+ <LocalMcpInstallCard className="mb-8" />
35+
3336 {/* Source list */}
3437 <LoadableBlock loadable={sources} loading="Loading sources...">
3538 {(items) =>
apps/web/vite.config.ts+2−2
@@ -11,8 +11,8 @@ export default defineConfig({ · 10 unmodified lines
1111 devServer({
1212 entry: "src/dev.ts",
1313 exclude: [
14− // Only let /v1 requests reach the API handler
15− /^\/(?!v1(\/|$))/,
14+ // Only let /v1 and /mcp requests reach the API handler
15+ /^\/(?!(v1|mcp)(\/|$))/,
1616 /^\/(src|node_modules|@vite|@id|@react-refresh)/,
1717 /\.(css|ts|tsx|js|jsx|svg|png|jpg|gif|ico|woff2?|json|map)(\?.*)?$/,
1818 ],
packages/codemode-mcp/src/mcp-tools.ts+18−6
@@ -2,6 +2,8 @@ import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js"; · 1 unmodified line
22 import * as Data from "effect/Data";
33 import * as Either from "effect/Either";
44 import * as Effect from "effect/Effect";
5+import * as Cause from "effect/Cause";
6+import * as Exit from "effect/Exit";
57 import * as PartitionedSemaphore from "effect/PartitionedSemaphore";
68
79 import {
@@ -88,10 +90,11 @@ const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => { · 80 unmodified lines
8890 const closeConnection = (connection: McpConnection): Effect.Effect<void> =>
8991 Effect.tryPromise({
9092 try: () => connection.close?.() ?? Promise.resolve(),
91− catch: () => undefined,
93+ catch: (cause) =>
94+ cause instanceof Error ? cause : new Error(String(cause ?? "mcp connection close failed")),
9295 }).pipe(
9396 Effect.asVoid,
94− Effect.catchAll(() => Effect.succeed(undefined)),
97+ Effect.catchAll(() => Effect.void),
9598 );
9699
97100 const withConnectionEffect = <A, E>(input: {
@@ -192,7 +195,13 @@ const installMcpElicitationHandler = (input: { · 94 unmodified lines
192195 }),
193196 ),
194197 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+ }),
196205 ),
197206 );
198207 });
@@ -427,8 +436,8 @@ export const createMcpToolsFromManifest = (input: { · 228 unmodified lines
427436 tool: {
428437 description: entry.description ?? `MCP tool: ${entry.toolName}`,
429438 inputSchema: inputSchemaFromManifest(entry.inputSchemaJson),
430− execute: (args: unknown, executionContext?: ToolExecutionContext) =>
431− Effect.runPromise(
439+ execute: async (args: unknown, executionContext?: ToolExecutionContext) => {
440+ const exit = await Effect.runPromiseExit(
432441 withConnectionEffect({
433442 connect: input.connect,
434443 onConnectError: (cause) =>
@@ -453,7 +462,10 @@ export const createMcpToolsFromManifest = (input: { · 18 unmodified lines
453462 : callEffect;
454463 },
455464 }),
456− ),
465+ );
466+ if (Exit.isSuccess(exit)) return exit.value;
467+ throw Cause.squash(exit.cause);
468+ },
457469 },
458470 metadata: {
459471 sourceKey,
packages/control-plane/src/persistence/index.ts+4−2
@@ -113,7 +113,8 @@ const runMigrationsEffect = ( · 112 unmodified lines
113113 const closeRuntimeEffect = (runtime: SqlRuntime) =>
114114 Effect.tryPromise({
115115 try: () => runtime.close(),
116− catch: () => undefined,
116+ catch: (cause) =>
117+ cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")),
117118 }).pipe(Effect.orDie);
118119
119120 export const createSqlControlPlanePersistence = (
@@ -150,7 +151,8 @@ export const SqlControlPlanePersistenceLive = ( · 30 unmodified lines
150151 (persistence) =>
151152 Effect.tryPromise({
152153 try: () => persistence.close(),
153− catch: () => undefined,
154+ catch: (cause) =>
155+ cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")),
154156 }).pipe(Effect.orDie),
155157 ),
156158 );
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
33 import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator";
44 import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
55 import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";
6−import { existsSync, readdirSync } from "node:fs";
7−import { mkdir } from "node:fs/promises";
6+import { existsSync, readdirSync, readFileSync } from "node:fs";
7+import { mkdir, unlink } from "node:fs/promises";
88 import * as path from "node:path";
99 import { fileURLToPath } from "node:url";
1010 import postgres from "postgres";
@@ -44,6 +44,46 @@ const trim = (value: string | undefined): string | undefined => { · 33 unmodified lines
4444 const isPostgresUrl = (value: string): boolean =>
4545 value.startsWith("postgres://") || value.startsWith("postgresql://");
4646
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+ */
59+const 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+
4787 const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => {
4888 const normalized = trim(localDataDir) ?? ".executor-v3/control-plane-pgdata";
4989
@@ -55,6 +95,7 @@ const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => · 5 unmodified lines
5595 if (!existsSync(resolvedDataDir)) {
5696 await mkdir(resolvedDataDir, { recursive: true });
5797 }
98+ await cleanupStalePGliteLock(resolvedDataDir);
5899 client = new PGlite(resolvedDataDir);
59100 }
60101
packages/control-plane/src/runtime/executor-tools.ts+14−5
@@ -15,8 +15,17 @@ import { · 14 unmodified lines
1515 type WorkspaceId,
1616 } from "#schema";
1717 import * as Effect from "effect/Effect";
18+import * as Cause from "effect/Cause";
19+import * as Exit from "effect/Exit";
1820 import * as Schema from "effect/Schema";
1921
22+/** Run an Effect as a Promise, preserving the original error (not FiberFailure). */
23+const 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+
2029 import {
2130 type ExecutorAddSourceInput,
2231 type ExecutorHttpSourceAuthInput,
@@ -277,7 +286,7 @@ export const createExecutorToolMap = (input: { · 254 unmodified lines
277286 executionId,
278287 interactionId,
279288 };
280− const result = await Effect.runPromise(
289+ const result = await runEffect(
281290 input.sourceAuthService.addExecutorSource(
282291 preparedArgs,
283292 context?.onElicitation
@@ -304,7 +313,7 @@ export const createExecutorToolMap = (input: { · 20 unmodified lines
304313 ExecutorAddSourceInput,
305314 { kind: "openapi" | "graphql" }
306315 >;
307− const selectedAuth = await Effect.runPromise(
316+ const selectedAuth = await runEffect(
308317 promptForSourceCredentialSelection({
309318 args: {
310319 ...preparedHttpArgs,
@@ -323,7 +332,7 @@ export const createExecutorToolMap = (input: { · 12 unmodified lines
323332 }),
324333 );
325334
326− const completed = await Effect.runPromise(
335+ const completed = await runEffect(
327336 input.sourceAuthService.addExecutorSource(
328337 {
329338 ...preparedHttpArgs,
@@ -355,7 +364,7 @@ export const createExecutorToolMap = (input: { · 25 unmodified lines
355364 throw new Error("executor.sources.add requires an elicitation-capable host");
356365 }
357366
358− const response: ElicitationResponse = await Effect.runPromise(
367+ const response: ElicitationResponse = await runEffect(
359368 context.onElicitation({
360369 interactionId,
361370 path: context.path ?? asToolPath("executor.sources.add"),
@@ -376,7 +385,7 @@ export const createExecutorToolMap = (input: { · 14 unmodified lines
376385 throw new Error(`Source add was not completed for ${result.source.id}`);
377386 }
378387
379− return await Effect.runPromise(
388+ return await runEffect(
380389 input.sourceAuthService.getSourceById({
381390 workspaceId: input.workspaceId,
382391 sourceId: result.source.id,
packages/control-plane/src/runtime/index.ts+1−1
@@ -192,6 +192,6 @@ export const createSqlControlPlaneRuntime = ( · 191 unmodified lines
192192 persistence,
193193 localInstallation,
194194 runtimeLayer: concreteRuntimeLayer,
195− close: () => Effect.runPromise(Scope.close(scope, Exit.void)),
195+ close: () => Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}),
196196 };
197197 });
packages/executor-mcp/src/index.ts+83−23
@@ -4,11 +4,16 @@ import type { · 3 unmodified lines
44 SqlControlPlaneRuntime,
55 } from "@executor-v3/control-plane";
66 import {
7+ EXECUTOR_SOURCES_ADD_HELP_LINES,
8+ ExecutionIdSchema,
9+ RuntimeExecutionResolverService,
710 createExecution,
811 getExecution,
912 resumeExecution,
1013 } from "@executor-v3/control-plane";
1114 import * as Effect from "effect/Effect";
15+import * as Cause from "effect/Cause";
16+import * as Exit from "effect/Exit";
1217 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1318 import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
1419 import { z } from "zod/v4";
@@ -110,14 +115,27 @@ const parseInteractionPayload = ( · 95 unmodified lines
110115 }
111116 };
112117
113−const runControlPlane = <A, E, R>(
118+const runControlPlane = async <A, E, R>(
114119 runtime: SqlControlPlaneRuntime,
115120 effect: Effect.Effect<A, E, R>,
116−): Promise<A> =>
117− Effect.runPromise(
121+): Promise<A> => {
122+ const exit = await Effect.runPromiseExit(
118123 effect.pipe(Effect.provide(runtime.runtimeLayer)) as Effect.Effect<A, E, never>,
119124 );
120125
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+
121139 const sleep = (ms: number) =>
122140 new Promise<void>((resolve) => {
123141 setTimeout(resolve, ms);
@@ -128,6 +146,63 @@ const supportsManagedElicitation = (server: McpServer): boolean => { · 4 unmodified lines
128146 return Boolean(capabilities?.elicitation?.form) && Boolean(capabilities?.elicitation?.url);
129147 };
130148
149+type CatalogLike = {
150+ listNamespaces: (input: { limit: number }) => Effect.Effect<
151+ ReadonlyArray<{ namespace: string; displayName?: string }>,
152+ unknown
153+ >;
154+};
155+
156+const 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+
175+const defaultExecuteDescription = buildExecuteWorkflowText();
176+
177+const 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+
131206 const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {
132207 switch (execution.status) {
133208 case "completed":
@@ -348,26 +423,16 @@ const driveExecutionWithoutElicitation = async (input: { · 214 unmodified lines
348423 return current;
349424 };
350425
351−const buildInstallInstructions = (baseUrl: string): string => {
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−
360−const createExecutorMcpServer = (config: {
426+const createExecutorMcpServer = async (config: {
361427 runtime: SqlControlPlaneRuntime;
362− baseUrl: string;
363−}): McpServer => {
428+}): Promise<McpServer> => {
429+ const executeDescription = await loadExecuteDescription(config.runtime);
364430 const server = new McpServer(
365431 { name: "executor", version: "1.0.0" },
366432 {
367433 capabilities: {
368434 tools: {},
369435 },
370− instructions: buildInstallInstructions(config.baseUrl),
371436 },
372437 );
373438
@@ -377,10 +442,7 @@ const createExecutorMcpServer = (config: { · 3 unmodified lines
377442 const executeTool = server.registerTool(
378443 "execute",
379444 {
380− description: [
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"),
445+ description: executeDescription,
384446 inputSchema: executeInputSchema,
385447 },
386448 async ({ code }: { code: string }) => {
@@ -431,7 +493,6 @@ const createExecutorMcpServer = (config: { · 44 unmodified lines
431493 },
432494 );
433495
434−
435496 const syncToolAvailability = () => {
436497 if (supportsManagedElicitation(server)) {
437498 executeTool.enable();
@@ -524,9 +585,8 @@ export const createExecutorMcpRequestHandler = ( · 86 unmodified lines
524585 };
525586
526587 try {
527− createdServer = createExecutorMcpServer({
588+ createdServer = await createExecutorMcpServer({
528589 runtime,
529− baseUrl: new URL(request.url).origin,
530590 });
531591 await createdServer.connect(transport);
532592 const response = await transport.handleRequest(request);
packages/react/src/index.ts+26−10
@@ -28,6 +28,8 @@ import type { · 27 unmodified lines
2828 } from "@executor-v3/control-plane";
2929 import * as Cause from "effect/Cause";
3030 import * as Effect from "effect/Effect";
31+import * as Exit from "effect/Exit";
32+import * as Runtime from "effect/Runtime";
3133 import * as React from "react";
3234
3335 const DEFAULT_EXECUTOR_API_BASE_URL = "http://127.0.0.1:8788";
@@ -177,6 +179,15 @@ const shouldLogExecutorDevErrors = (): boolean => { · 143 unmodified lines
177179 };
178180
179181 const 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+
180191 if (cause instanceof Error) {
181192 return {
182193 name: cause.name,
@@ -198,26 +209,31 @@ const logExecutorDevError = (label: string, details: Record<string, unknown>): v · 15 unmodified lines
198209 console.error(`[executor react] ${label}`, details);
199210 };
200211
201−const runControlPlane = <A>(input: {
212+const runControlPlane = async <A>(input: {
202213 accountId?: string;
203214 execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>;
204215 }): Promise<A> => {
205216 const accountId = input.accountId;
206217
207− return Effect.runPromise(
218+ const exit = await Effect.runPromiseExit(
208219 createControlPlaneClient({
209220 baseUrl: apiBaseUrl,
210221 ...(accountId !== undefined ? { accountId } : {}),
211222 }).pipe(Effect.flatMap(input.execute)),
212− ).catch((cause) => {
213− logExecutorDevError("control-plane request failed", {
214− baseUrl: apiBaseUrl,
215− accountId,
216− error: describeExecutorDevError(cause),
217− cause,
218− });
219− throw cause;
223+ );
224+
225+ if (Exit.isSuccess(exit)) {
226+ return exit.value;
227+ }
228+
229+ const error = Cause.squash(exit.cause);
230+ logExecutorDevError("control-plane request failed", {
231+ baseUrl: apiBaseUrl,
232+ accountId,
233+ error: describeExecutorDevError(error),
234+ cause: Cause.pretty(exit.cause),
220235 });
236+ throw error;
221237 };
222238
223239 const controlPlaneRequest = <A>(input: {
packages/server/src/index.ts+9−4
@@ -80,7 +80,11 @@ export type LocalExecutorRequestHandler = { · 79 unmodified lines
8080 };
8181
8282 const 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);
8488
8589 const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) =>
8690 Effect.acquireRelease(
@@ -92,7 +96,7 @@ const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => · 5 unmodified lines
9296 ),
9397 ),
9498 ),
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),
96100 );
97101
98102 const safeFilePath = (assetsDir: string, pathname: string): string | null => {
@@ -280,7 +284,7 @@ export const createLocalExecutorRequestHandler = ( · 181 unmodified lines
280284 const apiHandler = yield* createControlPlaneWebHandler(runtime);
281285 const mcpHandler = yield* Effect.acquireRelease(
282286 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),
284288 );
285289
286290 return {
@@ -389,7 +393,8 @@ export const runLocalExecutorServer = ( · 102 unmodified lines
389393 }),
390394 () => Effect.tryPromise({
391395 try: () => rm(pidFile, { force: true }),
392− catch: () => undefined,
396+ catch: (cause) =>
397+ cause instanceof Error ? cause : new Error(String(cause ?? "pid file cleanup failed")),
393398 }).pipe(Effect.orDie),
394399 );
395400