@j0hanz/fetch-url-mcp 1.2.0 → 1.3.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 (184) hide show
  1. package/dist/cache.d.ts +9 -3
  2. package/dist/cache.d.ts.map +1 -0
  3. package/dist/cache.js +54 -119
  4. package/dist/cache.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +7 -4
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +2 -3
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +19 -27
  12. package/dist/config.js.map +1 -0
  13. package/dist/crypto.d.ts +1 -0
  14. package/dist/crypto.d.ts.map +1 -0
  15. package/dist/crypto.js +7 -3
  16. package/dist/crypto.js.map +1 -0
  17. package/dist/dom-noise-removal.d.ts +2 -1
  18. package/dist/dom-noise-removal.d.ts.map +1 -0
  19. package/dist/dom-noise-removal.js +9 -6
  20. package/dist/dom-noise-removal.js.map +1 -0
  21. package/dist/download.d.ts +4 -0
  22. package/dist/download.d.ts.map +1 -0
  23. package/dist/download.js +106 -0
  24. package/dist/download.js.map +1 -0
  25. package/dist/errors.d.ts +1 -0
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/errors.js +2 -1
  28. package/dist/errors.js.map +1 -0
  29. package/dist/examples/mcp-fetch-url-client.js +19 -3
  30. package/dist/examples/mcp-fetch-url-client.js.map +1 -1
  31. package/dist/fetch-content.d.ts +1 -0
  32. package/dist/fetch-content.d.ts.map +1 -0
  33. package/dist/fetch-content.js +15 -14
  34. package/dist/fetch-content.js.map +1 -0
  35. package/dist/fetch-stream.d.ts +1 -0
  36. package/dist/fetch-stream.d.ts.map +1 -0
  37. package/dist/fetch-stream.js +1 -0
  38. package/dist/fetch-stream.js.map +1 -0
  39. package/dist/fetch.d.ts +1 -0
  40. package/dist/fetch.d.ts.map +1 -0
  41. package/dist/fetch.js +123 -54
  42. package/dist/fetch.js.map +1 -0
  43. package/dist/host-normalization.d.ts +1 -0
  44. package/dist/host-normalization.d.ts.map +1 -0
  45. package/dist/host-normalization.js +22 -9
  46. package/dist/host-normalization.js.map +1 -0
  47. package/dist/http/auth.d.ts +51 -0
  48. package/dist/http/auth.d.ts.map +1 -0
  49. package/dist/http/auth.js +344 -0
  50. package/dist/http/auth.js.map +1 -0
  51. package/dist/http/health.d.ts +7 -0
  52. package/dist/http/health.d.ts.map +1 -0
  53. package/dist/http/health.js +156 -0
  54. package/dist/http/health.js.map +1 -0
  55. package/dist/http/helpers.d.ts +58 -0
  56. package/dist/http/helpers.d.ts.map +1 -0
  57. package/dist/http/helpers.js +370 -0
  58. package/dist/http/helpers.js.map +1 -0
  59. package/dist/{http-native.d.ts → http/native.d.ts} +1 -0
  60. package/dist/http/native.d.ts.map +1 -0
  61. package/dist/http/native.js +618 -0
  62. package/dist/http/native.js.map +1 -0
  63. package/dist/http/rate-limit.d.ts +13 -0
  64. package/dist/http/rate-limit.d.ts.map +1 -0
  65. package/dist/http/rate-limit.js +92 -0
  66. package/dist/http/rate-limit.js.map +1 -0
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +20 -14
  70. package/dist/index.js.map +1 -0
  71. package/dist/instructions.d.ts +2 -0
  72. package/dist/instructions.d.ts.map +1 -0
  73. package/dist/instructions.js +41 -0
  74. package/dist/instructions.js.map +1 -0
  75. package/dist/ip-blocklist.d.ts +1 -0
  76. package/dist/ip-blocklist.d.ts.map +1 -0
  77. package/dist/ip-blocklist.js +13 -8
  78. package/dist/ip-blocklist.js.map +1 -0
  79. package/dist/json.d.ts +2 -1
  80. package/dist/json.d.ts.map +1 -0
  81. package/dist/json.js +16 -6
  82. package/dist/json.js.map +1 -0
  83. package/dist/language-detection.d.ts +1 -0
  84. package/dist/language-detection.d.ts.map +1 -0
  85. package/dist/language-detection.js +2 -7
  86. package/dist/language-detection.js.map +1 -0
  87. package/dist/markdown-cleanup.d.ts +2 -1
  88. package/dist/markdown-cleanup.d.ts.map +1 -0
  89. package/dist/markdown-cleanup.js +52 -54
  90. package/dist/markdown-cleanup.js.map +1 -0
  91. package/dist/mcp-validator.d.ts +1 -0
  92. package/dist/mcp-validator.d.ts.map +1 -0
  93. package/dist/mcp-validator.js +20 -18
  94. package/dist/mcp-validator.js.map +1 -0
  95. package/dist/mcp.d.ts +2 -2
  96. package/dist/mcp.d.ts.map +1 -0
  97. package/dist/mcp.js +35 -344
  98. package/dist/mcp.js.map +1 -0
  99. package/dist/observability.d.ts +2 -0
  100. package/dist/observability.d.ts.map +1 -0
  101. package/dist/observability.js +32 -6
  102. package/dist/observability.js.map +1 -0
  103. package/dist/prompts.d.ts +1 -0
  104. package/dist/prompts.d.ts.map +1 -0
  105. package/dist/prompts.js +15 -3
  106. package/dist/prompts.js.map +1 -0
  107. package/dist/resources.d.ts +1 -0
  108. package/dist/resources.d.ts.map +1 -0
  109. package/dist/resources.js +46 -25
  110. package/dist/resources.js.map +1 -0
  111. package/dist/server-tuning.d.ts +1 -0
  112. package/dist/server-tuning.d.ts.map +1 -0
  113. package/dist/server-tuning.js +14 -17
  114. package/dist/server-tuning.js.map +1 -0
  115. package/dist/server.d.ts +1 -0
  116. package/dist/server.d.ts.map +1 -0
  117. package/dist/server.js +29 -35
  118. package/dist/server.js.map +1 -0
  119. package/dist/session.d.ts +2 -0
  120. package/dist/session.d.ts.map +1 -0
  121. package/dist/session.js +58 -29
  122. package/dist/session.js.map +1 -0
  123. package/dist/tasks/execution.d.ts +42 -0
  124. package/dist/tasks/execution.d.ts.map +1 -0
  125. package/dist/tasks/execution.js +241 -0
  126. package/dist/tasks/execution.js.map +1 -0
  127. package/dist/{tasks.d.ts → tasks/manager.d.ts} +12 -0
  128. package/dist/tasks/manager.d.ts.map +1 -0
  129. package/dist/{tasks.js → tasks/manager.js} +95 -43
  130. package/dist/tasks/manager.js.map +1 -0
  131. package/dist/tasks/owner.d.ts +32 -0
  132. package/dist/tasks/owner.d.ts.map +1 -0
  133. package/dist/tasks/owner.js +92 -0
  134. package/dist/tasks/owner.js.map +1 -0
  135. package/dist/timer-utils.d.ts +1 -0
  136. package/dist/timer-utils.d.ts.map +1 -0
  137. package/dist/timer-utils.js +8 -4
  138. package/dist/timer-utils.js.map +1 -0
  139. package/dist/tool-errors.d.ts +12 -0
  140. package/dist/tool-errors.d.ts.map +1 -0
  141. package/dist/tool-errors.js +55 -0
  142. package/dist/tool-errors.js.map +1 -0
  143. package/dist/tool-pipeline.d.ts +72 -0
  144. package/dist/tool-pipeline.d.ts.map +1 -0
  145. package/dist/tool-pipeline.js +408 -0
  146. package/dist/tool-pipeline.js.map +1 -0
  147. package/dist/tool-progress.d.ts +32 -0
  148. package/dist/tool-progress.d.ts.map +1 -0
  149. package/dist/tool-progress.js +129 -0
  150. package/dist/tool-progress.js.map +1 -0
  151. package/dist/tools.d.ts +35 -111
  152. package/dist/tools.d.ts.map +1 -0
  153. package/dist/tools.js +150 -610
  154. package/dist/tools.js.map +1 -0
  155. package/dist/{transform.d.ts → transform/transform.d.ts} +2 -1
  156. package/dist/transform/transform.d.ts.map +1 -0
  157. package/dist/{transform.js → transform/transform.js} +81 -771
  158. package/dist/transform/transform.js.map +1 -0
  159. package/dist/{transform-types.d.ts → transform/types.d.ts} +2 -0
  160. package/dist/transform/types.d.ts.map +1 -0
  161. package/dist/{transform-types.js → transform/types.js} +1 -0
  162. package/dist/transform/types.js.map +1 -0
  163. package/dist/transform/worker-pool.d.ts +93 -0
  164. package/dist/transform/worker-pool.d.ts.map +1 -0
  165. package/dist/transform/worker-pool.js +757 -0
  166. package/dist/transform/worker-pool.js.map +1 -0
  167. package/dist/transform/workers/transform-child.d.ts +2 -0
  168. package/dist/transform/workers/transform-child.d.ts.map +1 -0
  169. package/dist/{workers → transform/workers}/transform-child.js +17 -13
  170. package/dist/transform/workers/transform-child.js.map +1 -0
  171. package/dist/transform/workers/transform-worker.d.ts +2 -0
  172. package/dist/transform/workers/transform-worker.d.ts.map +1 -0
  173. package/dist/{workers → transform/workers}/transform-worker.js +16 -13
  174. package/dist/transform/workers/transform-worker.js.map +1 -0
  175. package/dist/type-guards.d.ts +1 -0
  176. package/dist/type-guards.d.ts.map +1 -0
  177. package/dist/type-guards.js +4 -4
  178. package/dist/type-guards.js.map +1 -0
  179. package/package.json +6 -7
  180. package/dist/AGENTS.md +0 -152
  181. package/dist/http-native.js +0 -1320
  182. package/dist/instructions.md +0 -113
  183. package/dist/workers/transform-child.d.ts +0 -1
  184. package/dist/workers/transform-worker.d.ts +0 -1
