tiny-http-mcp-server 0.1.22 → 0.1.23

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.22",
21
+ "version": "0.1.23",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -41,6 +41,7 @@ const verifier = createJwksTokenVerifier({
41
41
  - `mode: "dynamic"` with optional `metadata`
42
42
  - `mode: "static"` with `clientId`, optional `clientSecret`, optional `metadata`
43
43
  - `allowInteractive: false` prevents interactive login while retaining cached tokens and silent refresh
44
+ - `initialGrant: { resource, tokens }` optionally imports an existing Bearer grant for one HTTP resource; requires the original client ID
44
45
  - `browser.openBrowser(url)` optional
45
46
  - `browser.readLine()` optional
46
47
  - `browser.createServer()` optional
@@ -83,6 +84,14 @@ observe the supplied signal and pass it to any work they start.
83
84
  Configure `client.metadata.scope` to request a precise scope set; broader
84
85
  discovery metadata does not override it.
85
86
 
87
+ Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
88
+ `tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
89
+ and optional `scope`. A fresh imported token is used only for its resource.
90
+ Discovery binds an expired or explicitly rejected grant before silent refresh,
91
+ using the original configured client. Persisted sessions take precedence,
92
+ including sessions whose tokens have been cleared; an import cannot revive them.
93
+ Input tokens are copied and invalid expiry values fail before authorization.
94
+
86
95
  `createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
87
96
 
88
97
  ## Environment Variables
@@ -21,12 +21,36 @@ export function createDefaultOAuthClientProvider(options) {
21
21
  const registeredClients = new Map();
22
22
  const refreshPromises = new Map();
23
23
  const authorizationPromises = new Map();
24
+ if (options.initialGrant !== undefined) {
25
+ let resource;
26
+ try {
27
+ resource = new URL(options.initialGrant.resource);
28
+ }
29
+ catch {
30
+ throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
31
+ }
32
+ if ((resource.protocol !== "http:" && resource.protocol !== "https:") || resource.username || resource.password || resource.hash)
33
+ throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
34
+ }
35
+ const initialGrant = options.initialGrant === undefined ? undefined : {
36
+ resource: canonicalizeResourceIndicator(options.initialGrant.resource),
37
+ tokens: normalizeStoredTokens(options.initialGrant.tokens),
38
+ client: normalizeConfiguredClient(options.client)
39
+ };
40
+ if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
41
+ throw new Error("OAuth initial grant requires valid tokens and the original client ID");
42
+ let initialGrantConsumed = false;
24
43
  return {
25
44
  async authorizeRequest(input) {
26
45
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
27
46
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
28
47
  const session = await ensureAuthorizedSession(requestUrl, undefined, input.fetch, false, false, input.signal);
29
48
  const accessToken = session?.tokens?.accessToken;
49
+ if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl &&
50
+ initialGrant.tokens !== undefined && !isExpired(initialGrant.tokens, now)) {
51
+ input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
52
+ return;
53
+ }
30
54
  if (session === null ||
31
55
  accessToken === undefined ||
32
56
  session.tokens === undefined ||
@@ -42,7 +66,7 @@ export function createDefaultOAuthClientProvider(options) {
42
66
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
43
67
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
44
68
  assertRequestMatchesResource(requestUrl, resource);
45
- const forceRefresh = hasCachedAccessToken(await loadSession(resource)) &&
69
+ const forceRefresh = (hasCachedAccessToken(await loadSession(resource)) || (!initialGrantConsumed && initialGrant?.resource === resource)) &&
46
70
  input.challenge?.params.error === "invalid_token";
47
71
  const session = await ensureAuthorizedSession(resource, {
48
72
  ...input.discovery,
@@ -66,6 +90,8 @@ export function createDefaultOAuthClientProvider(options) {
66
90
  signal?.throwIfAborted();
67
91
  const canonicalResource = canonicalizeResourceIndicator(resource);
68
92
  let session = await loadSession(canonicalResource);
93
+ if (session !== null && initialGrant?.resource === canonicalResource)
94
+ initialGrantConsumed = true;
69
95
  signal?.throwIfAborted();
70
96
  if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
71
97
  throw new Error("OAuth discovery authorization-server issuer mismatch");
@@ -76,6 +102,15 @@ export function createDefaultOAuthClientProvider(options) {
76
102
  await clearSession(canonicalResource);
77
103
  session = null;
78
104
  }
105
+ if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
106
+ initialGrant.tokens !== undefined && initialGrant.client !== null) {
107
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
108
+ session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
109
+ client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
110
+ await saveSession(canonicalResource, session);
111
+ initialGrantConsumed = true;
112
+ signal?.throwIfAborted();
113
+ }
79
114
  const sessionDiscovery = resolveDiscovery(discovery, session);
80
115
  if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
81
116
  return session;
@@ -99,6 +99,11 @@ export interface DefaultOAuthClientProviderOptions {
99
99
  };
100
100
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
101
101
  allowInteractive?: boolean;
102
+ /** Import an existing grant for one resource. Persisted sessions take precedence. */
103
+ initialGrant?: {
104
+ resource: string;
105
+ tokens: StoredOAuthTokens;
106
+ };
102
107
  browser: {
103
108
  openBrowser?(url: string): Promise<void>;
104
109
  /** Exact registered HTTP loopback redirect URI. */
@@ -176,6 +176,11 @@ interface DefaultOAuthClientProviderOptions {
176
176
  };
177
177
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
178
178
  allowInteractive?: boolean;
179
+ /** Import an existing grant for one resource. Persisted sessions take precedence. */
180
+ initialGrant?: {
181
+ resource: string;
182
+ tokens: StoredOAuthTokens;
183
+ };
179
184
  browser: {
180
185
  openBrowser?(url: string): Promise<void>;
181
186
  /** Exact registered HTTP loopback redirect URI. */
@@ -152,11 +152,17 @@ var HttpResponseMessages = class {
152
152
  };
153
153
 
154
154
  // ../toolcraft-schema/dist/json.js
155
- function isJsonValue(value) {
155
+ function isJsonValue(value, options = {}) {
156
+ const maxNodes = options.maxNodes ?? 1e4;
157
+ const maxDepth = options.maxDepth ?? 64;
158
+ if (!Number.isSafeInteger(maxNodes) || maxNodes < 1)
159
+ throw new Error("maxNodes must be a positive safe integer");
160
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 0 || maxDepth > 256)
161
+ throw new Error("maxDepth must be an integer between 0 and 256");
156
162
  const ancestors = /* @__PURE__ */ new Set();
157
163
  let nodes = 0;
158
164
  const visit = (item, depth) => {
159
- if (++nodes > 1e4 || depth > 64)
165
+ if (++nodes > maxNodes || depth > maxDepth)
160
166
  return false;
161
167
  if (item === null || typeof item === "string" || typeof item === "boolean")
162
168
  return true;
@@ -182,7 +188,7 @@ function isJsonValue(value) {
182
188
  ancestors.add(item);
183
189
  let valid = true;
184
190
  if (Array.isArray(item)) {
185
- if (item.length > 1e4)
191
+ if (item.length > maxNodes)
186
192
  valid = false;
187
193
  else
188
194
  for (let index = 0; index < item.length; index++) {
@@ -4594,12 +4600,34 @@ function createDefaultOAuthClientProvider(options) {
4594
4600
  const registeredClients = /* @__PURE__ */ new Map();
4595
4601
  const refreshPromises = /* @__PURE__ */ new Map();
4596
4602
  const authorizationPromises = /* @__PURE__ */ new Map();
4603
+ if (options.initialGrant !== void 0) {
4604
+ let resource;
4605
+ try {
4606
+ resource = new URL2(options.initialGrant.resource);
4607
+ } catch {
4608
+ throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
4609
+ }
4610
+ if (resource.protocol !== "http:" && resource.protocol !== "https:" || resource.username || resource.password || resource.hash)
4611
+ throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
4612
+ }
4613
+ const initialGrant = options.initialGrant === void 0 ? void 0 : {
4614
+ resource: canonicalizeResourceIndicator(options.initialGrant.resource),
4615
+ tokens: normalizeStoredTokens(options.initialGrant.tokens),
4616
+ client: normalizeConfiguredClient(options.client)
4617
+ };
4618
+ if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
4619
+ throw new Error("OAuth initial grant requires valid tokens and the original client ID");
4620
+ let initialGrantConsumed = false;
4597
4621
  return {
4598
4622
  async authorizeRequest(input) {
4599
4623
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
4600
4624
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
4601
4625
  const session = await ensureAuthorizedSession(requestUrl, void 0, input.fetch, false, false, input.signal);
4602
4626
  const accessToken = session?.tokens?.accessToken;
4627
+ if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl && initialGrant.tokens !== void 0 && !isExpired(initialGrant.tokens, now)) {
4628
+ input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
4629
+ return;
4630
+ }
4603
4631
  if (session === null || accessToken === void 0 || session.tokens === void 0 || isExpired(session.tokens, now)) {
4604
4632
  return;
4605
4633
  }
@@ -4612,7 +4640,7 @@ function createDefaultOAuthClientProvider(options) {
4612
4640
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
4613
4641
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
4614
4642
  assertRequestMatchesResource(requestUrl, resource);
4615
- const forceRefresh = hasCachedAccessToken(await loadSession(resource)) && input.challenge?.params.error === "invalid_token";
4643
+ const forceRefresh = (hasCachedAccessToken(await loadSession(resource)) || !initialGrantConsumed && initialGrant?.resource === resource) && input.challenge?.params.error === "invalid_token";
4616
4644
  const session = await ensureAuthorizedSession(resource, {
4617
4645
  ...input.discovery,
4618
4646
  resource
@@ -4634,6 +4662,8 @@ function createDefaultOAuthClientProvider(options) {
4634
4662
  signal?.throwIfAborted();
4635
4663
  const canonicalResource = canonicalizeResourceIndicator(resource);
4636
4664
  let session = await loadSession(canonicalResource);
4665
+ if (session !== null && initialGrant?.resource === canonicalResource)
4666
+ initialGrantConsumed = true;
4637
4667
  signal?.throwIfAborted();
4638
4668
  if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
4639
4669
  throw new Error("OAuth discovery authorization-server issuer mismatch");
@@ -4642,6 +4672,19 @@ function createDefaultOAuthClientProvider(options) {
4642
4672
  await clearSession(canonicalResource);
4643
4673
  session = null;
4644
4674
  }
4675
+ if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
4676
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
4677
+ session = {
4678
+ resource: canonicalResource,
4679
+ authorizationServer: discovery.authorizationServer,
4680
+ client: initialGrant.client,
4681
+ tokens: initialGrant.tokens,
4682
+ discovery: toStoredDiscovery(discovery)
4683
+ };
4684
+ await saveSession(canonicalResource, session);
4685
+ initialGrantConsumed = true;
4686
+ signal?.throwIfAborted();
4687
+ }
4645
4688
  const sessionDiscovery = resolveDiscovery(discovery, session);
4646
4689
  if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
4647
4690
  return session;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",