@j0hanz/superfetch 2.1.2 → 2.1.4
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.js +37 -7
- package/dist/http.d.ts +4 -1
- package/dist/http.js +61 -18
- package/dist/instructions.md +27 -54
- package/dist/mcp.js +17 -2
- package/dist/tools.d.ts +18 -0
- package/dist/tools.js +44 -4
- package/package.json +4 -4
package/dist/cache.js
CHANGED
|
@@ -410,6 +410,30 @@ function appendServerOnClose(server, handler) {
|
|
|
410
410
|
handler();
|
|
411
411
|
};
|
|
412
412
|
}
|
|
413
|
+
function attachInitializedGate(server) {
|
|
414
|
+
let initialized = false;
|
|
415
|
+
const previousInitialized = server.server.oninitialized;
|
|
416
|
+
server.server.oninitialized = () => {
|
|
417
|
+
initialized = true;
|
|
418
|
+
previousInitialized?.();
|
|
419
|
+
};
|
|
420
|
+
return () => initialized;
|
|
421
|
+
}
|
|
422
|
+
function getClientResourceCapabilities(server) {
|
|
423
|
+
const caps = server.server.getClientCapabilities();
|
|
424
|
+
if (!caps || !isRecord(caps)) {
|
|
425
|
+
return { listChanged: true, subscribe: true };
|
|
426
|
+
}
|
|
427
|
+
const { resources } = caps;
|
|
428
|
+
if (!isRecord(resources)) {
|
|
429
|
+
return { listChanged: true, subscribe: true };
|
|
430
|
+
}
|
|
431
|
+
const { listChanged, subscribe } = resources;
|
|
432
|
+
return {
|
|
433
|
+
listChanged: listChanged === true,
|
|
434
|
+
subscribe: subscribe === true,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
413
437
|
function registerResourceSubscriptionHandlers(server) {
|
|
414
438
|
const subscriptions = new Set();
|
|
415
439
|
server.server.setRequestHandler(SubscribeRequestSchema, (request) => {
|
|
@@ -438,9 +462,10 @@ function notifyResourceUpdate(server, uri, subscriptions) {
|
|
|
438
462
|
});
|
|
439
463
|
}
|
|
440
464
|
export function registerCachedContentResource(server) {
|
|
465
|
+
const isInitialized = attachInitializedGate(server);
|
|
441
466
|
const subscriptions = registerResourceSubscriptionHandlers(server);
|
|
442
467
|
registerCacheContentResource(server);
|
|
443
|
-
registerCacheUpdateSubscription(server, subscriptions);
|
|
468
|
+
registerCacheUpdateSubscription(server, subscriptions, isInitialized);
|
|
444
469
|
}
|
|
445
470
|
function buildCachedContentResponse(uri, cacheKey) {
|
|
446
471
|
const cached = requireCacheEntry(cacheKey);
|
|
@@ -452,20 +477,25 @@ function registerCacheContentResource(server) {
|
|
|
452
477
|
}), {
|
|
453
478
|
title: 'Cached Content',
|
|
454
479
|
description: 'Access previously fetched web content from cache. Namespace: markdown. UrlHash: SHA-256 hash of the URL.',
|
|
455
|
-
mimeType: 'text/
|
|
480
|
+
mimeType: 'text/markdown',
|
|
456
481
|
}, (uri, params) => {
|
|
457
482
|
const { namespace, urlHash } = resolveCacheParams(params);
|
|
458
483
|
const cacheKey = `${namespace}:${urlHash}`;
|
|
459
484
|
return buildCachedContentResponse(uri, cacheKey);
|
|
460
485
|
});
|
|
461
486
|
}
|
|
462
|
-
function registerCacheUpdateSubscription(server, subscriptions) {
|
|
487
|
+
function registerCacheUpdateSubscription(server, subscriptions, isInitialized) {
|
|
463
488
|
const unsubscribe = onCacheUpdate(({ cacheKey }) => {
|
|
464
|
-
|
|
465
|
-
if (!resourceUri)
|
|
489
|
+
if (!server.isConnected() || !isInitialized())
|
|
466
490
|
return;
|
|
467
|
-
|
|
468
|
-
if (
|
|
491
|
+
const { listChanged, subscribe } = getClientResourceCapabilities(server);
|
|
492
|
+
if (subscribe) {
|
|
493
|
+
const resourceUri = toResourceUri(cacheKey);
|
|
494
|
+
if (resourceUri) {
|
|
495
|
+
notifyResourceUpdate(server, resourceUri, subscriptions);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (listChanged) {
|
|
469
499
|
server.sendResourceListChanged();
|
|
470
500
|
}
|
|
471
501
|
});
|
package/dist/http.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ interface SessionEntry {
|
|
|
4
4
|
readonly transport: StreamableHTTPServerTransport;
|
|
5
5
|
createdAt: number;
|
|
6
6
|
lastSeen: number;
|
|
7
|
+
protocolInitialized: boolean;
|
|
7
8
|
}
|
|
8
9
|
interface McpRequestParams {
|
|
9
10
|
_meta?: Record<string, unknown>;
|
|
@@ -47,6 +48,7 @@ interface McpSessionOptions {
|
|
|
47
48
|
readonly sessionStore: SessionStore;
|
|
48
49
|
readonly maxSessions: number;
|
|
49
50
|
}
|
|
51
|
+
type JsonRpcId = string | number | null;
|
|
50
52
|
export declare function createSessionStore(sessionTtlMs: number): SessionStore;
|
|
51
53
|
export declare function reserveSessionSlot(store: SessionStore, maxSessions: number): boolean;
|
|
52
54
|
interface SlotTracker {
|
|
@@ -55,11 +57,12 @@ interface SlotTracker {
|
|
|
55
57
|
readonly isInitialized: () => boolean;
|
|
56
58
|
}
|
|
57
59
|
export declare function createSlotTracker(): SlotTracker;
|
|
58
|
-
export declare function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }: {
|
|
60
|
+
export declare function ensureSessionCapacity({ store, maxSessions, res, evictOldest, requestId, }: {
|
|
59
61
|
store: SessionStore;
|
|
60
62
|
maxSessions: number;
|
|
61
63
|
res: Response;
|
|
62
64
|
evictOldest: (store: SessionStore) => boolean;
|
|
65
|
+
requestId?: JsonRpcId;
|
|
63
66
|
}): boolean;
|
|
64
67
|
type CloseHandler = (() => void) | undefined;
|
|
65
68
|
export declare function composeCloseHandlers(first: CloseHandler, second: CloseHandler): CloseHandler;
|
package/dist/http.js
CHANGED
|
@@ -864,6 +864,13 @@ function sendJsonRpcError(res, code, message, status = 400, id = null) {
|
|
|
864
864
|
id,
|
|
865
865
|
});
|
|
866
866
|
}
|
|
867
|
+
function sendJsonRpcErrorOrNoContent(res, code, message, status, id) {
|
|
868
|
+
if (id === null) {
|
|
869
|
+
res.sendStatus(204);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
sendJsonRpcError(res, code, message, status, id ?? null);
|
|
873
|
+
}
|
|
867
874
|
function getSessionId(req) {
|
|
868
875
|
const header = req.headers['mcp-session-id'];
|
|
869
876
|
return Array.isArray(header) ? header[0] : header;
|
|
@@ -965,21 +972,29 @@ function tryEvictSlot(store, maxSessions, evictOldest) {
|
|
|
965
972
|
currentSize - 1 + inFlightSessions < maxSessions;
|
|
966
973
|
return canFreeSlot && evictOldest(store);
|
|
967
974
|
}
|
|
968
|
-
export function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }) {
|
|
975
|
+
export function ensureSessionCapacity({ store, maxSessions, res, evictOldest, requestId, }) {
|
|
969
976
|
if (!isServerAtCapacity(store, maxSessions)) {
|
|
970
977
|
return true;
|
|
971
978
|
}
|
|
972
979
|
if (tryEvictSlot(store, maxSessions, evictOldest)) {
|
|
973
980
|
return !isServerAtCapacity(store, maxSessions);
|
|
974
981
|
}
|
|
975
|
-
respondServerBusy(res);
|
|
982
|
+
respondServerBusy(res, requestId);
|
|
976
983
|
return false;
|
|
977
984
|
}
|
|
978
|
-
function respondServerBusy(res) {
|
|
979
|
-
|
|
985
|
+
function respondServerBusy(res, requestId) {
|
|
986
|
+
sendJsonRpcErrorOrNoContent(res, -32000, 'Server busy: maximum sessions reached', 503, requestId);
|
|
980
987
|
}
|
|
981
988
|
function respondBadRequest(res, id) {
|
|
982
|
-
|
|
989
|
+
sendJsonRpcErrorOrNoContent(res, -32000, 'Bad Request: Missing session ID or not an initialize request', 400, id);
|
|
990
|
+
}
|
|
991
|
+
function respondSessionNotInitialized(res, requestId) {
|
|
992
|
+
sendJsonRpcErrorOrNoContent(res, -32000, 'Bad Request: Session not initialized', 400, requestId);
|
|
993
|
+
}
|
|
994
|
+
function isAllowedBeforeInitialized(method) {
|
|
995
|
+
return (method === 'initialize' ||
|
|
996
|
+
method === 'notifications/initialized' ||
|
|
997
|
+
method === 'ping');
|
|
983
998
|
}
|
|
984
999
|
function createTimeoutController() {
|
|
985
1000
|
let initTimeout = null;
|
|
@@ -1109,6 +1124,7 @@ async function connectTransportOrThrow({ transport, clearInitTimeout, releaseSlo
|
|
|
1109
1124
|
logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
|
|
1110
1125
|
throw error;
|
|
1111
1126
|
}
|
|
1127
|
+
return mcpServer;
|
|
1112
1128
|
}
|
|
1113
1129
|
function evictExpiredSessionsWithClose(store) {
|
|
1114
1130
|
const evicted = store.evictExpired();
|
|
@@ -1132,13 +1148,15 @@ function evictOldestSessionWithClose(store) {
|
|
|
1132
1148
|
});
|
|
1133
1149
|
return true;
|
|
1134
1150
|
}
|
|
1135
|
-
function reserveSessionIfPossible({ options, res, }) {
|
|
1136
|
-
|
|
1151
|
+
function reserveSessionIfPossible({ options, res, requestId, }) {
|
|
1152
|
+
const capacityArgs = {
|
|
1137
1153
|
store: options.sessionStore,
|
|
1138
1154
|
maxSessions: options.maxSessions,
|
|
1139
1155
|
res,
|
|
1140
1156
|
evictOldest: evictOldestSessionWithClose,
|
|
1141
|
-
|
|
1157
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
1158
|
+
};
|
|
1159
|
+
if (!ensureSessionCapacity(capacityArgs)) {
|
|
1142
1160
|
return false;
|
|
1143
1161
|
}
|
|
1144
1162
|
if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
|
|
@@ -1147,14 +1165,19 @@ function reserveSessionIfPossible({ options, res, }) {
|
|
|
1147
1165
|
}
|
|
1148
1166
|
return true;
|
|
1149
1167
|
}
|
|
1150
|
-
function resolveExistingSessionTransport(store, sessionId, res, requestId) {
|
|
1168
|
+
function resolveExistingSessionTransport(store, sessionId, res, requestId, method) {
|
|
1151
1169
|
const existingSession = store.get(sessionId);
|
|
1152
1170
|
if (existingSession) {
|
|
1171
|
+
if (!existingSession.protocolInitialized &&
|
|
1172
|
+
!isAllowedBeforeInitialized(method)) {
|
|
1173
|
+
respondSessionNotInitialized(res, requestId);
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1153
1176
|
store.touch(sessionId);
|
|
1154
1177
|
return existingSession.transport;
|
|
1155
1178
|
}
|
|
1156
1179
|
// Client supplied a session id but it doesn't exist; Streamable HTTP: invalid session IDs => 404.
|
|
1157
|
-
|
|
1180
|
+
sendJsonRpcErrorOrNoContent(res, -32600, 'Session not found', 404, requestId);
|
|
1158
1181
|
return null;
|
|
1159
1182
|
}
|
|
1160
1183
|
function createSessionContext() {
|
|
@@ -1163,24 +1186,35 @@ function createSessionContext() {
|
|
|
1163
1186
|
const transport = createSessionTransport({ tracker, timeoutController });
|
|
1164
1187
|
return { tracker, timeoutController, transport };
|
|
1165
1188
|
}
|
|
1166
|
-
function
|
|
1189
|
+
function attachSessionInitializedHandler(server, store, sessionId) {
|
|
1190
|
+
const previousInitialized = server.server.oninitialized;
|
|
1191
|
+
server.server.oninitialized = () => {
|
|
1192
|
+
const entry = store.get(sessionId);
|
|
1193
|
+
if (entry) {
|
|
1194
|
+
entry.protocolInitialized = true;
|
|
1195
|
+
}
|
|
1196
|
+
previousInitialized?.();
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function finalizeSessionIfValid({ store, transport, mcpServer, tracker, clearInitTimeout, res, requestId, }) {
|
|
1167
1200
|
const { sessionId } = transport;
|
|
1168
1201
|
if (typeof sessionId !== 'string') {
|
|
1169
1202
|
clearInitTimeout();
|
|
1170
1203
|
tracker.releaseSlot();
|
|
1171
|
-
respondBadRequest(res, null);
|
|
1204
|
+
respondBadRequest(res, requestId ?? null);
|
|
1172
1205
|
return false;
|
|
1173
1206
|
}
|
|
1174
1207
|
finalizeSession({
|
|
1175
1208
|
store,
|
|
1176
1209
|
transport,
|
|
1177
1210
|
sessionId,
|
|
1211
|
+
mcpServer,
|
|
1178
1212
|
tracker,
|
|
1179
1213
|
clearInitTimeout,
|
|
1180
1214
|
});
|
|
1181
1215
|
return true;
|
|
1182
1216
|
}
|
|
1183
|
-
function finalizeSession({ store, transport, sessionId, tracker, clearInitTimeout, }) {
|
|
1217
|
+
function finalizeSession({ store, transport, sessionId, mcpServer, tracker, clearInitTimeout, }) {
|
|
1184
1218
|
clearInitTimeout();
|
|
1185
1219
|
tracker.markInitialized();
|
|
1186
1220
|
tracker.releaseSlot();
|
|
@@ -1189,7 +1223,9 @@ function finalizeSession({ store, transport, sessionId, tracker, clearInitTimeou
|
|
|
1189
1223
|
transport,
|
|
1190
1224
|
createdAt: now,
|
|
1191
1225
|
lastSeen: now,
|
|
1226
|
+
protocolInitialized: false,
|
|
1192
1227
|
});
|
|
1228
|
+
attachSessionInitializedHandler(mcpServer, store, sessionId);
|
|
1193
1229
|
const previousOnClose = transport.onclose;
|
|
1194
1230
|
transport.onclose = composeCloseHandlers(previousOnClose, () => {
|
|
1195
1231
|
store.remove(sessionId);
|
|
@@ -1197,11 +1233,16 @@ function finalizeSession({ store, transport, sessionId, tracker, clearInitTimeou
|
|
|
1197
1233
|
});
|
|
1198
1234
|
logInfo('Session initialized');
|
|
1199
1235
|
}
|
|
1200
|
-
async function createAndConnectTransport({ options, res, }) {
|
|
1201
|
-
|
|
1236
|
+
async function createAndConnectTransport({ options, res, requestId, }) {
|
|
1237
|
+
const reserveArgs = {
|
|
1238
|
+
options,
|
|
1239
|
+
res,
|
|
1240
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
1241
|
+
};
|
|
1242
|
+
if (!reserveSessionIfPossible(reserveArgs))
|
|
1202
1243
|
return null;
|
|
1203
1244
|
const { tracker, timeoutController, transport } = createSessionContext();
|
|
1204
|
-
await connectTransportOrThrow({
|
|
1245
|
+
const mcpServer = await connectTransportOrThrow({
|
|
1205
1246
|
transport,
|
|
1206
1247
|
clearInitTimeout: timeoutController.clear,
|
|
1207
1248
|
releaseSlot: tracker.releaseSlot,
|
|
@@ -1209,9 +1250,11 @@ async function createAndConnectTransport({ options, res, }) {
|
|
|
1209
1250
|
if (!finalizeSessionIfValid({
|
|
1210
1251
|
store: options.sessionStore,
|
|
1211
1252
|
transport,
|
|
1253
|
+
mcpServer,
|
|
1212
1254
|
tracker,
|
|
1213
1255
|
clearInitTimeout: timeoutController.clear,
|
|
1214
1256
|
res,
|
|
1257
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
1215
1258
|
})) {
|
|
1216
1259
|
return null;
|
|
1217
1260
|
}
|
|
@@ -1220,14 +1263,14 @@ async function createAndConnectTransport({ options, res, }) {
|
|
|
1220
1263
|
export async function resolveTransportForPost({ res, body, sessionId, options, }) {
|
|
1221
1264
|
const requestId = body.id ?? null;
|
|
1222
1265
|
if (sessionId) {
|
|
1223
|
-
return resolveExistingSessionTransport(options.sessionStore, sessionId, res, requestId);
|
|
1266
|
+
return resolveExistingSessionTransport(options.sessionStore, sessionId, res, requestId, body.method);
|
|
1224
1267
|
}
|
|
1225
1268
|
if (!isInitializeRequest(body)) {
|
|
1226
1269
|
respondBadRequest(res, requestId);
|
|
1227
1270
|
return null;
|
|
1228
1271
|
}
|
|
1229
1272
|
evictExpiredSessionsWithClose(options.sessionStore);
|
|
1230
|
-
return createAndConnectTransport({ options, res });
|
|
1273
|
+
return createAndConnectTransport({ options, res, requestId });
|
|
1231
1274
|
}
|
|
1232
1275
|
function startSessionCleanupLoop(store, sessionTtlMs) {
|
|
1233
1276
|
const controller = new AbortController();
|
package/dist/instructions.md
CHANGED
|
@@ -1,66 +1,39 @@
|
|
|
1
|
-
# superFetch
|
|
1
|
+
# superFetch Instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Guidance for the Agent:** These instructions are available as a resource (`internal://instructions`). Load them when you are confused about tool usage.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 1. Core Capability
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- If content is missing/truncated, check for a `resource_link` in the output and read the cache resource.
|
|
10
|
-
- If request is vague, ask clarifying questions.
|
|
7
|
+
- **Domain:** Fetch public http(s) URLs, extract readable content, and return clean Markdown.
|
|
8
|
+
- **Primary Resources:** `fetch-url` output (`markdown`, `title`, `url`) and cache resources (`superfetch://cache/markdown/{urlHash}`).
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
## 2. The "Golden Path" Workflows (Critical)
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
- **Action:** Read the Markdown content directly from the tool output or the referenced resource.
|
|
12
|
+
### Workflow A: Fetch and Read
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
1. Call `fetch-url` with a public http(s) URL.
|
|
15
|
+
2. Read `structuredContent.markdown` and `structuredContent.title`.
|
|
16
|
+
3. Cite using `resolvedUrl` or `url` from the response.
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
- **Resources:** Cached content accessible via `superfetch://cache/{namespace}/{hash}`.
|
|
18
|
+
### Workflow B: Large Content / Cache Resource
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
1. If the response includes a `resource_link`, read that resource URI.
|
|
21
|
+
2. If content is missing, list resources and select the matching `superfetch://cache/markdown/{urlHash}` entry.
|
|
22
|
+
> **Constraint:** Never guess resource URIs. Use the returned `resource_link` or list resources first.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
## 3. Tool Nuances & "Gotchas"
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
- **`fetch-url`**:
|
|
27
|
+
- **Latency:** Network-bound; expect slower responses for large pages.
|
|
28
|
+
- **Side Effects:** Calls external websites (open-world).
|
|
29
|
+
- **Input:** `url` must be public http/https. Private/internal addresses are blocked.
|
|
30
|
+
- **Output:** Large content may return a `resource_link` instead of full inline markdown.
|
|
31
|
+
- **Cache resources (`superfetch://cache/markdown/{urlHash}`)**:
|
|
32
|
+
- **Namespace:** Only `markdown` is valid.
|
|
33
|
+
- **Discovery:** Use resource listing or the `resource_link` returned by `fetch-url`.
|
|
30
34
|
|
|
31
|
-
##
|
|
35
|
+
## 4. Error Handling Strategy
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- **Use when:** You need the text content of a specific public URL.
|
|
38
|
-
- **Args:**
|
|
39
|
-
- `url` (string, required): The URL to fetch (must be http/https).
|
|
40
|
-
- **Returns:**
|
|
41
|
-
- `structuredContent` with `markdown`, `title`, `url`.
|
|
42
|
-
- Content block with standard text.
|
|
43
|
-
- Or `resource_link` block if content exceeds inline limits.
|
|
44
|
-
|
|
45
|
-
## Response Shape
|
|
46
|
-
|
|
47
|
-
Success: `{ "content": [...], "structuredContent": { "markdown": "...", "title": "...", "url": "..." } }`
|
|
48
|
-
Error: `{ "isError": true, "structuredContent": { "error": "...", "url": "..." } }`
|
|
49
|
-
|
|
50
|
-
### Common Errors
|
|
51
|
-
|
|
52
|
-
| Code | Meaning | Resolution |
|
|
53
|
-
| ------------------ | -------------------- | ------------------------------- |
|
|
54
|
-
| `VALIDATION_ERROR` | Invalid input URL | Ensure URL is valid http/https |
|
|
55
|
-
| `FETCH_ERROR` | Network/HTTP failure | Verify URL is public/accessible |
|
|
56
|
-
|
|
57
|
-
## Limits
|
|
58
|
-
|
|
59
|
-
- **Max Inline Characters:** 20000
|
|
60
|
-
- **Max Content Size:** 10MB
|
|
61
|
-
- **Fetch Timeout:** 15000ms
|
|
62
|
-
|
|
63
|
-
## Security
|
|
64
|
-
|
|
65
|
-
- Server blocks private/internal IP ranges (localhost, 127.x, 192.168.x, metadata services).
|
|
66
|
-
- Do not attempt to fetch internal network targets.
|
|
37
|
+
- **`VALIDATION_ERROR`**: URL is invalid or blocked. Confirm it is a public http(s) URL.
|
|
38
|
+
- **`FETCH_ERROR`**: Network/HTTP failure. Retry or verify the site is reachable.
|
|
39
|
+
- **Cache miss (`Content not found`)**: Re-run `fetch-url` or verify the cache entry exists.
|
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { registerCachedContentResource } from './cache.js';
|
|
5
5
|
import { config } from './config.js';
|
|
@@ -17,7 +17,6 @@ function createServerCapabilities() {
|
|
|
17
17
|
return {
|
|
18
18
|
tools: { listChanged: false },
|
|
19
19
|
resources: { listChanged: true, subscribe: true },
|
|
20
|
-
logging: {},
|
|
21
20
|
};
|
|
22
21
|
}
|
|
23
22
|
function createServerInstructions(serverVersion) {
|
|
@@ -32,6 +31,21 @@ function createServerInstructions(serverVersion) {
|
|
|
32
31
|
return `superFetch MCP server |${serverVersion}| A high-performance web content fetching and processing server.`;
|
|
33
32
|
}
|
|
34
33
|
}
|
|
34
|
+
function registerInstructionsResource(server) {
|
|
35
|
+
server.registerResource('instructions', new ResourceTemplate('internal://instructions', { list: undefined }), {
|
|
36
|
+
title: 'Server Instructions',
|
|
37
|
+
description: 'Usage guidance for the superFetch MCP server.',
|
|
38
|
+
mimeType: 'text/markdown',
|
|
39
|
+
}, (uri) => ({
|
|
40
|
+
contents: [
|
|
41
|
+
{
|
|
42
|
+
uri: uri.href,
|
|
43
|
+
mimeType: 'text/markdown',
|
|
44
|
+
text: createServerInstructions(config.server.version),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
35
49
|
export function createMcpServer() {
|
|
36
50
|
const server = new McpServer(createServerInfo(), {
|
|
37
51
|
capabilities: createServerCapabilities(),
|
|
@@ -39,6 +53,7 @@ export function createMcpServer() {
|
|
|
39
53
|
});
|
|
40
54
|
registerTools(server);
|
|
41
55
|
registerCachedContentResource(server);
|
|
56
|
+
registerInstructionsResource(server);
|
|
42
57
|
return server;
|
|
43
58
|
}
|
|
44
59
|
function attachServerErrorHandler(server) {
|
package/dist/tools.d.ts
CHANGED
|
@@ -62,9 +62,27 @@ export interface PipelineResult<T> {
|
|
|
62
62
|
fetchedAt: string;
|
|
63
63
|
cacheKey?: string | null;
|
|
64
64
|
}
|
|
65
|
+
export type ProgressToken = string | number;
|
|
66
|
+
export interface RequestMeta {
|
|
67
|
+
progressToken?: ProgressToken | undefined;
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
export interface ProgressNotificationParams {
|
|
71
|
+
progressToken: ProgressToken;
|
|
72
|
+
progress: number;
|
|
73
|
+
total?: number;
|
|
74
|
+
message?: string;
|
|
75
|
+
_meta?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
export interface ProgressNotification {
|
|
78
|
+
method: 'notifications/progress';
|
|
79
|
+
params: ProgressNotificationParams;
|
|
80
|
+
}
|
|
65
81
|
export interface ToolHandlerExtra {
|
|
66
82
|
signal?: AbortSignal;
|
|
67
83
|
requestId?: string | number;
|
|
84
|
+
_meta?: RequestMeta;
|
|
85
|
+
sendNotification?: (notification: ProgressNotification) => Promise<void>;
|
|
68
86
|
}
|
|
69
87
|
export declare const FETCH_URL_TOOL_NAME = "fetch-url";
|
|
70
88
|
export declare const FETCH_URL_TOOL_DESCRIPTION = "Fetches a webpage and converts it to clean Markdown format";
|
package/dist/tools.js
CHANGED
|
@@ -2,12 +2,13 @@ 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, isSystemError } from './errors.js';
|
|
5
|
+
import { FetchError, getErrorMessage, isSystemError } from './errors.js';
|
|
6
6
|
import { fetchNormalizedUrl, normalizeUrl, transformToRawUrl, } from './fetch.js';
|
|
7
7
|
import { getRequestId, logDebug, logError, logWarn, runWithRequestContext, } from './observability.js';
|
|
8
8
|
import { transformHtmlToMarkdown, } from './transform.js';
|
|
9
9
|
import { isRecord } from './utils.js';
|
|
10
10
|
const TRUNCATION_MARKER = '...[truncated]';
|
|
11
|
+
const FETCH_PROGRESS_TOTAL = 4;
|
|
11
12
|
const fetchUrlInputSchema = z.strictObject({
|
|
12
13
|
url: z.url({ protocol: /^https?$/i }).describe('The URL to fetch'),
|
|
13
14
|
});
|
|
@@ -30,6 +31,33 @@ const fetchUrlOutputSchema = z.strictObject({
|
|
|
30
31
|
});
|
|
31
32
|
export const FETCH_URL_TOOL_NAME = 'fetch-url';
|
|
32
33
|
export const FETCH_URL_TOOL_DESCRIPTION = 'Fetches a webpage and converts it to clean Markdown format';
|
|
34
|
+
function createProgressReporter(extra) {
|
|
35
|
+
const token = extra?._meta?.progressToken ?? null;
|
|
36
|
+
const sendNotification = extra?.sendNotification;
|
|
37
|
+
if (token === null || !sendNotification) {
|
|
38
|
+
return { report: async () => { } };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
report: async (progress, message) => {
|
|
42
|
+
try {
|
|
43
|
+
await sendNotification({
|
|
44
|
+
method: 'notifications/progress',
|
|
45
|
+
params: {
|
|
46
|
+
progressToken: token,
|
|
47
|
+
progress,
|
|
48
|
+
total: FETCH_PROGRESS_TOTAL,
|
|
49
|
+
message,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
logWarn('Failed to send progress notification', {
|
|
55
|
+
error: getErrorMessage(error),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
33
61
|
function serializeStructuredContent(structuredContent, fromCache) {
|
|
34
62
|
return JSON.stringify(structuredContent, fromCache ? undefined : null, fromCache ? undefined : 2);
|
|
35
63
|
}
|
|
@@ -354,11 +382,16 @@ function buildFetchUrlContentBlocks(structuredContent, pipeline, inlineResult) {
|
|
|
354
382
|
function logFetchStart(url) {
|
|
355
383
|
logDebug('Fetching URL', { url });
|
|
356
384
|
}
|
|
357
|
-
async function fetchPipeline(url, signal) {
|
|
385
|
+
async function fetchPipeline(url, signal, progress) {
|
|
358
386
|
return performSharedFetch({
|
|
359
387
|
url,
|
|
360
388
|
...(signal === undefined ? {} : { signal }),
|
|
361
|
-
transform: (html, normalizedUrl) =>
|
|
389
|
+
transform: async (html, normalizedUrl) => {
|
|
390
|
+
if (progress) {
|
|
391
|
+
await progress.report(3, 'Transforming content');
|
|
392
|
+
}
|
|
393
|
+
return buildMarkdownTransform()(html, normalizedUrl, signal);
|
|
394
|
+
},
|
|
362
395
|
serialize: serializeMarkdownResult,
|
|
363
396
|
deserialize: deserializeMarkdownResult,
|
|
364
397
|
});
|
|
@@ -376,11 +409,18 @@ async function executeFetch(input, extra) {
|
|
|
376
409
|
if (!url) {
|
|
377
410
|
return createToolErrorResponse('URL is required', '');
|
|
378
411
|
}
|
|
412
|
+
const progress = createProgressReporter(extra);
|
|
413
|
+
await progress.report(1, 'Validating URL');
|
|
379
414
|
logFetchStart(url);
|
|
380
|
-
|
|
415
|
+
await progress.report(2, 'Fetching content');
|
|
416
|
+
const { pipeline, inlineResult } = await fetchPipeline(url, extra?.signal, progress);
|
|
417
|
+
if (pipeline.fromCache) {
|
|
418
|
+
await progress.report(3, 'Using cached content');
|
|
419
|
+
}
|
|
381
420
|
if (inlineResult.error) {
|
|
382
421
|
return createToolErrorResponse(inlineResult.error, url);
|
|
383
422
|
}
|
|
423
|
+
await progress.report(4, 'Finalizing response');
|
|
384
424
|
return buildResponse(pipeline, inlineResult, url);
|
|
385
425
|
}
|
|
386
426
|
export async function fetchUrlToolHandler(input, extra) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/superfetch",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"mcpName": "io.github.j0hanz/superfetch",
|
|
5
5
|
"description": "Intelligent web content fetcher MCP server that converts HTML to clean, AI-readable Markdown",
|
|
6
6
|
"type": "module",
|
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
"@eslint/js": "^9.39.2",
|
|
65
65
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
66
66
|
"@types/express": "^5.0.6",
|
|
67
|
-
"@types/node": "^22.19.
|
|
67
|
+
"@types/node": "^22.19.6",
|
|
68
68
|
"eslint": "^9.23.2",
|
|
69
69
|
"eslint-config-prettier": "^10.1.8",
|
|
70
70
|
"eslint-plugin-de-morgan": "^2.0.0",
|
|
71
71
|
"eslint-plugin-depend": "^1.4.0",
|
|
72
72
|
"eslint-plugin-sonarjs": "^3.0.5",
|
|
73
73
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
74
|
-
"knip": "^5.
|
|
75
|
-
"prettier": "^3.
|
|
74
|
+
"knip": "^5.81.0",
|
|
75
|
+
"prettier": "^3.8.0",
|
|
76
76
|
"tsx": "^4.21.0",
|
|
77
77
|
"typescript": "^5.9.3",
|
|
78
78
|
"typescript-eslint": "^8.53.0"
|