@koduhai/mcp-kit 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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/auth/index.d.ts +16 -0
  4. package/dist/auth/index.d.ts.map +1 -0
  5. package/dist/auth/index.js +12 -0
  6. package/dist/auth/index.js.map +1 -0
  7. package/dist/auth/introspection-verifier.d.ts +37 -0
  8. package/dist/auth/introspection-verifier.d.ts.map +1 -0
  9. package/dist/auth/introspection-verifier.js +103 -0
  10. package/dist/auth/introspection-verifier.js.map +1 -0
  11. package/dist/auth/jwt-verifier.d.ts +35 -0
  12. package/dist/auth/jwt-verifier.d.ts.map +1 -0
  13. package/dist/auth/jwt-verifier.js +86 -0
  14. package/dist/auth/jwt-verifier.js.map +1 -0
  15. package/dist/auth/metadata.d.ts +16 -0
  16. package/dist/auth/metadata.d.ts.map +1 -0
  17. package/dist/auth/metadata.js +32 -0
  18. package/dist/auth/metadata.js.map +1 -0
  19. package/dist/auth/protect.d.ts +50 -0
  20. package/dist/auth/protect.d.ts.map +1 -0
  21. package/dist/auth/protect.js +42 -0
  22. package/dist/auth/protect.js.map +1 -0
  23. package/dist/index.d.ts +12 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +12 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/internal/http.d.ts +16 -0
  28. package/dist/internal/http.d.ts.map +1 -0
  29. package/dist/internal/http.js +28 -0
  30. package/dist/internal/http.js.map +1 -0
  31. package/dist/upstream/index.d.ts +70 -0
  32. package/dist/upstream/index.d.ts.map +1 -0
  33. package/dist/upstream/index.js +116 -0
  34. package/dist/upstream/index.js.map +1 -0
  35. package/dist/versioning/index.d.ts +50 -0
  36. package/dist/versioning/index.d.ts.map +1 -0
  37. package/dist/versioning/index.js +60 -0
  38. package/dist/versioning/index.js.map +1 -0
  39. package/package.json +97 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Koduhai
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.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @koduhai/mcp-kit
2
+
3
+ [![CI](https://github.com/koduhai/mcp-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/koduhai/mcp-kit/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@koduhai/mcp-kit.svg)](https://www.npmjs.com/package/@koduhai/mcp-kit)
5
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/koduhai/mcp-kit/badge)](https://scorecard.dev/viewer/?uri=github.com/koduhai/mcp-kit)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
7
+ [![Node](https://img.shields.io/node/v/@koduhai/mcp-kit.svg)](https://nodejs.org)
8
+
9
+ The two things people get wrong building [MCP](https://modelcontextprotocol.io) servers, solved: **auth** and **versioning**.
10
+
11
+ It does three things, on three import paths so the lightweight parts pull no heavy deps:
12
+
13
+ - **Upstream auth** (`/upstream`) — how your server authenticates to the API it wraps: API key, bearer, or OAuth client-credentials with automatic token caching/refresh.
14
+ - **Versioning** (`/versioning`) — pin the upstream API version, send it on every call, expose a `get_version` tool, and detect drift.
15
+ - **Server OAuth** (`/auth`) — turn a remote (Streamable HTTP) MCP server into a spec-compliant **OAuth 2.1 Resource Server** in one call. Ships the JWKS + introspection token verifiers the MCP SDK needs but does not include.
16
+
17
+ Built on top of `@modelcontextprotocol/sdk`. Aligned to the **2025-06-18** authorization spec (MCP servers are Resource Servers; they verify tokens and serve RFC 9728 metadata, they do not act as an Authorization Server).
18
+
19
+ ```bash
20
+ npm install @koduhai/mcp-kit
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 1. Upstream auth — `@koduhai/mcp-kit/upstream`
26
+
27
+ Most MCP servers wrap an API and need to authenticate to it. Stop hand-rolling this.
28
+
29
+ ```ts
30
+ import {
31
+ apiKeyAuth,
32
+ bearerAuth,
33
+ clientCredentialsAuth,
34
+ createUpstreamFetch,
35
+ } from '@koduhai/mcp-kit/upstream';
36
+
37
+ // Static API key (defaults to `Authorization: Bearer <key>`; pass a header for raw keys)
38
+ const auth = apiKeyAuth({ key: process.env.API_KEY! });
39
+ const auth2 = apiKeyAuth({ key: process.env.API_KEY!, header: 'X-Api-Key' });
40
+
41
+ // OAuth 2.0 machine-to-machine — fetches, caches, and refreshes the token for you
42
+ const m2m = clientCredentialsAuth({
43
+ tokenUrl: 'https://issuer/oauth/token',
44
+ clientId: process.env.CLIENT_ID!,
45
+ clientSecret: process.env.CLIENT_SECRET!,
46
+ audience: 'https://api.example.com', // if your IdP needs it (e.g. Auth0)
47
+ });
48
+
49
+ // A fetch that carries your auth (+ any standing headers) on every request
50
+ const api = createUpstreamFetch({ baseUrl: 'https://api.example.com', auth });
51
+ const res = await api('/things/123'); // -> GET https://api.example.com/things/123, authorized
52
+ ```
53
+
54
+ `clientCredentialsAuth` caches the access token and refreshes it shortly before expiry; concurrent callers during a refresh share a single in-flight request.
55
+
56
+ ## 2. Versioning — `@koduhai/mcp-kit/versioning`
57
+
58
+ APIs evolve; agents get confused when response shapes shift under them. Pin a version, send it everywhere, and surface it.
59
+
60
+ ```ts
61
+ import { apiVersioning, versionTool } from '@koduhai/mcp-kit/versioning';
62
+
63
+ const versioning = apiVersioning({
64
+ header: 'Api-Version',
65
+ version: '2026-01-01',
66
+ current: '2026-03-01', // optional: flags drift
67
+ supported: ['2026-01-01', '2026-03-01'], // optional: refuses an unknown pin at startup
68
+ });
69
+
70
+ // Feed it into createUpstreamFetch so every request carries the header:
71
+ const api = createUpstreamFetch({ baseUrl, auth, headers: () => versioning.headers() });
72
+
73
+ // Register this descriptor as a tool so the agent can ask which version it's talking to:
74
+ const tool = versionTool(versioning); // { name: 'get_version', inputSchema, handler }
75
+ ```
76
+
77
+ ## 3. Server-side OAuth — `@koduhai/mcp-kit/auth`
78
+
79
+ This is the part everyone gets stuck on. Per the current spec, a remote MCP server is an **OAuth 2.1 Resource Server**: it must verify access tokens and serve Protected Resource Metadata (RFC 9728) so clients can discover where to log in. The MCP SDK gives you `requireBearerAuth` and the metadata router, **but not a token verifier** — you have to write JWT/JWKS or introspection validation yourself. mcp-kit ships both, plus a one-call assembly.
80
+
81
+ ```ts
82
+ import express from 'express';
83
+ import { jwtVerifier, protectMcpServer } from '@koduhai/mcp-kit/auth';
84
+
85
+ const app = express();
86
+ const issuer = 'https://your-tenant.auth0.com';
87
+ const resourceServerUrl = 'https://mcp.example.com';
88
+
89
+ const { requireAuth } = await protectMcpServer({
90
+ app,
91
+ resourceServerUrl,
92
+ issuer, // AS metadata + JWKS auto-discovered
93
+ verifier: jwtVerifier({ issuer, audience: resourceServerUrl }),
94
+ scopesSupported: ['mcp:tools'],
95
+ requiredScopes: ['mcp:tools'],
96
+ });
97
+
98
+ app.post('/mcp', requireAuth, mcpHttpHandler); // req.auth is now populated
99
+ ```
100
+
101
+ That gives you, for free:
102
+
103
+ - `GET /.well-known/oauth-protected-resource` → RFC 9728 metadata pointing at your IdP.
104
+ - `401` on missing/invalid tokens with a `WWW-Authenticate: Bearer ... resource_metadata="..."` header, so compliant MCP clients can discover the auth server and start the flow.
105
+ - JWT validation of signature (via the issuer's JWKS), `iss`, `aud` (this is what stops token-passthrough/confused-deputy attacks), `exp`/`nbf`, and scope enforcement.
106
+
107
+ Opaque tokens instead of JWTs? Swap the verifier:
108
+
109
+ ```ts
110
+ import { introspectionVerifier } from '@koduhai/mcp-kit/auth';
111
+
112
+ const verifier = introspectionVerifier({
113
+ introspectionUrl: 'https://your-tenant.auth0.com/oauth/introspect',
114
+ clientId: process.env.RS_CLIENT_ID!,
115
+ clientSecret: process.env.RS_CLIENT_SECRET!,
116
+ cacheTtlSeconds: 60, // cache active results (default 60); set 0 to introspect every request
117
+ });
118
+ ```
119
+
120
+ Introspection results are cached for a short TTL (capped by the token's own `exp`) and deduplicated while a call is in flight, so a busy server doesn't introspect the same token on every request. Caching delays revocation visibility by at most the TTL; set `cacheTtlSeconds: 0` if every request must hit the AS.
121
+
122
+ Works with any standards-compliant IdP: Auth0, Logto, Clerk, Keycloak, Okta, Cognito, WorkOS, and friends. mcp-kit verifies tokens; it does not try to be your Authorization Server (the spec says don't, and you shouldn't).
123
+
124
+ See [`examples/`](./examples) for a full stdio server and a full remote OAuth server.
125
+
126
+ ---
127
+
128
+ ## Design
129
+
130
+ - **Layered, optional peers.** `/upstream` and `/versioning` have zero dependencies. `/auth` declares `@modelcontextprotocol/sdk`, `express`, and `jose` as _optional_ peers, so you only install them if you build a remote server.
131
+ - **Injectable everything.** Every network call and clock is injectable, so the whole thing is tested offline (46 tests, including a real Express + token-verification integration).
132
+ - **Resilient outbound calls.** Every control-plane request the library makes (token, introspection, JWKS, discovery) carries a timeout (default 10s, configurable via `timeoutMs`) so an unresponsive IdP can't hang your server.
133
+ - **ESM, Node ≥ 20, TypeScript-first.**
134
+
135
+ ## Compatibility
136
+
137
+ Targets `@modelcontextprotocol/sdk` ≥ 1.20 and the 2025-06-18 MCP authorization spec. The SDK's auth helpers are Express-based, so `/auth` integrates with Express; `/upstream` and `/versioning` are transport-agnostic.
138
+
139
+ ## License
140
+
141
+ MIT © [Koduhai](https://github.com/koduhai). Built alongside [KoduhMail](https://koduhmail.com), generalizing the auth + versioning patterns from its MCP server.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @koduhai/mcp-kit/auth — make a remote (Streamable HTTP) MCP server a spec-compliant
3
+ * OAuth 2.1 Resource Server. Supplies the token verifiers the MCP SDK needs but does not
4
+ * ship (JWKS + introspection) and a one-call `protectMcpServer` assembly.
5
+ *
6
+ * Peers: `@modelcontextprotocol/sdk`, `express`, `jose`.
7
+ */
8
+ export { discoverOAuthMetadata } from './metadata.js';
9
+ export type { DiscoverOptions } from './metadata.js';
10
+ export { jwtVerifier } from './jwt-verifier.js';
11
+ export type { JwtVerifierOptions } from './jwt-verifier.js';
12
+ export { introspectionVerifier } from './introspection-verifier.js';
13
+ export type { IntrospectionVerifierOptions } from './introspection-verifier.js';
14
+ export { protectMcpServer } from './protect.js';
15
+ export type { ProtectMcpServerOptions, ProtectMcpServerResult } from './protect.js';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,YAAY,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @koduhai/mcp-kit/auth — make a remote (Streamable HTTP) MCP server a spec-compliant
3
+ * OAuth 2.1 Resource Server. Supplies the token verifiers the MCP SDK needs but does not
4
+ * ship (JWKS + introspection) and a one-call `protectMcpServer` assembly.
5
+ *
6
+ * Peers: `@modelcontextprotocol/sdk`, `express`, `jose`.
7
+ */
8
+ export { discoverOAuthMetadata } from './metadata.js';
9
+ export { jwtVerifier } from './jwt-verifier.js';
10
+ export { introspectionVerifier } from './introspection-verifier.js';
11
+ export { protectMcpServer } from './protect.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
2
+ export interface IntrospectionVerifierOptions {
3
+ /** The OAuth 2.0 token introspection endpoint (RFC 7662). */
4
+ introspectionUrl: string;
5
+ /** Client credentials this Resource Server uses to authenticate to the introspection endpoint. */
6
+ clientId: string;
7
+ clientSecret: string;
8
+ /** How to present the client credentials. Default `basic` (HTTP Basic). */
9
+ authMethod?: 'basic' | 'post';
10
+ /**
11
+ * Cache successful introspection results for this many seconds, so a burst of
12
+ * requests bearing the same token does not introspect on every call. The cached
13
+ * entry never outlives the token's own `exp`. Default 60. Set 0 to introspect on
14
+ * every request (instant revocation visibility, at the cost of more AS load).
15
+ */
16
+ cacheTtlSeconds?: number;
17
+ /** Maximum number of cached tokens; oldest entries are evicted first. Default 1000. */
18
+ maxCacheEntries?: number;
19
+ /** Timeout (ms) for the introspection request. Default 10000. Pass 0 to disable. */
20
+ timeoutMs?: number;
21
+ /** Injectable clock (ms). */
22
+ now?: () => number;
23
+ /** Injectable for tests. */
24
+ fetch?: typeof globalThis.fetch;
25
+ }
26
+ /**
27
+ * An {@link OAuthTokenVerifier} that validates opaque (or any) access tokens by calling the
28
+ * Authorization Server's introspection endpoint (RFC 7662). Use this when your IdP issues
29
+ * opaque tokens, or when you want the AS to be the single source of truth on revocation.
30
+ *
31
+ * Successful results are cached for a short, configurable TTL (and deduplicated while a
32
+ * call is in flight) so high-traffic servers don't introspect the same token on every
33
+ * request. Caching delays revocation visibility by at most the TTL; set `cacheTtlSeconds: 0`
34
+ * if you need every request to hit the AS.
35
+ */
36
+ export declare function introspectionVerifier(opts: IntrospectionVerifierOptions): OAuthTokenVerifier;
37
+ //# sourceMappingURL=introspection-verifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection-verifier.d.ts","sourceRoot":"","sources":["../../src/auth/introspection-verifier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mDAAmD,CAAC;AAI5F,MAAM,WAAW,4BAA4B;IAC3C,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,CAAC;IACzB,kGAAkG;IAClG,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,kBAAkB,CA8F5F"}
@@ -0,0 +1,103 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
3
+ import { DEFAULT_TIMEOUT_MS, fetchWithTimeout } from '../internal/http.js';
4
+ /**
5
+ * An {@link OAuthTokenVerifier} that validates opaque (or any) access tokens by calling the
6
+ * Authorization Server's introspection endpoint (RFC 7662). Use this when your IdP issues
7
+ * opaque tokens, or when you want the AS to be the single source of truth on revocation.
8
+ *
9
+ * Successful results are cached for a short, configurable TTL (and deduplicated while a
10
+ * call is in flight) so high-traffic servers don't introspect the same token on every
11
+ * request. Caching delays revocation visibility by at most the TTL; set `cacheTtlSeconds: 0`
12
+ * if you need every request to hit the AS.
13
+ */
14
+ export function introspectionVerifier(opts) {
15
+ if (!opts.introspectionUrl)
16
+ throw new Error('introspectionVerifier: `introspectionUrl` is required');
17
+ if (!opts.clientId || !opts.clientSecret) {
18
+ throw new Error('introspectionVerifier: `clientId` and `clientSecret` are required');
19
+ }
20
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
21
+ const method = opts.authMethod ?? 'basic';
22
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
23
+ const ttlMs = (opts.cacheTtlSeconds ?? 60) * 1000;
24
+ const maxEntries = opts.maxCacheEntries ?? 1000;
25
+ const now = opts.now ?? (() => Date.now());
26
+ const cache = new Map();
27
+ const inflight = new Map();
28
+ // Hash so raw bearer tokens are not retained as cache keys.
29
+ const keyOf = (token) => createHash('sha256').update(token).digest('hex');
30
+ async function introspect(token) {
31
+ const headers = {
32
+ 'Content-Type': 'application/x-www-form-urlencoded',
33
+ Accept: 'application/json',
34
+ };
35
+ const body = new URLSearchParams({ token, token_type_hint: 'access_token' });
36
+ if (method === 'basic') {
37
+ headers.Authorization = `Basic ${Buffer.from(`${opts.clientId}:${opts.clientSecret}`).toString('base64')}`;
38
+ }
39
+ else {
40
+ body.set('client_id', opts.clientId);
41
+ body.set('client_secret', opts.clientSecret);
42
+ }
43
+ let res;
44
+ try {
45
+ res = await fetchWithTimeout(fetchImpl, opts.introspectionUrl, { method: 'POST', headers, body }, timeoutMs);
46
+ }
47
+ catch (e) {
48
+ throw new InvalidTokenError(`introspection request failed: ${e instanceof Error ? e.message : String(e)}`);
49
+ }
50
+ if (!res.ok)
51
+ throw new InvalidTokenError(`introspection endpoint returned HTTP ${res.status}`);
52
+ const data = (await res.json());
53
+ if (data.active !== true)
54
+ throw new InvalidTokenError('token is inactive or revoked');
55
+ const scopes = typeof data.scope === 'string' ? data.scope.split(' ').filter(Boolean) : [];
56
+ return {
57
+ token,
58
+ clientId: String(data.client_id ?? ''),
59
+ scopes,
60
+ expiresAt: typeof data.exp === 'number' ? data.exp : undefined,
61
+ extra: data,
62
+ };
63
+ }
64
+ function remember(key, info) {
65
+ // Cap the cached lifetime by both the TTL and the token's own expiry.
66
+ const tokenExpiryMs = info.expiresAt != null ? info.expiresAt * 1000 : Infinity;
67
+ const expiresAtMs = Math.min(now() + ttlMs, tokenExpiryMs);
68
+ if (expiresAtMs <= now())
69
+ return;
70
+ if (cache.size >= maxEntries) {
71
+ const oldest = cache.keys().next().value;
72
+ if (oldest !== undefined)
73
+ cache.delete(oldest);
74
+ }
75
+ cache.set(key, { info, expiresAtMs });
76
+ }
77
+ return {
78
+ async verifyAccessToken(token) {
79
+ if (ttlMs <= 0)
80
+ return introspect(token);
81
+ const key = keyOf(token);
82
+ const hit = cache.get(key);
83
+ if (hit && now() < hit.expiresAtMs)
84
+ return hit.info;
85
+ if (hit)
86
+ cache.delete(key);
87
+ const pending = inflight.get(key);
88
+ if (pending)
89
+ return pending;
90
+ const p = introspect(token)
91
+ .then((info) => {
92
+ remember(key, info);
93
+ return info;
94
+ })
95
+ .finally(() => {
96
+ inflight.delete(key);
97
+ });
98
+ inflight.set(key, p);
99
+ return p;
100
+ },
101
+ };
102
+ }
103
+ //# sourceMappingURL=introspection-verifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection-verifier.js","sourceRoot":"","sources":["../../src/auth/introspection-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAGpF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AA2B3E;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAkC;IACtE,IAAI,CAAC,IAAI,CAAC,gBAAgB;QAAE,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IACrG,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACjD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC;IAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACvD,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAE3C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAmD,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;IACtD,4DAA4D;IAC5D,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAElF,KAAK,UAAU,UAAU,CAAC,KAAa;QACrC,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,mCAAmC;YACnD,MAAM,EAAE,kBAAkB;SAC3B,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC,CAAC;QAC7E,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,OAAO,CAAC,aAAa,GAAG,SAAS,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7G,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrC,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC/C,CAAC;QAED,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,gBAAgB,CAC1B,SAAS,EACT,IAAI,CAAC,gBAAgB,EACrB,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EACjC,SAAS,CACV,CAAC;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,iBAAiB,CACzB,iCAAiC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAC9E,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,iBAAiB,CAAC,wCAAwC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAE/F,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;QAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;YAAE,MAAM,IAAI,iBAAiB,CAAC,8BAA8B,CAAC,CAAC;QAEtF,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3F,OAAO;YACL,KAAK;YACL,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;YACtC,MAAM;YACN,SAAS,EAAE,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;YAC9D,KAAK,EAAE,IAAI;SACZ,CAAC;IACJ,CAAC;IAED,SAAS,QAAQ,CAAC,GAAW,EAAE,IAAc;QAC3C,sEAAsE;QACtE,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;QAChF,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,aAAa,CAAC,CAAC;QAC3D,IAAI,WAAW,IAAI,GAAG,EAAE;YAAE,OAAO;QACjC,IAAI,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YACzC,IAAI,MAAM,KAAK,SAAS;gBAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACjD,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,OAAO;QACL,KAAK,CAAC,iBAAiB,CAAC,KAAa;YACnC,IAAI,KAAK,IAAI,CAAC;gBAAE,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;YACzC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YAEzB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,GAAG,IAAI,GAAG,EAAE,GAAG,GAAG,CAAC,WAAW;gBAAE,OAAO,GAAG,CAAC,IAAI,CAAC;YACpD,IAAI,GAAG;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAE3B,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,OAAO;gBAAE,OAAO,OAAO,CAAC;YAE5B,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC;iBACxB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;gBACb,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACpB,OAAO,IAAI,CAAC;YACd,CAAC,CAAC;iBACD,OAAO,CAAC,GAAG,EAAE;gBACZ,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC,CAAC,CAAC;YACL,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACrB,OAAO,CAAC,CAAC;QACX,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { jwtVerify } from 'jose';
2
+ import type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
3
+ /** The accepted forms for the verification key (a key, a JWKS, or a key-resolving function). */
4
+ type JwtKeyInput = Parameters<typeof jwtVerify>[1];
5
+ export interface JwtVerifierOptions {
6
+ /** Expected token issuer (`iss`). Also used to auto-discover the JWKS if `jwksUri` is omitted. */
7
+ issuer: string;
8
+ /**
9
+ * Expected audience (`aud`): your MCP server's resource identifier (RFC 8707). A token minted
10
+ * for a different resource is rejected, which is what stops token-passthrough attacks.
11
+ */
12
+ audience: string | string[];
13
+ /** JWKS URI. If omitted it is discovered from the issuer's AS metadata (`jwks_uri`). */
14
+ jwksUri?: string;
15
+ /** Allowed signing algorithms. Default `['RS256', 'ES256']`. Never allow `none`. */
16
+ algorithms?: string[];
17
+ /** Clock skew tolerance in seconds for `exp`/`nbf`. Default 5. */
18
+ clockToleranceSeconds?: number;
19
+ /** Timeout (ms) for JWKS fetch and issuer discovery. Default 10000. */
20
+ timeoutMs?: number;
21
+ /** Claim to read scopes from. By default tries `scope` (space-delimited) then `scp` (array). */
22
+ scopeClaim?: string;
23
+ /** Provide the key directly (a `KeyLike`/JWKS/resolver) instead of discovering it. Mainly for tests. */
24
+ key?: JwtKeyInput;
25
+ /** Injectable fetch for discovery. */
26
+ fetch?: typeof globalThis.fetch;
27
+ }
28
+ /**
29
+ * An {@link OAuthTokenVerifier} that validates JWT access tokens against an IdP's JWKS:
30
+ * signature, `iss`, `aud`, and `exp`/`nbf`. This is the verifier the MCP SDK's
31
+ * `requireBearerAuth` needs but does not ship. Drop it into {@link protectMcpServer}.
32
+ */
33
+ export declare function jwtVerifier(opts: JwtVerifierOptions): OAuthTokenVerifier;
34
+ export {};
35
+ //# sourceMappingURL=jwt-verifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt-verifier.d.ts","sourceRoot":"","sources":["../../src/auth/jwt-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,SAAS,EAAE,MAAM,MAAM,CAAC;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mDAAmD,CAAC;AAK5F,gGAAgG;AAChG,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnD,MAAM,WAAW,kBAAkB;IACjC,kGAAkG;IAClG,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,kEAAkE;IAClE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wGAAwG;IACxG,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,sCAAsC;IACtC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAcD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,CAyDxE"}
@@ -0,0 +1,86 @@
1
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
2
+ import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
3
+ import { DEFAULT_TIMEOUT_MS } from '../internal/http.js';
4
+ import { discoverOAuthMetadata } from './metadata.js';
5
+ function extractScopes(payload, scopeClaim) {
6
+ const toScopes = (v) => {
7
+ if (typeof v === 'string')
8
+ return v.split(' ').filter(Boolean);
9
+ if (Array.isArray(v))
10
+ return v.map(String);
11
+ return [];
12
+ };
13
+ if (scopeClaim)
14
+ return toScopes(payload[scopeClaim]);
15
+ if (payload.scope != null)
16
+ return toScopes(payload.scope);
17
+ if (payload.scp != null)
18
+ return toScopes(payload.scp);
19
+ return [];
20
+ }
21
+ /**
22
+ * An {@link OAuthTokenVerifier} that validates JWT access tokens against an IdP's JWKS:
23
+ * signature, `iss`, `aud`, and `exp`/`nbf`. This is the verifier the MCP SDK's
24
+ * `requireBearerAuth` needs but does not ship. Drop it into {@link protectMcpServer}.
25
+ */
26
+ export function jwtVerifier(opts) {
27
+ if (!opts.issuer)
28
+ throw new Error('jwtVerifier: `issuer` is required');
29
+ if (!opts.audience)
30
+ throw new Error('jwtVerifier: `audience` is required');
31
+ let keyInput = opts.key;
32
+ let resolving = null;
33
+ async function resolveKey() {
34
+ if (keyInput)
35
+ return keyInput;
36
+ if (resolving)
37
+ return resolving;
38
+ resolving = (async () => {
39
+ let jwksUri = opts.jwksUri;
40
+ if (!jwksUri) {
41
+ const meta = (await discoverOAuthMetadata(opts.issuer, {
42
+ fetch: opts.fetch,
43
+ timeoutMs: opts.timeoutMs,
44
+ }));
45
+ jwksUri = meta.jwks_uri;
46
+ if (!jwksUri)
47
+ throw new Error('jwtVerifier: issuer metadata has no jwks_uri; pass `jwksUri` or `key`');
48
+ }
49
+ const jwksTimeout = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
50
+ keyInput = createRemoteJWKSet(new URL(jwksUri), { timeoutDuration: jwksTimeout });
51
+ return keyInput;
52
+ })();
53
+ try {
54
+ return await resolving;
55
+ }
56
+ finally {
57
+ resolving = null;
58
+ }
59
+ }
60
+ return {
61
+ async verifyAccessToken(token) {
62
+ try {
63
+ const key = await resolveKey();
64
+ const { payload } = await jwtVerify(token, key, {
65
+ issuer: opts.issuer,
66
+ audience: opts.audience,
67
+ algorithms: opts.algorithms ?? ['RS256', 'ES256'],
68
+ clockTolerance: opts.clockToleranceSeconds ?? 5,
69
+ });
70
+ return {
71
+ token,
72
+ clientId: String(payload.client_id ?? payload.azp ?? payload.sub ?? ''),
73
+ scopes: extractScopes(payload, opts.scopeClaim),
74
+ expiresAt: typeof payload.exp === 'number' ? payload.exp : undefined,
75
+ extra: payload,
76
+ };
77
+ }
78
+ catch (e) {
79
+ if (e instanceof InvalidTokenError)
80
+ throw e;
81
+ throw new InvalidTokenError(e instanceof Error ? e.message : 'invalid token');
82
+ }
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=jwt-verifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt-verifier.js","sourceRoot":"","sources":["../../src/auth/jwt-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAGpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AA6BtD,SAAS,aAAa,CAAC,OAAgC,EAAE,UAAmB;IAC1E,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAY,EAAE;QACxC,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/D,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3C,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IACF,IAAI,UAAU;QAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,KAAK,IAAI,IAAI;QAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1D,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI;QAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvE,IAAI,CAAC,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAE3E,IAAI,QAAQ,GAA4B,IAAI,CAAC,GAAG,CAAC;IACjD,IAAI,SAAS,GAAgC,IAAI,CAAC;IAElD,KAAK,UAAU,UAAU;QACvB,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,IAAI,SAAS;YAAE,OAAO,SAAS,CAAC;QAChC,SAAS,GAAG,CAAC,KAAK,IAAI,EAAE;YACtB,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;YAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,GAAG,CAAC,MAAM,qBAAqB,CAAC,IAAI,CAAC,MAAM,EAAE;oBACrD,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,SAAS,EAAE,IAAI,CAAC,SAAS;iBAC1B,CAAC,CAED,CAAC;gBACF,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;gBACxB,IAAI,CAAC,OAAO;oBACV,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;YAC7F,CAAC;YACD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC;YAC/F,QAAQ,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,eAAe,EAAE,WAAW,EAAE,CAAC,CAAC;YAClF,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,EAAE,CAAC;QACL,IAAI,CAAC;YACH,OAAO,MAAM,SAAS,CAAC;QACzB,CAAC;gBAAS,CAAC;YACT,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,iBAAiB,CAAC,KAAa;YACnC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,UAAU,EAAE,CAAC;gBAC/B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE;oBAC9C,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC;oBACjD,cAAc,EAAE,IAAI,CAAC,qBAAqB,IAAI,CAAC;iBAChD,CAAC,CAAC;gBACH,OAAO;oBACL,KAAK;oBACL,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;oBACvE,MAAM,EAAE,aAAa,CAAC,OAAkC,EAAE,IAAI,CAAC,UAAU,CAAC;oBAC1E,SAAS,EAAE,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;oBACpE,KAAK,EAAE,OAAkC;iBAC1C,CAAC;YACJ,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,YAAY,iBAAiB;oBAAE,MAAM,CAAC,CAAC;gBAC5C,MAAM,IAAI,iBAAiB,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;YAChF,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';
2
+ export interface DiscoverOptions {
3
+ /** Timeout (ms) per discovery request. Default 10000. Pass 0 to disable. */
4
+ timeoutMs?: number;
5
+ /** Injectable for tests. */
6
+ fetch?: typeof globalThis.fetch;
7
+ }
8
+ /**
9
+ * Discover an Authorization Server's metadata (RFC 8414) from its issuer URL. Tries the
10
+ * OAuth well-known path first, then OIDC discovery. The result feeds `protectMcpServer`
11
+ * (and supplies `jwks_uri` for {@link jwtVerifier}).
12
+ *
13
+ * @throws when neither well-known document can be fetched.
14
+ */
15
+ export declare function discoverOAuthMetadata(issuer: string, opts?: DiscoverOptions): Promise<OAuthMetadata>;
16
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/auth/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AAG9E,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,aAAa,CAAC,CA4BxB"}
@@ -0,0 +1,32 @@
1
+ import { DEFAULT_TIMEOUT_MS, fetchWithTimeout } from '../internal/http.js';
2
+ /**
3
+ * Discover an Authorization Server's metadata (RFC 8414) from its issuer URL. Tries the
4
+ * OAuth well-known path first, then OIDC discovery. The result feeds `protectMcpServer`
5
+ * (and supplies `jwks_uri` for {@link jwtVerifier}).
6
+ *
7
+ * @throws when neither well-known document can be fetched.
8
+ */
9
+ export async function discoverOAuthMetadata(issuer, opts = {}) {
10
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
11
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12
+ const base = issuer.replace(/\/+$/, '');
13
+ const candidates = [
14
+ `${base}/.well-known/oauth-authorization-server`,
15
+ `${base}/.well-known/openid-configuration`,
16
+ ];
17
+ let lastErr;
18
+ for (const url of candidates) {
19
+ try {
20
+ const res = await fetchWithTimeout(fetchImpl, url, { headers: { Accept: 'application/json' } }, timeoutMs);
21
+ if (res.ok)
22
+ return (await res.json());
23
+ lastErr = new Error(`HTTP ${res.status} from ${url}`);
24
+ }
25
+ catch (e) {
26
+ lastErr = e;
27
+ }
28
+ }
29
+ throw new Error(`discoverOAuthMetadata: could not load AS metadata for issuer "${issuer}" ` +
30
+ `(${lastErr instanceof Error ? lastErr.message : String(lastErr)})`);
31
+ }
32
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/auth/metadata.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAS3E;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAc,EACd,OAAwB,EAAE;IAE1B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG;QACjB,GAAG,IAAI,yCAAyC;QAChD,GAAG,IAAI,mCAAmC;KAC3C,CAAC;IAEF,IAAI,OAAgB,CAAC;IACrB,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAChC,SAAS,EACT,GAAG,EACH,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAC3C,SAAS,CACV,CAAC;YACF,IAAI,GAAG,CAAC,EAAE;gBAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAkB,CAAC;YACvD,OAAO,GAAG,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,GAAG,CAAC,CAAC;QACd,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,iEAAiE,MAAM,IAAI;QACzE,IAAI,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CACtE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { Express, RequestHandler } from 'express';
2
+ import type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
3
+ import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';
4
+ export interface ProtectMcpServerOptions {
5
+ /** Your Express app. The RFC 9728 metadata endpoints are mounted on it. */
6
+ app: Express;
7
+ /** The public URL of this MCP server (its OAuth resource identifier). */
8
+ resourceServerUrl: string | URL;
9
+ /** A token verifier — e.g. {@link jwtVerifier} or {@link introspectionVerifier}. */
10
+ verifier: OAuthTokenVerifier;
11
+ /** The Authorization Server metadata. Provide this OR `issuer` to discover it. */
12
+ oauthMetadata?: OAuthMetadata;
13
+ /** Issuer URL to discover AS metadata from when `oauthMetadata` is not given. */
14
+ issuer?: string;
15
+ /** Scopes advertised in Protected Resource Metadata. */
16
+ scopesSupported?: string[];
17
+ /** Scopes a token must carry to be allowed through (enforced on each request). */
18
+ requiredScopes?: string[];
19
+ /** Human-readable resource name for the metadata document. */
20
+ resourceName?: string;
21
+ /** Docs URL advertised in the metadata document. */
22
+ serviceDocumentationUrl?: string | URL;
23
+ /** Injectable fetch for issuer discovery. */
24
+ fetch?: typeof globalThis.fetch;
25
+ }
26
+ export interface ProtectMcpServerResult {
27
+ /** Express middleware that guards your MCP endpoint(s): validates the bearer token and sets `req.auth`. */
28
+ requireAuth: RequestHandler;
29
+ /** The RFC 9728 protected-resource-metadata URL clients discover via the 401 `WWW-Authenticate` header. */
30
+ resourceMetadataUrl: string;
31
+ /** The resolved Authorization Server metadata. */
32
+ metadata: OAuthMetadata;
33
+ }
34
+ /**
35
+ * Turn an Express app into a spec-compliant MCP OAuth 2.1 **Resource Server** in one call:
36
+ * serve Protected Resource Metadata (RFC 9728), and return a `requireAuth` middleware that
37
+ * validates bearer tokens with your verifier and emits a discovery-pointing `WWW-Authenticate`
38
+ * header on 401. You wire `requireAuth` onto your Streamable-HTTP MCP route.
39
+ *
40
+ * @example
41
+ * const { requireAuth } = await protectMcpServer({
42
+ * app, resourceServerUrl: 'https://mcp.example.com',
43
+ * issuer: 'https://auth.example.com',
44
+ * verifier: jwtVerifier({ issuer: 'https://auth.example.com', audience: 'https://mcp.example.com' }),
45
+ * scopesSupported: ['mcp:tools'],
46
+ * });
47
+ * app.post('/mcp', requireAuth, mcpHttpHandler);
48
+ */
49
+ export declare function protectMcpServer(opts: ProtectMcpServerOptions): Promise<ProtectMcpServerResult>;
50
+ //# sourceMappingURL=protect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protect.d.ts","sourceRoot":"","sources":["../../src/auth/protect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mDAAmD,CAAC;AAC5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,2EAA2E;IAC3E,GAAG,EAAE,OAAO,CAAC;IACb,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,GAAG,GAAG,CAAC;IAChC,oFAAoF;IACpF,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,kFAAkF;IAClF,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,uBAAuB,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IACvC,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,2GAA2G;IAC3G,WAAW,EAAE,cAAc,CAAC;IAC5B,2GAA2G;IAC3G,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kDAAkD;IAClD,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA2BrG"}
@@ -0,0 +1,42 @@
1
+ import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
2
+ import { mcpAuthMetadataRouter, getOAuthProtectedResourceMetadataUrl, } from '@modelcontextprotocol/sdk/server/auth/router.js';
3
+ import { discoverOAuthMetadata } from './metadata.js';
4
+ /**
5
+ * Turn an Express app into a spec-compliant MCP OAuth 2.1 **Resource Server** in one call:
6
+ * serve Protected Resource Metadata (RFC 9728), and return a `requireAuth` middleware that
7
+ * validates bearer tokens with your verifier and emits a discovery-pointing `WWW-Authenticate`
8
+ * header on 401. You wire `requireAuth` onto your Streamable-HTTP MCP route.
9
+ *
10
+ * @example
11
+ * const { requireAuth } = await protectMcpServer({
12
+ * app, resourceServerUrl: 'https://mcp.example.com',
13
+ * issuer: 'https://auth.example.com',
14
+ * verifier: jwtVerifier({ issuer: 'https://auth.example.com', audience: 'https://mcp.example.com' }),
15
+ * scopesSupported: ['mcp:tools'],
16
+ * });
17
+ * app.post('/mcp', requireAuth, mcpHttpHandler);
18
+ */
19
+ export async function protectMcpServer(opts) {
20
+ const resourceServerUrl = new URL(opts.resourceServerUrl.toString());
21
+ const metadata = opts.oauthMetadata ??
22
+ (opts.issuer ? await discoverOAuthMetadata(opts.issuer, { fetch: opts.fetch }) : undefined);
23
+ if (!metadata)
24
+ throw new Error('protectMcpServer: provide either `oauthMetadata` or `issuer`');
25
+ opts.app.use(mcpAuthMetadataRouter({
26
+ oauthMetadata: metadata,
27
+ resourceServerUrl,
28
+ scopesSupported: opts.scopesSupported,
29
+ resourceName: opts.resourceName,
30
+ serviceDocumentationUrl: opts.serviceDocumentationUrl
31
+ ? new URL(opts.serviceDocumentationUrl.toString())
32
+ : undefined,
33
+ }));
34
+ const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(resourceServerUrl);
35
+ const requireAuth = requireBearerAuth({
36
+ verifier: opts.verifier,
37
+ requiredScopes: opts.requiredScopes,
38
+ resourceMetadataUrl,
39
+ });
40
+ return { requireAuth, resourceMetadataUrl, metadata };
41
+ }
42
+ //# sourceMappingURL=protect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protect.js","sourceRoot":"","sources":["../../src/auth/protect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gEAAgE,CAAC;AACnG,OAAO,EACL,qBAAqB,EACrB,oCAAoC,GACrC,MAAM,iDAAiD,CAAC;AAGzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAkCtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAA6B;IAClE,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrE,MAAM,QAAQ,GACZ,IAAI,CAAC,aAAa;QAClB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,qBAAqB,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC9F,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAE/F,IAAI,CAAC,GAAG,CAAC,GAAG,CACV,qBAAqB,CAAC;QACpB,aAAa,EAAE,QAAQ;QACvB,iBAAiB;QACjB,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,uBAAuB,EAAE,IAAI,CAAC,uBAAuB;YACnD,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,uBAAuB,CAAC,QAAQ,EAAE,CAAC;YAClD,CAAC,CAAC,SAAS;KACd,CAAC,CACH,CAAC;IAEF,MAAM,mBAAmB,GAAG,oCAAoC,CAAC,iBAAiB,CAAC,CAAC;IACpF,MAAM,WAAW,GAAG,iBAAiB,CAAC;QACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,mBAAmB;KACpB,CAAC,CAAC;IAEH,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,QAAQ,EAAE,CAAC;AACxD,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @koduhai/mcp-kit — the two things people get wrong building MCP servers: auth and
3
+ * versioning, solved.
4
+ *
5
+ * This root entry re-exports the dependency-free modules (`upstream`, `versioning`).
6
+ * The server-side OAuth resource-server helpers live at `@koduhai/mcp-kit/auth` so that
7
+ * their optional peers (`@modelcontextprotocol/sdk`, `express`, `jose`) are only loaded
8
+ * when you actually build a remote/HTTP server.
9
+ */
10
+ export * from './upstream/index.js';
11
+ export * from './versioning/index.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @koduhai/mcp-kit — the two things people get wrong building MCP servers: auth and
3
+ * versioning, solved.
4
+ *
5
+ * This root entry re-exports the dependency-free modules (`upstream`, `versioning`).
6
+ * The server-side OAuth resource-server helpers live at `@koduhai/mcp-kit/auth` so that
7
+ * their optional peers (`@modelcontextprotocol/sdk`, `express`, `jose`) are only loaded
8
+ * when you actually build a remote/HTTP server.
9
+ */
10
+ export * from './upstream/index.js';
11
+ export * from './versioning/index.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared helpers for the library's own outbound (control-plane) requests:
3
+ * token endpoints, introspection, and AS metadata discovery. Zero dependencies.
4
+ */
5
+ /** Default timeout (ms) applied to the library's own outbound requests. */
6
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
7
+ type FetchInput = Parameters<typeof globalThis.fetch>[0];
8
+ /**
9
+ * `fetch` with a timeout. Aborts the request after `timeoutMs` and reports a
10
+ * clear error rather than hanging on an unresponsive endpoint. A caller-supplied
11
+ * `init.signal` is honored too: whichever aborts first wins. Pass a non-positive
12
+ * or non-finite `timeoutMs` to disable the timeout entirely.
13
+ */
14
+ export declare function fetchWithTimeout(fetchImpl: typeof globalThis.fetch, input: FetchInput, init?: RequestInit, timeoutMs?: number): Promise<Response>;
15
+ export {};
16
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/internal/http.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,OAAO,UAAU,CAAC,KAAK,EAClC,KAAK,EAAE,UAAU,EACjB,IAAI,GAAE,WAAgB,EACtB,SAAS,GAAE,MAA2B,GACrC,OAAO,CAAC,QAAQ,CAAC,CAWnB"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared helpers for the library's own outbound (control-plane) requests:
3
+ * token endpoints, introspection, and AS metadata discovery. Zero dependencies.
4
+ */
5
+ /** Default timeout (ms) applied to the library's own outbound requests. */
6
+ export const DEFAULT_TIMEOUT_MS = 10_000;
7
+ /**
8
+ * `fetch` with a timeout. Aborts the request after `timeoutMs` and reports a
9
+ * clear error rather than hanging on an unresponsive endpoint. A caller-supplied
10
+ * `init.signal` is honored too: whichever aborts first wins. Pass a non-positive
11
+ * or non-finite `timeoutMs` to disable the timeout entirely.
12
+ */
13
+ export async function fetchWithTimeout(fetchImpl, input, init = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
14
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
15
+ return fetchImpl(input, init);
16
+ const timeout = AbortSignal.timeout(timeoutMs);
17
+ const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
18
+ try {
19
+ return await fetchImpl(input, { ...init, signal });
20
+ }
21
+ catch (e) {
22
+ // Distinguish our timeout from a caller-initiated abort or a network error.
23
+ if (timeout.aborted)
24
+ throw new Error(`request timed out after ${timeoutMs}ms`, { cause: e });
25
+ throw e;
26
+ }
27
+ }
28
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/internal/http.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAIzC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,SAAkC,EAClC,KAAiB,EACjB,OAAoB,EAAE,EACtB,YAAoB,kBAAkB;IAEtC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACjF,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC/E,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,4EAA4E;QAC5E,IAAI,OAAO,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,SAAS,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7F,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC"}
@@ -0,0 +1,70 @@
1
+ /** A strategy that produces the headers to attach to each upstream request. */
2
+ export interface UpstreamAuth {
3
+ /** Returns headers to merge into every upstream request. May refresh tokens internally. */
4
+ headers(): Promise<Record<string, string>>;
5
+ }
6
+ export interface ApiKeyAuthOptions {
7
+ /** The API key / token value. */
8
+ key: string;
9
+ /** Header to send it in. Default `Authorization`. */
10
+ header?: string;
11
+ /**
12
+ * Scheme prefix for the value, e.g. `Bearer` -> `Authorization: Bearer <key>`.
13
+ * Defaults to `Bearer` when the header is `Authorization`, otherwise no prefix
14
+ * (e.g. `X-Api-Key: <key>`). Pass `null` to force a raw value.
15
+ */
16
+ scheme?: string | null;
17
+ }
18
+ /** Static API-key auth. The most common MCP-server-to-API pattern. */
19
+ export declare function apiKeyAuth(opts: ApiKeyAuthOptions): UpstreamAuth;
20
+ /** A bearer token, or a (possibly async) function that resolves one per call. */
21
+ export type TokenSource = string | (() => string | Promise<string>);
22
+ /** Bearer-token auth. Pass a function when the token rotates or is fetched lazily. */
23
+ export declare function bearerAuth(token: TokenSource, header?: string): UpstreamAuth;
24
+ export interface ClientCredentialsOptions {
25
+ /** The OAuth token endpoint (e.g. `https://issuer/oauth/token`). */
26
+ tokenUrl: string;
27
+ clientId: string;
28
+ clientSecret: string;
29
+ /** Space-delimited scopes to request. */
30
+ scope?: string;
31
+ /** RFC 8707 audience / resource indicator, if your IdP needs it (e.g. Auth0). */
32
+ audience?: string;
33
+ /** Refresh this many seconds before the token actually expires. Default 60. */
34
+ refreshSkewSeconds?: number;
35
+ /** Timeout (ms) for the token request. Default 10000. Pass 0 to disable. */
36
+ timeoutMs?: number;
37
+ /** Injectable for tests. */
38
+ fetch?: typeof globalThis.fetch;
39
+ /** Injectable clock (ms). */
40
+ now?: () => number;
41
+ }
42
+ /**
43
+ * OAuth 2.0 client-credentials auth (machine-to-machine). Fetches an access token,
44
+ * caches it, and transparently refreshes before expiry. Concurrent callers during a
45
+ * refresh share one in-flight request.
46
+ */
47
+ export declare function clientCredentialsAuth(opts: ClientCredentialsOptions): UpstreamAuth;
48
+ /** A source of static-ish headers: an object, or a (possibly async) function returning one. */
49
+ export type HeaderSource = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
50
+ export interface UpstreamFetchOptions {
51
+ /** Base URL prepended to relative request paths. */
52
+ baseUrl?: string;
53
+ /** Auth strategy; its headers are merged into every request. */
54
+ auth?: UpstreamAuth;
55
+ /** Extra headers merged into every request (e.g. a versioning header source). */
56
+ headers?: HeaderSource;
57
+ /**
58
+ * Per-request timeout (ms). Off by default so long-running upstream calls are
59
+ * never cut short unexpectedly; set it to opt in. Pass 0 to disable explicitly.
60
+ */
61
+ timeoutMs?: number;
62
+ /** Injectable for tests. */
63
+ fetch?: typeof globalThis.fetch;
64
+ }
65
+ /**
66
+ * Wrap `fetch` so every request carries your auth + standing headers. Returns a
67
+ * drop-in `fetch` your tool handlers can call with relative paths.
68
+ */
69
+ export declare function createUpstreamFetch(opts: UpstreamFetchOptions): typeof globalThis.fetch;
70
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/upstream/index.ts"],"names":[],"mappings":"AAMA,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,2FAA2F;IAC3F,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,YAAY,CAQhE;AAED,iFAAiF;AACjF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAEpE,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,SAAkB,GAAG,YAAY,CAQrF;AAED,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,wBAAwB,GAAG,YAAY,CAiElF;AAED,+FAA+F;AAC/F,MAAM,MAAM,YAAY,GACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtB,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAMrE,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,iFAAiF;IACjF,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,UAAU,CAAC,KAAK,CAmBvF"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Upstream auth: how your MCP server authenticates to the API/service it wraps.
3
+ * Zero dependencies, zero transport assumptions. Works with stdio or HTTP servers.
4
+ */
5
+ import { DEFAULT_TIMEOUT_MS, fetchWithTimeout } from '../internal/http.js';
6
+ /** Static API-key auth. The most common MCP-server-to-API pattern. */
7
+ export function apiKeyAuth(opts) {
8
+ if (!opts.key)
9
+ throw new Error('apiKeyAuth: `key` is required');
10
+ const header = opts.header ?? 'Authorization';
11
+ const scheme = opts.scheme === undefined ? (header.toLowerCase() === 'authorization' ? 'Bearer' : null) : opts.scheme;
12
+ const value = scheme ? `${scheme} ${opts.key}` : opts.key;
13
+ const headers = { [header]: value };
14
+ return { headers: async () => ({ ...headers }) };
15
+ }
16
+ /** Bearer-token auth. Pass a function when the token rotates or is fetched lazily. */
17
+ export function bearerAuth(token, header = 'Authorization') {
18
+ return {
19
+ async headers() {
20
+ const t = typeof token === 'function' ? await token() : token;
21
+ if (!t)
22
+ throw new Error('bearerAuth: token resolved empty');
23
+ return { [header]: `Bearer ${t}` };
24
+ },
25
+ };
26
+ }
27
+ /**
28
+ * OAuth 2.0 client-credentials auth (machine-to-machine). Fetches an access token,
29
+ * caches it, and transparently refreshes before expiry. Concurrent callers during a
30
+ * refresh share one in-flight request.
31
+ */
32
+ export function clientCredentialsAuth(opts) {
33
+ if (!opts.tokenUrl || !opts.clientId || !opts.clientSecret) {
34
+ throw new Error('clientCredentialsAuth: tokenUrl, clientId and clientSecret are required');
35
+ }
36
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
37
+ const now = opts.now ?? (() => Date.now());
38
+ const skewMs = (opts.refreshSkewSeconds ?? 60) * 1000;
39
+ let cached = null;
40
+ let inflight = null;
41
+ async function fetchToken() {
42
+ const body = new URLSearchParams({
43
+ grant_type: 'client_credentials',
44
+ client_id: opts.clientId,
45
+ client_secret: opts.clientSecret,
46
+ });
47
+ if (opts.scope)
48
+ body.set('scope', opts.scope);
49
+ if (opts.audience)
50
+ body.set('audience', opts.audience);
51
+ const res = await fetchWithTimeout(fetchImpl, opts.tokenUrl, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
54
+ body,
55
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
56
+ const text = await res.text();
57
+ let json;
58
+ try {
59
+ json = text ? JSON.parse(text) : {};
60
+ }
61
+ catch {
62
+ json = {};
63
+ }
64
+ if (!res.ok) {
65
+ throw new Error(`clientCredentialsAuth: token request failed (${res.status}): ${String(json.error ?? text)}`);
66
+ }
67
+ const token = json.access_token;
68
+ if (typeof token !== 'string' || !token) {
69
+ throw new Error('clientCredentialsAuth: response missing access_token');
70
+ }
71
+ const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : 3600;
72
+ cached = { token, expiresAtMs: now() + expiresIn * 1000 };
73
+ return token;
74
+ }
75
+ async function getToken() {
76
+ if (cached && now() < cached.expiresAtMs - skewMs)
77
+ return cached.token;
78
+ if (inflight)
79
+ return inflight;
80
+ inflight = fetchToken().finally(() => {
81
+ inflight = null;
82
+ });
83
+ return inflight;
84
+ }
85
+ return {
86
+ async headers() {
87
+ return { Authorization: `Bearer ${await getToken()}` };
88
+ },
89
+ };
90
+ }
91
+ async function resolveHeaderSource(src) {
92
+ return typeof src === 'function' ? src() : src;
93
+ }
94
+ /**
95
+ * Wrap `fetch` so every request carries your auth + standing headers. Returns a
96
+ * drop-in `fetch` your tool handlers can call with relative paths.
97
+ */
98
+ export function createUpstreamFetch(opts) {
99
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
100
+ const base = opts.baseUrl?.replace(/\/+$/, '');
101
+ return (async (input, init = {}) => {
102
+ const authHeaders = opts.auth ? await opts.auth.headers() : {};
103
+ const standing = opts.headers ? await resolveHeaderSource(opts.headers) : {};
104
+ const merged = new Headers(init.headers);
105
+ for (const [k, v] of Object.entries({ ...standing, ...authHeaders }))
106
+ merged.set(k, v);
107
+ let target = input;
108
+ if (base && typeof input === 'string' && input.startsWith('/'))
109
+ target = base + input;
110
+ const req = { ...init, headers: merged };
111
+ return opts.timeoutMs != null
112
+ ? fetchWithTimeout(fetchImpl, target, req, opts.timeoutMs)
113
+ : fetchImpl(target, req);
114
+ });
115
+ }
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/upstream/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * API versioning for MCP servers: pin the upstream API version, send it on every
3
+ * request, expose it to the agent via a `get_version` tool, and detect drift between
4
+ * the version your server speaks and the API's current version.
5
+ */
6
+ export interface VersioningOptions {
7
+ /** Header used to send the pinned version upstream, e.g. `Api-Version` or `Koduh-Version`. */
8
+ header: string;
9
+ /** The version this server pins and sends. */
10
+ version: string;
11
+ /** The API's current/latest version, if known. Used to flag drift. */
12
+ current?: string;
13
+ /** Versions this server is known-compatible with. If set, `version` must be one of them. */
14
+ supported?: string[];
15
+ }
16
+ export interface DriftInfo {
17
+ /** True when the pinned version differs from the API's current version. */
18
+ behind: boolean;
19
+ /** True when `supported` is set and the pinned version is not in it. */
20
+ unsupported: boolean;
21
+ /** A human-readable warning, or null when everything lines up. */
22
+ message: string | null;
23
+ }
24
+ export interface Versioning {
25
+ readonly header: string;
26
+ readonly version: string;
27
+ readonly current?: string;
28
+ readonly supported?: readonly string[];
29
+ /** Headers to merge into upstream requests (a `HeaderSource` for `createUpstreamFetch`). */
30
+ headers(): Record<string, string>;
31
+ /** Evaluate drift between the pinned version and the API's current version. */
32
+ drift(): DriftInfo;
33
+ }
34
+ /** Build a {@link Versioning} from options. Throws if the pinned version is unsupported. */
35
+ export declare function apiVersioning(opts: VersioningOptions): Versioning;
36
+ /** A transport-agnostic tool definition you can register on any MCP server. */
37
+ export interface ToolDescriptor {
38
+ name: string;
39
+ description: string;
40
+ /** JSON Schema for the tool input. */
41
+ inputSchema: Record<string, unknown>;
42
+ handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
43
+ }
44
+ /**
45
+ * A `get_version` tool that reports which API version the server speaks. Agents
46
+ * (and humans debugging) constantly need this; surfacing it removes a class of
47
+ * "why is the response shaped differently" confusion.
48
+ */
49
+ export declare function versionTool(v: Versioning, name?: string): ToolDescriptor;
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/versioning/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,iBAAiB;IAChC,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,WAAW,EAAE,OAAO,CAAC;IACrB,kEAAkE;IAClE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,4FAA4F;IAC5F,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,+EAA+E;IAC/E,KAAK,IAAI,SAAS,CAAC;CACpB;AAED,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CA6BjE;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACxE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,SAAgB,GAAG,cAAc,CAkB/E"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * API versioning for MCP servers: pin the upstream API version, send it on every
3
+ * request, expose it to the agent via a `get_version` tool, and detect drift between
4
+ * the version your server speaks and the API's current version.
5
+ */
6
+ /** Build a {@link Versioning} from options. Throws if the pinned version is unsupported. */
7
+ export function apiVersioning(opts) {
8
+ if (!opts.header)
9
+ throw new Error('apiVersioning: `header` is required');
10
+ if (!opts.version)
11
+ throw new Error('apiVersioning: `version` is required');
12
+ if (opts.supported && !opts.supported.includes(opts.version)) {
13
+ throw new Error(`apiVersioning: pinned version "${opts.version}" is not in supported [${opts.supported.join(', ')}]`);
14
+ }
15
+ const header = opts.header;
16
+ const value = opts.version;
17
+ return {
18
+ header,
19
+ version: opts.version,
20
+ current: opts.current,
21
+ supported: opts.supported,
22
+ headers: () => ({ [header]: value }),
23
+ drift() {
24
+ const behind = opts.current != null && opts.current !== opts.version;
25
+ const unsupported = opts.supported != null && !opts.supported.includes(opts.version);
26
+ let message = null;
27
+ if (unsupported) {
28
+ message = `pinned API version ${opts.version} is not in the supported set [${(opts.supported ?? []).join(', ')}]`;
29
+ }
30
+ else if (behind) {
31
+ message = `pinned API version ${opts.version} is behind the current API version ${opts.current}`;
32
+ }
33
+ return { behind, unsupported, message };
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * A `get_version` tool that reports which API version the server speaks. Agents
39
+ * (and humans debugging) constantly need this; surfacing it removes a class of
40
+ * "why is the response shaped differently" confusion.
41
+ */
42
+ export function versionTool(v, name = 'get_version') {
43
+ return {
44
+ name,
45
+ description: 'Report the upstream API version this MCP server is pinned to, the header it is sent in, ' +
46
+ 'the current API version (if known), and whether the two have drifted.',
47
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
48
+ handler: () => {
49
+ const drift = v.drift();
50
+ return {
51
+ version: v.version,
52
+ header: v.header,
53
+ current: v.current ?? null,
54
+ supported: v.supported ?? null,
55
+ drift: drift.message,
56
+ };
57
+ },
58
+ };
59
+ }
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/versioning/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiCH,4FAA4F;AAC5F,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzE,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC3E,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CACb,kCAAkC,IAAI,CAAC,OAAO,0BAA0B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACrG,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC;IAE3B,OAAO;QACL,MAAM;QACN,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;QACpC,KAAK;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,CAAC;YACrE,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrF,IAAI,OAAO,GAAkB,IAAI,CAAC;YAClC,IAAI,WAAW,EAAE,CAAC;gBAChB,OAAO,GAAG,sBAAsB,IAAI,CAAC,OAAO,iCAAiC,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YACpH,CAAC;iBAAM,IAAI,MAAM,EAAE,CAAC;gBAClB,OAAO,GAAG,sBAAsB,IAAI,CAAC,OAAO,sCAAsC,IAAI,CAAC,OAAO,EAAE,CAAC;YACnG,CAAC;YACD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;QAC1C,CAAC;KACF,CAAC;AACJ,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,CAAa,EAAE,IAAI,GAAG,aAAa;IAC7D,OAAO;QACL,IAAI;QACJ,WAAW,EACT,0FAA0F;YAC1F,uEAAuE;QACzE,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,oBAAoB,EAAE,KAAK,EAAE;QAC5E,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;YACxB,OAAO;gBACL,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI;gBAC1B,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,IAAI;gBAC9B,KAAK,EAAE,KAAK,CAAC,OAAO;aACrB,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@koduhai/mcp-kit",
3
+ "version": "0.1.0",
4
+ "description": "Auth and versioning for MCP servers, solved. Upstream API auth (API key / bearer / OAuth client-credentials), API versioning with a get_version tool, and a one-call OAuth 2.1 Resource Server (JWKS + introspection verifiers, RFC 9728 metadata) on top of the MCP SDK.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "mcp-server",
9
+ "oauth",
10
+ "oauth2",
11
+ "authorization",
12
+ "jwt",
13
+ "jwks",
14
+ "token-introspection",
15
+ "ai-agents",
16
+ "llm",
17
+ "versioning"
18
+ ],
19
+ "license": "MIT",
20
+ "author": "Koduhai",
21
+ "homepage": "https://github.com/koduhai/mcp-kit#readme",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/koduhai/mcp-kit.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/koduhai/mcp-kit/issues"
28
+ },
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ },
35
+ "./upstream": {
36
+ "types": "./dist/upstream/index.d.ts",
37
+ "import": "./dist/upstream/index.js"
38
+ },
39
+ "./versioning": {
40
+ "types": "./dist/versioning/index.d.ts",
41
+ "import": "./dist/versioning/index.js"
42
+ },
43
+ "./auth": {
44
+ "types": "./dist/auth/index.d.ts",
45
+ "import": "./dist/auth/index.js"
46
+ }
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "README.md",
51
+ "LICENSE"
52
+ ],
53
+ "engines": {
54
+ "node": ">=20"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc",
58
+ "typecheck": "tsc --noEmit",
59
+ "lint": "eslint .",
60
+ "format": "prettier --write .",
61
+ "format:check": "prettier --check .",
62
+ "test": "vitest run",
63
+ "prepublishOnly": "npm run build"
64
+ },
65
+ "peerDependencies": {
66
+ "@modelcontextprotocol/sdk": ">=1.20.0",
67
+ "express": "^4.19.0 || ^5.0.0",
68
+ "jose": "^5.9.0 || ^6.0.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@modelcontextprotocol/sdk": {
72
+ "optional": true
73
+ },
74
+ "express": {
75
+ "optional": true
76
+ },
77
+ "jose": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@eslint/js": "^10.0.1",
83
+ "@modelcontextprotocol/sdk": "^1.29.0",
84
+ "@types/express": "^5.0.0",
85
+ "@types/node": "^25.9.3",
86
+ "@types/supertest": "^7.2.0",
87
+ "eslint": "^10.4.1",
88
+ "eslint-config-prettier": "^10.1.8",
89
+ "express": "^5.0.0",
90
+ "jose": "^6.0.0",
91
+ "prettier": "^3.8.4",
92
+ "supertest": "^7.0.0",
93
+ "typescript": "^6.0.3",
94
+ "typescript-eslint": "^8.61.0",
95
+ "vitest": "^4.1.8"
96
+ }
97
+ }