swagger-mcp-server 1.0.1 → 1.0.2
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/swagger_mcp_server.js +27 -14
- package/build/swagger_parser.js +238 -66
- package/package.json +5 -4
|
@@ -98,6 +98,26 @@ export class SwaggerMcpServer {
|
|
|
98
98
|
};
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
|
+
formatBodyExample(body) {
|
|
102
|
+
if (!body.hasBody) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
if (body.isJson) {
|
|
106
|
+
return JSON.stringify(body.value ?? {}, null, 2);
|
|
107
|
+
}
|
|
108
|
+
return String(body.value ?? '<non-JSON response body>');
|
|
109
|
+
}
|
|
110
|
+
appendResponseExample(result, response) {
|
|
111
|
+
result += "```http\n";
|
|
112
|
+
result += `HTTP/2 ${response.statusCode}${response.statusText ? ` ${response.statusText}` : ''}\n`;
|
|
113
|
+
if (response.hasBody) {
|
|
114
|
+
result += `Content-Type: ${response.contentType || (response.isJson ? 'application/json' : 'application/octet-stream')}\n\n`;
|
|
115
|
+
result += this.formatBodyExample(response);
|
|
116
|
+
result += "\n";
|
|
117
|
+
}
|
|
118
|
+
result += "```";
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
101
121
|
formatEndpointDetails(endpoint) {
|
|
102
122
|
let result = "";
|
|
103
123
|
result += `## ${endpoint.operationId} ${endpoint.summary}\n`;
|
|
@@ -142,31 +162,24 @@ export class SwaggerMcpServer {
|
|
|
142
162
|
result += `### Example Request\n`;
|
|
143
163
|
result += "```http\n";
|
|
144
164
|
result += `${endpoint.method.toUpperCase()} ${exampleUrl}\n`;
|
|
145
|
-
if (endpoint.requestBodyExample) {
|
|
146
|
-
result +=
|
|
165
|
+
if (endpoint.requestBodyExample.hasBody) {
|
|
166
|
+
result += `Content-Type: ${endpoint.requestBodyExample.contentType || 'application/json'}\n`;
|
|
147
167
|
}
|
|
148
168
|
for (const param of otherParams) {
|
|
149
169
|
if (param.in === 'header') {
|
|
150
170
|
result += `${param.name}: ${param.example || 'example-value'}\n`;
|
|
151
171
|
}
|
|
152
172
|
}
|
|
153
|
-
if (endpoint.requestBodyExample) {
|
|
173
|
+
if (endpoint.requestBodyExample.hasBody) {
|
|
154
174
|
result += "\n";
|
|
155
|
-
result +=
|
|
175
|
+
result += this.formatBodyExample(endpoint.requestBodyExample);
|
|
156
176
|
}
|
|
157
177
|
result += "\n```\n\n";
|
|
158
178
|
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";
|
|
179
|
+
result = this.appendResponseExample(result, endpoint.successExampleResponse);
|
|
180
|
+
result += "\n\n";
|
|
164
181
|
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```";
|
|
182
|
+
result = this.appendResponseExample(result, endpoint.errorExampleResponse);
|
|
170
183
|
return result;
|
|
171
184
|
}
|
|
172
185
|
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.2",
|
|
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",
|
|
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"
|