- Removed unnecessary MCP configuration from opencode.json. - Introduced a function to append authentication query parameters in the sign-in route. - Enhanced the ToolsView component to display OpenAPI source quality metrics. - Updated the useWorkspaceTools hook to return source quality data. - Added type definitions for OpenApiSourceQuality to improve type fidelity across the application.
Unified · Split
| @@ -21,6 +21,26 @@ function readOptionalQueryParam(request: NextRequest, keys: string[]): string | · 20 unmodified lines | ||
| 21 | 21 | return undefined; |
| 22 | 22 | } |
| 23 | 23 | |
| 24 | +const AUTHKIT_PASSTHROUGH_QUERY_KEYS = [ | |
| 25 | + "authorization_session_id", | |
| 26 | + "redirect_uri", | |
| 27 | + "state", | |
| 28 | +]; | |
| 29 | + | |
| 30 | +function appendAuthkitPassthroughQueryParams(request: NextRequest, authorizationUrl: string): string { | |
| 31 | + const nextUrl = new URL(authorizationUrl); | |
| 32 | + | |
| 33 | + for (const key of AUTHKIT_PASSTHROUGH_QUERY_KEYS) { | |
| 34 | + const value = request.nextUrl.searchParams.get(key); | |
| 35 | + if (!value || value.trim().length === 0) { | |
| 36 | + continue; | |
| 37 | + } | |
| 38 | + nextUrl.searchParams.set(key, value.trim()); | |
| 39 | + } | |
| 40 | + | |
| 41 | + return nextUrl.toString(); | |
| 42 | +} | |
| 43 | + | |
| 24 | 44 | const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL ?? process.env.CONVEX_URL; |
| 25 | 45 | const convexClient = convexUrl ? new ConvexHttpClient(convexUrl) : null; |
| 26 | 46 | |
| @@ -65,10 +85,12 @@ export async function GET(request: NextRequest) { · 38 unmodified lines | ||
| 65 | 85 | const loginHint = readOptionalQueryParam(request, ["loginHint", "login_hint", "email"]); |
| 66 | 86 | |
| 67 | 87 | const { getSignInUrl } = await import("@workos-inc/authkit-nextjs"); |
| 68 | − const authorizationUrl = await getSignInUrl({ | |
| 88 | + const baseAuthorizationUrl = await getSignInUrl({ | |
| 69 | 89 | redirectUri, |
| 70 | 90 | organizationId, |
| 71 | 91 | loginHint, |
| 72 | 92 | }); |
| 93 | + | |
| 94 | + const authorizationUrl = appendAuthkitPassthroughQueryParams(request, baseAuthorizationUrl); | |
| 73 | 95 | return redirect(authorizationUrl); |
| 74 | 96 | } |
| @@ -48,7 +48,13 @@ import { useSession } from "@/lib/session-context"; · 47 unmodified lines | ||
| 48 | 48 | import { useWorkspaceTools } from "@/hooks/use-workspace-tools"; |
| 49 | 49 | import { useAction, useMutation, useQuery } from "convex/react"; |
| 50 | 50 | import { convexApi } from "@/lib/convex-api"; |
| 51 | −import type { ToolSourceRecord, ToolDescriptor, CredentialRecord, CredentialScope } from "@/lib/types"; | |
| 51 | +import type { | |
| 52 | + ToolSourceRecord, | |
| 53 | + ToolDescriptor, | |
| 54 | + CredentialRecord, | |
| 55 | + CredentialScope, | |
| 56 | + OpenApiSourceQuality, | |
| 57 | +} from "@/lib/types"; | |
| 52 | 58 | import { parse as parseDomain } from "tldts"; |
| 53 | 59 | import { toast } from "sonner"; |
| 54 | 60 | import { cn } from "@/lib/utils"; |
| @@ -296,6 +302,20 @@ function formatSourceAuthBadge(source: ToolSourceRecord): string | null { · 241 unmodified lines | ||
| 296 | 302 | return `${auth.type}:${mode}`; |
| 297 | 303 | } |
| 298 | 304 | |
| 305 | +function formatQualityPercent(value: number): string { | |
| 306 | + return `${Math.round(value * 100)}%`; | |
| 307 | +} | |
| 308 | + | |
| 309 | +function qualityBadgeClass(quality: OpenApiSourceQuality): string { | |
| 310 | + if (quality.overallQuality >= 0.95) { | |
| 311 | + return "text-terminal-green border-terminal-green/30"; | |
| 312 | + } | |
| 313 | + if (quality.overallQuality >= 0.85) { | |
| 314 | + return "text-terminal-amber border-terminal-amber/30"; | |
| 315 | + } | |
| 316 | + return "text-terminal-red border-terminal-red/30"; | |
| 317 | +} | |
| 318 | + | |
| 299 | 319 | // ── Add Source Dialog ── |
| 300 | 320 | |
| 301 | 321 | const RAW_HOSTS = new Set([ |
| @@ -785,8 +805,12 @@ function ConfigureSourceAuthDialog({ · 483 unmodified lines | ||
| 785 | 805 | |
| 786 | 806 | function SourceCard({ |
| 787 | 807 | source, |
| 808 | + quality, | |
| 809 | + qualityLoading, | |
| 788 | 810 | }: { |
| 789 | 811 | source: ToolSourceRecord; |
| 812 | + quality?: OpenApiSourceQuality; | |
| 813 | + qualityLoading?: boolean; | |
| 790 | 814 | }) { |
| 791 | 815 | const { context } = useSession(); |
| 792 | 816 | const deleteToolSource = useMutation(convexApi.workspace.deleteToolSource); |
| @@ -849,6 +873,14 @@ function SourceCard({ · 56 unmodified lines | ||
| 849 | 873 | {authBadge} |
| 850 | 874 | </Badge> |
| 851 | 875 | )} |
| 876 | + {source.type === "openapi" && quality && ( | |
| 877 | + <Badge | |
| 878 | + variant="outline" | |
| 879 | + className={cn("text-[9px] font-mono uppercase tracking-wider", qualityBadgeClass(quality))} | |
| 880 | + > | |
| 881 | + quality {formatQualityPercent(quality.overallQuality)} | |
| 882 | + </Badge> | |
| 883 | + )} | |
| 852 | 884 | </div> |
| 853 | 885 | <span className="text-[11px] text-muted-foreground font-mono truncate block"> |
| 854 | 886 | {source.type === "mcp" |
| @@ -857,6 +889,19 @@ function SourceCard({ · 2 unmodified lines | ||
| 857 | 889 | ? (source.config.endpoint as string) |
| 858 | 890 | : (source.config.spec as string)} |
| 859 | 891 | </span> |
| 892 | + {source.type === "openapi" && quality && ( | |
| 893 | + <span className="text-[10px] text-muted-foreground/90 font-mono truncate block mt-0.5"> | |
| 894 | + args {formatQualityPercent(quality.argsQuality)} | returns {formatQualityPercent(quality.returnsQuality)} | |
| 895 | + {quality.unknownReturnsCount > 0 | |
| 896 | + ? ` | ${quality.unknownReturnsCount} unknown returns` | |
| 897 | + : " | fully typed returns"} | |
| 898 | + </span> | |
| 899 | + )} | |
| 900 | + {source.type === "openapi" && !quality && qualityLoading && ( | |
| 901 | + <span className="text-[10px] text-muted-foreground/70 font-mono truncate block mt-0.5"> | |
| 902 | + Computing OpenAPI type quality... | |
| 903 | + </span> | |
| 904 | + )} | |
| 860 | 905 | </div> |
| 861 | 906 | <div className="flex items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity shrink-0"> |
| 862 | 907 | <ConfigureSourceAuthDialog source={source} /> |
| @@ -1377,7 +1422,7 @@ export function ToolsView({ initialSource }: { initialSource?: string | null }) · 514 unmodified lines | ||
| 1377 | 1422 | ); |
| 1378 | 1423 | const credentialsLoading = !!context && credentials === undefined; |
| 1379 | 1424 | |
| 1380 | − const { tools, warnings, loading: toolsLoading } = useWorkspaceTools(context ?? null); | |
| 1425 | + const { tools, warnings, sourceQuality, loading: toolsLoading } = useWorkspaceTools(context ?? null); | |
| 1381 | 1426 | |
| 1382 | 1427 | if (sessionLoading) { |
| 1383 | 1428 | return ( |
| @@ -1476,9 +1521,18 @@ export function ToolsView({ initialSource }: { initialSource?: string | null }) · 92 unmodified lines | ||
| 1476 | 1521 | </div> |
| 1477 | 1522 | </div> |
| 1478 | 1523 | )} |
| 1479 | − {sources.map((s: any) => ( | |
| 1480 | − <SourceCard key={s.id} source={s} /> | |
| 1481 | − ))} | |
| 1524 | + {sources.map((s: any) => { | |
| 1525 | + const sourceKey = sourceKeyForSource(s); | |
| 1526 | + const quality = sourceKey ? sourceQuality[sourceKey] : undefined; | |
| 1527 | + return ( | |
| 1528 | + <SourceCard | |
| 1529 | + key={s.id} | |
| 1530 | + source={s} | |
| 1531 | + quality={quality} | |
| 1532 | + qualityLoading={toolsLoading} | |
| 1533 | + /> | |
| 1534 | + ); | |
| 1535 | + })} | |
| 1482 | 1536 | </div> |
| 1483 | 1537 | )} |
| 1484 | 1538 | </CardContent> |
| @@ -3,6 +3,7 @@ · 2 unmodified lines | ||
| 3 | 3 | import { useQuery as useTanstackQuery } from "@tanstack/react-query"; |
| 4 | 4 | import { useAction, useQuery as useConvexQuery } from "convex/react"; |
| 5 | 5 | import { convexApi } from "@/lib/convex-api"; |
| 6 | +import type { OpenApiSourceQuality } from "@/lib/types"; | |
| 6 | 7 | |
| 7 | 8 | interface WorkspaceContext { |
| 8 | 9 | workspaceId: string; |
| @@ -35,7 +36,7 @@ export function useWorkspaceTools(context: WorkspaceContext | null) { · 26 unmodified lines | ||
| 35 | 36 | toolSources, |
| 36 | 37 | ], |
| 37 | 38 | queryFn: async () => { |
| 38 | − if (!context) return { tools: [], warnings: [], dtsUrls: {} }; | |
| 39 | + if (!context) return { tools: [], warnings: [], dtsUrls: {}, sourceQuality: {} }; | |
| 39 | 40 | return await listToolsWithWarnings({ |
| 40 | 41 | workspaceId: context.workspaceId, |
| 41 | 42 | ...(context.actorId && { actorId: context.actorId }), |
| @@ -51,6 +52,8 @@ export function useWorkspaceTools(context: WorkspaceContext | null) { · 9 unmodified lines | ||
| 51 | 52 | warnings: data?.warnings ?? [], |
| 52 | 53 | /** Per-source .d.ts download URLs for Monaco IntelliSense. Keyed by source key (e.g. "openapi:cloudflare"). */ |
| 53 | 54 | dtsUrls: (data as any)?.dtsUrls ?? {}, |
| 55 | + /** Per-source OpenAPI quality metrics (unknown/fallback type rates). */ | |
| 56 | + sourceQuality: ((data as any)?.sourceQuality ?? {}) as Record<string, OpenApiSourceQuality>, | |
| 54 | 57 | loading: !!context && isLoading, |
| 55 | 58 | }; |
| 56 | 59 | } |
| @@ -15,6 +15,7 @@ export type { · 14 unmodified lines | ||
| 15 | 15 | CredentialRecord, |
| 16 | 16 | ToolSourceRecord, |
| 17 | 17 | ToolDescriptor, |
| 18 | + OpenApiSourceQuality, | |
| 18 | 19 | AnonymousContext, |
| 19 | 20 | } from "@executor/contracts"; |
| 20 | 21 | |
| @@ -31,6 +31,7 @@ import type { · 30 unmodified lines | ||
| 31 | 31 | ToolCredentialSpec, |
| 32 | 32 | ToolDefinition, |
| 33 | 33 | ToolDescriptor, |
| 34 | + OpenApiSourceQuality, | |
| 34 | 35 | ToolSourceRecord, |
| 35 | 36 | ToolRunContext, |
| 36 | 37 | } from "../lib/types"; |
| @@ -56,7 +57,7 @@ function policySpecificity(policy: AccessPolicyRecord, actorId?: string, clientI · 19 unmodified lines | ||
| 56 | 57 | } |
| 57 | 58 | |
| 58 | 59 | function sourceSignature(workspaceId: string, sources: Array<{ id: string; updatedAt: number; enabled: boolean }>): string { |
| 59 | − const signatureVersion = "v4"; | |
| 60 | + const signatureVersion = "v8"; | |
| 60 | 61 | const parts = sources |
| 61 | 62 | .map((source) => `${source.id}:${source.updatedAt}:${source.enabled ? 1 : 0}`) |
| 62 | 63 | .sort(); |
| @@ -131,7 +132,7 @@ interface DtsStorageEntry { · 68 unmodified lines | ||
| 131 | 132 | const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000; |
| 132 | 133 | |
| 133 | 134 | /** Cache version — bump when PreparedOpenApiSpec shape changes. */ |
| 134 | −const OPENAPI_CACHE_VERSION = "v7"; | |
| 135 | +const OPENAPI_CACHE_VERSION = "v11"; | |
| 135 | 136 | |
| 136 | 137 | async function publish( |
| 137 | 138 | ctx: any, |
| @@ -419,6 +420,67 @@ function toToolDescriptor(tool: ToolDefinition, approval: "auto" | "required"): · 281 unmodified lines | ||
| 419 | 420 | }; |
| 420 | 421 | } |
| 421 | 422 | |
| 423 | +function computeOpenApiSourceQuality( | |
| 424 | + workspaceTools: Map<string, ToolDefinition>, | |
| 425 | +): Record<string, OpenApiSourceQuality> { | |
| 426 | + const grouped = new Map<string, ToolDefinition[]>(); | |
| 427 | + | |
| 428 | + for (const tool of workspaceTools.values()) { | |
| 429 | + const sourceKey = tool.source; | |
| 430 | + if (!sourceKey || !sourceKey.startsWith("openapi:")) continue; | |
| 431 | + const list = grouped.get(sourceKey) ?? []; | |
| 432 | + list.push(tool); | |
| 433 | + grouped.set(sourceKey, list); | |
| 434 | + } | |
| 435 | + | |
| 436 | + const qualityBySource: Record<string, OpenApiSourceQuality> = {}; | |
| 437 | + | |
| 438 | + for (const [sourceKey, tools] of grouped.entries()) { | |
| 439 | + const toolCount = tools.length; | |
| 440 | + | |
| 441 | + let unknownArgsCount = 0; | |
| 442 | + let unknownReturnsCount = 0; | |
| 443 | + let partialUnknownArgsCount = 0; | |
| 444 | + let partialUnknownReturnsCount = 0; | |
| 445 | + | |
| 446 | + for (const tool of tools) { | |
| 447 | + const argsType = tool.metadata?.argsType?.trim() ?? ""; | |
| 448 | + const returnsType = tool.metadata?.returnsType?.trim() ?? ""; | |
| 449 | + | |
| 450 | + if (!argsType || argsType === "Record<string, unknown>") { | |
| 451 | + unknownArgsCount += 1; | |
| 452 | + } | |
| 453 | + if (!returnsType || returnsType === "unknown") { | |
| 454 | + unknownReturnsCount += 1; | |
| 455 | + } | |
| 456 | + if (argsType.includes("unknown")) { | |
| 457 | + partialUnknownArgsCount += 1; | |
| 458 | + } | |
| 459 | + if (returnsType.includes("unknown")) { | |
| 460 | + partialUnknownReturnsCount += 1; | |
| 461 | + } | |
| 462 | + } | |
| 463 | + | |
| 464 | + const argsQuality = toolCount > 0 ? (toolCount - unknownArgsCount) / toolCount : 1; | |
| 465 | + const returnsQuality = toolCount > 0 ? (toolCount - unknownReturnsCount) / toolCount : 1; | |
| 466 | + const overallQuality = (argsQuality + returnsQuality) / 2; | |
| 467 | + | |
| 468 | + qualityBySource[sourceKey] = { | |
| 469 | + sourceKey, | |
| 470 | + toolCount, | |
| 471 | + unknownArgsCount, | |
| 472 | + unknownReturnsCount, | |
| 473 | + partialUnknownArgsCount, | |
| 474 | + partialUnknownReturnsCount, | |
| 475 | + argsQuality, | |
| 476 | + returnsQuality, | |
| 477 | + overallQuality, | |
| 478 | + }; | |
| 479 | + } | |
| 480 | + | |
| 481 | + return qualityBySource; | |
| 482 | +} | |
| 483 | + | |
| 422 | 484 | function listVisibleToolDescriptors( |
| 423 | 485 | workspaceTools: Map<string, ToolDefinition>, |
| 424 | 486 | context: { workspaceId: string; actorId?: string; clientId?: string }, |
| @@ -453,13 +515,19 @@ async function listToolsForContext( · 28 unmodified lines | ||
| 453 | 515 | async function listToolsWithWarningsForContext( |
| 454 | 516 | ctx: any, |
| 455 | 517 | context: { workspaceId: string; actorId?: string; clientId?: string }, |
| 456 | −): Promise<{ tools: ToolDescriptor[]; warnings: string[]; dtsUrls: Record<string, string> }> { | |
| 518 | +): Promise<{ | |
| 519 | + tools: ToolDescriptor[]; | |
| 520 | + warnings: string[]; | |
| 521 | + dtsUrls: Record<string, string>; | |
| 522 | + sourceQuality: Record<string, OpenApiSourceQuality>; | |
| 523 | +}> { | |
| 457 | 524 | const [result, policies] = await Promise.all([ |
| 458 | 525 | getWorkspaceTools(ctx, context.workspaceId), |
| 459 | 526 | ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: context.workspaceId }), |
| 460 | 527 | ]); |
| 461 | 528 | const typedPolicies = policies as AccessPolicyRecord[]; |
| 462 | 529 | const tools = listVisibleToolDescriptors(result.tools, context, typedPolicies); |
| 530 | + const sourceQuality = computeOpenApiSourceQuality(result.tools); | |
| 463 | 531 | |
| 464 | 532 | // Generate download URLs for .d.ts blobs |
| 465 | 533 | const dtsUrls: Record<string, string> = {}; |
| @@ -476,6 +544,7 @@ async function listToolsWithWarningsForContext( · 10 unmodified lines | ||
| 476 | 544 | tools, |
| 477 | 545 | warnings: result.warnings, |
| 478 | 546 | dtsUrls, |
| 547 | + sourceQuality, | |
| 479 | 548 | }; |
| 480 | 549 | } |
| 481 | 550 | |
| @@ -754,7 +823,12 @@ export const listToolsWithWarnings = action({ · 272 unmodified lines | ||
| 754 | 823 | handler: async ( |
| 755 | 824 | ctx, |
| 756 | 825 | args, |
| 757 | − ): Promise<{ tools: ToolDescriptor[]; warnings: string[]; dtsUrls: Record<string, string> }> => { | |
| 826 | + ): Promise<{ | |
| 827 | + tools: ToolDescriptor[]; | |
| 828 | + warnings: string[]; | |
| 829 | + dtsUrls: Record<string, string>; | |
| 830 | + sourceQuality: Record<string, OpenApiSourceQuality>; | |
| 831 | + }> => { | |
| 758 | 832 | const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForRequest, { |
| 759 | 833 | workspaceId: args.workspaceId, |
| 760 | 834 | sessionId: args.sessionId, |
| @@ -793,7 +867,15 @@ export const listToolsWithWarningsInternal = internalAction({ · 32 unmodified lines | ||
| 793 | 867 | actorId: v.optional(v.string()), |
| 794 | 868 | clientId: v.optional(v.string()), |
| 795 | 869 | }, |
| 796 | − handler: async (ctx, args): Promise<{ tools: ToolDescriptor[]; warnings: string[]; dtsUrls: Record<string, string> }> => { | |
| 870 | + handler: async ( | |
| 871 | + ctx, | |
| 872 | + args, | |
| 873 | + ): Promise<{ | |
| 874 | + tools: ToolDescriptor[]; | |
| 875 | + warnings: string[]; | |
| 876 | + dtsUrls: Record<string, string>; | |
| 877 | + sourceQuality: Record<string, OpenApiSourceQuality>; | |
| 878 | + }> => { | |
| 797 | 879 | return await listToolsWithWarningsForContext(ctx, args); |
| 798 | 880 | }, |
| 799 | 881 | }); |
| @@ -422,6 +422,345 @@ describe("buildOpenApiToolsFromPrepared", () => { · 421 unmodified lines | ||
| 422 | 422 | expect(tool!.metadata?.returnsType).not.toBe("unknown"); |
| 423 | 423 | }); |
| 424 | 424 | |
| 425 | + test("resolves OpenAPI component schema refs for return type hints", async () => { | |
| 426 | + const spec: Record<string, unknown> = { | |
| 427 | + openapi: "3.0.3", | |
| 428 | + info: { title: "Ref return type", version: "1.0.0" }, | |
| 429 | + servers: [{ url: "https://api.example.com" }], | |
| 430 | + components: { | |
| 431 | + schemas: { | |
| 432 | + Runner: { | |
| 433 | + type: "object", | |
| 434 | + properties: { | |
| 435 | + id: { type: "number" }, | |
| 436 | + name: { type: "string" }, | |
| 437 | + }, | |
| 438 | + required: ["id", "name"], | |
| 439 | + }, | |
| 440 | + }, | |
| 441 | + }, | |
| 442 | + paths: { | |
| 443 | + "/orgs/{org}/actions/hosted-runners": { | |
| 444 | + post: { | |
| 445 | + operationId: "actions/create-hosted-runner-for-org", | |
| 446 | + tags: ["actions"], | |
| 447 | + parameters: [ | |
| 448 | + { name: "org", in: "path", required: true, schema: { type: "string" } }, | |
| 449 | + ], | |
| 450 | + requestBody: { | |
| 451 | + required: true, | |
| 452 | + content: { | |
| 453 | + "application/json": { | |
| 454 | + schema: { | |
| 455 | + type: "object", | |
| 456 | + properties: { | |
| 457 | + name: { type: "string" }, | |
| 458 | + }, | |
| 459 | + required: ["name"], | |
| 460 | + }, | |
| 461 | + }, | |
| 462 | + }, | |
| 463 | + }, | |
| 464 | + responses: { | |
| 465 | + "201": { | |
| 466 | + description: "created", | |
| 467 | + content: { | |
| 468 | + "application/json": { | |
| 469 | + schema: { $ref: "#/components/schemas/Runner" }, | |
| 470 | + }, | |
| 471 | + }, | |
| 472 | + }, | |
| 473 | + }, | |
| 474 | + }, | |
| 475 | + }, | |
| 476 | + }, | |
| 477 | + }; | |
| 478 | + | |
| 479 | + const prepared = await prepareOpenApiSpec(spec, "ref-return"); | |
| 480 | + const tools = buildOpenApiToolsFromPrepared( | |
| 481 | + { | |
| 482 | + type: "openapi", | |
| 483 | + name: "github", | |
| 484 | + spec, | |
| 485 | + baseUrl: "https://api.example.com", | |
| 486 | + }, | |
| 487 | + prepared, | |
| 488 | + ); | |
| 489 | + | |
| 490 | + const tool = tools.find((t) => t.metadata?.operationId === "actions/create-hosted-runner-for-org"); | |
| 491 | + expect(tool).toBeDefined(); | |
| 492 | + expect(tool!.metadata?.returnsType).toContain("id"); | |
| 493 | + expect(tool!.metadata?.returnsType).toContain("name"); | |
| 494 | + expect(tool!.metadata?.returnsType).not.toBe("unknown"); | |
| 495 | + }); | |
| 496 | + | |
| 497 | + test("resolves OpenAPI component response refs for return type hints", async () => { | |
| 498 | + const spec: Record<string, unknown> = { | |
| 499 | + openapi: "3.0.3", | |
| 500 | + info: { title: "Response ref type", version: "1.0.0" }, | |
| 501 | + servers: [{ url: "https://api.example.com" }], | |
| 502 | + components: { | |
| 503 | + schemas: { | |
| 504 | + BudgetList: { | |
| 505 | + type: "object", | |
| 506 | + properties: { | |
| 507 | + total: { type: "number" }, | |
| 508 | + }, | |
| 509 | + required: ["total"], | |
| 510 | + }, | |
| 511 | + }, | |
| 512 | + responses: { | |
| 513 | + BudgetResponse: { | |
| 514 | + description: "ok", | |
| 515 | + content: { | |
| 516 | + "application/json": { | |
| 517 | + schema: { $ref: "#/components/schemas/BudgetList" }, | |
| 518 | + }, | |
| 519 | + }, | |
| 520 | + }, | |
| 521 | + }, | |
| 522 | + }, | |
| 523 | + paths: { | |
| 524 | + "/organizations/{org}/settings/billing/budgets": { | |
| 525 | + get: { | |
| 526 | + operationId: "billing/get-all-budgets-org", | |
| 527 | + tags: ["billing"], | |
| 528 | + parameters: [ | |
| 529 | + { name: "org", in: "path", required: true, schema: { type: "string" } }, | |
| 530 | + ], | |
| 531 | + responses: { | |
| 532 | + "200": { $ref: "#/components/responses/BudgetResponse" }, | |
| 533 | + }, | |
| 534 | + }, | |
| 535 | + }, | |
| 536 | + }, | |
| 537 | + }; | |
| 538 | + | |
| 539 | + const prepared = await prepareOpenApiSpec(spec, "response-ref"); | |
| 540 | + const tools = buildOpenApiToolsFromPrepared( | |
| 541 | + { | |
| 542 | + type: "openapi", | |
| 543 | + name: "github", | |
| 544 | + spec, | |
| 545 | + baseUrl: "https://api.example.com", | |
| 546 | + }, | |
| 547 | + prepared, | |
| 548 | + ); | |
| 549 | + | |
| 550 | + const tool = tools.find((t) => t.metadata?.operationId === "billing/get-all-budgets-org"); | |
| 551 | + expect(tool).toBeDefined(); | |
| 552 | + expect(tool!.metadata?.returnsType).toContain("total"); | |
| 553 | + expect(tool!.metadata?.returnsType).not.toBe("unknown"); | |
| 554 | + }); | |
| 555 | + | |
| 556 | + test("resolves nested schema refs inside array items", async () => { | |
| 557 | + const spec: Record<string, unknown> = { | |
| 558 | + openapi: "3.0.3", | |
| 559 | + info: { title: "Nested refs", version: "1.0.0" }, | |
| 560 | + servers: [{ url: "https://api.example.com" }], | |
| 561 | + components: { | |
| 562 | + schemas: { | |
| 563 | + Budget: { | |
| 564 | + type: "object", | |
| 565 | + properties: { | |
| 566 | + id: { type: "string" }, | |
| 567 | + }, | |
| 568 | + required: ["id"], | |
| 569 | + }, | |
| 570 | + BudgetList: { | |
| 571 | + type: "object", | |
| 572 | + properties: { | |
| 573 | + budgets: { | |
| 574 | + type: "array", | |
| 575 | + items: { $ref: "#/components/schemas/Budget" }, | |
| 576 | + }, | |
| 577 | + }, | |
| 578 | + }, | |
| 579 | + }, | |
| 580 | + }, | |
| 581 | + paths: { | |
| 582 | + "/organizations/{org}/settings/billing/budgets": { | |
| 583 | + get: { | |
| 584 | + operationId: "billing/get-all-budgets-org", | |
| 585 | + tags: ["billing"], | |
| 586 | + parameters: [ | |
| 587 | + { name: "org", in: "path", required: true, schema: { type: "string" } }, | |
| 588 | + ], | |
| 589 | + responses: { | |
| 590 | + "200": { | |
| 591 | + description: "ok", | |
| 592 | + content: { | |
| 593 | + "application/json": { | |
| 594 | + schema: { $ref: "#/components/schemas/BudgetList" }, | |
| 595 | + }, | |
| 596 | + }, | |
| 597 | + }, | |
| 598 | + }, | |
| 599 | + }, | |
| 600 | + }, | |
| 601 | + }, | |
| 602 | + }; | |
| 603 | + | |
| 604 | + const prepared = await prepareOpenApiSpec(spec, "nested-refs"); | |
| 605 | + const tools = buildOpenApiToolsFromPrepared( | |
| 606 | + { | |
| 607 | + type: "openapi", | |
| 608 | + name: "github", | |
| 609 | + spec, | |
| 610 | + baseUrl: "https://api.example.com", | |
| 611 | + }, | |
| 612 | + prepared, | |
| 613 | + ); | |
| 614 | + | |
| 615 | + const tool = tools.find((t) => t.metadata?.operationId === "billing/get-all-budgets-org"); | |
| 616 | + expect(tool).toBeDefined(); | |
| 617 | + expect(tool!.metadata?.returnsType).toContain("budgets"); | |
| 618 | + expect(tool!.metadata?.returnsType).toContain("id: string"); | |
| 619 | + expect(tool!.metadata?.returnsType).not.toContain("unknown[]"); | |
| 620 | + }); | |
| 621 | + | |
| 622 | + test("supports allOf-composed response schemas", async () => { | |
| 623 | + const spec: Record<string, unknown> = { | |
| 624 | + openapi: "3.0.3", | |
| 625 | + info: { title: "AllOf response", version: "1.0.0" }, | |
| 626 | + servers: [{ url: "https://api.example.com" }], | |
| 627 | + components: { | |
| 628 | + schemas: { | |
| 629 | + Envelope: { | |
| 630 | + type: "object", | |
| 631 | + properties: { | |
| 632 | + success: { type: "boolean" }, | |
| 633 | + }, | |
| 634 | + required: ["success"], | |
| 635 | + }, | |
| 636 | + Account: { | |
| 637 | + type: "object", | |
| 638 | + properties: { | |
| 639 | + id: { type: "string" }, | |
| 640 | + }, | |
| 641 | + required: ["id"], | |
| 642 | + }, | |
| 643 | + AccountResponse: { | |
| 644 | + allOf: [ | |
| 645 | + { $ref: "#/components/schemas/Envelope" }, | |
| 646 | + { | |
| 647 | + type: "object", | |
| 648 | + properties: { | |
| 649 | + result: { $ref: "#/components/schemas/Account" }, | |
| 650 | + }, | |
| 651 | + }, | |
| 652 | + ], | |
| 653 | + }, | |
| 654 | + }, | |
| 655 | + }, | |
| 656 | + paths: { | |
| 657 | + "/accounts/{id}": { | |
| 658 | + get: { | |
| 659 | + operationId: "accounts/get", | |
| 660 | + tags: ["accounts"], | |
| 661 | + parameters: [ | |
| 662 | + { name: "id", in: "path", required: true, schema: { type: "string" } }, | |
| 663 | + ], | |
| 664 | + responses: { | |
| 665 | + "200": { | |
| 666 | + description: "ok", | |
| 667 | + content: { | |
| 668 | + "application/json": { | |
| 669 | + schema: { $ref: "#/components/schemas/AccountResponse" }, | |
| 670 | + }, | |
| 671 | + }, | |
| 672 | + }, | |
| 673 | + }, | |
| 674 | + }, | |
| 675 | + }, | |
| 676 | + }, | |
| 677 | + }; | |
| 678 | + | |
| 679 | + const prepared = await prepareOpenApiSpec(spec, "allof"); | |
| 680 | + const tools = buildOpenApiToolsFromPrepared( | |
| 681 | + { | |
| 682 | + type: "openapi", | |
| 683 | + name: "cf", | |
| 684 | + spec, | |
| 685 | + baseUrl: "https://api.example.com", | |
| 686 | + }, | |
| 687 | + prepared, | |
| 688 | + ); | |
| 689 | + | |
| 690 | + const tool = tools.find((t) => t.metadata?.operationId === "accounts/get"); | |
| 691 | + expect(tool).toBeDefined(); | |
| 692 | + expect(tool!.metadata?.returnsType).toContain("success"); | |
| 693 | + expect(tool!.metadata?.returnsType).toContain("result"); | |
| 694 | + expect(tool!.metadata?.returnsType).not.toBe("unknown"); | |
| 695 | + }); | |
| 696 | + | |
| 697 | + test("supports schemas that encode unions via top-level items arrays", async () => { | |
| 698 | + const spec: Record<string, unknown> = { | |
| 699 | + swagger: "2.0", | |
| 700 | + info: { title: "Tuple-style union", version: "1.0.0" }, | |
| 701 | + host: "api.example.com", | |
| 702 | + schemes: ["https"], | |
| 703 | + paths: { | |
| 704 | + "/users.identity": { | |
| 705 | + get: { | |
| 706 | + operationId: "users_identity", | |
| 707 | + tags: ["users"], | |
| 708 | + responses: { | |
| 709 | + "200": { | |
| 710 | + description: "ok", | |
| 711 | + schema: { | |
| 712 | + items: [ | |
| 713 | + { | |
| 714 | + type: "object", | |
| 715 | + properties: { | |
| 716 | + ok: { type: "boolean" }, | |
| 717 | + user: { | |
| 718 | + type: "object", | |
| 719 | + properties: { | |
| 720 | + id: { type: "string" }, | |
| 721 | + }, | |
| 722 | + }, | |
| 723 | + }, | |
| 724 | + }, | |
| 725 | + { | |
| 726 | + type: "object", | |
| 727 | + properties: { | |
| 728 | + ok: { type: "boolean" }, | |
| 729 | + user: { | |
| 730 | + type: "object", | |
| 731 | + properties: { | |
| 732 | + id: { type: "string" }, | |
| 733 | + email: { type: "string" }, | |
| 734 | + }, | |
| 735 | + }, | |
| 736 | + }, | |
| 737 | + }, | |
| 738 | + ], | |
| 739 | + }, | |
| 740 | + }, | |
| 741 | + }, | |
| 742 | + }, | |
| 743 | + }, | |
| 744 | + }, | |
| 745 | + }; | |
| 746 | + | |
| 747 | + const prepared = await prepareOpenApiSpec(spec, "tuple-union"); | |
| 748 | + const tools = buildOpenApiToolsFromPrepared( | |
| 749 | + { | |
| 750 | + type: "openapi", | |
| 751 | + name: "slack", | |
| 752 | + spec, | |
| 753 | + baseUrl: "https://api.example.com", | |
| 754 | + }, | |
| 755 | + prepared, | |
| 756 | + ); | |
| 757 | + | |
| 758 | + const tool = tools.find((t) => t.metadata?.operationId === "users_identity"); | |
| 759 | + expect(tool).toBeDefined(); | |
| 760 | + expect(tool!.metadata?.returnsType).toContain(" | "); | |
| 761 | + expect(tool!.metadata?.returnsType).not.toBe("unknown"); | |
| 762 | + }); | |
| 763 | + | |
| 425 | 764 | test("schemaTypes only attached to first tool from a source", async () => { |
| 426 | 765 | const specWithSchemas: Record<string, unknown> = { |
| 427 | 766 | openapi: "3.0.3", |
| @@ -206,6 +206,66 @@ describe("real-world OpenAPI specs", () => { · 205 unmodified lines | ||
| 206 | 206 | 300_000, |
| 207 | 207 | ); |
| 208 | 208 | |
| 209 | + test( | |
| 210 | + "github: create hosted runner for org has non-unknown return hint", | |
| 211 | + async () => { | |
| 212 | + const githubUrl = | |
| 213 | + "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml"; | |
| 214 | + | |
| 215 | + const prepared = await prepareOpenApiSpec(githubUrl, "github"); | |
| 216 | + const tools = buildOpenApiToolsFromPrepared( | |
| 217 | + { | |
| 218 | + type: "openapi", | |
| 219 | + name: "github", | |
| 220 | + spec: githubUrl, | |
| 221 | + baseUrl: prepared.servers[0] || "https://api.github.com", | |
| 222 | + }, | |
| 223 | + prepared, | |
| 224 | + ); | |
| 225 | + | |
| 226 | + const tool = tools.find( | |
| 227 | + (t) => t.metadata?.operationId === "actions/create-hosted-runner-for-org", | |
| 228 | + ); | |
| 229 | + | |
| 230 | + expect(tool).toBeDefined(); | |
| 231 | + expect(tool!.path).toBe("github.actions.actions_create_hosted_runner_for_org"); | |
| 232 | + expect(tool!.metadata!.argsType).toContain("org: string"); | |
| 233 | + expect(tool!.metadata!.returnsType).toContain("id"); | |
| 234 | + expect(tool!.metadata!.returnsType).not.toBe("unknown"); | |
| 235 | + }, | |
| 236 | + 300_000, | |
| 237 | + ); | |
| 238 | + | |
| 239 | + test( | |
| 240 | + "github: all budgets for org has non-unknown return hint", | |
| 241 | + async () => { | |
| 242 | + const githubUrl = | |
| 243 | + "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml"; | |
| 244 | + | |
| 245 | + const prepared = await prepareOpenApiSpec(githubUrl, "github"); | |
| 246 | + const tools = buildOpenApiToolsFromPrepared( | |
| 247 | + { | |
| 248 | + type: "openapi", | |
| 249 | + name: "github", | |
| 250 | + spec: githubUrl, | |
| 251 | + baseUrl: prepared.servers[0] || "https://api.github.com", | |
| 252 | + }, | |
| 253 | + prepared, | |
| 254 | + ); | |
| 255 | + | |
| 256 | + const tool = tools.find( | |
| 257 | + (t) => t.metadata?.operationId === "billing/get-all-budgets-org", | |
| 258 | + ); | |
| 259 | + | |
| 260 | + expect(tool).toBeDefined(); | |
| 261 | + expect(tool!.path).toBe("github.billing.billing_get_all_budgets_org"); | |
| 262 | + expect(tool!.metadata!.argsType).toContain("org: string"); | |
| 263 | + expect(tool!.metadata!.returnsType).toContain("budgets"); | |
| 264 | + expect(tool!.metadata!.returnsType).not.toBe("unknown"); | |
| 265 | + }, | |
| 266 | + 300_000, | |
| 267 | + ); | |
| 268 | + | |
| 209 | 269 | test( |
| 210 | 270 | "slack: approved apps list keeps typed query params and non-unknown output", |
| 211 | 271 | async () => { |
| @@ -266,6 +266,42 @@ function resolveSchemaRef( · 265 unmodified lines | ||
| 266 | 266 | return resolved; |
| 267 | 267 | } |
| 268 | 268 | |
| 269 | +function resolveRequestBodyRef( | |
| 270 | + requestBody: Record<string, unknown>, | |
| 271 | + componentRequestBodies: Record<string, unknown>, | |
| 272 | +): Record<string, unknown> { | |
| 273 | + const ref = typeof requestBody.$ref === "string" ? requestBody.$ref : ""; | |
| 274 | + const prefix = "#/components/requestBodies/"; | |
| 275 | + if (!ref.startsWith(prefix)) { | |
| 276 | + return requestBody; | |
| 277 | + } | |
| 278 | + | |
| 279 | + const key = ref.slice(prefix.length); | |
| 280 | + const resolved = asRecord(componentRequestBodies[key]); | |
| 281 | + if (Object.keys(resolved).length === 0) { | |
| 282 | + return requestBody; | |
| 283 | + } | |
| 284 | + return resolved; | |
| 285 | +} | |
| 286 | + | |
| 287 | +function resolveResponseRef( | |
| 288 | + response: Record<string, unknown>, | |
| 289 | + componentResponses: Record<string, unknown>, | |
| 290 | +): Record<string, unknown> { | |
| 291 | + const ref = typeof response.$ref === "string" ? response.$ref : ""; | |
| 292 | + const prefix = "#/components/responses/"; | |
| 293 | + if (!ref.startsWith(prefix)) { | |
| 294 | + return response; | |
| 295 | + } | |
| 296 | + | |
| 297 | + const key = ref.slice(prefix.length); | |
| 298 | + const resolved = asRecord(componentResponses[key]); | |
| 299 | + if (Object.keys(resolved).length === 0) { | |
| 300 | + return response; | |
| 301 | + } | |
| 302 | + return resolved; | |
| 303 | +} | |
| 304 | + | |
| 269 | 305 | function parameterSchemaFromEntry(entry: Record<string, unknown>): Record<string, unknown> { |
| 270 | 306 | // OpenAPI 3.x (and Swagger body params) use `schema` |
| 271 | 307 | const schema = asRecord(entry.schema); |
| @@ -291,9 +327,13 @@ function parameterSchemaFromEntry(entry: Record<string, unknown>): Record<string · 19 unmodified lines | ||
| 291 | 327 | return fallback; |
| 292 | 328 | } |
| 293 | 329 | |
| 294 | −function responseTypeHintFromSchema(responseSchema: Record<string, unknown>, responseStatus: string): string { | |
| 330 | +function responseTypeHintFromSchema( | |
| 331 | + responseSchema: Record<string, unknown>, | |
| 332 | + responseStatus: string, | |
| 333 | + componentSchemas?: Record<string, unknown>, | |
| 334 | +): string { | |
| 295 | 335 | if (Object.keys(responseSchema).length > 0) { |
| 296 | − return jsonSchemaTypeHintFallback(responseSchema); | |
| 336 | + return jsonSchemaTypeHintFallback(responseSchema, 0, componentSchemas); | |
| 297 | 337 | } |
| 298 | 338 | |
| 299 | 339 | // No-content success responses should be represented as void. |
| @@ -305,11 +345,27 @@ function responseTypeHintFromSchema(responseSchema: Record<string, unknown>, res · 5 unmodified lines | ||
| 305 | 345 | } |
| 306 | 346 | |
| 307 | 347 | /** Simple depth-limited type hint generator for schemas (used as fallback) */ |
| 308 | −function jsonSchemaTypeHintFallback(schema: unknown, depth = 0): string { | |
| 348 | +function jsonSchemaTypeHintFallback( | |
| 349 | + schema: unknown, | |
| 350 | + depth = 0, | |
| 351 | + componentSchemas?: Record<string, unknown>, | |
| 352 | +): string { | |
| 309 | 353 | if (!schema || typeof schema !== "object") return "unknown"; |
| 310 | 354 | if (depth > 4) return "unknown"; |
| 311 | 355 | |
| 312 | 356 | const shape = schema as JsonSchema; |
| 357 | + if (typeof shape.$ref === "string") { | |
| 358 | + const ref = shape.$ref; | |
| 359 | + const prefix = "#/components/schemas/"; | |
| 360 | + if (ref.startsWith(prefix)) { | |
| 361 | + const key = ref.slice(prefix.length); | |
| 362 | + const resolved = componentSchemas ? asRecord(componentSchemas[key]) : {}; | |
| 363 | + if (Object.keys(resolved).length > 0) { | |
| 364 | + return jsonSchemaTypeHintFallback(resolved, depth + 1, componentSchemas); | |
| 365 | + } | |
| 366 | + } | |
| 367 | + } | |
| 368 | + | |
| 313 | 369 | const enumValues = Array.isArray(shape.enum) ? shape.enum : undefined; |
| 314 | 370 | if (enumValues && enumValues.length > 0) { |
| 315 | 371 | return enumValues.map((value) => JSON.stringify(value)).join(" | "); |
| @@ -317,35 +373,55 @@ function jsonSchemaTypeHintFallback(schema: unknown, depth = 0): string { · 1 unmodified line | ||
| 317 | 373 | |
| 318 | 374 | const oneOf = Array.isArray(shape.oneOf) ? shape.oneOf : undefined; |
| 319 | 375 | if (oneOf && oneOf.length > 0) { |
| 320 | − return oneOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1)).join(" | "); | |
| 376 | + return oneOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas)).join(" | "); | |
| 321 | 377 | } |
| 322 | 378 | |
| 323 | 379 | const anyOf = Array.isArray(shape.anyOf) ? shape.anyOf : undefined; |
| 324 | 380 | if (anyOf && anyOf.length > 0) { |
| 325 | − return anyOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1)).join(" | "); | |
| 381 | + return anyOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas)).join(" | "); | |
| 382 | + } | |
| 383 | + | |
| 384 | + const allOf = Array.isArray(shape.allOf) ? shape.allOf : undefined; | |
| 385 | + if (allOf && allOf.length > 0) { | |
| 386 | + const parts = allOf | |
| 387 | + .map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas)) | |
| 388 | + .filter((part) => part.length > 0 && part !== "unknown"); | |
| 389 | + if (parts.length > 0) { | |
| 390 | + return parts.join(" & "); | |
| 391 | + } | |
| 326 | 392 | } |
| 327 | 393 | |
| 328 | 394 | const type = typeof shape.type === "string" ? shape.type : undefined; |
| 395 | + const tupleItems = Array.isArray(shape.items) ? shape.items : undefined; | |
| 396 | + if (!type && tupleItems && tupleItems.length > 0) { | |
| 397 | + return tupleItems | |
| 398 | + .map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas)) | |
| 399 | + .join(" | "); | |
| 400 | + } | |
| 329 | 401 | if (type === "integer") return "number"; |
| 330 | 402 | if (type === "string" || type === "number" || type === "boolean" || type === "null") { |
| 331 | 403 | return type; |
| 332 | 404 | } |
| 333 | 405 | |
| 334 | 406 | if (type === "array") { |
| 335 | − return `${jsonSchemaTypeHintFallback(shape.items, depth + 1)}[]`; | |
| 407 | + return `${jsonSchemaTypeHintFallback(shape.items, depth + 1, componentSchemas)}[]`; | |
| 336 | 408 | } |
| 337 | 409 | |
| 338 | 410 | const props = asRecord(shape.properties); |
| 411 | + const additionalProperties = shape.additionalProperties; | |
| 339 | 412 | const requiredRaw = Array.isArray(shape.required) ? shape.required : []; |
| 340 | 413 | const required = new Set(requiredRaw.filter((item): item is string => typeof item === "string")); |
| 341 | 414 | const propEntries = Object.entries(props); |
| 342 | 415 | if (type === "object" || propEntries.length > 0) { |
| 343 | 416 | if (propEntries.length === 0) { |
| 417 | + if (additionalProperties && typeof additionalProperties === "object") { | |
| 418 | + return `Record<string, ${jsonSchemaTypeHintFallback(additionalProperties, depth + 1, componentSchemas)}>`; | |
| 419 | + } | |
| 344 | 420 | return "Record<string, unknown>"; |
| 345 | 421 | } |
| 346 | 422 | const inner = propEntries |
| 347 | 423 | .slice(0, 12) |
| 348 | − .map(([key, value]) => `${key}${required.has(key) ? "" : "?"}: ${jsonSchemaTypeHintFallback(value, depth + 1)}`) | |
| 424 | + .map(([key, value]) => `${key}${required.has(key) ? "" : "?"}: ${jsonSchemaTypeHintFallback(value, depth + 1, componentSchemas)}`) | |
| 349 | 425 | .join("; "); |
| 350 | 426 | return `{ ${inner} }`; |
| 351 | 427 | } |
| @@ -541,12 +617,16 @@ function compactOpenApiPaths( · 189 unmodified lines | ||
| 541 | 617 | operationTypeIds: Set<string>, |
| 542 | 618 | componentParameters?: Record<string, unknown>, |
| 543 | 619 | componentSchemas?: Record<string, unknown>, |
| 620 | + componentResponses?: Record<string, unknown>, | |
| 621 | + componentRequestBodies?: Record<string, unknown>, | |
| 544 | 622 | ): Record<string, unknown> { |
| 545 | 623 | const paths = asRecord(pathsValue); |
| 546 | 624 | const methods = ["get", "post", "put", "delete", "patch", "head", "options"] as const; |
| 547 | 625 | const compactPaths: Record<string, unknown> = {}; |
| 548 | 626 | const compParams = componentParameters ? asRecord(componentParameters) : {}; |
| 549 | 627 | const compSchemas = componentSchemas ? asRecord(componentSchemas) : {}; |
| 628 | + const compResponses = componentResponses ? asRecord(componentResponses) : {}; | |
| 629 | + const compRequestBodies = componentRequestBodies ? asRecord(componentRequestBodies) : {}; | |
| 550 | 630 | |
| 551 | 631 | const resolveParam = (entry: Record<string, unknown>): Record<string, unknown> => { |
| 552 | 632 | if (typeof entry.$ref === "string") { |
| @@ -612,7 +692,7 @@ function compactOpenApiPaths( · 59 unmodified lines | ||
| 612 | 692 | // Extract request/response schemas for type hints. |
| 613 | 693 | // When .d.ts types exist, pre-compute lightweight hints and skip storing full schemas. |
| 614 | 694 | { |
| 615 | − const requestBody = asRecord(operation.requestBody); | |
| 695 | + const requestBody = resolveRequestBodyRef(asRecord(operation.requestBody), compRequestBodies); | |
| 616 | 696 | const requestBodyContent = asRecord(requestBody.content); |
| 617 | 697 | const requestBodySchema = resolveSchemaRef( |
| 618 | 698 | getPreferredContentSchema(requestBodyContent), |
| @@ -624,8 +704,9 @@ function compactOpenApiPaths( · 5 unmodified lines | ||
| 624 | 704 | let responseStatus = ""; |
| 625 | 705 | for (const [status, responseValue] of Object.entries(responses)) { |
| 626 | 706 | if (!status.startsWith("2")) continue; |
| 707 | + const resolvedResponse = resolveResponseRef(asRecord(responseValue), compResponses); | |
| 627 | 708 | responseSchema = resolveSchemaRef( |
| 628 | − getPreferredResponseSchema(asRecord(responseValue)), | |
| 709 | + getPreferredResponseSchema(resolvedResponse), | |
| 629 | 710 | compSchemas, |
| 630 | 711 | ); |
| 631 | 712 | responseStatus = status; |
| @@ -654,8 +735,8 @@ function compactOpenApiPaths( · 22 unmodified lines | ||
| 654 | 735 | : []) as string[]), |
| 655 | 736 | ], |
| 656 | 737 | }; |
| 657 | − compactOperation._argsTypeHint = jsonSchemaTypeHintFallback(combinedSchema); | |
| 658 | − compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus); | |
| 738 | + compactOperation._argsTypeHint = jsonSchemaTypeHintFallback(combinedSchema, 0, compSchemas); | |
| 739 | + compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus, compSchemas); | |
| 659 | 740 | } else { |
| 660 | 741 | // Keep full schemas for the fallback path |
| 661 | 742 | if (Object.keys(requestBodySchema).length > 0) { |
| @@ -766,6 +847,8 @@ export async function prepareOpenApiSpec( · 104 unmodified lines | ||
| 766 | 847 | operationTypeIds, |
| 767 | 848 | asRecord(asRecord(bundled.components).parameters), |
| 768 | 849 | asRecord(asRecord(bundled.components).schemas), |
| 850 | + asRecord(asRecord(bundled.components).responses), | |
| 851 | + asRecord(asRecord(bundled.components).requestBodies), | |
| 769 | 852 | ), |
| 770 | 853 | dts: dts ?? undefined, |
| 771 | 854 | warnings, |
| @@ -15,6 +15,7 @@ export type { · 14 unmodified lines | ||
| 15 | 15 | CredentialRecord, |
| 16 | 16 | ToolSourceRecord, |
| 17 | 17 | ToolDescriptor, |
| 18 | + OpenApiSourceQuality, | |
| 18 | 19 | AnonymousContext, |
| 19 | 20 | } from "@executor/contracts"; |
| 20 | 21 | |
| @@ -119,6 +119,18 @@ export interface ToolDescriptor { · 118 unmodified lines | ||
| 119 | 119 | schemaTypes?: Record<string, string>; |
| 120 | 120 | } |
| 121 | 121 | |
| 122 | +export interface OpenApiSourceQuality { | |
| 123 | + sourceKey: string; | |
| 124 | + toolCount: number; | |
| 125 | + unknownArgsCount: number; | |
| 126 | + unknownReturnsCount: number; | |
| 127 | + partialUnknownArgsCount: number; | |
| 128 | + partialUnknownReturnsCount: number; | |
| 129 | + argsQuality: number; | |
| 130 | + returnsQuality: number; | |
| 131 | + overallQuality: number; | |
| 132 | +} | |
| 133 | + | |
| 122 | 134 | export interface AnonymousContext { |
| 123 | 135 | sessionId: string; |
| 124 | 136 | workspaceId: string; |
| @@ -1,10 +1,3 @@ | ||
| 1 | 1 | { |
| 2 | − "$schema": "https://opencode.ai/config.json", | |
| 3 | − "mcp": { | |
| 4 | − "executor-kn7agh4e": { | |
| 5 | − "type": "remote", | |
| 6 | − "url": "https://perceptive-pigeon-577.convex.site/mcp?workspaceId=kn7agh4e6v5nx8mw03byq5f15580vg4c", | |
| 7 | − "enabled": true | |
| 8 | − } | |
| 9 | − } | |
| 10 | −} | |
| 2 | + "$schema": "https://opencode.ai/config.json" | |
| 3 | +} | |