@j0hanz/fetch-url-mcp 1.9.1 → 1.9.2

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 (41) hide show
  1. package/dist/http/auth.d.ts +0 -1
  2. package/dist/http/auth.d.ts.map +1 -1
  3. package/dist/http/auth.js +1 -13
  4. package/dist/http/native.d.ts.map +1 -1
  5. package/dist/http/native.js +2 -5
  6. package/dist/lib/content.d.ts.map +1 -1
  7. package/dist/lib/content.js +301 -350
  8. package/dist/lib/core.d.ts +78 -71
  9. package/dist/lib/core.d.ts.map +1 -1
  10. package/dist/lib/core.js +308 -372
  11. package/dist/lib/fetch-pipeline.d.ts +2 -6
  12. package/dist/lib/fetch-pipeline.d.ts.map +1 -1
  13. package/dist/lib/fetch-pipeline.js +51 -137
  14. package/dist/lib/http.d.ts.map +1 -1
  15. package/dist/lib/http.js +188 -130
  16. package/dist/lib/mcp-tools.d.ts +3 -5
  17. package/dist/lib/mcp-tools.d.ts.map +1 -1
  18. package/dist/lib/mcp-tools.js +22 -58
  19. package/dist/lib/task-handlers.js +4 -4
  20. package/dist/lib/utils.d.ts +6 -0
  21. package/dist/lib/utils.d.ts.map +1 -1
  22. package/dist/lib/utils.js +23 -0
  23. package/dist/resources/index.js +1 -1
  24. package/dist/schemas.d.ts +0 -1
  25. package/dist/schemas.d.ts.map +1 -1
  26. package/dist/schemas.js +4 -6
  27. package/dist/server.js +1 -1
  28. package/dist/tasks/owner.d.ts +1 -1
  29. package/dist/tasks/owner.d.ts.map +1 -1
  30. package/dist/tasks/tool-registry.d.ts +1 -1
  31. package/dist/tasks/tool-registry.d.ts.map +1 -1
  32. package/dist/tools/fetch-url.d.ts +2 -3
  33. package/dist/tools/fetch-url.d.ts.map +1 -1
  34. package/dist/tools/fetch-url.js +89 -152
  35. package/dist/transform/transform.d.ts +8 -0
  36. package/dist/transform/transform.d.ts.map +1 -1
  37. package/dist/transform/transform.js +109 -108
  38. package/dist/transform/worker-pool.d.ts +3 -6
  39. package/dist/transform/worker-pool.d.ts.map +1 -1
  40. package/dist/transform/worker-pool.js +148 -118
  41. package/package.json +2 -1
