History

Clean up Elysia routes: deduplicate MCP handler, remove redundant validation

- Extract MCP handler to shared function instead of copy-pasting for 3 HTTP methods - Remove redundant manual workspaceId/body validation (Elysia schema already validates) - Simplify getBaseToolCount() to use baseTools.size - Fix .gitignore wildcard patterns (_.log -> *.log)

Rhys Sullivan committed 9 months ago

Commit 85da31c1 parent 3a401ab3 files · +29 −80Browse files

Unified · Split

3 files
  1. .gitignore +2 −2
  2. executor/apps/server/src/database.ts +10 −0
  3. executor/apps/server/src/index.ts +17 −78
.gitignore+2−2
@@ -12,8 +12,8 @@ coverage · 11 unmodified lines
12 12
13# logs13# logs
14logs14logs
15_.log15*.log
16report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json16report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 17
18# dotenv environment variable files18# dotenv environment variable files
19.env19.env
executor/apps/server/src/database.ts+10−0
@@ -56,6 +56,16 @@ export class ExecutorDatabase { · 55 unmodified lines
56 this.client = new ConvexHttpClient(convexUrl);56 this.client = new ConvexHttpClient(convexUrl);
57 }57 }
58 58
59 // The `as never` casts below are intentional. ConvexHttpClient.mutation() and
60 // .query() expect the exact generated function reference type and its
61 // corresponding args type. Because we call them dynamically with string
62 // function names (e.g. "database:createTask") and generic arg objects,
63 // TypeScript cannot verify the relationship between name and args at the
64 // call site. Properly typing this would require a discriminated-union
65 // overload map for every Convex function, which adds significant
66 // complexity for little safety gain — the real type safety lives in the
67 // public method signatures of this class and the Convex handler
68 // definitions themselves.
59 private async mutation<TArgs extends Record<string, unknown>, TResult>(69 private async mutation<TArgs extends Record<string, unknown>, TResult>(
60 name: MutationName,70 name: MutationName,
61 args: TArgs,71 args: TArgs,
executor/apps/server/src/index.ts+17−78
@@ -159,6 +159,14 @@ function parseMcpContext(url: URL): McpWorkspaceContext | undefined { · 158 unmodified lines
159 return { workspaceId, actorId, clientId };159 return { workspaceId, actorId, clientId };
160}160}
161 161
162// ── MCP handler ──
163
164const handleMcp = async ({ request }: { request: Request }) => {
165 const url = new URL(request.url);
166 const context = parseMcpContext(url);
167 return await handleMcpRequest(service, request, context);
168};
169
162// ── Elysia app ──170// ── Elysia app ──
163 171
164const app = new Elysia()172const app = new Elysia()
@@ -173,21 +181,9 @@ const app = new Elysia() · 8 unmodified lines
173 // ── MCP (raw protocol passthrough) ──181 // ── MCP (raw protocol passthrough) ──
174 // Optional query params: ?workspaceId=...&actorId=... to bind workspace context.182 // Optional query params: ?workspaceId=...&actorId=... to bind workspace context.
175 // When bound, run_code description includes sandbox tool inventory and input is simplified.183 // When bound, run_code description includes sandbox tool inventory and input is simplified.
176 .post("/mcp", async ({ request }) => {184 .post("/mcp", handleMcp)
177 const url = new URL(request.url);185 .get("/mcp", handleMcp)
178 const context = parseMcpContext(url);186 .delete("/mcp", handleMcp)
179 return await handleMcpRequest(service, request, context);
180 })
181 .get("/mcp", async ({ request }) => {
182 const url = new URL(request.url);
183 const context = parseMcpContext(url);
184 return await handleMcpRequest(service, request, context);
185 })
186 .delete("/mcp", async ({ request }) => {
187 const url = new URL(request.url);
188 const context = parseMcpContext(url);
189 return await handleMcpRequest(service, request, context);
190 })
191 187
192 // ── Health ──188 // ── Health ──
193 .get("/api/health", () => ({189 .get("/api/health", () => ({
@@ -208,11 +204,7 @@ const app = new Elysia() · 14 unmodified lines
208 .get("/api/runtime-targets", () => service.listRuntimes())204 .get("/api/runtime-targets", () => service.listRuntimes())
209 205
210 // ── Tools ──206 // ── Tools ──
211 .get("/api/tools", async ({ query, set }) => {207 .get("/api/tools", async ({ query }) => {
212 if (!query.workspaceId) {
213 set.status = 400;
214 return { error: "workspaceId is required" };
215 }
216 return await service.listTools({208 return await service.listTools({
217 workspaceId: query.workspaceId,209 workspaceId: query.workspaceId,
218 actorId: query.actorId,210 actorId: query.actorId,
@@ -227,11 +219,7 @@ const app = new Elysia() · 8 unmodified lines
227 })219 })
228 220
229 // ── Tool Sources ──221 // ── Tool Sources ──
230 .get("/api/tool-sources", async ({ query, set }) => {222 .get("/api/tool-sources", async ({ query }) => {
231 if (!query.workspaceId) {
232 set.status = 400;
233 return { error: "workspaceId is required" };
234 }
235 return await service.listToolSources(query.workspaceId);223 return await service.listToolSources(query.workspaceId);
236 }, {224 }, {
237 query: t.Object({225 query: t.Object({
@@ -240,10 +228,6 @@ const app = new Elysia() · 2 unmodified lines
240 })228 })
241 229
242 .post("/api/tool-sources", async ({ body, set }) => {230 .post("/api/tool-sources", async ({ body, set }) => {
243 if (!body.workspaceId || !body.name || !body.type || !body.config) {
244 set.status = 400;
245 return { error: "workspaceId, name, type, and config are required" };
246 }
247 try {231 try {
248 return await service.upsertToolSource({232 return await service.upsertToolSource({
249 id: body.id,233 id: body.id,
@@ -269,10 +253,6 @@ const app = new Elysia() · 19 unmodified lines
269 })253 })
270 254
271 .delete("/api/tool-sources/:sourceId", async ({ params, query, set }) => {255 .delete("/api/tool-sources/:sourceId", async ({ params, query, set }) => {
272 if (!query.workspaceId) {
273 set.status = 400;
274 return { error: "workspaceId is required" };
275 }
276 const deleted = await service.deleteToolSource(query.workspaceId, params.sourceId);256 const deleted = await service.deleteToolSource(query.workspaceId, params.sourceId);
277 if (!deleted) {257 if (!deleted) {
278 set.status = 404;258 set.status = 404;
@@ -285,11 +265,7 @@ const app = new Elysia() · 6 unmodified lines
285 })265 })
286 266
287 // ── Tasks ──267 // ── Tasks ──
288 .get("/api/tasks", async ({ query, set }) => {268 .get("/api/tasks", async ({ query }) => {
289 if (!query.workspaceId) {
290 set.status = 400;
291 return { error: "workspaceId is required" };
292 }
293 return await service.listTasks(query.workspaceId);269 return await service.listTasks(query.workspaceId);
294 }, {270 }, {
295 query: t.Object({ workspaceId: t.String() }),271 query: t.Object({ workspaceId: t.String() }),
@@ -316,10 +292,6 @@ const app = new Elysia() · 20 unmodified lines
316 })292 })
317 293
318 .get("/api/tasks/:taskId", async ({ params, query, set }) => {294 .get("/api/tasks/:taskId", async ({ params, query, set }) => {
319 if (!query.workspaceId) {
320 set.status = 400;
321 return { error: "workspaceId is required" };
322 }
323 const task = await service.getTask(params.taskId, query.workspaceId);295 const task = await service.getTask(params.taskId, query.workspaceId);
324 if (!task) {296 if (!task) {
325 set.status = 404;297 set.status = 404;
@@ -332,10 +304,6 @@ const app = new Elysia() · 6 unmodified lines
332 })304 })
333 305
334 .get("/api/tasks/:taskId/events", async ({ params, query, set }) => {306 .get("/api/tasks/:taskId/events", async ({ params, query, set }) => {
335 if (!query.workspaceId) {
336 set.status = 400;
337 return { error: "workspaceId is required" };
338 }
339 const task = await service.getTask(params.taskId, query.workspaceId);307 const task = await service.getTask(params.taskId, query.workspaceId);
340 if (!task) {308 if (!task) {
341 set.status = 404;309 set.status = 404;
@@ -349,10 +317,6 @@ const app = new Elysia() · 7 unmodified lines
349 317
350 // ── Approvals ──318 // ── Approvals ──
351 .get("/api/approvals", async ({ query, set }) => {319 .get("/api/approvals", async ({ query, set }) => {
352 if (!query.workspaceId) {
353 set.status = 400;
354 return { error: "workspaceId is required" };
355 }
356 const status = query.status as ApprovalStatus | undefined;320 const status = query.status as ApprovalStatus | undefined;
357 321
358 if (status === "pending") {322 if (status === "pending") {
@@ -373,11 +337,6 @@ const app = new Elysia() · 14 unmodified lines
373 })337 })
374 338
375 .post("/api/approvals/:approvalId", async ({ params, body, set }) => {339 .post("/api/approvals/:approvalId", async ({ params, body, set }) => {
376 if (!body.workspaceId || (body.decision !== "approved" && body.decision !== "denied")) {
377 set.status = 400;
378 return { error: "workspaceId and decision are required" };
379 }
380
381 const resolved = await service.resolveApproval(340 const resolved = await service.resolveApproval(
382 body.workspaceId,341 body.workspaceId,
383 params.approvalId,342 params.approvalId,
@@ -402,21 +361,13 @@ const app = new Elysia() · 18 unmodified lines
402 })361 })
403 362
404 // ── Policies ──363 // ── Policies ──
405 .get("/api/policies", async ({ query, set }) => {364 .get("/api/policies", async ({ query }) => {
406 if (!query.workspaceId) {
407 set.status = 400;
408 return { error: "workspaceId is required" };
409 }
410 return await service.listAccessPolicies(query.workspaceId);365 return await service.listAccessPolicies(query.workspaceId);
411 }, {366 }, {
412 query: t.Object({ workspaceId: t.String() }),367 query: t.Object({ workspaceId: t.String() }),
413 })368 })
414 369
415 .post("/api/policies", async ({ body, set }) => {370 .post("/api/policies", async ({ body }) => {
416 if (!body.workspaceId || !body.toolPathPattern || !body.decision) {
417 set.status = 400;
418 return { error: "workspaceId, toolPathPattern, and decision are required" };
419 }
420 return await service.upsertAccessPolicy({371 return await service.upsertAccessPolicy({
421 id: body.id,372 id: body.id,
422 workspaceId: body.workspaceId,373 workspaceId: body.workspaceId,
@@ -439,21 +390,13 @@ const app = new Elysia() · 16 unmodified lines
439 })390 })
440 391
441 // ── Credentials ──392 // ── Credentials ──
442 .get("/api/credentials", async ({ query, set }) => {393 .get("/api/credentials", async ({ query }) => {
443 if (!query.workspaceId) {
444 set.status = 400;
445 return { error: "workspaceId is required" };
446 }
447 return await service.listCredentials(query.workspaceId);394 return await service.listCredentials(query.workspaceId);
448 }, {395 }, {
449 query: t.Object({ workspaceId: t.String() }),396 query: t.Object({ workspaceId: t.String() }),
450 })397 })
451 398
452 .post("/api/credentials", async ({ body, set }) => {399 .post("/api/credentials", async ({ body, set }) => {
453 if (!body.workspaceId || !body.sourceKey || !body.scope || !body.secretJson) {
454 set.status = 400;
455 return { error: "workspaceId, sourceKey, scope, and secretJson are required" };
456 }
457 if (body.scope === "actor" && (!body.actorId || body.actorId.trim().length === 0)) {400 if (body.scope === "actor" && (!body.actorId || body.actorId.trim().length === 0)) {
458 set.status = 400;401 set.status = 400;
459 return { error: "actorId is required for actor-scoped credential" };402 return { error: "actorId is required for actor-scoped credential" };
@@ -493,10 +436,6 @@ const app = new Elysia() · 33 unmodified lines
493 set.status = 401;436 set.status = 401;
494 return { error: "Unauthorized internal call" };437 return { error: "Unauthorized internal call" };
495 }438 }
496 if (!body.callId || !body.toolPath) {
497 set.status = 400;
498 return { error: "callId and toolPath are required" };
499 }
500 439
501 const call: ToolCallRequest = {440 const call: ToolCallRequest = {
502 runId: params.runId,441 runId: params.runId,