toolcraft 0.0.314 → 0.0.316

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.
Files changed (22) hide show
  1. package/composition.json +3 -3
  2. package/dist/composition.json +3 -3
  3. package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
  4. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/README.md +40 -2
  5. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +1 -6
  6. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +6 -18
  7. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.d.ts +6 -0
  8. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.js +148 -0
  9. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +112 -49
  10. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-auth-method.d.ts +2 -0
  11. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-auth-method.js +7 -0
  12. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +3 -1
  13. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.js +19 -9
  14. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +36 -5
  15. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.d.ts +2 -1
  16. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.js +1 -0
  17. package/node_modules/tiny-http-mcp-server/package.json +1 -1
  18. package/node_modules/tiny-mcp-client/dist/index.d.ts +36 -5
  19. package/node_modules/tiny-mcp-client/dist/index.js +1312 -1095
  20. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  21. package/node_modules/toolcraft-schema/package.json +1 -1
  22. package/package.json +2 -2
package/composition.json CHANGED
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.33",
101
+ "version": "0.1.36",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.314",
116
+ "version": "0.0.316",
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.314",
126
+ "version": "0.0.316",
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.33",
101
+ "version": "0.1.36",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.314",
116
+ "version": "0.0.316",
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.314",
126
+ "version": "0.0.316",
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.33",
21
+ "version": "0.1.36",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -106,12 +106,50 @@ Select a separate persistence namespace for another scope profile. No scope is
106
106
  invented when the client does not configure one.
107
107
 
108
108
  Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
