tiny-http-mcp-server 0.1.19 → 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.
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.19",
21
+ "version": "0.1.20",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -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, streamable HTTP transports, in-memory test pairs, JSON-RPC helpers, and OAuth metadata discovery for OAuth-protected MCP HTTP servers.
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 streamable HTTP MCP endpoints, including session IDs, SSE GET streams, and session termination. |
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",