@j0hanz/superfetch 2.5.2 → 2.6.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 +356 -223
- package/dist/assets/logo.svg +24837 -24835
- package/dist/cache.d.ts +28 -20
- package/dist/cache.js +292 -514
- package/dist/config.d.ts +41 -7
- package/dist/config.js +298 -148
- package/dist/crypto.js +25 -12
- package/dist/dom-noise-removal.js +379 -421
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +25 -8
- package/dist/fetch.d.ts +18 -16
- package/dist/fetch.js +1132 -526
- package/dist/host-normalization.js +40 -10
- package/dist/http-native.js +628 -287
- package/dist/index.js +67 -7
- package/dist/instructions.md +44 -30
- package/dist/ip-blocklist.d.ts +8 -0
- package/dist/ip-blocklist.js +65 -0
- package/dist/json.js +14 -9
- package/dist/language-detection.d.ts +2 -11
- package/dist/language-detection.js +289 -280
- package/dist/markdown-cleanup.d.ts +0 -1
- package/dist/markdown-cleanup.js +391 -429
- package/dist/mcp-validator.js +4 -2
- package/dist/mcp.js +184 -135
- package/dist/observability.js +89 -21
- package/dist/resources.js +16 -6
- package/dist/server-tuning.d.ts +2 -0
- package/dist/server-tuning.js +25 -23
- package/dist/session.d.ts +1 -0
- package/dist/session.js +41 -33
- package/dist/tasks.d.ts +2 -0
- package/dist/tasks.js +91 -9
- package/dist/timer-utils.d.ts +5 -0
- package/dist/timer-utils.js +20 -0
- package/dist/tools.d.ts +28 -5
- package/dist/tools.js +317 -183
- package/dist/transform-types.d.ts +5 -1
- package/dist/transform.d.ts +3 -2
- package/dist/transform.js +1138 -421
- package/dist/type-guards.d.ts +1 -0
- package/dist/type-guards.js +7 -0
- package/dist/workers/transform-child.d.ts +1 -0
- package/dist/workers/transform-child.js +118 -0
- package/dist/workers/transform-worker.js +87 -78
- package/package.json +21 -13
package/dist/mcp-validator.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
// --- Validation ---
|
|
3
3
|
const paramsSchema = z.looseObject({});
|
|
4
|
-
const mcpRequestSchema = z
|
|
4
|
+
const mcpRequestSchema = z
|
|
5
|
+
.object({
|
|
5
6
|
jsonrpc: z.literal('2.0'),
|
|
6
7
|
method: z.string().min(1),
|
|
7
8
|
id: z.union([z.string(), z.number(), z.null()]).optional(),
|
|
8
9
|
params: paramsSchema.optional(),
|
|
9
|
-
})
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
10
12
|
export function isJsonRpcBatchRequest(body) {
|
|
11
13
|
return Array.isArray(body);
|
|
12
14
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import process from 'node:process';
|
|
2
4
|
import { z } from 'zod';
|
|
3
5
|
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
7
|
import { CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
8
|
import { registerCachedContentResource } from './cache.js';
|
|
7
9
|
import { config } from './config.js';
|
|
8
|
-
import { logError, logInfo, setMcpServer } from './observability.js';
|
|
10
|
+
import { logError, logInfo, runWithRequestContext, setMcpServer, } from './observability.js';
|
|
9
11
|
import { registerConfigResource } from './resources.js';
|
|
10
12
|
import { taskManager } from './tasks.js';
|
|
11
|
-
import { FETCH_URL_TOOL_NAME, fetchUrlToolHandler, registerTools, } from './tools.js';
|
|
13
|
+
import { FETCH_URL_TOOL_NAME, fetchUrlInputSchema, fetchUrlToolHandler, registerTools, } from './tools.js';
|
|
12
14
|
import { shutdownTransformWorkerPool } from './transform.js';
|
|
13
15
|
import { isObject } from './type-guards.js';
|
|
14
|
-
|
|
16
|
+
/* -------------------------------------------------------------------------------------------------
|
|
17
|
+
* Icons + server info
|
|
18
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
19
|
+
async function getLocalIcons(signal) {
|
|
15
20
|
try {
|
|
16
21
|
const iconPath = new URL('../assets/logo.svg', import.meta.url);
|
|
17
|
-
const
|
|
22
|
+
const base64 = await readFile(iconPath, {
|
|
23
|
+
encoding: 'base64',
|
|
24
|
+
...(signal ? { signal } : {}),
|
|
25
|
+
});
|
|
18
26
|
return [
|
|
19
27
|
{
|
|
20
|
-
src: `data:image/svg+xml;base64,${
|
|
28
|
+
src: `data:image/svg+xml;base64,${base64}`,
|
|
21
29
|
mimeType: 'image/svg+xml',
|
|
22
30
|
sizes: ['any'],
|
|
23
31
|
},
|
|
@@ -27,18 +35,10 @@ async function getLocalIcons() {
|
|
|
27
35
|
return undefined;
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
|
-
async function createServerInfo() {
|
|
31
|
-
const localIcons = await getLocalIcons();
|
|
32
|
-
return {
|
|
33
|
-
name: config.server.name,
|
|
34
|
-
version: config.server.version,
|
|
35
|
-
...(localIcons ? { icons: localIcons } : {}),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
38
|
function createServerCapabilities() {
|
|
39
39
|
return {
|
|
40
40
|
tools: { listChanged: false },
|
|
41
|
-
resources: { listChanged:
|
|
41
|
+
resources: { listChanged: true, subscribe: true },
|
|
42
42
|
logging: {},
|
|
43
43
|
tasks: {
|
|
44
44
|
list: {},
|
|
@@ -51,15 +51,25 @@ function createServerCapabilities() {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
async function createServerInstructions(serverVersion) {
|
|
54
|
+
async function createServerInstructions(serverVersion, signal) {
|
|
55
55
|
try {
|
|
56
|
-
const raw = await readFile(new URL('./instructions.md', import.meta.url),
|
|
56
|
+
const raw = await readFile(new URL('./instructions.md', import.meta.url), {
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
...(signal ? { signal } : {}),
|
|
59
|
+
});
|
|
57
60
|
return raw.replaceAll('{{SERVER_VERSION}}', serverVersion).trim();
|
|
58
61
|
}
|
|
59
62
|
catch {
|
|
60
63
|
return `Instructions unavailable | ${serverVersion}`;
|
|
61
64
|
}
|
|
62
65
|
}
|
|
66
|
+
function createServerInfo(icons) {
|
|
67
|
+
return {
|
|
68
|
+
name: config.server.name,
|
|
69
|
+
version: config.server.version,
|
|
70
|
+
...(icons ? { icons } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
63
73
|
function registerInstructionsResource(server, instructions) {
|
|
64
74
|
server.registerResource('instructions', new ResourceTemplate('internal://instructions', { list: undefined }), {
|
|
65
75
|
title: `SuperFetch MCP | ${config.server.version}`,
|
|
@@ -75,7 +85,9 @@ function registerInstructionsResource(server, instructions) {
|
|
|
75
85
|
],
|
|
76
86
|
}));
|
|
77
87
|
}
|
|
78
|
-
|
|
88
|
+
/* -------------------------------------------------------------------------------------------------
|
|
89
|
+
* Tasks API schemas
|
|
90
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
79
91
|
const TaskGetSchema = z.object({
|
|
80
92
|
method: z.literal('tasks/get'),
|
|
81
93
|
params: z.object({ taskId: z.string() }),
|
|
@@ -96,53 +108,37 @@ const TaskResultSchema = z.object({
|
|
|
96
108
|
method: z.literal('tasks/result'),
|
|
97
109
|
params: z.object({ taskId: z.string() }),
|
|
98
110
|
});
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
const ExtendedCallToolRequestSchema = z.looseObject({
|
|
112
|
+
method: z.literal('tools/call'),
|
|
113
|
+
params: z.looseObject({
|
|
114
|
+
name: z.string().min(1),
|
|
115
|
+
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
116
|
+
task: z
|
|
117
|
+
.object({
|
|
118
|
+
ttl: z.number().optional(),
|
|
119
|
+
})
|
|
120
|
+
.optional(),
|
|
121
|
+
_meta: z
|
|
122
|
+
.looseObject({
|
|
123
|
+
progressToken: z.union([z.string(), z.number()]).optional(),
|
|
124
|
+
'io.modelcontextprotocol/related-task': z
|
|
125
|
+
.object({
|
|
126
|
+
taskId: z.string(),
|
|
127
|
+
})
|
|
128
|
+
.optional(),
|
|
129
|
+
})
|
|
130
|
+
.optional(),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
function parseExtendedCallToolRequest(request) {
|
|
134
|
+
const parsed = ExtendedCallToolRequestSchema.safeParse(request);
|
|
135
|
+
if (parsed.success)
|
|
136
|
+
return parsed.data;
|
|
137
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid tool request');
|
|
101
138
|
}
|
|
102
139
|
function isRecord(value) {
|
|
103
140
|
return isObject(value);
|
|
104
141
|
}
|
|
105
|
-
function isValidTask(task) {
|
|
106
|
-
if (task === undefined)
|
|
107
|
-
return true;
|
|
108
|
-
if (!isRecord(task))
|
|
109
|
-
return false;
|
|
110
|
-
const { ttl } = task;
|
|
111
|
-
return ttl === undefined || typeof ttl === 'number';
|
|
112
|
-
}
|
|
113
|
-
function isValidMeta(meta) {
|
|
114
|
-
if (meta === undefined)
|
|
115
|
-
return true;
|
|
116
|
-
if (!isRecord(meta))
|
|
117
|
-
return false;
|
|
118
|
-
const { progressToken } = meta;
|
|
119
|
-
if (progressToken !== undefined &&
|
|
120
|
-
typeof progressToken !== 'string' &&
|
|
121
|
-
typeof progressToken !== 'number') {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
const related = meta['io.modelcontextprotocol/related-task'];
|
|
125
|
-
if (related === undefined)
|
|
126
|
-
return true;
|
|
127
|
-
if (!isRecord(related))
|
|
128
|
-
return false;
|
|
129
|
-
const { taskId } = related;
|
|
130
|
-
return typeof taskId === 'string';
|
|
131
|
-
}
|
|
132
|
-
function isExtendedCallToolRequest(request) {
|
|
133
|
-
if (!isRecord(request))
|
|
134
|
-
return false;
|
|
135
|
-
const { method, params } = request;
|
|
136
|
-
if (method !== 'tools/call')
|
|
137
|
-
return false;
|
|
138
|
-
if (!isRecord(params))
|
|
139
|
-
return false;
|
|
140
|
-
const { name, arguments: args, task, _meta, } = params;
|
|
141
|
-
return (isNonEmptyString(name) &&
|
|
142
|
-
(args === undefined || isRecord(args)) &&
|
|
143
|
-
isValidTask(task) &&
|
|
144
|
-
isValidMeta(_meta));
|
|
145
|
-
}
|
|
146
142
|
function resolveTaskOwnerKey(extra) {
|
|
147
143
|
if (extra?.sessionId)
|
|
148
144
|
return `session:${extra.sessionId}`;
|
|
@@ -165,10 +161,11 @@ function resolveToolCallContext(extra) {
|
|
|
165
161
|
return context;
|
|
166
162
|
}
|
|
167
163
|
function requireFetchUrlArgs(args) {
|
|
168
|
-
|
|
164
|
+
const parsed = fetchUrlInputSchema.safeParse(args);
|
|
165
|
+
if (!parsed.success) {
|
|
169
166
|
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for fetch-url');
|
|
170
167
|
}
|
|
171
|
-
return
|
|
168
|
+
return parsed.data;
|
|
172
169
|
}
|
|
173
170
|
function throwTaskNotFound() {
|
|
174
171
|
throw new McpError(ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
|
|
@@ -200,63 +197,92 @@ function buildCreateTaskResult(task) {
|
|
|
200
197
|
},
|
|
201
198
|
};
|
|
202
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Track in-flight task executions so `tasks/cancel` can actually abort work.
|
|
202
|
+
* This is intentionally local to this module (no new files, no global singletons elsewhere).
|
|
203
|
+
*/
|
|
204
|
+
const taskAbortControllers = new Map();
|
|
205
|
+
function attachAbortController(taskId) {
|
|
206
|
+
const existing = taskAbortControllers.get(taskId);
|
|
207
|
+
if (existing) {
|
|
208
|
+
// Defensive: should not happen, but avoid leaking the old controller.
|
|
209
|
+
taskAbortControllers.delete(taskId);
|
|
210
|
+
}
|
|
211
|
+
const controller = new AbortController();
|
|
212
|
+
taskAbortControllers.set(taskId, controller);
|
|
213
|
+
return controller;
|
|
214
|
+
}
|
|
215
|
+
function abortTaskExecution(taskId) {
|
|
216
|
+
const controller = taskAbortControllers.get(taskId);
|
|
217
|
+
if (!controller)
|
|
218
|
+
return;
|
|
219
|
+
controller.abort();
|
|
220
|
+
taskAbortControllers.delete(taskId);
|
|
221
|
+
}
|
|
222
|
+
function clearTaskExecution(taskId) {
|
|
223
|
+
taskAbortControllers.delete(taskId);
|
|
224
|
+
}
|
|
203
225
|
async function runFetchTaskExecution(params) {
|
|
204
226
|
const { taskId, args, meta, sendNotification } = params;
|
|
205
|
-
|
|
206
|
-
const controller =
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
return runWithRequestContext({ requestId: taskId, operationId: taskId }, async () => {
|
|
228
|
+
const controller = attachAbortController(taskId);
|
|
229
|
+
try {
|
|
230
|
+
const relatedMeta = buildRelatedTaskMeta(taskId, meta);
|
|
231
|
+
const result = await fetchUrlToolHandler(args, {
|
|
232
|
+
signal: controller.signal,
|
|
233
|
+
requestId: taskId, // Correlation
|
|
234
|
+
_meta: relatedMeta,
|
|
235
|
+
...(sendNotification ? { sendNotification } : {}),
|
|
236
|
+
});
|
|
237
|
+
const isToolError = isRecord(result) &&
|
|
238
|
+
typeof result.isError === 'boolean' &&
|
|
239
|
+
result.isError;
|
|
240
|
+
taskManager.updateTask(taskId, {
|
|
241
|
+
status: isToolError ? 'failed' : 'completed',
|
|
242
|
+
...(isToolError
|
|
243
|
+
? {
|
|
244
|
+
statusMessage: result
|
|
245
|
+
.structuredContent?.error ?? 'Tool execution failed',
|
|
246
|
+
}
|
|
247
|
+
: {}),
|
|
248
|
+
result,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
253
|
+
const errorPayload = error instanceof McpError
|
|
220
254
|
? {
|
|
221
|
-
|
|
222
|
-
|
|
255
|
+
code: error.code,
|
|
256
|
+
message: errorMessage,
|
|
257
|
+
data: error.data,
|
|
223
258
|
}
|
|
224
|
-
: {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
message: errorMessage,
|
|
239
|
-
};
|
|
240
|
-
taskManager.updateTask(taskId, {
|
|
241
|
-
status: 'failed',
|
|
242
|
-
statusMessage: errorMessage,
|
|
243
|
-
error: errorPayload,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
259
|
+
: {
|
|
260
|
+
code: ErrorCode.InternalError,
|
|
261
|
+
message: errorMessage,
|
|
262
|
+
};
|
|
263
|
+
taskManager.updateTask(taskId, {
|
|
264
|
+
status: 'failed',
|
|
265
|
+
statusMessage: errorMessage,
|
|
266
|
+
error: errorPayload,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
clearTaskExecution(taskId);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
246
273
|
}
|
|
247
274
|
function handleTaskToolCall(params, context) {
|
|
248
275
|
requireFetchUrlToolName(params.name);
|
|
249
276
|
const validArgs = requireFetchUrlArgs(params.arguments);
|
|
250
277
|
const task = taskManager.createTask(params.task?.ttl !== undefined ? { ttl: params.task.ttl } : undefined, 'Task started', context.ownerKey);
|
|
251
|
-
|
|
278
|
+
void runFetchTaskExecution({
|
|
252
279
|
taskId: task.taskId,
|
|
253
280
|
args: validArgs,
|
|
254
281
|
...(params._meta ? { meta: params._meta } : {}),
|
|
255
282
|
...(context.sendNotification
|
|
256
283
|
? { sendNotification: context.sendNotification }
|
|
257
284
|
: {}),
|
|
258
|
-
};
|
|
259
|
-
void runFetchTaskExecution(executionParams);
|
|
285
|
+
});
|
|
260
286
|
return buildCreateTaskResult({
|
|
261
287
|
taskId: task.taskId,
|
|
262
288
|
status: task.status,
|
|
@@ -269,9 +295,11 @@ function handleTaskToolCall(params, context) {
|
|
|
269
295
|
}
|
|
270
296
|
async function handleDirectToolCall(params, context) {
|
|
271
297
|
const args = requireFetchUrlArgs(params.arguments);
|
|
272
|
-
return fetchUrlToolHandler(
|
|
298
|
+
return fetchUrlToolHandler(args, {
|
|
273
299
|
...(context.signal ? { signal: context.signal } : {}),
|
|
274
|
-
...(context.requestId
|
|
300
|
+
...(context.requestId !== undefined
|
|
301
|
+
? { requestId: context.requestId }
|
|
302
|
+
: {}),
|
|
275
303
|
...(context.sendNotification
|
|
276
304
|
? { sendNotification: context.sendNotification }
|
|
277
305
|
: {}),
|
|
@@ -288,22 +316,31 @@ async function handleToolCallRequest(request, context) {
|
|
|
288
316
|
}
|
|
289
317
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${params.name}`);
|
|
290
318
|
}
|
|
319
|
+
/* -------------------------------------------------------------------------------------------------
|
|
320
|
+
* Register handlers
|
|
321
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
291
322
|
function registerTaskHandlers(server) {
|
|
292
323
|
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
293
324
|
const context = resolveToolCallContext(extra);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
return
|
|
325
|
+
const requestId = context.requestId !== undefined
|
|
326
|
+
? String(context.requestId)
|
|
327
|
+
: randomUUID();
|
|
328
|
+
const sessionId = extra?.sessionId;
|
|
329
|
+
return runWithRequestContext({
|
|
330
|
+
requestId,
|
|
331
|
+
operationId: requestId,
|
|
332
|
+
...(sessionId ? { sessionId } : {}),
|
|
333
|
+
}, () => {
|
|
334
|
+
const parsed = parseExtendedCallToolRequest(request);
|
|
335
|
+
return handleToolCallRequest(parsed, context);
|
|
336
|
+
});
|
|
299
337
|
});
|
|
300
338
|
server.server.setRequestHandler(TaskGetSchema, async (request, extra) => {
|
|
301
339
|
const { taskId } = request.params;
|
|
302
340
|
const ownerKey = resolveTaskOwnerKey(extra);
|
|
303
341
|
const task = taskManager.getTask(taskId, ownerKey);
|
|
304
|
-
if (!task)
|
|
342
|
+
if (!task)
|
|
305
343
|
throwTaskNotFound();
|
|
306
|
-
}
|
|
307
344
|
return Promise.resolve({
|
|
308
345
|
taskId: task.taskId,
|
|
309
346
|
status: task.status,
|
|
@@ -318,9 +355,8 @@ function registerTaskHandlers(server) {
|
|
|
318
355
|
const { taskId } = request.params;
|
|
319
356
|
const ownerKey = resolveTaskOwnerKey(extra);
|
|
320
357
|
const task = await taskManager.waitForTerminalTask(taskId, ownerKey, extra?.signal);
|
|
321
|
-
if (!task)
|
|
358
|
+
if (!task)
|
|
322
359
|
throwTaskNotFound();
|
|
323
|
-
}
|
|
324
360
|
if (task.status === 'failed') {
|
|
325
361
|
if (task.error) {
|
|
326
362
|
throw new McpError(task.error.code, task.error.message, task.error.data);
|
|
@@ -375,9 +411,10 @@ function registerTaskHandlers(server) {
|
|
|
375
411
|
const { taskId } = request.params;
|
|
376
412
|
const ownerKey = resolveTaskOwnerKey(extra);
|
|
377
413
|
const task = taskManager.cancelTask(taskId, ownerKey);
|
|
378
|
-
if (!task)
|
|
414
|
+
if (!task)
|
|
379
415
|
throwTaskNotFound();
|
|
380
|
-
|
|
416
|
+
// Make cancellation actionable: abort any in-flight execution.
|
|
417
|
+
abortTaskExecution(taskId);
|
|
381
418
|
return Promise.resolve({
|
|
382
419
|
taskId: task.taskId,
|
|
383
420
|
status: task.status,
|
|
@@ -389,15 +426,21 @@ function registerTaskHandlers(server) {
|
|
|
389
426
|
});
|
|
390
427
|
});
|
|
391
428
|
}
|
|
429
|
+
/* -------------------------------------------------------------------------------------------------
|
|
430
|
+
* Server lifecycle
|
|
431
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
392
432
|
export async function createMcpServer() {
|
|
393
|
-
const
|
|
394
|
-
const
|
|
433
|
+
const startupSignal = AbortSignal.timeout(5000);
|
|
434
|
+
const [instructions, localIcons] = await Promise.all([
|
|
435
|
+
createServerInstructions(config.server.version, startupSignal),
|
|
436
|
+
getLocalIcons(startupSignal),
|
|
437
|
+
]);
|
|
438
|
+
const serverInfo = createServerInfo(localIcons);
|
|
395
439
|
const server = new McpServer(serverInfo, {
|
|
396
440
|
capabilities: createServerCapabilities(),
|
|
397
441
|
instructions,
|
|
398
442
|
});
|
|
399
443
|
setMcpServer(server);
|
|
400
|
-
const localIcons = await getLocalIcons();
|
|
401
444
|
registerTools(server);
|
|
402
445
|
registerCachedContentResource(server, localIcons);
|
|
403
446
|
registerInstructionsResource(server, instructions);
|
|
@@ -410,19 +453,13 @@ function attachServerErrorHandler(server) {
|
|
|
410
453
|
logError('[MCP Error]', error instanceof Error ? error : { error });
|
|
411
454
|
};
|
|
412
455
|
}
|
|
413
|
-
function
|
|
456
|
+
async function shutdownServer(server, signal) {
|
|
414
457
|
process.stderr.write(`\n${signal} received, shutting down superFetch MCP server...\n`);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
.catch((err) => {
|
|
421
|
-
logError('Error during shutdown', err instanceof Error ? err : undefined);
|
|
422
|
-
})
|
|
423
|
-
.finally(() => {
|
|
424
|
-
process.exit(0);
|
|
425
|
-
});
|
|
458
|
+
// Ensure any in-flight tool executions are aborted promptly.
|
|
459
|
+
for (const taskId of taskAbortControllers.keys())
|
|
460
|
+
abortTaskExecution(taskId);
|
|
461
|
+
await shutdownTransformWorkerPool();
|
|
462
|
+
await server.close();
|
|
426
463
|
}
|
|
427
464
|
function createShutdownHandler(server) {
|
|
428
465
|
let shuttingDown = false;
|
|
@@ -437,7 +474,17 @@ function createShutdownHandler(server) {
|
|
|
437
474
|
}
|
|
438
475
|
shuttingDown = true;
|
|
439
476
|
initialSignal = signal;
|
|
440
|
-
|
|
477
|
+
Promise.resolve()
|
|
478
|
+
.then(() => shutdownServer(server, signal))
|
|
479
|
+
.catch((err) => {
|
|
480
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
481
|
+
logError('Error during shutdown', error);
|
|
482
|
+
process.exitCode = 1;
|
|
483
|
+
})
|
|
484
|
+
.finally(() => {
|
|
485
|
+
if (process.exitCode === undefined)
|
|
486
|
+
process.exitCode = 0;
|
|
487
|
+
});
|
|
441
488
|
};
|
|
442
489
|
}
|
|
443
490
|
function registerSignalHandlers(handler) {
|
|
@@ -454,8 +501,10 @@ async function connectStdioServer(server, transport) {
|
|
|
454
501
|
logInfo('superFetch MCP server running on stdio');
|
|
455
502
|
}
|
|
456
503
|
catch (error) {
|
|
457
|
-
|
|
458
|
-
|
|
504
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
505
|
+
throw new Error(`Failed to start stdio server: ${err.message}`, {
|
|
506
|
+
cause: err,
|
|
507
|
+
});
|
|
459
508
|
}
|
|
460
509
|
}
|
|
461
510
|
export async function startStdioServer() {
|
package/dist/observability.js
CHANGED
|
@@ -1,38 +1,69 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { inspect, stripVTControlCharacters } from 'node:util';
|
|
2
4
|
import { config } from './config.js';
|
|
3
|
-
const requestContext = new AsyncLocalStorage(
|
|
5
|
+
const requestContext = new AsyncLocalStorage({
|
|
6
|
+
name: 'requestContext',
|
|
7
|
+
});
|
|
4
8
|
let mcpServer;
|
|
9
|
+
let stderrAvailable = true;
|
|
10
|
+
process.stderr.on('error', () => {
|
|
11
|
+
stderrAvailable = false;
|
|
12
|
+
});
|
|
5
13
|
export function setMcpServer(server) {
|
|
6
14
|
mcpServer = server;
|
|
7
15
|
}
|
|
8
16
|
export function runWithRequestContext(context, fn) {
|
|
9
17
|
return requestContext.run(context, fn);
|
|
10
18
|
}
|
|
19
|
+
function getRequestContext() {
|
|
20
|
+
return requestContext.getStore();
|
|
21
|
+
}
|
|
11
22
|
export function getRequestId() {
|
|
12
|
-
return
|
|
23
|
+
return getRequestContext()?.requestId;
|
|
13
24
|
}
|
|
14
25
|
function getSessionId() {
|
|
15
|
-
return
|
|
26
|
+
return getRequestContext()?.sessionId;
|
|
16
27
|
}
|
|
17
28
|
export function getOperationId() {
|
|
18
|
-
return
|
|
29
|
+
return getRequestContext()?.operationId;
|
|
30
|
+
}
|
|
31
|
+
function isDebugEnabled() {
|
|
32
|
+
return config.logging.level === 'debug';
|
|
19
33
|
}
|
|
20
34
|
function buildContextMetadata() {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
35
|
+
const ctx = requestContext.getStore();
|
|
36
|
+
if (!ctx)
|
|
37
|
+
return undefined;
|
|
38
|
+
const { requestId, operationId, sessionId } = ctx;
|
|
39
|
+
const includeSession = sessionId && isDebugEnabled();
|
|
40
|
+
if (!requestId && !operationId && !includeSession)
|
|
41
|
+
return undefined;
|
|
42
|
+
const meta = {};
|
|
25
43
|
if (requestId)
|
|
26
|
-
|
|
27
|
-
if (sessionId && config.logging.level === 'debug')
|
|
28
|
-
contextMeta.sessionId = sessionId;
|
|
44
|
+
meta.requestId = requestId;
|
|
29
45
|
if (operationId)
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
meta.operationId = operationId;
|
|
47
|
+
if (includeSession)
|
|
48
|
+
meta.sessionId = sessionId;
|
|
49
|
+
return meta;
|
|
50
|
+
}
|
|
51
|
+
function mergeMetadata(meta) {
|
|
52
|
+
const contextMeta = buildContextMetadata();
|
|
53
|
+
const hasMeta = meta && Object.keys(meta).length > 0;
|
|
54
|
+
if (!contextMeta && !hasMeta)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (!contextMeta)
|
|
57
|
+
return meta;
|
|
58
|
+
if (!hasMeta)
|
|
59
|
+
return contextMeta;
|
|
60
|
+
return { ...contextMeta, ...meta };
|
|
32
61
|
}
|
|
33
62
|
function formatMetadata(meta) {
|
|
34
|
-
const merged =
|
|
35
|
-
|
|
63
|
+
const merged = mergeMetadata(meta);
|
|
64
|
+
if (!merged)
|
|
65
|
+
return '';
|
|
66
|
+
return ` ${inspect(merged, { breakLength: Infinity, colors: false, compact: true, sorted: true })}`;
|
|
36
67
|
}
|
|
37
68
|
function createTimestamp() {
|
|
38
69
|
return new Date().toISOString();
|
|
@@ -43,7 +74,7 @@ function formatLogEntry(level, message, meta) {
|
|
|
43
74
|
function shouldLog(level) {
|
|
44
75
|
// Debug logs only when LOG_LEVEL=debug
|
|
45
76
|
if (level === 'debug')
|
|
46
|
-
return
|
|
77
|
+
return isDebugEnabled();
|
|
47
78
|
// All other levels always log
|
|
48
79
|
return true;
|
|
49
80
|
}
|
|
@@ -60,18 +91,55 @@ function mapToMcpLevel(level) {
|
|
|
60
91
|
return 'info';
|
|
61
92
|
}
|
|
62
93
|
}
|
|
94
|
+
function safeWriteStderr(line) {
|
|
95
|
+
if (!stderrAvailable)
|
|
96
|
+
return;
|
|
97
|
+
if (process.stderr.destroyed || process.stderr.writableEnded) {
|
|
98
|
+
stderrAvailable = false;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
process.stderr.write(line);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Logging must never take down the process (e.g. EPIPE).
|
|
106
|
+
stderrAvailable = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
63
109
|
function writeLog(level, message, meta) {
|
|
64
110
|
if (!shouldLog(level))
|
|
65
111
|
return;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
112
|
+
const line = formatLogEntry(level, message, meta);
|
|
113
|
+
safeWriteStderr(`${stripVTControlCharacters(line)}\n`);
|
|
114
|
+
const server = mcpServer;
|
|
115
|
+
if (!server)
|
|
116
|
+
return;
|
|
117
|
+
const sessionId = getSessionId();
|
|
118
|
+
try {
|
|
119
|
+
server.server
|
|
70
120
|
.sendLoggingMessage({
|
|
71
121
|
level: mapToMcpLevel(level),
|
|
122
|
+
// Preserve existing behavior: MCP payload includes only message + provided meta (not ALS context meta).
|
|
72
123
|
data: meta ? { message, ...meta } : message,
|
|
73
124
|
}, sessionId)
|
|
74
|
-
.catch(() => {
|
|
125
|
+
.catch((err) => {
|
|
126
|
+
if (!isDebugEnabled())
|
|
127
|
+
return;
|
|
128
|
+
let errorText = 'unknown error';
|
|
129
|
+
if (err instanceof Error) {
|
|
130
|
+
errorText = err.message;
|
|
131
|
+
}
|
|
132
|
+
else if (typeof err === 'string') {
|
|
133
|
+
errorText = err;
|
|
134
|
+
}
|
|
135
|
+
safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP${sessionId ? ` (sessionId=${sessionId})` : ''}: ${errorText}\n`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (!isDebugEnabled())
|
|
140
|
+
return;
|
|
141
|
+
const errorText = err instanceof Error ? err.message : 'unknown error';
|
|
142
|
+
safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP (sync error): ${errorText}\n`);
|
|
75
143
|
}
|
|
76
144
|
}
|
|
77
145
|
export function logInfo(message, meta) {
|