tiny-http-mcp-server 0.1.63 → 0.1.65

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.63",
21
+ "version": "0.1.65",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -94,6 +94,13 @@ accept the same `redirectUri`, `signal` and `timeoutMs` options. Cancellation,
94
94
  timeout and explicit close settle pending code waits and release listeners.
95
95
  Always close a successful standalone session in `finally`.
96
96
 
97
+ Native provider calls capture request option handles before host work. Unauthorized
98
+ handling also owns complete discovery metadata, presented grant values, rejected
99
+ request headers and the selected challenge error before reading persistence.
100
+ A browser callback cannot redirect a later code exchange by changing caller
101
+ metadata. Explicit authentication owns metadata when lazy discovery returns;
102
+ the selected discovery method retains its original receiver and live host state.
103
+
97
104
  Provider request inputs accept an optional `signal`. It reaches callback waits,
98
105
  registration, token requests and bounded token-body reads. Cancellation retains
99
106
  its original reason and does not retry authorization. Native provider calls
@@ -126,7 +133,11 @@ timeoutMs })`. Reset acquires the raw identity backend lock, so it can recover
126
133
  corrupt or undecryptable records without reading their old contents. It atomically
127
134
  retires the identity's grant and registrations and writes a marker that suppresses
128
135
  stale initial grants. The default lock wait is 30 seconds. Other names/profiles
129
- are untouched, and symlink paths are still refused.
136
+ are untouched, and symlink paths are still refused. Native reset, import and
137
+ transaction callbacks retain their original signal while locks or reconciliation
138
+ wait; replacing a caller handle cannot change subsequent cancellation checks.
139
+ Cancellation after a completed identity write prevents the transaction callback
140
+ from running and retains that already persisted identity.
130
141
 
131
142
  Configure `client.metadata.scope` to request a precise scope set; broader
132
143
  discovery metadata does not override it. Explicit scopes must match the cached
@@ -256,7 +267,11 @@ transient retry or restoration of the original grant. Gateway error pages do not
256
267
  Native persisted OAuth reads always reject corrupt encrypted documents and
257
268
  invalid stored JSON, with diagnostics that omit decrypted contents. They retain
258
269
  the existing record for explicit reset rather than interpreting corruption as
259
- an absent session and reviving an initial grant. Caller file-backend settings
270
+ an absent session and reviving an initial grant. Persisted access tokens that
271
+ cannot be sent as HTTP headers fail with diagnostics that omit token contents;
272
+ the original record remains available for explicit recovery. Token responses are
273
+ checked before activation or persistence. An unusable rotated access token keeps
274
+ refresh intent pending, preventing rotating-token replay. Caller file-backend settings
260
275
  cannot disable this policy.
261
276
 
262
277
  ## Environment Variables
@@ -73,6 +73,7 @@ export function createDefaultOAuthClientProvider(options) {
73
73
  let initialGrantConsumed = false;
74
74
  return {
75
75
  async authenticate(input) {
76
+ input = { ...input, ...(input.discover === undefined ? {} : { discover: input.discover.bind(input) }) };
76
77
  input.signal?.throwIfAborted();
77
78
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
78
79
  const resource = canonicalizeResourceIndicator(input.requestUrl);
@@ -84,7 +85,7 @@ export function createDefaultOAuthClientProvider(options) {
84
85
  return { ...initialGrant.tokens };
85
86
  if (input.discover === undefined)
86
87
  return;
87
- const discovery = await waitForOAuthOperation(input.discover(), input.signal);
88
+ const discovery = structuredClone(await waitForOAuthOperation(input.discover(), input.signal));
88
89
  input.signal?.throwIfAborted();
89
90
  assertRequestMatchesResource(resource, canonicalizeResourceIndicator(discovery.resource));
90
91
  session = await ensureAuthorizedSession(resource, discovery, input.fetch, true, false, input.signal);
@@ -93,6 +94,7 @@ export function createDefaultOAuthClientProvider(options) {
93
94
  return { ...session.tokens };
94
95
  },
95
96
  async authorizeRequest(input) {
97
+ input = { ...input };
96
98
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
97
99
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
98
100
  const session = await ensureAuthorizedSession(requestUrl, undefined, input.fetch, false, false, input.signal);
@@ -113,8 +115,16 @@ export function createDefaultOAuthClientProvider(options) {
113
115
  return { ...session.tokens };
114
116
  },
115
117
  async handleUnauthorized(input) {
118
+ input = { ...input };
116
119
  try {
117
120
  input.signal?.throwIfAborted();
121
+ const presented = input.presentedTokens == null ? input.presentedTokens : normalizeStoredTokens(input.presentedTokens);
122
+ if (input.presentedTokens != null && presented === undefined)
123
+ throw new Error("OAuth rejected-request provenance does not match its authorization header");
124
+ const challengeError = input.challenge?.params.error;
125
+ input = { ...input, discovery: structuredClone(input.discovery),
126
+ ...(input.requestHeaders === undefined ? {} : { requestHeaders: new Headers(input.requestHeaders) }),
127
+ ...(presented === undefined ? {} : { presentedTokens: presented }) };
118
128
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
119
129
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
120
130
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
@@ -126,16 +136,15 @@ export function createDefaultOAuthClientProvider(options) {
126
136
  if (input.presentedTokens !== undefined) {
127
137
  rejectedCurrentGrant = false;
128
138
  if (input.presentedTokens !== null) {
129
- const presented = normalizeStoredTokens(input.presentedTokens);
139
+ const presented = input.presentedTokens;
130
140
  const header = input.requestHeaders?.get("Authorization") ?? "";
131
141
  const separator = header.indexOf(" ");
132
- if (presented === undefined || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
142
+ if (header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
133
143
  throw new Error("OAuth rejected-request provenance does not match its authorization header");
134
144
  presentedTokens = presented;
135
145
  rejectedCurrentGrant = currentTokens !== undefined && sameTokenGrant(currentTokens, presented);
136
146
  }
137
147
  }
138
- const challengeError = input.challenge?.params.error;
139
148
  const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || (input.presentedTokens !== undefined && challengeError === undefined));
140
149
  const session = await ensureAuthorizedSession(resource, {
141
150
  ...input.discovery,
@@ -591,11 +600,16 @@ function normalizeLoadedSession(session) {
591
600
  if (client === null) {
592
601
  return { ...session, client: { clientId: "" }, tokens: undefined };
593
602
  }
594
- return {
595
- ...session,
596
- client,
597
- tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
598
- };
603
+ const tokens = normalizeStoredTokens(getOwnEntry(session, "tokens"));
604
+ if (tokens !== undefined) {
605
+ try {
606
+ new Headers({ Authorization: `Bearer ${tokens.accessToken}` });
607
+ }
608
+ catch {
609
+ throw new Error("Stored OAuth access token is not a valid HTTP header value");
610
+ }
611
+ }
612
+ return { ...session, client, tokens };
599
613
  }
600
614
  function normalizeImportedTokens(value, now) {
601
615
  if (!isObjectRecord(value))
@@ -51,6 +51,7 @@ export function createResourceBoundOAuthStores(options, namespace, identity) {
51
51
  }
52
52
  };
53
53
  async function replace(record, options) {
54
+ options = { ...options };
54
55
  options.signal?.throwIfAborted();
55
56
  const timeoutMs = options.timeoutMs ?? 30_000;
56
57
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647)
@@ -124,6 +125,7 @@ export function createResourceBoundOAuthStores(options, namespace, identity) {
124
125
  }
125
126
  result.sessionStore = {
126
127
  async withLock(resource, operation, options) {
128
+ options = { ...options };
127
129
  if (store.withLock === undefined)
128
130
  throw new Error("OAuth resource identity backend must support transaction locks");
129
131
  return store.withLock(async () => {
@@ -105,6 +105,12 @@ async function requestTokens(input) {
105
105
  throw new Error("OAuth token response missing access_token");
106
106
  }
107
107
  const normalizedAccessToken = accessToken.trim();
108
+ try {
109
+ new Headers({ Authorization: `Bearer ${normalizedAccessToken}` });
110
+ }
111
+ catch {
112
+ throw new Error("OAuth token response access_token is not a valid HTTP header value");
113
+ }
108
114
  const tokenType = normalizeBearerTokenType(getOwnEntry(payload, "token_type"));
109
115
  if (tokenType === null) {
110
116
  throw new Error("OAuth token response missing token_type=Bearer");
@@ -4793,6 +4793,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4793
4793
  }
4794
4794
  };
4795
4795
  async function replace(record2, options2) {
4796
+ options2 = { ...options2 };
4796
4797
  options2.signal?.throwIfAborted();
4797
4798
  const timeoutMs = options2.timeoutMs ?? 3e4;
4798
4799
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
@@ -4858,6 +4859,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4858
4859
  }
4859
4860
  result.sessionStore = {
4860
4861
  async withLock(resource, operation, options2) {
4862
+ options2 = { ...options2 };
4861
4863
  if (store.withLock === void 0)
4862
4864
  throw new Error("OAuth resource identity backend must support transaction locks");
4863
4865
  return store.withLock(async () => {
@@ -5114,6 +5116,11 @@ async function requestTokens(input) {
5114
5116
  throw new Error("OAuth token response missing access_token");
5115
5117
  }
5116
5118
  const normalizedAccessToken = accessToken.trim();
5119
+ try {
5120
+ new Headers({ Authorization: `Bearer ${normalizedAccessToken}` });
5121
+ } catch {
5122
+ throw new Error("OAuth token response access_token is not a valid HTTP header value");
5123
+ }
5117
5124
  const tokenType = normalizeBearerTokenType(getOwnEntry5(payload, "token_type"));
5118
5125
  if (tokenType === null) {
5119
5126
  throw new Error("OAuth token response missing token_type=Bearer");
@@ -5328,6 +5335,7 @@ function createDefaultOAuthClientProvider(options) {
5328
5335
  let initialGrantConsumed = false;
5329
5336
  return {
5330
5337
  async authenticate(input) {
5338
+ input = { ...input, ...input.discover === void 0 ? {} : { discover: input.discover.bind(input) } };
5331
5339
  input.signal?.throwIfAborted();
5332
5340
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
5333
5341
  const resource = canonicalizeResourceIndicator(input.requestUrl);
@@ -5338,7 +5346,7 @@ function createDefaultOAuthClientProvider(options) {
5338
5346
  return { ...initialGrant.tokens };
5339
5347
  if (input.discover === void 0)
5340
5348
  return;
5341
- const discovery = await waitForOAuthOperation(input.discover(), input.signal);
5349
+ const discovery = structuredClone(await waitForOAuthOperation(input.discover(), input.signal));
5342
5350
  input.signal?.throwIfAborted();
5343
5351
  assertRequestMatchesResource(resource, canonicalizeResourceIndicator(discovery.resource));
5344
5352
  session = await ensureAuthorizedSession(resource, discovery, input.fetch, true, false, input.signal);
@@ -5347,6 +5355,7 @@ function createDefaultOAuthClientProvider(options) {
5347
5355
  return { ...session.tokens };
5348
5356
  },
5349
5357
  async authorizeRequest(input) {
5358
+ input = { ...input };
5350
5359
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
5351
5360
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
5352
5361
  const session = await ensureAuthorizedSession(requestUrl, void 0, input.fetch, false, false, input.signal);
@@ -5363,8 +5372,19 @@ function createDefaultOAuthClientProvider(options) {
5363
5372
  return { ...session.tokens };
5364
5373
  },
5365
5374
  async handleUnauthorized(input) {
5375
+ input = { ...input };
5366
5376
  try {
5367
5377
  input.signal?.throwIfAborted();
5378
+ const presented = input.presentedTokens == null ? input.presentedTokens : normalizeStoredTokens(input.presentedTokens);
5379
+ if (input.presentedTokens != null && presented === void 0)
5380
+ throw new Error("OAuth rejected-request provenance does not match its authorization header");
5381
+ const challengeError = input.challenge?.params.error;
5382
+ input = {
5383
+ ...input,
5384
+ discovery: structuredClone(input.discovery),
5385
+ ...input.requestHeaders === void 0 ? {} : { requestHeaders: new Headers(input.requestHeaders) },
5386
+ ...presented === void 0 ? {} : { presentedTokens: presented }
5387
+ };
5368
5388
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
5369
5389
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
5370
5390
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
@@ -5376,16 +5396,15 @@ function createDefaultOAuthClientProvider(options) {
5376
5396
  if (input.presentedTokens !== void 0) {
5377
5397
  rejectedCurrentGrant = false;
5378
5398
  if (input.presentedTokens !== null) {
5379
- const presented = normalizeStoredTokens(input.presentedTokens);
5399
+ const presented2 = input.presentedTokens;
5380
5400
  const header = input.requestHeaders?.get("Authorization") ?? "";
5381
5401
  const separator = header.indexOf(" ");
5382
- if (presented === void 0 || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
5402
+ if (header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented2.accessToken)
5383
5403
  throw new Error("OAuth rejected-request provenance does not match its authorization header");
5384
- presentedTokens = presented;
5385
- rejectedCurrentGrant = currentTokens !== void 0 && sameTokenGrant(currentTokens, presented);
5404
+ presentedTokens = presented2;
5405
+ rejectedCurrentGrant = currentTokens !== void 0 && sameTokenGrant(currentTokens, presented2);
5386
5406
  }
5387
5407
  }
5388
- const challengeError = input.challenge?.params.error;
5389
5408
  const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || input.presentedTokens !== void 0 && challengeError === void 0);
5390
5409
  const session = await ensureAuthorizedSession(resource, {
5391
5410
  ...input.discovery,
@@ -5825,11 +5844,15 @@ function normalizeLoadedSession(session) {
5825
5844
  if (client === null) {
5826
5845
  return { ...session, client: { clientId: "" }, tokens: void 0 };
5827
5846
  }
5828
- return {
5829
- ...session,
5830
- client,
5831
- tokens: normalizeStoredTokens(getOwnEntry6(session, "tokens"))
5832
- };
5847
+ const tokens = normalizeStoredTokens(getOwnEntry6(session, "tokens"));
5848
+ if (tokens !== void 0) {
5849
+ try {
5850
+ new Headers({ Authorization: `Bearer ${tokens.accessToken}` });
5851
+ } catch {
5852
+ throw new Error("Stored OAuth access token is not a valid HTTP header value");
5853
+ }
5854
+ }
5855
+ return { ...session, client, tokens };
5833
5856
  }
5834
5857
  function normalizeImportedTokens(value, now) {
5835
5858
  if (!isObjectRecord3(value))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",