swagger-mcp-server 1.0.2 → 1.0.3

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/build/config.js CHANGED
@@ -2,23 +2,64 @@ import { z } from "zod";
2
2
  import fs from "fs";
3
3
  export const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
4
4
  export const jsonSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]));
5
+ const ConfigEndpointBaseSchema = z.object({
6
+ name: z.string(),
7
+ url: z.string().url()
8
+ });
9
+ const AvailableConfigEndpointSchema = ConfigEndpointBaseSchema.extend({
10
+ schema: jsonSchema
11
+ });
12
+ const UnreachableConfigEndpointSchema = ConfigEndpointBaseSchema.extend({
13
+ error: z.string()
14
+ });
5
15
  export const ConfigSchema = z.object({
6
- endpoints: z.array(z.object({
7
- name: z.string(),
8
- url: z.string().url(),
9
- schema: jsonSchema
10
- }))
16
+ endpoints: z.array(z.union([AvailableConfigEndpointSchema, UnreachableConfigEndpointSchema]))
17
+ });
18
+ const ConfigFileSchema = z.object({
19
+ endpoints: z.array(ConfigEndpointBaseSchema)
11
20
  });
21
+ function errorMessage(error) {
22
+ if (error instanceof Error) {
23
+ return error.message;
24
+ }
25
+ return String(error);
26
+ }
27
+ async function loadEndpoint({ url, name }) {
28
+ try {
29
+ const response = await fetch(url);
30
+ if (!response.ok) {
31
+ return {
32
+ url,
33
+ name,
34
+ error: `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
35
+ };
36
+ }
37
+ const schema = await response.json();
38
+ return { url, name, schema };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ url,
43
+ name,
44
+ error: errorMessage(error)
45
+ };
46
+ }
47
+ }
48
+ export function isAvailableConfigEndpoint(endpoint) {
49
+ return 'schema' in endpoint;
50
+ }
51
+ export function isUnreachableConfigEndpoint(endpoint) {
52
+ return 'error' in endpoint;
53
+ }
12
54
  export async function loadConfig(configPath) {
13
55
  try {
14
56
  const configFile = fs.readFileSync(configPath, 'utf8');
15
- const parsedConfig = JSON.parse(configFile);
16
- const endpointsWithSchema = await Promise.all(parsedConfig.endpoints
17
- .map(async ({ url, name }) => {
18
- const response = await fetch(url);
19
- const schema = await response.json();
20
- return { url, name, schema };
21
- }));
57
+ const parsedConfig = ConfigFileSchema.parse(JSON.parse(configFile));
58
+ const endpointsWithSchema = await Promise.all(parsedConfig.endpoints.map(loadEndpoint));
59
+ const unavailableEndpoints = endpointsWithSchema.filter(isUnreachableConfigEndpoint);
60
+ for (const endpoint of unavailableEndpoints) {
61
+ console.error(`Swagger endpoint unreachable: ${endpoint.name} (${endpoint.url}): ${endpoint.error}`);
62
+ }
22
63
  return ConfigSchema.parse({ endpoints: endpointsWithSchema });
23
64
  }
24
65
  catch (error) {
@@ -1,9 +1,9 @@
1
+ import { isAvailableConfigEndpoint } from "./config.js";
1
2
  import { SwaggerParser } from "./swagger_parser.js";
2
3
  export class SwaggerCollection {
3
4
  swaggers;
4
5
  constructor(swaggers) {
5
6
  this.swaggers = swaggers;
6
- this.swaggers = swaggers;
7
7
  }
8
8
  listSwaggers() {
9
9
  return this.swaggers.map((swagger) => this.mapSwagger(swagger));
@@ -16,10 +16,21 @@ export class SwaggerCollection {
16
16
  return null;
17
17
  }
18
18
  mapSwagger(swagger) {
19
+ if (!isAvailableConfigEndpoint(swagger)) {
20
+ return {
21
+ id: swagger.name,
22
+ url: swagger.url,
23
+ name: swagger.name,
24
+ status: 'unreachable',
25
+ error: swagger.error
26
+ };
27
+ }
28
+ const schema = swagger.schema;
19
29
  return {
20
30
  id: swagger.name,
21
31
  url: swagger.url,
22
- name: swagger.schema.info.title,
32
+ name: schema.info?.title || swagger.name,
33
+ status: 'available',
23
34
  instance: new SwaggerParser(swagger.name, swagger.schema)
24
35
  };
25
36
  }
@@ -10,49 +10,26 @@ export class SwaggerMcpServer {
10
10
  name: "swagger",
11
11
  version: "1.0.0",
12
12
  });
13
- this.server.tool("list-swaggers", "List all connected swagger endpoints", async () => {
14
- const swaggers = this.swaggerCollection.listSwaggers();
15
- let result = "List of available swaggers (id | name | url):\n";
16
- for (const swagger of swaggers) {
17
- result += `${swagger.id} | ${swagger.name} | ${swagger.url}\n`;
18
- }
13
+ this.server.tool("list-swaggers", "List all configured swagger endpoints and their availability status", async () => {
19
14
  return {
20
15
  content: [
21
16
  {
22
17
  type: "text",
23
- text: result
18
+ text: this.formatSwaggerList()
24
19
  }
25
20
  ]
26
21
  };
27
22
  });
28
23
  this.server.tool("list-endpoints", "List all available endpoints with a short description. " +
29
- "If swagger is provided, only endpoints from that swagger will be listed.", {
24
+ "If swagger is provided, only endpoints from that swagger will be listed. " +
25
+ "Unavailable swaggers are reported separately.", {
30
26
  swagger: z.string().optional().describe("Swagger id returned by list-swaggers")
31
27
  }, async ({ swagger }) => {
32
- let swaggers = swagger
33
- ? [swagger]
34
- : this.swaggerCollection.listSwaggers().map((s) => s.id);
35
- let endpoints = [];
36
- for (const swaggerId of swaggers) {
37
- const swagger = this.swaggerCollection.getSwagger(swaggerId);
38
- if (swagger) {
39
- endpoints.push(...swagger.instance.listEndpoints());
40
- }
41
- }
42
- let result = "List of available endpoints (endpointId | method | path | description):\n";
43
- for (const endpoint of endpoints) {
44
- const mergedText = endpoint.summary ? `${endpoint.summary}${endpoint.summary.endsWith('.') ? '' : '.'} ${endpoint.description}`.trim() : endpoint.description;
45
- result += [
46
- `${endpoint.swaggerName}-${endpoint.operationId}`,
47
- endpoint.method.toUpperCase() + ' ' + endpoint.path,
48
- mergedText
49
- ].join(' | ') + '\n';
50
- }
51
28
  return {
52
29
  content: [
53
30
  {
54
31
  type: "text",
55
- text: result
32
+ text: this.formatEndpointList(swagger)
56
33
  }
57
34
  ]
58
35
  };
@@ -62,42 +39,106 @@ export class SwaggerMcpServer {
62
39
  .describe("List of endpoint IDs to retrieve details for. " +
63
40
  "Endpoint ids can be found in the list-endpoints tool.")
64
41
  }, async ({ endpointIds }) => {
65
- let result = [];
66
- for (const endpointId of endpointIds) {
67
- const lastHyphenIndex = endpointId.lastIndexOf('-');
68
- if (lastHyphenIndex === -1) {
69
- result.push(`Invalid endpoint ID format: ${endpointId}`);
70
- continue;
71
- }
72
- const swaggerName = endpointId.substring(0, lastHyphenIndex);
73
- const operationId = endpointId.substring(lastHyphenIndex + 1);
74
- if (!swaggerName || !operationId) {
75
- result.push(`Invalid endpoint ID format: ${endpointId}`);
76
- continue;
77
- }
78
- const swagger = this.swaggerCollection.getSwagger(swaggerName);
79
- if (!swagger) {
80
- result.push(`Swagger not found: ${swaggerName} for endpoint ${endpointId}`);
81
- continue;
82
- }
83
- const endpoints = swagger.instance.listEndpoints();
84
- const endpoint = Array.from(endpoints).find(e => e.operationId === operationId);
85
- if (!endpoint) {
86
- result.push(`Endpoint not found: ${operationId} in swagger ${swaggerName}`);
87
- continue;
88
- }
89
- result.push(this.formatEndpointDetails(endpoint));
90
- }
91
42
  return {
92
43
  content: [
93
44
  {
94
45
  type: "text",
95
- text: result.join("\n\n---\n\n") || "No endpoint details found."
46
+ text: this.formatEndpointDetailsByIds(endpointIds)
96
47
  }
97
48
  ]
98
49
  };
99
50
  });
100
51
  }
52
+ formatSwaggerList() {
53
+ const swaggers = this.swaggerCollection.listSwaggers();
54
+ let result = "List of configured swaggers (id | status | name | url):\n";
55
+ for (const swagger of swaggers) {
56
+ result += `${swagger.id} | ${swagger.status} | ${swagger.name} | ${swagger.url}`;
57
+ if (swagger.status === 'unreachable') {
58
+ result += ` | ${swagger.error}`;
59
+ }
60
+ result += "\n";
61
+ }
62
+ return result;
63
+ }
64
+ formatEndpointList(swagger) {
65
+ const swaggerIds = swagger
66
+ ? [swagger]
67
+ : this.swaggerCollection.listSwaggers().map((s) => s.id);
68
+ let endpoints = [];
69
+ const unavailableSwaggers = [];
70
+ const missingSwaggers = [];
71
+ for (const swaggerId of swaggerIds) {
72
+ const swaggerInfo = this.swaggerCollection.getSwagger(swaggerId);
73
+ if (!swaggerInfo) {
74
+ missingSwaggers.push(swaggerId);
75
+ continue;
76
+ }
77
+ if (swaggerInfo.status === 'unreachable') {
78
+ unavailableSwaggers.push(swaggerInfo);
79
+ continue;
80
+ }
81
+ endpoints.push(...swaggerInfo.instance.listEndpoints());
82
+ }
83
+ let result = "List of available endpoints (endpointId | method | path | description):\n";
84
+ for (const endpoint of endpoints) {
85
+ const mergedText = endpoint.summary ? `${endpoint.summary}${endpoint.summary.endsWith('.') ? '' : '.'} ${endpoint.description}`.trim() : endpoint.description;
86
+ result += [
87
+ `${endpoint.swaggerName}-${endpoint.operationId}`,
88
+ endpoint.method.toUpperCase() + ' ' + endpoint.path,
89
+ mergedText
90
+ ].join(' | ') + '\n';
91
+ }
92
+ if (unavailableSwaggers.length > 0) {
93
+ result += "\nUnavailable swaggers:\n";
94
+ for (const unavailableSwagger of unavailableSwaggers) {
95
+ result += this.formatUnreachableSwagger(unavailableSwagger) + "\n";
96
+ }
97
+ }
98
+ if (missingSwaggers.length > 0) {
99
+ result += "\nUnknown swaggers:\n";
100
+ for (const missingSwagger of missingSwaggers) {
101
+ result += `${missingSwagger}\n`;
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+ formatEndpointDetailsByIds(endpointIds) {
107
+ let result = [];
108
+ for (const endpointId of endpointIds) {
109
+ const lastHyphenIndex = endpointId.lastIndexOf('-');
110
+ if (lastHyphenIndex === -1) {
111
+ result.push(`Invalid endpoint ID format: ${endpointId}`);
112
+ continue;
113
+ }
114
+ const swaggerName = endpointId.substring(0, lastHyphenIndex);
115
+ const operationId = endpointId.substring(lastHyphenIndex + 1);
116
+ if (!swaggerName || !operationId) {
117
+ result.push(`Invalid endpoint ID format: ${endpointId}`);
118
+ continue;
119
+ }
120
+ const swagger = this.swaggerCollection.getSwagger(swaggerName);
121
+ if (!swagger) {
122
+ result.push(`Swagger not found: ${swaggerName} for endpoint ${endpointId}`);
123
+ continue;
124
+ }
125
+ if (swagger.status === 'unreachable') {
126
+ result.push(`Swagger unreachable: ${this.formatUnreachableSwagger(swagger)} for endpoint ${endpointId}`);
127
+ continue;
128
+ }
129
+ const endpoints = swagger.instance.listEndpoints();
130
+ const endpoint = Array.from(endpoints).find(e => e.operationId === operationId);
131
+ if (!endpoint) {
132
+ result.push(`Endpoint not found: ${operationId} in swagger ${swaggerName}`);
133
+ continue;
134
+ }
135
+ result.push(this.formatEndpointDetails(endpoint));
136
+ }
137
+ return result.join("\n\n---\n\n") || "No endpoint details found.";
138
+ }
139
+ formatUnreachableSwagger(swagger) {
140
+ return `${swagger.id} | ${swagger.url} | ${swagger.error}`;
141
+ }
101
142
  formatBodyExample(body) {
102
143
  if (!body.hasBody) {
103
144
  return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swagger-mcp-server",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "main": "build/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc && chmod 755 build/index.js",
11
- "test": "npm run build && node test/swagger_parser.test.mjs",
11
+ "test": "npm run build && node test/swagger_parser.test.mjs && node test/degraded_startup.test.mjs",
12
12
  "run": "npx @modelcontextprotocol/inspector node build/index.js test_config.json",
13
13
  "prepublishOnly": "npm run build"
14
14
  },