@emcy/openapi-to-mcp 0.2.0 → 0.3.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.
Files changed (43) hide show
  1. package/README.md +144 -127
  2. package/dist/__tests__/cli-config.test.d.ts +2 -0
  3. package/dist/__tests__/cli-config.test.d.ts.map +1 -0
  4. package/dist/__tests__/cli-config.test.js +73 -0
  5. package/dist/__tests__/cli-config.test.js.map +1 -0
  6. package/dist/__tests__/generator.test.d.ts +1 -1
  7. package/dist/__tests__/generator.test.js +175 -201
  8. package/dist/__tests__/generator.test.js.map +1 -1
  9. package/dist/__tests__/integration.test.js +11 -11
  10. package/dist/__tests__/integration.test.js.map +1 -1
  11. package/dist/__tests__/mapper.test.js +20 -6
  12. package/dist/__tests__/mapper.test.js.map +1 -1
  13. package/dist/__tests__/tool-identity.test.d.ts +2 -0
  14. package/dist/__tests__/tool-identity.test.d.ts.map +1 -0
  15. package/dist/__tests__/tool-identity.test.js +15 -0
  16. package/dist/__tests__/tool-identity.test.js.map +1 -0
  17. package/dist/cli-config.d.ts +28 -0
  18. package/dist/cli-config.d.ts.map +1 -0
  19. package/dist/cli-config.js +124 -0
  20. package/dist/cli-config.js.map +1 -0
  21. package/dist/cli.js +56 -3
  22. package/dist/cli.js.map +1 -1
  23. package/dist/generator.d.ts +8 -3
  24. package/dist/generator.d.ts.map +1 -1
  25. package/dist/generator.js +681 -327
  26. package/dist/generator.js.map +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/mapper.d.ts.map +1 -1
  32. package/dist/mapper.js +3 -1
  33. package/dist/mapper.js.map +1 -1
  34. package/dist/parser.d.ts.map +1 -1
  35. package/dist/parser.js +14 -5
  36. package/dist/parser.js.map +1 -1
  37. package/dist/tool-identity.d.ts +6 -0
  38. package/dist/tool-identity.d.ts.map +1 -0
  39. package/dist/tool-identity.js +67 -0
  40. package/dist/tool-identity.js.map +1 -0
  41. package/dist/types.d.ts +98 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +1 -1
package/dist/generator.js CHANGED
@@ -1,38 +1,99 @@
1
1
  /**
2
- * Code Generator - Generates MCP server code from tool definitions
2
+ * Code generator for OpenAPI -> MCP runtimes.
3
+ *
4
+ * Supported runtime modes:
5
+ * - standalone_no_auth
6
+ * - standalone_headers
7
+ * - emcy_hosted_worker
3
8
  */
9
+ function getRuntimeMode(options) {
10
+ if (options.runtimeMode) {
11
+ return options.runtimeMode;
12
+ }
13
+ if (options.hostedWorkerConfig) {
14
+ return "emcy_hosted_worker";
15
+ }
16
+ if ((options.upstreamHeaders?.length ?? 0) > 0) {
17
+ return "standalone_headers";
18
+ }
19
+ return "standalone_no_auth";
20
+ }
21
+ function isHostedWorkerMode(options) {
22
+ return getRuntimeMode(options) === "emcy_hosted_worker";
23
+ }
24
+ function toEnvKey(value) {
25
+ return value.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
26
+ }
27
+ function formatHeaderDescription(headers) {
28
+ if (headers.length === 0) {
29
+ return "none";
30
+ }
31
+ return headers
32
+ .map((header) => header.valuePrefix
33
+ ? `${header.name} (${header.valuePrefix} <${header.envVar}>)`
34
+ : `${header.name} (<${header.envVar}>)`)
35
+ .join(", ");
36
+ }
37
+ function formatHostedOauthDescription(config) {
38
+ if (!config) {
39
+ return "none";
40
+ }
41
+ return [
42
+ config.provider || "manual",
43
+ config.authorizationServerUrl,
44
+ config.clientId ? `client ${config.clientId}` : undefined,
45
+ config.resource ? `resource ${config.resource}` : undefined,
46
+ ]
47
+ .filter(Boolean)
48
+ .join(" | ");
49
+ }
50
+ function formatToolInstructionSummary(toolInstructions) {
51
+ const entries = Object.entries(toolInstructions ?? {}).filter(([, config]) => Object.values(config).some((value) => typeof value === "string" && value.trim().length > 0));
52
+ if (entries.length === 0) {
53
+ return "none";
54
+ }
55
+ return entries.map(([toolKey]) => toolKey).join(", ");
56
+ }
4
57
  /**
5
- * Generate a complete MCP server from tool definitions
58
+ * Generate a complete MCP server from tool definitions.
6
59
  */
7
60
  export function generateMcpServer(tools, options, securitySchemes = {}) {
8
61
  const files = {};
9
62
  files["package.json"] = generatePackageJson(options);
10
63
  files["tsconfig.json"] = generateTsConfig();
11
64
  files["src/index.ts"] = generateServerEntry(tools, options, securitySchemes);
12
- files["src/transport.ts"] = generateTransport();
13
- files[".env.example"] = generateEnvExample(tools, securitySchemes);
14
- files["README.md"] = generateReadme(options);
65
+ files["src/transport.ts"] = generateTransport(options);
66
+ files[".env.example"] = generateEnvExample(tools, securitySchemes, options);
67
+ files["README.md"] = generateReadme(options, tools, securitySchemes);
15
68
  return files;
16
69
  }