109
- `tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
110
- and optional `scope`. A fresh imported token is used only for its resource.
109
+ `tokenType: "Bearer"`, optional `expiresAt` (Unix epoch milliseconds or `null`
110
+ if unknown), and optional `scope`. `expiresIn` is a lifetime in seconds and is
111
+ anchored once at import. For a delayed import, provide the original `issuedAt`
112
+ in epoch milliseconds or its real absolute `expiresAt`; a numeric absolute
113
+ expiry takes precedence. Without either, the relative value means remaining
114
+ lifetime at import. Omitted expiry stays unknown. A fresh imported token is
115
+ used only for its resource.
111
116
  Discovery binds an expired or explicitly rejected grant before silent refresh,
112
117
  using the original configured client. Persisted sessions take precedence,
113
118
  including sessions whose tokens have been cleared; an import cannot revive them.
114
119
  Input tokens are copied and invalid expiry values fail before authorization.
120
+ Pass a complete DCR response as `client.registration` (or validate untrusted JSON
121
+ with `parseOAuthClientRegistration`). Dynamic clients infer their original ID
122
+ and secret from that response and reuse it without registering another app.
123
+ Explicit ID/secret values must agree with the imported response. Sessions and
124
+ native registration stores retain arrays, issuance/expiry timestamps and JSON
125
+ provider metadata. Registration input is copied, bounded to 64 KiB and 64
126
+ levels, and rejects invalid standard field types and non-JSON extensions.
127
+ An optional registration `issuer` must match discovery and the persisted
128
+ authorization server exactly. Contradictory metadata fails without activating
129
+ or redeeming the grant. `client_secret_expires_at` uses Unix epoch seconds;
130
+ zero means no expiry. Live access tokens remain usable after secret expiry,
131
+ but an expired secret is never submitted for refresh. Native DCR can replace
132
+ an expired registration during explicit authorization; caller-owned imports
133
+ must be updated. Headless requests retain the old record and report recovery
134
+ is required without creating a pending refresh marker.
135
+ Native registrations retain `requestedRedirectUri`, the actual listener URI
136
+ submitted to DCR, separately from the full response metadata. Fresh responses
137
+ may normalize a loopback port or represent IPv4 loopback as portless localhost;
138
+ host, path, scheme, query and fragment differences outside that boundary fail.
139
+ Authorization and code exchange always use the actual listener URI. Silent
140
+ refresh keeps its original client regardless of callback changes. At interactive
141
+ authorization, a native registration with an obsolete captured callback is
142
+ replaced; caller-owned full registration imports retain their original identity.
143
+ Set `client.tokenEndpointAuthMethod` to `none`, `client_secret_post` or
144
+ `client_secret_basic`; a full registration can supply the same field as
145
+ `token_endpoint_auth_method`. Public clients never transmit a stored secret.
146
+ Basic credentials are individually form-encoded before Base64 encoding and
147
+ are omitted from the form body. Cached grants retain their registered method;
148
+ an explicitly different configured method requires separate persistence or a
149
+ reset. Native DCR chooses a supported method, preferring public PKCE when
150
+ advertised. Unsupported methods and missing confidential secrets fail before
151
+ token requests. Existing clients without a method keep the previous default:
152
+ body authentication when a secret is present, public authentication otherwise.
115
153
  Static clients and dynamic initial-grant imports require cached grants to match
116
154
  the original normalized client ID and secret. A different client configuration
117
155
  fails before attaching or refreshing credentials and retains the stored record;
@@ -1,9 +1,5 @@
1
1
  import { type CreateSecretStoreInput } from "auth-store";
2
- import type { OAuthSessionStore } from "./types.js";
3
- interface StoredOAuthClient {
4
- clientId: string;
5
- clientSecret?: string;
6
- }
2
+ import type { OAuthSessionStore, StoredOAuthClient } from "./types.js";
7
3
  export interface OAuthClientStore {
8
4
  load(issuer: string): Promise<StoredOAuthClient | null>;
9
5
  save(issuer: string, client: StoredOAuthClient): Promise<void>;
@@ -12,4 +8,3 @@ export interface OAuthClientStore {
12
8
  export declare function createAuthStoreSessionStore(options?: CreateSecretStoreInput, namespace?: string): OAuthSessionStore;
13
9
  export declare function createAuthStoreClientStore(options: CreateSecretStoreInput, namespace?: string): OAuthClientStore;
14
10
  export declare function assertPersistenceNamespace(namespace: string | undefined): void;
15
- export {};
@@ -1,3 +1,4 @@
1
+ import { normalizeStoredOAuthClient } from "./client-registration.js";
1
2
  import crypto from "node:crypto";
2
3
  import path from "node:path";
3
4
  import { createSecretStore } from "auth-store";
@@ -62,15 +63,10 @@ export function createAuthStoreClientStore(options, namespace) {
62
63
  catch {
63
64
  throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
64
65
  }
65
- const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : undefined;
66
- if (clientId !== undefined) {
67
- const client = { clientId };
68
- if (isObjectRecord(parsed) &&
69
- Object.prototype.hasOwnProperty.call(parsed, "clientSecret")) {
70
- client.clientSecret = getOwnEntry(parsed, "clientSecret");
71
- }
72
- return client;
73
- }
66
+ // Preserve all registration metadata; provider normalization validates its
67
+ // identity and schema before it can authorize a request.
68
+ if (isObjectRecord(parsed) && typeof getOwnEntry(parsed, "clientId") === "string")
69
+ return parsed;
74
70
  throw new Error("Stored OAuth client must be a JSON object with clientId");
75
71
  },
76
72
  async save(issuer, client) {
@@ -142,21 +138,13 @@ function isStoredOAuthSession(value) {
142
138
  }
143
139
  return (isNonBlankOwnString(value, "resource") &&
144
140
  isNonBlankOwnString(value, "authorizationServer") &&
145
- isStoredOAuthClient(getOwnEntry(value, "client")) &&
141
+ normalizeStoredOAuthClient(getOwnEntry(value, "client")) !== null &&
146
142
  isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
147
143
  (getOwnEntry(value, "requestedScope") === undefined || isNonBlankOwnString(value, "requestedScope")) &&
148
144
  (getOwnEntry(value, "refreshState") === undefined ||
149
145
  (getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
150
146
  isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
151
147
  }
152
- function isStoredOAuthClient(value) {
153
- if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
154
- return false;
155
- }
156
- const clientSecret = getOwnEntry(value, "clientSecret");
157
- return (clientSecret === undefined ||
158
- (typeof clientSecret === "string" && clientSecret.trim().length > 0));
159
- }
160
148
  function isStoredOAuthDiscovery(value) {
161
149
  if (!isObjectRecord(value)) {
162
150
  return false;
@@ -0,0 +1,6 @@
1
+ import type { OAuthClientRegistration, StoredOAuthClient } from "./types.js";
2
+ /** Validate and copy a bounded JSON DCR response without quoting credential input. */
3
+ export declare function parseOAuthClientRegistration(value: unknown): OAuthClientRegistration;
4
+ export declare function normalizeStoredOAuthClient(value: unknown): StoredOAuthClient | null;
5
+ /** Fresh DCR may describe a normalized loopback port; saved identity stays exact. */
6
+ export declare function registrationMatchesRedirect(client: StoredOAuthClient, requestedUri: string, fresh?: boolean): boolean;
@@ -0,0 +1,148 @@
1
+ import { loopbackTarget } from "./loopback-authorization.js";
2
+ import { normalizeOAuthScope } from "./scope.js";
3
+ import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
4
+ /** Validate and copy a bounded JSON DCR response without quoting credential input. */
5
+ export function parseOAuthClientRegistration(value) {
6
+ const invalid = () => new Error("Invalid OAuth client registration metadata");
7
+ let nodes = 0;
8
+ function copy(input, depth) {
9
+ if (++nodes > 20_000 || depth > 64)
10
+ throw invalid();
11
+ if (input === null || typeof input === "boolean" || typeof input === "string")
12
+ return input;
13
+ if (typeof input === "number" && Number.isFinite(input))
14
+ return input;
15
+ if (typeof input !== "object" || input === null)
16
+ throw invalid();
17
+ const descriptors = Object.getOwnPropertyDescriptors(input);
18
+ if (Array.isArray(input)) {
19
+ const length = descriptors.length?.value;
20
+ if (length > 20_000)
21
+ throw invalid();
22
+ const result = [];
23
+ for (let index = 0; index < length; index++) {
24
+ const descriptor = descriptors[String(index)];
25
+ if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
26
+ throw invalid();
27
+ result.push(copy(descriptor.value, depth + 1));
28
+ }
29
+ return result;
30
+ }
31
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
32
+ throw invalid();
33
+ return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key, descriptor]) => {
34
+ if (!Object.hasOwn(descriptor, "value"))
35
+ throw invalid();
36
+ return [key, copy(descriptor.value, depth + 1)];
37
+ }));
38
+ }
39
+ let result;
40
+ try {
41
+ result = copy(value, 0);
42
+ }
43
+ catch {
44
+ throw invalid();
45
+ }
46
+ if (typeof result !== "object" || result === null || Array.isArray(result))
47
+ throw invalid();
48
+ const record = result;
49
+ if (!Object.hasOwn(record, "client_id") || typeof record.client_id !== "string" || record.client_id.trim() === "")
50
+ throw new Error("OAuth client registration response missing client_id");
51
+ for (const key of ["client_id", "client_secret", "token_endpoint_auth_method", "application_type", "client_name", "client_uri", "logo_uri", "scope",
52
+ "tos_uri", "policy_uri", "jwks_uri", "software_id", "software_version", "software_statement", "registration_access_token", "registration_client_uri", "issuer"]) {
53
+ if (Object.hasOwn(record, key) && record[key] !== null && typeof record[key] !== "string")
54
+ throw invalid();
55
+ }
56
+ if (typeof record.client_secret === "string" && record.client_secret.trim() === "")
57
+ throw invalid();
58
+ for (const key of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
59
+ const entry = record[key];
60
+ if (Object.hasOwn(record, key) && entry !== null && (!Array.isArray(entry) || entry.some(item => typeof item !== "string")))
61
+ throw invalid();
62
+ }
63
+ for (const key of ["client_id_issued_at", "client_secret_expires_at"]) {
64
+ const entry = record[key];
65
+ if (Object.hasOwn(record, key) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
66
+ throw invalid();
67
+ }
68
+ try {
69
+ normalizeOAuthScope(Object.hasOwn(record, "scope") && record.scope !== null ? record.scope : undefined);
70
+ }
71
+ catch {
72
+ throw invalid();
73
+ }
74
+ if (Buffer.byteLength(JSON.stringify(record), "utf8") > 64 * 1024)
75
+ throw invalid();
76
+ return record;
77
+ }
78
+ export function normalizeStoredOAuthClient(value) {
79
+ if (typeof value !== "object" || value === null || Array.isArray(value))
80
+ return null;
81
+ const record = value;
82
+ const clientId = Object.hasOwn(record, "clientId") ? record.clientId : undefined;
83
+ const clientSecret = Object.hasOwn(record, "clientSecret") ? record.clientSecret : undefined;
84
+ if (typeof clientId !== "string" || clientId.trim() === "" ||
85
+ (clientSecret !== undefined && (typeof clientSecret !== "string" || clientSecret.trim() === "")))
86
+ return null;
87
+ const client = { clientId: clientId.trim(), ...(clientSecret === undefined ? {} : { clientSecret: clientSecret.trim() }) };
88
+ const requestedRedirectUri = Object.hasOwn(record, "requestedRedirectUri") ? record.requestedRedirectUri : undefined;
89
+ if (requestedRedirectUri !== undefined) {
90
+ try {
91
+ if (typeof requestedRedirectUri !== "string")
92
+ throw new Error("Invalid redirect identity");
93
+ loopbackTarget({ redirectUri: requestedRedirectUri });
94
+ }
95
+ catch {
96
+ throw new Error("Invalid stored OAuth registration redirect identity");
97
+ }
98
+ client.requestedRedirectUri = requestedRedirectUri;
99
+ }
100
+ const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record, "tokenEndpointAuthMethod") ? record.tokenEndpointAuthMethod : undefined);
101
+ if (Object.hasOwn(record, "registration") && record.registration !== undefined) {
102
+ const registration = parseOAuthClientRegistration(record.registration);
103
+ const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : undefined;
104
+ if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
105
+ throw new Error("OAuth client registration does not match the client identity");
106
+ client.registration = registration;
107
+ const registrationMethod = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(registration, "token_endpoint_auth_method") ? registration.token_endpoint_auth_method : undefined);
108
+ if (method !== undefined && registrationMethod !== undefined && method !== registrationMethod)
109
+ throw new Error("OAuth token endpoint authentication conflicts with the client registration");
110
+ if (registrationMethod !== undefined)
111
+ client.tokenEndpointAuthMethod = registrationMethod;
112
+ }
113
+ if (method !== undefined)
114
+ client.tokenEndpointAuthMethod = method;
115
+ return client;
116
+ }
117
+ /** Fresh DCR may describe a normalized loopback port; saved identity stays exact. */
118
+ export function registrationMatchesRedirect(client, requestedUri, fresh = false) {
119
+ if (client.requestedRedirectUri !== undefined)
120
+ return client.requestedRedirectUri === requestedUri;
121
+ const redirects = client.registration?.redirect_uris;
122
+ if (redirects === undefined || redirects === null || redirects.length === 0)
123
+ return true;
124
+ return redirects.some(returnedUri => {
125
+ if (returnedUri === requestedUri)
126
+ return true;
127
+ if (!fresh)
128
+ return false;
129
+ let returned, requested;
130
+ try {
131
+ returned = new URL(returnedUri);
132
+ requested = new URL(requestedUri);
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ if (requested.protocol !== "http:" || returned.protocol !== "http:" ||
138
+ !["127.0.0.1", "[::1]", "localhost"].includes(requested.hostname))
139
+ return false;
140
+ const sameHost = returned.hostname === requested.hostname;
141
+ const normalizedIpv4 = requested.hostname === "127.0.0.1" && returned.hostname === "localhost" && returned.port === "";
142
+ if (!sameHost && !normalizedIpv4)
143
+ return false;
144
+ returned.hostname = requested.hostname;
145
+ returned.port = requested.port;
146
+ return returned.href === requested.href;
147
+ });
148
+ }