@j0hanz/fetch-url-mcp 1.3.1 → 1.5.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 (205) hide show
  1. package/README.md +24 -21
  2. package/dist/cli.d.ts +3 -3
  3. package/dist/cli.js +15 -8
  4. package/dist/http/auth.d.ts +6 -6
  5. package/dist/http/auth.js +78 -23
  6. package/dist/http/health.d.ts +1 -2
  7. package/dist/http/health.js +7 -18
  8. package/dist/http/helpers.d.ts +3 -11
  9. package/dist/http/helpers.js +28 -26
  10. package/dist/http/native.d.ts +0 -1
  11. package/dist/http/native.js +63 -41
  12. package/dist/http/rate-limit.d.ts +2 -2
  13. package/dist/http/rate-limit.js +11 -16
  14. package/dist/index.d.ts +0 -1
  15. package/dist/index.js +17 -20
  16. package/dist/{markdown-cleanup.d.ts → lib/content.d.ts} +4 -2
  17. package/dist/lib/content.js +1356 -0
  18. package/dist/lib/core.d.ts +253 -0
  19. package/dist/lib/core.js +1228 -0
  20. package/dist/{tool-pipeline.d.ts → lib/fetch-pipeline.d.ts} +1 -3
  21. package/dist/{tool-pipeline.js → lib/fetch-pipeline.js} +18 -44
  22. package/dist/{fetch.d.ts → lib/http.d.ts} +7 -9
  23. package/dist/{fetch.js → lib/http.js} +721 -1004
  24. package/dist/lib/mcp-tools.d.ts +28 -0
  25. package/dist/lib/mcp-tools.js +107 -0
  26. package/dist/{tool-progress.d.ts → lib/progress.d.ts} +0 -2
  27. package/dist/{tool-progress.js → lib/progress.js} +9 -14
  28. package/dist/lib/task-handlers.d.ts +5 -0
  29. package/dist/{mcp.js → lib/task-handlers.js} +95 -31
  30. package/dist/lib/url.d.ts +70 -0
  31. package/dist/lib/url.js +686 -0
  32. package/dist/lib/utils.d.ts +58 -0
  33. package/dist/lib/utils.js +304 -0
  34. package/dist/{prompts.d.ts → prompts/index.d.ts} +0 -1
  35. package/dist/{prompts.js → prompts/index.js} +1 -2
  36. package/dist/{resources.d.ts → resources/index.d.ts} +0 -1
  37. package/dist/{resources.js → resources/index.js} +87 -64
  38. package/dist/{instructions.d.ts → resources/instructions.d.ts} +0 -1
  39. package/dist/{instructions.js → resources/instructions.js} +5 -3
  40. package/dist/schemas/inputs.d.ts +7 -0
  41. package/dist/schemas/inputs.js +24 -0
  42. package/dist/schemas/outputs.d.ts +23 -0
  43. package/dist/schemas/outputs.js +77 -0
  44. package/dist/server.d.ts +0 -1
  45. package/dist/server.js +26 -25
  46. package/dist/tasks/execution.d.ts +0 -1
  47. package/dist/tasks/execution.js +106 -70
  48. package/dist/tasks/manager.d.ts +11 -3
  49. package/dist/tasks/manager.js +97 -73
  50. package/dist/tasks/owner.d.ts +3 -3
  51. package/dist/tasks/owner.js +2 -2
  52. package/dist/tasks/tool-registry.d.ts +11 -0
  53. package/dist/tasks/tool-registry.js +13 -0
  54. package/dist/tools/fetch-url.d.ts +28 -0
  55. package/dist/{tools.js → tools/fetch-url.js} +95 -147
  56. package/dist/tools/index.d.ts +2 -0
  57. package/dist/tools/index.js +4 -0
  58. package/dist/transform/html-translators.d.ts +1 -0
  59. package/dist/transform/html-translators.js +454 -0
  60. package/dist/transform/metadata.d.ts +4 -0
  61. package/dist/transform/metadata.js +183 -0
  62. package/dist/transform/transform.d.ts +0 -1
  63. package/dist/transform/transform.js +44 -679
  64. package/dist/transform/types.d.ts +9 -12
  65. package/dist/transform/types.js +0 -1
  66. package/dist/transform/worker-pool.d.ts +0 -1
  67. package/dist/transform/worker-pool.js +7 -16
  68. package/dist/transform/workers/shared.d.ts +7 -0
  69. package/dist/transform/workers/shared.js +130 -0
  70. package/dist/transform/workers/transform-child.d.ts +0 -1
  71. package/dist/transform/workers/transform-child.js +5 -135
  72. package/dist/transform/workers/transform-worker.d.ts +0 -1
  73. package/dist/transform/workers/transform-worker.js +7 -128
  74. package/package.json +11 -7
  75. package/dist/cache.d.ts +0 -54
  76. package/dist/cache.d.ts.map +0 -1
  77. package/dist/cache.js +0 -261
  78. package/dist/cache.js.map +0 -1
  79. package/dist/cli.d.ts.map +0 -1
  80. package/dist/cli.js.map +0 -1
  81. package/dist/config.d.ts +0 -141
  82. package/dist/config.d.ts.map +0 -1
  83. package/dist/config.js +0 -473
  84. package/dist/config.js.map +0 -1
  85. package/dist/crypto.d.ts +0 -4
  86. package/dist/crypto.d.ts.map +0 -1
  87. package/dist/crypto.js +0 -56
  88. package/dist/crypto.js.map +0 -1
  89. package/dist/dom-noise-removal.d.ts +0 -2
  90. package/dist/dom-noise-removal.d.ts.map +0 -1
  91. package/dist/dom-noise-removal.js +0 -494
  92. package/dist/dom-noise-removal.js.map +0 -1
  93. package/dist/download.d.ts +0 -4
  94. package/dist/download.d.ts.map +0 -1
  95. package/dist/download.js +0 -106
  96. package/dist/download.js.map +0 -1
  97. package/dist/errors.d.ts +0 -11
  98. package/dist/errors.d.ts.map +0 -1
  99. package/dist/errors.js +0 -65
  100. package/dist/errors.js.map +0 -1
  101. package/dist/examples/mcp-fetch-url-client.js +0 -329
  102. package/dist/examples/mcp-fetch-url-client.js.map +0 -1
  103. package/dist/fetch-content.d.ts +0 -5
  104. package/dist/fetch-content.d.ts.map +0 -1
  105. package/dist/fetch-content.js +0 -164
  106. package/dist/fetch-content.js.map +0 -1
  107. package/dist/fetch-stream.d.ts +0 -5
  108. package/dist/fetch-stream.d.ts.map +0 -1
  109. package/dist/fetch-stream.js +0 -29
  110. package/dist/fetch-stream.js.map +0 -1
  111. package/dist/fetch.d.ts.map +0 -1
  112. package/dist/fetch.js.map +0 -1
  113. package/dist/host-normalization.d.ts +0 -2
  114. package/dist/host-normalization.d.ts.map +0 -1
  115. package/dist/host-normalization.js +0 -91
  116. package/dist/host-normalization.js.map +0 -1
  117. package/dist/http/auth.d.ts.map +0 -1
  118. package/dist/http/auth.js.map +0 -1
  119. package/dist/http/health.d.ts.map +0 -1
  120. package/dist/http/health.js.map +0 -1
  121. package/dist/http/helpers.d.ts.map +0 -1
  122. package/dist/http/helpers.js.map +0 -1
  123. package/dist/http/native.d.ts.map +0 -1
  124. package/dist/http/native.js.map +0 -1
  125. package/dist/http/rate-limit.d.ts.map +0 -1
  126. package/dist/http/rate-limit.js.map +0 -1
  127. package/dist/index.d.ts.map +0 -1
  128. package/dist/index.js.map +0 -1
  129. package/dist/instructions.d.ts.map +0 -1
  130. package/dist/instructions.js.map +0 -1
  131. package/dist/ip-blocklist.d.ts +0 -9
  132. package/dist/ip-blocklist.d.ts.map +0 -1
  133. package/dist/ip-blocklist.js +0 -79
  134. package/dist/ip-blocklist.js.map +0 -1
  135. package/dist/json.d.ts +0 -2
  136. package/dist/json.d.ts.map +0 -1
  137. package/dist/json.js +0 -45
  138. package/dist/json.js.map +0 -1
  139. package/dist/language-detection.d.ts +0 -3
  140. package/dist/language-detection.d.ts.map +0 -1
  141. package/dist/language-detection.js +0 -355
  142. package/dist/language-detection.js.map +0 -1
  143. package/dist/markdown-cleanup.d.ts.map +0 -1
  144. package/dist/markdown-cleanup.js +0 -534
  145. package/dist/markdown-cleanup.js.map +0 -1
  146. package/dist/mcp-validator.d.ts +0 -17
  147. package/dist/mcp-validator.d.ts.map +0 -1
  148. package/dist/mcp-validator.js +0 -45
  149. package/dist/mcp-validator.js.map +0 -1
  150. package/dist/mcp.d.ts +0 -4
  151. package/dist/mcp.d.ts.map +0 -1
  152. package/dist/mcp.js.map +0 -1
  153. package/dist/observability.d.ts +0 -23
  154. package/dist/observability.d.ts.map +0 -1
  155. package/dist/observability.js +0 -238
  156. package/dist/observability.js.map +0 -1
  157. package/dist/prompts.d.ts.map +0 -1
  158. package/dist/prompts.js.map +0 -1
  159. package/dist/resources.d.ts.map +0 -1
  160. package/dist/resources.js.map +0 -1
  161. package/dist/server-tuning.d.ts +0 -15
  162. package/dist/server-tuning.d.ts.map +0 -1
  163. package/dist/server-tuning.js +0 -49
  164. package/dist/server-tuning.js.map +0 -1
  165. package/dist/server.d.ts.map +0 -1
  166. package/dist/server.js.map +0 -1
  167. package/dist/session.d.ts +0 -42
  168. package/dist/session.d.ts.map +0 -1
  169. package/dist/session.js +0 -255
  170. package/dist/session.js.map +0 -1
  171. package/dist/tasks/execution.d.ts.map +0 -1
  172. package/dist/tasks/execution.js.map +0 -1
  173. package/dist/tasks/manager.d.ts.map +0 -1
  174. package/dist/tasks/manager.js.map +0 -1
  175. package/dist/tasks/owner.d.ts.map +0 -1
  176. package/dist/tasks/owner.js.map +0 -1
  177. package/dist/timer-utils.d.ts +0 -6
  178. package/dist/timer-utils.d.ts.map +0 -1
  179. package/dist/timer-utils.js +0 -27
  180. package/dist/timer-utils.js.map +0 -1
  181. package/dist/tool-errors.d.ts +0 -12
  182. package/dist/tool-errors.d.ts.map +0 -1
  183. package/dist/tool-errors.js +0 -55
  184. package/dist/tool-errors.js.map +0 -1
  185. package/dist/tool-pipeline.d.ts.map +0 -1
  186. package/dist/tool-pipeline.js.map +0 -1
  187. package/dist/tool-progress.d.ts.map +0 -1
  188. package/dist/tool-progress.js.map +0 -1
  189. package/dist/tools.d.ts +0 -54
  190. package/dist/tools.d.ts.map +0 -1
  191. package/dist/tools.js.map +0 -1
  192. package/dist/transform/transform.d.ts.map +0 -1
  193. package/dist/transform/transform.js.map +0 -1
  194. package/dist/transform/types.d.ts.map +0 -1
  195. package/dist/transform/types.js.map +0 -1
  196. package/dist/transform/worker-pool.d.ts.map +0 -1
  197. package/dist/transform/worker-pool.js.map +0 -1
  198. package/dist/transform/workers/transform-child.d.ts.map +0 -1
  199. package/dist/transform/workers/transform-child.js.map +0 -1
  200. package/dist/transform/workers/transform-worker.d.ts.map +0 -1
  201. package/dist/transform/workers/transform-worker.js.map +0 -1
  202. package/dist/type-guards.d.ts +0 -16
  203. package/dist/type-guards.d.ts.map +0 -1
  204. package/dist/type-guards.js +0 -13
  205. package/dist/type-guards.js.map +0 -1
