@j0hanz/fetch-url-mcp 1.10.4 → 1.10.5
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 +7 -9
- package/dist/http/auth.d.ts +1 -0
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/auth.js +1 -1
- package/dist/http/health.d.ts +1 -0
- package/dist/http/health.d.ts.map +1 -1
- package/dist/http/health.js +1 -1
- package/dist/http/helpers.d.ts +6 -0
- package/dist/http/helpers.d.ts.map +1 -1
- package/dist/http/helpers.js +4 -3
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +177 -112
- package/dist/lib/core.d.ts +4 -39
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +41 -161
- package/dist/lib/dom-prep.d.ts.map +1 -1
- package/dist/lib/dom-prep.js +4 -2
- package/dist/lib/fetch-pipeline.d.ts +12 -15
- package/dist/lib/fetch-pipeline.d.ts.map +1 -1
- package/dist/lib/fetch-pipeline.js +77 -36
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +4 -0
- package/dist/lib/progress.js +2 -2
- package/dist/lib/session.d.ts +41 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +140 -0
- package/dist/lib/task-handlers.d.ts.map +1 -1
- package/dist/lib/task-handlers.js +32 -25
- package/dist/resources/instructions.js +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +16 -5
- package/dist/tasks/execution.d.ts +2 -9
- package/dist/tasks/execution.d.ts.map +1 -1
- package/dist/tasks/execution.js +25 -18
- package/dist/tasks/manager.d.ts +3 -1
- package/dist/tasks/manager.d.ts.map +1 -1
- package/dist/tasks/manager.js +29 -11
- package/dist/tasks/tool-registry.d.ts +4 -0
- package/dist/tasks/tool-registry.d.ts.map +1 -1
- package/dist/tasks/tool-registry.js +13 -1
- package/dist/tools/fetch-url.d.ts +2 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +29 -33
- package/dist/transform/shared.d.ts.map +1 -1
- package/dist/transform/shared.js +54 -46
- package/dist/transform/transform.d.ts.map +1 -1
- package/dist/transform/transform.js +72 -51
- package/dist/transform/worker-pool.d.ts.map +1 -1
- package/dist/transform/worker-pool.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -477,7 +477,7 @@ For more info, see [Kilo Code docs](https://kilocode.ai/docs).
|
|
|
477
477
|
- Fetch documentation pages, blog posts, or reference material into Markdown before sending them to an LLM.
|
|
478
478
|
- Retrieve repository-hosted content from GitHub, GitLab, Bitbucket, or Gists and let the server rewrite page URLs to raw endpoints when possible.
|
|
479
479
|
- Reuse cached Markdown through `internal://cache/{namespace}/{hash}` or bypass the cache with `forceRefresh` for time-sensitive pages.
|
|
480
|
-
- Use task mode for large pages or slower sites when the
|
|
480
|
+
- Use task mode for large pages or slower sites when the fetch might otherwise be delayed, cancelled, or better handled asynchronously.
|
|
481
481
|
|
|
482
482
|
## Architecture
|
|
483
483
|
|
|
@@ -525,16 +525,14 @@ For more info, see [Kilo Code docs](https://kilocode.ai/docs).
|
|
|
525
525
|
|
|
526
526
|
#### `fetch-url`
|
|
527
527
|
|
|
528
|
-
Fetch public webpages and convert HTML into AI-readable Markdown. The tool is read-only, does not execute page JavaScript, can bypass the cache with `forceRefresh`, and supports optional task mode for larger or slower fetches.
|
|
528
|
+
Fetch public webpages and convert HTML into AI-readable Markdown. The tool is read-only, does not execute page JavaScript, can bypass the cache with `forceRefresh`, and supports optional task mode for larger or slower fetches. `forceRefresh` refreshes cached content only; it does not bypass fetch or inline truncation limits.
|
|
529
529
|
|
|
530
|
-
| Parameter
|
|
531
|
-
|
|
|
532
|
-
| `url`
|
|
533
|
-
| `
|
|
534
|
-
| `forceRefresh` | `boolean` | no | Bypass cache and fetch fresh content. |
|
|
535
|
-
| `maxInlineChars` | `integer` | no | Inline markdown limit (0-10485760, 0=unlimited). Lower of this or the global limit applies. |
|
|
530
|
+
| Parameter | Type | Required | Description |
|
|
531
|
+
| -------------- | --------- | -------- | ------------------------------------- |
|
|
532
|
+
| `url` | `string` | yes | Target URL. Max 2048 chars. |
|
|
533
|
+
| `forceRefresh` | `boolean` | no | Bypass cache and fetch fresh content. |
|
|
536
534
|
|
|
537
|
-
The response is returned as MCP text content and, when validation succeeds, as `structuredContent` containing `url`, `resolvedUrl`, `finalUrl`, `title`, `metadata`, `markdown`, `fromCache`, `fetchedAt`, `contentSize`, and `truncated`.
|
|
535
|
+
The response is returned as MCP text content and, when validation succeeds, as `structuredContent` containing `url`, `resolvedUrl`, `finalUrl`, `title`, `metadata`, `markdown`, `fromCache`, `fetchedAt`, `contentSize`, and `truncated`. A `truncated: true` result means the content hit server-enforced fetch or inline limits; `forceRefresh` does not remove that limit.
|
|
538
536
|
|
|
539
537
|
```text
|
|
540
538
|
1. [Client] -- tools/call {name: "fetch-url", arguments} --> [Server]
|
package/dist/http/auth.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ declare class HostOriginPolicy {
|
|
|
21
21
|
}
|
|
22
22
|
export declare const hostOriginPolicy: HostOriginPolicy;
|
|
23
23
|
export declare function assertHttpModeConfiguration(): void;
|
|
24
|
+
export declare const DEFAULT_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
24
25
|
export declare const SUPPORTED_MCP_PROTOCOL_VERSIONS: Set<string>;
|
|
25
26
|
interface McpProtocolVersionCheckOptions {
|
|
26
27
|
expectedVersion?: string;
|
package/dist/http/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/http/auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EACL,iBAAiB,EAElB,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAO/E,OAAO,EAEL,KAAK,cAAc,EAIpB,MAAM,cAAc,CAAC;AAMtB,cAAM,UAAU;IAId,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO;CAuBrC;AAED,eAAO,MAAM,UAAU,YAAmB,CAAC;AAS3C,cAAM,sBAAuB,SAAQ,iBAAiB;IAElD,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE;gBAAjC,cAAc,EAAE,SAAS,MAAM,EAAE,EAC1C,OAAO,SAAuB;CAKjC;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,sBAAsB,CAEjC;AAwCD,cAAM,gBAAgB;IACpB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO;IA2BtC,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,oBAAoB;IAwB5B,OAAO,CAAC,aAAa;IAwBrB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,MAAM;CAQf;AAED,eAAO,MAAM,gBAAgB,kBAAyB,CAAC;AAMvD,wBAAgB,2BAA2B,IAAI,IAAI,CA2BlD;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/http/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/http/auth.js
CHANGED
|
@@ -179,7 +179,7 @@ export function assertHttpModeConfiguration() {
|
|
|
179
179
|
// ---------------------------------------------------------------------------
|
|
180
180
|
// MCP protocol version
|
|
181
181
|
// ---------------------------------------------------------------------------
|
|
182
|
-
const DEFAULT_MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
182
|
+
export const DEFAULT_MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
183
183
|
export const SUPPORTED_MCP_PROTOCOL_VERSIONS = new Set([
|
|
184
184
|
DEFAULT_MCP_PROTOCOL_VERSION,
|
|
185
185
|
]);
|
package/dist/http/health.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { SessionStore } from '../lib/core.js';
|
|
|
2
2
|
import { type RequestContext } from './helpers.js';
|
|
3
3
|
export declare function resetEventLoopMonitoring(): void;
|
|
4
4
|
export declare function disableEventLoopMonitoring(): void;
|
|
5
|
+
export declare function isVerboseHealthRequest(ctx: RequestContext): boolean;
|
|
5
6
|
export declare function shouldHandleHealthRoute(ctx: RequestContext): boolean;
|
|
6
7
|
export declare function sendHealthRouteResponse(store: SessionStore, ctx: RequestContext, authPresent: boolean): boolean;
|
|
7
8
|
//# sourceMappingURL=health.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/http/health.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,EAAE,KAAK,cAAc,EAAY,MAAM,cAAc,CAAC;AAY7D,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/http/health.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,EAAE,KAAK,cAAc,EAAY,MAAM,cAAc,CAAC;AAY7D,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AA2JD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAKnE;AAiCD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAEpE;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,cAAc,EACnB,WAAW,EAAE,OAAO,GACnB,OAAO,CAOT"}
|
package/dist/http/health.js
CHANGED
|
@@ -103,7 +103,7 @@ function sendHealth(store, res, includeDiagnostics) {
|
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
104
|
// Health route helpers
|
|
105
105
|
// ---------------------------------------------------------------------------
|
|
106
|
-
function isVerboseHealthRequest(ctx) {
|
|
106
|
+
export function isVerboseHealthRequest(ctx) {
|
|
107
107
|
const value = ctx.url.searchParams.get('verbose');
|
|
108
108
|
if (!value)
|
|
109
109
|
return false;
|
package/dist/http/helpers.d.ts
CHANGED
|
@@ -36,6 +36,12 @@ export declare function closeTransportBestEffort(transport: {
|
|
|
36
36
|
}, context: string): Promise<void>;
|
|
37
37
|
export declare function closeMcpServerBestEffort(server: McpServer, context: string): Promise<void>;
|
|
38
38
|
export declare function createTransportAdapter(transportImpl: StreamableHTTPServerTransport): Transport;
|
|
39
|
+
export type JsonBodyErrorKind = 'payload-too-large' | 'invalid-json' | 'read-failed';
|
|
40
|
+
export declare class JsonBodyError extends Error {
|
|
41
|
+
readonly kind: JsonBodyErrorKind;
|
|
42
|
+
constructor(kind: JsonBodyErrorKind, message: string);
|
|
43
|
+
}
|
|
44
|
+
export declare function isJsonBodyError(error: unknown): error is JsonBodyError;
|
|
39
45
|
export declare const DEFAULT_BODY_LIMIT_BYTES: number;
|
|
40
46
|
declare class JsonBodyReader {
|
|
41
47
|
read(req: IncomingMessage, limit?: number, signal?: AbortSignal): Promise<unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/http/helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,YAAY,CAAC;AAKxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACxG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAK/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAQrD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,CAAC;AAcjD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,eAAe,CAAC;IACrB,GAAG,EAAE,cAAc,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,oBAAqB,SAAQ,cAAc;IAC1D,IAAI,EAAE,QAAQ,CAAC;CAChB;AAWD,wBAAgB,QAAQ,CACtB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,GACZ,IAAI,CAKN;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,SAAS,CACvB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,SAAM,EACZ,EAAE,GAAE,SAAgB,GACnB,IAAI,CAMN;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAIf;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAKnE;AAkBD,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,eAAe,GACnB,MAAM,GAAG,IAAI,CAKf;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAOvD;AAMD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,eAAe,GAAG;IAC9D,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CA2CA;AAgBD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAoBpE;AAMD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,GAAG,IAAI,CAkBvB;AAMD,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE;IAAE,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAC5C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAgB,sBAAsB,CACpC,aAAa,EAAE,6BAA6B,GAC3C,SAAS,CA4CX;
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/http/helpers.ts"],"names":[],"mappings":"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"}
|
package/dist/http/helpers.js
CHANGED
|
@@ -240,7 +240,7 @@ export function createTransportAdapter(transportImpl) {
|
|
|
240
240
|
},
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
|
-
class JsonBodyError extends Error {
|
|
243
|
+
export class JsonBodyError extends Error {
|
|
244
244
|
kind;
|
|
245
245
|
constructor(kind, message) {
|
|
246
246
|
super(message);
|
|
@@ -248,6 +248,9 @@ class JsonBodyError extends Error {
|
|
|
248
248
|
this.kind = kind;
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
+
export function isJsonBodyError(error) {
|
|
252
|
+
return error instanceof JsonBodyError;
|
|
253
|
+
}
|
|
251
254
|
export const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
252
255
|
function isRequestReadAborted(req) {
|
|
253
256
|
return req.destroyed && !req.complete;
|
|
@@ -261,7 +264,6 @@ class JsonBodyReader {
|
|
|
261
264
|
if (contentLengthHeader) {
|
|
262
265
|
const contentLength = Number.parseInt(contentLengthHeader, 10);
|
|
263
266
|
if (Number.isFinite(contentLength) && contentLength > limit) {
|
|
264
|
-
destroyRequestBestEffort(req);
|
|
265
267
|
throw new JsonBodyError('payload-too-large', 'Payload too large');
|
|
266
268
|
}
|
|
267
269
|
}
|
|
@@ -327,7 +329,6 @@ class JsonBodyReader {
|
|
|
327
329
|
const buf = this.normalizeChunk(chunk);
|
|
328
330
|
size += buf.length;
|
|
329
331
|
if (size > limit) {
|
|
330
|
-
destroyRequestBestEffort(req);
|
|
331
332
|
callback(new JsonBodyError('payload-too-large', 'Payload too large'));
|
|
332
333
|
return;
|
|
333
334
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../../src/http/native.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../../src/http/native.ts"],"names":[],"mappings":"AA69BA,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAC/C,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CA0DD"}
|
package/dist/http/native.js
CHANGED
|
@@ -15,15 +15,11 @@ import { toError } from '../lib/utils.js';
|
|
|
15
15
|
import { applyHttpServerTuning, drainConnectionsOnShutdown, } from '../lib/utils.js';
|
|
16
16
|
import { isObject } from '../lib/utils.js';
|
|
17
17
|
import { createMcpServerForHttpSession } from '../server.js';
|
|
18
|
-
import { applyInsufficientScopeAuthHeaders, applyUnauthorizedAuthHeaders, assertHttpModeConfiguration, authService, buildAuthFingerprint, buildProtectedResourceMetadataDocument, corsPolicy, ensureMcpProtocolVersion, hostOriginPolicy, isInsufficientScopeError, isOAuthMetadataEnabled, isProtectedResourceMetadataPath, SUPPORTED_MCP_PROTOCOL_VERSIONS, } from './auth.js';
|
|
19
|
-
import { disableEventLoopMonitoring, resetEventLoopMonitoring, sendHealthRouteResponse, shouldHandleHealthRoute, } from './health.js';
|
|
20
|
-
import { buildRequestContext, closeMcpServerBestEffort, closeTransportBestEffort, createRequestAbortSignal, createTransportAdapter, DEFAULT_BODY_LIMIT_BYTES, drainRequest, findDuplicateSingleValueHeader, getHeaderValue, getMcpSessionId, jsonBodyReader, registerInboundBlockList, sendEmpty, sendError, sendJson, } from './helpers.js';
|
|
18
|
+
import { applyInsufficientScopeAuthHeaders, applyUnauthorizedAuthHeaders, assertHttpModeConfiguration, authService, buildAuthFingerprint, buildProtectedResourceMetadataDocument, corsPolicy, DEFAULT_MCP_PROTOCOL_VERSION, ensureMcpProtocolVersion, hostOriginPolicy, isInsufficientScopeError, isOAuthMetadataEnabled, isProtectedResourceMetadataPath, SUPPORTED_MCP_PROTOCOL_VERSIONS, } from './auth.js';
|
|
19
|
+
import { disableEventLoopMonitoring, isVerboseHealthRequest, resetEventLoopMonitoring, sendHealthRouteResponse, shouldHandleHealthRoute, } from './health.js';
|
|
20
|
+
import { buildRequestContext, closeMcpServerBestEffort, closeTransportBestEffort, createRequestAbortSignal, createTransportAdapter, DEFAULT_BODY_LIMIT_BYTES, drainRequest, findDuplicateSingleValueHeader, getHeaderValue, getMcpSessionId, isJsonBodyError, jsonBodyReader, registerInboundBlockList, sendEmpty, sendError, sendJson, } from './helpers.js';
|
|
21
21
|
import { createRateLimitManagerImpl, } from './rate-limit.js';
|
|
22
22
|
import { teardownSessionRegistration, teardownSessionResources, } from './session-teardown.js';
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// MCP session gateway
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
const DEFAULT_MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
27
23
|
function resolveRequestedProtocolVersion(body) {
|
|
28
24
|
if (!isObject(body))
|
|
29
25
|
return DEFAULT_MCP_PROTOCOL_VERSION;
|
|
@@ -57,6 +53,7 @@ function isPingRequest(method) {
|
|
|
57
53
|
class McpSessionGateway {
|
|
58
54
|
store;
|
|
59
55
|
createSessionServer;
|
|
56
|
+
sessionInitTimeouts = new Map();
|
|
60
57
|
constructor(store, createSessionServer) {
|
|
61
58
|
this.store = store;
|
|
62
59
|
this.createSessionServer = createSessionServer;
|
|
@@ -89,28 +86,15 @@ class McpSessionGateway {
|
|
|
89
86
|
const session = sessionId
|
|
90
87
|
? this.getAuthenticatedSessionById(sessionId, ctx.auth, ctx.res, requestId)
|
|
91
88
|
: undefined;
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
if (!this.ensurePostSessionAccess({
|
|
90
|
+
ctx,
|
|
91
|
+
sessionId,
|
|
92
|
+
session,
|
|
93
|
+
requestId,
|
|
94
|
+
method,
|
|
95
|
+
isInitNotification,
|
|
96
|
+
}))
|
|
96
97
|
return;
|
|
97
|
-
}
|
|
98
|
-
if (session) {
|
|
99
|
-
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
100
|
-
return;
|
|
101
|
-
if (!session.protocolInitialized) {
|
|
102
|
-
const isPing = method !== null && isPingRequest(method);
|
|
103
|
-
if (!isInitNotification && !isPing) {
|
|
104
|
-
sendError(ctx.res, -32600, 'Session not initialized', 400, requestId);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
if (!ensureMcpProtocolVersion(ctx.req, ctx.res)) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
98
|
if (session && isInitNotification) {
|
|
115
99
|
this.markSessionInitialized(sessionId, session);
|
|
116
100
|
sendEmpty(ctx.res, 202);
|
|
@@ -127,14 +111,12 @@ class McpSessionGateway {
|
|
|
127
111
|
await transport.handleRequest(ctx.req, ctx.res, body);
|
|
128
112
|
}
|
|
129
113
|
async handleGet(ctx) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!session)
|
|
135
|
-
return;
|
|
136
|
-
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
114
|
+
const sessionState = this.getRequiredAuthenticatedSession(ctx, null, {
|
|
115
|
+
requireInitialized: true,
|
|
116
|
+
});
|
|
117
|
+
if (!sessionState)
|
|
137
118
|
return;
|
|
119
|
+
const { sessionId, session } = sessionState;
|
|
138
120
|
const acceptHeader = getHeaderValue(ctx.req, 'accept');
|
|
139
121
|
if (!acceptsEventStream(acceptHeader)) {
|
|
140
122
|
sendJson(ctx.res, 406, {
|
|
@@ -146,28 +128,54 @@ class McpSessionGateway {
|
|
|
146
128
|
await session.transport.handleRequest(ctx.req, ctx.res);
|
|
147
129
|
}
|
|
148
130
|
async handleDelete(ctx) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!session)
|
|
154
|
-
return;
|
|
155
|
-
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
131
|
+
const sessionState = this.getRequiredAuthenticatedSession(ctx, null, {
|
|
132
|
+
requireInitialized: true,
|
|
133
|
+
});
|
|
134
|
+
if (!sessionState)
|
|
156
135
|
return;
|
|
136
|
+
const { sessionId, session } = sessionState;
|
|
157
137
|
await session.transport.close();
|
|
158
138
|
this.cleanupSessionRecord(sessionId, 'session-delete');
|
|
159
139
|
sendJson(ctx.res, 200, { status: 'closed' });
|
|
160
140
|
}
|
|
141
|
+
ensurePostSessionAccess(params) {
|
|
142
|
+
const { ctx, sessionId, session, requestId, method, isInitNotification } = params;
|
|
143
|
+
if (sessionId && !session)
|
|
144
|
+
return false;
|
|
145
|
+
if (!session) {
|
|
146
|
+
if (isInitNotification) {
|
|
147
|
+
sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return ensureMcpProtocolVersion(ctx.req, ctx.res);
|
|
151
|
+
}
|
|
152
|
+
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
153
|
+
return false;
|
|
154
|
+
if (session.protocolInitialized)
|
|
155
|
+
return true;
|
|
156
|
+
if (isInitNotification)
|
|
157
|
+
return true;
|
|
158
|
+
if (method !== null && isPingRequest(method))
|
|
159
|
+
return true;
|
|
160
|
+
sendError(ctx.res, -32600, 'Session not initialized', 400, requestId);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
161
163
|
async getOrCreateTransport(ctx, requestId) {
|
|
162
164
|
const sessionId = getMcpSessionId(ctx.req);
|
|
163
165
|
if (sessionId) {
|
|
164
166
|
const fingerprint = buildAuthFingerprint(ctx.auth);
|
|
165
167
|
return this.getExistingTransport(sessionId, fingerprint, ctx.res, requestId);
|
|
166
168
|
}
|
|
167
|
-
if (!isMcpRequestBody(ctx.body)
|
|
169
|
+
if (!isMcpRequestBody(ctx.body)) {
|
|
168
170
|
sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
|
|
169
171
|
return null;
|
|
170
172
|
}
|
|
173
|
+
if (!isInitializeRequest(ctx.body)) {
|
|
174
|
+
sendError(ctx.res, ctx.body.method === 'initialize' ? -32602 : -32600, ctx.body.method === 'initialize'
|
|
175
|
+
? 'Invalid initialize request'
|
|
176
|
+
: 'Missing session ID', 400, requestId);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
171
179
|
const negotiatedProtocolVersion = resolveRequestedProtocolVersion(ctx.body);
|
|
172
180
|
if (!negotiatedProtocolVersion) {
|
|
173
181
|
sendError(ctx.res, -32602, `Unsupported protocolVersion; supported versions: ${[...SUPPORTED_MCP_PROTOCOL_VERSIONS].join(', ')}`, 400, requestId);
|
|
@@ -188,6 +196,21 @@ class McpSessionGateway {
|
|
|
188
196
|
this.store.touch(sessionId);
|
|
189
197
|
return session.transport;
|
|
190
198
|
}
|
|
199
|
+
getRequiredAuthenticatedSession(ctx, requestId = null, options) {
|
|
200
|
+
const sessionId = this.getRequiredSessionId(ctx.req, ctx.res, requestId);
|
|
201
|
+
if (!sessionId)
|
|
202
|
+
return null;
|
|
203
|
+
const session = this.getAuthenticatedSessionById(sessionId, ctx.auth, ctx.res, requestId);
|
|
204
|
+
if (!session)
|
|
205
|
+
return null;
|
|
206
|
+
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
207
|
+
return null;
|
|
208
|
+
if (options?.requireInitialized && !session.protocolInitialized) {
|
|
209
|
+
sendError(ctx.res, -32600, 'Session not initialized', 400, requestId);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return { sessionId, session };
|
|
213
|
+
}
|
|
191
214
|
getRequiredSessionId(req, res, requestId = null) {
|
|
192
215
|
const sessionId = getMcpSessionId(req);
|
|
193
216
|
if (sessionId)
|
|
@@ -219,6 +242,7 @@ class McpSessionGateway {
|
|
|
219
242
|
if (!session.protocolInitialized) {
|
|
220
243
|
session.protocolInitialized = true;
|
|
221
244
|
}
|
|
245
|
+
this.clearSessionInitTimeout(sessionId);
|
|
222
246
|
if (sessionId)
|
|
223
247
|
this.store.touch(sessionId);
|
|
224
248
|
}
|
|
@@ -244,17 +268,24 @@ class McpSessionGateway {
|
|
|
244
268
|
sessionIdGenerator: () => newSessionId,
|
|
245
269
|
});
|
|
246
270
|
const initTimeout = setTimeout(() => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
271
|
+
const session = this.store.get(newSessionId);
|
|
272
|
+
if (session) {
|
|
273
|
+
if (session.protocolInitialized) {
|
|
274
|
+
this.clearSessionInitTimeout(newSessionId);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.cleanupSessionRecord(newSessionId, 'session-init-timeout');
|
|
278
|
+
return;
|
|
251
279
|
}
|
|
280
|
+
tracker.releaseSlot();
|
|
281
|
+
void closeTransportBestEffort(transportImpl, 'session-init-timeout');
|
|
282
|
+
void closeMcpServerBestEffort(sessionServer, 'session-init-timeout');
|
|
252
283
|
}, config.server.sessionInitTimeoutMs);
|
|
253
284
|
initTimeout.unref();
|
|
254
285
|
transportImpl.onclose = () => {
|
|
255
286
|
clearTimeout(initTimeout);
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
this.sessionInitTimeouts.delete(newSessionId);
|
|
288
|
+
tracker.releaseSlot();
|
|
258
289
|
};
|
|
259
290
|
try {
|
|
260
291
|
const transport = createTransportAdapter(transportImpl);
|
|
@@ -267,7 +298,6 @@ class McpSessionGateway {
|
|
|
267
298
|
void closeMcpServerBestEffort(sessionServer, 'session-connect-failed');
|
|
268
299
|
throw err;
|
|
269
300
|
}
|
|
270
|
-
tracker.markInitialized();
|
|
271
301
|
tracker.releaseSlot();
|
|
272
302
|
this.store.set(newSessionId, {
|
|
273
303
|
server: sessionServer,
|
|
@@ -278,6 +308,7 @@ class McpSessionGateway {
|
|
|
278
308
|
negotiatedProtocolVersion,
|
|
279
309
|
authFingerprint,
|
|
280
310
|
});
|
|
311
|
+
this.sessionInitTimeouts.set(newSessionId, initTimeout);
|
|
281
312
|
registerMcpSessionServer(newSessionId, sessionServer);
|
|
282
313
|
transportImpl.onclose = composeCloseHandlers(transportImpl.onclose, () => {
|
|
283
314
|
this.cleanupSessionRecord(newSessionId, 'session-close');
|
|
@@ -285,6 +316,7 @@ class McpSessionGateway {
|
|
|
285
316
|
return transportImpl;
|
|
286
317
|
}
|
|
287
318
|
cleanupSessionRecord(sessionId, context) {
|
|
319
|
+
this.clearSessionInitTimeout(sessionId);
|
|
288
320
|
const session = this.store.remove(sessionId);
|
|
289
321
|
if (!session)
|
|
290
322
|
return;
|
|
@@ -293,6 +325,15 @@ class McpSessionGateway {
|
|
|
293
325
|
closeServerReason: `${context}-server`,
|
|
294
326
|
});
|
|
295
327
|
}
|
|
328
|
+
clearSessionInitTimeout(sessionId) {
|
|
329
|
+
if (!sessionId)
|
|
330
|
+
return;
|
|
331
|
+
const timeout = this.sessionInitTimeouts.get(sessionId);
|
|
332
|
+
if (!timeout)
|
|
333
|
+
return;
|
|
334
|
+
clearTimeout(timeout);
|
|
335
|
+
this.sessionInitTimeouts.delete(sessionId);
|
|
336
|
+
}
|
|
296
337
|
reserveCapacity(res, requestId) {
|
|
297
338
|
const allowed = ensureSessionCapacity({
|
|
298
339
|
store: this.store,
|
|
@@ -439,19 +480,6 @@ class HttpDispatcher {
|
|
|
439
480
|
}
|
|
440
481
|
}
|
|
441
482
|
}
|
|
442
|
-
// ---------------------------------------------------------------------------
|
|
443
|
-
// Verbose health helper (local to dispatcher)
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
function isVerboseHealthRequest(ctx) {
|
|
446
|
-
const value = ctx.url.searchParams.get('verbose');
|
|
447
|
-
if (!value)
|
|
448
|
-
return false;
|
|
449
|
-
const normalized = value.trim().toLowerCase();
|
|
450
|
-
return normalized === '1' || normalized === 'true';
|
|
451
|
-
}
|
|
452
|
-
// ---------------------------------------------------------------------------
|
|
453
|
-
// Request pipeline
|
|
454
|
-
// ---------------------------------------------------------------------------
|
|
455
483
|
class HttpRequestPipeline {
|
|
456
484
|
rateLimiter;
|
|
457
485
|
dispatcher;
|
|
@@ -469,59 +497,15 @@ class HttpRequestPipeline {
|
|
|
469
497
|
operationId: requestId,
|
|
470
498
|
...(sessionId ? { sessionId } : {}),
|
|
471
499
|
}, async () => {
|
|
472
|
-
|
|
473
|
-
if (duplicateHeader) {
|
|
474
|
-
sendJson(rawRes, 400, {
|
|
475
|
-
error: `Duplicate ${duplicateHeader} header is not allowed`,
|
|
476
|
-
});
|
|
477
|
-
drainRequest(rawReq);
|
|
500
|
+
if (this.rejectDuplicateHeaders(rawReq, rawRes))
|
|
478
501
|
return;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (!ctx) {
|
|
482
|
-
drainRequest(rawReq);
|
|
502
|
+
const ctx = this.buildContext(rawReq, rawRes, signal);
|
|
503
|
+
if (!ctx)
|
|
483
504
|
return;
|
|
484
|
-
|
|
485
|
-
if (!hostOriginPolicy.validate(ctx)) {
|
|
486
|
-
drainRequest(rawReq);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (corsPolicy.handle(ctx)) {
|
|
490
|
-
drainRequest(rawReq);
|
|
505
|
+
if (!this.applyRequestGuards(ctx, rawReq))
|
|
491
506
|
return;
|
|
492
|
-
|
|
493
|
-
if (!this.rateLimiter.check(ctx)) {
|
|
494
|
-
drainRequest(rawReq);
|
|
507
|
+
if (!(await this.populateRequestBody(ctx, rawReq)))
|
|
495
508
|
return;
|
|
496
|
-
}
|
|
497
|
-
if (ctx.method === 'POST') {
|
|
498
|
-
try {
|
|
499
|
-
ctx.body = await jsonBodyReader.read(ctx.req, DEFAULT_BODY_LIMIT_BYTES, ctx.signal);
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
if (ctx.url.pathname === '/mcp') {
|
|
503
|
-
sendError(ctx.res, -32700, 'Parse error', 400, null);
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
sendJson(ctx.res, 400, {
|
|
507
|
-
error: 'Invalid JSON or Payload too large',
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
drainRequest(rawReq);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
const contentLengthHeader = getHeaderValue(rawReq, 'content-length');
|
|
516
|
-
const transferEncodingHeader = getHeaderValue(rawReq, 'transfer-encoding');
|
|
517
|
-
const hasRequestBody = (contentLengthHeader !== null &&
|
|
518
|
-
Number.parseInt(contentLengthHeader, 10) > 0) ||
|
|
519
|
-
transferEncodingHeader !== null;
|
|
520
|
-
if (hasRequestBody) {
|
|
521
|
-
drainRequest(rawReq);
|
|
522
|
-
}
|
|
523
|
-
ctx.body = undefined;
|
|
524
|
-
}
|
|
525
509
|
await this.dispatcher.dispatch(ctx);
|
|
526
510
|
});
|
|
527
511
|
}
|
|
@@ -529,6 +513,87 @@ class HttpRequestPipeline {
|
|
|
529
513
|
cleanup();
|
|
530
514
|
}
|
|
531
515
|
}
|
|
516
|
+
rejectDuplicateHeaders(rawReq, rawRes) {
|
|
517
|
+
const duplicateHeader = findDuplicateSingleValueHeader(rawReq);
|
|
518
|
+
if (!duplicateHeader)
|
|
519
|
+
return false;
|
|
520
|
+
sendJson(rawRes, 400, {
|
|
521
|
+
error: `Duplicate ${duplicateHeader} header is not allowed`,
|
|
522
|
+
});
|
|
523
|
+
drainRequest(rawReq);
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
buildContext(rawReq, rawRes, signal) {
|
|
527
|
+
const ctx = buildRequestContext(rawReq, rawRes, signal);
|
|
528
|
+
if (ctx)
|
|
529
|
+
return ctx;
|
|
530
|
+
drainRequest(rawReq);
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
applyRequestGuards(ctx, rawReq) {
|
|
534
|
+
if (!hostOriginPolicy.validate(ctx)) {
|
|
535
|
+
drainRequest(rawReq);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
if (corsPolicy.handle(ctx)) {
|
|
539
|
+
drainRequest(rawReq);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
if (!this.rateLimiter.check(ctx)) {
|
|
543
|
+
drainRequest(rawReq);
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
async populateRequestBody(ctx, rawReq) {
|
|
549
|
+
if (ctx.method !== 'POST') {
|
|
550
|
+
this.clearUnexpectedRequestBody(ctx, rawReq);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
ctx.body = await jsonBodyReader.read(ctx.req, DEFAULT_BODY_LIMIT_BYTES, ctx.signal);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
const bodyErrorKind = isJsonBodyError(error) ? error.kind : null;
|
|
559
|
+
if (ctx.url.pathname === '/mcp') {
|
|
560
|
+
switch (bodyErrorKind) {
|
|
561
|
+
case 'payload-too-large':
|
|
562
|
+
sendError(ctx.res, -32600, 'Request body too large', 413, null);
|
|
563
|
+
break;
|
|
564
|
+
case 'read-failed':
|
|
565
|
+
if (!rawReq.destroyed) {
|
|
566
|
+
sendError(ctx.res, -32600, 'Request body read failed', 400, null);
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
case 'invalid-json':
|
|
570
|
+
default:
|
|
571
|
+
sendError(ctx.res, -32700, 'Parse error', 400, null);
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
sendJson(ctx.res, bodyErrorKind === 'payload-too-large' ? 413 : 400, {
|
|
577
|
+
error: bodyErrorKind === 'payload-too-large'
|
|
578
|
+
? 'Payload too large'
|
|
579
|
+
: 'Invalid JSON',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
drainRequest(rawReq);
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
clearUnexpectedRequestBody(ctx, rawReq) {
|
|
587
|
+
const contentLengthHeader = getHeaderValue(rawReq, 'content-length');
|
|
588
|
+
const transferEncodingHeader = getHeaderValue(rawReq, 'transfer-encoding');
|
|
589
|
+
const hasRequestBody = (contentLengthHeader !== null &&
|
|
590
|
+
Number.parseInt(contentLengthHeader, 10) > 0) ||
|
|
591
|
+
transferEncodingHeader !== null;
|
|
592
|
+
if (hasRequestBody) {
|
|
593
|
+
drainRequest(rawReq);
|
|
594
|
+
}
|
|
595
|
+
ctx.body = undefined;
|
|
596
|
+
}
|
|
532
597
|
}
|
|
533
598
|
// ---------------------------------------------------------------------------
|
|
534
599
|
// Server bootstrap
|
package/dist/lib/core.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { SessionEntry } from './session.js';
|
|
3
|
+
import type { SessionStore } from './session.js';
|
|
3
4
|
export declare const serverVersion: string;
|
|
4
5
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
5
6
|
type McpLogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
|
|
@@ -213,46 +214,10 @@ export declare function logError(message: string, error?: Error | LogMetadata):
|
|
|
213
214
|
export declare function getMcpLogLevel(sessionId?: string): McpLogLevel;
|
|
214
215
|
export declare function setLogLevel(level: string, sessionId?: string): void;
|
|
215
216
|
export declare function redactUrl(rawUrl: string): string;
|
|
216
|
-
export
|
|
217
|
-
|
|
218
|
-
readonly transport: StreamableHTTPServerTransport;
|
|
219
|
-
createdAt: number;
|
|
220
|
-
lastSeen: number;
|
|
221
|
-
protocolInitialized: boolean;
|
|
222
|
-
negotiatedProtocolVersion: string;
|
|
223
|
-
authFingerprint: string;
|
|
224
|
-
}
|
|
225
|
-
export interface SessionStore {
|
|
226
|
-
get: (sessionId: string) => SessionEntry | undefined;
|
|
227
|
-
touch: (sessionId: string) => void;
|
|
228
|
-
set: (sessionId: string, entry: SessionEntry) => void;
|
|
229
|
-
remove: (sessionId: string) => SessionEntry | undefined;
|
|
230
|
-
size: () => number;
|
|
231
|
-
inFlight: () => number;
|
|
232
|
-
incrementInFlight: () => void;
|
|
233
|
-
decrementInFlight: () => void;
|
|
234
|
-
clear: () => SessionEntry[];
|
|
235
|
-
evictExpired: () => SessionEntry[];
|
|
236
|
-
evictOldest: () => SessionEntry | undefined;
|
|
237
|
-
}
|
|
238
|
-
interface SlotTracker {
|
|
239
|
-
readonly releaseSlot: () => void;
|
|
240
|
-
readonly markInitialized: () => void;
|
|
241
|
-
readonly isInitialized: () => boolean;
|
|
242
|
-
}
|
|
243
|
-
type CloseHandler = (() => void) | undefined;
|
|
244
|
-
export declare function composeCloseHandlers(first: CloseHandler, second: CloseHandler): CloseHandler;
|
|
217
|
+
export type { SessionEntry, SessionStore } from './session.js';
|
|
218
|
+
export { composeCloseHandlers, createSessionStore, createSlotTracker, ensureSessionCapacity, reserveSessionSlot, } from './session.js';
|
|
245
219
|
export declare function startSessionCleanupLoop(store: SessionStore, sessionTtlMs: number, options?: {
|
|
246
220
|
onEvictSession?: (session: SessionEntry) => Promise<void> | void;
|
|
247
221
|
cleanupIntervalMs?: number;
|
|
248
222
|
}): AbortController;
|
|
249
|
-
export declare function createSessionStore(sessionTtlMs: number): SessionStore;
|
|
250
|
-
export declare function createSlotTracker(store: SessionStore): SlotTracker;
|
|
251
|
-
export declare function reserveSessionSlot(store: SessionStore, maxSessions: number): boolean;
|
|
252
|
-
export declare function ensureSessionCapacity({ store, maxSessions, evictOldest, }: {
|
|
253
|
-
store: SessionStore;
|
|
254
|
-
maxSessions: number;
|
|
255
|
-
evictOldest: (store: SessionStore) => boolean;
|
|
256
|
-
}): boolean;
|
|
257
|
-
export {};
|
|
258
223
|
//# sourceMappingURL=core.d.ts.map
|