package/dist/tools.js CHANGED
@@ -2,400 +2,131 @@ import { randomUUID } from 'node:crypto';
2
2
  import { z } from 'zod';
3
3
  import * as cache from './cache.js';
4
4
  import { config } from './config.js';
5
- import { FetchError, getErrorMessage, isSystemError } from './errors.js';
6
- import { fetchNormalizedUrlBuffer, normalizeUrl, transformToRawUrl, } from './fetch.js';
5
+ import { generateSafeFilename } from './download.js';
7
6
  import { getRequestId, logDebug, logError, logWarn, runWithRequestContext, } from './observability.js';
8
- import { transformBufferToMarkdown } from './transform.js';
7
+ import { handleToolError } from './tool-errors.js';
8
+ import { appendTruncationMarker, markdownTransform, parseCachedMarkdownResult, performSharedFetch, readNestedRecord, readString, serializeMarkdownResult, TRUNCATION_MARKER, withSignal, } from './tool-pipeline.js';
9
+ import { createProgressReporter, } from './tool-progress.js';
9
10
  import { isObject } from './type-guards.js';
10
- const TRUNCATION_MARKER = '...[truncated]';
11
- const FETCH_PROGRESS_TOTAL = 4;
12
- const PROGRESS_NOTIFICATION_TIMEOUT_MS = 5000;
11
+ // Re-export public API so existing consumers keep working.
12
+ export { createToolErrorResponse, handleToolError } from './tool-errors.js';
13
+ export { executeFetchPipeline, parseCachedMarkdownResult, performSharedFetch, } from './tool-pipeline.js';
14
+ export { createProgressReporter, } from './tool-progress.js';
13
15
  export const fetchUrlInputSchema = z.strictObject({
14
16
  url: z
15
17
  .url({ protocol: /^https?$/i })
16
18
  .min(1)
17
19
  .max(config.constants.maxUrlLength)
18
- .describe('The URL of the webpage to fetch and convert to Markdown'),
20
+ .describe(`Target URL. Max ${config.constants.maxUrlLength} chars.`),
19
21
  skipNoiseRemoval: z
20
22
  .boolean()
21
23
  .optional()
22
- .describe('When true, preserves navigation, footers, and other elements normally filtered as noise'),
24
+ .describe('Preserve navigation/footers (disable noise filtering).'),
23
25
  forceRefresh: z
24
26
  .boolean()
25
27
  .optional()
26
- .describe('When true, bypasses the cache and fetches fresh content from the URL'),
28
+ .describe('Bypass cache and fetch fresh content.'),
27
29
  maxInlineChars: z
28
30
  .number()
29
31
  .int()
30
32
  .min(0)
31
33
  .max(config.constants.maxHtmlSize)
32
34
  .optional()
33
- .describe('Optional per-call inline markdown limit. 0 means unlimited. If a global inline limit is configured, the lower value is used.'),
35
+ .describe(`Inline markdown limit (0-${config.constants.maxHtmlSize}, 0=unlimited). Lower of this or global limit applies.`),
34
36
  });
