swagger-mcp-server 1.0.1 → 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 +123 -69
- package/build/swagger_parser.js +238 -66
- package/package.json +5 -4
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,126 @@ 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
|
+
}
|
|
142
|
+
formatBodyExample(body) {
|
|
143
|
+
if (!body.hasBody) {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
if (body.isJson) {
|
|
147
|
+
return JSON.stringify(body.value ?? {}, null, 2);
|
|
148
|
+
}
|
|
149
|
+
return String(body.value ?? '<non-JSON response body>');
|
|
150
|
+
}
|
|
151
|
+
appendResponseExample(result, response) {
|
|
152
|
+
result += "```http\n";
|
|
153
|
+
result += `HTTP/2 ${response.statusCode}${response.statusText ? ` ${response.statusText}` : ''}\n`;
|
|
154
|
+
if (response.hasBody) {
|
|
155
|
+
result += `Content-Type: ${response.contentType || (response.isJson ? 'application/json' : 'application/octet-stream')}\n\n`;
|
|
156
|
+
result += this.formatBodyExample(response);
|
|
157
|
+
result += "\n";
|
|
158
|
+
}
|
|
159
|
+
result += "```";
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
101
162
|
formatEndpointDetails(endpoint) {
|
|
102
163
|
let result = "";
|
|
103
164
|
result += `## ${endpoint.operationId} ${endpoint.summary}\n`;
|
|
@@ -142,31 +203,24 @@ export class SwaggerMcpServer {
|
|
|
142
203
|
result += `### Example Request\n`;
|
|
143
204
|
result += "```http\n";
|
|
144
205
|
result += `${endpoint.method.toUpperCase()} ${exampleUrl}\n`;
|
|
145
|
-
if (endpoint.requestBodyExample) {
|
|
146
|
-
result +=
|
|
206
|
+
if (endpoint.requestBodyExample.hasBody) {
|
|
207
|
+
result += `Content-Type: ${endpoint.requestBodyExample.contentType || 'application/json'}\n`;
|
|
147
208
|
}
|
|
148
209
|
for (const param of otherParams) {
|
|
149
210
|
if (param.in === 'header') {
|
|
150
211
|
result += `${param.name}: ${param.example || 'example-value'}\n`;
|
|
151
212
|
}
|
|
152
213
|
}
|
|
153
|
-
if (endpoint.requestBodyExample) {
|
|
214
|
+
if (endpoint.requestBodyExample.hasBody) {
|
|
154
215
|
result += "\n";
|
|
155
|
-
result +=
|
|
216
|
+
result += this.formatBodyExample(endpoint.requestBodyExample);
|
|
156
217
|
}
|
|
157
218
|
result += "\n```\n\n";
|
|
158
219
|
result += `### Example Response\n`;
|
|
159
|
-
result
|
|
160
|
-
result += "
|
|
161
|
-
result += "Content-Type: application/json\n\n";
|
|
162
|
-
result += JSON.stringify(endpoint.successExampleResponse, null, 2);
|
|
163
|
-
result += "\n```\n\n";
|
|
220
|
+
result = this.appendResponseExample(result, endpoint.successExampleResponse);
|
|
221
|
+
result += "\n\n";
|
|
164
222
|
result += `### Error Response Example\n`;
|
|
165
|
-
result
|
|
166
|
-
result += "HTTP/2 400 Bad Request\n";
|
|
167
|
-
result += "Content-Type: application/json\n\n";
|
|
168
|
-
result += JSON.stringify(endpoint.errorExampleResponse, null, 2);
|
|
169
|
-
result += "\n```";
|
|
223
|
+
result = this.appendResponseExample(result, endpoint.errorExampleResponse);
|
|
170
224
|
return result;
|
|
171
225
|
}
|
|
172
226
|
async serve() {
|
package/build/swagger_parser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STATUS_CODES } from "node:http";
|
|
1
2
|
export class SwaggerParser {
|
|
2
3
|
name;
|
|
3
4
|
schema;
|
|
@@ -30,29 +31,185 @@ export class SwaggerParser {
|
|
|
30
31
|
default: return '';
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
resolveRef(ref) {
|
|
35
|
+
if (!ref.startsWith('#/')) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const path = ref
|
|
39
|
+
.slice(2)
|
|
40
|
+
.split('/')
|
|
41
|
+
.map((part) => decodeURIComponent(part.replace(/~1/g, '/').replace(/~0/g, '~')));
|
|
42
|
+
let current = this.schema;
|
|
43
|
+
for (const part of path) {
|
|
44
|
+
if (current === undefined || current === null) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
current = current[part];
|
|
48
|
+
}
|
|
49
|
+
return current;
|
|
50
|
+
}
|
|
51
|
+
normalizeMediaType(mediaType) {
|
|
52
|
+
return mediaType.split(';', 1)[0].trim().toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
isJsonMediaType(mediaType) {
|
|
55
|
+
const normalized = this.normalizeMediaType(mediaType);
|
|
56
|
+
return normalized === 'application/json'
|
|
57
|
+
|| normalized === 'application/*+json'
|
|
58
|
+
|| normalized.endsWith('+json');
|
|
59
|
+
}
|
|
60
|
+
resolveSchemaType(schema) {
|
|
61
|
+
if (Array.isArray(schema?.type)) {
|
|
62
|
+
return schema.type.find((type) => type !== 'null');
|
|
63
|
+
}
|
|
64
|
+
if (schema?.type) {
|
|
65
|
+
return schema.type;
|
|
66
|
+
}
|
|
67
|
+
if (schema?.properties || schema?.additionalProperties) {
|
|
68
|
+
return 'object';
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
schemaRepresentsBinary(schema, seenRefs = new Set()) {
|
|
73
|
+
if (!schema || typeof schema !== 'object') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (schema.$ref) {
|
|
77
|
+
if (seenRefs.has(schema.$ref)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const resolved = this.resolveRef(schema.$ref);
|
|
81
|
+
if (!resolved) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
seenRefs.add(schema.$ref);
|
|
85
|
+
const result = this.schemaRepresentsBinary(resolved, seenRefs);
|
|
86
|
+
seenRefs.delete(schema.$ref);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
const schemaType = this.resolveSchemaType(schema);
|
|
90
|
+
if (schemaType === 'string' && ['binary', 'byte'].includes(schema.format)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return ['oneOf', 'anyOf', 'allOf'].some((key) => Array.isArray(schema[key]) && schema[key].some((subSchema) => this.schemaRepresentsBinary(subSchema, seenRefs)));
|
|
94
|
+
}
|
|
95
|
+
wildcardContentLooksJson(media) {
|
|
96
|
+
if (!media) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (media.schema) {
|
|
100
|
+
return !this.schemaRepresentsBinary(media.schema);
|
|
101
|
+
}
|
|
102
|
+
return media.example !== undefined || this.extractFirstExample(media.examples) !== undefined;
|
|
103
|
+
}
|
|
104
|
+
selectJsonContent(content) {
|
|
105
|
+
if (!content || typeof content !== 'object') {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const entries = Object.entries(content);
|
|
109
|
+
const applicationJson = entries.find(([mediaType]) => this.normalizeMediaType(mediaType) === 'application/json');
|
|
110
|
+
if (applicationJson) {
|
|
111
|
+
return { mediaType: applicationJson[0], media: applicationJson[1] };
|
|
112
|
+
}
|
|
113
|
+
const jsonLike = entries.find(([mediaType]) => this.isJsonMediaType(mediaType));
|
|
114
|
+
if (jsonLike) {
|
|
115
|
+
return { mediaType: jsonLike[0], media: jsonLike[1] };
|
|
116
|
+
}
|
|
117
|
+
const wildcard = entries.find(([mediaType, media]) => this.normalizeMediaType(mediaType) === '*/*' && this.wildcardContentLooksJson(media));
|
|
118
|
+
if (wildcard) {
|
|
119
|
+
return { mediaType: 'application/json', media: wildcard[1] };
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
selectFirstContent(content) {
|
|
124
|
+
if (!content || typeof content !== 'object') {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const [first] = Object.entries(content);
|
|
128
|
+
if (!first) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return { mediaType: first[0], media: first[1] };
|
|
132
|
+
}
|
|
133
|
+
extractFirstExample(examples) {
|
|
134
|
+
if (!examples || typeof examples !== 'object') {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
for (const example of Object.values(examples)) {
|
|
138
|
+
if (example && typeof example === 'object' && 'value' in example) {
|
|
139
|
+
return example.value;
|
|
140
|
+
}
|
|
141
|
+
if (example !== undefined && (typeof example !== 'object' || example === null)) {
|
|
142
|
+
return example;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
extractMediaValue(media) {
|
|
148
|
+
if (media?.example !== undefined) {
|
|
149
|
+
return media.example;
|
|
150
|
+
}
|
|
151
|
+
const example = this.extractFirstExample(media?.examples);
|
|
152
|
+
if (example !== undefined) {
|
|
153
|
+
return example;
|
|
154
|
+
}
|
|
155
|
+
if (media?.schema?.example !== undefined) {
|
|
156
|
+
return media.schema.example;
|
|
157
|
+
}
|
|
158
|
+
if (media?.schema) {
|
|
159
|
+
return this.generateSampleFromSchema(media.schema);
|
|
160
|
+
}
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
generateSampleFromSchema(schema, seenRefs = new Set()) {
|
|
34
164
|
if (!schema)
|
|
35
165
|
return {};
|
|
166
|
+
if (schema.example !== undefined) {
|
|
167
|
+
return schema.example;
|
|
168
|
+
}
|
|
36
169
|
if (schema.$ref) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
170
|
+
if (seenRefs.has(schema.$ref)) {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
const resolvedSchema = this.resolveRef(schema.$ref);
|
|
174
|
+
if (resolvedSchema) {
|
|
175
|
+
seenRefs.add(schema.$ref);
|
|
176
|
+
const result = this.generateSampleFromSchema(resolvedSchema, seenRefs);
|
|
177
|
+
seenRefs.delete(schema.$ref);
|
|
178
|
+
return result;
|
|
41
179
|
}
|
|
42
180
|
return {};
|
|
43
181
|
}
|
|
44
|
-
|
|
182
|
+
if (schema.allOf && schema.allOf.length > 0) {
|
|
183
|
+
let result = {};
|
|
184
|
+
for (const subSchema of schema.allOf) {
|
|
185
|
+
result = { ...result, ...this.generateSampleFromSchema(subSchema, seenRefs) };
|
|
186
|
+
}
|
|
187
|
+
if (schema.properties) {
|
|
188
|
+
result = { ...result, ...this.generateSampleFromSchema({ type: 'object', properties: schema.properties }, seenRefs) };
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
193
|
+
return this.generateSampleFromSchema(schema.oneOf[0], seenRefs);
|
|
194
|
+
}
|
|
195
|
+
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
196
|
+
return this.generateSampleFromSchema(schema.anyOf[0], seenRefs);
|
|
197
|
+
}
|
|
198
|
+
switch (this.resolveSchemaType(schema)) {
|
|
45
199
|
case 'object':
|
|
46
200
|
const result = {};
|
|
47
201
|
if (schema.properties) {
|
|
48
202
|
for (const propName in schema.properties) {
|
|
49
|
-
result[propName] = this.generateSampleFromSchema(schema.properties[propName]);
|
|
203
|
+
result[propName] = this.generateSampleFromSchema(schema.properties[propName], seenRefs);
|
|
50
204
|
}
|
|
51
205
|
}
|
|
206
|
+
if (Object.keys(result).length === 0 && schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
207
|
+
result.additionalProperty = this.generateSampleFromSchema(schema.additionalProperties, seenRefs);
|
|
208
|
+
}
|
|
52
209
|
return result;
|
|
53
210
|
case 'array':
|
|
54
211
|
if (schema.items) {
|
|
55
|
-
return [this.generateSampleFromSchema(schema.items)];
|
|
212
|
+
return [this.generateSampleFromSchema(schema.items, seenRefs)];
|
|
56
213
|
}
|
|
57
214
|
return [];
|
|
58
215
|
case 'string':
|
|
@@ -76,38 +233,71 @@ export class SwaggerParser {
|
|
|
76
233
|
case 'null':
|
|
77
234
|
return null;
|
|
78
235
|
default:
|
|
79
|
-
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
80
|
-
return this.generateSampleFromSchema(schema.oneOf[0]);
|
|
81
|
-
}
|
|
82
|
-
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
83
|
-
return this.generateSampleFromSchema(schema.anyOf[0]);
|
|
84
|
-
}
|
|
85
|
-
if (schema.allOf && schema.allOf.length > 0) {
|
|
86
|
-
let result = {};
|
|
87
|
-
for (const subSchema of schema.allOf) {
|
|
88
|
-
result = { ...result, ...this.generateSampleFromSchema(subSchema) };
|
|
89
|
-
}
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
236
|
return {};
|
|
93
237
|
}
|
|
94
238
|
}
|
|
239
|
+
noBodyExample() {
|
|
240
|
+
return {
|
|
241
|
+
hasBody: false,
|
|
242
|
+
isJson: false
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
jsonBodyExample(contentType, media) {
|
|
246
|
+
const normalizedContentType = this.normalizeMediaType(contentType);
|
|
247
|
+
const displayContentType = normalizedContentType === 'application/*+json' ? 'application/json' : contentType;
|
|
248
|
+
return {
|
|
249
|
+
contentType: displayContentType,
|
|
250
|
+
hasBody: true,
|
|
251
|
+
isJson: true,
|
|
252
|
+
value: this.extractMediaValue(media)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
nonJsonBodyExample(contentType, media) {
|
|
256
|
+
const normalizedContentType = this.normalizeMediaType(contentType);
|
|
257
|
+
const displayContentType = normalizedContentType === '*/*' ? 'application/octet-stream' : contentType;
|
|
258
|
+
const isBinary = normalizedContentType.startsWith('image/')
|
|
259
|
+
|| normalizedContentType === 'application/octet-stream'
|
|
260
|
+
|| this.schemaRepresentsBinary(media?.schema);
|
|
261
|
+
return {
|
|
262
|
+
contentType: displayContentType,
|
|
263
|
+
hasBody: true,
|
|
264
|
+
isJson: false,
|
|
265
|
+
value: isBinary ? '<binary response body>' : '<non-JSON response body>'
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
extractJsonBodyExampleFromContent(content) {
|
|
269
|
+
const selectedContent = this.selectJsonContent(content);
|
|
270
|
+
if (!selectedContent) {
|
|
271
|
+
return this.noBodyExample();
|
|
272
|
+
}
|
|
273
|
+
return this.jsonBodyExample(selectedContent.mediaType, selectedContent.media);
|
|
274
|
+
}
|
|
275
|
+
extractResponseBodyExample(content) {
|
|
276
|
+
const selectedJsonContent = this.selectJsonContent(content);
|
|
277
|
+
if (selectedJsonContent) {
|
|
278
|
+
return this.jsonBodyExample(selectedJsonContent.mediaType, selectedJsonContent.media);
|
|
279
|
+
}
|
|
280
|
+
const selectedContent = this.selectFirstContent(content);
|
|
281
|
+
if (selectedContent) {
|
|
282
|
+
return this.nonJsonBodyExample(selectedContent.mediaType, selectedContent.media);
|
|
283
|
+
}
|
|
284
|
+
return this.noBodyExample();
|
|
285
|
+
}
|
|
95
286
|
extractRequestBodyExample(operation) {
|
|
96
|
-
if (operation.requestBody
|
|
97
|
-
|
|
98
|
-
if (content) {
|
|
99
|
-
if (content.example) {
|
|
100
|
-
return content.example;
|
|
101
|
-
}
|
|
102
|
-
if (content.schema && content.schema.example) {
|
|
103
|
-
return content.schema.example;
|
|
104
|
-
}
|
|
105
|
-
if (content.schema) {
|
|
106
|
-
return this.generateSampleFromSchema(content.schema);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
287
|
+
if (operation.requestBody?.content) {
|
|
288
|
+
return this.extractJsonBodyExampleFromContent(operation.requestBody.content);
|
|
109
289
|
}
|
|
110
|
-
return
|
|
290
|
+
return this.noBodyExample();
|
|
291
|
+
}
|
|
292
|
+
statusText(statusCode) {
|
|
293
|
+
return STATUS_CODES[statusCode] || '';
|
|
294
|
+
}
|
|
295
|
+
responseExample(statusCode, body) {
|
|
296
|
+
return {
|
|
297
|
+
...body,
|
|
298
|
+
statusCode,
|
|
299
|
+
statusText: this.statusText(statusCode)
|
|
300
|
+
};
|
|
111
301
|
}
|
|
112
302
|
resolveSuccessExampleResponse(operation) {
|
|
113
303
|
if (operation.responses) {
|
|
@@ -115,23 +305,11 @@ export class SwaggerParser {
|
|
|
115
305
|
const codeNum = parseInt(code, 10);
|
|
116
306
|
if (!isNaN(codeNum) && Math.floor(codeNum / 100) === 2) {
|
|
117
307
|
const successResponse = operation.responses[code];
|
|
118
|
-
|
|
119
|
-
const content = successResponse.content['application/json'];
|
|
120
|
-
if (content.example) {
|
|
121
|
-
return content.example;
|
|
122
|
-
}
|
|
123
|
-
if (content.schema && content.schema.example) {
|
|
124
|
-
return content.schema.example;
|
|
125
|
-
}
|
|
126
|
-
if (content.schema) {
|
|
127
|
-
return this.generateSampleFromSchema(content.schema);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return {};
|
|
308
|
+
return this.responseExample(codeNum, this.extractResponseBodyExample(successResponse.content));
|
|
131
309
|
}
|
|
132
310
|
}
|
|
133
311
|
}
|
|
134
|
-
return
|
|
312
|
+
return this.responseExample(200, this.noBodyExample());
|
|
135
313
|
}
|
|
136
314
|
resolveErrorExampleResponse(operation) {
|
|
137
315
|
if (operation.responses) {
|
|
@@ -139,27 +317,21 @@ export class SwaggerParser {
|
|
|
139
317
|
const codeNum = parseInt(code, 10);
|
|
140
318
|
if (!isNaN(codeNum) && Math.floor(codeNum / 100) >= 4) {
|
|
141
319
|
const errorResponse = operation.responses[code];
|
|
142
|
-
|
|
143
|
-
const content = errorResponse.content['application/json'];
|
|
144
|
-
if (content.example) {
|
|
145
|
-
return content.example;
|
|
146
|
-
}
|
|
147
|
-
if (content.schema && content.schema.example) {
|
|
148
|
-
return content.schema.example;
|
|
149
|
-
}
|
|
150
|
-
if (content.schema) {
|
|
151
|
-
return this.generateSampleFromSchema(content.schema);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
320
|
+
return this.responseExample(codeNum, this.extractResponseBodyExample(errorResponse.content));
|
|
154
321
|
}
|
|
155
322
|
}
|
|
156
323
|
}
|
|
157
|
-
return {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
324
|
+
return this.responseExample(400, {
|
|
325
|
+
contentType: 'application/json',
|
|
326
|
+
hasBody: true,
|
|
327
|
+
isJson: true,
|
|
328
|
+
value: {
|
|
329
|
+
error: {
|
|
330
|
+
code: 400,
|
|
331
|
+
message: "Bad Request"
|
|
332
|
+
}
|
|
161
333
|
}
|
|
162
|
-
};
|
|
334
|
+
});
|
|
163
335
|
}
|
|
164
336
|
listEndpoints() {
|
|
165
337
|
const endpoints = [];
|
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,6 +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 && node test/degraded_startup.test.mjs",
|
|
11
12
|
"run": "npx @modelcontextprotocol/inspector node build/index.js test_config.json",
|
|
12
13
|
"prepublishOnly": "npm run build"
|
|
13
14
|
},
|
|
@@ -24,12 +25,12 @@
|
|
|
24
25
|
"description": "Model Context Protocol server for swagger endpoints",
|
|
25
26
|
"repository": {
|
|
26
27
|
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/
|
|
28
|
+
"url": "git+https://github.com/marcin-sucharski/swagger-mcp-server.git"
|
|
28
29
|
},
|
|
29
30
|
"bugs": {
|
|
30
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "https://github.com/marcin-sucharski/swagger-mcp-server/issues"
|
|
31
32
|
},
|
|
32
|
-
"homepage": "https://github.com/
|
|
33
|
+
"homepage": "https://github.com/marcin-sucharski/swagger-mcp-server#readme",
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
35
36
|
"zod": "^3.24.2"
|