History

Update Assistant server port and configuration

- 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.

Rhys Sullivan committed 9 months ago

Commit 2ed5c861 parent c0afbbd18 files · +27 −783Browse files

Unified · Split

18 files
  1. .env.example +2 −1
  2. README.md +1 −1
  3. assistant/packages/bot/src/index.ts +1 −1
  4. assistant/packages/server/src/index.ts +1 −1
  5. dev.ts +5 −2
  6. executor/packages/convex/_generated/api.d.ts +0 −33
  7. executor/packages/convex/accounts.ts +0 −9
  8. executor/packages/convex/executor-mcp.e2e.test.ts +0 −1
  9. executor/packages/convex/http-anonymous-oauth.e2e.test.ts +0 −1
  10. executor/packages/convex/http/mcp_service.ts +3 −27
  11. executor/packages/convex/mcp-run-code-oom.e2e.test.ts +2 −3
  12. executor/packages/convex/mcpNode.ts +0 −279
  13. executor/packages/convex/schema.ts +0 −10
  14. executor/packages/convex/typecheckDeclarationCache.test.ts +0 −188
  15. executor/packages/convex/typecheckDeclarationCache.ts +0 −76
  16. executor/packages/core/src/mcp-server.test.ts +6 −48
  17. executor/packages/core/src/mcp-server.ts +3 −100
  18. kill-all.ts +3 −2