35
- const fetchUrlOutputSchema = z.strictObject({
37
+ export const fetchUrlOutputSchema = z.strictObject({
36
38
  url: z
37
39
  .string()
38
40
  .min(1)
39
41
  .max(config.constants.maxUrlLength)
40
- .describe('The fetched URL'),
42
+ .describe('Fetched URL.'),
41
43
  inputUrl: z
42
44
  .string()
43
45
  .max(config.constants.maxUrlLength)
44
46
  .optional()
45
- .describe('The original URL provided by the caller'),
47
+ .describe('Original requested URL.'),
46
48
  resolvedUrl: z
47
49
  .string()
48
50
  .max(config.constants.maxUrlLength)
49
51
  .optional()
50
- .describe('The normalized or transformed URL that was fetched'),
52
+ .describe('Final URL after raw-content transformations.'),
51
53
  finalUrl: z
52
54
  .string()
53
55
  .max(config.constants.maxUrlLength)
54
56
  .optional()
55
- .describe('The final response URL after redirects'),
57
+ .describe('Final URL after HTTP redirects.'),
56
58
  cacheResourceUri: z
57
59
  .string()
58
60
  .max(config.constants.maxUrlLength)
59
61
  .optional()
60
- .describe('Internal cache resource URI for retrieving full markdown via resources/read'),
61
- title: z.string().max(512).optional().describe('Page title'),
62
+ .describe('URI for resources/read to get full markdown.'),
63
+ title: z.string().max(512).optional().describe('Page title.'),
62
64
  metadata: z
63
65
  .strictObject({
64
- title: z.string().max(512).optional().describe('Detected page title'),
66
+ title: z.string().max(512).optional().describe('Detected page title.'),
65
67
  description: z
66
68
  .string()
67
69
  .max(2048)
68
70
  .optional()
69
- .describe('Detected page description'),
70
- author: z.string().max(512).optional().describe('Detected page author'),
71
+ .describe('Detected page description.'),
72
+ author: z.string().max(512).optional().describe('Detected page author.'),
71
73
  image: z
72
74
  .string()
73
75
  .max(config.constants.maxUrlLength)
74
76
  .optional()
75
- .describe('Detected page preview image URL'),
77
+ .describe('Detected page preview image URL.'),
76
78
  favicon: z
77
79
  .string()
78
80
  .max(config.constants.maxUrlLength)
79
81
  .optional()
80
- .describe('Detected page favicon URL'),
82
+ .describe('Detected page favicon URL.'),
81
83
  publishedAt: z
82
84
  .string()
83
85
  .max(64)
84
86
  .optional()
85
- .describe('Detected publication date (if present)'),
87
+ .describe('Detected publication date.'),
86
88
  modifiedAt: z
87
89
  .string()
88
90
  .max(64)
89
91
  .optional()
90
- .describe('Detected last modified date (if present)'),
92
+ .describe('Detected last modified date.'),
91
93
  })
92
94
  .optional()
93
- .describe('Detected metadata extracted from page markup'),
95
+ .describe('Extracted page metadata.'),
94
96
  markdown: (config.constants.maxInlineContentChars > 0
95
97
  ? z.string().max(config.constants.maxInlineContentChars)
96
98
  : z.string())
97
99
  .optional()
98
- .describe('The extracted content in Markdown format'),
99
- fromCache: z
100
- .boolean()
101
- .optional()
102
- .describe('Whether this response was served from cache'),
103
- fetchedAt: z
104
- .string()
105
- .max(64)
106
- .optional()
107
- .describe('ISO timestamp of fetch/cache retrieval time'),
100
+ .describe('Extracted Markdown. May be truncated (check truncated field).'),
101
+ fromCache: z.boolean().optional().describe('True if served from cache.'),
102
+ fetchedAt: z.string().max(64).optional().describe('ISO timestamp of fetch.'),
108
103
  contentSize: z
109
104
  .number()
110
105
  .int()
111
106
  .min(0)
112
107
  .max(config.constants.maxHtmlSize * 4)
113
108
  .optional()
114
- .describe('Full markdown size in characters before inline truncation'),
115
- truncated: z
116
- .boolean()
117
- .optional()
118
- .describe('Whether the returned markdown was truncated'),
119
- error: z
120
- .string()
121
- .max(2048)
122
- .optional()
123
- .describe('Error message if the request failed'),
124
- statusCode: z
125
- .number()
126
- .int()
127
- .optional()
128
- .describe('HTTP status code for failed requests'),
129
- details: z
130
- .record(z.string(), z.unknown())
131
- .optional()
132
- .describe('Additional error details when available'),
109
+ .describe('Full markdown size before truncation.'),
110
+ truncated: z.boolean().optional().describe('True if markdown was truncated.'),
133
111
  });
134
112
  export const FETCH_URL_TOOL_NAME = 'fetch-url';
135
113
  const FETCH_URL_TOOL_DESCRIPTION = `
136
- Fetches a webpage and converts it to clean Markdown format optimized for LLM context.
137
-
138
- This tool is useful for:
139
- - Reading documentation, blog posts, or articles.
140
- - Extracting main content while removing navigation and ads (noise removal).
141
- - Caching content to speed up repeated queries.
142
-
143
- Limitations:
144
- - Inline output may be truncated when MAX_INLINE_CONTENT_CHARS is set.
145
- - Does not execute complex client-side JavaScript interactions.
114
+ <role>Web Content Extractor</role>
115
+ <task>Fetch public webpages and convert HTML to clean Markdown.</task>
116
+ <constraints>
117
+ - READ-ONLY. No JavaScript execution.
118
+ - GitHub/GitLab/Bitbucket URLs auto-transform to raw endpoints (check resolvedUrl).
119
+ - If truncated=true, use cacheResourceUri with resources/read for full content.
120
+ - For large pages/timeouts, use task mode (task: {}).
121
+ - If error queue_full, retry with task mode.
122
+ </constraints>
146
123
  `.trim();
147
- // Specific icon for the fetch-url tool (download cloud / web)
148
124
  const TOOL_ICON = {
149
125
  src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCIvPjxwb2x5bGluZSBwb2ludHM9IjcgMTAgMTIgMTUgMTcgMTAiLz48bGluZSB4MT0iMTIiIHkxPSIxNSIgeDI9IjEyIiB5Mj0iMyIvPjwvc3ZnPg==',
150
126
  mimeType: 'image/svg+xml',
151
127
  };
152
- function asRecord(value) {
153
- return isObject(value) ? value : undefined;
154
- }
155
- function readUnknown(obj, key) {
156
- const record = asRecord(obj);
157
- return record ? record[key] : undefined;
158
- }
159
- function readString(obj, key) {
160
- const value = readUnknown(obj, key);
161
- return typeof value === 'string' ? value : undefined;
162
- }
163
- function readNestedRecord(obj, keys) {
164
- let current = obj;
165
- for (const key of keys) {
166
- current = readUnknown(current, key);
167
- if (current === undefined)
168
- return undefined;
169
- }
170
- return asRecord(current);
171
- }
172
- function safeJsonParse(value) {
173
- try {
174
- return JSON.parse(value);
175
- }
176
- catch {
177
- return undefined;
178
- }
179
- }
180
- function withSignal(signal) {
181
- return signal === undefined ? {} : { signal };
182
- }
183
- function buildToolAbortSignal(extraSignal) {
184
- const { timeoutMs } = config.tools;
185
- if (timeoutMs <= 0)
186
- return extraSignal;
187
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
188
- if (!extraSignal)
189
- return timeoutSignal;
190
- return AbortSignal.any([extraSignal, timeoutSignal]);
191
- }
192
128
  /* -------------------------------------------------------------------------------------------------
193
- * Progress reporting
194
- * ------------------------------------------------------------------------------------------------- */
195
- function resolveRelatedTaskMeta(meta) {
196
- const related = readUnknown(meta, 'io.modelcontextprotocol/related-task');
197
- const taskId = readString(related, 'taskId');
198
- return taskId ? { taskId } : undefined;
199
- }
200
- class ToolProgressReporter {
201
- token;
202
- sendNotification;
203
- relatedTaskMeta;
204
- onProgress;
205
- reportQueue = Promise.resolve();
206
- constructor(token, sendNotification, relatedTaskMeta, onProgress) {
207
- this.token = token;
208
- this.sendNotification = sendNotification;
209
- this.relatedTaskMeta = relatedTaskMeta;
210
- this.onProgress = onProgress;
211
- }
212
- static create(extra) {
213
- const token = extra?._meta?.progressToken ?? null;
214
- const sendNotification = extra?.sendNotification;
215
- const relatedTaskMeta = resolveRelatedTaskMeta(extra?._meta);
216
- const onProgress = extra?.onProgress;
217
- if (token === null && !onProgress) {
218
- return { report: async () => { } };
219
- }
220
- return new ToolProgressReporter(token, sendNotification, relatedTaskMeta, onProgress);
221
- }
222
- async report(progress, message) {
223
- if (this.onProgress) {
224
- try {
225
- this.onProgress(progress, message);
226
- }
227
- catch (error) {
228
- logWarn('Progress callback failed', {
229
- error: getErrorMessage(error),
230
- progress,
231
- message,
232
- });
233
- }
234
- }
235
- if (this.token === null || !this.sendNotification)
236
- return;
237
- const { sendNotification } = this;
238
- const notification = {
239
- method: 'notifications/progress',
240
- params: {
241
- progressToken: this.token,
242
- progress,
243
- total: FETCH_PROGRESS_TOTAL,
244
- message,
245
- ...(this.relatedTaskMeta
246
- ? {
247
- _meta: {
248
- 'io.modelcontextprotocol/related-task': this.relatedTaskMeta,
249
- },
250
- }
251
- : {}),
252
- },
253
- };
254
- this.reportQueue = this.reportQueue.then(async () => {
255
- let timeoutId;
256
- const timeoutPromise = new Promise((resolve) => {
257
- timeoutId = setTimeout(() => {
258
- resolve({ timeout: true });
259
- }, PROGRESS_NOTIFICATION_TIMEOUT_MS);
260
- timeoutId.unref();
261
- });
262
- try {
263
- const outcome = await Promise.race([
264
- sendNotification(notification).then(() => ({ ok: true })),
265
- timeoutPromise,
266
- ]);
267
- if ('timeout' in outcome) {
268
- logWarn('Progress notification timed out', { progress, message });
269
- }
270
- }
271
- catch (error) {
272
- logWarn('Failed to send progress notification', {
273
- error: getErrorMessage(error),
274
- progress,
275
- message,
276
- });
277
- }
278
- finally {
279
- if (timeoutId)
280
- clearTimeout(timeoutId);
281
- }
282
- });
283
- await this.reportQueue;
284
- }
285
- }
286
- export function createProgressReporter(extra) {
287
- return ToolProgressReporter.create(extra);
288
- }
289
- function getOpenCodeFence(content) {
290
- const FENCE_PATTERN = /^([ \t]*)(`{3,}|~{3,})/gm;
291
- let match;
292
- let inFence = false;
293
- let fenceChar = null;
294
- let fenceLength = 0;
295
- while ((match = FENCE_PATTERN.exec(content)) !== null) {
296
- const marker = match[2];
297
- if (!marker)
298
- continue;
299
- const [char] = marker;
300
- if (!char)
301
- continue;
302
- const { length } = marker;
303
- if (!inFence) {
304
- inFence = true;
305
- fenceChar = char;
306
- fenceLength = length;
307
- }
308
- else if (char === fenceChar && length >= fenceLength) {
309
- inFence = false;
310
- fenceChar = null;
311
- fenceLength = 0;
312
- }
313
- }
314
- if (inFence && fenceChar) {
315
- return { fenceChar, fenceLength };
316
- }
317
- return null;
318
- }
319
- function findSafeLinkBoundary(content, limit) {
320
- const lastBracket = content.lastIndexOf('[', limit);
321
- if (lastBracket === -1)
322
- return limit;
323
- const afterBracket = content.substring(lastBracket, limit);
324
- const closedPattern = /^\[[^\]]*\]\([^)]*\)/;
325
- if (closedPattern.test(afterBracket))
326
- return limit;
327
- const start = lastBracket > 0 && content[lastBracket - 1] === '!'
328
- ? lastBracket - 1
329
- : lastBracket;
330
- return start;
331
- }
332
- function truncateWithMarker(content, limit, marker) {
333
- if (content.length <= limit)
334
- return content;
335
- const maxContentLength = Math.max(0, limit - marker.length);
336
- const tentativeContent = content.substring(0, maxContentLength);
337
- const openFence = getOpenCodeFence(tentativeContent);
338
- if (openFence) {
339
- const fenceCloser = `\n${openFence.fenceChar.repeat(openFence.fenceLength)}\n`;
340
- const adjustedLength = Math.max(0, limit - marker.length - fenceCloser.length);
341
- return `${content.substring(0, adjustedLength)}${fenceCloser}${marker}`;
342
- }
343
- const safeBoundary = findSafeLinkBoundary(content, maxContentLength);
344
- if (safeBoundary < maxContentLength) {
345
- return `${content.substring(0, safeBoundary)}${marker}`;
346
- }
347
- return `${tentativeContent}${marker}`;
348
- }
349
- function appendTruncationMarker(content, marker) {
350
- if (!content)
351
- return marker;
352
- if (content.endsWith(marker))
353
- return content;
354
- const openFence = getOpenCodeFence(content);
355
- const contentWithFence = openFence
356
- ? `${content}\n${openFence.fenceChar.repeat(openFence.fenceLength)}\n`
357
- : content;
358
- const safeBoundary = findSafeLinkBoundary(contentWithFence, contentWithFence.length);
359
- if (safeBoundary < contentWithFence.length) {
360
- return `${contentWithFence.substring(0, safeBoundary)}${marker}`;
361
- }
362
- return `${contentWithFence}${marker}`;
363
- }
364
- class InlineContentLimiter {
365
- apply(content, inlineLimitOverride) {
366
- const contentSize = content.length;
367
- const inlineLimit = this.resolveInlineLimit(inlineLimitOverride);
368
- if (inlineLimit <= 0) {
369
- return { content, contentSize };
370
- }
371
- if (contentSize <= inlineLimit) {
372
- return { content, contentSize };
373
- }
374
- const truncatedContent = truncateWithMarker(content, inlineLimit, TRUNCATION_MARKER);
375
- return {
376
- content: truncatedContent,
377
- contentSize,
378
- truncated: true,
379
- };
380
- }
381
- resolveInlineLimit(inlineLimitOverride) {
382
- const globalLimit = config.constants.maxInlineContentChars;
383
- if (inlineLimitOverride === undefined)
384
- return globalLimit;
385
- if (globalLimit > 0 && inlineLimitOverride > 0) {
386
- return Math.min(inlineLimitOverride, globalLimit);
387
- }
388
- if (globalLimit > 0 && inlineLimitOverride === 0)
389
- return globalLimit;
390
- return inlineLimitOverride;
391
- }
392
- }
393
- const inlineLimiter = new InlineContentLimiter();
394
- function applyInlineContentLimit(content, inlineLimitOverride) {
395
- return inlineLimiter.apply(content, inlineLimitOverride);
396
- }
397
- /* -------------------------------------------------------------------------------------------------
398
- * Tool response blocks (text + optional embedded resource)
129
+ * Tool response builders
399
130
  * ------------------------------------------------------------------------------------------------- */
400
131
  function buildTextBlock(structuredContent) {
401
132
  return {
@@ -406,7 +137,7 @@ function buildTextBlock(structuredContent) {
406
137
  function buildEmbeddedResource(content, url, title) {
407
138
  if (!content)
408
139
  return null;
409
- const filename = cache.generateSafeFilename(url, title, undefined, '.md');
140
+ const filename = generateSafeFilename(url, title, undefined, '.md');
410
141
  const uri = new URL(filename, 'file:///').href;
411
142
  const resource = {
412
143
  uri,
@@ -436,297 +167,48 @@ function buildCacheResourceLink(cacheResourceUri, contentSize, fetchedAt) {
436
167
  }
437
168
  function buildToolContentBlocks(structuredContent, resourceLink, embeddedResource) {
438
169
  const blocks = [buildTextBlock(structuredContent)];
439
- if (resourceLink) {
440
- blocks.push(resourceLink);
441
- }
442
- if (embeddedResource) {
443
- blocks.push(embeddedResource);
444
- }
170
+ appendIfPresent(blocks, resourceLink);
171
+ appendIfPresent(blocks, embeddedResource);
445
172
  return blocks;
446
173
  }
447
- function resolveNormalizedUrl(url) {
448
- const { normalizedUrl: validatedUrl } = normalizeUrl(url);
449
- const transformedResult = transformToRawUrl(validatedUrl);
450
- if (!transformedResult.transformed) {
451
- return {
452
- normalizedUrl: validatedUrl,
453
- originalUrl: validatedUrl,
454
- transformed: false,
455
- };
456
- }
457
- // Re-validate transformed URLs so blocked-host and length policies still apply.
458
- const { normalizedUrl: transformedUrl } = normalizeUrl(transformedResult.url);
459
- return {
460
- normalizedUrl: transformedUrl,
461
- originalUrl: validatedUrl,
462
- transformed: true,
463
- };
464
- }
465
- function logRawUrlTransformation(resolvedUrl) {
466
- if (!resolvedUrl.transformed)
467
- return;
468
- logDebug('Using transformed raw content URL', {
469
- original: resolvedUrl.originalUrl,
470
- });
471
- }
472
- function extractTitle(value) {
473
- const record = asRecord(value);
474
- const title = record ? record['title'] : undefined;
475
- return typeof title === 'string' ? title : undefined;
476
- }
477
- function logCacheMiss(reason, cacheNamespace, normalizedUrl, error) {
478
- const log = reason.startsWith('deserialize') ? logWarn : logDebug;
479
- log(`Cache miss due to ${reason}`, {
480
- namespace: cacheNamespace,
481
- url: normalizedUrl,
482
- ...(error ? { error: getErrorMessage(error) } : {}),
483
- });
484
- }
485
- function attemptCacheRetrieval(params) {
486
- const { cacheKey, deserialize, cacheNamespace, normalizedUrl } = params;
487
- if (!cacheKey)
488
- return null;
489
- const cached = cache.get(cacheKey);
490
- if (!cached)
491
- return null;
492
- if (!deserialize) {
493
- logCacheMiss('missing deserializer', cacheNamespace, normalizedUrl);
494
- return null;
495
- }
496
- let data;
497
- try {
498
- data = deserialize(cached.content);
499
- }
500
- catch (error) {
501
- logCacheMiss('deserialize exception', cacheNamespace, normalizedUrl, error);
502
- return null;
503
- }
504
- if (data === undefined) {
505
- logCacheMiss('deserialize failure', cacheNamespace, normalizedUrl);
506
- return null;
507
- }
508
- logDebug('Cache hit', { namespace: cacheNamespace, url: normalizedUrl });
509
- const finalUrl = cached.url !== normalizedUrl ? cached.url : undefined;
510
- return {
511
- data,
512
- fromCache: true,
513
- url: normalizedUrl,
514
- ...(finalUrl ? { finalUrl } : {}),
515
- fetchedAt: cached.fetchedAt,
516
- cacheKey,
517
- };
518
- }
519
- function persistCache(params) {
520
- const { cacheKey, data, serialize, normalizedUrl, cacheNamespace, force } = params;
521
- if (!cacheKey)
522
- return;
523
- const serializer = serialize ?? JSON.stringify;
524
- const title = extractTitle(data);
525
- const metadata = {
526
- url: normalizedUrl,
527
- ...(title === undefined ? {} : { title }),
528
- };
529
- try {
530
- cache.set(cacheKey, serializer(data), metadata, force ? { force: true } : undefined);
531
- }
532
- catch (error) {
533
- logWarn('Failed to persist cache entry', {
534
- namespace: cacheNamespace,
535
- url: normalizedUrl,
536
- error: getErrorMessage(error),
537
- });
538
- }
539
- }
540
- export async function executeFetchPipeline(options) {
541
- const resolvedUrl = resolveNormalizedUrl(options.url);
542
- logRawUrlTransformation(resolvedUrl);
543
- const cacheKey = cache.createCacheKey(options.cacheNamespace, resolvedUrl.normalizedUrl, options.cacheVary);
544
- if (!options.forceRefresh) {
545
- const cachedResult = attemptCacheRetrieval({
546
- cacheKey,
547
- deserialize: options.deserialize,
548
- cacheNamespace: options.cacheNamespace,
549
- normalizedUrl: resolvedUrl.normalizedUrl,
550
- });
551
- if (cachedResult) {
552
- return { ...cachedResult, originalUrl: resolvedUrl.originalUrl };
553
- }
554
- }
555
- logDebug('Fetching URL', { url: resolvedUrl.normalizedUrl });
556
- const { buffer, encoding, truncated, finalUrl } = await fetchNormalizedUrlBuffer(resolvedUrl.normalizedUrl, withSignal(options.signal));
557
- const transformUrl = finalUrl || resolvedUrl.normalizedUrl;
558
- const data = await options.transform({ buffer, encoding, ...(truncated ? { truncated: true } : {}) }, transformUrl);
559
- if (cache.isEnabled()) {
560
- persistCache({
561
- cacheKey,
562
- data,
563
- serialize: options.serialize,
564
- normalizedUrl: finalUrl || resolvedUrl.normalizedUrl,
565
- cacheNamespace: options.cacheNamespace,
566
- });
567
- if (finalUrl && finalUrl !== resolvedUrl.normalizedUrl) {
568
- const finalCacheKey = cache.createCacheKey(options.cacheNamespace, finalUrl, options.cacheVary);
569
- if (finalCacheKey && finalCacheKey !== cacheKey) {
570
- persistCache({
571
- cacheKey: finalCacheKey,
572
- data,
573
- serialize: options.serialize,
574
- normalizedUrl: finalUrl,
575
- cacheNamespace: options.cacheNamespace,
576
- });
577
- }
578
- }
579
- }
580
- return {
581
- data,
582
- fromCache: false,
583
- url: resolvedUrl.normalizedUrl,
584
- originalUrl: resolvedUrl.originalUrl,
585
- finalUrl,
586
- fetchedAt: new Date().toISOString(),
587
- cacheKey,
588
- };
589
- }
590
- export async function performSharedFetch(options, deps = {}) {
591
- const executePipeline = deps.executeFetchPipeline ?? executeFetchPipeline;
592
- const pipelineOptions = {
593
- url: options.url,
594
- cacheNamespace: 'markdown',
595
- ...withSignal(options.signal),
596
- ...(options.cacheVary ? { cacheVary: options.cacheVary } : {}),
597
- ...(options.forceRefresh ? { forceRefresh: true } : {}),
598
- transform: options.transform,
599
- ...(options.serialize ? { serialize: options.serialize } : {}),
600
- ...(options.deserialize ? { deserialize: options.deserialize } : {}),
601
- };
602
- const pipeline = await executePipeline(pipelineOptions);
603
- const inlineResult = applyInlineContentLimit(pipeline.data.content, options.maxInlineChars);
604
- return { pipeline, inlineResult };
174
+ function appendIfPresent(items, value) {
175
+ if (value !== null && value !== undefined)
176
+ items.push(value);
605
177
  }
606
178
  /* -------------------------------------------------------------------------------------------------
607
- * Tool error mapping
179
+ * Tool abort signal
608
180
  * ------------------------------------------------------------------------------------------------- */
609
- export function createToolErrorResponse(message, url, extra) {
610
- const structuredContent = {
611
- error: message,
612
- url,
613
- ...(extra?.statusCode !== undefined
614
- ? { statusCode: extra.statusCode }
615
- : {}),
616
- ...(extra?.details ? { details: extra.details } : {}),
617
- };
618
- return {
619
- content: [buildTextBlock(structuredContent)],
620
- structuredContent,
621
- isError: true,
622
- };
623
- }
624
- function isValidationError(error) {
625
- return (error instanceof Error &&
626
- isSystemError(error) &&
627
- error.code === 'VALIDATION_ERROR');
628
- }
629
- function resolveToolErrorMessage(error, fallbackMessage) {
630
- if (isValidationError(error) || error instanceof FetchError) {
631
- return error.message;
632
- }
633
- if (error instanceof Error) {
634
- return `${fallbackMessage}: ${error.message}`;
635
- }
636
- return `${fallbackMessage}: Unknown error`;
637
- }
638
- export function handleToolError(error, url, fallbackMessage = 'Operation failed') {
639
- const message = resolveToolErrorMessage(error, fallbackMessage);
640
- if (error instanceof FetchError) {
641
- return createToolErrorResponse(message, url, {
642
- statusCode: error.statusCode,
643
- details: error.details,
644
- });
645
- }
646
- return createToolErrorResponse(message, url);
181
+ function buildToolAbortSignal(extraSignal) {
182
+ const { timeoutMs } = config.tools;
183
+ if (timeoutMs <= 0)
184
+ return extraSignal;
185
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
186
+ if (!extraSignal)
187
+ return timeoutSignal;
188
+ return AbortSignal.any([extraSignal, timeoutSignal]);
647
189
  }
648
- function normalizeExtractedMetadata(metadata) {
649
- if (!metadata)
650
- return undefined;
651
- const normalized = {
652
- ...(metadata.title ? { title: metadata.title } : {}),
653
- ...(metadata.description ? { description: metadata.description } : {}),
654
- ...(metadata.author ? { author: metadata.author } : {}),
655
- ...(metadata.image ? { image: metadata.image } : {}),
656
- ...(metadata.favicon ? { favicon: metadata.favicon } : {}),
657
- ...(metadata.publishedAt ? { publishedAt: metadata.publishedAt } : {}),
658
- ...(metadata.modifiedAt ? { modifiedAt: metadata.modifiedAt } : {}),
659
- };
660
- if (Object.keys(normalized).length === 0)
661
- return undefined;
662
- return normalized;
190
+ /* -------------------------------------------------------------------------------------------------
191
+ * Structured response assembly
192
+ * ------------------------------------------------------------------------------------------------- */
193
+ function truncateStr(value, max) {
194
+ if (value === undefined || value.length <= max)
195
+ return value;
196
+ return value.slice(0, max);
663
197
  }
664
- const cachedMarkdownSchema = z
665
- .object({
666
- markdown: z.string().optional(),
667
- content: z.string().optional(),
668
- title: z.string().optional(),
669
- metadata: z
670
- .strictObject({
671
- title: z.string().optional(),
672
- description: z.string().optional(),
673
- author: z.string().optional(),
674
- image: z.string().optional(),
675
- favicon: z.string().optional(),
676
- publishedAt: z.string().optional(),
677
- modifiedAt: z.string().optional(),
678
- })
679
- .optional(),
680
- truncated: z.boolean().optional(),
681
- })
682
- .catchall(z.unknown())
683
- .refine((value) => typeof value.markdown === 'string' || typeof value.content === 'string', { message: 'Missing markdown/content' });
684
- export function parseCachedMarkdownResult(cached) {
685
- const parsed = safeJsonParse(cached);
686
- const result = cachedMarkdownSchema.safeParse(parsed);
687
- if (!result.success)
688
- return undefined;
689
- const markdown = result.data.markdown ?? result.data.content;
690
- if (typeof markdown !== 'string')
691
- return undefined;
692
- const metadata = normalizeExtractedMetadata(result.data.metadata);
693
- const truncated = result.data.truncated ?? false;
694
- const persistedMarkdown = truncated
695
- ? appendTruncationMarker(markdown, TRUNCATION_MARKER)
696
- : markdown;
198
+ function truncateMetadata(metadata) {
697
199
  return {
698
- content: persistedMarkdown,
699
- markdown: persistedMarkdown,
700
- title: result.data.title,
701
- ...(metadata ? { metadata } : {}),
702
- truncated,
200
+ ...metadata,
201
+ ...(metadata.title !== undefined
202
+ ? { title: truncateStr(metadata.title, 512) }
203
+ : {}),
204
+ ...(metadata.description !== undefined
205
+ ? { description: truncateStr(metadata.description, 2048) }
206
+ : {}),
207
+ ...(metadata.author !== undefined
208
+ ? { author: truncateStr(metadata.author, 512) }
209
+ : {}),
703
210
  };
704
211
  }
705
- const markdownTransform = async (input, url, signal, skipNoiseRemoval) => {
706
- const result = await transformBufferToMarkdown(input.buffer, url, {
707
- includeMetadata: true,
708
- encoding: input.encoding,
709
- ...withSignal(signal),
710
- ...(skipNoiseRemoval ? { skipNoiseRemoval: true } : {}),
711
- ...(input.truncated ? { inputTruncated: true } : {}),
712
- });
713
- const truncated = Boolean(result.truncated || input.truncated);
714
- return { ...result, content: result.markdown, truncated };
715
- };
716
- function serializeMarkdownResult(result) {
717
- const persistedMarkdown = result.truncated
718
- ? appendTruncationMarker(result.markdown, TRUNCATION_MARKER)
719
- : result.markdown;
720
- return JSON.stringify({
721
- markdown: persistedMarkdown,
722
- title: result.title,
723
- metadata: result.metadata,
724
- truncated: result.truncated,
725
- });
726
- }
727
- /* -------------------------------------------------------------------------------------------------
728
- * fetch-url tool implementation
729
- * ------------------------------------------------------------------------------------------------- */
730
212
  function buildStructuredContent(pipeline, inlineResult, inputUrl) {
731
213
  const cacheResourceUri = resolveCacheResourceUri(pipeline.cacheKey);
732
214
  const truncated = inlineResult.truncated ?? pipeline.data.truncated;
@@ -738,8 +220,8 @@ function buildStructuredContent(pipeline, inlineResult, inputUrl) {
738
220
  ...(pipeline.finalUrl ? { finalUrl: pipeline.finalUrl } : {}),
739
221
  ...(cacheResourceUri ? { cacheResourceUri } : {}),
740
222
  inputUrl,
741
- title: pipeline.data.title,
742
- ...(metadata ? { metadata } : {}),
223
+ title: truncateStr(pipeline.data.title, 512),
224
+ ...(metadata ? { metadata: truncateMetadata(metadata) } : {}),
743
225
  markdown,
744
226
  fromCache: pipeline.fromCache,
745
227
  fetchedAt: pipeline.fetchedAt,
@@ -780,20 +262,70 @@ function buildFetchUrlContentBlocks(structuredContent, pipeline, inlineResult) {
780
262
  function buildResponse(pipeline, inlineResult, inputUrl) {
781
263
  const structuredContent = buildStructuredContent(pipeline, inlineResult, inputUrl);
782
264
  const content = buildFetchUrlContentBlocks(structuredContent, pipeline, inlineResult);
783
- // Runtime validation guard: verify output matches schema
784
265
  const validation = fetchUrlOutputSchema.safeParse(structuredContent);
785
266
  if (!validation.success) {
786
267
  logWarn('Tool output schema validation failed', {
787
268
  url: inputUrl,
788
269
  issues: validation.error.issues,
789
270
  });
271
+ // Omit structuredContent so the SDK does not receive data that fails its
272
+ // output schema validation. The client still gets the payload via content[0].text.
273
+ return { content };
790
274
  }
791
275
  return {
792
276
  content,
793
277
  structuredContent,
794
278
  };
795
279
  }
280
+ /* -------------------------------------------------------------------------------------------------
281
+ * fetch-url tool implementation
282
+ * ------------------------------------------------------------------------------------------------- */
283
+ export function getUrlContext(urlStr) {
284
+ try {
285
+ const u = new URL(urlStr);
286
+ const host = u.hostname.replace(/^www\./, '');
287
+ const path = u.pathname;
288
+ if (path === '/' || path === '')
289
+ return host;
290
+ const parts = path.split('/').filter(Boolean);
291
+ if (parts.length === 0)
292
+ return host;
293
+ // Special case for GitHub/GitLab/Bitbucket
294
+ if (host === 'github.com' ||
295
+ host === 'gitlab.com' ||
296
+ host === 'bitbucket.org') {
297
+ if (parts.length >= 2) {
298
+ const p0 = parts[0] ?? '';
299
+ const p1 = parts[1] ?? '';
300
+ return `${host}/${p0}/${p1}`;
301
+ }
302
+ }
303
+ // Special case for Wikipedia
304
+ if (host.endsWith('wikipedia.org') &&
305
+ parts[0] === 'wiki' &&
306
+ parts.length >= 2) {
307
+ const p1 = parts[1] ?? '';
308
+ return `wikipedia.org/${p1}`;
309
+ }
310
+ let basename = parts.pop() ?? '';
311
+ if (basename && basename.length > 20) {
312
+ basename = `${basename.substring(0, 17)}...`;
313
+ }
314
+ if (parts.length === 0) {
315
+ return `${host}/${basename}`;
316
+ }
317
+ return basename ? `${host}/…/${basename}` : host;
318
+ }
319
+ catch {
320
+ return 'unknown';
321
+ }
322
+ }
796
323
  async function fetchPipeline(url, signal, progress, skipNoiseRemoval, forceRefresh, maxInlineChars) {
324
+ const reportProgress = (step, message) => {
325
+ if (!progress)
326
+ return;
327
+ progress.report(step, message);
328
+ };
797
329
  return performSharedFetch({
798
330
  url,
799
331
  ...withSignal(signal),
@@ -801,9 +333,8 @@ async function fetchPipeline(url, signal, progress, skipNoiseRemoval, forceRefre
801
333
  ...(forceRefresh ? { forceRefresh: true } : {}),
802
334
  ...(maxInlineChars !== undefined ? { maxInlineChars } : {}),
803
335
  transform: async ({ buffer, encoding, truncated }, normalizedUrl) => {
804
- if (progress) {
805
- void progress.report(3, 'Transforming content');
806
- }
336
+ const contextStr = getUrlContext(url);
337
+ reportProgress(2, `fetch-url: ${contextStr} [converting to Markdown]`);
807
338
  return markdownTransform({ buffer, encoding, ...(truncated ? { truncated } : {}) }, normalizedUrl, signal, skipNoiseRemoval);
808
339
  },
809
340
  serialize: serializeMarkdownResult,
@@ -812,20 +343,25 @@ async function fetchPipeline(url, signal, progress, skipNoiseRemoval, forceRefre
812
343
  }
813
344
  async function executeFetch(input, extra) {
814
345
  const { url } = input;
815
- if (!url) {
816
- return createToolErrorResponse('URL is required', '');
817
- }
818
346
  const signal = buildToolAbortSignal(extra?.signal);
819
347
  const progress = createProgressReporter(extra);
820
- void progress.report(1, 'Validating URL');
348
+ const contextStr = getUrlContext(url);
349
+ progress.report(0, `fetch-url: ${contextStr} [starting]`);
821
350
  logDebug('Fetching URL', { url });
822
- void progress.report(2, 'Fetching content');
823
- const { pipeline, inlineResult } = await fetchPipeline(url, signal, progress, input.skipNoiseRemoval, input.forceRefresh, input.maxInlineChars);
824
- if (pipeline.fromCache) {
825
- void progress.report(3, 'Using cached content');
351
+ try {
352
+ progress.report(1, `fetch-url: ${contextStr} [fetching HTML]`);
353
+ const { pipeline, inlineResult } = await fetchPipeline(url, signal, progress, input.skipNoiseRemoval, input.forceRefresh, input.maxInlineChars);
354
+ if (pipeline.fromCache) {
355
+ progress.report(3, `fetch-url: ${contextStr} [loaded from cache]`);
356
+ }
357
+ progress.report(4, `fetch-url: ${contextStr} • completed`);
358
+ return buildResponse(pipeline, inlineResult, url);
359
+ }
360
+ catch (error) {
361
+ const isAbort = error instanceof Error && error.name === 'AbortError';
362
+ progress.report(4, `fetch-url: ${contextStr} • ${isAbort ? 'cancelled' : 'failed'}`);
363
+ throw error;
826
364
  }
827
- void progress.report(4, 'Finalizing response');
828
- return buildResponse(pipeline, inlineResult, url);
829
365
  }
830
366
  export async function fetchUrlToolHandler(input, extra) {
831
367
  return executeFetch(input, extra).catch((error) => {
@@ -838,7 +374,7 @@ const TOOL_DEFINITION = {
838
374
  title: 'Fetch URL',
839
375
  description: FETCH_URL_TOOL_DESCRIPTION,
840
376
  inputSchema: fetchUrlInputSchema,
841
- outputSchema: fetchUrlOutputSchema,
377
+ outputSchema: z.toJSONSchema(fetchUrlOutputSchema),
842
378
  handler: fetchUrlToolHandler,
843
379
  execution: {
844
380
  taskSupport: 'optional',
@@ -897,5 +433,9 @@ export function registerTools(server) {
897
433
  execution: TOOL_DEFINITION.execution,
898
434
  icons: [TOOL_ICON],
899
435
  }, withRequestContextIfMissing(TOOL_DEFINITION.handler));
436
+ // SDK workaround: RegisteredTool does not expose `execution` in its public type, so we
437
+ // assign it directly post-registration to enable task-augmented tool calls (taskSupport).
438
+ // TODO: Remove when @modelcontextprotocol/sdk exposes `execution` in RegisteredTool type.
900
439
  registeredTool.execution = TOOL_DEFINITION.execution;
901
440
  }
441
+ //# sourceMappingURL=tools.js.map