toolcraft 0.0.317 → 0.0.319

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.37",
101
+ "version": "0.1.40",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.317",
116
+ "version": "0.0.319",
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.317",
126
+ "version": "0.0.319",
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.37",
101
+ "version": "0.1.40",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.317",
116
+ "version": "0.0.319",
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.317",
126
+ "version": "0.0.319",
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.37",
21
+ "version": "0.1.40",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -95,6 +95,14 @@ without redeeming its refresh token again. A proven current token is refreshed
95
95
  on 401 even when the server omits `error="invalid_token"`. Invalid provenance
96
96
  fails without quoting token values.
97
97
 
98
+ The native provider also exposes `authenticate({ requestUrl, fetch, signal,
99
+ discover })` for explicit login, including servers whose initialization is
100
+ public. `discover` lazily supplies validated OAuth metadata and is skipped for
101
+ usable existing grants. Omit it to recover/reuse known sessions only; the method
102
+ returns `void` if a new discovery lookup is needed. It honors `allowInteractive`,
103
+ recovers pending refresh outcomes through consent, and returns an owned token
104
+ snapshot. Normal transport request authorization remains noninteractive.
105
+
98
106
  Configure `client.metadata.scope` to request a precise scope set; broader
99
107
  discovery metadata does not override it. Explicit scopes must match the cached
100
108
  or imported grant's scope set; ordering, repeated spaces and duplicates are
@@ -66,6 +66,26 @@ export function createDefaultOAuthClientProvider(options) {
66
66
  }
67
67
  let initialGrantConsumed = false;
