toolcraft 0.0.344 → 0.0.346

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/composition.json CHANGED
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.70",
101
+ "version": "0.1.74",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.344",
116
+ "version": "0.0.346",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.344",
126
+ "version": "0.0.346",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.70",
101
+ "version": "0.1.74",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.344",
116
+ "version": "0.0.346",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.344",
126
+ "version": "0.0.346",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.70",
21
+ "version": "0.1.74",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -91,14 +91,21 @@ later environment changes cannot move a transaction between file and Keychain.
91
91
  Fixed redirects support `localhost`, `127.0.0.1`, and `::1` over HTTP. Their
92
92
  exact spelling, port, path and query are preserved through registration,
93
93
  authorization and code exchange. Credentials, fragments, port zero and reserved
94
- OAuth callback query parameters are rejected before binding a listener.
94
+ OAuth callback query parameters, including `error_uri`, are rejected before binding a listener.
95
95
  `createDefaultOAuthClientProvider` also checks the configured redirect before
96
96
  creating the provider. Imported tokens that cannot be sent as HTTP header
97
97
  values fail with diagnostics that omit their contents. Omit
98
98
  `redirectUri` to allocate a random loopback port. Standalone callback sessions
99
- accept the same `redirectUri`, `signal` and `timeoutMs` options. Cancellation,
99
+ accept the same `redirectUri`, `signal` and `timeoutMs` options. They capture configuration,
100
+ landing-page data and selected callbacks before invoking a host listener factory;
101
+ original signals and selected callback receivers/live method state remain active. Cancellation,
100
102
  timeout and explicit close settle pending code waits and release listeners.
101
- Always close a successful standalone session in `finally`.
103
+ Always close a standalone session in `finally`. Failed callback responses declare
104
+ plain UTF-8 text with content sniffing disabled; provider markup remains literal
105
+ text. Successful landing pages remain HTML with escaped custom title/body. Recognized
106
+ OAuth callback parameters must occur only once in HTTP callbacks or pasted URLs;
107
+ duplicates fail without reflecting values and code extraction returns null.
108
+ Unrecognized extension parameters remain ignored, including repeated fields.
102
109
 
103
110
  Native provider calls capture request option handles before host work. Unauthorized
104
111
  handling also owns complete discovery metadata, presented grant values, rejected
@@ -266,6 +273,8 @@ Before sending a refresh request, the provider persists a tokenless session with
266
273
  A successful response replaces it with the rotated grant. A crash, cancellation,
267
274
  network disconnect or incomplete response leaves the marker, so another process
268
275
  cannot replay a possibly consumed refresh token or revive the initial import.
276
+ An explicit `refresh_token` must be a nonempty string; malformed fields also
277
+ retain the pending marker. Omitting it preserves the previous refresh token.
269
278
  Such a session requires fresh authorization. Interactive unauthorized handling
270
279
  can recover it; headless requests fail with an explicit unknown-outcome error.
271
280
  Only complete OAuth error responses establish a rejected request and allow a
@@ -1,6 +1,7 @@
1
1
  import http from "node:http";
2
2
  import { parseAuthorizationState } from "./authorization-state.js";
3
3
  const authorizationErrorBrand = Symbol.for("poe-platform.mcp-oauth.OAuthAuthorizationError");
4
+ const oauthCallbackParameters = ["code", "state", "iss", "error", "error_description", "error_uri"];
4
5
  /** Authorization callback denial, retaining the provider diagnostic for host observers. */
5
6
  export class OAuthAuthorizationError extends Error {
6
7
  error;
@@ -18,6 +19,10 @@ export class OAuthAuthorizationError extends Error {
18
19
  }
19
20
  }
