swagger-mcp-server 1.0.0

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/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Swagger MCP Server
2
+
3
+ A Model Context Protocol server for Swagger/OpenAPI endpoints. This tool allows you to expose Swagger-defined APIs through the Model Context Protocol, making them accessible to AI agents.
4
+
5
+ ## Installation
6
+
7
+ You can install the package globally:
8
+
9
+ ```bash
10
+ npm install -g swagger-mcp-server
11
+ ```
12
+
13
+ Or use it directly with npx:
14
+
15
+ ```bash
16
+ npx swagger-mcp-server <config-file>
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ 1. Create a configuration file (JSON) that defines the Swagger endpoints you want to expose:
22
+
23
+ ```json
24
+ {
25
+ "endpoints": [
26
+ {
27
+ "name": "example-api",
28
+ "url": "https://example.com/api/swagger.json"
29
+ }
30
+ ]
31
+ }
32
+ ```
33
+
34
+ 2. Run the server:
35
+
36
+ ```bash
37
+ swagger-mcp-server config.json
38
+ ```
39
+
40
+ Or with npx:
41
+
42
+ ```bash
43
+ npx swagger-mcp-server config.json
44
+ ```
45
+
46
+ ## Configuration Options
47
+
48
+ The configuration file supports the following options:
49
+
50
+ - `endpoints`: An array of Swagger endpoints to expose
51
+ - `name`: A unique identifier for the endpoint
52
+ - `url`: URL to the Swagger/OpenAPI JSON definition
53
+
54
+ ## Development
55
+
56
+ To build the project:
57
+
58
+ ```bash
59
+ npm run build
60
+ ```
61
+
62
+ To run locally:
63
+
64
+ ```bash
65
+ npm run run
66
+ ```
67
+
68
+ ## License
69
+
70
+ ISC
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import fs from "fs";
3
+ export const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
4
+ export const jsonSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]));
5
+ export const ConfigSchema = z.object({
6
+ endpoints: z.array(z.object({
7
+ name: z.string(),
8
+ url: z.string().url(),
9
+ schema: jsonSchema
10
+ }))
11
+ });
12
+ export async function loadConfig(configPath) {
13
+ try {
14
+ const configFile = fs.readFileSync(configPath, 'utf8');
15
+ const parsedConfig = JSON.parse(configFile);
16
+ const endpointsWithSchema = await Promise.all(parsedConfig.endpoints
17
+ .map(async ({ url, name }) => {
18
+ const response = await fetch(url);
19
+ const schema = await response.json();
20
+ return { url, name, schema };
21
+ }));
22
+ return ConfigSchema.parse({ endpoints: endpointsWithSchema });
23
+ }
24
+ catch (error) {
25
+ console.error(`Error loading config from ${configPath}:`, error);
26
+ process.exit(1);
27
+ }
28
+ }
package/build/index.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from './config.js';
3
+ import { SwaggerCollection } from "./swager_collection.js";
4
+ import { SwaggerMcpServer } from "./swagger_mcp_server.js";
5
+ let config;
6
+ async function main() {
7
+ const args = process.argv.slice(2);
8
+ if (args.length > 0) {
9
+ const configPath = args[0];
10
+ console.error(`Loading configuration from: ${configPath}`);
11
+ config = await loadConfig(configPath);
12
+ console.error(`Loaded ${config.endpoints.length} endpoints from config`);
13
+ }
14
+ else {
15
+ console.error("No config file provided. Exiting.");
16
+ process.exit(1);
17
+ }
18
+ const swaggerCollection = new SwaggerCollection(config.endpoints);
19
+ const swaggerMcpServer = new SwaggerMcpServer(swaggerCollection);
20
+ await swaggerMcpServer.serve();
21
+ }
22
+ main().catch((error) => {
23
+ console.error("Fatal error in main():", error);
24
+ process.exit(1);
25
+ });
@@ -0,0 +1,26 @@
1
+ import { SwaggerParser } from "./swagger_parser.js";
2
+ export class SwaggerCollection {
3
+ swaggers;
4
+ constructor(swaggers) {
5
+ this.swaggers = swaggers;
6
+ this.swaggers = swaggers;
7
+ }
8
+ listSwaggers() {
9
+ return this.swaggers.map((swagger) => this.mapSwagger(swagger));
10
+ }
11
+ getSwagger(id) {
12
+ let result = this.swaggers.find((swagger) => swagger.name === id);
13
+ if (result) {
14
+ return this.mapSwagger(result);
15
+ }
16
+ return null;
17
+ }
18
+ mapSwagger(swagger) {
19
+ return {
20
+ id: swagger.name,
21
+ url: swagger.url,
22
+ name: swagger.schema.info.title,
23
+ instance: new SwaggerParser(swagger.name, swagger.schema)
24
+ };
25
+ }
26
+ }
@@ -0,0 +1,186 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ export class SwaggerMcpServer {
5
+ swaggerCollection;
6
+ server;
7
+ constructor(swaggerCollection) {
8
+ this.swaggerCollection = swaggerCollection;
9
+ this.server = new McpServer({
10
+ name: "swagger",
11
+ version: "1.0.0",
12
+ });
13
+ this.server.tool("list-swaggers", "List all connected swagger endpoints", async () => {
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
+ }
19
+ return {
20
+ content: [
21
+ {
22
+ type: "text",
23
+ text: result
24
+ }
25
+ ]
26
+ };
27
+ });
28
+ 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.", {
30
+ swagger: z.string().optional().describe("Swagger id returned by list-swaggers")
31
+ }, 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
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: result
56
+ }
57
+ ]
58
+ };
59
+ });
60
+ this.server.tool("get-endpoints", "Get detailed information about specific endpoints", {
61
+ endpointIds: z.array(z.string())
62
+ .describe("List of endpoint IDs to retrieve details for. " +
63
+ "Endpoint ids can be found in the list-endpoints tool.")
64
+ }, 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
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: result.join("\n\n---\n\n") || "No endpoint details found."
96
+ }
97
+ ]
98
+ };
99
+ });
100
+ }
101
+ formatEndpointDetails(endpoint) {
102
+ let result = "";
103
+ result += `## ${endpoint.operationId} ${endpoint.summary}\n`;
104
+ result += `### URL: ${endpoint.method.toUpperCase()} ${endpoint.path}\n`;
105
+ if (endpoint.description) {
106
+ result += `### Description\n${endpoint.description}\n`;
107
+ }
108
+ const pathParams = endpoint.parameters.filter(p => p.in === 'path');
109
+ 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));
112
+ if (pathParams.length > 0) {
113
+ result += `### Path Parameters\n`;
114
+ for (const param of pathParams) {
115
+ result += `- \`${param.name}\` (${param.type}${param.required ? ', required' : ''}): ${param.description}\n`;
116
+ }
117
+ result += `\n`;
118
+ }
119
+ if (queryParams.length > 0) {
120
+ result += `### Query Parameters\n`;
121
+ for (const param of queryParams) {
122
+ result += `- \`${param.name}\` (${param.type}${param.required ? ', required' : ''}): ${param.description}\n`;
123
+ }
124
+ result += `\n`;
125
+ }
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
+ if (otherParams.length > 0) {
134
+ result += `### Other Parameters\n`;
135
+ for (const param of otherParams) {
136
+ result += `- \`${param.name}\` (${param.in}, ${param.type}${param.required ? ', required' : ''}): ${param.description}\n`;
137
+ }
138
+ result += `\n`;
139
+ }
140
+ let exampleUrl = endpoint.path;
141
+ for (const param of pathParams) {
142
+ exampleUrl = exampleUrl.replace(`{${param.name}}`, param.example || `{${param.name}}`);
143
+ }
144
+ if (queryParams.length > 0) {
145
+ exampleUrl += '?';
146
+ exampleUrl += queryParams
147
+ .map(p => `${p.name}=${encodeURIComponent(p.example || `{${p.name}}`)}`)
148
+ .join('&');
149
+ }
150
+ result += `### Example Request\n`;
151
+ result += "```http\n";
152
+ result += `${endpoint.method.toUpperCase()} ${exampleUrl}\n`;
153
+ const hasRequestBody = bodyParams.length > 0;
154
+ if (hasRequestBody) {
155
+ result += "Content-Type: application/json\n";
156
+ }
157
+ for (const param of otherParams) {
158
+ if (param.in === 'header') {
159
+ result += `${param.name}: ${param.example || 'example-value'}\n`;
160
+ }
161
+ }
162
+ if (hasRequestBody) {
163
+ result += "\n";
164
+ result += JSON.stringify(endpoint.requestBodyExample, null, 2);
165
+ }
166
+ result += "\n```\n\n";
167
+ 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";
173
+ 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```";
179
+ return result;
180
+ }
181
+ async serve() {
182
+ const transport = new StdioServerTransport();
183
+ await this.server.connect(transport);
184
+ console.error("Swagger MCP Server running on stdio");
185
+ }
186
+ }
@@ -0,0 +1,184 @@
1
+ export class SwaggerParser {
2
+ name;
3
+ schema;
4
+ constructor(name, schema) {
5
+ this.name = name;
6
+ this.schema = schema;
7
+ }
8
+ extractParameters(operation) {
9
+ const parameters = [];
10
+ if (operation.parameters) {
11
+ for (const param of operation.parameters) {
12
+ parameters.push({
13
+ name: param.name,
14
+ in: param.in || 'query', // Default to query if not specified
15
+ type: param.schema?.type || param.type || 'string',
16
+ description: param.description || '',
17
+ required: param.required || false,
18
+ example: param.example || this.generateExampleForType(param.schema?.type || param.type || 'string')
19
+ });
20
+ }
21
+ }
22
+ return parameters;
23
+ }
24
+ generateExampleForType(type) {
25
+ switch (type) {
26
+ case 'string': return 'example_string';
27
+ case 'integer': return '123';
28
+ case 'number': return '123.45';
29
+ case 'boolean': return 'true';
30
+ default: return '';
31
+ }
32
+ }
33
+ generateSampleFromSchema(schema) {
34
+ if (!schema)
35
+ return {};
36
+ 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]);
41
+ }
42
+ return {};
43
+ }
44
+ switch (schema.type) {
45
+ case 'object':
46
+ const result = {};
47
+ if (schema.properties) {
48
+ for (const propName in schema.properties) {
49
+ result[propName] = this.generateSampleFromSchema(schema.properties[propName]);
50
+ }
51
+ }
52
+ return result;
53
+ case 'array':
54
+ if (schema.items) {
55
+ return [this.generateSampleFromSchema(schema.items)];
56
+ }
57
+ return [];
58
+ case 'string':
59
+ if (schema.enum && schema.enum.length > 0) {
60
+ return schema.enum[0];
61
+ }
62
+ if (schema.format === 'date-time')
63
+ return new Date().toISOString();
64
+ if (schema.format === 'date')
65
+ return new Date().toISOString().split('T')[0];
66
+ if (schema.format === 'email')
67
+ return 'user@example.com';
68
+ if (schema.format === 'uuid')
69
+ return '00000000-0000-0000-0000-000000000000';
70
+ return 'string';
71
+ case 'number':
72
+ case 'integer':
73
+ return 0;
74
+ case 'boolean':
75
+ return false;
76
+ case 'null':
77
+ return null;
78
+ 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
+ return {};
93
+ }
94
+ }
95
+ 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
+ }
109
+ }
110
+ return {};
111
+ }
112
+ 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);
125
+ }
126
+ }
127
+ }
128
+ return {};
129
+ }
130
+ resolveErrorExampleResponse(operation) {
131
+ if (operation.responses) {
132
+ for (const code in operation.responses) {
133
+ const codeNum = parseInt(code, 10);
134
+ if (!isNaN(codeNum) && Math.floor(codeNum / 100) >= 4) {
135
+ 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
+ }
148
+ }
149
+ }
150
+ }
151
+ return {
152
+ error: {
153
+ code: 400,
154
+ message: "Bad Request"
155
+ }
156
+ };
157
+ }
158
+ listEndpoints() {
159
+ const endpoints = [];
160
+ const schemaObj = this.schema;
161
+ const paths = schemaObj?.paths || {};
162
+ for (const path in paths) {
163
+ const pathItem = paths[path];
164
+ for (const method in pathItem) {
165
+ if (['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method)) {
166
+ const operation = pathItem[method];
167
+ endpoints.push({
168
+ swaggerName: this.name,
169
+ path,
170
+ operationId: operation.operationId || `${method}${path.replace(/\//g, '_')}`,
171
+ method,
172
+ summary: operation.summary || '',
173
+ description: operation.description || '',
174
+ parameters: this.extractParameters(operation),
175
+ requestBodyExample: this.extractRequestBodyExample(operation),
176
+ successExampleResponse: this.resolveSuccessExampleResponse(operation),
177
+ errorExampleResponse: this.resolveErrorExampleResponse(operation)
178
+ });
179
+ }
180
+ }
181
+ }
182
+ return endpoints;
183
+ }
184
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "swagger-mcp-server",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "build/index.js",
6
+ "bin": {
7
+ "swagger-mcp-server": "build/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && chmod 755 build/index.js",
11
+ "run": "npx @modelcontextprotocol/inspector node build/index.js test_config.json",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "swagger",
16
+ "openapi",
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "ai",
20
+ "api"
21
+ ],
22
+ "author": "Marcin Sucharski",
23
+ "license": "ISC",
24
+ "description": "Model Context Protocol server for swagger endpoints",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/yourusername/swagger-mcp-server.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/yourusername/swagger-mcp-server/issues"
31
+ },
32
+ "homepage": "https://github.com/yourusername/swagger-mcp-server#readme",
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.7.0",
35
+ "zod": "^3.24.2"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.13.10",
39
+ "typescript": "^5.8.2"
40
+ },
41
+ "engines": {
42
+ "node": ">=16.0.0"
43
+ },
44
+ "files": [
45
+ "build/**/*"
46
+ ]
47
+ }