68
68
  return {
69
+ async authenticate(input) {
70
+ input.signal?.throwIfAborted();
71
+ assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
72
+ const resource = canonicalizeResourceIndicator(input.requestUrl);
73
+ let session = await ensureAuthorizedSession(resource, undefined, input.fetch, true, false, input.signal);
74
+ if (session?.tokens !== undefined && !isExpired(session.tokens, now))
75
+ return { ...session.tokens };
76
+ if (session === null && !initialGrantConsumed && initialGrant?.resource === resource &&
77
+ initialGrant.tokens !== undefined && !isExpired(initialGrant.tokens, now))
78
+ return { ...initialGrant.tokens };
79
+ if (input.discover === undefined)
80
+ return;
81
+ const discovery = await input.discover();
82
+ input.signal?.throwIfAborted();
83
+ assertRequestMatchesResource(resource, canonicalizeResourceIndicator(discovery.resource));
84
+ session = await ensureAuthorizedSession(resource, discovery, input.fetch, true, false, input.signal);
85
+ if (session?.tokens === undefined || isExpired(session.tokens, now))
86
+ throw new Error("OAuth authentication did not establish a usable grant");
87
+ return { ...session.tokens };
88
+ },
69
89
  async authorizeRequest(input) {
70
90
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
71
91
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
@@ -65,12 +65,22 @@ export function createResourceBoundOAuthStores(options, namespace, identity) {
65
65
  return record;
66
66
  }
67
67
  result.sessionStore = {
68
- async withLock(_resource, operation, options) {
68
+ async withLock(resource, operation, options) {
69
69
  if (store.withLock === undefined)
70
70
  throw new Error("OAuth resource identity backend must support transaction locks");
71
- return store.withLock(operation, options);
71
+ return store.withLock(async () => {
72
+ options.signal?.throwIfAborted();
73
+ await reconcile(resource);
74
+ options.signal?.throwIfAborted();
75
+ return operation();
76
+ }, options);
77
+ },
78
+ async load(resource) {
79
+ // Unauthorized provenance may peek before acquiring the transaction lock.
80
+ // Only the lock owner may adopt a URL or retire the previous credentials.
81
+ const record = await read();
82
+ return record?.resource === canonicalizeResourceIndicator(resource) ? record.session : null;
72
83
  },
73
- async load(resource) { return (await reconcile(resource)).session; },
74
84
  async save(resource, session) {
75
85
  const record = await reconcile(resource);
76
86
  if (canonicalizeResourceIndicator(session.resource) !== record.resource)
@@ -28,6 +28,14 @@ export interface OAuthUnauthorizedChallenge {
28
28
  raw: string;
29
29
  }
30
30
  export interface OAuthClientProvider {
31
+ /** Establish a grant explicitly, including when resource initialization is public. */
32
+ authenticate?(input: {
33
+ requestUrl: URL;
34
+ fetch: OAuthMetadataFetch;
35
+ /** Lazy validated discovery. Without it, recover/reuse known grants only. */
36
+ discover?: () => Promise<OAuthDiscoveryResult>;
37
+ signal?: AbortSignal;
38
+ }): Promise<StoredOAuthTokens | void>;
31
39
  authorizeRequest?(input: {
32
40
  requestUrl: URL;
33
41
  headers: Headers;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.37",
3
+ "version": "0.1.40",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,10 @@ by the server. Announced endpoints must remain on the original origin without
37
37
  embedded credentials or fragments; endpoint changes close the connection.
38
38
  Both transports accept the same headers, OAuth provider and response limits.
39
39
  HTTP failures expose `HttpTransportError.status` and `.method`, so callers can
40
- make transport decisions without parsing error messages.
40
+ make transport decisions without parsing error messages. Legacy HTTP/SSE
41
+ `connect()` also waits for the initialized notification POST to complete before
42
+ reporting ready. Completion failures reject the connection and expose
43
+ `rpcMethod: "notifications/initialized"`; they are not setup transport mismatches.
41
44
 
42
45
  ## OAuth HTTP support
43
46
 
@@ -131,6 +131,14 @@ interface OAuthUnauthorizedChallenge {
131
131
  raw: string;
132
132
  }
133
133
  interface OAuthClientProvider {
134
+ /** Establish a grant explicitly, including when resource initialization is public. */
135
+ authenticate?(input: {
136
+ requestUrl: URL;
137
+ fetch: OAuthMetadataFetch;
138
+ /** Lazy validated discovery. Without it, recover/reuse known grants only. */
139
+ discover?: () => Promise<OAuthDiscoveryResult>;
140
+ signal?: AbortSignal;
141
+ }): Promise<StoredOAuthTokens | void>;
134
142
  authorizeRequest?(input: {
135
143
  requestUrl: URL;
136
144
  headers: Headers;
@@ -641,6 +649,11 @@ interface McpTransport {
641
649
  closed: Promise<McpTransportClosedEvent>;
642
650
  dispose(reason?: Error): void;
643
651
  filterTools?(tools: Tool[], reset?: boolean): Tool[];
652
+ /** Complete a legacy initialization handshake before the client reports ready. */
653
+ completeInitialization?(options: {
654
+ signal?: AbortSignal;
655
+ timeoutMs: number;
656
+ }): Promise<void>;
644
657
  }
645
658
  interface InMemoryServerTransport {
646
659
  readable: Readable;
@@ -676,7 +689,8 @@ type HttpTransportFetch = (input: string | URL, init?: RequestInit) => Promise<R
676
689
  declare class HttpTransportError extends Error {
677
690
  readonly status: number;
678
691
  readonly method: "GET" | "POST" | "DELETE";
679
- constructor(message: string, status: number, method: "GET" | "POST" | "DELETE");
692
+ readonly rpcMethod?: string | undefined;
693
+ constructor(message: string, status: number, method: "GET" | "POST" | "DELETE", rpcMethod?: string | undefined);
680
694
  }
681
695
  interface HttpTransportOptions {
682
696
  url: string;
@@ -733,6 +747,10 @@ declare class HttpTransport implements McpTransport {
733
747
  private readonly toolParameterHeaders;
734
748
  private readonly onWarning;
735
749
  constructor({ url, mode, headers, fetch: fetchImpl, oauth, oauthDiscoveryCache, onWarning, maxResponseBytes, }: HttpTransportOptions);
750
+ completeInitialization(options: {
751
+ signal?: AbortSignal;
752
+ timeoutMs: number;
753
+ }): Promise<void>;
736
754
  filterTools(tools: Tool[], reset?: boolean): Tool[];
737
755
  dispose(reason?: Error): void;
738
756
  private closeWithSessionTermination;
@@ -4771,13 +4771,19 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4771
4771
  return record2;
4772
4772
  }
4773
4773
  result.sessionStore = {
4774
- async withLock(_resource, operation, options2) {
4774
+ async withLock(resource, operation, options2) {
4775
4775
  if (store.withLock === void 0)
4776
4776
  throw new Error("OAuth resource identity backend must support transaction locks");
4777
- return store.withLock(operation, options2);
4777
+ return store.withLock(async () => {
4778
+ options2.signal?.throwIfAborted();
4779
+ await reconcile(resource);
4780
+ options2.signal?.throwIfAborted();
4781
+ return operation();
4782
+ }, options2);
4778
4783
  },
4779
4784
  async load(resource) {
4780
- return (await reconcile(resource)).session;
4785
+ const record2 = await read();
4786
+ return record2?.resource === canonicalizeResourceIndicator(resource) ? record2.session : null;
4781
4787
  },
4782
4788
  async save(resource, session) {
4783
4789
  const record2 = await reconcile(resource);
@@ -5166,6 +5172,25 @@ function createDefaultOAuthClientProvider(options) {
5166
5172
  }
5167
5173
  let initialGrantConsumed = false;
5168
5174
  return {
5175
+ async authenticate(input) {
5176
+ input.signal?.throwIfAborted();
5177
+ assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
5178
+ const resource = canonicalizeResourceIndicator(input.requestUrl);
5179
+ let session = await ensureAuthorizedSession(resource, void 0, input.fetch, true, false, input.signal);
5180
+ if (session?.tokens !== void 0 && !isExpired(session.tokens, now))
5181
+ return { ...session.tokens };
5182
+ if (session === null && !initialGrantConsumed && initialGrant?.resource === resource && initialGrant.tokens !== void 0 && !isExpired(initialGrant.tokens, now))
5183
+ return { ...initialGrant.tokens };
5184
+ if (input.discover === void 0)
5185
+ return;
5186
+ const discovery = await input.discover();
5187
+ input.signal?.throwIfAborted();
5188
+ assertRequestMatchesResource(resource, canonicalizeResourceIndicator(discovery.resource));
5189
+ session = await ensureAuthorizedSession(resource, discovery, input.fetch, true, false, input.signal);
5190
+ if (session?.tokens === void 0 || isExpired(session.tokens, now))
5191
+ throw new Error("OAuth authentication did not establish a usable grant");
5192
+ return { ...session.tokens };
5193
+ },
5169
5194
  async authorizeRequest(input) {
5170
5195
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
5171
5196
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
@@ -6782,7 +6807,8 @@ var McpClient = class {
6782
6807
  async (params, context) => this.options.onElicitationRequest(params, context)
6783
6808
  );
6784
6809
  }
6785
- messageLayer.sendNotification("notifications/initialized");
6810
+ if (transport.completeInitialization === void 0) messageLayer.sendNotification("notifications/initialized");
6811
+ else await transport.completeInitialization({ signal: options.signal, timeoutMs: this.options.requestTimeoutMs ?? 3e4 });
6786
6812
  this.currentState = "ready";
6787
6813
  return initializeResult;
6788
6814
  } catch (error) {
@@ -7282,14 +7308,16 @@ async function createTestPair(server, createClient) {
7282
7308
  return { client, cleanup };
7283
7309
  }
7284
7310
  var HttpTransportError = class extends Error {
7285
- constructor(message, status, method) {
7311
+ constructor(message, status, method, rpcMethod) {
7286
7312
  super(message);
7287
7313
  this.status = status;
7288
7314
  this.method = method;
7315
+ this.rpcMethod = rpcMethod;
7289
7316
  this.name = "HttpTransportError";
7290
7317
  }
7291
7318
  status;
7292
7319
  method;
7320
+ rpcMethod;
7293
7321
  };
7294
7322
  function defaultStdioSpawn(command, args, options) {
7295
7323
  return spawn2(command, args, options);
@@ -7464,6 +7492,21 @@ var HttpTransport = class {
7464
7492
  this.dispose(error instanceof Error ? error : new Error(String(error)));
7465
7493
  });
7466
7494
  }
7495
+ async completeInitialization(options) {
7496
+ const deadline = options.timeoutMs > 0 ? AbortSignal.timeout(Math.ceil(options.timeoutMs)) : void 0;
7497
+ const signals = [options.signal, deadline].filter((signal2) => signal2 !== void 0);
7498
+ const signal = signals.length === 0 ? new AbortController().signal : AbortSignal.any(signals);
7499
+ signal.throwIfAborted();
7500
+ try {
7501
+ await this.sendPost(serializeJsonRpcMessage({ jsonrpc: "2.0", method: "notifications/initialized" }), signal);
7502
+ signal.throwIfAborted();
7503
+ if (this.disposed) throw (await this.closed).reason;
7504
+ } catch (error) {
7505
+ if (error instanceof HttpTransportError)
7506
+ throw new HttpTransportError(error.message, error.status, error.method, "notifications/initialized");
7507
+ throw error;
7508
+ }
7509
+ }
7467
7510
  filterTools(tools, reset = true) {
7468
7511
  if (reset) this.toolParameterHeaders.clear();
7469
7512
  const accepted = [];
@@ -7575,7 +7618,8 @@ var HttpTransport = class {
7575
7618
  }
7576
7619
  }
7577
7620
  }
7578
- async sendPost(line) {
7621
+ async sendPost(line, signal) {
7622
+ signal?.throwIfAborted();
7579
7623
  const parsed = parseJsonRpcMessage(line);
7580
7624
  const message = parsed.type === "request" || parsed.type === "notification" ? parsed.message : void 0;
7581
7625
  const metadata = isObjectRecord5(message?.params) ? message.params._meta : void 0;
@@ -7589,7 +7633,10 @@ var HttpTransport = class {
7589
7633
  this.modernRequests.get(requestId)?.abort();
7590
7634
  return;
7591
7635
  }
7592
- const controller = modern && parsed.type === "request" ? new AbortController() : void 0;
7636
+ const controller = modern && parsed.type === "request" || signal !== void 0 ? new AbortController() : void 0;
7637
+ const aborted = () => controller?.abort(signal?.reason);
7638
+ signal?.addEventListener("abort", aborted, { once: true });
7639
+ if (signal?.aborted) aborted();
7593
7640
  const id = parsed.type === "request" ? parsed.message.id : void 0;
7594
7641
  if (controller !== void 0 && id !== void 0) this.modernRequests.set(id, controller);
7595
7642
  try {
@@ -7598,7 +7645,7 @@ var HttpTransport = class {
7598
7645
  const response = await this.fetchWithOAuthRetry({
7599
7646
  url: postUrl,
7600
7647
  method: "POST",
7601
- createHeaders: (signal) => this.createPostHeaders(message, modern, signal),
7648
+ createHeaders: (signal2) => this.createPostHeaders(message, modern, signal2),
7602
7649
  body: line,
7603
7650
  controller
7604
7651
  });
@@ -7634,6 +7681,7 @@ var HttpTransport = class {
7634
7681
  } catch (error) {
7635
7682
  if (!controller?.signal.aborted) throw error;
7636
7683
  } finally {
7684
+ signal?.removeEventListener("abort", aborted);
7637
7685
  if (id !== void 0 && this.modernRequests.get(id) === controller)
7638
7686
  this.modernRequests.delete(id);
7639
7687
  }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.317",
11
+ "version": "0.0.319",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.317",
3
+ "version": "0.0.319",
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.317",
3
+ "version": "0.0.319",
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.317",
161
+ "toolcraft-schema": "0.0.319",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",