swagger-mcp-server 1.0.0 → 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`;
@@ -107,8 +127,7 @@ export class SwaggerMcpServer {
107
127
  }
108
128
  const pathParams = endpoint.parameters.filter(p => p.in === 'path');
109
129
  const queryParams = endpoint.parameters.filter(p => p.in === 'query');
110
- const bodyParams = endpoint.parameters.filter(p => p.in === 'body');
111
- const otherParams = endpoint.parameters.filter(p => !['path', 'query', 'body'].includes(p.in));
130
+ const otherParams = endpoint.parameters.filter(p => !['path', 'query'].includes(p.in));
112
131
  if (pathParams.length > 0) {
113
132
  result += `### Path Parameters\n`;
114
133
  for (const param of pathParams) {
@@ -123,13 +142,6 @@ export class SwaggerMcpServer {
123
142
  }
124
143
  result += `\n`;
125
144
  }
126
- if (bodyParams.length > 0) {
127
- result += `### Body Parameters\n`;
128
- for (const param of bodyParams) {
129
- result += `- \`${param.name}\` (${param.type}${param.required ? ', required' : ''}): ${param.description}\n`;
130
- }
131
- result += `\n`;
132
- }
133
145
  if (otherParams.length > 0) {
134
146
  result += `### Other Parameters\n`;
135
147
  for (const param of otherParams) {
@@ -150,32 +162,24 @@ export class SwaggerMcpServer {
150
162
  result += `### Example Request\n`;
151
163
  result += "```http\n";
152
164
  result += `${endpoint.method.toUpperCase()} ${exampleUrl}\n`;
153
- const hasRequestBody = bodyParams.length > 0;
154
- if (hasRequestBody) {
155
- result += "Content-Type: application/json\n";
165
+ if (endpoint.requestBodyExample.hasBody) {
166
+ result += `Content-Type: ${endpoint.requestBodyExample.contentType || 'application/json'}\n`;
156
167
  }
157
168
  for (const param of otherParams) {
158
169
  if (param.in === 'header') {
159
170
  result += `${param.name}: ${param.example || 'example-value'}\n`;
160
171
  }
161
172
  }
162
- if (hasRequestBody) {
173
+ if (endpoint.requestBodyExample.hasBody) {
163
174
  result += "\n";
164
- result += JSON.stringify(endpoint.requestBodyExample, null, 2);
175
+ result += this.formatBodyExample(endpoint.requestBodyExample);
165
176
  }
166
177
  result += "\n```\n\n";
167
178
  result += `### Example Response\n`;
168
- result += "```http\n";
169
- result += "HTTP/2 200 OK\n";
170
- result += "Content-Type: application/json\n\n";
171
- result += JSON.stringify(endpoint.successExampleResponse, null, 2);
172
- result += "\n```\n\n";
179
+ result = this.appendResponseExample(result, endpoint.successExampleResponse);
180
+ result += "\n\n";
173
181
  result += `### Error Response Example\n`;
174
- result += "```http\n";
175
- result += "HTTP/2 400 Bad Request\n";
176
- result += "Content-Type: application/json\n\n";
177
- result += JSON.stringify(endpoint.errorExampleResponse, null, 2);
178
- result += "\n```";
182
+ result = this.appendResponseExample(result, endpoint.errorExampleResponse);
179
183
  return result;
180
184
  }
181
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,56 +233,83 @@ 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
- if (operation.responses && operation.responses['200']) {
114
- const successResponse = operation.responses['200'];
115
- if (successResponse.content && successResponse.content['application/json']) {
116
- const content = successResponse.content['application/json'];
117
- if (content.example) {
118
- return content.example;
119
- }
120
- if (content.schema && content.schema.example) {
121
- return content.schema.example;
122
- }
123
- if (content.schema) {
124
- return this.generateSampleFromSchema(content.schema);
303
+ if (operation.responses) {
304
+ for (const code in operation.responses) {
305
+ const codeNum = parseInt(code, 10);
306
+ if (!isNaN(codeNum) && Math.floor(codeNum / 100) === 2) {
307
+ const successResponse = operation.responses[code];
308
+ return this.responseExample(codeNum, this.extractResponseBodyExample(successResponse.content));
125
309
  }
126
310
  }
127
311
  }
128
- return {};
312
+ return this.responseExample(200, this.noBodyExample());
129
313
  }
130
314
  resolveErrorExampleResponse(operation) {
131
315
  if (operation.responses) {
@@ -133,27 +317,21 @@ export class SwaggerParser {
133
317
  const codeNum = parseInt(code, 10);
134
318
  if (!isNaN(codeNum) && Math.floor(codeNum / 100) >= 4) {
135
319
  const errorResponse = operation.responses[code];
136
- if (errorResponse.content && errorResponse.content['application/json']) {
137
- const content = errorResponse.content['application/json'];
138
- if (content.example) {
139
- return content.example;
140
- }
141
- if (content.schema && content.schema.example) {
142
- return content.schema.example;
143
- }
144
- if (content.schema) {
145
- return this.generateSampleFromSchema(content.schema);
146
- }
147
- }
320
+ return this.responseExample(codeNum, this.extractResponseBodyExample(errorResponse.content));
148
321
  }
149
322
  }
150
323
  }
151
- return {
152
- error: {
153
- code: 400,
154
- 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
+ }
155
333
  }
156
- };
334
+ });
157
335
  }
158
336
  listEndpoints() {
159
337
  const endpoints = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swagger-mcp-server",
3
- "version": "1.0.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/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"