- Bump version in package.json - Add tests for completed MCP return values in server.test.ts - Introduce new source templates for Neon MCP and Neon API in source-templates.ts - Refactor OpenAPI response handling in openapi-tools.ts and related tests - Improve result preview formatting in executor-mcp
Showing the changes to packages/codemode-openapi/src (2 of 8 files). Show all
Unified · Split
| @@ -305,13 +305,8 @@ describe("openapi-tools", () => { · 304 unmodified lines | ||
| 305 | 305 | ); |
| 306 | 306 | |
| 307 | 307 | expect(result).toEqual({ |
| 308 | − ok: true, | |
| 309 | − status: 200, | |
| 310 | − headers: expect.any(Object), | |
| 311 | − body: { | |
| 312 | − full_name: "octocat/hello-world", | |
| 313 | − include: null, | |
| 314 | − }, | |
| 308 | + full_name: "octocat/hello-world", | |
| 309 | + include: null, | |
| 315 | 310 | }); |
| 316 | 311 | expect(server.requests).toHaveLength(1); |
| 317 | 312 | expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world"); |
| @@ -340,13 +335,8 @@ describe("openapi-tools", () => { · 22 unmodified lines | ||
| 340 | 335 | ); |
| 341 | 336 | |
| 342 | 337 | expect(getResult).toEqual({ |
| 343 | − ok: true, | |
| 344 | − status: 200, | |
| 345 | − headers: expect.any(Object), | |
| 346 | − body: { | |
| 347 | − full_name: "octocat/hello-world", | |
| 348 | − include: "all", | |
| 349 | − }, | |
| 338 | + full_name: "octocat/hello-world", | |
| 339 | + include: "all", | |
| 350 | 340 | }); |
| 351 | 341 | |
| 352 | 342 | const postResult = yield* Effect.promise(() => |
| @@ -358,15 +348,10 @@ describe("openapi-tools", () => { · 5 unmodified lines | ||
| 358 | 348 | ); |
| 359 | 349 | |
| 360 | 350 | expect(postResult).toEqual({ |
| 361 | − ok: true, | |
| 362 | − status: 201, | |
| 363 | − headers: expect.any(Object), | |
| 364 | − body: { | |
| 365 | − created: true, | |
| 366 | − owner: "octocat", | |
| 367 | − repo: "hello-world", | |
| 368 | − body: { title: "Bug report" }, | |
| 369 | − }, | |
| 351 | + created: true, | |
| 352 | + owner: "octocat", | |
| 353 | + repo: "hello-world", | |
| 354 | + body: { title: "Bug report" }, | |
| 370 | 355 | }); |
| 371 | 356 | |
| 372 | 357 | const missingBody = yield* Effect.either( |
| @@ -398,6 +383,50 @@ describe("openapi-tools", () => { · 25 unmodified lines | ||
| 398 | 383 | }), |
| 399 | 384 | ); |
| 400 | 385 | |
| 386 | + it.effect("throws on non-2xx OpenAPI responses", () => | |
| 387 | + Effect.gen(function* () { | |
| 388 | + const manifest = yield* extractOpenApiManifest("generated", generatedOpenApiSpec); | |
| 389 | + const httpClientLayer = Layer.succeed( | |
| 390 | + HttpClient.HttpClient, | |
| 391 | + HttpClient.make((request) => | |
| 392 | + Effect.sync(() => | |
| 393 | + HttpClientResponse.fromWeb( | |
| 394 | + request, | |
| 395 | + new Response( | |
| 396 | + JSON.stringify({ error: "missing" }), | |
| 397 | + { | |
| 398 | + status: 404, | |
| 399 | + headers: { | |
| 400 | + "content-type": "application/json", | |
| 401 | + }, | |
| 402 | + }, | |
| 403 | + ), | |
| 404 | + ), | |
| 405 | + ), | |
| 406 | + ), | |
| 407 | + ); | |
| 408 | + const tools = createOpenApiToolsFromManifest({ | |
| 409 | + manifest, | |
| 410 | + baseUrl: "https://example.com", | |
| 411 | + namespace: "source.generated", | |
| 412 | + httpClientLayer, | |
| 413 | + }); | |
| 414 | + const getRepo = resolveToolExecutor(tools, "source.generated.repos.getRepo"); | |
| 415 | + const failure = yield* Effect.either( | |
| 416 | + Effect.tryPromise({ | |
| 417 | + try: () => getRepo({ owner: "octocat", repo: "hello-world" }), | |
| 418 | + catch: (error: unknown) => | |
| 419 | + error instanceof Error ? error : new Error(String(error)), | |
| 420 | + }), | |
| 421 | + ); | |
| 422 | + | |
| 423 | + assertTrue(failure._tag === "Left"); | |
| 424 | + if (failure._tag === "Left" && failure.left instanceof Error) { | |
| 425 | + assertInclude(failure.left.message, "HTTP 404"); | |
| 426 | + } | |
| 427 | + }), | |
| 428 | + ); | |
| 429 | + | |
| 401 | 430 | it.effect("preserves base-path URLs when invoking tools", () => |
| 402 | 431 | Effect.gen(function* () { |
| 403 | 432 | const manifest = yield* extractOpenApiManifest("generated", generatedOpenApiSpec); |
| @@ -438,13 +467,8 @@ describe("openapi-tools", () => { · 34 unmodified lines | ||
| 438 | 467 | ); |
| 439 | 468 | |
| 440 | 469 | expect(result).toEqual({ |
| 441 | − ok: true, | |
| 442 | − status: 200, | |
| 443 | − headers: expect.any(Object), | |
| 444 | − body: { | |
| 445 | − full_name: "octocat/hello-world", | |
| 446 | − include: null, | |
| 447 | − }, | |
| 470 | + full_name: "octocat/hello-world", | |
| 471 | + include: null, | |
| 448 | 472 | }); |
| 449 | 473 | |
| 450 | 474 | expect(capturedUrl).toBe("https://example.com/api/v3/repos/octocat/hello-world"); |
| @@ -488,13 +512,8 @@ describe("openapi-tools", () => { · 37 unmodified lines | ||
| 488 | 512 | ); |
| 489 | 513 | |
| 490 | 514 | expect(result).toEqual({ |
| 491 | − ok: true, | |
| 492 | − status: 200, | |
| 493 | − headers: expect.any(Object), | |
| 494 | − body: { | |
| 495 | − full_name: "octocat/hello-world", | |
| 496 | − include: null, | |
| 497 | − }, | |
| 515 | + full_name: "octocat/hello-world", | |
| 516 | + include: null, | |
| 498 | 517 | }); |
| 499 | 518 | expect(server.requests).toHaveLength(1); |
| 500 | 519 | expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world"); |
| @@ -40,44 +40,6 @@ export class OpenApiToolInvocationError extends Data.TaggedError( · 39 unmodified lines | ||
| 40 | 40 | details: string | null; |
| 41 | 41 | }> {} |
| 42 | 42 | |
| 43 | −const BLOCKED_RESPONSE_HEADER_NAMES = new Set([ | |
| 44 | − "authorization", | |
| 45 | − "authentication-info", | |
| 46 | − "cookie", | |
| 47 | − "proxy-authenticate", | |
| 48 | − "proxy-authorization", | |
| 49 | − "set-cookie", | |
| 50 | − "set-cookie2", | |
| 51 | − "www-authenticate", | |
| 52 | − "x-api-key", | |
| 53 | − "x-auth-token", | |
| 54 | − "x-csrf-token", | |
| 55 | −]); | |
| 56 | − | |
| 57 | −const NOISY_RESPONSE_HEADER_NAMES = new Set([ | |
| 58 | − "alt-svc", | |
| 59 | − "cf-ray", | |
| 60 | − "server", | |
| 61 | − "traceparent", | |
| 62 | − "tracestate", | |
| 63 | − "via", | |
| 64 | − "x-cache", | |
| 65 | − "x-cache-hits", | |
| 66 | − "x-powered-by", | |
| 67 | − "x-request-id", | |
| 68 | − "x-runtime", | |
| 69 | − "x-served-by", | |
| 70 | − "x-trace-id", | |
| 71 | −]); | |
| 72 | − | |
| 73 | −const NOISY_RESPONSE_HEADER_PREFIXES = [ | |
| 74 | − "cf-", | |
| 75 | − "trace-", | |
| 76 | − "x-amz-cf-", | |
| 77 | − "x-b3-", | |
| 78 | − "x-cloud-trace-", | |
| 79 | −]; | |
| 80 | − | |
| 81 | 43 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| 82 | 44 | typeof value === "object" && value !== null && !Array.isArray(value); |
| 83 | 45 | |
| @@ -244,27 +206,7 @@ const normalizeHttpUrl = (value: string): string => { · 160 unmodified lines | ||
| 244 | 206 | } |
| 245 | 207 | }; |
| 246 | 208 | |
| 247 | −const shouldDropResponseHeader = (headerName: string): boolean => | |
| 248 | − BLOCKED_RESPONSE_HEADER_NAMES.has(headerName) | |
| 249 | − || NOISY_RESPONSE_HEADER_NAMES.has(headerName) | |
| 250 | − || NOISY_RESPONSE_HEADER_PREFIXES.some((prefix) => headerName.startsWith(prefix)); | |
| 251 | − | |
| 252 | −const sanitizeResponseHeaders = ( | |
| 253 | − headers: Readonly<Record<string, string>>, | |
| 254 | −): Record<string, string> => { | |
| 255 | − const sanitized: Record<string, string> = {}; | |
| 256 | − | |
| 257 | − for (const [rawName, rawValue] of Object.entries(headers)) { | |
| 258 | − const name = rawName.trim().toLowerCase(); | |
| 259 | − if (name.length === 0 || shouldDropResponseHeader(name)) { | |
| 260 | − continue; | |
| 261 | − } | |
| 262 | 209 | |
| 263 | − sanitized[name] = rawValue.length > 4000 ? `${rawValue.slice(0, 4000)}...` : rawValue; | |
| 264 | − } | |
| 265 | − | |
| 266 | − return sanitized; | |
| 267 | −}; | |
| 268 | 210 | |
| 269 | 211 | const decodeHttpClientResponseBody = ( |
| 270 | 212 | response: Awaited<ReturnType<HttpClient.HttpClient["execute"]>> extends Effect.Effect< |
| @@ -292,6 +234,28 @@ const decodeHttpClientResponseBody = ( · 21 unmodified lines | ||
| 292 | 234 | ); |
| 293 | 235 | }; |
| 294 | 236 | |
| 237 | +const summarizeHttpResponseBody = (body: unknown): string | null => { | |
| 238 | + if (body === null || body === undefined) { | |
| 239 | + return null; | |
| 240 | + } | |
| 241 | + | |
| 242 | + if (typeof body === "string") { | |
| 243 | + const trimmed = body.trim(); | |
| 244 | + if (trimmed.length === 0) { | |
| 245 | + return null; | |
| 246 | + } | |
| 247 | + | |
| 248 | + return trimmed.length > 500 ? `${trimmed.slice(0, 500)}...` : trimmed; | |
| 249 | + } | |
| 250 | + | |
| 251 | + try { | |
| 252 | + const serialized = JSON.stringify(body); | |
| 253 | + return serialized.length > 500 ? `${serialized.slice(0, 500)}...` : serialized; | |
| 254 | + } catch { | |
| 255 | + return String(body); | |
| 256 | + } | |
| 257 | +}; | |
| 258 | + | |
| 295 | 259 | const inputSchemaFromTypingJson = (inputSchemaJson: string | undefined) => { |
| 296 | 260 | if (!inputSchemaJson) { |
| 297 | 261 | return unknownInputSchema; |
| @@ -491,12 +455,15 @@ export const createOpenApiToolsFromManifest = ( · 193 unmodified lines | ||
| 491 | 455 | ); |
| 492 | 456 | const body = yield* decodeHttpClientResponseBody(response); |
| 493 | 457 | |
| 494 | − return { | |
| 495 | − ok: response.status >= 200 && response.status < 300, | |
| 496 | − status: response.status, | |
| 497 | − headers: sanitizeResponseHeaders(response.headers), | |
| 498 | − body, | |
| 499 | − }; | |
| 458 | + if (response.status < 200 || response.status >= 300) { | |
| 459 | + throw new OpenApiToolInvocationError({ | |
| 460 | + operation: "http_response", | |
| 461 | + message: `OpenAPI request failed with HTTP ${response.status}`, | |
| 462 | + details: summarizeHttpResponseBody(body), | |
| 463 | + }); | |
| 464 | + } | |
| 465 | + | |
| 466 | + return body; | |
| 500 | 467 | }).pipe( |
| 501 | 468 | Effect.provide(httpClientLayer), |
| 502 | 469 | ), |