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