@j0hanz/superfetch 2.1.0 → 2.1.2
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 +11 -10
- package/dist/cache.js +125 -16
- package/dist/config.d.ts +6 -1
- package/dist/config.js +14 -1
- package/dist/fetch.js +91 -71
- package/dist/http.d.ts +9 -1
- package/dist/http.js +126 -56
- package/dist/instructions.md +66 -0
- package/dist/mcp.js +11 -1
- package/dist/observability.js +1 -1
- package/dist/tools.d.ts +7 -2
- package/dist/tools.js +29 -16
- package/dist/transform.js +714 -409
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +3 -0
- package/dist/workers/transform-worker.js +1 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -444,16 +444,17 @@ Set environment variables in your MCP client `env` or in the shell before starti
|
|
|
444
444
|
|
|
445
445
|
### Core Server Settings
|
|
446
446
|
|
|
447
|
-
| Variable
|
|
448
|
-
|
|
|
449
|
-
| `HOST`
|
|
450
|
-
| `PORT`
|
|
451
|
-
| `USER_AGENT`
|
|
452
|
-
| `CACHE_ENABLED`
|
|
453
|
-
| `CACHE_TTL`
|
|
454
|
-
| `LOG_LEVEL`
|
|
455
|
-
| `ALLOW_REMOTE`
|
|
456
|
-
| `ALLOWED_HOSTS`
|
|
447
|
+
| Variable | Default | Description |
|
|
448
|
+
| ---------------------- | -------------------- | --------------------------------------------------------------------------------- |
|
|
449
|
+
| `HOST` | `127.0.0.1` | HTTP bind address |
|
|
450
|
+
| `PORT` | `3000` | HTTP server port (1024-65535) |
|
|
451
|
+
| `USER_AGENT` | `superFetch-MCP/2.0` | User-Agent header for outgoing requests |
|
|
452
|
+
| `CACHE_ENABLED` | `true` | Enable response caching |
|
|
453
|
+
| `CACHE_TTL` | `3600` | Cache TTL in seconds (60-86400) |
|
|
454
|
+
| `LOG_LEVEL` | `info` | Logging level. Only `debug` enables verbose logs; other values behave like `info` |
|
|
455
|
+
| `ALLOW_REMOTE` | `false` | Allow binding to non-loopback hosts (OAuth required) |
|
|
456
|
+
| `ALLOWED_HOSTS` | (empty) | Additional allowed Host/Origin values (comma/space separated) |
|
|
457
|
+
| `TRANSFORM_TIMEOUT_MS` | `30000` | Worker transform timeout in milliseconds (5000-120000) |
|
|
457
458
|
|
|
458
459
|
For HTTP server tuning (`SERVER_HEADERS_TIMEOUT_MS`, `SERVER_REQUEST_TIMEOUT_MS`, `SERVER_KEEP_ALIVE_TIMEOUT_MS`, `SERVER_SHUTDOWN_CLOSE_IDLE`, `SERVER_SHUTDOWN_CLOSE_ALL`), see `CONFIGURATION.md`.
|
|
459
460
|
|
package/dist/cache.js
CHANGED
|
@@ -5,9 +5,7 @@ import { config } from './config.js';
|
|
|
5
5
|
import { sha256Hex } from './crypto.js';
|
|
6
6
|
import { getErrorMessage } from './errors.js';
|
|
7
7
|
import { logDebug, logWarn } from './observability.js';
|
|
8
|
-
|
|
9
|
-
return typeof value === 'object' && value !== null;
|
|
10
|
-
}
|
|
8
|
+
import { isRecord } from './utils.js';
|
|
11
9
|
export function parseCachedPayload(raw) {
|
|
12
10
|
try {
|
|
13
11
|
const parsed = JSON.parse(raw);
|
|
@@ -47,21 +45,121 @@ const CACHE_HASH = {
|
|
|
47
45
|
URL_HASH_LENGTH: 16,
|
|
48
46
|
VARY_HASH_LENGTH: 12,
|
|
49
47
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const CACHE_VARY_LIMITS = {
|
|
49
|
+
MAX_STRING_LENGTH: 4096,
|
|
50
|
+
MAX_KEYS: 64,
|
|
51
|
+
MAX_ARRAY_LENGTH: 64,
|
|
52
|
+
MAX_DEPTH: 6,
|
|
53
|
+
MAX_NODES: 512,
|
|
54
|
+
};
|
|
55
|
+
function bumpStableStringifyNodeCount(state) {
|
|
56
|
+
state.nodes += 1;
|
|
57
|
+
return state.nodes <= CACHE_VARY_LIMITS.MAX_NODES;
|
|
58
|
+
}
|
|
59
|
+
function stableStringifyPrimitive(value) {
|
|
60
|
+
if (value === null || value === undefined) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
const json = JSON.stringify(value);
|
|
64
|
+
return typeof json === 'string' ? json : '';
|
|
65
|
+
}
|
|
66
|
+
function stableStringifyArray(value, state) {
|
|
67
|
+
if (value.length > CACHE_VARY_LIMITS.MAX_ARRAY_LENGTH) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const parts = ['['];
|
|
71
|
+
let length = 1;
|
|
72
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
73
|
+
if (index > 0) {
|
|
74
|
+
parts.push(',');
|
|
75
|
+
length += 1;
|
|
76
|
+
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const entry = stableStringifyInner(value[index], state);
|
|
80
|
+
if (entry === null)
|
|
81
|
+
return null;
|
|
82
|
+
parts.push(entry);
|
|
83
|
+
length += entry.length;
|
|
84
|
+
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
parts.push(']');
|
|
88
|
+
length += 1;
|
|
89
|
+
return length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH ? null : parts.join('');
|
|
90
|
+
}
|
|
91
|
+
function stableStringifyRecord(value, state) {
|
|
92
|
+
const keys = Object.keys(value);
|
|
93
|
+
if (keys.length > CACHE_VARY_LIMITS.MAX_KEYS) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
keys.sort((a, b) => a.localeCompare(b));
|
|
97
|
+
const parts = ['{'];
|
|
98
|
+
let length = 1;
|
|
99
|
+
let isFirst = true;
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const entryValue = value[key];
|
|
102
|
+
if (entryValue === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
const encodedValue = stableStringifyInner(entryValue, state);
|
|
105
|
+
if (encodedValue === null)
|
|
106
|
+
return null;
|
|
107
|
+
const entry = `${JSON.stringify(key)}:${encodedValue}`;
|
|
108
|
+
if (!isFirst) {
|
|
109
|
+
parts.push(',');
|
|
110
|
+
length += 1;
|
|
111
|
+
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
parts.push(entry);
|
|
115
|
+
length += entry.length;
|
|
116
|
+
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
117
|
+
return null;
|
|
118
|
+
isFirst = false;
|
|
119
|
+
}
|
|
120
|
+
parts.push('}');
|
|
121
|
+
length += 1;
|
|
122
|
+
return length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH ? null : parts.join('');
|
|
123
|
+
}
|
|
124
|
+
function stableStringifyObject(value, state) {
|
|
125
|
+
if (state.stack.has(value)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (state.depth >= CACHE_VARY_LIMITS.MAX_DEPTH) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
state.stack.add(value);
|
|
132
|
+
state.depth += 1;
|
|
133
|
+
try {
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
return stableStringifyArray(value, state);
|
|
54
136
|
}
|
|
55
|
-
return
|
|
137
|
+
return isRecord(value) ? stableStringifyRecord(value, state) : null;
|
|
56
138
|
}
|
|
57
|
-
|
|
58
|
-
|
|
139
|
+
finally {
|
|
140
|
+
state.depth -= 1;
|
|
141
|
+
state.stack.delete(value);
|
|
59
142
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
143
|
+
}
|
|
144
|
+
function stableStringifyInner(value, state) {
|
|
145
|
+
if (!bumpStableStringifyNodeCount(state)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (value === null || value === undefined) {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
if (typeof value !== 'object') {
|
|
152
|
+
return stableStringifyPrimitive(value);
|
|
153
|
+
}
|
|
154
|
+
return stableStringifyObject(value, state);
|
|
155
|
+
}
|
|
156
|
+
function stableStringify(value) {
|
|
157
|
+
const state = {
|
|
158
|
+
depth: 0,
|
|
159
|
+
nodes: 0,
|
|
160
|
+
stack: new WeakSet(),
|
|
161
|
+
};
|
|
162
|
+
return stableStringifyInner(value, state);
|
|
65
163
|
}
|
|
66
164
|
function createHashFragment(input, length) {
|
|
67
165
|
return sha256Hex(input).substring(0, length);
|
|
@@ -74,7 +172,16 @@ function buildCacheKey(namespace, urlHash, varyHash) {
|
|
|
74
172
|
function getVaryHash(vary) {
|
|
75
173
|
if (!vary)
|
|
76
174
|
return undefined;
|
|
77
|
-
|
|
175
|
+
let varyString;
|
|
176
|
+
if (typeof vary === 'string') {
|
|
177
|
+
varyString =
|
|
178
|
+
vary.length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH ? null : vary;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
varyString = stableStringify(vary);
|
|
182
|
+
}
|
|
183
|
+
if (varyString === null)
|
|
184
|
+
return null;
|
|
78
185
|
if (!varyString)
|
|
79
186
|
return undefined;
|
|
80
187
|
return createHashFragment(varyString, CACHE_HASH.VARY_HASH_LENGTH);
|
|
@@ -84,6 +191,8 @@ export function createCacheKey(namespace, url, vary) {
|
|
|
84
191
|
return null;
|
|
85
192
|
const urlHash = createHashFragment(url, CACHE_HASH.URL_HASH_LENGTH);
|
|
86
193
|
const varyHash = getVaryHash(vary);
|
|
194
|
+
if (varyHash === null)
|
|
195
|
+
return null;
|
|
87
196
|
return buildCacheKey(namespace, urlHash, varyHash);
|
|
88
197
|
}
|
|
89
198
|
export function parseCacheKey(cacheKey) {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
export type TransformMetadataFormat = 'markdown' | 'frontmatter';
|
|
2
3
|
interface AuthConfig {
|
|
3
4
|
mode: 'oauth' | 'static';
|
|
4
5
|
issuerUrl: URL | undefined;
|
|
@@ -40,6 +41,10 @@ export declare const config: {
|
|
|
40
41
|
userAgent: string;
|
|
41
42
|
maxContentLength: number;
|
|
42
43
|
};
|
|
44
|
+
transform: {
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
metadataFormat: TransformMetadataFormat;
|
|
47
|
+
};
|
|
43
48
|
cache: {
|
|
44
49
|
enabled: boolean;
|
|
45
50
|
ttl: number;
|
|
@@ -59,7 +64,7 @@ export declare const config: {
|
|
|
59
64
|
};
|
|
60
65
|
security: {
|
|
61
66
|
blockedHosts: Set<string>;
|
|
62
|
-
blockedIpPatterns: RegExp
|
|
67
|
+
blockedIpPatterns: readonly [RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp];
|
|
63
68
|
allowedHosts: Set<string>;
|
|
64
69
|
apiKey: string | undefined;
|
|
65
70
|
allowRemote: boolean;
|
package/dist/config.js
CHANGED
|
@@ -104,12 +104,19 @@ function parseLogLevel(envValue) {
|
|
|
104
104
|
return 'info';
|
|
105
105
|
return isLogLevel(level) ? level : 'info';
|
|
106
106
|
}
|
|
107
|
+
function parseTransformMetadataFormat(envValue) {
|
|
108
|
+
const normalized = envValue?.trim().toLowerCase();
|
|
109
|
+
if (normalized === 'frontmatter')
|
|
110
|
+
return 'frontmatter';
|
|
111
|
+
return 'markdown';
|
|
112
|
+
}
|
|
107
113
|
const SIZE_LIMITS = {
|
|
108
114
|
TEN_MB: 10 * 1024 * 1024,
|
|
109
115
|
};
|
|
110
116
|
const TIMEOUT = {
|
|
111
117
|
DEFAULT_FETCH_TIMEOUT_MS: 15000,
|
|
112
118
|
DEFAULT_SESSION_TTL_MS: 30 * 60 * 1000,
|
|
119
|
+
DEFAULT_TRANSFORM_TIMEOUT_MS: parseInteger(process.env.TRANSFORM_TIMEOUT_MS, 30000, 5000, 120000),
|
|
113
120
|
};
|
|
114
121
|
function readCoreOAuthUrls() {
|
|
115
122
|
return {
|
|
@@ -168,7 +175,9 @@ const ANY_V4 = buildIpv4([0, 0, 0, 0]);
|
|
|
168
175
|
const METADATA_V4_AWS = buildIpv4([169, 254, 169, 254]);
|
|
169
176
|
const METADATA_V4_AZURE = buildIpv4([100, 100, 100, 200]);
|
|
170
177
|
const host = process.env.HOST ?? LOOPBACK_V4;
|
|
171
|
-
const port =
|
|
178
|
+
const port = process.env.PORT?.trim() === '0'
|
|
179
|
+
? 0
|
|
180
|
+
: parseInteger(process.env.PORT, 3000, 1024, 65535);
|
|
172
181
|
const baseUrl = new URL(`http://${formatHostForUrl(host)}:${port}`);
|
|
173
182
|
const allowRemote = parseBoolean(process.env.ALLOW_REMOTE, false);
|
|
174
183
|
const runtimeState = {
|
|
@@ -197,6 +206,10 @@ export const config = {
|
|
|
197
206
|
userAgent: process.env.USER_AGENT ?? 'superFetch-MCP/2.0',
|
|
198
207
|
maxContentLength: SIZE_LIMITS.TEN_MB,
|
|
199
208
|
},
|
|
209
|
+
transform: {
|
|
210
|
+
timeoutMs: TIMEOUT.DEFAULT_TRANSFORM_TIMEOUT_MS,
|
|
211
|
+
metadataFormat: parseTransformMetadataFormat(process.env.TRANSFORM_METADATA_FORMAT),
|
|
212
|
+
},
|
|
200
213
|
cache: {
|
|
201
214
|
enabled: parseBoolean(process.env.CACHE_ENABLED, true),
|
|
202
215
|
ttl: parseInteger(process.env.CACHE_TTL, 3600, 60, 86400),
|
package/dist/fetch.js
CHANGED
|
@@ -8,9 +8,7 @@ import { Agent } from 'undici';
|
|
|
8
8
|
import { config } from './config.js';
|
|
9
9
|
import { createErrorWithCode, FetchError, isSystemError } from './errors.js';
|
|
10
10
|
import { getOperationId, getRequestId, logDebug, logError, logWarn, redactUrl, } from './observability.js';
|
|
11
|
-
|
|
12
|
-
return typeof value === 'object' && value !== null;
|
|
13
|
-
}
|
|
11
|
+
import { isRecord } from './utils.js';
|
|
14
12
|
function buildIpv4(parts) {
|
|
15
13
|
return parts.join('.');
|
|
16
14
|
}
|
|
@@ -368,23 +366,32 @@ function selectLookupResult(list, useAll, hostname) {
|
|
|
368
366
|
function findLookupError(list, hostname) {
|
|
369
367
|
return (findInvalidFamilyError(list, hostname) ?? findBlockedIpError(list, hostname));
|
|
370
368
|
}
|
|
369
|
+
function normalizeAndValidateLookupResults(addresses, resolvedFamily, hostname) {
|
|
370
|
+
const list = normalizeLookupResults(addresses, resolvedFamily);
|
|
371
|
+
const error = findLookupError(list, hostname);
|
|
372
|
+
return { list, error };
|
|
373
|
+
}
|
|
374
|
+
function respondLookupError(callback, error, addresses) {
|
|
375
|
+
callback(error, addresses);
|
|
376
|
+
}
|
|
377
|
+
function respondLookupSelection(callback, selection) {
|
|
378
|
+
if (selection.error) {
|
|
379
|
+
callback(selection.error, selection.fallback);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
callback(null, selection.address, selection.family);
|
|
383
|
+
}
|
|
371
384
|
function handleLookupResult(error, addresses, hostname, resolvedFamily, useAll, callback) {
|
|
372
385
|
if (error) {
|
|
373
|
-
callback
|
|
386
|
+
respondLookupError(callback, error, addresses);
|
|
374
387
|
return;
|
|
375
388
|
}
|
|
376
|
-
const list =
|
|
377
|
-
const lookupError = findLookupError(list, hostname);
|
|
389
|
+
const { list, error: lookupError } = normalizeAndValidateLookupResults(addresses, resolvedFamily, hostname);
|
|
378
390
|
if (lookupError) {
|
|
379
|
-
callback
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
const selection = selectLookupResult(list, useAll, hostname);
|
|
383
|
-
if (selection.error) {
|
|
384
|
-
callback(selection.error, selection.fallback);
|
|
391
|
+
respondLookupError(callback, lookupError, list);
|
|
385
392
|
return;
|
|
386
393
|
}
|
|
387
|
-
callback(
|
|
394
|
+
respondLookupSelection(callback, selectLookupResult(list, useAll, hostname));
|
|
388
395
|
}
|
|
389
396
|
function resolveDns(hostname, options, callback) {
|
|
390
397
|
const { normalizedOptions, useAll, resolvedFamily } = buildLookupContext(options);
|
|
@@ -556,11 +563,64 @@ function publishFetchEvent(event) {
|
|
|
556
563
|
// Avoid crashing the publisher if a subscriber throws.
|
|
557
564
|
}
|
|
558
565
|
}
|
|
559
|
-
|
|
566
|
+
function buildContextFields(context) {
|
|
567
|
+
const fields = {};
|
|
568
|
+
if (context.contextRequestId) {
|
|
569
|
+
fields.contextRequestId = context.contextRequestId;
|
|
570
|
+
}
|
|
571
|
+
if (context.operationId) {
|
|
572
|
+
fields.operationId = context.operationId;
|
|
573
|
+
}
|
|
574
|
+
return fields;
|
|
575
|
+
}
|
|
576
|
+
function buildResponseMetadata(response, contentSize) {
|
|
577
|
+
const contentType = response.headers.get('content-type') ?? undefined;
|
|
578
|
+
const contentLengthHeader = response.headers.get('content-length');
|
|
579
|
+
const size = contentLengthHeader ??
|
|
580
|
+
(contentSize === undefined ? undefined : String(contentSize));
|
|
581
|
+
const metadata = {};
|
|
582
|
+
if (contentType)
|
|
583
|
+
metadata.contentType = contentType;
|
|
584
|
+
if (size)
|
|
585
|
+
metadata.size = size;
|
|
586
|
+
return metadata;
|
|
587
|
+
}
|
|
588
|
+
function logSlowRequest(context, duration, durationLabel, contextFields) {
|
|
589
|
+
if (duration <= 5000)
|
|
590
|
+
return;
|
|
591
|
+
logWarn('Slow HTTP request detected', {
|
|
592
|
+
requestId: context.requestId,
|
|
593
|
+
url: context.url,
|
|
594
|
+
duration: durationLabel,
|
|
595
|
+
...contextFields,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function resolveSystemErrorCode(error) {
|
|
599
|
+
return isSystemError(error) ? error.code : undefined;
|
|
600
|
+
}
|
|
601
|
+
function buildFetchErrorEvent(context, err, duration, contextFields, status, code) {
|
|
602
|
+
const event = {
|
|
603
|
+
v: 1,
|
|
604
|
+
type: 'error',
|
|
605
|
+
requestId: context.requestId,
|
|
606
|
+
url: context.url,
|
|
607
|
+
error: err.message,
|
|
608
|
+
duration,
|
|
609
|
+
...contextFields,
|
|
610
|
+
};
|
|
611
|
+
if (code !== undefined) {
|
|
612
|
+
event.code = code;
|
|
613
|
+
}
|
|
614
|
+
if (status !== undefined) {
|
|
615
|
+
event.status = status;
|
|
616
|
+
}
|
|
617
|
+
return event;
|
|
618
|
+
}
|
|
619
|
+
function createTelemetryContext(url, method) {
|
|
560
620
|
const safeUrl = redactUrl(url);
|
|
561
621
|
const contextRequestId = getRequestId();
|
|
562
622
|
const operationId = getOperationId();
|
|
563
|
-
|
|
623
|
+
return {
|
|
564
624
|
requestId: randomUUID(),
|
|
565
625
|
startTime: performance.now(),
|
|
566
626
|
url: safeUrl,
|
|
@@ -568,92 +628,55 @@ export function startFetchTelemetry(url, method) {
|
|
|
568
628
|
...(contextRequestId ? { contextRequestId } : {}),
|
|
569
629
|
...(operationId ? { operationId } : {}),
|
|
570
630
|
};
|
|
631
|
+
}
|
|
632
|
+
export function startFetchTelemetry(url, method) {
|
|
633
|
+
const context = createTelemetryContext(url, method);
|
|
634
|
+
const contextFields = buildContextFields(context);
|
|
571
635
|
publishFetchEvent({
|
|
572
636
|
v: 1,
|
|
573
637
|
type: 'start',
|
|
574
638
|
requestId: context.requestId,
|
|
575
639
|
method: context.method,
|
|
576
640
|
url: context.url,
|
|
577
|
-
...
|
|
578
|
-
? { contextRequestId: context.contextRequestId }
|
|
579
|
-
: {}),
|
|
580
|
-
...(context.operationId ? { operationId: context.operationId } : {}),
|
|
641
|
+
...contextFields,
|
|
581
642
|
});
|
|
582
643
|
logDebug('HTTP Request', {
|
|
583
644
|
requestId: context.requestId,
|
|
584
645
|
method: context.method,
|
|
585
646
|
url: context.url,
|
|
586
|
-
...
|
|
587
|
-
? { contextRequestId: context.contextRequestId }
|
|
588
|
-
: {}),
|
|
589
|
-
...(context.operationId ? { operationId: context.operationId } : {}),
|
|
647
|
+
...contextFields,
|
|
590
648
|
});
|
|
591
649
|
return context;
|
|
592
650
|
}
|
|
593
651
|
export function recordFetchResponse(context, response, contentSize) {
|
|
594
652
|
const duration = performance.now() - context.startTime;
|
|
595
653
|
const durationLabel = `${Math.round(duration)}ms`;
|
|
654
|
+
const contextFields = buildContextFields(context);
|
|
655
|
+
const responseMetadata = buildResponseMetadata(response, contentSize);
|
|
596
656
|
publishFetchEvent({
|
|
597
657
|
v: 1,
|
|
598
658
|
type: 'end',
|
|
599
659
|
requestId: context.requestId,
|
|
600
660
|
status: response.status,
|
|
601
661
|
duration,
|
|
602
|
-
...
|
|
603
|
-
? { contextRequestId: context.contextRequestId }
|
|
604
|
-
: {}),
|
|
605
|
-
...(context.operationId ? { operationId: context.operationId } : {}),
|
|
662
|
+
...contextFields,
|
|
606
663
|
});
|
|
607
|
-
const contentType = response.headers.get('content-type');
|
|
608
|
-
const contentLength = response.headers.get('content-length') ??
|
|
609
|
-
(contentSize === undefined ? undefined : String(contentSize));
|
|
610
664
|
logDebug('HTTP Response', {
|
|
611
665
|
requestId: context.requestId,
|
|
612
666
|
status: response.status,
|
|
613
667
|
url: context.url,
|
|
614
668
|
duration: durationLabel,
|
|
615
|
-
...
|
|
616
|
-
|
|
617
|
-
: {}),
|
|
618
|
-
...(context.operationId ? { operationId: context.operationId } : {}),
|
|
619
|
-
...(contentType ? { contentType } : {}),
|
|
620
|
-
...(contentLength ? { size: contentLength } : {}),
|
|
669
|
+
...contextFields,
|
|
670
|
+
...responseMetadata,
|
|
621
671
|
});
|
|
622
|
-
|
|
623
|
-
logWarn('Slow HTTP request detected', {
|
|
624
|
-
requestId: context.requestId,
|
|
625
|
-
url: context.url,
|
|
626
|
-
duration: durationLabel,
|
|
627
|
-
...(context.contextRequestId
|
|
628
|
-
? { contextRequestId: context.contextRequestId }
|
|
629
|
-
: {}),
|
|
630
|
-
...(context.operationId ? { operationId: context.operationId } : {}),
|
|
631
|
-
});
|
|
632
|
-
}
|
|
672
|
+
logSlowRequest(context, duration, durationLabel, contextFields);
|
|
633
673
|
}
|
|
634
674
|
export function recordFetchError(context, error, status) {
|
|
635
675
|
const duration = performance.now() - context.startTime;
|
|
636
676
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
requestId: context.requestId,
|
|
641
|
-
url: context.url,
|
|
642
|
-
error: err.message,
|
|
643
|
-
duration,
|
|
644
|
-
...(context.contextRequestId
|
|
645
|
-
? { contextRequestId: context.contextRequestId }
|
|
646
|
-
: {}),
|
|
647
|
-
...(context.operationId ? { operationId: context.operationId } : {}),
|
|
648
|
-
};
|
|
649
|
-
const code = isSystemError(err) ? err.code : undefined;
|
|
650
|
-
if (code !== undefined) {
|
|
651
|
-
event.code = code;
|
|
652
|
-
}
|
|
653
|
-
if (status !== undefined) {
|
|
654
|
-
event.status = status;
|
|
655
|
-
}
|
|
656
|
-
publishFetchEvent(event);
|
|
677
|
+
const contextFields = buildContextFields(context);
|
|
678
|
+
const code = resolveSystemErrorCode(err);
|
|
679
|
+
publishFetchEvent(buildFetchErrorEvent(context, err, duration, contextFields, status, code));
|
|
657
680
|
const log = status === 429 ? logWarn : logError;
|
|
658
681
|
log('HTTP Request Error', {
|
|
659
682
|
requestId: context.requestId,
|
|
@@ -661,10 +684,7 @@ export function recordFetchError(context, error, status) {
|
|
|
661
684
|
status,
|
|
662
685
|
code,
|
|
663
686
|
error: err.message,
|
|
664
|
-
...
|
|
665
|
-
? { contextRequestId: context.contextRequestId }
|
|
666
|
-
: {}),
|
|
667
|
-
...(context.operationId ? { operationId: context.operationId } : {}),
|
|
687
|
+
...contextFields,
|
|
668
688
|
});
|
|
669
689
|
}
|
|
670
690
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
package/dist/http.d.ts
CHANGED
|
@@ -15,8 +15,14 @@ interface McpRequestBody {
|
|
|
15
15
|
id?: string | number;
|
|
16
16
|
params?: McpRequestParams;
|
|
17
17
|
}
|
|
18
|
-
export declare function startHttpServer(
|
|
18
|
+
export declare function startHttpServer(options?: {
|
|
19
|
+
registerSignalHandlers?: boolean;
|
|
20
|
+
}): Promise<{
|
|
19
21
|
shutdown: (signal: string) => Promise<void>;
|
|
22
|
+
stop: () => Promise<void>;
|
|
23
|
+
url: string;
|
|
24
|
+
host: string;
|
|
25
|
+
port: number;
|
|
20
26
|
}>;
|
|
21
27
|
export declare function errorHandler(err: Error, req: Request, res: Response, next: NextFunction): void;
|
|
22
28
|
export declare function normalizeHost(value: string): string | null;
|
|
@@ -55,6 +61,8 @@ export declare function ensureSessionCapacity({ store, maxSessions, res, evictOl
|
|
|
55
61
|
res: Response;
|
|
56
62
|
evictOldest: (store: SessionStore) => boolean;
|
|
57
63
|
}): boolean;
|
|
64
|
+
type CloseHandler = (() => void) | undefined;
|
|
65
|
+
export declare function composeCloseHandlers(first: CloseHandler, second: CloseHandler): CloseHandler;
|
|
58
66
|
export declare function resolveTransportForPost({ res, body, sessionId, options, }: {
|
|
59
67
|
res: Response;
|
|
60
68
|
body: Pick<McpRequestBody, 'method' | 'id'>;
|