@autoview/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +407 -0
- package/lib/AutoViewAgent.d.ts +109 -0
- package/lib/AutoViewAgent.js +123 -0
- package/lib/AutoViewAgent.js.map +1 -0
- package/lib/agent/emitMcpServer.d.ts +15 -0
- package/lib/agent/emitMcpServer.js +157 -0
- package/lib/agent/emitMcpServer.js.map +1 -0
- package/lib/agent/emitReport.d.ts +14 -0
- package/lib/agent/emitReport.js +85 -0
- package/lib/agent/emitReport.js.map +1 -0
- package/lib/agent/toolSurface.d.ts +130 -0
- package/lib/agent/toolSurface.js +342 -0
- package/lib/agent/toolSurface.js.map +1 -0
- package/lib/agent/verifyAgentTasks.d.ts +87 -0
- package/lib/agent/verifyAgentTasks.js +126 -0
- package/lib/agent/verifyAgentTasks.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.js +295 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
- package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
- package/lib/constants/AutoViewFrontendTemplate.js +46 -0
- package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
- package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
- package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
- package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
- package/lib/context/IAutoViewAgentContext.d.ts +60 -0
- package/lib/context/IAutoViewAgentContext.js +3 -0
- package/lib/context/IAutoViewAgentContext.js.map +1 -0
- package/lib/fromSwagger.d.ts +53 -0
- package/lib/fromSwagger.js +513 -0
- package/lib/fromSwagger.js.map +1 -0
- package/lib/generateDeterministic.d.ts +26 -0
- package/lib/generateDeterministic.js +75 -0
- package/lib/generateDeterministic.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +41 -0
- package/lib/index.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
- package/lib/orchestrate/orchestrateAutoView.js +491 -0
- package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
- package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
- package/lib/orchestrate/utils/HistoryMessage.js +25 -0
- package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
- package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
- package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
- package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
- package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
- package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
- package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
- package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
- package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
- package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
- package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
- package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
- package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
- package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
- package/lib/preview/renderPreview.d.ts +22 -0
- package/lib/preview/renderPreview.js +198 -0
- package/lib/preview/renderPreview.js.map +1 -0
- package/lib/typings/compiler.d.ts +39 -0
- package/lib/typings/compiler.js +3 -0
- package/lib/typings/compiler.js.map +1 -0
- package/lib/typings/events.d.ts +106 -0
- package/lib/typings/events.js +3 -0
- package/lib/typings/events.js.map +1 -0
- package/lib/typings/index.d.ts +10 -0
- package/lib/typings/index.js +27 -0
- package/lib/typings/index.js.map +1 -0
- package/lib/typings/misc.d.ts +78 -0
- package/lib/typings/misc.js +3 -0
- package/lib/typings/misc.js.map +1 -0
- package/lib/utils/ArrayUtil.d.ts +8 -0
- package/lib/utils/ArrayUtil.js +30 -0
- package/lib/utils/ArrayUtil.js.map +1 -0
- package/lib/utils/StringUtil.d.ts +11 -0
- package/lib/utils/StringUtil.js +28 -0
- package/lib/utils/StringUtil.js.map +1 -0
- package/lib/utils/classifyEndpoints.d.ts +62 -0
- package/lib/utils/classifyEndpoints.js +216 -0
- package/lib/utils/classifyEndpoints.js.map +1 -0
- package/lib/utils/endpointFilter.d.ts +26 -0
- package/lib/utils/endpointFilter.js +0 -0
- package/lib/utils/endpointFilter.js.map +1 -0
- package/lib/utils/extractFields.d.ts +85 -0
- package/lib/utils/extractFields.js +231 -0
- package/lib/utils/extractFields.js.map +1 -0
- package/lib/utils/index.d.ts +13 -0
- package/lib/utils/index.js +30 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/normalizeForNestia.d.ts +34 -0
- package/lib/utils/normalizeForNestia.js +133 -0
- package/lib/utils/normalizeForNestia.js.map +1 -0
- package/lib/utils/resourcePlan.d.ts +39 -0
- package/lib/utils/resourcePlan.js +95 -0
- package/lib/utils/resourcePlan.js.map +1 -0
- package/lib/utils/sliceDocument.d.ts +17 -0
- package/lib/utils/sliceDocument.js +114 -0
- package/lib/utils/sliceDocument.js.map +1 -0
- package/lib/utils/toEndpoints.d.ts +90 -0
- package/lib/utils/toEndpoints.js +227 -0
- package/lib/utils/toEndpoints.js.map +1 -0
- package/lib/verify/runWorkflows.d.ts +25 -0
- package/lib/verify/runWorkflows.js +366 -0
- package/lib/verify/runWorkflows.js.map +1 -0
- package/lib/verify/workflows.d.ts +53 -0
- package/lib/verify/workflows.js +107 -0
- package/lib/verify/workflows.js.map +1 -0
- package/package.json +82 -0
- package/prompts/AUTOVIEW_RENDER.md +398 -0
- package/prompts/AUTOVIEW_REVIEW.md +60 -0
- package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
- package/src/AutoViewAgent.ts +222 -0
- package/src/agent/emitMcpServer.integration.test.ts +168 -0
- package/src/agent/emitMcpServer.test.ts +51 -0
- package/src/agent/emitMcpServer.ts +178 -0
- package/src/agent/emitReport.ts +117 -0
- package/src/agent/toolSurface.test.ts +243 -0
- package/src/agent/toolSurface.ts +501 -0
- package/src/agent/verifyAgentTasks.test.ts +106 -0
- package/src/agent/verifyAgentTasks.ts +171 -0
- package/src/cli/main.ts +363 -0
- package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
- package/src/constants/AutoViewFrontendTemplate.ts +42 -0
- package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
- package/src/context/IAutoViewAgentContext.ts +84 -0
- package/src/fromSwagger.test.ts +269 -0
- package/src/fromSwagger.ts +500 -0
- package/src/generateDeterministic.test.ts +39 -0
- package/src/generateDeterministic.ts +77 -0
- package/src/index.ts +30 -0
- package/src/orchestrate/orchestrateAutoView.ts +590 -0
- package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
- package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
- package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
- package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
- package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
- package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
- package/src/orchestrate/renderNavTs.test.ts +74 -0
- package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
- package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
- package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
- package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
- package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
- package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
- package/src/orchestrate/utils/HistoryMessage.ts +41 -0
- package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
- package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
- package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
- package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
- package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
- package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
- package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
- package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
- package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
- package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
- package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
- package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
- package/src/preview/renderPreview.ts +273 -0
- package/src/typings/compiler.ts +47 -0
- package/src/typings/events.ts +155 -0
- package/src/typings/index.ts +10 -0
- package/src/typings/misc.ts +93 -0
- package/src/utils/ArrayUtil.ts +16 -0
- package/src/utils/StringUtil.ts +29 -0
- package/src/utils/classifyEndpoints.test.ts +86 -0
- package/src/utils/classifyEndpoints.ts +291 -0
- package/src/utils/endpointFilter.test.ts +50 -0
- package/src/utils/endpointFilter.ts +0 -0
- package/src/utils/extractFields.test.ts +82 -0
- package/src/utils/extractFields.ts +306 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/normalizeForNestia.test.ts +93 -0
- package/src/utils/normalizeForNestia.ts +139 -0
- package/src/utils/resourcePlan.test.ts +104 -0
- package/src/utils/resourcePlan.ts +180 -0
- package/src/utils/sliceDocument.test.ts +85 -0
- package/src/utils/sliceDocument.ts +119 -0
- package/src/utils/toEndpoints.test.ts +251 -0
- package/src/utils/toEndpoints.ts +343 -0
- package/src/verify/runWorkflows.ts +403 -0
- package/src/verify/workflows.test.ts +117 -0
- package/src/verify/workflows.ts +154 -0
- package/template/CLAUDE.md +140 -0
- package/template/Dockerfile +31 -0
- package/template/PROMPT.md +80 -0
- package/template/SANDBOX.md +70 -0
- package/template/app/api/health/route.ts +10 -0
- package/template/app/globals.css +97 -0
- package/template/app/layout.tsx +30 -0
- package/template/app/page.tsx +19 -0
- package/template/components/AppShell.tsx +114 -0
- package/template/components/auto/CatalogGrid.tsx +159 -0
- package/template/components/auto/ConfirmButton.tsx +67 -0
- package/template/components/auto/EmbeddedCollection.tsx +144 -0
- package/template/components/auto/ResourceDashboard.tsx +104 -0
- package/template/components/auto/ResourceDetail.tsx +93 -0
- package/template/components/auto/ResourceForm.tsx +235 -0
- package/template/components/auto/ResourceIcon.tsx +88 -0
- package/template/components/auto/ResourceLanding.tsx +155 -0
- package/template/components/auto/ResourceTable.tsx +223 -0
- package/template/components/auto/formatValue.tsx +186 -0
- package/template/components/auto/types.ts +42 -0
- package/template/components/ui/badge.tsx +40 -0
- package/template/components/ui/button.tsx +57 -0
- package/template/components/ui/card.tsx +86 -0
- package/template/components/ui/dialog.tsx +119 -0
- package/template/components/ui/input.tsx +23 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/pagination.tsx +117 -0
- package/template/components/ui/select.tsx +92 -0
- package/template/components/ui/sheet.tsx +135 -0
- package/template/components/ui/skeleton.tsx +15 -0
- package/template/components/ui/table.tsx +120 -0
- package/template/components/ui/tabs.tsx +55 -0
- package/template/lib/utils.ts +35 -0
- package/template/next.config.mjs +52 -0
- package/template/package.json +46 -0
- package/template/postcss.config.js +6 -0
- package/template/scripts/start-shopping-backend.sh +56 -0
- package/template/tailwind.config.ts +96 -0
- package/template/tsconfig.json +29 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
import { buildToolSurface, coverToolSurface } from "./toolSurface";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Emit a runnable MCP (Model Context Protocol) server from an OpenAPI document —
|
|
7
|
+
* the agent-facing counterpart of the generated frontend. The server exposes the
|
|
8
|
+
* shaped tool surface (read/write annotations + producer hints) and, on a tool
|
|
9
|
+
* call, executes the real API over HTTP against the configured backend.
|
|
10
|
+
*
|
|
11
|
+
* Returns a flat `path → content` project map (server + package.json + README),
|
|
12
|
+
* the same shape the frontend scaffold returns, so the CLI writes it the same
|
|
13
|
+
* way. Deterministic: same document → same server.
|
|
14
|
+
*/
|
|
15
|
+
export function emitMcpServer(
|
|
16
|
+
document: OpenApi.IDocument,
|
|
17
|
+
options: { backend: string | null; title?: string } = { backend: null },
|
|
18
|
+
): Record<string, string> {
|
|
19
|
+
const tools = buildToolSurface(document);
|
|
20
|
+
const coverage = coverToolSurface(tools);
|
|
21
|
+
const appName = (options.title ?? document.info?.title ?? "API").trim() || "API";
|
|
22
|
+
// Only the fields the server needs at runtime (keep the bundle lean).
|
|
23
|
+
const runtimeTools = tools.map((t) => ({
|
|
24
|
+
name: t.name,
|
|
25
|
+
description: t.description,
|
|
26
|
+
inputSchema: t.inputSchema,
|
|
27
|
+
annotations: t.annotations,
|
|
28
|
+
operation: t.operation,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"server.mjs": renderServer(appName, options.backend, runtimeTools),
|
|
33
|
+
"package.json": renderPackageJson(appName),
|
|
34
|
+
"README.md": renderReadme(appName, options.backend, coverage),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderServer(
|
|
39
|
+
appName: string,
|
|
40
|
+
backend: string | null,
|
|
41
|
+
tools: unknown[],
|
|
42
|
+
): string {
|
|
43
|
+
return `#!/usr/bin/env node
|
|
44
|
+
// MCP server generated by AutoView from the OpenAPI document of "${appName}".
|
|
45
|
+
// Exposes the API as agent-callable tools and executes calls against the live
|
|
46
|
+
// backend. Configure the backend with API_HOST / API_TOKEN env vars.
|
|
47
|
+
|
|
48
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
49
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
50
|
+
import {
|
|
51
|
+
CallToolRequestSchema,
|
|
52
|
+
ListToolsRequestSchema,
|
|
53
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
54
|
+
|
|
55
|
+
const TOOLS = ${JSON.stringify(tools, null, 2)};
|
|
56
|
+
|
|
57
|
+
const BASE = (process.env.API_HOST ?? ${JSON.stringify(backend ?? "")}).replace(/\\/$/, "");
|
|
58
|
+
const AUTH = process.env.API_TOKEN ?? "";
|
|
59
|
+
|
|
60
|
+
if (BASE.length === 0) {
|
|
61
|
+
console.error("Set API_HOST to the backend base URL (e.g. https://api.example.com).");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const server = new Server(
|
|
66
|
+
{ name: ${JSON.stringify(appName + " (AutoView MCP)")}, version: "1.0.0" },
|
|
67
|
+
{ capabilities: { tools: {} } },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
71
|
+
tools: TOOLS.map((t) => ({
|
|
72
|
+
name: t.name,
|
|
73
|
+
description: t.description,
|
|
74
|
+
inputSchema: t.inputSchema,
|
|
75
|
+
annotations: t.annotations,
|
|
76
|
+
})),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
80
|
+
const tool = TOOLS.find((t) => t.name === req.params.name);
|
|
81
|
+
if (tool === undefined) {
|
|
82
|
+
return { content: [{ type: "text", text: "Unknown tool: " + req.params.name }], isError: true };
|
|
83
|
+
}
|
|
84
|
+
const args = req.params.arguments ?? {};
|
|
85
|
+
let path = tool.operation.path;
|
|
86
|
+
for (const p of tool.operation.pathParams) {
|
|
87
|
+
path = path.replace("{" + p + "}", encodeURIComponent(String(args[p] ?? "")));
|
|
88
|
+
}
|
|
89
|
+
const url = new URL(BASE + path);
|
|
90
|
+
if (tool.operation.hasQuery && args.query && typeof args.query === "object") {
|
|
91
|
+
for (const [k, v] of Object.entries(args.query)) {
|
|
92
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const method = tool.operation.method;
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(url, {
|
|
98
|
+
method,
|
|
99
|
+
headers: { "content-type": "application/json", ...(AUTH ? { authorization: AUTH } : {}) },
|
|
100
|
+
body: ["GET", "HEAD", "DELETE"].includes(method) ? undefined : JSON.stringify(args.body ?? {}),
|
|
101
|
+
});
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
return { content: [{ type: "text", text: text.length > 0 ? text : "(empty) status " + res.status }], isError: !res.ok };
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return { content: [{ type: "text", text: "Request failed: " + (err && err.message ? err.message : String(err)) }], isError: true };
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await server.connect(new StdioServerTransport());
|
|
110
|
+
console.error(${JSON.stringify(appName)} + " MCP server ready (" + TOOLS.length + " tools) → " + BASE);
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderPackageJson(appName: string): string {
|
|
115
|
+
const slug = appName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "api";
|
|
116
|
+
return JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
name: `${slug}-mcp`,
|
|
119
|
+
version: "1.0.0",
|
|
120
|
+
private: true,
|
|
121
|
+
type: "module",
|
|
122
|
+
bin: { [`${slug}-mcp`]: "./server.mjs" },
|
|
123
|
+
scripts: { start: "node server.mjs" },
|
|
124
|
+
dependencies: { "@modelcontextprotocol/sdk": "^1.0.0" },
|
|
125
|
+
},
|
|
126
|
+
null,
|
|
127
|
+
2,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderReadme(
|
|
132
|
+
appName: string,
|
|
133
|
+
backend: string | null,
|
|
134
|
+
coverage: ReturnType<typeof coverToolSurface>,
|
|
135
|
+
): string {
|
|
136
|
+
return [
|
|
137
|
+
`# ${appName} — MCP server`,
|
|
138
|
+
"",
|
|
139
|
+
"Generated by AutoView. Exposes the API as agent-callable tools (read/write",
|
|
140
|
+
"annotated, with producer hints) and executes calls against the live backend.",
|
|
141
|
+
"",
|
|
142
|
+
"## Run",
|
|
143
|
+
"",
|
|
144
|
+
"```bash",
|
|
145
|
+
"npm install",
|
|
146
|
+
`API_HOST=${backend ?? "https://api.example.com"} \\`,
|
|
147
|
+
' API_TOKEN="Bearer <token>" \\',
|
|
148
|
+
" npm start",
|
|
149
|
+
"```",
|
|
150
|
+
"",
|
|
151
|
+
"## Wire into an agent (Claude Desktop / Cursor / Claude Code)",
|
|
152
|
+
"",
|
|
153
|
+
"```json",
|
|
154
|
+
JSON.stringify(
|
|
155
|
+
{
|
|
156
|
+
mcpServers: {
|
|
157
|
+
[appName.toLowerCase().replace(/[^a-z0-9]+/g, "-")]: {
|
|
158
|
+
command: "node",
|
|
159
|
+
args: ["server.mjs"],
|
|
160
|
+
env: { API_HOST: backend ?? "https://api.example.com", API_TOKEN: "Bearer <token>" },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
null,
|
|
165
|
+
2,
|
|
166
|
+
),
|
|
167
|
+
"```",
|
|
168
|
+
"",
|
|
169
|
+
"## Surface",
|
|
170
|
+
"",
|
|
171
|
+
`- **${coverage.tools}** tools (${coverage.readTools} read, ${coverage.writeTools} write)`,
|
|
172
|
+
`- **${coverage.resolvedInputs}/${coverage.referenceInputs}** id inputs have a declared producer (navigable)`,
|
|
173
|
+
coverage.orphanInputs > 0
|
|
174
|
+
? `- ⚠️ **${coverage.orphanInputs}** id inputs have NO producer in the API — an agent cannot obtain them from any tool (an API-design gap; see \`--report\`).`
|
|
175
|
+
: "- ✅ every id input is obtainable from another tool (fully navigable).",
|
|
176
|
+
"",
|
|
177
|
+
].join("\n");
|
|
178
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
import { unsupportedOperations } from "../utils/toEndpoints";
|
|
4
|
+
import { analyzeConsumability } from "./toolSurface";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Consumability report — the deterministic, LLM-free verdict on whether an API
|
|
8
|
+
* can actually be driven as a product (by an agent or a generated UI).
|
|
9
|
+
*
|
|
10
|
+
* Unlike a contract test (which checks each endpoint in isolation), this checks
|
|
11
|
+
* the API as a *navigable graph*: every id input a tool needs should be
|
|
12
|
+
* obtainable from another tool. It is honest about its own limits — an id whose
|
|
13
|
+
* producing endpoint returns an un-introspectable inline schema is reported as
|
|
14
|
+
* "undetermined", not as a defect, so the report never falsely accuses an API.
|
|
15
|
+
*
|
|
16
|
+
* Returns Markdown. Pure and deterministic.
|
|
17
|
+
*/
|
|
18
|
+
export function emitConsumabilityReport(document: OpenApi.IDocument): string {
|
|
19
|
+
const title = (document.info?.title ?? "API").trim() || "API";
|
|
20
|
+
const a = analyzeConsumability(document);
|
|
21
|
+
// Judgeable id inputs = those we can rule on: obtainable (resolved + nested)
|
|
22
|
+
// vs a genuine gap (orphan). userInput/untraceable are not entity-id gaps.
|
|
23
|
+
const obtainable = a.resolved + a.nested;
|
|
24
|
+
const denom = obtainable + a.orphan.length;
|
|
25
|
+
const navigable = denom > 0 ? Math.round((100 * obtainable) / denom) : 100;
|
|
26
|
+
const dropped = unsupportedOperations(document);
|
|
27
|
+
|
|
28
|
+
const lines: string[] = [
|
|
29
|
+
`# Consumability report — ${title}`,
|
|
30
|
+
"",
|
|
31
|
+
"Can this API be driven as a product? Checked deterministically (no LLM) by",
|
|
32
|
+
"projecting the spec into an agent tool surface and verifying the call graph —",
|
|
33
|
+
"every id a tool needs should be obtainable from another tool.",
|
|
34
|
+
"",
|
|
35
|
+
"## Summary",
|
|
36
|
+
"",
|
|
37
|
+
`- **${a.tools}** operations (${a.readTools} read, ${a.writeTools} write); **${a.referenceInputs}** id inputs.`,
|
|
38
|
+
`- ✅ **${a.resolved}** inputs resolved — a list/search tool produces the id, and the surface tells the agent which one.`,
|
|
39
|
+
a.nested > 0
|
|
40
|
+
? `- ✅ **${a.nested}** obtainable nested — the id appears inside a parent read's response (e.g. \`order\` → \`goods[].id\`); navigable, just not via a dedicated list tool.`
|
|
41
|
+
: null,
|
|
42
|
+
a.orphan.length === 0
|
|
43
|
+
? "- ✅ **0** genuine orphan inputs."
|
|
44
|
+
: `- ⚠️ **${a.orphan.length}** orphan inputs — a required id NO endpoint can supply (design gap).`,
|
|
45
|
+
a.untraceable.length > 0
|
|
46
|
+
? `- ❔ **${a.untraceable.length}** undetermined — the resource is read but no id could be traced in its (often inline) response; navigability not provable either way.`
|
|
47
|
+
: null,
|
|
48
|
+
a.userInputs.length > 0
|
|
49
|
+
? `- 👤 **${a.userInputs.length}** caller-supplied keys (\`*_name\` / \`*_key\` / \`scope\` …) — not entity ids, so a missing producer is expected, not a gap.`
|
|
50
|
+
: null,
|
|
51
|
+
`- **Navigability (of judgeable inputs): ${navigable}%** (${obtainable}/${denom}).`,
|
|
52
|
+
dropped.length > 0
|
|
53
|
+
? `- **${dropped.length}** operation(s) could not be exposed at all (unsupported shape).`
|
|
54
|
+
: null,
|
|
55
|
+
"",
|
|
56
|
+
].filter((l): l is string => l !== null);
|
|
57
|
+
|
|
58
|
+
if (a.orphan.length > 0) {
|
|
59
|
+
lines.push(
|
|
60
|
+
"## ⚠️ Orphan inputs (genuine design gaps)",
|
|
61
|
+
"",
|
|
62
|
+
"These tools require an id, but **no endpoint reads that resource** — nothing",
|
|
63
|
+
"in the API can supply the value. An agent or UI cannot call them without an",
|
|
64
|
+
"out-of-band id. Usually a missing list/detail endpoint, or an id named",
|
|
65
|
+
"inconsistently with its resource.",
|
|
66
|
+
"",
|
|
67
|
+
);
|
|
68
|
+
for (const o of a.orphan.slice(0, 50)) {
|
|
69
|
+
lines.push(`- \`${o.tool}\` needs \`${o.param}\` (a \`${o.resource}\` id) — no endpoint produces it.`);
|
|
70
|
+
}
|
|
71
|
+
if (a.orphan.length > 50) lines.push(`- …and ${a.orphan.length - 50} more.`);
|
|
72
|
+
lines.push("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (a.untraceable.length > 0) {
|
|
76
|
+
lines.push(
|
|
77
|
+
"## ❔ Undetermined inputs",
|
|
78
|
+
"",
|
|
79
|
+
"The resource has a read endpoint, but its response is an inline schema this",
|
|
80
|
+
"analysis does not model, so the producer link could not be traced. Not a",
|
|
81
|
+
"reported defect — likely fine, just not machine-verified here.",
|
|
82
|
+
"",
|
|
83
|
+
`- ${a.untraceable.length} inputs across ${new Set(a.untraceable.map((u) => u.resource)).size} resources (e.g. ${[...new Set(a.untraceable.slice(0, 5).map((u) => u.resource))].join(", ")}).`,
|
|
84
|
+
"",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (a.userInputs.length > 0) {
|
|
89
|
+
lines.push(
|
|
90
|
+
"## 👤 Caller-supplied keys",
|
|
91
|
+
"",
|
|
92
|
+
"These path params are human-known keys (a name, a `scope`, a `template_key`),",
|
|
93
|
+
"not entity ids chained from another endpoint. The caller already knows the",
|
|
94
|
+
"value, so the absence of a producing tool is by design — not a gap.",
|
|
95
|
+
"",
|
|
96
|
+
`- ${a.userInputs.length} inputs across ${new Set(a.userInputs.map((u) => u.param)).size} key(s) (e.g. ${[...new Set(a.userInputs.slice(0, 6).map((u) => `\`${u.param}\``))].join(", ")}).`,
|
|
97
|
+
"",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (dropped.length > 0) {
|
|
102
|
+
lines.push("## Unexposed operations", "");
|
|
103
|
+
for (const d of dropped.slice(0, 20)) lines.push(`- \`${d.method} ${d.path}\` — ${d.reason}`);
|
|
104
|
+
if (dropped.length > 20) lines.push(`- …and ${dropped.length - 20} more.`);
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push(
|
|
109
|
+
"## Verdict",
|
|
110
|
+
"",
|
|
111
|
+
a.orphan.length === 0
|
|
112
|
+
? "✅ No genuine consumability gaps found — every id a tool needs is obtainable from the API (within what could be analyzed)."
|
|
113
|
+
: `⚠️ ${a.orphan.length} input(s) cannot be supplied by the API. Add the missing list/detail endpoints (or align id field names) so every id is obtainable, then re-run.`,
|
|
114
|
+
"",
|
|
115
|
+
);
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { analyzeConsumability, buildToolSurface, coverToolSurface } from "./toolSurface";
|
|
5
|
+
|
|
6
|
+
function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
|
|
7
|
+
return OpenApiConverter.upgradeDocument({
|
|
8
|
+
openapi: "3.0.0",
|
|
9
|
+
info: { title: "t", version: "1.0.0" },
|
|
10
|
+
paths,
|
|
11
|
+
components: { schemas },
|
|
12
|
+
} as never);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ok = { 200: { description: "ok" } };
|
|
16
|
+
const listResp = (item: string) => ({
|
|
17
|
+
200: {
|
|
18
|
+
description: "ok",
|
|
19
|
+
content: { "application/json": { schema: { type: "array", items: { $ref: `#/components/schemas/${item}` } } } },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const objResp = (name: string) => ({
|
|
23
|
+
200: { description: "ok", content: { "application/json": { schema: { $ref: `#/components/schemas/${name}` } } } },
|
|
24
|
+
});
|
|
25
|
+
const param = (n: string) => ({ name: n, in: "path", required: true, schema: { type: "string" } });
|
|
26
|
+
|
|
27
|
+
const fixture = doc(
|
|
28
|
+
{
|
|
29
|
+
"/sales": {
|
|
30
|
+
get: { operationId: "index", summary: "List sales.", responses: listResp("Sale") },
|
|
31
|
+
post: { operationId: "create", summary: "Create a sale.", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Sale" } } } }, responses: ok },
|
|
32
|
+
},
|
|
33
|
+
"/sales/{saleId}": {
|
|
34
|
+
get: { operationId: "at", summary: "Get a sale.", parameters: [param("saleId")], responses: objResp("Sale") },
|
|
35
|
+
delete: { operationId: "erase", summary: "Delete a sale.", parameters: [param("saleId")], responses: ok },
|
|
36
|
+
},
|
|
37
|
+
"/sales/{saleId}/questions": {
|
|
38
|
+
get: { operationId: "index", summary: "List questions on a sale.", parameters: [param("saleId")], responses: listResp("Question") },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
Sale: { type: "object", properties: { id: { type: "string" }, title: { type: "string" } }, required: ["id"] },
|
|
43
|
+
Question: { type: "object", properties: { id: { type: "string" }, body: { type: "string" } }, required: ["id"] },
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
describe("buildToolSurface", () => {
|
|
48
|
+
const tools = buildToolSurface(fixture);
|
|
49
|
+
// nestia accessors are method-derived: `/sales` GET → sales.get, POST →
|
|
50
|
+
// sales.post, `/sales/{saleId}` DELETE → sales.eraseBySaleid, etc.
|
|
51
|
+
const list = tools.find((t) => t.name === "sales.get")!;
|
|
52
|
+
const create = tools.find((t) => t.name === "sales.post")!;
|
|
53
|
+
const erase = tools.find((t) => t.name === "sales.eraseBySaleid")!;
|
|
54
|
+
const questions = tools.find((t) => t.name === "sales.questions.getBySaleid")!;
|
|
55
|
+
|
|
56
|
+
it("tags read vs write (and destructive) so an agent can tell them apart", () => {
|
|
57
|
+
expect(list.annotations.readOnlyHint).toBe(true);
|
|
58
|
+
expect(list.description).toContain("[READ]");
|
|
59
|
+
expect(create.annotations.readOnlyHint).toBe(false);
|
|
60
|
+
expect(create.description).toContain("[WRITE");
|
|
61
|
+
expect(erase.annotations.destructiveHint).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("declares the producer chain for an id input (not left for the agent to guess)", () => {
|
|
65
|
+
// the questions list needs a saleId — it comes from the sales list's id
|
|
66
|
+
expect(questions.producers).toEqual([
|
|
67
|
+
{ param: "saleId", tool: "sales.get", field: "id" },
|
|
68
|
+
]);
|
|
69
|
+
expect(questions.inputSchema.required).toContain("saleId");
|
|
70
|
+
expect(questions.inputSchema.properties.saleId).toMatchObject({ type: "string" });
|
|
71
|
+
expect((questions.inputSchema.properties.saleId as { description: string }).description).toContain("sales.get");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("a create tool carries a body input", () => {
|
|
75
|
+
expect(create.inputSchema.required).toContain("body");
|
|
76
|
+
expect(create.operation.hasBody).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("coverage reports the navigable fraction (producer-resolved inputs)", () => {
|
|
80
|
+
const cov = coverToolSurface(tools);
|
|
81
|
+
expect(cov.tools).toBe(tools.length);
|
|
82
|
+
expect(cov.readTools + cov.writeTools).toBe(tools.length);
|
|
83
|
+
// every path param here is resolvable (sales + questions both trace to sales.index)
|
|
84
|
+
expect(cov.resolvedInputs).toBe(cov.referenceInputs);
|
|
85
|
+
expect(cov.orphanInputs).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("is deterministic", () => {
|
|
89
|
+
expect(buildToolSurface(fixture)).toEqual(buildToolSurface(fixture));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("analyzeConsumability", () => {
|
|
94
|
+
it("splits id inputs into resolved vs genuine orphan (no reader)", () => {
|
|
95
|
+
const d = doc(
|
|
96
|
+
{
|
|
97
|
+
// sales are listed → questions' saleId is resolvable
|
|
98
|
+
"/sales": { get: { operationId: "index", responses: listResp("Sale") } },
|
|
99
|
+
"/sales/{saleId}/questions": { get: { operationId: "index", parameters: [param("saleId")], responses: listResp("Question") } },
|
|
100
|
+
// imports are NEVER read → importId is a genuine orphan (nothing produces it)
|
|
101
|
+
"/imports/{importId}/process": { post: { operationId: "process", parameters: [param("importId")], responses: ok } },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
105
|
+
Question: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
const a = analyzeConsumability(d);
|
|
109
|
+
expect(a.resolved).toBeGreaterThanOrEqual(1); // saleId
|
|
110
|
+
expect(a.orphan.some((o) => o.param === "importId")).toBe(true); // genuine gap
|
|
111
|
+
expect(a.orphan.some((o) => o.param === "saleId")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("resolves an id obtainable nested inside a parent's response (not an orphan)", () => {
|
|
115
|
+
// `goods` has NO dedicated list/detail endpoint, but an order's detail
|
|
116
|
+
// returns `goods: Good[]` — so a goodId IS obtainable (nested), not orphan.
|
|
117
|
+
const d = doc(
|
|
118
|
+
{
|
|
119
|
+
"/orders": { get: { operationId: "index", responses: listResp("Order") } },
|
|
120
|
+
"/orders/{orderId}": { get: { operationId: "at", parameters: [param("orderId")], responses: objResp("Order") } },
|
|
121
|
+
"/orders/{orderId}/goods/{goodId}/cancel": {
|
|
122
|
+
post: { operationId: "cancel", parameters: [param("orderId"), param("goodId")], responses: ok },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
Order: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: { id: { type: "string" }, goods: { type: "array", items: { $ref: "#/components/schemas/Good" } } },
|
|
129
|
+
required: ["id"],
|
|
130
|
+
},
|
|
131
|
+
Good: { type: "object", properties: { id: { type: "string" }, name: { type: "string" } }, required: ["id"] },
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
const a = analyzeConsumability(d);
|
|
135
|
+
expect(a.orphan.some((o) => o.param === "goodId")).toBe(false);
|
|
136
|
+
expect(a.orphan).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("separates a caller-supplied key from a genuine id orphan", () => {
|
|
140
|
+
// Neither resource is read. But `template_key` is a human-known key (caller
|
|
141
|
+
// supplies it), whereas `widget_id` is an entity id nothing produces — only
|
|
142
|
+
// the latter is a real consumability gap.
|
|
143
|
+
const d = doc(
|
|
144
|
+
{
|
|
145
|
+
"/templates/{template_key}/apply": { post: { operationId: "apply", parameters: [param("template_key")], responses: ok } },
|
|
146
|
+
"/widgets/{widget_id}/process": { post: { operationId: "process", parameters: [param("widget_id")], responses: ok } },
|
|
147
|
+
},
|
|
148
|
+
{},
|
|
149
|
+
);
|
|
150
|
+
const a = analyzeConsumability(d);
|
|
151
|
+
expect(a.orphan.map((o) => o.param)).toEqual(["widget_id"]);
|
|
152
|
+
expect(a.userInputs.map((u) => u.param)).toEqual(["template_key"]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("resolves a producer from a resource-named collection wrapper (`{ namespaces: [...] }`)", () => {
|
|
156
|
+
// a list whose response wraps the array under the resource name (not `data`),
|
|
157
|
+
// with the id as `uuid` — the producer hint must point at it with field uuid.
|
|
158
|
+
const d = doc(
|
|
159
|
+
{
|
|
160
|
+
"/namespaces": { get: { operationId: "index", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "object", properties: { namespaces: { type: "array", items: { $ref: "#/components/schemas/Namespace" } } } } } } } } } },
|
|
161
|
+
"/namespaces/{namespaceId}/process": { post: { operationId: "process", parameters: [param("namespaceId")], responses: ok } },
|
|
162
|
+
},
|
|
163
|
+
{ Namespace: { type: "object", properties: { uuid: { type: "string" }, label: { type: "string" } }, required: ["uuid"] } },
|
|
164
|
+
);
|
|
165
|
+
const a = analyzeConsumability(d);
|
|
166
|
+
expect(a.orphan).toHaveLength(0);
|
|
167
|
+
const proc = buildToolSurface(d).find((t) => t.name.includes("process"))!;
|
|
168
|
+
const hint = proc.producers.find((p) => p.param === "namespaceId");
|
|
169
|
+
expect(hint).toBeDefined();
|
|
170
|
+
expect(hint!.field).toBe("uuid"); // the real id field, not a guessed "id"
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("does not treat a detail read as a producer of its own id (no circular resolve)", () => {
|
|
174
|
+
// a resource with ONLY a detail read (no list/search). Its id cannot be
|
|
175
|
+
// produced — calling the detail already needs it. So it is undetermined,
|
|
176
|
+
// never `resolved`, and the surface gives no (circular) producer hint.
|
|
177
|
+
const d = doc(
|
|
178
|
+
{ "/widgets/{widgetId}": { get: { operationId: "at", parameters: [param("widgetId")], responses: objResp("Widget") } } },
|
|
179
|
+
{ Widget: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
180
|
+
);
|
|
181
|
+
const a = analyzeConsumability(d);
|
|
182
|
+
expect(a.resolved).toBe(0);
|
|
183
|
+
const detail = buildToolSurface(d).find((t) => t.name === "widgets.getByWidgetid")!;
|
|
184
|
+
expect(detail.producers).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("does not treat an id-scoped sub-view as a producer of its parent resource", () => {
|
|
188
|
+
// `/files/{fileId}/trash` classifies under `files` and returns a File id, but
|
|
189
|
+
// it NEEDS fileId — it cannot enumerate files, so it must not be files' producer.
|
|
190
|
+
const d = doc(
|
|
191
|
+
{
|
|
192
|
+
"/files/{fileId}": { get: { operationId: "at", parameters: [param("fileId")], responses: objResp("File") } },
|
|
193
|
+
"/files/{fileId}/trash": { get: { operationId: "trash", parameters: [param("fileId")], responses: objResp("File") } },
|
|
194
|
+
},
|
|
195
|
+
{ File: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
196
|
+
);
|
|
197
|
+
const trash = buildToolSurface(d).find((t) => t.name.includes("trash"))!;
|
|
198
|
+
expect(trash.producers).toEqual([]); // no circular self-producer hint
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("the report's resolved count equals the tool surface's producer hints (consistency)", () => {
|
|
202
|
+
const d = doc(
|
|
203
|
+
{
|
|
204
|
+
"/sales": { get: { operationId: "index", responses: listResp("Sale") } },
|
|
205
|
+
"/sales/{saleId}": { get: { operationId: "at", parameters: [param("saleId")], responses: objResp("Sale") } },
|
|
206
|
+
"/sales/{saleId}/questions": { get: { operationId: "qi", parameters: [param("saleId")], responses: listResp("Question") } },
|
|
207
|
+
"/sales/{saleId}/questions/{questionId}": { get: { operationId: "qa", parameters: [param("saleId"), param("questionId")], responses: objResp("Question") } },
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
211
|
+
Question: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
const hints = new Set<string>();
|
|
215
|
+
for (const t of buildToolSurface(d)) for (const p of t.producers) hints.add(`${t.name}|${p.param}`);
|
|
216
|
+
const a = analyzeConsumability(d);
|
|
217
|
+
// the surface hints every navigable input — direct producers + nested ones
|
|
218
|
+
expect(hints.size).toBe(a.resolved + a.nested);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("hands the agent a nested producer hint with the array path", () => {
|
|
222
|
+
// goodId lives in an order's `goods[]` — the tool that needs it should be
|
|
223
|
+
// told to read it from the order detail's response at `goods[].id`.
|
|
224
|
+
const d = doc(
|
|
225
|
+
{
|
|
226
|
+
"/orders": { get: { operationId: "index", responses: listResp("Order") } },
|
|
227
|
+
"/orders/{orderId}": { get: { operationId: "at", parameters: [param("orderId")], responses: objResp("Order") } },
|
|
228
|
+
"/orders/{orderId}/goods/{goodId}/cancel": {
|
|
229
|
+
post: { operationId: "cancel", parameters: [param("orderId"), param("goodId")], responses: ok },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
Order: { type: "object", properties: { id: { type: "string" }, goods: { type: "array", items: { $ref: "#/components/schemas/Good" } } }, required: ["id"] },
|
|
234
|
+
Good: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
const cancel = buildToolSurface(d).find((t) => t.name.includes("cancel"))!;
|
|
238
|
+
const goodHint = cancel.producers.find((p) => p.param === "goodId");
|
|
239
|
+
expect(goodHint).toBeDefined();
|
|
240
|
+
expect(goodHint!.field).toBe("goods[].id"); // the nested path to the id
|
|
241
|
+
expect(goodHint!.tool).toContain("orders"); // produced by an orders read
|
|
242
|
+
});
|
|
243
|
+
});
|