tiny-http-mcp-server 0.1.27 → 0.1.28

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.27",
21
+ "version": "0.1.28",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -118,9 +118,17 @@ acquisition, while token and browser operations retain their own deadlines.
118
118
  The native `auth-store` session adapter implements this hook for both encrypted
119
119
  files and Keychain identities, including across independent processes. Locks
120
120
  cover the complete read, refresh/authorization and persisted winner. Dead-owner
121
- claims are recovered without stealing a live transaction. A process crash or
122
- cancellation after refresh redemption but before persistence is still an
123
- uncertain token outcome; recovery for that case is under development.
121
+ claims are recovered without stealing a live transaction.
122
+
123
+ Before sending a refresh request, the provider persists a tokenless session with
124
+ `refreshState: "pending"`, retaining the original client and discovery binding.
125
+ A successful response replaces it with the rotated grant. A crash, cancellation,
126
+ network disconnect or incomplete response leaves the marker, so another process
127
+ cannot replay a possibly consumed refresh token or revive the initial import.
128
+ Such a session requires fresh authorization. Interactive unauthorized handling
129
+ can recover it; headless requests fail with an explicit unknown-outcome error.
130
+ Only complete OAuth error responses establish a rejected request and allow a
131
+ transient retry or restoration of the original grant. Gateway error pages do not.
124
132
 
125
133
  ## Environment Variables
126
134
 
