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 +21 -0
- package/README.md +286 -0
- package/dist/auth.d.ts +29 -0
- package/dist/auth.js +93 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +51 -0
- package/dist/oauth-provider.d.ts +39 -0
- package/dist/oauth-provider.js +200 -0
- package/dist/pages.d.ts +3 -0
- package/dist/pages.js +48 -0
- package/dist/process-pool.d.ts +15 -0
- package/dist/process-pool.js +75 -0
- package/dist/reconfigure-tool.d.ts +44 -0
- package/dist/reconfigure-tool.js +66 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +268 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +44 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +2 -0
- package/package.json +42 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
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.createApp = void 0;
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-deprecated -- Using low-level Server to proxy JSON Schema without Zod conversion */
|
|
8
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
9
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
10
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
11
|
+
const router_js_1 = require("@modelcontextprotocol/sdk/server/auth/router.js");
|
|
12
|
+
const bearerAuth_js_1 = require("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js");
|
|
13
|
+
const express_1 = __importDefault(require("express"));
|
|
14
|
+
const pages_js_1 = require("./pages.js");
|
|
15
|
+
const reconfigure_tool_js_1 = require("./reconfigure-tool.js");
|
|
16
|
+
/** Safely extract a string from a parsed form body (may be string, array, or undefined) */
|
|
17
|
+
const getString = (value) => typeof value === 'string' ? value : undefined;
|
|
18
|
+
const createProxyServer = (pool, store, userId, config, baseUrl, accessToken) => {
|
|
19
|
+
const server = new index_js_1.Server({ name: 'mcp-auth-wrapper', version: '1.0.0' }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
20
|
+
const envPerUser = config.envPerUser ?? [];
|
|
21
|
+
const hasParams = envPerUser.length > 0;
|
|
22
|
+
const reconfigureUrl = `${baseUrl}/reconfigure?token=${accessToken}`;
|
|
23
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
24
|
+
const client = await pool.getClient(userId);
|
|
25
|
+
const result = await client.listTools();
|
|
26
|
+
if (hasParams) {
|
|
27
|
+
result.tools.push((0, reconfigure_tool_js_1.getReconfigureTool)(reconfigureUrl, envPerUser));
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
});
|
|
31
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
32
|
+
if (hasParams && request.params.name === reconfigure_tool_js_1.RECONFIGURE_TOOL_NAME) {
|
|
33
|
+
return (0, reconfigure_tool_js_1.handleReconfigureCall)(request.params.arguments ?? {}, {
|
|
34
|
+
store, pool, userId, envPerUser, reconfigureUrl,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const client = await pool.getClient(userId);
|
|
38
|
+
return client.callTool({
|
|
39
|
+
name: request.params.name,
|
|
40
|
+
arguments: request.params.arguments,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
44
|
+
const client = await pool.getClient(userId);
|
|
45
|
+
return client.listResources();
|
|
46
|
+
});
|
|
47
|
+
server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
|
|
48
|
+
const client = await pool.getClient(userId);
|
|
49
|
+
return client.readResource({
|
|
50
|
+
uri: request.params.uri,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => {
|
|
54
|
+
const client = await pool.getClient(userId);
|
|
55
|
+
return client.listPrompts();
|
|
56
|
+
});
|
|
57
|
+
server.setRequestHandler(types_js_1.GetPromptRequestSchema, async (request) => {
|
|
58
|
+
const client = await pool.getClient(userId);
|
|
59
|
+
return client.getPrompt({
|
|
60
|
+
name: request.params.name,
|
|
61
|
+
arguments: request.params.arguments,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
return server;
|
|
65
|
+
};
|
|
66
|
+
const createApp = (config, pool, provider, oidcClient, store) => {
|
|
67
|
+
const app = (0, express_1.default)();
|
|
68
|
+
const baseUrl = config.issuerUrl ?? `http://localhost:${config.port ?? 3000}`;
|
|
69
|
+
const issuerUrl = new URL(baseUrl);
|
|
70
|
+
const mcpUrl = new URL('/mcp', issuerUrl);
|
|
71
|
+
// Custom /authorize handler — accepts any client_id and redirect_uri without
|
|
72
|
+
// requiring prior registration. The SDK's built-in handler validates these against
|
|
73
|
+
// a client registry, which we don't need. PKCE is still enforced.
|
|
74
|
+
app.all('/authorize', (req, res) => {
|
|
75
|
+
const params = req.method === 'POST' ? req.body : req.query;
|
|
76
|
+
const clientId = getString(params.client_id);
|
|
77
|
+
const redirectUri = getString(params.redirect_uri);
|
|
78
|
+
const codeChallenge = getString(params.code_challenge);
|
|
79
|
+
const codeChallengeMethod = getString(params.code_challenge_method);
|
|
80
|
+
const scope = getString(params.scope);
|
|
81
|
+
const state = getString(params.state);
|
|
82
|
+
if (!clientId || !redirectUri || !codeChallenge) {
|
|
83
|
+
res.status(400).json({ error: 'invalid_request', error_description: 'Missing client_id, redirect_uri, or code_challenge' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (codeChallengeMethod && codeChallengeMethod !== 'S256') {
|
|
87
|
+
res.status(400).json({ error: 'invalid_request', error_description: 'code_challenge_method must be S256' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const client = { client_id: clientId, redirect_uris: [redirectUri] };
|
|
91
|
+
const authParams = {
|
|
92
|
+
scopes: scope ? scope.split(' ') : [],
|
|
93
|
+
redirectUri,
|
|
94
|
+
codeChallenge,
|
|
95
|
+
};
|
|
96
|
+
if (state) {
|
|
97
|
+
authParams.state = state;
|
|
98
|
+
}
|
|
99
|
+
void provider.authorize(client, authParams, res).catch((err) => {
|
|
100
|
+
console.error('Authorize error:', err);
|
|
101
|
+
if (!res.headersSent) {
|
|
102
|
+
res.status(500).json({ error: 'server_error', error_description: 'Failed to initiate authorization. See server logs for more details.' });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
// OAuth routes (discovery, token, register, revoke — /authorize is handled above)
|
|
107
|
+
app.use((0, router_js_1.mcpAuthRouter)({
|
|
108
|
+
provider,
|
|
109
|
+
issuerUrl,
|
|
110
|
+
baseUrl: issuerUrl,
|
|
111
|
+
resourceServerUrl: mcpUrl,
|
|
112
|
+
}));
|
|
113
|
+
// Upstream OIDC callback
|
|
114
|
+
app.get('/callback', async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
const code = getString(req.query.code);
|
|
117
|
+
const sealedState = getString(req.query.state);
|
|
118
|
+
if (!code || !sealedState) {
|
|
119
|
+
res.status(400).send('Missing code or state parameter');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const pending = provider.unsealState(sealedState);
|
|
123
|
+
if (!pending) {
|
|
124
|
+
res.status(400).send('Invalid or expired authorization session');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const callbackUrl = `${baseUrl}/callback`;
|
|
128
|
+
const { userId } = await oidcClient.exchangeCode(code, callbackUrl, pending.upstreamCodeVerifier);
|
|
129
|
+
// Check if user needs params
|
|
130
|
+
const existingParams = store.getUser(userId);
|
|
131
|
+
const needsParams = config.envPerUser
|
|
132
|
+
&& config.envPerUser.length > 0
|
|
133
|
+
&& (!existingParams || config.envPerUser.some((p) => !existingParams[p.name]));
|
|
134
|
+
if (needsParams) {
|
|
135
|
+
// Re-seal with userId attached so /params can use it
|
|
136
|
+
pending.userId = userId;
|
|
137
|
+
const newSealedState = provider.sealState(pending);
|
|
138
|
+
res.redirect(`${baseUrl}/params?session=${newSealedState}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// User is fully configured — if not in store yet, create empty entry
|
|
142
|
+
if (!existingParams) {
|
|
143
|
+
store.upsertUser(userId, {});
|
|
144
|
+
}
|
|
145
|
+
const { redirectUrl } = provider.completeAuthorization(pending, userId);
|
|
146
|
+
res.redirect(redirectUrl);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
console.error('Callback error:', err);
|
|
150
|
+
res.status(500).send('Authentication failed');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// Params form
|
|
154
|
+
app.get('/params', (req, res) => {
|
|
155
|
+
const sealedSession = getString(req.query.session);
|
|
156
|
+
if (!sealedSession) {
|
|
157
|
+
res.status(400).send('Missing session parameter');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const pending = provider.unsealState(sealedSession);
|
|
161
|
+
if (!pending?.userId) {
|
|
162
|
+
res.status(400).send('Invalid or expired session');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const existingValues = store.getUser(pending.userId);
|
|
166
|
+
res.send((0, pages_js_1.renderParamsForm)(config.envPerUser ?? [], sealedSession, existingValues));
|
|
167
|
+
});
|
|
168
|
+
app.post('/params', express_1.default.urlencoded({ extended: false }), (req, res) => {
|
|
169
|
+
const sealedSession = getString(req.body.session);
|
|
170
|
+
if (!sealedSession) {
|
|
171
|
+
res.status(400).send('Missing session parameter');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const pending = provider.unsealState(sealedSession);
|
|
175
|
+
if (!pending?.userId) {
|
|
176
|
+
res.status(400).send('Invalid or expired session');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const params = {};
|
|
180
|
+
for (const p of config.envPerUser ?? []) {
|
|
181
|
+
const value = getString(req.body[p.name]);
|
|
182
|
+
if (value) {
|
|
183
|
+
params[p.name] = value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
store.upsertUser(pending.userId, params);
|
|
187
|
+
pool.invalidateUser(pending.userId);
|
|
188
|
+
const { redirectUrl } = provider.completeAuthorization(pending, pending.userId);
|
|
189
|
+
res.redirect(redirectUrl);
|
|
190
|
+
});
|
|
191
|
+
// Reconfigure page (auth via access token in URL)
|
|
192
|
+
app.get('/reconfigure', async (req, res) => {
|
|
193
|
+
const token = getString(req.query.token);
|
|
194
|
+
if (!token) {
|
|
195
|
+
res.status(401).send('Missing token');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const authInfo = await provider.verifyAccessToken(token);
|
|
200
|
+
const userId = getString(authInfo.extra?.userId);
|
|
201
|
+
if (!userId) {
|
|
202
|
+
res.status(401).send('Missing user identity');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const existingValues = store.getUser(userId) ?? {};
|
|
206
|
+
res.send((0, pages_js_1.renderReconfigurePage)(config.envPerUser ?? [], token, existingValues));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
res.status(401).send('Invalid or expired token');
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
app.post('/reconfigure', express_1.default.urlencoded({ extended: false }), async (req, res) => {
|
|
213
|
+
const token = getString(req.body.token);
|
|
214
|
+
if (!token) {
|
|
215
|
+
res.status(401).send('Missing token');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const authInfo = await provider.verifyAccessToken(token);
|
|
220
|
+
const userId = getString(authInfo.extra?.userId);
|
|
221
|
+
if (!userId) {
|
|
222
|
+
res.status(401).send('Missing user identity');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const params = {};
|
|
226
|
+
for (const p of config.envPerUser ?? []) {
|
|
227
|
+
const value = getString(req.body[p.name]);
|
|
228
|
+
if (value) {
|
|
229
|
+
params[p.name] = value;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
store.upsertUser(userId, params);
|
|
233
|
+
pool.invalidateUser(userId);
|
|
234
|
+
res.send((0, pages_js_1.renderReconfigurePage)(config.envPerUser ?? [], token, params, true));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
res.status(401).send('Invalid or expired token');
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
// Protected MCP endpoint
|
|
241
|
+
const bearerAuth = (0, bearerAuth_js_1.requireBearerAuth)({
|
|
242
|
+
verifier: provider,
|
|
243
|
+
resourceMetadataUrl: (0, router_js_1.getOAuthProtectedResourceMetadataUrl)(mcpUrl),
|
|
244
|
+
});
|
|
245
|
+
app.all('/mcp', bearerAuth, async (req, res) => {
|
|
246
|
+
const userId = getString(req.auth?.extra?.userId);
|
|
247
|
+
if (!userId) {
|
|
248
|
+
res.status(401).json({ error: 'Missing user identity' });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const accessToken = req.auth.token;
|
|
252
|
+
// Check if user has params configured
|
|
253
|
+
const userParams = store.getUser(userId);
|
|
254
|
+
if (!userParams) {
|
|
255
|
+
res.status(403).json({ error: 'User not configured. Please reconfigure via the reconfigure tool.' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Stateless: fresh transport and proxy server per request (no session tracking)
|
|
259
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
260
|
+
enableJsonResponse: true,
|
|
261
|
+
});
|
|
262
|
+
const server = createProxyServer(pool, store, userId, config, baseUrl, accessToken);
|
|
263
|
+
await server.connect(transport);
|
|
264
|
+
await transport.handleRequest(req, res);
|
|
265
|
+
});
|
|
266
|
+
return app;
|
|
267
|
+
};
|
|
268
|
+
exports.createApp = createApp;
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WrapperConfig } from './types.js';
|
|
2
|
+
export declare class Store {
|
|
3
|
+
private readonly db;
|
|
4
|
+
private readonly readOnly;
|
|
5
|
+
constructor(config: WrapperConfig);
|
|
6
|
+
getUser(userId: string): Record<string, string> | undefined;
|
|
7
|
+
upsertUser(userId: string, params: Record<string, string>): void;
|
|
8
|
+
close(): void;
|
|
9
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Store = void 0;
|
|
4
|
+
const node_sqlite_1 = require("node:sqlite");
|
|
5
|
+
class Store {
|
|
6
|
+
db;
|
|
7
|
+
readOnly;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
const inlineUsers = typeof config.storage === 'object' ? config.storage : undefined;
|
|
10
|
+
const storagePath = typeof config.storage === 'string' ? config.storage : undefined;
|
|
11
|
+
const isFile = storagePath && storagePath !== 'memory';
|
|
12
|
+
this.db = new node_sqlite_1.DatabaseSync(isFile ? storagePath : ':memory:');
|
|
13
|
+
this.readOnly = inlineUsers !== undefined;
|
|
14
|
+
this.db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
16
|
+
user_id TEXT PRIMARY KEY,
|
|
17
|
+
params TEXT NOT NULL DEFAULT '{}'
|
|
18
|
+
)
|
|
19
|
+
`);
|
|
20
|
+
if (inlineUsers) {
|
|
21
|
+
const insert = this.db.prepare('INSERT OR REPLACE INTO users (user_id, params) VALUES (?, ?)');
|
|
22
|
+
for (const [userId, params] of Object.entries(inlineUsers)) {
|
|
23
|
+
insert.run(userId, JSON.stringify(params));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
getUser(userId) {
|
|
28
|
+
const row = this.db.prepare('SELECT params FROM users WHERE user_id = ?').get(userId);
|
|
29
|
+
if (!row) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return JSON.parse(row.params);
|
|
33
|
+
}
|
|
34
|
+
upsertUser(userId, params) {
|
|
35
|
+
if (this.readOnly) {
|
|
36
|
+
throw new Error('Cannot modify users in inline storage mode');
|
|
37
|
+
}
|
|
38
|
+
this.db.prepare('INSERT INTO users (user_id, params) VALUES (?, ?) ON CONFLICT(user_id) DO UPDATE SET params = excluded.params').run(userId, JSON.stringify(params));
|
|
39
|
+
}
|
|
40
|
+
close() {
|
|
41
|
+
this.db.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.Store = Store;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type EnvParam = {
|
|
2
|
+
name: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
secret?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type AuthConfig = {
|
|
8
|
+
issuer: string;
|
|
9
|
+
clientId: string;
|
|
10
|
+
clientSecret?: string;
|
|
11
|
+
scopes?: string[];
|
|
12
|
+
userClaim?: string;
|
|
13
|
+
};
|
|
14
|
+
export type WrapperConfig = {
|
|
15
|
+
command: string[];
|
|
16
|
+
auth: AuthConfig;
|
|
17
|
+
/** `"memory"`, a file path for SQLite, or an inline user map (read-only). Defaults to `"memory"`. */
|
|
18
|
+
storage: string | Record<string, Record<string, string>>;
|
|
19
|
+
envBase?: Record<string, string>;
|
|
20
|
+
envPerUser?: EnvParam[];
|
|
21
|
+
port?: number;
|
|
22
|
+
host?: string;
|
|
23
|
+
issuerUrl?: string;
|
|
24
|
+
secret?: string;
|
|
25
|
+
};
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-auth-wrapper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Wrap any stdio MCP server with per-user auth, exposing it as a streamable HTTP endpoint.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Adam Jones (domdomegg)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/domdomegg/mcp-auth-wrapper.git"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"mcp-auth-wrapper": "dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest --watch",
|
|
22
|
+
"lint": "eslint",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"build": "tsc --project tsconfig.build.json",
|
|
25
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@tsconfig/node-lts": "^22.0.2",
|
|
29
|
+
"@types/express": "^5.0.6",
|
|
30
|
+
"eslint": "^9.32.0",
|
|
31
|
+
"eslint-config-domdomegg": "^2.0.9",
|
|
32
|
+
"tsconfig-domdomegg": "^1.0.0",
|
|
33
|
+
"typescript": "^5.8.3",
|
|
34
|
+
"vitest": "^3.2.4",
|
|
35
|
+
"zod": "^4.3.6"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
39
|
+
"express": "^5.2.1",
|
|
40
|
+
"jose": "^6.1.3"
|
|
41
|
+
}
|
|
42
|
+
}
|