@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.
- package/README.md +180 -127
- package/dist/__tests__/cli-config.test.d.ts +2 -0
- package/dist/__tests__/cli-config.test.d.ts.map +1 -0
- package/dist/__tests__/cli-config.test.js +95 -0
- package/dist/__tests__/cli-config.test.js.map +1 -0
- package/dist/__tests__/generator.test.d.ts +1 -1
- package/dist/__tests__/generator.test.js +178 -200
- package/dist/__tests__/generator.test.js.map +1 -1
- package/dist/__tests__/integration.test.js +11 -11
- package/dist/__tests__/integration.test.js.map +1 -1
- package/dist/__tests__/mapper.test.js +30 -6
- package/dist/__tests__/mapper.test.js.map +1 -1
- package/dist/__tests__/parser.test.js +2 -0
- package/dist/__tests__/parser.test.js.map +1 -1
- package/dist/__tests__/tool-identity.test.d.ts +2 -0
- package/dist/__tests__/tool-identity.test.d.ts.map +1 -0
- package/dist/__tests__/tool-identity.test.js +15 -0
- package/dist/__tests__/tool-identity.test.js.map +1 -0
- package/dist/cli-config.d.ts +31 -0
- package/dist/cli-config.d.ts.map +1 -0
- package/dist/cli-config.js +148 -0
- package/dist/cli-config.js.map +1 -0
- package/dist/cli.js +66 -4
- package/dist/cli.js.map +1 -1
- package/dist/generator.d.ts +8 -3
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +711 -327
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mapper.d.ts.map +1 -1
- package/dist/mapper.js +6 -2
- package/dist/mapper.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +18 -5
- package/dist/parser.js.map +1 -1
- package/dist/tool-identity.d.ts +6 -0
- package/dist/tool-identity.d.ts.map +1 -0
- package/dist/tool-identity.js +67 -0
- package/dist/tool-identity.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/generator.js
CHANGED
|
@@ -1,38 +1,121 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Code
|
|
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
|
|
97
|
+
description: `MCP runtime generated from OpenAPI`,
|
|
22
98
|
type: "module",
|
|
23
99
|
main: "build/index.js",
|
|
24
|
-
scripts:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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 ===
|
|
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 () =>
|
|
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
|
|
124
|
-
* Generated by Emcy OpenAPI-to-MCP
|
|
242
|
+
* MCP Runtime: ${options.name}
|
|
243
|
+
* Generated by Emcy OpenAPI-to-MCP
|
|
125
244
|
*/
|
|
126
245
|
|
|
127
|
-
import dotenv from
|
|
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
|
|
253
|
+
type CallToolRequest,
|
|
135
254
|
type CallToolResult,
|
|
136
|
-
type
|
|
255
|
+
type Tool${promptsImport}
|
|
137
256
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
138
|
-
import axios, { type AxiosRequestConfig
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
const
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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:
|
|
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> = {
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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[
|
|
392
|
+
headers["content-type"] = def.requestBodyContentType;
|
|
238
393
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await setupStreamableHttpServer(
|
|
286
|
-
|
|
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
|
|
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
|
|
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
|
|
305
|
-
import { cors } from
|
|
306
|
-
import { serve } from
|
|
307
|
-
import {
|
|
308
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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:
|
|
699
|
+
transport: "streamable-http",
|
|
335
700
|
endpoints: {
|
|
336
|
-
mcp:
|
|
337
|
-
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
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
|
-
|
|
401
|
-
console.error(
|
|
402
|
-
console.error(
|
|
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
|
-
|
|
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
|
|
815
|
+
"# Server Port",
|
|
428
816
|
"PORT=3000",
|
|
429
817
|
];
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
|
442
|
-
if (scheme?.type === "apiKey") {
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
482
|
-
- \`PORT\`:
|
|
483
|
-
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
934
|
+
MCP server generated from an OpenAPI specification by [Emcy](https://emcy.ai).
|
|
935
|
+
${promptSection}
|
|
936
|
+
## Runtime Shape
|
|
521
937
|
|
|
522
|
-
|
|
938
|
+
\`${runtimeMode}\`
|
|
523
939
|
|
|
524
|
-
|
|
940
|
+
${runtimeMode === "standalone_headers"
|
|
941
|
+
? `This server runs as a standalone MCP endpoint and injects static headers into upstream API calls.
|
|
525
942
|
|
|
526
|
-
|
|
527
|
-
{
|
|
528
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
### Claude Desktop
|
|
947
|
+
## Quick Start
|
|
540
948
|
|
|
541
|
-
|
|
949
|
+
\`\`\`bash
|
|
950
|
+
npm install
|
|
951
|
+
npm run build
|
|
542
952
|
|
|
543
|
-
|
|
953
|
+
# Streamable HTTP
|
|
954
|
+
npm run start:http
|
|
544
955
|
|
|
545
|
-
|
|
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
|
-
|
|
962
|
+
Copy \`.env.example\` to \`.env\`.
|
|
573
963
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
968
|
+
## Client Usage
|
|
581
969
|
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
973
|
+
## Notes
|
|
587
974
|
|
|
588
|
-
|
|
589
|
-
|
|
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
|