tiny-http-mcp-server 0.1.0
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/LICENSE +21 -0
- package/README.md +778 -0
- package/dist/auth.d.ts +69 -0
- package/dist/auth.js +261 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +465 -0
- package/dist/composition.json +25 -0
- package/dist/express-middleware.d.ts +18 -0
- package/dist/express-middleware.js +91 -0
- package/dist/http-server.d.ts +48 -0
- package/dist/http-server.js +263 -0
- package/dist/http-transport.d.ts +164 -0
- package/dist/http-transport.js +897 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +6 -0
- package/dist/load-oauth-verifier.d.ts +6 -0
- package/dist/load-oauth-verifier.js +43 -0
- package/dist/parse-body.d.ts +22 -0
- package/dist/parse-body.js +150 -0
- package/dist/session.d.ts +18 -0
- package/dist/session.js +37 -0
- package/dist/sse.d.ts +11 -0
- package/dist/sse.js +22 -0
- package/dist/test-support.d.ts +10 -0
- package/dist/test-support.js +398 -0
- package/dist/testing.d.ts +59 -0
- package/dist/testing.js +191 -0
- package/node_modules/auth-store/LICENSE +21 -0
- package/node_modules/auth-store/README.md +62 -0
- package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
- package/node_modules/auth-store/dist/create-secret-store.js +44 -0
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +47 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +303 -0
- package/node_modules/auth-store/dist/error-codes.d.ts +1 -0
- package/node_modules/auth-store/dist/error-codes.js +5 -0
- package/node_modules/auth-store/dist/index.d.ts +7 -0
- package/node_modules/auth-store/dist/index.js +4 -0
- package/node_modules/auth-store/dist/keychain-store.d.ts +25 -0
- package/node_modules/auth-store/dist/keychain-store.js +154 -0
- package/node_modules/auth-store/dist/provider-store.d.ts +14 -0
- package/node_modules/auth-store/dist/provider-store.js +78 -0
- package/node_modules/auth-store/dist/types.d.ts +22 -0
- package/node_modules/auth-store/dist/types.js +1 -0
- package/node_modules/auth-store/package.json +27 -0
- package/node_modules/mcp-oauth/LICENSE +21 -0
- package/node_modules/mcp-oauth/README.md +70 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +169 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.js +47 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +627 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +207 -0
- package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +164 -0
- package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
- package/node_modules/mcp-oauth/dist/client/types.js +1 -0
- package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
- package/node_modules/mcp-oauth/dist/index.js +7 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +32 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +388 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
- package/node_modules/mcp-oauth/package.json +33 -0
- package/node_modules/tiny-mcp-client/LICENSE +21 -0
- package/node_modules/tiny-mcp-client/README.md +104 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +660 -0
- package/node_modules/tiny-mcp-client/dist/index.js +3870 -0
- package/node_modules/tiny-mcp-client/package.json +30 -0
- package/package.json +63 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { decodeProtectedHeader, errors, importJWK, jwtVerify } from "jose";
|
|
2
|
+
import { canonicalizeResourceIndicator } from "../resource-indicator.js";
|
|
3
|
+
const DEFAULT_ALLOWED_ALGORITHMS = [
|
|
4
|
+
"ES256",
|
|
5
|
+
"ES384",
|
|
6
|
+
"ES512",
|
|
7
|
+
"RS256",
|
|
8
|
+
"RS384",
|
|
9
|
+
"RS512",
|
|
10
|
+
"PS256",
|
|
11
|
+
"PS384",
|
|
12
|
+
"PS512",
|
|
13
|
+
"EdDSA"
|
|
14
|
+
];
|
|
15
|
+
function isTokenVerificationErrorShape(error) {
|
|
16
|
+
if (!(error instanceof Error)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const challengeError = error;
|
|
20
|
+
return (challengeError.error === "invalid_token" ||
|
|
21
|
+
challengeError.error === "insufficient_scope" ||
|
|
22
|
+
challengeError.error === "temporarily_unavailable");
|
|
23
|
+
}
|
|
24
|
+
function isObjectRecord(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function isStringArray(value) {
|
|
28
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
29
|
+
}
|
|
30
|
+
function getOwnEntry(record, key) {
|
|
31
|
+
return Object.prototype.hasOwnProperty.call(record, key)
|
|
32
|
+
? record[key]
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
function getOwnString(record, key) {
|
|
36
|
+
const value = getOwnEntry(record, key);
|
|
37
|
+
return typeof value === "string" ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
function toUrl(value, label) {
|
|
40
|
+
try {
|
|
41
|
+
return new URL(String(value));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error(`${label} must be an absolute URL`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function normalizeAudience(value) {
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
return [value];
|
|
50
|
+
}
|
|
51
|
+
return isStringArray(value) ? [...value] : [];
|
|
52
|
+
}
|
|
53
|
+
function normalizeVerifiedAudience(value, expectedResource) {
|
|
54
|
+
const audiences = normalizeAudience(value);
|
|
55
|
+
for (const audience of audiences) {
|
|
56
|
+
try {
|
|
57
|
+
const normalizedAudience = canonicalizeResourceIndicator(audience);
|
|
58
|
+
if (normalizedAudience === expectedResource) {
|
|
59
|
+
return [normalizedAudience];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw createInvalidTokenError("audience mismatch");
|
|
67
|
+
}
|
|
68
|
+
function parseScopes(payload) {
|
|
69
|
+
const scope = getOwnString(payload, "scope");
|
|
70
|
+
const scopes = getOwnEntry(payload, "scopes");
|
|
71
|
+
const raw = scope !== undefined ? scope : typeof scopes === "string" ? scopes : null;
|
|
72
|
+
if (raw !== null) {
|
|
73
|
+
return raw
|
|
74
|
+
.split(" ")
|
|
75
|
+
.map((scope) => scope.trim())
|
|
76
|
+
.filter((scope) => scope.length > 0);
|
|
77
|
+
}
|
|
78
|
+
return isStringArray(scopes) ? [...scopes] : [];
|
|
79
|
+
}
|
|
80
|
+
function toVerifiedAccessToken(token, payload, audience = normalizeAudience(getOwnEntry(payload, "aud"))) {
|
|
81
|
+
const issuer = getOwnString(payload, "iss");
|
|
82
|
+
const expiresAt = getOwnEntry(payload, "exp");
|
|
83
|
+
const subject = getOwnString(payload, "sub");
|
|
84
|
+
const clientId = getOwnString(payload, "client_id");
|
|
85
|
+
return {
|
|
86
|
+
token,
|
|
87
|
+
issuer: issuer ?? "",
|
|
88
|
+
audience,
|
|
89
|
+
scopes: parseScopes(payload),
|
|
90
|
+
expiresAt: expiresAt,
|
|
91
|
+
claims: { ...payload },
|
|
92
|
+
...(subject !== undefined
|
|
93
|
+
? {
|
|
94
|
+
subject
|
|
95
|
+
}
|
|
96
|
+
: {}),
|
|
97
|
+
...(clientId !== undefined
|
|
98
|
+
? {
|
|
99
|
+
clientId
|
|
100
|
+
}
|
|
101
|
+
: {})
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function createTokenVerificationError(input) {
|
|
105
|
+
return Object.assign(new Error(input.errorDescription ?? input.error), input);
|
|
106
|
+
}
|
|
107
|
+
function createTemporarilyUnavailableError() {
|
|
108
|
+
return createTokenVerificationError({
|
|
109
|
+
error: "temporarily_unavailable",
|
|
110
|
+
errorDescription: "token verification temporarily unavailable"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function createInvalidTokenError(errorDescription) {
|
|
114
|
+
return createTokenVerificationError({
|
|
115
|
+
error: "invalid_token",
|
|
116
|
+
errorDescription
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function normalizeVerificationError(error) {
|
|
120
|
+
if (isTokenVerificationErrorShape(error)) {
|
|
121
|
+
return error;
|
|
122
|
+
}
|
|
123
|
+
if (error instanceof errors.JWTExpired) {
|
|
124
|
+
return createInvalidTokenError("token expired");
|
|
125
|
+
}
|
|
126
|
+
if (error instanceof errors.JWTClaimValidationFailed) {
|
|
127
|
+
if (error.claim === "aud") {
|
|
128
|
+
return createInvalidTokenError("audience mismatch");
|
|
129
|
+
}
|
|
130
|
+
if (error.claim === "iss") {
|
|
131
|
+
return createInvalidTokenError("issuer mismatch");
|
|
132
|
+
}
|
|
133
|
+
if (error.claim === "nbf") {
|
|
134
|
+
return createInvalidTokenError("token not active yet");
|
|
135
|
+
}
|
|
136
|
+
if (error.claim === "exp") {
|
|
137
|
+
if (error.reason === "missing") {
|
|
138
|
+
return createInvalidTokenError("token missing expiry");
|
|
139
|
+
}
|
|
140
|
+
return createInvalidTokenError("token expired");
|
|
141
|
+
}
|
|
142
|
+
if (error.claim === "typ") {
|
|
143
|
+
return createInvalidTokenError("invalid access token type");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (error instanceof errors.JOSEAlgNotAllowed || error instanceof errors.JOSENotSupported) {
|
|
147
|
+
return createInvalidTokenError("unsupported token algorithm");
|
|
148
|
+
}
|
|
149
|
+
if (error instanceof errors.JWKSNoMatchingKey ||
|
|
150
|
+
error instanceof errors.JWKSMultipleMatchingKeys ||
|
|
151
|
+
error instanceof errors.JWSSignatureVerificationFailed) {
|
|
152
|
+
return createInvalidTokenError("token signature invalid");
|
|
153
|
+
}
|
|
154
|
+
if (error instanceof errors.JWKSInvalid || error instanceof errors.JWKSTimeout) {
|
|
155
|
+
return createTemporarilyUnavailableError();
|
|
156
|
+
}
|
|
157
|
+
return createInvalidTokenError("token verification failed");
|
|
158
|
+
}
|
|
159
|
+
async function loadJwks(jwksUrl, fetchImplementation, timeoutMs) {
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetchImplementation(jwksUrl, {
|
|
162
|
+
headers: {
|
|
163
|
+
Accept: "application/json"
|
|
164
|
+
},
|
|
165
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw createTemporarilyUnavailableError();
|
|
169
|
+
}
|
|
170
|
+
const payload = (await response.json());
|
|
171
|
+
const keys = isObjectRecord(payload) ? getOwnEntry(payload, "keys") : undefined;
|
|
172
|
+
if (!isObjectRecord(payload) || !Array.isArray(keys) || !keys.every(isObjectRecord)) {
|
|
173
|
+
throw createTemporarilyUnavailableError();
|
|
174
|
+
}
|
|
175
|
+
return { keys };
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (isTokenVerificationErrorShape(error)) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
throw createTemporarilyUnavailableError();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function resolveAlgorithm(token, allowedAlgorithms) {
|
|
185
|
+
const header = decodeProtectedHeader(token);
|
|
186
|
+
const alg = getOwnString(header, "alg");
|
|
187
|
+
if (hasCriticalHeaderClaims(header)) {
|
|
188
|
+
throw createInvalidTokenError("unsupported critical token claims");
|
|
189
|
+
}
|
|
190
|
+
if (typeof alg !== "string" ||
|
|
191
|
+
alg === "none" ||
|
|
192
|
+
alg.startsWith("HS") ||
|
|
193
|
+
!allowedAlgorithms.includes(alg)) {
|
|
194
|
+
throw createInvalidTokenError("unsupported token algorithm");
|
|
195
|
+
}
|
|
196
|
+
return alg;
|
|
197
|
+
}
|
|
198
|
+
function hasCriticalHeaderClaims(header) {
|
|
199
|
+
const criticalClaims = getOwnEntry(header, "crit");
|
|
200
|
+
return Array.isArray(criticalClaims) && criticalClaims.length > 0;
|
|
201
|
+
}
|
|
202
|
+
function isVerificationCandidate(key, alg, kid) {
|
|
203
|
+
const keyId = getOwnString(key, "kid");
|
|
204
|
+
const keyAlgorithm = getOwnString(key, "alg");
|
|
205
|
+
const keyUse = getOwnString(key, "use");
|
|
206
|
+
const keyOps = getOwnEntry(key, "key_ops");
|
|
207
|
+
if (kid !== undefined && keyId !== kid) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (keyAlgorithm !== undefined && keyAlgorithm !== alg) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (keyUse !== undefined && keyUse !== "sig") {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(keyOps) && !keyOps.includes("verify")) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
function shouldContinueWithNextKey(error) {
|
|
222
|
+
return error instanceof errors.JWSSignatureVerificationFailed;
|
|
223
|
+
}
|
|
224
|
+
async function verifyJwtAgainstJwks(input) {
|
|
225
|
+
const protectedHeader = decodeProtectedHeader(input.token);
|
|
226
|
+
const kid = getOwnString(protectedHeader, "kid");
|
|
227
|
+
const candidateKeys = input.jwks.keys.filter((key) => isVerificationCandidate(key, input.alg, kid));
|
|
228
|
+
if (candidateKeys.length === 0) {
|
|
229
|
+
throw new NoMatchingKeyError();
|
|
230
|
+
}
|
|
231
|
+
let lastSignatureError;
|
|
232
|
+
let hasMalformedKey = false;
|
|
233
|
+
for (const candidate of candidateKeys) {
|
|
234
|
+
let key;
|
|
235
|
+
try {
|
|
236
|
+
key = await importJWK(candidate, input.alg);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
hasMalformedKey = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
return await jwtVerify(input.token, key, {
|
|
244
|
+
algorithms: [input.alg],
|
|
245
|
+
issuer: [...input.authorizationServers],
|
|
246
|
+
clockTolerance: input.clockSkewSeconds,
|
|
247
|
+
requiredClaims: ["exp"],
|
|
248
|
+
...(input.requireAccessTokenType ? { typ: "at+jwt" } : {})
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (shouldContinueWithNextKey(error)) {
|
|
253
|
+
lastSignatureError = error;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (hasMalformedKey) {
|
|
260
|
+
throw createTemporarilyUnavailableError();
|
|
261
|
+
}
|
|
262
|
+
throw lastSignatureError ?? createInvalidTokenError("token signature invalid");
|
|
263
|
+
}
|
|
264
|
+
class NoMatchingKeyError extends Error {
|
|
265
|
+
}
|
|
266
|
+
function isLoopbackHostname(hostname) {
|
|
267
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (hostname === "[::1]" || hostname === "::1") {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const parts = hostname.split(".");
|
|
274
|
+
return (parts.length === 4 &&
|
|
275
|
+
parts[0] === "127" &&
|
|
276
|
+
parts.slice(1).every((part) => {
|
|
277
|
+
const value = Number(part);
|
|
278
|
+
return Number.isInteger(value) && value >= 0 && value <= 255;
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
export function createJwksTokenVerifier(options) {
|
|
282
|
+
const jwksUrl = toUrl(options.jwksUrl, "jwksUrl");
|
|
283
|
+
const clockSkewSeconds = options.clockSkewSeconds ?? 30;
|
|
284
|
+
const allowedAlgorithms = options.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS;
|
|
285
|
+
const jwksCacheTtlMs = options.jwksCacheTtlMs ?? 300_000;
|
|
286
|
+
const jwksFetchTimeoutMs = options.jwksFetchTimeoutMs ?? 5_000;
|
|
287
|
+
const jwksRefreshCooldownMs = options.jwksRefreshCooldownMs ?? 30_000;
|
|
288
|
+
const requireAccessTokenType = options.requireAccessTokenType ?? false;
|
|
289
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
290
|
+
let cachedJwks;
|
|
291
|
+
let pendingJwks;
|
|
292
|
+
let pendingForcedRefresh;
|
|
293
|
+
let lastForcedRefreshAt = Number.NEGATIVE_INFINITY;
|
|
294
|
+
if (jwksUrl.protocol !== "https:" &&
|
|
295
|
+
!isLoopbackHostname(jwksUrl.hostname) &&
|
|
296
|
+
options.allowInsecureJwks !== true) {
|
|
297
|
+
throw new Error("jwksUrl must use HTTPS for non-loopback hosts");
|
|
298
|
+
}
|
|
299
|
+
if (typeof fetchImplementation !== "function") {
|
|
300
|
+
throw new Error("fetch is not available; pass options.fetch explicitly");
|
|
301
|
+
}
|
|
302
|
+
async function getJwks(forceRefresh = false) {
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
if (!forceRefresh && cachedJwks !== undefined && cachedJwks.expiresAt > now) {
|
|
305
|
+
return cachedJwks.value;
|
|
306
|
+
}
|
|
307
|
+
if (pendingJwks !== undefined) {
|
|
308
|
+
return pendingJwks;
|
|
309
|
+
}
|
|
310
|
+
pendingJwks = loadJwks(jwksUrl, fetchImplementation, jwksFetchTimeoutMs)
|
|
311
|
+
.then((jwks) => {
|
|
312
|
+
cachedJwks = {
|
|
313
|
+
value: jwks,
|
|
314
|
+
expiresAt: Date.now() + jwksCacheTtlMs
|
|
315
|
+
};
|
|
316
|
+
return jwks;
|
|
317
|
+
})
|
|
318
|
+
.finally(() => {
|
|
319
|
+
pendingJwks = undefined;
|
|
320
|
+
});
|
|
321
|
+
return pendingJwks;
|
|
322
|
+
}
|
|
323
|
+
function refreshJwksAfterUnknownKey() {
|
|
324
|
+
if (pendingForcedRefresh !== undefined) {
|
|
325
|
+
return pendingForcedRefresh;
|
|
326
|
+
}
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
if (now - lastForcedRefreshAt < jwksRefreshCooldownMs) {
|
|
329
|
+
throw new NoMatchingKeyError();
|
|
330
|
+
}
|
|
331
|
+
lastForcedRefreshAt = now;
|
|
332
|
+
pendingForcedRefresh = getJwks(true).finally(() => {
|
|
333
|
+
pendingForcedRefresh = undefined;
|
|
334
|
+
});
|
|
335
|
+
return pendingForcedRefresh;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
async verify(input) {
|
|
339
|
+
try {
|
|
340
|
+
const expectedResource = canonicalizeResourceIndicator(input.resource);
|
|
341
|
+
const algorithm = resolveAlgorithm(input.token, allowedAlgorithms);
|
|
342
|
+
let jwks = await getJwks();
|
|
343
|
+
let verified;
|
|
344
|
+
try {
|
|
345
|
+
verified = await verifyJwtAgainstJwks({
|
|
346
|
+
token: input.token,
|
|
347
|
+
jwks,
|
|
348
|
+
alg: algorithm,
|
|
349
|
+
authorizationServers: input.authorizationServers,
|
|
350
|
+
clockSkewSeconds,
|
|
351
|
+
requireAccessTokenType
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (!(error instanceof NoMatchingKeyError)) {
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
jwks = await refreshJwksAfterUnknownKey();
|
|
359
|
+
verified = await verifyJwtAgainstJwks({
|
|
360
|
+
token: input.token,
|
|
361
|
+
jwks,
|
|
362
|
+
alg: algorithm,
|
|
363
|
+
authorizationServers: input.authorizationServers,
|
|
364
|
+
clockSkewSeconds,
|
|
365
|
+
requireAccessTokenType
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const audience = normalizeVerifiedAudience(getOwnEntry(verified.payload, "aud"), expectedResource);
|
|
369
|
+
const accessToken = toVerifiedAccessToken(input.token, verified.payload, audience);
|
|
370
|
+
if (input.requiredScopes.length > 0 &&
|
|
371
|
+
!input.requiredScopes.every((scope) => accessToken.scopes.includes(scope))) {
|
|
372
|
+
throw createTokenVerificationError({
|
|
373
|
+
error: "insufficient_scope",
|
|
374
|
+
errorDescription: "insufficient scope",
|
|
375
|
+
scope: [...input.requiredScopes]
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return accessToken;
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof NoMatchingKeyError) {
|
|
382
|
+
throw createInvalidTokenError("token signature invalid");
|
|
383
|
+
}
|
|
384
|
+
throw normalizeVerificationError(error);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const ignoredImplicitGrantOptions = {
|
|
2
|
+
client: {
|
|
3
|
+
mode: "static",
|
|
4
|
+
clientId: "client-id",
|
|
5
|
+
// @ts-expect-error implicit response types are not configurable via the public API
|
|
6
|
+
responseType: "token",
|
|
7
|
+
},
|
|
8
|
+
browser: {
|
|
9
|
+
openBrowser: async () => { },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
const ignoredPasswordGrantOptions = {
|
|
13
|
+
client: {
|
|
14
|
+
mode: "dynamic",
|
|
15
|
+
// @ts-expect-error password grants are not configurable via the public API
|
|
16
|
+
grantType: "password",
|
|
17
|
+
},
|
|
18
|
+
browser: {
|
|
19
|
+
openBrowser: async () => { },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-oauth",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"jose": "^6.1.2"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tiny-oauth-test-server": "*"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/poe-platform/poe-code.git",
|
|
31
|
+
"directory": "packages/mcp-oauth"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Poe Platform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# tiny-mcp-client
|
|
2
|
+
|
|
3
|
+
`tiny-mcp-client` is a lightweight Model Context Protocol client used by tests, fixtures, and package integrations. It supports stdio transports, streamable HTTP transports, in-memory test pairs, JSON-RPC helpers, and OAuth metadata discovery for OAuth-protected MCP HTTP servers.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { HttpTransport, McpClient } from "tiny-mcp-client";
|
|
9
|
+
|
|
10
|
+
const client = new McpClient({
|
|
11
|
+
clientInfo: { name: "demo", version: "0.1.0" }
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
await client.connect(new HttpTransport({ url: "http://127.0.0.1:3000/mcp" }));
|
|
15
|
+
const tools = await client.listTools();
|
|
16
|
+
console.log(tools);
|
|
17
|
+
const result = await client.callTool({
|
|
18
|
+
name: "search",
|
|
19
|
+
arguments: { query: "typed outputs" }
|
|
20
|
+
});
|
|
21
|
+
console.log(result.structuredContent);
|
|
22
|
+
await client.close();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`Tool` includes MCP `outputSchema` when a server advertises typed tool output. `CallToolResult` includes `structuredContent?: Record<string, unknown>` for typed results; legacy/content-block tools still use `content[]`.
|
|
26
|
+
|
|
27
|
+
## Transports
|
|
28
|
+
|
|
29
|
+
| Transport | Description |
|
|
30
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| `StdioTransport` | Spawns an MCP server process and communicates over stdio. |
|
|
32
|
+
| `HttpTransport` | Connects to streamable HTTP MCP endpoints, including session IDs, SSE GET streams, and session termination. |
|
|
33
|
+
| `createInMemoryTransportPair()` | Creates paired streams for in-process tests. |
|
|
34
|
+
|
|
35
|
+
## OAuth HTTP support
|
|
36
|
+
|
|
37
|
+
`HttpTransport` accepts `oauth` options from `mcp-oauth`. When a protected server returns a Bearer `WWW-Authenticate` challenge, the transport discovers protected-resource metadata, loads authorization-server metadata, lets the OAuth provider handle authorization, and retries the request when credentials are available.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { HttpTransport, createDefaultOAuthClientProvider } from "tiny-mcp-client";
|
|
41
|
+
|
|
42
|
+
const transport = new HttpTransport({
|
|
43
|
+
url: "https://mcp.example.com/mcp",
|
|
44
|
+
oauth: {
|
|
45
|
+
provider: createDefaultOAuthClientProvider({
|
|
46
|
+
client: {
|
|
47
|
+
mode: "dynamic",
|
|
48
|
+
metadata: {
|
|
49
|
+
clientName: "tiny-client"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
browser: {
|
|
53
|
+
openBrowser: async (url) => {
|
|
54
|
+
console.log(`Open ${url}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You can also call `discoverOAuthMetadata(resourceUrl, options)` directly, or instantiate `OAuthMetadataDiscovery` with a custom `fetch` implementation and shared cache.
|
|
63
|
+
|
|
64
|
+
## Testing helpers
|
|
65
|
+
|
|
66
|
+
- `createTestPair()` and `createSdkTestPair()` create paired client/server fixtures.
|
|
67
|
+
- `createInMemoryTransportPair()` is useful for fast unit tests without opening sockets or spawning processes.
|
|
68
|
+
- JSON-RPC error constants are exported for assertions: `ERROR_PARSE`, `ERROR_INVALID_REQUEST`, `ERROR_METHOD_NOT_FOUND`, `ERROR_INVALID_PARAMS`, and `ERROR_INTERNAL`.
|
|
69
|
+
|
|
70
|
+
## Configuration Options
|
|
71
|
+
|
|
72
|
+
### `McpClientOptions`
|
|
73
|
+
|
|
74
|
+
| Option | Type | Description |
|
|
75
|
+
| ---------------------------------------------------------- | ----------------------------------- | ------------------------------------------------ |
|
|
76
|
+
| `clientInfo` | `{ name: string; version: string }` | Required client identity sent during initialize. |
|
|
77
|
+
| `capabilities` | `ClientCapabilities` | Optional MCP client capabilities. |
|
|
78
|
+
| `onToolsChanged`, `onResourcesChanged`, `onPromptsChanged` | callbacks | Optional notification handlers. |
|
|
79
|
+
| `onResourceUpdated`, `onLog`, `onProgress` | callbacks | Optional resource/log/progress handlers. |
|
|
80
|
+
| `onSamplingRequest`, `onRootsList` | callbacks | Optional server-to-client request handlers. |
|
|
81
|
+
|
|
82
|
+
### `HttpTransportOptions`
|
|
83
|
+
|
|
84
|
+
| Option | Type | Description |
|
|
85
|
+
| --------------------- | ------------------------------------- | ------------------------------------- |
|
|
86
|
+
| `url` | `string` | MCP HTTP endpoint URL. |
|
|
87
|
+
| `headers` | `HeadersInit` | Static headers added to requests. |
|
|
88
|
+
| `fetch` | `(input, init?) => Promise<Response>` | Custom fetch implementation. |
|
|
89
|
+
| `oauth` | `OAuthClientProviderOptions` | Enables OAuth authorization handling. |
|
|
90
|
+
| `oauthDiscoveryCache` | `OAuthDiscoveryCache` | Optional shared metadata cache. |
|
|
91
|
+
|
|
92
|
+
### `StdioTransportOptions`
|
|
93
|
+
|
|
94
|
+
| Option | Type | Description |
|
|
95
|
+
| --------- | ------------------- | -------------------------------- |
|
|
96
|
+
| `command` | `string` | Server executable. |
|
|
97
|
+
| `args` | `string[]` | Server args. |
|
|
98
|
+
| `cwd` | `string` | Server working directory. |
|
|
99
|
+
| `env` | `NodeJS.ProcessEnv` | Server environment. |
|
|
100
|
+
| `spawn` | `StdioSpawn` | Custom spawn function for tests. |
|
|
101
|
+
|
|
102
|
+
## Environment Variables
|
|
103
|
+
|
|
104
|
+
This package does not expose public environment variables. Pass process environment explicitly through `StdioTransportOptions.env` when a spawned MCP server needs it.
|