@emreyc/swagger-mcp 0.1.0 → 0.2.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/README.md +24 -6
- package/dist/index.js +53 -41
- package/package.json +8 -2
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,7 +222,7 @@ 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
226
|
async function fetchAndNormalize(docsUrl) {
|
|
227
227
|
let bundled;
|
|
228
228
|
try {
|
|
@@ -238,13 +238,15 @@ async function fetchAndNormalize(docsUrl) {
|
|
|
238
238
|
return normalizeSpec(bundled);
|
|
239
239
|
}
|
|
240
240
|
function loadSpec(docsUrl) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
let entry = cache.get(docsUrl);
|
|
242
|
+
if (!entry) {
|
|
243
|
+
entry = fetchAndNormalize(docsUrl).catch((err) => {
|
|
244
|
+
cache.delete(docsUrl);
|
|
244
245
|
throw err;
|
|
245
246
|
});
|
|
247
|
+
cache.set(docsUrl, entry);
|
|
246
248
|
}
|
|
247
|
-
return
|
|
249
|
+
return entry;
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
// src/format.ts
|
|
@@ -459,21 +461,26 @@ function findEndpoint(spec, method, path) {
|
|
|
459
461
|
const m = method.toUpperCase();
|
|
460
462
|
return spec.endpoints.find((ep) => ep.method === m && ep.path === path);
|
|
461
463
|
}
|
|
462
|
-
function createServer(
|
|
464
|
+
function createServer(defaultDocsUrl) {
|
|
463
465
|
const server = new McpServer({
|
|
464
466
|
name: "swagger-mcp",
|
|
465
467
|
version: "0.1.0"
|
|
466
468
|
});
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
const specParam = z.string().optional().describe(
|
|
470
|
+
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."
|
|
471
|
+
);
|
|
472
|
+
function resolveUrl(spec) {
|
|
473
|
+
const url = spec ?? defaultDocsUrl;
|
|
474
|
+
if (!url) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
"No spec specified. Pass `spec` (an OpenAPI/Swagger URL or path), or set API_DOCS_URL when launching the server."
|
|
477
|
+
);
|
|
472
478
|
}
|
|
473
|
-
|
|
474
|
-
|
|
479
|
+
return url;
|
|
480
|
+
}
|
|
481
|
+
const tool = (handler) => async (args) => {
|
|
475
482
|
try {
|
|
476
|
-
return handler(await loadSpec(
|
|
483
|
+
return handler(await loadSpec(resolveUrl(args.spec)), args);
|
|
477
484
|
} catch (err) {
|
|
478
485
|
return fail(err instanceof Error ? err.message : String(err));
|
|
479
486
|
}
|
|
@@ -483,20 +490,20 @@ function createServer(docsUrl) {
|
|
|
483
490
|
{
|
|
484
491
|
title: "API overview",
|
|
485
492
|
description: "Top-level API metadata: title, version, description, servers, global security, and counts. Start here on an unfamiliar API.",
|
|
486
|
-
inputSchema: {},
|
|
493
|
+
inputSchema: { spec: specParam },
|
|
487
494
|
annotations: readOnly
|
|
488
495
|
},
|
|
489
|
-
|
|
496
|
+
tool((spec) => ok(overview(spec)))
|
|
490
497
|
);
|
|
491
498
|
server.registerTool(
|
|
492
499
|
"list_tags",
|
|
493
500
|
{
|
|
494
501
|
title: "List tags",
|
|
495
502
|
description: "List the API's tags (logical groups) with descriptions and endpoint counts.",
|
|
496
|
-
inputSchema: {},
|
|
503
|
+
inputSchema: { spec: specParam },
|
|
497
504
|
annotations: readOnly
|
|
498
505
|
},
|
|
499
|
-
|
|
506
|
+
tool((spec) => {
|
|
500
507
|
if (spec.tags.length === 0) return ok("No tags declared in this spec.");
|
|
501
508
|
const lines = spec.tags.map(
|
|
502
509
|
(t) => `${t.name} (${t.endpointCount})${t.description ? ` \u2014 ${t.description}` : ""}`
|
|
@@ -510,11 +517,12 @@ function createServer(docsUrl) {
|
|
|
510
517
|
title: "List endpoints",
|
|
511
518
|
description: "Compact index of endpoints (method, path, summary, tags). Optionally filter by tag.",
|
|
512
519
|
inputSchema: {
|
|
520
|
+
spec: specParam,
|
|
513
521
|
tag: z.string().optional().describe("Only list endpoints with this tag.")
|
|
514
522
|
},
|
|
515
523
|
annotations: readOnly
|
|
516
524
|
},
|
|
517
|
-
|
|
525
|
+
tool((spec, { tag }) => {
|
|
518
526
|
let endpoints = spec.endpoints;
|
|
519
527
|
if (tag) endpoints = endpoints.filter((ep) => ep.tags.includes(tag));
|
|
520
528
|
if (endpoints.length === 0) {
|
|
@@ -529,11 +537,12 @@ function createServer(docsUrl) {
|
|
|
529
537
|
title: "Search endpoints",
|
|
530
538
|
description: "Substring search over endpoint path, summary, description, tags, and parameter names. Returns matches ranked by relevance.",
|
|
531
539
|
inputSchema: {
|
|
540
|
+
spec: specParam,
|
|
532
541
|
query: z.string().describe("Case-insensitive substring to match.")
|
|
533
542
|
},
|
|
534
543
|
annotations: readOnly
|
|
535
544
|
},
|
|
536
|
-
|
|
545
|
+
tool((spec, { query }) => {
|
|
537
546
|
const matches = searchEndpoints(spec, query);
|
|
538
547
|
if (matches.length === 0) return ok(`No endpoints match "${query}".`);
|
|
539
548
|
return ok(matches.map(endpointLine).join("\n"));
|
|
@@ -544,10 +553,10 @@ function createServer(docsUrl) {
|
|
|
544
553
|
{
|
|
545
554
|
title: "List schemas",
|
|
546
555
|
description: "List named component/definition schemas with one-line descriptions.",
|
|
547
|
-
inputSchema: {},
|
|
556
|
+
inputSchema: { spec: specParam },
|
|
548
557
|
annotations: readOnly
|
|
549
558
|
},
|
|
550
|
-
|
|
559
|
+
tool((spec) => {
|
|
551
560
|
const names = Object.keys(spec.schemas);
|
|
552
561
|
if (names.length === 0) return ok("No named schemas in this spec.");
|
|
553
562
|
const lines = names.map((n) => schemaLine(n, spec.schemas[n]));
|
|
@@ -560,11 +569,12 @@ function createServer(docsUrl) {
|
|
|
560
569
|
title: "Search schemas",
|
|
561
570
|
description: "Substring search over schema names, property names, and descriptions. Returns matches ranked by relevance.",
|
|
562
571
|
inputSchema: {
|
|
572
|
+
spec: specParam,
|
|
563
573
|
query: z.string().describe("Case-insensitive substring to match.")
|
|
564
574
|
},
|
|
565
575
|
annotations: readOnly
|
|
566
576
|
},
|
|
567
|
-
|
|
577
|
+
tool((spec, { query }) => {
|
|
568
578
|
const matches = searchSchemas(spec, query);
|
|
569
579
|
if (matches.length === 0) return ok(`No schemas match "${query}".`);
|
|
570
580
|
return ok(matches.map((m) => schemaLine(m.name, m.schema)).join("\n"));
|
|
@@ -576,20 +586,23 @@ function createServer(docsUrl) {
|
|
|
576
586
|
title: "Get endpoint",
|
|
577
587
|
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
588
|
inputSchema: {
|
|
589
|
+
spec: specParam,
|
|
579
590
|
method: z.string().describe("HTTP method, e.g. GET, POST."),
|
|
580
591
|
path: z.string().describe("Exact path as it appears in the spec, e.g. /pets/{id}.")
|
|
581
592
|
},
|
|
582
593
|
annotations: readOnly
|
|
583
594
|
},
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
595
|
+
tool(
|
|
596
|
+
(spec, { method, path }) => {
|
|
597
|
+
const ep = findEndpoint(spec, method, path);
|
|
598
|
+
if (!ep) {
|
|
599
|
+
return fail(
|
|
600
|
+
`No endpoint ${method.toUpperCase()} ${path}. Call list_endpoints to see available paths.`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return ok(endpointDetail(ep, spec));
|
|
590
604
|
}
|
|
591
|
-
|
|
592
|
-
})
|
|
605
|
+
)
|
|
593
606
|
);
|
|
594
607
|
server.registerTool(
|
|
595
608
|
"get_schema",
|
|
@@ -597,11 +610,12 @@ function createServer(docsUrl) {
|
|
|
597
610
|
title: "Get schema",
|
|
598
611
|
description: 'Resolve one named schema. Nested named schemas are shown as { "$schema": "Name" } markers \u2014 call get_schema again to expand them.',
|
|
599
612
|
inputSchema: {
|
|
613
|
+
spec: specParam,
|
|
600
614
|
name: z.string().describe("Schema name as shown by list_schemas.")
|
|
601
615
|
},
|
|
602
616
|
annotations: readOnly
|
|
603
617
|
},
|
|
604
|
-
|
|
618
|
+
tool((spec, { name }) => {
|
|
605
619
|
if (spec.schemas[name] === void 0) {
|
|
606
620
|
return fail(
|
|
607
621
|
`No schema named "${name}". Call list_schemas to see available schemas.`
|
|
@@ -615,10 +629,10 @@ function createServer(docsUrl) {
|
|
|
615
629
|
{
|
|
616
630
|
title: "Get auth",
|
|
617
631
|
description: "Full security schemes for the API (OAuth2 flows, scopes, API-key locations) plus the global security requirement.",
|
|
618
|
-
inputSchema: {},
|
|
632
|
+
inputSchema: { spec: specParam },
|
|
619
633
|
annotations: readOnly
|
|
620
634
|
},
|
|
621
|
-
|
|
635
|
+
tool((spec) => {
|
|
622
636
|
if (spec.securitySchemes.length === 0) {
|
|
623
637
|
return ok("This spec declares no security schemes.");
|
|
624
638
|
}
|
|
@@ -633,15 +647,13 @@ ${parts.join("\n\n")}`);
|
|
|
633
647
|
|
|
634
648
|
// src/index.ts
|
|
635
649
|
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);
|
|
650
|
+
const defaultDocsUrl = process.env.API_DOCS_URL;
|
|
651
|
+
const server = createServer(defaultDocsUrl);
|
|
642
652
|
const transport = new StdioServerTransport();
|
|
643
653
|
await server.connect(transport);
|
|
644
|
-
console.error(
|
|
654
|
+
console.error(
|
|
655
|
+
defaultDocsUrl ? `swagger-mcp: ready (stdio), default spec ${defaultDocsUrl}` : "swagger-mcp: ready (stdio), pass `spec` per tool call"
|
|
656
|
+
);
|
|
645
657
|
}
|
|
646
658
|
main().catch((err) => {
|
|
647
659
|
console.error("swagger-mcp: fatal error:", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emreyc/swagger-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Read-only MCP server for navigating OpenAPI/Swagger specifications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
"api-docs"
|
|
11
11
|
],
|
|
12
12
|
"type": "module",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/emreycolakoglu/swagger-mcp.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/emreycolakoglu/swagger-mcp#readme",
|
|
18
|
+
"bugs": "https://github.com/emreycolakoglu/swagger-mcp/issues",
|
|
13
19
|
"bin": {
|
|
14
20
|
"swagger-mcp": "dist/index.js"
|
|
15
21
|
},
|
|
@@ -37,7 +43,7 @@
|
|
|
37
43
|
},
|
|
38
44
|
"devDependencies": {
|
|
39
45
|
"@types/node": "^22.10.0",
|
|
40
|
-
"
|
|
46
|
+
"semantic-release": "^25.0.5",
|
|
41
47
|
"tsup": "^8.5.1",
|
|
42
48
|
"typescript": "^5.7.0",
|
|
43
49
|
"vitest": "^4.1.10"
|