@scorehub/mcp-server 0.2.7 → 0.2.9
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 +10 -5
- package/dist/auth.d.ts +3 -7
- package/dist/auth.js +396 -103
- package/dist/index.js +88 -106
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ In `mcp.json`:
|
|
|
82
82
|
2. It fetches the list of available tools
|
|
83
83
|
3. The first time you use a remote tool, you'll be prompted to run the `authorize` tool
|
|
84
84
|
4. Your browser opens to the ScoreHub login page
|
|
85
|
-
5. After authorizing, the browser redirects to `
|
|
85
|
+
5. After authorizing, the browser redirects to `127.0.0.1:<random-port>/callback`, shows a local status page, and the token is saved
|
|
86
86
|
6. Subsequent calls use the cached token — no repeated login needed
|
|
87
87
|
|
|
88
88
|
## Configuration
|
|
@@ -151,18 +151,21 @@ These tools are **dynamically loaded** from the remote MCP endpoint at startup a
|
|
|
151
151
|
├─ tools/call: authorize ──────────┤
|
|
152
152
|
│ (clears any existing token) │
|
|
153
153
|
│ │
|
|
154
|
-
├─
|
|
154
|
+
├─ opens local callback on 127.0.0.1:<random-port>
|
|
155
|
+
├─ generates random state + PKCE │
|
|
155
156
|
├─ opens browser → /authorize ────▶│
|
|
156
157
|
│ user logs in + grants access
|
|
157
|
-
│ ◀── redirect to
|
|
158
|
+
│ ◀── redirect to 127.0.0.1:<random-port>
|
|
158
159
|
│ with authorization_code │
|
|
159
160
|
│ │
|
|
160
|
-
├─ POST /token
|
|
161
|
+
├─ POST /token (code_verifier) ───▶│
|
|
161
162
|
│◀── access_token + refresh_token │
|
|
162
163
|
│ │
|
|
163
164
|
├─ saves to ~/.scorehub/<endpoint>/oauth.json
|
|
164
165
|
```
|
|
165
166
|
|
|
167
|
+
After the redirect, the local callback page gives a clear success or failure state and tells the user whether to return to the terminal or rerun `authorize`.
|
|
168
|
+
|
|
166
169
|
### Token lifecycle
|
|
167
170
|
|
|
168
171
|
| Stage | Behavior |
|
|
@@ -175,7 +178,9 @@ These tools are **dynamically loaded** from the remote MCP endpoint at startup a
|
|
|
175
178
|
|
|
176
179
|
### Headless environments
|
|
177
180
|
|
|
178
|
-
If you're running on a server or in a headless environment where a browser cannot open automatically, the authorization URL will be printed to stderr. Copy it and open it manually in any browser. After login, the callback goes to `http://127.0.0.1
|
|
181
|
+
If you're running on a server or in a headless environment where a browser cannot open automatically, the authorization URL will be printed to stderr. Copy it and open it manually in any browser. After login, the callback goes to `http://127.0.0.1:<random-port>/callback` — make sure the local loopback port chosen by mcp-server is reachable on the same machine.
|
|
182
|
+
|
|
183
|
+
On Windows, the same flow uses `cmd.exe /c start` to open the browser. If Windows blocks the automatic launch, use the printed authorization URL manually; the callback still lands on `127.0.0.1` and writes the token to your local profile.
|
|
179
184
|
|
|
180
185
|
### Local remote development
|
|
181
186
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -10,20 +10,17 @@ export interface TokenStore {
|
|
|
10
10
|
expires_at: number;
|
|
11
11
|
updated_at: string;
|
|
12
12
|
}
|
|
13
|
-
export interface AuthorizationCodeResult {
|
|
14
|
-
code: string;
|
|
15
|
-
state: string;
|
|
16
|
-
port: number;
|
|
17
|
-
}
|
|
18
13
|
export interface AuthorizeParams {
|
|
19
14
|
authEndpoint: string;
|
|
20
15
|
tokenEndpoint: string;
|
|
21
16
|
clientId: string;
|
|
22
|
-
redirectUri
|
|
17
|
+
redirectUri?: string;
|
|
23
18
|
port?: number;
|
|
24
19
|
scope?: string;
|
|
25
20
|
timeoutMs?: number;
|
|
26
21
|
host?: string;
|
|
22
|
+
callbackHost?: string;
|
|
23
|
+
callbackPath?: string;
|
|
27
24
|
}
|
|
28
25
|
export interface AuthorizeResult {
|
|
29
26
|
accessToken: string;
|
|
@@ -43,6 +40,5 @@ export declare function getValidAccessToken(host: string, tokenEndpoint: string,
|
|
|
43
40
|
export declare function generateCodeVerifier(length?: number): string;
|
|
44
41
|
export declare function generateCodeChallenge(verifier: string): string;
|
|
45
42
|
export declare function generateState(length?: number): string;
|
|
46
|
-
export declare function startCallbackServer(port: number, timeoutMs?: number): Promise<AuthorizationCodeResult>;
|
|
47
43
|
export declare function openBrowser(url: string): void;
|
|
48
44
|
export declare function authorize(params: AuthorizeParams): Promise<AuthorizeResult>;
|
package/dist/auth.js
CHANGED
|
@@ -9,7 +9,6 @@ exports.getValidAccessToken = getValidAccessToken;
|
|
|
9
9
|
exports.generateCodeVerifier = generateCodeVerifier;
|
|
10
10
|
exports.generateCodeChallenge = generateCodeChallenge;
|
|
11
11
|
exports.generateState = generateState;
|
|
12
|
-
exports.startCallbackServer = startCallbackServer;
|
|
13
12
|
exports.openBrowser = openBrowser;
|
|
14
13
|
exports.authorize = authorize;
|
|
15
14
|
const node_https_1 = require("node:https");
|
|
@@ -17,12 +16,23 @@ const node_http_1 = require("node:http");
|
|
|
17
16
|
const node_fs_1 = require("node:fs");
|
|
18
17
|
const node_path_1 = require("node:path");
|
|
19
18
|
const node_os_1 = require("node:os");
|
|
20
|
-
const node_http_2 = require("node:http");
|
|
21
19
|
const node_crypto_1 = require("node:crypto");
|
|
22
20
|
const node_child_process_1 = require("node:child_process");
|
|
21
|
+
class CallbackError extends Error {
|
|
22
|
+
page;
|
|
23
|
+
statusCode;
|
|
24
|
+
constructor(message, page, statusCode = 400) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "CallbackError";
|
|
27
|
+
this.page = page;
|
|
28
|
+
this.statusCode = statusCode;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
23
31
|
// ── Token Storage ────────────────────────────────────────────
|
|
24
32
|
const SCOREHUB_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".scorehub");
|
|
25
33
|
const TOKEN_FILE = (host) => (0, node_path_1.join)(SCOREHUB_DIR, host, "oauth.json");
|
|
34
|
+
const DEFAULT_CALLBACK_HOST = "127.0.0.1";
|
|
35
|
+
const DEFAULT_CALLBACK_PATH = "/callback";
|
|
26
36
|
function ensureDir(filePath) {
|
|
27
37
|
const dir = (0, node_path_1.dirname)(filePath);
|
|
28
38
|
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
@@ -54,6 +64,221 @@ function clearTokens(host) {
|
|
|
54
64
|
function hasSignedTokenShape(token) {
|
|
55
65
|
return token.split(".").length === 3;
|
|
56
66
|
}
|
|
67
|
+
function normalizeCallbackPath(path) {
|
|
68
|
+
if (!path || path === "/")
|
|
69
|
+
return DEFAULT_CALLBACK_PATH;
|
|
70
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
71
|
+
}
|
|
72
|
+
function buildLocalRedirectUri(host, port, path) {
|
|
73
|
+
const url = new URL(`http://${host}:${port}`);
|
|
74
|
+
url.pathname = normalizeCallbackPath(path);
|
|
75
|
+
return url.toString();
|
|
76
|
+
}
|
|
77
|
+
function escapeHtml(value) {
|
|
78
|
+
return value
|
|
79
|
+
.replaceAll("&", "&")
|
|
80
|
+
.replaceAll("<", "<")
|
|
81
|
+
.replaceAll(">", ">")
|
|
82
|
+
.replaceAll("\"", """)
|
|
83
|
+
.replaceAll("'", "'");
|
|
84
|
+
}
|
|
85
|
+
function buildCallbackPage(page) {
|
|
86
|
+
const palette = page.tone === "success"
|
|
87
|
+
? {
|
|
88
|
+
badgeBg: "#dcfce7",
|
|
89
|
+
badgeFg: "#166534",
|
|
90
|
+
accent: "#16a34a",
|
|
91
|
+
border: "rgba(22, 163, 74, 0.18)",
|
|
92
|
+
}
|
|
93
|
+
: {
|
|
94
|
+
badgeBg: "#fee2e2",
|
|
95
|
+
badgeFg: "#991b1b",
|
|
96
|
+
accent: "#dc2626",
|
|
97
|
+
border: "rgba(220, 38, 38, 0.16)",
|
|
98
|
+
};
|
|
99
|
+
const detail = page.detail
|
|
100
|
+
? `<p class="detail">${escapeHtml(page.detail)}</p>`
|
|
101
|
+
: "";
|
|
102
|
+
const steps = page.steps
|
|
103
|
+
.map((step) => `<li>${escapeHtml(step)}</li>`)
|
|
104
|
+
.join("");
|
|
105
|
+
return `<!DOCTYPE html>
|
|
106
|
+
<html lang="zh-CN">
|
|
107
|
+
<head>
|
|
108
|
+
<meta charset="utf-8">
|
|
109
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
110
|
+
<title>${escapeHtml(page.title)} | ScoreHub MCP</title>
|
|
111
|
+
<style>
|
|
112
|
+
:root {
|
|
113
|
+
color-scheme: light;
|
|
114
|
+
font-family: "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
115
|
+
background:
|
|
116
|
+
radial-gradient(circle at top left, rgba(168, 85, 247, 0.18), transparent 38%),
|
|
117
|
+
radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.14), transparent 36%),
|
|
118
|
+
linear-gradient(180deg, #f5f3ff 0%, #eef4ff 100%);
|
|
119
|
+
}
|
|
120
|
+
* {
|
|
121
|
+
box-sizing: border-box;
|
|
122
|
+
}
|
|
123
|
+
body {
|
|
124
|
+
margin: 0;
|
|
125
|
+
min-height: 100vh;
|
|
126
|
+
display: grid;
|
|
127
|
+
place-items: center;
|
|
128
|
+
padding: 24px;
|
|
129
|
+
color: #1f2937;
|
|
130
|
+
}
|
|
131
|
+
.shell {
|
|
132
|
+
width: min(100%, 640px);
|
|
133
|
+
border-radius: 28px;
|
|
134
|
+
padding: 32px;
|
|
135
|
+
background: rgba(255, 255, 255, 0.92);
|
|
136
|
+
border: 1px solid rgba(148, 163, 184, 0.22);
|
|
137
|
+
box-shadow: 0 28px 80px rgba(76, 29, 149, 0.16);
|
|
138
|
+
backdrop-filter: blur(18px);
|
|
139
|
+
}
|
|
140
|
+
.brand {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 10px;
|
|
144
|
+
font-size: 13px;
|
|
145
|
+
font-weight: 700;
|
|
146
|
+
letter-spacing: 0.08em;
|
|
147
|
+
text-transform: uppercase;
|
|
148
|
+
color: #6d28d9;
|
|
149
|
+
}
|
|
150
|
+
.brand-mark {
|
|
151
|
+
width: 12px;
|
|
152
|
+
height: 12px;
|
|
153
|
+
border-radius: 999px;
|
|
154
|
+
background: linear-gradient(135deg, #7c3aed, #2563eb);
|
|
155
|
+
box-shadow: 0 0 0 6px rgba(124, 58, 237, 0.12);
|
|
156
|
+
}
|
|
157
|
+
.panel {
|
|
158
|
+
margin-top: 28px;
|
|
159
|
+
border-radius: 24px;
|
|
160
|
+
padding: 28px;
|
|
161
|
+
border: 1px solid ${palette.border};
|
|
162
|
+
background:
|
|
163
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 250, 252, 0.92) 100%);
|
|
164
|
+
}
|
|
165
|
+
.eyebrow {
|
|
166
|
+
display: inline-flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
padding: 7px 12px;
|
|
169
|
+
border-radius: 999px;
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
font-weight: 700;
|
|
172
|
+
letter-spacing: 0.04em;
|
|
173
|
+
color: ${palette.badgeFg};
|
|
174
|
+
background: ${palette.badgeBg};
|
|
175
|
+
}
|
|
176
|
+
h1 {
|
|
177
|
+
margin: 18px 0 10px;
|
|
178
|
+
font-size: clamp(30px, 4vw, 38px);
|
|
179
|
+
line-height: 1.08;
|
|
180
|
+
color: #0f172a;
|
|
181
|
+
}
|
|
182
|
+
.message {
|
|
183
|
+
margin: 0;
|
|
184
|
+
font-size: 16px;
|
|
185
|
+
line-height: 1.7;
|
|
186
|
+
color: #334155;
|
|
187
|
+
}
|
|
188
|
+
.detail {
|
|
189
|
+
margin: 16px 0 0;
|
|
190
|
+
padding: 12px 14px;
|
|
191
|
+
border-radius: 16px;
|
|
192
|
+
font-size: 14px;
|
|
193
|
+
line-height: 1.6;
|
|
194
|
+
color: #475569;
|
|
195
|
+
background: rgba(248, 250, 252, 0.96);
|
|
196
|
+
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
197
|
+
}
|
|
198
|
+
.next {
|
|
199
|
+
margin-top: 24px;
|
|
200
|
+
padding: 20px;
|
|
201
|
+
border-radius: 20px;
|
|
202
|
+
background: rgba(248, 250, 252, 0.94);
|
|
203
|
+
}
|
|
204
|
+
.next h2 {
|
|
205
|
+
margin: 0 0 12px;
|
|
206
|
+
font-size: 15px;
|
|
207
|
+
color: ${palette.accent};
|
|
208
|
+
}
|
|
209
|
+
ol {
|
|
210
|
+
margin: 0;
|
|
211
|
+
padding-left: 20px;
|
|
212
|
+
color: #334155;
|
|
213
|
+
}
|
|
214
|
+
li + li {
|
|
215
|
+
margin-top: 10px;
|
|
216
|
+
}
|
|
217
|
+
.footer {
|
|
218
|
+
margin-top: 22px;
|
|
219
|
+
font-size: 13px;
|
|
220
|
+
color: #64748b;
|
|
221
|
+
}
|
|
222
|
+
@media (max-width: 640px) {
|
|
223
|
+
.shell {
|
|
224
|
+
padding: 22px;
|
|
225
|
+
border-radius: 24px;
|
|
226
|
+
}
|
|
227
|
+
.panel {
|
|
228
|
+
padding: 22px;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
</style>
|
|
232
|
+
</head>
|
|
233
|
+
<body>
|
|
234
|
+
<main class="shell">
|
|
235
|
+
<div class="brand">
|
|
236
|
+
<span class="brand-mark"></span>
|
|
237
|
+
<span>ScoreHub MCP</span>
|
|
238
|
+
</div>
|
|
239
|
+
<section class="panel">
|
|
240
|
+
<div class="eyebrow">${escapeHtml(page.eyebrow)}</div>
|
|
241
|
+
<h1>${escapeHtml(page.title)}</h1>
|
|
242
|
+
<p class="message">${escapeHtml(page.message)}</p>
|
|
243
|
+
${detail}
|
|
244
|
+
<div class="next">
|
|
245
|
+
<h2>下一步</h2>
|
|
246
|
+
<ol>${steps}</ol>
|
|
247
|
+
</div>
|
|
248
|
+
</section>
|
|
249
|
+
<p class="footer">这是本机回调页。完成后可以直接关闭当前页面。</p>
|
|
250
|
+
</main>
|
|
251
|
+
</body>
|
|
252
|
+
</html>`;
|
|
253
|
+
}
|
|
254
|
+
function getTokenErrorMessage(payload) {
|
|
255
|
+
if (payload && typeof payload === "object") {
|
|
256
|
+
const record = payload;
|
|
257
|
+
if (typeof record.error_description === "string" && record.error_description) {
|
|
258
|
+
return record.error_description;
|
|
259
|
+
}
|
|
260
|
+
if (typeof record.error === "string" && record.error) {
|
|
261
|
+
return record.error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return JSON.stringify(payload);
|
|
265
|
+
}
|
|
266
|
+
function parseOAuthTokens(payload) {
|
|
267
|
+
if (payload && typeof payload === "object") {
|
|
268
|
+
const record = payload;
|
|
269
|
+
if (typeof record.access_token === "string"
|
|
270
|
+
&& typeof record.refresh_token === "string"
|
|
271
|
+
&& typeof record.expires_in === "number") {
|
|
272
|
+
return {
|
|
273
|
+
access_token: record.access_token,
|
|
274
|
+
refresh_token: record.refresh_token,
|
|
275
|
+
expires_in: record.expires_in,
|
|
276
|
+
...(typeof record.scope === "string" ? { scope: record.scope } : {}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
throw new Error(getTokenErrorMessage(payload));
|
|
281
|
+
}
|
|
57
282
|
// ── Token Exchange ───────────────────────────────────────────
|
|
58
283
|
function tokenPostRequest(tokenEndpoint, body) {
|
|
59
284
|
const url = new URL(tokenEndpoint);
|
|
@@ -91,7 +316,7 @@ async function exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, clientId
|
|
|
91
316
|
client_id: clientId,
|
|
92
317
|
redirect_uri: redirectUri,
|
|
93
318
|
}).toString();
|
|
94
|
-
return tokenPostRequest(tokenEndpoint, body);
|
|
319
|
+
return parseOAuthTokens(await tokenPostRequest(tokenEndpoint, body));
|
|
95
320
|
}
|
|
96
321
|
async function refreshAccessToken(tokenEndpoint, refreshToken, clientId) {
|
|
97
322
|
const body = new URLSearchParams({
|
|
@@ -99,7 +324,7 @@ async function refreshAccessToken(tokenEndpoint, refreshToken, clientId) {
|
|
|
99
324
|
refresh_token: refreshToken,
|
|
100
325
|
client_id: clientId,
|
|
101
326
|
}).toString();
|
|
102
|
-
return tokenPostRequest(tokenEndpoint, body);
|
|
327
|
+
return parseOAuthTokens(await tokenPostRequest(tokenEndpoint, body));
|
|
103
328
|
}
|
|
104
329
|
async function getValidAccessToken(host, tokenEndpoint, clientId) {
|
|
105
330
|
const cached = loadTokens(host);
|
|
@@ -135,125 +360,193 @@ function generateCodeChallenge(verifier) {
|
|
|
135
360
|
function generateState(length = 16) {
|
|
136
361
|
return (0, node_crypto_1.randomBytes)(length).toString("hex");
|
|
137
362
|
}
|
|
138
|
-
// ── Local Callback Server ────────────────────────────────────
|
|
139
|
-
function startCallbackServer(port, timeoutMs = 120_000) {
|
|
140
|
-
return new Promise((resolve, reject) => {
|
|
141
|
-
let timeoutId;
|
|
142
|
-
const server = (0, node_http_2.createServer)((req, res) => {
|
|
143
|
-
const actualPort = server.address()?.port || port;
|
|
144
|
-
const requestUrl = req.url ?? "/";
|
|
145
|
-
const url = new URL(requestUrl, `http://127.0.0.1:${actualPort}`);
|
|
146
|
-
const code = url.searchParams.get("code");
|
|
147
|
-
const state = url.searchParams.get("state");
|
|
148
|
-
if (code) {
|
|
149
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
150
|
-
res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Complete</title></head>" +
|
|
151
|
-
"<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
|
|
152
|
-
"<h2>✅ Authorization Successful</h2>" +
|
|
153
|
-
"<p>You may close this window and return to the terminal.</p>" +
|
|
154
|
-
"</body></html>");
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
158
|
-
res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Failed</title></head>" +
|
|
159
|
-
"<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
|
|
160
|
-
"<h2>❌ Authorization Failed</h2>" +
|
|
161
|
-
"<p>No authorization code received from the provider.</p>" +
|
|
162
|
-
"</body></html>");
|
|
163
|
-
}
|
|
164
|
-
if (timeoutId)
|
|
165
|
-
clearTimeout(timeoutId);
|
|
166
|
-
server.close();
|
|
167
|
-
if (code) {
|
|
168
|
-
resolve({ code, state: state ?? "", port: actualPort });
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
reject(new Error("No authorization code in callback"));
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
server.on("error", (err) => {
|
|
175
|
-
if (timeoutId)
|
|
176
|
-
clearTimeout(timeoutId);
|
|
177
|
-
reject(err);
|
|
178
|
-
});
|
|
179
|
-
server.listen(port, "127.0.0.1", () => {
|
|
180
|
-
server.unref();
|
|
181
|
-
timeoutId = setTimeout(() => {
|
|
182
|
-
server.close();
|
|
183
|
-
reject(new Error(`Authorization timed out after ${timeoutMs / 1000}s. Please try again.`));
|
|
184
|
-
}, timeoutMs);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
363
|
// ── Browser Launch ───────────────────────────────────────────
|
|
189
364
|
function openBrowser(url) {
|
|
190
365
|
try {
|
|
191
366
|
const platform = process.platform;
|
|
192
367
|
if (platform === "darwin") {
|
|
193
|
-
(0, node_child_process_1.
|
|
368
|
+
(0, node_child_process_1.execFileSync)("open", [url], { stdio: "ignore", timeout: 3000 });
|
|
194
369
|
}
|
|
195
370
|
else if (platform === "win32") {
|
|
196
|
-
(0, node_child_process_1.
|
|
371
|
+
(0, node_child_process_1.execFileSync)("cmd.exe", ["/c", "start", "", url], { stdio: "ignore", timeout: 3000 });
|
|
197
372
|
}
|
|
198
373
|
else {
|
|
199
|
-
(0, node_child_process_1.
|
|
374
|
+
(0, node_child_process_1.execFileSync)("xdg-open", [url], { stdio: "ignore", timeout: 3000 });
|
|
200
375
|
}
|
|
201
376
|
}
|
|
202
377
|
catch {
|
|
203
|
-
console.error(
|
|
378
|
+
console.error("[scorehub] Could not open browser automatically.");
|
|
204
379
|
console.error(`[scorehub] Please visit this URL manually:\n ${url}`);
|
|
205
380
|
}
|
|
206
381
|
}
|
|
207
382
|
// ── Full OAuth Flow ──────────────────────────────────────────
|
|
208
383
|
async function authorize(params) {
|
|
209
|
-
const { authEndpoint, tokenEndpoint, clientId, redirectUri,
|
|
384
|
+
const { authEndpoint, tokenEndpoint, clientId, redirectUri, scope, timeoutMs = 120_000, host, } = params;
|
|
385
|
+
const redirectUrl = redirectUri ? new URL(redirectUri) : null;
|
|
386
|
+
const callbackHost = params.callbackHost ?? redirectUrl?.hostname ?? DEFAULT_CALLBACK_HOST;
|
|
387
|
+
const callbackPath = normalizeCallbackPath(params.callbackPath ?? redirectUrl?.pathname ?? DEFAULT_CALLBACK_PATH);
|
|
388
|
+
const listenPort = params.port ?? (redirectUrl?.port ? Number(redirectUrl.port) : 0);
|
|
210
389
|
const storageHost = host ?? new URL(authEndpoint).host;
|
|
211
390
|
const codeVerifier = generateCodeVerifier();
|
|
212
391
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
213
392
|
const state = generateState();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
393
|
+
return new Promise((resolve, reject) => {
|
|
394
|
+
let timeoutId;
|
|
395
|
+
let settled = false;
|
|
396
|
+
const stopServer = (server) => {
|
|
397
|
+
if (timeoutId) {
|
|
398
|
+
clearTimeout(timeoutId);
|
|
399
|
+
timeoutId = undefined;
|
|
400
|
+
}
|
|
401
|
+
server.close();
|
|
402
|
+
};
|
|
403
|
+
const server = (0, node_http_1.createServer)((req, res) => {
|
|
404
|
+
const actualPort = server.address()?.port ?? listenPort;
|
|
405
|
+
const requestUrl = req.url ?? "/";
|
|
406
|
+
const url = new URL(requestUrl, `http://${callbackHost}:${actualPort}`);
|
|
407
|
+
if (url.pathname !== callbackPath) {
|
|
408
|
+
res.writeHead(204);
|
|
409
|
+
res.end();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (settled) {
|
|
413
|
+
res.writeHead(409, { "Content-Type": "text/html; charset=utf-8" });
|
|
414
|
+
res.end(buildCallbackPage({
|
|
415
|
+
tone: "error",
|
|
416
|
+
eyebrow: "Request Closed",
|
|
417
|
+
title: "授权请求已结束",
|
|
418
|
+
message: "当前授权请求已经结束,请返回终端重新执行 authorize。",
|
|
419
|
+
steps: [
|
|
420
|
+
"回到 MCP 客户端终端查看最近一次授权结果。",
|
|
421
|
+
"如果仍需登录,请重新执行 authorize。",
|
|
422
|
+
],
|
|
423
|
+
}));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
void (async () => {
|
|
427
|
+
try {
|
|
428
|
+
const oauthError = url.searchParams.get("error");
|
|
429
|
+
const oauthErrorDescription = url.searchParams.get("error_description") ?? undefined;
|
|
430
|
+
if (oauthError) {
|
|
431
|
+
throw new CallbackError(oauthError === "access_denied" ? "User denied authorization" : `OAuth callback returned error: ${oauthError}`, {
|
|
432
|
+
tone: "error",
|
|
433
|
+
eyebrow: "Authorization Canceled",
|
|
434
|
+
title: oauthError === "access_denied" ? "你已取消授权" : "授权未完成",
|
|
435
|
+
message: "ScoreHub 没有返回可用的授权结果,本次登录不会写入本地 token。",
|
|
436
|
+
detail: oauthErrorDescription ?? oauthError,
|
|
437
|
+
steps: [
|
|
438
|
+
"返回终端查看本次授权失败提示。",
|
|
439
|
+
"如仍需使用 ScoreHub,请重新执行 authorize。",
|
|
440
|
+
],
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const code = url.searchParams.get("code");
|
|
444
|
+
if (!code) {
|
|
445
|
+
throw new CallbackError("No authorization code in callback", {
|
|
446
|
+
tone: "error",
|
|
447
|
+
eyebrow: "Missing Code",
|
|
448
|
+
title: "没有收到授权码",
|
|
449
|
+
message: "本地回调页已打开,但远端没有带回可交换 token 的授权码。",
|
|
450
|
+
steps: [
|
|
451
|
+
"返回终端查看详细错误信息。",
|
|
452
|
+
"重新执行 authorize,并确认浏览器完成了全部授权步骤。",
|
|
453
|
+
],
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
const returnedState = url.searchParams.get("state") ?? "";
|
|
457
|
+
if (returnedState !== state) {
|
|
458
|
+
throw new CallbackError("OAuth state mismatch — possible CSRF attack. Please restart the authorization flow.", {
|
|
459
|
+
tone: "error",
|
|
460
|
+
eyebrow: "State Mismatch",
|
|
461
|
+
title: "本地状态校验失败",
|
|
462
|
+
message: "回调中的 state 与本地发起的请求不一致,本次授权已被拒绝写入。",
|
|
463
|
+
steps: [
|
|
464
|
+
"关闭当前页面并返回终端。",
|
|
465
|
+
"重新执行 authorize 发起一次新的授权流程。",
|
|
466
|
+
],
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const actualRedirectUri = redirectUri ?? buildLocalRedirectUri(callbackHost, actualPort, callbackPath);
|
|
470
|
+
const tokens = await exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, clientId, actualRedirectUri);
|
|
471
|
+
saveTokens(storageHost, {
|
|
472
|
+
access_token: tokens.access_token,
|
|
473
|
+
refresh_token: tokens.refresh_token,
|
|
474
|
+
expires_at: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
475
|
+
updated_at: new Date().toISOString(),
|
|
476
|
+
});
|
|
477
|
+
settled = true;
|
|
478
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
479
|
+
res.end(buildCallbackPage({
|
|
480
|
+
tone: "success",
|
|
481
|
+
eyebrow: "Authorization Complete",
|
|
482
|
+
title: "ScoreHub 已完成授权",
|
|
483
|
+
message: "本地 token 已写入当前终端配置,你现在可以返回 MCP 客户端继续使用工具。",
|
|
484
|
+
steps: [
|
|
485
|
+
"回到终端继续执行刚才的操作。",
|
|
486
|
+
"如果当前客户端没有自动继续,可直接重新调用目标 ScoreHub 工具。",
|
|
487
|
+
],
|
|
488
|
+
}));
|
|
489
|
+
stopServer(server);
|
|
490
|
+
resolve({
|
|
491
|
+
accessToken: tokens.access_token,
|
|
492
|
+
refreshToken: tokens.refresh_token,
|
|
493
|
+
expiresIn: tokens.expires_in,
|
|
494
|
+
scope: tokens.scope,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
settled = true;
|
|
499
|
+
clearTokens(storageHost);
|
|
500
|
+
const err = error instanceof CallbackError
|
|
501
|
+
? error
|
|
502
|
+
: new CallbackError(error instanceof Error ? error.message : String(error), {
|
|
503
|
+
tone: "error",
|
|
504
|
+
eyebrow: "Token Exchange Failed",
|
|
505
|
+
title: "授权结果未能保存",
|
|
506
|
+
message: "浏览器已经完成跳转,但本地未能成功换取或保存 ScoreHub token。",
|
|
507
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
508
|
+
steps: [
|
|
509
|
+
"返回终端查看失败原因。",
|
|
510
|
+
"确认网络和远端状态后,重新执行 authorize。",
|
|
511
|
+
],
|
|
512
|
+
});
|
|
513
|
+
res.writeHead(err.statusCode, { "Content-Type": "text/html; charset=utf-8" });
|
|
514
|
+
res.end(buildCallbackPage(err.page));
|
|
515
|
+
stopServer(server);
|
|
516
|
+
reject(err);
|
|
517
|
+
}
|
|
518
|
+
})();
|
|
519
|
+
});
|
|
520
|
+
server.on("error", (err) => {
|
|
521
|
+
if (settled)
|
|
522
|
+
return;
|
|
523
|
+
settled = true;
|
|
524
|
+
clearTokens(storageHost);
|
|
525
|
+
stopServer(server);
|
|
526
|
+
reject(err);
|
|
527
|
+
});
|
|
528
|
+
server.listen(listenPort, callbackHost, () => {
|
|
529
|
+
server.unref();
|
|
530
|
+
const actualPort = server.address().port;
|
|
531
|
+
const actualRedirectUri = redirectUri ?? buildLocalRedirectUri(callbackHost, actualPort, callbackPath);
|
|
532
|
+
const authUrl = new URL(authEndpoint);
|
|
533
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
534
|
+
authUrl.searchParams.set("redirect_uri", actualRedirectUri);
|
|
535
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
536
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
537
|
+
authUrl.searchParams.set("state", state);
|
|
538
|
+
authUrl.searchParams.set("response_type", "code");
|
|
539
|
+
if (scope)
|
|
540
|
+
authUrl.searchParams.set("scope", scope);
|
|
541
|
+
openBrowser(authUrl.toString());
|
|
542
|
+
timeoutId = setTimeout(() => {
|
|
543
|
+
if (settled)
|
|
544
|
+
return;
|
|
545
|
+
settled = true;
|
|
546
|
+
clearTokens(storageHost);
|
|
547
|
+
stopServer(server);
|
|
548
|
+
reject(new Error(`Authorization timed out after ${timeoutMs / 1000}s. Please try again.`));
|
|
549
|
+
}, timeoutMs);
|
|
550
|
+
});
|
|
252
551
|
});
|
|
253
|
-
return {
|
|
254
|
-
accessToken: tokens.access_token,
|
|
255
|
-
refreshToken: tokens.refresh_token,
|
|
256
|
-
expiresIn: tokens.expires_in,
|
|
257
|
-
scope: tokens.scope,
|
|
258
|
-
};
|
|
259
552
|
}
|
package/dist/index.js
CHANGED
|
@@ -37,12 +37,57 @@ function endpointTokenKey(baseUrl) {
|
|
|
37
37
|
}
|
|
38
38
|
// ── Auth State ──────────────────────────────────────────────
|
|
39
39
|
const auth_js_1 = require("./auth.js");
|
|
40
|
-
const node_http_1 = require("node:http");
|
|
41
|
-
const node_child_process_1 = require("node:child_process");
|
|
42
40
|
const TOKEN_STORE_KEY = endpointTokenKey(SCOREHUB_BASE_URL);
|
|
43
41
|
const CLIENT_ID = "scorehub-mcp";
|
|
44
42
|
// Proactively refresh token every 4 hours to prevent expiry during long sessions
|
|
45
43
|
const TOKEN_KEEPALIVE_INTERVAL = 4 * 60 * 60 * 1000;
|
|
44
|
+
function isRemoteToolErrorType(value) {
|
|
45
|
+
return value === "oauth_invalid"
|
|
46
|
+
|| value === "shop_auth_invalid"
|
|
47
|
+
|| value === "rate_limited"
|
|
48
|
+
|| value === "quota_exhausted"
|
|
49
|
+
|| value === "invalid_input"
|
|
50
|
+
|| value === "permission_denied"
|
|
51
|
+
|| value === "upstream_error";
|
|
52
|
+
}
|
|
53
|
+
function parseRemoteToolEnvelope(text) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(text);
|
|
56
|
+
if (parsed.ok === true && Object.prototype.hasOwnProperty.call(parsed, "data")) {
|
|
57
|
+
return { ok: true, data: parsed.data };
|
|
58
|
+
}
|
|
59
|
+
if (parsed.ok === false
|
|
60
|
+
&& isRemoteToolErrorType(parsed.error_type)
|
|
61
|
+
&& typeof parsed.user_message === "string"
|
|
62
|
+
&& typeof parsed.retryable === "boolean") {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error_type: parsed.error_type,
|
|
66
|
+
user_message: parsed.user_message,
|
|
67
|
+
retryable: parsed.retryable,
|
|
68
|
+
...(Object.prototype.hasOwnProperty.call(parsed, "upstream") ? { upstream: parsed.upstream } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function extractRemoteTextContent(content) {
|
|
78
|
+
if (!Array.isArray(content)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return content.flatMap((item) => {
|
|
82
|
+
if (item
|
|
83
|
+
&& typeof item === "object"
|
|
84
|
+
&& item.type === "text"
|
|
85
|
+
&& typeof item.text === "string") {
|
|
86
|
+
return [item.text];
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
46
91
|
// ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
|
|
47
92
|
async function fetchRemoteTools(token) {
|
|
48
93
|
try {
|
|
@@ -74,18 +119,20 @@ async function callRemoteTool(name, args, token) {
|
|
|
74
119
|
try {
|
|
75
120
|
await client.connect(transport);
|
|
76
121
|
const result = await client.callTool({ name, arguments: args });
|
|
77
|
-
|
|
122
|
+
const textBlocks = extractRemoteTextContent(result.content);
|
|
123
|
+
const rawText = textBlocks.length > 0
|
|
124
|
+
? textBlocks.join("\n\n")
|
|
125
|
+
: JSON.stringify(result.content, null, 2);
|
|
126
|
+
const envelope = textBlocks
|
|
127
|
+
.map((text) => parseRemoteToolEnvelope(text))
|
|
128
|
+
.find((candidate) => candidate !== null)
|
|
129
|
+
?? parseRemoteToolEnvelope(rawText);
|
|
130
|
+
return { rawText, envelope };
|
|
78
131
|
}
|
|
79
132
|
finally {
|
|
80
133
|
await client.close();
|
|
81
134
|
}
|
|
82
135
|
}
|
|
83
|
-
// ── OAuth authorize helper ──────────────────────────────────
|
|
84
|
-
function generateAuthUrl(port = 19836) {
|
|
85
|
-
// Generate random state for CSRF protection
|
|
86
|
-
const state = Math.random().toString(36).substring(2);
|
|
87
|
-
return `${AUTHORIZE_URL}?client_id=scorehub-mcp&redirect_uri=http://127.0.0.1:${port}/callback&state=${state}`;
|
|
88
|
-
}
|
|
89
136
|
// ── Fetch remote tools on startup ───────────────────────────
|
|
90
137
|
let remoteTools = [];
|
|
91
138
|
let lastFetchError = null;
|
|
@@ -153,97 +200,23 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
153
200
|
else {
|
|
154
201
|
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
155
202
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const url = new URL(requestUrl, `http://127.0.0.1:${actualPort}`);
|
|
164
|
-
// 只处理 /callback 路径的请求,忽略浏览器探测请求(favicon、安全检查等)
|
|
165
|
-
if (url.pathname !== "/callback") {
|
|
166
|
-
res.writeHead(204);
|
|
167
|
-
res.end();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (timeoutId)
|
|
171
|
-
clearTimeout(timeoutId);
|
|
172
|
-
const code = url.searchParams.get("code");
|
|
173
|
-
if (code) {
|
|
174
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
175
|
-
res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Complete</title></head>" +
|
|
176
|
-
"<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
|
|
177
|
-
"<h2>✅ Authorization Successful</h2>" +
|
|
178
|
-
"<p>You may close this window and return to the terminal.</p>" +
|
|
179
|
-
"</body></html>");
|
|
180
|
-
callbackServer.close();
|
|
181
|
-
fetch(TOKEN_ENDPOINT, {
|
|
182
|
-
method: "POST",
|
|
183
|
-
headers: { "Content-Type": "application/json" },
|
|
184
|
-
body: JSON.stringify({ grant_type: "authorization_code", code }),
|
|
185
|
-
})
|
|
186
|
-
.then((r) => r.json())
|
|
187
|
-
.then((tokenData) => {
|
|
188
|
-
if (!tokenData.access_token) {
|
|
189
|
-
resolve({
|
|
190
|
-
content: [{ type: "text", text: `❌ Token 获取失败: ${JSON.stringify(tokenData)}` }],
|
|
191
|
-
});
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
(0, auth_js_1.saveTokens)(TOKEN_STORE_KEY, {
|
|
195
|
-
access_token: tokenData.access_token,
|
|
196
|
-
refresh_token: tokenData.refresh_token ?? "",
|
|
197
|
-
expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
|
|
198
|
-
updated_at: new Date().toISOString(),
|
|
199
|
-
});
|
|
200
|
-
resolve({
|
|
201
|
-
content: [{ type: "text", text: "✅ 授权成功!可以开始使用工具了。" }],
|
|
202
|
-
});
|
|
203
|
-
})
|
|
204
|
-
.catch((e) => resolve({
|
|
205
|
-
content: [{ type: "text", text: `❌ Token 获取失败: ${String(e)}` }],
|
|
206
|
-
}));
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
// 到达 /callback 但没有 code —— 真正的错误
|
|
210
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
211
|
-
res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Failed</title></head>" +
|
|
212
|
-
"<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
|
|
213
|
-
"<h2>❌ Authorization Failed</h2>" +
|
|
214
|
-
"<p>No authorization code received from the provider.</p>" +
|
|
215
|
-
"</body></html>");
|
|
216
|
-
callbackServer.close();
|
|
217
|
-
resolve({
|
|
218
|
-
content: [{ type: "text", text: "❌ 授权失败: No authorization code in callback" }],
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
callbackServer.on("error", (err) => {
|
|
223
|
-
if (timeoutId)
|
|
224
|
-
clearTimeout(timeoutId);
|
|
225
|
-
resolve({
|
|
226
|
-
content: [{ type: "text", text: `❌ 授权失败: ${String(err)}` }],
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
callbackServer.listen(0, "127.0.0.1", () => {
|
|
230
|
-
actualPort = callbackServer.address().port;
|
|
231
|
-
callbackServer.unref();
|
|
232
|
-
const authUrl = generateAuthUrl(actualPort);
|
|
233
|
-
const os = process.platform;
|
|
234
|
-
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
235
|
-
try {
|
|
236
|
-
(0, node_child_process_1.execSync)(`${cmd} "${authUrl}"`);
|
|
237
|
-
}
|
|
238
|
-
catch { /* headless env: user opens manually */ }
|
|
239
|
-
timeoutId = setTimeout(() => {
|
|
240
|
-
callbackServer.close();
|
|
241
|
-
resolve({
|
|
242
|
-
content: [{ type: "text", text: "❌ 授权超时 (120s)。请重试。" }],
|
|
243
|
-
});
|
|
244
|
-
}, 120_000);
|
|
203
|
+
try {
|
|
204
|
+
await (0, auth_js_1.authorize)({
|
|
205
|
+
authEndpoint: AUTHORIZE_URL,
|
|
206
|
+
tokenEndpoint: TOKEN_ENDPOINT,
|
|
207
|
+
clientId: CLIENT_ID,
|
|
208
|
+
host: TOKEN_STORE_KEY,
|
|
209
|
+
port: 0,
|
|
245
210
|
});
|
|
246
|
-
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: "✅ 授权成功!浏览器中的本地回调页已确认,可以直接继续使用 ScoreHub 工具。" }],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: `❌ 授权失败: ${error instanceof Error ? error.message : String(error)}` }],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
247
220
|
}
|
|
248
221
|
case "status": {
|
|
249
222
|
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
@@ -266,31 +239,40 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
266
239
|
// Check authorization for remote tools — re-validates on every call
|
|
267
240
|
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
268
241
|
if (!tokenResult) {
|
|
269
|
-
const authUrl = generateAuthUrl();
|
|
270
242
|
return {
|
|
271
243
|
content: [
|
|
272
244
|
{
|
|
273
245
|
type: "text",
|
|
274
|
-
text:
|
|
246
|
+
text: "⚠️ 未授权。请先调用 \"authorize\" 工具登录 ScoreHub。若浏览器未自动打开,请复制终端输出的授权链接到浏览器完成登录。",
|
|
275
247
|
},
|
|
276
248
|
],
|
|
277
249
|
};
|
|
278
250
|
}
|
|
279
251
|
try {
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
252
|
+
const remoteResult = await callRemoteTool(name, args, tokenResult.access_token);
|
|
253
|
+
if (remoteResult.envelope?.ok === false && remoteResult.envelope.error_type === "oauth_invalid") {
|
|
254
|
+
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: "⚠️ 授权已失效,已清除本地缓存。请调用 \"authorize\" 工具重新登录 ScoreHub。若浏览器未自动打开,请复制终端输出的授权链接到浏览器完成登录。",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (remoteResult.rawText.includes("ScoreHub OAuth 授权已过期或无效")) {
|
|
282
265
|
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
283
|
-
const authUrl = generateAuthUrl();
|
|
284
266
|
return {
|
|
285
267
|
content: [
|
|
286
268
|
{
|
|
287
269
|
type: "text",
|
|
288
|
-
text:
|
|
270
|
+
text: "⚠️ 授权已失效,已清除本地缓存。请调用 \"authorize\" 工具重新登录 ScoreHub。若浏览器未自动打开,请复制终端输出的授权链接到浏览器完成登录。",
|
|
289
271
|
},
|
|
290
272
|
],
|
|
291
273
|
};
|
|
292
274
|
}
|
|
293
|
-
return { content: [{ type: "text", text:
|
|
275
|
+
return { content: [{ type: "text", text: remoteResult.rawText }] };
|
|
294
276
|
}
|
|
295
277
|
catch (e) {
|
|
296
278
|
return {
|