@j0hanz/fetch-url-mcp 0.0.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/README.md +570 -0
- package/dist/AGENTS.md +115 -0
- package/dist/assets/logo.svg +24837 -0
- package/dist/cache.d.ts +47 -0
- package/dist/cache.js +316 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +48 -0
- package/dist/config.d.ts +142 -0
- package/dist/config.js +480 -0
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.js +49 -0
- package/dist/dom-noise-removal.d.ts +1 -0
- package/dist/dom-noise-removal.js +488 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +61 -0
- package/dist/fetch.d.ts +42 -0
- package/dist/fetch.js +1544 -0
- package/dist/host-normalization.d.ts +1 -0
- package/dist/host-normalization.js +77 -0
- package/dist/http-native.d.ts +5 -0
- package/dist/http-native.js +1313 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +91 -0
- package/dist/instructions.md +57 -0
- package/dist/ip-blocklist.d.ts +8 -0
- package/dist/ip-blocklist.js +74 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +34 -0
- package/dist/language-detection.d.ts +2 -0
- package/dist/language-detection.js +364 -0
- package/dist/markdown-cleanup.d.ts +6 -0
- package/dist/markdown-cleanup.js +474 -0
- package/dist/mcp-validator.d.ts +15 -0
- package/dist/mcp-validator.js +44 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +421 -0
- package/dist/observability.d.ts +21 -0
- package/dist/observability.js +211 -0
- package/dist/prompts.d.ts +7 -0
- package/dist/prompts.js +28 -0
- package/dist/resources.d.ts +8 -0
- package/dist/resources.js +216 -0
- package/dist/server-tuning.d.ts +13 -0
- package/dist/server-tuning.js +47 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +174 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.js +218 -0
- package/dist/tasks.d.ts +63 -0
- package/dist/tasks.js +327 -0
- package/dist/timer-utils.d.ts +5 -0
- package/dist/timer-utils.js +20 -0
- package/dist/tools.d.ts +135 -0
- package/dist/tools.js +812 -0
- package/dist/transform-types.d.ts +126 -0
- package/dist/transform-types.js +5 -0
- package/dist/transform.d.ts +36 -0
- package/dist/transform.js +2341 -0
- package/dist/type-guards.d.ts +14 -0
- package/dist/type-guards.js +13 -0
- package/dist/workers/transform-child.d.ts +1 -0
- package/dist/workers/transform-child.js +136 -0
- package/dist/workers/transform-worker.d.ts +1 -0
- package/dist/workers/transform-worker.js +128 -0
- package/package.json +91 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { get as getCacheEntry, keys as listCacheKeys, parseCachedPayload, parseCacheKey, resolveCachedPayloadContent, } from './cache.js';
|
|
4
|
+
const CACHE_RESOURCE_TEMPLATE_URI = 'internal://cache/{namespace}/{hash}';
|
|
5
|
+
const CACHE_RESOURCE_PREFIX = 'internal://cache/';
|
|
6
|
+
const CACHE_NAMESPACE_PATTERN = /^[a-z0-9_-]{1,64}$/i;
|
|
7
|
+
const CACHE_HASH_PATTERN = /^[a-f0-9.]{8,64}$/i;
|
|
8
|
+
const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
|
|
9
|
+
const MAX_COMPLETION_VALUES = 100;
|
|
10
|
+
function isValidCacheResourceParts(parts) {
|
|
11
|
+
return (CACHE_NAMESPACE_PATTERN.test(parts.namespace) &&
|
|
12
|
+
CACHE_HASH_PATTERN.test(parts.hash));
|
|
13
|
+
}
|
|
14
|
+
function decodeSegment(value) {
|
|
15
|
+
try {
|
|
16
|
+
return decodeURIComponent(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function firstVariableValue(value) {
|
|
23
|
+
if (typeof value === 'string') {
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
const first = value[0];
|
|
29
|
+
if (typeof first !== 'string')
|
|
30
|
+
return undefined;
|
|
31
|
+
const trimmed = first.trim();
|
|
32
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function parseCacheResourceFromVariables(variables) {
|
|
37
|
+
const namespace = firstVariableValue(variables.namespace);
|
|
38
|
+
const hash = firstVariableValue(variables.hash);
|
|
39
|
+
if (!namespace || !hash)
|
|
40
|
+
return null;
|
|
41
|
+
const decoded = {
|
|
42
|
+
namespace: decodeSegment(namespace),
|
|
43
|
+
hash: decodeSegment(hash),
|
|
44
|
+
};
|
|
45
|
+
return isValidCacheResourceParts(decoded) ? decoded : null;
|
|
46
|
+
}
|
|
47
|
+
function parseCacheResourceFromUri(uri) {
|
|
48
|
+
if (!uri.href.startsWith(CACHE_RESOURCE_PREFIX))
|
|
49
|
+
return null;
|
|
50
|
+
const rawPath = uri.pathname.startsWith('/')
|
|
51
|
+
? uri.pathname.slice(1)
|
|
52
|
+
: uri.pathname;
|
|
53
|
+
const segments = rawPath.split('/');
|
|
54
|
+
if (segments.length !== 2)
|
|
55
|
+
return null;
|
|
56
|
+
const namespace = segments[0];
|
|
57
|
+
const hash = segments[1];
|
|
58
|
+
if (!namespace || !hash)
|
|
59
|
+
return null;
|
|
60
|
+
const decoded = {
|
|
61
|
+
namespace: decodeSegment(namespace),
|
|
62
|
+
hash: decodeSegment(hash),
|
|
63
|
+
};
|
|
64
|
+
return isValidCacheResourceParts(decoded) ? decoded : null;
|
|
65
|
+
}
|
|
66
|
+
function toCacheResourceUri(parts) {
|
|
67
|
+
const namespace = encodeURIComponent(parts.namespace);
|
|
68
|
+
const hash = encodeURIComponent(parts.hash);
|
|
69
|
+
return `${CACHE_RESOURCE_PREFIX}${namespace}/${hash}`;
|
|
70
|
+
}
|
|
71
|
+
function listCacheNamespaces() {
|
|
72
|
+
const namespaces = new Set();
|
|
73
|
+
for (const key of listCacheKeys()) {
|
|
74
|
+
const parsed = parseCacheKey(key);
|
|
75
|
+
if (!parsed)
|
|
76
|
+
continue;
|
|
77
|
+
namespaces.add(parsed.namespace);
|
|
78
|
+
}
|
|
79
|
+
return [...namespaces].sort((left, right) => left.localeCompare(right));
|
|
80
|
+
}
|
|
81
|
+
function completeCacheNamespaces(value) {
|
|
82
|
+
const normalized = value.trim().toLowerCase();
|
|
83
|
+
return listCacheNamespaces()
|
|
84
|
+
.filter((namespace) => namespace.toLowerCase().startsWith(normalized))
|
|
85
|
+
.slice(0, MAX_COMPLETION_VALUES);
|
|
86
|
+
}
|
|
87
|
+
function completeCacheHashes(value, context) {
|
|
88
|
+
const normalized = value.trim().toLowerCase();
|
|
89
|
+
const namespace = context?.arguments?.namespace?.trim();
|
|
90
|
+
const hashes = new Set();
|
|
91
|
+
for (const key of listCacheKeys()) {
|
|
92
|
+
const parsed = parseCacheKey(key);
|
|
93
|
+
if (!parsed)
|
|
94
|
+
continue;
|
|
95
|
+
if (namespace && parsed.namespace !== namespace)
|
|
96
|
+
continue;
|
|
97
|
+
if (!parsed.urlHash.toLowerCase().startsWith(normalized))
|
|
98
|
+
continue;
|
|
99
|
+
hashes.add(parsed.urlHash);
|
|
100
|
+
}
|
|
101
|
+
return [...hashes]
|
|
102
|
+
.sort((left, right) => left.localeCompare(right))
|
|
103
|
+
.slice(0, MAX_COMPLETION_VALUES);
|
|
104
|
+
}
|
|
105
|
+
function listCacheResources() {
|
|
106
|
+
const resources = listCacheKeys()
|
|
107
|
+
.map((key) => parseCacheKey(key))
|
|
108
|
+
.filter((parts) => Boolean(parts))
|
|
109
|
+
.slice(0, MAX_COMPLETION_VALUES)
|
|
110
|
+
.map((parts) => {
|
|
111
|
+
const cacheParts = {
|
|
112
|
+
namespace: parts.namespace,
|
|
113
|
+
hash: parts.urlHash,
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
uri: toCacheResourceUri(cacheParts),
|
|
117
|
+
name: `${parts.namespace}:${parts.urlHash}`,
|
|
118
|
+
title: 'Cached Markdown',
|
|
119
|
+
description: 'Cached markdown output generated by fetch-url',
|
|
120
|
+
mimeType: 'text/markdown',
|
|
121
|
+
annotations: {
|
|
122
|
+
audience: ['assistant'],
|
|
123
|
+
priority: 0.6,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
return { resources };
|
|
128
|
+
}
|
|
129
|
+
function resolveCacheResourceParts(uri, variables) {
|
|
130
|
+
const fromVariables = parseCacheResourceFromVariables(variables);
|
|
131
|
+
if (fromVariables)
|
|
132
|
+
return fromVariables;
|
|
133
|
+
const fromUri = parseCacheResourceFromUri(uri);
|
|
134
|
+
if (fromUri)
|
|
135
|
+
return fromUri;
|
|
136
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource URI or template arguments');
|
|
137
|
+
}
|
|
138
|
+
function readCacheResource(uri, variables) {
|
|
139
|
+
const parts = resolveCacheResourceParts(uri, variables);
|
|
140
|
+
const cacheKey = `${parts.namespace}:${parts.hash}`;
|
|
141
|
+
const entry = getCacheEntry(cacheKey);
|
|
142
|
+
if (!entry) {
|
|
143
|
+
throw new McpError(RESOURCE_NOT_FOUND_ERROR_CODE, 'Resource not found', {
|
|
144
|
+
uri: uri.href,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const payload = parseCachedPayload(entry.content);
|
|
148
|
+
const markdown = payload ? resolveCachedPayloadContent(payload) : null;
|
|
149
|
+
const text = markdown ?? entry.content;
|
|
150
|
+
return {
|
|
151
|
+
contents: [
|
|
152
|
+
{
|
|
153
|
+
uri: uri.href,
|
|
154
|
+
mimeType: 'text/markdown',
|
|
155
|
+
text,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function registerInstructionResource(server, instructions, iconInfo) {
|
|
161
|
+
server.registerResource('fetch-url-mcp-instructions', 'internal://instructions', {
|
|
162
|
+
title: 'Server Instructions',
|
|
163
|
+
description: 'Guidance for using the Fetch URL MCP server.',
|
|
164
|
+
mimeType: 'text/markdown',
|
|
165
|
+
annotations: {
|
|
166
|
+
audience: ['assistant'],
|
|
167
|
+
priority: 0.9,
|
|
168
|
+
},
|
|
169
|
+
...(iconInfo
|
|
170
|
+
? {
|
|
171
|
+
icons: [
|
|
172
|
+
{
|
|
173
|
+
src: iconInfo.src,
|
|
174
|
+
mimeType: iconInfo.mimeType,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
179
|
+
}, (uri) => ({
|
|
180
|
+
contents: [
|
|
181
|
+
{
|
|
182
|
+
uri: uri.href,
|
|
183
|
+
mimeType: 'text/markdown',
|
|
184
|
+
text: instructions,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
export function registerCacheResourceTemplate(server, iconInfo) {
|
|
190
|
+
const template = new ResourceTemplate(CACHE_RESOURCE_TEMPLATE_URI, {
|
|
191
|
+
list: () => listCacheResources(),
|
|
192
|
+
complete: {
|
|
193
|
+
namespace: (value) => completeCacheNamespaces(value),
|
|
194
|
+
hash: (value, context) => completeCacheHashes(value, context),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
server.registerResource('fetch-url-mcp-cache-entry', template, {
|
|
198
|
+
title: 'Cached Fetch Output',
|
|
199
|
+
description: 'Read cached markdown generated by previous fetch-url calls.',
|
|
200
|
+
mimeType: 'text/markdown',
|
|
201
|
+
annotations: {
|
|
202
|
+
audience: ['assistant'],
|
|
203
|
+
priority: 0.6,
|
|
204
|
+
},
|
|
205
|
+
...(iconInfo
|
|
206
|
+
? {
|
|
207
|
+
icons: [
|
|
208
|
+
{
|
|
209
|
+
src: iconInfo.src,
|
|
210
|
+
mimeType: iconInfo.mimeType,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
}
|
|
214
|
+
: {}),
|
|
215
|
+
}, (uri, variables) => readCacheResource(uri, variables));
|
|
216
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface HttpServerTuningTarget {
|
|
2
|
+
headersTimeout?: number;
|
|
3
|
+
requestTimeout?: number;
|
|
4
|
+
keepAliveTimeout?: number;
|
|
5
|
+
keepAliveTimeoutBuffer?: number;
|
|
6
|
+
maxHeadersCount?: number | null;
|
|
7
|
+
maxConnections?: number;
|
|
8
|
+
on?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
9
|
+
closeIdleConnections?: () => void;
|
|
10
|
+
closeAllConnections?: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function applyHttpServerTuning(server: HttpServerTuningTarget): void;
|
|
13
|
+
export declare function drainConnectionsOnShutdown(server: HttpServerTuningTarget): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { config } from './config.js';
|
|
2
|
+
import { logDebug, logWarn } from './observability.js';
|
|
3
|
+
const DROP_LOG_INTERVAL_MS = 10_000;
|
|
4
|
+
export function applyHttpServerTuning(server) {
|
|
5
|
+
const { headersTimeoutMs, requestTimeoutMs, keepAliveTimeoutMs, keepAliveTimeoutBufferMs, maxHeadersCount, maxConnections, } = config.server.http;
|
|
6
|
+
if (headersTimeoutMs !== undefined) {
|
|
7
|
+
server.headersTimeout = headersTimeoutMs;
|
|
8
|
+
}
|
|
9
|
+
if (requestTimeoutMs !== undefined) {
|
|
10
|
+
server.requestTimeout = requestTimeoutMs;
|
|
11
|
+
}
|
|
12
|
+
if (keepAliveTimeoutMs !== undefined) {
|
|
13
|
+
server.keepAliveTimeout = keepAliveTimeoutMs;
|
|
14
|
+
}
|
|
15
|
+
if (keepAliveTimeoutBufferMs !== undefined) {
|
|
16
|
+
server.keepAliveTimeoutBuffer = keepAliveTimeoutBufferMs;
|
|
17
|
+
}
|
|
18
|
+
if (maxHeadersCount !== undefined) {
|
|
19
|
+
server.maxHeadersCount = maxHeadersCount;
|
|
20
|
+
}
|
|
21
|
+
if (typeof maxConnections === 'number' && maxConnections > 0) {
|
|
22
|
+
server.maxConnections = maxConnections;
|
|
23
|
+
if (typeof server.on === 'function') {
|
|
24
|
+
let lastLoggedAt = 0;
|
|
25
|
+
let droppedSinceLastLog = 0;
|
|
26
|
+
server.on('drop', (data) => {
|
|
27
|
+
droppedSinceLastLog += 1;
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (now - lastLoggedAt < DROP_LOG_INTERVAL_MS)
|
|
30
|
+
return;
|
|
31
|
+
logWarn('Incoming connection dropped (maxConnections reached)', {
|
|
32
|
+
maxConnections,
|
|
33
|
+
dropped: droppedSinceLastLog,
|
|
34
|
+
data,
|
|
35
|
+
});
|
|
36
|
+
lastLoggedAt = now;
|
|
37
|
+
droppedSinceLastLog = 0;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function drainConnectionsOnShutdown(server) {
|
|
43
|
+
if (typeof server.closeIdleConnections === 'function') {
|
|
44
|
+
server.closeIdleConnections();
|
|
45
|
+
logDebug('Closed idle HTTP connections during shutdown');
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { config } from './config.js';
|
|
9
|
+
import { getErrorMessage } from './errors.js';
|
|
10
|
+
import { abortAllTaskExecutions, registerTaskHandlers } from './mcp.js';
|
|
11
|
+
import { logError, logInfo, setMcpServer } from './observability.js';
|
|
12
|
+
import { registerGetHelpPrompt } from './prompts.js';
|
|
13
|
+
import { registerCacheResourceTemplate, registerInstructionResource, } from './resources.js';
|
|
14
|
+
import { registerTools } from './tools.js';
|
|
15
|
+
import { shutdownTransformWorkerPool } from './transform.js';
|
|
16
|
+
async function getLocalIconInfo() {
|
|
17
|
+
const name = 'logo.svg';
|
|
18
|
+
const mime = 'image/svg+xml';
|
|
19
|
+
try {
|
|
20
|
+
const iconPath = new URL(`../assets/${name}`, import.meta.url);
|
|
21
|
+
const buffer = await fs.readFile(iconPath);
|
|
22
|
+
return {
|
|
23
|
+
src: `data:${mime};base64,${buffer.toString('base64')}`,
|
|
24
|
+
mimeType: mime,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
let serverInstructions = `
|
|
33
|
+
Fetch URL MCP Instructions
|
|
34
|
+
(Detailed instructions failed to load - check logs)
|
|
35
|
+
`;
|
|
36
|
+
try {
|
|
37
|
+
serverInstructions = await fs.readFile(path.join(currentDir, 'instructions.md'), 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('[WARNING] Failed to load instructions.md:', getErrorMessage(error));
|
|
41
|
+
}
|
|
42
|
+
function createServerCapabilities() {
|
|
43
|
+
return {
|
|
44
|
+
logging: {},
|
|
45
|
+
resources: {},
|
|
46
|
+
tools: {},
|
|
47
|
+
prompts: {},
|
|
48
|
+
completions: {},
|
|
49
|
+
tasks: {
|
|
50
|
+
list: {},
|
|
51
|
+
cancel: {},
|
|
52
|
+
requests: {
|
|
53
|
+
tools: {
|
|
54
|
+
call: {},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createServerInfo(icons) {
|
|
61
|
+
return {
|
|
62
|
+
name: config.server.name,
|
|
63
|
+
title: 'Fetch URL',
|
|
64
|
+
description: 'Fetch web pages and convert them into clean, AI-readable Markdown.',
|
|
65
|
+
version: config.server.version,
|
|
66
|
+
websiteUrl: 'https://github.com/j0hanz/fetch-url-mcp',
|
|
67
|
+
...(icons ? { icons } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/* -------------------------------------------------------------------------------------------------
|
|
71
|
+
* Server lifecycle
|
|
72
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
73
|
+
export async function createMcpServer() {
|
|
74
|
+
return createMcpServerWithOptions({ registerObservabilityServer: true });
|
|
75
|
+
}
|
|
76
|
+
async function createMcpServerWithOptions(options) {
|
|
77
|
+
const localIcon = await getLocalIconInfo();
|
|
78
|
+
const taskStore = new InMemoryTaskStore();
|
|
79
|
+
const taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
80
|
+
const serverConfig = {
|
|
81
|
+
capabilities: createServerCapabilities(),
|
|
82
|
+
taskStore,
|
|
83
|
+
taskMessageQueue,
|
|
84
|
+
};
|
|
85
|
+
if (serverInstructions) {
|
|
86
|
+
serverConfig.instructions = serverInstructions;
|
|
87
|
+
}
|
|
88
|
+
const serverInfo = createServerInfo(localIcon
|
|
89
|
+
? [
|
|
90
|
+
{
|
|
91
|
+
src: localIcon.src,
|
|
92
|
+
mimeType: localIcon.mimeType,
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
: undefined);
|
|
96
|
+
const server = new McpServer(serverInfo, serverConfig);
|
|
97
|
+
if (options?.registerObservabilityServer ?? true) {
|
|
98
|
+
setMcpServer(server);
|
|
99
|
+
}
|
|
100
|
+
registerTools(server);
|
|
101
|
+
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
102
|
+
registerInstructionResource(server, serverInstructions, localIcon);
|
|
103
|
+
registerCacheResourceTemplate(server, localIcon);
|
|
104
|
+
registerTaskHandlers(server);
|
|
105
|
+
return server;
|
|
106
|
+
}
|
|
107
|
+
export async function createMcpServerForHttpSession() {
|
|
108
|
+
return createMcpServerWithOptions({ registerObservabilityServer: false });
|
|
109
|
+
}
|
|
110
|
+
function attachServerErrorHandler(server) {
|
|
111
|
+
server.server.onerror = (error) => {
|
|
112
|
+
logError('[MCP Error]', error instanceof Error ? error : { error });
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function shutdownServer(server, signal) {
|
|
116
|
+
process.stderr.write(`\n${signal} received, shutting down Fetch URL MCP server...\n`);
|
|
117
|
+
// Ensure any in-flight tool executions are aborted promptly.
|
|
118
|
+
abortAllTaskExecutions();
|
|
119
|
+
await shutdownTransformWorkerPool();
|
|
120
|
+
await server.close();
|
|
121
|
+
}
|
|
122
|
+
function createShutdownHandler(server) {
|
|
123
|
+
let shuttingDown = false;
|
|
124
|
+
let initialSignal = null;
|
|
125
|
+
return (signal) => {
|
|
126
|
+
if (shuttingDown) {
|
|
127
|
+
logInfo('Shutdown already in progress; ignoring signal', {
|
|
128
|
+
signal,
|
|
129
|
+
initialSignal,
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
shuttingDown = true;
|
|
134
|
+
initialSignal = signal;
|
|
135
|
+
Promise.resolve()
|
|
136
|
+
.then(() => shutdownServer(server, signal))
|
|
137
|
+
.catch((err) => {
|
|
138
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
139
|
+
logError('Error during shutdown', error);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
})
|
|
142
|
+
.finally(() => {
|
|
143
|
+
if (process.exitCode === undefined)
|
|
144
|
+
process.exitCode = 0;
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function registerSignalHandlers(handler) {
|
|
149
|
+
process.once('SIGINT', () => {
|
|
150
|
+
handler('SIGINT');
|
|
151
|
+
});
|
|
152
|
+
process.once('SIGTERM', () => {
|
|
153
|
+
handler('SIGTERM');
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function connectStdioServer(server, transport) {
|
|
157
|
+
try {
|
|
158
|
+
await server.connect(transport);
|
|
159
|
+
logInfo('Fetch URL MCP server running on stdio');
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
163
|
+
throw new Error(`Failed to start stdio server: ${err.message}`, {
|
|
164
|
+
cause: err,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function startStdioServer() {
|
|
169
|
+
const server = await createMcpServer();
|
|
170
|
+
const transport = new StdioServerTransport();
|
|
171
|
+
attachServerErrorHandler(server);
|
|
172
|
+
registerSignalHandlers(createShutdownHandler(server));
|
|
173
|
+
await connectStdioServer(server, transport);
|
|
174
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
export interface SessionEntry {
|
|
4
|
+
readonly server: McpServer;
|
|
5
|
+
readonly transport: StreamableHTTPServerTransport;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
lastSeen: number;
|
|
8
|
+
protocolInitialized: boolean;
|
|
9
|
+
authFingerprint: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SessionStore {
|
|
12
|
+
get: (sessionId: string) => SessionEntry | undefined;
|
|
13
|
+
touch: (sessionId: string) => void;
|
|
14
|
+
set: (sessionId: string, entry: SessionEntry) => void;
|
|
15
|
+
remove: (sessionId: string) => SessionEntry | undefined;
|
|
16
|
+
size: () => number;
|
|
17
|
+
inFlight: () => number;
|
|
18
|
+
incrementInFlight: () => void;
|
|
19
|
+
decrementInFlight: () => void;
|
|
20
|
+
clear: () => SessionEntry[];
|
|
21
|
+
evictExpired: () => SessionEntry[];
|
|
22
|
+
evictOldest: () => SessionEntry | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface SlotTracker {
|
|
25
|
+
readonly releaseSlot: () => void;
|
|
26
|
+
readonly markInitialized: () => void;
|
|
27
|
+
readonly isInitialized: () => boolean;
|
|
28
|
+
}
|
|
29
|
+
export type CloseHandler = (() => void) | undefined;
|
|
30
|
+
export declare function composeCloseHandlers(first: CloseHandler, second: CloseHandler): CloseHandler;
|
|
31
|
+
export declare function startSessionCleanupLoop(store: SessionStore, sessionTtlMs: number): AbortController;
|
|
32
|
+
export declare function createSessionStore(sessionTtlMs: number): SessionStore;
|
|
33
|
+
export declare function createSlotTracker(store: SessionStore): SlotTracker;
|
|
34
|
+
export declare function reserveSessionSlot(store: SessionStore, maxSessions: number): boolean;
|
|
35
|
+
export declare function ensureSessionCapacity({ store, maxSessions, evictOldest, }: {
|
|
36
|
+
store: SessionStore;
|
|
37
|
+
maxSessions: number;
|
|
38
|
+
evictOldest: (store: SessionStore) => boolean;
|
|
39
|
+
}): boolean;
|