@@ -0,0 +1,58 @@
1
+ export declare function throwIfAborted(signal: AbortSignal | undefined, url: string, stage: string): void;
2
+ export declare function createAbortError(url: string, stage: string): FetchError;
3
+ export declare function timingSafeEqualUtf8(a: string, b: string): boolean;
4
+ export declare function sha256Hex(input: string | Uint8Array): string;
5
+ export declare function hmacSha256Hex(key: string | Uint8Array, input: string | Uint8Array): string;
6
+ export declare class FetchError extends Error {
7
+ readonly url: string;
8
+ readonly statusCode: number;
9
+ readonly code: string;
10
+ readonly details: Readonly<Record<string, unknown>>;
11
+ constructor(message: string, url: string, httpStatus?: number, details?: Record<string, unknown>, options?: ErrorOptions);
12
+ }
13
+ export declare function getErrorMessage(error: unknown): string;
14
+ export declare function toError(error: unknown): Error;
15
+ export declare function isAbortError(error: unknown): boolean;
16
+ export declare function createErrorWithCode(message: string, code: string, options?: ErrorOptions): NodeJS.ErrnoException;
17
+ export declare function isSystemError(error: unknown): error is NodeJS.ErrnoException;
18
+ export declare const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
19
+ export declare function stableStringify(obj: unknown, depth?: number, seen?: WeakSet<WeakKey>): string;
20
+ interface HttpServerTuningTarget {
21
+ headersTimeout?: number;
22
+ requestTimeout?: number;
23
+ keepAliveTimeout?: number;
24
+ keepAliveTimeoutBuffer?: number;
25
+ maxHeadersCount?: number | null;
26
+ maxConnections?: number;
27
+ on?: (event: string, listener: (...args: unknown[]) => void) => void;
28
+ closeIdleConnections?: () => void;
29
+ closeAllConnections?: () => void;
30
+ }
31
+ export declare function applyHttpServerTuning(server: HttpServerTuningTarget): void;
32
+ export declare function drainConnectionsOnShutdown(server: HttpServerTuningTarget): void;
33
+ export interface CancellableTimeout<T> {
34
+ promise: Promise<T>;
35
+ cancel: () => void;
36
+ }
37
+ interface IntervalLoopOptions<T> {
38
+ signal: AbortSignal;
39
+ onTick: (value: T) => void | Promise<void>;
40
+ onError?: (error: unknown) => void;
41
+ }
42
+ export declare function createUnrefTimeout<T>(timeoutMs: number, value: T): CancellableTimeout<T>;
43
+ export declare function startAbortableIntervalLoop<T>(intervalMs: number, value: T, options: IntervalLoopOptions<T>): void;
44
+ export declare function isObject(value: unknown): value is Record<PropertyKey, unknown>;
45
+ export declare function isError(value: unknown): value is Error;
46
+ interface LikeNode {
47
+ readonly tagName?: string | undefined;
48
+ readonly nodeName?: string | undefined;
49
+ readonly nodeType?: number | undefined;
50
+ readonly textContent?: string | null | undefined;
51
+ readonly innerHTML?: string | undefined;
52
+ readonly parentNode?: unknown;
53
+ readonly childNodes?: ArrayLike<unknown>;
54
+ readonly rawTagName?: string | undefined;
55
+ getAttribute?(name: string): string | null;
56
+ }
57
+ export declare function isLikeNode(value: unknown): value is LikeNode;
58
+ export {};
@@ -0,0 +1,304 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { createHash, createHmac, hash as oneShotHash, timingSafeEqual, } from 'node:crypto';
3
+ import { setInterval as setIntervalPromise, setTimeout as setTimeoutPromise, } from 'node:timers/promises';
4
+ import { inspect } from 'node:util';
5
+ import { config, logDebug, logWarn } from './core.js';
6
+ const UNKNOWN_ERROR_MESSAGE = 'Unknown error';
7
+ function getAbortReason(signal) {
8
+ const record = isObject(signal) ? signal : null;
9
+ return record && 'reason' in record ? record['reason'] : undefined;
10
+ }
11
+ function isTimeoutAbortReason(reason) {
12
+ return reason instanceof Error && reason.name === 'TimeoutError';
13
+ }
14
+ export function throwIfAborted(signal, url, stage) {
15
+ if (!signal?.aborted)
16
+ return;
17
+ const reason = getAbortReason(signal);
18
+ if (isTimeoutAbortReason(reason)) {
19
+ throw new FetchError('Request timeout', url, 504, {
20
+ reason: 'timeout',
21
+ stage,
22
+ });
23
+ }
24
+ throw new FetchError('Request was canceled', url, 499, {
25
+ reason: 'aborted',
26
+ stage,
27
+ });
28
+ }
29
+ export function createAbortError(url, stage) {
30
+ return new FetchError('Request was canceled', url, 499, {
31
+ reason: 'aborted',
32
+ stage,
33
+ });
34
+ }
35
+ const MAX_HASH_INPUT_BYTES = 5 * 1024 * 1024;
36
+ const ALLOWED_HASH_ALGORITHMS = new Set([
37
+ 'sha256',
38
+ 'sha512',
39
+ ]);
40
+ function byteLengthUtf8(input) {
41
+ // Avoid allocating (unlike TextEncoder().encode()).
42
+ return Buffer.byteLength(input, 'utf8');
43
+ }
44
+ function byteLength(input) {
45
+ return typeof input === 'string' ? byteLengthUtf8(input) : input.byteLength;
46
+ }
47
+ function assertAllowedAlgorithm(algorithm) {
48
+ // Defensive: protects against `any` / unchecked external inputs.
49
+ if (!ALLOWED_HASH_ALGORITHMS.has(algorithm)) {
50
+ throw new Error(`Hash algorithm not allowed: ${algorithm}`);
51
+ }
52
+ }
53
+ function padBuffer(buffer, length) {
54
+ const padded = Buffer.alloc(length);
55
+ buffer.copy(padded);
56
+ return padded;
57
+ }
58
+ function equalWithPadding(aBuffer, bBuffer, paddedLength) {
59
+ const paddedA = padBuffer(aBuffer, paddedLength);
60
+ const paddedB = padBuffer(bBuffer, paddedLength);
61
+ return timingSafeEqual(paddedA, paddedB) && aBuffer.length === bBuffer.length;
62
+ }
63
+ export function timingSafeEqualUtf8(a, b) {
64
+ const aBuffer = Buffer.from(a, 'utf8');
65
+ const bBuffer = Buffer.from(b, 'utf8');
66
+ if (aBuffer.length === bBuffer.length) {
67
+ return timingSafeEqual(aBuffer, bBuffer);
68
+ }
69
+ // Avoid early return timing differences on length mismatch.
70
+ const maxLength = Math.max(aBuffer.length, bBuffer.length);
71
+ return equalWithPadding(aBuffer, bBuffer, maxLength);
72
+ }
73
+ function hashHex(algorithm, input) {
74
+ assertAllowedAlgorithm(algorithm);
75
+ if (byteLength(input) <= MAX_HASH_INPUT_BYTES) {
76
+ return oneShotHash(algorithm, input, 'hex');
77
+ }
78
+ const hasher = createHash(algorithm);
79
+ hasher.update(input);
80
+ return hasher.digest('hex');
81
+ }
82
+ export function sha256Hex(input) {
83
+ return hashHex('sha256', input);
84
+ }
85
+ export function hmacSha256Hex(key, input) {
86
+ return createHmac('sha256', key).update(input).digest('hex');
87
+ }
88
+ const DEFAULT_HTTP_STATUS = 502;
89
+ export class FetchError extends Error {
90
+ url;
91
+ statusCode;
92
+ code;
93
+ details;
94
+ constructor(message, url, httpStatus, details = {}, options) {
95
+ super(message, options);
96
+ this.url = url;
97
+ this.name = 'FetchError';
98
+ this.statusCode = httpStatus ?? DEFAULT_HTTP_STATUS;
99
+ this.code = httpStatus ? `HTTP_${httpStatus}` : 'FETCH_ERROR';
100
+ this.details = Object.freeze({ url, httpStatus, ...details });
101
+ Error.captureStackTrace(this, this.constructor);
102
+ }
103
+ }
104
+ export function getErrorMessage(error) {
105
+ if (isError(error))
106
+ return error.message;
107
+ if (isNonEmptyString(error))
108
+ return error;
109
+ if (isErrorWithMessage(error))
110
+ return error.message;
111
+ return formatUnknownError(error);
112
+ }
113
+ export function toError(error) {
114
+ return isError(error) ? error : new Error(getErrorMessage(error));
115
+ }
116
+ export function isAbortError(error) {
117
+ return isError(error) && error.name === 'AbortError';
118
+ }
119
+ function isNonEmptyString(value) {
120
+ return typeof value === 'string' && value.length > 0;
121
+ }
122
+ function isErrorWithMessage(error) {
123
+ if (!isObject(error))
124
+ return false;
125
+ const { message } = error;
126
+ return typeof message === 'string' && message.length > 0;
127
+ }
128
+ function formatUnknownError(error) {
129
+ if (error === null || error === undefined)
130
+ return UNKNOWN_ERROR_MESSAGE;
131
+ try {
132
+ return inspect(error, {
133
+ depth: 2,
134
+ maxStringLength: 200,
135
+ breakLength: Infinity,
136
+ compact: true,
137
+ colors: false,
138
+ });
139
+ }
140
+ catch {
141
+ return UNKNOWN_ERROR_MESSAGE;
142
+ }
143
+ }
144
+ export function createErrorWithCode(message, code, options) {
145
+ const error = new Error(message, options);
146
+ return Object.assign(error, { code });
147
+ }
148
+ export function isSystemError(error) {
149
+ if (!isError(error))
150
+ return false;
151
+ if (!('code' in error))
152
+ return false;
153
+ const { code } = error;
154
+ return typeof code === 'string';
155
+ }
156
+ export const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
157
+ const MAX_DEPTH = 20;
158
+ const MAX_DEPTH_ERROR = `stableStringify: Max depth (${MAX_DEPTH}) exceeded`;
159
+ const CIRCULAR_ERROR = 'stableStringify: Circular reference detected';
160
+ function compareObjectKeys(a, b) {
161
+ if (a === b)
162
+ return 0;
163
+ return a < b ? -1 : 1;
164
+ }
165
+ function getSortedObjectKeys(obj) {
166
+ return Object.keys(obj).sort(compareObjectKeys);
167
+ }
168
+ function processValue(obj, depth, seen) {
169
+ if (typeof obj !== 'object' || obj === null) {
170
+ return obj;
171
+ }
172
+ // Depth guard
173
+ if (depth > MAX_DEPTH) {
174
+ throw new Error(MAX_DEPTH_ERROR);
175
+ }
176
+ // Cycle detection (track active recursion stack only).
177
+ if (seen.has(obj)) {
178
+ throw new Error(CIRCULAR_ERROR);
179
+ }
180
+ seen.add(obj);
181
+ try {
182
+ if (Array.isArray(obj)) {
183
+ return obj.map((item) => processValue(item, depth + 1, seen));
184
+ }
185
+ const keys = getSortedObjectKeys(obj);
186
+ const record = obj;
187
+ const sortedObj = {};
188
+ for (const key of keys) {
189
+ sortedObj[key] = processValue(record[key], depth + 1, seen);
190
+ }
191
+ return sortedObj;
192
+ }
193
+ finally {
194
+ seen.delete(obj);
195
+ }
196
+ }
197
+ export function stableStringify(obj, depth = 0, seen = new WeakSet()) {
198
+ const processed = processValue(obj, depth, seen);
199
+ return JSON.stringify(processed);
200
+ }
201
+ const DROP_LOG_INTERVAL_MS = 10_000;
202
+ function setIfDefined(value, setter) {
203
+ if (value === undefined)
204
+ return;
205
+ setter(value);
206
+ }
207
+ function assignServerValue(server, key, value) {
208
+ setIfDefined(value, (resolved) => {
209
+ server[key] = resolved;
210
+ });
211
+ }
212
+ export function applyHttpServerTuning(server) {
213
+ const { headersTimeoutMs, requestTimeoutMs, keepAliveTimeoutMs, keepAliveTimeoutBufferMs, maxHeadersCount, maxConnections, } = config.server.http;
214
+ const tuningValues = [
215
+ ['headersTimeout', headersTimeoutMs],
216
+ ['requestTimeout', requestTimeoutMs],
217
+ ['keepAliveTimeout', keepAliveTimeoutMs],
218
+ ['keepAliveTimeoutBuffer', keepAliveTimeoutBufferMs],
219
+ ['maxHeadersCount', maxHeadersCount],
220
+ ];
221
+ for (const [key, value] of tuningValues) {
222
+ assignServerValue(server, key, value);
223
+ }
224
+ if (typeof maxConnections === 'number' && maxConnections > 0) {
225
+ server.maxConnections = maxConnections;
226
+ if (typeof server.on === 'function') {
227
+ let lastLoggedAt = 0;
228
+ let droppedSinceLastLog = 0;
229
+ const onDrop = (data) => {
230
+ droppedSinceLastLog += 1;
231
+ const now = Date.now();
232
+ if (now - lastLoggedAt < DROP_LOG_INTERVAL_MS)
233
+ return;
234
+ logWarn('Incoming connection dropped (maxConnections reached)', {
235
+ maxConnections,
236
+ dropped: droppedSinceLastLog,
237
+ data,
238
+ });
239
+ lastLoggedAt = now;
240
+ droppedSinceLastLog = 0;
241
+ };
242
+ server.on('drop', onDrop);
243
+ }
244
+ }
245
+ }
246
+ export function drainConnectionsOnShutdown(server) {
247
+ if (typeof server.closeIdleConnections === 'function') {
248
+ server.closeIdleConnections();
249
+ logDebug('Closed idle HTTP connections during shutdown');
250
+ }
251
+ }
252
+ function createAbortSafeTimeoutPromise(timeoutMs, value, signal) {
253
+ return setTimeoutPromise(timeoutMs, value, {
254
+ ref: false,
255
+ signal,
256
+ }).catch((err) => {
257
+ if (isAbortError(err)) {
258
+ return new Promise(() => { });
259
+ }
260
+ throw err;
261
+ });
262
+ }
263
+ export function createUnrefTimeout(timeoutMs, value) {
264
+ const controller = new AbortController();
265
+ const promise = createAbortSafeTimeoutPromise(timeoutMs, value, controller.signal);
266
+ return {
267
+ promise,
268
+ cancel: () => {
269
+ controller.abort();
270
+ },
271
+ };
272
+ }
273
+ export function startAbortableIntervalLoop(intervalMs, value, options) {
274
+ const ticks = setIntervalPromise(intervalMs, value, {
275
+ signal: options.signal,
276
+ ref: false,
277
+ });
278
+ void (async () => {
279
+ try {
280
+ for await (const tickValue of ticks) {
281
+ await options.onTick(tickValue);
282
+ if (options.signal.aborted)
283
+ return;
284
+ }
285
+ }
286
+ catch (error) {
287
+ if (isAbortError(error))
288
+ return;
289
+ options.onError?.(error);
290
+ }
291
+ })();
292
+ }
293
+ export function isObject(value) {
294
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
295
+ }
296
+ export function isError(value) {
297
+ const { isError: isErrorFn } = Error;
298
+ return typeof isErrorFn === 'function'
299
+ ? isErrorFn(value)
300
+ : value instanceof Error;
301
+ }
302
+ export function isLikeNode(value) {
303
+ return isObject(value);
304
+ }
@@ -5,4 +5,3 @@ interface IconInfo {
5
5
  }
