tiny-http-mcp-server 0.1.18 → 0.1.20
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/dist/composition.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# tiny-mcp-client
|
|
2
2
|
|
|
3
|
-
`tiny-mcp-client` is a lightweight Model Context Protocol client used by tests, fixtures, and package integrations. It supports stdio transports,
|
|
3
|
+
`tiny-mcp-client` is a lightweight Model Context Protocol client used by tests, fixtures, and package integrations. It supports stdio transports, Streamable HTTP and legacy HTTP/SSE transports, in-memory test pairs, JSON-RPC helpers, and OAuth metadata discovery for OAuth-protected MCP HTTP servers.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -29,9 +29,14 @@ await client.close();
|
|
|
29
29
|
| Transport | Description |
|
|
30
30
|
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
31
31
|
| `StdioTransport` | Spawns an MCP server process and communicates over stdio. |
|
|
32
|
-
| `HttpTransport` | Connects to
|
|
32
|
+
| `HttpTransport` | Connects to Streamable HTTP endpoints, or legacy HTTP/SSE with `mode: "sse"`. |
|
|
33
33
|
| `createInMemoryTransportPair()` | Creates paired streams for in-process tests. |
|
|
34
34
|
|
|
35
|
+
Legacy SSE mode opens a GET stream and posts messages to the endpoint announced
|
|
36
|
+
by the server. Announced endpoints must remain on the original origin without
|
|
37
|
+
embedded credentials or fragments; endpoint changes close the connection.
|
|
38
|
+
Both transports accept the same headers, OAuth provider and response limits.
|
|
39
|
+
|
|
35
40
|
## OAuth HTTP support
|
|
36
41
|
|
|
37
42
|
`HttpTransport` accepts `oauth` options from `mcp-oauth`. When a protected server returns a Bearer `WWW-Authenticate` challenge, the transport discovers protected-resource metadata, loads authorization-server metadata, lets the OAuth provider handle authorization, and retries the request when credentials are available.
|
|
@@ -585,6 +585,8 @@ interface StdioTransportOptions {
|
|
|
585
585
|
type HttpTransportFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
586
586
|
interface HttpTransportOptions {
|
|
587
587
|
url: string;
|
|
588
|
+
/** Legacy SSE uses a GET stream that announces the RPC POST endpoint. */
|
|
589
|
+
mode?: "streamable-http" | "sse";
|
|
588
590
|
headers?: RequestInit["headers"];
|
|
589
591
|
fetch?: HttpTransportFetch;
|
|
590
592
|
oauth?: OAuthClientProviderOptions;
|
|
@@ -610,6 +612,11 @@ declare class HttpTransport implements McpTransport {
|
|
|
610
612
|
readonly writable: Writable;
|
|
611
613
|
readonly closed: Promise<McpTransportClosedEvent>;
|
|
612
614
|
private readonly url;
|
|
615
|
+
private readonly mode;
|
|
616
|
+
private legacyEndpoint;
|
|
617
|
+
private legacyEndpointReady;
|
|
618
|
+
private resolveLegacyEndpoint;
|
|
619
|
+
private rejectLegacyEndpoint;
|
|
613
620
|
private readonly headers;
|
|
614
621
|
private readonly fetchImpl;
|
|
615
622
|
private readonly readStream;
|
|
@@ -628,7 +635,7 @@ declare class HttpTransport implements McpTransport {
|
|
|
628
635
|
private readonly maxResponseBytes;
|
|
629
636
|
private readonly toolParameterHeaders;
|
|
630
637
|
private readonly onWarning;
|
|
631
|
-
constructor({ url, headers, fetch: fetchImpl, oauth, oauthDiscoveryCache, onWarning, maxResponseBytes, }: HttpTransportOptions);
|
|
638
|
+
constructor({ url, mode, headers, fetch: fetchImpl, oauth, oauthDiscoveryCache, onWarning, maxResponseBytes, }: HttpTransportOptions);
|
|
632
639
|
filterTools(tools: Tool[], reset?: boolean): Tool[];
|
|
633
640
|
dispose(reason?: Error): void;
|
|
634
641
|
private closeWithSessionTermination;
|
|
@@ -643,6 +650,7 @@ declare class HttpTransport implements McpTransport {
|
|
|
643
650
|
private authorizeRequestHeaders;
|
|
644
651
|
private captureSessionId;
|
|
645
652
|
private maybeOpenGetSseStream;
|
|
653
|
+
private ensureLegacyEndpoint;
|
|
646
654
|
private sendSessionTerminationRequest;
|
|
647
655
|
private consumeGetSseStream;
|
|
648
656
|
private throwForPostHttpError;
|
|
@@ -6615,6 +6615,11 @@ var HttpTransport = class {
|
|
|
6615
6615
|
writable;
|
|
6616
6616
|
closed;
|
|
6617
6617
|
url;
|
|
6618
|
+
mode;
|
|
6619
|
+
legacyEndpoint;
|
|
6620
|
+
legacyEndpointReady;
|
|
6621
|
+
resolveLegacyEndpoint;
|
|
6622
|
+
rejectLegacyEndpoint;
|
|
6618
6623
|
headers;
|
|
6619
6624
|
fetchImpl;
|
|
6620
6625
|
readStream = new PassThrough();
|
|
@@ -6635,6 +6640,7 @@ var HttpTransport = class {
|
|
|
6635
6640
|
onWarning;
|
|
6636
6641
|
constructor({
|
|
6637
6642
|
url,
|
|
6643
|
+
mode = "streamable-http",
|
|
6638
6644
|
headers = {},
|
|
6639
6645
|
fetch: fetchImpl = defaultHttpTransportFetch,
|
|
6640
6646
|
oauth,
|
|
@@ -6646,6 +6652,7 @@ var HttpTransport = class {
|
|
|
6646
6652
|
throw new Error("HTTP response byte limit must be a positive safe integer");
|
|
6647
6653
|
this.maxResponseBytes = maxResponseBytes;
|
|
6648
6654
|
this.url = url;
|
|
6655
|
+
this.mode = mode;
|
|
6649
6656
|
this.headers = headers;
|
|
6650
6657
|
this.fetchImpl = fetchImpl;
|
|
6651
6658
|
this.onWarning = onWarning;
|
|
@@ -6690,6 +6697,9 @@ var HttpTransport = class {
|
|
|
6690
6697
|
return;
|
|
6691
6698
|
}
|
|
6692
6699
|
this.disposed = true;
|
|
6700
|
+
this.rejectLegacyEndpoint?.(reason);
|
|
6701
|
+
this.rejectLegacyEndpoint = void 0;
|
|
6702
|
+
this.resolveLegacyEndpoint = void 0;
|
|
6693
6703
|
this.toolParameterHeaders.clear();
|
|
6694
6704
|
this.abortInFlightFetches();
|
|
6695
6705
|
this.cancelOpenResponseReaders();
|
|
@@ -6793,8 +6803,10 @@ var HttpTransport = class {
|
|
|
6793
6803
|
const id = parsed.type === "request" ? parsed.message.id : void 0;
|
|
6794
6804
|
if (controller !== void 0 && id !== void 0) this.modernRequests.set(id, controller);
|
|
6795
6805
|
try {
|
|
6806
|
+
const postUrl = this.mode === "sse" ? await this.ensureLegacyEndpoint() : this.url;
|
|
6796
6807
|
const hasSessionId = !modern && this.sessionId !== void 0;
|
|
6797
6808
|
const response = await this.fetchWithOAuthRetry({
|
|
6809
|
+
url: postUrl,
|
|
6798
6810
|
method: "POST",
|
|
6799
6811
|
createHeaders: () => this.createPostHeaders(message, modern),
|
|
6800
6812
|
body: line,
|
|
@@ -6820,7 +6832,7 @@ var HttpTransport = class {
|
|
|
6820
6832
|
void response.body?.cancel().catch(() => void 0);
|
|
6821
6833
|
return;
|
|
6822
6834
|
}
|
|
6823
|
-
if (!modern) {
|
|
6835
|
+
if (!modern && this.mode !== "sse") {
|
|
6824
6836
|
this.captureSessionId(response);
|
|
6825
6837
|
this.maybeOpenGetSseStream();
|
|
6826
6838
|
}
|
|
@@ -6916,6 +6928,17 @@ var HttpTransport = class {
|
|
|
6916
6928
|
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
6917
6929
|
});
|
|
6918
6930
|
}
|
|
6931
|
+
ensureLegacyEndpoint() {
|
|
6932
|
+
if (this.legacyEndpointReady !== void 0) return this.legacyEndpointReady;
|
|
6933
|
+
this.legacyEndpointReady = new Promise((resolve, reject) => {
|
|
6934
|
+
this.resolveLegacyEndpoint = resolve;
|
|
6935
|
+
this.rejectLegacyEndpoint = reject;
|
|
6936
|
+
});
|
|
6937
|
+
void this.consumeGetSseStream().catch((error) => {
|
|
6938
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
6939
|
+
});
|
|
6940
|
+
return this.legacyEndpointReady;
|
|
6941
|
+
}
|
|
6919
6942
|
async sendSessionTerminationRequest(sessionId, signal) {
|
|
6920
6943
|
const headers = await this.createDeleteHeaders(sessionId);
|
|
6921
6944
|
signal.throwIfAborted();
|
|
@@ -6964,10 +6987,15 @@ var HttpTransport = class {
|
|
|
6964
6987
|
const contentType = response.headers.get("Content-Type");
|
|
6965
6988
|
if (contentType === null) {
|
|
6966
6989
|
void response.body?.cancel().catch(() => void 0);
|
|
6990
|
+
if (this.mode === "sse") throw new Error("Legacy SSE GET returned an unsupported content type");
|
|
6967
6991
|
return;
|
|
6968
6992
|
}
|
|
6969
6993
|
if (contentType.split(";")[0]?.trim().toLowerCase() === "text/event-stream") {
|
|
6970
|
-
await this.forwardSseResponseMessages(response);
|
|
6994
|
+
await this.forwardSseResponseMessages(response, void 0, void 0, this.mode === "sse");
|
|
6995
|
+
if (this.mode === "sse") {
|
|
6996
|
+
if (!this.disposed) throw new Error("Legacy SSE stream ended");
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6971
6999
|
this.getSseStreamStarted = false;
|
|
6972
7000
|
if (!this.disposed && this.sessionId !== void 0 && this.lastEventId !== void 0) {
|
|
6973
7001
|
this.maybeOpenGetSseStream();
|
|
@@ -6975,6 +7003,7 @@ var HttpTransport = class {
|
|
|
6975
7003
|
return;
|
|
6976
7004
|
}
|
|
6977
7005
|
void response.body?.cancel().catch(() => void 0);
|
|
7006
|
+
if (this.mode === "sse") throw new Error("Legacy SSE GET returned an unsupported content type");
|
|
6978
7007
|
}
|
|
6979
7008
|
async throwForPostHttpError(response, request, signal) {
|
|
6980
7009
|
if (response.status < 400) {
|
|
@@ -7074,11 +7103,11 @@ var HttpTransport = class {
|
|
|
7074
7103
|
void response.body?.cancel().catch(() => void 0);
|
|
7075
7104
|
throw new Error("HTTP transport POST returned an unsupported response content type");
|
|
7076
7105
|
}
|
|
7077
|
-
async forwardSseResponseMessages(response, signal, context) {
|
|
7106
|
+
async forwardSseResponseMessages(response, signal, context, acceptEndpoint = false) {
|
|
7078
7107
|
if (response.body === null) {
|
|
7079
7108
|
return;
|
|
7080
7109
|
}
|
|
7081
|
-
const parser = new SseParser(this.maxResponseBytes);
|
|
7110
|
+
const parser = new SseParser(this.maxResponseBytes, acceptEndpoint);
|
|
7082
7111
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
7083
7112
|
const reader = response.body.getReader();
|
|
7084
7113
|
this.openResponseReaders.add(reader);
|
|
@@ -7142,6 +7171,19 @@ var HttpTransport = class {
|
|
|
7142
7171
|
}
|
|
7143
7172
|
writeSseMessages(messages, context) {
|
|
7144
7173
|
for (const message of messages) {
|
|
7174
|
+
if (message.event === "endpoint") {
|
|
7175
|
+
const endpoint = new URL(message.data, this.url);
|
|
7176
|
+
const resource = new URL(this.url);
|
|
7177
|
+
if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:" || endpoint.origin !== resource.origin || endpoint.username || endpoint.password || endpoint.hash)
|
|
7178
|
+
throw new Error("Unsafe legacy SSE endpoint");
|
|
7179
|
+
if (this.legacyEndpoint !== void 0 && this.legacyEndpoint !== endpoint.href)
|
|
7180
|
+
throw new Error("Legacy SSE endpoint changed during the active connection");
|
|
7181
|
+
this.legacyEndpoint = endpoint.href;
|
|
7182
|
+
this.resolveLegacyEndpoint?.(endpoint.href);
|
|
7183
|
+
this.resolveLegacyEndpoint = void 0;
|
|
7184
|
+
this.rejectLegacyEndpoint = void 0;
|
|
7185
|
+
continue;
|
|
7186
|
+
}
|
|
7145
7187
|
this.writeReadableLine(context?.validate(message.data, true) ?? message.data);
|
|
7146
7188
|
if (context?.completed) return;
|
|
7147
7189
|
}
|
|
@@ -7155,7 +7197,7 @@ var HttpTransport = class {
|
|
|
7155
7197
|
}
|
|
7156
7198
|
async fetchWithOAuthRetry(input) {
|
|
7157
7199
|
const request = async () => this.fetchWithAbort(
|
|
7158
|
-
this.url,
|
|
7200
|
+
input.url ?? this.url,
|
|
7159
7201
|
{
|
|
7160
7202
|
method: input.method,
|
|
7161
7203
|
headers: await input.createHeaders(),
|
|
@@ -7252,12 +7294,14 @@ async function* readLines(stream, maxLineBytes = 16 * 1024 * 1024) {
|
|
|
7252
7294
|
if (parts.length > 0) yield normalizeLine(parts.join(""));
|
|
7253
7295
|
}
|
|
7254
7296
|
var SseParser = class {
|
|
7255
|
-
constructor(maxEventBytes = 16 * 1024 * 1024) {
|
|
7297
|
+
constructor(maxEventBytes = 16 * 1024 * 1024, acceptEndpoint = false) {
|
|
7256
7298
|
this.maxEventBytes = maxEventBytes;
|
|
7299
|
+
this.acceptEndpoint = acceptEndpoint;
|
|
7257
7300
|
if (!Number.isSafeInteger(maxEventBytes) || maxEventBytes < 1)
|
|
7258
7301
|
throw new Error("SSE event byte limit must be a positive safe integer");
|
|
7259
7302
|
}
|
|
7260
7303
|
maxEventBytes;
|
|
7304
|
+
acceptEndpoint;
|
|
7261
7305
|
buffer = "";
|
|
7262
7306
|
skipLf = false;
|
|
7263
7307
|
eventType;
|
|
@@ -7338,13 +7382,14 @@ var SseParser = class {
|
|
|
7338
7382
|
if (this.hasEventId) {
|
|
7339
7383
|
this._lastEventId = this.eventId;
|
|
7340
7384
|
}
|
|
7341
|
-
if (this.dataLines.length === 0 || eventType !== "message") {
|
|
7385
|
+
if (this.dataLines.length === 0 || eventType !== "message" && !(this.acceptEndpoint && eventType === "endpoint")) {
|
|
7342
7386
|
this.resetEvent();
|
|
7343
7387
|
return;
|
|
7344
7388
|
}
|
|
7345
7389
|
const message = {
|
|
7346
7390
|
data: this.dataLines.join("\n")
|
|
7347
7391
|
};
|
|
7392
|
+
if (eventType === "endpoint") message.event = "endpoint";
|
|
7348
7393
|
if (this.hasEventId) {
|
|
7349
7394
|
message.id = this.eventId;
|
|
7350
7395
|
}
|