- 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 | expect(result).toEqual({ | 307 | expect(result).toEqual({ |
| 308 | ok: true, | 308 | full_name: "octocat/hello-world", |
| 309 | status: 200, | 309 | include: null, |
| 310 | headers: expect.any(Object), | ||
| 311 | body: { | ||
| 312 | full_name: "octocat/hello-world", | ||
| 313 | include: null, | ||
| 314 | }, | ||
| 315 | }); | 310 | }); |
| 316 | expect(server.requests).toHaveLength(1); | 311 | expect(server.requests).toHaveLength(1); |
| 317 | expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world"); | 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 | expect(getResult).toEqual({ | 337 | expect(getResult).toEqual({ |
| 343 | ok: true, | 338 | full_name: "octocat/hello-world", |
| 344 | status: 200, | 339 | include: "all", |
| 345 | headers: expect.any(Object), | ||
| 346 | body: { | ||
| 347 | full_name: "octocat/hello-world", | ||
| 348 | include: "all", | ||
| 349 | }, | ||
| 350 | }); | 340 | }); |
| 351 | 341 | ||
| 352 | const postResult = yield* Effect.promise(() => | 342 | const postResult = yield* Effect.promise(() => |
| @@ -358,15 +348,10 @@ describe("openapi-tools", () => { · 5 unmodified lines | |||
| 358 | ); | 348 | ); |
| 359 | 349 | ||
| 360 | expect(postResult).toEqual({ | 350 | expect(postResult).toEqual({ |
| 361 | ok: true, | 351 | created: true, |
| 362 | status: 201, | 352 | owner: "octocat", |
| 363 | headers: expect.any(Object), | 353 | repo: "hello-world", |
| 364 | body: { | 354 | body: { title: "Bug report" }, |
| 365 | created: true, | ||
| 366 | owner: "octocat", | ||
| 367 | repo: "hello-world", | ||
| 368 | body: { title: "Bug report" }, | ||
| 369 | }, | ||
| 370 | }); | 355 | }); |
| 371 | 356 | ||
| 372 | const missingBody = yield* Effect.either( | 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 | it.effect("preserves base-path URLs when invoking tools", () => | 430 | it.effect("preserves base-path URLs when invoking tools", () => |
| 402 | Effect.gen(function* () { | 431 | Effect.gen(function* () { |
| 403 | const manifest = yield* extractOpenApiManifest("generated", generatedOpenApiSpec); | 432 | const manifest = yield* extractOpenApiManifest("generated", generatedOpenApiSpec); |
| @@ -438,13 +467,8 @@ describe("openapi-tools", () => { · 34 unmodified lines | |||
| 438 | ); | 467 | ); |
| 439 | 468 | ||
| 440 | expect(result).toEqual({ | 469 | expect(result).toEqual({ |
| 441 | ok: true, | 470 | full_name: "octocat/hello-world", |
| 442 | status: 200, | 471 | include: null, |
| 443 | headers: expect.any(Object), | ||
| 444 | body: { | ||
| 445 | full_name: "octocat/hello-world", | ||
| 446 | include: null, | ||
| 447 | }, | ||
| 448 | }); | 472 | }); |
| 449 | 473 | ||
| 450 | expect(capturedUrl).toBe("https://example.com/api/v3/repos/octocat/hello-world"); | 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 | expect(result).toEqual({ | 514 | expect(result).toEqual({ |
| 491 | ok: true, | 515 | full_name: "octocat/hello-world", |
| 492 | status: 200, | 516 | include: null, |
| 493 | headers: expect.any(Object), | ||
| 494 | body: { | ||
| 495 | full_name: "octocat/hello-world", | ||
| 496 | include: null, | ||
| 497 | }, | ||
| 498 | }); | 517 | }); |
| 499 | expect(server.requests).toHaveLength(1); | 518 | expect(server.requests).toHaveLength(1); |
| 500 | expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world"); | 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 | details: string | null; | 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 | const isRecord = (value: unknown): value is Record<string, unknown> => | 43 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| 82 | typeof value === "object" && value !== null && !Array.isArray(value); | 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 | const decodeHttpClientResponseBody = ( | 211 | const decodeHttpClientResponseBody = ( |
| 270 | response: Awaited<ReturnType<HttpClient.HttpClient["execute"]>> extends Effect.Effect< | 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 | const inputSchemaFromTypingJson = (inputSchemaJson: string | undefined) => { | 259 | const inputSchemaFromTypingJson = (inputSchemaJson: string | undefined) => { |
| 296 | if (!inputSchemaJson) { | 260 | if (!inputSchemaJson) { |
| 297 | return unknownInputSchema; | 261 | return unknownInputSchema; |
| @@ -491,12 +455,15 @@ export const createOpenApiToolsFromManifest = ( · 193 unmodified lines | |||
| 491 | ); | 455 | ); |
| 492 | const body = yield* decodeHttpClientResponseBody(response); | 456 | const body = yield* decodeHttpClientResponseBody(response); |
| 493 | 457 | ||
| 494 | return { | 458 | if (response.status < 200 || response.status >= 300) { |
| 495 | ok: response.status >= 200 && response.status < 300, | 459 | throw new OpenApiToolInvocationError({ |
| 496 | status: response.status, | 460 | operation: "http_response", |
| 497 | headers: sanitizeResponseHeaders(response.headers), | 461 | message: `OpenAPI request failed with HTTP ${response.status}`, |
| 498 | body, | 462 | details: summarizeHttpResponseBody(body), |
| 499 | }; | 463 | }); |
| 464 | } | ||
| 465 | |||
| 466 | return body; | ||
| 500 | }).pipe( | 467 | }).pipe( |
| 501 | Effect.provide(httpClientLayer), | 468 | Effect.provide(httpClientLayer), |
| 502 | ), | 469 | ), |