@j0hanz/superfetch 2.5.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +356 -223
- package/dist/assets/logo.svg +24837 -24835
- package/dist/cache.d.ts +28 -20
- package/dist/cache.js +292 -514
- package/dist/config.d.ts +41 -7
- package/dist/config.js +298 -148
- package/dist/crypto.js +25 -12
- package/dist/dom-noise-removal.js +379 -421
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +25 -8
- package/dist/fetch.d.ts +18 -16
- package/dist/fetch.js +1132 -526
- package/dist/host-normalization.js +40 -10
- package/dist/http-native.js +628 -287
- package/dist/index.js +67 -7
- package/dist/instructions.md +44 -30
- package/dist/ip-blocklist.d.ts +8 -0
- package/dist/ip-blocklist.js +65 -0
- package/dist/json.js +14 -9
- package/dist/language-detection.d.ts +2 -11
- package/dist/language-detection.js +289 -280
- package/dist/markdown-cleanup.d.ts +0 -1
- package/dist/markdown-cleanup.js +391 -429
- package/dist/mcp-validator.js +4 -2
- package/dist/mcp.js +184 -135
- package/dist/observability.js +89 -21
- package/dist/resources.js +16 -6
- package/dist/server-tuning.d.ts +2 -0
- package/dist/server-tuning.js +25 -23
- package/dist/session.d.ts +1 -0
- package/dist/session.js +41 -33
- package/dist/tasks.d.ts +2 -0
- package/dist/tasks.js +91 -9
- package/dist/timer-utils.d.ts +5 -0
- package/dist/timer-utils.js +20 -0
- package/dist/tools.d.ts +28 -5
- package/dist/tools.js +317 -183
- package/dist/transform-types.d.ts +5 -1
- package/dist/transform.d.ts +3 -2
- package/dist/transform.js +1138 -421
- package/dist/type-guards.d.ts +1 -0
- package/dist/type-guards.js +7 -0
- package/dist/workers/transform-child.d.ts +1 -0
- package/dist/workers/transform-child.js +118 -0
- package/dist/workers/transform-worker.js +87 -78
- package/package.json +21 -13
package/dist/resources.js
CHANGED
|
@@ -4,17 +4,27 @@ import { stableStringify } from './json.js';
|
|
|
4
4
|
/* -------------------------------------------------------------------------------------------------
|
|
5
5
|
* Configuration Resource
|
|
6
6
|
* ------------------------------------------------------------------------------------------------- */
|
|
7
|
+
const REDACTED = '<REDACTED>';
|
|
8
|
+
const CONFIG_RESOURCE_NAME = 'config';
|
|
9
|
+
const CONFIG_RESOURCE_URI = 'internal://config';
|
|
10
|
+
const JSON_MIME = 'application/json';
|
|
11
|
+
function redactIfPresent(value) {
|
|
12
|
+
return value ? REDACTED : undefined;
|
|
13
|
+
}
|
|
14
|
+
function redactArray(values) {
|
|
15
|
+
return values.map(() => REDACTED);
|
|
16
|
+
}
|
|
7
17
|
function scrubAuth(auth) {
|
|
8
18
|
return {
|
|
9
19
|
...auth,
|
|
10
|
-
clientSecret: auth.clientSecret
|
|
11
|
-
staticTokens: auth.staticTokens
|
|
20
|
+
clientSecret: redactIfPresent(auth.clientSecret),
|
|
21
|
+
staticTokens: redactArray(auth.staticTokens),
|
|
12
22
|
};
|
|
13
23
|
}
|
|
14
24
|
function scrubSecurity(security) {
|
|
15
25
|
return {
|
|
16
26
|
...security,
|
|
17
|
-
apiKey: security.apiKey
|
|
27
|
+
apiKey: redactIfPresent(security.apiKey),
|
|
18
28
|
};
|
|
19
29
|
}
|
|
20
30
|
function scrubConfig(source) {
|
|
@@ -25,17 +35,17 @@ function scrubConfig(source) {
|
|
|
25
35
|
};
|
|
26
36
|
}
|
|
27
37
|
export function registerConfigResource(server) {
|
|
28
|
-
server.registerResource(
|
|
38
|
+
server.registerResource(CONFIG_RESOURCE_NAME, new ResourceTemplate(CONFIG_RESOURCE_URI, { list: undefined }), {
|
|
29
39
|
title: 'Server Configuration',
|
|
30
40
|
description: 'Current runtime configuration (secrets redacted)',
|
|
31
|
-
mimeType:
|
|
41
|
+
mimeType: JSON_MIME,
|
|
32
42
|
}, (uri) => {
|
|
33
43
|
const scrubbed = scrubConfig(config);
|
|
34
44
|
return {
|
|
35
45
|
contents: [
|
|
36
46
|
{
|
|
37
47
|
uri: uri.href,
|
|
38
|
-
mimeType:
|
|
48
|
+
mimeType: JSON_MIME,
|
|
39
49
|
text: stableStringify(scrubbed),
|
|
40
50
|
},
|
|
41
51
|
],
|
package/dist/server-tuning.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export interface HttpServerTuningTarget {
|
|
|
2
2
|
headersTimeout?: number;
|
|
3
3
|
requestTimeout?: number;
|
|
4
4
|
keepAliveTimeout?: number;
|
|
5
|
+
maxConnections?: number;
|
|
6
|
+
on?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
5
7
|
closeIdleConnections?: () => void;
|
|
6
8
|
closeAllConnections?: () => void;
|
|
7
9
|
}
|
package/dist/server-tuning.js
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
import { config } from './config.js';
|
|
2
|
-
import { logDebug } from './observability.js';
|
|
2
|
+
import { logDebug, logWarn } from './observability.js';
|
|
3
|
+
const DROP_LOG_INTERVAL_MS = 10_000;
|
|
3
4
|
export function applyHttpServerTuning(server) {
|
|
4
|
-
const {
|
|
5
|
-
if (
|
|
6
|
-
server.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
const { maxConnections } = config.server.http;
|
|
6
|
+
if (typeof maxConnections === 'number' && maxConnections > 0) {
|
|
7
|
+
server.maxConnections = maxConnections;
|
|
8
|
+
if (typeof server.on === 'function') {
|
|
9
|
+
let lastLoggedAt = 0;
|
|
10
|
+
let droppedSinceLastLog = 0;
|
|
11
|
+
server.on('drop', (data) => {
|
|
12
|
+
droppedSinceLastLog += 1;
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
if (now - lastLoggedAt < DROP_LOG_INTERVAL_MS)
|
|
15
|
+
return;
|
|
16
|
+
logWarn('Incoming connection dropped (maxConnections reached)', {
|
|
17
|
+
maxConnections,
|
|
18
|
+
dropped: droppedSinceLastLog,
|
|
19
|
+
data,
|
|
20
|
+
});
|
|
21
|
+
lastLoggedAt = now;
|
|
22
|
+
droppedSinceLastLog = 0;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
13
25
|
}
|
|
14
26
|
}
|
|
15
27
|
export function drainConnectionsOnShutdown(server) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
server.closeAllConnections();
|
|
20
|
-
logDebug('Closed all HTTP connections during shutdown');
|
|
21
|
-
}
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (shutdownCloseIdleConnections) {
|
|
25
|
-
if (typeof server.closeIdleConnections === 'function') {
|
|
26
|
-
server.closeIdleConnections();
|
|
27
|
-
logDebug('Closed idle HTTP connections during shutdown');
|
|
28
|
-
}
|
|
28
|
+
if (typeof server.closeIdleConnections === 'function') {
|
|
29
|
+
server.closeIdleConnections();
|
|
30
|
+
logDebug('Closed idle HTTP connections during shutdown');
|
|
29
31
|
}
|
|
30
32
|
}
|
package/dist/session.d.ts
CHANGED
package/dist/session.js
CHANGED
|
@@ -14,11 +14,13 @@ export function composeCloseHandlers(first, second) {
|
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* ------------------------------------------------------------------------------------------------- */
|
|
17
|
+
const MIN_CLEANUP_INTERVAL_MS = 10_000;
|
|
18
|
+
const MAX_CLEANUP_INTERVAL_MS = 60_000;
|
|
20
19
|
function getCleanupIntervalMs(sessionTtlMs) {
|
|
21
|
-
return Math.min(Math.max(Math.floor(sessionTtlMs / 2),
|
|
20
|
+
return Math.min(Math.max(Math.floor(sessionTtlMs / 2), MIN_CLEANUP_INTERVAL_MS), MAX_CLEANUP_INTERVAL_MS);
|
|
21
|
+
}
|
|
22
|
+
function formatError(error) {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
22
24
|
}
|
|
23
25
|
function isAbortError(error) {
|
|
24
26
|
return error instanceof Error && error.name === 'AbortError';
|
|
@@ -26,11 +28,11 @@ function isAbortError(error) {
|
|
|
26
28
|
function handleSessionCleanupError(error) {
|
|
27
29
|
if (isAbortError(error))
|
|
28
30
|
return;
|
|
29
|
-
logWarn('Session cleanup loop failed', {
|
|
30
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
31
|
-
});
|
|
31
|
+
logWarn('Session cleanup loop failed', { error: formatError(error) });
|
|
32
32
|
}
|
|
33
33
|
function isSessionExpired(session, now, sessionTtlMs) {
|
|
34
|
+
if (sessionTtlMs <= 0)
|
|
35
|
+
return false;
|
|
34
36
|
return now - session.lastSeen > sessionTtlMs;
|
|
35
37
|
}
|
|
36
38
|
class SessionCleanupLoop {
|
|
@@ -47,16 +49,17 @@ class SessionCleanupLoop {
|
|
|
47
49
|
}
|
|
48
50
|
async run(signal) {
|
|
49
51
|
const intervalMs = getCleanupIntervalMs(this.sessionTtlMs);
|
|
50
|
-
|
|
52
|
+
const ticks = setIntervalPromise(intervalMs, Date.now, {
|
|
51
53
|
signal,
|
|
52
54
|
ref: false,
|
|
53
|
-
})
|
|
55
|
+
});
|
|
56
|
+
for await (const getNow of ticks) {
|
|
54
57
|
const now = getNow();
|
|
55
58
|
const evicted = this.store.evictExpired();
|
|
56
59
|
for (const session of evicted) {
|
|
57
60
|
void session.transport.close().catch((err) => {
|
|
58
61
|
logWarn('Failed to close expired session', {
|
|
59
|
-
error:
|
|
62
|
+
error: formatError(err),
|
|
60
63
|
});
|
|
61
64
|
});
|
|
62
65
|
}
|
|
@@ -72,9 +75,6 @@ class SessionCleanupLoop {
|
|
|
72
75
|
export function startSessionCleanupLoop(store, sessionTtlMs) {
|
|
73
76
|
return new SessionCleanupLoop(store, sessionTtlMs).start();
|
|
74
77
|
}
|
|
75
|
-
/* -------------------------------------------------------------------------------------------------
|
|
76
|
-
* Session store (in-memory, Map order used for LRU)
|
|
77
|
-
* ------------------------------------------------------------------------------------------------- */
|
|
78
78
|
function moveSessionToEnd(sessions, sessionId, session) {
|
|
79
79
|
sessions.delete(sessionId);
|
|
80
80
|
sessions.set(sessionId, session);
|
|
@@ -87,9 +87,13 @@ class InMemorySessionStore {
|
|
|
87
87
|
this.sessionTtlMs = sessionTtlMs;
|
|
88
88
|
}
|
|
89
89
|
get(sessionId) {
|
|
90
|
+
if (!sessionId)
|
|
91
|
+
return undefined;
|
|
90
92
|
return this.sessions.get(sessionId);
|
|
91
93
|
}
|
|
92
94
|
touch(sessionId) {
|
|
95
|
+
if (!sessionId)
|
|
96
|
+
return;
|
|
93
97
|
const session = this.sessions.get(sessionId);
|
|
94
98
|
if (!session)
|
|
95
99
|
return;
|
|
@@ -97,9 +101,13 @@ class InMemorySessionStore {
|
|
|
97
101
|
moveSessionToEnd(this.sessions, sessionId, session);
|
|
98
102
|
}
|
|
99
103
|
set(sessionId, entry) {
|
|
100
|
-
|
|
104
|
+
if (!sessionId)
|
|
105
|
+
return;
|
|
106
|
+
moveSessionToEnd(this.sessions, sessionId, entry);
|
|
101
107
|
}
|
|
102
108
|
remove(sessionId) {
|
|
109
|
+
if (!sessionId)
|
|
110
|
+
return undefined;
|
|
103
111
|
const session = this.sessions.get(sessionId);
|
|
104
112
|
this.sessions.delete(sessionId);
|
|
105
113
|
return session;
|
|
@@ -114,8 +122,7 @@ class InMemorySessionStore {
|
|
|
114
122
|
this.inflight += 1;
|
|
115
123
|
}
|
|
116
124
|
decrementInFlight() {
|
|
117
|
-
|
|
118
|
-
this.inflight -= 1;
|
|
125
|
+
this.inflight = Math.max(0, this.inflight - 1);
|
|
119
126
|
}
|
|
120
127
|
clear() {
|
|
121
128
|
const entries = [...this.sessions.values()];
|
|
@@ -126,10 +133,10 @@ class InMemorySessionStore {
|
|
|
126
133
|
const now = Date.now();
|
|
127
134
|
const evicted = [];
|
|
128
135
|
for (const [id, session] of this.sessions.entries()) {
|
|
129
|
-
if (isSessionExpired(session, now, this.sessionTtlMs))
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
if (!isSessionExpired(session, now, this.sessionTtlMs))
|
|
137
|
+
continue;
|
|
138
|
+
this.sessions.delete(id);
|
|
139
|
+
evicted.push(session);
|
|
133
140
|
}
|
|
134
141
|
return evicted;
|
|
135
142
|
}
|
|
@@ -146,9 +153,6 @@ class InMemorySessionStore {
|
|
|
146
153
|
export function createSessionStore(sessionTtlMs) {
|
|
147
154
|
return new InMemorySessionStore(sessionTtlMs);
|
|
148
155
|
}
|
|
149
|
-
/* -------------------------------------------------------------------------------------------------
|
|
150
|
-
* Slot tracker
|
|
151
|
-
* ------------------------------------------------------------------------------------------------- */
|
|
152
156
|
class SessionSlotTracker {
|
|
153
157
|
store;
|
|
154
158
|
slotReleased = false;
|
|
@@ -172,27 +176,31 @@ class SessionSlotTracker {
|
|
|
172
176
|
export function createSlotTracker(store) {
|
|
173
177
|
return new SessionSlotTracker(store);
|
|
174
178
|
}
|
|
179
|
+
function currentLoad(store) {
|
|
180
|
+
return store.size() + store.inFlight();
|
|
181
|
+
}
|
|
175
182
|
export function reserveSessionSlot(store, maxSessions) {
|
|
176
|
-
if (
|
|
183
|
+
if (maxSessions <= 0)
|
|
184
|
+
return false;
|
|
185
|
+
if (currentLoad(store) >= maxSessions)
|
|
177
186
|
return false;
|
|
178
|
-
}
|
|
179
187
|
store.incrementInFlight();
|
|
180
188
|
return true;
|
|
181
189
|
}
|
|
182
|
-
/* -------------------------------------------------------------------------------------------------
|
|
183
|
-
* Capacity policy
|
|
184
|
-
* ------------------------------------------------------------------------------------------------- */
|
|
185
190
|
function isAtCapacity(store, maxSessions) {
|
|
186
|
-
return
|
|
191
|
+
return currentLoad(store) >= maxSessions;
|
|
187
192
|
}
|
|
188
193
|
export function ensureSessionCapacity({ store, maxSessions, evictOldest, }) {
|
|
194
|
+
if (maxSessions <= 0)
|
|
195
|
+
return false;
|
|
189
196
|
const currentSize = store.size();
|
|
190
197
|
const inflight = store.inFlight();
|
|
191
198
|
if (currentSize + inflight < maxSessions)
|
|
192
199
|
return true;
|
|
193
200
|
const canFreeSlot = currentSize >= maxSessions && currentSize - 1 + inflight < maxSessions;
|
|
194
|
-
if (canFreeSlot
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
if (!canFreeSlot)
|
|
202
|
+
return false;
|
|
203
|
+
if (!evictOldest(store))
|
|
204
|
+
return false;
|
|
205
|
+
return !isAtCapacity(store, maxSessions);
|
|
198
206
|
}
|
package/dist/tasks.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface CreateTaskResult {
|
|
|
34
34
|
declare class TaskManager {
|
|
35
35
|
private tasks;
|
|
36
36
|
private waiters;
|
|
37
|
+
constructor();
|
|
38
|
+
private startCleanupLoop;
|
|
37
39
|
createTask(options?: CreateTaskOptions, statusMessage?: string, ownerKey?: string): TaskState;
|
|
38
40
|
getTask(taskId: string, ownerKey?: string): TaskState | undefined;
|
|
39
41
|
updateTask(taskId: string, updates: Partial<Omit<TaskState, 'taskId' | 'createdAt'>>): void;
|
package/dist/tasks.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
1
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { setInterval } from 'node:timers';
|
|
2
5
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { createUnrefTimeout } from './timer-utils.js';
|
|
3
7
|
const DEFAULT_TTL_MS = 60000;
|
|
4
8
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
5
9
|
const DEFAULT_OWNER_KEY = 'default';
|
|
@@ -15,6 +19,19 @@ function isTerminalStatus(status) {
|
|
|
15
19
|
class TaskManager {
|
|
16
20
|
tasks = new Map();
|
|
17
21
|
waiters = new Map();
|
|
22
|
+
constructor() {
|
|
23
|
+
this.startCleanupLoop();
|
|
24
|
+
}
|
|
25
|
+
startCleanupLoop() {
|
|
26
|
+
const interval = setInterval(() => {
|
|
27
|
+
for (const [id, task] of this.tasks) {
|
|
28
|
+
if (this.isExpired(task)) {
|
|
29
|
+
this.tasks.delete(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, 60000);
|
|
33
|
+
interval.unref();
|
|
34
|
+
}
|
|
18
35
|
createTask(options, statusMessage = 'Task started', ownerKey = DEFAULT_OWNER_KEY) {
|
|
19
36
|
const taskId = randomUUID();
|
|
20
37
|
const now = new Date().toISOString();
|
|
@@ -99,13 +116,52 @@ class TaskManager {
|
|
|
99
116
|
return undefined;
|
|
100
117
|
if (isTerminalStatus(task.status))
|
|
101
118
|
return task;
|
|
119
|
+
const createdAtMs = Date.parse(task.createdAt);
|
|
120
|
+
const deadlineMs = Number.isFinite(createdAtMs)
|
|
121
|
+
? createdAtMs + task.ttl
|
|
122
|
+
: Number.NaN;
|
|
123
|
+
if (Number.isFinite(deadlineMs) && deadlineMs <= Date.now()) {
|
|
124
|
+
this.tasks.delete(taskId);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
102
127
|
return new Promise((resolve, reject) => {
|
|
128
|
+
const runInContext = AsyncLocalStorage.snapshot();
|
|
129
|
+
const resolveInContext = (value) => {
|
|
130
|
+
runInContext(() => {
|
|
131
|
+
resolve(value);
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
const rejectInContext = (error) => {
|
|
135
|
+
runInContext(() => {
|
|
136
|
+
if (error instanceof Error) {
|
|
137
|
+
reject(error);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
reject(new Error(String(error)));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
let settled = false;
|
|
145
|
+
let waiter = null;
|
|
146
|
+
let deadlineTimeout;
|
|
147
|
+
const settle = (fn) => {
|
|
148
|
+
if (settled)
|
|
149
|
+
return;
|
|
150
|
+
settled = true;
|
|
151
|
+
fn();
|
|
152
|
+
};
|
|
103
153
|
const onAbort = () => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
154
|
+
settle(() => {
|
|
155
|
+
cleanup();
|
|
156
|
+
removeWaiter();
|
|
157
|
+
rejectInContext(new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled'));
|
|
158
|
+
});
|
|
107
159
|
};
|
|
108
160
|
const cleanup = () => {
|
|
161
|
+
if (deadlineTimeout) {
|
|
162
|
+
deadlineTimeout.cancel();
|
|
163
|
+
deadlineTimeout = undefined;
|
|
164
|
+
}
|
|
109
165
|
if (signal) {
|
|
110
166
|
signal.removeEventListener('abort', onAbort);
|
|
111
167
|
}
|
|
@@ -114,13 +170,16 @@ class TaskManager {
|
|
|
114
170
|
const waiters = this.waiters.get(taskId);
|
|
115
171
|
if (!waiters)
|
|
116
172
|
return;
|
|
117
|
-
|
|
173
|
+
if (waiter)
|
|
174
|
+
waiters.delete(waiter);
|
|
118
175
|
if (waiters.size === 0)
|
|
119
176
|
this.waiters.delete(taskId);
|
|
120
177
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
178
|
+
waiter = (updated) => {
|
|
179
|
+
settle(() => {
|
|
180
|
+
cleanup();
|
|
181
|
+
resolveInContext(updated);
|
|
182
|
+
});
|
|
124
183
|
};
|
|
125
184
|
if (signal?.aborted) {
|
|
126
185
|
onAbort();
|
|
@@ -132,6 +191,29 @@ class TaskManager {
|
|
|
132
191
|
if (signal) {
|
|
133
192
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
134
193
|
}
|
|
194
|
+
if (Number.isFinite(deadlineMs)) {
|
|
195
|
+
const timeoutMs = Math.max(0, deadlineMs - Date.now());
|
|
196
|
+
if (timeoutMs === 0) {
|
|
197
|
+
settle(() => {
|
|
198
|
+
cleanup();
|
|
199
|
+
removeWaiter();
|
|
200
|
+
this.tasks.delete(taskId);
|
|
201
|
+
resolveInContext(undefined);
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
deadlineTimeout = createUnrefTimeout(timeoutMs, { timeout: true });
|
|
206
|
+
void deadlineTimeout.promise
|
|
207
|
+
.then(() => {
|
|
208
|
+
settle(() => {
|
|
209
|
+
cleanup();
|
|
210
|
+
removeWaiter();
|
|
211
|
+
this.tasks.delete(taskId);
|
|
212
|
+
resolveInContext(undefined);
|
|
213
|
+
});
|
|
214
|
+
})
|
|
215
|
+
.catch(rejectInContext);
|
|
216
|
+
}
|
|
135
217
|
});
|
|
136
218
|
}
|
|
137
219
|
notifyWaiters(task) {
|
|
@@ -151,11 +233,11 @@ class TaskManager {
|
|
|
151
233
|
return Date.now() - createdAt > task.ttl;
|
|
152
234
|
}
|
|
153
235
|
encodeCursor(index) {
|
|
154
|
-
return Buffer.from(String(index)).toString('
|
|
236
|
+
return Buffer.from(String(index)).toString('base64url');
|
|
155
237
|
}
|
|
156
238
|
decodeCursor(cursor) {
|
|
157
239
|
try {
|
|
158
|
-
const decoded = Buffer.from(cursor, '
|
|
240
|
+
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
159
241
|
const value = Number.parseInt(decoded, 10);
|
|
160
242
|
if (!Number.isFinite(value) || value < 0)
|
|
161
243
|
return null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
|
|
2
|
+
import { isError } from './type-guards.js';
|
|
3
|
+
export function createUnrefTimeout(timeoutMs, value) {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const promise = setTimeoutPromise(timeoutMs, value, {
|
|
6
|
+
ref: false,
|
|
7
|
+
signal: controller.signal,
|
|
8
|
+
}).catch((err) => {
|
|
9
|
+
if (isError(err) && err.name === 'AbortError') {
|
|
10
|
+
return new Promise(() => { });
|
|
11
|
+
}
|
|
12
|
+
throw err;
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
promise,
|
|
16
|
+
cancel: () => {
|
|
17
|
+
controller.abort();
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { CallToolResult, ContentBlock } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import type { MarkdownTransformResult } from './transform-types.js';
|
|
4
5
|
export interface FetchUrlInput {
|
|
5
6
|
url: string;
|
|
7
|
+
skipNoiseRemoval?: boolean | undefined;
|
|
8
|
+
forceRefresh?: boolean | undefined;
|
|
6
9
|
}
|
|
7
10
|
export interface ToolContentBlock {
|
|
8
11
|
type: 'text';
|
|
@@ -29,6 +32,8 @@ export type ToolErrorResponse = CallToolResult & {
|
|
|
29
32
|
structuredContent: {
|
|
30
33
|
error: string;
|
|
31
34
|
url: string;
|
|
35
|
+
statusCode?: number;
|
|
36
|
+
details?: Record<string, unknown>;
|
|
32
37
|
};
|
|
33
38
|
isError: true;
|
|
34
39
|
};
|
|
@@ -38,7 +43,11 @@ export interface FetchPipelineOptions<T> {
|
|
|
38
43
|
cacheNamespace: string;
|
|
39
44
|
signal?: AbortSignal;
|
|
40
45
|
cacheVary?: Record<string, unknown> | string;
|
|
41
|
-
|
|
46
|
+
forceRefresh?: boolean;
|
|
47
|
+
transform: (input: {
|
|
48
|
+
buffer: Uint8Array;
|
|
49
|
+
encoding: string;
|
|
50
|
+
}, url: string) => T | Promise<T>;
|
|
42
51
|
serialize?: (result: T) => string;
|
|
43
52
|
deserialize?: (cached: string) => T | undefined;
|
|
44
53
|
}
|
|
@@ -46,6 +55,7 @@ export interface PipelineResult<T> {
|
|
|
46
55
|
data: T;
|
|
47
56
|
fromCache: boolean;
|
|
48
57
|
url: string;
|
|
58
|
+
originalUrl?: string;
|
|
49
59
|
fetchedAt: string;
|
|
50
60
|
cacheKey?: string | null;
|
|
51
61
|
}
|
|
@@ -68,9 +78,16 @@ export interface ProgressNotification {
|
|
|
68
78
|
export interface ToolHandlerExtra {
|
|
69
79
|
signal?: AbortSignal;
|
|
70
80
|
requestId?: string | number;
|
|
81
|
+
sessionId?: unknown;
|
|
82
|
+
requestInfo?: unknown;
|
|
71
83
|
_meta?: RequestMeta;
|
|
72
84
|
sendNotification?: (notification: ProgressNotification) => Promise<void>;
|
|
73
85
|
}
|
|
86
|
+
export declare const fetchUrlInputSchema: z.ZodObject<{
|
|
87
|
+
url: z.ZodURL;
|
|
88
|
+
skipNoiseRemoval: z.ZodOptional<z.ZodBoolean>;
|
|
89
|
+
forceRefresh: z.ZodOptional<z.ZodBoolean>;
|
|
90
|
+
}, z.core.$strict>;
|
|
74
91
|
export declare const FETCH_URL_TOOL_NAME = "fetch-url";
|
|
75
92
|
interface ProgressReporter {
|
|
76
93
|
report: (progress: number, message: string) => Promise<void>;
|
|
@@ -87,8 +104,6 @@ interface InlineContentResult {
|
|
|
87
104
|
export type InlineResult = ReturnType<InlineContentLimiter['apply']>;
|
|
88
105
|
declare class InlineContentLimiter {
|
|
89
106
|
apply(content: string, cacheKey: string | null): InlineContentResult;
|
|
90
|
-
private resolveResourceUri;
|
|
91
|
-
private buildTruncatedFallback;
|
|
92
107
|
}
|
|
93
108
|
export declare function executeFetchPipeline<T>(options: FetchPipelineOptions<T>): Promise<PipelineResult<T>>;
|
|
94
109
|
interface SharedFetchOptions<T extends {
|
|
@@ -96,7 +111,12 @@ interface SharedFetchOptions<T extends {
|
|
|
96
111
|
}> {
|
|
97
112
|
readonly url: string;
|
|
98
113
|
readonly signal?: AbortSignal;
|
|
99
|
-
readonly
|
|
114
|
+
readonly cacheVary?: Record<string, unknown> | string;
|
|
115
|
+
readonly forceRefresh?: boolean;
|
|
116
|
+
readonly transform: (input: {
|
|
117
|
+
buffer: Uint8Array;
|
|
118
|
+
encoding: string;
|
|
119
|
+
}, normalizedUrl: string) => T | Promise<T>;
|
|
100
120
|
readonly serialize?: (result: T) => string;
|
|
101
121
|
readonly deserialize?: (cached: string) => T | undefined;
|
|
102
122
|
}
|
|
@@ -109,7 +129,10 @@ export declare function performSharedFetch<T extends {
|
|
|
109
129
|
pipeline: PipelineResult<T>;
|
|
110
130
|
inlineResult: InlineResult;
|
|
111
131
|
}>;
|
|
112
|
-
export declare function createToolErrorResponse(message: string, url: string
|
|
132
|
+
export declare function createToolErrorResponse(message: string, url: string, extra?: {
|
|
133
|
+
statusCode?: number;
|
|
134
|
+
details?: Record<string, unknown>;
|
|
135
|
+
}): ToolErrorResponse;
|
|
113
136
|
export declare function handleToolError(error: unknown, url: string, fallbackMessage?: string): ToolErrorResponse;
|
|
114
137
|
type MarkdownPipelineResult = MarkdownTransformResult & {
|
|
115
138
|
readonly content: string;
|