@j0hanz/fetch-url-mcp 0.0.1

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 (65) hide show
  1. package/README.md +570 -0
  2. package/dist/AGENTS.md +115 -0
  3. package/dist/assets/logo.svg +24837 -0
  4. package/dist/cache.d.ts +47 -0
  5. package/dist/cache.js +316 -0
  6. package/dist/cli.d.ts +17 -0
  7. package/dist/cli.js +48 -0
  8. package/dist/config.d.ts +142 -0
  9. package/dist/config.js +480 -0
  10. package/dist/crypto.d.ts +3 -0
  11. package/dist/crypto.js +49 -0
  12. package/dist/dom-noise-removal.d.ts +1 -0
  13. package/dist/dom-noise-removal.js +488 -0
  14. package/dist/errors.d.ts +10 -0
  15. package/dist/errors.js +61 -0
  16. package/dist/fetch.d.ts +42 -0
  17. package/dist/fetch.js +1544 -0
  18. package/dist/host-normalization.d.ts +1 -0
  19. package/dist/host-normalization.js +77 -0
  20. package/dist/http-native.d.ts +5 -0
  21. package/dist/http-native.js +1313 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +91 -0
  24. package/dist/instructions.md +57 -0
  25. package/dist/ip-blocklist.d.ts +8 -0
  26. package/dist/ip-blocklist.js +74 -0
  27. package/dist/json.d.ts +1 -0
  28. package/dist/json.js +34 -0
  29. package/dist/language-detection.d.ts +2 -0
  30. package/dist/language-detection.js +364 -0
  31. package/dist/markdown-cleanup.d.ts +6 -0
  32. package/dist/markdown-cleanup.js +474 -0
  33. package/dist/mcp-validator.d.ts +15 -0
  34. package/dist/mcp-validator.js +44 -0
  35. package/dist/mcp.d.ts +4 -0
  36. package/dist/mcp.js +421 -0
  37. package/dist/observability.d.ts +21 -0
  38. package/dist/observability.js +211 -0
  39. package/dist/prompts.d.ts +7 -0
  40. package/dist/prompts.js +28 -0
  41. package/dist/resources.d.ts +8 -0
  42. package/dist/resources.js +216 -0
  43. package/dist/server-tuning.d.ts +13 -0
  44. package/dist/server-tuning.js +47 -0
  45. package/dist/server.d.ts +4 -0
  46. package/dist/server.js +174 -0
  47. package/dist/session.d.ts +39 -0
  48. package/dist/session.js +218 -0
  49. package/dist/tasks.d.ts +63 -0
  50. package/dist/tasks.js +327 -0
  51. package/dist/timer-utils.d.ts +5 -0
  52. package/dist/timer-utils.js +20 -0
  53. package/dist/tools.d.ts +135 -0
  54. package/dist/tools.js +812 -0
  55. package/dist/transform-types.d.ts +126 -0
  56. package/dist/transform-types.js +5 -0
  57. package/dist/transform.d.ts +36 -0
  58. package/dist/transform.js +2341 -0
  59. package/dist/type-guards.d.ts +14 -0
  60. package/dist/type-guards.js +13 -0
  61. package/dist/workers/transform-child.d.ts +1 -0
  62. package/dist/workers/transform-child.js +136 -0
  63. package/dist/workers/transform-worker.d.ts +1 -0
  64. package/dist/workers/transform-worker.js +128 -0
  65. package/package.json +91 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,421 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { logWarn, runWithRequestContext } from './observability.js';
