- Changed the default port for the Assistant server from 3000 to 3002 in various configuration files. - Updated the `.env.example` to reflect the new `ASSISTANT_PORT` variable. - Adjusted the `dev.ts` script to utilize the new port and ensure proper environment variable handling. - Modified the README to document the updated Assistant server URL. - Removed deprecated typecheck declaration cache functionality and related tests to streamline the codebase.
Unified · Split
| @@ -25,6 +25,7 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 24 unmodified lines | ||
| 25 | 25 | # EXECUTOR_BACKEND_PORT=5410 |
| 26 | 26 | # EXECUTOR_BACKEND_SITE_PORT=5411 |
| 27 | 27 | # EXECUTOR_WEB_PORT=5312 |
| 28 | +# ASSISTANT_PORT=3002 | |
| 28 | 29 | |
| 29 | 30 | # ── MCP OAuth (optional) ── |
| 30 | 31 | # Set to 1 to enable self-issued anonymous OAuth even without WorkOS auth. |
| @@ -35,7 +36,7 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 4 unmodified lines | ||
| 35 | 36 | |
| 36 | 37 | # ── Assistant (optional) ── |
| 37 | 38 | # OPENASSISTANT_EXECUTOR_URL=http://localhost:3001 |
| 38 | −# OPENASSISTANT_CALLBACK_BASE_URL=http://localhost:3000 | |
| 39 | +# OPENASSISTANT_CALLBACK_BASE_URL=http://localhost:3002 | |
| 39 | 40 | |
| 40 | 41 | # ── Tool source API keys (optional) ── |
| 41 | 42 | # POSTHOG_PERSONAL_API_KEY=... |
| @@ -64,7 +64,7 @@ bun run dev · 63 unmodified lines | ||
| 64 | 64 | - Convex dev watcher for `executor/` |
| 65 | 65 | - Executor web app (`http://localhost:4312`) |
| 66 | 66 | - Executor MCP endpoint (`<CONVEX_SITE_URL>/mcp`) |
| 67 | −- Assistant server (`http://localhost:3000`) | |
| 67 | +- Assistant server (`http://localhost:3002`) | |
| 68 | 68 | - Discord bot (only if `DISCORD_BOT_TOKEN` is set) |
| 69 | 69 | |
| 70 | 70 | PIDs are tracked in `.dev.pids`. To stop all processes: |
| @@ -26,7 +26,7 @@ if (!DISCORD_TOKEN) { · 25 unmodified lines | ||
| 26 | 26 | throw new Error("DISCORD_BOT_TOKEN is required"); |
| 27 | 27 | } |
| 28 | 28 | |
| 29 | −const SERVER_URL = Bun.env.ASSISTANT_SERVER_URL ?? "http://localhost:3000"; | |
| 29 | +const SERVER_URL = Bun.env.ASSISTANT_SERVER_URL ?? `http://localhost:${Bun.env.ASSISTANT_PORT ?? "3002"}`; | |
| 30 | 30 | const CONVEX_URL = Bun.env.CONVEX_URL; |
| 31 | 31 | if (!CONVEX_URL) { |
| 32 | 32 | throw new Error("CONVEX_URL is required. Set it in your environment."); |
| @@ -7,7 +7,7 @@ import { ConvexHttpClient } from "convex/browser"; · 6 unmodified lines | ||
| 7 | 7 | import { api } from "@executor/convex/_generated/api"; |
| 8 | 8 | import type { Id } from "@executor/convex/_generated/dataModel"; |
| 9 | 9 | |
| 10 | −const PORT = Number(Bun.env.PORT ?? 3000); | |
| 10 | +const PORT = Number(Bun.env.ASSISTANT_PORT ?? Bun.env.PORT ?? 3002); | |
| 11 | 11 | const CONVEX_URL = Bun.env.CONVEX_URL; |
| 12 | 12 | if (!CONVEX_URL) { |
| 13 | 13 | throw new Error("CONVEX_URL is required. Set it in your environment."); |
| @@ -10,7 +10,7 @@ · 9 unmodified lines | ||
| 10 | 10 | * 2. Convex cloud dev function watcher │ |
| 11 | 11 | * 3. CF sandbox host worker (port 8787) ├─ all started concurrently |
| 12 | 12 | * 4. Executor web UI (port 4312) │ |
| 13 | − * 5. Assistant server (port 3000) │ | |
| 13 | + * 5. Assistant server (port 3002) │ | |
| 14 | 14 | * 6. Discord bot ─┘ |
| 15 | 15 | * |
| 16 | 16 | * All processes are killed when this script exits (Ctrl+C). |
| @@ -122,7 +122,8 @@ process.on("SIGTERM", shutdown); · 105 unmodified lines | ||
| 122 | 122 | |
| 123 | 123 | const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312); |
| 124 | 124 | const SANDBOX_PORT = Number(Bun.env.SANDBOX_PORT ?? 8787); |
| 125 | −const DEV_PORTS = [3000, EXECUTOR_WEB_PORT, SANDBOX_PORT]; | |
| 125 | +const ASSISTANT_PORT = Number(Bun.env.ASSISTANT_PORT ?? 3002); | |
| 126 | +const DEV_PORTS = [ASSISTANT_PORT, EXECUTOR_WEB_PORT, SANDBOX_PORT]; | |
| 126 | 127 | |
| 127 | 128 | async function killStaleProcesses() { |
| 128 | 129 | let killed = 0; |
| @@ -198,6 +199,7 @@ spawnService("web", ["bun", "run", "dev"], { · 69 unmodified lines | ||
| 198 | 199 | spawnService("assistant", ["bun", "run", "dev"], { |
| 199 | 200 | cwd: "./assistant/packages/server", |
| 200 | 201 | env: { |
| 202 | + ASSISTANT_PORT: String(ASSISTANT_PORT), | |
| 201 | 203 | EXECUTOR_URL: urls.executorUrl, |
| 202 | 204 | CONVEX_URL: urls.convexUrl, |
| 203 | 205 | EXECUTOR_ANON_SESSION_ID: Bun.env.EXECUTOR_ANON_SESSION_ID ?? "assistant-discord-dev", |
| @@ -209,6 +211,7 @@ if (Bun.env.DISCORD_BOT_TOKEN) { · 5 unmodified lines | ||
| 209 | 211 | spawnService("bot", ["bun", "run", "dev"], { |
| 210 | 212 | cwd: "./assistant/packages/bot", |
| 211 | 213 | env: { |
| 214 | + ASSISTANT_SERVER_URL: Bun.env.ASSISTANT_SERVER_URL ?? `http://localhost:${ASSISTANT_PORT}`, | |
| 212 | 215 | CONVEX_URL: urls.convexUrl, |
| 213 | 216 | }, |
| 214 | 217 | }); |
| @@ -970,19 +970,6 @@ export declare const internal: { · 969 unmodified lines | ||
| 970 | 970 | any |
| 971 | 971 | >; |
| 972 | 972 | }; |
| 973 | − mcpNode: { | |
| 974 | − typecheckRunCodeInternal: FunctionReference< | |
| 975 | − "action", | |
| 976 | − "internal", | |
| 977 | − { | |
| 978 | − actorId?: string; | |
| 979 | − clientId?: string; | |
| 980 | − code: string; | |
| 981 | − workspaceId: Id<"workspaces">; | |
| 982 | − }, | |
| 983 | − any | |
| 984 | − >; | |
| 985 | − }; | |
| 986 | 973 | migrations: { |
| 987 | 974 | backfillDtsStorageIds: FunctionReference< |
| 988 | 975 | "mutation", |
| @@ -1112,26 +1099,6 @@ export declare const internal: { · 123 unmodified lines | ||
| 1112 | 1099 | any |
| 1113 | 1100 | >; |
| 1114 | 1101 | }; |
| 1115 | − typecheckDeclarationCache: { | |
| 1116 | − getEntry: FunctionReference< | |
| 1117 | − "query", | |
| 1118 | − "internal", | |
| 1119 | − { cacheKey: string; maxAgeMs: number; workspaceId: Id<"workspaces"> }, | |
| 1120 | − any | |
| 1121 | − >; | |
| 1122 | − putEntry: FunctionReference< | |
| 1123 | − "mutation", | |
| 1124 | − "internal", | |
| 1125 | − { | |
| 1126 | − cacheKey: string; | |
| 1127 | − maxEntriesPerWorkspace?: number; | |
| 1128 | − sizeBytes: number; | |
| 1129 | − storageId: Id<"_storage">; | |
| 1130 | − workspaceId: Id<"workspaces">; | |
| 1131 | − }, | |
| 1132 | − any | |
| 1133 | − >; | |
| 1134 | − }; | |
| 1135 | 1102 | workspaceAuthInternal: { |
| 1136 | 1103 | getWorkspaceAccessForRequest: FunctionReference< |
| 1137 | 1104 | "query", |
| @@ -72,15 +72,6 @@ async function deleteWorkspaceData( · 71 unmodified lines | ||
| 72 | 72 | await ctx.db.delete(cacheEntry._id); |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | − const cachedDeclarations = await ctx.db | |
| 76 | − .query("typecheckDeclarationCache") | |
| 77 | − .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId)) | |
| 78 | − .collect(); | |
| 79 | − for (const cacheEntry of cachedDeclarations) { | |
| 80 | − await ctx.storage.delete(cacheEntry.storageId).catch(() => {}); | |
| 81 | − await ctx.db.delete(cacheEntry._id); | |
| 82 | − } | |
| 83 | − | |
| 84 | 75 | const workspaceMembers = await ctx.db |
| 85 | 76 | .query("workspaceMembers") |
| 86 | 77 | .withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceId)) |
| @@ -17,7 +17,6 @@ function setup() { · 16 unmodified lines | ||
| 17 | 17 | "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"), |
| 18 | 18 | "./workspaceToolCache.ts": () => import("./workspaceToolCache"), |
| 19 | 19 | "./openApiSpecCache.ts": () => import("./openApiSpecCache"), |
| 20 | − "./mcpNode.ts": () => import("./mcpNode"), | |
| 21 | 20 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 22 | 21 | }); |
| 23 | 22 | } |
| @@ -14,7 +14,6 @@ function setup() { · 13 unmodified lines | ||
| 14 | 14 | "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"), |
| 15 | 15 | "./workspaceToolCache.ts": () => import("./workspaceToolCache"), |
| 16 | 16 | "./openApiSpecCache.ts": () => import("./openApiSpecCache"), |
| 17 | − "./mcpNode.ts": () => import("./mcpNode"), | |
| 18 | 17 | "./anonymousOauth.ts": () => import("./anonymousOauth"), |
| 19 | 18 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 20 | 19 | }); |
| @@ -19,10 +19,11 @@ export function createMcpExecutorService(ctx: ActionCtx) { · 18 unmodified lines | ||
| 19 | 19 | actorId: string; |
| 20 | 20 | clientId?: string; |
| 21 | 21 | }) => { |
| 22 | − return (await ctx.runMutation(internal.executor.createTaskInternal, { | |
| 22 | + const taskInput = { | |
| 23 | 23 | ...input, |
| 24 | 24 | scheduleAfterCreate: false, |
| 25 | − })) as { task: TaskRecord }; | |
| 25 | + } as Parameters<typeof ctx.runMutation<typeof internal.executor.createTaskInternal>>[1]; | |
| 26 | + return (await ctx.runMutation(internal.executor.createTaskInternal, taskInput)) as { task: TaskRecord }; | |
| 26 | 27 | }, |
| 27 | 28 | runTaskNow: async (taskId: string) => { |
| 28 | 29 | return (await ctx.runAction(internal.executorNode.runTask, { taskId })) as null; |
| @@ -46,31 +47,6 @@ export function createMcpExecutorService(ctx: ActionCtx) { · 17 unmodified lines | ||
| 46 | 47 | |
| 47 | 48 | return (await ctx.runAction(internal.executorNode.listToolsInternal, { ...toolContext })) as ToolDescriptor[]; |
| 48 | 49 | }, |
| 49 | − listToolsForTypecheck: async (toolContext: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => { | |
| 50 | − const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, { ...toolContext }) as { | |
| 51 | − tools: ToolDescriptor[]; | |
| 52 | − dtsUrls?: Record<string, string>; | |
| 53 | − }; | |
| 54 | − | |
| 55 | − return { | |
| 56 | − tools: result.tools, | |
| 57 | − dtsUrls: result.dtsUrls ?? {}, | |
| 58 | − }; | |
| 59 | − }, | |
| 60 | − typecheckRunCode: async (input: { | |
| 61 | − code: string; | |
| 62 | − workspaceId: Id<"workspaces">; | |
| 63 | − actorId?: string; | |
| 64 | − clientId?: string; | |
| 65 | − }) => { | |
| 66 | − const result = await ctx.runAction(internal.mcpNode.typecheckRunCodeInternal, { | |
| 67 | − code: input.code, | |
| 68 | − workspaceId: input.workspaceId, | |
| 69 | − actorId: input.actorId, | |
| 70 | − clientId: input.clientId, | |
| 71 | − }); | |
| 72 | − return result; | |
| 73 | − }, | |
| 74 | 50 | listPendingApprovals: async (workspaceId: Id<"workspaces">) => { |
| 75 | 51 | return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId })) as PendingApprovalRecord[]; |
| 76 | 52 | }, |
| @@ -17,7 +17,6 @@ function setup() { · 16 unmodified lines | ||
| 17 | 17 | "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"), |
| 18 | 18 | "./workspaceToolCache.ts": () => import("./workspaceToolCache"), |
| 19 | 19 | "./openApiSpecCache.ts": () => import("./openApiSpecCache"), |
| 20 | − "./mcpNode.ts": () => import("./mcpNode"), | |
| 21 | 20 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 22 | 21 | }); |
| 23 | 22 | } |
| @@ -43,7 +42,7 @@ function createMcpTransport( · 19 unmodified lines | ||
| 43 | 42 | }); |
| 44 | 43 | } |
| 45 | 44 | |
| 46 | −test("MCP run_code reproduces Convex OOM failure surface", async () => { | |
| 45 | +test("MCP run_code no longer hits typecheck OOM path", async () => { | |
| 47 | 46 | const t = setup(); |
| 48 | 47 | const session = await t.mutation(internal.database.bootstrapAnonymousSession, {}); |
| 49 | 48 | |
| @@ -61,7 +60,7 @@ test("MCP run_code reproduces Convex OOM failure surface", async () => { · 11 unmodified lines | ||
| 61 | 60 | }); |
| 62 | 61 | |
| 63 | 62 | const serialized = JSON.stringify(result); |
| 64 | − expect(serialized.includes(OOM_MESSAGE)).toBe(true); | |
| 63 | + expect(serialized.includes(OOM_MESSAGE)).toBe(false); | |
| 65 | 64 | } finally { |
| 66 | 65 | await transport.close().catch(() => {}); |
| 67 | 66 | await client.close().catch(() => {}); |
| @@ -1,279 +0,0 @@ | ||
| 1 | −"use node"; | |
| 2 | − | |
| 3 | −import { createHash } from "node:crypto"; | |
| 4 | −import { createRequire } from "node:module"; | |
| 5 | −import { v } from "convex/values"; | |
| 6 | −import { internal } from "./_generated/api"; | |
| 7 | −import { internalAction } from "./_generated/server"; | |
| 8 | −import { loadSourceDtsByUrl } from "../core/src/dts-loader"; | |
| 9 | −import { | |
| 10 | − analyzeToolReferences, | |
| 11 | − generateToolDeclarations, | |
| 12 | − sliceOpenApiOperationsDts, | |
| 13 | − typecheckCode, | |
| 14 | −} from "../core/src/typechecker"; | |
| 15 | −import type { ToolDescriptor } from "../core/src/types"; | |
| 16 | −import { sourceSignature } from "./executor_node/tool_source_loading"; | |
| 17 | − | |
| 18 | −if (typeof (globalThis as { require?: unknown }).require !== "function") { | |
| 19 | − (globalThis as { require?: unknown }).require = createRequire(import.meta.url); | |
| 20 | −} | |
| 21 | − | |
| 22 | −const DECLARATION_CACHE_VERSION = "v1"; | |
| 23 | −const DECLARATION_CACHE_TTL_MS = 24 * 60 * 60_000; | |
| 24 | −const DECLARATION_CACHE_MAX_ENTRIES_PER_WORKSPACE = 96; | |
| 25 | − | |
| 26 | −function hashString(input: string): string { | |
| 27 | − return createHash("sha256").update(input).digest("hex"); | |
| 28 | −} | |
| 29 | − | |
| 30 | −function normalizeEnginePreference(): string { | |
| 31 | − const value = process.env.EXECUTOR_TYPECHECK_ENGINE?.trim().toLowerCase(); | |
| 32 | − if (!value) return "auto"; | |
| 33 | − if (value === "typescript") return "typescript"; | |
| 34 | − if (value === "tsgo") return "tsgo"; | |
| 35 | − return value; | |
| 36 | −} | |
| 37 | − | |
| 38 | −function toolFingerprint(tools: ToolDescriptor[]): string { | |
| 39 | − const compact = tools | |
| 40 | − .map((tool) => ({ | |
| 41 | − path: tool.path, | |
| 42 | − approval: tool.approval, | |
| 43 | − source: tool.source ?? "", | |
| 44 | − argsType: tool.argsType ?? "", | |
| 45 | − returnsType: tool.returnsType ?? "", | |
| 46 | − strictArgsType: tool.strictArgsType ?? "", | |
| 47 | − strictReturnsType: tool.strictReturnsType ?? "", | |
| 48 | − operationId: tool.operationId ?? "", | |
| 49 | − argPreviewKeys: Array.isArray(tool.argPreviewKeys) ? [...tool.argPreviewKeys].sort() : [], | |
| 50 | − })) | |
| 51 | − .sort((a, b) => a.path.localeCompare(b.path)); | |
| 52 | − return hashString(JSON.stringify(compact)); | |
| 53 | −} | |
| 54 | − | |
| 55 | −function declarationSelectionFingerprint( | |
| 56 | − selectiveMode: boolean, | |
| 57 | − operationIdsBySource: Map<string, Set<string>>, | |
| 58 | − sourcesRequiringFullDts: Set<string>, | |
| 59 | −): string { | |
| 60 | − if (!selectiveMode) { | |
| 61 | − return "mode:full"; | |
| 62 | − } | |
| 63 | − | |
| 64 | − const parts: string[] = []; | |
| 65 | − for (const source of [...sourcesRequiringFullDts].sort((a, b) => a.localeCompare(b))) { | |
| 66 | − parts.push(`${source}:*`); | |
| 67 | − } | |
| 68 | − for (const [source, operationIds] of [...operationIdsBySource.entries()].sort(([a], [b]) => a.localeCompare(b))) { | |
| 69 | − parts.push(`${source}:${[...operationIds].sort((a, b) => a.localeCompare(b)).join(",")}`); | |
| 70 | − } | |
| 71 | − | |
| 72 | − return parts.length > 0 ? parts.join("|") : "mode:selective:none"; | |
| 73 | −} | |
| 74 | − | |
| 75 | −function declarationCacheKey(input: { | |
| 76 | − workspaceSignature: string; | |
| 77 | − toolsFingerprint: string; | |
| 78 | − selectionFingerprint: string; | |
| 79 | − enginePreference: string; | |
| 80 | − hasDynamicToolAccess: boolean; | |
| 81 | − hasNonCallToolAccess: boolean; | |
| 82 | −}): string { | |
| 83 | − const payload = [ | |
| 84 | − DECLARATION_CACHE_VERSION, | |
| 85 | − input.workspaceSignature, | |
| 86 | − input.toolsFingerprint, | |
| 87 | − input.selectionFingerprint, | |
| 88 | − input.enginePreference, | |
| 89 | − input.hasDynamicToolAccess ? "dynamic:1" : "dynamic:0", | |
| 90 | − input.hasNonCallToolAccess ? "noncall:1" : "noncall:0", | |
| 91 | − ].join("|"); | |
| 92 | − return `${DECLARATION_CACHE_VERSION}:${hashString(payload)}`; | |
| 93 | −} | |
| 94 | − | |
| 95 | −function shouldUseSelectiveOpenApiDts(analysis: ReturnType<typeof analyzeToolReferences>): boolean { | |
| 96 | − return !analysis.hasDynamicToolAccess && !analysis.hasNonCallToolAccess; | |
| 97 | −} | |
| 98 | − | |
| 99 | −function mapOperationIdsBySource( | |
| 100 | − tools: ToolDescriptor[], | |
| 101 | − callPaths: readonly string[], | |
| 102 | − availableDtsSources: ReadonlySet<string>, | |
| 103 | −): { | |
| 104 | − operationIdsBySource: Map<string, Set<string>>; | |
| 105 | − sourcesRequiringFullDts: Set<string>; | |
| 106 | −} { | |
| 107 | − const toolByPath = new Map<string, ToolDescriptor>(tools.map((tool) => [tool.path, tool])); | |
| 108 | − const operationIdsBySource = new Map<string, Set<string>>(); | |
| 109 | − const sourcesRequiringFullDts = new Set<string>(); | |
| 110 | − | |
| 111 | − for (const path of callPaths) { | |
| 112 | − const descriptor = toolByPath.get(path); | |
| 113 | − if (!descriptor?.source || !availableDtsSources.has(descriptor.source)) continue; | |
| 114 | − | |
| 115 | − if (!descriptor.operationId) { | |
| 116 | − sourcesRequiringFullDts.add(descriptor.source); | |
| 117 | − continue; | |
| 118 | − } | |
| 119 | − | |
| 120 | − const operationIds = operationIdsBySource.get(descriptor.source) ?? new Set<string>(); | |
| 121 | − operationIds.add(descriptor.operationId); | |
| 122 | − operationIdsBySource.set(descriptor.source, operationIds); | |
| 123 | − } | |
| 124 | − | |
| 125 | − return { operationIdsBySource, sourcesRequiringFullDts }; | |
| 126 | −} | |
| 127 | − | |
| 128 | −function selectToolsForTypecheck( | |
| 129 | − tools: ToolDescriptor[], | |
| 130 | − analysis: ReturnType<typeof analyzeToolReferences>, | |
| 131 | −): ToolDescriptor[] { | |
| 132 | − if (!shouldUseSelectiveOpenApiDts(analysis)) { | |
| 133 | − return tools; | |
| 134 | − } | |
| 135 | − | |
| 136 | − if (analysis.callPaths.length === 0) { | |
| 137 | − return []; | |
| 138 | − } | |
| 139 | − | |
| 140 | − const wanted = new Set(analysis.callPaths); | |
| 141 | − return tools.filter((tool) => wanted.has(tool.path)); | |
| 142 | −} | |
| 143 | − | |
| 144 | −export const typecheckRunCodeInternal = internalAction({ | |
| 145 | − args: { | |
| 146 | − code: v.string(), | |
| 147 | − workspaceId: v.id("workspaces"), | |
| 148 | − actorId: v.optional(v.string()), | |
| 149 | − clientId: v.optional(v.string()), | |
| 150 | − }, | |
| 151 | − handler: async (ctx, args) => { | |
| 152 | − const toolContext = { | |
| 153 | − workspaceId: args.workspaceId, | |
| 154 | − actorId: args.actorId, | |
| 155 | − clientId: args.clientId, | |
| 156 | − }; | |
| 157 | − | |
| 158 | − const [result, sources] = await Promise.all([ | |
| 159 | − ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, toolContext) as Promise<{ | |
| 160 | − tools: ToolDescriptor[]; | |
| 161 | − dtsUrls?: Record<string, string>; | |
| 162 | − }>, | |
| 163 | − ctx.runQuery(internal.database.listToolSources, { workspaceId: args.workspaceId }) as Promise<Array<{ | |
| 164 | − id: string; | |
| 165 | − updatedAt: number; | |
| 166 | − enabled: boolean; | |
| 167 | − }>>, | |
| 168 | − ]); | |
| 169 | − | |
| 170 | − const enabledSources = sources.filter((source) => source.enabled); | |
| 171 | − const workspaceSignature = sourceSignature(args.workspaceId, enabledSources); | |
| 172 | − | |
| 173 | − const dtsUrls = result.dtsUrls ?? {}; | |
| 174 | − const dtsSources = new Set(Object.keys(dtsUrls)); | |
| 175 | − const analysis = analyzeToolReferences(args.code); | |
| 176 | − const selectiveMode = shouldUseSelectiveOpenApiDts(analysis); | |
| 177 | − const toolsForTypecheck = selectToolsForTypecheck(result.tools, analysis); | |
| 178 | − | |
| 179 | − let dtsUrlsToLoad: Record<string, string> = {}; | |
| 180 | − let operationIdsBySource = new Map<string, Set<string>>(); | |
| 181 | − let sourcesRequiringFullDts = new Set<string>(); | |
| 182 | − | |
| 183 | − if (selectiveMode) { | |
| 184 | − const mapped = mapOperationIdsBySource(result.tools, analysis.callPaths, dtsSources); | |
| 185 | − operationIdsBySource = mapped.operationIdsBySource; | |
| 186 | − sourcesRequiringFullDts = mapped.sourcesRequiringFullDts; | |
| 187 | − | |
| 188 | − for (const source of mapped.sourcesRequiringFullDts) { | |
| 189 | − const url = dtsUrls[source]; | |
| 190 | − if (url) { | |
| 191 | − dtsUrlsToLoad[source] = url; | |
| 192 | − } | |
| 193 | − } | |
| 194 | − for (const [source] of operationIdsBySource) { | |
| 195 | − const url = dtsUrls[source]; | |
| 196 | − if (url) { | |
| 197 | − dtsUrlsToLoad[source] = url; | |
| 198 | − } | |
| 199 | − } | |
| 200 | − } else { | |
| 201 | − dtsUrlsToLoad = dtsUrls; | |
| 202 | − } | |
| 203 | − | |
| 204 | − const cacheKey = declarationCacheKey({ | |
| 205 | − workspaceSignature, | |
| 206 | − toolsFingerprint: toolFingerprint(toolsForTypecheck), | |
| 207 | − selectionFingerprint: declarationSelectionFingerprint( | |
| 208 | − selectiveMode, | |
| 209 | − operationIdsBySource, | |
| 210 | − sourcesRequiringFullDts, | |
| 211 | − ), | |
| 212 | − enginePreference: normalizeEnginePreference(), | |
| 213 | − hasDynamicToolAccess: analysis.hasDynamicToolAccess, | |
| 214 | − hasNonCallToolAccess: analysis.hasNonCallToolAccess, | |
| 215 | − }); | |
| 216 | − | |
| 217 | − let declarations: string | null = null; | |
| 218 | − | |
| 219 | − try { | |
| 220 | − const cacheEntry = await ctx.runQuery(internal.typecheckDeclarationCache.getEntry, { | |
| 221 | − workspaceId: args.workspaceId, | |
| 222 | − cacheKey, | |
| 223 | − maxAgeMs: DECLARATION_CACHE_TTL_MS, | |
| 224 | − }); | |
| 225 | − if (cacheEntry) { | |
| 226 | − const blob = await ctx.storage.get(cacheEntry.storageId); | |
| 227 | − if (blob) { | |
| 228 | − declarations = await blob.text(); | |
| 229 | − } | |
| 230 | − } | |
| 231 | − } catch (error) { | |
| 232 | − const message = error instanceof Error ? error.message : String(error); | |
| 233 | − console.warn(`[executor] declaration cache read failed for '${args.workspaceId}': ${message}`); | |
| 234 | − } | |
| 235 | − | |
| 236 | − if (!declarations) { | |
| 237 | − const sourceDtsBySource = await loadSourceDtsByUrl(dtsUrlsToLoad); | |
| 238 | − | |
| 239 | − if (selectiveMode) { | |
| 240 | − for (const [source, operationIds] of operationIdsBySource.entries()) { | |
| 241 | − const fullDts = sourceDtsBySource[source]; | |
| 242 | − if (!fullDts) continue; | |
| 243 | − const sliced = sliceOpenApiOperationsDts(fullDts, operationIds); | |
| 244 | − if (sliced) { | |
| 245 | − sourceDtsBySource[source] = sliced; | |
| 246 | − } | |
| 247 | − } | |
| 248 | − } | |
| 249 | − | |
| 250 | − declarations = generateToolDeclarations(toolsForTypecheck, { | |
| 251 | − sourceDtsBySource, | |
| 252 | − }); | |
| 253 | − | |
| 254 | − try { | |
| 255 | − const blob = new Blob([declarations], { type: "text/plain" }); | |
| 256 | − const storageId = await ctx.storage.store(blob); | |
| 257 | − await ctx.runMutation(internal.typecheckDeclarationCache.putEntry, { | |
| 258 | − workspaceId: args.workspaceId, | |
| 259 | − cacheKey, | |
| 260 | − storageId, | |
| 261 | − sizeBytes: declarations.length, | |
| 262 | − maxEntriesPerWorkspace: DECLARATION_CACHE_MAX_ENTRIES_PER_WORKSPACE, | |
| 263 | − }); | |
| 264 | − } catch (error) { | |
| 265 | − const message = error instanceof Error ? error.message : String(error); | |
| 266 | − console.warn(`[executor] declaration cache write failed for '${args.workspaceId}': ${message}`); | |
| 267 | − } | |
| 268 | − } | |
| 269 | − | |
| 270 | − const declarationText = declarations ?? "declare const tools: {};"; | |
| 271 | − const typecheck = typecheckCode(args.code, declarationText); | |
| 272 | − | |
| 273 | − return { | |
| 274 | − ok: typecheck.ok, | |
| 275 | − errors: [...typecheck.errors], | |
| 276 | − tools: result.tools, | |
| 277 | − }; | |
| 278 | − }, | |
| 279 | −}); | |
| @@ -313,16 +313,6 @@ export default defineSchema({ · 312 unmodified lines | ||
| 313 | 313 | }) |
| 314 | 314 | .index("by_workspace", ["workspaceId"]), |
| 315 | 315 | |
| 316 | − typecheckDeclarationCache: defineTable({ | |
| 317 | − workspaceId: v.id("workspaces"), | |
| 318 | − cacheKey: v.string(), | |
| 319 | − storageId: v.id("_storage"), | |
| 320 | − sizeBytes: v.number(), | |
| 321 | − createdAt: v.number(), | |
| 322 | − }) | |
| 323 | − .index("by_workspace_cache_key", ["workspaceId", "cacheKey"]) | |
| 324 | − .index("by_workspace_created", ["workspaceId", "createdAt"]), | |
| 325 | − | |
| 326 | 316 | anonymousSessions: defineTable({ |
| 327 | 317 | sessionId: v.string(), // domain ID: anon_session_<uuid> or mcp_<uuid> |
| 328 | 318 | workspaceId: v.id("workspaces"), |
| @@ -1,188 +0,0 @@ | ||
| 1 | −import { describe, expect, test } from "bun:test"; | |
| 2 | −import { convexTest } from "convex-test"; | |
| 3 | −import { internal } from "./_generated/api"; | |
| 4 | −import schema from "./schema"; | |
| 5 | − | |
| 6 | −function setup() { | |
| 7 | − return convexTest(schema, { | |
| 8 | − "./typecheckDeclarationCache.ts": () => import("./typecheckDeclarationCache"), | |
| 9 | − "./_generated/api.js": () => import("./_generated/api.js"), | |
| 10 | − }); | |
| 11 | −} | |
| 12 | − | |
| 13 | −describe("typecheckDeclarationCache table operations", () => { | |
| 14 | − test("getEntry returns null for empty cache", async () => { | |
| 15 | − const t = setup(); | |
| 16 | − const workspaceId = await t.run(async (ctx) => { | |
| 17 | − const orgId = await ctx.db.insert("organizations", { | |
| 18 | − slug: "org", | |
| 19 | − name: "Org", | |
| 20 | − status: "active", | |
| 21 | − createdAt: Date.now(), | |
| 22 | − updatedAt: Date.now(), | |
| 23 | − }); | |
| 24 | − return await ctx.db.insert("workspaces", { | |
| 25 | − organizationId: orgId, | |
| 26 | − slug: "ws", | |
| 27 | − name: "Workspace", | |
| 28 | − createdAt: Date.now(), | |
| 29 | − updatedAt: Date.now(), | |
| 30 | − }); | |
| 31 | − }); | |
| 32 | − | |
| 33 | − const result = await t.query(internal.typecheckDeclarationCache.getEntry, { | |
| 34 | − workspaceId, | |
| 35 | − cacheKey: "cache:a", | |
| 36 | − maxAgeMs: 10_000, | |
| 37 | − }); | |
| 38 | − | |
| 39 | − expect(result).toBeNull(); | |
| 40 | − }); | |
| 41 | − | |
| 42 | − test("putEntry stores and getEntry retrieves", async () => { | |
| 43 | − const t = setup(); | |
| 44 | − const workspaceId = await t.run(async (ctx) => { | |
| 45 | − const orgId = await ctx.db.insert("organizations", { | |
| 46 | − slug: "org2", | |
| 47 | − name: "Org 2", | |
| 48 | − status: "active", | |
| 49 | − createdAt: Date.now(), | |
| 50 | − updatedAt: Date.now(), | |
| 51 | − }); | |
| 52 | − return await ctx.db.insert("workspaces", { | |
| 53 | − organizationId: orgId, | |
| 54 | − slug: "ws2", | |
| 55 | − name: "Workspace 2", | |
| 56 | − createdAt: Date.now(), | |
| 57 | − updatedAt: Date.now(), | |
| 58 | − }); | |
| 59 | − }); | |
| 60 | − | |
| 61 | − const storageId = await t.run(async (ctx) => { | |
| 62 | − return await ctx.storage.store(new Blob(["declare const tools: {};"], { type: "text/plain" })); | |
| 63 | − }); | |
| 64 | − | |
| 65 | − await t.mutation(internal.typecheckDeclarationCache.putEntry, { | |
| 66 | − workspaceId, | |
| 67 | − cacheKey: "cache:b", | |
| 68 | − storageId, | |
| 69 | − sizeBytes: 24, | |
| 70 | − }); | |
| 71 | − | |
| 72 | − const entry = await t.query(internal.typecheckDeclarationCache.getEntry, { | |
| 73 | − workspaceId, | |
| 74 | − cacheKey: "cache:b", | |
| 75 | − maxAgeMs: 10_000, | |
| 76 | − }); | |
| 77 | − | |
| 78 | − expect(entry).not.toBeNull(); | |
| 79 | − expect(entry!.storageId).toBe(storageId); | |
| 80 | − expect(entry!.sizeBytes).toBe(24); | |
| 81 | − }); | |
| 82 | − | |
| 83 | − test("putEntry replaces existing key and deletes stale blob", async () => { | |
| 84 | − const t = setup(); | |
| 85 | − const workspaceId = await t.run(async (ctx) => { | |
| 86 | − const orgId = await ctx.db.insert("organizations", { | |
| 87 | − slug: "org3", | |
| 88 | − name: "Org 3", | |
| 89 | − status: "active", | |
| 90 | − createdAt: Date.now(), | |
| 91 | − updatedAt: Date.now(), | |
| 92 | − }); | |
| 93 | − return await ctx.db.insert("workspaces", { | |
| 94 | − organizationId: orgId, | |
| 95 | − slug: "ws3", | |
| 96 | − name: "Workspace 3", | |
| 97 | − createdAt: Date.now(), | |
| 98 | − updatedAt: Date.now(), | |
| 99 | − }); | |
| 100 | − }); | |
| 101 | − | |
| 102 | − const firstStorageId = await t.run(async (ctx) => { | |
| 103 | − return await ctx.storage.store(new Blob(["first"], { type: "text/plain" })); | |
| 104 | − }); | |
| 105 | − | |
| 106 | − await t.mutation(internal.typecheckDeclarationCache.putEntry, { | |
| 107 | − workspaceId, | |
| 108 | − cacheKey: "cache:c", | |
| 109 | − storageId: firstStorageId, | |
| 110 | − sizeBytes: 5, | |
| 111 | − }); | |
| 112 | − | |
| 113 | − const secondStorageId = await t.run(async (ctx) => { | |
| 114 | − return await ctx.storage.store(new Blob(["second"], { type: "text/plain" })); | |
| 115 | − }); | |
| 116 | − | |
| 117 | − await t.mutation(internal.typecheckDeclarationCache.putEntry, { | |
| 118 | − workspaceId, | |
| 119 | − cacheKey: "cache:c", | |
| 120 | − storageId: secondStorageId, | |
| 121 | − sizeBytes: 6, | |
| 122 | − }); | |
| 123 | − | |
| 124 | − const entry = await t.query(internal.typecheckDeclarationCache.getEntry, { | |
| 125 | − workspaceId, | |
| 126 | − cacheKey: "cache:c", | |
| 127 | − maxAgeMs: 10_000, | |
| 128 | − }); | |
| 129 | − expect(entry!.storageId).toBe(secondStorageId); | |
| 130 | − | |
| 131 | − const staleBlob = await t.run(async (ctx) => await ctx.storage.get(firstStorageId)); | |
| 132 | − expect(staleBlob).toBeNull(); | |
| 133 | − }); | |
| 134 | − | |
| 135 | − test("putEntry prunes old rows when maxEntriesPerWorkspace is set", async () => { | |
| 136 | − const t = setup(); | |
| 137 | − const workspaceId = await t.run(async (ctx) => { | |
| 138 | − const orgId = await ctx.db.insert("organizations", { | |
| 139 | − slug: "org4", | |
| 140 | − name: "Org 4", | |
| 141 | − status: "active", | |
| 142 | − createdAt: Date.now(), | |
| 143 | − updatedAt: Date.now(), | |
| 144 | − }); | |
| 145 | − return await ctx.db.insert("workspaces", { | |
| 146 | − organizationId: orgId, | |
| 147 | − slug: "ws4", | |
| 148 | − name: "Workspace 4", | |
| 149 | − createdAt: Date.now(), | |
| 150 | − updatedAt: Date.now(), | |
| 151 | − }); | |
| 152 | − }); | |
| 153 | − | |
| 154 | − for (const key of ["cache:1", "cache:2", "cache:3"]) { | |
| 155 | − const storageId = await t.run(async (ctx) => { | |
| 156 | − return await ctx.storage.store(new Blob([key], { type: "text/plain" })); | |
| 157 | − }); | |
| 158 | − await t.mutation(internal.typecheckDeclarationCache.putEntry, { | |
| 159 | − workspaceId, | |
| 160 | − cacheKey: key, | |
| 161 | − storageId, | |
| 162 | − sizeBytes: key.length, | |
| 163 | − maxEntriesPerWorkspace: 2, | |
| 164 | − }); | |
| 165 | − await new Promise((resolve) => setTimeout(resolve, 2)); | |
| 166 | − } | |
| 167 | − | |
| 168 | − const oldest = await t.query(internal.typecheckDeclarationCache.getEntry, { | |
| 169 | − workspaceId, | |
| 170 | − cacheKey: "cache:1", | |
| 171 | − maxAgeMs: 10_000, | |
| 172 | − }); | |
| 173 | − const newestA = await t.query(internal.typecheckDeclarationCache.getEntry, { | |
| 174 | − workspaceId, | |
| 175 | − cacheKey: "cache:2", | |
| 176 | − maxAgeMs: 10_000, | |
| 177 | − }); | |
| 178 | − const newestB = await t.query(internal.typecheckDeclarationCache.getEntry, { | |
| 179 | − workspaceId, | |
| 180 | − cacheKey: "cache:3", | |
| 181 | − maxAgeMs: 10_000, | |
| 182 | − }); | |
| 183 | − | |
| 184 | − expect(oldest).toBeNull(); | |
| 185 | − expect(newestA).not.toBeNull(); | |
| 186 | − expect(newestB).not.toBeNull(); | |
| 187 | − }); | |
| 188 | −}); | |
| @@ -1,76 +0,0 @@ | ||
| 1 | −import { v } from "convex/values"; | |
| 2 | −import { internalMutation, internalQuery } from "./_generated/server"; | |
| 3 | − | |
| 4 | −export const getEntry = internalQuery({ | |
| 5 | − args: { | |
| 6 | − workspaceId: v.id("workspaces"), | |
| 7 | − cacheKey: v.string(), | |
| 8 | − maxAgeMs: v.number(), | |
| 9 | − }, | |
| 10 | − handler: async (ctx, args) => { | |
| 11 | − const entry = await ctx.db | |
| 12 | − .query("typecheckDeclarationCache") | |
| 13 | − .withIndex("by_workspace_cache_key", (q) => | |
| 14 | − q.eq("workspaceId", args.workspaceId).eq("cacheKey", args.cacheKey), | |
| 15 | − ) | |
| 16 | − .unique(); | |
| 17 | − | |
| 18 | − if (!entry) return null; | |
| 19 | − | |
| 20 | − const ageMs = Date.now() - entry.createdAt; | |
| 21 | − if (ageMs > args.maxAgeMs) return null; | |
| 22 | − | |
| 23 | − return { | |
| 24 | − storageId: entry.storageId, | |
| 25 | − sizeBytes: entry.sizeBytes, | |
| 26 | − createdAt: entry.createdAt, | |
| 27 | − }; | |
| 28 | − }, | |
| 29 | −}); | |
| 30 | − | |
| 31 | −export const putEntry = internalMutation({ | |
| 32 | − args: { | |
| 33 | − workspaceId: v.id("workspaces"), | |
| 34 | − cacheKey: v.string(), | |
| 35 | − storageId: v.id("_storage"), | |
| 36 | − sizeBytes: v.number(), | |
| 37 | − maxEntriesPerWorkspace: v.optional(v.number()), | |
| 38 | − }, | |
| 39 | − handler: async (ctx, args) => { | |
| 40 | − const existing = await ctx.db | |
| 41 | − .query("typecheckDeclarationCache") | |
| 42 | − .withIndex("by_workspace_cache_key", (q) => | |
| 43 | − q.eq("workspaceId", args.workspaceId).eq("cacheKey", args.cacheKey), | |
| 44 | − ) | |
| 45 | − .unique(); | |
| 46 | − | |
| 47 | − if (existing) { | |
| 48 | − await ctx.storage.delete(existing.storageId).catch(() => {}); | |
| 49 | − await ctx.db.delete(existing._id); | |
| 50 | − } | |
| 51 | − | |
| 52 | − await ctx.db.insert("typecheckDeclarationCache", { | |
| 53 | − workspaceId: args.workspaceId, | |
| 54 | − cacheKey: args.cacheKey, | |
| 55 | − storageId: args.storageId, | |
| 56 | − sizeBytes: args.sizeBytes, | |
| 57 | − createdAt: Date.now(), | |
| 58 | − }); | |
| 59 | − | |
| 60 | − const maxEntries = Math.max(1, Math.floor(args.maxEntriesPerWorkspace ?? 64)); | |
| 61 | − const entries = await ctx.db | |
| 62 | − .query("typecheckDeclarationCache") | |
| 63 | − .withIndex("by_workspace_created", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 64 | − .order("desc") | |
| 65 | − .collect(); | |
| 66 | − | |
| 67 | − if (entries.length <= maxEntries) { | |
| 68 | − return; | |
| 69 | − } | |
| 70 | − | |
| 71 | − for (const stale of entries.slice(maxEntries)) { | |
| 72 | − await ctx.storage.delete(stale.storageId).catch(() => {}); | |
| 73 | − await ctx.db.delete(stale._id); | |
| 74 | − } | |
| 75 | − }, | |
| 76 | −}); | |
| @@ -10,11 +10,6 @@ class FakeMcpService { · 9 unmodified lines | ||
| 10 | 10 | private readonly tasks = new Map<string, TaskRecord>(); |
| 11 | 11 | private readonly sessions = new Map<string, AnonymousContext>(); |
| 12 | 12 | private readonly listeners = new Map<string, Set<(event: LiveTaskEvent) => void>>(); |
| 13 | − private typecheckPayload?: { tools: ToolDescriptor[]; dtsUrls: Record<string, string> }; | |
| 14 | − | |
| 15 | − setTypecheckPayload(payload: { tools: ToolDescriptor[]; dtsUrls: Record<string, string> }) { | |
| 16 | − this.typecheckPayload = payload; | |
| 17 | − } | |
| 18 | 13 | |
| 19 | 14 | async createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }> { |
| 20 | 15 | const id = `task_${crypto.randomUUID()}`; |
| @@ -99,18 +94,6 @@ class FakeMcpService { · 78 unmodified lines | ||
| 99 | 94 | { path: "utils.get_time", description: "Get the current time", approval: "auto" }, |
| 100 | 95 | ]; |
| 101 | 96 | } |
| 102 | − | |
| 103 | − async listToolsForTypecheck( | |
| 104 | − _context: { workspaceId: string; actorId?: string; clientId?: string }, | |
| 105 | − ): Promise<{ tools: ToolDescriptor[]; dtsUrls: Record<string, string> }> { | |
| 106 | − if (this.typecheckPayload) { | |
| 107 | − return this.typecheckPayload; | |
| 108 | − } | |
| 109 | − return { | |
| 110 | − tools: await this.listTools(_context), | |
| 111 | − dtsUrls: {}, | |
| 112 | − }; | |
| 113 | − } | |
| 114 | 97 | } |
| 115 | 98 | |
| 116 | 99 | async function withMcpClient<T>( |
| @@ -219,37 +202,14 @@ test("run_code MCP tool bootstraps anonymous context when workspace is omitted", · 102 unmodified lines | ||
| 219 | 202 | }); |
| 220 | 203 | }); |
| 221 | 204 | |
| 222 | −test("run_code typecheck uses OpenAPI .d.ts from typecheck context", async () => { | |
| 205 | +test("run_code MCP tool does not gate on TypeScript errors", async () => { | |
| 223 | 206 | const service = new FakeMcpService(); |
| 224 | − service.setTypecheckPayload({ | |
| 225 | − tools: [ | |
| 226 | − { | |
| 227 | − path: "github.issues.list_for_repo", | |
| 228 | − description: "List issues for a repository", | |
| 229 | − approval: "auto", | |
| 230 | − source: "openapi:github", | |
| 231 | − operationId: "issues/list-for-repo", | |
| 232 | − }, | |
| 233 | − ], | |
| 234 | − dtsUrls: { | |
| 235 | − "openapi:github": | |
| 236 | − "data:text/plain," | |
| 237 | − + encodeURIComponent( | |
| 238 | − `export interface operations { | |
| 239 | − "issues/list-for-repo": { | |
| 240 | − parameters: { path: { owner: string; repo: string } }; | |
| 241 | − responses: { 200: { content: { "application/json": { id: number }[] } } }; | |
| 242 | − }; | |
| 243 | −}`, | |
| 244 | − ), | |
| 245 | − }, | |
| 246 | − }); | |
| 247 | 207 | |
| 248 | 208 | await withMcpClient(service, async (client) => { |
| 249 | 209 | const result = (await client.callTool({ |
| 250 | 210 | name: "run_code", |
| 251 | 211 | arguments: { |
| 252 | − code: "await tools.github.issues.list_for_repo({ owner: 123, repo: 'answeroverflow' });", | |
| 212 | + code: "const value: string = 123; return value;", | |
| 253 | 213 | }, |
| 254 | 214 | })) as { |
| 255 | 215 | isError?: boolean; |
| @@ -257,11 +217,9 @@ test("run_code typecheck uses OpenAPI .d.ts from typecheck context", async () => · 1 unmodified line | ||
| 257 | 217 | structuredContent?: Record<string, unknown>; |
| 258 | 218 | }; |
| 259 | 219 | |
| 260 | − expect(result.isError).toBe(true); | |
| 261 | − const typecheckErrors = Array.isArray(result.structuredContent?.typecheckErrors) | |
| 262 | − ? (result.structuredContent!.typecheckErrors as string[]) | |
| 263 | − : []; | |
| 264 | − expect(typecheckErrors.length).toBeGreaterThan(0); | |
| 265 | − expect(typecheckErrors.some((error) => error.includes("owner") || error.includes("number"))).toBe(true); | |
| 220 | + expect(result.isError).toBeUndefined(); | |
| 221 | + const text = result.content.find((part) => part.type === "text"); | |
| 222 | + expect(text?.text).toContain("status: completed"); | |
| 223 | + expect(result.structuredContent?.typecheckErrors).toBeUndefined(); | |
| 266 | 224 | }); |
| 267 | 225 | }); |
| @@ -2,8 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; · 1 unmodified line | ||
| 2 | 2 | import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; |
| 3 | 3 | import { Result } from "better-result"; |
| 4 | 4 | import { z } from "zod"; |
| 5 | −import { loadSourceDtsByUrl } from "./dts-loader"; | |
| 6 | −import { generateToolDeclarations, generateToolInventory, typecheckCode } from "./typechecker"; | |
| 5 | +import { generateToolInventory } from "./typechecker"; | |
| 7 | 6 | import type { LiveTaskEvent } from "./events"; |
| 8 | 7 | import type { |
| 9 | 8 | AnonymousContext, |
| @@ -29,15 +28,6 @@ interface McpExecutorService { · 19 unmodified lines | ||
| 29 | 28 | subscribe(taskId: string, workspaceId: Id<"workspaces">, listener: (event: LiveTaskEvent) => void): () => void; |
| 30 | 29 | bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>; |
| 31 | 30 | listTools(context?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }): Promise<ToolDescriptor[]>; |
| 32 | − listToolsForTypecheck?( | |
| 33 | − context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }, | |
| 34 | − ): Promise<{ tools: ToolDescriptor[]; dtsUrls: Record<string, string> }>; | |
| 35 | − typecheckRunCode?(input: { | |
| 36 | − code: string; | |
| 37 | − workspaceId: Id<"workspaces">; | |
| 38 | − actorId?: string; | |
| 39 | − clientId?: string; | |
| 40 | − }): Promise<{ ok: boolean; errors: string[]; tools: ToolDescriptor[] }>; | |
| 41 | 31 | listPendingApprovals?(workspaceId: Id<"workspaces">): Promise<PendingApprovalRecord[]>; |
| 42 | 32 | resolveApproval?(input: { |
| 43 | 33 | workspaceId: Id<"workspaces">; |
| @@ -238,7 +228,7 @@ function waitForTerminalTask( · 194 unmodified lines | ||
| 238 | 228 | |
| 239 | 229 | function buildRunCodeDescription(tools?: ToolDescriptor[]): string { |
| 240 | 230 | const base = |
| 241 | − "Execute TypeScript code in a sandboxed runtime. The code has access to a `tools` object with typed methods for calling external services. Use `return` to return a value. Waits for completion and returns only explicit return values (console output is not returned). Code is typechecked before execution — type errors are returned without running. Runtime has no filesystem/process/import access; use `tools.*` for external calls."; | |
| 231 | + "Execute TypeScript code in a sandboxed runtime. The code has access to a `tools` object with typed methods for calling external services. Use `return` to return a value. Waits for completion and returns only explicit return values (console output is not returned). Runtime has no filesystem/process/import access; use `tools.*` for external calls."; | |
| 242 | 232 | const toolList = tools ?? []; |
| 243 | 233 | const topLevelKeys = listTopLevelToolKeys(toolList); |
| 244 | 234 | const rootKeysNote = topLevelKeys.length > 0 |
| @@ -381,83 +371,6 @@ function createRunCodeTool( · 136 unmodified lines | ||
| 381 | 371 | }; |
| 382 | 372 | } |
| 383 | 373 | |
| 384 | − // Typecheck code before execution — align with Monaco by loading source .d.ts | |
| 385 | − // for OpenAPI tools when available. | |
| 386 | − let toolsForContext: ToolDescriptor[]; | |
| 387 | − let typecheck: { ok: boolean; errors: readonly string[] }; | |
| 388 | − if (service.typecheckRunCode) { | |
| 389 | − const result = await service.typecheckRunCode({ | |
| 390 | − code: input.code, | |
| 391 | − workspaceId: context.workspaceId, | |
| 392 | − actorId: context.actorId, | |
| 393 | − clientId: context.clientId, | |
| 394 | − }); | |
| 395 | − toolsForContext = result.tools; | |
| 396 | − typecheck = { | |
| 397 | − ok: result.ok, | |
| 398 | − errors: result.errors, | |
| 399 | − }; | |
| 400 | − } else { | |
| 401 | − let sourceDtsBySource: Record<string, string> = {}; | |
| 402 | − if (service.listToolsForTypecheck) { | |
| 403 | − const { tools, dtsUrls } = await service.listToolsForTypecheck({ | |
| 404 | − workspaceId: context.workspaceId, | |
| 405 | − actorId: context.actorId, | |
| 406 | − clientId: context.clientId, | |
| 407 | − }); | |
| 408 | − toolsForContext = tools; | |
| 409 | − sourceDtsBySource = await loadSourceDtsByUrl(dtsUrls ?? {}); | |
| 410 | − } else { | |
| 411 | − toolsForContext = await service.listTools({ | |
| 412 | − workspaceId: context.workspaceId, | |
| 413 | − actorId: context.actorId, | |
| 414 | − clientId: context.clientId, | |
| 415 | − }); | |
| 416 | − } | |
| 417 | − | |
| 418 | − const declarations = generateToolDeclarations(toolsForContext, { | |
| 419 | − sourceDtsBySource, | |
| 420 | − }); | |
| 421 | − typecheck = typecheckCode(input.code, declarations); | |
| 422 | − } | |
| 423 | − | |
| 424 | − if (!typecheck.ok) { | |
| 425 | − const topLevelKeys = listTopLevelToolKeys(toolsForContext); | |
| 426 | − const toolsShadowingError = typecheck.errors.some((error) => | |
| 427 | − error.includes("Block-scoped variable 'tools' used before its declaration") | |
| 428 | − || error.includes("'tools' implicitly has type 'any'"), | |
| 429 | − ); | |
| 430 | − | |
| 431 | − const hintLines: string[] = []; | |
| 432 | − if (toolsShadowingError) { | |
| 433 | − hintLines.push("Hint: avoid declaring a local variable named `tools`."); | |
| 434 | − hintLines.push("Example: `const discovered = await tools.discover({ query: \"github issues\" });`"); | |
| 435 | − } | |
| 436 | − if (topLevelKeys.length > 0) { | |
| 437 | − hintLines.push(`Available top-level tool keys: ${topLevelKeys.join(", ")}`); | |
| 438 | − } | |
| 439 | − | |
| 440 | − const errorText = [ | |
| 441 | − "TypeScript type errors in generated code:", | |
| 442 | − "", | |
| 443 | − ...typecheck.errors.map((e) => ` ${e}`), | |
| 444 | − ...(hintLines.length > 0 ? ["", ...hintLines] : []), | |
| 445 | − "", | |
| 446 | − "Fix the type errors and try again.", | |
| 447 | − ].join("\n"); | |
| 448 | − | |
| 449 | − return { | |
| 450 | − content: [textContent(errorText)], | |
| 451 | − isError: true, | |
| 452 | − structuredContent: { | |
| 453 | − typecheckErrors: typecheck.errors, | |
| 454 | − workspaceId: context.workspaceId, | |
| 455 | − actorId: context.actorId, | |
| 456 | − sessionId: context.sessionId, | |
| 457 | − }, | |
| 458 | − }; | |
| 459 | − } | |
| 460 | − | |
| 461 | 374 | const created = await service.createTask({ |
| 462 | 375 | code: input.code, |
| 463 | 376 | timeoutMs: requestedTimeoutMs, |
| @@ -589,20 +502,10 @@ async function createMcpServer( · 125 unmodified lines | ||
| 589 | 502 | ); |
| 590 | 503 | const onApprovalPrompt = createMcpApprovalPrompt(mcp); |
| 591 | 504 | |
| 592 | − // If workspace context is provided, fetch tool inventory for richer description | |
| 593 | − let tools: ToolDescriptor[] | undefined; | |
| 594 | − if (context) { | |
| 595 | − tools = await service.listTools({ | |
| 596 | − workspaceId: context.workspaceId, | |
| 597 | − actorId: context.actorId, | |
| 598 | − clientId: context.clientId, | |
| 599 | − }); | |
| 600 | − } | |
| 601 | − | |
| 602 | 505 | mcp.registerTool( |
| 603 | 506 | "run_code", |
| 604 | 507 | { |
| 605 | − description: buildRunCodeDescription(tools), | |
| 508 | + description: buildRunCodeDescription(), | |
| 606 | 509 | inputSchema: context ? BOUND_INPUT : FULL_INPUT, |
| 607 | 510 | }, |
| 608 | 511 | createRunCodeTool(service, context, onApprovalPrompt), |
| @@ -2,7 +2,7 @@ · 1 unmodified line | ||
| 2 | 2 | * Kills all processes started by `bun dev`. |
| 3 | 3 | * |
| 4 | 4 | * 1. Reads PIDs from .dev.pids and kills process trees |
| 5 | − * 2. Falls back to killing anything on known dev ports (3000, 3002) | |
| 5 | + * 2. Falls back to killing anything on known dev ports (assistant port) | |
| 6 | 6 | * |
| 7 | 7 | * Usage: bun run kill:all |
| 8 | 8 | */ |
| @@ -11,7 +11,8 @@ import { join } from "node:path"; · 2 unmodified lines | ||
| 11 | 11 | import { unlinkSync } from "node:fs"; |
| 12 | 12 | |
| 13 | 13 | const PID_FILE = join(import.meta.dir, ".dev.pids"); |
| 14 | −const DEV_PORTS = [3000, 3002]; | |
| 14 | +const ASSISTANT_PORT = Number(Bun.env.ASSISTANT_PORT ?? 3002); | |
| 15 | +const DEV_PORTS = [ASSISTANT_PORT]; | |
| 15 | 16 | |
| 16 | 17 | let killed = 0; |
| 17 | 18 | let skipped = 0; |