tiny-http-mcp-server 0.1.60 → 0.1.61

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.60",
21
+ "version": "0.1.61",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -95,7 +95,11 @@ Always close a successful standalone session in `finally`.
95
95
 
96
96
  Provider request inputs accept an optional `signal`. It reaches callback waits,
97
97
  registration, token requests and bounded token-body reads. Cancellation retains
98
- its original reason and does not retry authorization. Custom providers should
98
+ its original reason and does not retry authorization. Native provider calls
99
+ also settle cancellation while host persistence or lazy discovery callbacks are
100
+ waiting. An unfinished transaction keeps its lease until its host work finishes;
101
+ following callers must wait or reach their own lock-acquisition limit. This
102
+ prevents overlap with a pending refresh-intent write. Custom providers should
99
103
  observe the supplied signal and pass it to any work they start.
100
104
 
101
105
  `authorizeRequest` may return an owned token snapshot for the request it
@@ -0,0 +1,2 @@
1
+ /** Settle the caller on cancellation without abandoning observation of host completion. */
2
+ export declare function waitForOAuthOperation<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T>;
@@ -0,0 +1,18 @@
1
+ /** Settle the caller on cancellation without abandoning observation of host completion. */
2
+ export async function waitForOAuthOperation(operation, signal) {
3
+ if (signal === undefined)
4
+ return operation;
5
+ let abort;
6
+ try {
7
+ return await new Promise((resolve, reject) => {
8
+ abort = () => reject(signal.reason);
9
+ signal.addEventListener("abort", abort, { once: true });
10
+ operation.then(resolve, reject);
11
+ if (signal.aborted)
12
+ abort();
13
+ });
14
+ }
15
+ finally {
16
+ signal.removeEventListener("abort", abort);
17
+ }
18
+ }
@@ -12,6 +12,7 @@ import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
12
12
  import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse } from "./token-endpoint.js";
13
13
  import { canonicalizeResourceIndicator } from "../resource-indicator.js";
14
14
  import { withOAuthSessionTransaction } from "./session-transaction.js";
15
+ import { waitForOAuthOperation } from "./cancellable-operation.js";
15
16
  const MAX_JS_DATE_MS = 8_640_000_000_000_000;