5
+ import { taskManager } from './tasks.js';
6
+ import { FETCH_URL_TOOL_NAME, fetchUrlInputSchema, fetchUrlToolHandler, } from './tools.js';
7
+ import { isObject } from './type-guards.js';
8
+ /* -------------------------------------------------------------------------------------------------
9
+ * Tasks API schemas
10
+ * ------------------------------------------------------------------------------------------------- */
11
+ const TaskGetSchema = z.strictObject({
12
+ method: z.literal('tasks/get'),
13
+ params: z.strictObject({ taskId: z.string() }),
14
+ });
15
+ const TaskListSchema = z.strictObject({
16
+ method: z.literal('tasks/list'),
17
+ params: z
18
+ .strictObject({
19
+ cursor: z.string().optional(),
20
+ })
21
+ .optional(),
22
+ });
23
+ const TaskCancelSchema = z.strictObject({
24
+ method: z.literal('tasks/cancel'),
25
+ params: z.strictObject({ taskId: z.string() }),
26
+ });
27
+ const TaskResultSchema = z.strictObject({
28
+ method: z.literal('tasks/result'),
29
+ params: z.strictObject({ taskId: z.string() }),
30
+ });
31
+ const MIN_TASK_TTL_MS = 1_000;
32
+ const MAX_TASK_TTL_MS = 86_400_000;
33
+ const ExtendedCallToolRequestSchema = z
34
+ .object({
35
+ method: z.literal('tools/call'),
36
+ params: z
37
+ .object({
38
+ name: z.string().min(1),
39
+ arguments: z.record(z.string(), z.unknown()).optional(),
40
+ task: z
41
+ .strictObject({
42
+ ttl: z
43
+ .number()
44
+ .int()
45
+ .min(MIN_TASK_TTL_MS)
46
+ .max(MAX_TASK_TTL_MS)
47
+ .optional(),
48
+ })
49
+ .optional(),
50
+ _meta: z
51
+ .object({
52
+ progressToken: z.union([z.string(), z.number()]).optional(),
53
+ 'io.modelcontextprotocol/related-task': z
54
+ .strictObject({
55
+ taskId: z.string(),
56
+ })
57
+ .optional(),
58
+ })
59
+ .loose()
60
+ .optional(),
61
+ })
62
+ .loose(),
63
+ })
64
+ .loose();
65
+ function parseExtendedCallToolRequest(request) {
66
+ const parsed = ExtendedCallToolRequestSchema.safeParse(request);
67
+ if (parsed.success)
68
+ return parsed.data;
69
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid tool request');
70
+ }
71
+ function isRecord(value) {
72
+ return isObject(value);
73
+ }
74
+ function resolveTaskOwnerKey(extra) {
75
+ if (extra?.sessionId)
76
+ return `session:${extra.sessionId}`;
77
+ if (extra?.authInfo?.clientId)
78
+ return `client:${extra.authInfo.clientId}`;
79
+ if (extra?.authInfo?.token)
80
+ return `token:${extra.authInfo.token}`;
81
+ return 'default';
82
+ }
83
+ function resolveToolCallContext(extra) {
84
+ const context = {
85
+ ownerKey: resolveTaskOwnerKey(extra),
86
+ };
87
+ if (extra?.signal)
88
+ context.signal = extra.signal;
89
+ if (extra?.requestId !== undefined)
90
+ context.requestId = extra.requestId;
91
+ if (extra?.sendNotification)
92
+ context.sendNotification = extra.sendNotification;
93
+ return context;
94
+ }
95
+ function requireFetchUrlArgs(args) {
96
+ const parsed = fetchUrlInputSchema.safeParse(args);
97
+ if (!parsed.success) {
98
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for fetch-url');
99
+ }
100
+ return parsed.data;
101
+ }
102
+ function throwTaskNotFound() {
103
+ throw new McpError(ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
104
+ }
105
+ function requireFetchUrlToolName(name) {
106
+ if (name === FETCH_URL_TOOL_NAME)
107
+ return;
108
+ throw new McpError(ErrorCode.MethodNotFound, `Tool '${name}' does not support task execution`);
109
+ }
110
+ function buildRelatedTaskMeta(taskId, meta) {
111
+ return {
112
+ ...(meta ?? {}),
113
+ 'io.modelcontextprotocol/related-task': { taskId },
114
+ };
115
+ }
116
+ function buildCreateTaskResult(task) {
117
+ return {
118
+ task,
119
+ _meta: {
120
+ 'io.modelcontextprotocol/related-task': {
121
+ taskId: task.taskId,
122
+ status: task.status,
123
+ ...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
124
+ createdAt: task.createdAt,
125
+ lastUpdatedAt: task.lastUpdatedAt,
126
+ ttl: task.ttl,
127
+ pollInterval: task.pollInterval,
128
+ },
129
+ },
130
+ };
131
+ }
132
+ /**
133
+ * Track in-flight task executions so `tasks/cancel` can actually abort work.
134
+ * This is intentionally local to this module (no new files, no global singletons elsewhere).
135
+ */
136
+ const taskAbortControllers = new Map();
137
+ function attachAbortController(taskId) {
138
+ const existing = taskAbortControllers.get(taskId);
139
+ if (existing) {
140
+ // Defensive: should not happen, but avoid leaking the old controller.
141
+ taskAbortControllers.delete(taskId);
142
+ }
143
+ const controller = new AbortController();
144
+ taskAbortControllers.set(taskId, controller);
145
+ return controller;
146
+ }
147
+ function abortTaskExecution(taskId) {
148
+ const controller = taskAbortControllers.get(taskId);
149
+ if (!controller)
150
+ return;
151
+ controller.abort();
152
+ taskAbortControllers.delete(taskId);
153
+ }
154
+ function clearTaskExecution(taskId) {
155
+ taskAbortControllers.delete(taskId);
156
+ }
157
+ export function cancelTasksForOwner(ownerKey, statusMessage = 'The task was cancelled because its owner session ended.') {
158
+ if (!ownerKey)
159
+ return 0;
160
+ const cancelled = taskManager.cancelTasksByOwner(ownerKey, statusMessage);
161
+ for (const task of cancelled) {
162
+ abortTaskExecution(task.taskId);
163
+ }
164
+ return cancelled.length;
165
+ }
166
+ export function abortAllTaskExecutions() {
167
+ for (const taskId of taskAbortControllers.keys())
168
+ abortTaskExecution(taskId);
169
+ }
170
+ function buildTaskStatusParams(task) {
171
+ return {
172
+ taskId: task.taskId,
173
+ status: task.status,
174
+ ...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
175
+ createdAt: task.createdAt,
176
+ lastUpdatedAt: task.lastUpdatedAt,
177
+ ttl: task.ttl,
178
+ pollInterval: task.pollInterval,
179
+ };
180
+ }
181
+ function emitTaskStatusNotification(server, task) {
182
+ if (!server.isConnected())
183
+ return;
184
+ void server.server
185
+ .notification({
186
+ method: 'notifications/tasks/status',
187
+ params: buildTaskStatusParams(task),
188
+ })
189
+ .catch((error) => {
190
+ logWarn('Failed to send task status notification', {
191
+ taskId: task.taskId,
192
+ status: task.status,
193
+ error,
194
+ });
195
+ });
196
+ }
197
+ function updateWorkingTaskStatus(server, taskId, statusMessage) {
198
+ const current = taskManager.getTask(taskId);
199
+ if (current?.status !== 'working')
200
+ return;
201
+ if (current.statusMessage === statusMessage)
202
+ return;
203
+ taskManager.updateTask(taskId, { statusMessage });
204
+ const updated = taskManager.getTask(taskId);
205
+ if (updated)
206
+ emitTaskStatusNotification(server, updated);
207
+ }
208
+ async function runFetchTaskExecution(params) {
209
+ const { server, taskId, args, meta, sendNotification } = params;
210
+ return runWithRequestContext({ requestId: taskId, operationId: taskId }, async () => {
211
+ const controller = attachAbortController(taskId);
212
+ try {
213
+ const relatedMeta = buildRelatedTaskMeta(taskId, meta);
214
+ const result = await fetchUrlToolHandler(args, {
215
+ signal: controller.signal,
216
+ requestId: taskId, // Correlation
217
+ _meta: relatedMeta,
218
+ ...(sendNotification ? { sendNotification } : {}),
219
+ onProgress: (_progress, message) => {
220
+ updateWorkingTaskStatus(server, taskId, message);
221
+ },
222
+ });
223
+ const isToolError = isRecord(result) &&
224
+ typeof result.isError === 'boolean' &&
225
+ result.isError;
226
+ taskManager.updateTask(taskId, {
227
+ status: isToolError ? 'failed' : 'completed',
228
+ statusMessage: isToolError
229
+ ? (result
230
+ .structuredContent?.error ?? 'Tool execution failed')
231
+ : 'Task completed successfully.',
232
+ result,
233
+ });
234
+ const task = taskManager.getTask(taskId);
235
+ if (task)
236
+ emitTaskStatusNotification(server, task);
237
+ }
238
+ catch (error) {
239
+ const errorMessage = error instanceof Error ? error.message : String(error);
240
+ const errorPayload = error instanceof McpError
241
+ ? {
242
+ code: error.code,
243
+ message: errorMessage,
244
+ data: error.data,
245
+ }
246
+ : {
247
+ code: ErrorCode.InternalError,
248
+ message: errorMessage,
249
+ };
250
+ taskManager.updateTask(taskId, {
251
+ status: 'failed',
252
+ statusMessage: errorMessage,
253
+ error: errorPayload,
254
+ });
255
+ const task = taskManager.getTask(taskId);
256
+ if (task)
257
+ emitTaskStatusNotification(server, task);
258
+ }
259
+ finally {
260
+ clearTaskExecution(taskId);
261
+ }
262
+ });
263
+ }
264
+ function handleTaskToolCall(server, params, context) {
265
+ requireFetchUrlToolName(params.name);
266
+ const validArgs = requireFetchUrlArgs(params.arguments);
267
+ const task = taskManager.createTask(params.task?.ttl !== undefined ? { ttl: params.task.ttl } : undefined, 'Task started', context.ownerKey);
268
+ void runFetchTaskExecution({
269
+ server,
270
+ taskId: task.taskId,
271
+ args: validArgs,
272
+ ...(params._meta ? { meta: params._meta } : {}),
273
+ ...(context.sendNotification
274
+ ? { sendNotification: context.sendNotification }
275
+ : {}),
276
+ });
277
+ return buildCreateTaskResult({
278
+ taskId: task.taskId,
279
+ status: task.status,
280
+ ...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
281
+ createdAt: task.createdAt,
282
+ lastUpdatedAt: task.lastUpdatedAt,
283
+ ttl: task.ttl,
284
+ pollInterval: task.pollInterval,
285
+ });
286
+ }
287
+ async function handleDirectToolCall(params, context) {
288
+ const args = requireFetchUrlArgs(params.arguments);
289
+ const extra = {
290
+ ...(context.signal ? { signal: context.signal } : {}),
291
+ ...(context.requestId !== undefined
292
+ ? { requestId: context.requestId }
293
+ : {}),
294
+ ...(context.sendNotification
295
+ ? { sendNotification: context.sendNotification }
296
+ : {}),
297
+ ...(params._meta ? { _meta: params._meta } : {}),
298
+ };
299
+ return fetchUrlToolHandler(args, extra);
300
+ }
301
+ async function handleToolCallRequest(server, request, context) {
302
+ const { params } = request;
303
+ if (params.task) {
304
+ return handleTaskToolCall(server, params, context);
305
+ }
306
+ if (params.name === FETCH_URL_TOOL_NAME) {
307
+ return handleDirectToolCall(params, context);
308
+ }
309
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${params.name}`);
310
+ }
311
+ /* -------------------------------------------------------------------------------------------------
312
+ * Register handlers
313
+ * ------------------------------------------------------------------------------------------------- */
314
+ export function registerTaskHandlers(server) {
315
+ server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
316
+ const context = resolveToolCallContext(extra);
317
+ const requestId = context.requestId !== undefined
318
+ ? String(context.requestId)
319
+ : randomUUID();
320
+ const sessionId = extra?.sessionId;
321
+ return runWithRequestContext({
322
+ requestId,
323
+ operationId: requestId,
324
+ ...(sessionId ? { sessionId } : {}),
325
+ }, () => {
326
+ const parsed = parseExtendedCallToolRequest(request);
327
+ return handleToolCallRequest(server, parsed, context);
328
+ });
329
+ });
330
+ server.server.setRequestHandler(TaskGetSchema, async (request, extra) => {
331
+ const { taskId } = request.params;
332
+ const ownerKey = resolveTaskOwnerKey(extra);
333
+ const task = taskManager.getTask(taskId, ownerKey);
334
+ if (!task)
335
+ throwTaskNotFound();
336
+ return Promise.resolve({
337
+ taskId: task.taskId,
338
+ status: task.status,
339
+ statusMessage: task.statusMessage,
340
+ createdAt: task.createdAt,
341
+ lastUpdatedAt: task.lastUpdatedAt,
342
+ ttl: task.ttl,
343
+ pollInterval: task.pollInterval,
344
+ });
345
+ });
346
+ server.server.setRequestHandler(TaskResultSchema, async (request, extra) => {
347
+ const { taskId } = request.params;
348
+ const ownerKey = resolveTaskOwnerKey(extra);
349
+ const task = await taskManager.waitForTerminalTask(taskId, ownerKey, extra?.signal);
350
+ if (!task)
351
+ throwTaskNotFound();
352
+ if (task.status === 'failed') {
353
+ if (task.error) {
354
+ throw new McpError(task.error.code, task.error.message, task.error.data);
355
+ }
356
+ const failedResult = (task.result ?? null);
357
+ const fallback = failedResult ?? {
358
+ content: [
359
+ {
360
+ type: 'text',
361
+ text: task.statusMessage ?? 'Task execution failed',
362
+ },
363
+ ],
364
+ isError: true,
365
+ };
366
+ return Promise.resolve({
367
+ ...fallback,
368
+ _meta: {
369
+ ...fallback._meta,
370
+ 'io.modelcontextprotocol/related-task': { taskId: task.taskId },
371
+ },
372
+ });
373
+ }
374
+ if (task.status === 'cancelled') {
375
+ throw new McpError(ErrorCode.InvalidRequest, 'Task was cancelled');
376
+ }
377
+ const result = (task.result ?? { content: [] });
378
+ return Promise.resolve({
379
+ ...result,
380
+ _meta: {
381
+ ...result._meta,
382
+ 'io.modelcontextprotocol/related-task': { taskId: task.taskId },
383
+ },
384
+ });
385
+ });
386
+ server.server.setRequestHandler(TaskListSchema, async (request, extra) => {
387
+ const ownerKey = resolveTaskOwnerKey(extra);
388
+ const cursor = request.params?.cursor;
389
+ const { tasks, nextCursor } = taskManager.listTasks(cursor === undefined ? { ownerKey } : { ownerKey, cursor });
390
+ return Promise.resolve({
391
+ tasks: tasks.map((t) => ({
392
+ taskId: t.taskId,
393
+ status: t.status,
394
+ createdAt: t.createdAt,
395
+ lastUpdatedAt: t.lastUpdatedAt,
396
+ ttl: t.ttl,
397
+ pollInterval: t.pollInterval,
398
+ })),
399
+ nextCursor,
400
+ });
401
+ });
402
+ server.server.setRequestHandler(TaskCancelSchema, async (request, extra) => {
403
+ const { taskId } = request.params;
404
+ const ownerKey = resolveTaskOwnerKey(extra);
405
+ const task = taskManager.cancelTask(taskId, ownerKey);
406
+ if (!task)
407
+ throwTaskNotFound();
408
+ // Make cancellation actionable: abort any in-flight execution.
409
+ abortTaskExecution(taskId);
410
+ emitTaskStatusNotification(server, task);
411
+ return Promise.resolve({
412
+ taskId: task.taskId,
413
+ status: task.status,
414
+ statusMessage: task.statusMessage,
415
+ createdAt: task.createdAt,
416
+ lastUpdatedAt: task.lastUpdatedAt,
417
+ ttl: task.ttl,
418
+ pollInterval: task.pollInterval,
419
+ });
420
+ });
421
+ }
@@ -0,0 +1,21 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export type LogMetadata = Record<string, unknown>;
3
+ interface RequestContext {
4
+ readonly requestId: string;
5
+ readonly sessionId?: string;
6
+ readonly operationId?: string;
7
+ }
8
+ export declare function setMcpServer(server: McpServer): void;
9
+ export declare function registerMcpSessionServer(sessionId: string, server: McpServer): void;
10
+ export declare function unregisterMcpSessionServer(sessionId: string): void;
11
+ export declare function unregisterMcpSessionServerByServer(server: McpServer): void;
12
+ export declare function resolveMcpSessionIdByServer(server: McpServer): string | undefined;
13
+ export declare function runWithRequestContext<T>(context: RequestContext, fn: () => T): T;
14
+ export declare function getRequestId(): string | undefined;
15
+ export declare function getOperationId(): string | undefined;
16
+ export declare function logInfo(message: string, meta?: LogMetadata): void;
17
+ export declare function logDebug(message: string, meta?: LogMetadata): void;
18
+ export declare function logWarn(message: string, meta?: LogMetadata): void;
19
+ export declare function logError(message: string, error?: Error | LogMetadata): void;
20
+ export declare function redactUrl(rawUrl: string): string;
21
+ export {};
@@ -0,0 +1,211 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import process from 'node:process';
3
+ import { inspect, stripVTControlCharacters } from 'node:util';
4
+ import { config } from './config.js';
5
+ const requestContext = new AsyncLocalStorage({
6
+ name: 'requestContext',
7
+ });
8
+ let mcpServer;
9
+ const sessionServers = new Map();
10
+ let stderrAvailable = true;
11
+ process.stderr.on('error', () => {
12
+ stderrAvailable = false;
13
+ });
14
+ export function setMcpServer(server) {
15
+ mcpServer = server;
16
+ }
17
+ export function registerMcpSessionServer(sessionId, server) {
18
+ if (!sessionId)
19
+ return;
20
+ sessionServers.set(sessionId, server);
21
+ }
22
+ export function unregisterMcpSessionServer(sessionId) {
23
+ if (!sessionId)
24
+ return;
25
+ sessionServers.delete(sessionId);
26
+ }
27
+ export function unregisterMcpSessionServerByServer(server) {
28
+ for (const [sessionId, mappedServer] of sessionServers.entries()) {
29
+ if (mappedServer !== server)
30
+ continue;
31
+ sessionServers.delete(sessionId);
32
+ }
33
+ }
34
+ export function resolveMcpSessionIdByServer(server) {
35
+ for (const [sessionId, mappedServer] of sessionServers.entries()) {
36
+ if (mappedServer === server)
37
+ return sessionId;
38
+ }
39
+ return undefined;
40
+ }
41
+ export function runWithRequestContext(context, fn) {
42
+ return requestContext.run(context, fn);
43
+ }
44
+ function getRequestContext() {
45
+ return requestContext.getStore();
46
+ }
47
+ export function getRequestId() {
48
+ return getRequestContext()?.requestId;
49
+ }
50
+ function getSessionId() {
51
+ return getRequestContext()?.sessionId;
52
+ }
53
+ export function getOperationId() {
54
+ return getRequestContext()?.operationId;
55
+ }
56
+ function isDebugEnabled() {
57
+ return config.logging.level === 'debug';
58
+ }
59
+ function buildContextMetadata() {
60
+ const ctx = requestContext.getStore();
61
+ if (!ctx)
62
+ return undefined;
63
+ const { requestId, operationId, sessionId } = ctx;
64
+ const includeSession = sessionId && isDebugEnabled();
65
+ if (!requestId && !operationId && !includeSession)
66
+ return undefined;
67
+ const meta = {};
68
+ if (requestId)
69
+ meta.requestId = requestId;
70
+ if (operationId)
71
+ meta.operationId = operationId;
72
+ if (includeSession)
73
+ meta.sessionId = sessionId;
74
+ return meta;
75
+ }
76
+ function mergeMetadata(meta) {
77
+ const contextMeta = buildContextMetadata();
78
+ const hasMeta = meta && Object.keys(meta).length > 0;
79
+ if (!contextMeta && !hasMeta)
80
+ return undefined;
81
+ if (!contextMeta)
82
+ return meta;
83
+ if (!hasMeta)
84
+ return contextMeta;
85
+ return { ...contextMeta, ...meta };
86
+ }
87
+ function formatMetadata(meta) {
88
+ const merged = mergeMetadata(meta);
89
+ if (!merged)
90
+ return '';
91
+ return ` ${inspect(merged, { breakLength: Infinity, colors: false, compact: true, sorted: true })}`;
92
+ }
93
+ function createTimestamp() {
94
+ return new Date().toISOString();
95
+ }
96
+ function formatLogEntry(level, message, meta) {
97
+ if (config.logging.format === 'json') {
98
+ const merged = mergeMetadata(meta);
99
+ const entry = {
100
+ timestamp: createTimestamp(),
101
+ level: level.toUpperCase(),
102
+ message,
103
+ };
104
+ if (merged) {
105
+ Object.assign(entry, merged);
106
+ }
107
+ return JSON.stringify(entry);
108
+ }
109
+ return `[${createTimestamp()}] ${level.toUpperCase()}: ${message}${formatMetadata(meta)}`;
110
+ }
111
+ function shouldLog(level) {
112
+ // Debug logs only when LOG_LEVEL=debug
113
+ if (level === 'debug')
114
+ return isDebugEnabled();
115
+ // All other levels always log
116
+ return true;
117
+ }
118
+ function mapToMcpLevel(level) {
119
+ switch (level) {
120
+ case 'warn':
121
+ return 'warning';
122
+ case 'error':
123
+ return 'error';
124
+ case 'debug':
125
+ return 'debug';
126
+ case 'info':
127
+ default:
128
+ return 'info';
129
+ }
130
+ }
131
+ function safeWriteStderr(line) {
132
+ if (!stderrAvailable)
133
+ return;
134
+ if (process.stderr.destroyed || process.stderr.writableEnded) {
135
+ stderrAvailable = false;
136
+ return;
137
+ }
138
+ try {
139
+ process.stderr.write(line);
140
+ }
141
+ catch {
142
+ // Logging must never take down the process (e.g. EPIPE).
143
+ stderrAvailable = false;
144
+ }
145
+ }
146
+ function writeLog(level, message, meta) {
147
+ if (!shouldLog(level))
148
+ return;
149
+ const line = formatLogEntry(level, message, meta);
150
+ safeWriteStderr(`${stripVTControlCharacters(line)}\n`);
151
+ const sessionId = getSessionId();
152
+ const server = sessionId
153
+ ? (sessionServers.get(sessionId) ?? mcpServer)
154
+ : mcpServer;
155
+ if (!server)
156
+ return;
157
+ try {
158
+ server.server
159
+ .sendLoggingMessage({
160
+ level: mapToMcpLevel(level),
161
+ // Preserve existing behavior: MCP payload includes only message + provided meta (not ALS context meta).
162
+ data: meta ? { message, ...meta } : message,
163
+ }, sessionId)
164
+ .catch((err) => {
165
+ if (!isDebugEnabled())
166
+ return;
167
+ let errorText = 'unknown error';
168
+ if (err instanceof Error) {
169
+ errorText = err.message;
170
+ }
171
+ else if (typeof err === 'string') {
172
+ errorText = err;
173
+ }
174
+ safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP${sessionId ? ` (sessionId=${sessionId})` : ''}: ${errorText}\n`);
175
+ });
176
+ }
177
+ catch (err) {
178
+ if (!isDebugEnabled())
179
+ return;
180
+ const errorText = err instanceof Error ? err.message : 'unknown error';
181
+ safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP (sync error): ${errorText}\n`);
182
+ }
183
+ }
184
+ export function logInfo(message, meta) {
185
+ writeLog('info', message, meta);
186
+ }
187
+ export function logDebug(message, meta) {
188
+ writeLog('debug', message, meta);
189
+ }
190
+ export function logWarn(message, meta) {
191
+ writeLog('warn', message, meta);
192
+ }
193
+ export function logError(message, error) {
194
+ const errorMeta = error instanceof Error
195
+ ? { error: error.message, stack: error.stack }
196
+ : (error ?? {});
197
+ writeLog('error', message, errorMeta);
198
+ }
199
+ export function redactUrl(rawUrl) {
200
+ try {
201
+ const url = new URL(rawUrl);
202
+ url.username = '';
203
+ url.password = '';
204
+ url.hash = '';
205
+ url.search = '';
206
+ return url.toString();
207
+ }
208
+ catch {
209
+ return rawUrl;
210
+ }
211
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ interface IconInfo {
3
+ src: string;
4
+ mimeType: string;
5
+ }
6
+ export declare function registerGetHelpPrompt(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
7
+ export {};
@@ -0,0 +1,28 @@
1
+ export function registerGetHelpPrompt(server, instructions, iconInfo) {
2
+ const description = 'Return the Fetch URL usage instructions.';
3
+ server.registerPrompt('get-help', {
4
+ title: 'Get Help',
5
+ description,
6
+ ...(iconInfo
7
+ ? {
8
+ icons: [
9
+ {
10
+ src: iconInfo.src,
11
+ mimeType: iconInfo.mimeType,
12
+ },
13
+ ],
14
+ }
15
+ : {}),
16
+ }, () => ({
17
+ description,
18
+ messages: [
19
+ {
20
+ role: 'user',
21
+ content: {
22
+ type: 'text',
23
+ text: instructions,
24
+ },
25
+ },
26
+ ],
27
+ }));
28
+ }
@@ -0,0 +1,8 @@
1
+ import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ interface IconInfo {
3
+ src: string;
4
+ mimeType: string;
5
+ }
6
+ export declare function registerInstructionResource(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
7
+ export declare function registerCacheResourceTemplate(server: McpServer, iconInfo?: IconInfo): void;
8
+ export {};