17
70
  function generatePackageJson(options) {
71
+ const isHostedWorker = isHostedWorkerMode(options);
18
72
  const pkg = {
19
73
  name: options.name,
20
74
  version: options.version || "1.0.0",
21
- description: `MCP Server generated from OpenAPI spec`,
75
+ description: `MCP runtime generated from OpenAPI`,
22
76
  type: "module",
23
77
  main: "build/index.js",
24
- scripts: {
25
- build: "tsc",
26
- start: "node build/index.js",
27
- "start:http": "node build/index.js --transport=streamable-http",
28
- dev: "tsc --watch",
29
- },
78
+ scripts: isHostedWorker
79
+ ? {
80
+ build: "tsc",
81
+ start: "node build/index.js --transport=streamable-http",
82
+ "start:http": "node build/index.js --transport=streamable-http",
83
+ dev: "tsc --watch",
84
+ }
85
+ : {
86
+ build: "tsc",
87
+ start: "node build/index.js",
88
+ "start:http": "node build/index.js --transport=streamable-http",
89
+ dev: "tsc --watch",
90
+ },
30
91
  dependencies: {
31
92
  "@modelcontextprotocol/sdk": "^1.10.0",
93
+ "@hono/node-server": "^1.14.1",
32
94
  axios: "^1.9.0",
33
95
  dotenv: "^16.4.5",
34
96
  hono: "^4.7.7",
35
- "@hono/node-server": "^1.14.1",
36
97
  ...(options.emcyEnabled
37
98
  ? {
38
99
  "@emcy/sdk": options.localSdkPath
@@ -73,9 +134,13 @@ function generateTsConfig() {
73
134
  return JSON.stringify(config, null, 2);
74
135
  }
75
136
  function generateServerEntry(tools, options, securitySchemes) {
137
+ const runtimeMode = getRuntimeMode(options);
138
+ const hasHostedWorker = runtimeMode === "emcy_hosted_worker";
139
+ const configuredHeaders = options.upstreamHeaders ?? [];
140
+ const hostedOauthConfig = options.hostedOauthConfig;
141
+ const toolInstructions = options.toolInstructions;
76
142
  const toolDefinitions = tools
77
- .map((tool) => {
78
- return ` ["${tool.name}", {
143
+ .map((tool) => ` ["${tool.name}", {
79
144
  name: "${tool.name}",
80
145
  description: ${JSON.stringify(tool.description)},
81
146
  inputSchema: ${JSON.stringify(tool.inputSchema)},
@@ -83,28 +148,26 @@ function generateServerEntry(tools, options, securitySchemes) {
83
148
  pathTemplate: "${tool.pathTemplate}",
84
149
  parameters: ${JSON.stringify(tool.parameters)},
85
150
  requestBodyContentType: ${tool.requestBodyContentType
86
- ? `"${tool.requestBodyContentType}"`
87
- : "undefined"},
151
+ ? `"${tool.requestBodyContentType}"`
152
+ : "undefined"},
88
153
  securitySchemes: ${JSON.stringify(tool.securitySchemes)},
89
- }]`;
90
- })
154
+ requiredScopes: ${JSON.stringify(tool.requiredScopes)},
155
+ }]`)
91
156
  .join(",\n");
92
157
  const emcyImport = options.emcyEnabled
93
- ? `import { EmcyTelemetry } from '@emcy/sdk';\n`
158
+ ? `import { EmcyTelemetry } from "@emcy/sdk";\n`
94
159
  : "";
95
160
  const emcyInit = options.emcyEnabled
96
161
  ? `
97
- // Initialize Emcy telemetry if API key is provided
98
162
  const emcy = process.env.EMCY_API_KEY
99
163
  ? new EmcyTelemetry({
100
164
  apiKey: process.env.EMCY_API_KEY,
101
165
  endpoint: process.env.EMCY_TELEMETRY_URL,
102
166
  mcpServerId: process.env.EMCY_MCP_SERVER_ID,
103
- debug: process.env.EMCY_DEBUG === 'true',
167
+ debug: process.env.EMCY_DEBUG === "true",
104
168
  })
105
169
  : null;
106
170
 
107
- // Set server info for telemetry metadata
108
171
  if (emcy) {
109
172
  emcy.setServerInfo(SERVER_NAME, SERVER_VERSION);
110
173
  }
@@ -112,39 +175,65 @@ if (emcy) {
112
175
  : "";
113
176
  const emcyTrace = options.emcyEnabled
114
177
  ? `
115
- // Wrap with Emcy telemetry if enabled
116
178
  if (emcy) {
117
- return emcy.trace(toolName, async () => executeRequest(toolDefinition, toolArgs ?? {}));
179
+ return emcy.trace(toolName, async () =>
180
+ executeRequest(toolDefinition, toolArgs ?? {}, getUpstreamAccessToken?.())
181
+ );
118
182
  }
119
183
  `
120
184
  : "";
185
+ const hostedWorkerConfig = hasHostedWorker
186
+ ? `
187
+ const HOSTED_WORKER_CONFIG = {
188
+ workerSecretHeader: process.env.EMCY_WORKER_SECRET_HEADER || ${JSON.stringify(options.hostedWorkerConfig?.workerSecretHeader || "x-emcy-worker-secret")},
189
+ workerSecretEnvVar: process.env.EMCY_WORKER_SECRET_ENV_VAR || ${JSON.stringify(options.hostedWorkerConfig?.workerSecretEnvVar || "EMCY_WORKER_SHARED_SECRET")},
190
+ upstreamAccessTokenHeader: process.env.EMCY_UPSTREAM_ACCESS_TOKEN_HEADER || ${JSON.stringify(options.hostedWorkerConfig?.upstreamAccessTokenHeader ||
191
+ "x-emcy-upstream-access-token")},
192
+ };
193
+ `
194
+ : "";
195
+ const upstreamHeaderConfig = `
196
+ type RuntimeMode = "standalone_no_auth" | "standalone_headers" | "emcy_hosted_worker";
197
+ const RUNTIME_MODE: RuntimeMode = ${JSON.stringify(runtimeMode)};
198
+ const UPSTREAM_HEADERS: RuntimeUpstreamHeader[] = ${JSON.stringify(configuredHeaders, null, 2)};
199
+ `;
200
+ const hasPrompts = options.prompts && options.prompts.length > 0;
201
+ const promptsImport = hasPrompts
202
+ ? `,
203
+ GetPromptRequestSchema,
204
+ ListPromptsRequestSchema,
205
+ type GetPromptResult`
206
+ : "";
207
+ const promptDefinitions = hasPrompts
208
+ ? generatePromptDefinitions(options.prompts)
209
+ : "";
210
+ const promptsCapability = hasPrompts ? ", prompts: {}" : "";
211
+ const promptHandlers = hasPrompts ? generatePromptHandlers() : "";
121
212
  return `#!/usr/bin/env node
122
213
  /**
123
- * MCP Server: ${options.name}
124
- * Generated by Emcy OpenAPI-to-MCP Generator
214
+ * MCP Runtime: ${options.name}
215
+ * Generated by Emcy OpenAPI-to-MCP
125
216
  */
126
217
 
127
- import dotenv from 'dotenv';
218
+ import dotenv from "dotenv";
128
219
  dotenv.config();
129
220
 
130
221
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
131
222
  import {
132
223
  CallToolRequestSchema,
133
224
  ListToolsRequestSchema,
134
- type Tool,
225
+ type CallToolRequest,
135
226
  type CallToolResult,
136
- type CallToolRequest
227
+ type Tool${promptsImport}
137
228
  } from "@modelcontextprotocol/sdk/types.js";
138
- import axios, { type AxiosRequestConfig, type AxiosError } from 'axios';
229
+ import axios, { type AxiosRequestConfig } from "axios";
139
230
  import { setupStreamableHttpServer } from "./transport.js";
140
231
  ${emcyImport}
141
- // Configuration
142
232
  export const SERVER_NAME = "${options.name}";
143
233
  export const SERVER_VERSION = "${options.version || "1.0.0"}";
144
234
  export const API_BASE_URL = process.env.API_BASE_URL || "${options.baseUrl}";
145
235
 
146
- // Tool definition interface
147
- interface McpToolDefinition {
236
+ interface RuntimeToolDefinition {
148
237
  name: string;
149
238
  description: string;
150
239
  inputSchema: Record<string, unknown>;
@@ -153,443 +242,708 @@ interface McpToolDefinition {
153
242
  parameters: { name: string; in: string; required: boolean }[];
154
243
  requestBodyContentType?: string;
155
244
  securitySchemes: string[];
245
+ requiredScopes: string[];
246
+ }
247
+
248
+ interface RuntimeUpstreamHeader {
249
+ name: string;
250
+ envVar: string;
251
+ valuePrefix?: string;
252
+ defaultValue?: string;
253
+ }
254
+
255
+ interface RuntimeHostedOauthConfig {
256
+ provider?: string;
257
+ authorizationServerUrl?: string;
258
+ clientId?: string;
259
+ resource?: string;
260
+ scopes?: string[];
261
+ }
262
+
263
+ interface RuntimeToolInstruction {
264
+ customInstructions?: string;
265
+ exampleUsage?: string;
266
+ whenToUse?: string;
267
+ whenNotToUse?: string;
156
268
  }
157
269
 
158
- // Security schemes
159
270
  const securitySchemes: Record<string, unknown> = ${JSON.stringify(securitySchemes, null, 2)};
160
- ${emcyInit}
161
- // Tool definitions
162
- const toolDefinitionMap: Map<string, McpToolDefinition> = new Map([
271
+ ${upstreamHeaderConfig}${hostedWorkerConfig}${emcyInit}
272
+ const HOSTED_OAUTH_CONFIG: RuntimeHostedOauthConfig | null = ${JSON.stringify(hostedOauthConfig ?? null, null, 2)};
273
+ const TOOL_INSTRUCTIONS: Record<string, RuntimeToolInstruction> = ${JSON.stringify(toolInstructions ?? {}, null, 2)};
274
+ const toolDefinitionMap: Map<string, RuntimeToolDefinition> = new Map([
163
275
  ${toolDefinitions}
164
276
  ]);
277
+ ${promptDefinitions}
278
+
279
+ export function createServer(getUpstreamAccessToken?: () => string | undefined): Server {
280
+ const server = new Server(
281
+ { name: SERVER_NAME, version: SERVER_VERSION },
282
+ { capabilities: { tools: {}${promptsCapability} } }
283
+ );
284
+
285
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
286
+ const toolsForClient: Tool[] = Array.from(toolDefinitionMap.values()).map((def) => ({
287
+ name: def.name,
288
+ description: def.description,
289
+ inputSchema: def.inputSchema as Tool["inputSchema"],
290
+ }));
291
+ return { tools: toolsForClient };
292
+ });
165
293
 
166
- // Create MCP server
167
- const server = new Server(
168
- { name: SERVER_NAME, version: SERVER_VERSION },
169
- { capabilities: { tools: {} } }
170
- );
294
+ server.setRequestHandler(
295
+ CallToolRequestSchema,
296
+ async (request: CallToolRequest): Promise<CallToolResult> => {
297
+ const { name: toolName, arguments: toolArgs } = request.params;
298
+ const toolDefinition = toolDefinitionMap.get(toolName);
299
+
300
+ if (!toolDefinition) {
301
+ return {
302
+ content: [{ type: "text", text: \`Error: Unknown tool: \${toolName}\` }],
303
+ isError: true,
304
+ };
305
+ }
171
306
 
172
- // List tools handler
173
- server.setRequestHandler(ListToolsRequestSchema, async () => {
174
- const toolsForClient: Tool[] = Array.from(toolDefinitionMap.values()).map(def => ({
175
- name: def.name,
176
- description: def.description,
177
- inputSchema: def.inputSchema as Tool['inputSchema'],
178
- }));
179
- return { tools: toolsForClient };
180
- });
181
-
182
- // Call tool handler
183
- server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise<CallToolResult> => {
184
- const { name: toolName, arguments: toolArgs } = request.params;
185
- const toolDefinition = toolDefinitionMap.get(toolName);
186
-
187
- if (!toolDefinition) {
188
- return { content: [{ type: "text", text: \`Error: Unknown tool: \${toolName}\` }] };
189
- }
190
-
191
- try {
307
+ try {
192
308
  ${emcyTrace}
193
- return await executeRequest(toolDefinition, toolArgs ?? {});
194
- } catch (error) {
195
- const message = error instanceof Error ? error.message : String(error);
196
- return { content: [{ type: "text", text: \`Error: \${message}\` }] };
197
- }
198
- });
309
+ return await executeRequest(
310
+ toolDefinition,
311
+ toolArgs ?? {},
312
+ getUpstreamAccessToken?.()
313
+ );
314
+ } catch (error) {
315
+ const message = error instanceof Error ? error.message : String(error);
316
+ return {
317
+ content: [{ type: "text", text: \`Error: \${message}\` }],
318
+ isError: true,
319
+ };
320
+ }
321
+ }
322
+ );
323
+ ${promptHandlers}
324
+ return server;
325
+ }
199
326
 
200
- // Execute API request
201
327
  async function executeRequest(
202
- def: McpToolDefinition,
203
- args: Record<string, unknown>
328
+ def: RuntimeToolDefinition,
329
+ args: Record<string, unknown>,
330
+ upstreamAccessToken?: string
204
331
  ): Promise<CallToolResult> {
205
332
  let url = def.pathTemplate;
206
333
  const queryParams: Record<string, unknown> = {};
207
- const headers: Record<string, string> = { 'Accept': 'application/json' };
208
-
209
- // Apply path and query parameters
334
+ const headers: Record<string, string> = { accept: "application/json" };
335
+
210
336
  for (const param of def.parameters) {
211
337
  const value = args[param.name];
212
- if (value !== undefined && value !== null) {
213
- if (param.in === 'path') {
214
- url = url.replace(\`{\${param.name}}\`, encodeURIComponent(String(value)));
215
- } else if (param.in === 'query') {
216
- queryParams[param.name] = value;
217
- } else if (param.in === 'header') {
218
- headers[param.name.toLowerCase()] = String(value);
219
- }
338
+ if (value === undefined || value === null) {
339
+ continue;
340
+ }
341
+
342
+ if (param.in === "path") {
343
+ url = url.replace(\`{\${param.name}}\`, encodeURIComponent(String(value)));
344
+ } else if (param.in === "query") {
345
+ queryParams[param.name] = value;
346
+ } else if (param.in === "header") {
347
+ headers[param.name.toLowerCase()] = String(value);
220
348
  }
221
349
  }
222
-
223
- // Apply security (API key, Bearer token)
350
+
224
351
  applySecurityHeaders(headers, def.securitySchemes);
225
-
226
- // Build request config
352
+ applyConfiguredUpstreamHeaders(headers);
353
+ applyHostedWorkerAccessToken(headers, upstreamAccessToken);
354
+
227
355
  const config: AxiosRequestConfig = {
228
356
  method: def.method,
229
357
  url: \`\${API_BASE_URL}\${url}\`,
230
358
  params: queryParams,
231
359
  headers,
232
360
  };
233
-
234
- // Add request body if present
361
+
235
362
  if (def.requestBodyContentType && args.requestBody !== undefined) {
236
363
  config.data = args.requestBody;
237
- headers['content-type'] = def.requestBodyContentType;
364
+ headers["content-type"] = def.requestBodyContentType;
238
365
  }
239
-
240
- console.error(\`Executing: \${def.method.toUpperCase()} \${config.url}\`);
241
-
242
- const response = await axios(config);
243
-
244
- let responseText: string;
245
- if (typeof response.data === 'object') {
246
- responseText = JSON.stringify(response.data, null, 2);
247
- } else {
248
- responseText = String(response.data ?? '');
366
+
367
+ if (def.requestBodyContentType && !config.data) {
368
+ const paramNames = new Set(def.parameters.map((p) => p.name));
369
+ const bodyArgs: Record<string, unknown> = {};
370
+ for (const [key, value] of Object.entries(args)) {
371
+ if (key !== "requestBody" && !paramNames.has(key)) {
372
+ bodyArgs[key] = value;
373
+ }
374
+ }
375
+
376
+ if (Object.keys(bodyArgs).length > 0) {
377
+ config.data = bodyArgs;
378
+ headers["content-type"] = def.requestBodyContentType;
379
+ }
249
380
  }
250
-
381
+
382
+ const response = await axios(config);
383
+ const responseText =
384
+ typeof response.data === "object"
385
+ ? JSON.stringify(response.data, null, 2)
386
+ : String(response.data ?? "");
387
+
251
388
  return {
252
- content: [{ type: "text", text: \`Status: \${response.status}\\n\\n\${responseText}\` }]
389
+ content: [{ type: "text", text: \`Status: \${response.status}\\n\\n\${responseText}\` }],
253
390
  };
254
391
  }
255
392
 
256
- // Apply security headers based on environment variables
257
- function applySecurityHeaders(headers: Record<string, string>, schemeNames: string[]) {
393
+ function applySecurityHeaders(headers: Record<string, string>, schemeNames: string[]): void {
394
+ if (RUNTIME_MODE !== "standalone_headers") {
395
+ return;
396
+ }
397
+
258
398
  for (const schemeName of schemeNames) {
259
399
  const scheme = securitySchemes[schemeName] as Record<string, unknown> | undefined;
260
- if (!scheme) continue;
261
-
262
- const envKey = schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
263
-
264
- if (scheme.type === 'apiKey') {
265
- const apiKey = process.env[\`API_KEY_\${envKey}\`];
266
- if (apiKey && scheme.in === 'header' && typeof scheme.name === 'string') {
400
+ if (!scheme) {
401
+ continue;
402
+ }
403
+
404
+ const resolvedEnvKey = schemeName.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
405
+
406
+ if (scheme.type === "apiKey") {
407
+ const apiKey = process.env[\`API_KEY_\${resolvedEnvKey}\`];
408
+ if (apiKey && scheme.in === "header" && typeof scheme.name === "string") {
267
409
  headers[scheme.name.toLowerCase()] = apiKey;
268
410
  }
269
- } else if (scheme.type === 'http' && scheme.scheme === 'bearer') {
270
- const token = process.env[\`BEARER_TOKEN_\${envKey}\`];
271
- if (token) {
272
- headers['authorization'] = \`Bearer \${token}\`;
411
+ continue;
412
+ }
413
+
414
+ if (scheme.type === "http" && scheme.scheme === "bearer") {
415
+ const bearerToken = process.env[\`BEARER_TOKEN_\${resolvedEnvKey}\`];
416
+ if (bearerToken) {
417
+ headers.authorization = \`Bearer \${bearerToken}\`;
273
418
  }
274
419
  }
275
420
  }
276
421
  }
277
422
 
278
- // Main
279
- async function main() {
423
+ function applyConfiguredUpstreamHeaders(headers: Record<string, string>): void {
424
+ for (const header of UPSTREAM_HEADERS) {
425
+ const rawValue = process.env[header.envVar] || header.defaultValue;
426
+ if (!rawValue) {
427
+ continue;
428
+ }
429
+
430
+ headers[header.name.toLowerCase()] = header.valuePrefix
431
+ ? \`\${header.valuePrefix} \${rawValue}\`
432
+ : rawValue;
433
+ }
434
+ }
435
+
436
+ function applyHostedWorkerAccessToken(
437
+ headers: Record<string, string>,
438
+ upstreamAccessToken?: string
439
+ ): void {
440
+ if (RUNTIME_MODE !== "emcy_hosted_worker" || !upstreamAccessToken) {
441
+ return;
442
+ }
443
+
444
+ headers.authorization = \`Bearer \${upstreamAccessToken}\`;
445
+ }
446
+
447
+ async function main(): Promise<void> {
280
448
  const args = process.argv.slice(2);
281
- const useHttp = args.includes('--transport=streamable-http');
282
-
283
- if (useHttp) {
284
- const port = parseInt(process.env.PORT || '3000', 10);
285
- await setupStreamableHttpServer(server, port);
286
- } else {
287
- // Stdio transport for Claude Desktop, etc.
288
- const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
289
- const transport = new StdioServerTransport();
290
- await server.connect(transport);
291
- console.error(\`\${SERVER_NAME} running on stdio\`);
449
+ const useHttp = args.includes("--transport=streamable-http");
450
+ const port = parseInt(process.env.PORT || "3000", 10);
451
+
452
+ if (RUNTIME_MODE === "emcy_hosted_worker" || useHttp) {
453
+ await setupStreamableHttpServer(port${hasHostedWorker ? ", HOSTED_WORKER_CONFIG" : ""});
454
+ return;
292
455
  }
456
+
457
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
458
+ const server = createServer();
459
+ const transport = new StdioServerTransport();
460
+ await server.connect(transport);
461
+ console.error(\`\${SERVER_NAME} running on stdio\`);
293
462
  }
294
463
 
295
464
  main().catch(console.error);
296
465
  `;
297
466
  }
298
- function generateTransport() {
467
+ function generatePromptDefinitions(prompts) {
468
+ return `
469
+
470
+ interface PromptDef {
471
+ name: string;
472
+ title?: string;
473
+ description: string;
474
+ content: string;
475
+ arguments?: { name: string; description: string; required: boolean }[];
476
+ }
477
+
478
+ const promptDefinitionMap: Map<string, PromptDef> = new Map([
479
+ ${prompts
480
+ .map((prompt) => ` ["${prompt.name}", {
481
+ name: "${prompt.name}",
482
+ ${prompt.title ? `title: ${JSON.stringify(prompt.title)},` : ""}
483
+ description: ${JSON.stringify(prompt.description)},
484
+ content: ${JSON.stringify(prompt.content)},
485
+ ${prompt.arguments ? `arguments: ${JSON.stringify(prompt.arguments)},` : ""}
486
+ }]`)
487
+ .join(",\n")}
488
+ ]);`;
489
+ }
490
+ function generatePromptHandlers() {
491
+ return `
492
+
493
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
494
+ const promptsForClient = Array.from(promptDefinitionMap.values()).map((def) => ({
495
+ name: def.name,
496
+ title: def.title,
497
+ description: def.description,
498
+ arguments: def.arguments,
499
+ }));
500
+
501
+ return { prompts: promptsForClient };
502
+ });
503
+
504
+ server.setRequestHandler(
505
+ GetPromptRequestSchema,
506
+ async (request): Promise<GetPromptResult> => {
507
+ const { name, arguments: args } = request.params;
508
+ const promptDef = promptDefinitionMap.get(name);
509
+
510
+ if (!promptDef) {
511
+ throw new Error(\`Unknown prompt: \${name}\`);
512
+ }
513
+
514
+ let content = promptDef.content;
515
+ if (args && promptDef.arguments) {
516
+ for (const argDef of promptDef.arguments) {
517
+ const value = args[argDef.name];
518
+ if (value !== undefined) {
519
+ content = content.replace(
520
+ new RegExp(\`{{\\\\s*\${argDef.name}\\\\s*}}\`, "g"),
521
+ String(value)
522
+ );
523
+ } else if (argDef.required) {
524
+ throw new Error(\`Missing required argument: \${argDef.name}\`);
525
+ }
526
+ }
527
+ }
528
+
529
+ return {
530
+ messages: [
531
+ {
532
+ role: "user",
533
+ content: { type: "text", text: content },
534
+ },
535
+ ],
536
+ };
537
+ }
538
+ );`;
539
+ }
540
+ function generateTransport(options) {
541
+ const runtimeMode = getRuntimeMode(options);
542
+ const hasHostedWorker = runtimeMode === "emcy_hosted_worker";
543
+ const hostedWorkerTypes = hasHostedWorker
544
+ ? `
545
+ interface HostedWorkerRuntimeConfig {
546
+ workerSecretHeader: string;
547
+ workerSecretEnvVar: string;
548
+ upstreamAccessTokenHeader: string;
549
+ }
550
+
551
+ let hostedWorkerRuntimeConfig: HostedWorkerRuntimeConfig | undefined;
552
+
553
+ function getHostedWorkerConfig(): HostedWorkerRuntimeConfig {
554
+ if (!hostedWorkerRuntimeConfig) {
555
+ throw new Error("Hosted worker runtime config was not initialized.");
556
+ }
557
+
558
+ return hostedWorkerRuntimeConfig;
559
+ }
560
+ `
561
+ : "";
562
+ const requestTokenResolver = hasHostedWorker
563
+ ? `
564
+ function getRequestAccessToken(c: any): string | undefined {
565
+ const forwarded = c.req.header(getHostedWorkerConfig().upstreamAccessTokenHeader);
566
+ if (forwarded) {
567
+ return forwarded;
568
+ }
569
+
570
+ const authHeader = c.req.header("authorization");
571
+ if (authHeader?.startsWith("Bearer ")) {
572
+ return authHeader.substring(7);
573
+ }
574
+
575
+ return undefined;
576
+ }
577
+ `
578
+ : `
579
+ function getRequestAccessToken(_c: any): string | undefined {
580
+ return undefined;
581
+ }
582
+ `;
583
+ const hostedWorkerMiddleware = hasHostedWorker
584
+ ? `
585
+ app.use("/mcp", async (c, next) => {
586
+ const workerConfig = getHostedWorkerConfig();
587
+ const expectedSecret = process.env[workerConfig.workerSecretEnvVar];
588
+
589
+ if (!expectedSecret) {
590
+ return c.json(
591
+ {
592
+ error: "server_error",
593
+ error_description: \`Missing worker secret env var: \${workerConfig.workerSecretEnvVar}\`,
594
+ },
595
+ 500
596
+ );
597
+ }
598
+
599
+ const providedSecret = c.req.header(workerConfig.workerSecretHeader);
600
+ if (providedSecret !== expectedSecret) {
601
+ return c.json(
602
+ {
603
+ error: "unauthorized",
604
+ error_description: "Internal worker secret is missing or invalid.",
605
+ },
606
+ 401
607
+ );
608
+ }
609
+
610
+ return next();
611
+ });
612
+ `
613
+ : "";
614
+ const startupDetails = hasHostedWorker
615
+ ? `
616
+ console.error(\`║ Mode: Emcy hosted worker ║\`);
617
+ console.error(\`║ Header: \${getHostedWorkerConfig().workerSecretHeader.padEnd(53)} ║\`);
618
+ console.error(\`║ Clients: Emcy should call this worker, not end users. ║\`);
619
+ `
620
+ : `
621
+ console.error(\`║ Mode: Standalone MCP server ║\`);
622
+ console.error(\`║ HTTP: http://localhost:\${info.port}/mcp\`.padEnd(64) + \`║\`);
623
+ console.error(\`║ Stdio: npm start\`.padEnd(64) + \`║\`);
624
+ `;
299
625
  return `/**
300
- * HTTP Transport for MCP
301
- * Uses Streamable HTTP transport (MCP specification 2025-03-26)
626
+ * Streamable HTTP transport for the generated MCP runtime.
302
627
  */
303
628
 
304
- import { Hono } from 'hono';
305
- import { cors } from 'hono/cors';
306
- import { serve } from '@hono/node-server';
307
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
308
- import { SERVER_NAME, SERVER_VERSION } from './index.js';
309
-
629
+ import { Hono } from "hono";
630
+ import { cors } from "hono/cors";
631
+ import { serve } from "@hono/node-server";
632
+ import { createServer, SERVER_NAME, SERVER_VERSION } from "./index.js";
633
+ ${hostedWorkerTypes}
310
634
  const { WebStandardStreamableHTTPServerTransport } = await import(
311
635
  "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
312
636
  );
313
637
 
314
638
  const transports: Map<string, InstanceType<typeof WebStandardStreamableHTTPServerTransport>> = new Map();
315
-
316
- export async function setupStreamableHttpServer(mcpServer: Server, port = 3000) {
317
- const app = new Hono();
318
-
319
- // CORS configuration for browser/client access
320
- app.use('*', cors({
321
- origin: '*',
322
- allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
323
- allowHeaders: ['Content-Type', 'Accept', 'mcp-session-id', 'Last-Event-ID'],
324
- exposeHeaders: ['mcp-session-id'],
325
- }));
326
-
327
- // Health check endpoint
328
- app.get('/health', (c) => {
329
- return c.json({
330
- status: 'OK',
331
- server: SERVER_NAME,
639
+ const sessionTokens: Map<string, { current: string }> = new Map();
640
+ ${requestTokenResolver}
641
+
642
+ export async function setupStreamableHttpServer(
643
+ port = 3000${hasHostedWorker ? ", hostedWorkerConfig?: HostedWorkerRuntimeConfig" : ""}
644
+ ): Promise<Hono> {
645
+ ${hasHostedWorker ? " hostedWorkerRuntimeConfig = hostedWorkerConfig;\n" : ""} const app = new Hono();
646
+
647
+ app.use(
648
+ "*",
649
+ cors({
650
+ origin: "*",
651
+ allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
652
+ allowHeaders: [
653
+ "Content-Type",
654
+ "Accept",
655
+ "Authorization",
656
+ "mcp-session-id",
657
+ "Last-Event-ID",
658
+ "x-emcy-worker-secret",
659
+ "x-emcy-upstream-access-token",
660
+ ],
661
+ exposeHeaders: ["mcp-session-id"],
662
+ })
663
+ );
664
+ ${hostedWorkerMiddleware}
665
+ app.get("/health", (c) => {
666
+ return c.json({
667
+ status: "OK",
668
+ server: SERVER_NAME,
332
669
  version: SERVER_VERSION,
333
670
  mcp: {
334
- transport: 'streamable-http',
671
+ transport: "streamable-http",
335
672
  endpoints: {
336
- mcp: '/mcp',
337
- health: '/health'
338
- }
339
- }
673
+ mcp: "/mcp",
674
+ health: "/health",
675
+ },
676
+ ${hasHostedWorker ? ` hosted_worker: {
677
+ enabled: true,
678
+ worker_secret_header: getHostedWorkerConfig().workerSecretHeader,
679
+ upstream_access_token_header: getHostedWorkerConfig().upstreamAccessTokenHeader,
680
+ },` : ` public_server: true,`}
681
+ },
340
682
  });
341
683
  });
342
-
343
- // Streamable HTTP Transport (MCP spec 2025-03-26)
344
- // Supports ChatGPT, Cursor, and other modern MCP clients
684
+
345
685
  app.all("/mcp", async (c) => {
346
- const sessionId = c.req.header('mcp-session-id');
347
-
348
- // Existing session
686
+ const sessionId = c.req.header("mcp-session-id");
687
+
349
688
  if (sessionId && transports.has(sessionId)) {
689
+ const tokenRef = sessionTokens.get(sessionId);
690
+ if (tokenRef) {
691
+ const requestToken = getRequestAccessToken(c);
692
+ if (requestToken) {
693
+ tokenRef.current = requestToken;
694
+ }
695
+ }
696
+
350
697
  return transports.get(sessionId)!.handleRequest(c.req.raw);
351
698
  }
352
-
353
- // New session - create transport
699
+
354
700
  if (!sessionId) {
701
+ const sessionTokenRef = { current: "" };
702
+ const requestToken = getRequestAccessToken(c);
703
+ if (requestToken) {
704
+ sessionTokenRef.current = requestToken;
705
+ }
706
+
355
707
  const transport = new WebStandardStreamableHTTPServerTransport({
356
708
  sessionIdGenerator: () => crypto.randomUUID(),
357
709
  onsessioninitialized: (newSessionId: string) => {
358
710
  transports.set(newSessionId, transport);
711
+ sessionTokens.set(newSessionId, sessionTokenRef);
359
712
  console.error(\`New MCP session: \${newSessionId}\`);
360
- }
713
+ },
361
714
  });
362
-
363
- transport.onerror = (err: Error) => console.error('Transport error:', err);
715
+
716
+ transport.onerror = (err: Error) => console.error("Transport error:", err);
364
717
  transport.onclose = () => {
365
718
  const sid = transport.sessionId;
366
- if (sid) {
367
- transports.delete(sid);
368
- console.error(\`Session closed: \${sid}\`);
719
+ if (!sid) {
720
+ return;
369
721
  }
722
+
723
+ transports.delete(sid);
724
+ sessionTokens.delete(sid);
725
+ console.error(\`Session closed: \${sid}\`);
370
726
  };
371
-
372
- await mcpServer.connect(transport);
727
+
728
+ const sessionServer = createServer(
729
+ () => sessionTokenRef.current || undefined
730
+ );
731
+ await sessionServer.connect(transport);
373
732
  return transport.handleRequest(c.req.raw);
374
733
  }
375
-
376
- // Session not found
377
- return c.json({
378
- error: 'Session not found',
379
- message: 'The specified session ID does not exist. Start a new session by omitting the mcp-session-id header.'
380
- }, 404);
734
+
735
+ return c.json(
736
+ {
737
+ error: "Session not found",
738
+ message:
739
+ "The specified session ID does not exist. Start a new session by omitting the mcp-session-id header.",
740
+ },
741
+ 404
742
+ );
381
743
  });
382
-
383
- // Legacy /sse endpoint - redirect to /mcp with guidance
744
+
384
745
  app.get("/sse", (c) => {
385
- return c.json({
386
- error: 'SSE transport deprecated',
387
- message: 'The SSE transport was deprecated in MCP specification 2025-03-26. Please use the Streamable HTTP transport at /mcp instead.',
388
- redirect: '/mcp'
389
- }, 410);
746
+ return c.json(
747
+ {
748
+ error: "SSE transport deprecated",
749
+ message:
750
+ "The SSE transport was deprecated in MCP specification 2025-03-26. Please use /mcp instead.",
751
+ redirect: "/mcp",
752
+ },
753
+ 410
754
+ );
390
755
  });
391
-
756
+
392
757
  serve({ fetch: app.fetch, port }, (info) => {
393
- console.error('');
394
- console.error(\`╔═══════════════════════════════════════════════════════════════╗\`);
395
- console.error(\`║ MCP Server: \${SERVER_NAME.padEnd(46)} ║\`);
396
- console.error(\`╠═══════════════════════════════════════════════════════════════╣\`);
758
+ console.error("");
759
+ console.error("╔═══════════════════════════════════════════════════════════════╗");
760
+ console.error(\`║ MCP Runtime: \${SERVER_NAME.padEnd(45)} ║\`);
761
+ console.error("╠═══════════════════════════════════════════════════════════════╣");
397
762
  console.error(\`║ Status: Running ║\`);
398
763
  console.error(\`║ Port: \${String(info.port).padEnd(53)} ║\`);
399
- console.error(\`╠═══════════════════════════════════════════════════════════════╣\`);
400
- console.error(\`║ Endpoints: ║\`);
401
- console.error(\`║ MCP: http://localhost:\${info.port}/mcp\`.padEnd(64) + \`║\`);
402
- console.error(\`║ Health: http://localhost:\${info.port}/health\`.padEnd(64) + \`║\`);
403
- console.error(\`╠═══════════════════════════════════════════════════════════════╣\`);
404
- console.error(\`║ For AI Clients: ║\`);
405
- console.error(\`║ ChatGPT/Cursor URL: http://localhost:\${info.port}/mcp\`.padEnd(64) + \`║\`);
406
- console.error(\`║ Claude Desktop: Use stdio transport (npm start) ║\`);
407
- console.error(\`╚═══════════════════════════════════════════════════════════════╝\`);
408
- console.error('');
764
+ console.error("╠═══════════════════════════════════════════════════════════════╣");
765
+ ${startupDetails}
766
+ console.error("╚═══════════════════════════════════════════════════════════════╝");
767
+ console.error("");
409
768
  });
410
-
769
+
411
770
  return app;
412
771
  }
413
772
  `;
414
773
  }
415
- function generateEnvExample(tools, securitySchemes) {
774
+ function generateEnvExample(tools, securitySchemes, options) {
775
+ const runtimeMode = getRuntimeMode(options);
416
776
  const lines = [
417
777
  "# API Configuration",
418
- "API_BASE_URL=http://localhost:5001",
778
+ `API_BASE_URL=${options.baseUrl}`,
419
779
  "",
420
780
  "# Emcy Telemetry (optional)",
421
- "# Set these to enable telemetry to Emcy platform",
422
781
  "# EMCY_API_KEY=your-api-key-from-emcy-dashboard",
423
782
  "# EMCY_TELEMETRY_URL=http://localhost:5140/api/v1/telemetry",
424
783
  "# EMCY_MCP_SERVER_ID=mcp_xxxxxxxxxxxx",
425
784
  "# EMCY_DEBUG=false",
426
785
  "",
427
- "# Server Port (for HTTP transport)",
786
+ "# Server Port",
428
787
  "PORT=3000",
429
788
  ];
430
- // Collect unique security schemes used by tools
431
- const usedSchemes = new Set();
432
- for (const tool of tools) {
433
- for (const scheme of tool.securitySchemes) {
434
- usedSchemes.add(scheme);
789
+ if (runtimeMode === "emcy_hosted_worker") {
790
+ lines.push("", "# Hosted worker configuration", "EMCY_WORKER_SHARED_SECRET=change-me", "# EMCY_WORKER_SECRET_HEADER=x-emcy-worker-secret", "# EMCY_UPSTREAM_ACCESS_TOKEN_HEADER=x-emcy-upstream-access-token");
791
+ if (options.hostedOauthConfig?.authorizationServerUrl) {
792
+ lines.push("", "# Hosted OAuth reference", `# Provider: ${options.hostedOauthConfig.provider ?? "manual"}`, `# Authorization server: ${options.hostedOauthConfig.authorizationServerUrl}`, `# Client ID: ${options.hostedOauthConfig.clientId ?? ""}`, `# Resource: ${options.hostedOauthConfig.resource ?? ""}`, `# Scopes: ${(options.hostedOauthConfig.scopes ?? []).join(" ")}`);
435
793
  }
436
794
  }
437
- if (usedSchemes.size > 0) {
438
- lines.push("", "# Security Credentials");
795
+ const configuredHeaders = options.upstreamHeaders ?? [];
796
+ if (configuredHeaders.length > 0) {
797
+ lines.push("", "# Configured upstream headers");
798
+ const seenEnvVars = new Set();
799
+ for (const header of configuredHeaders) {
800
+ if (seenEnvVars.has(header.envVar)) {
801
+ continue;
802
+ }
803
+ seenEnvVars.add(header.envVar);
804
+ if (header.valuePrefix) {
805
+ lines.push(`# ${header.name} will be sent as "${header.valuePrefix} <value>"`);
806
+ }
807
+ else {
808
+ lines.push(`# ${header.name} will be sent as-is`);
809
+ }
810
+ lines.push(`${header.envVar}=${header.defaultValue ?? ""}`);
811
+ }
812
+ }
813
+ if (runtimeMode === "standalone_headers") {
814
+ const usedSchemes = new Set();
815
+ for (const tool of tools) {
816
+ for (const scheme of tool.securitySchemes) {
817
+ usedSchemes.add(scheme);
818
+ }
819
+ }
820
+ const schemeLines = [];
439
821
  for (const schemeName of usedSchemes) {
440
822
  const scheme = securitySchemes[schemeName];
441
- const envKey = schemeName.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
442
- if (scheme?.type === "apiKey") {
443
- lines.push(`API_KEY_${envKey}=your-api-key`);
823
+ const envKey = toEnvKey(schemeName);
824
+ if (scheme?.type === "apiKey" && scheme.in === "header") {
825
+ schemeLines.push(`API_KEY_${envKey}=`);
444
826
  }
445
827
  else if (scheme?.type === "http" && scheme.scheme === "bearer") {
446
- lines.push(`BEARER_TOKEN_${envKey}=your-bearer-token`);
447
- }
448
- else if (scheme?.type === "oauth2") {
449
- lines.push(`OAUTH_CLIENT_ID_${envKey}=your-client-id`);
450
- lines.push(`OAUTH_CLIENT_SECRET_${envKey}=your-client-secret`);
828
+ schemeLines.push(`BEARER_TOKEN_${envKey}=`);
451
829
  }
452
830
  }
831
+ if (schemeLines.length > 0) {
832
+ lines.push("", "# OpenAPI-derived upstream credentials");
833
+ lines.push(...schemeLines);
834
+ }
453
835
  }
454
836
  return lines.join("\n");
455
837
  }
456
- function generateReadme(options) {
457
- return `# ${options.name}
838
+ function generateReadme(options, tools, securitySchemes) {
839
+ const runtimeMode = getRuntimeMode(options);
840
+ const configuredHeaders = options.upstreamHeaders ?? [];
841
+ const hostedOauthSummary = formatHostedOauthDescription(options.hostedOauthConfig);
842
+ const toolInstructionSummary = formatToolInstructionSummary(options.toolInstructions);
843
+ const hasPrompts = options.prompts && options.prompts.length > 0;
844
+ const promptSection = hasPrompts
845
+ ? `
846
+ ## Context Prompts
847
+
848
+ This runtime includes ${options.prompts.length} pre-defined prompt(s):
849
+
850
+ ${options.prompts.map((prompt) => `- **${prompt.name}**: ${prompt.description}`).join("\n")}
851
+ `
852
+ : "";
853
+ if (runtimeMode === "emcy_hosted_worker") {
854
+ return `# ${options.name}
855
+
856
+ Hosted worker runtime generated from an OpenAPI specification by [Emcy](https://emcy.ai).
857
+ ${promptSection}
858
+ ## Runtime Mode
458
859
 
459
- MCP Server generated from OpenAPI specification by [Emcy](https://emcy.dev).
860
+ This runtime is intended to run behind Emcy-hosted MCP auth.
861
+
862
+ - Emcy owns the public MCP URL and OAuth flow
863
+ - Emcy forwards a short-lived downstream access token to this worker
864
+ - Hosted OAuth config: ${hostedOauthSummary}
865
+ - Tool instructions configured for: ${toolInstructionSummary}
866
+ - MCP clients should connect to Emcy, not directly to this worker
460
867
 
461
868
  ## Quick Start
462
869
 
463
870
  \`\`\`bash
464
- # Install dependencies
465
871
  npm install
466
-
467
- # Build
468
872
  npm run build
469
-
470
- # Run with HTTP transport (for ChatGPT, Cursor, web clients)
471
873
  npm run start:http
472
-
473
- # Or run with stdio transport (for Claude Desktop)
474
- npm start
475
874
  \`\`\`
476
875
 
477
876
  ## Configuration
478
877
 
479
878
  Copy \`.env.example\` to \`.env\` and configure:
480
879
 
481
- - \`API_BASE_URL\`: Base URL of the API (default: ${options.baseUrl})
482
- - \`PORT\`: Server port for HTTP transport (default: 3000)
483
- - Security credentials as needed
484
-
485
- ---
880
+ - \`API_BASE_URL\`: Base URL of the downstream API
881
+ - \`PORT\`: HTTP port for the worker runtime
882
+ - \`EMCY_WORKER_SHARED_SECRET\`: Shared secret Emcy uses to call the worker
486
883
 
487
- ## 🤖 AI Client Configuration
884
+ ## Local Validation
488
885
 
489
- ### ChatGPT (OpenAI)
490
-
491
- ChatGPT supports MCP servers via Developer Mode. Use the Streamable HTTP transport:
492
-
493
- 1. Start the server with HTTP transport:
494
- \`\`\`bash
495
- npm run start:http
496
- \`\`\`
497
-
498
- 2. In ChatGPT Developer Mode, add your MCP server:
499
- - **URL**: \`http://your-server-url:3000/mcp\`
500
- - For local development, you'll need to expose via a tunnel (ngrok, cloudflare tunnel, etc.)
501
-
502
- ### Cursor IDE
503
-
504
- Cursor supports both HTTP and stdio transports:
505
-
506
- **Option A: HTTP Transport (Recommended)**
507
-
508
- Add to your project's \`.cursor/mcp.json\`:
509
-
510
- \`\`\`json
511
- {
512
- "mcpServers": {
513
- "${options.name}": {
514
- "url": "http://localhost:3000/mcp"
886
+ 1. Run the worker with \`npm run start:http\`
887
+ 2. Configure Emcy to call this worker
888
+ 3. Let Emcy host the public MCP server, OAuth flow, and client registration
889
+ 4. Validate tool calls through Emcy
890
+ `;
515
891
  }
516
- }
517
- }
518
- \`\`\`
519
-
520
- Then start the server: \`npm run start:http\`
892
+ const derivedSecuritySupport = Array.from(new Set(tools.flatMap((tool) => tool.securitySchemes).map((schemeName) => {
893
+ const scheme = securitySchemes[schemeName];
894
+ if (scheme?.type === "apiKey") {
895
+ return `${schemeName} (API key)`;
896
+ }
897
+ if (scheme?.type === "http" && scheme.scheme === "bearer") {
898
+ return `${schemeName} (Bearer token)`;
899
+ }
900
+ return null;
901
+ }))).filter(Boolean);
902
+ return `# ${options.name}
521
903
 
522
- **Option B: Stdio Transport**
904
+ MCP server generated from an OpenAPI specification by [Emcy](https://emcy.ai).
905
+ ${promptSection}
906
+ ## Runtime Mode
523
907
 
524
- Add to your project's \`.cursor/mcp.json\`:
908
+ \`${runtimeMode}\`
525
909
 
526
- \`\`\`json
527
- {
528
- "mcpServers": {
529
- "${options.name}": {
530
- "command": "node",
531
- "args": ["<absolute-path-to>/build/index.js"]
532
- }
533
- }
534
- }
535
- \`\`\`
910
+ ${runtimeMode === "standalone_headers"
911
+ ? `This server runs as a standalone MCP endpoint and injects static headers into upstream API calls.
536
912
 
537
- Restart Cursor after adding the configuration.
913
+ - Configured headers: ${formatHeaderDescription(configuredHeaders)}
914
+ - OpenAPI header security schemes: ${derivedSecuritySupport.length > 0 ? derivedSecuritySupport.join(", ") : "none detected"}`
915
+ : `This server runs as a standalone MCP endpoint with no built-in upstream authentication logic.`}
538
916
 
539
- ### Claude Desktop
917
+ ## Quick Start
540
918
 
541
- Claude Desktop uses stdio transport:
919
+ \`\`\`bash
920
+ npm install
921
+ npm run build
542
922
 
543
- Add to your Claude Desktop config (\`~/Library/Application Support/Claude/claude_desktop_config.json\` on macOS):
923
+ # Streamable HTTP
924
+ npm run start:http
544
925
 
545
- \`\`\`json
546
- {
547
- "mcpServers": {
548
- "${options.name}": {
549
- "command": "node",
550
- "args": ["<absolute-path-to>/build/index.js"]
551
- }
552
- }
553
- }
926
+ # Or stdio for local desktop clients
927
+ npm start
554
928
  \`\`\`
555
929
 
556
- ---
557
-
558
- ## Transport Endpoints
559
-
560
- When running with HTTP transport (\`npm run start:http\`):
561
-
562
- | Endpoint | Transport | Description |
563
- |----------|-----------|-------------|
564
- | \`/mcp\` | Streamable HTTP | Modern transport (MCP spec 2025-03-26). **Recommended.** |
565
- | \`/sse\` | Server-Sent Events | Legacy transport for older clients. |
566
- | \`/health\` | - | Health check endpoint. |
567
-
568
- ---
569
-
570
- ## Troubleshooting
930
+ ## Configuration
571
931
 
572
- ### "No Resources Found" in Cursor
932
+ Copy \`.env.example\` to \`.env\`.
573
933
 
574
- 1. Make sure the server is running: \`npm run start:http\`
575
- 2. Check the health endpoint: \`curl http://localhost:3000/health\`
576
- 3. Verify your \`mcp.json\` path is correct
577
- 4. Restart Cursor after configuration changes
578
- 5. Try using stdio transport instead of HTTP
934
+ - \`API_BASE_URL\`: Base URL of the target API
935
+ - \`PORT\`: HTTP port for the MCP server
936
+ ${runtimeMode === "standalone_headers" ? "- Set the configured header env vars before starting the server" : ""}
579
937
 
580
- ### Connection Errors
938
+ ## Client Usage
581
939
 
582
- 1. Ensure the API base URL is correct in \`.env\`
583
- 2. Check that required API keys are set in \`.env\`
584
- 3. Verify the target API is accessible from your machine
940
+ - HTTP clients: connect to \`http://localhost:3000/mcp\`
941
+ - Stdio clients: run \`npm start\`
585
942
 
586
- ### TypeScript Build Errors
943
+ ## Notes
587
944
 
588
- \`\`\`bash
589
- # Clean and rebuild
590
- rm -rf build/
591
- npm run build
592
- \`\`\`
945
+ - This generator no longer produces standalone public OAuth resource servers.
946
+ - For user-scoped OAuth APIs, use Emcy-hosted MCP auth with \`emcy_hosted_worker\` mode.
593
947
  `;
594
948
  }
595
949
  //# sourceMappingURL=generator.js.map