History

Update executor version to 1.1.6, enhance tests, and add new source templates

- 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

Rhys Sullivan committed 8 months ago

Commit 9f5d7cf1 parent cc270618 files · +179 −125Browse files

Showing the changes to packages/codemode-openapi/src (2 of 8 files). Show all

Unified · Split

2 files
  1. packages/codemode-openapi/src/openapi-tools.test.ts +56 −37
  2. packages/codemode-openapi/src/openapi-tools.ts +31 −64
packages/codemode-openapi/src/openapi-tools.test.ts+56−37
@@ -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");
packages/codemode-openapi/src/openapi-tools.ts+31−64
@@ -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
43const 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
57const 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
73const NOISY_RESPONSE_HEADER_PREFIXES = [
74 "cf-",
75 "trace-",
76 "x-amz-cf-",
77 "x-b3-",
78 "x-cloud-trace-",
79];
80
81const isRecord = (value: unknown): value is Record<string, unknown> =>43const 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
247const 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
252const 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
269const decodeHttpClientResponseBody = (211const 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
237const 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
295const inputSchemaFromTypingJson = (inputSchemaJson: string | undefined) => {259const 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 ),

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t