mcp-compress-router 3.0.0 → 3.0.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.
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML pages served by the temporary loopback OAuth callback server.
|
|
3
|
+
*
|
|
4
|
+
* The pages are self-contained — inline CSS and inline SVG, no external
|
|
5
|
+
* requests — and carry the `mcp-compress-router` name, so the browser
|
|
6
|
+
* window that opens during `login` is identifiable as this tool. Every
|
|
7
|
+
* interpolated value is HTML-escaped, so OAuth error text reflected
|
|
8
|
+
* from the callback query cannot inject markup.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Styles shared by every callback page. A single constant keeps the
|
|
12
|
+
* pages self-contained (no external stylesheet) and defines the layout
|
|
13
|
+
* once for both variants; the error variant only swaps the accent
|
|
14
|
+
* colors.
|
|
15
|
+
*/
|
|
16
|
+
const PAGE_STYLES = ` :root {
|
|
17
|
+
color-scheme: light dark;
|
|
18
|
+
--bg: #f4f5f7;
|
|
19
|
+
--card-bg: #ffffff;
|
|
20
|
+
--card-border: #e3e5e8;
|
|
21
|
+
--text: #1b1f24;
|
|
22
|
+
--muted: #656d76;
|
|
23
|
+
--code-bg: #f0f1f3;
|
|
24
|
+
--accent: #1a7f37;
|
|
25
|
+
--accent-soft: #dafbe1;
|
|
26
|
+
}
|
|
27
|
+
@media (prefers-color-scheme: dark) {
|
|
28
|
+
:root {
|
|
29
|
+
--bg: #0d1117;
|
|
30
|
+
--card-bg: #161b22;
|
|
31
|
+
--card-border: #30363d;
|
|
32
|
+
--text: #e6edf3;
|
|
33
|
+
--muted: #8b949e;
|
|
34
|
+
--code-bg: #21262d;
|
|
35
|
+
--accent: #3fb950;
|
|
36
|
+
--accent-soft: #12261e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
* {
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
}
|
|
42
|
+
body {
|
|
43
|
+
margin: 0;
|
|
44
|
+
min-height: 100vh;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
padding: 24px;
|
|
49
|
+
background: var(--bg);
|
|
50
|
+
color: var(--text);
|
|
51
|
+
font-family:
|
|
52
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
|
53
|
+
sans-serif;
|
|
54
|
+
-webkit-font-smoothing: antialiased;
|
|
55
|
+
}
|
|
56
|
+
.card {
|
|
57
|
+
width: 100%;
|
|
58
|
+
max-width: 420px;
|
|
59
|
+
padding: 40px 32px 28px;
|
|
60
|
+
background: var(--card-bg);
|
|
61
|
+
border: 1px solid var(--card-border);
|
|
62
|
+
border-radius: 14px;
|
|
63
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 8%);
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
.card.error {
|
|
67
|
+
--accent: #cf222e;
|
|
68
|
+
--accent-soft: #ffebe9;
|
|
69
|
+
}
|
|
70
|
+
@media (prefers-color-scheme: dark) {
|
|
71
|
+
.card.error {
|
|
72
|
+
--accent: #f85149;
|
|
73
|
+
--accent-soft: #2d1416;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
.icon {
|
|
77
|
+
display: block;
|
|
78
|
+
width: 56px;
|
|
79
|
+
height: 56px;
|
|
80
|
+
margin: 0 auto 20px;
|
|
81
|
+
}
|
|
82
|
+
h1 {
|
|
83
|
+
margin: 0 0 12px;
|
|
84
|
+
font-size: 20px;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
letter-spacing: -0.01em;
|
|
87
|
+
}
|
|
88
|
+
p {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-size: 14px;
|
|
91
|
+
line-height: 1.5;
|
|
92
|
+
color: var(--muted);
|
|
93
|
+
}
|
|
94
|
+
p + p {
|
|
95
|
+
margin-top: 12px;
|
|
96
|
+
}
|
|
97
|
+
.detail code {
|
|
98
|
+
display: inline-block;
|
|
99
|
+
padding: 3px 8px;
|
|
100
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
101
|
+
font-size: 13px;
|
|
102
|
+
color: var(--text);
|
|
103
|
+
background: var(--code-bg);
|
|
104
|
+
border-radius: 6px;
|
|
105
|
+
}
|
|
106
|
+
footer {
|
|
107
|
+
margin-top: 28px;
|
|
108
|
+
padding-top: 16px;
|
|
109
|
+
border-top: 1px solid var(--card-border);
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
color: var(--muted);
|
|
112
|
+
}`;
|
|
113
|
+
/**
|
|
114
|
+
* Renders one self-contained callback page.
|
|
115
|
+
*
|
|
116
|
+
* @param options - Page content and variant.
|
|
117
|
+
* @returns A complete HTML document.
|
|
118
|
+
*/
|
|
119
|
+
export function renderCallbackPage(options) {
|
|
120
|
+
const title = `${escapeHtml(options.heading)} — mcp-compress-router`;
|
|
121
|
+
const detail = options.detail
|
|
122
|
+
? `\n <p class="detail"><code>${escapeHtml(options.detail)}</code></p>`
|
|
123
|
+
: '';
|
|
124
|
+
return `<!doctype html>
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="utf-8" />
|
|
128
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
129
|
+
<title>${title}</title>
|
|
130
|
+
<style>
|
|
131
|
+
${PAGE_STYLES}
|
|
132
|
+
</style>
|
|
133
|
+
</head>
|
|
134
|
+
<body>
|
|
135
|
+
<main class="card ${options.kind}">
|
|
136
|
+
${statusIcon(options.kind)}
|
|
137
|
+
<h1>${escapeHtml(options.heading)}</h1>
|
|
138
|
+
<p>${escapeHtml(options.message)}</p>${detail}
|
|
139
|
+
<footer>mcp-compress-router</footer>
|
|
140
|
+
</main>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Builds the inline SVG status icon for a page variant.
|
|
147
|
+
*
|
|
148
|
+
* @param kind - Page variant.
|
|
149
|
+
* @returns The SVG markup (static, contains no user input).
|
|
150
|
+
*/
|
|
151
|
+
function statusIcon(kind) {
|
|
152
|
+
const path = kind === 'success'
|
|
153
|
+
? '<path d="M19 29l7 7 12-14" fill="none" stroke="var(--accent)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />'
|
|
154
|
+
: '<path d="M21 21l14 14M35 21L21 35" fill="none" stroke="var(--accent)" stroke-width="4" stroke-linecap="round" />';
|
|
155
|
+
return `<svg class="icon" viewBox="0 0 56 56" aria-hidden="true" focusable="false">
|
|
156
|
+
<circle cx="28" cy="28" r="28" fill="var(--accent-soft)" />
|
|
157
|
+
${path}
|
|
158
|
+
</svg>`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Escapes the HTML special characters in a reflected value, so an OAuth
|
|
162
|
+
* error string cannot inject markup into a callback page.
|
|
163
|
+
*
|
|
164
|
+
* @param value - The raw value to escape.
|
|
165
|
+
* @returns The escaped value.
|
|
166
|
+
*/
|
|
167
|
+
function escapeHtml(value) {
|
|
168
|
+
return value
|
|
169
|
+
.replaceAll('&', '&')
|
|
170
|
+
.replaceAll('<', '<')
|
|
171
|
+
.replaceAll('>', '>')
|
|
172
|
+
.replaceAll('"', '"')
|
|
173
|
+
.replaceAll("'", ''');
|
|
174
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import * as http from 'node:http';
|
|
3
|
+
import { renderCallbackPage } from './login-callback-page.js';
|
|
3
4
|
/**
|
|
4
5
|
* Starts a temporary loopback HTTP server on `callbackPort`, runs the
|
|
5
6
|
* authorization-code flow through `beginAuthorization`, and waits for the
|
|
@@ -90,76 +91,136 @@ function validateCallback(flow, url) {
|
|
|
90
91
|
return undefined;
|
|
91
92
|
}
|
|
92
93
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
94
|
+
* Response headers for every callback page: self-contained HTML plus a
|
|
95
|
+
* strict CSP. The pages load no scripts and no external resources; the
|
|
96
|
+
* policy is defense in depth behind the renderer's HTML escaping.
|
|
97
|
+
*/
|
|
98
|
+
const PAGE_HEADERS = {
|
|
99
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
100
|
+
'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'",
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Writes one callback page response.
|
|
95
104
|
*
|
|
96
|
-
* @param
|
|
97
|
-
* @
|
|
105
|
+
* @param res - The server response.
|
|
106
|
+
* @param status - HTTP status code.
|
|
107
|
+
* @param html - The complete page document.
|
|
98
108
|
*/
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.replaceAll('<', '<')
|
|
103
|
-
.replaceAll('>', '>')
|
|
104
|
-
.replaceAll('"', '"')
|
|
105
|
-
.replaceAll("'", ''');
|
|
109
|
+
function sendPage(res, status, html) {
|
|
110
|
+
res.writeHead(status, PAGE_HEADERS);
|
|
111
|
+
res.end(html);
|
|
106
112
|
}
|
|
107
113
|
/**
|
|
108
|
-
*
|
|
114
|
+
* Responds with the failure page for a callback that failed validation
|
|
115
|
+
* (a stray or forged request that must not settle the flow).
|
|
116
|
+
*
|
|
117
|
+
* @param res - The server response.
|
|
118
|
+
*/
|
|
119
|
+
function respondInvalidCallback(res) {
|
|
120
|
+
sendPage(res, 400, renderCallbackPage({
|
|
121
|
+
kind: 'error',
|
|
122
|
+
heading: 'Authorization failed',
|
|
123
|
+
message: 'Invalid OAuth callback.',
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Responds with the failure page for an authorization error, showing the
|
|
128
|
+
* OAuth error code and, when provided, the server's description.
|
|
109
129
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
* @param res - The server response.
|
|
131
|
+
* @param error - The OAuth error code from the callback.
|
|
132
|
+
* @param description - The OAuth `error_description`, or null.
|
|
133
|
+
*/
|
|
134
|
+
function respondAuthorizationError(res, error, description) {
|
|
135
|
+
sendPage(res, 400, renderCallbackPage({
|
|
136
|
+
kind: 'error',
|
|
137
|
+
heading: 'Authorization failed',
|
|
138
|
+
message: description ?? 'The authorization server rejected the request.',
|
|
139
|
+
detail: error,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Responds with the success page after a valid callback.
|
|
144
|
+
*
|
|
145
|
+
* @param res - The server response.
|
|
146
|
+
*/
|
|
147
|
+
function respondSuccess(res) {
|
|
148
|
+
sendPage(res, 200, renderCallbackPage({
|
|
149
|
+
kind: 'success',
|
|
150
|
+
heading: 'Authorization successful',
|
|
151
|
+
message: 'You can close this window and return to your terminal.',
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Settles the flow with a failure: clears the callback timeout, closes
|
|
156
|
+
* the temporary server, and rejects the pending promise.
|
|
157
|
+
*
|
|
158
|
+
* @param flow - Callback flow state for this login attempt.
|
|
159
|
+
* @param tempServer - The temporary callback server (closed on failure).
|
|
160
|
+
* @param reject - Promise reject function.
|
|
161
|
+
* @param message - The error message for the rejection.
|
|
162
|
+
*/
|
|
163
|
+
function failFlow(flow, tempServer, reject, message) {
|
|
164
|
+
if (flow.timeoutHandle.current)
|
|
165
|
+
clearTimeout(flow.timeoutHandle.current);
|
|
166
|
+
tempServer.close();
|
|
167
|
+
reject(new Error(message));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Handles one callback request: validates the CSRF `state` echoed by the
|
|
171
|
+
* authorization server and, when present, the RFC 9207 `iss` issuer
|
|
172
|
+
* parameter before acting on the callback. A callback that fails
|
|
173
|
+
* validation is ignored: it gets a 400 response but never settles the
|
|
174
|
+
* flow, so a stray loopback request cannot abort a login whose
|
|
175
|
+
* legitimate callback is still on its way.
|
|
117
176
|
*
|
|
118
177
|
* @param flow - Callback flow state for this login attempt.
|
|
119
178
|
* @param tempServer - The temporary callback server (closed on completion).
|
|
120
179
|
* @param resolve - Promise resolve function (authorization code).
|
|
121
180
|
* @param reject - Promise reject function.
|
|
122
|
-
* @
|
|
181
|
+
* @param req - The incoming request.
|
|
182
|
+
* @param res - The server response.
|
|
123
183
|
*/
|
|
124
|
-
function
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const code = url.searchParams.get('code');
|
|
135
|
-
// Only authorization responses (a code or an error) carry a state to
|
|
136
|
-
// validate; anything else is not a callback.
|
|
137
|
-
if (error !== null || code !== null) {
|
|
138
|
-
const validationError = validateCallback(flow, url);
|
|
139
|
-
if (validationError) {
|
|
140
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
141
|
-
res.end('<html><body><h1>Authorization failed</h1><p>Invalid OAuth callback.</p></body></html>');
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (error !== null) {
|
|
146
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
147
|
-
res.end(`<html><body><h1>Authorization failed</h1><p>${escapeHtml(error)}</p></body></html>`);
|
|
148
|
-
fail(`Authorization failed: ${error}`);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (code === null) {
|
|
152
|
-
res.writeHead(404);
|
|
153
|
-
res.end('Not found');
|
|
184
|
+
function handleCallbackRequest(flow, tempServer, resolve, reject, req, res) {
|
|
185
|
+
const url = new URL(req.url, `http://127.0.0.1`);
|
|
186
|
+
const error = url.searchParams.get('error');
|
|
187
|
+
const code = url.searchParams.get('code');
|
|
188
|
+
// Only authorization responses (a code or an error) carry a state to
|
|
189
|
+
// validate; anything else is not a callback.
|
|
190
|
+
if (error !== null || code !== null) {
|
|
191
|
+
const validationError = validateCallback(flow, url);
|
|
192
|
+
if (validationError) {
|
|
193
|
+
respondInvalidCallback(res);
|
|
154
194
|
return;
|
|
155
195
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
res
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
}
|
|
197
|
+
if (error !== null) {
|
|
198
|
+
respondAuthorizationError(res, error, url.searchParams.get('error_description'));
|
|
199
|
+
failFlow(flow, tempServer, reject, `Authorization failed: ${error}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (code === null) {
|
|
203
|
+
res.writeHead(404);
|
|
204
|
+
res.end('Not found');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (flow.timeoutHandle.current)
|
|
208
|
+
clearTimeout(flow.timeoutHandle.current);
|
|
209
|
+
respondSuccess(res);
|
|
210
|
+
tempServer.close();
|
|
211
|
+
resolve(code);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Creates the HTTP request listener for the temporary callback server.
|
|
215
|
+
*
|
|
216
|
+
* @param flow - Callback flow state for this login attempt.
|
|
217
|
+
* @param tempServer - The temporary callback server (closed on completion).
|
|
218
|
+
* @param resolve - Promise resolve function (authorization code).
|
|
219
|
+
* @param reject - Promise reject function.
|
|
220
|
+
* @returns An HTTP request listener.
|
|
221
|
+
*/
|
|
222
|
+
function makeCallbackHandler(flow, tempServer, resolve, reject) {
|
|
223
|
+
return (req, res) => handleCallbackRequest(flow, tempServer, resolve, reject, req, res);
|
|
163
224
|
}
|
|
164
225
|
/**
|
|
165
226
|
* Listen callback: records the bound port, prepares the authorization
|
|
@@ -59,9 +59,11 @@ async function _getOpenBrowser() {
|
|
|
59
59
|
*
|
|
60
60
|
* @param targetServer - Typed downstream server configuration.
|
|
61
61
|
* @param name - Server name (for error messages).
|
|
62
|
-
* @returns The discovered OAuth metadata
|
|
62
|
+
* @returns The discovered OAuth metadata, whether the authorization
|
|
63
63
|
* server commits to the RFC 9207 `iss` parameter (read from the raw
|
|
64
|
-
* metadata document, so it also covers OpenID Connect discovery)
|
|
64
|
+
* metadata document, so it also covers OpenID Connect discovery),
|
|
65
|
+
* and the RFC 9728 Protected Resource Metadata when the server
|
|
66
|
+
* publishes it (used to derive the RFC 8707 `resource` indicator).
|
|
65
67
|
* @throws If the server does not expose OAuth metadata.
|
|
66
68
|
*/
|
|
67
69
|
async function discoverOAuthMetadata(targetServer, name) {
|
|
@@ -74,6 +76,7 @@ async function discoverOAuthMetadata(targetServer, name) {
|
|
|
74
76
|
return {
|
|
75
77
|
metadata: metadata,
|
|
76
78
|
requireIssuer: discovered.authorizationResponseIssParameterSupported,
|
|
79
|
+
resourceMetadata: discovered.resourceMetadata,
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
@@ -147,9 +150,12 @@ async function registerClientIfNeeded(mgr, metadata, name) {
|
|
|
147
150
|
* @param targetServer - Typed downstream server configuration.
|
|
148
151
|
* @param name - Server name (for error messages).
|
|
149
152
|
* @param state - CSRF state generated for this login attempt.
|
|
153
|
+
* @param resource - RFC 8707 resource indicator selected from the
|
|
154
|
+
* server's Protected Resource Metadata, or undefined when the server
|
|
155
|
+
* publishes none (the SDK then omits the `resource` parameter).
|
|
150
156
|
* @returns The `startAuthorization` result.
|
|
151
157
|
*/
|
|
152
|
-
async function beginAuthorization(mgr, metadata, targetServer, name, state) {
|
|
158
|
+
async function beginAuthorization(mgr, metadata, targetServer, name, state, resource) {
|
|
153
159
|
await registerClientIfNeeded(mgr, metadata, name);
|
|
154
160
|
const { startAuthorization } = await _getSdkAuth();
|
|
155
161
|
return startAuthorization(new URL(metadata.authorization_endpoint), {
|
|
@@ -158,6 +164,7 @@ async function beginAuthorization(mgr, metadata, targetServer, name, state) {
|
|
|
158
164
|
redirectUrl: mgr.redirectUrl,
|
|
159
165
|
scope: targetServer.oauth?.scope,
|
|
160
166
|
state,
|
|
167
|
+
resource,
|
|
161
168
|
});
|
|
162
169
|
}
|
|
163
170
|
/**
|
|
@@ -202,7 +209,9 @@ function _resolveCallbackPort(override, server) {
|
|
|
202
209
|
* temporary loopback callback server, registers the client (when
|
|
203
210
|
* needed) with the portless loopback callback URI, opens a browser,
|
|
204
211
|
* handles the redirect callback, exchanges the authorization code for
|
|
205
|
-
* tokens, and persists them in credentials.json.
|
|
212
|
+
* tokens, and persists them in credentials.json. The authorization and
|
|
213
|
+
* token requests carry the RFC 8707 `resource` indicator derived from
|
|
214
|
+
* the server's Protected Resource Metadata when it publishes one.
|
|
206
215
|
*
|
|
207
216
|
* @param configPath - Absolute path to the mcp.json file.
|
|
208
217
|
* @param name - Server name to authenticate.
|
|
@@ -219,16 +228,21 @@ export async function handleLogin(configPath, name, portOverride) {
|
|
|
219
228
|
const callbackPort = _resolveCallbackPort(_validatePortOverride(portOverride), targetServer);
|
|
220
229
|
const { OAuthCredentialManager } = await import('../services/oauth.js');
|
|
221
230
|
const mgr = new OAuthCredentialManager(configPath, targetServer);
|
|
222
|
-
const { metadata, requireIssuer } = await discoverOAuthMetadata(targetServer, name);
|
|
231
|
+
const { metadata, requireIssuer, resourceMetadata } = await discoverOAuthMetadata(targetServer, name);
|
|
232
|
+
const { selectResourceURL, exchangeAuthorization } = await _getSdkAuth();
|
|
233
|
+
// RFC 8707 resource indicator, selected from the server's Protected
|
|
234
|
+
// Resource Metadata the same way the SDK's auth() flow selects it.
|
|
235
|
+
// Undefined when the server publishes no such metadata: the SDK then
|
|
236
|
+
// omits the parameter, matching the runtime transport path.
|
|
237
|
+
const resource = await selectResourceURL(new URL(targetServer.url), mgr, resourceMetadata);
|
|
223
238
|
const { authorizationCode, authResult } = await acquireAuthorizationCode({
|
|
224
239
|
mgr,
|
|
225
240
|
callbackPort,
|
|
226
241
|
openBrowser: await _getOpenBrowser(),
|
|
227
242
|
expectedIssuer: metadata.issuer,
|
|
228
243
|
requireIssuer,
|
|
229
|
-
beginAuthorization: (state) => beginAuthorization(mgr, metadata, targetServer, name, state),
|
|
244
|
+
beginAuthorization: (state) => beginAuthorization(mgr, metadata, targetServer, name, state, resource),
|
|
230
245
|
});
|
|
231
|
-
const { exchangeAuthorization } = await _getSdkAuth();
|
|
232
246
|
const realRedirectUrl = mgr.redirectUrl;
|
|
233
247
|
const tokens = await exchangeAuthorization(new URL(metadata.token_endpoint), {
|
|
234
248
|
metadata,
|
|
@@ -236,6 +250,7 @@ export async function handleLogin(configPath, name, portOverride) {
|
|
|
236
250
|
authorizationCode,
|
|
237
251
|
codeVerifier: authResult.codeVerifier,
|
|
238
252
|
redirectUri: realRedirectUrl,
|
|
253
|
+
resource,
|
|
239
254
|
});
|
|
240
255
|
await mgr.saveTokens(tokens);
|
|
241
256
|
// Best-effort: discover tools and save to cache so the next router
|
package/build/services/oauth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { refreshAuthorization, } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
1
|
+
import { refreshAuthorization, selectResourceURL, } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
2
2
|
import { readCredentials, writeCredentials, removeCredentials } from '../cli/config-io.js';
|
|
3
3
|
import { discoverAuth } from './oauth-discovery.js';
|
|
4
4
|
import { GuidedAuthError } from './index.js';
|
|
@@ -337,16 +337,20 @@ export class OAuthCredentialManager {
|
|
|
337
337
|
* Performs the token refresh: discovers the authorization server
|
|
338
338
|
* metadata, calls the SDK's `refreshAuthorization`, and persists the
|
|
339
339
|
* refreshed tokens via {@link saveTokens} (which recomputes
|
|
340
|
-
* `expires_at`).
|
|
341
|
-
*
|
|
342
|
-
*
|
|
340
|
+
* `expires_at`). The request carries the RFC 8707 `resource`
|
|
341
|
+
* indicator selected from the server's Protected Resource Metadata —
|
|
342
|
+
* the same indicator `login` sent — so a provider that binds tokens
|
|
343
|
+
* to a specific resource accepts the refresh. Errors are logged
|
|
344
|
+
* (when a logger is supplied) and swallowed — proactive refresh is
|
|
345
|
+
* best-effort; the SDK's 401 path handles terminal failures.
|
|
343
346
|
*/
|
|
344
347
|
async _refreshTokens(refreshToken, logger) {
|
|
345
348
|
try {
|
|
346
349
|
if (!this._server.url) {
|
|
347
350
|
return;
|
|
348
351
|
}
|
|
349
|
-
const
|
|
352
|
+
const serverUrl = new URL(this._server.url);
|
|
353
|
+
const discovered = await discoverAuth(serverUrl);
|
|
350
354
|
if (!discovered.serverMetadata) {
|
|
351
355
|
return;
|
|
352
356
|
}
|
|
@@ -354,10 +358,12 @@ export class OAuthCredentialManager {
|
|
|
354
358
|
if (!clientInformation) {
|
|
355
359
|
return;
|
|
356
360
|
}
|
|
361
|
+
const resource = await selectResourceURL(serverUrl, this, discovered.resourceMetadata);
|
|
357
362
|
const newTokens = await refreshAuthorization(discovered.authorizationServerUrl, {
|
|
358
363
|
metadata: discovered.serverMetadata,
|
|
359
364
|
clientInformation,
|
|
360
365
|
refreshToken,
|
|
366
|
+
resource,
|
|
361
367
|
});
|
|
362
368
|
await this.saveTokens(newTokens);
|
|
363
369
|
}
|