@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.
Files changed (3) hide show
  1. package/README.md +24 -6
  2. package/dist/index.js +59 -42
  3. 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
- `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
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
- if (!cache) {
242
- cache = fetchAndNormalize(docsUrl).catch((err) => {
243
- cache = void 0;
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 cache;
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(docsUrl) {
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 withSpec = (handler) => async () => {
468
- try {
469
- return handler(await loadSpec(docsUrl));
470
- } catch (err) {
471
- return fail(err instanceof Error ? err.message : String(err));
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
- const withSpecArgs = (handler) => async (args) => {
484
+ return url;
485
+ }
486
+ const tool = (handler) => async (args) => {
475
487
  try {
476
- return handler(await loadSpec(docsUrl), args);
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
- withSpec((spec) => ok(overview(spec)))
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
- withSpec((spec) => {
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
- withSpecArgs((spec, { tag }) => {
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
- withSpecArgs((spec, { query }) => {
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
- withSpec((spec) => {
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
- withSpecArgs((spec, { query }) => {
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
- 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
- );
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
- return ok(endpointDetail(ep, spec));
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
- withSpecArgs((spec, { name }) => {
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
- withSpec((spec) => {
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 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);
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("swagger-mcp: ready (stdio).");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emreyc/swagger-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Read-only MCP server for navigating OpenAPI/Swagger specifications.",
5
5
  "keywords": [
6
6
  "mcp",