@godscene/shared 1.7.11
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 +9 -0
- package/dist/es/baseDB.mjs +109 -0
- package/dist/es/cli/cli-args.mjs +95 -0
- package/dist/es/cli/cli-error.mjs +24 -0
- package/dist/es/cli/cli-runner.mjs +122 -0
- package/dist/es/cli/index.mjs +4 -0
- package/dist/es/common.mjs +37 -0
- package/dist/es/constants/example-code.mjs +227 -0
- package/dist/es/constants/index.mjs +124 -0
- package/dist/es/env/basic.mjs +6 -0
- package/dist/es/env/constants.mjs +110 -0
- package/dist/es/env/global-config-manager.mjs +94 -0
- package/dist/es/env/helper.mjs +43 -0
- package/dist/es/env/index.mjs +5 -0
- package/dist/es/env/init-debug.mjs +18 -0
- package/dist/es/env/model-config-manager.mjs +79 -0
- package/dist/es/env/parse-model-config.mjs +165 -0
- package/dist/es/env/types.mjs +232 -0
- package/dist/es/env/utils.mjs +18 -0
- package/dist/es/extractor/constants.mjs +2 -0
- package/dist/es/extractor/cs_postmessage.mjs +61 -0
- package/dist/es/extractor/customLocator.mjs +641 -0
- package/dist/es/extractor/debug.mjs +6 -0
- package/dist/es/extractor/dom-util.mjs +96 -0
- package/dist/es/extractor/index.mjs +5 -0
- package/dist/es/extractor/locator.mjs +250 -0
- package/dist/es/extractor/tree.mjs +78 -0
- package/dist/es/extractor/util.mjs +245 -0
- package/dist/es/extractor/web-extractor.mjs +393 -0
- package/dist/es/img/box-select.mjs +824 -0
- package/dist/es/img/canvas-fallback.mjs +238 -0
- package/dist/es/img/get-photon.mjs +45 -0
- package/dist/es/img/get-sharp.mjs +11 -0
- package/dist/es/img/index.mjs +4 -0
- package/dist/es/img/info.mjs +35 -0
- package/dist/es/img/transform.mjs +275 -0
- package/dist/es/index.mjs +2 -0
- package/dist/es/key-alias-utils.mjs +19 -0
- package/dist/es/logger.mjs +64 -0
- package/dist/es/mcp/base-server.mjs +282 -0
- package/dist/es/mcp/base-tools.mjs +159 -0
- package/dist/es/mcp/chrome-path.mjs +35 -0
- package/dist/es/mcp/cli-report-session.mjs +78 -0
- package/dist/es/mcp/error-formatter.mjs +19 -0
- package/dist/es/mcp/index.mjs +9 -0
- package/dist/es/mcp/init-arg-utils.mjs +38 -0
- package/dist/es/mcp/inject-report-html-plugin.mjs +53 -0
- package/dist/es/mcp/launcher-helper.mjs +52 -0
- package/dist/es/mcp/tool-generator.mjs +419 -0
- package/dist/es/mcp/types.mjs +3 -0
- package/dist/es/node/fs.mjs +44 -0
- package/dist/es/node/index.mjs +2 -0
- package/dist/es/node/port.mjs +24 -0
- package/dist/es/polyfills/async-hooks.mjs +2 -0
- package/dist/es/polyfills/index.mjs +1 -0
- package/dist/es/types/index.mjs +3 -0
- package/dist/es/us-keyboard-layout.mjs +1414 -0
- package/dist/es/us-keyboard-layout.mjs.LICENSE.txt +5 -0
- package/dist/es/utils.mjs +72 -0
- package/dist/es/zod-schema-utils.mjs +54 -0
- package/dist/lib/baseDB.js +149 -0
- package/dist/lib/cli/cli-args.js +138 -0
- package/dist/lib/cli/cli-error.js +61 -0
- package/dist/lib/cli/cli-runner.js +181 -0
- package/dist/lib/cli/index.js +53 -0
- package/dist/lib/common.js +93 -0
- package/dist/lib/constants/example-code.js +264 -0
- package/dist/lib/constants/index.js +221 -0
- package/dist/lib/env/basic.js +40 -0
- package/dist/lib/env/constants.js +153 -0
- package/dist/lib/env/global-config-manager.js +128 -0
- package/dist/lib/env/helper.js +80 -0
- package/dist/lib/env/index.js +90 -0
- package/dist/lib/env/init-debug.js +52 -0
- package/dist/lib/env/model-config-manager.js +113 -0
- package/dist/lib/env/parse-model-config.js +211 -0
- package/dist/lib/env/types.js +572 -0
- package/dist/lib/env/utils.js +61 -0
- package/dist/lib/extractor/constants.js +42 -0
- package/dist/lib/extractor/cs_postmessage.js +98 -0
- package/dist/lib/extractor/customLocator.js +693 -0
- package/dist/lib/extractor/debug.js +12 -0
- package/dist/lib/extractor/dom-util.js +157 -0
- package/dist/lib/extractor/index.js +87 -0
- package/dist/lib/extractor/locator.js +296 -0
- package/dist/lib/extractor/tree.js +124 -0
- package/dist/lib/extractor/util.js +336 -0
- package/dist/lib/extractor/web-extractor.js +442 -0
- package/dist/lib/img/box-select.js +875 -0
- package/dist/lib/img/canvas-fallback.js +305 -0
- package/dist/lib/img/get-photon.js +82 -0
- package/dist/lib/img/get-sharp.js +45 -0
- package/dist/lib/img/index.js +95 -0
- package/dist/lib/img/info.js +92 -0
- package/dist/lib/img/transform.js +364 -0
- package/dist/lib/index.js +36 -0
- package/dist/lib/key-alias-utils.js +62 -0
- package/dist/lib/logger.js +114 -0
- package/dist/lib/mcp/base-server.js +332 -0
- package/dist/lib/mcp/base-tools.js +193 -0
- package/dist/lib/mcp/chrome-path.js +72 -0
- package/dist/lib/mcp/cli-report-session.js +121 -0
- package/dist/lib/mcp/error-formatter.js +53 -0
- package/dist/lib/mcp/index.js +114 -0
- package/dist/lib/mcp/init-arg-utils.js +78 -0
- package/dist/lib/mcp/inject-report-html-plugin.js +98 -0
- package/dist/lib/mcp/launcher-helper.js +86 -0
- package/dist/lib/mcp/tool-generator.js +456 -0
- package/dist/lib/mcp/types.js +40 -0
- package/dist/lib/node/fs.js +97 -0
- package/dist/lib/node/index.js +65 -0
- package/dist/lib/node/port.js +61 -0
- package/dist/lib/polyfills/async-hooks.js +36 -0
- package/dist/lib/polyfills/index.js +58 -0
- package/dist/lib/types/index.js +37 -0
- package/dist/lib/us-keyboard-layout.js +1457 -0
- package/dist/lib/us-keyboard-layout.js.LICENSE.txt +5 -0
- package/dist/lib/utils.js +148 -0
- package/dist/lib/zod-schema-utils.js +97 -0
- package/dist/types/baseDB.d.ts +25 -0
- package/dist/types/cli/cli-args.d.ts +8 -0
- package/dist/types/cli/cli-error.d.ts +5 -0
- package/dist/types/cli/cli-runner.d.ts +19 -0
- package/dist/types/cli/index.d.ts +4 -0
- package/dist/types/common.d.ts +12 -0
- package/dist/types/constants/example-code.d.ts +2 -0
- package/dist/types/constants/index.d.ts +61 -0
- package/dist/types/env/basic.d.ts +6 -0
- package/dist/types/env/constants.d.ts +50 -0
- package/dist/types/env/global-config-manager.d.ts +32 -0
- package/dist/types/env/helper.d.ts +4 -0
- package/dist/types/env/index.d.ts +4 -0
- package/dist/types/env/init-debug.d.ts +1 -0
- package/dist/types/env/model-config-manager.d.ts +25 -0
- package/dist/types/env/parse-model-config.d.ts +31 -0
- package/dist/types/env/types.d.ts +339 -0
- package/dist/types/env/utils.d.ts +7 -0
- package/dist/types/extractor/constants.d.ts +1 -0
- package/dist/types/extractor/cs_postmessage.d.ts +2 -0
- package/dist/types/extractor/customLocator.d.ts +69 -0
- package/dist/types/extractor/debug.d.ts +1 -0
- package/dist/types/extractor/dom-util.d.ts +57 -0
- package/dist/types/extractor/index.d.ts +33 -0
- package/dist/types/extractor/locator.d.ts +9 -0
- package/dist/types/extractor/tree.d.ts +6 -0
- package/dist/types/extractor/util.d.ts +47 -0
- package/dist/types/extractor/web-extractor.d.ts +24 -0
- package/dist/types/img/box-select.d.ts +26 -0
- package/dist/types/img/canvas-fallback.d.ts +105 -0
- package/dist/types/img/get-photon.d.ts +19 -0
- package/dist/types/img/get-sharp.d.ts +3 -0
- package/dist/types/img/index.d.ts +3 -0
- package/dist/types/img/info.d.ts +34 -0
- package/dist/types/img/transform.d.ts +98 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/key-alias-utils.d.ts +9 -0
- package/dist/types/logger.d.ts +5 -0
- package/dist/types/mcp/base-server.d.ts +93 -0
- package/dist/types/mcp/base-tools.d.ts +148 -0
- package/dist/types/mcp/chrome-path.d.ts +2 -0
- package/dist/types/mcp/cli-report-session.d.ts +12 -0
- package/dist/types/mcp/error-formatter.d.ts +12 -0
- package/dist/types/mcp/index.d.ts +9 -0
- package/dist/types/mcp/init-arg-utils.d.ts +13 -0
- package/dist/types/mcp/inject-report-html-plugin.d.ts +18 -0
- package/dist/types/mcp/launcher-helper.d.ts +94 -0
- package/dist/types/mcp/tool-generator.d.ts +10 -0
- package/dist/types/mcp/types.d.ts +113 -0
- package/dist/types/node/fs.d.ts +15 -0
- package/dist/types/node/index.d.ts +2 -0
- package/dist/types/node/port.d.ts +8 -0
- package/dist/types/polyfills/async-hooks.d.ts +6 -0
- package/dist/types/polyfills/index.d.ts +4 -0
- package/dist/types/types/index.d.ts +36 -0
- package/dist/types/us-keyboard-layout.d.ts +32 -0
- package/dist/types/utils.d.ts +34 -0
- package/dist/types/zod-schema-utils.d.ts +23 -0
- package/package.json +125 -0
- package/src/baseDB.ts +158 -0
- package/src/cli/cli-args.ts +173 -0
- package/src/cli/cli-error.ts +24 -0
- package/src/cli/cli-runner.ts +230 -0
- package/src/cli/index.ts +4 -0
- package/src/common.ts +67 -0
- package/src/constants/example-code.ts +227 -0
- package/src/constants/index.ts +139 -0
- package/src/env/basic.ts +12 -0
- package/src/env/constants.ts +303 -0
- package/src/env/global-config-manager.ts +191 -0
- package/src/env/helper.ts +58 -0
- package/src/env/index.ts +4 -0
- package/src/env/init-debug.ts +34 -0
- package/src/env/model-config-manager.ts +149 -0
- package/src/env/parse-model-config.ts +357 -0
- package/src/env/types.ts +583 -0
- package/src/env/utils.ts +39 -0
- package/src/extractor/constants.ts +5 -0
- package/src/extractor/cs_postmessage.ts +136 -0
- package/src/extractor/customLocator.ts +1245 -0
- package/src/extractor/debug.ts +10 -0
- package/src/extractor/dom-util.ts +231 -0
- package/src/extractor/index.ts +50 -0
- package/src/extractor/locator.ts +469 -0
- package/src/extractor/tree.ts +179 -0
- package/src/extractor/util.ts +482 -0
- package/src/extractor/web-extractor.ts +617 -0
- package/src/img/box-select.ts +588 -0
- package/src/img/canvas-fallback.ts +393 -0
- package/src/img/get-photon.ts +108 -0
- package/src/img/get-sharp.ts +18 -0
- package/src/img/index.ts +27 -0
- package/src/img/info.ts +102 -0
- package/src/img/transform.ts +553 -0
- package/src/index.ts +1 -0
- package/src/key-alias-utils.ts +23 -0
- package/src/logger.ts +96 -0
- package/src/mcp/base-server.ts +500 -0
- package/src/mcp/base-tools.ts +391 -0
- package/src/mcp/chrome-path.ts +48 -0
- package/src/mcp/cli-report-session.ts +130 -0
- package/src/mcp/error-formatter.ts +52 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/init-arg-utils.ts +105 -0
- package/src/mcp/inject-report-html-plugin.ts +119 -0
- package/src/mcp/launcher-helper.ts +200 -0
- package/src/mcp/tool-generator.ts +658 -0
- package/src/mcp/types.ts +131 -0
- package/src/node/fs.ts +84 -0
- package/src/node/index.ts +2 -0
- package/src/node/port.ts +37 -0
- package/src/polyfills/async-hooks.ts +6 -0
- package/src/polyfills/index.ts +4 -0
- package/src/types/index.ts +54 -0
- package/src/us-keyboard-layout.ts +723 -0
- package/src/utils.ts +149 -0
- package/src/zod-schema-utils.ts +133 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { ParseArgsConfig } from 'node:util';
|
|
3
|
+
import { setIsMcp } from '@godscene/shared/utils';
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
|
+
import express, {
|
|
8
|
+
type Application,
|
|
9
|
+
type Request,
|
|
10
|
+
type Response,
|
|
11
|
+
} from 'express';
|
|
12
|
+
import { getErrorMessage } from './error-formatter';
|
|
13
|
+
import type { IMidsceneTools } from './types';
|
|
14
|
+
|
|
15
|
+
export interface BaseMCPServerConfig {
|
|
16
|
+
name: string;
|
|
17
|
+
version: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HttpLaunchOptions {
|
|
22
|
+
port: number;
|
|
23
|
+
host?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LaunchMCPServerResult {
|
|
27
|
+
/**
|
|
28
|
+
* The MCP server port (for HTTP mode)
|
|
29
|
+
*/
|
|
30
|
+
port?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The server host (for HTTP mode)
|
|
34
|
+
*/
|
|
35
|
+
host?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Function to gracefully shutdown the MCP server
|
|
39
|
+
*/
|
|
40
|
+
close: () => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SessionData {
|
|
44
|
+
transport: StreamableHTTPServerTransport;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
lastAccessedAt: Date;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* CLI argument configuration for MCP servers
|
|
51
|
+
*/
|
|
52
|
+
export const CLI_ARGS_CONFIG: ParseArgsConfig['options'] = {
|
|
53
|
+
mode: { type: 'string', default: 'stdio' },
|
|
54
|
+
port: { type: 'string', default: '3000' },
|
|
55
|
+
host: { type: 'string', default: 'localhost' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface CLIArgs {
|
|
59
|
+
mode?: string;
|
|
60
|
+
port?: string;
|
|
61
|
+
host?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Launch an MCP server based on CLI arguments
|
|
66
|
+
* Shared helper to reduce duplication across platform CLI entry points
|
|
67
|
+
*/
|
|
68
|
+
export function launchMCPServer(
|
|
69
|
+
server: BaseMCPServer,
|
|
70
|
+
args: CLIArgs,
|
|
71
|
+
): Promise<LaunchMCPServerResult> {
|
|
72
|
+
if (args.mode === 'http') {
|
|
73
|
+
return server.launchHttp({
|
|
74
|
+
port: Number.parseInt(args.port || '3000', 10),
|
|
75
|
+
host: args.host || 'localhost',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return server.launch();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
82
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
83
|
+
const MAX_SESSIONS = 100; // Maximum concurrent sessions to prevent DoS
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Base MCP Server class with programmatic launch() API
|
|
87
|
+
* Each platform extends this to provide their own tools manager
|
|
88
|
+
*/
|
|
89
|
+
export abstract class BaseMCPServer {
|
|
90
|
+
protected mcpServer: McpServer;
|
|
91
|
+
protected toolsManager?: IMidsceneTools;
|
|
92
|
+
protected config: BaseMCPServerConfig;
|
|
93
|
+
protected providedToolsManager?: IMidsceneTools;
|
|
94
|
+
|
|
95
|
+
constructor(config: BaseMCPServerConfig, toolsManager?: IMidsceneTools) {
|
|
96
|
+
this.config = config;
|
|
97
|
+
this.mcpServer = new McpServer({
|
|
98
|
+
name: config.name,
|
|
99
|
+
version: config.version,
|
|
100
|
+
description: config.description,
|
|
101
|
+
});
|
|
102
|
+
this.providedToolsManager = toolsManager;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Platform-specific: create tools manager instance
|
|
107
|
+
* This is only called if no tools manager was provided in constructor
|
|
108
|
+
*/
|
|
109
|
+
protected abstract createToolsManager(): IMidsceneTools;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Initialize tools manager and attach to MCP server
|
|
113
|
+
*/
|
|
114
|
+
private async initializeToolsManager(): Promise<void> {
|
|
115
|
+
setIsMcp(true);
|
|
116
|
+
|
|
117
|
+
// Use provided tools manager if available, otherwise create new one
|
|
118
|
+
this.toolsManager = this.providedToolsManager || this.createToolsManager();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await this.toolsManager.initTools();
|
|
122
|
+
} catch (error: unknown) {
|
|
123
|
+
const message = getErrorMessage(error);
|
|
124
|
+
console.error(`Failed to initialize tools: ${message}`);
|
|
125
|
+
console.error('Tools will be initialized on first use');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.toolsManager.attachToServer(this.mcpServer);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Perform cleanup on shutdown
|
|
133
|
+
*/
|
|
134
|
+
private async performCleanup(): Promise<void> {
|
|
135
|
+
console.error(`${this.config.name} closing...`);
|
|
136
|
+
this.mcpServer.close();
|
|
137
|
+
await this.toolsManager?.destroy?.().catch(console.error);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Initialize and launch the MCP server with stdio transport
|
|
142
|
+
*/
|
|
143
|
+
public async launch(): Promise<LaunchMCPServerResult> {
|
|
144
|
+
// Hijack stdout-based console methods to stderr for stdio mode
|
|
145
|
+
// This prevents them from breaking MCP JSON-RPC protocol on stdout
|
|
146
|
+
// Note: console.warn and console.error already output to stderr
|
|
147
|
+
console.log = (...args: unknown[]) => {
|
|
148
|
+
console.error('[LOG]', ...args);
|
|
149
|
+
};
|
|
150
|
+
console.info = (...args: unknown[]) => {
|
|
151
|
+
console.error('[INFO]', ...args);
|
|
152
|
+
};
|
|
153
|
+
console.debug = (...args: unknown[]) => {
|
|
154
|
+
console.error('[DEBUG]', ...args);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await this.initializeToolsManager();
|
|
158
|
+
|
|
159
|
+
const transport = new StdioServerTransport();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await this.mcpServer.connect(transport);
|
|
163
|
+
} catch (error: unknown) {
|
|
164
|
+
const message = getErrorMessage(error);
|
|
165
|
+
console.error(`Failed to connect MCP stdio transport: ${message}`);
|
|
166
|
+
throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Setup signal handlers for graceful shutdown
|
|
170
|
+
let isShuttingDown = false;
|
|
171
|
+
const cleanup = () => {
|
|
172
|
+
if (isShuttingDown) return;
|
|
173
|
+
isShuttingDown = true;
|
|
174
|
+
console.error(`${this.config.name} shutting down...`);
|
|
175
|
+
this.performCleanup().finally(() => process.exit(0));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Setup process-level error handlers to prevent crashes
|
|
179
|
+
process.on('uncaughtException', (error: Error & { code?: string }) => {
|
|
180
|
+
// Exit on pipe errors — parent process is gone
|
|
181
|
+
if (error.code === 'EPIPE' || error.code === 'ERR_STREAM_DESTROYED') {
|
|
182
|
+
cleanup();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
console.error(`[${this.config.name}] Uncaught Exception:`, error);
|
|
186
|
+
console.error('Stack:', error.stack);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
process.on('unhandledRejection', (reason: unknown) => {
|
|
190
|
+
console.error(`[${this.config.name}] Unhandled Rejection:`, reason);
|
|
191
|
+
if (reason instanceof Error) {
|
|
192
|
+
console.error('Stack:', reason.stack);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Exit when stdin closes (parent process gone) or reaches EOF
|
|
197
|
+
process.stdin.on('close', cleanup);
|
|
198
|
+
process.stdin.on('end', cleanup);
|
|
199
|
+
|
|
200
|
+
// Exit when stdout/stderr pipe breaks (parent process gone)
|
|
201
|
+
process.stdout.on('error', cleanup);
|
|
202
|
+
|
|
203
|
+
process.once('SIGINT', cleanup);
|
|
204
|
+
process.once('SIGTERM', cleanup);
|
|
205
|
+
process.once('SIGHUP', cleanup);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
close: async () => {
|
|
209
|
+
this.performCleanup();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Launch MCP server with HTTP transport
|
|
216
|
+
* Supports stateful sessions for web applications and service integration
|
|
217
|
+
*/
|
|
218
|
+
public async launchHttp(
|
|
219
|
+
options: HttpLaunchOptions,
|
|
220
|
+
): Promise<LaunchMCPServerResult> {
|
|
221
|
+
// Validate port number
|
|
222
|
+
if (
|
|
223
|
+
!Number.isInteger(options.port) ||
|
|
224
|
+
options.port < 1 ||
|
|
225
|
+
options.port > 65535
|
|
226
|
+
) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Invalid port number: ${options.port}. Port must be between 1 and 65535.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await this.initializeToolsManager();
|
|
233
|
+
|
|
234
|
+
const app: Application = express();
|
|
235
|
+
|
|
236
|
+
// Add JSON body parser with size limit
|
|
237
|
+
app.use(express.json({ limit: '10mb' }));
|
|
238
|
+
|
|
239
|
+
const sessions = new Map<string, SessionData>();
|
|
240
|
+
|
|
241
|
+
app.all('/mcp', async (req: Request, res: Response) => {
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
const requestId = randomUUID().substring(0, 8);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const rawSessionId = req.headers['mcp-session-id'];
|
|
247
|
+
const sessionId = Array.isArray(rawSessionId)
|
|
248
|
+
? rawSessionId[0]
|
|
249
|
+
: rawSessionId;
|
|
250
|
+
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
251
|
+
|
|
252
|
+
if (!session && req.method === 'POST') {
|
|
253
|
+
// Check session limit to prevent DoS
|
|
254
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
255
|
+
console.error(
|
|
256
|
+
`[${new Date().toISOString()}] [${requestId}] Session limit reached: ${sessions.size}/${MAX_SESSIONS}`,
|
|
257
|
+
);
|
|
258
|
+
res.status(503).json({
|
|
259
|
+
error: 'Too many active sessions',
|
|
260
|
+
message: 'Server is at maximum capacity. Please try again later.',
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
session = await this.createHttpSession(sessions);
|
|
265
|
+
console.log(
|
|
266
|
+
`[${new Date().toISOString()}] [${requestId}] New session created: ${session.transport.sessionId}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (session) {
|
|
271
|
+
session.lastAccessedAt = new Date();
|
|
272
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
273
|
+
const duration = Date.now() - startTime;
|
|
274
|
+
console.log(
|
|
275
|
+
`[${new Date().toISOString()}] [${requestId}] Request completed in ${duration}ms`,
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
console.error(
|
|
279
|
+
`[${new Date().toISOString()}] [${requestId}] Invalid session or GET without session`,
|
|
280
|
+
);
|
|
281
|
+
res
|
|
282
|
+
.status(400)
|
|
283
|
+
.json({ error: 'Invalid session or GET without session' });
|
|
284
|
+
}
|
|
285
|
+
} catch (error: unknown) {
|
|
286
|
+
const message = getErrorMessage(error);
|
|
287
|
+
const duration = Date.now() - startTime;
|
|
288
|
+
console.error(
|
|
289
|
+
`[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`,
|
|
290
|
+
);
|
|
291
|
+
if (!res.headersSent) {
|
|
292
|
+
res.status(500).json({
|
|
293
|
+
error: 'Internal server error',
|
|
294
|
+
message: 'Failed to process MCP request',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const host = options.host || 'localhost';
|
|
301
|
+
|
|
302
|
+
// Create server with error handling
|
|
303
|
+
const server = app
|
|
304
|
+
.listen(options.port, host, () => {
|
|
305
|
+
console.log(
|
|
306
|
+
`${this.config.name} HTTP server listening on http://${host}:${options.port}/mcp`,
|
|
307
|
+
);
|
|
308
|
+
})
|
|
309
|
+
.on('error', (error: NodeJS.ErrnoException) => {
|
|
310
|
+
if (error.code === 'EADDRINUSE') {
|
|
311
|
+
console.error(
|
|
312
|
+
`ERROR: Port ${options.port} is already in use.\nPlease try a different port: --port=<number>\nExample: --mode=http --port=${options.port + 1}`,
|
|
313
|
+
);
|
|
314
|
+
} else if (error.code === 'EACCES') {
|
|
315
|
+
console.error(
|
|
316
|
+
`ERROR: Permission denied to bind to port ${options.port}.\nPorts below 1024 require root/admin privileges.\nPlease use a port above 1024 or run with elevated privileges.`,
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
console.error(
|
|
320
|
+
`ERROR: Failed to start HTTP server on ${host}:${options.port}\n` +
|
|
321
|
+
`Reason: ${error.message}\n` +
|
|
322
|
+
`Code: ${error.code || 'unknown'}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const cleanupInterval = this.startSessionCleanup(sessions);
|
|
329
|
+
this.setupHttpShutdownHandlers(server, sessions, cleanupInterval);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
port: options.port,
|
|
333
|
+
host,
|
|
334
|
+
close: async () => {
|
|
335
|
+
clearInterval(cleanupInterval);
|
|
336
|
+
for (const session of sessions.values()) {
|
|
337
|
+
try {
|
|
338
|
+
await session.transport.close();
|
|
339
|
+
} catch (error: unknown) {
|
|
340
|
+
const message = getErrorMessage(error);
|
|
341
|
+
console.error(
|
|
342
|
+
`Failed to close session ${session.transport.sessionId}: ${message}`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
sessions.clear();
|
|
347
|
+
|
|
348
|
+
return new Promise<void>((resolve) => {
|
|
349
|
+
server.close(async (err) => {
|
|
350
|
+
if (err) {
|
|
351
|
+
console.error('Error closing HTTP server:', err);
|
|
352
|
+
}
|
|
353
|
+
await this.performCleanup();
|
|
354
|
+
resolve();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Create a new HTTP session with transport
|
|
363
|
+
*/
|
|
364
|
+
private async createHttpSession(
|
|
365
|
+
sessions: Map<string, SessionData>,
|
|
366
|
+
): Promise<SessionData> {
|
|
367
|
+
const transport = new StreamableHTTPServerTransport({
|
|
368
|
+
sessionIdGenerator: () => randomUUID(),
|
|
369
|
+
onsessioninitialized: (sid: string) => {
|
|
370
|
+
sessions.set(sid, {
|
|
371
|
+
transport,
|
|
372
|
+
createdAt: new Date(),
|
|
373
|
+
lastAccessedAt: new Date(),
|
|
374
|
+
});
|
|
375
|
+
console.log(
|
|
376
|
+
`[${new Date().toISOString()}] Session ${sid} initialized (total: ${sessions.size})`,
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
transport.onclose = () => {
|
|
382
|
+
if (transport.sessionId) {
|
|
383
|
+
sessions.delete(transport.sessionId);
|
|
384
|
+
console.log(
|
|
385
|
+
`[${new Date().toISOString()}] Session ${transport.sessionId} closed (remaining: ${sessions.size})`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
await this.mcpServer.connect(transport);
|
|
392
|
+
} catch (error: unknown) {
|
|
393
|
+
const message = getErrorMessage(error);
|
|
394
|
+
console.error(
|
|
395
|
+
`[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`,
|
|
396
|
+
);
|
|
397
|
+
// Clean up the failed transport
|
|
398
|
+
if (transport.sessionId) {
|
|
399
|
+
sessions.delete(transport.sessionId);
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`Failed to initialize MCP session: ${message}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
transport,
|
|
406
|
+
createdAt: new Date(),
|
|
407
|
+
lastAccessedAt: new Date(),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Start periodic session cleanup for inactive sessions
|
|
413
|
+
*/
|
|
414
|
+
private startSessionCleanup(
|
|
415
|
+
sessions: Map<string, SessionData>,
|
|
416
|
+
): ReturnType<typeof setInterval> {
|
|
417
|
+
return setInterval(() => {
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
for (const [sid, session] of sessions) {
|
|
420
|
+
if (now - session.lastAccessedAt.getTime() > SESSION_TIMEOUT_MS) {
|
|
421
|
+
try {
|
|
422
|
+
session.transport.close();
|
|
423
|
+
sessions.delete(sid);
|
|
424
|
+
console.log(
|
|
425
|
+
`[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`,
|
|
426
|
+
);
|
|
427
|
+
} catch (error: unknown) {
|
|
428
|
+
const message = getErrorMessage(error);
|
|
429
|
+
console.error(
|
|
430
|
+
`[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`,
|
|
431
|
+
);
|
|
432
|
+
// Still delete from map to prevent retry loops
|
|
433
|
+
sessions.delete(sid);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}, CLEANUP_INTERVAL_MS);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Setup shutdown handlers for HTTP server
|
|
442
|
+
*/
|
|
443
|
+
private setupHttpShutdownHandlers(
|
|
444
|
+
server: ReturnType<Application['listen']>,
|
|
445
|
+
sessions: Map<string, SessionData>,
|
|
446
|
+
cleanupInterval: ReturnType<typeof setInterval>,
|
|
447
|
+
): void {
|
|
448
|
+
const cleanup = () => {
|
|
449
|
+
console.error(`${this.config.name} shutting down...`);
|
|
450
|
+
clearInterval(cleanupInterval);
|
|
451
|
+
|
|
452
|
+
// Close all sessions with error handling
|
|
453
|
+
for (const session of sessions.values()) {
|
|
454
|
+
try {
|
|
455
|
+
session.transport.close();
|
|
456
|
+
} catch (error: unknown) {
|
|
457
|
+
const message = getErrorMessage(error);
|
|
458
|
+
console.error(`Error closing session during shutdown: ${message}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
sessions.clear();
|
|
462
|
+
|
|
463
|
+
// Close HTTP server gracefully
|
|
464
|
+
try {
|
|
465
|
+
server.close(() => {
|
|
466
|
+
// Server closed callback - all connections finished
|
|
467
|
+
this.performCleanup().finally(() => process.exit(0));
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Set a timeout in case server.close() hangs
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
console.error('Forcefully shutting down after timeout');
|
|
473
|
+
this.performCleanup().finally(() => process.exit(1));
|
|
474
|
+
}, 5000);
|
|
475
|
+
} catch (error: unknown) {
|
|
476
|
+
const message = getErrorMessage(error);
|
|
477
|
+
console.error(`Error closing HTTP server: ${message}`);
|
|
478
|
+
this.performCleanup().finally(() => process.exit(1));
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// Use once() to prevent multiple registrations
|
|
483
|
+
process.once('SIGINT', cleanup);
|
|
484
|
+
process.once('SIGTERM', cleanup);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get the underlying MCP server instance
|
|
489
|
+
*/
|
|
490
|
+
public getServer(): McpServer {
|
|
491
|
+
return this.mcpServer;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get the tools manager instance
|
|
496
|
+
*/
|
|
497
|
+
public getToolsManager(): IMidsceneTools | undefined {
|
|
498
|
+
return this.toolsManager;
|
|
499
|
+
}
|
|
500
|
+
}
|