toolcraft 0.0.344 → 0.0.345

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.71",
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.345",
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.345",
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.71",
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.345",
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.345",
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.71",
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.71",
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,6 +58,10 @@ 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
@@ -725,6 +725,8 @@ declare class StdioTransport implements McpTransport {
725
725
  private appendStderrOutput;
726
726
  dispose(reason?: Error): void;
727
727
  }
728
+ /** Own and validate static HTTP headers without reflecting malformed values. */
729
+ declare function snapshotHttpTransportHeaders(headers?: RequestInit["headers"]): Headers;
728
730
  declare class HttpTransport implements McpTransport {
729
731
  readonly readable: Readable;
730
732
  readonly writable: Writable;
@@ -872,5 +874,5 @@ declare class JsonRpcMessageLayer {
872
874
  private handleCancellationNotification;
873
875
  }
874
876
 
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 };
877
+ 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, snapshotHttpTransportHeaders };
876
878
  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);
@@ -7659,6 +7671,13 @@ var StdioTransport = class _StdioTransport {
7659
7671
  }
7660
7672
  }
7661
7673
  };
7674
+ function snapshotHttpTransportHeaders(headers) {
7675
+ try {
7676
+ return new Headers(headers);
7677
+ } catch {
7678
+ throw new Error("Invalid HTTP transport headers");
7679
+ }
7680
+ }
7662
7681
  var HttpTransport = class {
7663
7682
  readable;
7664
7683
  writable;
@@ -7708,7 +7727,7 @@ var HttpTransport = class {
7708
7727
  this.maxResponseBytes = maxResponseBytes;
7709
7728
  this.url = url;
7710
7729
  this.mode = mode;
7711
- this.headers = headers;
7730
+ this.headers = snapshotHttpTransportHeaders(headers);
7712
7731
  this.fetchImpl = fetchImpl;
7713
7732
  this.onWarning = onWarning;
7714
7733
  this.oauthProvider = oauth === void 0 ? void 0 : createOAuthClientProvider(oauth);
@@ -9268,7 +9287,8 @@ export {
9268
9287
  createSdkTestPair,
9269
9288
  createTestPair,
9270
9289
  discoverOAuthMetadata,
9271
- fetchMcpResponse
9290
+ fetchMcpResponse,
9291
+ snapshotHttpTransportHeaders
9272
9292
  };
9273
9293
  /*!
9274
9294
  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.345",
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.345",
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.345",
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.345",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",