@ni-c/mcp-hub 0.9.2 → 0.10.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 (59) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +114 -54
  3. package/dist/admin.js +231 -7
  4. package/dist/admin.js.map +1 -1
  5. package/dist/auth/address.js +89 -0
  6. package/dist/auth/address.js.map +1 -0
  7. package/dist/auth/cimd.js +367 -0
  8. package/dist/auth/cimd.js.map +1 -0
  9. package/dist/auth/consent-page.js +4 -7
  10. package/dist/auth/consent-page.js.map +1 -1
  11. package/dist/auth/login-page.js +4 -7
  12. package/dist/auth/login-page.js.map +1 -1
  13. package/dist/auth/page.js +22 -0
  14. package/dist/auth/page.js.map +1 -1
  15. package/dist/auth/pinned-fetch.js +129 -0
  16. package/dist/auth/pinned-fetch.js.map +1 -0
  17. package/dist/auth/private-key-jwt.js +213 -0
  18. package/dist/auth/private-key-jwt.js.map +1 -0
  19. package/dist/auth/provider.js +102 -45
  20. package/dist/auth/provider.js.map +1 -1
  21. package/dist/auth/rate-limit.js +43 -0
  22. package/dist/auth/rate-limit.js.map +1 -0
  23. package/dist/auth/redirect-uri.js +58 -0
  24. package/dist/auth/redirect-uri.js.map +1 -0
  25. package/dist/auth/registration.js +146 -0
  26. package/dist/auth/registration.js.map +1 -0
  27. package/dist/auth/routes.js +67 -41
  28. package/dist/auth/routes.js.map +1 -1
  29. package/dist/auth/signed-token.js +49 -0
  30. package/dist/auth/signed-token.js.map +1 -0
  31. package/dist/auth/store.js +357 -5
  32. package/dist/auth/store.js.map +1 -1
  33. package/dist/auth/text.js +51 -0
  34. package/dist/auth/text.js.map +1 -0
  35. package/dist/config.js +130 -4
  36. package/dist/config.js.map +1 -1
  37. package/dist/health.js +22 -3
  38. package/dist/health.js.map +1 -1
  39. package/dist/hub.js +28 -0
  40. package/dist/hub.js.map +1 -1
  41. package/dist/index.js +123 -10
  42. package/dist/index.js.map +1 -1
  43. package/dist/proxy.js +25 -3
  44. package/dist/proxy.js.map +1 -1
  45. package/dist/stdio.js +27 -2
  46. package/dist/stdio.js.map +1 -1
  47. package/dist/supervisor.js +152 -8
  48. package/dist/supervisor.js.map +1 -1
  49. package/dist/tool-filter.js +58 -0
  50. package/dist/tool-filter.js.map +1 -0
  51. package/dist/upstream/auth.js +435 -0
  52. package/dist/upstream/auth.js.map +1 -0
  53. package/dist/upstream/login.js +94 -0
  54. package/dist/upstream/login.js.map +1 -0
  55. package/dist/upstream/provider.js +286 -0
  56. package/dist/upstream/provider.js.map +1 -0
  57. package/dist/upstream/routes.js +96 -0
  58. package/dist/upstream/routes.js.map +1 -0
  59. package/package.json +5 -2
