@frontmcp/utils 1.11.3 → 1.12.0-rc.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 +4 -2
- package/browser/crypto/index.mjs +25 -1
- package/browser/crypto/standard-webhooks.mjs +96 -0
- package/browser/fs/index.mjs +3 -0
- package/browser/fs/watch-files.mjs +177 -0
- package/browser/index.mjs +45 -2
- package/browser/net/index.mjs +15 -2
- package/browser/net/ip-address.mjs +30 -0
- package/browser/net/outbound-request.mjs +190 -0
- package/browser/net/outbound-url.mjs +43 -0
- package/browser/serialization/canonical-json.mjs +35 -0
- package/browser/serialization/index.mjs +2 -0
- package/browser/stdio-bridge/claude-code-channels.mjs +73 -0
- package/browser/stdio-bridge/notification-stream.mjs +111 -0
- package/browser/stdio-bridge/sse-frames.mjs +48 -0
- package/browser/stdio-bridge/upstream-client.mjs +89 -37
- package/crypto/index.d.ts +1 -0
- package/crypto/standard-webhooks.d.ts +35 -0
- package/env/browser-runtime-context.d.ts +1 -1
- package/env/runtime-context.d.ts +1 -1
- package/esm/index.mjs +1 -1
- package/fs/index.d.ts +2 -0
- package/fs/watch-files.d.ts +22 -0
- package/index.d.ts +6 -6
- package/index.js +1 -1
- package/net/index.d.ts +5 -1
- package/net/ip-address.d.ts +2 -0
- package/net/outbound-request.d.ts +46 -0
- package/net/outbound-url.d.ts +18 -0
- package/package.json +2 -2
- package/serialization/canonical-json.d.ts +2 -0
- package/serialization/index.d.ts +1 -0
- package/stdio-bridge/claude-code-channels.d.ts +28 -0
- package/stdio-bridge/notification-stream.d.ts +29 -0
- package/stdio-bridge/sse-frames.d.ts +8 -0
- package/stdio-bridge/upstream-client.d.ts +13 -0
package/net/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export { classifyIpAddress, expandIpv6, extractEmbeddedIpv4, parseIpv4 } from './ip-address';
|
|
1
|
+
export { classifyIpAddress, expandIpv6, extractEmbeddedIpv4, isGloballyRoutableIp, parseIpv4 } from './ip-address';
|
|
2
2
|
export type { IpAddressClassification, IpAddressRange, SpecialPurposeIpRange } from './ip-address';
|
|
3
3
|
export { isRedirectResponse } from './redirect';
|
|
4
4
|
export type { RedirectCandidate } from './redirect';
|
|
5
|
+
export { validateOutboundUrl } from './outbound-url';
|
|
6
|
+
export type { OutboundTargetOptions, OutboundUrlCheck, OutboundUrlProblem } from './outbound-url';
|
|
7
|
+
export { OUTBOUND_MAX_REQUEST_BYTES, OUTBOUND_TIMEOUT_MS, outboundFailureCategory, sendOutboundRequest, } from './outbound-request';
|
|
8
|
+
export type { OutboundAnswer, OutboundFailure, OutboundFailureCategory, OutboundFailureReason, OutboundRequest, OutboundResult, } from './outbound-request';
|
package/net/ip-address.d.ts
CHANGED
|
@@ -33,3 +33,5 @@ export declare function extractEmbeddedIpv4(groups: readonly number[]): string |
|
|
|
33
33
|
* @returns undefined when the input is not a well-formed IP literal.
|
|
34
34
|
*/
|
|
35
35
|
export declare function classifyIpAddress(address: string): IpAddressClassification | undefined;
|
|
36
|
+
/** Whether an IP literal is globally routable: public for {@link classifyIpAddress} and in no unrouted IANA block. */
|
|
37
|
+
export declare function isGloballyRoutableIp(address: string): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** SSRF-hardened HTTP for a URL a client chose: validated addresses only, no redirects, bounded time and sizes. */
|
|
2
|
+
import { type OutboundTargetOptions } from './outbound-url';
|
|
3
|
+
export declare const OUTBOUND_TIMEOUT_MS = 10000;
|
|
4
|
+
export declare const OUTBOUND_MAX_REQUEST_BYTES = 262144;
|
|
5
|
+
export interface OutboundRequest extends OutboundTargetOptions {
|
|
6
|
+
url: string;
|
|
7
|
+
/** @default 'POST' with a body, 'GET' without */
|
|
8
|
+
method?: 'GET' | 'POST';
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/** Sent as these exact UTF-8 bytes. */
|
|
11
|
+
body?: string;
|
|
12
|
+
/** Cancels the request, which then ends as a timeout. */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** @default OUTBOUND_TIMEOUT_MS */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** @default OUTBOUND_MAX_REQUEST_BYTES */
|
|
17
|
+
maxRequestBytes?: number;
|
|
18
|
+
/** How many bytes of the answer's body to read. @default 0 */
|
|
19
|
+
maxResponseBytes?: number;
|
|
20
|
+
/** The answer's headers to return, by name. @default none */
|
|
21
|
+
responseHeaders?: readonly string[];
|
|
22
|
+
/** Where the connection cannot be pinned to checked addresses (browser, edge), send to a hostname anyway. @default false */
|
|
23
|
+
allowPlatformDns?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface OutboundAnswer {
|
|
26
|
+
answered: true;
|
|
27
|
+
/** A redirect is not followed: its 3xx status is returned, or 0 where a browser hides it. */
|
|
28
|
+
status: number;
|
|
29
|
+
/** The first `maxResponseBytes` of the body, as UTF-8 text. */
|
|
30
|
+
body: string;
|
|
31
|
+
/** Whether the body was longer than `maxResponseBytes`; false when that is 0, as the body is then not read. */
|
|
32
|
+
bodyTruncated: boolean;
|
|
33
|
+
/** The headers named in `responseHeaders` that the answer carries, by lowercase name. */
|
|
34
|
+
headers: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
export type OutboundFailureReason = 'invalid_url' | 'forbidden_address' | 'unresolved_host' | 'unpinned_dns' | 'request_too_large' | 'connection_refused' | 'timeout' | 'tls_error';
|
|
37
|
+
export interface OutboundFailure {
|
|
38
|
+
answered: false;
|
|
39
|
+
reason: OutboundFailureReason;
|
|
40
|
+
}
|
|
41
|
+
export type OutboundResult = OutboundAnswer | OutboundFailure;
|
|
42
|
+
export type OutboundFailureCategory = 'connection_refused' | 'timeout' | 'tls_error' | 'http_4xx' | 'http_5xx';
|
|
43
|
+
/** The fixed category of a result that is not a 2xx answer: all that the party who chose the URL may be told. */
|
|
44
|
+
export declare function outboundFailureCategory(result: OutboundResult): OutboundFailureCategory | undefined;
|
|
45
|
+
/** Sends the request; without Node's `dns` and `net` (browser, edge) a hostname is refused unless `allowPlatformDns`. */
|
|
46
|
+
export declare function sendOutboundRequest(request: OutboundRequest): Promise<OutboundResult>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface OutboundTargetOptions {
|
|
2
|
+
/** Test-only: also accept `http:` and loopback or private targets. */
|
|
3
|
+
allowLocal?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export type OutboundUrlProblem = 'malformed' | 'scheme' | 'credentials' | 'host';
|
|
6
|
+
export type OutboundUrlCheck = {
|
|
7
|
+
ok: true;
|
|
8
|
+
url: URL;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
problem: OutboundUrlProblem;
|
|
12
|
+
};
|
|
13
|
+
/** Whether a request may connect to an IP address: globally routable, or loopback or private with `allowLocal`. */
|
|
14
|
+
export declare function isAllowedOutboundAddress(address: string, options?: OutboundTargetOptions): boolean;
|
|
15
|
+
/** Whether a URL host is an IP address, which needs no DNS resolution. */
|
|
16
|
+
export declare function isIpLiteralHostname(hostname: string): boolean;
|
|
17
|
+
/** Checks a URL a server did not choose before any I/O: `https:`, no credentials, and a host that is not local. */
|
|
18
|
+
export declare function validateOutboundUrl(target: string, options?: OutboundTargetOptions): OutboundUrlCheck;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0-rc.1",
|
|
4
4
|
"description": "Shared helpers for FrontMCP: URI templates, naming, cross-platform crypto and PKCE, key-value storage, file system and environment detection",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@noble/hashes": "^2.0.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@frontmcp/lazy-zod": "1.
|
|
36
|
+
"@frontmcp/lazy-zod": "1.12.0-rc.1",
|
|
37
37
|
"@upstash/redis": "^1.34.0",
|
|
38
38
|
"@vercel/kv": "^2.0.0 || ^3.0.0",
|
|
39
39
|
"ioredis": "^5.0.0"
|
package/serialization/index.d.ts
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the HTTP upstream does so Claude Code channels reach a bridged stdio session. Claude Code probes a stdio
|
|
3
|
+
* server on MCP 2026-07-28, delivers channel events only to a session started with `initialize`, and never
|
|
4
|
+
* declares the channel capability. The constants mirror `@frontmcp/protocol`, which this package can't import.
|
|
5
|
+
*/
|
|
6
|
+
import type { JsonRpcFrame } from './stdio-framer';
|
|
7
|
+
/** The headers a 2026-07-28 `server/discover` must mirror from its body over HTTP. */
|
|
8
|
+
export declare const DISCOVER_HEADERS: {
|
|
9
|
+
'mcp-protocol-version': string;
|
|
10
|
+
'mcp-method': string;
|
|
11
|
+
};
|
|
12
|
+
/** What a server says of itself: whether it pushes channels, and the revisions its `initialize` negotiates. */
|
|
13
|
+
export interface ServerRevisions {
|
|
14
|
+
pushesChannels: boolean;
|
|
15
|
+
supported: string[];
|
|
16
|
+
}
|
|
17
|
+
/** Whether a client frame belongs to 2026-07-28: its `_meta` declares a revision, or its method exists only there. */
|
|
18
|
+
export declare function namesProtocol20260728(frame: JsonRpcFrame): boolean;
|
|
19
|
+
/** The `-32022` answer to a 2026-07-28 request, naming the revisions the server speaks instead. */
|
|
20
|
+
export declare function unsupportedProtocolVersion(request: JsonRpcFrame, supported: readonly string[]): JsonRpcFrame;
|
|
21
|
+
/** The `server/discover` request the bridge sends for itself, under an id of its own. */
|
|
22
|
+
export declare function discoverRequest(id: string): JsonRpcFrame;
|
|
23
|
+
/** Whether a `server/discover` or `initialize` result advertises `experimental['claude/channel']`. */
|
|
24
|
+
export declare function advertisesChannels(result: unknown): boolean;
|
|
25
|
+
/** What a `server/discover` result says of the server; `undefined` when it lists no revisions. */
|
|
26
|
+
export declare function serverRevisionsOf(discoverResult: unknown): ServerRevisions | undefined;
|
|
27
|
+
/** The client's `initialize` with `experimental['claude/channel']` declared, when it does not declare it itself. */
|
|
28
|
+
export declare function declaringChannels(initialize: JsonRpcFrame): JsonRpcFrame;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standalone notification stream of an MCP session over streamable HTTP: the `GET` a server answers with the
|
|
3
|
+
* messages it starts itself, such as a channel event, `list_changed`, a log line or a resource update.
|
|
4
|
+
*/
|
|
5
|
+
import type { JsonRpcFrame } from './stdio-framer';
|
|
6
|
+
import type { BridgeLogger } from './types';
|
|
7
|
+
export declare const SESSION_HEADER = "mcp-session-id";
|
|
8
|
+
export interface NotificationStreamOptions {
|
|
9
|
+
url: string;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
log: BridgeLogger;
|
|
13
|
+
/** Called for every frame the server sends on the stream. */
|
|
14
|
+
onFrame: (frame: JsonRpcFrame) => void | Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Called once when the stream of a live session is given up: it could not be opened five times in a row, or the
|
|
17
|
+
* server no longer knows a session it streamed to.
|
|
18
|
+
*/
|
|
19
|
+
onLost?: () => void;
|
|
20
|
+
}
|
|
21
|
+
export interface NotificationStream {
|
|
22
|
+
/**
|
|
23
|
+
* Relay the stream of `sessionId` and end the one relayed before; with no session, only end it. Resolves once
|
|
24
|
+
* the server answered the stream's first `GET`, or after 2 s without an answer.
|
|
25
|
+
*/
|
|
26
|
+
follow(sessionId: string | undefined): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** Relays one session's notification stream at a time, reopening it when it drops while the session lives. */
|
|
29
|
+
export declare function createNotificationStream(options: NotificationStreamOptions): NotificationStream;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonRpcFrame } from './stdio-framer';
|
|
2
|
+
import type { BridgeLogger } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Hand the frame of each event of an SSE body to `deliver`, until the body ends. An event is its `data:` lines up
|
|
5
|
+
* to a blank line, joined by newlines; the event a body ends in is delivered without one. A body whose pending
|
|
6
|
+
* event grows past `maxEventChars` is cancelled, and the relay rejects.
|
|
7
|
+
*/
|
|
8
|
+
export declare function relaySseFrames(body: ReadableStream<Uint8Array>, deliver: (frame: JsonRpcFrame) => void | Promise<void>, log: BridgeLogger, maxEventChars?: number): Promise<void>;
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* A restarted child has no session at all, so the bridge replays the client's
|
|
17
17
|
* MCP handshake against it (`reinitialize`) before draining buffered requests.
|
|
18
18
|
* The client keeps its stdio connection and never learns the server restarted.
|
|
19
|
+
*
|
|
20
|
+
* In HTTP mode the bridge also relays the session's standalone notification
|
|
21
|
+
* stream, which carries what the server sends on its own.
|
|
19
22
|
*/
|
|
20
23
|
import type { ChildProcess } from 'node:child_process';
|
|
21
24
|
import type { JsonRpcFrame } from './stdio-framer';
|
|
@@ -43,10 +46,20 @@ export interface HttpUpstreamOptions extends UpstreamClientOptions {
|
|
|
43
46
|
headers?: Record<string, string>;
|
|
44
47
|
/** Sends the requests instead of the global `fetch`, such as one that reaches a Unix socket. */
|
|
45
48
|
fetch?: typeof fetch;
|
|
49
|
+
/**
|
|
50
|
+
* Serve Claude Code channels: declare `experimental['claude/channel']` on the client's `initialize`, and answer
|
|
51
|
+
* the 2026-07-28 requests of a server that advertises it with `-32022`, since Claude Code delivers channel
|
|
52
|
+
* events only to a session. Without it, every frame is relayed as it is.
|
|
53
|
+
*/
|
|
54
|
+
claudeCodeChannels?: boolean;
|
|
55
|
+
/** Called once when the session's notification stream is given up, as a server that went away makes it. */
|
|
56
|
+
onNotificationStreamLost?: () => void;
|
|
46
57
|
}
|
|
47
58
|
export interface HttpUpstreamClient extends UpstreamClient {
|
|
48
59
|
/** End the MCP session the server issued (`DELETE` with its id); the next `initialize` starts a new one. */
|
|
49
60
|
endSession(): Promise<void>;
|
|
61
|
+
/** Forget the session of a server that went away, without telling it: its notification stream is closed. */
|
|
62
|
+
leaveSession(): void;
|
|
50
63
|
}
|
|
51
64
|
export declare function createHttpUpstream(options: HttpUpstreamOptions): HttpUpstreamClient;
|
|
52
65
|
export interface PipeUpstreamOptions extends UpstreamClientOptions {
|