mcp-researchpowerpack 3.9.4 → 4.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 +116 -208
- package/dist/index.js +280 -337
- package/dist/index.js.map +7 -1
- package/dist/mcp-use.json +7 -0
- package/dist/src/clients/reddit.js +278 -0
- package/dist/src/clients/reddit.js.map +7 -0
- package/dist/src/clients/scraper.js +326 -0
- package/dist/src/clients/scraper.js.map +7 -0
- package/dist/src/clients/search.js +217 -0
- package/dist/src/clients/search.js.map +7 -0
- package/dist/src/config/index.js +138 -0
- package/dist/src/config/index.js.map +7 -0
- package/dist/src/prompts/deep-research.js +41 -0
- package/dist/src/prompts/deep-research.js.map +7 -0
- package/dist/src/prompts/reddit-sentiment.js +47 -0
- package/dist/src/prompts/reddit-sentiment.js.map +7 -0
- package/dist/src/schemas/reddit.js +21 -0
- package/dist/src/schemas/reddit.js.map +7 -0
- package/dist/src/schemas/scrape-links.js +26 -0
- package/dist/src/schemas/scrape-links.js.map +7 -0
- package/dist/src/schemas/start-research.js +17 -0
- package/dist/src/schemas/start-research.js.map +7 -0
- package/dist/src/schemas/web-search.js +53 -0
- package/dist/src/schemas/web-search.js.map +7 -0
- package/dist/src/services/llm-processor.js +675 -0
- package/dist/src/services/llm-processor.js.map +7 -0
- package/dist/src/services/markdown-cleaner.js +62 -0
- package/dist/src/services/markdown-cleaner.js.map +7 -0
- package/dist/src/services/workflow-state.js +116 -0
- package/dist/src/services/workflow-state.js.map +7 -0
- package/dist/src/tools/mcp-helpers.js +50 -0
- package/dist/src/tools/mcp-helpers.js.map +7 -0
- package/dist/src/tools/reddit.js +277 -0
- package/dist/src/tools/reddit.js.map +7 -0
- package/dist/src/tools/registry.js +18 -0
- package/dist/src/tools/registry.js.map +7 -0
- package/dist/src/tools/scrape.js +334 -0
- package/dist/src/tools/scrape.js.map +7 -0
- package/dist/src/tools/search.js +423 -0
- package/dist/src/tools/search.js.map +7 -0
- package/dist/src/tools/start-research.js +199 -0
- package/dist/src/tools/start-research.js.map +7 -0
- package/dist/src/tools/utils.js +29 -0
- package/dist/src/tools/utils.js.map +7 -0
- package/dist/src/utils/bootstrap-guard.js +27 -0
- package/dist/src/utils/bootstrap-guard.js.map +7 -0
- package/dist/src/utils/concurrency.js +62 -0
- package/dist/src/utils/concurrency.js.map +7 -0
- package/dist/src/utils/content-extractor.js +61 -0
- package/dist/src/utils/content-extractor.js.map +7 -0
- package/dist/src/utils/errors.js +211 -0
- package/dist/src/utils/errors.js.map +7 -0
- package/dist/src/utils/logger.js +25 -0
- package/dist/src/utils/logger.js.map +7 -0
- package/dist/src/utils/markdown-formatter.js +15 -0
- package/dist/src/utils/markdown-formatter.js.map +7 -0
- package/dist/src/utils/reddit-keyword-guard.js +29 -0
- package/dist/src/utils/reddit-keyword-guard.js.map +7 -0
- package/dist/src/utils/response.js +81 -0
- package/dist/src/utils/response.js.map +7 -0
- package/dist/src/utils/retry.js +13 -0
- package/dist/src/utils/retry.js.map +7 -0
- package/dist/src/utils/sanitize.js +10 -0
- package/dist/src/utils/sanitize.js.map +7 -0
- package/dist/src/utils/source-type.js +41 -0
- package/dist/src/utils/source-type.js.map +7 -0
- package/dist/src/utils/url-aggregator.js +227 -0
- package/dist/src/utils/url-aggregator.js.map +7 -0
- package/dist/src/utils/workflow-key.js +14 -0
- package/dist/src/utils/workflow-key.js.map +7 -0
- package/dist/src/version.js +32 -0
- package/dist/src/version.js.map +7 -0
- package/package.json +33 -28
- package/dist/clients/reddit.d.ts +0 -69
- package/dist/clients/reddit.d.ts.map +0 -1
- package/dist/clients/reddit.js +0 -369
- package/dist/clients/reddit.js.map +0 -1
- package/dist/clients/research.d.ts +0 -67
- package/dist/clients/research.d.ts.map +0 -1
- package/dist/clients/research.js +0 -290
- package/dist/clients/research.js.map +0 -1
- package/dist/clients/scraper.d.ts +0 -72
- package/dist/clients/scraper.d.ts.map +0 -1
- package/dist/clients/scraper.js +0 -338
- package/dist/clients/scraper.js.map +0 -1
- package/dist/clients/search.d.ts +0 -57
- package/dist/clients/search.d.ts.map +0 -1
- package/dist/clients/search.js +0 -223
- package/dist/clients/search.js.map +0 -1
- package/dist/config/index.d.ts +0 -78
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -201
- package/dist/config/index.js.map +0 -1
- package/dist/config/loader.d.ts +0 -40
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js +0 -322
- package/dist/config/loader.js.map +0 -1
- package/dist/config/types.d.ts +0 -81
- package/dist/config/types.d.ts.map +0 -1
- package/dist/config/types.js +0 -6
- package/dist/config/types.js.map +0 -1
- package/dist/config/yaml/tools.yaml +0 -146
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/schemas/deep-research.d.ts +0 -64
- package/dist/schemas/deep-research.d.ts.map +0 -1
- package/dist/schemas/deep-research.js +0 -224
- package/dist/schemas/deep-research.js.map +0 -1
- package/dist/schemas/scrape-links.d.ts +0 -32
- package/dist/schemas/scrape-links.d.ts.map +0 -1
- package/dist/schemas/scrape-links.js +0 -34
- package/dist/schemas/scrape-links.js.map +0 -1
- package/dist/schemas/web-search.d.ts +0 -22
- package/dist/schemas/web-search.d.ts.map +0 -1
- package/dist/schemas/web-search.js +0 -21
- package/dist/schemas/web-search.js.map +0 -1
- package/dist/services/file-attachment.d.ts +0 -30
- package/dist/services/file-attachment.d.ts.map +0 -1
- package/dist/services/file-attachment.js +0 -207
- package/dist/services/file-attachment.js.map +0 -1
- package/dist/services/llm-processor.d.ts +0 -29
- package/dist/services/llm-processor.d.ts.map +0 -1
- package/dist/services/llm-processor.js +0 -244
- package/dist/services/llm-processor.js.map +0 -1
- package/dist/services/markdown-cleaner.d.ts +0 -8
- package/dist/services/markdown-cleaner.d.ts.map +0 -1
- package/dist/services/markdown-cleaner.js +0 -74
- package/dist/services/markdown-cleaner.js.map +0 -1
- package/dist/tools/definitions.d.ts +0 -16
- package/dist/tools/definitions.d.ts.map +0 -1
- package/dist/tools/definitions.js +0 -17
- package/dist/tools/definitions.js.map +0 -1
- package/dist/tools/reddit.d.ts +0 -14
- package/dist/tools/reddit.d.ts.map +0 -1
- package/dist/tools/reddit.js +0 -265
- package/dist/tools/reddit.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -71
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -252
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/research.d.ts +0 -14
- package/dist/tools/research.d.ts.map +0 -1
- package/dist/tools/research.js +0 -196
- package/dist/tools/research.js.map +0 -1
- package/dist/tools/scrape.d.ts +0 -14
- package/dist/tools/scrape.d.ts.map +0 -1
- package/dist/tools/scrape.js +0 -234
- package/dist/tools/scrape.js.map +0 -1
- package/dist/tools/search.d.ts +0 -10
- package/dist/tools/search.d.ts.map +0 -1
- package/dist/tools/search.js +0 -158
- package/dist/tools/search.js.map +0 -1
- package/dist/tools/utils.d.ts +0 -105
- package/dist/tools/utils.d.ts.map +0 -1
- package/dist/tools/utils.js +0 -159
- package/dist/tools/utils.js.map +0 -1
- package/dist/utils/concurrency.d.ts +0 -28
- package/dist/utils/concurrency.d.ts.map +0 -1
- package/dist/utils/concurrency.js +0 -92
- package/dist/utils/concurrency.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -95
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -390
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -39
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -57
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/markdown-formatter.d.ts +0 -5
- package/dist/utils/markdown-formatter.d.ts.map +0 -1
- package/dist/utils/markdown-formatter.js +0 -15
- package/dist/utils/markdown-formatter.js.map +0 -1
- package/dist/utils/response.d.ts +0 -93
- package/dist/utils/response.d.ts.map +0 -1
- package/dist/utils/response.js +0 -170
- package/dist/utils/response.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -43
- package/dist/utils/retry.d.ts.map +0 -1
- package/dist/utils/retry.js +0 -57
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/url-aggregator.d.ts +0 -90
- package/dist/utils/url-aggregator.d.ts.map +0 -1
- package/dist/utils/url-aggregator.js +0 -538
- package/dist/utils/url-aggregator.js.map +0 -1
- package/dist/version.d.ts +0 -29
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -55
- package/dist/version.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,356 +1,299 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Implements robust error handling - server NEVER crashes on tool failures
|
|
5
|
-
*/
|
|
6
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
import { TOOLS } from './tools/definitions.js';
|
|
10
|
-
import { executeTool, getToolCapabilities } from './tools/registry.js';
|
|
11
|
-
import { classifyError, createToolErrorFromStructured } from './utils/errors.js';
|
|
12
|
-
import { SERVER, getCapabilities } from './config/index.js';
|
|
13
|
-
import { initLogger } from './utils/logger.js';
|
|
14
|
-
const BROKEN_PIPE_ERROR_CODES = new Set([
|
|
15
|
-
'EPIPE',
|
|
16
|
-
'EIO',
|
|
17
|
-
'ERR_STREAM_DESTROYED',
|
|
18
|
-
'ERR_STREAM_WRITE_AFTER_END',
|
|
19
|
-
]);
|
|
20
|
-
const DEFAULT_MCP_PORT = 3000;
|
|
21
|
-
function extractErrorCode(error) {
|
|
22
|
-
if (!error || typeof error !== 'object')
|
|
23
|
-
return undefined;
|
|
24
|
-
const maybeCode = error.code;
|
|
25
|
-
return typeof maybeCode === 'string' ? maybeCode : undefined;
|
|
2
|
+
if (!process.env.UV_THREADPOOL_SIZE) {
|
|
3
|
+
process.env.UV_THREADPOOL_SIZE = "8";
|
|
26
4
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
5
|
+
import { Logger } from "mcp-use";
|
|
6
|
+
import {
|
|
7
|
+
InMemorySessionStore,
|
|
8
|
+
InMemoryStreamManager,
|
|
9
|
+
MCPServer,
|
|
10
|
+
RedisSessionStore,
|
|
11
|
+
RedisStreamManager,
|
|
12
|
+
object
|
|
13
|
+
} from "mcp-use/server";
|
|
14
|
+
import { createClient } from "redis";
|
|
15
|
+
import { SERVER } from "./src/config/index.js";
|
|
16
|
+
import { getLLMHealth } from "./src/services/llm-processor.js";
|
|
17
|
+
import {
|
|
18
|
+
closeWorkflowStateStore,
|
|
19
|
+
configureWorkflowStateStore,
|
|
20
|
+
getWorkflowStateStore
|
|
21
|
+
} from "./src/services/workflow-state.js";
|
|
22
|
+
import { registerAllTools } from "./src/tools/registry.js";
|
|
23
|
+
const DEFAULT_PORT = 3e3;
|
|
24
|
+
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
25
|
+
const WEBSITE_URL = "https://github.com/yigitkonur/mcp-researchpowerpack-http";
|
|
26
|
+
const LOCAL_DEFAULT_HOST = "127.0.0.1";
|
|
27
|
+
const startupLogger = Logger.get("startup");
|
|
28
|
+
function parseCsvEnv(value) {
|
|
29
|
+
if (!value) return void 0;
|
|
30
|
+
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
31
|
+
return parts.length > 0 ? parts : void 0;
|
|
31
32
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
message.includes('eio') ||
|
|
39
|
-
message.includes('broken pipe') ||
|
|
40
|
-
message.includes('stream destroyed') ||
|
|
41
|
-
message.includes('write after end'));
|
|
33
|
+
function parsePort(value, fallback) {
|
|
34
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
35
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
return fallback;
|
|
42
39
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
40
|
+
function resolvePort() {
|
|
41
|
+
const portFlagIndex = process.argv.findIndex((arg) => arg === "--port");
|
|
42
|
+
if (portFlagIndex >= 0) {
|
|
43
|
+
return parsePort(process.argv[portFlagIndex + 1], DEFAULT_PORT);
|
|
44
|
+
}
|
|
45
|
+
return parsePort(process.env.PORT, DEFAULT_PORT);
|
|
50
46
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
function resolveHost() {
|
|
48
|
+
const explicitHost = process.env.HOST?.trim();
|
|
49
|
+
if (explicitHost) {
|
|
50
|
+
return explicitHost;
|
|
51
|
+
}
|
|
52
|
+
if (process.env.PORT?.trim()) {
|
|
53
|
+
return "0.0.0.0";
|
|
54
|
+
}
|
|
55
|
+
return LOCAL_DEFAULT_HOST;
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
function buildCors(allowedOrigins) {
|
|
58
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
origin: allowedOrigins,
|
|
63
|
+
allowMethods: ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
64
|
+
allowHeaders: [
|
|
65
|
+
"Content-Type",
|
|
66
|
+
"Accept",
|
|
67
|
+
"Authorization",
|
|
68
|
+
"mcp-protocol-version",
|
|
69
|
+
"mcp-session-id",
|
|
70
|
+
"X-Proxy-Token",
|
|
71
|
+
"X-Target-URL"
|
|
72
|
+
],
|
|
73
|
+
exposeHeaders: ["mcp-session-id"]
|
|
74
|
+
};
|
|
70
75
|
}
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
function configureLogging() {
|
|
77
|
+
Logger.configure({
|
|
78
|
+
level: process.env.NODE_ENV === "production" ? "info" : "debug",
|
|
79
|
+
format: "minimal"
|
|
80
|
+
});
|
|
81
|
+
const debug = process.env.DEBUG?.trim();
|
|
82
|
+
if (debug === "2") {
|
|
83
|
+
Logger.setDebug(2);
|
|
84
|
+
} else if (debug) {
|
|
85
|
+
Logger.setDebug(1);
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
function normalizeOrigin(value, envName) {
|
|
89
|
+
try {
|
|
90
|
+
return new URL(value).origin;
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`${envName} must contain absolute URLs with protocol. Received: ${value}`);
|
|
93
|
+
}
|
|
76
94
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*/
|
|
84
|
-
function registerToolHandlers(srv) {
|
|
85
|
-
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
86
|
-
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
87
|
-
const { name, arguments: args } = request.params;
|
|
88
|
-
try {
|
|
89
|
-
return await executeTool(name, args, capabilities);
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
if (error instanceof McpError)
|
|
93
|
-
throw error;
|
|
94
|
-
const structuredError = classifyError(error);
|
|
95
|
-
console.error(`[MCP Server] Tool "${name}" error:`, {
|
|
96
|
-
code: structuredError.code,
|
|
97
|
-
message: structuredError.message,
|
|
98
|
-
retryable: structuredError.retryable,
|
|
99
|
-
});
|
|
100
|
-
return createToolErrorFromStructured(structuredError);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
95
|
+
function resolveAllowedOrigins() {
|
|
96
|
+
const explicitOrigins = parseCsvEnv(process.env.ALLOWED_ORIGINS);
|
|
97
|
+
if (explicitOrigins && explicitOrigins.length > 0) {
|
|
98
|
+
return explicitOrigins.map((origin) => normalizeOrigin(origin, "ALLOWED_ORIGINS"));
|
|
99
|
+
}
|
|
100
|
+
return void 0;
|
|
103
101
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
102
|
+
async function buildSessionConfig() {
|
|
103
|
+
const redisUrl = process.env.REDIS_URL?.trim();
|
|
104
|
+
if (!redisUrl) {
|
|
105
|
+
return {
|
|
106
|
+
sessionConfig: {
|
|
107
|
+
sessionStore: new InMemorySessionStore(),
|
|
108
|
+
streamManager: new InMemoryStreamManager()
|
|
109
|
+
},
|
|
110
|
+
cleanupFns: []
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const commandClient = createClient({ url: redisUrl });
|
|
114
|
+
const pubSubClient = commandClient.duplicate();
|
|
115
|
+
await Promise.all([commandClient.connect(), pubSubClient.connect()]);
|
|
116
|
+
return {
|
|
117
|
+
sessionConfig: {
|
|
118
|
+
sessionStore: new RedisSessionStore({
|
|
119
|
+
client: commandClient
|
|
120
|
+
}),
|
|
121
|
+
streamManager: new RedisStreamManager({
|
|
122
|
+
client: commandClient,
|
|
123
|
+
pubSubClient
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
cleanupFns: [
|
|
127
|
+
async () => {
|
|
128
|
+
await pubSubClient.quit();
|
|
129
|
+
},
|
|
130
|
+
async () => {
|
|
131
|
+
await commandClient.quit();
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
136
135
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
136
|
+
function buildHealthPayload(server, startedAt) {
|
|
137
|
+
const llm = getLLMHealth();
|
|
138
|
+
let workflowStateSize = null;
|
|
139
|
+
try {
|
|
140
|
+
const store = getWorkflowStateStore();
|
|
141
|
+
workflowStateSize = store.size?.() ?? null;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
status: "ok",
|
|
146
|
+
name: SERVER.NAME,
|
|
147
|
+
version: SERVER.VERSION,
|
|
148
|
+
transport: "http",
|
|
149
|
+
uptime_seconds: Math.floor((Date.now() - startedAt) / 1e3),
|
|
150
|
+
active_sessions: server.getActiveSessions().length,
|
|
151
|
+
workflow_state_size: workflowStateSize,
|
|
152
|
+
// LLM health — surfaced so capability-aware clients render degraded mode
|
|
153
|
+
// once at session start instead of parsing per-call footers.
|
|
154
|
+
llm_planner_ok: llm.lastPlannerOk,
|
|
155
|
+
llm_extractor_ok: llm.lastExtractorOk,
|
|
156
|
+
llm_planner_checked_at: llm.lastPlannerCheckedAt,
|
|
157
|
+
llm_extractor_checked_at: llm.lastExtractorCheckedAt,
|
|
158
|
+
llm_planner_error: llm.lastPlannerError,
|
|
159
|
+
llm_extractor_error: llm.lastExtractorError,
|
|
160
|
+
planner_configured: llm.plannerConfigured,
|
|
161
|
+
extractor_configured: llm.extractorConfigured,
|
|
162
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
163
|
+
};
|
|
164
164
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
process.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
gracefulShutdown(1).catch(() => process.exit(1));
|
|
186
|
-
});
|
|
187
|
-
// Handle unhandled promise rejections - MUST EXIT (Node v15+ behavior)
|
|
188
|
-
// Suppressing this risks memory leaks and corrupted state
|
|
189
|
-
process.on('unhandledRejection', (reason) => {
|
|
190
|
-
if (isBrokenPipeLikeError(reason)) {
|
|
191
|
-
exitOnBrokenPipe('unhandledRejection', reason);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (fatalHandlerInProgress) {
|
|
195
|
-
process.exit(1);
|
|
196
|
-
return;
|
|
165
|
+
async function main() {
|
|
166
|
+
configureLogging();
|
|
167
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
168
|
+
const host = resolveHost();
|
|
169
|
+
const port = resolvePort();
|
|
170
|
+
const baseUrl = process.env.MCP_URL?.trim() || void 0;
|
|
171
|
+
const allowedOrigins = resolveAllowedOrigins();
|
|
172
|
+
const { sessionConfig, cleanupFns } = await buildSessionConfig();
|
|
173
|
+
await configureWorkflowStateStore(process.env.REDIS_URL?.trim());
|
|
174
|
+
startupLogger.info(`Starting ${SERVER.NAME} v${SERVER.VERSION}`);
|
|
175
|
+
startupLogger.info(`Binding HTTP server to ${host}:${port}`);
|
|
176
|
+
if (allowedOrigins && allowedOrigins.length > 0) {
|
|
177
|
+
startupLogger.info(`Host validation enabled for origins: ${allowedOrigins.join(", ")}`);
|
|
178
|
+
} else if (isProduction) {
|
|
179
|
+
if (!baseUrl) {
|
|
180
|
+
startupLogger.error(
|
|
181
|
+
"Production mode requires ALLOWED_ORIGINS or MCP_URL to be set. Without host validation, the server is vulnerable to DNS rebinding attacks. Set ALLOWED_ORIGINS to the public deployment URL or custom domain."
|
|
182
|
+
);
|
|
183
|
+
process.exit(1);
|
|
197
184
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// Stdin disconnect detection
|
|
224
|
-
// The MCP SDK's StdioServerTransport does NOT listen for stdin 'close'/'end'.
|
|
225
|
-
// When the parent process disconnects (closes the pipe), stdin emits these
|
|
226
|
-
// events but nobody handles them — Node.js keeps polling the dead fd at 100%
|
|
227
|
-
// CPU. We fix this by detecting the disconnect and exiting cleanly.
|
|
228
|
-
// ============================================================================
|
|
229
|
-
process.stdin.on('close', () => {
|
|
230
|
-
safeStderrWrite(`[MCP Server] stdin closed (parent disconnected) at ${new Date().toISOString()}, shutting down`);
|
|
231
|
-
gracefulShutdown(0);
|
|
232
|
-
});
|
|
233
|
-
process.stdin.on('end', () => {
|
|
234
|
-
safeStderrWrite(`[MCP Server] stdin ended (parent disconnected) at ${new Date().toISOString()}, shutting down`);
|
|
235
|
-
gracefulShutdown(0);
|
|
236
|
-
});
|
|
237
|
-
// ============================================================================
|
|
238
|
-
// Start Server — STDIO (default) or HTTP Streamable (MCP_TRANSPORT=http)
|
|
239
|
-
// ============================================================================
|
|
240
|
-
const transportMode = (process.env.MCP_TRANSPORT || 'stdio').toLowerCase();
|
|
241
|
-
if (transportMode === 'http') {
|
|
242
|
-
// HTTP Streamable transport — stateful sessions over HTTP
|
|
243
|
-
const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
244
|
-
const { createServer: createHttpServer } = await import('node:http');
|
|
245
|
-
const { randomUUID } = await import('node:crypto');
|
|
246
|
-
const PORT = parseInt(process.env.MCP_PORT || String(DEFAULT_MCP_PORT), 10);
|
|
247
|
-
// Map of session ID → transport + server for multi-session support
|
|
248
|
-
const sessions = new Map();
|
|
249
|
-
/** Safely close a session's server, ignoring errors. */
|
|
250
|
-
async function closeSession(session, sessionId) {
|
|
251
|
-
sessions.delete(sessionId);
|
|
185
|
+
startupLogger.warn(
|
|
186
|
+
"Host validation is disabled because ALLOWED_ORIGINS is not set. MCP_URL is set, so the server will start \u2014 but set ALLOWED_ORIGINS for full origin protection."
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
startupLogger.info("Host validation disabled for local development");
|
|
190
|
+
}
|
|
191
|
+
const server = new MCPServer({
|
|
192
|
+
name: SERVER.NAME,
|
|
193
|
+
title: "Research Powerpack",
|
|
194
|
+
version: SERVER.VERSION,
|
|
195
|
+
description: SERVER.DESCRIPTION,
|
|
196
|
+
websiteUrl: WEBSITE_URL,
|
|
197
|
+
host,
|
|
198
|
+
baseUrl,
|
|
199
|
+
cors: buildCors(allowedOrigins),
|
|
200
|
+
allowedOrigins,
|
|
201
|
+
...sessionConfig
|
|
202
|
+
});
|
|
203
|
+
registerAllTools(server);
|
|
204
|
+
try {
|
|
205
|
+
const patched = server;
|
|
206
|
+
const original = patched.getServerForSession?.bind(server);
|
|
207
|
+
if (original) {
|
|
208
|
+
patched.getServerForSession = (sessionId) => {
|
|
209
|
+
const native = original(sessionId);
|
|
252
210
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
/** Handle POST /mcp — route to existing session or create a new one. */
|
|
267
|
-
async function handleMcpPost(req, res, sessionId) {
|
|
268
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
269
|
-
await sessions.get(sessionId).transport.handleRequest(req, res);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
if (sessionId) {
|
|
273
|
-
res.writeHead(400).end('Bad request — missing session ID');
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
// New session (initialization)
|
|
277
|
-
const sessionServer = new Server({ name: SERVER.NAME, version: SERVER.VERSION }, { capabilities: { tools: {}, logging: {} } });
|
|
278
|
-
const transport = new StreamableHTTPServerTransport({
|
|
279
|
-
sessionIdGenerator: () => randomUUID(),
|
|
280
|
-
onsessioninitialized: (id) => {
|
|
281
|
-
sessions.set(id, { transport, server: sessionServer });
|
|
282
|
-
console.error(`[HTTP] Session ${id} initialized`);
|
|
283
|
-
},
|
|
284
|
-
onsessionclosed: async (id) => {
|
|
285
|
-
const session = sessions.get(id);
|
|
286
|
-
if (session) {
|
|
287
|
-
await closeSession(session, id);
|
|
288
|
-
}
|
|
289
|
-
console.error(`[HTTP] Session ${id} closed`);
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
// Note: initLogger overwrites a global serverRef, so logs from all
|
|
293
|
-
// HTTP sessions route to the most-recently-initialized session.
|
|
294
|
-
// A true per-session logger is out of scope for this fix.
|
|
295
|
-
initLogger(sessionServer);
|
|
296
|
-
registerToolHandlers(sessionServer);
|
|
297
|
-
await sessionServer.connect(transport);
|
|
298
|
-
await transport.handleRequest(req, res);
|
|
299
|
-
}
|
|
300
|
-
/** Handle DELETE /mcp — terminate a session. */
|
|
301
|
-
async function handleMcpDelete(req, res, sessionId) {
|
|
302
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
303
|
-
const session = sessions.get(sessionId);
|
|
304
|
-
await session.transport.handleRequest(req, res);
|
|
305
|
-
await closeSession(session, sessionId);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
res.writeHead(404).end('Session not found');
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const httpServer = createHttpServer(async (req, res) => {
|
|
312
|
-
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
313
|
-
// Health check
|
|
314
|
-
if (url.pathname === '/health') {
|
|
315
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
316
|
-
res.end(JSON.stringify({ status: 'ok', name: SERVER.NAME, version: SERVER.VERSION }));
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
// MCP endpoint
|
|
320
|
-
if (url.pathname === '/mcp') {
|
|
321
|
-
const sessionId = req.headers['mcp-session-id'];
|
|
322
|
-
switch (req.method) {
|
|
323
|
-
case 'DELETE':
|
|
324
|
-
await handleMcpDelete(req, res, sessionId);
|
|
325
|
-
return;
|
|
326
|
-
case 'GET':
|
|
327
|
-
await handleMcpGet(req, res, sessionId);
|
|
328
|
-
return;
|
|
329
|
-
case 'POST':
|
|
330
|
-
await handleMcpPost(req, res, sessionId);
|
|
331
|
-
return;
|
|
332
|
-
default:
|
|
333
|
-
res.writeHead(405).end('Method not allowed');
|
|
334
|
-
return;
|
|
211
|
+
const llm = getLLMHealth();
|
|
212
|
+
native.server?.registerCapabilities?.({
|
|
213
|
+
experimental: {
|
|
214
|
+
research_powerpack: {
|
|
215
|
+
planner_available: llm.plannerConfigured,
|
|
216
|
+
extractor_available: llm.extractorConfigured,
|
|
217
|
+
planner_model: process.env.LLM_MODEL ?? process.env.LLM_EXTRACTION_MODEL ?? null,
|
|
218
|
+
extractor_model: process.env.LLM_MODEL ?? process.env.LLM_EXTRACTION_MODEL ?? null,
|
|
219
|
+
// Tools that require start-research to bootstrap the session first.
|
|
220
|
+
requires_bootstrap: ["web-search", "scrape-links", "get-reddit-post"]
|
|
221
|
+
}
|
|
335
222
|
}
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
336
225
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
httpServer.listen(PORT, () => {
|
|
340
|
-
console.error(`🚀 ${SERVER.NAME} v${SERVER.VERSION} listening on http://localhost:${PORT}/mcp`);
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
// STDIO transport (default)
|
|
345
|
-
const transport = new StdioServerTransport();
|
|
346
|
-
try {
|
|
347
|
-
await server.connect(transport);
|
|
348
|
-
console.error(`🚀 ${SERVER.NAME} v${SERVER.VERSION} ready (stdio)`);
|
|
226
|
+
return native;
|
|
227
|
+
};
|
|
349
228
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
229
|
+
} catch (err) {
|
|
230
|
+
startupLogger.warn(`Could not patch session-server factory: ${String(err)}`);
|
|
231
|
+
}
|
|
232
|
+
const startedAt = Date.now();
|
|
233
|
+
server.get("/health", (c) => c.json(buildHealthPayload(server, startedAt)));
|
|
234
|
+
server.get("/healthz", (c) => c.json(buildHealthPayload(server, startedAt)));
|
|
235
|
+
const resourceBaseUrl = baseUrl ?? `http://${host}:${port}`;
|
|
236
|
+
server.get(
|
|
237
|
+
"/.well-known/oauth-protected-resource",
|
|
238
|
+
(c) => c.json({ resource: resourceBaseUrl })
|
|
239
|
+
);
|
|
240
|
+
server.get(
|
|
241
|
+
"/.well-known/oauth-protected-resource/mcp",
|
|
242
|
+
(c) => c.json({ resource: `${resourceBaseUrl}/mcp` })
|
|
243
|
+
);
|
|
244
|
+
server.resource(
|
|
245
|
+
{
|
|
246
|
+
name: "server-health",
|
|
247
|
+
uri: "health://status",
|
|
248
|
+
description: "Current server health, uptime, and active MCP session count.",
|
|
249
|
+
mimeType: "application/json"
|
|
250
|
+
},
|
|
251
|
+
async () => object(buildHealthPayload(server, startedAt))
|
|
252
|
+
);
|
|
253
|
+
let isShuttingDown = false;
|
|
254
|
+
async function shutdown(signal, exitCode) {
|
|
255
|
+
if (isShuttingDown) return;
|
|
256
|
+
isShuttingDown = true;
|
|
257
|
+
const forceExit = setTimeout(() => {
|
|
258
|
+
startupLogger.error(`Forced exit after ${SHUTDOWN_TIMEOUT_MS}ms (${signal})`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
261
|
+
try {
|
|
262
|
+
startupLogger.warn(`Shutdown signal received: ${signal}`);
|
|
263
|
+
await server.close();
|
|
264
|
+
await closeWorkflowStateStore();
|
|
265
|
+
for (const cleanupFn of cleanupFns) {
|
|
266
|
+
await cleanupFn();
|
|
267
|
+
}
|
|
268
|
+
clearTimeout(forceExit);
|
|
269
|
+
process.exit(exitCode);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
clearTimeout(forceExit);
|
|
272
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
273
|
+
startupLogger.error(`Error while stopping server: ${message}`);
|
|
274
|
+
process.exit(1);
|
|
354
275
|
}
|
|
276
|
+
}
|
|
277
|
+
process.on("SIGTERM", () => {
|
|
278
|
+
void shutdown("SIGTERM", 0);
|
|
279
|
+
});
|
|
280
|
+
process.on("SIGINT", () => {
|
|
281
|
+
void shutdown("SIGINT", 0);
|
|
282
|
+
});
|
|
283
|
+
process.on("uncaughtException", (error) => {
|
|
284
|
+
startupLogger.error(`Uncaught exception: ${error.stack ?? error.message}`);
|
|
285
|
+
void shutdown("uncaughtException", 1);
|
|
286
|
+
});
|
|
287
|
+
process.on("unhandledRejection", (reason) => {
|
|
288
|
+
startupLogger.error(`Unhandled rejection: ${String(reason)}`);
|
|
289
|
+
void shutdown("unhandledRejection", 1);
|
|
290
|
+
});
|
|
291
|
+
await server.listen(port);
|
|
292
|
+
startupLogger.info(`${SERVER.NAME} v${SERVER.VERSION} listening on http://${host}:${port}/mcp`);
|
|
355
293
|
}
|
|
356
|
-
|
|
294
|
+
void main().catch((error) => {
|
|
295
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
296
|
+
startupLogger.error(`Server failed to start: ${message}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
});
|
|
299
|
+
//# sourceMappingURL=index.js.map
|