tiny-http-mcp-server 0.1.24 → 0.1.25
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 +28 -5
- package/node_modules/mcp-oauth/dist/client/types.d.ts +5 -1
- package/node_modules/tiny-mcp-client/dist/index.d.ts +6 -1
- package/node_modules/tiny-mcp-client/dist/index.js +40 -9
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -84,6 +84,15 @@ registration, token requests and bounded token-body reads. Cancellation retains
|
|
|
84
84
|
its original reason and does not retry authorization. Custom providers should
|
|
85
85
|
observe the supplied signal and pass it to any work they start.
|
|
86
86
|
|
|
87
|
+
`authorizeRequest` may return an owned token snapshot for the request it
|
|
88
|
+
authorized. The HTTP client supplies that snapshot as `presentedTokens`, along
|
|
89
|
+
with the actual request's `requestHeaders`, to `handleUnauthorized`. Providers
|
|
90
|
+
that return `void` remain supported. The native provider compares the rejected
|
|
91
|
+
snapshot with persisted credentials: delayed 401s retry with a newer grant
|
|
92
|
+
without redeeming its refresh token again. A proven current token is refreshed
|
|
93
|
+
on 401 even when the server omits `error="invalid_token"`. Invalid provenance
|
|
94
|
+
fails without quoting token values.
|
|
95
|
+
|
|
87
96
|
Configure `client.metadata.scope` to request a precise scope set; broader
|
|
88
97
|
discovery metadata does not override it.
|
|
89
98
|
|
|
@@ -58,7 +58,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
58
58
|
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl &&
|
|
59
59
|
initialGrant.tokens !== undefined && !isExpired(initialGrant.tokens, now)) {
|
|
60
60
|
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
61
|
-
return;
|
|
61
|
+
return { ...initialGrant.tokens };
|
|
62
62
|
}
|
|
63
63
|
if (session === null ||
|
|
64
64
|
accessToken === undefined ||
|
|
@@ -68,6 +68,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
68
68
|
}
|
|
69
69
|
assertRequestMatchesResource(requestUrl, session.resource);
|
|
70
70
|
input.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
71
|
+
return { ...session.tokens };
|
|
71
72
|
},
|
|
72
73
|
async handleUnauthorized(input) {
|
|
73
74
|
try {
|
|
@@ -75,12 +76,28 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
75
76
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
76
77
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
77
78
|
assertRequestMatchesResource(requestUrl, resource);
|
|
78
|
-
const
|
|
79
|
-
|
|
79
|
+
const cached = await loadSession(resource);
|
|
80
|
+
const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : undefined);
|
|
81
|
+
let rejectedCurrentGrant = hasCachedAccessToken(cached) || (!initialGrantConsumed && initialGrant?.resource === resource);
|
|
82
|
+
let presentedTokens = input.presentedTokens;
|
|
83
|
+
if (input.presentedTokens !== undefined) {
|
|
84
|
+
rejectedCurrentGrant = false;
|
|
85
|
+
if (input.presentedTokens !== null) {
|
|
86
|
+
const presented = normalizeStoredTokens(input.presentedTokens);
|
|
87
|
+
const header = input.requestHeaders?.get("Authorization") ?? "";
|
|
88
|
+
const separator = header.indexOf(" ");
|
|
89
|
+
if (presented === undefined || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
|
|
90
|
+
throw new Error("OAuth rejected-request provenance does not match its authorization header");
|
|
91
|
+
presentedTokens = presented;
|
|
92
|
+
rejectedCurrentGrant = currentTokens !== undefined && sameTokenGrant(currentTokens, presented);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const challengeError = input.challenge?.params.error;
|
|
96
|
+
const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || (input.presentedTokens !== undefined && challengeError === undefined));
|
|
80
97
|
const session = await ensureAuthorizedSession(resource, {
|
|
81
98
|
...input.discovery,
|
|
82
99
|
resource
|
|
83
|
-
}, input.fetch, true, forceRefresh, input.signal);
|
|
100
|
+
}, input.fetch, true, forceRefresh, input.signal, presentedTokens);
|
|
84
101
|
if (session?.tokens?.accessToken === undefined) {
|
|
85
102
|
return { action: "fail" };
|
|
86
103
|
}
|
|
@@ -95,7 +112,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
95
112
|
}
|
|
96
113
|
}
|
|
97
114
|
};
|
|
98
|
-
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal) {
|
|
115
|
+
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
99
116
|
signal?.throwIfAborted();
|
|
100
117
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
101
118
|
let session = await loadSession(canonicalResource);
|
|
@@ -120,6 +137,8 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
120
137
|
initialGrantConsumed = true;
|
|
121
138
|
signal?.throwIfAborted();
|
|
122
139
|
}
|
|
140
|
+
if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
141
|
+
forceRefresh = false;
|
|
123
142
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
124
143
|
if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
125
144
|
return session;
|
|
@@ -454,6 +473,10 @@ function resolveDiscovery(discovery, session) {
|
|
|
454
473
|
authorizationServerMetadata: metadata
|
|
455
474
|
};
|
|
456
475
|
}
|
|
476
|
+
function sameTokenGrant(left, right) {
|
|
477
|
+
return left.accessToken === right.accessToken && left.refreshToken === right.refreshToken &&
|
|
478
|
+
left.tokenType === right.tokenType && left.expiresAt === right.expiresAt && left.scope === right.scope;
|
|
479
|
+
}
|
|
457
480
|
function clearSessionTokens(session) {
|
|
458
481
|
const nextSession = { ...session };
|
|
459
482
|
delete nextSession.tokens;
|
|
@@ -33,7 +33,7 @@ export interface OAuthClientProvider {
|
|
|
33
33
|
headers: Headers;
|
|
34
34
|
fetch: OAuthMetadataFetch;
|
|
35
35
|
signal?: AbortSignal;
|
|
36
|
-
}): Promise<void> | void;
|
|
36
|
+
}): Promise<StoredOAuthTokens | void> | StoredOAuthTokens | void;
|
|
37
37
|
handleUnauthorized(input: {
|
|
38
38
|
requestUrl: URL;
|
|
39
39
|
response: Response;
|
|
@@ -41,6 +41,10 @@ export interface OAuthClientProvider {
|
|
|
41
41
|
discovery: OAuthDiscoveryResult;
|
|
42
42
|
fetch: OAuthMetadataFetch;
|
|
43
43
|
signal?: AbortSignal;
|
|
44
|
+
/** Headers actually attached to this rejected request. */
|
|
45
|
+
requestHeaders?: Headers;
|
|
46
|
+
/** Owned snapshot returned when this request was authorized, or null if absent. */
|
|
47
|
+
presentedTokens?: StoredOAuthTokens | null;
|
|
44
48
|
}): Promise<{
|
|
45
49
|
action: "retry";
|
|
46
50
|
} | {
|
|
@@ -110,7 +110,7 @@ interface OAuthClientProvider {
|
|
|
110
110
|
headers: Headers;
|
|
111
111
|
fetch: OAuthMetadataFetch;
|
|
112
112
|
signal?: AbortSignal;
|
|
113
|
-
}): Promise<void> | void;
|
|
113
|
+
}): Promise<StoredOAuthTokens | void> | StoredOAuthTokens | void;
|
|
114
114
|
handleUnauthorized(input: {
|
|
115
115
|
requestUrl: URL;
|
|
116
116
|
response: Response;
|
|
@@ -118,6 +118,10 @@ interface OAuthClientProvider {
|
|
|
118
118
|
discovery: OAuthDiscoveryResult;
|
|
119
119
|
fetch: OAuthMetadataFetch;
|
|
120
120
|
signal?: AbortSignal;
|
|
121
|
+
/** Headers actually attached to this rejected request. */
|
|
122
|
+
requestHeaders?: Headers;
|
|
123
|
+
/** Owned snapshot returned when this request was authorized, or null if absent. */
|
|
124
|
+
presentedTokens?: StoredOAuthTokens | null;
|
|
121
125
|
}): Promise<{
|
|
122
126
|
action: "retry";
|
|
123
127
|
} | {
|
|
@@ -649,6 +653,7 @@ declare class HttpTransport implements McpTransport {
|
|
|
649
653
|
private readonly oauthMetadataDiscovery;
|
|
650
654
|
private readonly inFlightFetchAbortControllers;
|
|
651
655
|
private readonly inFlightOAuthAbortControllers;
|
|
656
|
+
private readonly oauthRequestTokens;
|
|
652
657
|
private readonly openResponseReaders;
|
|
653
658
|
private readonly modernRequests;
|
|
654
659
|
private modernMode;
|
|
@@ -4634,13 +4634,14 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4634
4634
|
const accessToken = session?.tokens?.accessToken;
|
|
4635
4635
|
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl && initialGrant.tokens !== void 0 && !isExpired(initialGrant.tokens, now)) {
|
|
4636
4636
|
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
4637
|
-
return;
|
|
4637
|
+
return { ...initialGrant.tokens };
|
|
4638
4638
|
}
|
|
4639
4639
|
if (session === null || accessToken === void 0 || session.tokens === void 0 || isExpired(session.tokens, now)) {
|
|
4640
4640
|
return;
|
|
4641
4641
|
}
|
|
4642
4642
|
assertRequestMatchesResource(requestUrl, session.resource);
|
|
4643
4643
|
input.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
4644
|
+
return { ...session.tokens };
|
|
4644
4645
|
},
|
|
4645
4646
|
async handleUnauthorized(input) {
|
|
4646
4647
|
try {
|
|
@@ -4648,11 +4649,28 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4648
4649
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
4649
4650
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
4650
4651
|
assertRequestMatchesResource(requestUrl, resource);
|
|
4651
|
-
const
|
|
4652
|
+
const cached = await loadSession(resource);
|
|
4653
|
+
const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : void 0);
|
|
4654
|
+
let rejectedCurrentGrant = hasCachedAccessToken(cached) || !initialGrantConsumed && initialGrant?.resource === resource;
|
|
4655
|
+
let presentedTokens = input.presentedTokens;
|
|
4656
|
+
if (input.presentedTokens !== void 0) {
|
|
4657
|
+
rejectedCurrentGrant = false;
|
|
4658
|
+
if (input.presentedTokens !== null) {
|
|
4659
|
+
const presented = normalizeStoredTokens(input.presentedTokens);
|
|
4660
|
+
const header = input.requestHeaders?.get("Authorization") ?? "";
|
|
4661
|
+
const separator = header.indexOf(" ");
|
|
4662
|
+
if (presented === void 0 || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
|
|
4663
|
+
throw new Error("OAuth rejected-request provenance does not match its authorization header");
|
|
4664
|
+
presentedTokens = presented;
|
|
4665
|
+
rejectedCurrentGrant = currentTokens !== void 0 && sameTokenGrant(currentTokens, presented);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
const challengeError = input.challenge?.params.error;
|
|
4669
|
+
const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || input.presentedTokens !== void 0 && challengeError === void 0);
|
|
4652
4670
|
const session = await ensureAuthorizedSession(resource, {
|
|
4653
4671
|
...input.discovery,
|
|
4654
4672
|
resource
|
|
4655
|
-
}, input.fetch, true, forceRefresh, input.signal);
|
|
4673
|
+
}, input.fetch, true, forceRefresh, input.signal, presentedTokens);
|
|
4656
4674
|
if (session?.tokens?.accessToken === void 0) {
|
|
4657
4675
|
return { action: "fail" };
|
|
4658
4676
|
}
|
|
@@ -4666,7 +4684,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4666
4684
|
}
|
|
4667
4685
|
}
|
|
4668
4686
|
};
|
|
4669
|
-
async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal) {
|
|
4687
|
+
async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
4670
4688
|
signal?.throwIfAborted();
|
|
4671
4689
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
4672
4690
|
let session = await loadSession(canonicalResource);
|
|
@@ -4693,6 +4711,8 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4693
4711
|
initialGrantConsumed = true;
|
|
4694
4712
|
signal?.throwIfAborted();
|
|
4695
4713
|
}
|
|
4714
|
+
if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
4715
|
+
forceRefresh = false;
|
|
4696
4716
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
4697
4717
|
if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
4698
4718
|
return session;
|
|
@@ -5014,6 +5034,9 @@ function resolveDiscovery(discovery, session) {
|
|
|
5014
5034
|
authorizationServerMetadata: metadata
|
|
5015
5035
|
};
|
|
5016
5036
|
}
|
|
5037
|
+
function sameTokenGrant(left, right) {
|
|
5038
|
+
return left.accessToken === right.accessToken && left.refreshToken === right.refreshToken && left.tokenType === right.tokenType && left.expiresAt === right.expiresAt && left.scope === right.scope;
|
|
5039
|
+
}
|
|
5017
5040
|
function clearSessionTokens(session) {
|
|
5018
5041
|
const nextSession = { ...session };
|
|
5019
5042
|
delete nextSession.tokens;
|
|
@@ -6789,6 +6812,7 @@ var HttpTransport = class {
|
|
|
6789
6812
|
oauthMetadataDiscovery;
|
|
6790
6813
|
inFlightFetchAbortControllers = /* @__PURE__ */ new Set();
|
|
6791
6814
|
inFlightOAuthAbortControllers = /* @__PURE__ */ new Set();
|
|
6815
|
+
oauthRequestTokens = /* @__PURE__ */ new WeakMap();
|
|
6792
6816
|
openResponseReaders = /* @__PURE__ */ new Set();
|
|
6793
6817
|
modernRequests = /* @__PURE__ */ new Map();
|
|
6794
6818
|
modernMode = false;
|
|
@@ -7059,7 +7083,7 @@ var HttpTransport = class {
|
|
|
7059
7083
|
}
|
|
7060
7084
|
async authorizeRequestHeaders(headers, signal) {
|
|
7061
7085
|
signal?.throwIfAborted();
|
|
7062
|
-
await this.oauthProvider?.authorizeRequest?.({
|
|
7086
|
+
const tokens = await this.oauthProvider?.authorizeRequest?.({
|
|
7063
7087
|
requestUrl: new URL(this.url),
|
|
7064
7088
|
headers,
|
|
7065
7089
|
signal,
|
|
@@ -7068,6 +7092,7 @@ var HttpTransport = class {
|
|
|
7068
7092
|
signal: signal === void 0 ? init?.signal : init?.signal == null ? signal : AbortSignal.any([signal, init.signal])
|
|
7069
7093
|
})
|
|
7070
7094
|
});
|
|
7095
|
+
if (tokens !== void 0) this.oauthRequestTokens.set(headers, { ...tokens });
|
|
7071
7096
|
signal?.throwIfAborted();
|
|
7072
7097
|
return headers;
|
|
7073
7098
|
}
|
|
@@ -7211,7 +7236,7 @@ var HttpTransport = class {
|
|
|
7211
7236
|
const message = responseBody.length === 0 ? `HTTP transport POST failed (${statusDescriptor})` : `HTTP transport POST failed (${statusDescriptor}): ${responseBody}`;
|
|
7212
7237
|
throw new HttpTransportError(message, response.status, "POST");
|
|
7213
7238
|
}
|
|
7214
|
-
async maybeHandleUnauthorizedResponse(response, signal) {
|
|
7239
|
+
async maybeHandleUnauthorizedResponse(response, signal, requestHeaders, presentedTokens) {
|
|
7215
7240
|
if (response.status !== 401 || this.oauthProvider === void 0) {
|
|
7216
7241
|
return false;
|
|
7217
7242
|
}
|
|
@@ -7232,6 +7257,8 @@ var HttpTransport = class {
|
|
|
7232
7257
|
challenge,
|
|
7233
7258
|
discovery,
|
|
7234
7259
|
signal,
|
|
7260
|
+
requestHeaders: new Headers(requestHeaders),
|
|
7261
|
+
presentedTokens,
|
|
7235
7262
|
fetch: (url, init) => fetchMcpResponse(this.fetchImpl, url, {
|
|
7236
7263
|
...init,
|
|
7237
7264
|
signal: init?.signal == null ? signal : AbortSignal.any([signal, init.signal])
|
|
@@ -7372,16 +7399,20 @@ var HttpTransport = class {
|
|
|
7372
7399
|
const request = async () => {
|
|
7373
7400
|
controller.signal.throwIfAborted();
|
|
7374
7401
|
const headers = await input.createHeaders(controller.signal);
|
|
7402
|
+
const headerSnapshot = new Headers(headers);
|
|
7403
|
+
const tokens = this.oauthRequestTokens.get(headers) ?? null;
|
|
7375
7404
|
controller.signal.throwIfAborted();
|
|
7376
|
-
|
|
7405
|
+
const response = await this.fetchWithAbort(input.url ?? this.url, {
|
|
7377
7406
|
method: input.method,
|
|
7378
7407
|
headers,
|
|
7379
7408
|
body: input.body
|
|
7380
7409
|
}, controller);
|
|
7410
|
+
return { response, headers: headerSnapshot, tokens };
|
|
7381
7411
|
};
|
|
7382
7412
|
try {
|
|
7383
|
-
let
|
|
7384
|
-
if (await this.maybeHandleUnauthorizedResponse(response, controller.signal))
|
|
7413
|
+
let attempt = await request();
|
|
7414
|
+
if (await this.maybeHandleUnauthorizedResponse(attempt.response, controller.signal, attempt.headers, attempt.tokens)) attempt = await request();
|
|
7415
|
+
const response = attempt.response;
|
|
7385
7416
|
const oauthError = this.oauthProvider === void 0 ? null : this.readOAuthChallengeError(response);
|
|
7386
7417
|
if (oauthError !== null) {
|
|
7387
7418
|
void response.body?.cancel().catch(() => void 0);
|