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
@@ -1,4 +1,6 @@
1
+ import { normalizeStoredOAuthClient, parseOAuthClientRegistration, registrationMatchesRedirect } from "./client-registration.js";
1
2
  import { normalizeOAuthScope } from "./scope.js";
3
+ import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
2
4
  import { isIP } from "node:net";
3
5
  import { fetchMcpResponse } from "../http-fetch.js";
4
6
  import { URL } from "node:url";
@@ -21,6 +23,9 @@ export function createDefaultOAuthClientProvider(options) {
21
23
  assertPersistenceNamespace(options.persistenceNamespace);
22
24
  const clientMetadata = getClientMetadata(options.client);
23
25
  const requestedScope = clientMetadata?.scope;
26
+ const configuredClient = normalizeConfiguredClient(options.client);
27
+ const requestedTokenMethod = normalizeOAuthTokenEndpointAuthMethod(options.client.tokenEndpointAuthMethod);
28
+ const configuredTokenMethod = requestedTokenMethod ?? configuredClient?.tokenEndpointAuthMethod;
24
29
  const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
25
30
  const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
26
31
  const now = options.now ?? Date.now;
@@ -38,8 +43,8 @@ export function createDefaultOAuthClientProvider(options) {
38
43
  }
39
44
  const initialGrant = options.initialGrant === undefined ? undefined : {
40
45
  resource: canonicalizeResourceIndicator(options.initialGrant.resource),
41
- tokens: normalizeStoredTokens(options.initialGrant.tokens),
42
- client: normalizeConfiguredClient(options.client)
46
+ tokens: normalizeImportedTokens(options.initialGrant.tokens, now),
47
+ client: configuredClient
43
48
  };
44
49
  if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
45
50
  throw new Error("OAuth initial grant requires valid tokens and the original client ID");
@@ -122,6 +127,10 @@ export function createDefaultOAuthClientProvider(options) {
122
127
  const canonicalResource = canonicalizeResourceIndicator(resource);
123
128
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
124
129
  let session = await loadSession(canonicalResource);
130
+ if (session !== null)
131
+ assertRegistrationIssuer(session.client, session.authorizationServer);
132
+ if (configuredClient !== null && discovery !== undefined)
133
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
125
134
  if (session !== null && initialGrant?.resource === canonicalResource)
126
135
  initialGrantConsumed = true;
127
136
  signal?.throwIfAborted();
@@ -147,13 +156,16 @@ export function createDefaultOAuthClientProvider(options) {
147
156
  if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
148
157
  forceRefresh = false;
149
158
  const sessionDiscovery = resolveDiscovery(discovery, session);
150
- if ((options.client.mode === "static" || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
151
- const configured = normalizeConfiguredClient(options.client);
159
+ if ((options.client.mode === "static" || configuredClient?.registration !== undefined || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
160
+ const configured = configuredClient;
152
161
  if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
153
162
  throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
154
163
  }
155
164
  if (requestedScope !== undefined && session?.tokens !== undefined && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
156
165
  throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
166
+ if (configuredTokenMethod !== undefined && session !== null && (session.tokens !== undefined || session.refreshState === "pending") &&
167
+ (session.client.tokenEndpointAuthMethod ?? (session.client.clientSecret === undefined ? "none" : "client_secret_post")) !== configuredTokenMethod)
168
+ throw new Error("Stored session does not match the requested OAuth token endpoint authentication; select separate persistence or reset it");
157
169
  if (session?.refreshState === "pending") {
158
170
  if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
159
171
  throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
@@ -165,6 +177,11 @@ export function createDefaultOAuthClientProvider(options) {
165
177
  if (session?.tokens?.refreshToken !== undefined &&
166
178
  sessionDiscovery !== undefined &&
167
179
  (forceRefresh || isExpired(session.tokens, now))) {
180
+ if (hasExpiredClientSecret(session.client, now)) {
181
+ if (!allowInteractive || options.allowInteractive === false || options.client.mode === "static" || configuredClient?.registration !== undefined)
182
+ throw new Error("OAuth client secret has expired; authorize again or update the imported registration");
183
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
184
+ }
168
185
  session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch, signal);
169
186
  if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
170
187
  return session;
@@ -188,6 +205,7 @@ export function createDefaultOAuthClientProvider(options) {
188
205
  if (session.tokens?.refreshToken === undefined) {
189
206
  return session;
190
207
  }
208
+ assertTokenEndpointAuthentication(session.client, discovery.authorizationServerMetadata);
191
209
  const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
192
210
  await saveSession(resource, pendingSession);
193
211
  signal?.throwIfAborted();
@@ -199,6 +217,7 @@ export function createDefaultOAuthClientProvider(options) {
199
217
  tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
200
218
  clientId: session.client.clientId,
201
219
  clientSecret: session.client.clientSecret,
220
+ tokenEndpointAuthMethod: session.client.tokenEndpointAuthMethod,
202
221
  refreshToken: session.tokens.refreshToken,
203
222
  resource,
204
223
  fetch, signal,
@@ -264,6 +283,7 @@ export function createDefaultOAuthClientProvider(options) {
264
283
  let resolvedClient = null;
265
284
  try {
266
285
  resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch, signal);
286
+ assertTokenEndpointAuthentication(resolvedClient.client, discovery.authorizationServerMetadata);
267
287
  const sessionWithoutTokens = {
268
288
  resource,
269
289
  authorizationServer: discovery.authorizationServer,
@@ -287,6 +307,7 @@ export function createDefaultOAuthClientProvider(options) {
287
307
  tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
288
308
  clientId: resolvedClient.client.clientId,
289
309
  clientSecret: resolvedClient.client.clientSecret,
310
+ tokenEndpointAuthMethod: resolvedClient.client.tokenEndpointAuthMethod,
290
311
  code,
291
312
  codeVerifier: verifier,
292
313
  redirectUri: loopback.redirectUri,
@@ -327,11 +348,13 @@ export function createDefaultOAuthClientProvider(options) {
327
348
  }
328
349
  async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
329
350
  parentSignal?.throwIfAborted();
330
- const configuredClient = normalizeConfiguredClient(options.client);
331
- if (options.client.mode === "static") {
351
+ if (options.client.mode === "static" || configuredClient?.registration !== undefined) {
332
352
  if (configuredClient === null) {
333
353
  throw new Error("OAuth client_id must not be blank");
334
354
  }
355
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
356
+ if (hasExpiredClientSecret(configuredClient, now))
357
+ throw new Error("OAuth client secret has expired; update the imported registration");
335
358
  return {
336
359
  kind: "static",
337
360
  fromStoredRegistration: false,
@@ -346,7 +369,14 @@ export function createDefaultOAuthClientProvider(options) {
346
369
  client: configuredClient
347
370
  };
348
371
  }
349
- const storedClient = await loadRegisteredClient(discovery.authorizationServer);
372
+ let storedClient = await loadRegisteredClient(discovery.authorizationServer);
373
+ if (storedClient !== null) {
374
+ assertRegistrationIssuer(storedClient, discovery.authorizationServer);
375
+ if (hasExpiredClientSecret(storedClient, now) || !registrationMatchesRedirect(storedClient, redirectUri)) {
376
+ await clearRegisteredClient(discovery.authorizationServer);
377
+ storedClient = null;
378
+ }
379
+ }
350
380
  if (storedClient !== null) {
351
381
  return {
352
382
  kind: "dynamic",
@@ -355,7 +385,7 @@ export function createDefaultOAuthClientProvider(options) {
355
385
  };
356
386
  }
357
387
  if (registrationEndpoint === undefined) {
358
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
388
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
359
389
  return {
360
390
  kind: "dynamic",
361
391
  fromStoredRegistration: true,
@@ -364,7 +394,7 @@ export function createDefaultOAuthClientProvider(options) {
364
394
  }
365
395
  throw new Error("Authorization server metadata is missing registration_endpoint");
366
396
  }
367
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
397
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
368
398
  const isConfiguredStaticFallback = configuredClient !== null &&
369
399
  existingSession.client.clientId === configuredClient.clientId &&
370
400
  existingSession.client.clientSecret === configuredClient.clientSecret;
@@ -377,7 +407,12 @@ export function createDefaultOAuthClientProvider(options) {
377
407
  };
378
408
  }
379
409
  }
380
- const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
410
+ const supported = getSupportedTokenAuthMethods(discovery.authorizationServerMetadata);
411
+ const registrationMethod = requestedTokenMethod ?? (supported === undefined ? "none" :
412
+ ["none", "client_secret_basic", "client_secret_post"].find(method => supported.includes(method)));
413
+ if (registrationMethod === undefined || (supported !== undefined && !supported.includes(registrationMethod)))
414
+ throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
415
+ const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri, registrationMethod);
381
416
  const deadline = AbortSignal.timeout(30_000);
382
417
  const signal = parentSignal === undefined ? deadline : AbortSignal.any([parentSignal, deadline]);
383
418
  const response = await fetchMcpResponse(fetch, registrationEndpoint, {
@@ -389,29 +424,34 @@ export function createDefaultOAuthClientProvider(options) {
389
424
  signal
390
425
  });
391
426
  const payload = await readOAuthJsonObjectResponse(response, signal);
392
- const clientId = getOwnString(payload, "client_id");
393
- if (clientId === undefined || clientId.trim().length === 0) {
394
- throw new Error("OAuth client registration response missing client_id");
395
- }
396
- const clientSecret = getOwnString(payload, "client_secret");
427
+ const registration = parseOAuthClientRegistration(payload);
428
+ const registeredSecret = getOwnString(registration, "client_secret");
429
+ const responseMethod = normalizeOAuthTokenEndpointAuthMethod(getOwnEntry(registration, "token_endpoint_auth_method")) ??
430
+ requestedTokenMethod ?? (supported === undefined ? undefined : normalizeOAuthTokenEndpointAuthMethod(registrationMethod));
397
431
  const registeredClient = {
398
- clientId: clientId.trim(),
399
- clientSecret: clientSecret !== undefined && clientSecret.trim().length > 0
400
- ? clientSecret.trim()
401
- : undefined
432
+ clientId: registration.client_id.trim(),
433
+ ...(registeredSecret === undefined ? {} : { clientSecret: registeredSecret.trim() }),
434
+ ...(responseMethod === undefined ? {} : { tokenEndpointAuthMethod: responseMethod }),
435
+ registration
402
436
  };
403
- await saveRegisteredClient(discovery.authorizationServer, registeredClient);
437
+ assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
438
+ if (hasExpiredClientSecret(registeredClient, now))
439
+ throw new Error("OAuth client secret has expired in the registration response");
440
+ if (!registrationMatchesRedirect(registeredClient, redirectUri, true))
441
+ throw new Error("OAuth registration response does not match the requested redirect URI");
442
+ const clientWithRedirect = { ...registeredClient, requestedRedirectUri: redirectUri };
443
+ await saveRegisteredClient(discovery.authorizationServer, clientWithRedirect);
404
444
  return {
405
445
  kind: "dynamic",
406
446
  fromStoredRegistration: false,
407
- client: registeredClient
447
+ client: clientWithRedirect
408
448
  };
409
449
  }
410
450
  async function loadSession(resource) {
411
451
  return normalizeLoadedSession(await sessionStore.load(resource));
412
452
  }
413
453
  async function saveSession(resource, session) {
414
- await sessionStore.save(resource, session);
454
+ await sessionStore.save(resource, structuredClone(session));
415
455
  }
416
456
  async function clearSession(resource) {
417
457
  await sessionStore.clear(resource);
@@ -424,7 +464,7 @@ export function createDefaultOAuthClientProvider(options) {
424
464
  return null;
425
465
  }
426
466
  const client = await clientStore.load(issuer);
427
- const normalizedClient = client === null ? null : normalizeStoredClient(client);
467
+ const normalizedClient = client === null ? null : normalizeStoredOAuthClient(client);
428
468
  if (client !== null && normalizedClient === null) {
429
469
  await clientStore.clear(issuer);
430
470
  return null;
@@ -503,7 +543,7 @@ function normalizeLoadedSession(session) {
503
543
  const refreshState = getOwnEntry(session, "refreshState");
504
544
  if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
505
545
  throw new Error("Stored OAuth refresh state is invalid");
506
- const client = normalizeStoredClient(getOwnEntry(session, "client"));
546
+ const client = normalizeStoredOAuthClient(getOwnEntry(session, "client"));
507
547
  if (client === null) {
508
548
  return { ...session, client: { clientId: "" }, tokens: undefined };
509
549
  }
@@ -513,24 +553,20 @@ function normalizeLoadedSession(session) {
513
553
  tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
514
554
  };
515
555
  }
516
- function normalizeStoredClient(value) {
517
- if (!isObjectRecord(value)) {
518
- return null;
519
- }
520
- const clientId = getOwnString(value, "clientId");
521
- if (clientId === undefined || clientId.trim().length === 0) {
522
- return null;
523
- }
524
- const normalizedClientId = clientId.trim();
525
- const clientSecret = getOwnEntry(value, "clientSecret");
526
- if (clientSecret === undefined) {
527
- return { clientId: normalizedClientId };
528
- }
529
- if (typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
530
- return null;
531
- }
532
- const normalizedClientSecret = clientSecret.trim();
533
- return { clientId: normalizedClientId, clientSecret: normalizedClientSecret };
556
+ function normalizeImportedTokens(value, now) {
557
+ if (!isObjectRecord(value))
558
+ return undefined;
559
+ const absolute = getOwnEntry(value, "expiresAt");
560
+ const lifetime = getOwnEntry(value, "expiresIn");
561
+ const issuedAt = getOwnEntry(value, "issuedAt");
562
+ if (lifetime !== undefined && (typeof lifetime !== "number" || !Number.isSafeInteger(lifetime) || lifetime < 0))
563
+ throw new Error("OAuth initial grant has invalid relative expiry");
564
+ if (issuedAt !== undefined && (typeof issuedAt !== "number" || !Number.isSafeInteger(issuedAt) ||
565
+ Math.abs(issuedAt) > MAX_JS_DATE_MS))
566
+ throw new Error("OAuth initial grant has invalid issuance time");
567
+ const expiresAt = absolute !== undefined && absolute !== null ? absolute :
568
+ lifetime === undefined ? null : (issuedAt === undefined ? now() : issuedAt) + lifetime * 1000;
569
+ return normalizeStoredTokens({ ...value, expiresAt });
534
570
  }
535
571
  function normalizeStoredTokens(value) {
536
572
  if (value === undefined || !isObjectRecord(value)) {
@@ -581,12 +617,12 @@ function getClientMetadata(client) {
581
617
  };
582
618
  }
583
619
  function normalizeConfiguredClient(client) {
584
- const clientId = normalizeOptionalOAuthString(client.clientId);
585
- if (clientId === undefined) {
620
+ const registration = client.registration === undefined ? undefined : parseOAuthClientRegistration(client.registration);
621
+ const clientId = normalizeOptionalOAuthString(client.clientId) ?? registration?.client_id.trim();
622
+ if (clientId === undefined)
586
623
  return null;
587
- }
588
- const clientSecret = normalizeOptionalOAuthString(client.clientSecret);
589
- return clientSecret === undefined ? { clientId } : { clientId, clientSecret };
624
+ const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === undefined ? undefined : getOwnString(registration, "client_secret")?.trim());
625
+ return normalizeStoredOAuthClient({ clientId, clientSecret, registration, tokenEndpointAuthMethod: client.tokenEndpointAuthMethod });
590
626
  }
591
627
  function normalizeOptionalOAuthString(value) {
592
628
  if (value === undefined) {
@@ -693,12 +729,39 @@ function assertRequestMatchesResource(requestUrl, resource) {
693
729
  throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
694
730
  }
695
731
  }
696
- function buildClientRegistrationBody(metadata, redirectUri) {
732
+ function assertRegistrationIssuer(client, issuer) {
733
+ const registrationIssuer = client.registration === undefined ? undefined : getOwnString(client.registration, "issuer");
734
+ if (registrationIssuer !== undefined && registrationIssuer !== issuer)
735
+ throw new Error("OAuth client registration issuer does not match the authorization server");
736
+ }
737
+ function hasExpiredClientSecret(client, now) {
738
+ if (client.clientSecret === undefined || client.tokenEndpointAuthMethod === "none" || client.registration === undefined)
739
+ return false;
740
+ const expiry = getOwnEntry(client.registration, "client_secret_expires_at");
741
+ return typeof expiry === "number" && expiry !== 0 && expiry <= now() / 1000;
742
+ }
743
+ function getSupportedTokenAuthMethods(metadata) {
744
+ const value = getOwnEntry(metadata, "token_endpoint_auth_methods_supported");
745
+ if (value === undefined)
746
+ return undefined;
747
+ if (!Array.isArray(value) || value.length > 128 || value.some(method => typeof method !== "string"))
748
+ throw new Error("Invalid OAuth token endpoint authentication metadata");
749
+ return value;
750
+ }
751
+ function assertTokenEndpointAuthentication(client, metadata) {
752
+ const method = client.tokenEndpointAuthMethod ?? (client.clientSecret === undefined ? "none" : "client_secret_post");
753
+ if (method !== "none" && client.clientSecret === undefined)
754
+ throw new Error("OAuth token endpoint authentication requires a client secret");
755
+ const supported = getSupportedTokenAuthMethods(metadata);
756
+ if (supported !== undefined && !supported.includes(method))
757
+ throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
758
+ }
759
+ function buildClientRegistrationBody(metadata, redirectUri, tokenEndpointAuthMethod) {
697
760
  const body = {
698
761
  redirect_uris: [redirectUri],
699
762
  grant_types: ["authorization_code", "refresh_token"],
700
763
  response_types: ["code"],
701
- token_endpoint_auth_method: "none"
764
+ token_endpoint_auth_method: tokenEndpointAuthMethod
702
765
  };
703
766
  const clientName = metadata === undefined ? undefined : getOwnString(metadata, "clientName");
704
767
  const scope = metadata === undefined ? undefined : getOwnString(metadata, "scope");
@@ -0,0 +1,2 @@
1
+ import type { OAuthTokenEndpointAuthMethod } from "./types.js";
2
+ export declare function normalizeOAuthTokenEndpointAuthMethod(value: unknown): OAuthTokenEndpointAuthMethod | undefined;
@@ -0,0 +1,7 @@
1
+ export function normalizeOAuthTokenEndpointAuthMethod(value) {
2
+ if (value === undefined || value === null)
3
+ return undefined;
4
+ if (value !== "none" && value !== "client_secret_post" && value !== "client_secret_basic")
5
+ throw new Error("Unsupported OAuth token endpoint authentication method");
6
+ return value;
7
+ }
@@ -1,4 +1,4 @@
1
- import type { OAuthMetadataFetch, StoredOAuthTokens } from "./types.js";
1
+ import type { OAuthMetadataFetch, StoredOAuthTokens, OAuthTokenEndpointAuthMethod } from "./types.js";
2
2
  interface OAuthErrorShape {
3
3
  error: string;
4
4
  error_description?: string;
@@ -22,6 +22,7 @@ export declare function exchangeAuthorizationCode(input: {
22
22
  tokenEndpoint: string;
23
23
  clientId: string;
24
24
  clientSecret?: string;
25
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
25
26
  code: string;
26
27
  codeVerifier: string;
27
28
  redirectUri: string;
@@ -34,6 +35,7 @@ export declare function refreshAccessToken(input: {
34
35
  tokenEndpoint: string;
35
36
  clientId: string;
36
37
  clientSecret?: string;
38
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
37
39
  refreshToken: string;
38
40
  resource: string;
39
41
  fetch: OAuthMetadataFetch;
@@ -1,3 +1,4 @@
1
+ import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
1
2
  import { canonicalizeResourceIndicator } from "../resource-indicator.js";
2
3
  import { readBoundedResponseText } from "../http-response.js";
3
4
  import { fetchMcpResponse } from "../http-fetch.js";
@@ -40,6 +41,7 @@ export async function exchangeAuthorizationCode(input) {
40
41
  tokenEndpoint: input.tokenEndpoint,
41
42
  clientId: input.clientId,
42
43
  clientSecret: input.clientSecret,
44
+ tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
43
45
  params: {
44
46
  grant_type: "authorization_code",
45
47
  code: input.code,
@@ -58,6 +60,7 @@ export async function refreshAccessToken(input) {
58
60
  tokenEndpoint: input.tokenEndpoint,
59
61
  clientId: input.clientId,
60
62
  clientSecret: input.clientSecret,
63
+ tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
61
64
  params: {
62
65
  grant_type: "refresh_token",
63
66
  refresh_token: input.refreshToken,
@@ -69,21 +72,28 @@ export async function refreshAccessToken(input) {
69
72
  });
70
73
  }
71
74
  async function requestTokens(input) {
72
- const body = new URLSearchParams({
73
- client_id: input.clientId,
74
- ...input.params
75
- });
76
- if (input.clientSecret !== undefined) {
77
- body.set("client_secret", input.clientSecret);
75
+ const method = normalizeOAuthTokenEndpointAuthMethod(input.tokenEndpointAuthMethod) ??
76
+ (input.clientSecret === undefined ? "none" : "client_secret_post");
77
+ if (method !== "none" && (input.clientSecret === undefined || input.clientSecret.trim() === ""))
78
+ throw new Error("OAuth token endpoint authentication requires a client secret");
79
+ const body = new URLSearchParams(input.params);
80
+ const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
81
+ if (method === "client_secret_basic") {
82
+ const encoded = new URLSearchParams({ credential: input.clientId }).toString().slice("credential=".length);
83
+ const encodedSecret = new URLSearchParams({ credential: input.clientSecret }).toString().slice("credential=".length);
84
+ headers.set("Authorization", `Basic ${Buffer.from(`${encoded}:${encodedSecret}`).toString("base64")}`);
85
+ }
86
+ else {
87
+ body.set("client_id", input.clientId);
88
+ if (method === "client_secret_post")
89
+ body.set("client_secret", input.clientSecret);
78
90
  }
79
91
  input.signal?.throwIfAborted();
80
92
  const deadline = AbortSignal.timeout(30_000);
81
93
  const signal = input.signal === undefined ? deadline : AbortSignal.any([input.signal, deadline]);
82
94
  const response = await fetchMcpResponse(input.fetch, input.tokenEndpoint, {
83
95
  method: "POST",
84
- headers: {
85
- "Content-Type": "application/x-www-form-urlencoded"
86
- },
96
+ headers,
87
97
  body: body.toString(),
88
98
  signal
89
99
  });
@@ -70,13 +70,39 @@ export interface StoredOAuthTokens {
70
70
  expiresAt: number | null;
71
71
  scope?: string;
72
72
  }
73
+ /** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
74
+ export interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
75
+ expiresAt?: number | null;
76
+ /** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
77
+ expiresIn?: number;
78
+ /** Original issuance time in Unix epoch milliseconds. */
79
+ issuedAt?: number;
80
+ }
81
+ /** Full RFC 7591 response, including JSON provider extensions. */
82
+ export interface OAuthClientRegistration extends Record<string, unknown> {
83
+ client_id: string;
84
+ client_secret?: string | null;
85
+ redirect_uris?: string[] | null;
86
+ grant_types?: string[] | null;
87
+ response_types?: string[] | null;
88
+ contacts?: string[] | null;
89
+ client_id_issued_at?: number | null;
90
+ client_secret_expires_at?: number | null;
91
+ token_endpoint_auth_method?: string | null;
92
+ }
93
+ export interface StoredOAuthClient {
94
+ clientId: string;
95
+ /** Actual listener URI submitted when this native client was registered. */
96
+ requestedRedirectUri?: string;
97
+ clientSecret?: string;
98
+ registration?: OAuthClientRegistration;
99
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
100
+ }
101
+ export type OAuthTokenEndpointAuthMethod = "none" | "client_secret_post" | "client_secret_basic";
73
102
  export interface StoredOAuthSession {
74
103
  resource: string;
75
104
  authorizationServer: string;
76
- client: {
77
- clientId: string;
78
- clientSecret?: string;
79
- };
105
+ client: StoredOAuthClient;
80
106
  tokens?: StoredOAuthTokens;
81
107
  /** A refresh was begun; its winning response may not have been persisted. */
82
108
  refreshState?: "pending";
@@ -104,11 +130,16 @@ export interface DefaultOAuthClientProviderOptions {
104
130
  clientId?: string;
105
131
  clientSecret?: string;
106
132
  metadata?: OAuthClientMetadata;
133
+ /** Import a complete registration owned by the caller. */
134
+ registration?: OAuthClientRegistration;
135
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
107
136
  } | {
108
137
  mode: "static";
109
138
  clientId: string;
110
139
  clientSecret?: string;
111
140
  metadata?: OAuthClientMetadata;
141
+ registration?: OAuthClientRegistration;
142
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
112
143
  };
113
144
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
114
145
  allowInteractive?: boolean;
@@ -119,7 +150,7 @@ export interface DefaultOAuthClientProviderOptions {
119
150
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
120
151
  initialGrant?: {
121
152
  resource: string;
122
- tokens: StoredOAuthTokens;
153
+ tokens: ImportedOAuthTokens;
123
154
  };
124
155
  browser: {
125
156
  openBrowser?(url: string): Promise<void>;
@@ -1,11 +1,12 @@
1
1
  export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
2
+ export { parseOAuthClientRegistration } from "./client/client-registration.js";
2
3
  export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
3
4
  export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
4
5
  export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
5
6
  export { OAuthError, } from "./client/token-endpoint.js";
6
7
  export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
7
8
  export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
8
- export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, StoredOAuthSession, StoredOAuthTokens, } from "./client/types.js";
9
+ export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, OAuthTokenEndpointAuthMethod, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, ImportedOAuthTokens, } from "./client/types.js";
9
10
  export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessToken, } from "./server/jwks-token-verifier.js";
10
11
  export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
11
12
  export { readBoundedResponseText } from "./http-response.js";
@@ -1,4 +1,5 @@
1
1
  export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
2
+ export { parseOAuthClientRegistration } from "./client/client-registration.js";
2
3
  export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
3
4
  export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
4
5
  export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.33",
3
+ "version": "0.1.36",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -173,13 +173,39 @@ interface StoredOAuthTokens {
173
173
  expiresAt: number | null;
174
174
  scope?: string;
175
175
  }
176
+ /** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
177
+ interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
178
+ expiresAt?: number | null;
179
+ /** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
180
+ expiresIn?: number;
181
+ /** Original issuance time in Unix epoch milliseconds. */
182
+ issuedAt?: number;
183
+ }
184
+ /** Full RFC 7591 response, including JSON provider extensions. */
185
+ interface OAuthClientRegistration extends Record<string, unknown> {
186
+ client_id: string;
187
+ client_secret?: string | null;
188
+ redirect_uris?: string[] | null;
189
+ grant_types?: string[] | null;
190
+ response_types?: string[] | null;
191
+ contacts?: string[] | null;
192
+ client_id_issued_at?: number | null;
193
+ client_secret_expires_at?: number | null;
194
+ token_endpoint_auth_method?: string | null;
195
+ }
196
+ interface StoredOAuthClient {
197
+ clientId: string;
198
+ /** Actual listener URI submitted when this native client was registered. */
199
+ requestedRedirectUri?: string;
200
+ clientSecret?: string;
201
+ registration?: OAuthClientRegistration;
202
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
203
+ }
204
+ type OAuthTokenEndpointAuthMethod = "none" | "client_secret_post" | "client_secret_basic";
176
205
  interface StoredOAuthSession {
177
206
  resource: string;
178
207
  authorizationServer: string;
179
- client: {
180
- clientId: string;
181
- clientSecret?: string;
182
- };
208
+ client: StoredOAuthClient;
183
209
  tokens?: StoredOAuthTokens;
184
210
  /** A refresh was begun; its winning response may not have been persisted. */
185
211
  refreshState?: "pending";
@@ -207,11 +233,16 @@ interface DefaultOAuthClientProviderOptions {
207
233
  clientId?: string;
208
234
  clientSecret?: string;
209
235
  metadata?: OAuthClientMetadata;
236
+ /** Import a complete registration owned by the caller. */
237
+ registration?: OAuthClientRegistration;
238
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
210
239
  } | {
211
240
  mode: "static";
212
241
  clientId: string;
213
242
  clientSecret?: string;
214
243
  metadata?: OAuthClientMetadata;
244
+ registration?: OAuthClientRegistration;
245
+ tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
215
246
  };
216
247
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
217
248
  allowInteractive?: boolean;
@@ -222,7 +253,7 @@ interface DefaultOAuthClientProviderOptions {
222
253
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
223
254
  initialGrant?: {
224
255
  resource: string;
225
- tokens: StoredOAuthTokens;
256
+ tokens: ImportedOAuthTokens;
226
257
  };
227
258
  browser: {
228
259
  openBrowser?(url: string): Promise<void>;