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