@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.
- package/dist/cache.d.ts +9 -3
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +54 -119
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -4
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +2 -3
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +19 -27
- package/dist/config.js.map +1 -0
- package/dist/crypto.d.ts +1 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +7 -3
- package/dist/crypto.js.map +1 -0
- package/dist/dom-noise-removal.d.ts +2 -1
- package/dist/dom-noise-removal.d.ts.map +1 -0
- package/dist/dom-noise-removal.js +9 -6
- package/dist/dom-noise-removal.js.map +1 -0
- package/dist/download.d.ts +4 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +106 -0
- package/dist/download.js.map +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -0
- package/dist/examples/mcp-fetch-url-client.js +19 -3
- package/dist/examples/mcp-fetch-url-client.js.map +1 -1
- package/dist/fetch-content.d.ts +1 -0
- package/dist/fetch-content.d.ts.map +1 -0
- package/dist/fetch-content.js +15 -14
- package/dist/fetch-content.js.map +1 -0
- package/dist/fetch-stream.d.ts +1 -0
- package/dist/fetch-stream.d.ts.map +1 -0
- package/dist/fetch-stream.js +1 -0
- package/dist/fetch-stream.js.map +1 -0
- package/dist/fetch.d.ts +1 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +123 -54
- package/dist/fetch.js.map +1 -0
- package/dist/host-normalization.d.ts +1 -0
- package/dist/host-normalization.d.ts.map +1 -0
- package/dist/host-normalization.js +22 -9
- package/dist/host-normalization.js.map +1 -0
- package/dist/http/auth.d.ts +51 -0
- package/dist/http/auth.d.ts.map +1 -0
- package/dist/http/auth.js +344 -0
- package/dist/http/auth.js.map +1 -0
- package/dist/http/health.d.ts +7 -0
- package/dist/http/health.d.ts.map +1 -0
- package/dist/http/health.js +156 -0
- package/dist/http/health.js.map +1 -0
- package/dist/http/helpers.d.ts +58 -0
- package/dist/http/helpers.d.ts.map +1 -0
- package/dist/http/helpers.js +370 -0
- package/dist/http/helpers.js.map +1 -0
- package/dist/{http-native.d.ts → http/native.d.ts} +1 -0
- package/dist/http/native.d.ts.map +1 -0
- package/dist/http/native.js +618 -0
- package/dist/http/native.js.map +1 -0
- package/dist/http/rate-limit.d.ts +13 -0
- package/dist/http/rate-limit.d.ts.map +1 -0
- package/dist/http/rate-limit.js +92 -0
- package/dist/http/rate-limit.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -14
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +41 -0
- package/dist/instructions.js.map +1 -0
- package/dist/ip-blocklist.d.ts +1 -0
- package/dist/ip-blocklist.d.ts.map +1 -0
- package/dist/ip-blocklist.js +13 -8
- package/dist/ip-blocklist.js.map +1 -0
- package/dist/json.d.ts +2 -1
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +16 -6
- package/dist/json.js.map +1 -0
- package/dist/language-detection.d.ts +1 -0
- package/dist/language-detection.d.ts.map +1 -0
- package/dist/language-detection.js +2 -7
- package/dist/language-detection.js.map +1 -0
- package/dist/markdown-cleanup.d.ts +2 -1
- package/dist/markdown-cleanup.d.ts.map +1 -0
- package/dist/markdown-cleanup.js +52 -54
- package/dist/markdown-cleanup.js.map +1 -0
- package/dist/mcp-validator.d.ts +1 -0
- package/dist/mcp-validator.d.ts.map +1 -0
- package/dist/mcp-validator.js +20 -18
- package/dist/mcp-validator.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +35 -344
- package/dist/mcp.js.map +1 -0
- package/dist/observability.d.ts +2 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +32 -6
- package/dist/observability.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +15 -3
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +1 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +46 -25
- package/dist/resources.js.map +1 -0
- package/dist/server-tuning.d.ts +1 -0
- package/dist/server-tuning.d.ts.map +1 -0
- package/dist/server-tuning.js +14 -17
- package/dist/server-tuning.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +29 -35
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +2 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +58 -29
- package/dist/session.js.map +1 -0
- package/dist/tasks/execution.d.ts +42 -0
- package/dist/tasks/execution.d.ts.map +1 -0
- package/dist/tasks/execution.js +241 -0
- package/dist/tasks/execution.js.map +1 -0
- package/dist/{tasks.d.ts → tasks/manager.d.ts} +12 -0
- package/dist/tasks/manager.d.ts.map +1 -0
- package/dist/{tasks.js → tasks/manager.js} +95 -43
- package/dist/tasks/manager.js.map +1 -0
- package/dist/tasks/owner.d.ts +32 -0
- package/dist/tasks/owner.d.ts.map +1 -0
- package/dist/tasks/owner.js +92 -0
- package/dist/tasks/owner.js.map +1 -0
- package/dist/timer-utils.d.ts +1 -0
- package/dist/timer-utils.d.ts.map +1 -0
- package/dist/timer-utils.js +8 -4
- package/dist/timer-utils.js.map +1 -0
- package/dist/tool-errors.d.ts +12 -0
- package/dist/tool-errors.d.ts.map +1 -0
- package/dist/tool-errors.js +55 -0
- package/dist/tool-errors.js.map +1 -0
- package/dist/tool-pipeline.d.ts +72 -0
- package/dist/tool-pipeline.d.ts.map +1 -0
- package/dist/tool-pipeline.js +408 -0
- package/dist/tool-pipeline.js.map +1 -0
- package/dist/tool-progress.d.ts +32 -0
- package/dist/tool-progress.d.ts.map +1 -0
- package/dist/tool-progress.js +129 -0
- package/dist/tool-progress.js.map +1 -0
- package/dist/tools.d.ts +35 -111
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +150 -610
- package/dist/tools.js.map +1 -0
- package/dist/{transform.d.ts → transform/transform.d.ts} +2 -1
- package/dist/transform/transform.d.ts.map +1 -0
- package/dist/{transform.js → transform/transform.js} +81 -771
- package/dist/transform/transform.js.map +1 -0
- package/dist/{transform-types.d.ts → transform/types.d.ts} +2 -0
- package/dist/transform/types.d.ts.map +1 -0
- package/dist/{transform-types.js → transform/types.js} +1 -0
- package/dist/transform/types.js.map +1 -0
- package/dist/transform/worker-pool.d.ts +93 -0
- package/dist/transform/worker-pool.d.ts.map +1 -0
- package/dist/transform/worker-pool.js +757 -0
- package/dist/transform/worker-pool.js.map +1 -0
- package/dist/transform/workers/transform-child.d.ts +2 -0
- package/dist/transform/workers/transform-child.d.ts.map +1 -0
- package/dist/{workers → transform/workers}/transform-child.js +17 -13
- package/dist/transform/workers/transform-child.js.map +1 -0
- package/dist/transform/workers/transform-worker.d.ts +2 -0
- package/dist/transform/workers/transform-worker.d.ts.map +1 -0
- package/dist/{workers → transform/workers}/transform-worker.js +16 -13
- package/dist/transform/workers/transform-worker.js.map +1 -0
- package/dist/type-guards.d.ts +1 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +4 -4
- package/dist/type-guards.js.map +1 -0
- package/package.json +6 -7
- package/dist/AGENTS.md +0 -152
- package/dist/http-native.js +0 -1320
- package/dist/instructions.md +0 -113
- package/dist/workers/transform-child.d.ts +0 -1
- 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 {
|
|
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 {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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(
|
|
20
|
+
.describe(`Target URL. Max ${config.constants.maxUrlLength} chars.`),
|
|
19
21
|
skipNoiseRemoval: z
|
|
20
22
|
.boolean()
|
|
21
23
|
.optional()
|
|
22
|
-
.describe('
|
|
24
|
+
.describe('Preserve navigation/footers (disable noise filtering).'),
|
|
23
25
|
forceRefresh: z
|
|
24
26
|
.boolean()
|
|
25
27
|
.optional()
|
|
26
|
-
.describe('
|
|
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(
|
|
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('
|
|
42
|
+
.describe('Fetched URL.'),
|
|
41
43
|
inputUrl: z
|
|
42
44
|
.string()
|
|
43
45
|
.max(config.constants.maxUrlLength)
|
|
44
46
|
.optional()
|
|
45
|
-
.describe('
|
|
47
|
+
.describe('Original requested URL.'),
|
|
46
48
|
resolvedUrl: z
|
|
47
49
|
.string()
|
|
48
50
|
.max(config.constants.maxUrlLength)
|
|
49
51
|
.optional()
|
|
50
|
-
.describe('
|
|
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('
|
|
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('
|
|
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
|
|
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
|
|
92
|
+
.describe('Detected last modified date.'),
|
|
91
93
|
})
|
|
92
94
|
.optional()
|
|
93
|
-
.describe('
|
|
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('
|
|
99
|
-
fromCache: z
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
|
|
440
|
-
|
|
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
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
179
|
+
* Tool abort signal
|
|
608
180
|
* ------------------------------------------------------------------------------------------------- */
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
805
|
-
|
|
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
|
-
|
|
348
|
+
const contextStr = getUrlContext(url);
|
|
349
|
+
progress.report(0, `fetch-url: ${contextStr} [starting]`);
|
|
821
350
|
logDebug('Fetching URL', { url });
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|