@emreyc/swagger-mcp 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/dist/index.js +649 -0
  4. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emre Colakoglu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # swagger-mcp
2
+
3
+ A read-only [Model Context Protocol](https://modelcontextprotocol.io) server for
4
+ **navigating** an OpenAPI / Swagger specification. It gives a coding agent tools
5
+ to explore a large API's documentation — search endpoints, read parameters and
6
+ schemas, inspect auth — without dumping the whole spec into context.
7
+
8
+ It **never calls the described API.** It only reads its documentation.
9
+
10
+ Works with any MCP-capable harness (Claude Code, Cursor, Cline, Windsurf,
11
+ Zed, opencode, …) over stdio.
12
+
13
+ ## Usage
14
+
15
+ Add it to your MCP client config. No install step — `npx` fetches it on demand:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "swagger-mcp": {
21
+ "command": "npx",
22
+ "args": ["-y", "@emreyc/swagger-mcp@latest"],
23
+ "env": {
24
+ "API_DOCS_URL": "https://api.example.com/swagger.json"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ `API_DOCS_URL` is the only configuration — a URL (or local path) to a Swagger 2.0
32
+ or OpenAPI 3.x document. Internal/localhost URLs (e.g. Spring Boot's
33
+ `http://localhost:8080/v3/api-docs`) are supported.
34
+
35
+ ## Tools
36
+
37
+ Designed for progressive disclosure — orient, then discover, then drill down —
38
+ so the agent loads only what it needs.
39
+
40
+ | Tool | Purpose |
41
+ | --- | --- |
42
+ | `get_api_overview` | Title, version, servers, global security, counts. Start here. |
43
+ | `list_tags` | Tags (logical groups) with endpoint counts. |
44
+ | `list_endpoints` | Compact index of endpoints; optional `tag` filter. |
45
+ | `search_endpoints` | Substring search over path/summary/description/params. |
46
+ | `list_schemas` | Named schemas with one-line descriptions. |
47
+ | `search_schemas` | Substring search over schema names, fields, descriptions. |
48
+ | `get_endpoint` | Full detail for one `method`+`path`: params, request/response schemas, auth. |
49
+ | `get_schema` | Resolve one named schema. |
50
+ | `get_auth` | Security schemes in full (OAuth2 flows, scopes, API-key locations). |
51
+
52
+ Swagger 2.0 and OpenAPI 3.x are normalized into one consistent output shape.
53
+ Nested named schemas are shown as `{ "$schema": "Name" }` markers — call
54
+ `get_schema` to expand them. This keeps every response bounded and is safe with
55
+ circular schemas.
56
+
57
+ ## Development
58
+
59
+ ```bash
60
+ npm install
61
+ npm run typecheck
62
+ npm test
63
+ npm run build
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,649 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { z } from "zod";
9
+
10
+ // src/spec/loader.ts
11
+ import SwaggerParser from "@apidevtools/swagger-parser";
12
+
13
+ // src/spec/normalize.ts
14
+ var HTTP_METHODS = [
15
+ "get",
16
+ "put",
17
+ "post",
18
+ "delete",
19
+ "options",
20
+ "head",
21
+ "patch",
22
+ "trace"
23
+ ];
24
+ function isOpenApi3(spec) {
25
+ return typeof spec.openapi === "string" && spec.openapi.startsWith("3.");
26
+ }
27
+ function schemaFromV2Param(param) {
28
+ const keys = [
29
+ "type",
30
+ "format",
31
+ "enum",
32
+ "items",
33
+ "default",
34
+ "minimum",
35
+ "maximum",
36
+ "minLength",
37
+ "maxLength",
38
+ "pattern",
39
+ "collectionFormat"
40
+ ];
41
+ const schema = {};
42
+ for (const key of keys) {
43
+ if (param[key] !== void 0) schema[key] = param[key];
44
+ }
45
+ return schema;
46
+ }
47
+ function normalizeInfo(spec) {
48
+ const info = spec.info ?? {};
49
+ const externalDocs = spec.externalDocs ? { url: spec.externalDocs.url, description: spec.externalDocs.description } : void 0;
50
+ return {
51
+ title: info.title ?? "(untitled)",
52
+ version: info.version ?? "(unversioned)",
53
+ description: info.description,
54
+ externalDocs
55
+ };
56
+ }
57
+ function normalizeServers(spec, v3) {
58
+ if (v3) {
59
+ const servers = Array.isArray(spec.servers) ? spec.servers : [];
60
+ return servers.map((s) => s.url).filter((u) => typeof u === "string");
61
+ }
62
+ if (typeof spec.host !== "string" || spec.host.length === 0) return [];
63
+ const schemes = Array.isArray(spec.schemes) && spec.schemes.length ? spec.schemes : ["https"];
64
+ const basePath = typeof spec.basePath === "string" ? spec.basePath : "";
65
+ return schemes.map((scheme) => `${scheme}://${spec.host}${basePath}`);
66
+ }
67
+ function normalizeSecuritySchemes(spec, v3) {
68
+ const source = v3 ? spec.components?.securitySchemes ?? {} : spec.securityDefinitions ?? {};
69
+ return Object.entries(source).map(([name, raw]) => {
70
+ const scheme = raw;
71
+ return {
72
+ name,
73
+ type: scheme.type ?? "unknown",
74
+ description: scheme.description,
75
+ raw: scheme
76
+ };
77
+ });
78
+ }
79
+ function normalizeParams(raw, v3) {
80
+ const params = [];
81
+ let v2Body;
82
+ const v2FormData = [];
83
+ for (const p of raw) {
84
+ if (!v3 && p.in === "body") {
85
+ v2Body = p;
86
+ continue;
87
+ }
88
+ if (!v3 && p.in === "formData") {
89
+ v2FormData.push(p);
90
+ continue;
91
+ }
92
+ params.push({
93
+ name: p.name,
94
+ in: p.in,
95
+ required: p.required ?? p.in === "path",
96
+ description: p.description,
97
+ deprecated: p.deprecated,
98
+ schema: v3 ? p.schema ?? {} : schemaFromV2Param(p)
99
+ });
100
+ }
101
+ return { params, v2Body, v2FormData };
102
+ }
103
+ function normalizeRequestBody(operation, v3, v2Body, v2FormData) {
104
+ if (v3) {
105
+ const rb = operation.requestBody;
106
+ if (!rb) return void 0;
107
+ const content = {};
108
+ for (const [mediaType, media] of Object.entries(rb.content ?? {})) {
109
+ content[mediaType] = media.schema ?? {};
110
+ }
111
+ return {
112
+ description: rb.description,
113
+ required: rb.required ?? false,
114
+ content
115
+ };
116
+ }
117
+ if (v2Body) {
118
+ const mediaType = (operation.consumes ?? [])[0] ?? "application/json";
119
+ return {
120
+ description: v2Body.description,
121
+ required: v2Body.required ?? false,
122
+ content: { [mediaType]: v2Body.schema ?? {} }
123
+ };
124
+ }
125
+ if (v2FormData.length > 0) {
126
+ const properties = {};
127
+ const required = [];
128
+ for (const p of v2FormData) {
129
+ properties[p.name] = schemaFromV2Param(p);
130
+ if (p.required) required.push(p.name);
131
+ }
132
+ const mediaType = (operation.consumes ?? [])[0] ?? "application/x-www-form-urlencoded";
133
+ const schema = { type: "object", properties };
134
+ if (required.length) schema.required = required;
135
+ return { required: required.length > 0, content: { [mediaType]: schema } };
136
+ }
137
+ return void 0;
138
+ }
139
+ function normalizeResponses(operation, v3) {
140
+ const responses = operation.responses ?? {};
141
+ return Object.entries(responses).map(([status, raw]) => {
142
+ const res = raw;
143
+ let content;
144
+ if (v3 && res.content) {
145
+ content = {};
146
+ for (const [mediaType, media] of Object.entries(res.content)) {
147
+ content[mediaType] = media.schema ?? {};
148
+ }
149
+ } else if (!v3 && res.schema) {
150
+ const mediaType = (operation.produces ?? [])[0] ?? "application/json";
151
+ content = { [mediaType]: res.schema };
152
+ }
153
+ return { status, description: res.description, content };
154
+ });
155
+ }
156
+ function mergeParams(pathParams, opParams) {
157
+ const byKey = /* @__PURE__ */ new Map();
158
+ for (const p of pathParams) byKey.set(`${p.in}:${p.name}`, p);
159
+ for (const p of opParams) byKey.set(`${p.in}:${p.name}`, p);
160
+ return [...byKey.values()];
161
+ }
162
+ function normalizeSpec(raw) {
163
+ const spec = raw;
164
+ const v3 = isOpenApi3(spec);
165
+ const schemas = v3 ? spec.components?.schemas ?? {} : spec.definitions ?? {};
166
+ const globalSecurity = Array.isArray(spec.security) ? spec.security : [];
167
+ const endpoints = [];
168
+ const paths = spec.paths ?? {};
169
+ for (const [path, pathItemRaw] of Object.entries(paths)) {
170
+ const pathItem = pathItemRaw;
171
+ const pathParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
172
+ for (const method of HTTP_METHODS) {
173
+ const operation = pathItem[method];
174
+ if (!operation) continue;
175
+ const merged = mergeParams(
176
+ pathParams,
177
+ Array.isArray(operation.parameters) ? operation.parameters : []
178
+ );
179
+ const { params, v2Body, v2FormData } = normalizeParams(merged, v3);
180
+ endpoints.push({
181
+ method: method.toUpperCase(),
182
+ path,
183
+ operationId: operation.operationId,
184
+ summary: operation.summary,
185
+ description: operation.description,
186
+ tags: Array.isArray(operation.tags) ? operation.tags : [],
187
+ deprecated: operation.deprecated ?? false,
188
+ parameters: params,
189
+ requestBody: normalizeRequestBody(operation, v3, v2Body, v2FormData),
190
+ responses: normalizeResponses(operation, v3),
191
+ security: Array.isArray(operation.security) ? operation.security : void 0
192
+ });
193
+ }
194
+ }
195
+ const tags = normalizeTags(spec, endpoints);
196
+ return {
197
+ sourceVersion: v3 ? spec.openapi : spec.swagger ?? "2.0",
198
+ info: normalizeInfo(spec),
199
+ servers: normalizeServers(spec, v3),
200
+ globalSecurity,
201
+ securitySchemes: normalizeSecuritySchemes(spec, v3),
202
+ tags,
203
+ endpoints,
204
+ schemas
205
+ };
206
+ }
207
+ function normalizeTags(spec, endpoints) {
208
+ const counts = /* @__PURE__ */ new Map();
209
+ for (const ep of endpoints) {
210
+ for (const tag of ep.tags) counts.set(tag, (counts.get(tag) ?? 0) + 1);
211
+ }
212
+ const declared = (Array.isArray(spec.tags) ? spec.tags : []).map((t) => ({
213
+ name: t.name,
214
+ description: t.description,
215
+ endpointCount: counts.get(t.name) ?? 0
216
+ }));
217
+ const declaredNames = new Set(declared.map((t) => t.name));
218
+ const undeclared = [...counts.keys()].filter((name) => !declaredNames.has(name)).map((name) => ({ name, endpointCount: counts.get(name) ?? 0 }));
219
+ return [...declared, ...undeclared];
220
+ }
221
+
222
+ // src/spec/loader.ts
223
+ var SpecLoadError = class extends Error {
224
+ };
225
+ var cache;
226
+ async function fetchAndNormalize(docsUrl) {
227
+ let bundled;
228
+ try {
229
+ bundled = await SwaggerParser.bundle(docsUrl, {
230
+ resolve: { http: { safeUrlResolver: false } }
231
+ });
232
+ } catch (err) {
233
+ const reason = err instanceof Error ? err.message : String(err);
234
+ throw new SpecLoadError(
235
+ `Failed to load API spec from ${docsUrl}: ${reason}`
236
+ );
237
+ }
238
+ return normalizeSpec(bundled);
239
+ }
240
+ function loadSpec(docsUrl) {
241
+ if (!cache) {
242
+ cache = fetchAndNormalize(docsUrl).catch((err) => {
243
+ cache = void 0;
244
+ throw err;
245
+ });
246
+ }
247
+ return cache;
248
+ }
249
+
250
+ // src/format.ts
251
+ var REF_PREFIXES = ["#/definitions/", "#/components/schemas/"];
252
+ function refName(node) {
253
+ if (!node || typeof node !== "object") return void 0;
254
+ const ref = node.$ref;
255
+ if (typeof ref !== "string") return void 0;
256
+ for (const prefix of REF_PREFIXES) {
257
+ if (ref.startsWith(prefix)) return ref.slice(prefix.length);
258
+ }
259
+ const parts = ref.split("/");
260
+ return parts[parts.length - 1];
261
+ }
262
+ function resolveShallow(node, schemas, remaining) {
263
+ if (Array.isArray(node)) {
264
+ return node.map((n) => resolveShallow(n, schemas, remaining));
265
+ }
266
+ if (node && typeof node === "object") {
267
+ const name = refName(node);
268
+ if (name !== void 0) {
269
+ if (remaining <= 0 || schemas[name] === void 0) {
270
+ return { $schema: name };
271
+ }
272
+ return resolveShallow(schemas[name], schemas, remaining - 1);
273
+ }
274
+ const out = {};
275
+ for (const [key, value] of Object.entries(node)) {
276
+ out[key] = resolveShallow(value, schemas, remaining);
277
+ }
278
+ return out;
279
+ }
280
+ return node;
281
+ }
282
+ function fence(value) {
283
+ return "```json\n" + JSON.stringify(value, null, 2) + "\n```";
284
+ }
285
+ function shortType(schema, schemas) {
286
+ if (!schema) return "\u2014";
287
+ const name = refName(schema);
288
+ if (name) return name;
289
+ if (typeof schema.$schema === "string") return schema.$schema;
290
+ if (schema.type === "array") {
291
+ return `${shortType(schema.items, schemas)}[]`;
292
+ }
293
+ if (typeof schema.type === "string") return schema.type;
294
+ if (schema.properties) return "object";
295
+ return "\u2014";
296
+ }
297
+ function endpointLine(ep) {
298
+ const summary = ep.summary ? ` \u2014 ${ep.summary}` : "";
299
+ const tags = ep.tags.length ? ` [${ep.tags.join(", ")}]` : "";
300
+ const deprecated = ep.deprecated ? " (deprecated)" : "";
301
+ return `${ep.method} ${ep.path}${summary}${tags}${deprecated}`;
302
+ }
303
+ function paramTable(params, schemas) {
304
+ if (params.length === 0) return "";
305
+ const rows = params.map((p) => {
306
+ const desc = (p.description ?? "").replace(/\n/g, " ");
307
+ return `| ${p.name} | ${p.in} | ${p.required ? "yes" : "no"} | ${shortType(p.schema, schemas)} | ${desc} |`;
308
+ });
309
+ return [
310
+ "**Parameters**",
311
+ "",
312
+ "| Name | In | Required | Type | Description |",
313
+ "| --- | --- | --- | --- | --- |",
314
+ ...rows
315
+ ].join("\n");
316
+ }
317
+ function securityLines(requirements) {
318
+ if (!requirements || requirements.length === 0) return "";
319
+ const parts = requirements.map(
320
+ (req) => Object.entries(req).map(
321
+ ([name, scopes]) => scopes.length ? `${name} (${scopes.join(", ")})` : name
322
+ ).join(" + ")
323
+ );
324
+ return `**Security:** ${parts.join(" OR ")}`;
325
+ }
326
+ function endpointDetail(ep, spec) {
327
+ const sections = [];
328
+ sections.push(`# ${ep.method} ${ep.path}`);
329
+ const meta = [];
330
+ if (ep.operationId) meta.push(`operationId: \`${ep.operationId}\``);
331
+ if (ep.tags.length) meta.push(`tags: ${ep.tags.join(", ")}`);
332
+ if (ep.deprecated) meta.push("**deprecated**");
333
+ if (meta.length) sections.push(meta.join(" \xB7 "));
334
+ if (ep.summary) sections.push(ep.summary);
335
+ if (ep.description) sections.push(ep.description);
336
+ const security = securityLines(ep.security ?? spec.globalSecurity);
337
+ if (security) sections.push(security);
338
+ const table = paramTable(ep.parameters, spec.schemas);
339
+ if (table) sections.push(table);
340
+ if (ep.requestBody) {
341
+ const rb = ep.requestBody;
342
+ const lines = [`**Request body**${rb.required ? " (required)" : ""}`];
343
+ if (rb.description) lines.push(rb.description);
344
+ for (const [mediaType, schema] of Object.entries(rb.content)) {
345
+ lines.push(`
346
+ _${mediaType}_`);
347
+ lines.push(fence(resolveShallow(schema, spec.schemas, 1)));
348
+ }
349
+ sections.push(lines.join("\n"));
350
+ }
351
+ if (ep.responses.length) {
352
+ const lines = ["**Responses**"];
353
+ for (const res of ep.responses) {
354
+ const desc = res.description ? ` \u2014 ${res.description}` : "";
355
+ lines.push(`
356
+ _${res.status}${desc}_`);
357
+ if (res.content) {
358
+ for (const [mediaType, schema] of Object.entries(res.content)) {
359
+ lines.push(`${mediaType}:`);
360
+ lines.push(fence(resolveShallow(schema, spec.schemas, 1)));
361
+ }
362
+ }
363
+ }
364
+ sections.push(lines.join("\n"));
365
+ }
366
+ return sections.join("\n\n");
367
+ }
368
+ function schemaLine(name, schema) {
369
+ const desc = typeof schema.description === "string" ? ` \u2014 ${schema.description.replace(/\n/g, " ")}` : "";
370
+ return `${name}${desc}`;
371
+ }
372
+ function schemaDetail(name, schemas) {
373
+ const resolved = resolveShallow(schemas[name], schemas, 0);
374
+ return `# ${name}
375
+
376
+ ${fence(resolved)}`;
377
+ }
378
+ function overview(spec) {
379
+ const sections = [];
380
+ sections.push(`# ${spec.info.title} (v${spec.info.version})`);
381
+ sections.push(`Spec version: ${spec.sourceVersion}`);
382
+ if (spec.info.description) sections.push(spec.info.description);
383
+ if (spec.servers.length) {
384
+ sections.push(
385
+ `**Servers**
386
+ ${spec.servers.map((s) => `- ${s}`).join("\n")}`
387
+ );
388
+ }
389
+ const security = securityLines(spec.globalSecurity);
390
+ if (security) sections.push(`${security} _(global default)_`);
391
+ if (spec.securitySchemes.length) {
392
+ sections.push(
393
+ `**Security schemes:** ${spec.securitySchemes.map((s) => `${s.name} (${s.type})`).join(", ")}`
394
+ );
395
+ }
396
+ if (spec.info.externalDocs) {
397
+ const d = spec.info.externalDocs;
398
+ sections.push(`**External docs:** ${d.description ?? ""} ${d.url}`.trim());
399
+ }
400
+ sections.push(
401
+ `**Counts:** ${spec.endpoints.length} endpoints \xB7 ${spec.tags.length} tags \xB7 ${Object.keys(spec.schemas).length} schemas`
402
+ );
403
+ return sections.join("\n\n");
404
+ }
405
+ function authDetail(scheme) {
406
+ const header = `## ${scheme.name} (${scheme.type})`;
407
+ const desc = scheme.description ? `
408
+
409
+ ${scheme.description}` : "";
410
+ return `${header}${desc}
411
+
412
+ ${fence(scheme.raw)}`;
413
+ }
414
+
415
+ // src/search.ts
416
+ function has(haystack, needle) {
417
+ return typeof haystack === "string" && haystack.toLowerCase().includes(needle);
418
+ }
419
+ function searchEndpoints(spec, query) {
420
+ const q = query.toLowerCase();
421
+ const scored = [];
422
+ for (const ep of spec.endpoints) {
423
+ let score = 0;
424
+ if (has(ep.path, q) || has(ep.operationId, q)) score = Math.max(score, 4);
425
+ if (has(ep.summary, q)) score = Math.max(score, 3);
426
+ if (ep.tags.some((t) => has(t, q))) score = Math.max(score, 2);
427
+ if (has(ep.description, q)) score = Math.max(score, 2);
428
+ if (ep.parameters.some((p) => has(p.name, q))) score = Math.max(score, 1);
429
+ if (score > 0) scored.push({ ep, score });
430
+ }
431
+ scored.sort((a, b) => b.score - a.score);
432
+ return scored.map((s) => s.ep);
433
+ }
434
+ function searchSchemas(spec, query) {
435
+ const q = query.toLowerCase();
436
+ const scored = [];
437
+ for (const [name, schema] of Object.entries(spec.schemas)) {
438
+ let score = 0;
439
+ if (has(name, q)) score = Math.max(score, 3);
440
+ const props = schema.properties && typeof schema.properties === "object" ? Object.keys(schema.properties) : [];
441
+ if (props.some((p) => p.toLowerCase().includes(q))) score = Math.max(score, 2);
442
+ if (has(schema.description, q)) {
443
+ score = Math.max(score, 1);
444
+ }
445
+ if (score > 0) scored.push({ name, schema, score });
446
+ }
447
+ scored.sort((a, b) => b.score - a.score);
448
+ return scored.map((s) => ({ name: s.name, schema: s.schema }));
449
+ }
450
+
451
+ // src/server.ts
452
+ var ok = (text) => ({ content: [{ type: "text", text }] });
453
+ var fail = (text) => ({
454
+ content: [{ type: "text", text }],
455
+ isError: true
456
+ });
457
+ var readOnly = { readOnlyHint: true, openWorldHint: false };
458
+ function findEndpoint(spec, method, path) {
459
+ const m = method.toUpperCase();
460
+ return spec.endpoints.find((ep) => ep.method === m && ep.path === path);
461
+ }
462
+ function createServer(docsUrl) {
463
+ const server = new McpServer({
464
+ name: "swagger-mcp",
465
+ version: "0.1.0"
466
+ });
467
+ const withSpec = (handler) => async () => {
468
+ try {
469
+ return handler(await loadSpec(docsUrl));
470
+ } catch (err) {
471
+ return fail(err instanceof Error ? err.message : String(err));
472
+ }
473
+ };
474
+ const withSpecArgs = (handler) => async (args) => {
475
+ try {
476
+ return handler(await loadSpec(docsUrl), args);
477
+ } catch (err) {
478
+ return fail(err instanceof Error ? err.message : String(err));
479
+ }
480
+ };
481
+ server.registerTool(
482
+ "get_api_overview",
483
+ {
484
+ title: "API overview",
485
+ description: "Top-level API metadata: title, version, description, servers, global security, and counts. Start here on an unfamiliar API.",
486
+ inputSchema: {},
487
+ annotations: readOnly
488
+ },
489
+ withSpec((spec) => ok(overview(spec)))
490
+ );
491
+ server.registerTool(
492
+ "list_tags",
493
+ {
494
+ title: "List tags",
495
+ description: "List the API's tags (logical groups) with descriptions and endpoint counts.",
496
+ inputSchema: {},
497
+ annotations: readOnly
498
+ },
499
+ withSpec((spec) => {
500
+ if (spec.tags.length === 0) return ok("No tags declared in this spec.");
501
+ const lines = spec.tags.map(
502
+ (t) => `${t.name} (${t.endpointCount})${t.description ? ` \u2014 ${t.description}` : ""}`
503
+ );
504
+ return ok(lines.join("\n"));
505
+ })
506
+ );
507
+ server.registerTool(
508
+ "list_endpoints",
509
+ {
510
+ title: "List endpoints",
511
+ description: "Compact index of endpoints (method, path, summary, tags). Optionally filter by tag.",
512
+ inputSchema: {
513
+ tag: z.string().optional().describe("Only list endpoints with this tag.")
514
+ },
515
+ annotations: readOnly
516
+ },
517
+ withSpecArgs((spec, { tag }) => {
518
+ let endpoints = spec.endpoints;
519
+ if (tag) endpoints = endpoints.filter((ep) => ep.tags.includes(tag));
520
+ if (endpoints.length === 0) {
521
+ return ok(tag ? `No endpoints with tag "${tag}".` : "No endpoints found.");
522
+ }
523
+ return ok(endpoints.map(endpointLine).join("\n"));
524
+ })
525
+ );
526
+ server.registerTool(
527
+ "search_endpoints",
528
+ {
529
+ title: "Search endpoints",
530
+ description: "Substring search over endpoint path, summary, description, tags, and parameter names. Returns matches ranked by relevance.",
531
+ inputSchema: {
532
+ query: z.string().describe("Case-insensitive substring to match.")
533
+ },
534
+ annotations: readOnly
535
+ },
536
+ withSpecArgs((spec, { query }) => {
537
+ const matches = searchEndpoints(spec, query);
538
+ if (matches.length === 0) return ok(`No endpoints match "${query}".`);
539
+ return ok(matches.map(endpointLine).join("\n"));
540
+ })
541
+ );
542
+ server.registerTool(
543
+ "list_schemas",
544
+ {
545
+ title: "List schemas",
546
+ description: "List named component/definition schemas with one-line descriptions.",
547
+ inputSchema: {},
548
+ annotations: readOnly
549
+ },
550
+ withSpec((spec) => {
551
+ const names = Object.keys(spec.schemas);
552
+ if (names.length === 0) return ok("No named schemas in this spec.");
553
+ const lines = names.map((n) => schemaLine(n, spec.schemas[n]));
554
+ return ok(lines.join("\n"));
555
+ })
556
+ );
557
+ server.registerTool(
558
+ "search_schemas",
559
+ {
560
+ title: "Search schemas",
561
+ description: "Substring search over schema names, property names, and descriptions. Returns matches ranked by relevance.",
562
+ inputSchema: {
563
+ query: z.string().describe("Case-insensitive substring to match.")
564
+ },
565
+ annotations: readOnly
566
+ },
567
+ withSpecArgs((spec, { query }) => {
568
+ const matches = searchSchemas(spec, query);
569
+ if (matches.length === 0) return ok(`No schemas match "${query}".`);
570
+ return ok(matches.map((m) => schemaLine(m.name, m.schema)).join("\n"));
571
+ })
572
+ );
573
+ server.registerTool(
574
+ "get_endpoint",
575
+ {
576
+ title: "Get endpoint",
577
+ description: "Full detail for one endpoint: parameters, request/response schemas, and required auth. Named schemas are shown by name \u2014 resolve them with get_schema.",
578
+ inputSchema: {
579
+ method: z.string().describe("HTTP method, e.g. GET, POST."),
580
+ path: z.string().describe("Exact path as it appears in the spec, e.g. /pets/{id}.")
581
+ },
582
+ annotations: readOnly
583
+ },
584
+ withSpecArgs((spec, { method, path }) => {
585
+ const ep = findEndpoint(spec, method, path);
586
+ if (!ep) {
587
+ return fail(
588
+ `No endpoint ${method.toUpperCase()} ${path}. Call list_endpoints to see available paths.`
589
+ );
590
+ }
591
+ return ok(endpointDetail(ep, spec));
592
+ })
593
+ );
594
+ server.registerTool(
595
+ "get_schema",
596
+ {
597
+ title: "Get schema",
598
+ description: 'Resolve one named schema. Nested named schemas are shown as { "$schema": "Name" } markers \u2014 call get_schema again to expand them.',
599
+ inputSchema: {
600
+ name: z.string().describe("Schema name as shown by list_schemas.")
601
+ },
602
+ annotations: readOnly
603
+ },
604
+ withSpecArgs((spec, { name }) => {
605
+ if (spec.schemas[name] === void 0) {
606
+ return fail(
607
+ `No schema named "${name}". Call list_schemas to see available schemas.`
608
+ );
609
+ }
610
+ return ok(schemaDetail(name, spec.schemas));
611
+ })
612
+ );
613
+ server.registerTool(
614
+ "get_auth",
615
+ {
616
+ title: "Get auth",
617
+ description: "Full security schemes for the API (OAuth2 flows, scopes, API-key locations) plus the global security requirement.",
618
+ inputSchema: {},
619
+ annotations: readOnly
620
+ },
621
+ withSpec((spec) => {
622
+ if (spec.securitySchemes.length === 0) {
623
+ return ok("This spec declares no security schemes.");
624
+ }
625
+ const parts = spec.securitySchemes.map(authDetail);
626
+ return ok(`# Security schemes
627
+
628
+ ${parts.join("\n\n")}`);
629
+ })
630
+ );
631
+ return server;
632
+ }
633
+
634
+ // src/index.ts
635
+ async function main() {
636
+ const docsUrl = process.env.API_DOCS_URL;
637
+ if (!docsUrl) {
638
+ console.error("swagger-mcp: API_DOCS_URL environment variable is required.");
639
+ process.exit(1);
640
+ }
641
+ const server = createServer(docsUrl);
642
+ const transport = new StdioServerTransport();
643
+ await server.connect(transport);
644
+ console.error("swagger-mcp: ready (stdio).");
645
+ }
646
+ main().catch((err) => {
647
+ console.error("swagger-mcp: fatal error:", err);
648
+ process.exit(1);
649
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@emreyc/swagger-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Read-only MCP server for navigating OpenAPI/Swagger specifications.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "openapi",
9
+ "swagger",
10
+ "api-docs"
11
+ ],
12
+ "type": "module",
13
+ "bin": {
14
+ "swagger-mcp": "dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@apidevtools/swagger-parser": "^12.1.0",
35
+ "@modelcontextprotocol/sdk": "^1.29.0",
36
+ "zod": "^3.25.76"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.10.0",
40
+ "openapi-types": "^12.1.3",
41
+ "tsup": "^8.5.1",
42
+ "typescript": "^5.7.0",
43
+ "vitest": "^4.1.10"
44
+ }
45
+ }