6
6
  export declare function registerGetHelpPrompt(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
7
7
  export {};
8
- //# sourceMappingURL=prompts.d.ts.map
@@ -20,7 +20,7 @@ export function registerGetHelpPrompt(server, instructions, iconInfo) {
20
20
  description,
21
21
  messages: [
22
22
  {
23
- role: 'user',
23
+ role: 'assistant',
24
24
  content: {
25
25
  type: 'text',
26
26
  text: instructions,
@@ -29,4 +29,3 @@ export function registerGetHelpPrompt(server, instructions, iconInfo) {
29
29
  ],
30
30
  }));
31
31
  }
32
- //# sourceMappingURL=prompts.js.map
@@ -6,4 +6,3 @@ interface IconInfo {
6
6
  export declare function registerInstructionResource(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
7
7
  export declare function registerCacheResourceTemplate(server: McpServer, iconInfo?: IconInfo): void;
8
8
  export {};
9
- //# sourceMappingURL=resources.d.ts.map
@@ -1,16 +1,23 @@
1
1
  import { ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { ErrorCode, McpError, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
3
- import { get as getCacheEntry, getEntryMeta, keys as listCacheKeys, onCacheUpdate, parseCachedPayload, parseCacheKey, resolveCachedPayloadContent, } from './cache.js';
4
- import { logWarn } from './observability.js';
5
- import { isObject } from './type-guards.js';
3
+ import { get as getCacheEntry, getEntryMeta, keys as listCacheKeys, onCacheUpdate, parseCachedPayload, parseCacheKey, resolveCachedPayloadContent, } from '../lib/core.js';
4
+ import { logWarn } from '../lib/core.js';
5
+ import { registerServerLifecycleCleanup } from '../lib/mcp-tools.js';
6
+ import { RESOURCE_NOT_FOUND_ERROR_CODE } from '../lib/utils.js';
7
+ import { isObject } from '../lib/utils.js';
6
8
  const CACHE_RESOURCE_TEMPLATE_URI = 'internal://cache/{namespace}/{hash}';
7
9
  const CACHE_RESOURCE_PREFIX = 'internal://cache/';
8
10
  const CACHE_NAMESPACE_PATTERN = /^[a-z0-9_-]{1,64}$/i;
9
11
  const CACHE_HASH_PATTERN = /^[a-f0-9.]{8,64}$/i;
10
- // -32002 is the MCP extension code for resource-not-found. The SDK ErrorCode enum
11
- // does not export this value, so it is defined locally.
12
- const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
13
12
  const MAX_COMPLETION_VALUES = 100;
13
+ function normalizeCompletionPrefix(value) {
14
+ return value.trim().toLowerCase();
15
+ }
16
+ function sortAndLimitValues(values) {
17
+ return [...values]
18
+ .sort((left, right) => left.localeCompare(right))
19
+ .slice(0, MAX_COMPLETION_VALUES);
20
+ }
14
21
  function buildOptionalIcons(iconInfo) {
15
22
  if (!iconInfo)
16
23
  return {};
@@ -51,17 +58,20 @@ function firstVariableValue(value) {
51
58
  }
52
59
  return undefined;
53
60
  }
54
- function parseCacheResourceFromVariables(variables) {
55
- const namespace = firstVariableValue(variables['namespace']);
56
- const hash = firstVariableValue(variables['hash']);
57
- if (!namespace || !hash)
58
- return null;
61
+ function validateCacheResourceParts(namespace, hash) {
59
62
  const decoded = {
60
63
  namespace: decodeSegment(namespace),
61
64
  hash: decodeSegment(hash),
62
65
  };
63
66
  return isValidCacheResourceParts(decoded) ? decoded : null;
64
67
  }
68
+ function parseCacheResourceFromVariables(variables) {
69
+ const namespace = firstVariableValue(variables['namespace']);
70
+ const hash = firstVariableValue(variables['hash']);
71
+ if (!namespace || !hash)
72
+ return null;
73
+ return validateCacheResourceParts(namespace, hash);
74
+ }
65
75
  function parseCacheResourceFromUri(uri) {
66
76
  if (!uri.href.startsWith(CACHE_RESOURCE_PREFIX))
67
77
  return null;
@@ -75,50 +85,75 @@ function parseCacheResourceFromUri(uri) {
75
85
  const hash = segments[1];
76
86
  if (!namespace || !hash)
77
87
  return null;
78
- const decoded = {
79
- namespace: decodeSegment(namespace),
80
- hash: decodeSegment(hash),
81
- };
82
- return isValidCacheResourceParts(decoded) ? decoded : null;
88
+ return validateCacheResourceParts(namespace, hash);
83
89
  }
84
90
  function toCacheResourceUri(parts) {
85
91
  const namespace = encodeURIComponent(parts.namespace);
86
92
  const hash = encodeURIComponent(parts.hash);
87
93
  return `${CACHE_RESOURCE_PREFIX}${namespace}/${hash}`;
88
94
  }
89
- function listCacheNamespaces() {
90
- const namespaces = new Set();
91
- for (const key of listCacheKeys()) {
92
- const parsed = parseCacheKey(key);
93
- if (!parsed)
94
- continue;
95
- namespaces.add(parsed.namespace);
95
+ class CompletionIndex {
96
+ namespaces = null;
97
+ hashIndex = null;
98
+ invalidate() {
99
+ this.namespaces = null;
100
+ this.hashIndex = null;
101
+ }
102
+ getNamespaces() {
103
+ if (!this.namespaces) {
104
+ const seen = new Set();
105
+ for (const key of listCacheKeys()) {
106
+ const parsed = parseCacheKey(key);
107
+ if (parsed)
108
+ seen.add(parsed.namespace);
109
+ }
110
+ this.namespaces = [...seen].sort((a, b) => a.localeCompare(b));
111
+ }
112
+ return this.namespaces;
113
+ }
114
+ getHashes(namespace) {
115
+ if (!this.hashIndex) {
116
+ const index = new Map();
117
+ for (const key of listCacheKeys()) {
118
+ const parsed = parseCacheKey(key);
119
+ if (!parsed)
120
+ continue;
121
+ let set = index.get(parsed.namespace);
122
+ if (!set) {
123
+ set = new Set();
124
+ index.set(parsed.namespace, set);
125
+ }
126
+ set.add(parsed.urlHash);
127
+ }
128
+ this.hashIndex = new Map();
129
+ for (const [ns, set] of index) {
130
+ this.hashIndex.set(ns, [...set].sort((a, b) => a.localeCompare(b)));
131
+ }
132
+ }
133
+ if (namespace)
134
+ return this.hashIndex.get(namespace) ?? [];
135
+ const all = [];
136
+ for (const hashes of this.hashIndex.values()) {
137
+ all.push(...hashes);
138
+ }
139
+ return all.sort((a, b) => a.localeCompare(b));
96
140
  }
97
- return [...namespaces].sort((left, right) => left.localeCompare(right));
98
141
  }
142
+ const completionIndex = new CompletionIndex();
99
143
  function completeCacheNamespaces(value) {
100
- const normalized = value.trim().toLowerCase();
101
- return listCacheNamespaces()
102
- .filter((namespace) => namespace.toLowerCase().startsWith(normalized))
103
- .slice(0, MAX_COMPLETION_VALUES);
144
+ const normalized = normalizeCompletionPrefix(value);
145
+ const namespaces = completionIndex
146
+ .getNamespaces()
147
+ .filter((ns) => ns.toLowerCase().startsWith(normalized));
148
+ return sortAndLimitValues(namespaces);
104
149
  }
105
150
  function completeCacheHashes(value, context) {
106
- const normalized = value.trim().toLowerCase();
151
+ const normalized = normalizeCompletionPrefix(value);
107
152
  const namespace = context?.arguments?.['namespace']?.trim();
108
- const hashes = new Set();
109
- for (const key of listCacheKeys()) {
110
- const parsed = parseCacheKey(key);
111
- if (!parsed)
112
- continue;
113
- if (namespace && parsed.namespace !== namespace)
114
- continue;
115
- if (!parsed.urlHash.toLowerCase().startsWith(normalized))
116
- continue;
117
- hashes.add(parsed.urlHash);
118
- }
119
- return [...hashes]
120
- .sort((left, right) => left.localeCompare(right))
121
- .slice(0, MAX_COMPLETION_VALUES);
153
+ const hashes = completionIndex
154
+ .getHashes(namespace)
155
+ .filter((h) => h.toLowerCase().startsWith(normalized));
156
+ return sortAndLimitValues(hashes);
122
157
  }
123
158
  function listCacheResources() {
124
159
  const resources = listCacheKeys()
@@ -158,7 +193,14 @@ function normalizeSubscriptionUri(uri) {
158
193
  return toCacheResourceUri(cacheParts);
159
194
  return parsedUri.href;
160
195
  }
196
+ // Must only be called once per server instance; duplicate calls would register
197
+ // redundant Subscribe/Unsubscribe handlers and create a second onCacheUpdate
198
+ // listener, causing duplicate notifications.
199
+ let cacheNotificationsRegistered = false;
161
200
  function registerCacheResourceNotifications(server) {
201
+ if (cacheNotificationsRegistered)
202
+ return;
203
+ cacheNotificationsRegistered = true;
162
204
  const subscribedResourceUris = new Set();
163
205
  const setSubscription = (uri, subscribed) => {
164
206
  const normalized = normalizeSubscriptionUri(uri);
@@ -178,6 +220,7 @@ function registerCacheResourceNotifications(server) {
178
220
  return Promise.resolve({});
179
221
  });
180
222
  const unsubscribe = onCacheUpdate((event) => {
223
+ completionIndex.invalidate();
181
224
  const changedUri = toCacheResourceUri({
182
225
  namespace: event.namespace,
183
226
  hash: event.urlHash,
@@ -210,26 +253,7 @@ function registerCacheResourceNotifications(server) {
210
253
  cleanedUp = true;
211
254
  unsubscribe();
212
255
  };
213
- // Both patches are intentional and handle distinct disconnection scenarios:
214
- // • `server.server.onclose` fires when the transport closes unexpectedly
215
- // (e.g., client drops the connection) — the SDK calls it directly from
216
- // `_onclose()` without going through `server.close()`.
217
- // • `server.close()` fires when the application shuts the server down
218
- // gracefully. The `server.close()` path also eventually triggers `onclose`,
219
- // so the `cleanedUp` guard below prevents double-invocation.
220
- const originalOnClose = server.server.onclose;
221
- server.server.onclose = () => {
222
- cleanup();
223
- originalOnClose?.();
224
- };
225
- // FIXME: Monkey-patching server.close is fragile if other modules wrap it.
226
- // A proper fix requires a first-class lifecycle hook or cleanup registration API
227
- // in the SDK. Until then, the cleanedUp guard prevents double-invocation.
228
- const originalClose = server.close.bind(server);
229
- server.close = async () => {
230
- cleanup();
231
- await originalClose();
232
- };
256
+ registerServerLifecycleCleanup(server, cleanup);
233
257
  }
234
258
  function normalizeTemplateVariables(variables) {
235
259
  if (!isObject(variables))
@@ -317,4 +341,3 @@ export function registerCacheResourceTemplate(server, iconInfo) {
317
341
  }, (uri, variables) => readCacheResource(uri, normalizeTemplateVariables(variables)));
318
342
  registerCacheResourceNotifications(server);
319
343
  }
320
- //# sourceMappingURL=resources.js.map
@@ -1,2 +1 @@
1
1
  export declare function buildServerInstructions(): string;
2
- //# sourceMappingURL=instructions.d.ts.map
@@ -1,5 +1,5 @@
1
- import { config } from './config.js';
2
- import { FETCH_URL_TOOL_NAME } from './tools.js';
1
+ import { config } from '../lib/core.js';
2
+ import { FETCH_URL_TOOL_NAME } from '../tools/fetch-url.js';
3
3
  export function buildServerInstructions() {
4
4
  const maxHtmlSizeMb = config.constants.maxHtmlSize / 1024 / 1024;
5
5
  const cacheSizeMb = config.cache.maxSizeBytes / 1024 / 1024;
@@ -24,10 +24,13 @@ export function buildServerInstructions() {
24
24
  - Blocked: localhost, private IPs (10.x, 172.16-31.x, 192.168.x), metadata endpoints (169.254.169.254), .local/.internal.
25
25
  - Limits: Max HTML ${maxHtmlSizeMb}MB. Max ${config.fetcher.maxRedirects} redirects.
26
26
  - Cache: ${config.cache.maxKeys} entries, ${cacheSizeMb}MB, ${cacheTtlHours}h TTL.
27
+ - Cache scope: process-local and ephemeral. Cache URIs are invalid across server restarts or separate CLI invocations (-32002).
27
28
  - No JS: Client-side rendered pages may be incomplete.
28
29
  - Binary: Not supported.
29
30
  - Batch JSON-RPC: Array requests (\`[{...}]\`) are rejected with HTTP 400.
30
31
  - Tasks API: Experimental (SDK v1.26). \`tasks/get\`, \`tasks/result\`, \`tasks/list\`, \`tasks/cancel\` may change.
32
+ - Notifications: Optional non-spec extension. Set \`TASKS_STATUS_NOTIFICATIONS=true\` to emit \`notifications/tasks/status\`.
33
+ - URI scheme: \`internal://\` is a server-scoped custom scheme for cache resources. Not IANA-registered; valid only within this server's session lifetime.
31
34
  </constraints>
32
35
 
33
36
  <error_handling>
@@ -38,4 +41,3 @@ export function buildServerInstructions() {
38
41
  - queue_full: Worker pool busy. Wait and retry, or use task mode.
39
42
  </error_handling>`;
40
43
  }
41
- //# sourceMappingURL=instructions.js.map
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ export declare const fetchUrlInputSchema: z.ZodObject<{
3
+ url: z.ZodURL;
4
+ skipNoiseRemoval: z.ZodOptional<z.ZodBoolean>;
5
+ forceRefresh: z.ZodOptional<z.ZodBoolean>;
6
+ maxInlineChars: z.ZodOptional<z.ZodNumber>;
7
+ }, z.core.$strict>;
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import { config } from '../lib/core.js';
3
+ export const fetchUrlInputSchema = z.strictObject({
4
+ url: z
5
+ .httpUrl()
6
+ .min(1)
7
+ .max(config.constants.maxUrlLength)
8
+ .describe(`Target URL. Max ${config.constants.maxUrlLength} chars.`),
9
+ skipNoiseRemoval: z
10
+ .boolean()
11
+ .optional()
12
+ .describe('Preserve navigation/footers (disable noise filtering).'),
13
+ forceRefresh: z
14
+ .boolean()
15
+ .optional()
16
+ .describe('Bypass cache and fetch fresh content.'),
17
+ maxInlineChars: z
18
+ .number()
19
+ .int()
20
+ .min(0)
21
+ .max(config.constants.maxHtmlSize)
22
+ .optional()
23
+ .describe(`Inline markdown limit (0-${config.constants.maxHtmlSize}, 0=unlimited). Lower of this or global limit applies.`),
24
+ });