.env.example+2−1
@@ -25,6 +25,7 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 24 unmodified lines
25# EXECUTOR_BACKEND_PORT=541025# EXECUTOR_BACKEND_PORT=5410
26# EXECUTOR_BACKEND_SITE_PORT=541126# EXECUTOR_BACKEND_SITE_PORT=5411
27# EXECUTOR_WEB_PORT=531227# EXECUTOR_WEB_PORT=5312
28# ASSISTANT_PORT=3002
28 29
29# ── MCP OAuth (optional) ──30# ── MCP OAuth (optional) ──
30# Set to 1 to enable self-issued anonymous OAuth even without WorkOS auth.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# ── Assistant (optional) ──37# ── Assistant (optional) ──
37# OPENASSISTANT_EXECUTOR_URL=http://localhost:300138# OPENASSISTANT_EXECUTOR_URL=http://localhost:3001
38# OPENASSISTANT_CALLBACK_BASE_URL=http://localhost:300039# OPENASSISTANT_CALLBACK_BASE_URL=http://localhost:3002
39 40
40# ── Tool source API keys (optional) ──41# ── Tool source API keys (optional) ──
41# POSTHOG_PERSONAL_API_KEY=...42# POSTHOG_PERSONAL_API_KEY=...
README.md+1−1
@@ -64,7 +64,7 @@ bun run dev · 63 unmodified lines
64- Convex dev watcher for `executor/`64- Convex dev watcher for `executor/`
65- Executor web app (`http://localhost:4312`)65- Executor web app (`http://localhost:4312`)
66- Executor MCP endpoint (`<CONVEX_SITE_URL>/mcp`)66- Executor MCP endpoint (`<CONVEX_SITE_URL>/mcp`)
67- Assistant server (`http://localhost:3000`)67- Assistant server (`http://localhost:3002`)
68- Discord bot (only if `DISCORD_BOT_TOKEN` is set)68- Discord bot (only if `DISCORD_BOT_TOKEN` is set)
69 69
70PIDs are tracked in `.dev.pids`. To stop all processes:70PIDs are tracked in `.dev.pids`. To stop all processes:
assistant/packages/bot/src/index.ts+1−1
@@ -26,7 +26,7 @@ if (!DISCORD_TOKEN) { · 25 unmodified lines
26 throw new Error("DISCORD_BOT_TOKEN is required");26 throw new Error("DISCORD_BOT_TOKEN is required");
27}27}
28 28
29const SERVER_URL = Bun.env.ASSISTANT_SERVER_URL ?? "http://localhost:3000";29const SERVER_URL = Bun.env.ASSISTANT_SERVER_URL ?? `http://localhost:${Bun.env.ASSISTANT_PORT ?? "3002"}`;
30const CONVEX_URL = Bun.env.CONVEX_URL;30const CONVEX_URL = Bun.env.CONVEX_URL;
31if (!CONVEX_URL) {31if (!CONVEX_URL) {
32 throw new Error("CONVEX_URL is required. Set it in your environment.");32 throw new Error("CONVEX_URL is required. Set it in your environment.");
assistant/packages/server/src/index.ts+1−1
@@ -7,7 +7,7 @@ import { ConvexHttpClient } from "convex/browser"; · 6 unmodified lines
7import { api } from "@executor/convex/_generated/api";7import { api } from "@executor/convex/_generated/api";
8import type { Id } from "@executor/convex/_generated/dataModel";8import type { Id } from "@executor/convex/_generated/dataModel";
9 9
10const PORT = Number(Bun.env.PORT ?? 3000);10const PORT = Number(Bun.env.ASSISTANT_PORT ?? Bun.env.PORT ?? 3002);
11const CONVEX_URL = Bun.env.CONVEX_URL;11const CONVEX_URL = Bun.env.CONVEX_URL;
12if (!CONVEX_URL) {12if (!CONVEX_URL) {
13 throw new Error("CONVEX_URL is required. Set it in your environment.");13 throw new Error("CONVEX_URL is required. Set it in your environment.");
dev.ts+5−2
@@ -10,7 +10,7 @@ · 9 unmodified lines
10 * 2. Convex cloud dev function watcher │10 * 2. Convex cloud dev function watcher │
11 * 3. CF sandbox host worker (port 8787) ├─ all started concurrently11 * 3. CF sandbox host worker (port 8787) ├─ all started concurrently
12 * 4. Executor web UI (port 4312) │12 * 4. Executor web UI (port 4312) │
13 * 5. Assistant server (port 3000) │13 * 5. Assistant server (port 3002) │
14 * 6. Discord bot ─┘14 * 6. Discord bot ─┘
15 *15 *
16 * All processes are killed when this script exits (Ctrl+C).16 * All processes are killed when this script exits (Ctrl+C).
@@ -122,7 +122,8 @@ process.on("SIGTERM", shutdown); · 105 unmodified lines
122 122
123const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312);123const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312);
124const SANDBOX_PORT = Number(Bun.env.SANDBOX_PORT ?? 8787);124const SANDBOX_PORT = Number(Bun.env.SANDBOX_PORT ?? 8787);
125const DEV_PORTS = [3000, EXECUTOR_WEB_PORT, SANDBOX_PORT];125const ASSISTANT_PORT = Number(Bun.env.ASSISTANT_PORT ?? 3002);
126const DEV_PORTS = [ASSISTANT_PORT, EXECUTOR_WEB_PORT, SANDBOX_PORT];
126 127
127async function killStaleProcesses() {128async function killStaleProcesses() {
128 let killed = 0;129 let killed = 0;
@@ -198,6 +199,7 @@ spawnService("web", ["bun", "run", "dev"], { · 69 unmodified lines
198spawnService("assistant", ["bun", "run", "dev"], {199spawnService("assistant", ["bun", "run", "dev"], {
199 cwd: "./assistant/packages/server",200 cwd: "./assistant/packages/server",
200 env: {201 env: {
202 ASSISTANT_PORT: String(ASSISTANT_PORT),
201 EXECUTOR_URL: urls.executorUrl,203 EXECUTOR_URL: urls.executorUrl,
202 CONVEX_URL: urls.convexUrl,204 CONVEX_URL: urls.convexUrl,
203 EXECUTOR_ANON_SESSION_ID: Bun.env.EXECUTOR_ANON_SESSION_ID ?? "assistant-discord-dev",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 spawnService("bot", ["bun", "run", "dev"], {211 spawnService("bot", ["bun", "run", "dev"], {
210 cwd: "./assistant/packages/bot",212 cwd: "./assistant/packages/bot",
211 env: {213 env: {
214 ASSISTANT_SERVER_URL: Bun.env.ASSISTANT_SERVER_URL ?? `http://localhost:${ASSISTANT_PORT}`,
212 CONVEX_URL: urls.convexUrl,215 CONVEX_URL: urls.convexUrl,
213 },216 },
214 });217 });
executor/packages/convex/_generated/api.d.ts+0−33
@@ -970,19 +970,6 @@ export declare const internal: { · 969 unmodified lines
970 any970 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 migrations: {973 migrations: {
987 backfillDtsStorageIds: FunctionReference<974 backfillDtsStorageIds: FunctionReference<
988 "mutation",975 "mutation",
@@ -1112,26 +1099,6 @@ export declare const internal: { · 123 unmodified lines
1112 any1099 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 workspaceAuthInternal: {1102 workspaceAuthInternal: {
1136 getWorkspaceAccessForRequest: FunctionReference<1103 getWorkspaceAccessForRequest: FunctionReference<
1137 "query",1104 "query",
executor/packages/convex/accounts.ts+0−9
@@ -72,15 +72,6 @@ async function deleteWorkspaceData( · 71 unmodified lines
72 await ctx.db.delete(cacheEntry._id);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 const workspaceMembers = await ctx.db75 const workspaceMembers = await ctx.db
85 .query("workspaceMembers")76 .query("workspaceMembers")
86 .withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceId))77 .withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceId))
executor/packages/convex/executor-mcp.e2e.test.ts+0−1
@@ -17,7 +17,6 @@ function setup() { · 16 unmodified lines
17 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),17 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),
18 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),18 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),
19 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),19 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),
20 "./mcpNode.ts": () => import("./mcpNode"),
21 "./_generated/api.js": () => import("./_generated/api.js"),20 "./_generated/api.js": () => import("./_generated/api.js"),
22 });21 });
23}22}
executor/packages/convex/http-anonymous-oauth.e2e.test.ts+0−1
@@ -14,7 +14,6 @@ function setup() { · 13 unmodified lines
14 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),14 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),
15 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),15 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),
16 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),16 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),
17 "./mcpNode.ts": () => import("./mcpNode"),
18 "./anonymousOauth.ts": () => import("./anonymousOauth"),17 "./anonymousOauth.ts": () => import("./anonymousOauth"),
19 "./_generated/api.js": () => import("./_generated/api.js"),18 "./_generated/api.js": () => import("./_generated/api.js"),
20 });19 });
executor/packages/convex/http/mcp_service.ts+3−27
@@ -19,10 +19,11 @@ export function createMcpExecutorService(ctx: ActionCtx) { · 18 unmodified lines
19 actorId: string;19 actorId: string;
20 clientId?: string;20 clientId?: string;
21 }) => {21 }) => {
22 return (await ctx.runMutation(internal.executor.createTaskInternal, {22 const taskInput = {
23 ...input,23 ...input,
24 scheduleAfterCreate: false,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 runTaskNow: async (taskId: string) => {28 runTaskNow: async (taskId: string) => {
28 return (await ctx.runAction(internal.executorNode.runTask, { taskId })) as null;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 return (await ctx.runAction(internal.executorNode.listToolsInternal, { ...toolContext })) as ToolDescriptor[];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 listPendingApprovals: async (workspaceId: Id<"workspaces">) => {50 listPendingApprovals: async (workspaceId: Id<"workspaces">) => {
75 return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId })) as PendingApprovalRecord[];51 return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId })) as PendingApprovalRecord[];
76 },52 },
executor/packages/convex/mcp-run-code-oom.e2e.test.ts+2−3
@@ -17,7 +17,6 @@ function setup() { · 16 unmodified lines
17 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),17 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),
18 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),18 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),
19 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),19 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),
20 "./mcpNode.ts": () => import("./mcpNode"),
21 "./_generated/api.js": () => import("./_generated/api.js"),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
46test("MCP run_code reproduces Convex OOM failure surface", async () => {45test("MCP run_code no longer hits typecheck OOM path", async () => {
47 const t = setup();46 const t = setup();
48 const session = await t.mutation(internal.database.bootstrapAnonymousSession, {});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 const serialized = JSON.stringify(result);62 const serialized = JSON.stringify(result);
64 expect(serialized.includes(OOM_MESSAGE)).toBe(true);63 expect(serialized.includes(OOM_MESSAGE)).toBe(false);
65 } finally {64 } finally {
66 await transport.close().catch(() => {});65 await transport.close().catch(() => {});
67 await client.close().catch(() => {});66 await client.close().catch(() => {});
executor/packages/convex/mcpNode.ts+0−279
@@ -1,279 +0,0 @@
1"use node";
2
3import { createHash } from "node:crypto";
4import { createRequire } from "node:module";
5import { v } from "convex/values";
6import { internal } from "./_generated/api";
7import { internalAction } from "./_generated/server";
8import { loadSourceDtsByUrl } from "../core/src/dts-loader";
9import {
10 analyzeToolReferences,
11 generateToolDeclarations,
12 sliceOpenApiOperationsDts,
13 typecheckCode,
14} from "../core/src/typechecker";
15import type { ToolDescriptor } from "../core/src/types";
16import { sourceSignature } from "./executor_node/tool_source_loading";
17
18if (typeof (globalThis as { require?: unknown }).require !== "function") {
19 (globalThis as { require?: unknown }).require = createRequire(import.meta.url);
20}
21
22const DECLARATION_CACHE_VERSION = "v1";
23const DECLARATION_CACHE_TTL_MS = 24 * 60 * 60_000;
24const DECLARATION_CACHE_MAX_ENTRIES_PER_WORKSPACE = 96;
25
26function hashString(input: string): string {
27 return createHash("sha256").update(input).digest("hex");
28}
29
30function 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
38function 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
55function 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
75function 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
95function shouldUseSelectiveOpenApiDts(analysis: ReturnType<typeof analyzeToolReferences>): boolean {
96 return !analysis.hasDynamicToolAccess && !analysis.hasNonCallToolAccess;
97}
98
99function 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
128function 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
144export 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});
executor/packages/convex/schema.ts+0−10
@@ -313,16 +313,6 @@ export default defineSchema({ · 312 unmodified lines
313 })313 })
314 .index("by_workspace", ["workspaceId"]),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 anonymousSessions: defineTable({316 anonymousSessions: defineTable({
327 sessionId: v.string(), // domain ID: anon_session_<uuid> or mcp_<uuid>317 sessionId: v.string(), // domain ID: anon_session_<uuid> or mcp_<uuid>
328 workspaceId: v.id("workspaces"),318 workspaceId: v.id("workspaces"),
executor/packages/convex/typecheckDeclarationCache.test.ts+0−188
@@ -1,188 +0,0 @@
1import { describe, expect, test } from "bun:test";
2import { convexTest } from "convex-test";
3import { internal } from "./_generated/api";
4import schema from "./schema";
5
6function setup() {
7 return convexTest(schema, {
8 "./typecheckDeclarationCache.ts": () => import("./typecheckDeclarationCache"),
9 "./_generated/api.js": () => import("./_generated/api.js"),
10 });
11}
12
13describe("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});
executor/packages/convex/typecheckDeclarationCache.ts+0−76
@@ -1,76 +0,0 @@
1import { v } from "convex/values";
2import { internalMutation, internalQuery } from "./_generated/server";
3
4export 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
31export 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});
executor/packages/core/src/mcp-server.test.ts+6−48
@@ -10,11 +10,6 @@ class FakeMcpService { · 9 unmodified lines
10 private readonly tasks = new Map<string, TaskRecord>();10 private readonly tasks = new Map<string, TaskRecord>();
11 private readonly sessions = new Map<string, AnonymousContext>();11 private readonly sessions = new Map<string, AnonymousContext>();
12 private readonly listeners = new Map<string, Set<(event: LiveTaskEvent) => void>>();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 async createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }> {14 async createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }> {
20 const id = `task_${crypto.randomUUID()}`;15 const id = `task_${crypto.randomUUID()}`;
@@ -99,18 +94,6 @@ class FakeMcpService { · 78 unmodified lines
99 { path: "utils.get_time", description: "Get the current time", approval: "auto" },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
116async function withMcpClient<T>(99async 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
222test("run_code typecheck uses OpenAPI .d.ts from typecheck context", async () => {205test("run_code MCP tool does not gate on TypeScript errors", async () => {
223 const service = new FakeMcpService();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 await withMcpClient(service, async (client) => {208 await withMcpClient(service, async (client) => {
249 const result = (await client.callTool({209 const result = (await client.callTool({
250 name: "run_code",210 name: "run_code",
251 arguments: {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 })) as {214 })) as {
255 isError?: boolean;215 isError?: boolean;
@@ -257,11 +217,9 @@ test("run_code typecheck uses OpenAPI .d.ts from typecheck context", async () => · 1 unmodified line
257 structuredContent?: Record<string, unknown>;217 structuredContent?: Record<string, unknown>;
258 };218 };
259 219
260 expect(result.isError).toBe(true);220 expect(result.isError).toBeUndefined();
261 const typecheckErrors = Array.isArray(result.structuredContent?.typecheckErrors)221 const text = result.content.find((part) => part.type === "text");
262 ? (result.structuredContent!.typecheckErrors as string[])222 expect(text?.text).toContain("status: completed");
263 : [];223 expect(result.structuredContent?.typecheckErrors).toBeUndefined();
264 expect(typecheckErrors.length).toBeGreaterThan(0);
265 expect(typecheckErrors.some((error) => error.includes("owner") || error.includes("number"))).toBe(true);
266 });224 });
267});225});
executor/packages/core/src/mcp-server.ts+3−100
@@ -2,8 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; · 1 unmodified line
2import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";2import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3import { Result } from "better-result";3import { Result } from "better-result";
4import { z } from "zod";4import { z } from "zod";
5import { loadSourceDtsByUrl } from "./dts-loader";5import { generateToolInventory } from "./typechecker";
6import { generateToolDeclarations, generateToolInventory, typecheckCode } from "./typechecker";
7import type { LiveTaskEvent } from "./events";6import type { LiveTaskEvent } from "./events";
8import type {7import type {
9 AnonymousContext,8 AnonymousContext,
@@ -29,15 +28,6 @@ interface McpExecutorService { · 19 unmodified lines
29 subscribe(taskId: string, workspaceId: Id<"workspaces">, listener: (event: LiveTaskEvent) => void): () => void;28 subscribe(taskId: string, workspaceId: Id<"workspaces">, listener: (event: LiveTaskEvent) => void): () => void;
30 bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>;29 bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>;
31 listTools(context?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }): Promise<ToolDescriptor[]>;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 listPendingApprovals?(workspaceId: Id<"workspaces">): Promise<PendingApprovalRecord[]>;31 listPendingApprovals?(workspaceId: Id<"workspaces">): Promise<PendingApprovalRecord[]>;
42 resolveApproval?(input: {32 resolveApproval?(input: {
43 workspaceId: Id<"workspaces">;33 workspaceId: Id<"workspaces">;
@@ -238,7 +228,7 @@ function waitForTerminalTask( · 194 unmodified lines
238 228
239function buildRunCodeDescription(tools?: ToolDescriptor[]): string {229function buildRunCodeDescription(tools?: ToolDescriptor[]): string {
240 const base =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 const toolList = tools ?? [];232 const toolList = tools ?? [];
243 const topLevelKeys = listTopLevelToolKeys(toolList);233 const topLevelKeys = listTopLevelToolKeys(toolList);
244 const rootKeysNote = topLevelKeys.length > 0234 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 const created = await service.createTask({374 const created = await service.createTask({
462 code: input.code,375 code: input.code,
463 timeoutMs: requestedTimeoutMs,376 timeoutMs: requestedTimeoutMs,
@@ -589,20 +502,10 @@ async function createMcpServer( · 125 unmodified lines
589 );502 );
590 const onApprovalPrompt = createMcpApprovalPrompt(mcp);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 mcp.registerTool(505 mcp.registerTool(
603 "run_code",506 "run_code",
604 {507 {
605 description: buildRunCodeDescription(tools),508 description: buildRunCodeDescription(),
606 inputSchema: context ? BOUND_INPUT : FULL_INPUT,509 inputSchema: context ? BOUND_INPUT : FULL_INPUT,
607 },510 },
608 createRunCodeTool(service, context, onApprovalPrompt),511 createRunCodeTool(service, context, onApprovalPrompt),
kill-all.ts+3−2
@@ -2,7 +2,7 @@ · 1 unmodified line
2 * Kills all processes started by `bun dev`.2 * Kills all processes started by `bun dev`.
3 *3 *
4 * 1. Reads PIDs from .dev.pids and kills process trees4 * 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 * Usage: bun run kill:all7 * Usage: bun run kill:all
8 */8 */
@@ -11,7 +11,8 @@ import { join } from "node:path"; · 2 unmodified lines
11import { unlinkSync } from "node:fs";11import { unlinkSync } from "node:fs";
12 12
13const PID_FILE = join(import.meta.dir, ".dev.pids");13const PID_FILE = join(import.meta.dir, ".dev.pids");
14const DEV_PORTS = [3000, 3002];14const ASSISTANT_PORT = Number(Bun.env.ASSISTANT_PORT ?? 3002);
15const DEV_PORTS = [ASSISTANT_PORT];
15 16
16let killed = 0;17let killed = 0;
17let skipped = 0;18let skipped = 0;