@ttoss/http-server-mcp 0.16.0 → 0.16.1
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/README.md +2 -86
- package/dist/index.cjs +19 -501
- package/dist/index.d.cts +13 -373
- package/dist/index.d.mts +13 -373
- package/dist/index.mjs +20 -499
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -2,452 +2,13 @@
|
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
4
|
import { Router } from "@ttoss/http-server";
|
|
5
|
+
import { oauthVerify } from "@ttoss/http-server-oauth";
|
|
5
6
|
import { z, z as z$1 } from "zod";
|
|
6
|
-
import { CognitoJwtVerifier } from "@ttoss/auth-core/amazon-cognito";
|
|
7
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
8
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
8
|
|
|
10
|
-
//#region src/
|
|
9
|
+
//#region src/index.ts
|
|
11
10
|
/** MCP lifecycle/discovery methods reachable before a client authenticates. */
|
|
12
11
|
var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
|
|
13
|
-
/**
|
|
14
|
-
* Builds the token verifier from the auth options, preferring an explicit
|
|
15
|
-
* `verifyToken` and otherwise creating a `CognitoJwtVerifier`.
|
|
16
|
-
*/
|
|
17
|
-
var buildTokenVerifier = auth => {
|
|
18
|
-
if (auth.cognitoUserPool) {
|
|
19
|
-
const v = CognitoJwtVerifier.create({
|
|
20
|
-
tokenUse: "access",
|
|
21
|
-
...auth.cognitoUserPool
|
|
22
|
-
});
|
|
23
|
-
return t => {
|
|
24
|
-
return v.verify(t);
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
if (auth.verifyToken) return auth.verifyToken;
|
|
28
|
-
throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
|
|
29
|
-
};
|
|
30
|
-
/** Returns true when the identity carries every required scope (or none are required). */
|
|
31
|
-
var hasRequiredScopes = (requiredScopes, identity) => {
|
|
32
|
-
if (!requiredScopes?.length) return true;
|
|
33
|
-
const tokenScopes = (identity?.scope ?? "").split(" ");
|
|
34
|
-
return requiredScopes.every(s => {
|
|
35
|
-
return tokenScopes.includes(s);
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Registers the token-verification middleware (with public-method bypass and
|
|
40
|
-
* RFC 9728 discovery) and, when configured, the OAuth protected-resource
|
|
41
|
-
* metadata endpoint on the given router.
|
|
42
|
-
*/
|
|
43
|
-
var registerAuthRoutes = (router, path, auth, tokenVerifier) => {
|
|
44
|
-
const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
|
|
45
|
-
const unauthorizedHeader = auth.resourceMetadataUrl ? `Bearer resource_metadata="${auth.resourceMetadataUrl}"` : "Bearer";
|
|
46
|
-
router.use(path, async (ctx, next) => {
|
|
47
|
-
const method = ctx.request.body?.method;
|
|
48
|
-
if (publicMethods.has(method ?? "")) {
|
|
49
|
-
await next();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const authHeader = ctx.headers.authorization;
|
|
53
|
-
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
54
|
-
let identity;
|
|
55
|
-
try {
|
|
56
|
-
identity = await tokenVerifier(token);
|
|
57
|
-
} catch {
|
|
58
|
-
ctx.status = 401;
|
|
59
|
-
ctx.set("WWW-Authenticate", unauthorizedHeader);
|
|
60
|
-
ctx.body = "Unauthorized";
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (!hasRequiredScopes(auth.requiredScopes, identity)) {
|
|
64
|
-
ctx.status = 403;
|
|
65
|
-
ctx.body = "Forbidden";
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
ctx.state.identity = identity;
|
|
69
|
-
await next();
|
|
70
|
-
});
|
|
71
|
-
if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
|
|
72
|
-
ctx.body = {
|
|
73
|
-
resource: auth.resourceServerUrl,
|
|
74
|
-
authorization_servers: [auth.authorizationServerUrl]
|
|
75
|
-
};
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
//#endregion
|
|
80
|
-
//#region src/authServerHandlers.ts
|
|
81
|
-
var base64UrlEncode = buffer => {
|
|
82
|
-
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
83
|
-
};
|
|
84
|
-
/** Generates a URL-safe random token of the given byte length. */
|
|
85
|
-
var generateToken = (bytes = 32) => {
|
|
86
|
-
return base64UrlEncode(randomBytes(bytes));
|
|
87
|
-
};
|
|
88
|
-
var sha256Base64Url = input => {
|
|
89
|
-
return base64UrlEncode(createHash("sha256").update(input).digest());
|
|
90
|
-
};
|
|
91
|
-
/** Verifies a PKCE `code_verifier` against a stored S256 `code_challenge`. */
|
|
92
|
-
var verifyPkce = (codeVerifier, codeChallenge) => {
|
|
93
|
-
return sha256Base64Url(codeVerifier) === codeChallenge;
|
|
94
|
-
};
|
|
95
|
-
/** Joins an issuer base URL with a path, collapsing any duplicate slash. */
|
|
96
|
-
var joinUrl = (issuer, path) => {
|
|
97
|
-
return `${issuer.replace(/\/$/, "")}${path}`;
|
|
98
|
-
};
|
|
99
|
-
/** Returns the value if it is a string, otherwise `undefined`. */
|
|
100
|
-
var asString = value => {
|
|
101
|
-
return typeof value === "string" ? value : void 0;
|
|
102
|
-
};
|
|
103
|
-
var parseScopes = scope => {
|
|
104
|
-
const value = asString(scope);
|
|
105
|
-
return value ? value.split(" ").filter(Boolean) : [];
|
|
106
|
-
};
|
|
107
|
-
/** Writes a spec-compliant OAuth error response (RFC 6749 §5.2). */
|
|
108
|
-
var sendOAuthError = (ctx, status, error, description) => {
|
|
109
|
-
ctx.status = status;
|
|
110
|
-
ctx.body = {
|
|
111
|
-
error,
|
|
112
|
-
error_description: description
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
var buildTokenResponse = (tokens, scopes) => {
|
|
116
|
-
return {
|
|
117
|
-
access_token: tokens.accessToken,
|
|
118
|
-
token_type: "Bearer",
|
|
119
|
-
...(tokens.expiresIn !== void 0 ? {
|
|
120
|
-
expires_in: tokens.expiresIn
|
|
121
|
-
} : {}),
|
|
122
|
-
...(tokens.refreshToken !== void 0 ? {
|
|
123
|
-
refresh_token: tokens.refreshToken
|
|
124
|
-
} : {}),
|
|
125
|
-
scope: tokens.scope ?? scopes.join(" ")
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Authenticates the client at the token endpoint via `client_secret_basic`
|
|
130
|
-
* (Authorization header), `client_secret_post` (body), or `none` (public,
|
|
131
|
-
* PKCE-only). Returns the client, or `undefined` when authentication fails.
|
|
132
|
-
*/
|
|
133
|
-
var authenticateClient = async (ctx, body, clientStore) => {
|
|
134
|
-
let clientId = asString(body.client_id);
|
|
135
|
-
let clientSecret = asString(body.client_secret);
|
|
136
|
-
const authHeader = ctx.headers.authorization;
|
|
137
|
-
if (authHeader?.startsWith("Basic ")) {
|
|
138
|
-
const decoded = Buffer.from(authHeader.slice(6), "base64").toString();
|
|
139
|
-
const separatorIndex = decoded.indexOf(":");
|
|
140
|
-
if (separatorIndex !== -1) {
|
|
141
|
-
clientId = decodeURIComponent(decoded.slice(0, separatorIndex));
|
|
142
|
-
clientSecret = decodeURIComponent(decoded.slice(separatorIndex + 1));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (!clientId) return;
|
|
146
|
-
const client = await clientStore.get(clientId);
|
|
147
|
-
if (!client) return;
|
|
148
|
-
if (client.client_secret && client.client_secret !== clientSecret) return;
|
|
149
|
-
return client;
|
|
150
|
-
};
|
|
151
|
-
/** Handles `grant_type=authorization_code` token requests (with PKCE verify). */
|
|
152
|
-
var handleAuthorizationCodeGrant = async (ctx, body, options) => {
|
|
153
|
-
const {
|
|
154
|
-
clientStore,
|
|
155
|
-
authCodeStore,
|
|
156
|
-
issueTokens
|
|
157
|
-
} = options;
|
|
158
|
-
const client = await authenticateClient(ctx, body, clientStore);
|
|
159
|
-
if (!client) {
|
|
160
|
-
sendOAuthError(ctx, 401, "invalid_client", "client authentication failed");
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const code = asString(body.code);
|
|
164
|
-
if (!code) {
|
|
165
|
-
sendOAuthError(ctx, 400, "invalid_request", "code is required");
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const stored = await authCodeStore.get(code);
|
|
169
|
-
await authCodeStore.delete(code);
|
|
170
|
-
if (!stored) {
|
|
171
|
-
sendOAuthError(ctx, 400, "invalid_grant", "invalid or expired authorization code");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (stored.expiresAt < Date.now()) {
|
|
175
|
-
sendOAuthError(ctx, 400, "invalid_grant", "authorization code expired");
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (stored.clientId !== client.client_id) {
|
|
179
|
-
sendOAuthError(ctx, 400, "invalid_grant", "authorization code was not issued to this client");
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (stored.redirectUri !== asString(body.redirect_uri)) {
|
|
183
|
-
sendOAuthError(ctx, 400, "invalid_grant", "redirect_uri mismatch");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const codeVerifier = asString(body.code_verifier);
|
|
187
|
-
if (!codeVerifier) {
|
|
188
|
-
sendOAuthError(ctx, 400, "invalid_request", "code_verifier is required");
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (!verifyPkce(codeVerifier, stored.codeChallenge)) {
|
|
192
|
-
sendOAuthError(ctx, 400, "invalid_grant", "PKCE verification failed");
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
ctx.body = buildTokenResponse(await issueTokens({
|
|
196
|
-
subject: stored.subject,
|
|
197
|
-
scopes: stored.scopes,
|
|
198
|
-
client
|
|
199
|
-
}), stored.scopes);
|
|
200
|
-
};
|
|
201
|
-
/** Handles `grant_type=refresh_token` token requests. */
|
|
202
|
-
var handleRefreshTokenGrant = async (ctx, body, options) => {
|
|
203
|
-
const {
|
|
204
|
-
clientStore,
|
|
205
|
-
issueTokens,
|
|
206
|
-
onRefreshToken
|
|
207
|
-
} = options;
|
|
208
|
-
const client = await authenticateClient(ctx, body, clientStore);
|
|
209
|
-
if (!client) {
|
|
210
|
-
sendOAuthError(ctx, 401, "invalid_client", "client authentication failed");
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if (!onRefreshToken) {
|
|
214
|
-
sendOAuthError(ctx, 400, "unsupported_grant_type", "refresh_token grant is not supported");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const refreshToken = asString(body.refresh_token);
|
|
218
|
-
if (!refreshToken) {
|
|
219
|
-
sendOAuthError(ctx, 400, "invalid_request", "refresh_token is required");
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const result = await onRefreshToken({
|
|
223
|
-
refreshToken,
|
|
224
|
-
client,
|
|
225
|
-
scopes: parseScopes(body.scope)
|
|
226
|
-
});
|
|
227
|
-
if (!result) {
|
|
228
|
-
sendOAuthError(ctx, 400, "invalid_grant", "invalid refresh token");
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
ctx.body = buildTokenResponse(await issueTokens({
|
|
232
|
-
subject: result.subject,
|
|
233
|
-
scopes: result.scopes,
|
|
234
|
-
client
|
|
235
|
-
}), result.scopes);
|
|
236
|
-
};
|
|
237
|
-
/**
|
|
238
|
-
* Handles the authorization request after `client_id` and `redirect_uri` have
|
|
239
|
-
* been validated: enforces PKCE, runs the consent hook, and issues a code.
|
|
240
|
-
*/
|
|
241
|
-
var handleAuthorize = async (ctx, options, redirectUri) => {
|
|
242
|
-
const {
|
|
243
|
-
clientStore,
|
|
244
|
-
authCodeStore,
|
|
245
|
-
onAuthorize
|
|
246
|
-
} = options;
|
|
247
|
-
const ttl = options.authorizationCodeTtl ?? 600;
|
|
248
|
-
const clientId = asString(ctx.query.client_id);
|
|
249
|
-
const client = await clientStore.get(clientId);
|
|
250
|
-
const state = asString(ctx.query.state);
|
|
251
|
-
const redirectError = (error, description) => {
|
|
252
|
-
const url = new URL(redirectUri);
|
|
253
|
-
url.searchParams.set("error", error);
|
|
254
|
-
url.searchParams.set("error_description", description);
|
|
255
|
-
if (state) url.searchParams.set("state", state);
|
|
256
|
-
ctx.redirect(url.toString());
|
|
257
|
-
};
|
|
258
|
-
if (asString(ctx.query.response_type) !== "code") {
|
|
259
|
-
redirectError("unsupported_response_type", "response_type must be code");
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const codeChallenge = asString(ctx.query.code_challenge);
|
|
263
|
-
if (!codeChallenge) {
|
|
264
|
-
redirectError("invalid_request", "code_challenge is required (PKCE)");
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const codeChallengeMethod = asString(ctx.query.code_challenge_method) ?? "plain";
|
|
268
|
-
if (codeChallengeMethod !== "S256") {
|
|
269
|
-
redirectError("invalid_request", "code_challenge_method must be S256");
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
const scopes = parseScopes(ctx.query.scope);
|
|
273
|
-
const result = await onAuthorize({
|
|
274
|
-
ctx,
|
|
275
|
-
client,
|
|
276
|
-
request: {
|
|
277
|
-
clientId,
|
|
278
|
-
redirectUri,
|
|
279
|
-
scopes,
|
|
280
|
-
state,
|
|
281
|
-
codeChallenge,
|
|
282
|
-
codeChallengeMethod
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
if (!result.approved) return;
|
|
286
|
-
const grantedScopes = result.scopes ?? scopes;
|
|
287
|
-
const code = generateToken();
|
|
288
|
-
await authCodeStore.save({
|
|
289
|
-
code,
|
|
290
|
-
clientId,
|
|
291
|
-
redirectUri,
|
|
292
|
-
codeChallenge,
|
|
293
|
-
scopes: grantedScopes,
|
|
294
|
-
subject: result.subject,
|
|
295
|
-
expiresAt: Date.now() + ttl * 1e3
|
|
296
|
-
});
|
|
297
|
-
const url = new URL(redirectUri);
|
|
298
|
-
url.searchParams.set("code", code);
|
|
299
|
-
if (state) url.searchParams.set("state", state);
|
|
300
|
-
ctx.redirect(url.toString());
|
|
301
|
-
};
|
|
302
|
-
/** Handles Dynamic Client Registration (RFC 7591) requests. */
|
|
303
|
-
var handleRegister = async (ctx, clientStore) => {
|
|
304
|
-
const metadata = ctx.request.body ?? {};
|
|
305
|
-
const redirectUris = metadata.redirect_uris;
|
|
306
|
-
if (!Array.isArray(redirectUris) || redirectUris.length === 0 || !redirectUris.every(uri => {
|
|
307
|
-
return typeof uri === "string";
|
|
308
|
-
})) {
|
|
309
|
-
sendOAuthError(ctx, 400, "invalid_redirect_uri", "redirect_uris is required and must be an array of strings");
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
const tokenAuthMethod = asString(metadata.token_endpoint_auth_method) ?? "client_secret_basic";
|
|
313
|
-
const isPublic = tokenAuthMethod === "none";
|
|
314
|
-
const client = {
|
|
315
|
-
...metadata,
|
|
316
|
-
client_id: generateToken(16),
|
|
317
|
-
redirect_uris: redirectUris,
|
|
318
|
-
token_endpoint_auth_method: tokenAuthMethod,
|
|
319
|
-
grant_types: metadata.grant_types ?? ["authorization_code", "refresh_token"],
|
|
320
|
-
response_types: metadata.response_types ?? ["code"],
|
|
321
|
-
client_id_issued_at: Math.floor(Date.now() / 1e3)
|
|
322
|
-
};
|
|
323
|
-
if (!isPublic) {
|
|
324
|
-
client.client_secret = generateToken(32);
|
|
325
|
-
client.client_secret_expires_at = 0;
|
|
326
|
-
}
|
|
327
|
-
await clientStore.register(client);
|
|
328
|
-
ctx.status = 201;
|
|
329
|
-
ctx.body = client;
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
//#endregion
|
|
333
|
-
//#region src/authServer.ts
|
|
334
|
-
/**
|
|
335
|
-
* Creates transport-agnostic OAuth 2.1 Authorization Server primitives for MCP.
|
|
336
|
-
*
|
|
337
|
-
* Mounts the authorization endpoint (`/authorize`, PKCE S256 required), token
|
|
338
|
-
* endpoint (`/token`, `authorization_code` + `refresh_token` grants), Dynamic
|
|
339
|
-
* Client Registration (`/register`, RFC 7591), and AS metadata discovery
|
|
340
|
-
* (`/.well-known/oauth-authorization-server`, RFC 8414). When `resource` is
|
|
341
|
-
* set, it also serves the protected-resource metadata (RFC 9728).
|
|
342
|
-
*
|
|
343
|
-
* ttoss owns only the protocol mechanics — the app supplies its own stores,
|
|
344
|
-
* token signing/verification, and login/consent UI through the option hooks, so
|
|
345
|
-
* the user model, JWT/IAM, and authentication never leave the consuming app.
|
|
346
|
-
*
|
|
347
|
-
* @param options - Authorization server configuration and pluggable hooks.
|
|
348
|
-
* @returns A Koa `Router` exposing `routes()` / `allowedMethods()`.
|
|
349
|
-
*
|
|
350
|
-
* @example
|
|
351
|
-
* ```typescript
|
|
352
|
-
* import { App, bodyParser } from '@ttoss/http-server';
|
|
353
|
-
* import { createMcpAuthServer } from '@ttoss/http-server-mcp';
|
|
354
|
-
*
|
|
355
|
-
* const authServer = createMcpAuthServer({
|
|
356
|
-
* issuer: 'https://api.soat.dev',
|
|
357
|
-
* clientStore,
|
|
358
|
-
* authCodeStore,
|
|
359
|
-
* issueTokens: async ({ subject, scopes }) => ({
|
|
360
|
-
* accessToken: signJwt({ sub: subject, scope: scopes.join(' ') }),
|
|
361
|
-
* refreshToken: createRefreshToken(subject),
|
|
362
|
-
* expiresIn: 3600,
|
|
363
|
-
* }),
|
|
364
|
-
* onAuthorize: async ({ ctx }) => {
|
|
365
|
-
* const session = await getSession(ctx);
|
|
366
|
-
* if (!session) {
|
|
367
|
-
* ctx.redirect('/login');
|
|
368
|
-
* return { approved: false };
|
|
369
|
-
* }
|
|
370
|
-
* return { approved: true, subject: session.userId };
|
|
371
|
-
* },
|
|
372
|
-
* scopesSupported: ['mcp:access'],
|
|
373
|
-
* });
|
|
374
|
-
*
|
|
375
|
-
* const app = new App();
|
|
376
|
-
* app.use(bodyParser());
|
|
377
|
-
* app.use(authServer.routes());
|
|
378
|
-
* ```
|
|
379
|
-
*/
|
|
380
|
-
var createMcpAuthServer = options => {
|
|
381
|
-
const {
|
|
382
|
-
issuer,
|
|
383
|
-
clientStore,
|
|
384
|
-
scopesSupported,
|
|
385
|
-
resource
|
|
386
|
-
} = options;
|
|
387
|
-
const authorizePath = options.endpoints?.authorize ?? "/authorize";
|
|
388
|
-
const tokenPath = options.endpoints?.token ?? "/token";
|
|
389
|
-
const registerPath = options.endpoints?.register ?? "/register";
|
|
390
|
-
const router = new Router();
|
|
391
|
-
router.get("/.well-known/oauth-authorization-server", ctx => {
|
|
392
|
-
ctx.body = {
|
|
393
|
-
issuer,
|
|
394
|
-
authorization_endpoint: joinUrl(issuer, authorizePath),
|
|
395
|
-
token_endpoint: joinUrl(issuer, tokenPath),
|
|
396
|
-
registration_endpoint: joinUrl(issuer, registerPath),
|
|
397
|
-
response_types_supported: ["code"],
|
|
398
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
399
|
-
code_challenge_methods_supported: ["S256"],
|
|
400
|
-
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"],
|
|
401
|
-
...(scopesSupported ? {
|
|
402
|
-
scopes_supported: scopesSupported
|
|
403
|
-
} : {})
|
|
404
|
-
};
|
|
405
|
-
});
|
|
406
|
-
if (resource) router.get("/.well-known/oauth-protected-resource", ctx => {
|
|
407
|
-
ctx.body = {
|
|
408
|
-
resource,
|
|
409
|
-
authorization_servers: [issuer]
|
|
410
|
-
};
|
|
411
|
-
});
|
|
412
|
-
router.get(authorizePath, async ctx => {
|
|
413
|
-
const clientId = asString(ctx.query.client_id);
|
|
414
|
-
if (!clientId) {
|
|
415
|
-
sendOAuthError(ctx, 400, "invalid_request", "client_id is required");
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
const client = await clientStore.get(clientId);
|
|
419
|
-
if (!client) {
|
|
420
|
-
sendOAuthError(ctx, 400, "invalid_client", "unknown client_id");
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const redirectUri = asString(ctx.query.redirect_uri);
|
|
424
|
-
if (!redirectUri || !client.redirect_uris.includes(redirectUri)) {
|
|
425
|
-
sendOAuthError(ctx, 400, "invalid_request", "redirect_uri is missing or not registered for this client");
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
await handleAuthorize(ctx, options, redirectUri);
|
|
429
|
-
});
|
|
430
|
-
router.post(tokenPath, async ctx => {
|
|
431
|
-
const body = ctx.request.body ?? {};
|
|
432
|
-
const grantType = asString(body.grant_type);
|
|
433
|
-
if (grantType === "authorization_code") {
|
|
434
|
-
await handleAuthorizationCodeGrant(ctx, body, options);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (grantType === "refresh_token") {
|
|
438
|
-
await handleRefreshTokenGrant(ctx, body, options);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
sendOAuthError(ctx, 400, "unsupported_grant_type", "grant_type must be authorization_code or refresh_token");
|
|
442
|
-
});
|
|
443
|
-
router.post(registerPath, async ctx => {
|
|
444
|
-
await handleRegister(ctx, clientStore);
|
|
445
|
-
});
|
|
446
|
-
return router;
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
//#endregion
|
|
450
|
-
//#region src/index.ts
|
|
451
12
|
var requestContextStore = new AsyncLocalStorage();
|
|
452
13
|
/**
|
|
453
14
|
* Generic HTTP helper for use inside MCP tool handlers.
|
|
@@ -627,7 +188,23 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
627
188
|
return result;
|
|
628
189
|
};
|
|
629
190
|
const router = new Router();
|
|
630
|
-
if (auth)
|
|
191
|
+
if (auth) {
|
|
192
|
+
const {
|
|
193
|
+
resourceServerUrl,
|
|
194
|
+
authorizationServerUrl,
|
|
195
|
+
...verifyOptions
|
|
196
|
+
} = auth;
|
|
197
|
+
router.use(path, oauthVerify({
|
|
198
|
+
...verifyOptions,
|
|
199
|
+
publicMethods: verifyOptions.publicMethods ?? DEFAULT_PUBLIC_METHODS
|
|
200
|
+
}));
|
|
201
|
+
if (resourceServerUrl && authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
|
|
202
|
+
ctx.body = {
|
|
203
|
+
resource: resourceServerUrl,
|
|
204
|
+
authorization_servers: [authorizationServerUrl]
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
631
208
|
const handleWithContext = async (ctx, body) => {
|
|
632
209
|
const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
|
|
633
210
|
const identity = ctx.state.identity;
|
|
@@ -772,62 +349,6 @@ var registerToolFromSchema = (server, params) => {
|
|
|
772
349
|
});
|
|
773
350
|
}
|
|
774
351
|
};
|
|
775
|
-
/**
|
|
776
|
-
* Returns the `WWW-Authenticate` header value for a 401 response on a
|
|
777
|
-
* protected resource, following the MCP auth spec requirement that
|
|
778
|
-
* unauthorized responses advertise the resource metadata URL so MCP
|
|
779
|
-
* clients can bootstrap OAuth discovery.
|
|
780
|
-
*
|
|
781
|
-
* Use this in your own auth middleware when you are not using the built-in
|
|
782
|
-
* `auth` option on `createMcpRouter`.
|
|
783
|
-
*
|
|
784
|
-
* @example
|
|
785
|
-
* ```typescript
|
|
786
|
-
* import { getWwwAuthenticateHeader } from '@ttoss/http-server-mcp';
|
|
787
|
-
*
|
|
788
|
-
* // Inside a Koa middleware
|
|
789
|
-
* ctx.status = 401;
|
|
790
|
-
* ctx.set('WWW-Authenticate', getWwwAuthenticateHeader({ resource: 'https://mcp.example.com' }));
|
|
791
|
-
* ```
|
|
792
|
-
*/
|
|
793
|
-
var getWwwAuthenticateHeader = args => {
|
|
794
|
-
return `Bearer resource_metadata="${`${args.resource.replace(/\/$/, "")}/.well-known/oauth-protected-resource`}"`;
|
|
795
|
-
};
|
|
796
|
-
/**
|
|
797
|
-
* Creates a standalone Koa middleware that serves
|
|
798
|
-
* `GET /.well-known/oauth-protected-resource` (RFC 9728) without requiring
|
|
799
|
-
* the built-in `auth` option on `createMcpRouter`.
|
|
800
|
-
*
|
|
801
|
-
* Mount this **before** your own auth middleware so the discovery endpoint
|
|
802
|
-
* remains unauthenticated (MCP clients fetch it before they have a token).
|
|
803
|
-
*
|
|
804
|
-
* @example
|
|
805
|
-
* ```typescript
|
|
806
|
-
* import Koa from 'koa';
|
|
807
|
-
* import { createProtectedResourceMetadataMiddleware } from '@ttoss/http-server-mcp';
|
|
808
|
-
*
|
|
809
|
-
* const app = new Koa();
|
|
810
|
-
* app.use(
|
|
811
|
-
* createProtectedResourceMetadataMiddleware({
|
|
812
|
-
* resource: 'https://mcp.example.com',
|
|
813
|
-
* authorizationServers: ['https://api.example.com'],
|
|
814
|
-
* })
|
|
815
|
-
* );
|
|
816
|
-
* app.use(myOwnAuthMiddleware);
|
|
817
|
-
* ```
|
|
818
|
-
*/
|
|
819
|
-
var createProtectedResourceMetadataMiddleware = args => {
|
|
820
|
-
return async (ctx, next) => {
|
|
821
|
-
if (ctx.method === "GET" && ctx.path === "/.well-known/oauth-protected-resource") {
|
|
822
|
-
ctx.body = {
|
|
823
|
-
resource: args.resource,
|
|
824
|
-
authorization_servers: args.authorizationServers
|
|
825
|
-
};
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
await next();
|
|
829
|
-
};
|
|
830
|
-
};
|
|
831
352
|
|
|
832
353
|
//#endregion
|
|
833
|
-
export { McpServer, apiCall, checkScopes,
|
|
354
|
+
export { McpServer, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/http-server-mcp",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"zod": "^4.4.3",
|
|
38
|
-
"@ttoss/
|
|
39
|
-
"@ttoss/http-server": "^0.
|
|
38
|
+
"@ttoss/http-server": "^0.7.0",
|
|
39
|
+
"@ttoss/http-server-oauth": "^0.1.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/koa": "^3.0.3",
|