@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -2
  3. package/dist/activity/runtime/mcp-query-tools.js +16 -10
  4. package/dist/cli/mcp/external-mcp.js +481 -0
  5. package/dist/cli/mcp/status.js +295 -0
  6. package/dist/cli.js +14 -1
  7. package/dist/lsp/test-support/server-fixture.js +1 -2
  8. package/dist/mcp/oauth/auth-protocol.js +375 -0
  9. package/dist/mcp/oauth/oauth-provider.js +11 -12
  10. package/dist/mcp/oauth/oauth-store.js +2 -2
  11. package/dist/mcp/oauth/router.js +2 -6
  12. package/dist/mcp/operations/batch/executor.js +1 -1
  13. package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
  14. package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +23 -6
  15. package/dist/mcp/operations/external-mcp/external-mcp.js +178 -23
  16. package/dist/mcp/operations/native-bulk-mutations.js +1 -1
  17. package/dist/mcp/process/tools.js +19 -15
  18. package/dist/mcp/request-context.js +18 -0
  19. package/dist/mcp/request-meta.js +28 -3
  20. package/dist/mcp/server/core/capabilities.js +1 -4
  21. package/dist/mcp/server/operations/runtime/filesystem-tools.js +35 -28
  22. package/dist/mcp/server/operations/runtime/operation-runtime.js +7 -7
  23. package/dist/mcp/server/transport/http-server.js +41 -20
  24. package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
  25. package/dist/mcp/server/workspace/runtime/workspace-tools.js +29 -25
  26. package/dist/runtime/config/external-mcp-auth-store.js +225 -0
  27. package/dist/runtime/config/external-mcp-config.js +76 -22
  28. package/dist/runtime/config/external-mcp-registry.js +165 -0
  29. package/dist/runtime/testing/server-fixture.js +4 -4
  30. package/dist/server.js +15 -9
  31. package/dist/ui/.vite/manifest.json +297 -298
  32. package/dist/ui/activity-panel-app.html +1 -1
  33. package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
  34. package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
  35. package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
  36. package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
  37. package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
  38. package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
  39. package/dist/ui/assets/c-BquuA6bv.js +1 -0
  40. package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
  41. package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
  42. package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
  43. package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
  44. package/dist/ui/assets/css-Be_T8idh.js +1 -0
  45. package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
  46. package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
  47. package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
  48. package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
  49. package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
  50. package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
  51. package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
  52. package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
  53. package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
  54. package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
  55. package/dist/ui/assets/haml-h18lG0pS.js +1 -0
  56. package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
  57. package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
  58. package/dist/ui/assets/html-DtILqTE1.js +1 -0
  59. package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
  60. package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
  61. package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
  62. package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
  63. package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
  64. package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
  65. package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
  66. package/dist/ui/assets/json-DW2UTv1q.js +1 -0
  67. package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
  68. package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
  69. package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
  70. package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
  71. package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
  72. package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
  73. package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
  74. package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
  75. package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
  76. package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
  77. package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
  78. package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
  79. package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
  80. package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
  81. package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
  82. package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
  83. package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
  84. package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
  85. package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
  86. package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
  87. package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
  88. package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
  89. package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
  90. package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
  91. package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
  92. package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
  93. package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
  94. package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
  95. package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
  96. package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
  97. package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
  98. package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
  99. package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
  100. package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
  101. package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
  102. package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
  103. package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
  104. package/dist/ui/assets/xml-_44rx9_2.js +1 -0
  105. package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
  106. package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
  107. package/dist/workspaces/relay/auth/remote-auth.js +3 -5
  108. package/dist/workspaces/relay/tests/test-support.js +7 -4
  109. package/dist/workspaces/relay/workspace-relay.js +14 -8
  110. package/docs/configuration.md +154 -4
  111. package/docs/debugging.md +8 -5
  112. package/docs/roadmap.md +64 -3
  113. package/docs/security.md +18 -0
  114. package/package.json +11 -4
  115. package/scripts/debug/accept/bootstrap.mjs +2 -0
  116. package/scripts/debug/accept/harness.mjs +9 -2
  117. package/scripts/debug/accept/modern-http.mjs +169 -0
  118. package/scripts/debug/accept/support.mjs +10 -6
  119. package/scripts/debug/accept.mjs +1 -0
  120. package/scripts/debug/runtime.mjs +4 -1
  121. package/scripts/debug/runtime.test.mjs +9 -0
  122. package/scripts/release/parity-sandbox.mjs +35 -0
  123. package/scripts/release/release-gate.test.mjs +42 -1
  124. package/scripts/release-parity.mjs +3 -22
  125. package/scripts/release-proof.mjs +24 -4
  126. package/scripts/release-proof.test.mjs +12 -4
  127. package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
  128. package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
  129. package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
  130. package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
  131. package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
  132. package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
  133. package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
  134. package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
  135. package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
  136. package/dist/ui/assets/html-dUOs5pA9.js +0 -1
  137. package/dist/ui/assets/java-BFZCrORY.js +0 -1
  138. package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
  139. package/dist/ui/assets/json-BQC_Yp48.js +0 -1
  140. package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
  141. package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
  142. package/dist/ui/assets/r-B5nYsy-M.js +0 -1
  143. package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
  144. package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
  145. package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
  146. package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
  147. package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
  148. package/dist/ui/assets/typescript-BGIABksB.js +0 -1
  149. package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
  150. 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 { AccessDeniedError, InvalidGrantError, InvalidRequestError, InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
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 InvalidRequestError("Invalid or missing OAuth resource");
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 InvalidRequestError("Requested scope is not supported");
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 InvalidGrantError("redirect_uri does not match the authorization request");
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 InvalidGrantError("Invalid resource");
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 InvalidGrantError("Invalid refresh token");
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 InvalidGrantError("Invalid resource");
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 AccessDeniedError("Refresh token cannot grant requested scopes");
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 InvalidTokenError("Invalid or expired access token");
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 InvalidGrantError("Invalid authorization code");
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 InvalidGrantError("Invalid refresh token");
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 { InvalidRequestError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
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 InvalidRequestError("Client redirect_uri is not allowed for this ForgeRelay server");
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 = {
@@ -1,10 +1,6 @@
1
1
  import express from "express";
2
- import { createOAuthMetadata, getOAuthProtectedResourceMetadataUrl, } from "@modelcontextprotocol/sdk/server/auth/router.js";
3
- import { authorizationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/authorize.js";
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),