@toolfactory.dev/core 1.0.0-rc
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/LICENSE +21 -0
- package/README.md +88 -0
- package/out/codegen/access-stubs.d.ts +2 -0
- package/out/codegen/access-stubs.js +6 -0
- package/out/codegen/auth-module-names.d.ts +11 -0
- package/out/codegen/auth-module-names.js +34 -0
- package/out/codegen/auth-pipeline-render.d.ts +10 -0
- package/out/codegen/auth-pipeline-render.js +157 -0
- package/out/codegen/auth-stub-bootstrap.d.ts +42 -0
- package/out/codegen/auth-stub-bootstrap.js +252 -0
- package/out/codegen/document-validation.d.ts +22 -0
- package/out/codegen/document-validation.js +76 -0
- package/out/codegen/generated-layout.d.ts +15 -0
- package/out/codegen/generated-layout.js +53 -0
- package/out/codegen/index.d.ts +20 -0
- package/out/codegen/index.js +20 -0
- package/out/codegen/langium-cli-types.d.ts +45 -0
- package/out/codegen/langium-cli-types.js +1 -0
- package/out/codegen/logging-adapter-bootstrap.d.ts +6 -0
- package/out/codegen/logging-adapter-bootstrap.js +69 -0
- package/out/codegen/mcp-host-credential-validation.d.ts +5 -0
- package/out/codegen/mcp-host-credential-validation.js +15 -0
- package/out/codegen/mcp-host-product-runtime.d.ts +22 -0
- package/out/codegen/mcp-host-product-runtime.js +413 -0
- package/out/codegen/project-bootstrap.d.ts +29 -0
- package/out/codegen/project-bootstrap.js +153 -0
- package/out/codegen/render-http-mcp-server.d.ts +3 -0
- package/out/codegen/render-http-mcp-server.js +194 -0
- package/out/codegen/render-mcp-host-shared.d.ts +7 -0
- package/out/codegen/render-mcp-host-shared.js +671 -0
- package/out/codegen/render-oauth-http-mcp-server.d.ts +5 -0
- package/out/codegen/render-oauth-http-mcp-server.js +220 -0
- package/out/codegen/render-stdio-mcp-server.d.ts +5 -0
- package/out/codegen/render-stdio-mcp-server.js +58 -0
- package/out/codegen/write-demos-test-support.d.ts +2 -0
- package/out/codegen/write-demos-test-support.js +28 -0
- package/out/codegen/zod-codegen.d.ts +9 -0
- package/out/codegen/zod-codegen.js +149 -0
- package/out/scripts/generated-scripts-banner.d.ts +2 -0
- package/out/scripts/generated-scripts-banner.js +2 -0
- package/out/scripts/render-kill-listeners-on-port.mjs.d.ts +1 -0
- package/out/scripts/render-kill-listeners-on-port.mjs.js +81 -0
- package/out/scripts/render-load-env-local.mjs.d.ts +1 -0
- package/out/scripts/render-load-env-local.mjs.js +67 -0
- package/out/scripts/render-require-env.mjs.d.ts +1 -0
- package/out/scripts/render-require-env.mjs.js +36 -0
- package/out/scripts/write-generated-scripts.d.ts +4 -0
- package/out/scripts/write-generated-scripts.js +24 -0
- package/package.json +58 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import { hostCredentialValidationRelaySource } from './mcp-host-credential-validation.js';
|
|
2
|
+
import { dbOnlyHelperFunctions, hostCoreTypes, readGeneratedModuleTail, resolveHostContextForCallFn, resolveHostContextForHttpCallFn, withDbConnectionHostContextFn, oauthHostContextBaseUrlFieldsFn, generatedModuleParam, validateHostAtStartupFn, validateOAuthHttpHostAtStartupFn, validateHttpMcpHostAtStartupFn } from './mcp-host-product-runtime.js';
|
|
3
|
+
function httpMcpProfileForMode(mode) {
|
|
4
|
+
if (mode === 'public-http') {
|
|
5
|
+
return 'public';
|
|
6
|
+
}
|
|
7
|
+
return 'passthrough';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Shared generated MCP host runtime (env loading, host config, tool registration).
|
|
11
|
+
*/
|
|
12
|
+
export function renderMcpHostSharedSource(mode, product = 'api2ai') {
|
|
13
|
+
const core = `
|
|
14
|
+
const LOCAL_ENV_FILES = ['.env', '.env.local'];
|
|
15
|
+
|
|
16
|
+
${hostCoreTypes(product)}
|
|
17
|
+
|
|
18
|
+
function stripOptionalQuotes(value: string): string {
|
|
19
|
+
if (value.length < 2) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
const first = value[0];
|
|
23
|
+
const last = value[value.length - 1];
|
|
24
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
25
|
+
return value.slice(1, -1);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseEnvLine(line: string): [string, string] | undefined {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (trimmed.length === 0 || trimmed.startsWith('#')) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const assignment = trimmed.startsWith('export ') ? trimmed.slice('export '.length).trim() : trimmed;
|
|
36
|
+
const separator = assignment.indexOf('=');
|
|
37
|
+
if (separator <= 0) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const key = assignment.slice(0, separator).trim();
|
|
41
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const value = stripOptionalQuotes(assignment.slice(separator + 1).trim());
|
|
45
|
+
return [key, value];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ancestorDirectories(startDir: string): string[] {
|
|
49
|
+
const directories: string[] = [];
|
|
50
|
+
let current = path.resolve(startDir);
|
|
51
|
+
while (true) {
|
|
52
|
+
directories.unshift(current);
|
|
53
|
+
const parent = path.dirname(current);
|
|
54
|
+
if (parent === current) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
return directories;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loadLocalEnvFiles(startDirs: string[], options?: { refresh?: boolean }): string[] {
|
|
63
|
+
const refresh = options?.refresh === true;
|
|
64
|
+
const protectedKeys = refresh ? new Set<string>() : new Set(Object.keys(process.env));
|
|
65
|
+
const loadedKeys = new Set<string>();
|
|
66
|
+
const loadedFiles: string[] = [];
|
|
67
|
+
const visitedFiles = new Set<string>();
|
|
68
|
+
for (const startDir of startDirs) {
|
|
69
|
+
for (const directory of ancestorDirectories(startDir)) {
|
|
70
|
+
for (const fileName of LOCAL_ENV_FILES) {
|
|
71
|
+
const filePath = path.join(directory, fileName);
|
|
72
|
+
if (visitedFiles.has(filePath) || !fs.existsSync(filePath)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
visitedFiles.add(filePath);
|
|
76
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
77
|
+
const overrideExisting = fileName === '.env.local';
|
|
78
|
+
for (const line of content.split(/\\r?\\n/u)) {
|
|
79
|
+
const parsed = parseEnvLine(line);
|
|
80
|
+
if (!parsed) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const [key, value] = parsed;
|
|
84
|
+
if (overrideExisting || !protectedKeys.has(key) || loadedKeys.has(key)) {
|
|
85
|
+
process.env[key] = value;
|
|
86
|
+
loadedKeys.add(key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
loadedFiles.push(filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return loadedFiles;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
${dbOnlyHelperFunctions(product)}
|
|
97
|
+
|
|
98
|
+
${mode === 'oauth-http' ? '' : hostCredentialValidationRelaySource()}
|
|
99
|
+
|
|
100
|
+
function formatToolError(err: unknown): string {
|
|
101
|
+
if (err instanceof Error) {
|
|
102
|
+
return err.message;
|
|
103
|
+
}
|
|
104
|
+
return String(err);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readGeneratedModule(imported: Record<string, unknown>): GeneratedHostModule {
|
|
108
|
+
const generatedTools = imported.generatedTools;
|
|
109
|
+
const invokeTool = imported.invokeTool;
|
|
110
|
+
if (!Array.isArray(generatedTools)) {
|
|
111
|
+
throw new Error('Generated module must export "generatedTools" array.');
|
|
112
|
+
}
|
|
113
|
+
if (typeof invokeTool !== 'function') {
|
|
114
|
+
throw new Error('Generated module must export async "invokeTool" function.');
|
|
115
|
+
}
|
|
116
|
+
const inputZodByTool = imported.inputZodByTool;
|
|
117
|
+
const mcpServerName = imported.mcpServerName;
|
|
118
|
+
const mcpServerVersion = imported.mcpServerVersion;
|
|
119
|
+
${readGeneratedModuleTail(product)}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function requireMcpServerIdentity(generated: GeneratedHostModule): { name: string; version: string } {
|
|
123
|
+
const name = generated.mcpServerName?.trim();
|
|
124
|
+
const version = generated.mcpServerVersion?.trim();
|
|
125
|
+
if (!name) {
|
|
126
|
+
throw new Error('Generated module must export "mcpServerName". Regenerate tool code.');
|
|
127
|
+
}
|
|
128
|
+
if (!version) {
|
|
129
|
+
throw new Error('Generated module must export "mcpServerVersion". Regenerate tool code.');
|
|
130
|
+
}
|
|
131
|
+
return { name, version };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function requireInputZodSchema(inputZodByTool: Record<string, unknown> | undefined, toolName: string): z.ZodTypeAny {
|
|
135
|
+
if (!inputZodByTool) {
|
|
136
|
+
throw new Error('Generated module must export "inputZodByTool". Regenerate tool code.');
|
|
137
|
+
}
|
|
138
|
+
const schema = inputZodByTool[toolName];
|
|
139
|
+
if (!schema || typeof schema !== 'object') {
|
|
140
|
+
throw new Error(\`Generated module inputZodByTool has no schema for tool "\${toolName}". Regenerate tool code.\`);
|
|
141
|
+
}
|
|
142
|
+
return schema as z.ZodTypeAny;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Log when the MCP client requests tools/list (wraps SDK handler set by registerTool). */
|
|
146
|
+
function attachListToolsDebugLogging(mcpServer: McpServer, generated: GeneratedHostModule): void {
|
|
147
|
+
type ListToolsHandler = (request: unknown, extra: unknown) => Promise<ListToolsResult>;
|
|
148
|
+
const handlers = (mcpServer.server as unknown as { _requestHandlers: Map<string, ListToolsHandler> })._requestHandlers;
|
|
149
|
+
const previous = handlers.get('tools/list');
|
|
150
|
+
if (!previous) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
mcpServer.server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
|
|
154
|
+
loggingAdapter.debug('listTools', {
|
|
155
|
+
toolCount: generated.generatedTools.length,
|
|
156
|
+
toolNames: generated.generatedTools.map((t) => t.toolName)
|
|
157
|
+
});
|
|
158
|
+
return previous(request, extra);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function registerMcpTools(
|
|
163
|
+
server: McpServer,
|
|
164
|
+
generated: GeneratedHostModule,
|
|
165
|
+
options: { envDirs: string[]; resolveContext: () => ApiLikeHostContext | Promise<ApiLikeHostContext> }
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
for (const tool of generated.generatedTools) {
|
|
168
|
+
const inputSchema = requireInputZodSchema(generated.inputZodByTool, tool.toolName);
|
|
169
|
+
server.registerTool(
|
|
170
|
+
tool.toolName,
|
|
171
|
+
{
|
|
172
|
+
title: typeof tool.title === 'string' && tool.title.length > 0 ? tool.title : undefined,
|
|
173
|
+
description: tool.description,
|
|
174
|
+
inputSchema
|
|
175
|
+
},
|
|
176
|
+
async (args) => {
|
|
177
|
+
loadLocalEnvFiles(options.envDirs, { refresh: true });
|
|
178
|
+
const hostContext = await Promise.resolve(options.resolveContext());
|
|
179
|
+
try {
|
|
180
|
+
const result = await generated.invokeTool(
|
|
181
|
+
tool.toolName,
|
|
182
|
+
(args ?? {}) as Record<string, unknown>,
|
|
183
|
+
hostContext
|
|
184
|
+
);
|
|
185
|
+
return {
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: 'text',
|
|
189
|
+
text: JSON.stringify(result, null, 2)
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return {
|
|
195
|
+
isError: true,
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: formatToolError(err)
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
attachListToolsDebugLogging(server, generated);
|
|
208
|
+
}
|
|
209
|
+
`.trim();
|
|
210
|
+
const stdioExtras = `
|
|
211
|
+
type HostRuntimeConfig = {
|
|
212
|
+
baseUrlEnvKey?: string;
|
|
213
|
+
authEnvKey?: string;
|
|
214
|
+
envDirs: string[];
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function parseHostArgv(argv: string[], envDirs: string[]): HostRuntimeConfig {
|
|
218
|
+
let baseUrlEnv: string | undefined;
|
|
219
|
+
let authEnv: string | undefined;
|
|
220
|
+
for (let i = 0; i < argv.length; i++) {
|
|
221
|
+
const arg = argv[i];
|
|
222
|
+
if (arg === '--base-url-env') {
|
|
223
|
+
baseUrlEnv = argv[++i];
|
|
224
|
+
if (!baseUrlEnv) {
|
|
225
|
+
throw new Error('Missing value after --base-url-env');
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (arg === '--auth-env') {
|
|
230
|
+
authEnv = argv[++i];
|
|
231
|
+
if (!authEnv) {
|
|
232
|
+
throw new Error('Missing value after --auth-env');
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (arg.startsWith('-')) {
|
|
237
|
+
throw new Error('Unknown option: ' + arg);
|
|
238
|
+
}
|
|
239
|
+
throw new Error('Unexpected positional argument: ' + arg);
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
baseUrlEnvKey: baseUrlEnv,
|
|
243
|
+
authEnvKey: authEnv,
|
|
244
|
+
envDirs
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readCredentialFromEnv(authEnvKey: string | undefined): string | undefined {
|
|
249
|
+
const key = authEnvKey?.trim();
|
|
250
|
+
if (!key) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
const value = process.env[key]?.trim();
|
|
254
|
+
return value && value.length > 0 ? value : undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
${validateHostAtStartupFn(product)}
|
|
258
|
+
|
|
259
|
+
${resolveHostContextForCallFn(product)}
|
|
260
|
+
`.trim();
|
|
261
|
+
const httpMcpProfile = httpMcpProfileForMode(mode === 'public-http' ? mode : 'passthrough-http');
|
|
262
|
+
const httpExtras = `
|
|
263
|
+
type HttpMcpHostRuntimeConfig = {
|
|
264
|
+
baseUrlEnvKey?: string;
|
|
265
|
+
authEnvKey?: string;
|
|
266
|
+
envDirs: string[];
|
|
267
|
+
listenHost: string;
|
|
268
|
+
port: number;
|
|
269
|
+
mcpPath: string;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
function parseHttpMcpHostArgv(argv: string[], envDirs: string[]): HttpMcpHostRuntimeConfig {
|
|
273
|
+
let baseUrlEnv: string | undefined;
|
|
274
|
+
let authEnv: string | undefined;
|
|
275
|
+
let listenHost = '127.0.0.1';
|
|
276
|
+
let port: number | undefined;
|
|
277
|
+
let mcpPath = '/mcp';
|
|
278
|
+
for (let i = 0; i < argv.length; i++) {
|
|
279
|
+
const arg = argv[i];
|
|
280
|
+
if (arg === '--base-url-env') {
|
|
281
|
+
baseUrlEnv = argv[++i];
|
|
282
|
+
if (!baseUrlEnv) {
|
|
283
|
+
throw new Error('Missing value after --base-url-env');
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (arg === '--auth-env') {
|
|
288
|
+
authEnv = argv[++i];
|
|
289
|
+
if (!authEnv) {
|
|
290
|
+
throw new Error('Missing value after --auth-env');
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (arg === '--host') {
|
|
295
|
+
listenHost = argv[++i];
|
|
296
|
+
if (!listenHost) {
|
|
297
|
+
throw new Error('Missing value after --host');
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (arg === '--port') {
|
|
302
|
+
const raw = argv[++i];
|
|
303
|
+
if (!raw) {
|
|
304
|
+
throw new Error('Missing value after --port');
|
|
305
|
+
}
|
|
306
|
+
port = Number.parseInt(raw, 10);
|
|
307
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
308
|
+
throw new Error('Invalid --port value: ' + raw);
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (arg === '--path') {
|
|
313
|
+
mcpPath = argv[++i];
|
|
314
|
+
if (!mcpPath) {
|
|
315
|
+
throw new Error('Missing value after --path');
|
|
316
|
+
}
|
|
317
|
+
if (!mcpPath.startsWith('/')) {
|
|
318
|
+
mcpPath = '/' + mcpPath;
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (arg.startsWith('-')) {
|
|
323
|
+
throw new Error('Unknown option: ' + arg);
|
|
324
|
+
}
|
|
325
|
+
throw new Error('Unexpected positional argument: ' + arg);
|
|
326
|
+
}
|
|
327
|
+
if (port === undefined) {
|
|
328
|
+
throw new Error('Required: --port <number>');
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
baseUrlEnvKey: baseUrlEnv,
|
|
332
|
+
authEnvKey: authEnv,
|
|
333
|
+
envDirs,
|
|
334
|
+
listenHost,
|
|
335
|
+
port,
|
|
336
|
+
mcpPath
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
${httpMcpProfile === 'public'
|
|
341
|
+
? ''
|
|
342
|
+
: `function readCredentialFromEnv(authEnvKey: string | undefined): string | undefined {
|
|
343
|
+
const key = authEnvKey?.trim();
|
|
344
|
+
if (!key) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const value = process.env[key]?.trim();
|
|
348
|
+
return value && value.length > 0 ? value : undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const DEFAULT_MCP_AUTH_HEADER = 'x-api-token';
|
|
352
|
+
|
|
353
|
+
function readAuthHeaderNameFromEnv(): string {
|
|
354
|
+
const configured = process.env.MCP_AUTH_HEADER?.trim();
|
|
355
|
+
return configured && configured.length > 0 ? configured : DEFAULT_MCP_AUTH_HEADER;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function readCredentialFromHttpHeaders(
|
|
359
|
+
headers: Record<string, string | string[] | undefined>,
|
|
360
|
+
headerName: string
|
|
361
|
+
): string | undefined {
|
|
362
|
+
const normalized = headerName.trim().toLowerCase();
|
|
363
|
+
const raw = headers[normalized];
|
|
364
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
365
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
366
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
367
|
+
}
|
|
368
|
+
`}
|
|
369
|
+
|
|
370
|
+
${validateHttpMcpHostAtStartupFn(product)}
|
|
371
|
+
|
|
372
|
+
${resolveHostContextForHttpCallFn(product, httpMcpProfile)}
|
|
373
|
+
`.trim();
|
|
374
|
+
const oauthExtras = `
|
|
375
|
+
type OAuthHttpHostRuntimeConfig = {
|
|
376
|
+
baseUrlEnvKey?: string;
|
|
377
|
+
envDirs: string[];
|
|
378
|
+
listenHost: string;
|
|
379
|
+
port: number;
|
|
380
|
+
mcpPath: string;
|
|
381
|
+
oauthIdpUrl: string;
|
|
382
|
+
oauthScope: string;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
type McpOAuthSession = {
|
|
386
|
+
sessionId: string;
|
|
387
|
+
upstreamCredential?: string;
|
|
388
|
+
credentials?: unknown;
|
|
389
|
+
verifiedAt?: number;
|
|
390
|
+
createdAt: number;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
function parseOAuthHttpHostArgv(argv: string[], envDirs: string[]): OAuthHttpHostRuntimeConfig {
|
|
394
|
+
let baseUrlEnv: string | undefined;
|
|
395
|
+
let listenHost = '127.0.0.1';
|
|
396
|
+
let port: number | undefined;
|
|
397
|
+
let mcpPath = '/mcp';
|
|
398
|
+
let oauthIdpUrl: string | undefined;
|
|
399
|
+
let oauthScope = 'mcp';
|
|
400
|
+
for (let i = 0; i < argv.length; i++) {
|
|
401
|
+
const arg = argv[i];
|
|
402
|
+
if (arg === '--base-url-env') {
|
|
403
|
+
baseUrlEnv = argv[++i];
|
|
404
|
+
if (!baseUrlEnv) {
|
|
405
|
+
throw new Error('Missing value after --base-url-env');
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (arg === '--oauth-idp-url') {
|
|
410
|
+
oauthIdpUrl = argv[++i];
|
|
411
|
+
if (!oauthIdpUrl) {
|
|
412
|
+
throw new Error('Missing value after --oauth-idp-url');
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (arg === '--oauth-scope') {
|
|
417
|
+
oauthScope = argv[++i];
|
|
418
|
+
if (!oauthScope?.trim()) {
|
|
419
|
+
throw new Error('Missing value after --oauth-scope');
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (arg === '--host') {
|
|
424
|
+
listenHost = argv[++i];
|
|
425
|
+
if (!listenHost) {
|
|
426
|
+
throw new Error('Missing value after --host');
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (arg === '--port') {
|
|
431
|
+
const raw = argv[++i];
|
|
432
|
+
if (!raw) {
|
|
433
|
+
throw new Error('Missing value after --port');
|
|
434
|
+
}
|
|
435
|
+
port = Number.parseInt(raw, 10);
|
|
436
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
437
|
+
throw new Error('Invalid --port value: ' + raw);
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (arg === '--path') {
|
|
442
|
+
mcpPath = argv[++i];
|
|
443
|
+
if (!mcpPath) {
|
|
444
|
+
throw new Error('Missing value after --path');
|
|
445
|
+
}
|
|
446
|
+
if (!mcpPath.startsWith('/')) {
|
|
447
|
+
mcpPath = '/' + mcpPath;
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (arg.startsWith('-')) {
|
|
452
|
+
throw new Error('Unknown option: ' + arg);
|
|
453
|
+
}
|
|
454
|
+
throw new Error('Unexpected positional argument: ' + arg);
|
|
455
|
+
}
|
|
456
|
+
if (port === undefined) {
|
|
457
|
+
throw new Error('Required: --port <number>');
|
|
458
|
+
}
|
|
459
|
+
if (!oauthIdpUrl?.trim()) {
|
|
460
|
+
throw new Error('Required: --oauth-idp-url <url>');
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
baseUrlEnvKey: baseUrlEnv,
|
|
464
|
+
envDirs,
|
|
465
|
+
listenHost,
|
|
466
|
+
port,
|
|
467
|
+
mcpPath,
|
|
468
|
+
oauthIdpUrl: oauthIdpUrl.replace(/\\/$/, ''),
|
|
469
|
+
oauthScope: oauthScope.trim()
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function readBearerFromHeaders(headers: Record<string, string | string[] | undefined>): string | undefined {
|
|
474
|
+
const raw = headers.authorization ?? headers.Authorization;
|
|
475
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
476
|
+
if (typeof value !== 'string') {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
const match = /^Bearer\\s+(.+)$/i.exec(value.trim());
|
|
480
|
+
return match?.[1]?.trim() || undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function generatedHasPublicTool(generated: GeneratedHostModule): boolean {
|
|
484
|
+
return generated.generatedTools.some((t) => t.access === 'public');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function generatedHasProtectedTool(generated: GeneratedHostModule): boolean {
|
|
488
|
+
return generated.generatedTools.some((t) => t.access === 'protected');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
${validateOAuthHttpHostAtStartupFn(product)}
|
|
492
|
+
|
|
493
|
+
function resolveOAuthHostBaseUrl(httpHostConfig: OAuthHttpHostRuntimeConfig): string {
|
|
494
|
+
const baseUrlKey = httpHostConfig.baseUrlEnvKey?.trim();
|
|
495
|
+
const baseUrl = baseUrlKey ? process.env[baseUrlKey]?.trim() : undefined;
|
|
496
|
+
if (!baseUrl) {
|
|
497
|
+
throw new Error('Missing host base URL. Pass --base-url-env on oauth-http-mcp-server.js and set the variable.');
|
|
498
|
+
}
|
|
499
|
+
return baseUrl;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
${oauthHostContextBaseUrlFieldsFn(product)}
|
|
503
|
+
|
|
504
|
+
${withDbConnectionHostContextFn(product)}
|
|
505
|
+
|
|
506
|
+
async function verifyCredentialForGate(
|
|
507
|
+
generated: GeneratedHostModule,
|
|
508
|
+
bearer: string | undefined
|
|
509
|
+
): Promise<boolean> {
|
|
510
|
+
const token = bearer?.trim();
|
|
511
|
+
if (!token) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
if (!generated.requiresAuth) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
const verify = generated.verifyCredential;
|
|
518
|
+
if (typeof verify !== 'function') {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
await verify({ inboundCredential: token });
|
|
523
|
+
return true;
|
|
524
|
+
} catch {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function resolveHostContextForOAuthSession(
|
|
530
|
+
httpHostConfig: OAuthHttpHostRuntimeConfig,
|
|
531
|
+
${generatedModuleParam(product)}: GeneratedHostModule,
|
|
532
|
+
headers: Record<string, string | string[] | undefined>,
|
|
533
|
+
sessionStore: Map<string, McpOAuthSession>,
|
|
534
|
+
sessionId: string | undefined
|
|
535
|
+
): Promise<ApiLikeHostContext> {
|
|
536
|
+
const apiFields = oauthHostContextBaseUrlFields(httpHostConfig, ${generatedModuleParam(product)});
|
|
537
|
+
let session = sessionId ? sessionStore.get(sessionId) : undefined;
|
|
538
|
+
if (sessionId && !session) {
|
|
539
|
+
session = { sessionId, createdAt: Date.now() };
|
|
540
|
+
sessionStore.set(sessionId, session);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (session?.verifiedAt && session.upstreamCredential) {
|
|
544
|
+
const credentials = session.credentials;
|
|
545
|
+
return withDbConnectionHostContext(${generatedModuleParam(product)}, {
|
|
546
|
+
...apiFields,
|
|
547
|
+
credential: session.upstreamCredential,
|
|
548
|
+
upstreamCredential: session.upstreamCredential,
|
|
549
|
+
credentials
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const bearer = readBearerFromHeaders(headers);
|
|
554
|
+
const inbound = bearer?.trim();
|
|
555
|
+
if (!inbound) {
|
|
556
|
+
if (session?.upstreamCredential) {
|
|
557
|
+
return withDbConnectionHostContext(${generatedModuleParam(product)}, {
|
|
558
|
+
...apiFields,
|
|
559
|
+
credential: session.upstreamCredential,
|
|
560
|
+
upstreamCredential: session.upstreamCredential,
|
|
561
|
+
credentials: session.credentials
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return withDbConnectionHostContext(${generatedModuleParam(product)}, { ...apiFields });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const verify = ${generatedModuleParam(product)}.verifyCredential;
|
|
568
|
+
if (typeof verify !== 'function') {
|
|
569
|
+
throw new Error('verifyCredential is not exported from generated tools.');
|
|
570
|
+
}
|
|
571
|
+
const verified = await verify({ inboundCredential: inbound });
|
|
572
|
+
const upstreamCredential = verified.upstreamCredential.trim();
|
|
573
|
+
if (upstreamCredential.length === 0) {
|
|
574
|
+
throw new Error('verifyCredential returned an empty upstream credential.');
|
|
575
|
+
}
|
|
576
|
+
const credentials = JSON.parse(JSON.stringify(verified.credentials));
|
|
577
|
+
if (session) {
|
|
578
|
+
session.upstreamCredential = upstreamCredential;
|
|
579
|
+
session.credentials = credentials;
|
|
580
|
+
session.verifiedAt = Date.now();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return withDbConnectionHostContext(${generatedModuleParam(product)}, {
|
|
584
|
+
...apiFields,
|
|
585
|
+
credential: upstreamCredential,
|
|
586
|
+
upstreamCredential,
|
|
587
|
+
credentials
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function oauthResourceMetadataDocument(httpHostConfig: OAuthHttpHostRuntimeConfig): Record<string, unknown> {
|
|
592
|
+
const resource = 'http://' + httpHostConfig.listenHost + ':' + httpHostConfig.port + httpHostConfig.mcpPath;
|
|
593
|
+
return {
|
|
594
|
+
resource,
|
|
595
|
+
authorization_servers: [httpHostConfig.oauthIdpUrl],
|
|
596
|
+
bearer_methods_supported: ['header'],
|
|
597
|
+
scopes_supported: [httpHostConfig.oauthScope]
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function sendOAuthUnauthorized(res: ServerResponse, httpHostConfig: OAuthHttpHostRuntimeConfig): void {
|
|
602
|
+
const resource = 'http://' + httpHostConfig.listenHost + ':' + httpHostConfig.port + httpHostConfig.mcpPath;
|
|
603
|
+
const metadataUrl =
|
|
604
|
+
'http://' + httpHostConfig.listenHost + ':' + httpHostConfig.port + '/.well-known/oauth-protected-resource';
|
|
605
|
+
res.writeHead(401, {
|
|
606
|
+
'content-type': 'application/json',
|
|
607
|
+
'www-authenticate':
|
|
608
|
+
'Bearer error="invalid_token", realm="mcp", resource_metadata="' +
|
|
609
|
+
metadataUrl +
|
|
610
|
+
'", resource="' +
|
|
611
|
+
resource +
|
|
612
|
+
'", scope="' +
|
|
613
|
+
httpHostConfig.oauthScope +
|
|
614
|
+
'"'
|
|
615
|
+
});
|
|
616
|
+
res.end(
|
|
617
|
+
JSON.stringify({
|
|
618
|
+
jsonrpc: '2.0',
|
|
619
|
+
error: { code: -32_001, message: 'Unauthorized' },
|
|
620
|
+
id: null
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
`.trim();
|
|
625
|
+
const httpTransportExtras = `
|
|
626
|
+
async function readMcpHttpJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
627
|
+
const chunks: Buffer[] = [];
|
|
628
|
+
for await (const chunk of req) {
|
|
629
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
630
|
+
}
|
|
631
|
+
if (chunks.length === 0) {
|
|
632
|
+
return undefined;
|
|
633
|
+
}
|
|
634
|
+
const text = Buffer.concat(chunks).toString('utf-8');
|
|
635
|
+
if (text.trim().length === 0) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
return JSON.parse(text) as unknown;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function writeJsonRpcError(res: ServerResponse, status: number, code: number, message: string): void {
|
|
642
|
+
if (res.headersSent) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
646
|
+
res.end(
|
|
647
|
+
JSON.stringify({
|
|
648
|
+
jsonrpc: '2.0',
|
|
649
|
+
error: { code, message },
|
|
650
|
+
id: null
|
|
651
|
+
})
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function writeJsonRpcInternalError(res: ServerResponse): void {
|
|
656
|
+
writeJsonRpcError(res, 500, -32_603, 'Internal server error');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** GET/DELETE without an established session — spec-allowed probe response (Open WebUI Verify Connection). */
|
|
660
|
+
function writeJsonRpcMethodNotAllowed(res: ServerResponse): void {
|
|
661
|
+
writeJsonRpcError(res, 405, -32_000, 'Method not allowed.');
|
|
662
|
+
}
|
|
663
|
+
`.trim();
|
|
664
|
+
const httpMcpModes = ['public-http', 'passthrough-http'];
|
|
665
|
+
const modeExtras = mode === 'stdio' ? stdioExtras : httpMcpModes.includes(mode) ? httpExtras : oauthExtras;
|
|
666
|
+
const usesHttpTransport = httpMcpModes.includes(mode) || mode === 'oauth-http';
|
|
667
|
+
if (usesHttpTransport) {
|
|
668
|
+
return `${core}\n\n${httpTransportExtras}\n\n${modeExtras}`;
|
|
669
|
+
}
|
|
670
|
+
return `${core}\n\n${modeExtras}`;
|
|
671
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type McpHostProduct } from './mcp-host-product-runtime.js';
|
|
2
|
+
/**
|
|
3
|
+
* Static OAuth + stateful MCP Streamable HTTP host for generated `cli/oauth-http-mcp-server.ts`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderOAuthHttpMcpServerSource(product: McpHostProduct | undefined, loggingImport: string): string;
|