@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.
- package/CHANGELOG.md +231 -0
- package/README.md +114 -54
- package/dist/admin.js +231 -7
- package/dist/admin.js.map +1 -1
- package/dist/auth/address.js +89 -0
- package/dist/auth/address.js.map +1 -0
- package/dist/auth/cimd.js +367 -0
- package/dist/auth/cimd.js.map +1 -0
- package/dist/auth/consent-page.js +4 -7
- package/dist/auth/consent-page.js.map +1 -1
- package/dist/auth/login-page.js +4 -7
- package/dist/auth/login-page.js.map +1 -1
- package/dist/auth/page.js +22 -0
- package/dist/auth/page.js.map +1 -1
- package/dist/auth/pinned-fetch.js +129 -0
- package/dist/auth/pinned-fetch.js.map +1 -0
- package/dist/auth/private-key-jwt.js +213 -0
- package/dist/auth/private-key-jwt.js.map +1 -0
- package/dist/auth/provider.js +102 -45
- package/dist/auth/provider.js.map +1 -1
- package/dist/auth/rate-limit.js +43 -0
- package/dist/auth/rate-limit.js.map +1 -0
- package/dist/auth/redirect-uri.js +58 -0
- package/dist/auth/redirect-uri.js.map +1 -0
- package/dist/auth/registration.js +146 -0
- package/dist/auth/registration.js.map +1 -0
- package/dist/auth/routes.js +67 -41
- package/dist/auth/routes.js.map +1 -1
- package/dist/auth/signed-token.js +49 -0
- package/dist/auth/signed-token.js.map +1 -0
- package/dist/auth/store.js +357 -5
- package/dist/auth/store.js.map +1 -1
- package/dist/auth/text.js +51 -0
- package/dist/auth/text.js.map +1 -0
- package/dist/config.js +130 -4
- package/dist/config.js.map +1 -1
- package/dist/health.js +22 -3
- package/dist/health.js.map +1 -1
- package/dist/hub.js +28 -0
- package/dist/hub.js.map +1 -1
- package/dist/index.js +123 -10
- package/dist/index.js.map +1 -1
- package/dist/proxy.js +25 -3
- package/dist/proxy.js.map +1 -1
- package/dist/stdio.js +27 -2
- package/dist/stdio.js.map +1 -1
- package/dist/supervisor.js +152 -8
- package/dist/supervisor.js.map +1 -1
- package/dist/tool-filter.js +58 -0
- package/dist/tool-filter.js.map +1 -0
- package/dist/upstream/auth.js +435 -0
- package/dist/upstream/auth.js.map +1 -0
- package/dist/upstream/login.js +94 -0
- package/dist/upstream/login.js.map +1 -0
- package/dist/upstream/provider.js +286 -0
- package/dist/upstream/provider.js.map +1 -0
- package/dist/upstream/routes.js +96 -0
- package/dist/upstream/routes.js.map +1 -0
- 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.
|
|
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
|
},
|