@akira-tl/forgerelay 1.1.0 → 1.1.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/CHANGELOG.md +19 -0
- package/README.md +4 -2
- package/dist/activity/runtime/mcp-query-tools.js +16 -10
- package/dist/cli/mcp/external-mcp.js +481 -0
- package/dist/cli/mcp/status.js +295 -0
- package/dist/cli.js +14 -1
- package/dist/lsp/test-support/server-fixture.js +1 -2
- package/dist/mcp/oauth/auth-protocol.js +375 -0
- package/dist/mcp/oauth/oauth-provider.js +11 -12
- package/dist/mcp/oauth/oauth-store.js +2 -2
- package/dist/mcp/oauth/router.js +2 -6
- package/dist/mcp/operations/batch/executor.js +1 -1
- package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +23 -6
- package/dist/mcp/operations/external-mcp/external-mcp.js +178 -23
- package/dist/mcp/operations/native-bulk-mutations.js +1 -1
- package/dist/mcp/process/tools.js +19 -15
- package/dist/mcp/request-context.js +18 -0
- package/dist/mcp/request-meta.js +28 -3
- package/dist/mcp/server/core/capabilities.js +1 -4
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +35 -28
- package/dist/mcp/server/operations/runtime/operation-runtime.js +7 -7
- package/dist/mcp/server/transport/http-server.js +41 -20
- package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +29 -25
- package/dist/runtime/config/external-mcp-auth-store.js +225 -0
- package/dist/runtime/config/external-mcp-config.js +76 -22
- package/dist/runtime/config/external-mcp-registry.js +165 -0
- package/dist/runtime/testing/server-fixture.js +4 -4
- package/dist/server.js +15 -9
- package/dist/ui/.vite/manifest.json +297 -298
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
- package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
- package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
- package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
- package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
- package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
- package/dist/ui/assets/c-BquuA6bv.js +1 -0
- package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
- package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
- package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
- package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
- package/dist/ui/assets/css-Be_T8idh.js +1 -0
- package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
- package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
- package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
- package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
- package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
- package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
- package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
- package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
- package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
- package/dist/ui/assets/haml-h18lG0pS.js +1 -0
- package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
- package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
- package/dist/ui/assets/html-DtILqTE1.js +1 -0
- package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
- package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
- package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
- package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
- package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
- package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
- package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
- package/dist/ui/assets/json-DW2UTv1q.js +1 -0
- package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
- package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
- package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
- package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
- package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
- package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
- package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
- package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
- package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
- package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
- package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
- package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
- package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
- package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
- package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
- package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
- package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
- package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
- package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
- package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
- package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
- package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
- package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
- package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
- package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
- package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
- package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
- package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
- package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
- package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
- package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
- package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
- package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
- package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
- package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
- package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
- package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
- package/dist/ui/assets/xml-_44rx9_2.js +1 -0
- package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
- package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
- package/dist/workspaces/relay/auth/remote-auth.js +3 -5
- package/dist/workspaces/relay/tests/test-support.js +7 -4
- package/dist/workspaces/relay/workspace-relay.js +14 -8
- package/docs/configuration.md +154 -4
- package/docs/debugging.md +8 -5
- package/docs/roadmap.md +64 -3
- package/docs/security.md +18 -0
- package/package.json +11 -4
- package/scripts/debug/accept/bootstrap.mjs +2 -0
- package/scripts/debug/accept/harness.mjs +9 -2
- package/scripts/debug/accept/modern-http.mjs +169 -0
- package/scripts/debug/accept/support.mjs +10 -6
- package/scripts/debug/accept.mjs +1 -0
- package/scripts/debug/runtime.mjs +4 -1
- package/scripts/debug/runtime.test.mjs +9 -0
- package/scripts/release/parity-sandbox.mjs +35 -0
- package/scripts/release/release-gate.test.mjs +42 -1
- package/scripts/release-parity.mjs +3 -22
- package/scripts/release-proof.mjs +24 -4
- package/scripts/release-proof.test.mjs +12 -4
- package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
- package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
- package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
- package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
- package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
- package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
- package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
- package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
- package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
- package/dist/ui/assets/html-dUOs5pA9.js +0 -1
- package/dist/ui/assets/java-BFZCrORY.js +0 -1
- package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
- package/dist/ui/assets/json-BQC_Yp48.js +0 -1
- package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
- package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
- package/dist/ui/assets/r-B5nYsy-M.js +0 -1
- package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
- package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
- package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
- package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
- package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
- package/dist/ui/assets/typescript-BGIABksB.js +0 -1
- package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
- package/dist/ui/assets/yaml-umbj2r1g.js +0 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { rateLimit } from "express-rate-limit";
|
|
4
|
+
import { OAuthClientMetadataSchema, OAuthTokenRevocationRequestSchema, } from "@modelcontextprotocol/core";
|
|
5
|
+
import { OAuthError, OAuthErrorCode, } from "@modelcontextprotocol/server";
|
|
6
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
7
|
+
function oauthError(code, message) {
|
|
8
|
+
return new OAuthError(code, message);
|
|
9
|
+
}
|
|
10
|
+
function statusForOAuthError(error) {
|
|
11
|
+
if (error.code === OAuthErrorCode.ServerError)
|
|
12
|
+
return 500;
|
|
13
|
+
if (error.code === OAuthErrorCode.TooManyRequests)
|
|
14
|
+
return 429;
|
|
15
|
+
if (error.code === OAuthErrorCode.MethodNotAllowed)
|
|
16
|
+
return 405;
|
|
17
|
+
return 400;
|
|
18
|
+
}
|
|
19
|
+
function sendOAuthError(res, error) {
|
|
20
|
+
const normalized = error instanceof OAuthError
|
|
21
|
+
? error
|
|
22
|
+
: oauthError(OAuthErrorCode.ServerError, "Internal Server Error");
|
|
23
|
+
res.status(statusForOAuthError(normalized)).json(normalized.toResponseObject());
|
|
24
|
+
}
|
|
25
|
+
function applyCors(res, methods) {
|
|
26
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
27
|
+
res.setHeader("Access-Control-Allow-Headers", "content-type");
|
|
28
|
+
res.setHeader("Access-Control-Allow-Methods", [...methods, "OPTIONS"].join(", "));
|
|
29
|
+
}
|
|
30
|
+
function allowMethods(methods) {
|
|
31
|
+
return (req, res, next) => {
|
|
32
|
+
applyCors(res, methods);
|
|
33
|
+
if (req.method === "OPTIONS") {
|
|
34
|
+
res.status(204).end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!methods.includes(req.method)) {
|
|
38
|
+
res.setHeader("Allow", methods.join(", "));
|
|
39
|
+
sendOAuthError(res, oauthError(OAuthErrorCode.MethodNotAllowed, "Method not allowed"));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
next();
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function endpointRateLimit(max, message) {
|
|
46
|
+
return rateLimit({
|
|
47
|
+
windowMs: 15 * 60 * 1000,
|
|
48
|
+
max,
|
|
49
|
+
standardHeaders: true,
|
|
50
|
+
legacyHeaders: false,
|
|
51
|
+
message: oauthError(OAuthErrorCode.TooManyRequests, message).toResponseObject(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function redirectUriMatches(requested, registered) {
|
|
55
|
+
if (requested === registered)
|
|
56
|
+
return true;
|
|
57
|
+
let req;
|
|
58
|
+
let reg;
|
|
59
|
+
try {
|
|
60
|
+
req = new URL(requested);
|
|
61
|
+
reg = new URL(registered);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (!LOOPBACK_HOSTS.has(req.hostname) || !LOOPBACK_HOSTS.has(reg.hostname))
|
|
67
|
+
return false;
|
|
68
|
+
return req.protocol === reg.protocol
|
|
69
|
+
&& req.hostname === reg.hostname
|
|
70
|
+
&& req.pathname === reg.pathname
|
|
71
|
+
&& req.search === reg.search;
|
|
72
|
+
}
|
|
73
|
+
function withIssuerOnCallbackRedirect(res, redirectUri, issuer) {
|
|
74
|
+
const callback = new URL(redirectUri);
|
|
75
|
+
const appendIssuer = (value) => {
|
|
76
|
+
let target;
|
|
77
|
+
try {
|
|
78
|
+
target = new URL(value);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (target.origin === callback.origin
|
|
84
|
+
&& target.pathname === callback.pathname
|
|
85
|
+
&& !target.searchParams.has("iss")) {
|
|
86
|
+
target.searchParams.set("iss", issuer);
|
|
87
|
+
return target.href;
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
const originalRedirect = res.redirect.bind(res);
|
|
92
|
+
res.redirect = ((statusOrUrl, maybeUrl) => {
|
|
93
|
+
if (typeof statusOrUrl === "number") {
|
|
94
|
+
originalRedirect(statusOrUrl, appendIssuer(String(maybeUrl)));
|
|
95
|
+
}
|
|
96
|
+
else if (typeof maybeUrl === "number") {
|
|
97
|
+
originalRedirect(appendIssuer(statusOrUrl), maybeUrl);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
originalRedirect(appendIssuer(statusOrUrl));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return res;
|
|
104
|
+
}
|
|
105
|
+
function errorRedirect(redirectUri, error, state, issuer) {
|
|
106
|
+
const target = new URL(redirectUri);
|
|
107
|
+
target.searchParams.set("error", String(error.code));
|
|
108
|
+
target.searchParams.set("error_description", error.message);
|
|
109
|
+
if (error.errorUri)
|
|
110
|
+
target.searchParams.set("error_uri", error.errorUri);
|
|
111
|
+
if (state)
|
|
112
|
+
target.searchParams.set("state", state);
|
|
113
|
+
if (issuer)
|
|
114
|
+
target.searchParams.set("iss", issuer);
|
|
115
|
+
return target.href;
|
|
116
|
+
}
|
|
117
|
+
async function authenticateClient(provider, body) {
|
|
118
|
+
if (typeof body !== "object" || body === null) {
|
|
119
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "OAuth request body is required");
|
|
120
|
+
}
|
|
121
|
+
const record = body;
|
|
122
|
+
const clientId = typeof record.client_id === "string" ? record.client_id : undefined;
|
|
123
|
+
const clientSecret = typeof record.client_secret === "string" ? record.client_secret : undefined;
|
|
124
|
+
if (!clientId)
|
|
125
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "client_id is required");
|
|
126
|
+
const client = await provider.clientsStore.getClient(clientId);
|
|
127
|
+
if (!client)
|
|
128
|
+
throw oauthError(OAuthErrorCode.InvalidClient, "Invalid client_id");
|
|
129
|
+
if (client.client_secret) {
|
|
130
|
+
if (!clientSecret || client.client_secret !== clientSecret) {
|
|
131
|
+
throw oauthError(OAuthErrorCode.InvalidClient, "Invalid client_secret");
|
|
132
|
+
}
|
|
133
|
+
if (client.client_secret_expires_at
|
|
134
|
+
&& client.client_secret_expires_at < Math.floor(Date.now() / 1000)) {
|
|
135
|
+
throw oauthError(OAuthErrorCode.InvalidClient, "Client secret has expired");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return client;
|
|
139
|
+
}
|
|
140
|
+
function verifyPkce(verifier, challenge) {
|
|
141
|
+
return createHash("sha256").update(verifier).digest("base64url") === challenge;
|
|
142
|
+
}
|
|
143
|
+
export function authorizationHandler(options) {
|
|
144
|
+
const { provider, issuerUrl } = options;
|
|
145
|
+
const issuer = issuerUrl?.href;
|
|
146
|
+
const router = express.Router();
|
|
147
|
+
router.use(allowMethods(["GET", "POST"]));
|
|
148
|
+
router.use(express.urlencoded({ extended: false }));
|
|
149
|
+
router.use(endpointRateLimit(100, "You have exceeded the rate limit for authorization requests"));
|
|
150
|
+
router.all("/", async (req, res) => {
|
|
151
|
+
res.setHeader("Cache-Control", "no-store");
|
|
152
|
+
const raw = (req.method === "POST" ? req.body : req.query);
|
|
153
|
+
let redirectUri;
|
|
154
|
+
let client;
|
|
155
|
+
try {
|
|
156
|
+
const clientId = typeof raw.client_id === "string" ? raw.client_id : undefined;
|
|
157
|
+
redirectUri = typeof raw.redirect_uri === "string" ? raw.redirect_uri : undefined;
|
|
158
|
+
if (!clientId)
|
|
159
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "client_id is required");
|
|
160
|
+
if (redirectUri !== undefined && !URL.canParse(redirectUri)) {
|
|
161
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "redirect_uri must be a valid URL");
|
|
162
|
+
}
|
|
163
|
+
client = await provider.clientsStore.getClient(clientId);
|
|
164
|
+
if (!client)
|
|
165
|
+
throw oauthError(OAuthErrorCode.InvalidClient, "Invalid client_id");
|
|
166
|
+
if (redirectUri !== undefined) {
|
|
167
|
+
if (!client.redirect_uris.some((registered) => redirectUriMatches(redirectUri, String(registered)))) {
|
|
168
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "Unregistered redirect_uri");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (client.redirect_uris.length === 1) {
|
|
172
|
+
redirectUri = String(client.redirect_uris[0]);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "redirect_uri must be specified when client has multiple registered URIs");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
sendOAuthError(res, error);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const state = typeof raw.state === "string" ? raw.state : undefined;
|
|
183
|
+
try {
|
|
184
|
+
if (raw.response_type !== "code") {
|
|
185
|
+
throw oauthError(OAuthErrorCode.UnsupportedResponseType, "response_type must be code");
|
|
186
|
+
}
|
|
187
|
+
const codeChallenge = typeof raw.code_challenge === "string" ? raw.code_challenge : undefined;
|
|
188
|
+
if (!codeChallenge || raw.code_challenge_method !== "S256") {
|
|
189
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "S256 PKCE code_challenge is required");
|
|
190
|
+
}
|
|
191
|
+
const scope = typeof raw.scope === "string" ? raw.scope : undefined;
|
|
192
|
+
const resource = typeof raw.resource === "string" ? raw.resource : undefined;
|
|
193
|
+
if (resource !== undefined && !URL.canParse(resource)) {
|
|
194
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "resource must be a valid URL");
|
|
195
|
+
}
|
|
196
|
+
await provider.authorize(client, {
|
|
197
|
+
state,
|
|
198
|
+
scopes: scope === undefined ? [] : scope.split(" ").filter(Boolean),
|
|
199
|
+
redirectUri: redirectUri,
|
|
200
|
+
codeChallenge,
|
|
201
|
+
resource: resource ? new URL(resource) : undefined,
|
|
202
|
+
issuer,
|
|
203
|
+
}, issuer ? withIssuerOnCallbackRedirect(res, redirectUri, issuer) : res);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const normalized = error instanceof OAuthError
|
|
207
|
+
? error
|
|
208
|
+
: oauthError(OAuthErrorCode.ServerError, "Internal Server Error");
|
|
209
|
+
res.redirect(302, errorRedirect(redirectUri, normalized, state, issuer));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
return router;
|
|
213
|
+
}
|
|
214
|
+
export function tokenHandler(options) {
|
|
215
|
+
const { provider } = options;
|
|
216
|
+
const router = express.Router();
|
|
217
|
+
router.use(allowMethods(["POST"]));
|
|
218
|
+
router.use(express.urlencoded({ extended: false }));
|
|
219
|
+
router.use(endpointRateLimit(50, "You have exceeded the rate limit for token requests"));
|
|
220
|
+
router.post("/", async (req, res) => {
|
|
221
|
+
res.setHeader("Cache-Control", "no-store");
|
|
222
|
+
try {
|
|
223
|
+
const client = await authenticateClient(provider, req.body);
|
|
224
|
+
const body = req.body;
|
|
225
|
+
const grantType = typeof body.grant_type === "string" ? body.grant_type : undefined;
|
|
226
|
+
if (!grantType)
|
|
227
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "grant_type is required");
|
|
228
|
+
if (grantType === "authorization_code") {
|
|
229
|
+
const code = typeof body.code === "string" ? body.code : undefined;
|
|
230
|
+
const verifier = typeof body.code_verifier === "string" ? body.code_verifier : undefined;
|
|
231
|
+
const redirectUri = typeof body.redirect_uri === "string" ? body.redirect_uri : undefined;
|
|
232
|
+
const resource = typeof body.resource === "string" ? body.resource : undefined;
|
|
233
|
+
if (!code || !verifier) {
|
|
234
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "code and code_verifier are required");
|
|
235
|
+
}
|
|
236
|
+
if (resource !== undefined && !URL.canParse(resource)) {
|
|
237
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "resource must be a valid URL");
|
|
238
|
+
}
|
|
239
|
+
if (!provider.skipLocalPkceValidation) {
|
|
240
|
+
const challenge = await provider.challengeForAuthorizationCode(client, code);
|
|
241
|
+
if (!verifyPkce(verifier, challenge)) {
|
|
242
|
+
throw oauthError(OAuthErrorCode.InvalidGrant, "code_verifier does not match the challenge");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const tokens = await provider.exchangeAuthorizationCode(client, code, provider.skipLocalPkceValidation ? verifier : undefined, redirectUri, resource ? new URL(resource) : undefined);
|
|
246
|
+
res.status(200).json(tokens);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (grantType === "refresh_token") {
|
|
250
|
+
const refreshToken = typeof body.refresh_token === "string" ? body.refresh_token : undefined;
|
|
251
|
+
const scope = typeof body.scope === "string" ? body.scope : undefined;
|
|
252
|
+
const resource = typeof body.resource === "string" ? body.resource : undefined;
|
|
253
|
+
if (!refreshToken)
|
|
254
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "refresh_token is required");
|
|
255
|
+
if (resource !== undefined && !URL.canParse(resource)) {
|
|
256
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, "resource must be a valid URL");
|
|
257
|
+
}
|
|
258
|
+
const tokens = await provider.exchangeRefreshToken(client, refreshToken, scope?.split(" ").filter(Boolean), resource ? new URL(resource) : undefined);
|
|
259
|
+
res.status(200).json(tokens);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
throw oauthError(OAuthErrorCode.UnsupportedGrantType, "The grant type is not supported by this authorization server.");
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
sendOAuthError(res, error);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
return router;
|
|
269
|
+
}
|
|
270
|
+
export function clientRegistrationHandler(options) {
|
|
271
|
+
const { clientsStore } = options;
|
|
272
|
+
if (!clientsStore.registerClient) {
|
|
273
|
+
throw new Error("Client registration store does not support registering clients");
|
|
274
|
+
}
|
|
275
|
+
const router = express.Router();
|
|
276
|
+
router.use(allowMethods(["POST"]));
|
|
277
|
+
router.use(express.json({ limit: "16kb" }));
|
|
278
|
+
router.use(endpointRateLimit(20, "You have exceeded the rate limit for client registration requests"));
|
|
279
|
+
router.post("/", async (req, res) => {
|
|
280
|
+
res.setHeader("Cache-Control", "no-store");
|
|
281
|
+
try {
|
|
282
|
+
const parsed = OAuthClientMetadataSchema.safeParse(req.body);
|
|
283
|
+
if (!parsed.success) {
|
|
284
|
+
throw oauthError(OAuthErrorCode.InvalidClientMetadata, parsed.error.message);
|
|
285
|
+
}
|
|
286
|
+
const metadata = parsed.data;
|
|
287
|
+
const publicClient = metadata.token_endpoint_auth_method === "none";
|
|
288
|
+
const issuedAt = Math.floor(Date.now() / 1000);
|
|
289
|
+
const registered = await clientsStore.registerClient({
|
|
290
|
+
...metadata,
|
|
291
|
+
...(publicClient
|
|
292
|
+
? {}
|
|
293
|
+
: {
|
|
294
|
+
client_secret: randomBytes(32).toString("hex"),
|
|
295
|
+
client_secret_expires_at: issuedAt + (720 * 60 * 60),
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
res.status(201).json(registered);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
sendOAuthError(res, error);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return router;
|
|
305
|
+
}
|
|
306
|
+
export function revocationHandler(options) {
|
|
307
|
+
const { provider } = options;
|
|
308
|
+
if (!provider.revokeToken)
|
|
309
|
+
throw new Error("Auth provider does not support revoking tokens");
|
|
310
|
+
const router = express.Router();
|
|
311
|
+
router.use(allowMethods(["POST"]));
|
|
312
|
+
router.use(express.urlencoded({ extended: false }));
|
|
313
|
+
router.use(endpointRateLimit(50, "You have exceeded the rate limit for token revocation requests"));
|
|
314
|
+
router.post("/", async (req, res) => {
|
|
315
|
+
res.setHeader("Cache-Control", "no-store");
|
|
316
|
+
try {
|
|
317
|
+
const client = await authenticateClient(provider, req.body);
|
|
318
|
+
const parsed = OAuthTokenRevocationRequestSchema.safeParse(req.body);
|
|
319
|
+
if (!parsed.success)
|
|
320
|
+
throw oauthError(OAuthErrorCode.InvalidRequest, parsed.error.message);
|
|
321
|
+
await provider.revokeToken(client, parsed.data);
|
|
322
|
+
res.status(200).json({});
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
sendOAuthError(res, error);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
return router;
|
|
329
|
+
}
|
|
330
|
+
export function metadataHandler(metadata) {
|
|
331
|
+
const router = express.Router();
|
|
332
|
+
router.use(allowMethods(["GET"]));
|
|
333
|
+
router.get("/", (_req, res) => {
|
|
334
|
+
res.status(200).json(metadata);
|
|
335
|
+
});
|
|
336
|
+
return router;
|
|
337
|
+
}
|
|
338
|
+
function insecureIssuerAllowed() {
|
|
339
|
+
return process.env.MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL === "true"
|
|
340
|
+
|| process.env.MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL === "1";
|
|
341
|
+
}
|
|
342
|
+
function validateIssuerUrl(issuer) {
|
|
343
|
+
if (issuer.protocol !== "https:"
|
|
344
|
+
&& issuer.hostname !== "localhost"
|
|
345
|
+
&& issuer.hostname !== "127.0.0.1"
|
|
346
|
+
&& !insecureIssuerAllowed()) {
|
|
347
|
+
throw new Error("Issuer URL must be HTTPS");
|
|
348
|
+
}
|
|
349
|
+
if (issuer.hash)
|
|
350
|
+
throw new Error(`Issuer URL must not have a fragment: ${issuer}`);
|
|
351
|
+
if (issuer.search)
|
|
352
|
+
throw new Error(`Issuer URL must not have a query string: ${issuer}`);
|
|
353
|
+
}
|
|
354
|
+
export function createOAuthMetadata(options) {
|
|
355
|
+
const { provider, issuerUrl, baseUrl = issuerUrl, scopesSupported } = options;
|
|
356
|
+
validateIssuerUrl(issuerUrl);
|
|
357
|
+
return {
|
|
358
|
+
issuer: issuerUrl.href,
|
|
359
|
+
authorization_endpoint: new URL("authorize", baseUrl.href.endsWith("/") ? baseUrl : new URL(`${baseUrl.href}/`)).href,
|
|
360
|
+
response_types_supported: ["code"],
|
|
361
|
+
code_challenge_methods_supported: ["S256"],
|
|
362
|
+
token_endpoint: new URL("token", baseUrl.href.endsWith("/") ? baseUrl : new URL(`${baseUrl.href}/`)).href,
|
|
363
|
+
token_endpoint_auth_methods_supported: ["client_secret_post", "none"],
|
|
364
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
365
|
+
scopes_supported: scopesSupported,
|
|
366
|
+
revocation_endpoint: provider.revokeToken
|
|
367
|
+
? new URL("revoke", baseUrl.href.endsWith("/") ? baseUrl : new URL(`${baseUrl.href}/`)).href
|
|
368
|
+
: undefined,
|
|
369
|
+
revocation_endpoint_auth_methods_supported: provider.revokeToken ? ["client_secret_post"] : undefined,
|
|
370
|
+
registration_endpoint: provider.clientsStore.registerClient
|
|
371
|
+
? new URL("register", baseUrl.href.endsWith("/") ? baseUrl : new URL(`${baseUrl.href}/`)).href
|
|
372
|
+
: undefined,
|
|
373
|
+
authorization_response_iss_parameter_supported: provider.authorizationResponseIssParameterSupported ?? true,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { timingSafeEqual, randomBytes, randomUUID, createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
|
|
2
|
+
import { OAuthError, OAuthErrorCode, checkResourceAllowed, resourceUrlFromServerUrl, } from "@modelcontextprotocol/server";
|
|
4
3
|
import { SqliteOAuthClientsStore, SqliteOAuthStore } from "./oauth-store.js";
|
|
5
4
|
const CODE_TTL_MS = 5 * 60 * 1000;
|
|
6
5
|
const CLI_CLIENT_ID = "forgerelay-cli";
|
|
@@ -99,10 +98,10 @@ export class SingleUserOAuthProvider {
|
|
|
99
98
|
}
|
|
100
99
|
async authorize(client, params, res) {
|
|
101
100
|
if (!params.resource || !checkResourceAllowed({ requestedResource: params.resource, configuredResource: this.resourceServerUrl })) {
|
|
102
|
-
throw new
|
|
101
|
+
throw new OAuthError(OAuthErrorCode.InvalidRequest, "Invalid or missing OAuth resource");
|
|
103
102
|
}
|
|
104
103
|
if (!requestedScopesAllowed(params.scopes ?? [], this.config.scopes)) {
|
|
105
|
-
throw new
|
|
104
|
+
throw new OAuthError(OAuthErrorCode.InvalidRequest, "Requested scope is not supported");
|
|
106
105
|
}
|
|
107
106
|
if (res.req.method !== "POST") {
|
|
108
107
|
res.status(200).setHeader("Content-Type", "text/html; charset=utf-8");
|
|
@@ -146,10 +145,10 @@ export class SingleUserOAuthProvider {
|
|
|
146
145
|
async exchangeAuthorizationCode(client, authorizationCode, _codeVerifier, redirectUri, resource) {
|
|
147
146
|
const record = this.validCodeRecord(client, authorizationCode);
|
|
148
147
|
if (redirectUri && redirectUri !== record.params.redirectUri) {
|
|
149
|
-
throw new
|
|
148
|
+
throw new OAuthError(OAuthErrorCode.InvalidGrant, "redirect_uri does not match the authorization request");
|
|
150
149
|
}
|
|
151
150
|
if (resource && !checkResourceAllowed({ requestedResource: resource, configuredResource: this.resourceServerUrl })) {
|
|
152
|
-
throw new
|
|
151
|
+
throw new OAuthError(OAuthErrorCode.InvalidGrant, "Invalid resource");
|
|
153
152
|
}
|
|
154
153
|
this.codes.delete(authorizationCode);
|
|
155
154
|
return this.issueTokens(client.client_id, record.params.scopes ?? this.config.scopes, record.params.resource);
|
|
@@ -157,14 +156,14 @@ export class SingleUserOAuthProvider {
|
|
|
157
156
|
async exchangeRefreshToken(client, refreshToken, scopes, resource) {
|
|
158
157
|
const refresh = this.validRefreshToken(refreshToken, client.client_id);
|
|
159
158
|
if (!refresh)
|
|
160
|
-
throw new
|
|
159
|
+
throw new OAuthError(OAuthErrorCode.InvalidGrant, "Invalid refresh token");
|
|
161
160
|
const { refreshTokenHash, record } = refresh;
|
|
162
161
|
if (resource && !checkResourceAllowed({ requestedResource: resource, configuredResource: this.resourceServerUrl })) {
|
|
163
|
-
throw new
|
|
162
|
+
throw new OAuthError(OAuthErrorCode.InvalidGrant, "Invalid resource");
|
|
164
163
|
}
|
|
165
164
|
const requestedScopes = scopes ?? record.scopes;
|
|
166
165
|
if (!requestedScopes.every((scope) => record.scopes.includes(scope))) {
|
|
167
|
-
throw new
|
|
166
|
+
throw new OAuthError(OAuthErrorCode.AccessDenied, "Refresh token cannot grant requested scopes");
|
|
168
167
|
}
|
|
169
168
|
return this.issueTokens(client.client_id, requestedScopes, resource ?? (record.resource ? new URL(record.resource) : undefined), refreshTokenHash);
|
|
170
169
|
}
|
|
@@ -182,7 +181,7 @@ export class SingleUserOAuthProvider {
|
|
|
182
181
|
async verifyAccessToken(token) {
|
|
183
182
|
const record = this.oauthStore.getAccessToken(hashToken(token));
|
|
184
183
|
if (!record || record.expiresAt < Math.floor(Date.now() / 1000)) {
|
|
185
|
-
throw new
|
|
184
|
+
throw new OAuthError(OAuthErrorCode.InvalidToken, "Invalid or expired access token");
|
|
186
185
|
}
|
|
187
186
|
return {
|
|
188
187
|
token,
|
|
@@ -219,7 +218,7 @@ export class SingleUserOAuthProvider {
|
|
|
219
218
|
this.pruneExpiredAuthorizationCodes();
|
|
220
219
|
const record = this.codes.get(authorizationCode);
|
|
221
220
|
if (!record || record.clientId !== client.client_id || record.expiresAtMs < Date.now()) {
|
|
222
|
-
throw new
|
|
221
|
+
throw new OAuthError(OAuthErrorCode.InvalidGrant, "Invalid authorization code");
|
|
223
222
|
}
|
|
224
223
|
return record;
|
|
225
224
|
}
|
|
@@ -246,7 +245,7 @@ export class SingleUserOAuthProvider {
|
|
|
246
245
|
},
|
|
247
246
|
}, consumedRefreshTokenHash);
|
|
248
247
|
if (!saved) {
|
|
249
|
-
throw new
|
|
248
|
+
throw new OAuthError(OAuthErrorCode.InvalidGrant, "Invalid refresh token");
|
|
250
249
|
}
|
|
251
250
|
return {
|
|
252
251
|
access_token: accessToken,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { OAuthError, OAuthErrorCode } from "@modelcontextprotocol/server";
|
|
3
3
|
import { openDatabase } from "../../runtime/state/db/client.js";
|
|
4
4
|
function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
5
5
|
let parsed;
|
|
@@ -37,7 +37,7 @@ export class SqliteOAuthStore {
|
|
|
37
37
|
}
|
|
38
38
|
registerClient(client, allowedRedirectHosts) {
|
|
39
39
|
if (!client.redirect_uris.every((uri) => redirectHostAllowed(String(uri), allowedRedirectHosts))) {
|
|
40
|
-
throw new
|
|
40
|
+
throw new OAuthError(OAuthErrorCode.InvalidRequest, "Client redirect_uri is not allowed for this ForgeRelay server");
|
|
41
41
|
}
|
|
42
42
|
const now = Math.floor(Date.now() / 1000);
|
|
43
43
|
const registered = {
|
package/dist/mcp/oauth/router.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
-
import {
|
|
3
|
-
import { authorizationHandler } from "
|
|
4
|
-
import { tokenHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/token.js";
|
|
5
|
-
import { clientRegistrationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/register.js";
|
|
6
|
-
import { revocationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/revoke.js";
|
|
7
|
-
import { metadataHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/metadata.js";
|
|
2
|
+
import { getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/express";
|
|
3
|
+
import { authorizationHandler, clientRegistrationHandler, createOAuthMetadata, metadataHandler, revocationHandler, tokenHandler, } from "./auth-protocol.js";
|
|
8
4
|
import { oauthAuthorizationServerMetadataPath, publicEndpointPaths, publicEndpointUrl, } from "./public-url.js";
|
|
9
5
|
export function createForgeRelayAuthRouter(options) {
|
|
10
6
|
const { provider, cliAuthenticationProvider, instanceId, issuerUrl, resourceServerUrl, routeBaseUrls = [issuerUrl], scopesSupported, resourceName, } = options;
|
|
@@ -16,7 +16,7 @@ export class BatchExecutor {
|
|
|
16
16
|
await this.dependencies.lifecycle.run({
|
|
17
17
|
tool: "batch",
|
|
18
18
|
workspace: workspaceSnapshot(workspace),
|
|
19
|
-
conversationScopeId: openAiConversationScopeId(context.requestMeta),
|
|
19
|
+
conversationScopeId: context.conversationScopeId ?? openAiConversationScopeId(context.requestMeta),
|
|
20
20
|
request: {
|
|
21
21
|
workspaceId,
|
|
22
22
|
concurrency: input.concurrency ?? Math.min(input.tasks.length, 10),
|