@@ -125,6 +125,8 @@ function isStoredOAuthSession(value) {
125
125
  isNonBlankOwnString(value, "authorizationServer") &&
126
126
  isStoredOAuthClient(getOwnEntry(value, "client")) &&
127
127
  isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
128
+ (getOwnEntry(value, "refreshState") === undefined ||
129
+ (getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
128
130
  isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
129
131
  }
130
132
  function isStoredOAuthClient(value) {
@@ -140,6 +140,11 @@ export function createDefaultOAuthClientProvider(options) {
140
140
  if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
141
141
  forceRefresh = false;
142
142
  const sessionDiscovery = resolveDiscovery(discovery, session);
143
+ if (session?.refreshState === "pending") {
144
+ if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
145
+ throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
146
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
147
+ }
143
148
  if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
144
149
  return session;
145
150
  }
@@ -169,6 +174,9 @@ export function createDefaultOAuthClientProvider(options) {
169
174
  if (session.tokens?.refreshToken === undefined) {
170
175
  return session;
171
176
  }
177
+ const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
178
+ await saveSession(resource, pendingSession);
179
+ signal?.throwIfAborted();
172
180
  let refreshAttempted = false;
173
181
  let refreshedTokens;
174
182
  while (true) {
@@ -186,7 +194,11 @@ export function createDefaultOAuthClientProvider(options) {
186
194
  }
187
195
  catch (error) {
188
196
  signal?.throwIfAborted();
189
- if (error instanceof OAuthError && error.error === "invalid_grant") {
197
+ // Network errors, lost/malformed bodies and gateway failures cannot
198
+ // establish whether a rotating refresh token was already consumed.
199
+ if (!(error instanceof OAuthError) || !error.outcomeKnown)
200
+ throw error;
201
+ if (error.error === "invalid_grant") {
190
202
  const clearedSession = clearSessionTokens(session);
191
203
  await saveSession(resource, clearedSession);
192
204
  return clearedSession;
@@ -200,6 +212,7 @@ export function createDefaultOAuthClientProvider(options) {
200
212
  refreshAttempted = true;
201
213
  continue;
202
214
  }
215
+ await saveSession(resource, session);
203
216
  throw error;
204
217
  }
205
218
  }
@@ -457,6 +470,7 @@ function sameTokenGrant(left, right) {
457
470
  function clearSessionTokens(session) {
458
471
  const nextSession = { ...session };
459
472
  delete nextSession.tokens;
473
+ delete nextSession.refreshState;
460
474
  return nextSession;
461
475
  }
462
476
  function hasCachedAccessToken(session) {
@@ -466,6 +480,9 @@ function normalizeLoadedSession(session) {
466
480
  if (session === null) {
467
481
  return null;
468
482
  }
483
+ const refreshState = getOwnEntry(session, "refreshState");
484
+ if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
485
+ throw new Error("Stored OAuth refresh state is invalid");
469
486
  const client = normalizeStoredClient(getOwnEntry(session, "client"));
470
487
  if (client === null) {
471
488
  return { ...session, client: { clientId: "" }, tokens: undefined };
@@ -13,7 +13,9 @@ export declare class OAuthError extends Error {
13
13
  readonly status: number;
14
14
  readonly retryable: boolean;
15
15
  readonly terminal: boolean;
16
- constructor(shape: OAuthErrorShape, status: number);
16
+ /** True only when a complete OAuth error response establishes rejection. */
17
+ readonly outcomeKnown: boolean;
18
+ constructor(shape: OAuthErrorShape, status: number, outcomeKnown?: boolean);
17
19
  }
18
20
  export declare function isRetryableOAuthError(error: unknown): error is OAuthError;
19
21
  export declare function exchangeAuthorizationCode(input: {
@@ -11,7 +11,9 @@ export class OAuthError extends Error {
11
11
  status;
12
12
  retryable;
13
13
  terminal;
14
- constructor(shape, status) {
14
+ /** True only when a complete OAuth error response establishes rejection. */
15
+ outcomeKnown;
16
+ constructor(shape, status, outcomeKnown = true) {
15
17
  super(shape.error_description ?? shape.error);
16
18
  this.name = "OAuthError";
17
19
  this.error = shape.error;
@@ -22,6 +24,7 @@ export class OAuthError extends Error {
22
24
  this.status = status;
23
25
  this.retryable = isRetryableOAuthError(this);
24
26
  this.terminal = !this.retryable;
27
+ this.outcomeKnown = outcomeKnown;
25
28
  }
26
29
  }
27
30
  export function isRetryableOAuthError(error) {
@@ -147,7 +150,8 @@ export async function readOAuthJsonObjectResponse(response, signal) {
147
150
  }
148
151
  const record = payload;
149
152
  if (!response.ok) {
150
- throw new OAuthError(readOAuthError(record, fallbackError.error), response.status);
153
+ const error = getOwnEntry(record, "error");
154
+ throw new OAuthError(readOAuthError(record, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
151
155
  }
152
156
  return record;
153
157
  }
@@ -166,7 +170,7 @@ function getOwnEntry(record, key) {
166
170
  }
167
171
  function createFallbackOAuthError(status) {
168
172
  const error = status === 503 ? "temporarily_unavailable" : "server_error";
169
- return new OAuthError({ error }, status);
173
+ return new OAuthError({ error }, status, false);
170
174
  }
171
175
  function normalizeBearerTokenType(value) {
172
176
  if (typeof value !== "string") {
@@ -78,6 +78,8 @@ export interface StoredOAuthSession {
78
78
  clientSecret?: string;
79
79
  };
80
80
  tokens?: StoredOAuthTokens;
81
+ /** A refresh was begun; its winning response may not have been persisted. */
82
+ refreshState?: "pending";
81
83
  discovery: {
82
84
  resourceMetadataUrl: string;
83
85
  resourceMetadata: Record<string, unknown>;
@@ -179,6 +179,8 @@ interface StoredOAuthSession {
179
179
  clientSecret?: string;
180
180
  };
181
181
  tokens?: StoredOAuthTokens;
182
+ /** A refresh was begun; its winning response may not have been persisted. */
183
+ refreshState?: "pending";
182
184
  discovery: {
183
185
  resourceMetadataUrl: string;
184
186
  resourceMetadata: Record<string, unknown>;
@@ -4176,7 +4176,7 @@ function isStoredOAuthSession(value) {
4176
4176
  if (!isObjectRecord(value)) {
4177
4177
  return false;
4178
4178
  }
4179
- return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
4179
+ return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && (getOwnEntry3(value, "refreshState") === void 0 || getOwnEntry3(value, "refreshState") === "pending" && getOwnEntry3(value, "tokens") === void 0) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
4180
4180
  }
4181
4181
  function isStoredOAuthClient(value) {
4182
4182
  if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
@@ -4605,7 +4605,9 @@ var OAuthError = class extends Error {
4605
4605
  status;
4606
4606
  retryable;
4607
4607
  terminal;
4608
- constructor(shape, status) {
4608
+ /** True only when a complete OAuth error response establishes rejection. */
4609
+ outcomeKnown;
4610
+ constructor(shape, status, outcomeKnown = true) {
4609
4611
  super(shape.error_description ?? shape.error);
4610
4612
  this.name = "OAuthError";
4611
4613
  this.error = shape.error;
@@ -4616,6 +4618,7 @@ var OAuthError = class extends Error {
4616
4618
  this.status = status;
4617
4619
  this.retryable = isRetryableOAuthError(this);
4618
4620
  this.terminal = !this.retryable;
4621
+ this.outcomeKnown = outcomeKnown;
4619
4622
  }
4620
4623
  };
4621
4624
  function isRetryableOAuthError(error) {
@@ -4730,7 +4733,8 @@ async function readOAuthJsonObjectResponse(response, signal) {
4730
4733
  }
4731
4734
  const record2 = payload;
4732
4735
  if (!response.ok) {
4733
- throw new OAuthError(readOAuthError(record2, fallbackError.error), response.status);
4736
+ const error = getOwnEntry5(record2, "error");
4737
+ throw new OAuthError(readOAuthError(record2, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
4734
4738
  }
4735
4739
  return record2;
4736
4740
  }
@@ -4749,7 +4753,7 @@ function getOwnEntry5(record2, key2) {
4749
4753
  }
4750
4754
  function createFallbackOAuthError(status) {
4751
4755
  const error = status === 503 ? "temporarily_unavailable" : "server_error";
4752
- return new OAuthError({ error }, status);
4756
+ return new OAuthError({ error }, status, false);
4753
4757
  }
4754
4758
  function normalizeBearerTokenType(value) {
4755
4759
  if (typeof value !== "string") {
@@ -4935,6 +4939,11 @@ function createDefaultOAuthClientProvider(options) {
4935
4939
  if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
4936
4940
  forceRefresh = false;
4937
4941
  const sessionDiscovery = resolveDiscovery(discovery, session);
4942
+ if (session?.refreshState === "pending") {
4943
+ if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
4944
+ throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
4945
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch2, signal);
4946
+ }
4938
4947
  if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
4939
4948
  return session;
4940
4949
  }
@@ -4962,6 +4971,9 @@ function createDefaultOAuthClientProvider(options) {
4962
4971
  if (session.tokens?.refreshToken === void 0) {
4963
4972
  return session;
4964
4973
  }
4974
+ const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
4975
+ await saveSession(resource, pendingSession);
4976
+ signal?.throwIfAborted();
4965
4977
  let refreshAttempted = false;
4966
4978
  let refreshedTokens;
4967
4979
  while (true) {
@@ -4979,7 +4991,9 @@ function createDefaultOAuthClientProvider(options) {
4979
4991
  break;
4980
4992
  } catch (error) {
4981
4993
  signal?.throwIfAborted();
4982
- if (error instanceof OAuthError && error.error === "invalid_grant") {
4994
+ if (!(error instanceof OAuthError) || !error.outcomeKnown)
4995
+ throw error;
4996
+ if (error.error === "invalid_grant") {
4983
4997
  const clearedSession = clearSessionTokens(session);
4984
4998
  await saveSession(resource, clearedSession);
4985
4999
  return clearedSession;
@@ -4993,6 +5007,7 @@ function createDefaultOAuthClientProvider(options) {
4993
5007
  refreshAttempted = true;
4994
5008
  continue;
4995
5009
  }
5010
+ await saveSession(resource, session);
4996
5011
  throw error;
4997
5012
  }
4998
5013
  }
@@ -5239,6 +5254,7 @@ function sameTokenGrant(left, right) {
5239
5254
  function clearSessionTokens(session) {
5240
5255
  const nextSession = { ...session };
5241
5256
  delete nextSession.tokens;
5257
+ delete nextSession.refreshState;
5242
5258
  return nextSession;
5243
5259
  }
5244
5260
  function hasCachedAccessToken(session) {
@@ -5248,6 +5264,9 @@ function normalizeLoadedSession(session) {
5248
5264
  if (session === null) {
5249
5265
  return null;
5250
5266
  }
5267
+ const refreshState = getOwnEntry6(session, "refreshState");
5268
+ if (refreshState !== void 0 && (refreshState !== "pending" || getOwnEntry6(session, "tokens") !== void 0))
5269
+ throw new Error("Stored OAuth refresh state is invalid");
5251
5270
  const client = normalizeStoredClient(getOwnEntry6(session, "client"));
5252
5271
  if (client === null) {
5253
5272
  return { ...session, client: { clientId: "" }, tokens: void 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",