swaggertools-mcp 1.0.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.
@@ -0,0 +1,143 @@
1
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
2
+ export type MatchMode = 'exact' | 'fuzzy';
3
+ export interface ApiError {
4
+ code: 'OPENAPI_FETCH_ERROR' | 'OPENAPI_PARSE_ERROR' | 'DOC_NOT_FOUND' | 'OPERATION_NOT_FOUND' | 'SCHEMA_NOT_FOUND' | 'VALIDATION_ERROR';
5
+ message: string;
6
+ details?: Record<string, unknown>;
7
+ }
8
+ export interface ApiEnvelope<T> {
9
+ ok: boolean;
10
+ data: T | null;
11
+ meta?: Record<string, unknown>;
12
+ error: ApiError | null;
13
+ }
14
+ export interface OperationSummary {
15
+ operationId?: string;
16
+ method: HttpMethod;
17
+ path: string;
18
+ summary?: string;
19
+ tags?: string[];
20
+ }
21
+ export interface ParameterItem {
22
+ name: string;
23
+ in: 'path' | 'query' | 'header' | 'cookie';
24
+ required?: boolean;
25
+ description?: string;
26
+ schema?: Record<string, unknown>;
27
+ example?: unknown;
28
+ }
29
+ export interface MediaTypeObject {
30
+ schema?: Record<string, unknown>;
31
+ example?: unknown;
32
+ examples?: Record<string, unknown>;
33
+ }
34
+ export interface RequestBodyItem {
35
+ required?: boolean;
36
+ description?: string;
37
+ content: Record<string, MediaTypeObject>;
38
+ }
39
+ export interface ResponseItem {
40
+ statusCode: string;
41
+ description?: string;
42
+ content?: Record<string, MediaTypeObject>;
43
+ }
44
+ export interface OperationDetail extends OperationSummary {
45
+ description?: string;
46
+ parameters: ParameterItem[];
47
+ requestBody?: RequestBodyItem;
48
+ responses: ResponseItem[];
49
+ security?: Array<Record<string, string[]>>;
50
+ }
51
+ export interface OperationCandidate extends OperationSummary {
52
+ score: number;
53
+ matchedBy: string[];
54
+ }
55
+ export interface ToolOpenApiLoadInput {
56
+ url: string;
57
+ headers?: Record<string, string>;
58
+ forceRefresh?: boolean;
59
+ }
60
+ export interface ToolOpenApiLoadOutput {
61
+ docId: string;
62
+ title?: string;
63
+ version?: string;
64
+ servers?: string[];
65
+ stats: {
66
+ operationCount: number;
67
+ schemaCount: number;
68
+ };
69
+ }
70
+ export interface ToolListOperationsInput {
71
+ docId: string;
72
+ tag?: string;
73
+ method?: HttpMethod;
74
+ keyword?: string;
75
+ page?: number;
76
+ pageSize?: number;
77
+ }
78
+ export interface ToolListOperationsOutput {
79
+ items: OperationSummary[];
80
+ page: number;
81
+ pageSize: number;
82
+ total: number;
83
+ }
84
+ export interface ToolResolveOperationInput {
85
+ docId: string;
86
+ query: string;
87
+ method?: HttpMethod;
88
+ tag?: string;
89
+ topK?: number;
90
+ }
91
+ export interface ToolResolveOperationOutput {
92
+ bestMatch?: OperationCandidate;
93
+ candidates: OperationCandidate[];
94
+ }
95
+ export interface ToolGetOperationInput {
96
+ docId: string;
97
+ operationId?: string;
98
+ method?: HttpMethod;
99
+ path?: string;
100
+ matchMode?: MatchMode;
101
+ query?: string;
102
+ topK?: number;
103
+ minScore?: number;
104
+ resolveRefs?: boolean;
105
+ }
106
+ export interface ToolGetOperationOutput {
107
+ matchMode: MatchMode;
108
+ matched?: OperationCandidate;
109
+ operation?: OperationDetail;
110
+ candidates?: OperationCandidate[];
111
+ }
112
+ export interface ToolGetSchemaInput {
113
+ docId: string;
114
+ schemaName: string;
115
+ resolveRefs?: boolean;
116
+ }
117
+ export interface ToolGetSchemaOutput {
118
+ schemaName: string;
119
+ schema: Record<string, unknown>;
120
+ usedByOperations?: Array<Pick<OperationSummary, 'operationId' | 'method' | 'path'>>;
121
+ }
122
+ export interface SwaggerMcpTools {
123
+ 'openapi.load': {
124
+ input: ToolOpenApiLoadInput;
125
+ output: ApiEnvelope<ToolOpenApiLoadOutput>;
126
+ };
127
+ 'openapi.list_operations': {
128
+ input: ToolListOperationsInput;
129
+ output: ApiEnvelope<ToolListOperationsOutput>;
130
+ };
131
+ 'openapi.resolve_operation': {
132
+ input: ToolResolveOperationInput;
133
+ output: ApiEnvelope<ToolResolveOperationOutput>;
134
+ };
135
+ 'openapi.get_operation': {
136
+ input: ToolGetOperationInput;
137
+ output: ApiEnvelope<ToolGetOperationOutput>;
138
+ };
139
+ 'openapi.get_schema': {
140
+ input: ToolGetSchemaInput;
141
+ output: ApiEnvelope<ToolGetSchemaOutput>;
142
+ };
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,243 @@
1
+ # Swagger MCP Design (V1)
2
+
3
+ ## 1. Goal
4
+
5
+ Build an MCP server that accepts a user-provided OpenAPI URL (for example `http://dev.manage.zw.uav.sczlcq.com/v3/api-docs`) and exposes stable tools for:
6
+
7
+ - loading OpenAPI docs
8
+ - browsing operations
9
+ - inspecting request/response schemas
10
+ - resolving incomplete or fuzzy operation input
11
+
12
+ ## 2. Scope
13
+
14
+ ### P0 (must-have)
15
+
16
+ 1. `openapi.load`
17
+ 2. `openapi.list_operations`
18
+ 3. `openapi.get_operation` (exact + fuzzy modes)
19
+ 4. `openapi.resolve_operation` (candidate resolver for incomplete paths)
20
+ 5. `openapi.get_schema`
21
+
22
+ ### P1 (nice-to-have)
23
+
24
+ 1. `openapi.search` (cross-field keyword search)
25
+ 2. `openapi.validate_request_example`
26
+ 3. `openapi.mock_response_example`
27
+
28
+ ## 3. Core Concepts
29
+
30
+ ### 3.1 Document Session
31
+
32
+ - Each loaded OpenAPI file is assigned a `docId`.
33
+ - A process can hold multiple documents.
34
+ - In-memory cache with TTL (default: 10 minutes).
35
+ - Optional `forceRefresh` to bypass cache.
36
+
37
+ ### 3.2 Indexing
38
+
39
+ Build indexes during load:
40
+
41
+ - `operationId -> operation`
42
+ - `METHOD + path -> operation`
43
+ - `tag -> operations[]`
44
+ - `schemaName -> schema`
45
+
46
+ ### 3.3 Normalized Operation Output
47
+
48
+ All operation details should be normalized into:
49
+
50
+ - `parameters`: path/query/header/cookie
51
+ - `requestBody`: by content-type
52
+ - `responses`: by status code and content-type
53
+ - `security`: auth requirements
54
+
55
+ ## 4. Fuzzy Resolution (P0)
56
+
57
+ Many users provide partial or inaccurate paths. Fuzzy matching is required.
58
+
59
+ ### 4.1 Match Inputs
60
+
61
+ - partial path (example: `/user/li`)
62
+ - natural keyword (example: `user list`)
63
+ - optional method constraint (`GET`, `POST`, ...)
64
+
65
+ ### 4.2 Match Strategy
66
+
67
+ Compute weighted score from:
68
+
69
+ 1. path prefix match (high weight)
70
+ 2. segment overlap ratio
71
+ 3. path parameter tolerance (`{id}` treated as wildcard)
72
+ 4. edit distance for typo tolerance
73
+ 5. summary/tag keyword hits
74
+
75
+ ### 4.3 Match Behavior
76
+
77
+ - clear winner: return `bestMatch`
78
+ - close ties: return `candidates` sorted by score
79
+ - no match: return top 3 nearest hints
80
+
81
+ ## 5. Tool List and Descriptor Draft
82
+
83
+ ## 5.1 `openapi.load`
84
+
85
+ Purpose: fetch and parse remote OpenAPI document.
86
+
87
+ Input:
88
+
89
+ - `url: string`
90
+ - `headers?: Record<string,string>`
91
+ - `forceRefresh?: boolean`
92
+
93
+ Output:
94
+
95
+ - `docId`
96
+ - `title`
97
+ - `version`
98
+ - `servers[]`
99
+ - `stats` (`operationCount`, `schemaCount`)
100
+
101
+ ## 5.2 `openapi.list_operations`
102
+
103
+ Purpose: list operations with lightweight filters.
104
+
105
+ Input:
106
+
107
+ - `docId: string`
108
+ - `tag?: string`
109
+ - `method?: HttpMethod`
110
+ - `keyword?: string`
111
+ - `page?: number`
112
+ - `pageSize?: number`
113
+
114
+ Output:
115
+
116
+ - paged operation summaries (`operationId`, `method`, `path`, `summary`, `tags`)
117
+
118
+ ## 5.3 `openapi.get_operation`
119
+
120
+ Purpose: get full operation details by exact or fuzzy match.
121
+
122
+ Input:
123
+
124
+ - `docId: string`
125
+ - `operationId?: string`
126
+ - `method?: HttpMethod`
127
+ - `path?: string`
128
+ - `matchMode?: "exact" | "fuzzy"` (default `exact`)
129
+ - `query?: string` (required when fuzzy mode is used)
130
+ - `topK?: number` (default 5)
131
+ - `minScore?: number` (default 0.55)
132
+ - `resolveRefs?: boolean` (default true)
133
+
134
+ Resolution rule:
135
+
136
+ - exact mode: requires `operationId` or (`method` + `path`)
137
+ - fuzzy mode: requires `query`, optional `method` filter
138
+
139
+ ## 5.4 `openapi.resolve_operation`
140
+
141
+ Purpose: resolve incomplete path/keyword into operation candidates.
142
+
143
+ Input:
144
+
145
+ - `docId: string`
146
+ - `query: string`
147
+ - `method?: HttpMethod`
148
+ - `tag?: string`
149
+ - `topK?: number` (1..20, default 5)
150
+
151
+ Output:
152
+
153
+ - `bestMatch?`
154
+ - `candidates[]` with `score` and `matchedBy`
155
+
156
+ ## 5.5 `openapi.get_schema`
157
+
158
+ Purpose: fetch a schema in `components.schemas`.
159
+
160
+ Input:
161
+
162
+ - `docId: string`
163
+ - `schemaName: string`
164
+ - `resolveRefs?: boolean`
165
+
166
+ Output:
167
+
168
+ - schema object
169
+ - optional references (`usedByOperations[]`)
170
+
171
+ ## 6. Unified Response Envelope
172
+
173
+ Success:
174
+
175
+ ```json
176
+ {
177
+ "ok": true,
178
+ "data": {},
179
+ "meta": {
180
+ "docId": "doc_xxx",
181
+ "cached": true
182
+ },
183
+ "error": null
184
+ }
185
+ ```
186
+
187
+ Failure:
188
+
189
+ ```json
190
+ {
191
+ "ok": false,
192
+ "data": null,
193
+ "meta": {},
194
+ "error": {
195
+ "code": "OPENAPI_PARSE_ERROR",
196
+ "message": "Invalid OpenAPI document",
197
+ "details": {}
198
+ }
199
+ }
200
+ ```
201
+
202
+ Recommended error codes:
203
+
204
+ - `OPENAPI_FETCH_ERROR`
205
+ - `OPENAPI_PARSE_ERROR`
206
+ - `DOC_NOT_FOUND`
207
+ - `OPERATION_NOT_FOUND`
208
+ - `SCHEMA_NOT_FOUND`
209
+ - `VALIDATION_ERROR`
210
+
211
+ ## 7. Non-functional Requirements
212
+
213
+ 1. Security
214
+ - allow only `http` / `https`
215
+ - timeout and max payload limits
216
+ - optional domain allowlist
217
+ - SSRF protections (block private network targets)
218
+
219
+ 2. Performance
220
+ - cache parsed docs and indexes
221
+ - paginate list/search results
222
+ - cap max candidate count for fuzzy tools
223
+
224
+ 3. Observability
225
+ - structured logs with `docId`, `toolName`, duration, cache hit/miss
226
+
227
+ ## 8. Recommended Implementation Stack
228
+
229
+ - parser: `@apidevtools/swagger-parser`
230
+ - validation: `ajv`
231
+ - schema/type validation: `zod`
232
+ - http client: `undici` or `axios`
233
+ - mcp sdk: `@modelcontextprotocol/sdk`
234
+
235
+ ## 9. Delivery Order
236
+
237
+ 1. `openapi.load`
238
+ 2. `openapi.list_operations`
239
+ 3. `openapi.resolve_operation`
240
+ 4. `openapi.get_operation` (exact + fuzzy)
241
+ 5. `openapi.get_schema`
242
+ 6. P1 tools
243
+
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "swaggertools-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for querying OpenAPI/Swagger documents, including fuzzy operation matching.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "dev": "tsx src/index.ts",
9
+ "build": "tsc -p tsconfig.json",
10
+ "typecheck": "tsc -p tsconfig.json --noEmit",
11
+ "start": "node dist/index.js"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "packageManager": "pnpm@10.12.1",
17
+ "dependencies": {
18
+ "@apidevtools/swagger-parser": "^12.1.0",
19
+ "@modelcontextprotocol/sdk": "^1.27.1",
20
+ "dotenv": "^17.3.1",
21
+ "undici": "^7.22.0",
22
+ "zod": "^4.3.6"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^25.3.2",
26
+ "tsx": "^4.21.0",
27
+ "typescript": "^5.9.3"
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { config as dotenvConfig } from 'dotenv';
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import * as z from 'zod/v4';
8
+ import { OpenApiRegistry } from './openapi-service.js';
9
+ import type { ApiError } from './tool-types.js';
10
+
11
+ const loadedEnvFiles = loadEnvFiles();
12
+ const server = new McpServer({
13
+ name: 'swagger-mcp',
14
+ version: '1.0.0',
15
+ });
16
+
17
+ const registry = new OpenApiRegistry();
18
+
19
+ server.registerTool(
20
+ 'openapi.load',
21
+ {
22
+ description: 'Load and index a remote OpenAPI document URL.',
23
+ inputSchema: {
24
+ url: z.string().url().optional().describe('OpenAPI JSON/YAML URL. If omitted, read from .env'),
25
+ headers: z.record(z.string(), z.string()).optional().describe('Optional HTTP headers'),
26
+ forceRefresh: z.boolean().optional().describe('Bypass cache and reload'),
27
+ },
28
+ },
29
+ async ({ url, headers, forceRefresh }) => {
30
+ try {
31
+ const finalUrl = url ?? getDefaultOpenApiUrl();
32
+ if (!finalUrl) {
33
+ return failure({
34
+ code: 'VALIDATION_ERROR',
35
+ message: `Missing OpenAPI URL. Provide url input or set OPENAPI_DEFAULT_URL/OPENAPI_URL in .env (searched: ${loadedEnvFiles.join(', ') || 'none'})`,
36
+ });
37
+ }
38
+ const result = await registry.load(finalUrl, headers, forceRefresh ?? false);
39
+ return success(
40
+ {
41
+ docId: result.docId,
42
+ title: result.title,
43
+ version: result.version,
44
+ servers: result.servers,
45
+ stats: {
46
+ operationCount: result.operationCount,
47
+ schemaCount: result.schemaCount,
48
+ },
49
+ },
50
+ {
51
+ docId: result.docId,
52
+ cached: result.cached,
53
+ },
54
+ );
55
+ } catch (error) {
56
+ return failure(mapError(error));
57
+ }
58
+ },
59
+ );
60
+
61
+ server.registerTool(
62
+ 'openapi.list_operations',
63
+ {
64
+ description: 'List operations in a loaded OpenAPI document with basic filters.',
65
+ inputSchema: {
66
+ docId: z.string(),
67
+ tag: z.string().optional(),
68
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).optional(),
69
+ keyword: z.string().optional(),
70
+ page: z.number().int().positive().optional(),
71
+ pageSize: z.number().int().positive().max(200).optional(),
72
+ },
73
+ },
74
+ async ({ docId, tag, method, keyword, page, pageSize }) => {
75
+ try {
76
+ const resolvedDocId = await resolveDocId(docId);
77
+ const result = registry.listOperations({ docId: resolvedDocId, tag, method, keyword, page, pageSize });
78
+ return success(result, { docId: resolvedDocId, requestedDocId: docId });
79
+ } catch (error) {
80
+ return failure(mapError(error), { docId });
81
+ }
82
+ },
83
+ );
84
+
85
+ server.registerTool(
86
+ 'openapi.resolve_operation',
87
+ {
88
+ description: 'Resolve incomplete path/keyword into operation candidates.',
89
+ inputSchema: {
90
+ docId: z.string(),
91
+ query: z.string().min(1),
92
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).optional(),
93
+ tag: z.string().optional(),
94
+ topK: z.number().int().min(1).max(20).optional(),
95
+ },
96
+ },
97
+ async ({ docId, query, method, tag, topK }) => {
98
+ try {
99
+ const resolvedDocId = await resolveDocId(docId);
100
+ const result = registry.resolveOperation(resolvedDocId, query, { method, tag, topK });
101
+ return success(result, { docId: resolvedDocId, requestedDocId: docId });
102
+ } catch (error) {
103
+ return failure(mapError(error), { docId });
104
+ }
105
+ },
106
+ );
107
+
108
+ server.registerTool(
109
+ 'openapi.get_operation',
110
+ {
111
+ description: 'Get operation details by exact or fuzzy mode, including parameters and responses.',
112
+ inputSchema: {
113
+ docId: z.string(),
114
+ operationId: z.string().optional(),
115
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).optional(),
116
+ path: z.string().optional(),
117
+ matchMode: z.enum(['exact', 'fuzzy']).optional(),
118
+ query: z.string().optional(),
119
+ topK: z.number().int().min(1).max(20).optional(),
120
+ minScore: z.number().min(0).max(1).optional(),
121
+ resolveRefs: z.boolean().optional(),
122
+ },
123
+ },
124
+ async (args) => {
125
+ try {
126
+ const resolvedDocId = await resolveDocId(args.docId);
127
+ const result = registry.getOperation({ ...args, docId: resolvedDocId });
128
+ const matchMode = args.matchMode ?? 'exact';
129
+ if (!result.operation) {
130
+ return failure(
131
+ {
132
+ code: 'OPERATION_NOT_FOUND',
133
+ message: 'No operation matched the input',
134
+ details: { candidates: result.candidates ?? [] },
135
+ },
136
+ { docId: resolvedDocId, requestedDocId: args.docId, matchMode },
137
+ );
138
+ }
139
+ return success(
140
+ {
141
+ matchMode,
142
+ matched: result.matched,
143
+ candidates: result.candidates,
144
+ operation: result.operation,
145
+ },
146
+ { docId: resolvedDocId, requestedDocId: args.docId, matchMode },
147
+ );
148
+ } catch (error) {
149
+ return failure(mapError(error), { docId: args.docId });
150
+ }
151
+ },
152
+ );
153
+
154
+ server.registerTool(
155
+ 'openapi.get_schema',
156
+ {
157
+ description: 'Get a schema from components.schemas by schemaName.',
158
+ inputSchema: {
159
+ docId: z.string(),
160
+ schemaName: z.string().min(1),
161
+ resolveRefs: z.boolean().optional(),
162
+ },
163
+ },
164
+ async ({ docId, schemaName, resolveRefs }) => {
165
+ try {
166
+ const resolvedDocId = await resolveDocId(docId);
167
+ const result = registry.getSchema(resolvedDocId, schemaName, resolveRefs ?? true);
168
+ return success(result, { docId: resolvedDocId, requestedDocId: docId });
169
+ } catch (error) {
170
+ return failure(mapError(error), { docId, schemaName });
171
+ }
172
+ },
173
+ );
174
+
175
+ async function main(): Promise<void> {
176
+ const transport = new StdioServerTransport();
177
+ await server.connect(transport);
178
+ console.error('swagger-mcp server running on stdio');
179
+ }
180
+
181
+ main().catch((error) => {
182
+ console.error('swagger-mcp fatal error:', error);
183
+ process.exit(1);
184
+ });
185
+
186
+ async function resolveDocId(docId: string): Promise<string> {
187
+ if (docId !== 'default') {
188
+ return docId;
189
+ }
190
+ const latest = registry.getLatestDocId();
191
+ if (latest) {
192
+ return latest;
193
+ }
194
+ const defaultUrl = getDefaultOpenApiUrl();
195
+ if (!defaultUrl) {
196
+ throw new Error(
197
+ `Document not found: default. Set OPENAPI_DEFAULT_URL/OPENAPI_URL in AI workspace .env or pass explicit docId from openapi.load`,
198
+ );
199
+ }
200
+ const loaded = await registry.load(defaultUrl);
201
+ return loaded.docId;
202
+ }
203
+
204
+ function getDefaultOpenApiUrl(): string | undefined {
205
+ const raw = process.env.OPENAPI_DEFAULT_URL ?? process.env.OPENAPI_URL;
206
+ const normalized = raw?.trim();
207
+ return normalized ? normalized : undefined;
208
+ }
209
+
210
+ function loadEnvFiles(): string[] {
211
+ const found: string[] = [];
212
+ const candidates = new Set<string>();
213
+ const explicitFile = process.env.OPENAPI_ENV_FILE?.trim();
214
+ const explicitDir = process.env.OPENAPI_ENV_DIR?.trim();
215
+ if (explicitFile) {
216
+ candidates.add(path.resolve(explicitFile));
217
+ }
218
+ if (explicitDir) {
219
+ candidates.add(path.resolve(explicitDir, '.env'));
220
+ }
221
+ candidates.add(path.resolve(process.cwd(), '.env'));
222
+ const initCwd = process.env.INIT_CWD?.trim();
223
+ if (initCwd) {
224
+ candidates.add(path.resolve(initCwd, '.env'));
225
+ }
226
+
227
+ for (const envFile of candidates) {
228
+ if (!existsSync(envFile)) continue;
229
+ dotenvConfig({ path: envFile, override: false });
230
+ found.push(envFile);
231
+ }
232
+ return found;
233
+ }
234
+
235
+ function success(data: unknown, meta: Record<string, unknown> = {}) {
236
+ const envelope = { ok: true, data, meta, error: null };
237
+ return {
238
+ content: [{ type: 'text' as const, text: safeJson(envelope) }],
239
+ structuredContent: envelope,
240
+ };
241
+ }
242
+
243
+ function failure(error: ApiError, meta: Record<string, unknown> = {}) {
244
+ const envelope = { ok: false, data: null, meta, error };
245
+ return {
246
+ content: [{ type: 'text' as const, text: safeJson(envelope) }],
247
+ structuredContent: envelope,
248
+ isError: true,
249
+ };
250
+ }
251
+
252
+ function mapError(raw: unknown): ApiError {
253
+ const message = raw instanceof Error ? raw.message : String(raw);
254
+ if (message.includes('Document not found')) {
255
+ return { code: 'DOC_NOT_FOUND', message };
256
+ }
257
+ if (message.includes('Schema not found')) {
258
+ return { code: 'SCHEMA_NOT_FOUND', message };
259
+ }
260
+ if (message.includes('Operation not found') || message.includes('exact mode requires') || message.includes('query is required')) {
261
+ return { code: 'OPERATION_NOT_FOUND', message };
262
+ }
263
+ if (message.includes('Only http/https') || message.includes('Invalid URL') || message.includes('blocked')) {
264
+ return { code: 'VALIDATION_ERROR', message };
265
+ }
266
+ if (message.toLowerCase().includes('parse')) {
267
+ return { code: 'OPENAPI_PARSE_ERROR', message };
268
+ }
269
+ if (message.toLowerCase().includes('fetch') || message.toLowerCase().includes('network') || message.toLowerCase().includes('timeout')) {
270
+ return { code: 'OPENAPI_FETCH_ERROR', message };
271
+ }
272
+ return { code: 'VALIDATION_ERROR', message };
273
+ }
274
+
275
+ function safeJson(value: unknown): string {
276
+ const seen = new WeakSet<object>();
277
+ return JSON.stringify(
278
+ value,
279
+ (_, currentValue) => {
280
+ if (currentValue && typeof currentValue === 'object') {
281
+ if (seen.has(currentValue)) {
282
+ return '[Circular]';
283
+ }
284
+ seen.add(currentValue);
285
+ }
286
+ return currentValue;
287
+ },
288
+ 2,
289
+ );
290
+ }