20
21
  export async function createLoopbackAuthorizationSession(options = {}) {
22
+ const selected = { ...options };
23
+ options = { ...selected, createServer: selected.createServer?.bind(options),
24
+ openBrowser: selected.openBrowser?.bind(options), readLine: selected.readLine?.bind(options),
25
+ landingPage: selected.landingPage === undefined ? undefined : { ...selected.landingPage } };
21
26
  options.signal?.throwIfAborted();
22
27
  const timeoutMs = options.timeoutMs ?? 120_000;
23
28
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647)
@@ -80,10 +85,9 @@ export function loopbackTarget(options) {
80
85
  catch (cause) {
81
86
  throw new Error("Invalid OAuth loopback redirect URI", { cause });
82
87
  }
83
- const forbiddenQuery = ["code", "state", "error", "error_description", "iss"];
84
88
  if (url.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)
85
89
  || url.username || url.password || url.hash || url.port === "0"
86
- || forbiddenQuery.some(name => url.searchParams.has(name))
90
+ || oauthCallbackParameters.some(name => url.searchParams.has(name))
87
91
  || [...options.redirectUri].some(char => char.codePointAt(0) <= 32)
88
92
  || (options.callbackPath !== undefined && options.callbackPath !== url.pathname))
89
93
  throw new Error("Invalid OAuth loopback redirect URI");
@@ -154,11 +158,8 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
154
158
  res.end("Not found");
155
159
  return;
156
160
  }
157
- const callbackParameters = {
158
- code: url.searchParams.get("code"), error: url.searchParams.get("error"),
159
- errorDescription: url.searchParams.get("error_description"), state: url.searchParams.get("state"), iss: url.searchParams.get("iss")
160
- };
161
161
  try {
162
+ const callbackParameters = readAuthorizationCallbackParameters(url);
162
163
  validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
163
164
  if (callbackParameters.error !== null)
164
165
  throw new OAuthAuthorizationError(callbackParameters.error, callbackParameters.errorDescription ?? callbackParameters.error);
@@ -168,7 +169,7 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
168
169
  settle(() => resolve(code));
169
170
  }
170
171
  catch (error) {
171
- res.writeHead(400);
172
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8", "X-Content-Type-Options": "nosniff" });
172
173
  res.end(error instanceof Error ? error.message : "Invalid OAuth callback");
173
174
  settle(() => reject(error));
174
175
  }
@@ -196,22 +197,21 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
196
197
  });
197
198
  }
198
199
  export function extractCodeFromInput(input) {
199
- return extractCallbackParametersFromInput(input)?.code ?? null;
200
+ try {
201
+ return extractCallbackParametersFromInput(input)?.code ?? null;
202
+ }
203
+ catch {
204
+ return null;
205
+ }
200
206
  }
