@emreyc/swagger-mcp 0.1.1 → 0.2.1
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/README.md +24 -6
- package/dist/index.js +59 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,19 +19,34 @@ Add it to your MCP client config. No install step — `npx` fetches it on demand
|
|
|
19
19
|
"mcpServers": {
|
|
20
20
|
"swagger-mcp": {
|
|
21
21
|
"command": "npx",
|
|
22
|
-
"args": ["-y", "@emreyc/swagger-mcp@latest"]
|
|
23
|
-
"env": {
|
|
24
|
-
"API_DOCS_URL": "https://api.example.com/swagger.json"
|
|
25
|
-
}
|
|
22
|
+
"args": ["-y", "@emreyc/swagger-mcp@latest"]
|
|
26
23
|
}
|
|
27
24
|
}
|
|
28
25
|
}
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
Every tool takes a `spec` parameter — a URL (or local path) to a Swagger 2.0 or
|
|
29
|
+
OpenAPI 3.x document — so a single server can navigate any number of specs in one
|
|
30
|
+
session. Internal/localhost URLs (e.g. Spring Boot's
|
|
33
31
|
`http://localhost:8080/v3/api-docs`) are supported.
|
|
34
32
|
|
|
33
|
+
If you mostly work with one spec, set `API_DOCS_URL` as a default so `spec` can be
|
|
34
|
+
omitted:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"swagger-mcp": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "@emreyc/swagger-mcp@latest"],
|
|
42
|
+
"env": { "API_DOCS_URL": "https://api.example.com/swagger.json" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Specs are fetched lazily and cached per URL for the process lifetime.
|
|
49
|
+
|
|
35
50
|
## Tools
|
|
36
51
|
|
|
37
52
|
Designed for progressive disclosure — orient, then discover, then drill down —
|
|
@@ -49,6 +64,9 @@ so the agent loads only what it needs.
|
|
|
49
64
|
| `get_schema` | Resolve one named schema. |
|
|
50
65
|
| `get_auth` | Security schemes in full (OAuth2 flows, scopes, API-key locations). |
|
|
51
66
|
|
|
67
|
+
Every tool also accepts an optional `spec` argument (a spec URL or path); when
|
|
68
|
+
omitted it falls back to `API_DOCS_URL`.
|
|
69
|
+
|
|
52
70
|
Swagger 2.0 and OpenAPI 3.x are normalized into one consistent output shape.
|
|
53
71
|
Nested named schemas are shown as `{ "$schema": "Name" }` markers — call
|
|
54
72
|
`get_schema` to expand them. This keeps every response bounded and is safe with
|
package/dist/index.js
CHANGED
|
@@ -222,12 +222,17 @@ function normalizeTags(spec, endpoints) {
|
|
|
222
222
|
// src/spec/loader.ts
|
|
223
223
|
var SpecLoadError = class extends Error {
|
|
224
224
|
};
|
|
225
|
-
var cache;
|
|
225
|
+
var cache = /* @__PURE__ */ new Map();
|
|
226
|
+
var fetchHeaders = {
|
|
227
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
|
228
|
+
Accept: "application/json, application/yaml, text/yaml, */*",
|
|
229
|
+
"Accept-Language": "en"
|
|
230
|
+
};
|
|
226
231
|
async function fetchAndNormalize(docsUrl) {
|
|
227
232
|
let bundled;
|
|
228
233
|
try {
|
|
229
234
|
bundled = await SwaggerParser.bundle(docsUrl, {
|
|
230
|
-
resolve: { http: { safeUrlResolver: false } }
|
|
235
|
+
resolve: { http: { safeUrlResolver: false, headers: fetchHeaders } }
|
|
231
236
|
});
|
|
232
237
|
} catch (err) {
|
|
233
238
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -238,13 +243,15 @@ async function fetchAndNormalize(docsUrl) {
|
|
|
238
243
|
return normalizeSpec(bundled);
|
|
239
244
|
}
|
|
240
245
|
function loadSpec(docsUrl) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
let entry = cache.get(docsUrl);
|
|
247
|
+
if (!entry) {
|
|
248
|
+
entry = fetchAndNormalize(docsUrl).catch((err) => {
|
|
249
|
+
cache.delete(docsUrl);
|
|
244
250
|
throw err;
|
|
245
251
|
});
|
|
252
|
+
cache.set(docsUrl, entry);
|
|
246
253
|
}
|
|
247
|
-
return
|
|
254
|
+
return entry;
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
// src/format.ts
|
|
@@ -459,21 +466,26 @@ function findEndpoint(spec, method, path) {
|
|
|
459
466
|
const m = method.toUpperCase();
|
|
460
467
|
return spec.endpoints.find((ep) => ep.method === m && ep.path === path);
|
|
461
468
|
}
|
|
462
|
-
function createServer(
|
|
469
|
+
function createServer(defaultDocsUrl) {
|
|
463
470
|
const server = new McpServer({
|
|
464
471
|
name: "swagger-mcp",
|
|
465
472
|
version: "0.1.0"
|
|
466
473
|
});
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
474
|
+
const specParam = z.string().optional().describe(
|
|
475
|
+
defaultDocsUrl ? "URL or local path of the OpenAPI/Swagger spec. Defaults to the server's configured API_DOCS_URL." : "URL or local path of the OpenAPI/Swagger spec to read."
|
|
476
|
+
);
|
|
477
|
+
function resolveUrl(spec) {
|
|
478
|
+
const url = spec ?? defaultDocsUrl;
|
|
479
|
+
if (!url) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
"No spec specified. Pass `spec` (an OpenAPI/Swagger URL or path), or set API_DOCS_URL when launching the server."
|
|
482
|
+
);
|
|
472
483
|
}
|
|
473
|
-
|
|
474
|
-
|
|
484
|
+
return url;
|
|
485
|
+
}
|
|
486
|
+
const tool = (handler) => async (args) => {
|
|
475
487
|
try {
|
|
476
|
-
return handler(await loadSpec(
|
|
488
|
+
return handler(await loadSpec(resolveUrl(args.spec)), args);
|
|
477
489
|
} catch (err) {
|
|
478
490
|
return fail(err instanceof Error ? err.message : String(err));
|
|
479
491
|
}
|
|
@@ -483,20 +495,20 @@ function createServer(docsUrl) {
|
|
|
483
495
|
{
|
|
484
496
|
title: "API overview",
|
|
485
497
|
description: "Top-level API metadata: title, version, description, servers, global security, and counts. Start here on an unfamiliar API.",
|
|
486
|
-
inputSchema: {},
|
|
498
|
+
inputSchema: { spec: specParam },
|
|
487
499
|
annotations: readOnly
|
|
488
500
|
},
|
|
489
|
-
|
|
501
|
+
tool((spec) => ok(overview(spec)))
|
|
490
502
|
);
|
|
491
503
|
server.registerTool(
|
|
492
504
|
"list_tags",
|
|
493
505
|
{
|
|
494
506
|
title: "List tags",
|
|
495
507
|
description: "List the API's tags (logical groups) with descriptions and endpoint counts.",
|
|
496
|
-
inputSchema: {},
|
|
508
|
+
inputSchema: { spec: specParam },
|
|
497
509
|
annotations: readOnly
|
|
498
510
|
},
|
|
499
|
-
|
|
511
|
+
tool((spec) => {
|
|
500
512
|
if (spec.tags.length === 0) return ok("No tags declared in this spec.");
|
|
501
513
|
const lines = spec.tags.map(
|
|
502
514
|
(t) => `${t.name} (${t.endpointCount})${t.description ? ` \u2014 ${t.description}` : ""}`
|
|
@@ -510,11 +522,12 @@ function createServer(docsUrl) {
|
|
|
510
522
|
title: "List endpoints",
|
|
511
523
|
description: "Compact index of endpoints (method, path, summary, tags). Optionally filter by tag.",
|
|
512
524
|
inputSchema: {
|
|
525
|
+
spec: specParam,
|
|
513
526
|
tag: z.string().optional().describe("Only list endpoints with this tag.")
|
|
514
527
|
},
|
|
515
528
|
annotations: readOnly
|
|
516
529
|
},
|
|
517
|
-
|
|
530
|
+
tool((spec, { tag }) => {
|
|
518
531
|
let endpoints = spec.endpoints;
|
|
519
532
|
if (tag) endpoints = endpoints.filter((ep) => ep.tags.includes(tag));
|
|
520
533
|
if (endpoints.length === 0) {
|
|
@@ -529,11 +542,12 @@ function createServer(docsUrl) {
|
|
|
529
542
|
title: "Search endpoints",
|
|
530
543
|
description: "Substring search over endpoint path, summary, description, tags, and parameter names. Returns matches ranked by relevance.",
|
|
531
544
|
inputSchema: {
|
|
545
|
+
spec: specParam,
|
|
532
546
|
query: z.string().describe("Case-insensitive substring to match.")
|
|
533
547
|
},
|
|
534
548
|
annotations: readOnly
|
|
535
549
|
},
|
|
536
|
-
|
|
550
|
+
tool((spec, { query }) => {
|
|
537
551
|
const matches = searchEndpoints(spec, query);
|
|
538
552
|
if (matches.length === 0) return ok(`No endpoints match "${query}".`);
|
|
539
553
|
return ok(matches.map(endpointLine).join("\n"));
|
|
@@ -544,10 +558,10 @@ function createServer(docsUrl) {
|
|
|
544
558
|
{
|
|
545
559
|
title: "List schemas",
|
|
546
560
|
description: "List named component/definition schemas with one-line descriptions.",
|
|
547
|
-
inputSchema: {},
|
|
561
|
+
inputSchema: { spec: specParam },
|
|
548
562
|
annotations: readOnly
|
|
549
563
|
},
|
|
550
|
-
|
|
564
|
+
tool((spec) => {
|
|
551
565
|
const names = Object.keys(spec.schemas);
|
|
552
566
|
if (names.length === 0) return ok("No named schemas in this spec.");
|
|
553
567
|
const lines = names.map((n) => schemaLine(n, spec.schemas[n]));
|
|
@@ -560,11 +574,12 @@ function createServer(docsUrl) {
|
|
|
560
574
|
title: "Search schemas",
|
|
561
575
|
description: "Substring search over schema names, property names, and descriptions. Returns matches ranked by relevance.",
|
|
562
576
|
inputSchema: {
|
|
577
|
+
spec: specParam,
|
|
563
578
|
query: z.string().describe("Case-insensitive substring to match.")
|
|
564
579
|
},
|
|
565
580
|
annotations: readOnly
|
|
566
581
|
},
|
|
567
|
-
|
|
582
|
+
tool((spec, { query }) => {
|
|
568
583
|
const matches = searchSchemas(spec, query);
|
|
569
584
|
if (matches.length === 0) return ok(`No schemas match "${query}".`);
|
|
570
585
|
return ok(matches.map((m) => schemaLine(m.name, m.schema)).join("\n"));
|
|
@@ -576,20 +591,23 @@ function createServer(docsUrl) {
|
|
|
576
591
|
title: "Get endpoint",
|
|
577
592
|
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
593
|
inputSchema: {
|
|
594
|
+
spec: specParam,
|
|
579
595
|
method: z.string().describe("HTTP method, e.g. GET, POST."),
|
|
580
596
|
path: z.string().describe("Exact path as it appears in the spec, e.g. /pets/{id}.")
|
|
581
597
|
},
|
|
582
598
|
annotations: readOnly
|
|
583
599
|
},
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
600
|
+
tool(
|
|
601
|
+
(spec, { method, path }) => {
|
|
602
|
+
const ep = findEndpoint(spec, method, path);
|
|
603
|
+
if (!ep) {
|
|
604
|
+
return fail(
|
|
605
|
+
`No endpoint ${method.toUpperCase()} ${path}. Call list_endpoints to see available paths.`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return ok(endpointDetail(ep, spec));
|
|
590
609
|
}
|
|
591
|
-
|
|
592
|
-
})
|
|
610
|
+
)
|
|
593
611
|
);
|
|
594
612
|
server.registerTool(
|
|
595
613
|
"get_schema",
|
|
@@ -597,11 +615,12 @@ function createServer(docsUrl) {
|
|
|
597
615
|
title: "Get schema",
|
|
598
616
|
description: 'Resolve one named schema. Nested named schemas are shown as { "$schema": "Name" } markers \u2014 call get_schema again to expand them.',
|
|
599
617
|
inputSchema: {
|
|
618
|
+
spec: specParam,
|
|
600
619
|
name: z.string().describe("Schema name as shown by list_schemas.")
|
|
601
620
|
},
|
|
602
621
|
annotations: readOnly
|
|
603
622
|
},
|
|
604
|
-
|
|
623
|
+
tool((spec, { name }) => {
|
|
605
624
|
if (spec.schemas[name] === void 0) {
|
|
606
625
|
return fail(
|
|
607
626
|
`No schema named "${name}". Call list_schemas to see available schemas.`
|
|
@@ -615,10 +634,10 @@ function createServer(docsUrl) {
|
|
|
615
634
|
{
|
|
616
635
|
title: "Get auth",
|
|
617
636
|
description: "Full security schemes for the API (OAuth2 flows, scopes, API-key locations) plus the global security requirement.",
|
|
618
|
-
inputSchema: {},
|
|
637
|
+
inputSchema: { spec: specParam },
|
|
619
638
|
annotations: readOnly
|
|
620
639
|
},
|
|
621
|
-
|
|
640
|
+
tool((spec) => {
|
|
622
641
|
if (spec.securitySchemes.length === 0) {
|
|
623
642
|
return ok("This spec declares no security schemes.");
|
|
624
643
|
}
|
|
@@ -633,15 +652,13 @@ ${parts.join("\n\n")}`);
|
|
|
633
652
|
|
|
634
653
|
// src/index.ts
|
|
635
654
|
async function main() {
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
console.error("swagger-mcp: API_DOCS_URL environment variable is required.");
|
|
639
|
-
process.exit(1);
|
|
640
|
-
}
|
|
641
|
-
const server = createServer(docsUrl);
|
|
655
|
+
const defaultDocsUrl = process.env.API_DOCS_URL;
|
|
656
|
+
const server = createServer(defaultDocsUrl);
|
|
642
657
|
const transport = new StdioServerTransport();
|
|
643
658
|
await server.connect(transport);
|
|
644
|
-
console.error(
|
|
659
|
+
console.error(
|
|
660
|
+
defaultDocsUrl ? `swagger-mcp: ready (stdio), default spec ${defaultDocsUrl}` : "swagger-mcp: ready (stdio), pass `spec` per tool call"
|
|
661
|
+
);
|
|
645
662
|
}
|
|
646
663
|
main().catch((err) => {
|
|
647
664
|
console.error("swagger-mcp: fatal error:", err);
|