tiny-http-mcp-server 0.1.19 → 0.1.21

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.21",
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,16 @@ 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
+ HTTP failures expose `HttpTransportError.status` and `.method`, so callers can
40
+ make transport decisions without parsing error messages.
41
+
35
42
  ## OAuth HTTP support
36
43
 
37
44
  `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.
@@ -583,8 +583,15 @@ interface StdioTransportOptions {
583
583
  spawn?: StdioSpawn;
584
584
  }
585
585
  type HttpTransportFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
586
+ declare class HttpTransportError extends Error {
587
+ readonly status: number;
588
+ readonly method: "GET" | "POST" | "DELETE";
589
+ constructor(message: string, status: number, method: "GET" | "POST" | "DELETE");
590
+ }
586
591
  interface HttpTransportOptions {
587
592
  url: string;
593
+ /** Legacy SSE uses a GET stream that announces the RPC POST endpoint. */
594
+ mode?: "streamable-http" | "sse";
588
595
  headers?: RequestInit["headers"];
589
596
  fetch?: HttpTransportFetch;
590
597
  oauth?: OAuthClientProviderOptions;
@@ -610,6 +617,11 @@ declare class HttpTransport implements McpTransport {
610
617
  readonly writable: Writable;
611
618
  readonly closed: Promise<McpTransportClosedEvent>;
612
619
  private readonly url;
620
+ private readonly mode;
621
+ private legacyEndpoint;
622
+ private legacyEndpointReady;
623
+ private resolveLegacyEndpoint;
624
+ private rejectLegacyEndpoint;
613
625
  private readonly headers;
614
626
  private readonly fetchImpl;
615
627
  private readonly readStream;
@@ -628,7 +640,7 @@ declare class HttpTransport implements McpTransport {
628
640
  private readonly maxResponseBytes;
629
641
  private readonly toolParameterHeaders;
630
642
  private readonly onWarning;
631
- constructor({ url, headers, fetch: fetchImpl, oauth, oauthDiscoveryCache, onWarning, maxResponseBytes, }: HttpTransportOptions);
643
+ constructor({ url, mode, headers, fetch: fetchImpl, oauth, oauthDiscoveryCache, onWarning, maxResponseBytes, }: HttpTransportOptions);
632
644
  filterTools(tools: Tool[], reset?: boolean): Tool[];
633
645
  dispose(reason?: Error): void;
634
646
  private closeWithSessionTermination;
@@ -643,6 +655,7 @@ declare class HttpTransport implements McpTransport {
643
655
  private authorizeRequestHeaders;
644
656
  private captureSessionId;
645
657
  private maybeOpenGetSseStream;
658
+ private ensureLegacyEndpoint;
646
659
  private sendSessionTerminationRequest;
647
660
  private consumeGetSseStream;
648
661
  private throwForPostHttpError;
@@ -735,5 +748,5 @@ declare class JsonRpcMessageLayer {
735
748
  private handleCancellationNotification;
736
749
  }
737
750
 
738
- export { ERROR_INTERNAL, ERROR_INVALID_PARAMS, ERROR_INVALID_REQUEST, ERROR_METHOD_NOT_FOUND, ERROR_PARSE, HttpTransport, JsonRpcMessageLayer, McpClient, McpError, OAuthMetadataDiscovery, StdioTransport, createInMemoryTransportPair, createSdkTestPair, createTestPair, discoverOAuthMetadata, fetchMcpResponse };
751
+ export { ERROR_INTERNAL, ERROR_INVALID_PARAMS, ERROR_INVALID_REQUEST, ERROR_METHOD_NOT_FOUND, ERROR_PARSE, HttpTransport, HttpTransportError, JsonRpcMessageLayer, McpClient, McpError, OAuthMetadataDiscovery, StdioTransport, createInMemoryTransportPair, createSdkTestPair, createTestPair, discoverOAuthMetadata, fetchMcpResponse };
739
752
  export type { AudioContent, BlobResourceContents, CacheableResultMetadata, CallToolOptions, CallToolParams, CallToolResult, ClientCapabilities, CompleteArgument, CompleteParams, CompleteResult, Completion, ConnectResult, ContentItem, CreateMessageParams, CreateMessageResult, ElicitationParams, ElicitationResult, EmbeddedResource, GetPromptParams, GetPromptResult, HttpTransportFetch, HttpTransportOptions, ImageContent, InMemoryTransportPair, IncludeContext, InitializeParams, InitializeResult, JsonRpcErrorObject, JsonRpcErrorResponse, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcRequestOptions, JsonRpcResponse, JsonRpcSuccessResponse, LogLevel, LogMessage, McpClientConnection, McpClientOptions, McpRequestContext, McpSubscription, McpTransport, McpTransportClosedEvent, ModelHint, ModelPreferences, NotificationFilter, OAuthAuthorizationServerMetadata, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryCache, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, PaginatedParams, PaginatedResult, ProgressParams, ProgressToken, PromptMessage, PromptReference, ReadResourceParams, RequestId, ResourceContents, ResourceReference, ResultMetadata, Root, SamplingContent, SamplingMessage, SdkTestPair, ServerCapabilities, StdioSpawn, StdioTransportOptions, StoredOAuthSession, SubscriptionOptions, TextContent, TextResourceContents, Tool, ToolResultContent, ToolUseContent };
@@ -6505,6 +6505,16 @@ async function createTestPair(server, createClient) {
6505
6505
  };
6506
6506
  return { client, cleanup };
6507
6507
  }
6508
+ var HttpTransportError = class extends Error {
6509
+ constructor(message, status, method) {
6510
+ super(message);
6511
+ this.status = status;
6512
+ this.method = method;
6513
+ this.name = "HttpTransportError";
6514
+ }
6515
+ status;
6516
+ method;
6517
+ };
6508
6518
  function defaultStdioSpawn(command, args, options) {
6509
6519
  return spawn2(command, args, options);
6510
6520
  }
@@ -6615,6 +6625,11 @@ var HttpTransport = class {
6615
6625
  writable;
6616
6626
  closed;
6617
6627
  url;
6628
+ mode;
6629
+ legacyEndpoint;
6630
+ legacyEndpointReady;
6631
+ resolveLegacyEndpoint;
6632
+ rejectLegacyEndpoint;
6618
6633
  headers;
6619
6634
  fetchImpl;
6620
6635
  readStream = new PassThrough();
@@ -6635,6 +6650,7 @@ var HttpTransport = class {
6635
6650
  onWarning;
6636
6651
  constructor({
6637
6652
  url,
6653
+ mode = "streamable-http",
6638
6654
  headers = {},
6639
6655
  fetch: fetchImpl = defaultHttpTransportFetch,
6640
6656
  oauth,
@@ -6646,6 +6662,7 @@ var HttpTransport = class {
6646
6662
  throw new Error("HTTP response byte limit must be a positive safe integer");
6647
6663
  this.maxResponseBytes = maxResponseBytes;
6648
6664
  this.url = url;
6665
+ this.mode = mode;
6649
6666
  this.headers = headers;
6650
6667
  this.fetchImpl = fetchImpl;
6651
6668
  this.onWarning = onWarning;
@@ -6690,6 +6707,9 @@ var HttpTransport = class {
6690
6707
  return;
6691
6708
  }
6692
6709
  this.disposed = true;
6710
+ this.rejectLegacyEndpoint?.(reason);
6711
+ this.rejectLegacyEndpoint = void 0;
6712
+ this.resolveLegacyEndpoint = void 0;
6693
6713
  this.toolParameterHeaders.clear();
6694
6714
  this.abortInFlightFetches();
6695
6715
  this.cancelOpenResponseReaders();
@@ -6793,8 +6813,10 @@ var HttpTransport = class {
6793
6813
  const id = parsed.type === "request" ? parsed.message.id : void 0;
6794
6814
  if (controller !== void 0 && id !== void 0) this.modernRequests.set(id, controller);
6795
6815
  try {
6816
+ const postUrl = this.mode === "sse" ? await this.ensureLegacyEndpoint() : this.url;
6796
6817
  const hasSessionId = !modern && this.sessionId !== void 0;
6797
6818
  const response = await this.fetchWithOAuthRetry({
6819
+ url: postUrl,
6798
6820
  method: "POST",
6799
6821
  createHeaders: () => this.createPostHeaders(message, modern),
6800
6822
  body: line,
@@ -6807,7 +6829,7 @@ var HttpTransport = class {
6807
6829
  if (hasSessionId && response.status === 404) {
6808
6830
  void response.body?.cancel().catch(() => void 0);
6809
6831
  this.sessionId = void 0;
6810
- this.dispose(new Error("HTTP transport session expired (404 response)"));
6832
+ this.dispose(new HttpTransportError("HTTP transport session expired (404 response)", 404, "POST"));
6811
6833
  return;
6812
6834
  }
6813
6835
  if (await this.throwForPostHttpError(
@@ -6820,7 +6842,7 @@ var HttpTransport = class {
6820
6842
  void response.body?.cancel().catch(() => void 0);
6821
6843
  return;
6822
6844
  }
6823
- if (!modern) {
6845
+ if (!modern && this.mode !== "sse") {
6824
6846
  this.captureSessionId(response);
6825
6847
  this.maybeOpenGetSseStream();
6826
6848
  }
@@ -6916,6 +6938,17 @@ var HttpTransport = class {
6916
6938
  this.dispose(error instanceof Error ? error : new Error(String(error)));
6917
6939
  });
6918
6940
  }
6941
+ ensureLegacyEndpoint() {
6942
+ if (this.legacyEndpointReady !== void 0) return this.legacyEndpointReady;
6943
+ this.legacyEndpointReady = new Promise((resolve, reject) => {
6944
+ this.resolveLegacyEndpoint = resolve;
6945
+ this.rejectLegacyEndpoint = reject;
6946
+ });
6947
+ void this.consumeGetSseStream().catch((error) => {
6948
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
6949
+ });
6950
+ return this.legacyEndpointReady;
6951
+ }
6919
6952
  async sendSessionTerminationRequest(sessionId, signal) {
6920
6953
  const headers = await this.createDeleteHeaders(sessionId);
6921
6954
  signal.throwIfAborted();
@@ -6935,7 +6968,7 @@ var HttpTransport = class {
6935
6968
  const responseBody = (await readBoundedResponseText(response, this.maxResponseBytes, this.openResponseReaders, signal)).trim();
6936
6969
  const statusDescriptor = `${response.status} ${response.statusText}`.trim();
6937
6970
  const message = responseBody.length === 0 ? `HTTP transport DELETE failed (${statusDescriptor})` : `HTTP transport DELETE failed (${statusDescriptor}): ${responseBody}`;
6938
- throw new Error(message);
6971
+ throw new HttpTransportError(message, response.status, "DELETE");
6939
6972
  }
6940
6973
  async consumeGetSseStream() {
6941
6974
  const response = await this.fetchWithOAuthRetry({
@@ -6948,26 +6981,32 @@ var HttpTransport = class {
6948
6981
  }
6949
6982
  if (response.status === 405) {
6950
6983
  void response.body?.cancel().catch(() => void 0);
6984
+ if (this.mode === "sse") throw new HttpTransportError("Legacy SSE GET failed (405)", 405, "GET");
6951
6985
  throw new HttpTransportGetSseNotSupportedError();
6952
6986
  }
6953
6987
  if (response.status === 404) {
6954
6988
  void response.body?.cancel().catch(() => void 0);
6955
6989
  this.sessionId = void 0;
6956
- throw new Error("HTTP transport session expired (GET 404 response)");
6990
+ throw new HttpTransportError("HTTP transport session expired (GET 404 response)", 404, "GET");
6957
6991
  }
6958
6992
  if (!response.ok) {
6959
6993
  const responseBody = (await readBoundedResponseText(response, this.maxResponseBytes, this.openResponseReaders)).trim();
6960
6994
  const statusDescriptor = `${response.status} ${response.statusText}`.trim();
6961
6995
  const message = responseBody.length === 0 ? `HTTP transport GET failed (${statusDescriptor})` : `HTTP transport GET failed (${statusDescriptor}): ${responseBody}`;
6962
- throw new Error(message);
6996
+ throw new HttpTransportError(message, response.status, "GET");
6963
6997
  }
6964
6998
  const contentType = response.headers.get("Content-Type");
6965
6999
  if (contentType === null) {
6966
7000
  void response.body?.cancel().catch(() => void 0);
7001
+ if (this.mode === "sse") throw new Error("Legacy SSE GET returned an unsupported content type");
6967
7002
  return;
6968
7003
  }
6969
7004
  if (contentType.split(";")[0]?.trim().toLowerCase() === "text/event-stream") {
6970
- await this.forwardSseResponseMessages(response);
7005
+ await this.forwardSseResponseMessages(response, void 0, void 0, this.mode === "sse");
7006
+ if (this.mode === "sse") {
7007
+ if (!this.disposed) throw new Error("Legacy SSE stream ended");
7008
+ return;
7009
+ }
6971
7010
  this.getSseStreamStarted = false;
6972
7011
  if (!this.disposed && this.sessionId !== void 0 && this.lastEventId !== void 0) {
6973
7012
  this.maybeOpenGetSseStream();
@@ -6975,6 +7014,7 @@ var HttpTransport = class {
6975
7014
  return;
6976
7015
  }
6977
7016
  void response.body?.cancel().catch(() => void 0);
7017
+ if (this.mode === "sse") throw new Error("Legacy SSE GET returned an unsupported content type");
6978
7018
  }
6979
7019
  async throwForPostHttpError(response, request, signal) {
6980
7020
  if (response.status < 400) {
@@ -7014,7 +7054,7 @@ var HttpTransport = class {
7014
7054
  }
7015
7055
  const statusDescriptor = `${response.status} ${response.statusText}`.trim();
7016
7056
  const message = responseBody.length === 0 ? `HTTP transport POST failed (${statusDescriptor})` : `HTTP transport POST failed (${statusDescriptor}): ${responseBody}`;
7017
- throw new Error(message);
7057
+ throw new HttpTransportError(message, response.status, "POST");
7018
7058
  }
7019
7059
  async maybeHandleUnauthorizedResponse(response) {
7020
7060
  if (response.status !== 401 || this.oauthProvider === void 0) {
@@ -7074,11 +7114,11 @@ var HttpTransport = class {
7074
7114
  void response.body?.cancel().catch(() => void 0);
7075
7115
  throw new Error("HTTP transport POST returned an unsupported response content type");
7076
7116
  }
7077
- async forwardSseResponseMessages(response, signal, context) {
7117
+ async forwardSseResponseMessages(response, signal, context, acceptEndpoint = false) {
7078
7118
  if (response.body === null) {
7079
7119
  return;
7080
7120
  }
7081
- const parser = new SseParser(this.maxResponseBytes);
7121
+ const parser = new SseParser(this.maxResponseBytes, acceptEndpoint);
7082
7122
  const decoder = new TextDecoder("utf-8", { fatal: true });
7083
7123
  const reader = response.body.getReader();
7084
7124
  this.openResponseReaders.add(reader);
@@ -7142,6 +7182,19 @@ var HttpTransport = class {
7142
7182
  }
7143
7183
  writeSseMessages(messages, context) {
7144
7184
  for (const message of messages) {
7185
+ if (message.event === "endpoint") {
7186
+ const endpoint = new URL(message.data, this.url);
7187
+ const resource = new URL(this.url);
7188
+ if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:" || endpoint.origin !== resource.origin || endpoint.username || endpoint.password || endpoint.hash)
7189
+ throw new Error("Unsafe legacy SSE endpoint");
7190
+ if (this.legacyEndpoint !== void 0 && this.legacyEndpoint !== endpoint.href)
7191
+ throw new Error("Legacy SSE endpoint changed during the active connection");
7192
+ this.legacyEndpoint = endpoint.href;
7193
+ this.resolveLegacyEndpoint?.(endpoint.href);
7194
+ this.resolveLegacyEndpoint = void 0;
7195
+ this.rejectLegacyEndpoint = void 0;
7196
+ continue;
7197
+ }
7145
7198
  this.writeReadableLine(context?.validate(message.data, true) ?? message.data);
7146
7199
  if (context?.completed) return;
7147
7200
  }
@@ -7155,7 +7208,7 @@ var HttpTransport = class {
7155
7208
  }
7156
7209
  async fetchWithOAuthRetry(input) {
7157
7210
  const request = async () => this.fetchWithAbort(
7158
- this.url,
7211
+ input.url ?? this.url,
7159
7212
  {
7160
7213
  method: input.method,
7161
7214
  headers: await input.createHeaders(),
@@ -7252,12 +7305,14 @@ async function* readLines(stream, maxLineBytes = 16 * 1024 * 1024) {
7252
7305
  if (parts.length > 0) yield normalizeLine(parts.join(""));
7253
7306
  }
7254
7307
  var SseParser = class {
7255
- constructor(maxEventBytes = 16 * 1024 * 1024) {
7308
+ constructor(maxEventBytes = 16 * 1024 * 1024, acceptEndpoint = false) {
7256
7309
  this.maxEventBytes = maxEventBytes;
7310
+ this.acceptEndpoint = acceptEndpoint;
7257
7311
  if (!Number.isSafeInteger(maxEventBytes) || maxEventBytes < 1)
7258
7312
  throw new Error("SSE event byte limit must be a positive safe integer");
7259
7313
  }
7260
7314
  maxEventBytes;
7315
+ acceptEndpoint;
7261
7316
  buffer = "";
7262
7317
  skipLf = false;
7263
7318
  eventType;
@@ -7338,13 +7393,14 @@ var SseParser = class {
7338
7393
  if (this.hasEventId) {
7339
7394
  this._lastEventId = this.eventId;
7340
7395
  }
7341
- if (this.dataLines.length === 0 || eventType !== "message") {
7396
+ if (this.dataLines.length === 0 || eventType !== "message" && !(this.acceptEndpoint && eventType === "endpoint")) {
7342
7397
  this.resetEvent();
7343
7398
  return;
7344
7399
  }
7345
7400
  const message = {
7346
7401
  data: this.dataLines.join("\n")
7347
7402
  };
7403
+ if (eventType === "endpoint") message.event = "endpoint";
7348
7404
  if (this.hasEventId) {
7349
7405
  message.id = this.eventId;
7350
7406
  }
@@ -8078,6 +8134,7 @@ export {
8078
8134
  ERROR_METHOD_NOT_FOUND,
8079
8135
  ERROR_PARSE,
8080
8136
  HttpTransport,
8137
+ HttpTransportError,
8081
8138
  JsonRpcMessageLayer,
8082
8139
  McpClient,
8083
8140
  McpError,
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.21",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",