tiny-http-mcp-server 0.1.22 → 0.1.24
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 +13 -1
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -2
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +5 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +1 -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 +55 -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
|
|
@@ -69,7 +70,10 @@ const verifier = createJwksTokenVerifier({
|
|
|
69
70
|
Fixed redirects support `localhost`, `127.0.0.1`, and `::1` over HTTP. Their
|
|
70
71
|
exact spelling, port, path and query are preserved through registration,
|
|
71
72
|
authorization and code exchange. Credentials, fragments, port zero and reserved
|
|
72
|
-
OAuth callback query parameters are rejected before binding a listener.
|
|
73
|
+
OAuth callback query parameters are rejected before binding a listener.
|
|
74
|
+
`createDefaultOAuthClientProvider` also checks the configured redirect before
|
|
75
|
+
creating the provider. Imported tokens that cannot be sent as HTTP header
|
|
76
|
+
values fail with diagnostics that omit their contents. Omit
|
|
73
77
|
`redirectUri` to allocate a random loopback port. Standalone callback sessions
|
|
74
78
|
accept the same `redirectUri`, `signal` and `timeoutMs` options. Cancellation,
|
|
75
79
|
timeout and explicit close settle pending code waits and release listeners.
|
|
@@ -83,6 +87,14 @@ observe the supplied signal and pass it to any work they start.
|
|
|
83
87
|
Configure `client.metadata.scope` to request a precise scope set; broader
|
|
84
88
|
discovery metadata does not override it.
|
|
85
89
|
|
|
90
|
+
Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
|
|
91
|
+
`tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
|
|
92
|
+
and optional `scope`. A fresh imported token is used only for its resource.
|
|
93
|
+
Discovery binds an expired or explicitly rejected grant before silent refresh,
|
|
94
|
+
using the original configured client. Persisted sessions take precedence,
|
|
95
|
+
including sessions whose tokens have been cleared; an import cannot revive them.
|
|
96
|
+
Input tokens are copied and invalid expiry values fail before authorization.
|
|
97
|
+
|
|
86
98
|
`createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
|
|
87
99
|
|
|
88
100
|
## Environment Variables
|
|
@@ -2,7 +2,7 @@ import { isIP } from "node:net";
|
|
|
2
2
|
import { fetchMcpResponse } from "../http-fetch.js";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { createAuthStoreClientStore, createAuthStoreSessionStore } from "./auth-store-session-store.js";
|
|
5
|
-
import { createLoopbackAuthorizationSession } from "./loopback-authorization.js";
|
|
5
|
+
import { createLoopbackAuthorizationSession, loopbackTarget } from "./loopback-authorization.js";
|
|
6
6
|
import { createAuthorizationState } from "./authorization-state.js";
|
|
7
7
|
import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
|
|
8
8
|
import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse } from "./token-endpoint.js";
|
|
@@ -15,18 +15,51 @@ export function createOAuthClientProvider(options) {
|
|
|
15
15
|
return createDefaultOAuthClientProvider(options);
|
|
16
16
|
}
|
|
17
17
|
export function createDefaultOAuthClientProvider(options) {
|
|
18
|
+
loopbackTarget(options.browser);
|
|
18
19
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
|
|
19
20
|
const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
|
|
20
21
|
const now = options.now ?? Date.now;
|
|
21
22
|
const registeredClients = new Map();
|
|
22
23
|
const refreshPromises = new Map();
|
|
23
24
|
const authorizationPromises = new Map();
|
|
25
|
+
if (options.initialGrant !== undefined) {
|
|
26
|
+
let resource;
|
|
27
|
+
try {
|
|
28
|
+
resource = new URL(options.initialGrant.resource);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
|
|
32
|
+
}
|
|
33
|
+
if ((resource.protocol !== "http:" && resource.protocol !== "https:") || resource.username || resource.password || resource.hash)
|
|
34
|
+
throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
|
|
35
|
+
}
|
|
36
|
+
const initialGrant = options.initialGrant === undefined ? undefined : {
|
|
37
|
+
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
38
|
+
tokens: normalizeStoredTokens(options.initialGrant.tokens),
|
|
39
|
+
client: normalizeConfiguredClient(options.client)
|
|
40
|
+
};
|
|
41
|
+
if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
|
|
42
|
+
throw new Error("OAuth initial grant requires valid tokens and the original client ID");
|
|
43
|
+
if (initialGrant?.tokens !== undefined) {
|
|
44
|
+
try {
|
|
45
|
+
new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("OAuth initial grant access token is not a valid HTTP header value");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let initialGrantConsumed = false;
|
|
24
52
|
return {
|
|
25
53
|
async authorizeRequest(input) {
|
|
26
54
|
assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
|
|
27
55
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
28
56
|
const session = await ensureAuthorizedSession(requestUrl, undefined, input.fetch, false, false, input.signal);
|
|
29
57
|
const accessToken = session?.tokens?.accessToken;
|
|
58
|
+
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl &&
|
|
59
|
+
initialGrant.tokens !== undefined && !isExpired(initialGrant.tokens, now)) {
|
|
60
|
+
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
30
63
|
if (session === null ||
|
|
31
64
|
accessToken === undefined ||
|
|
32
65
|
session.tokens === undefined ||
|
|
@@ -42,7 +75,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
42
75
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
43
76
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
44
77
|
assertRequestMatchesResource(requestUrl, resource);
|
|
45
|
-
const forceRefresh = hasCachedAccessToken(await loadSession(resource)) &&
|
|
78
|
+
const forceRefresh = (hasCachedAccessToken(await loadSession(resource)) || (!initialGrantConsumed && initialGrant?.resource === resource)) &&
|
|
46
79
|
input.challenge?.params.error === "invalid_token";
|
|
47
80
|
const session = await ensureAuthorizedSession(resource, {
|
|
48
81
|
...input.discovery,
|
|
@@ -66,6 +99,8 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
66
99
|
signal?.throwIfAborted();
|
|
67
100
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
68
101
|
let session = await loadSession(canonicalResource);
|
|
102
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
103
|
+
initialGrantConsumed = true;
|
|
69
104
|
signal?.throwIfAborted();
|
|
70
105
|
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
71
106
|
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
@@ -76,6 +111,15 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
76
111
|
await clearSession(canonicalResource);
|
|
77
112
|
session = null;
|
|
78
113
|
}
|
|
114
|
+
if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
|
|
115
|
+
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
116
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
117
|
+
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
118
|
+
client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
|
|
119
|
+
await saveSession(canonicalResource, session);
|
|
120
|
+
initialGrantConsumed = true;
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
}
|
|
79
123
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
80
124
|
if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
81
125
|
return session;
|
|
@@ -21,5 +21,10 @@ export interface LoopbackAuthorizationSession {
|
|
|
21
21
|
close(): void;
|
|
22
22
|
}
|
|
23
23
|
export declare function createLoopbackAuthorizationSession(options?: LoopbackAuthorizationOptions): Promise<LoopbackAuthorizationSession>;
|
|
24
|
+
export declare function loopbackTarget(options: LoopbackAuthorizationOptions): {
|
|
25
|
+
port: number;
|
|
26
|
+
host: string;
|
|
27
|
+
callbackPath: string;
|
|
28
|
+
};
|
|
24
29
|
export declare function extractCodeFromInput(input: string): string | null;
|
|
25
30
|
export declare function buildSuccessPage(landingPage?: OAuthLandingPage): string;
|
|
@@ -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++) {
|
|
@@ -4588,18 +4594,48 @@ function createOAuthClientProvider(options) {
|
|
|
4588
4594
|
return createDefaultOAuthClientProvider(options);
|
|
4589
4595
|
}
|
|
4590
4596
|
function createDefaultOAuthClientProvider(options) {
|
|
4597
|
+
loopbackTarget(options.browser);
|
|
4591
4598
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
|
|
4592
4599
|
const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore);
|
|
4593
4600
|
const now = options.now ?? Date.now;
|
|
4594
4601
|
const registeredClients = /* @__PURE__ */ new Map();
|
|
4595
4602
|
const refreshPromises = /* @__PURE__ */ new Map();
|
|
4596
4603
|
const authorizationPromises = /* @__PURE__ */ new Map();
|
|
4604
|
+
if (options.initialGrant !== void 0) {
|
|
4605
|
+
let resource;
|
|
4606
|
+
try {
|
|
4607
|
+
resource = new URL2(options.initialGrant.resource);
|
|
4608
|
+
} catch {
|
|
4609
|
+
throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
|
|
4610
|
+
}
|
|
4611
|
+
if (resource.protocol !== "http:" && resource.protocol !== "https:" || resource.username || resource.password || resource.hash)
|
|
4612
|
+
throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
|
|
4613
|
+
}
|
|
4614
|
+
const initialGrant = options.initialGrant === void 0 ? void 0 : {
|
|
4615
|
+
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
4616
|
+
tokens: normalizeStoredTokens(options.initialGrant.tokens),
|
|
4617
|
+
client: normalizeConfiguredClient(options.client)
|
|
4618
|
+
};
|
|
4619
|
+
if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
|
|
4620
|
+
throw new Error("OAuth initial grant requires valid tokens and the original client ID");
|
|
4621
|
+
if (initialGrant?.tokens !== void 0) {
|
|
4622
|
+
try {
|
|
4623
|
+
new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
|
|
4624
|
+
} catch {
|
|
4625
|
+
throw new Error("OAuth initial grant access token is not a valid HTTP header value");
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
let initialGrantConsumed = false;
|
|
4597
4629
|
return {
|
|
4598
4630
|
async authorizeRequest(input) {
|
|
4599
4631
|
assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
|
|
4600
4632
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
4601
4633
|
const session = await ensureAuthorizedSession(requestUrl, void 0, input.fetch, false, false, input.signal);
|
|
4602
4634
|
const accessToken = session?.tokens?.accessToken;
|
|
4635
|
+
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl && initialGrant.tokens !== void 0 && !isExpired(initialGrant.tokens, now)) {
|
|
4636
|
+
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
4637
|
+
return;
|
|
4638
|
+
}
|
|
4603
4639
|
if (session === null || accessToken === void 0 || session.tokens === void 0 || isExpired(session.tokens, now)) {
|
|
4604
4640
|
return;
|
|
4605
4641
|
}
|
|
@@ -4612,7 +4648,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4612
4648
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
4613
4649
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
4614
4650
|
assertRequestMatchesResource(requestUrl, resource);
|
|
4615
|
-
const forceRefresh = hasCachedAccessToken(await loadSession(resource)) && input.challenge?.params.error === "invalid_token";
|
|
4651
|
+
const forceRefresh = (hasCachedAccessToken(await loadSession(resource)) || !initialGrantConsumed && initialGrant?.resource === resource) && input.challenge?.params.error === "invalid_token";
|
|
4616
4652
|
const session = await ensureAuthorizedSession(resource, {
|
|
4617
4653
|
...input.discovery,
|
|
4618
4654
|
resource
|
|
@@ -4634,6 +4670,8 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4634
4670
|
signal?.throwIfAborted();
|
|
4635
4671
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
4636
4672
|
let session = await loadSession(canonicalResource);
|
|
4673
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
4674
|
+
initialGrantConsumed = true;
|
|
4637
4675
|
signal?.throwIfAborted();
|
|
4638
4676
|
if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
4639
4677
|
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
@@ -4642,6 +4680,19 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4642
4680
|
await clearSession(canonicalResource);
|
|
4643
4681
|
session = null;
|
|
4644
4682
|
}
|
|
4683
|
+
if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
|
|
4684
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4685
|
+
session = {
|
|
4686
|
+
resource: canonicalResource,
|
|
4687
|
+
authorizationServer: discovery.authorizationServer,
|
|
4688
|
+
client: initialGrant.client,
|
|
4689
|
+
tokens: initialGrant.tokens,
|
|
4690
|
+
discovery: toStoredDiscovery(discovery)
|
|
4691
|
+
};
|
|
4692
|
+
await saveSession(canonicalResource, session);
|
|
4693
|
+
initialGrantConsumed = true;
|
|
4694
|
+
signal?.throwIfAborted();
|
|
4695
|
+
}
|
|
4645
4696
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
4646
4697
|
if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
4647
4698
|
return session;
|