api2ai 1.0.1
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 +189 -0
- package/bun.lock +17 -0
- package/example-petstore/.env.example +14 -0
- package/example-petstore/README.md +145 -0
- package/example-petstore/bun.lock +387 -0
- package/example-petstore/package.json +19 -0
- package/example-petstore/src/http-client.js +120 -0
- package/example-petstore/src/index.js +493 -0
- package/example-petstore/src/tools-config.js +247 -0
- package/example.js +164 -0
- package/generate-mcp-use-server.js +832 -0
- package/package.json +31 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAPI to MCP Server Generator (mcp-use framework)
|
|
5
|
+
*
|
|
6
|
+
* Generates a complete MCP server using the mcp-use framework from any OpenAPI spec.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node generate-mcp-use-server.js <openapi-spec> [output-folder] [options]
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* node generate-mcp-use-server.js ./petstore.json ./my-mcp-server
|
|
13
|
+
* node generate-mcp-use-server.js https://petstore3.swagger.io/api/v3/openapi.json ./petstore-mcp --base-url https://petstore3.swagger.io/api/v3
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs/promises';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// OpenAPI Spec Loading & Parsing
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
async function loadOpenApiSpec(specPathOrUrl) {
|
|
24
|
+
if (specPathOrUrl.startsWith('http://') || specPathOrUrl.startsWith('https://')) {
|
|
25
|
+
const response = await fetch(specPathOrUrl);
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Failed to fetch spec: ${response.status} ${response.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
return response.json();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const content = await fs.readFile(specPathOrUrl, 'utf-8');
|
|
33
|
+
|
|
34
|
+
if (specPathOrUrl.endsWith('.yaml') || specPathOrUrl.endsWith('.yml')) {
|
|
35
|
+
const yaml = await import('js-yaml').catch(() => null);
|
|
36
|
+
if (yaml) {
|
|
37
|
+
return yaml.load(content);
|
|
38
|
+
}
|
|
39
|
+
throw new Error('YAML spec detected but js-yaml is not installed. Run: npm install js-yaml');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return JSON.parse(content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Convert OpenAPI schema to Zod schema string
|
|
46
|
+
function schemaToZod(schema, required = false) {
|
|
47
|
+
if (!schema) return 'z.unknown()';
|
|
48
|
+
|
|
49
|
+
let zodStr;
|
|
50
|
+
|
|
51
|
+
switch (schema.type) {
|
|
52
|
+
case 'string':
|
|
53
|
+
if (schema.enum) {
|
|
54
|
+
zodStr = `z.enum([${schema.enum.map(e => `'${e}'`).join(', ')}])`;
|
|
55
|
+
} else if (schema.format === 'date-time') {
|
|
56
|
+
zodStr = 'z.string().datetime()';
|
|
57
|
+
} else if (schema.format === 'date') {
|
|
58
|
+
zodStr = 'z.string().date()';
|
|
59
|
+
} else if (schema.format === 'email') {
|
|
60
|
+
zodStr = 'z.string().email()';
|
|
61
|
+
} else if (schema.format === 'uri' || schema.format === 'url') {
|
|
62
|
+
zodStr = 'z.string().url()';
|
|
63
|
+
} else {
|
|
64
|
+
zodStr = 'z.string()';
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case 'integer':
|
|
69
|
+
zodStr = 'z.number().int()';
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'number':
|
|
73
|
+
zodStr = 'z.number()';
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case 'boolean':
|
|
77
|
+
zodStr = 'z.boolean()';
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'array':
|
|
81
|
+
zodStr = `z.array(${schemaToZod(schema.items, true)})`;
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'object':
|
|
85
|
+
if (schema.properties) {
|
|
86
|
+
const props = Object.entries(schema.properties)
|
|
87
|
+
.map(([key, val]) => {
|
|
88
|
+
const isReq = schema.required?.includes(key);
|
|
89
|
+
const propZod = schemaToZod(val, isReq);
|
|
90
|
+
// Add description if available
|
|
91
|
+
const desc = val.description ? `.describe('${val.description.replace(/'/g, "\\'")}')` : '';
|
|
92
|
+
return ` ${sanitizePropertyName(key)}: ${propZod}${desc}`;
|
|
93
|
+
})
|
|
94
|
+
.join(',\n');
|
|
95
|
+
zodStr = `z.object({\n${props}\n })`;
|
|
96
|
+
} else if (schema.additionalProperties) {
|
|
97
|
+
zodStr = `z.record(z.string(), ${schemaToZod(schema.additionalProperties, true)})`;
|
|
98
|
+
} else {
|
|
99
|
+
zodStr = 'z.record(z.string(), z.unknown())';
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
// Handle anyOf, oneOf, allOf
|
|
105
|
+
if (schema.anyOf) {
|
|
106
|
+
const options = schema.anyOf.map(s => schemaToZod(s, true)).join(', ');
|
|
107
|
+
zodStr = `z.union([${options}])`;
|
|
108
|
+
} else if (schema.oneOf) {
|
|
109
|
+
const options = schema.oneOf.map(s => schemaToZod(s, true)).join(', ');
|
|
110
|
+
zodStr = `z.union([${options}])`;
|
|
111
|
+
} else {
|
|
112
|
+
zodStr = 'z.unknown()';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add optional if not required
|
|
117
|
+
if (!required) {
|
|
118
|
+
zodStr += '.optional()';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return zodStr;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Sanitize property name for JS object key
|
|
125
|
+
function sanitizePropertyName(name) {
|
|
126
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
127
|
+
return name;
|
|
128
|
+
}
|
|
129
|
+
return `'${name}'`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Build Zod schema for tool parameters
|
|
133
|
+
function buildZodSchema(operation, pathParams) {
|
|
134
|
+
const allParams = [...(pathParams || []), ...(operation.parameters || [])];
|
|
135
|
+
const properties = [];
|
|
136
|
+
|
|
137
|
+
for (const param of allParams) {
|
|
138
|
+
const schema = param.schema || { type: 'string' };
|
|
139
|
+
const zodType = schemaToZod(schema, param.required);
|
|
140
|
+
const desc = param.description ? `.describe('${param.description.replace(/'/g, "\\'")}')` : '';
|
|
141
|
+
properties.push(` ${sanitizePropertyName(param.name)}: ${zodType}${desc}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle request body
|
|
145
|
+
if (operation.requestBody) {
|
|
146
|
+
const content = operation.requestBody.content;
|
|
147
|
+
const mediaType = content?.['application/json'] || Object.values(content || {})[0];
|
|
148
|
+
|
|
149
|
+
if (mediaType?.schema) {
|
|
150
|
+
const zodType = schemaToZod(mediaType.schema, operation.requestBody.required);
|
|
151
|
+
const desc = operation.requestBody.description
|
|
152
|
+
? `.describe('${operation.requestBody.description.replace(/'/g, "\\'")}')`
|
|
153
|
+
: `.describe('Request body')`;
|
|
154
|
+
properties.push(` requestBody: ${zodType}${desc}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (properties.length === 0) {
|
|
159
|
+
return 'z.object({})';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return `z.object({\n${properties.join(',\n')}\n })`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Extract tools from OpenAPI spec
|
|
166
|
+
function extractTools(spec, options = {}) {
|
|
167
|
+
const tools = [];
|
|
168
|
+
const { baseUrl: overrideBaseUrl, excludeOperationIds = [], filterFn } = options;
|
|
169
|
+
|
|
170
|
+
let baseUrl = overrideBaseUrl;
|
|
171
|
+
if (!baseUrl && spec.servers && spec.servers.length > 0) {
|
|
172
|
+
baseUrl = spec.servers[0].url;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [pathTemplate, pathItem] of Object.entries(spec.paths || {})) {
|
|
176
|
+
const pathParams = pathItem.parameters || [];
|
|
177
|
+
|
|
178
|
+
for (const method of ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']) {
|
|
179
|
+
const operation = pathItem[method];
|
|
180
|
+
if (!operation) continue;
|
|
181
|
+
|
|
182
|
+
const operationId = operation.operationId ||
|
|
183
|
+
`${method}_${pathTemplate.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
184
|
+
|
|
185
|
+
if (excludeOperationIds.includes(operationId)) continue;
|
|
186
|
+
|
|
187
|
+
// Build tool name (sanitized)
|
|
188
|
+
const name = operationId
|
|
189
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
190
|
+
.replace(/_+/g, '_')
|
|
191
|
+
.replace(/^_|_$/g, '');
|
|
192
|
+
|
|
193
|
+
const description = operation.summary ||
|
|
194
|
+
operation.description ||
|
|
195
|
+
`${method.toUpperCase()} ${pathTemplate}`;
|
|
196
|
+
|
|
197
|
+
// Build Zod schema string
|
|
198
|
+
const zodSchema = buildZodSchema(operation, pathParams);
|
|
199
|
+
|
|
200
|
+
// Extract execution parameters
|
|
201
|
+
const allParams = [...pathParams, ...(operation.parameters || [])];
|
|
202
|
+
const executionParameters = allParams.map(p => ({
|
|
203
|
+
name: p.name,
|
|
204
|
+
in: p.in,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
let requestBodyContentType;
|
|
208
|
+
if (operation.requestBody?.content) {
|
|
209
|
+
requestBodyContentType = Object.keys(operation.requestBody.content)[0];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const tool = {
|
|
213
|
+
name,
|
|
214
|
+
description: description.substring(0, 1024),
|
|
215
|
+
zodSchema,
|
|
216
|
+
method,
|
|
217
|
+
pathTemplate,
|
|
218
|
+
executionParameters,
|
|
219
|
+
requestBodyContentType,
|
|
220
|
+
operationId,
|
|
221
|
+
baseUrl,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (filterFn && !filterFn(tool)) continue;
|
|
225
|
+
|
|
226
|
+
tools.push(tool);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return tools;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Code Generation
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
function generatePackageJson(serverName, tools, port) {
|
|
238
|
+
return JSON.stringify({
|
|
239
|
+
name: serverName,
|
|
240
|
+
version: '1.0.0',
|
|
241
|
+
description: `MCP server generated from OpenAPI spec (${tools.length} tools)`,
|
|
242
|
+
type: 'module',
|
|
243
|
+
main: 'src/index.js',
|
|
244
|
+
scripts: {
|
|
245
|
+
start: 'node src/index.js',
|
|
246
|
+
dev: 'node --watch src/index.js',
|
|
247
|
+
},
|
|
248
|
+
dependencies: {
|
|
249
|
+
'mcp-use': '^0.2.0',
|
|
250
|
+
'zod': '^3.23.0',
|
|
251
|
+
'dotenv': '^16.4.0',
|
|
252
|
+
},
|
|
253
|
+
engines: { node: '>=18.0.0' },
|
|
254
|
+
}, null, 2);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function generateEnvFile(baseUrl, port) {
|
|
258
|
+
return `# Server Configuration
|
|
259
|
+
PORT=${port}
|
|
260
|
+
NODE_ENV=development
|
|
261
|
+
|
|
262
|
+
# API Configuration
|
|
263
|
+
API_BASE_URL=${baseUrl || 'https://api.example.com'}
|
|
264
|
+
|
|
265
|
+
# Authentication (uncomment and configure as needed)
|
|
266
|
+
# API_KEY=your-api-key
|
|
267
|
+
# API_AUTH_HEADER=X-Custom-Auth:your-token
|
|
268
|
+
|
|
269
|
+
# MCP Server URL (for UI widgets in production)
|
|
270
|
+
# MCP_URL=https://your-production-url.com
|
|
271
|
+
|
|
272
|
+
# Allowed Origins (comma-separated, for production)
|
|
273
|
+
# ALLOWED_ORIGINS=https://app1.com,https://app2.com
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function generateEnvExampleFile(baseUrl, port) {
|
|
278
|
+
return `# Server Configuration
|
|
279
|
+
PORT=${port}
|
|
280
|
+
NODE_ENV=development
|
|
281
|
+
|
|
282
|
+
# API Configuration
|
|
283
|
+
API_BASE_URL=${baseUrl || 'https://api.example.com'}
|
|
284
|
+
|
|
285
|
+
# Authentication
|
|
286
|
+
API_KEY=your-api-key-here
|
|
287
|
+
# API_AUTH_HEADER=Header-Name:header-value
|
|
288
|
+
|
|
289
|
+
# MCP Configuration
|
|
290
|
+
# MCP_URL=https://your-mcp-server.com
|
|
291
|
+
# ALLOWED_ORIGINS=https://allowed-origin.com
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function generateHttpClient() {
|
|
296
|
+
return `// HTTP client for API requests
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Build URL with path parameters substituted
|
|
300
|
+
*/
|
|
301
|
+
export function buildUrl(baseUrl, pathTemplate, pathParams = {}) {
|
|
302
|
+
let url = pathTemplate;
|
|
303
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
|
304
|
+
url = url.replace(\`{\${key}}\`, encodeURIComponent(String(value)));
|
|
305
|
+
}
|
|
306
|
+
return new URL(url, baseUrl).toString();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Build query string from parameters
|
|
311
|
+
*/
|
|
312
|
+
export function buildQueryString(queryParams = {}) {
|
|
313
|
+
const params = new URLSearchParams();
|
|
314
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
315
|
+
if (value !== undefined && value !== null) {
|
|
316
|
+
if (Array.isArray(value)) {
|
|
317
|
+
value.forEach(v => params.append(key, String(v)));
|
|
318
|
+
} else {
|
|
319
|
+
params.append(key, String(value));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return params.toString();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Execute HTTP request for a tool
|
|
328
|
+
*/
|
|
329
|
+
export async function executeRequest(toolConfig, args, config = {}) {
|
|
330
|
+
const { baseUrl: configBaseUrl, headers: configHeaders = {} } = config;
|
|
331
|
+
const baseUrl = configBaseUrl || toolConfig.baseUrl;
|
|
332
|
+
|
|
333
|
+
if (!baseUrl) {
|
|
334
|
+
throw new Error(\`No base URL configured for tool: \${toolConfig.name}\`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Separate parameters by location
|
|
338
|
+
const pathParams = {};
|
|
339
|
+
const queryParams = {};
|
|
340
|
+
const headerParams = {};
|
|
341
|
+
let body;
|
|
342
|
+
|
|
343
|
+
for (const param of toolConfig.executionParameters || []) {
|
|
344
|
+
const value = args[param.name];
|
|
345
|
+
if (value === undefined) continue;
|
|
346
|
+
|
|
347
|
+
switch (param.in) {
|
|
348
|
+
case 'path':
|
|
349
|
+
pathParams[param.name] = value;
|
|
350
|
+
break;
|
|
351
|
+
case 'query':
|
|
352
|
+
queryParams[param.name] = value;
|
|
353
|
+
break;
|
|
354
|
+
case 'header':
|
|
355
|
+
headerParams[param.name] = value;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Handle request body
|
|
361
|
+
if (args.requestBody !== undefined) {
|
|
362
|
+
body = args.requestBody;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Build URL
|
|
366
|
+
let url = buildUrl(baseUrl, toolConfig.pathTemplate, pathParams);
|
|
367
|
+
|
|
368
|
+
// Add query parameters
|
|
369
|
+
const queryString = buildQueryString(queryParams);
|
|
370
|
+
if (queryString) {
|
|
371
|
+
url += (url.includes('?') ? '&' : '?') + queryString;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Build headers
|
|
375
|
+
const headers = {
|
|
376
|
+
'Accept': 'application/json',
|
|
377
|
+
...configHeaders,
|
|
378
|
+
...headerParams,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Set content type for request body
|
|
382
|
+
if (body !== undefined) {
|
|
383
|
+
headers['Content-Type'] = toolConfig.requestBodyContentType || 'application/json';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Build request options
|
|
387
|
+
const requestOptions = {
|
|
388
|
+
method: toolConfig.method.toUpperCase(),
|
|
389
|
+
headers,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
if (body !== undefined && ['POST', 'PUT', 'PATCH'].includes(requestOptions.method)) {
|
|
393
|
+
requestOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Execute request
|
|
397
|
+
const response = await fetch(url, requestOptions);
|
|
398
|
+
|
|
399
|
+
// Parse response
|
|
400
|
+
const contentType = response.headers.get('content-type') || '';
|
|
401
|
+
let data;
|
|
402
|
+
|
|
403
|
+
if (contentType.includes('application/json')) {
|
|
404
|
+
data = await response.json();
|
|
405
|
+
} else {
|
|
406
|
+
data = await response.text();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
status: response.status,
|
|
411
|
+
statusText: response.statusText,
|
|
412
|
+
data,
|
|
413
|
+
ok: response.ok,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function generateToolsConfig(tools) {
|
|
420
|
+
const toolConfigs = tools.map(tool => ({
|
|
421
|
+
name: tool.name,
|
|
422
|
+
description: tool.description,
|
|
423
|
+
method: tool.method,
|
|
424
|
+
pathTemplate: tool.pathTemplate,
|
|
425
|
+
executionParameters: tool.executionParameters,
|
|
426
|
+
requestBodyContentType: tool.requestBodyContentType,
|
|
427
|
+
baseUrl: tool.baseUrl,
|
|
428
|
+
}));
|
|
429
|
+
|
|
430
|
+
return `// Tool configurations extracted from OpenAPI spec
|
|
431
|
+
// Generated: ${new Date().toISOString()}
|
|
432
|
+
|
|
433
|
+
export const toolConfigs = ${JSON.stringify(toolConfigs, null, 2)};
|
|
434
|
+
|
|
435
|
+
// Create a map for quick lookup
|
|
436
|
+
export const toolConfigMap = new Map(toolConfigs.map(t => [t.name, t]));
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function generateServerIndex(serverName, tools, baseUrl, port) {
|
|
441
|
+
// Generate tool registration code
|
|
442
|
+
const toolRegistrations = tools.map(tool => {
|
|
443
|
+
return `
|
|
444
|
+
// ${tool.description}
|
|
445
|
+
server.tool('${tool.name}', {
|
|
446
|
+
description: '${tool.description.replace(/'/g, "\\'")}',
|
|
447
|
+
parameters: ${tool.zodSchema},
|
|
448
|
+
execute: async (params) => {
|
|
449
|
+
const toolConfig = toolConfigMap.get('${tool.name}');
|
|
450
|
+
const result = await executeRequest(toolConfig, params, apiConfig);
|
|
451
|
+
|
|
452
|
+
if (result.ok) {
|
|
453
|
+
return typeof result.data === 'string'
|
|
454
|
+
? result.data
|
|
455
|
+
: JSON.stringify(result.data, null, 2);
|
|
456
|
+
} else {
|
|
457
|
+
throw new Error(\`API Error (\${result.status}): \${
|
|
458
|
+
typeof result.data === 'string' ? result.data : JSON.stringify(result.data)
|
|
459
|
+
}\`);
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
});`;
|
|
463
|
+
}).join('\n');
|
|
464
|
+
|
|
465
|
+
return `#!/usr/bin/env node
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* ${serverName} - MCP Server
|
|
469
|
+
*
|
|
470
|
+
* Features:
|
|
471
|
+
* - ${tools.length} API tools available
|
|
472
|
+
* - Built-in Inspector at http://localhost:${port}/inspector
|
|
473
|
+
*/
|
|
474
|
+
|
|
475
|
+
import 'dotenv/config';
|
|
476
|
+
import { MCPServer } from 'mcp-use/server';
|
|
477
|
+
import { z } from 'zod';
|
|
478
|
+
import { executeRequest } from './http-client.js';
|
|
479
|
+
import { toolConfigMap } from './tools-config.js';
|
|
480
|
+
|
|
481
|
+
// ============================================================================
|
|
482
|
+
// Configuration
|
|
483
|
+
// ============================================================================
|
|
484
|
+
|
|
485
|
+
const PORT = parseInt(process.env.PORT || '${port}');
|
|
486
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
487
|
+
|
|
488
|
+
// API configuration
|
|
489
|
+
const apiConfig = {
|
|
490
|
+
baseUrl: process.env.API_BASE_URL || ${baseUrl ? `'${baseUrl}'` : 'null'},
|
|
491
|
+
headers: {},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Set up authentication headers
|
|
495
|
+
if (process.env.API_KEY) {
|
|
496
|
+
apiConfig.headers['Authorization'] = \`Bearer \${process.env.API_KEY}\`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (process.env.API_AUTH_HEADER) {
|
|
500
|
+
const [key, ...valueParts] = process.env.API_AUTH_HEADER.split(':');
|
|
501
|
+
const value = valueParts.join(':'); // Handle values with colons
|
|
502
|
+
if (key && value) {
|
|
503
|
+
apiConfig.headers[key.trim()] = value.trim();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ============================================================================
|
|
508
|
+
// Server Setup
|
|
509
|
+
// ============================================================================
|
|
510
|
+
|
|
511
|
+
const server = new MCPServer({
|
|
512
|
+
name: '${serverName}',
|
|
513
|
+
version: '1.0.0',
|
|
514
|
+
description: 'MCP server generated from OpenAPI specification',
|
|
515
|
+
baseUrl: process.env.MCP_URL || \`http://localhost:\${PORT}\`,
|
|
516
|
+
allowedOrigins: isDev
|
|
517
|
+
? undefined // Development: allow all origins
|
|
518
|
+
: process.env.ALLOWED_ORIGINS?.split(',').map(s => s.trim()) || [],
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// ============================================================================
|
|
522
|
+
// Tool Registrations
|
|
523
|
+
// ============================================================================
|
|
524
|
+
${toolRegistrations}
|
|
525
|
+
|
|
526
|
+
// ============================================================================
|
|
527
|
+
// Start Server
|
|
528
|
+
// ============================================================================
|
|
529
|
+
|
|
530
|
+
server.listen(PORT);
|
|
531
|
+
|
|
532
|
+
console.log(\`
|
|
533
|
+
š ${serverName} MCP Server Started
|
|
534
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
535
|
+
|
|
536
|
+
š Server: http://localhost:\${PORT}
|
|
537
|
+
š Inspector: http://localhost:\${PORT}/inspector
|
|
538
|
+
š” MCP: http://localhost:\${PORT}/mcp
|
|
539
|
+
š SSE: http://localhost:\${PORT}/sse
|
|
540
|
+
|
|
541
|
+
š ļø Tools Available: ${tools.length}
|
|
542
|
+
${tools.slice(0, 5).map(t => ` ⢠${t.name}`).join('\n')}${tools.length > 5 ? `\n ... and ${tools.length - 5} more` : ''}
|
|
543
|
+
Environment: \${isDev ? 'Development' : 'Production'}
|
|
544
|
+
API Base: \${apiConfig.baseUrl || 'Not configured'}
|
|
545
|
+
\`);
|
|
546
|
+
`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function generateReadme(serverName, tools, specPath, baseUrl, port) {
|
|
550
|
+
const toolList = tools
|
|
551
|
+
.map(t => `| \`${t.name}\` | ${t.method.toUpperCase()} | ${t.pathTemplate} | ${t.description.substring(0, 50)}${t.description.length > 50 ? '...' : ''} |`)
|
|
552
|
+
.join('\n');
|
|
553
|
+
|
|
554
|
+
return `# ${serverName}
|
|
555
|
+
|
|
556
|
+
MCP server auto-generated from OpenAPI specification using the [mcp-use](https://mcp-use.com) framework.
|
|
557
|
+
|
|
558
|
+
## Features
|
|
559
|
+
|
|
560
|
+
- š ļø **${tools.length} API Tools** - All operations from the OpenAPI spec
|
|
561
|
+
- š **Built-in Inspector** - Test tools at \`/inspector\`
|
|
562
|
+
- š” **Streamable HTTP** - Modern MCP transport
|
|
563
|
+
- š **Authentication Support** - Bearer tokens & custom headers
|
|
564
|
+
- šØ **UI Widgets** - Compatible with ChatGPT and MCP-UI
|
|
565
|
+
|
|
566
|
+
## Quick Start
|
|
567
|
+
|
|
568
|
+
\`\`\`bash
|
|
569
|
+
# Install dependencies
|
|
570
|
+
npm install
|
|
571
|
+
|
|
572
|
+
# Configure environment
|
|
573
|
+
cp .env.example .env
|
|
574
|
+
# Edit .env with your API credentials
|
|
575
|
+
|
|
576
|
+
# Start the server
|
|
577
|
+
npm start
|
|
578
|
+
|
|
579
|
+
# Or with hot reload
|
|
580
|
+
npm run dev
|
|
581
|
+
\`\`\`
|
|
582
|
+
|
|
583
|
+
Then open http://localhost:${port}/inspector to test your tools!
|
|
584
|
+
|
|
585
|
+
## Environment Variables
|
|
586
|
+
|
|
587
|
+
| Variable | Description | Default |
|
|
588
|
+
|----------|-------------|---------|
|
|
589
|
+
| \`PORT\` | Server port | ${port} |
|
|
590
|
+
| \`NODE_ENV\` | Environment (development/production) | development |
|
|
591
|
+
| \`API_BASE_URL\` | Base URL for API requests | ${baseUrl || 'From OpenAPI spec'} |
|
|
592
|
+
| \`API_KEY\` | Bearer token for Authorization header | - |
|
|
593
|
+
| \`API_AUTH_HEADER\` | Custom auth header (format: \`Header:value\`) | - |
|
|
594
|
+
| \`MCP_URL\` | Public MCP server URL (for widgets) | http://localhost:${port} |
|
|
595
|
+
| \`ALLOWED_ORIGINS\` | Allowed origins in production (comma-separated) | - |
|
|
596
|
+
|
|
597
|
+
## Connect to Claude Desktop
|
|
598
|
+
|
|
599
|
+
Add to your Claude Desktop configuration:
|
|
600
|
+
|
|
601
|
+
**macOS**: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
|
|
602
|
+
**Windows**: \`%APPDATA%\\Claude\\claude_desktop_config.json\`
|
|
603
|
+
|
|
604
|
+
\`\`\`json
|
|
605
|
+
{
|
|
606
|
+
"mcpServers": {
|
|
607
|
+
"${serverName}": {
|
|
608
|
+
"url": "http://localhost:${port}/mcp"
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
\`\`\`
|
|
613
|
+
|
|
614
|
+
## Connect to ChatGPT
|
|
615
|
+
|
|
616
|
+
This server supports the OpenAI Apps SDK. Configure your ChatGPT integration to use:
|
|
617
|
+
|
|
618
|
+
\`\`\`
|
|
619
|
+
http://localhost:${port}/mcp
|
|
620
|
+
\`\`\`
|
|
621
|
+
|
|
622
|
+
## Available Tools
|
|
623
|
+
|
|
624
|
+
| Tool | Method | Path | Description |
|
|
625
|
+
|------|--------|------|-------------|
|
|
626
|
+
${toolList}
|
|
627
|
+
|
|
628
|
+
## API Endpoints
|
|
629
|
+
|
|
630
|
+
| Endpoint | Description |
|
|
631
|
+
|----------|-------------|
|
|
632
|
+
| \`GET /inspector\` | Interactive tool testing UI |
|
|
633
|
+
| \`POST /mcp\` | MCP protocol endpoint |
|
|
634
|
+
| \`GET /sse\` | Server-Sent Events endpoint |
|
|
635
|
+
| \`GET /health\` | Health check endpoint |
|
|
636
|
+
|
|
637
|
+
## Project Structure
|
|
638
|
+
|
|
639
|
+
\`\`\`
|
|
640
|
+
${serverName}/
|
|
641
|
+
āāā .env # Environment configuration
|
|
642
|
+
āāā .env.example # Example environment file
|
|
643
|
+
āāā package.json # Dependencies
|
|
644
|
+
āāā README.md # This file
|
|
645
|
+
āāā src/
|
|
646
|
+
āāā index.js # Main server with tool registrations
|
|
647
|
+
āāā http-client.js # HTTP utilities for API calls
|
|
648
|
+
āāā tools-config.js # Tool configurations from OpenAPI
|
|
649
|
+
\`\`\`
|
|
650
|
+
|
|
651
|
+
## Production Deployment
|
|
652
|
+
|
|
653
|
+
### Docker
|
|
654
|
+
|
|
655
|
+
\`\`\`dockerfile
|
|
656
|
+
FROM node:20-alpine
|
|
657
|
+
WORKDIR /app
|
|
658
|
+
COPY package*.json ./
|
|
659
|
+
RUN npm ci --only=production
|
|
660
|
+
COPY . .
|
|
661
|
+
ENV NODE_ENV=production
|
|
662
|
+
EXPOSE ${port}
|
|
663
|
+
CMD ["npm", "start"]
|
|
664
|
+
\`\`\`
|
|
665
|
+
|
|
666
|
+
### PM2
|
|
667
|
+
|
|
668
|
+
\`\`\`bash
|
|
669
|
+
pm2 start src/index.js --name ${serverName}
|
|
670
|
+
\`\`\`
|
|
671
|
+
|
|
672
|
+
## Source
|
|
673
|
+
|
|
674
|
+
- **OpenAPI Spec**: \`${specPath}\`
|
|
675
|
+
- **Generated**: ${new Date().toISOString()}
|
|
676
|
+
- **Framework**: [mcp-use](https://mcp-use.com)
|
|
677
|
+
|
|
678
|
+
## License
|
|
679
|
+
|
|
680
|
+
MIT
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// Main Generator
|
|
686
|
+
// ============================================================================
|
|
687
|
+
|
|
688
|
+
async function generateMcpServer(specPathOrUrl, outputFolder, options = {}) {
|
|
689
|
+
const {
|
|
690
|
+
baseUrl,
|
|
691
|
+
serverName = 'openapi-mcp-server',
|
|
692
|
+
port = 3000,
|
|
693
|
+
} = options;
|
|
694
|
+
|
|
695
|
+
console.log(`\nš Loading OpenAPI spec: ${specPathOrUrl}`);
|
|
696
|
+
const spec = await loadOpenApiSpec(specPathOrUrl);
|
|
697
|
+
|
|
698
|
+
console.log(` Title: ${spec.info?.title || 'Unknown'}`);
|
|
699
|
+
console.log(` Version: ${spec.info?.version || 'Unknown'}`);
|
|
700
|
+
|
|
701
|
+
const tools = extractTools(spec, { baseUrl, ...options });
|
|
702
|
+
console.log(`ā
Extracted ${tools.length} tools\n`);
|
|
703
|
+
|
|
704
|
+
// Create directory structure
|
|
705
|
+
const srcDir = path.join(outputFolder, 'src');
|
|
706
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
707
|
+
|
|
708
|
+
const effectiveBaseUrl = baseUrl || tools[0]?.baseUrl;
|
|
709
|
+
|
|
710
|
+
// Generate all files
|
|
711
|
+
const files = [
|
|
712
|
+
{
|
|
713
|
+
path: path.join(outputFolder, 'package.json'),
|
|
714
|
+
content: generatePackageJson(serverName, tools, port)
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
path: path.join(outputFolder, '.env'),
|
|
718
|
+
content: generateEnvFile(effectiveBaseUrl, port)
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
path: path.join(outputFolder, '.env.example'),
|
|
722
|
+
content: generateEnvExampleFile(effectiveBaseUrl, port)
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
path: path.join(srcDir, 'http-client.js'),
|
|
726
|
+
content: generateHttpClient()
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
path: path.join(srcDir, 'tools-config.js'),
|
|
730
|
+
content: generateToolsConfig(tools)
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
path: path.join(srcDir, 'index.js'),
|
|
734
|
+
content: generateServerIndex(serverName, tools, effectiveBaseUrl, port)
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
path: path.join(outputFolder, 'README.md'),
|
|
738
|
+
content: generateReadme(serverName, tools, specPathOrUrl, effectiveBaseUrl, port)
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
path: path.join(outputFolder, '.gitignore'),
|
|
742
|
+
content: 'node_modules/\n.env\n*.log\n'
|
|
743
|
+
},
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
for (const file of files) {
|
|
747
|
+
await fs.writeFile(file.path, file.content);
|
|
748
|
+
console.log(` ā ${path.relative(outputFolder, file.path)}`);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
console.log(`
|
|
752
|
+
š MCP-Use Server Generated
|
|
753
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
cd ${outputFolder}
|
|
757
|
+
npm install
|
|
758
|
+
npm start
|
|
759
|
+
|
|
760
|
+
Then open http://localhost:${port}/inspector to test your tools!
|
|
761
|
+
`);
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
outputFolder,
|
|
765
|
+
toolCount: tools.length,
|
|
766
|
+
tools: tools.map(t => t.name),
|
|
767
|
+
port,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ============================================================================
|
|
772
|
+
// Exports & CLI
|
|
773
|
+
// ============================================================================
|
|
774
|
+
|
|
775
|
+
export { generateMcpServer, extractTools, loadOpenApiSpec };
|
|
776
|
+
|
|
777
|
+
// CLI entry point
|
|
778
|
+
const isMainModule = process.argv[1]?.includes('generate-mcp-use-server');
|
|
779
|
+
|
|
780
|
+
if (isMainModule) {
|
|
781
|
+
const args = process.argv.slice(2);
|
|
782
|
+
|
|
783
|
+
if (args.length < 1 || args.includes('--help') || args.includes('-h')) {
|
|
784
|
+
console.log(`
|
|
785
|
+
OpenAPI to MCP Server Generator (mcp-use framework)
|
|
786
|
+
|
|
787
|
+
Usage:
|
|
788
|
+
node generate-mcp-use-server.js <openapi-spec> [output-folder] [options]
|
|
789
|
+
|
|
790
|
+
Arguments:
|
|
791
|
+
openapi-spec Path to local file or URL to remote OpenAPI spec
|
|
792
|
+
output-folder Directory to create the server in (default: ./mcp-server)
|
|
793
|
+
|
|
794
|
+
Options:
|
|
795
|
+
--name <name> Server name (default: openapi-mcp-server)
|
|
796
|
+
--base-url <url> Override API base URL from the spec
|
|
797
|
+
--port <port> Server port (default: 3000)
|
|
798
|
+
--help, -h Show this help message
|
|
799
|
+
|
|
800
|
+
Examples:
|
|
801
|
+
node generate-mcp-use-server.js ./petstore.json ./my-server
|
|
802
|
+
node generate-mcp-use-server.js https://petstore3.swagger.io/api/v3/openapi.json ./petstore-mcp \\
|
|
803
|
+
--name petstore-api --port 8080
|
|
804
|
+
`);
|
|
805
|
+
process.exit(0);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const options = {
|
|
809
|
+
specPath: args[0],
|
|
810
|
+
outputFolder: './mcp-server',
|
|
811
|
+
baseUrl: null,
|
|
812
|
+
serverName: 'api-mcp-server',
|
|
813
|
+
port: 3000,
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
for (let i = 1; i < args.length; i++) {
|
|
817
|
+
if (args[i] === '--base-url' && args[i + 1]) {
|
|
818
|
+
options.baseUrl = args[++i];
|
|
819
|
+
} else if (args[i] === '--name' && args[i + 1]) {
|
|
820
|
+
options.serverName = args[++i];
|
|
821
|
+
} else if (args[i] === '--port' && args[i + 1]) {
|
|
822
|
+
options.port = parseInt(args[++i]);
|
|
823
|
+
} else if (!args[i].startsWith('--')) {
|
|
824
|
+
options.outputFolder = args[i];
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
generateMcpServer(options.specPath, options.outputFolder, options).catch(e => {
|
|
829
|
+
console.error('ā Error:', e.message);
|
|
830
|
+
process.exit(1);
|
|
831
|
+
});
|
|
832
|
+
}
|