16
17
  export function createOAuthClientProvider(options) {
17
18
  if (isProviderOptions(options)) {
@@ -83,7 +84,7 @@ export function createDefaultOAuthClientProvider(options) {
83
84
  return { ...initialGrant.tokens };
84
85
  if (input.discover === undefined)
85
86
  return;
86
- const discovery = await input.discover();
87
+ const discovery = await waitForOAuthOperation(input.discover(), input.signal);
87
88
  input.signal?.throwIfAborted();
88
89
  assertRequestMatchesResource(resource, canonicalizeResourceIndicator(discovery.resource));
89
90
  session = await ensureAuthorizedSession(resource, discovery, input.fetch, true, false, input.signal);
@@ -113,11 +114,12 @@ export function createDefaultOAuthClientProvider(options) {
113
114
  },
114
115
  async handleUnauthorized(input) {
115
116
  try {
117
+ input.signal?.throwIfAborted();
116
118
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
117
119
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
118
120
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
119
121
  assertRequestMatchesResource(requestUrl, resource);
120
- const cached = await loadSession(resource);
122
+ const cached = await waitForOAuthOperation(loadSession(resource), input.signal);
121
123
  const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : undefined);
122
124
  let rejectedCurrentGrant = hasCachedAccessToken(cached) || (!initialGrantConsumed && initialGrant?.resource === resource);
123
125
  let presentedTokens = input.presentedTokens;
@@ -1,3 +1,4 @@
1
+ import { waitForOAuthOperation } from "./cancellable-operation.js";
1
2
  const queues = new WeakMap();
2
3
  /** Serialize the complete read/redeem/write operation, with independent cancellation for waiters. */
3
4
  export async function withOAuthSessionTransaction(store, resource, operation, options = {}) {
@@ -13,6 +14,7 @@ export async function withOAuthSessionTransaction(store, resource, operation, op
13
14
  const current = new Promise(resolve => { release = resolve; });
14
15
  const tail = previous.then(() => current);
15
16
  pending.set(resource, tail);
17
+ let running;
16
18
  try {
17
19
  let timer;
18
20
  let rejectWait;
@@ -30,12 +32,17 @@ export async function withOAuthSessionTransaction(store, resource, operation, op
30
32
  options.signal?.removeEventListener("abort", abort);
31
33
  }
32
34
  options.signal?.throwIfAborted();
33
- return store.withLock === undefined ? await operation() : await store.withLock(resource, operation, {
35
+ running = (async () => store.withLock === undefined ? operation() : store.withLock(resource, operation, {
34
36
  signal: options.signal, timeoutMs: Math.max(0, timeoutMs - (performance.now() - started))
35
- });
37
+ }))();
38
+ return await waitForOAuthOperation(running, options.signal);
36
39
  }
37
40
  finally {
38
- release();
41
+ // Cancellation settles the caller, but unfinished host work still owns the lease.
42
+ if (running === undefined)
43
+ release();
44
+ else
45
+ void running.then(release, release);
39
46
  void tail.then(() => { if (pending.get(resource) === tail)
40
47
  pending.delete(resource); });
41
48
  }
@@ -5194,6 +5194,24 @@ function normalizeBearerTokenType(value) {
5194
5194
  return value.toLowerCase() === "bearer" ? "Bearer" : null;
5195
5195
  }
5196
5196
 
5197
+ // ../mcp-oauth/dist/client/cancellable-operation.js
5198
+ async function waitForOAuthOperation(operation, signal) {
5199
+ if (signal === void 0)
5200
+ return operation;
5201
+ let abort;
5202
+ try {
5203
+ return await new Promise((resolve, reject) => {
5204
+ abort = () => reject(signal.reason);
5205
+ signal.addEventListener("abort", abort, { once: true });
5206
+ operation.then(resolve, reject);
5207
+ if (signal.aborted)
5208
+ abort();
5209
+ });
5210
+ } finally {
5211
+ signal.removeEventListener("abort", abort);
5212
+ }
5213
+ }
5214
+
5197
5215
  // ../mcp-oauth/dist/client/session-transaction.js
5198
5216
  var queues = /* @__PURE__ */ new WeakMap();
5199
5217
  async function withOAuthSessionTransaction(store, resource, operation, options = {}) {
@@ -5211,6 +5229,7 @@ async function withOAuthSessionTransaction(store, resource, operation, options =
5211
5229
  });
5212
5230
  const tail = previous.then(() => current);
5213
5231
  pending.set(resource, tail);
5232
+ let running;
5214
5233
  try {
5215
5234
  let timer;
5216
5235
  let rejectWait;
@@ -5230,12 +5249,16 @@ async function withOAuthSessionTransaction(store, resource, operation, options =
5230
5249
  options.signal?.removeEventListener("abort", abort);
5231
5250
  }
5232
5251
  options.signal?.throwIfAborted();
5233
- return store.withLock === void 0 ? await operation() : await store.withLock(resource, operation, {
5252
+ running = (async () => store.withLock === void 0 ? operation() : store.withLock(resource, operation, {
5234
5253
  signal: options.signal,
5235
5254
  timeoutMs: Math.max(0, timeoutMs - (performance.now() - started))
5236
- });
5255
+ }))();
5256
+ return await waitForOAuthOperation(running, options.signal);
5237
5257
  } finally {
5238
- release();
5258
+ if (running === void 0)
5259
+ release();
5260
+ else
5261
+ void running.then(release, release);
5239
5262
  void tail.then(() => {
5240
5263
  if (pending.get(resource) === tail)
5241
5264
  pending.delete(resource);
@@ -5312,7 +5335,7 @@ function createDefaultOAuthClientProvider(options) {
5312
5335
  return { ...initialGrant.tokens };
5313
5336
  if (input.discover === void 0)
5314
5337
  return;
5315
- const discovery = await input.discover();
5338
+ const discovery = await waitForOAuthOperation(input.discover(), input.signal);
5316
5339
  input.signal?.throwIfAborted();
5317
5340
  assertRequestMatchesResource(resource, canonicalizeResourceIndicator(discovery.resource));
5318
5341
  session = await ensureAuthorizedSession(resource, discovery, input.fetch, true, false, input.signal);
@@ -5338,11 +5361,12 @@ function createDefaultOAuthClientProvider(options) {
5338
5361
  },
5339
5362
  async handleUnauthorized(input) {
5340
5363
  try {
5364
+ input.signal?.throwIfAborted();
5341
5365
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
5342
5366
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
5343
5367
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
5344
5368
  assertRequestMatchesResource(requestUrl, resource);
5345
- const cached = await loadSession(resource);
5369
+ const cached = await waitForOAuthOperation(loadSession(resource), input.signal);
5346
5370
  const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : void 0);
5347
5371
  let rejectedCurrentGrant = hasCachedAccessToken(cached) || !initialGrantConsumed && initialGrant?.resource === resource;
5348
5372
  let presentedTokens = input.presentedTokens;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",