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.
Files changed (188) hide show
  1. package/README.md +116 -208
  2. package/dist/index.js +280 -337
  3. package/dist/index.js.map +7 -1
  4. package/dist/mcp-use.json +7 -0
  5. package/dist/src/clients/reddit.js +278 -0
  6. package/dist/src/clients/reddit.js.map +7 -0
  7. package/dist/src/clients/scraper.js +326 -0
  8. package/dist/src/clients/scraper.js.map +7 -0
  9. package/dist/src/clients/search.js +217 -0
  10. package/dist/src/clients/search.js.map +7 -0
  11. package/dist/src/config/index.js +138 -0
  12. package/dist/src/config/index.js.map +7 -0
  13. package/dist/src/prompts/deep-research.js +41 -0
  14. package/dist/src/prompts/deep-research.js.map +7 -0
  15. package/dist/src/prompts/reddit-sentiment.js +47 -0
  16. package/dist/src/prompts/reddit-sentiment.js.map +7 -0
  17. package/dist/src/schemas/reddit.js +21 -0
  18. package/dist/src/schemas/reddit.js.map +7 -0
  19. package/dist/src/schemas/scrape-links.js +26 -0
  20. package/dist/src/schemas/scrape-links.js.map +7 -0
  21. package/dist/src/schemas/start-research.js +17 -0
  22. package/dist/src/schemas/start-research.js.map +7 -0
  23. package/dist/src/schemas/web-search.js +53 -0
  24. package/dist/src/schemas/web-search.js.map +7 -0
  25. package/dist/src/services/llm-processor.js +675 -0
  26. package/dist/src/services/llm-processor.js.map +7 -0
  27. package/dist/src/services/markdown-cleaner.js +62 -0
  28. package/dist/src/services/markdown-cleaner.js.map +7 -0
  29. package/dist/src/services/workflow-state.js +116 -0
  30. package/dist/src/services/workflow-state.js.map +7 -0
  31. package/dist/src/tools/mcp-helpers.js +50 -0
  32. package/dist/src/tools/mcp-helpers.js.map +7 -0
  33. package/dist/src/tools/reddit.js +277 -0
  34. package/dist/src/tools/reddit.js.map +7 -0
  35. package/dist/src/tools/registry.js +18 -0
  36. package/dist/src/tools/registry.js.map +7 -0
  37. package/dist/src/tools/scrape.js +334 -0
  38. package/dist/src/tools/scrape.js.map +7 -0
  39. package/dist/src/tools/search.js +423 -0
  40. package/dist/src/tools/search.js.map +7 -0
  41. package/dist/src/tools/start-research.js +199 -0
  42. package/dist/src/tools/start-research.js.map +7 -0
  43. package/dist/src/tools/utils.js +29 -0
  44. package/dist/src/tools/utils.js.map +7 -0
  45. package/dist/src/utils/bootstrap-guard.js +27 -0
  46. package/dist/src/utils/bootstrap-guard.js.map +7 -0
  47. package/dist/src/utils/concurrency.js +62 -0
  48. package/dist/src/utils/concurrency.js.map +7 -0
  49. package/dist/src/utils/content-extractor.js +61 -0
  50. package/dist/src/utils/content-extractor.js.map +7 -0
  51. package/dist/src/utils/errors.js +211 -0
  52. package/dist/src/utils/errors.js.map +7 -0
  53. package/dist/src/utils/logger.js +25 -0
  54. package/dist/src/utils/logger.js.map +7 -0
  55. package/dist/src/utils/markdown-formatter.js +15 -0
  56. package/dist/src/utils/markdown-formatter.js.map +7 -0
  57. package/dist/src/utils/reddit-keyword-guard.js +29 -0
  58. package/dist/src/utils/reddit-keyword-guard.js.map +7 -0
  59. package/dist/src/utils/response.js +81 -0
  60. package/dist/src/utils/response.js.map +7 -0
  61. package/dist/src/utils/retry.js +13 -0
  62. package/dist/src/utils/retry.js.map +7 -0
  63. package/dist/src/utils/sanitize.js +10 -0
  64. package/dist/src/utils/sanitize.js.map +7 -0
  65. package/dist/src/utils/source-type.js +41 -0
  66. package/dist/src/utils/source-type.js.map +7 -0
  67. package/dist/src/utils/url-aggregator.js +227 -0
  68. package/dist/src/utils/url-aggregator.js.map +7 -0
  69. package/dist/src/utils/workflow-key.js +14 -0
  70. package/dist/src/utils/workflow-key.js.map +7 -0
  71. package/dist/src/version.js +32 -0
  72. package/dist/src/version.js.map +7 -0
  73. package/package.json +33 -28
  74. package/dist/clients/reddit.d.ts +0 -69
  75. package/dist/clients/reddit.d.ts.map +0 -1
  76. package/dist/clients/reddit.js +0 -369
  77. package/dist/clients/reddit.js.map +0 -1
  78. package/dist/clients/research.d.ts +0 -67
  79. package/dist/clients/research.d.ts.map +0 -1
  80. package/dist/clients/research.js +0 -290
  81. package/dist/clients/research.js.map +0 -1
  82. package/dist/clients/scraper.d.ts +0 -72
  83. package/dist/clients/scraper.d.ts.map +0 -1
  84. package/dist/clients/scraper.js +0 -338
  85. package/dist/clients/scraper.js.map +0 -1
  86. package/dist/clients/search.d.ts +0 -57
  87. package/dist/clients/search.d.ts.map +0 -1
  88. package/dist/clients/search.js +0 -223
  89. package/dist/clients/search.js.map +0 -1
  90. package/dist/config/index.d.ts +0 -78
  91. package/dist/config/index.d.ts.map +0 -1
  92. package/dist/config/index.js +0 -201
  93. package/dist/config/index.js.map +0 -1
  94. package/dist/config/loader.d.ts +0 -40
  95. package/dist/config/loader.d.ts.map +0 -1
  96. package/dist/config/loader.js +0 -322
  97. package/dist/config/loader.js.map +0 -1
  98. package/dist/config/types.d.ts +0 -81
  99. package/dist/config/types.d.ts.map +0 -1
  100. package/dist/config/types.js +0 -6
  101. package/dist/config/types.js.map +0 -1
  102. package/dist/config/yaml/tools.yaml +0 -146
  103. package/dist/index.d.ts +0 -7
  104. package/dist/index.d.ts.map +0 -1
  105. package/dist/schemas/deep-research.d.ts +0 -64
  106. package/dist/schemas/deep-research.d.ts.map +0 -1
  107. package/dist/schemas/deep-research.js +0 -224
  108. package/dist/schemas/deep-research.js.map +0 -1
  109. package/dist/schemas/scrape-links.d.ts +0 -32
  110. package/dist/schemas/scrape-links.d.ts.map +0 -1
  111. package/dist/schemas/scrape-links.js +0 -34
  112. package/dist/schemas/scrape-links.js.map +0 -1
  113. package/dist/schemas/web-search.d.ts +0 -22
  114. package/dist/schemas/web-search.d.ts.map +0 -1
  115. package/dist/schemas/web-search.js +0 -21
  116. package/dist/schemas/web-search.js.map +0 -1
  117. package/dist/services/file-attachment.d.ts +0 -30
  118. package/dist/services/file-attachment.d.ts.map +0 -1
  119. package/dist/services/file-attachment.js +0 -207
  120. package/dist/services/file-attachment.js.map +0 -1
  121. package/dist/services/llm-processor.d.ts +0 -29
  122. package/dist/services/llm-processor.d.ts.map +0 -1
  123. package/dist/services/llm-processor.js +0 -244
  124. package/dist/services/llm-processor.js.map +0 -1
  125. package/dist/services/markdown-cleaner.d.ts +0 -8
  126. package/dist/services/markdown-cleaner.d.ts.map +0 -1
  127. package/dist/services/markdown-cleaner.js +0 -74
  128. package/dist/services/markdown-cleaner.js.map +0 -1
  129. package/dist/tools/definitions.d.ts +0 -16
  130. package/dist/tools/definitions.d.ts.map +0 -1
  131. package/dist/tools/definitions.js +0 -17
  132. package/dist/tools/definitions.js.map +0 -1
  133. package/dist/tools/reddit.d.ts +0 -14
  134. package/dist/tools/reddit.d.ts.map +0 -1
  135. package/dist/tools/reddit.js +0 -265
  136. package/dist/tools/reddit.js.map +0 -1
  137. package/dist/tools/registry.d.ts +0 -71
  138. package/dist/tools/registry.d.ts.map +0 -1
  139. package/dist/tools/registry.js +0 -252
  140. package/dist/tools/registry.js.map +0 -1
  141. package/dist/tools/research.d.ts +0 -14
  142. package/dist/tools/research.d.ts.map +0 -1
  143. package/dist/tools/research.js +0 -196
  144. package/dist/tools/research.js.map +0 -1
  145. package/dist/tools/scrape.d.ts +0 -14
  146. package/dist/tools/scrape.d.ts.map +0 -1
  147. package/dist/tools/scrape.js +0 -234
  148. package/dist/tools/scrape.js.map +0 -1
  149. package/dist/tools/search.d.ts +0 -10
  150. package/dist/tools/search.d.ts.map +0 -1
  151. package/dist/tools/search.js +0 -158
  152. package/dist/tools/search.js.map +0 -1
  153. package/dist/tools/utils.d.ts +0 -105
  154. package/dist/tools/utils.d.ts.map +0 -1
  155. package/dist/tools/utils.js +0 -159
  156. package/dist/tools/utils.js.map +0 -1
  157. package/dist/utils/concurrency.d.ts +0 -28
  158. package/dist/utils/concurrency.d.ts.map +0 -1
  159. package/dist/utils/concurrency.js +0 -92
  160. package/dist/utils/concurrency.js.map +0 -1
  161. package/dist/utils/errors.d.ts +0 -95
  162. package/dist/utils/errors.d.ts.map +0 -1
  163. package/dist/utils/errors.js +0 -390
  164. package/dist/utils/errors.js.map +0 -1
  165. package/dist/utils/logger.d.ts +0 -39
  166. package/dist/utils/logger.d.ts.map +0 -1
  167. package/dist/utils/logger.js +0 -57
  168. package/dist/utils/logger.js.map +0 -1
  169. package/dist/utils/markdown-formatter.d.ts +0 -5
  170. package/dist/utils/markdown-formatter.d.ts.map +0 -1
  171. package/dist/utils/markdown-formatter.js +0 -15
  172. package/dist/utils/markdown-formatter.js.map +0 -1
  173. package/dist/utils/response.d.ts +0 -93
  174. package/dist/utils/response.d.ts.map +0 -1
  175. package/dist/utils/response.js +0 -170
  176. package/dist/utils/response.js.map +0 -1
  177. package/dist/utils/retry.d.ts +0 -43
  178. package/dist/utils/retry.d.ts.map +0 -1
  179. package/dist/utils/retry.js +0 -57
  180. package/dist/utils/retry.js.map +0 -1
  181. package/dist/utils/url-aggregator.d.ts +0 -90
  182. package/dist/utils/url-aggregator.d.ts.map +0 -1
  183. package/dist/utils/url-aggregator.js +0 -538
  184. package/dist/utils/url-aggregator.js.map +0 -1
  185. package/dist/version.d.ts +0 -29
  186. package/dist/version.d.ts.map +0 -1
  187. package/dist/version.js +0 -55
  188. package/dist/version.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,356 +1,299 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Research Powerpack MCP Server
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
- function extractErrorMessage(error) {
28
- if (error instanceof Error)
29
- return error.message;
30
- return typeof error === 'string' ? error : String(error);
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 isBrokenPipeLikeError(error) {
33
- const code = extractErrorCode(error);
34
- if (code && BROKEN_PIPE_ERROR_CODES.has(code))
35
- return true;
36
- const message = extractErrorMessage(error).toLowerCase();
37
- return (message.includes('epipe') ||
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 safeStderrWrite(line) {
44
- try {
45
- process.stderr.write(`${line}\n`);
46
- }
47
- catch {
48
- // Swallow stderr failures while shutting down from stream errors.
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
- let streamExitInProgress = false;
52
- let fatalHandlerInProgress = false;
53
- function exitOnBrokenPipe(source, error) {
54
- if (streamExitInProgress || !isBrokenPipeLikeError(error))
55
- return;
56
- streamExitInProgress = true;
57
- safeStderrWrite(`[MCP Server] ${source} broken pipe at ${new Date().toISOString()}, exiting`);
58
- process.exit(fatalHandlerInProgress ? 1 : 0);
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
- // Install stream guards early (before startup logs) to avoid orphaned hot loops.
61
- process.stdout.on('error', (err) => exitOnBrokenPipe('stdout', err));
62
- process.stderr.on('error', (err) => exitOnBrokenPipe('stderr', err));
63
- // ============================================================================
64
- // Capability Detection (uses registry for tool capability mapping)
65
- // ============================================================================
66
- const capabilities = getCapabilities();
67
- const { enabled: enabledTools, disabled: disabledTools } = getToolCapabilities();
68
- if (enabledTools.length > 0) {
69
- console.error(`✅ Enabled tools: ${enabledTools.join(', ')}`);
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
- if (disabledTools.length > 0) {
72
- console.error(`⚠️ Disabled tools (missing ENV): ${disabledTools.join(', ')}`);
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
- if (capabilities.scraping && !capabilities.llmExtraction) {
75
- console.error(`ℹ️ scrape_links: AI extraction (use_llm) disabled - set OPENROUTER_API_KEY to enable`);
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
- // Server Setup
79
- // ============================================================================
80
- /**
81
- * Register shared tool handlers on any Server instance.
82
- * Used by both STDIO and HTTP session servers to avoid duplication.
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
- const server = new Server({ name: SERVER.NAME, version: SERVER.VERSION }, { capabilities: { tools: {}, logging: {} } });
105
- initLogger(server);
106
- registerToolHandlers(server);
107
- // ============================================================================
108
- // Global Error Handlers - MUST EXIT on fatal errors per Node.js best practices
109
- // See: https://nodejs.org/api/process.html#warning-using-uncaughtexception-correctly
110
- // ============================================================================
111
- // Track shutdown state to prevent double shutdown
112
- let isShuttingDown = false;
113
- /**
114
- * Graceful shutdown handler - closes server and exits
115
- * @param exitCode - Exit code (0 for clean shutdown, 1 for error)
116
- */
117
- async function gracefulShutdown(exitCode) {
118
- if (isShuttingDown)
119
- return;
120
- isShuttingDown = true;
121
- try {
122
- await server.close();
123
- safeStderrWrite(`[MCP Server] Server closed at ${new Date().toISOString()}`);
124
- }
125
- catch (closeError) {
126
- if (isBrokenPipeLikeError(closeError)) {
127
- // Preserve caller intent: fatal paths should still exit non-zero.
128
- process.exit(exitCode);
129
- return;
130
- }
131
- safeStderrWrite(`[MCP Server] Error closing server: ${safeErrorString(closeError)}`);
132
- }
133
- finally {
134
- process.exit(exitCode);
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
- * Safely extract error information without triggering another exception
139
- * Prevents infinite loops when error objects have problematic getters
140
- */
141
- function safeErrorString(error) {
142
- try {
143
- if (error instanceof Error) {
144
- // Try to get message and stack safely
145
- const message = String(error.message || 'Unknown error');
146
- try {
147
- const stack = String(error.stack || '');
148
- if (!stack) {
149
- return message;
150
- }
151
- // Avoid duplicating the message when the stack already includes it
152
- return stack.includes(message) ? stack : `${message}\n${stack}`;
153
- }
154
- catch {
155
- return message; // Stack serialization failed, just return message
156
- }
157
- }
158
- return String(error);
159
- }
160
- catch {
161
- // Even String() can fail on some objects
162
- return '[Error: Unable to serialize error object]';
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
- // Handle uncaught exceptions - MUST EXIT per Node.js docs
166
- // The VM is in an unstable state after uncaught exception
167
- process.on('uncaughtException', (error) => {
168
- if (isBrokenPipeLikeError(error)) {
169
- exitOnBrokenPipe('uncaughtException', error);
170
- return;
171
- }
172
- if (fatalHandlerInProgress) {
173
- process.exit(1);
174
- return;
175
- }
176
- fatalHandlerInProgress = true;
177
- try {
178
- safeStderrWrite(`[MCP Server] FATAL uncaughtException at ${new Date().toISOString()}:`);
179
- safeStderrWrite(safeErrorString(error));
180
- }
181
- catch {
182
- // Even logging failed - just exit
183
- safeStderrWrite('[MCP Server] FATAL uncaughtException (unable to log details)');
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
- fatalHandlerInProgress = true;
199
- try {
200
- const error = classifyError(reason);
201
- safeStderrWrite(`[MCP Server] FATAL unhandledRejection at ${new Date().toISOString()}:`);
202
- safeStderrWrite(` Message: ${error.message}`);
203
- safeStderrWrite(` Code: ${error.code}`);
204
- }
205
- catch {
206
- // classifyError or logging failed, use safeErrorString as fallback
207
- safeStderrWrite('[MCP Server] FATAL unhandledRejection (unable to classify error):');
208
- safeStderrWrite(safeErrorString(reason));
209
- }
210
- gracefulShutdown(1).catch(() => process.exit(1));
211
- });
212
- // Handle SIGTERM gracefully (Docker/Kubernetes stop signal)
213
- process.on('SIGTERM', () => {
214
- safeStderrWrite(`[MCP Server] Received SIGTERM at ${new Date().toISOString()}, shutting down gracefully`);
215
- gracefulShutdown(0);
216
- });
217
- // Handle SIGINT gracefully (Ctrl+C) - use once() to prevent double-fire
218
- process.once('SIGINT', () => {
219
- safeStderrWrite(`[MCP Server] Received SIGINT at ${new Date().toISOString()}, shutting down gracefully`);
220
- gracefulShutdown(0);
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
- await session.server.close();
254
- }
255
- catch { /* ignore close errors */ }
256
- }
257
- /** Handle GET /mcp — resume existing session's SSE stream. */
258
- async function handleMcpGet(req, res, sessionId) {
259
- if (sessionId && sessions.has(sessionId)) {
260
- await sessions.get(sessionId).transport.handleRequest(req, res);
261
- }
262
- else {
263
- res.writeHead(400).end('Bad request — missing session ID');
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
- res.writeHead(404).end('Not found');
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
- catch (error) {
351
- const err = classifyError(error);
352
- console.error(`[MCP Server] Failed to start: ${err.message}`);
353
- process.exit(1);
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
- //# sourceMappingURL=index.js.map
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