@@ -0,0 +1,286 @@
1
+ import crypto from 'node:crypto';
2
+ import { SignJWT, exportJWK } from 'jose';
3
+ import { sign } from '../auth/signed-token.js';
4
+ import { VERSION } from '../version.js';
5
+ /**
6
+ * The hub as an OAuth *client*, for one upstream MCP server.
7
+ *
8
+ * The SDK drives the protocol; this supplies the identity and the storage. Two
9
+ * things shape the design:
10
+ *
11
+ * The transport is rebuilt on every connection attempt (`buildRemoteTransport`
12
+ * runs inside `start()`), so nothing durable may live in a field here. Every
13
+ * read goes to the store, which is also how a token written by
14
+ * `mcp-hub-admin upstream login` in another process reaches a running hub —
15
+ * `reloadIfChanged()` is a single stat.
16
+ *
17
+ * And `redirectUrl` returning `undefined` is what selects the SDK's
18
+ * non-interactive path, so it is the switch between the two grants rather than
19
+ * a mere detail.
20
+ */
21
+ /** Where the upstream sends the operator's browser back to. */
22
+ export const UPSTREAM_CALLBACK_PATH = 'upstream/callback';
23
+ /** Where the hub publishes a client metadata document, one per upstream. */
24
+ export const UPSTREAM_CLIENT_METADATA_PREFIX = '.well-known/mcp-hub-client';
25
+ /**
26
+ * Identifies the configuration a stored credential belongs to. Change the URL,
27
+ * the client or the scopes and the old tokens describe an identity the upstream
28
+ * no longer knows — presenting them would at best fail confusingly. Deliberately
29
+ * excludes everything that does not affect identity, so renaming a header does
30
+ * not force a new login.
31
+ */
32
+ export function credentialFingerprint(identity) {
33
+ const { oauth } = identity;
34
+ return crypto
35
+ .createHash('sha256')
36
+ .update(JSON.stringify([identity.serverUrl, oauth.mode, oauth.grant, oauth.clientId ?? null, [...oauth.scopes].sort()]))
37
+ .digest('hex');
38
+ }
39
+ export function callbackUrl(externalUrl) {
40
+ return new URL(UPSTREAM_CALLBACK_PATH, externalUrl).href;
41
+ }
42
+ /**
43
+ * The identifier a client metadata document is published under.
44
+ *
45
+ * Derived from the server name rather than being it. The document is fetched by
46
+ * a third party and is therefore unauthenticated, and `/health` is behind bearer
47
+ * auth precisely so that the deployment's topology is not public — a document at
48
+ * `/…/paperless.json` would hand that back. Losing `cookieSecret` already
49
+ * invalidates every session and approval, so deriving from it costs nothing.
50
+ */
51
+ export function clientDocumentId(serverName, secret) {
52
+ return sign(`upstream-client:${serverName}`, secret).slice(0, 22);
53
+ }
54
+ /** One document per upstream: two servers using `cimd` may want different
55
+ * scopes, and a shared document would give the second one the first's. */
56
+ export function clientMetadataUrl(externalUrl, serverName, secret) {
57
+ return new URL(`${UPSTREAM_CLIENT_METADATA_PREFIX}/${clientDocumentId(serverName, secret)}.json`, externalUrl).href;
58
+ }
59
+ /** The document the hub publishes about itself when an upstream supports CIMD,
60
+ * and the body it sends when registering dynamically. Both are the same thing. */
61
+ export function hubClientMetadata(identity, publicJwk) {
62
+ const interactive = identity.oauth.grant === 'authorization_code';
63
+ const usesKey = identity.oauth.clientAuth === 'private_key_jwt';
64
+ return {
65
+ client_name: `mcp-hub ${VERSION}`,
66
+ client_uri: identity.externalUrl,
67
+ // A client_credentials client has no redirect at all, and the SDK's own
68
+ // provider sets an empty array for exactly that case.
69
+ redirect_uris: interactive ? [callbackUrl(identity.externalUrl)] : [],
70
+ grant_types: interactive ? ['authorization_code', 'refresh_token'] : ['client_credentials'],
71
+ response_types: ['code'],
72
+ token_endpoint_auth_method: usesKey ? 'private_key_jwt' : identity.oauth.clientSecret ? 'client_secret_basic' : 'none',
73
+ // The upstream verifies our assertions against this, so it has to travel
74
+ // with the document and with the registration request.
75
+ ...(usesKey && publicJwk ? { jwks: { keys: [publicJwk] } } : {}),
76
+ ...(identity.oauth.scopes.length > 0 ? { scope: identity.oauth.scopes.join(' ') } : {})
77
+ };
78
+ }
79
+ /** RFC 7523 §2.2: assertions are single-use and short-lived. */
80
+ const ASSERTION_LIFETIME_S = 300;
81
+ export class UpstreamAuthProvider {
82
+ identity;
83
+ store;
84
+ options;
85
+ /** Captured rather than followed: this process has no browser, and the URL is
86
+ * what `upstream login` prints for the operator. */
87
+ authorizationUrl;
88
+ /** The SDK hands the verifier over during startAuthorization; the caller
89
+ * persists it alongside the login it is about to record. */
90
+ issuedCodeVerifier;
91
+ fingerprint;
92
+ constructor(identity, store, options = {}) {
93
+ this.identity = identity;
94
+ this.store = store;
95
+ this.options = options;
96
+ this.fingerprint = credentialFingerprint(identity);
97
+ }
98
+ get record() {
99
+ return this.store.getUpstreamCredentials(this.identity.serverName, this.fingerprint);
100
+ }
101
+ /** Merges into whatever is on disk right now, so a concurrent writer — the
102
+ * admin CLI, or the hub itself — is not clobbered. */
103
+ patch(changes) {
104
+ const fingerprint = this.fingerprint;
105
+ this.store.updateUpstreamCredentials(this.identity.serverName, current => ({
106
+ // A record from a superseded configuration is replaced, not merged into.
107
+ ...(current?.fingerprint === fingerprint ? current : {}),
108
+ fingerprint,
109
+ obtainedAt: Math.floor(Date.now() / 1000),
110
+ ...changes
111
+ }));
112
+ }
113
+ // --- OAuthClientProvider -------------------------------------------------
114
+ get redirectUrl() {
115
+ // Undefined is the SDK's switch into the non-interactive flow.
116
+ return this.identity.oauth.grant === 'authorization_code' ? callbackUrl(this.identity.externalUrl) : undefined;
117
+ }
118
+ get clientMetadata() {
119
+ return hubClientMetadata(this.identity);
120
+ }
121
+ /** Only set for `mode: "cimd"`. The SDK uses it as the client_id, but only if
122
+ * the upstream advertises support; otherwise it falls back to registering. */
123
+ get clientMetadataUrl() {
124
+ if (this.identity.oauth.mode !== 'cimd')
125
+ return undefined;
126
+ return clientMetadataUrl(this.identity.externalUrl, this.identity.serverName, this.store.cookieSecret);
127
+ }
128
+ clientInformation() {
129
+ // Configured credentials win: with them there is nothing to register, and
130
+ // the SDK skips both CIMD and dynamic registration.
131
+ if (this.identity.oauth.mode === 'static') {
132
+ return {
133
+ client_id: this.identity.oauth.clientId,
134
+ ...(this.identity.oauth.clientSecret ? { client_secret: this.identity.oauth.clientSecret } : {})
135
+ };
136
+ }
137
+ const record = this.record;
138
+ if (!record?.clientId)
139
+ return undefined;
140
+ return {
141
+ client_id: record.clientId,
142
+ ...(record.clientSecret ? { client_secret: record.clientSecret } : {})
143
+ };
144
+ }
145
+ saveClientInformation(information) {
146
+ this.patch({
147
+ clientId: information.client_id,
148
+ ...(typeof information.client_secret === 'string' ? { clientSecret: information.client_secret } : {}),
149
+ // RFC 7592 credentials, when the upstream issued them — what `upstream
150
+ // logout` needs to delete the registration again.
151
+ ...(typeof information.registration_access_token === 'string'
152
+ ? { registrationAccessToken: information.registration_access_token }
153
+ : {}),
154
+ ...(typeof information.registration_client_uri === 'string'
155
+ ? { registrationClientUri: information.registration_client_uri }
156
+ : {})
157
+ });
158
+ }
159
+ /**
160
+ * The access token, deliberately without the refresh token.
161
+ *
162
+ * That withholding is the point: the SDK refreshes reactively on any 401 and
163
+ * has no single-flight, so a burst of parallel requests would each spend the
164
+ * same rotating refresh token and an upstream that detects reuse would revoke
165
+ * the whole family. Refresh belongs to UpstreamAuth, which serializes it.
166
+ */
167
+ tokens() {
168
+ const stored = this.record?.tokens;
169
+ if (!stored)
170
+ return undefined;
171
+ const { refresh_token: _withheld, ...rest } = stored;
172
+ return rest;
173
+ }
174
+ /** The full pair, for the one caller that is allowed to spend it. */
175
+ storedTokens() {
176
+ return this.record?.tokens;
177
+ }
178
+ saveTokens(tokens) {
179
+ const expiresIn = typeof tokens.expires_in === 'number' ? tokens.expires_in : undefined;
180
+ this.patch({
181
+ tokens: tokens,
182
+ ...(expiresIn !== undefined ? { accessTokenValidUntil: Math.floor(Date.now() / 1000) + expiresIn } : {})
183
+ });
184
+ }
185
+ /** Never actually redirects: there is no user agent in this process. The URL
186
+ * is handed to whoever started the login to print. */
187
+ redirectToAuthorization(authorizationUrl) {
188
+ this.authorizationUrl = authorizationUrl.href;
189
+ }
190
+ saveCodeVerifier(codeVerifier) {
191
+ // Kept in memory only. The caller stores it with the pending login, because
192
+ // the browser round-trip may finish in a different process.
193
+ this.issuedCodeVerifier = codeVerifier;
194
+ }
195
+ codeVerifier() {
196
+ const verifier = this.options.pendingCodeVerifier ?? this.issuedCodeVerifier;
197
+ if (!verifier)
198
+ throw new Error('no PKCE verifier for this login; start it again');
199
+ return verifier;
200
+ }
201
+ /** Client credentials for the non-interactive grant. Without this the SDK
202
+ * would try an authorization-code exchange and fail for want of a code. */
203
+ prepareTokenRequest(scope) {
204
+ if (this.identity.oauth.grant !== 'client_credentials')
205
+ return undefined;
206
+ const params = new URLSearchParams({ grant_type: 'client_credentials' });
207
+ const requested = scope ?? this.identity.oauth.scopes.join(' ');
208
+ if (requested)
209
+ params.set('scope', requested);
210
+ return params;
211
+ }
212
+ /**
213
+ * The SDK's recovery path: on `invalid_grant` it clears tokens and retries, on
214
+ * `invalid_client` it clears everything. Without this a dead refresh token
215
+ * loops into the same failure and then throws.
216
+ */
217
+ invalidateCredentials(scope) {
218
+ if (scope === 'verifier') {
219
+ this.issuedCodeVerifier = undefined;
220
+ return;
221
+ }
222
+ if (scope === 'all') {
223
+ this.store.forgetUpstreamCredentials(this.identity.serverName);
224
+ return;
225
+ }
226
+ const record = this.record;
227
+ if (!record)
228
+ return;
229
+ const cleared = { ...record };
230
+ if (scope === 'tokens') {
231
+ delete cleared.tokens;
232
+ delete cleared.accessTokenValidUntil;
233
+ }
234
+ if (scope === 'client') {
235
+ delete cleared.clientId;
236
+ delete cleared.clientSecret;
237
+ delete cleared.registrationAccessToken;
238
+ delete cleared.registrationClientUri;
239
+ }
240
+ if (scope === 'discovery')
241
+ delete cleared.discovery;
242
+ this.store.updateUpstreamCredentials(this.identity.serverName, () => cleared);
243
+ }
244
+ /** The public half of the outbound signing key, for the document the upstream
245
+ * fetches. */
246
+ async publicJwk() {
247
+ const jwk = await exportJWK(crypto.createPublicKey(this.store.upstreamPrivateKey));
248
+ return { ...jwk, use: 'sig', alg: 'EdDSA', kid: 'mcp-hub-upstream' };
249
+ }
250
+ /**
251
+ * RFC 7523 client authentication, replacing the SDK's built-in choice.
252
+ *
253
+ * The SDK picks between `client_secret_basic`, `client_secret_post` and
254
+ * `none`; it has no assertion path at all. Providing this hook takes the
255
+ * decision away from it entirely, which is also why the shared-secret cases
256
+ * fall through to doing nothing — the SDK's own logic is right for those.
257
+ */
258
+ addClientAuthentication = async (_headers, params, url) => {
259
+ if (this.identity.oauth.clientAuth !== 'private_key_jwt')
260
+ return;
261
+ const clientId = this.clientInformation()?.client_id;
262
+ if (!clientId)
263
+ throw new Error('cannot sign a client assertion before the client_id is known');
264
+ params.set('client_id', clientId);
265
+ params.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
266
+ params.set('client_assertion', await this.signAssertion(clientId, String(url)));
267
+ };
268
+ async signAssertion(clientId, audience) {
269
+ return new SignJWT({})
270
+ .setProtectedHeader({ alg: 'EdDSA', kid: 'mcp-hub-upstream' })
271
+ .setIssuer(clientId)
272
+ .setSubject(clientId)
273
+ .setAudience(audience)
274
+ .setIssuedAt()
275
+ .setExpirationTime(`${ASSERTION_LIFETIME_S}s`)
276
+ .setJti(crypto.randomBytes(16).toString('base64url'))
277
+ .sign(this.store.upstreamPrivateKey);
278
+ }
279
+ saveDiscoveryState(state) {
280
+ this.patch({ discovery: state });
281
+ }
282
+ discoveryState() {
283
+ return this.record?.discovery;
284
+ }
285
+ }
286
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/upstream/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ import { Router } from 'express';
2
+ import { readSignedPayload } from '../auth/signed-token.js';
3
+ import { renderPage, escapeHtml } from '../auth/page.js';
4
+ import { logSafe } from '../auth/text.js';
5
+ import { UPSTREAM_CALLBACK_PATH, UPSTREAM_CLIENT_METADATA_PREFIX, UpstreamAuthProvider, clientDocumentId, clientMetadataUrl, hubClientMetadata } from './provider.js';
6
+ const page = (res, status, title, body) => {
7
+ res.status(status).type('html').send(renderPage(title, `<form><h1>${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></form>`));
8
+ };
9
+ export function createUpstreamRoutes(options) {
10
+ const { store, provider, registry, supervisor, watcher, externalUrl } = options;
11
+ const router = Router();
12
+ /**
13
+ * One document per upstream, addressed by a derived identifier.
14
+ *
15
+ * Registered unconditionally but answered conditionally: the config is
16
+ * hot-reloadable, so a server can become `cimd` long after boot, and an
17
+ * Express route cannot be added later.
18
+ */
19
+ router.get(`/${UPSTREAM_CLIENT_METADATA_PREFIX}/:id.json`, (req, res) => {
20
+ void (async () => {
21
+ const wanted = String(req.params.id);
22
+ for (const [name, server] of watcher.current) {
23
+ if (server.kind !== 'remote' || server.oauth?.mode !== 'cimd')
24
+ continue;
25
+ if (clientDocumentId(name, store.cookieSecret) !== wanted)
26
+ continue;
27
+ const identity = { serverName: name, serverUrl: server.url, oauth: server.oauth, externalUrl };
28
+ const provider = new UpstreamAuthProvider(identity, store);
29
+ // The document must name itself byte-for-byte or the upstream refuses it.
30
+ res.json({
31
+ client_id: clientMetadataUrl(externalUrl, name, store.cookieSecret),
32
+ ...hubClientMetadata(identity, server.oauth.clientAuth === 'private_key_jwt' ? await provider.publicJwk() : undefined)
33
+ });
34
+ return;
35
+ }
36
+ res.status(404).json({ error: 'not_found', error_description: 'No upstream publishes a document here' });
37
+ })();
38
+ });
39
+ router.get(`/${UPSTREAM_CALLBACK_PATH}`, (req, res) => {
40
+ void handleCallback(req.query, req.headers.cookie, res);
41
+ });
42
+ async function handleCallback(query, cookie, res) {
43
+ const state = typeof query.state === 'string' ? query.state : '';
44
+ const decoded = readSignedPayload(state, store.cookieSecret);
45
+ if (!decoded || decoded.exp < Date.now()) {
46
+ console.warn(`mcp-hub: upstream callback with an invalid or expired state`);
47
+ page(res, 400, 'Login expired', 'This authorization is no longer valid. Start it again with mcp-hub-admin upstream login.');
48
+ return;
49
+ }
50
+ // Proving the browser belongs to the operator, not just to whoever ended up
51
+ // holding the redirect. The session cookie rides along because the upstream
52
+ // sends a top-level navigation and the cookie is SameSite=Lax.
53
+ if (!provider.hasValidSession(cookie)) {
54
+ page(res, 401, 'Not signed in', 'Sign in to this hub in the same browser, then run the login again.');
55
+ return;
56
+ }
57
+ // Single use, and taken before the exchange so a refreshed browser tab
58
+ // cannot redeem the same code twice.
59
+ const login = store.takeUpstreamLogin(state);
60
+ if (!login || login.serverName !== decoded.n) {
61
+ page(res, 400, 'Login expired', 'This authorization was already used or has expired. Start it again.');
62
+ return;
63
+ }
64
+ if (typeof query.error === 'string') {
65
+ console.warn(`mcp-hub: upstream login for ${logSafe(login.serverName)} was declined: ${logSafe(query.error)}`);
66
+ page(res, 400, 'Authorization declined', `The upstream reported "${query.error}". Nothing was changed.`);
67
+ return;
68
+ }
69
+ const code = typeof query.code === 'string' ? query.code : '';
70
+ if (!code) {
71
+ page(res, 400, 'No authorization code', 'The upstream did not send a code. Start the login again.');
72
+ return;
73
+ }
74
+ const auth = registry.get(login.serverName);
75
+ if (!auth) {
76
+ page(res, 400, 'Unknown server', `"${login.serverName}" no longer has an OAuth configuration.`);
77
+ return;
78
+ }
79
+ try {
80
+ await auth.finishLogin(login, code);
81
+ }
82
+ catch (error) {
83
+ // Never the response body — an OAuth error can carry a token or an
84
+ // assertion. The reason goes to the log, the page says only that it failed.
85
+ console.error(`mcp-hub: upstream login for ${logSafe(login.serverName)} failed: ${logSafe(error.message)}`);
86
+ page(res, 400, 'Login failed', 'The upstream refused the exchange. The hub log has the reason.');
87
+ return;
88
+ }
89
+ console.log(`mcp-hub: upstream login for ${logSafe(login.serverName)} succeeded`);
90
+ // Bring the server back without waiting for it.
91
+ supervisor.get(login.serverName)?.reauthorize();
92
+ page(res, 200, 'Upstream authorized', `${login.serverName} is authorized. You can close this window.`);
93
+ }
94
+ return router;
95
+ }
96
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/upstream/routes.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@ni-c/mcp-hub",
3
- "version": "0.9.2",
4
- "description": "Serve multiple stdio MCP servers from one container: Claude-Code-style mcpServers config, path-based routing, hub meta-tools, and OAuth 2.1 + API tokens for ChatGPT, Claude and any Streamable-HTTP MCP client.",
3
+ "version": "0.10.0",
4
+ "description": "Serve multiple stdio MCP servers from one container: Claude-Code-style mcpServers config, path-based routing, hub meta-tools, and CIMD-first OAuth 2.1 + API tokens for ChatGPT, Claude and any Streamable-HTTP MCP client.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "model-context-protocol",
8
8
  "gateway",
9
9
  "proxy",
10
10
  "oauth",
11
+ "oauth2.1",
12
+ "cimd",
11
13
  "claude"
12
14
  ],
13
15
  "type": "module",
@@ -21,6 +23,7 @@
21
23
  "url": "https://github.com/ni-c/mcp-hub/issues"
22
24
  },
23
25
  "homepage": "https://mcp-hub.ni-c.de",
26
+ "funding": "https://github.com/sponsors/ni-c",
24
27
  "engines": {
25
28
  "node": ">=22"
26
29
  },