201
207
  function extractCallbackParametersFromInput(input) {
202
208
  const trimmed = input.replaceAll("\r", "").replaceAll("\n", "").trim();
203
209
  if (trimmed.length === 0) {
204
210
  return null;
205
211
  }
212
+ let url;
206
213
  try {
207
- const url = new URL(trimmed);
208
- return {
209
- code: url.searchParams.get("code"),
210
- error: url.searchParams.get("error"),
211
- errorDescription: url.searchParams.get("error_description"),
212
- state: url.searchParams.get("state"),
213
- iss: url.searchParams.get("iss")
214
- };
214
+ url = new URL(trimmed);
215
215
  }
216
216
  catch {
217
217
  return {
@@ -222,6 +222,15 @@ function extractCallbackParametersFromInput(input) {
222
222
  iss: null
223
223
  };
224
224
  }
225
+ return readAuthorizationCallbackParameters(url);
226
+ }
227
+ function readAuthorizationCallbackParameters(url) {
228
+ for (const parameter of oauthCallbackParameters) {
229
+ if (url.searchParams.getAll(parameter).length > 1)
230
+ throw new Error(`OAuth callback parameter '${parameter}' must occur only once`);
231
+ }
232
+ return { code: url.searchParams.get("code"), error: url.searchParams.get("error"),
233
+ errorDescription: url.searchParams.get("error_description"), state: url.searchParams.get("state"), iss: url.searchParams.get("iss") };
225
234
  }
226
235
  function readExpectedAuthorizationCallback(authorizationUrl) {
227
236
  const url = new URL(authorizationUrl);
@@ -138,6 +138,8 @@ async function requestTokens(input) {
138
138
  }
139
139
  }
140
140
  const refreshToken = getOwnEntry(payload, "refresh_token");
141
+ if (refreshToken !== undefined && (typeof refreshToken !== "string" || refreshToken.trim().length === 0))
142
+ throw new Error("OAuth token response has invalid refresh_token");
141
143
  const scope = getOwnEntry(payload, "scope");
142
144
  const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0
143
145
  ? refreshToken.trim()
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.70",
3
+ "version": "0.1.74",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -58,10 +58,18 @@ Legacy SSE mode opens a GET stream and posts messages to the endpoint announced
58
58
  by the server. Announced endpoints must remain on the original origin without
59
59
  embedded credentials or fragments; endpoint changes close the connection.
60
60
  Both transports accept the same headers, OAuth provider and response limits.
61
+ Static HTTP headers are copied and validated when the transport is constructed,
62
+ before OAuth callbacks run. Invalid headers fail without reflecting their values.
63
+ Use `snapshotHttpTransportHeaders(headers)` to apply the same ownership and safe
64
+ validation when preparing a registry before constructing transports.
61
65
  `maxResponseBytes` bounds a complete JSON body or each SSE event (default 16 MiB).
62
66
  Receive streams may carry many bounded events and keepalive comments; the limit
63
67
  does not cap their lifetime bytes. Use request deadlines and cancellation to
64
68
  bound pending operations.
69
+ Receive reconnections retain the last completed event ID through events and
70
+ keepalives without IDs. An explicit empty `id:` clears the resume header,
71
+ including any initially configured value.
72
+ Unicode event IDs use UTF-8 bytes in `Last-Event-ID` headers.
65
73
  HTTP failures expose `HttpTransportError.status` and `.method`, so callers can
66
74
  make transport decisions without parsing error messages. Legacy HTTP/SSE
67
75
  `connect()` also waits for the initialized notification POST to complete before
@@ -79,29 +87,30 @@ the same optional `closeReason` promise when closing requires asynchronous work.
79
87
  `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.
80
88
 
81
89
  ```ts
82
- import { HttpTransport, createDefaultOAuthClientProvider } from "tiny-mcp-client";
90
+ import { HttpTransport } from "tiny-mcp-client";
83
91
 
84
92
  const transport = new HttpTransport({
85
93
  url: "https://mcp.example.com/mcp",
86
94
  oauth: {
87
- provider: createDefaultOAuthClientProvider({
88
- client: {
89
- mode: "dynamic",
90
- metadata: {
91
- clientName: "tiny-client"
92
- }
93
- },
94
- browser: {
95
- openBrowser: async (url) => {
96
- console.log(`Open ${url}`);
97
- }
95
+ client: {
96
+ mode: "dynamic",
97
+ metadata: {
98
+ clientName: "tiny-client"
98
99
  }
99
- })
100
+ },
101
+ browser: {
102
+ openBrowser: async (url) => {
103
+ console.log(`Open ${url}`);
104
+ }
105
+ }
100
106
  }
101
107
  });
102
108
  ```
103
109
 
104
110
  You can also call `discoverOAuthMetadata(resourceUrl, options)` directly, or instantiate `OAuthMetadataDiscovery` with a custom `fetch` implementation and shared cache. An explicit `resourceMetadataUrl` requires fresh network discovery without reading either cache; validated results still populate both caches. Lookup options accept `signal`; cancellation stops metadata fetches and body reads without trying another discovery candidate. It also settles while shared-cache reads, writes or eviction wait. Host cache work may finish afterward; its late rejection remains observed, and canceled discovery does not start another candidate.
111
+ Resource/issuer binding mismatches and exhausted authorization-server discovery
112
+ expose `OAuthMetadataError.phase` while retaining complete SDK messages.
113
+ `OAuthMetadataError.is(value)` recognizes separately bundled native copies.
105
114
 
106
115
  OAuth provider inputs receive the originating request's `signal`, covering header authorization and unauthorized handling. Modern request cancellation stops its OAuth work while leaving other requests usable; transport disposal cancels all pending OAuth operations. The default provider propagates this signal to callback, registration and token work. Custom providers must observe it for their own operations.
107
116
 
@@ -304,6 +304,12 @@ interface OAuthMetadataLookupOptions {
304
304
  resourceMetadataUrl?: string | URL;
305
305
  signal?: AbortSignal;
306
306
  }
307
+ /** Native metadata failure with complete SDK diagnostics and a safe phase. */
308
+ declare class OAuthMetadataError extends Error {
309
+ readonly phase: "protected-resource" | "authorization-server";
310
+ static is(value: unknown): value is OAuthMetadataError;
311
+ constructor(phase: "protected-resource" | "authorization-server", message: string);
312
+ }
307
313
  declare class OAuthMetadataDiscovery {
308
314
  private readonly fetchImpl;
309
315
  private readonly cache;
@@ -725,6 +731,8 @@ declare class StdioTransport implements McpTransport {
725
731
  private appendStderrOutput;
726
732
  dispose(reason?: Error): void;
727
733
  }
734
+ /** Own and validate static HTTP headers without reflecting malformed values. */
735
+ declare function snapshotHttpTransportHeaders(headers?: RequestInit["headers"]): Headers;
728
736
  declare class HttpTransport implements McpTransport {
729
737
  readonly readable: Readable;
730
738
  readonly writable: Writable;
@@ -872,5 +880,5 @@ declare class JsonRpcMessageLayer {
872
880
  private handleCancellationNotification;
873
881
  }
874
882
 
875
- export { ERROR_INTERNAL, ERROR_INVALID_PARAMS, ERROR_INVALID_REQUEST, ERROR_METHOD_NOT_FOUND, ERROR_PARSE, HttpTransport, HttpTransportError, JsonRpcMessageLayer, MCP_PROTOCOL_VERSIONS, McpClient, McpError, OAuthMetadataDiscovery, StdioTransport, createInMemoryTransportPair, createSdkTestPair, createTestPair, discoverOAuthMetadata, fetchMcpResponse };
883
+ export { ERROR_INTERNAL, ERROR_INVALID_PARAMS, ERROR_INVALID_REQUEST, ERROR_METHOD_NOT_FOUND, ERROR_PARSE, HttpTransport, HttpTransportError, JsonRpcMessageLayer, MCP_PROTOCOL_VERSIONS, McpClient, McpError, OAuthMetadataDiscovery, OAuthMetadataError, StdioTransport, createInMemoryTransportPair, createSdkTestPair, createTestPair, discoverOAuthMetadata, fetchMcpResponse, snapshotHttpTransportHeaders };
876
884
  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, McpProtocolVersion, 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 };
@@ -3517,6 +3517,7 @@ function getOwnEntry(record2, key2) {
3517
3517
 
3518
3518
  // ../mcp-oauth/dist/client/loopback-authorization.js
3519
3519
  var authorizationErrorBrand = /* @__PURE__ */ Symbol.for("poe-platform.mcp-oauth.OAuthAuthorizationError");
3520
+ var oauthCallbackParameters = ["code", "state", "iss", "error", "error_description", "error_uri"];
3520
3521
  var OAuthAuthorizationError = class extends Error {
3521
3522
  error;
3522
3523
  errorDescription;
@@ -3533,6 +3534,14 @@ var OAuthAuthorizationError = class extends Error {
3533
3534
  }
3534
3535
  };
3535
3536
  async function createLoopbackAuthorizationSession(options = {}) {
3537
+ const selected = { ...options };
3538
+ options = {
3539
+ ...selected,
3540
+ createServer: selected.createServer?.bind(options),
3541
+ openBrowser: selected.openBrowser?.bind(options),
3542
+ readLine: selected.readLine?.bind(options),
3543
+ landingPage: selected.landingPage === void 0 ? void 0 : { ...selected.landingPage }
3544
+ };
3536
3545
  options.signal?.throwIfAborted();
3537
3546
  const timeoutMs = options.timeoutMs ?? 12e4;
3538
3547
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
@@ -3592,8 +3601,7 @@ function loopbackTarget(options) {
3592
3601
  } catch (cause) {
3593
3602
  throw new Error("Invalid OAuth loopback redirect URI", { cause });
3594
3603
  }
3595
- const forbiddenQuery = ["code", "state", "error", "error_description", "iss"];
3596
- if (url.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password || url.hash || url.port === "0" || forbiddenQuery.some((name) => url.searchParams.has(name)) || [...options.redirectUri].some((char) => char.codePointAt(0) <= 32) || options.callbackPath !== void 0 && options.callbackPath !== url.pathname)
3604
+ if (url.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password || url.hash || url.port === "0" || oauthCallbackParameters.some((name) => url.searchParams.has(name)) || [...options.redirectUri].some((char) => char.codePointAt(0) <= 32) || options.callbackPath !== void 0 && options.callbackPath !== url.pathname)
3597
3605
  throw new Error("Invalid OAuth loopback redirect URI");
3598
3606
  return { port: url.port ? Number(url.port) : 80, host: url.hostname === "[::1]" ? "::1" : url.hostname, callbackPath: url.pathname };
3599
3607
  }
@@ -3669,14 +3677,8 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
3669
3677
  res.end("Not found");
3670
3678
  return;
3671
3679
  }
3672
- const callbackParameters = {
3673
- code: url.searchParams.get("code"),
3674
- error: url.searchParams.get("error"),
3675
- errorDescription: url.searchParams.get("error_description"),
3676
- state: url.searchParams.get("state"),
3677
- iss: url.searchParams.get("iss")
3678
- };
3679
3680
  try {
3681
+ const callbackParameters = readAuthorizationCallbackParameters(url);
3680
3682
  validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
3681
3683
  if (callbackParameters.error !== null)
3682
3684
  throw new OAuthAuthorizationError(callbackParameters.error, callbackParameters.errorDescription ?? callbackParameters.error);
@@ -3685,7 +3687,7 @@ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPat
3685
3687
  res.end(buildSuccessPage(options.landingPage));
3686
3688
  settle(() => resolve(code));
3687
3689
  } catch (error) {
3688
- res.writeHead(400);
3690
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8", "X-Content-Type-Options": "nosniff" });
3689
3691
  res.end(error instanceof Error ? error.message : "Invalid OAuth callback");
3690
3692
  settle(() => reject(error));
3691
3693
  }
@@ -3716,15 +3718,9 @@ function extractCallbackParametersFromInput(input) {
3716
3718
  if (trimmed.length === 0) {
3717
3719
  return null;
3718
3720
  }
3721
+ let url;
3719
3722
  try {
3720
- const url = new URL(trimmed);
3721
- return {
3722
- code: url.searchParams.get("code"),
3723
- error: url.searchParams.get("error"),
3724
- errorDescription: url.searchParams.get("error_description"),
3725
- state: url.searchParams.get("state"),
3726
- iss: url.searchParams.get("iss")
3727
- };
3723
+ url = new URL(trimmed);
3728
3724
  } catch {
3729
3725
  return {
3730
3726
  code: trimmed,
@@ -3734,6 +3730,20 @@ function extractCallbackParametersFromInput(input) {
3734
3730
  iss: null
3735
3731
  };
3736
3732
  }
3733
+ return readAuthorizationCallbackParameters(url);
3734
+ }
3735
+ function readAuthorizationCallbackParameters(url) {
3736
+ for (const parameter of oauthCallbackParameters) {
3737
+ if (url.searchParams.getAll(parameter).length > 1)
3738
+ throw new Error(`OAuth callback parameter '${parameter}' must occur only once`);
3739
+ }
3740
+ return {
3741
+ code: url.searchParams.get("code"),
3742
+ error: url.searchParams.get("error"),
3743
+ errorDescription: url.searchParams.get("error_description"),
3744
+ state: url.searchParams.get("state"),
3745
+ iss: url.searchParams.get("iss")
3746
+ };
3737
3747
  }
3738
3748
  function readExpectedAuthorizationCallback(authorizationUrl) {
3739
3749
  const url = new URL(authorizationUrl);
@@ -5174,6 +5184,8 @@ async function requestTokens(input) {
5174
5184
  }
5175
5185
  }
5176
5186
  const refreshToken = getOwnEntry5(payload, "refresh_token");
5187
+ if (refreshToken !== void 0 && (typeof refreshToken !== "string" || refreshToken.trim().length === 0))
5188
+ throw new Error("OAuth token response has invalid refresh_token");
5177
5189
  const scope = getOwnEntry5(payload, "scope");
5178
5190
  const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : void 0;
5179
5191
  const normalizedScope = normalizeOAuthScope(scope);
@@ -6232,6 +6244,19 @@ function createParameterHeaders(definitions, argumentsValue) {
6232
6244
 
6233
6245
  // src/oauth-discovery.ts
6234
6246
  import { isIP as isIP2 } from "node:net";
6247
+ var metadataErrorBrand = /* @__PURE__ */ Symbol.for("poe-platform.tiny-mcp-client.OAuthMetadataError");
6248
+ var OAuthMetadataError = class extends Error {
6249
+ constructor(phase, message) {
6250
+ super(message);
6251
+ this.phase = phase;
6252
+ this.name = "OAuthMetadataError";
6253
+ Object.defineProperty(this, metadataErrorBrand, { value: true });
6254
+ }
6255
+ phase;
6256
+ static is(value) {
6257
+ return value instanceof Error && Object.getOwnPropertyDescriptor(value, metadataErrorBrand)?.value === true;
6258
+ }
6259
+ };
6235
6260
  function defaultOAuthMetadataFetch(input, init) {
6236
6261
  return fetch(input, init);
6237
6262
  }
@@ -6269,7 +6294,8 @@ function validateProtectedResourceMetadata(value, resourceUrl) {
6269
6294
  }
6270
6295
  const normalizedResource = canonicalizeResourceIndicator(value.resource);
6271
6296
  if (normalizedResource !== resourceUrl) {
6272
- throw new Error(
6297
+ throw new OAuthMetadataError(
6298
+ "protected-resource",
6273
6299
  `Protected resource metadata resource mismatch: expected ${resourceUrl}, received ${value.resource}`
6274
6300
  );
6275
6301
  }
@@ -6291,7 +6317,8 @@ function validateAuthorizationServerMetadata(value, issuer) {
6291
6317
  throw new Error("Authorization server metadata must include issuer");
6292
6318
  }
6293
6319
  if (value.issuer !== issuer) {
6294
- throw new Error(
6320
+ throw new OAuthMetadataError(
6321
+ "authorization-server",
6295
6322
  `Authorization server metadata issuer mismatch: expected ${issuer}, received ${value.issuer}`
6296
6323
  );
6297
6324
  }
@@ -6534,7 +6561,8 @@ var OAuthMetadataDiscovery = class {
6534
6561
  }
6535
6562
  }
6536
6563
  }
6537
- throw new Error(
6564
+ throw new OAuthMetadataError(
6565
+ "authorization-server",
6538
6566
  `Unable to load authorization server metadata for ${cacheKey}: ${authorizationServerErrors.join(
6539
6567
  "; "
6540
6568
  )}`
@@ -7659,6 +7687,13 @@ var StdioTransport = class _StdioTransport {
7659
7687
  }
7660
7688
  }
7661
7689
  };
7690
+ function snapshotHttpTransportHeaders(headers) {
7691
+ try {
7692
+ return new Headers(headers);
7693
+ } catch {
7694
+ throw new Error("Invalid HTTP transport headers");
7695
+ }
7696
+ }
7662
7697
  var HttpTransport = class {
7663
7698
  readable;
7664
7699
  writable;
@@ -7708,7 +7743,7 @@ var HttpTransport = class {
7708
7743
  this.maxResponseBytes = maxResponseBytes;
7709
7744
  this.url = url;
7710
7745
  this.mode = mode;
7711
- this.headers = headers;
7746
+ this.headers = snapshotHttpTransportHeaders(headers);
7712
7747
  this.fetchImpl = fetchImpl;
7713
7748
  this.onWarning = onWarning;
7714
7749
  this.oauthProvider = oauth === void 0 ? void 0 : createOAuthClientProvider(oauth);
@@ -7985,7 +8020,8 @@ var HttpTransport = class {
7985
8020
  }
7986
8021
  if (this.legacyProtocolVersion !== void 0) headers.set("MCP-Protocol-Version", this.legacyProtocolVersion);
7987
8022
  if (this.lastEventId !== void 0) {
7988
- headers.set("Last-Event-ID", this.lastEventId);
8023
+ if (this.lastEventId === "") headers.delete("Last-Event-ID");
8024
+ else headers.set("Last-Event-ID", Buffer.from(this.lastEventId, "utf8").toString("latin1"));
7989
8025
  }
7990
8026
  return this.authorizeRequestHeaders(headers, signal);
7991
8027
  }
@@ -8244,15 +8280,15 @@ var HttpTransport = class {
8244
8280
  void reader.cancel().catch(() => void 0);
8245
8281
  return;
8246
8282
  }
8247
- this.lastEventId = parser.lastEventId;
8283
+ this.lastEventId = parser.lastEventId ?? this.lastEventId;
8248
8284
  }
8249
8285
  const trailingChunk = decoder.decode();
8250
8286
  if (trailingChunk.length > 0) {
8251
8287
  this.writeSseMessages(parser.push(trailingChunk), context);
8252
- this.lastEventId = parser.lastEventId;
8288
+ this.lastEventId = parser.lastEventId ?? this.lastEventId;
8253
8289
  }
8254
8290
  this.writeSseMessages(parser.flush(), context);
8255
- this.lastEventId = parser.lastEventId;
8291
+ this.lastEventId = parser.lastEventId ?? this.lastEventId;
8256
8292
  if (context !== void 0 && !context.completed) throw new Error("MCP HTTP stream ended before its final response");
8257
8293
  } catch (error) {
8258
8294
  void reader.cancel().catch(() => void 0);
@@ -9263,12 +9299,14 @@ export {
9263
9299
  McpClient,
9264
9300
  McpError,
9265
9301
  OAuthMetadataDiscovery,
9302
+ OAuthMetadataError,
9266
9303
  StdioTransport,
9267
9304
  createInMemoryTransportPair,
9268
9305
  createSdkTestPair,
9269
9306
  createTestPair,
9270
9307
  discoverOAuthMetadata,
9271
- fetchMcpResponse
9308
+ fetchMcpResponse,
9309
+ snapshotHttpTransportHeaders
9272
9310
  };
9273
9311
  /*!
9274
9312
  The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.344",
11
+ "version": "0.0.346",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.344",
3
+ "version": "0.0.346",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.344",
3
+ "version": "0.0.346",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -158,7 +158,7 @@
158
158
  "yaml"
159
159
  ],
160
160
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.344",
161
+ "toolcraft-schema": "0.0.346",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",