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