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 +53 -12
- package/build/swager_collection.js +13 -2
- package/build/swagger_mcp_server.js +96 -55
- package/package.json +2 -2
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.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
},
|