@j0hanz/superfetch 2.0.0 → 2.1.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 +139 -46
- package/dist/cache.d.ts +42 -0
- package/dist/cache.js +565 -0
- package/dist/config/env-parsers.d.ts +1 -0
- package/dist/config/env-parsers.js +12 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +20 -8
- package/dist/config/types/content.d.ts +1 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +261 -0
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.js +32 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +28 -0
- package/dist/fetch.d.ts +40 -0
- package/dist/fetch.js +910 -0
- package/dist/http/auth.js +161 -2
- package/dist/http/base-middleware.d.ts +7 -0
- package/dist/http/base-middleware.js +143 -0
- package/dist/http/cors.d.ts +0 -5
- package/dist/http/cors.js +0 -6
- package/dist/http/download-routes.js +6 -2
- package/dist/http/error-handler.d.ts +2 -0
- package/dist/http/error-handler.js +55 -0
- package/dist/http/host-allowlist.d.ts +3 -0
- package/dist/http/host-allowlist.js +117 -0
- package/dist/http/mcp-routes.d.ts +8 -2
- package/dist/http/mcp-routes.js +101 -8
- package/dist/http/mcp-session-eviction.d.ts +3 -0
- package/dist/http/mcp-session-eviction.js +24 -0
- package/dist/http/mcp-session-init.d.ts +7 -0
- package/dist/http/mcp-session-init.js +94 -0
- package/dist/http/mcp-session-slots.d.ts +17 -0
- package/dist/http/mcp-session-slots.js +55 -0
- package/dist/http/mcp-session-transport-init.d.ts +7 -0
- package/dist/http/mcp-session-transport-init.js +41 -0
- package/dist/http/mcp-session-types.d.ts +5 -0
- package/dist/http/mcp-session-types.js +1 -0
- package/dist/http/mcp-session.d.ts +9 -9
- package/dist/http/mcp-session.js +5 -114
- package/dist/http/mcp-sessions.d.ts +41 -0
- package/dist/http/mcp-sessions.js +392 -0
- package/dist/http/rate-limit.js +2 -2
- package/dist/http/server-middleware.d.ts +6 -1
- package/dist/http/server-middleware.js +3 -117
- package/dist/http/server-shutdown.js +1 -1
- package/dist/http/server-tuning.d.ts +9 -0
- package/dist/http/server-tuning.js +45 -0
- package/dist/http/server.js +206 -9
- package/dist/http/session-cleanup.js +8 -5
- package/dist/http.d.ts +78 -0
- package/dist/http.js +1437 -0
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +94 -0
- package/dist/middleware/error-handler.d.ts +1 -1
- package/dist/middleware/error-handler.js +31 -30
- package/dist/observability.d.ts +16 -0
- package/dist/observability.js +78 -0
- package/dist/resources/cached-content-params.d.ts +5 -0
- package/dist/resources/cached-content-params.js +36 -0
- package/dist/resources/cached-content.js +33 -33
- package/dist/server.js +21 -6
- package/dist/services/cache-events.d.ts +8 -0
- package/dist/services/cache-events.js +19 -0
- package/dist/services/cache.d.ts +5 -4
- package/dist/services/cache.js +49 -45
- package/dist/services/context.d.ts +2 -0
- package/dist/services/context.js +3 -0
- package/dist/services/extractor.d.ts +1 -0
- package/dist/services/extractor.js +77 -40
- package/dist/services/fetcher/agents.js +1 -1
- package/dist/services/fetcher/dns-selection.js +1 -1
- package/dist/services/fetcher/interceptors.js +29 -60
- package/dist/services/fetcher/redirects.js +12 -4
- package/dist/services/fetcher/response.js +18 -8
- package/dist/services/fetcher.d.ts +23 -0
- package/dist/services/fetcher.js +553 -13
- package/dist/services/logger.js +4 -1
- package/dist/services/telemetry.d.ts +19 -0
- package/dist/services/telemetry.js +43 -0
- package/dist/services/transform-worker-pool.d.ts +10 -3
- package/dist/services/transform-worker-pool.js +213 -184
- package/dist/tools/handlers/fetch-single.shared.d.ts +11 -3
- package/dist/tools/handlers/fetch-single.shared.js +131 -2
- package/dist/tools/handlers/fetch-url.tool.d.ts +6 -0
- package/dist/tools/handlers/fetch-url.tool.js +56 -12
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +13 -1
- package/dist/tools/schemas.d.ts +2 -0
- package/dist/tools/schemas.js +8 -0
- package/dist/tools/utils/content-shaping.js +19 -4
- package/dist/tools/utils/content-transform-core.d.ts +5 -0
- package/dist/tools/utils/content-transform-core.js +180 -0
- package/dist/tools/utils/content-transform-workers.d.ts +1 -0
- package/dist/tools/utils/content-transform-workers.js +1 -0
- package/dist/tools/utils/content-transform.d.ts +2 -1
- package/dist/tools/utils/content-transform.js +37 -136
- package/dist/tools/utils/fetch-pipeline.js +47 -56
- package/dist/tools/utils/frontmatter.d.ts +3 -0
- package/dist/tools/utils/frontmatter.js +73 -0
- package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
- package/dist/tools/utils/markdown-heuristics.js +19 -0
- package/dist/tools/utils/markdown-signals.d.ts +1 -0
- package/dist/tools/utils/markdown-signals.js +19 -0
- package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
- package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
- package/dist/tools/utils/raw-markdown.d.ts +6 -0
- package/dist/tools/utils/raw-markdown.js +149 -0
- package/dist/tools.d.ts +104 -0
- package/dist/tools.js +421 -0
- package/dist/transform.d.ts +69 -0
- package/dist/transform.js +1509 -0
- package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
- package/dist/transformers/markdown/fenced-code-rule.js +38 -0
- package/dist/transformers/markdown/frontmatter.d.ts +2 -0
- package/dist/transformers/markdown/frontmatter.js +45 -0
- package/dist/transformers/markdown/noise-rule.d.ts +2 -0
- package/dist/transformers/markdown/noise-rule.js +80 -0
- package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
- package/dist/transformers/markdown/turndown-instance.js +19 -0
- package/dist/transformers/markdown.d.ts +5 -0
- package/dist/transformers/markdown.js +314 -0
- package/dist/transformers/markdown.transformer.js +2 -189
- package/dist/utils/cancellation.d.ts +1 -0
- package/dist/utils/cancellation.js +18 -0
- package/dist/utils/code-language-bash.d.ts +1 -0
- package/dist/utils/code-language-bash.js +48 -0
- package/dist/utils/code-language-core.d.ts +2 -0
- package/dist/utils/code-language-core.js +13 -0
- package/dist/utils/code-language-detectors.d.ts +5 -0
- package/dist/utils/code-language-detectors.js +142 -0
- package/dist/utils/code-language-helpers.d.ts +5 -0
- package/dist/utils/code-language-helpers.js +62 -0
- package/dist/utils/code-language-parsing.d.ts +5 -0
- package/dist/utils/code-language-parsing.js +62 -0
- package/dist/utils/code-language.js +250 -46
- package/dist/utils/error-details.d.ts +3 -0
- package/dist/utils/error-details.js +12 -0
- package/dist/utils/filename-generator.js +14 -3
- package/dist/utils/host-normalizer.d.ts +1 -0
- package/dist/utils/host-normalizer.js +37 -0
- package/dist/utils/ip-address.d.ts +4 -0
- package/dist/utils/ip-address.js +6 -0
- package/dist/utils/tool-error-handler.js +12 -17
- package/dist/utils/url-redactor.d.ts +1 -0
- package/dist/utils/url-redactor.js +13 -0
- package/dist/utils/url-validator.js +35 -20
- package/dist/workers/transform-worker.js +82 -38
- package/package.json +13 -10
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { startHttpServer } from './http.js';
|
|
4
|
+
import { startStdioServer } from './mcp.js';
|
|
5
|
+
import { logError } from './observability.js';
|
|
6
6
|
const { values } = parseArgs({
|
|
7
7
|
options: {
|
|
8
8
|
stdio: { type: 'boolean', default: false },
|
package/dist/mcp.d.ts
ADDED
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { registerCachedContentResource } from './cache.js';
|
|
4
|
+
import { config } from './config.js';
|
|
5
|
+
import { destroyAgents } from './fetch.js';
|
|
6
|
+
import { logError, logInfo } from './observability.js';
|
|
7
|
+
import { registerTools } from './tools.js';
|
|
8
|
+
import { shutdownTransformWorkerPool } from './transform.js';
|
|
9
|
+
function createServerInfo() {
|
|
10
|
+
return {
|
|
11
|
+
name: config.server.name,
|
|
12
|
+
version: config.server.version,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function createServerCapabilities() {
|
|
16
|
+
return {
|
|
17
|
+
tools: { listChanged: false },
|
|
18
|
+
resources: { listChanged: true, subscribe: true },
|
|
19
|
+
logging: {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function createServerInstructions(serverVersion) {
|
|
23
|
+
return `superFetch MCP server |${serverVersion}| A high-performance web content fetching and processing server.`;
|
|
24
|
+
}
|
|
25
|
+
export function createMcpServer() {
|
|
26
|
+
const server = new McpServer(createServerInfo(), {
|
|
27
|
+
capabilities: createServerCapabilities(),
|
|
28
|
+
instructions: createServerInstructions(config.server.version),
|
|
29
|
+
});
|
|
30
|
+
registerTools(server);
|
|
31
|
+
registerCachedContentResource(server);
|
|
32
|
+
return server;
|
|
33
|
+
}
|
|
34
|
+
function attachServerErrorHandler(server) {
|
|
35
|
+
server.server.onerror = (error) => {
|
|
36
|
+
logError('[MCP Error]', error instanceof Error ? error : { error });
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function handleShutdownSignal(server, signal) {
|
|
40
|
+
process.stderr.write(`\n${signal} received, shutting down superFetch MCP server...\n`);
|
|
41
|
+
Promise.resolve()
|
|
42
|
+
.then(async () => {
|
|
43
|
+
destroyAgents();
|
|
44
|
+
await shutdownTransformWorkerPool();
|
|
45
|
+
await server.close();
|
|
46
|
+
})
|
|
47
|
+
.catch((err) => {
|
|
48
|
+
logError('Error during shutdown', err instanceof Error ? err : undefined);
|
|
49
|
+
})
|
|
50
|
+
.finally(() => {
|
|
51
|
+
process.exit(0);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function createShutdownHandler(server) {
|
|
55
|
+
let shuttingDown = false;
|
|
56
|
+
let initialSignal = null;
|
|
57
|
+
return (signal) => {
|
|
58
|
+
if (shuttingDown) {
|
|
59
|
+
logInfo('Shutdown already in progress; ignoring signal', {
|
|
60
|
+
signal,
|
|
61
|
+
initialSignal,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
shuttingDown = true;
|
|
66
|
+
initialSignal = signal;
|
|
67
|
+
handleShutdownSignal(server, signal);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function registerSignalHandlers(handler) {
|
|
71
|
+
process.once('SIGINT', () => {
|
|
72
|
+
handler('SIGINT');
|
|
73
|
+
});
|
|
74
|
+
process.once('SIGTERM', () => {
|
|
75
|
+
handler('SIGTERM');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function connectStdioServer(server, transport) {
|
|
79
|
+
try {
|
|
80
|
+
await server.connect(transport);
|
|
81
|
+
logInfo('superFetch MCP server running on stdio');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logError('Failed to start stdio server', error instanceof Error ? error : undefined);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function startStdioServer() {
|
|
89
|
+
const server = createMcpServer();
|
|
90
|
+
const transport = new StdioServerTransport();
|
|
91
|
+
attachServerErrorHandler(server);
|
|
92
|
+
registerSignalHandlers(createShutdownHandler(server));
|
|
93
|
+
await connectStdioServer(server, transport);
|
|
94
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
-
export declare function errorHandler(err: Error, req: Request, res: Response,
|
|
2
|
+
export declare function errorHandler(err: Error, req: Request, res: Response, next: NextFunction): void;
|
|
@@ -1,55 +1,56 @@
|
|
|
1
1
|
import { FetchError } from '../errors/app-error.js';
|
|
2
2
|
import { logError } from '../services/logger.js';
|
|
3
|
-
function getStatusCode(
|
|
4
|
-
return
|
|
3
|
+
function getStatusCode(fetchError) {
|
|
4
|
+
return fetchError ? fetchError.statusCode : 500;
|
|
5
5
|
}
|
|
6
|
-
function getErrorCode(
|
|
7
|
-
return
|
|
6
|
+
function getErrorCode(fetchError) {
|
|
7
|
+
return fetchError ? fetchError.code : 'INTERNAL_ERROR';
|
|
8
8
|
}
|
|
9
|
-
function getErrorMessage(
|
|
10
|
-
return
|
|
9
|
+
function getErrorMessage(fetchError) {
|
|
10
|
+
return fetchError ? fetchError.message : 'Internal Server Error';
|
|
11
11
|
}
|
|
12
|
-
function getErrorDetails(
|
|
13
|
-
if (
|
|
14
|
-
return
|
|
12
|
+
function getErrorDetails(fetchError) {
|
|
13
|
+
if (fetchError && Object.keys(fetchError.details).length > 0) {
|
|
14
|
+
return fetchError.details;
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
|
-
function setRetryAfterHeader(res,
|
|
19
|
-
const retryAfter = resolveRetryAfter(
|
|
20
|
-
if (
|
|
18
|
+
function setRetryAfterHeader(res, fetchError) {
|
|
19
|
+
const retryAfter = resolveRetryAfter(fetchError);
|
|
20
|
+
if (retryAfter === undefined)
|
|
21
21
|
return;
|
|
22
22
|
res.set('Retry-After', retryAfter);
|
|
23
23
|
}
|
|
24
|
-
function buildErrorResponse(
|
|
25
|
-
const details = getErrorDetails(
|
|
24
|
+
function buildErrorResponse(fetchError) {
|
|
25
|
+
const details = getErrorDetails(fetchError);
|
|
26
26
|
const response = {
|
|
27
27
|
error: {
|
|
28
|
-
message: getErrorMessage(
|
|
29
|
-
code: getErrorCode(
|
|
30
|
-
statusCode: getStatusCode(
|
|
28
|
+
message: getErrorMessage(fetchError),
|
|
29
|
+
code: getErrorCode(fetchError),
|
|
30
|
+
statusCode: getStatusCode(fetchError),
|
|
31
31
|
...(details && { details }),
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
34
|
// Never expose stack traces in production
|
|
35
35
|
return response;
|
|
36
36
|
}
|
|
37
|
-
function resolveRetryAfter(
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const { retryAfter } = err.details;
|
|
43
|
-
if (!isRetryAfterValue(retryAfter))
|
|
44
|
-
return null;
|
|
45
|
-
return String(retryAfter);
|
|
37
|
+
function resolveRetryAfter(fetchError) {
|
|
38
|
+
if (fetchError?.statusCode !== 429)
|
|
39
|
+
return undefined;
|
|
40
|
+
const { retryAfter } = fetchError.details;
|
|
41
|
+
return isRetryAfterValue(retryAfter) ? String(retryAfter) : undefined;
|
|
46
42
|
}
|
|
47
43
|
function isRetryAfterValue(value) {
|
|
48
44
|
return typeof value === 'number' || typeof value === 'string';
|
|
49
45
|
}
|
|
50
|
-
export function errorHandler(err, req, res,
|
|
51
|
-
|
|
46
|
+
export function errorHandler(err, req, res, next) {
|
|
47
|
+
if (res.headersSent) {
|
|
48
|
+
next(err);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const fetchError = err instanceof FetchError ? err : null;
|
|
52
|
+
const statusCode = getStatusCode(fetchError);
|
|
52
53
|
logError(`HTTP ${statusCode}: ${err.message} - ${req.method} ${req.path}`, err);
|
|
53
|
-
setRetryAfterHeader(res,
|
|
54
|
-
res.status(statusCode).json(buildErrorResponse(
|
|
54
|
+
setRetryAfterHeader(res, fetchError);
|
|
55
|
+
res.status(statusCode).json(buildErrorResponse(fetchError));
|
|
55
56
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type LogMetadata = Record<string, unknown>;
|
|
2
|
+
interface RequestContext {
|
|
3
|
+
readonly requestId: string;
|
|
4
|
+
readonly sessionId?: string;
|
|
5
|
+
readonly operationId?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function runWithRequestContext<T>(context: RequestContext, fn: () => T): T;
|
|
8
|
+
export declare function getRequestId(): string | undefined;
|
|
9
|
+
export declare function getSessionId(): string | undefined;
|
|
10
|
+
export declare function getOperationId(): string | undefined;
|
|
11
|
+
export declare function logInfo(message: string, meta?: LogMetadata): void;
|
|
12
|
+
export declare function logDebug(message: string, meta?: LogMetadata): void;
|
|
13
|
+
export declare function logWarn(message: string, meta?: LogMetadata): void;
|
|
14
|
+
export declare function logError(message: string, error?: Error | LogMetadata): void;
|
|
15
|
+
export declare function redactUrl(rawUrl: string): string;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
const requestContext = new AsyncLocalStorage();
|
|
4
|
+
export function runWithRequestContext(context, fn) {
|
|
5
|
+
return requestContext.run(context, fn);
|
|
6
|
+
}
|
|
7
|
+
export function getRequestId() {
|
|
8
|
+
return requestContext.getStore()?.requestId;
|
|
9
|
+
}
|
|
10
|
+
export function getSessionId() {
|
|
11
|
+
return requestContext.getStore()?.sessionId;
|
|
12
|
+
}
|
|
13
|
+
export function getOperationId() {
|
|
14
|
+
return requestContext.getStore()?.operationId;
|
|
15
|
+
}
|
|
16
|
+
function formatMetadata(meta) {
|
|
17
|
+
const requestId = getRequestId();
|
|
18
|
+
const sessionId = getSessionId();
|
|
19
|
+
const operationId = getOperationId();
|
|
20
|
+
const contextMeta = {};
|
|
21
|
+
if (requestId)
|
|
22
|
+
contextMeta.requestId = requestId;
|
|
23
|
+
if (sessionId)
|
|
24
|
+
contextMeta.sessionId = sessionId;
|
|
25
|
+
if (operationId)
|
|
26
|
+
contextMeta.operationId = operationId;
|
|
27
|
+
const merged = { ...contextMeta, ...meta };
|
|
28
|
+
return Object.keys(merged).length > 0 ? ` ${JSON.stringify(merged)}` : '';
|
|
29
|
+
}
|
|
30
|
+
function createTimestamp() {
|
|
31
|
+
return new Date().toISOString();
|
|
32
|
+
}
|
|
33
|
+
function formatLogEntry(level, message, meta) {
|
|
34
|
+
return `[${createTimestamp()}] ${level.toUpperCase()}: ${message}${formatMetadata(meta)}`;
|
|
35
|
+
}
|
|
36
|
+
function shouldLog(level) {
|
|
37
|
+
// Debug logs only when LOG_LEVEL=debug
|
|
38
|
+
if (level === 'debug')
|
|
39
|
+
return config.logging.level === 'debug';
|
|
40
|
+
// All other levels always log
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
export function logInfo(message, meta) {
|
|
44
|
+
if (shouldLog('info')) {
|
|
45
|
+
process.stderr.write(`${formatLogEntry('info', message, meta)}\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function logDebug(message, meta) {
|
|
49
|
+
if (shouldLog('debug')) {
|
|
50
|
+
process.stderr.write(`${formatLogEntry('debug', message, meta)}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function logWarn(message, meta) {
|
|
54
|
+
if (shouldLog('warn')) {
|
|
55
|
+
process.stderr.write(`${formatLogEntry('warn', message, meta)}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function logError(message, error) {
|
|
59
|
+
if (!shouldLog('error'))
|
|
60
|
+
return;
|
|
61
|
+
const errorMeta = error instanceof Error
|
|
62
|
+
? { error: error.message, stack: error.stack }
|
|
63
|
+
: (error ?? {});
|
|
64
|
+
process.stderr.write(`${formatLogEntry('error', message, errorMeta)}\n`);
|
|
65
|
+
}
|
|
66
|
+
export function redactUrl(rawUrl) {
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL(rawUrl);
|
|
69
|
+
url.username = '';
|
|
70
|
+
url.password = '';
|
|
71
|
+
url.hash = '';
|
|
72
|
+
url.search = '';
|
|
73
|
+
return url.toString();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return rawUrl;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { isRecord } from '../utils/guards.js';
|
|
3
|
+
export const CACHE_NAMESPACE = 'markdown';
|
|
4
|
+
const HASH_PATTERN = /^[a-f0-9.]+$/i;
|
|
5
|
+
export function resolveCacheParams(params) {
|
|
6
|
+
const parsed = requireRecordParams(params);
|
|
7
|
+
const namespace = requireParamString(parsed, 'namespace');
|
|
8
|
+
const urlHash = requireParamString(parsed, 'urlHash');
|
|
9
|
+
if (!isValidNamespace(namespace) || !isValidHash(urlHash)) {
|
|
10
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
11
|
+
}
|
|
12
|
+
return { namespace, urlHash };
|
|
13
|
+
}
|
|
14
|
+
function requireRecordParams(value) {
|
|
15
|
+
if (!isRecord(value)) {
|
|
16
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function requireParamString(params, key) {
|
|
21
|
+
const raw = params[key];
|
|
22
|
+
const resolved = resolveStringParam(raw);
|
|
23
|
+
if (!resolved) {
|
|
24
|
+
throw new McpError(ErrorCode.InvalidParams, 'Both namespace and urlHash parameters are required');
|
|
25
|
+
}
|
|
26
|
+
return resolved;
|
|
27
|
+
}
|
|
28
|
+
function isValidNamespace(namespace) {
|
|
29
|
+
return namespace === CACHE_NAMESPACE;
|
|
30
|
+
}
|
|
31
|
+
function isValidHash(hash) {
|
|
32
|
+
return HASH_PATTERN.test(hash) && hash.length >= 8 && hash.length <= 64;
|
|
33
|
+
}
|
|
34
|
+
function resolveStringParam(value) {
|
|
35
|
+
return typeof value === 'string' ? value : null;
|
|
36
|
+
}
|
|
@@ -4,10 +4,42 @@ import * as cache from '../services/cache.js';
|
|
|
4
4
|
import { parseCacheKey, toResourceUri } from '../services/cache-keys.js';
|
|
5
5
|
import { logWarn } from '../services/logger.js';
|
|
6
6
|
import { parseCachedPayload, resolveCachedPayloadContent, } from '../utils/cached-payload.js';
|
|
7
|
-
import { getErrorMessage } from '../utils/error-
|
|
7
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
8
8
|
import { isRecord } from '../utils/guards.js';
|
|
9
9
|
const CACHE_NAMESPACE = 'markdown';
|
|
10
10
|
const HASH_PATTERN = /^[a-f0-9.]+$/i;
|
|
11
|
+
function resolveCacheParams(params) {
|
|
12
|
+
const parsed = requireRecordParams(params);
|
|
13
|
+
const namespace = requireParamString(parsed, 'namespace');
|
|
14
|
+
const urlHash = requireParamString(parsed, 'urlHash');
|
|
15
|
+
if (!isValidNamespace(namespace) || !isValidHash(urlHash)) {
|
|
16
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
17
|
+
}
|
|
18
|
+
return { namespace, urlHash };
|
|
19
|
+
}
|
|
20
|
+
function requireRecordParams(value) {
|
|
21
|
+
if (!isRecord(value)) {
|
|
22
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function requireParamString(params, key) {
|
|
27
|
+
const raw = params[key];
|
|
28
|
+
const resolved = resolveStringParam(raw);
|
|
29
|
+
if (!resolved) {
|
|
30
|
+
throw new McpError(ErrorCode.InvalidParams, 'Both namespace and urlHash parameters are required');
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
function isValidNamespace(namespace) {
|
|
35
|
+
return namespace === CACHE_NAMESPACE;
|
|
36
|
+
}
|
|
37
|
+
function isValidHash(hash) {
|
|
38
|
+
return HASH_PATTERN.test(hash) && hash.length >= 8 && hash.length <= 64;
|
|
39
|
+
}
|
|
40
|
+
function resolveStringParam(value) {
|
|
41
|
+
return typeof value === 'string' ? value : null;
|
|
42
|
+
}
|
|
11
43
|
function buildResourceEntry(namespace, urlHash) {
|
|
12
44
|
return {
|
|
13
45
|
name: `${namespace}:${urlHash}`,
|
|
@@ -42,29 +74,6 @@ export function registerCachedContentResource(server) {
|
|
|
42
74
|
registerCacheContentResource(server);
|
|
43
75
|
registerCacheUpdateSubscription(server);
|
|
44
76
|
}
|
|
45
|
-
function resolveCacheParams(params) {
|
|
46
|
-
const parsed = requireRecordParams(params);
|
|
47
|
-
const namespace = requireParamString(parsed, 'namespace');
|
|
48
|
-
const urlHash = requireParamString(parsed, 'urlHash');
|
|
49
|
-
if (!isValidNamespace(namespace) || !isValidHash(urlHash)) {
|
|
50
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
51
|
-
}
|
|
52
|
-
return { namespace, urlHash };
|
|
53
|
-
}
|
|
54
|
-
function requireRecordParams(value) {
|
|
55
|
-
if (!isRecord(value)) {
|
|
56
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
function requireParamString(params, key) {
|
|
61
|
-
const raw = params[key];
|
|
62
|
-
const resolved = resolveStringParam(raw);
|
|
63
|
-
if (!resolved) {
|
|
64
|
-
throw new McpError(ErrorCode.InvalidParams, 'Both namespace and urlHash parameters are required');
|
|
65
|
-
}
|
|
66
|
-
return resolved;
|
|
67
|
-
}
|
|
68
77
|
function buildCachedContentResponse(uri, cacheKey) {
|
|
69
78
|
const cached = requireCacheEntry(cacheKey);
|
|
70
79
|
return buildMarkdownContentResponse(uri, cached.content);
|
|
@@ -98,15 +107,6 @@ function registerCacheUpdateSubscription(server) {
|
|
|
98
107
|
unsubscribe();
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
|
-
function isValidNamespace(namespace) {
|
|
102
|
-
return namespace === CACHE_NAMESPACE;
|
|
103
|
-
}
|
|
104
|
-
function isValidHash(hash) {
|
|
105
|
-
return HASH_PATTERN.test(hash) && hash.length >= 8 && hash.length <= 64;
|
|
106
|
-
}
|
|
107
|
-
function resolveStringParam(value) {
|
|
108
|
-
return typeof value === 'string' ? value : null;
|
|
109
|
-
}
|
|
110
110
|
function requireCacheEntry(cacheKey) {
|
|
111
111
|
const cached = cache.get(cacheKey);
|
|
112
112
|
if (!cached) {
|
package/dist/server.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { config } from './config/index.js';
|
|
4
|
-
import { destroyAgents } from './services/fetcher
|
|
4
|
+
import { destroyAgents } from './services/fetcher.js';
|
|
5
5
|
import { logError, logInfo } from './services/logger.js';
|
|
6
|
+
import { shutdownTransformWorkerPool } from './services/transform-worker-pool.js';
|
|
6
7
|
import { registerTools } from './tools/index.js';
|
|
7
8
|
import { registerCachedContentResource } from './resources/cached-content.js';
|
|
8
9
|
function createServerInfo() {
|
|
@@ -37,9 +38,12 @@ function attachServerErrorHandler(server) {
|
|
|
37
38
|
}
|
|
38
39
|
function handleShutdownSignal(server, signal) {
|
|
39
40
|
process.stderr.write(`\n${signal} received, shutting down superFetch MCP server...\n`);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
Promise.resolve()
|
|
42
|
+
.then(async () => {
|
|
43
|
+
destroyAgents();
|
|
44
|
+
await shutdownTransformWorkerPool();
|
|
45
|
+
await server.close();
|
|
46
|
+
})
|
|
43
47
|
.catch((err) => {
|
|
44
48
|
logError('Error during shutdown', err instanceof Error ? err : undefined);
|
|
45
49
|
})
|
|
@@ -48,15 +52,26 @@ function handleShutdownSignal(server, signal) {
|
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
54
|
function createShutdownHandler(server) {
|
|
55
|
+
let shuttingDown = false;
|
|
56
|
+
let initialSignal = null;
|
|
51
57
|
return (signal) => {
|
|
58
|
+
if (shuttingDown) {
|
|
59
|
+
logInfo('Shutdown already in progress; ignoring signal', {
|
|
60
|
+
signal,
|
|
61
|
+
initialSignal,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
shuttingDown = true;
|
|
66
|
+
initialSignal = signal;
|
|
52
67
|
handleShutdownSignal(server, signal);
|
|
53
68
|
};
|
|
54
69
|
}
|
|
55
70
|
function registerSignalHandlers(handler) {
|
|
56
|
-
process.
|
|
71
|
+
process.once('SIGINT', () => {
|
|
57
72
|
handler('SIGINT');
|
|
58
73
|
});
|
|
59
|
-
process.
|
|
74
|
+
process.once('SIGTERM', () => {
|
|
60
75
|
handler('SIGTERM');
|
|
61
76
|
});
|
|
62
77
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CacheKeyParts } from './cache-keys.js';
|
|
2
|
+
export interface CacheUpdateEvent extends CacheKeyParts {
|
|
3
|
+
cacheKey: string;
|
|
4
|
+
}
|
|
5
|
+
type CacheUpdateListener = (event: CacheUpdateEvent) => void;
|
|
6
|
+
export declare function onCacheUpdate(listener: CacheUpdateListener): () => void;
|
|
7
|
+
export declare function notifyCacheUpdate(cacheKey: string): void;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parseCacheKey } from './cache-keys.js';
|
|
2
|
+
const updateListeners = new Set();
|
|
3
|
+
export function onCacheUpdate(listener) {
|
|
4
|
+
updateListeners.add(listener);
|
|
5
|
+
return () => {
|
|
6
|
+
updateListeners.delete(listener);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function notifyCacheUpdate(cacheKey) {
|
|
10
|
+
if (updateListeners.size === 0)
|
|
11
|
+
return;
|
|
12
|
+
const parts = parseCacheKey(cacheKey);
|
|
13
|
+
if (!parts)
|
|
14
|
+
return;
|
|
15
|
+
const event = { cacheKey, ...parts };
|
|
16
|
+
for (const listener of updateListeners) {
|
|
17
|
+
listener(event);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/services/cache.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { CacheEntry } from '../config/types/content.js';
|
|
2
|
-
|
|
3
|
-
interface CacheUpdateEvent extends CacheKeyParts {
|
|
2
|
+
interface CacheUpdateEvent {
|
|
4
3
|
cacheKey: string;
|
|
4
|
+
namespace: string;
|
|
5
|
+
urlHash: string;
|
|
5
6
|
}
|
|
7
|
+
type CacheUpdateListener = (event: CacheUpdateEvent) => void;
|
|
8
|
+
export declare function onCacheUpdate(listener: CacheUpdateListener): () => void;
|
|
6
9
|
interface CacheEntryMetadata {
|
|
7
10
|
url: string;
|
|
8
11
|
title?: string;
|
|
9
12
|
}
|
|
10
|
-
type CacheUpdateListener = (event: CacheUpdateEvent) => void;
|
|
11
|
-
export declare function onCacheUpdate(listener: CacheUpdateListener): () => void;
|
|
12
13
|
export declare function get(cacheKey: string | null): CacheEntry | undefined;
|
|
13
14
|
export declare function set(cacheKey: string | null, content: string, metadata: CacheEntryMetadata): void;
|
|
14
15
|
export declare function keys(): readonly string[];
|