mcp-auth-wrapper 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Adam Jones (domdomegg)
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,286 @@
1
+ # mcp-auth-wrapper
2
+
3
+ > Turn any local [MCP server](https://modelcontextprotocol.io/) into a multi-tenant hosted remote MCP, with per-user credentials.
4
+
5
+ Connecting AI agents to tools can help you and your team be more productive. [MCP servers](https://modelcontextprotocol.io/docs/learn/server-concepts) are a great way to do this — but many of them only run locally and require per-user setup (like API keys) that can be difficult for non-technical users. What if you want your whole team to use one, each with their own credentials?
6
+
7
+ mcp-auth-wrapper lets you do exactly this: it hosts any MCP server for multiple users with auth and configuration. Your team can login via your existing identity provider (Google Workspace, Microsoft Entra ID, Okta, Auth0, Keycloak, etc.), provide their per-user config in a simple form interface, and mcp-auth-wrapper will automatically spin up the MCP for each user.
8
+
9
+ mcp-auth-wrapper works with Claude.ai, Claude Code and any other MCP client that supports remote servers.
10
+
11
+ For those interested in the technical details, mcp-auth-wrapper wraps stdio MCP servers that accept environment variables as [streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) servers with [OAuth 2.1](https://oauth.net/2.1/) / [OpenID Connect](https://openid.net/developers/how-connect-works/). By default, user credentials are held only in memory but can be persisted to sqlite - it is recommended to use an encrypted volume for storage if doing this. mcp-auth-wrapper is horizontally scalable for larger deployments, and can be run easily with npx, Docker, Docker Compose or Kubernetes.
12
+
13
+ ## Usage
14
+
15
+ Set `MCP_AUTH_WRAPPER_CONFIG` to a JSON config object and run:
16
+
17
+ ```bash
18
+ MCP_AUTH_WRAPPER_CONFIG='{
19
+ "command": ["npx", "-y", "airtable-mcp-server"],
20
+ "auth": {"issuer": "https://auth.example.com"}
21
+ }' npx mcp-auth-wrapper
22
+ ```
23
+
24
+ This starts an HTTP MCP server on localhost:3000. When a user connects, they'll be redirected to your login provider. After logging in, if you've configured per-user environment variables (like API keys), they'll see a form to enter them. Then they're connected to their own MCP server process.
25
+
26
+ <details>
27
+ <summary>Other configuration methods</summary>
28
+
29
+ The env var can also point to a file path:
30
+
31
+ ```bash
32
+ MCP_AUTH_WRAPPER_CONFIG=/path/to/config.json npx mcp-auth-wrapper
33
+ ```
34
+
35
+ Or create `mcp-auth-wrapper.config.json` in the working directory — it's picked up automatically:
36
+
37
+ ```bash
38
+ npx mcp-auth-wrapper
39
+ ```
40
+
41
+ </details>
42
+
43
+ <details>
44
+ <summary>Running with Docker</summary>
45
+
46
+ ```bash
47
+ docker run -e 'MCP_AUTH_WRAPPER_CONFIG={"command":["npx","-y","airtable-mcp-server"],"auth":{"issuer":"https://auth.example.com"}}' -p 3000:3000 ghcr.io/domdomegg/mcp-auth-wrapper
48
+ ```
49
+
50
+ </details>
51
+
52
+ ### Config
53
+
54
+ Only `command` and `auth.issuer` are required. Everything else has sensible defaults.
55
+
56
+ A full example:
57
+
58
+ ```json
59
+ {
60
+ "command": ["npx", "-y", "airtable-mcp-server"],
61
+ "auth": {
62
+ "issuer": "https://keycloak.example.com/realms/myrealm",
63
+ "clientId": "my-wrapper",
64
+ "clientSecret": "...",
65
+ "scopes": ["openid", "profile"],
66
+ "userClaim": "preferred_username"
67
+ },
68
+ "envBase": {"NODE_ENV": "production"},
69
+ "envPerUser": [
70
+ {"name": "AIRTABLE_API_KEY", "label": "Airtable API Key", "secret": true}
71
+ ],
72
+ "storage": "/data/mcp.sqlite",
73
+ "port": 3000,
74
+ "host": "0.0.0.0",
75
+ "issuerUrl": "https://mcp.example.com",
76
+ "secret": "a-fixed-signing-key"
77
+ }
78
+ ```
79
+
80
+ | Field | Required | Description |
81
+ |-------|----------|-------------|
82
+ | `command` | Yes | Command to spawn the MCP server, as an array (e.g. `["npx", "-y", "some-server"]`). |
83
+ | `auth.issuer` | Yes | Your login provider's URL. Must support [OpenID Connect discovery](https://openid.net/specs/openid-connect-discovery-1_0.html). |
84
+ | `auth.clientId` | No | Client ID registered with your login provider. Defaults to `"mcp-auth-wrapper"`. |
85
+ | `auth.clientSecret` | No | Client secret. Omit for public clients. |
86
+ | `auth.scopes` | No | Scopes to request during login. Defaults to `["openid"]`. |
87
+ | `auth.userClaim` | No | Which field from the login token identifies the user. Defaults to `"sub"`. |
88
+ | `envBase` | No | Environment variables shared across all user processes. |
89
+ | `envPerUser` | No | Per-user env vars to collect during first login (e.g. API keys). Each has `name`, `label`, optional `description` and `secret`. |
90
+ | `storage` | No | Where to store user params: `"memory"` (default), a SQLite file path, or an inline object (see [below](#other-examples)). |
91
+ | `port` | No | Port to listen on. Defaults to `3000`. |
92
+ | `host` | No | Host to bind to. Defaults to `0.0.0.0`. |
93
+ | `issuerUrl` | No | Public URL of this server. Required when behind a reverse proxy. |
94
+ | `secret` | No | Signing key for tokens. Random if not set. Set a fixed value to survive restarts. |
95
+
96
+ Users can update their per-user env vars at any time via a **reconfigure** tool that's automatically added to the MCP server's tool list.
97
+
98
+ <details>
99
+ <summary>Advanced: scaling and persistence</summary>
100
+
101
+ All auth state (tokens, sessions, in-flight logins) is stateless — tokens are self-contained encrypted blobs and each request gets a fresh transport. Nothing is stored server-side except user params (in `storage`) and the process pool (one subprocess per user).
102
+
103
+ To survive restarts, set `secret` to a fixed value and use a SQLite file or inline storage for user params.
104
+
105
+ To run multiple instances behind a load balancer, set `secret` to the same value across instances and point `storage` at a shared SQLite file (or use inline storage). If a user hits a different instance, it just spawns a new subprocess — this is transparent for stateless MCPs.
106
+
107
+ </details>
108
+
109
+ ### Login provider examples
110
+
111
+ <details>
112
+ <summary>Google Workspace</summary>
113
+
114
+ ```json
115
+ {
116
+ "command": ["npx", "-y", "some-mcp-server"],
117
+ "auth": {
118
+ "issuer": "https://accounts.google.com",
119
+ "clientId": "...",
120
+ "clientSecret": "..."
121
+ },
122
+ "envPerUser": [{"name": "API_KEY", "label": "API Key", "secret": true}]
123
+ }
124
+ ```
125
+
126
+ Create OAuth 2.0 credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Choose "Web application", add `https://<wrapper-host>/callback` as an authorized redirect URI. To restrict access to your organization, configure the OAuth consent screen as "Internal".
127
+
128
+ </details>
129
+
130
+ <details>
131
+ <summary>Microsoft Entra ID</summary>
132
+
133
+ ```json
134
+ {
135
+ "command": ["npx", "-y", "some-mcp-server"],
136
+ "auth": {
137
+ "issuer": "https://login.microsoftonline.com/<tenant-id>/v2.0",
138
+ "clientId": "...",
139
+ "clientSecret": "..."
140
+ },
141
+ "envPerUser": [{"name": "API_KEY", "label": "API Key", "secret": true}]
142
+ }
143
+ ```
144
+
145
+ Register an application in the [Azure portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps). Add `https://<wrapper-host>/callback` as a redirect URI under "Web". Create a client secret under "Certificates & secrets". Replace `<tenant-id>` with your directory (tenant) ID.
146
+
147
+ </details>
148
+
149
+ <details>
150
+ <summary>Okta</summary>
151
+
152
+ ```json
153
+ {
154
+ "command": ["npx", "-y", "some-mcp-server"],
155
+ "auth": {
156
+ "issuer": "https://your-org.okta.com",
157
+ "clientId": "...",
158
+ "clientSecret": "..."
159
+ },
160
+ "envPerUser": [{"name": "API_KEY", "label": "API Key", "secret": true}]
161
+ }
162
+ ```
163
+
164
+ Create a Web Application in Okta. Set the sign-in redirect URI to `https://<wrapper-host>/callback`. The issuer URL is your Okta org URL (or a custom authorization server URL if you use one).
165
+
166
+ </details>
167
+
168
+ <details>
169
+ <summary>Keycloak</summary>
170
+
171
+ ```json
172
+ {
173
+ "command": ["npx", "-y", "some-mcp-server"],
174
+ "auth": {
175
+ "issuer": "https://keycloak.example.com/realms/myrealm",
176
+ "clientSecret": "..."
177
+ },
178
+ "envPerUser": [{"name": "API_KEY", "label": "API Key", "secret": true}]
179
+ }
180
+ ```
181
+
182
+ Create an OpenID Connect client in your Keycloak realm with client ID `mcp-auth-wrapper` (or set `auth.clientId` to match). Set the redirect URI to `https://<wrapper-host>/callback`. Users are identified by `sub` (Keycloak user ID) by default. Set `auth.userClaim` to `preferred_username` to match by username instead.
183
+
184
+ </details>
185
+
186
+ <details>
187
+ <summary>Auth0</summary>
188
+
189
+ ```json
190
+ {
191
+ "command": ["npx", "-y", "some-mcp-server"],
192
+ "auth": {
193
+ "issuer": "https://your-tenant.auth0.com",
194
+ "clientId": "...",
195
+ "clientSecret": "..."
196
+ },
197
+ "envPerUser": [{"name": "API_KEY", "label": "API Key", "secret": true}]
198
+ }
199
+ ```
200
+
201
+ Create a Regular Web Application in Auth0. Add `https://<wrapper-host>/callback` as an allowed callback URL. Set `auth.clientId` to the Auth0 application's client ID. The `sub` claim in Auth0 is typically prefixed with the connection type (e.g. `auth0|abc123`).
202
+
203
+ </details>
204
+
205
+ <details>
206
+ <summary>Authentik</summary>
207
+
208
+ ```json
209
+ {
210
+ "command": ["npx", "-y", "some-mcp-server"],
211
+ "auth": {
212
+ "issuer": "https://authentik.example.com/application/o/myapp/",
213
+ "clientSecret": "...",
214
+ "userClaim": "preferred_username"
215
+ },
216
+ "envPerUser": [{"name": "API_KEY", "label": "API Key", "secret": true}]
217
+ }
218
+ ```
219
+
220
+ Create an OAuth2/OpenID Provider in Authentik with client ID `mcp-auth-wrapper` (or set `auth.clientId` to match). Set the redirect URI to `https://<wrapper-host>/callback`.
221
+
222
+ </details>
223
+
224
+ <details>
225
+ <summary>Home Assistant (via hass-oidc-provider)</summary>
226
+
227
+ Home Assistant doesn't natively support OpenID Connect. Use [hass-oidc-provider](https://github.com/domdomegg/hass-oidc-provider) to bridge the gap — it runs alongside Home Assistant and adds the missing pieces.
228
+
229
+ ```json
230
+ {
231
+ "command": ["npx", "-y", "some-mcp-server"],
232
+ "auth": {
233
+ "issuer": "https://hass-oidc-provider.example.com"
234
+ },
235
+ "envPerUser": [{"name": "API_KEY", "label": "API Key", "secret": true}]
236
+ }
237
+ ```
238
+
239
+ Point `auth.issuer` at your hass-oidc-provider instance (not Home Assistant directly). The `sub` claim is the Home Assistant user ID. No `clientId` or `clientSecret` needed.
240
+
241
+ </details>
242
+
243
+ ### Other examples
244
+
245
+ <details>
246
+ <summary>Inline users (no self-registration)</summary>
247
+
248
+ By default, users enter their own credentials (e.g. API keys) via a form during first login, and can update them later via the reconfigure tool. If you'd rather hardcode all users upfront, use an inline `storage` object:
249
+
250
+ ```json
251
+ {
252
+ "command": ["npx", "-y", "some-mcp-server"],
253
+ "auth": {
254
+ "issuer": "https://auth.example.com",
255
+ "clientSecret": "..."
256
+ },
257
+ "storage": {
258
+ "adam": {"API_KEY": "patXXX_adam"},
259
+ "bob": {"API_KEY": "patXXX_bob"}
260
+ }
261
+ }
262
+ ```
263
+
264
+ Users are matched by the `auth.userClaim` (default: `sub`) from the login token. Inline storage is read-only — users cannot update their own credentials.
265
+
266
+ </details>
267
+
268
+ ## Contributing
269
+
270
+ Pull requests are welcomed on GitHub! To get started:
271
+
272
+ 1. Install Git and Node.js
273
+ 2. Clone the repository
274
+ 3. Install dependencies with `npm install`
275
+ 4. Run `npm run test` to run tests
276
+ 5. Build with `npm run build`
277
+
278
+ ## Releases
279
+
280
+ Versions follow the [semantic versioning spec](https://semver.org/).
281
+
282
+ To release:
283
+
284
+ 1. Use `npm version <major | minor | patch>` to bump the version
285
+ 2. Run `git push --follow-tags` to push with tags
286
+ 3. Wait for GitHub Actions to publish to the NPM registry and GHCR (Docker).
package/dist/auth.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { AuthConfig } from './types.js';
2
+ type OidcDiscovery = {
3
+ authorization_endpoint: string;
4
+ token_endpoint: string;
5
+ jwks_uri: string;
6
+ issuer: string;
7
+ };
8
+ export declare class OidcClient {
9
+ private readonly config;
10
+ private discovery;
11
+ private discoveryExpiresAt;
12
+ private jwks;
13
+ constructor(config: AuthConfig);
14
+ getDiscovery(): Promise<OidcDiscovery>;
15
+ buildAuthorizeUrl(params: {
16
+ redirectUri: string;
17
+ state: string;
18
+ codeChallenge: string;
19
+ }): Promise<string>;
20
+ generateCodeVerifierAndChallenge(): {
21
+ codeVerifier: string;
22
+ codeChallenge: string;
23
+ };
24
+ exchangeCode(code: string, redirectUri: string, codeVerifier: string): Promise<{
25
+ claims: Record<string, unknown>;
26
+ userId: string;
27
+ }>;
28
+ }
29
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OidcClient = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const jose_1 = require("jose");
6
+ const parseCacheTtl = (headers, defaultMs) => {
7
+ const cacheControl = headers.get('cache-control');
8
+ if (cacheControl) {
9
+ const match = /max-age=(\d+)/.exec(cacheControl);
10
+ if (match) {
11
+ return Number(match[1]) * 1000;
12
+ }
13
+ }
14
+ return defaultMs;
15
+ };
16
+ const DEFAULT_DISCOVERY_TTL_MS = 3_600_000; // 1 hour
17
+ class OidcClient {
18
+ config;
19
+ discovery;
20
+ discoveryExpiresAt = 0;
21
+ jwks;
22
+ constructor(config) {
23
+ this.config = config;
24
+ }
25
+ async getDiscovery() {
26
+ if (this.discovery && Date.now() < this.discoveryExpiresAt) {
27
+ return this.discovery;
28
+ }
29
+ const url = `${this.config.issuer.replace(/\/$/, '')}/.well-known/openid-configuration`;
30
+ const res = await fetch(url);
31
+ if (!res.ok) {
32
+ throw new Error(`OIDC discovery failed: ${res.status} ${res.statusText}`);
33
+ }
34
+ this.discovery = await res.json();
35
+ this.discoveryExpiresAt = Date.now() + parseCacheTtl(res.headers, DEFAULT_DISCOVERY_TTL_MS);
36
+ return this.discovery;
37
+ }
38
+ async buildAuthorizeUrl(params) {
39
+ const disc = await this.getDiscovery();
40
+ const url = new URL(disc.authorization_endpoint);
41
+ url.searchParams.set('client_id', this.config.clientId);
42
+ url.searchParams.set('response_type', 'code');
43
+ url.searchParams.set('redirect_uri', params.redirectUri);
44
+ url.searchParams.set('state', params.state);
45
+ url.searchParams.set('scope', (this.config.scopes ?? ['openid']).join(' '));
46
+ url.searchParams.set('code_challenge', params.codeChallenge);
47
+ url.searchParams.set('code_challenge_method', 'S256');
48
+ return url.toString();
49
+ }
50
+ generateCodeVerifierAndChallenge() {
51
+ const codeVerifier = (0, node_crypto_1.randomBytes)(32).toString('base64url');
52
+ const codeChallenge = (0, node_crypto_1.createHash)('sha256').update(codeVerifier).digest('base64url');
53
+ return { codeVerifier, codeChallenge };
54
+ }
55
+ async exchangeCode(code, redirectUri, codeVerifier) {
56
+ const disc = await this.getDiscovery();
57
+ const params = new URLSearchParams({
58
+ grant_type: 'authorization_code',
59
+ code,
60
+ redirect_uri: redirectUri,
61
+ client_id: this.config.clientId,
62
+ code_verifier: codeVerifier,
63
+ });
64
+ if (this.config.clientSecret) {
65
+ params.set('client_secret', this.config.clientSecret);
66
+ }
67
+ const res = await fetch(disc.token_endpoint, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
70
+ body: params.toString(),
71
+ });
72
+ if (!res.ok) {
73
+ const body = await res.text();
74
+ throw new Error(`Upstream token exchange failed: ${res.status} ${body}`);
75
+ }
76
+ const tokens = await res.json();
77
+ if (tokens.id_token) {
78
+ this.jwks ||= (0, jose_1.createRemoteJWKSet)(new URL(disc.jwks_uri));
79
+ const { payload } = await (0, jose_1.jwtVerify)(tokens.id_token, this.jwks, {
80
+ issuer: disc.issuer,
81
+ audience: this.config.clientId,
82
+ });
83
+ const claim = this.config.userClaim ?? 'sub';
84
+ const userId = payload[claim];
85
+ if (typeof userId !== 'string') {
86
+ throw new Error(`Upstream ID token missing claim "${claim}"`);
87
+ }
88
+ return { claims: payload, userId };
89
+ }
90
+ throw new Error('Upstream did not return an id_token');
91
+ }
92
+ }
93
+ exports.OidcClient = OidcClient;
@@ -0,0 +1,3 @@
1
+ import type { WrapperConfig } from './types.js';
2
+ export declare const parseConfig: (json: string) => WrapperConfig;
3
+ export declare const loadConfig: (configOrPath: string) => WrapperConfig;
package/dist/config.js ADDED
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadConfig = exports.parseConfig = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const parseConfig = (json) => {
9
+ const raw = JSON.parse(json);
10
+ if (!Array.isArray(raw.command) || raw.command.length === 0 || !raw.command.every((c) => typeof c === 'string')) {
11
+ throw new Error('Config must have a "command" array of strings');
12
+ }
13
+ if (!raw.auth || typeof raw.auth !== 'object') {
14
+ throw new Error('Config must have an "auth" object');
15
+ }
16
+ if (!raw.auth.issuer || typeof raw.auth.issuer !== 'string') {
17
+ throw new Error('Config auth must have an "issuer" string');
18
+ }
19
+ // Validate storage: string, object, or omitted (defaults to "memory")
20
+ if (raw.storage !== undefined) {
21
+ if (typeof raw.storage === 'object') {
22
+ if (Array.isArray(raw.storage) || Object.keys(raw.storage).length === 0) {
23
+ throw new Error('Config "storage" object must be a non-empty map of user IDs to env vars');
24
+ }
25
+ }
26
+ else if (typeof raw.storage !== 'string') {
27
+ throw new Error('Config "storage" must be a string, object, or omitted');
28
+ }
29
+ }
30
+ if (raw.envPerUser !== undefined) {
31
+ if (!Array.isArray(raw.envPerUser)) {
32
+ throw new Error('Config "envPerUser" must be an array');
33
+ }
34
+ for (const p of raw.envPerUser) {
35
+ if (!p.name || typeof p.name !== 'string') {
36
+ throw new Error('Each envPerUser entry must have a "name" string');
37
+ }
38
+ if (!p.label || typeof p.label !== 'string') {
39
+ throw new Error('Each envPerUser entry must have a "label" string');
40
+ }
41
+ }
42
+ }
43
+ return {
44
+ command: raw.command,
45
+ auth: {
46
+ issuer: raw.auth.issuer,
47
+ clientId: raw.auth.clientId ?? 'mcp-auth-wrapper',
48
+ clientSecret: raw.auth.clientSecret,
49
+ scopes: raw.auth.scopes ?? ['openid'],
50
+ userClaim: raw.auth.userClaim ?? 'sub',
51
+ },
52
+ storage: raw.storage ?? 'memory',
53
+ envBase: raw.envBase,
54
+ envPerUser: raw.envPerUser,
55
+ port: raw.port ?? 3000,
56
+ host: raw.host ?? '0.0.0.0',
57
+ issuerUrl: raw.issuerUrl,
58
+ secret: raw.secret,
59
+ };
60
+ };
61
+ exports.parseConfig = parseConfig;
62
+ const loadConfig = (configOrPath) => {
63
+ if (configOrPath.trimStart().startsWith('{')) {
64
+ return (0, exports.parseConfig)(configOrPath);
65
+ }
66
+ return (0, exports.parseConfig)(node_fs_1.default.readFileSync(configOrPath, 'utf-8'));
67
+ };
68
+ exports.loadConfig = loadConfig;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const config_js_1 = require("./config.js");
9
+ const store_js_1 = require("./store.js");
10
+ const auth_js_1 = require("./auth.js");
11
+ const oauth_provider_js_1 = require("./oauth-provider.js");
12
+ const process_pool_js_1 = require("./process-pool.js");
13
+ const server_js_1 = require("./server.js");
14
+ const DEFAULT_CONFIG_PATH = 'mcp-auth-wrapper.config.json';
15
+ const main = async () => {
16
+ let configStr = process.env.MCP_AUTH_WRAPPER_CONFIG;
17
+ if (!configStr && node_fs_1.default.existsSync(DEFAULT_CONFIG_PATH)) {
18
+ configStr = DEFAULT_CONFIG_PATH;
19
+ }
20
+ if (!configStr) {
21
+ console.error('No config found. Set MCP_AUTH_WRAPPER_CONFIG or create mcp-auth-wrapper.config.json');
22
+ process.exit(1);
23
+ }
24
+ const config = (0, config_js_1.loadConfig)(configStr);
25
+ const store = new store_js_1.Store(config);
26
+ const oidcClient = new auth_js_1.OidcClient(config.auth);
27
+ const pool = new process_pool_js_1.ProcessPool(config.command[0], config.command.slice(1), config.envBase ?? {}, store);
28
+ const provider = new oauth_provider_js_1.WrapperOAuthProvider(oidcClient, config);
29
+ const app = (0, server_js_1.createApp)(config, pool, provider, oidcClient, store);
30
+ const port = config.port ?? 3000;
31
+ const host = config.host ?? '0.0.0.0';
32
+ const server = app.listen(port, host, () => {
33
+ console.log(`mcp-auth-wrapper listening on ${host}:${port}`);
34
+ console.log(`Wrapping: ${config.command.join(' ')}`);
35
+ console.log(`Auth: ${config.auth.issuer}`);
36
+ console.log(`Storage: ${typeof config.storage === 'string' ? config.storage : 'inline'}`);
37
+ });
38
+ const shutdown = async () => {
39
+ console.log('\nShutting down...');
40
+ server.close();
41
+ await pool.shutdown();
42
+ store.close();
43
+ process.exit(0);
44
+ };
45
+ process.on('SIGINT', shutdown);
46
+ process.on('SIGTERM', shutdown);
47
+ };
48
+ main().catch((err) => {
49
+ console.error(err);
50
+ process.exit(1);
51
+ });
@@ -0,0 +1,39 @@
1
+ import type { Response } from 'express';
2
+ import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
3
+ import type { AuthorizationParams, OAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/provider.js';
4
+ import type { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
5
+ import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
6
+ import type { OidcClient } from './auth.js';
7
+ import type { WrapperConfig } from './types.js';
8
+ /** Data encoded into the upstream state parameter. */
9
+ type PendingAuthPayload = {
10
+ upstreamCodeVerifier: string;
11
+ clientId: string;
12
+ redirectUri: string;
13
+ codeChallenge: string;
14
+ state?: string;
15
+ scopes: string[];
16
+ expiresAt: number;
17
+ userId?: string;
18
+ };
19
+ export declare class WrapperOAuthProvider implements OAuthServerProvider {
20
+ private readonly oidcClient;
21
+ private readonly config;
22
+ readonly clientsStore: OAuthRegisteredClientsStore;
23
+ private readonly key;
24
+ constructor(oidcClient: OidcClient, config: WrapperConfig);
25
+ authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
26
+ /** Unseal the state returned from the upstream callback. */
27
+ unsealState(sealedState: string): PendingAuthPayload | undefined;
28
+ /** Re-seal a modified payload (e.g. after adding userId). */
29
+ sealState(payload: PendingAuthPayload): string;
30
+ completeAuthorization(pending: PendingAuthPayload, userId: string): {
31
+ redirectUrl: string;
32
+ };
33
+ challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
34
+ exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise<OAuthTokens>;
35
+ exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string): Promise<OAuthTokens>;
36
+ verifyAccessToken(token: string): Promise<AuthInfo>;
37
+ revokeToken(client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest): Promise<void>;
38
+ }
39
+ export {};