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.
- package/dist/composition.json +1 -1
- package/node_modules/mcp-oauth/README.md +9 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +36 -1
- package/node_modules/mcp-oauth/dist/client/types.d.ts +5 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +5 -0
- package/node_modules/tiny-mcp-client/dist/index.js +47 -4
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -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 >
|
|
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 >
|
|
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;
|