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.
@@ -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 += "Content-Type: application/json\n";
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 += JSON.stringify(endpoint.requestBodyExample, null, 2);
175
+ result += this.formatBodyExample(endpoint.requestBodyExample);
156
176
  }
157
177
  result += "\n```\n\n";
158
178
  result += `### Example Response\n`;
159
- result += "```http\n";
160
- result += "HTTP/2 200 OK\n";
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 += "```http\n";
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() {
@@ -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
- generateSampleFromSchema(schema) {
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
- const refPath = schema.$ref.replace('#/components/schemas/', '');
38
- const schemaObj = this.schema;
39
- if (schemaObj?.components?.schemas?.[refPath]) {
40
- return this.generateSampleFromSchema(schemaObj.components.schemas[refPath]);
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
- switch (schema.type) {
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 && operation.requestBody.content) {
97
- const content = operation.requestBody.content['application/json'];
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
- if (successResponse.content && successResponse.content['application/json']) {
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
- if (errorResponse.content && errorResponse.content['application/json']) {
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
- error: {
159
- code: 400,
160
- message: "Bad Request"
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.1",
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/yourusername/swagger-mcp-server.git"
28
+ "url": "git+https://github.com/marcin-sucharski/swagger-mcp-server.git"
28
29
  },
29
30
  "bugs": {
30
- "url": "https://github.com/yourusername/swagger-mcp-server/issues"
31
+ "url": "https://github.com/marcin-sucharski/swagger-mcp-server/issues"
31
32
  },
32
- "homepage": "https://github.com/yourusername/swagger-mcp-server#readme",
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"