package/dist/lib/http.js CHANGED
@@ -18,6 +18,9 @@ import { get as cacheGet, config, getOperationId, getRequestId, logDebug, logErr
18
18
  import { BLOCKED_HOST_SUFFIXES, createDnsPreflight, IpBlocker, RawUrlTransformer, SafeDnsResolver, UrlNormalizer, VALIDATION_ERROR_CODE, } from './url.js';
19
19
  import { createErrorWithCode, FetchError, isAbortError, isError, isObject, isSystemError, toError, } from './utils.js';
20
20
  import { formatZodError } from './zod.js';
21
+ // ═══════════════════════════════════════════════════════════════════
22
+ // FILENAME GENERATION & DOWNLOAD
23
+ // ═══════════════════════════════════════════════════════════════════
21
24
  const FILENAME_RULES = {
22
25
  MAX_LEN: 200,
23
26
  UNSAFE_CHARS: /[<>:"/\\|?*\p{C}]/gu,
@@ -113,6 +116,9 @@ export function handleDownload(res, namespace, hash) {
113
116
  res.setHeader('X-Content-Type-Options', 'nosniff');
114
117
  res.end(content);
115
118
  }
119
+ // ═══════════════════════════════════════════════════════════════════
120
+ // ENCODING DETECTION
121
+ // ═══════════════════════════════════════════════════════════════════
116
122
  const UTF8_ENCODING = 'utf-8';
117
123
  function getCharsetFromContentType(contentType) {
118
124
  if (!contentType)
@@ -152,14 +158,22 @@ function isUnicodeWideEncoding(encoding) {
152
158
  normalized === 'unicodefffe' ||
153
159
  normalized === 'unicodefeff');
154
160
  }
155
- const BOM_SIGNATURES = [
156
- // 4-byte BOMs must come first to avoid false matches with 2-byte prefixes
161
+ const BOM_ENTRIES = [
162
+ // 4-byte BOMs must come before shorter prefixes to avoid false matches
157
163
  { bytes: [0xff, 0xfe, 0x00, 0x00], encoding: 'utf-32le' },
158
164
  { bytes: [0x00, 0x00, 0xfe, 0xff], encoding: 'utf-32be' },
159
165
  { bytes: [0xef, 0xbb, 0xbf], encoding: 'utf-8' },
160
166
  { bytes: [0xff, 0xfe], encoding: 'utf-16le' },
161
167
  { bytes: [0xfe, 0xff], encoding: 'utf-16be' },
162
168
  ];
169
+ const BOM_BY_FIRST_BYTE = new Map();
170
+ for (const entry of BOM_ENTRIES) {
171
+ const key = entry.bytes[0];
172
+ if (key === undefined)
173
+ continue;
174
+ const existing = BOM_BY_FIRST_BYTE.get(key);
175
+ BOM_BY_FIRST_BYTE.set(key, existing ? [...existing, entry] : [entry]);
176
+ }
163
177
  function startsWithBytes(buffer, signature) {
164
178
  const sigLen = signature.length;
165
179
  if (buffer.length < sigLen)
@@ -171,7 +185,15 @@ function startsWithBytes(buffer, signature) {
171
185
  return true;
172
186
  }
173
187
  function detectBomEncoding(buffer) {
174
- for (const { bytes, encoding } of BOM_SIGNATURES) {
188
+ if (buffer.length === 0)
189
+ return undefined;
190
+ const first = buffer[0];
191
+ if (first === undefined)
192
+ return undefined;
193
+ const candidates = BOM_BY_FIRST_BYTE.get(first);
194
+ if (!candidates)
195
+ return undefined;
196
+ for (const { bytes, encoding } of candidates) {
175
197
  if (startsWithBytes(buffer, bytes))
176
198
  return encoding;
177
199
  }
@@ -231,6 +253,9 @@ function resolveEncoding(declaredEncoding, sample) {
231
253
  return declaredEncoding;
232
254
  return detectHtmlDeclaredEncoding(sample);
233
255
  }
256
+ // ═══════════════════════════════════════════════════════════════════
257
+ // BINARY DETECTION
258
+ // ═══════════════════════════════════════════════════════════════════
234
259
  const BINARY_SIGNATURES = [
235
260
  [0x25, 0x50, 0x44, 0x46],
236
261
  [0x89, 0x50, 0x4e, 0x47],
@@ -275,6 +300,9 @@ function isBinaryContent(buffer, encoding) {
275
300
  }
276
301
  return !isUnicodeWideEncoding(encoding) && hasNullByte(buffer, 1000);
277
302
  }
303
+ // ═══════════════════════════════════════════════════════════════════
304
+ // FETCH ERRORS
305
+ // ═══════════════════════════════════════════════════════════════════
278
306
  function parseRetryAfter(header) {
279
307
  if (!header)
280
308
  return 60;
@@ -357,6 +385,9 @@ function mapFetchError(error, fallbackUrl, timeoutMs) {
357
385
  }
358
386
  return createFetchError({ kind: 'network', message: error.message }, url);
359
387
  }
388
+ // ═══════════════════════════════════════════════════════════════════
389
+ // REDIRECT FOLLOWING
390
+ // ═══════════════════════════════════════════════════════════════════
360
391
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
361
392
  function isRedirectStatus(status) {
362
393
  return REDIRECT_STATUSES.has(status);
@@ -503,6 +534,9 @@ class RedirectFollower {
503
534
  }
504
535
  }
505
536
  }
537
+ // ═══════════════════════════════════════════════════════════════════
538
+ // CONTENT VALIDATION & DECOMPRESSION
539
+ // ═══════════════════════════════════════════════════════════════════
506
540
  function resolveResponseError(response, finalUrl) {
507
541
  if (response.status === 429) {
508
542
  return createFetchError({ kind: 'rate-limited', retryAfter: response.headers.get('retry-after') }, finalUrl);
@@ -633,27 +667,10 @@ function createPumpedStream(initialChunk, reader) {
633
667
  },
634
668
  });
635
669
  }
636
- async function decodeResponseIfNeeded(response, url, signal) {
637
- const encodingHeader = response.headers.get('content-encoding');
638
- const parsedEncodings = parseContentEncodings(encodingHeader);
639
- if (!parsedEncodings)
640
- return response;
641
- const encodings = parsedEncodings.filter((token) => token !== 'identity');
642
- if (encodings.length === 0)
643
- return response;
644
- for (const encoding of encodings) {
645
- if (!isSupportedContentEncoding(encoding)) {
646
- throw createUnsupportedContentEncodingError(url, encodingHeader ?? encoding);
647
- }
648
- }
649
- if (!response.body)
650
- return response;
651
- const [decodeBranch, passthroughBranch] = response.body.tee();
652
- const decodeOrder = encodings
653
- .slice()
654
- .reverse()
655
- .filter(isSupportedContentEncoding);
656
- const decompressors = decodeOrder.map((encoding) => createDecompressor(encoding));
670
+ function buildDecodePipeline(body, encodings, url, response, signal) {
671
+ const [decodeBranch, passthroughBranch] = body.tee();
672
+ const decodeOrder = encodings.slice().reverse();
673
+ const decompressors = decodeOrder.map((enc) => createDecompressor(enc));
657
674
  const decodeSource = Readable.fromWeb(toNodeReadableStream(decodeBranch, url, 'response:decode-content-encoding'));
658
675
  const decodedNodeStream = new PassThrough();
659
676
  const decodedPipeline = pipeline([
@@ -664,7 +681,7 @@ async function decodeResponseIfNeeded(response, url, signal) {
664
681
  const headers = new Headers(response.headers);
665
682
  headers.delete('content-encoding');
666
683
  headers.delete('content-length');
667
- const abortDecodePipeline = () => {
684
+ const cleanup = () => {
668
685
  decodeSource.destroy();
669
686
  for (const decompressor of decompressors) {
670
687
  decompressor.destroy();
@@ -672,33 +689,57 @@ async function decodeResponseIfNeeded(response, url, signal) {
672
689
  decodedNodeStream.destroy();
673
690
  };
674
691
  if (signal) {
675
- signal.addEventListener('abort', abortDecodePipeline, { once: true });
692
+ signal.addEventListener('abort', cleanup, { once: true });
676
693
  }
677
694
  void decodedPipeline.catch((error) => {
678
695
  decodedNodeStream.destroy(toError(error));
679
696
  });
680
697
  const decodedBodyStream = toWebReadableStream(decodedNodeStream, url, 'response:decode-content-encoding');
681
698
  const decodedReader = decodedBodyStream.getReader();
699
+ return {
700
+ decodedReader,
701
+ passthroughBranch,
702
+ decodedNodeStream,
703
+ headers,
704
+ cleanup,
705
+ };
706
+ }
707
+ async function decodeResponseIfNeeded(response, url, signal) {
708
+ const encodingHeader = response.headers.get('content-encoding');
709
+ const parsedEncodings = parseContentEncodings(encodingHeader);
710
+ if (!parsedEncodings)
711
+ return response;
712
+ const encodings = parsedEncodings.filter((token) => token !== 'identity');
713
+ if (encodings.length === 0)
714
+ return response;
715
+ for (const encoding of encodings) {
716
+ if (!isSupportedContentEncoding(encoding)) {
717
+ throw createUnsupportedContentEncodingError(url, encodingHeader ?? encoding);
718
+ }
719
+ }
720
+ if (!response.body)
721
+ return response;
722
+ const pipe = buildDecodePipeline(response.body, encodings.filter(isSupportedContentEncoding), url, response, signal);
682
723
  const clearAbortListener = () => {
683
724
  if (!signal)
684
725
  return;
685
- signal.removeEventListener('abort', abortDecodePipeline);
726
+ signal.removeEventListener('abort', pipe.cleanup);
686
727
  };
687
728
  try {
688
- const first = await decodedReader.read();
729
+ const first = await pipe.decodedReader.read();
689
730
  if (first.done) {
690
731
  clearAbortListener();
691
- void passthroughBranch.cancel().catch(() => undefined);
732
+ void pipe.passthroughBranch.cancel().catch(() => undefined);
692
733
  return new Response(null, {
693
734
  status: response.status,
694
735
  statusText: response.statusText,
695
- headers,
736
+ headers: pipe.headers,
696
737
  });
697
738
  }
698
- void passthroughBranch.cancel().catch(() => undefined);
699
- const body = createPumpedStream(first.value, decodedReader);
739
+ void pipe.passthroughBranch.cancel().catch(() => undefined);
740
+ const body = createPumpedStream(first.value, pipe.decodedReader);
700
741
  if (signal) {
701
- void finished(decodedNodeStream, { cleanup: true })
742
+ void finished(pipe.decodedNodeStream, { cleanup: true })
702
743
  .catch(() => { })
703
744
  .finally(() => {
704
745
  clearAbortListener();
@@ -707,17 +748,76 @@ async function decodeResponseIfNeeded(response, url, signal) {
707
748
  return new Response(body, {
708
749
  status: response.status,
709
750
  statusText: response.statusText,
710
- headers,
751
+ headers: pipe.headers,
711
752
  });
712
753
  }
713
754
  catch (error) {
714
755
  clearAbortListener();
715
- abortDecodePipeline();
716
- void decodedReader.cancel(error).catch(() => undefined);
717
- void passthroughBranch.cancel().catch(() => undefined);
756
+ pipe.cleanup();
757
+ void pipe.decodedReader.cancel(error).catch(() => undefined);
758
+ void pipe.passthroughBranch.cancel().catch(() => undefined);
718
759
  throw new FetchError(`Content-Encoding decode failed for ${redactUrl(url)}: ${isError(error) ? error.message : String(error)}`, url);
719
760
  }
720
761
  }
762
+ // ═══════════════════════════════════════════════════════════════════
763
+ // RESPONSE READING
764
+ // ═══════════════════════════════════════════════════════════════════
765
+ class BoundedBufferTransform extends Transform {
766
+ byteLimit;
767
+ captureChunks;
768
+ url;
769
+ declaredEncoding;
770
+ total = 0;
771
+ chunks = [];
772
+ effectiveEncoding;
773
+ encodingResolved = false;
774
+ constructor(byteLimit, captureChunks, url, declaredEncoding) {
775
+ super();
776
+ this.byteLimit = byteLimit;
777
+ this.captureChunks = captureChunks;
778
+ this.url = url;
779
+ this.declaredEncoding = declaredEncoding;
780
+ this.effectiveEncoding = declaredEncoding ?? 'utf-8';
781
+ }
782
+ _transform(chunk, _encoding, callback) {
783
+ try {
784
+ const buf = Buffer.isBuffer(chunk)
785
+ ? chunk
786
+ : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
787
+ if (!this.encodingResolved) {
788
+ this.encodingResolved = true;
789
+ this.effectiveEncoding =
790
+ resolveEncoding(this.declaredEncoding, buf) ??
791
+ this.declaredEncoding ??
792
+ 'utf-8';
793
+ }
794
+ if (isBinaryContent(buf, this.effectiveEncoding)) {
795
+ callback(new FetchError('Detailed content type check failed: binary content detected', this.url, 500, { reason: 'binary_content_detected' }));
796
+ return;
797
+ }
798
+ const newTotal = this.total + buf.length;
799
+ if (newTotal > this.byteLimit) {
800
+ const remaining = this.byteLimit - this.total;
801
+ if (remaining > 0) {
802
+ const slice = buf.subarray(0, remaining);
803
+ this.total += remaining;
804
+ if (this.captureChunks)
805
+ this.chunks.push(slice);
806
+ this.push(slice);
807
+ }
808
+ callback(new MaxBytesError());
809
+ return;
810
+ }
811
+ this.total = newTotal;
812
+ if (this.captureChunks)
813
+ this.chunks.push(buf);
814
+ callback(null, buf);
815
+ }
816
+ catch (error) {
817
+ callback(toError(error));
818
+ }
819
+ }
820
+ }
721
821
  class ResponseTextReader {
722
822
  async read(response, url, maxBytes, signal, encoding) {
723
823
  const { buffer, encoding: effectiveEncoding, truncated, } = await this.readBuffer(response, url, maxBytes, signal, encoding);
@@ -738,27 +838,10 @@ class ResponseTextReader {
738
838
  if (signal?.aborted)
739
839
  throw createFetchError({ kind: 'canceled' }, url);
740
840
  const limit = maxBytes <= 0 ? Number.POSITIVE_INFINITY : maxBytes;
741
- let buffer;
742
- let truncated = false;
743
- try {
744
- // Try safe blob slicing if available (Node 18+) to avoid OOM
745
- const blob = await response.blob();
746
- if (Number.isFinite(limit) && blob.size > limit) {
747
- const sliced = blob.slice(0, limit);
748
- buffer = new Uint8Array(await sliced.arrayBuffer());
749
- truncated = true;
750
- }
751
- else {
752
- buffer = new Uint8Array(await blob.arrayBuffer());
753
- }
754
- }
755
- catch {
756
- // Fallback if blob() fails
757
- const arrayBuffer = await response.arrayBuffer();
758
- const length = Math.min(arrayBuffer.byteLength, limit);
759
- buffer = new Uint8Array(arrayBuffer, 0, length);
760
- truncated = Number.isFinite(limit) && arrayBuffer.byteLength > limit;
761
- }
841
+ const arrayBuffer = await response.arrayBuffer();
842
+ const truncated = Number.isFinite(limit) && arrayBuffer.byteLength > limit;
843
+ const length = truncated ? limit : arrayBuffer.byteLength;
844
+ const buffer = new Uint8Array(arrayBuffer, 0, length);
762
845
  const effectiveEncoding = resolveEncoding(encoding, buffer) ?? encoding ?? 'utf-8';
763
846
  if (isBinaryContent(buffer, effectiveEncoding)) {
764
847
  throw new FetchError('Detailed content type check failed: binary content detected', url, 500, { reason: 'binary_content_detected' });
@@ -773,49 +856,8 @@ class ResponseTextReader {
773
856
  async readStreamToBuffer(stream, url, maxBytes, signal, encoding) {
774
857
  const byteLimit = maxBytes <= 0 ? Number.POSITIVE_INFINITY : maxBytes;
775
858
  const captureChunks = byteLimit !== Number.POSITIVE_INFINITY;
776
- let effectiveEncoding = encoding ?? 'utf-8';
777
- let encodingResolved = false;
778
- let total = 0;
779
- const chunks = [];
780
859
  const source = Readable.fromWeb(toNodeReadableStream(stream, url, 'response:read-stream-buffer'));
781
- const guard = new Transform({
782
- transform(chunk, _encoding, callback) {
783
- try {
784
- const buf = Buffer.isBuffer(chunk)
785
- ? chunk
786
- : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
787
- if (!encodingResolved) {
788
- encodingResolved = true;
789
- effectiveEncoding =
790
- resolveEncoding(encoding, buf) ?? encoding ?? 'utf-8';
791
- }
792
- if (isBinaryContent(buf, effectiveEncoding)) {
793
- callback(new FetchError('Detailed content type check failed: binary content detected', url, 500, { reason: 'binary_content_detected' }));
794
- return;
795
- }
796
- const newTotal = total + buf.length;
797
- if (newTotal > byteLimit) {
798
- const remaining = byteLimit - total;
799
- if (remaining > 0) {
800
- const slice = buf.subarray(0, remaining);
801
- total += remaining;
802
- if (captureChunks)
803
- chunks.push(slice);
804
- this.push(slice);
805
- }
806
- callback(new MaxBytesError());
807
- return;
808
- }
809
- total = newTotal;
810
- if (captureChunks)
811
- chunks.push(buf);
812
- callback(null, buf);
813
- }
814
- catch (error) {
815
- callback(toError(error));
816
- }
817
- },
818
- });
860
+ const guard = new BoundedBufferTransform(byteLimit, captureChunks, url, encoding);
819
861
  const guarded = source.pipe(guard);
820
862
  const abortHandler = () => {
821
863
  source.destroy();
@@ -828,8 +870,8 @@ class ResponseTextReader {
828
870
  const buffer = await consumeBuffer(guarded);
829
871
  return {
830
872
  buffer,
831
- encoding: effectiveEncoding,
832
- size: total,
873
+ encoding: guard.effectiveEncoding,
874
+ size: guard.total,
833
875
  truncated: false,
834
876
  };
835
877
  }
@@ -842,9 +884,9 @@ class ResponseTextReader {
842
884
  source.destroy();
843
885
  guard.destroy();
844
886
  return {
845
- buffer: Buffer.concat(chunks, total),
846
- encoding: effectiveEncoding,
847
- size: total,
887
+ buffer: Buffer.concat(guard.chunks, guard.total),
888
+ encoding: guard.effectiveEncoding,
889
+ size: guard.total,
848
890
  truncated: true,
849
891
  };
850
892
  }
@@ -914,14 +956,6 @@ class FetchTelemetry {
914
956
  redact(url) {
915
957
  return this.redactor.redact(url);
916
958
  }
917
- contextFields(ctx) {
918
- return {
919
- ...(ctx.contextRequestId
920
- ? { contextRequestId: ctx.contextRequestId }
921
- : {}),
922
- ...(ctx.operationId ? { operationId: ctx.operationId } : {}),
923
- };
924
- }
925
959
  start(url, method) {
926
960
  const safeUrl = this.redactor.redact(url);
927
961
  const contextRequestId = this.context.getRequestId();
@@ -936,7 +970,12 @@ class FetchTelemetry {
936
970
  ctx.contextRequestId = contextRequestId;
937
971
  if (operationId)
938
972
  ctx.operationId = operationId;
939
- const ctxFields = this.contextFields(ctx);
973
+ const ctxFields = {
974
+ ...(ctx.contextRequestId
975
+ ? { contextRequestId: ctx.contextRequestId }
976
+ : {}),
977
+ ...(ctx.operationId ? { operationId: ctx.operationId } : {}),
978
+ };
940
979
  this.publish({
941
980
  v: 1,
942
981
  type: 'start',
@@ -956,7 +995,12 @@ class FetchTelemetry {
956
995
  recordResponse(context, response, contentSize) {
957
996
  const duration = performance.now() - context.startTime;
958
997
  const durationLabel = `${Math.round(duration)}ms`;
959
- const ctxFields = this.contextFields(context);
998
+ const ctxFields = {
999
+ ...(context.contextRequestId
1000
+ ? { contextRequestId: context.contextRequestId }
1001
+ : {}),
1002
+ ...(context.operationId ? { operationId: context.operationId } : {}),
1003
+ };
960
1004
  this.publish({
961
1005
  v: 1,
962
1006
  type: 'end',
@@ -991,7 +1035,12 @@ class FetchTelemetry {
991
1035
  const duration = performance.now() - context.startTime;
992
1036
  const err = toError(error);
993
1037
  const code = isSystemError(err) ? err.code : undefined;
994
- const ctxFields = this.contextFields(context);
1038
+ const ctxFields = {
1039
+ ...(context.contextRequestId
1040
+ ? { contextRequestId: context.contextRequestId }
1041
+ : {}),
1042
+ ...(context.operationId ? { operationId: context.operationId } : {}),
1043
+ };
995
1044
  this.publish({
996
1045
  v: 1,
997
1046
  type: 'error',
@@ -1060,7 +1109,7 @@ class HttpFetcher {
1060
1109
  }
1061
1110
  async fetchNormalized(normalizedUrl, mode, options) {
1062
1111
  const timeoutMs = this.fetcherConfig.timeout;
1063
- const headers = buildHeaders();
1112
+ const headers = DEFAULT_HEADERS;
1064
1113
  const signal = buildRequestSignal(timeoutMs, options?.signal);
1065
1114
  const init = buildRequestInit(headers, signal);
1066
1115
  const ctx = this.telemetry.start(normalizedUrl, 'GET');
@@ -1107,9 +1156,6 @@ const DEFAULT_HEADERS = {
1107
1156
  // The undici-based globalThis.fetch manages content negotiation and
1108
1157
  // decompression transparently per the Fetch spec.
1109
1158
  };
1110
- function buildHeaders() {
1111
- return DEFAULT_HEADERS;
1112
- }
1113
1159
  function buildRequestSignal(timeoutMs, external) {
1114
1160
  if (timeoutMs <= 0)
1115
1161
  return external;
@@ -1123,16 +1169,28 @@ function buildRequestInit(headers, signal) {
1123
1169
  ...(signal ? { signal } : {}),
1124
1170
  };
1125
1171
  }
1126
- const ipBlocker = new IpBlocker(config.security);
1127
- const urlNormalizer = new UrlNormalizer(config.constants, config.security, ipBlocker, BLOCKED_HOST_SUFFIXES);
1128
- const rawUrlTransformer = new RawUrlTransformer(defaultLogger);
1129
- const dnsResolver = new SafeDnsResolver(ipBlocker, config.security, BLOCKED_HOST_SUFFIXES);
1130
- const telemetry = new FetchTelemetry(defaultLogger, defaultContext, defaultRedactor);
1131
- const normalizeRedirectUrl = (url) => urlNormalizer.validateAndNormalize(url);
1132
- const dnsPreflight = createDnsPreflight(dnsResolver);
1133
- const secureRedirectFollower = new RedirectFollower(defaultFetch, normalizeRedirectUrl, dnsPreflight);
1134
- const responseReader = new ResponseTextReader();
1135
- const httpFetcher = new HttpFetcher(config.fetcher, secureRedirectFollower, responseReader, telemetry);
1172
+ function createHttpModule() {
1173
+ const ipBlocker = new IpBlocker(config.security);
1174
+ const urlNormalizer = new UrlNormalizer(config.constants, config.security, ipBlocker, BLOCKED_HOST_SUFFIXES);
1175
+ const rawUrlTransformer = new RawUrlTransformer(defaultLogger);
1176
+ const dnsResolver = new SafeDnsResolver(ipBlocker, config.security, BLOCKED_HOST_SUFFIXES);
1177
+ const tel = new FetchTelemetry(defaultLogger, defaultContext, defaultRedactor);
1178
+ const normalizeRedirectUrl = (url) => urlNormalizer.validateAndNormalize(url);
1179
+ const dnsPreflight = createDnsPreflight(dnsResolver);
1180
+ const secureRedirectFollower = new RedirectFollower(defaultFetch, normalizeRedirectUrl, dnsPreflight);
1181
+ const responseReader = new ResponseTextReader();
1182
+ const httpFetcher = new HttpFetcher(config.fetcher, secureRedirectFollower, responseReader, tel);
1183
+ return {
1184
+ ipBlocker,
1185
+ urlNormalizer,
1186
+ rawUrlTransformer,
1187
+ telemetry: tel,
1188
+ secureRedirectFollower,
1189
+ responseReader,
1190
+ httpFetcher,
1191
+ };
1192
+ }
1193
+ const { ipBlocker, urlNormalizer, rawUrlTransformer, telemetry, secureRedirectFollower, responseReader, httpFetcher, } = createHttpModule();
1136
1194
  export function isBlockedIp(ip) {
1137
1195
  return ipBlocker.isBlockedIp(ip);
1138
1196
  }
@@ -17,7 +17,7 @@ declare const jsonRpcResponseSchema: z.ZodUnion<readonly [z.ZodObject<{
17
17
  result: z.ZodRecord<z.ZodString, z.ZodUnknown>;
18
18
  }, z.core.$strict>, z.ZodObject<{
19
19
  jsonrpc: z.ZodLiteral<"2.0">;
20
- id: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
20
+ id: z.ZodOptional<z.ZodUnion<[z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodNull]>>;
21
21
  error: z.ZodObject<{
22
22
  code: z.ZodNumber;
23
23
  message: z.ZodString;
@@ -35,7 +35,7 @@ declare const jsonRpcMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
35
35
  result: z.ZodRecord<z.ZodString, z.ZodUnknown>;
36
36
  }, z.core.$strict>, z.ZodObject<{
37
37
  jsonrpc: z.ZodLiteral<"2.0">;
38
- id: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
38
+ id: z.ZodOptional<z.ZodUnion<[z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodNull]>>;
39
39
  error: z.ZodObject<{
40
40
  code: z.ZodNumber;
41
41
  message: z.ZodString;
@@ -60,7 +60,5 @@ export declare function createToolErrorResponse(message: string, url: string, ex
60
60
  details?: Record<string, unknown>;
61
61
  }): ToolErrorResponse;
62
62
  export declare function handleToolError(error: unknown, url: string, fallbackMessage?: string): ToolErrorResponse;
63
- export { registerServerLifecycleCleanup, registerTaskHandlers, cancelTasksForOwner, abortAllTaskExecutions, } from './task-handlers.js';
64
- export { readNestedRecord, withSignal, TRUNCATION_MARKER, type InlineContentResult, appendTruncationMarker, type PipelineResult, type SharedFetchStage, executeFetchPipeline, type MarkdownPipelineResult, parseCachedMarkdownResult, markdownTransform, serializeMarkdownResult, performSharedFetch, } from './fetch-pipeline.js';
65
- export { type ProgressNotificationParams, type ProgressNotification, type ToolHandlerExtra, type ProgressReporter, createProgressReporter, } from './progress.js';
63
+ export {};
66
64
  //# sourceMappingURL=mcp-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-tools.d.ts","sourceRoot":"","sources":["../../src/lib/mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAC/C,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,QAAA,MAAM,oBAAoB;;;;;kBAKxB,CAAC;AAeH,QAAA,MAAM,qBAAqB;;;;;;;;;;;;oBAGzB,CAAC;AACH,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;sBAGxB,CAAC;AACH,KAAK,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3D,KAAK,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACjE,KAAK,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC/D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAE5D;AACD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,cAAc,CAEtE;AACD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,GACZ,IAAI,IAAI,mBAAmB,CAE7B;AACD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,kBAAkB,CAE1E;AAaD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAG7E;AAeD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,OAAO,CAUT;AAMD,KAAK,iBAAiB,GAAG,cAAc,GAAG;IACxC,OAAO,EAAE,IAAI,CAAC;CACf,CAAC;AAsCF,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE;IACN,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,GACA,iBAAiB,CAenB;AAiCD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,MAAM,EACX,eAAe,SAAqB,GACnC,iBAAiB,CAYnB;AASD,OAAO,EACL,8BAA8B,EAC9B,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,oBAAoB,EACpB,KAAK,sBAAsB,EAC3B,yBAAyB,EACzB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,sBAAsB,GACvB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"mcp-tools.d.ts","sourceRoot":"","sources":["../../src/lib/mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAC/C,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,QAAA,MAAM,oBAAoB;;;;;kBAKxB,CAAC;AAeH,QAAA,MAAM,qBAAqB;;;;;;;;;;;;oBAGzB,CAAC;AACH,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;sBAGxB,CAAC;AACH,KAAK,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3D,KAAK,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACjE,KAAK,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC/D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAE5D;AACD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,cAAc,CAEtE;AACD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,GACZ,IAAI,IAAI,mBAAmB,CAE7B;AACD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,kBAAkB,CAE1E;AAUD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAG7E;AACD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,OAAO,CAUT;AAMD,KAAK,iBAAiB,GAAG,cAAc,GAAG;IACxC,OAAO,EAAE,IAAI,CAAC;CACf,CAAC;AA6BF,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE;IACN,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,GACA,iBAAiB,CAenB;AAQD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,MAAM,EACX,eAAe,SAAqB,GACnC,iBAAiB,CA2BnB"}
@@ -11,12 +11,12 @@ const jsonRpcRequestSchema = z.strictObject({
11
11
  });
12
12
  const jsonRpcResultResponseSchema = z.strictObject({
13
13
  jsonrpc: z.literal('2.0'),
14
- id: z.union([z.string(), z.number()]),
14
+ id: jsonRpcRequestIdSchema,
15
15
  result: z.record(z.string(), z.unknown()),
16
16
  });
17
17
  const jsonRpcErrorResponseSchema = z.strictObject({
18
18
  jsonrpc: z.literal('2.0'),
19
- id: z.union([z.string(), z.number(), z.null()]).optional(),
19
+ id: jsonRpcRequestIdSchema.or(z.null()).optional(),
20
20
  error: z.strictObject({
21
21
  code: z.number().int(),
22
22
  message: z.string(),
@@ -48,29 +48,19 @@ function parseAcceptMediaTypes(header) {
48
48
  return [];
49
49
  return header
50
50
  .split(',')
51
- .map((value) => extractAcceptMediaType(value.trim()))
52
- .filter((value) => value.length > 0);
53
- }
54
- function extractAcceptMediaType(value) {
55
- return value.split(';', 1)[0]?.trim().toLowerCase() ?? '';
51
+ .map((v) => v.split(';', 1)[0]?.trim().toLowerCase() ?? '')
52
+ .filter((v) => v.length > 0);
56
53
  }
57
54
  export function acceptsEventStream(header) {
58
55
  const mediaTypes = parseAcceptMediaTypes(header);
59
56
  return mediaTypes.some((mediaType) => mediaType === 'text/event-stream');
60
57
  }
61
- function hasAcceptedMediaType(mediaTypes, exact, wildcardPrefix) {
62
- return mediaTypes.some((mediaType) => typeof mediaType === 'string' &&
63
- mediaType.length > 0 &&
64
- (mediaType === '*/*' ||
65
- mediaType === exact ||
66
- mediaType === wildcardPrefix));
67
- }
68
58
  export function acceptsJsonAndEventStream(header) {
69
59
  const mediaTypes = parseAcceptMediaTypes(header);
70
- const acceptsJson = hasAcceptedMediaType(mediaTypes, 'application/json', 'application/*');
60
+ const acceptsJson = mediaTypes.some((m) => m === '*/*' || m === 'application/json' || m === 'application/*');
71
61
  if (!acceptsJson)
72
62
  return false;
73
- return hasAcceptedMediaType(mediaTypes, 'text/event-stream', 'text/*');
63
+ return mediaTypes.some((m) => m === '*/*' || m === 'text/event-stream' || m === 'text/*');
74
64
  }
75
65
  const PUBLIC_ERROR_REASONS = new Set(['aborted', 'queue_full', 'timeout']);
76
66
  function sanitizeToolErrorDetails(details) {
@@ -89,14 +79,6 @@ function sanitizeToolErrorDetails(details) {
89
79
  }
90
80
  return Object.keys(sanitized).length > 0 ? sanitized : undefined;
91
81
  }
92
- function resolvePublicFetchErrorCode(error) {
93
- const { code: detailsCode, reason } = error.details;
94
- if (typeof detailsCode === 'string')
95
- return detailsCode;
96
- if (reason === 'queue_full')
97
- return 'queue_full';
98
- return undefined;
99
- }
100
82
  export function createToolErrorResponse(message, url, extra) {
101
83
  const errorContent = {
102
84
  error: message,
@@ -117,47 +99,29 @@ function isValidationError(error) {
117
99
  isSystemError(error) &&
118
100
  error.code === 'VALIDATION_ERROR');
119
101
  }
120
- function isHandledToolError(error) {
121
- return error instanceof FetchError || isValidationError(error);
122
- }
123
- function resolveToolErrorMessage(error, fallbackMessage) {
124
- if (isHandledToolError(error)) {
125
- return error.message;
126
- }
127
- if (error instanceof Error) {
128
- return `${fallbackMessage}: ${error.message}`;
129
- }
130
- return `${fallbackMessage}: Unknown error`;
131
- }
132
- function resolveToolErrorCode(error) {
133
- if (error instanceof FetchError) {
134
- return resolvePublicFetchErrorCode(error) ?? error.code;
135
- }
136
- if (isValidationError(error))
137
- return 'VALIDATION_ERROR';
138
- if (isAbortError(error))
139
- return 'ABORTED';
140
- return 'FETCH_ERROR';
141
- }
142
102
  export function handleToolError(error, url, fallbackMessage = 'Operation failed') {
143
- const message = resolveToolErrorMessage(error, fallbackMessage);
144
- const code = resolveToolErrorCode(error);
145
103
  if (error instanceof FetchError) {
104
+ const { code: detailsCode, reason } = error.details;
105
+ const code = (typeof detailsCode === 'string'
106
+ ? detailsCode
107
+ : reason === 'queue_full'
108
+ ? 'queue_full'
109
+ : undefined) ?? error.code;
146
110
  const details = sanitizeToolErrorDetails(error.details);
147
- return createToolErrorResponse(message, url, {
111
+ return createToolErrorResponse(error.message, url, {
148
112
  code,
149
113
  statusCode: error.statusCode,
150
114
  ...(details ? { details } : {}),
151
115
  });
152
116
  }
117
+ if (isValidationError(error)) {
118
+ return createToolErrorResponse(error.message, url, {
119
+ code: 'VALIDATION_ERROR',
120
+ });
121
+ }
122
+ const code = isAbortError(error) ? 'ABORTED' : 'FETCH_ERROR';
123
+ const message = error instanceof Error
124
+ ? `${fallbackMessage}: ${error.message}`
125
+ : `${fallbackMessage}: Unknown error`;
153
126
  return createToolErrorResponse(message, url, { code });
154
127
  }
155
- /* -------------------------------------------------------------------------------------------------
156
- * Re-exports from split modules
157
- *
158
- * Preserves backward compatibility — consumers import from 'lib/mcp-tools.js'
159
- * without changes. Direct imports from the sub-modules are preferred for new code.
160
- * ------------------------------------------------------------------------------------------------- */
161
- export { registerServerLifecycleCleanup, registerTaskHandlers, cancelTasksForOwner, abortAllTaskExecutions, } from './task-handlers.js';
162
- export { readNestedRecord, withSignal, TRUNCATION_MARKER, appendTruncationMarker, executeFetchPipeline, parseCachedMarkdownResult, markdownTransform, serializeMarkdownResult, performSharedFetch, } from './fetch-pipeline.js';
163
- export { createProgressReporter, } from './progress.js';