@scorehub/mcp-server 0.2.6 → 0.2.8
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 +3 -1
- package/dist/auth.js +4 -1
- package/dist/index.js +101 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,8 @@ The `<endpoint>` is derived from the `SCOREHUB_REMOTE_MCP_URL` host and path (fo
|
|
|
116
116
|
|
|
117
117
|
To log out or switch accounts, delete the `~/.scorehub/` directory or just the host-specific subdirectory.
|
|
118
118
|
|
|
119
|
+
The `authorize` tool reuses cached credentials by default. If the cached access token is still valid, or the refresh token can renew it, `authorize` returns without opening the browser. To force a new browser login, call `authorize` with `force: true`.
|
|
120
|
+
|
|
119
121
|
## Available Tools
|
|
120
122
|
|
|
121
123
|
### Local tools
|
|
@@ -124,7 +126,7 @@ These tools run locally on your machine — they do not call the remote endpoint
|
|
|
124
126
|
|
|
125
127
|
| Tool | Description |
|
|
126
128
|
|---|---|
|
|
127
|
-
| `authorize` |
|
|
129
|
+
| `authorize` | Checks ScoreHub OAuth status. Reuses cached credentials by default; pass `force: true` to open the browser for a fresh login. |
|
|
128
130
|
| `status` | Returns connection status: remote URL, authorization state, and count of available tools. |
|
|
129
131
|
|
|
130
132
|
### Remote tools
|
package/dist/auth.js
CHANGED
|
@@ -51,6 +51,9 @@ function clearTokens(host) {
|
|
|
51
51
|
(0, node_fs_1.writeFileSync)(file, JSON.stringify({}), "utf-8");
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
function hasSignedTokenShape(token) {
|
|
55
|
+
return token.split(".").length === 3;
|
|
56
|
+
}
|
|
54
57
|
// ── Token Exchange ───────────────────────────────────────────
|
|
55
58
|
function tokenPostRequest(tokenEndpoint, body) {
|
|
56
59
|
const url = new URL(tokenEndpoint);
|
|
@@ -100,7 +103,7 @@ async function refreshAccessToken(tokenEndpoint, refreshToken, clientId) {
|
|
|
100
103
|
}
|
|
101
104
|
async function getValidAccessToken(host, tokenEndpoint, clientId) {
|
|
102
105
|
const cached = loadTokens(host);
|
|
103
|
-
if (cached && cached.access_token && cached.expires_at > Date.now() + 5 * 60 * 1000) {
|
|
106
|
+
if (cached && cached.access_token && hasSignedTokenShape(cached.access_token) && cached.expires_at > Date.now() + 5 * 60 * 1000) {
|
|
104
107
|
return { access_token: cached.access_token, source: "cache" };
|
|
105
108
|
}
|
|
106
109
|
if (cached?.refresh_token) {
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,53 @@ const TOKEN_STORE_KEY = endpointTokenKey(SCOREHUB_BASE_URL);
|
|
|
43
43
|
const CLIENT_ID = "scorehub-mcp";
|
|
44
44
|
// Proactively refresh token every 4 hours to prevent expiry during long sessions
|
|
45
45
|
const TOKEN_KEEPALIVE_INTERVAL = 4 * 60 * 60 * 1000;
|
|
46
|
+
function isRemoteToolErrorType(value) {
|
|
47
|
+
return value === "oauth_invalid"
|
|
48
|
+
|| value === "shop_auth_invalid"
|
|
49
|
+
|| value === "rate_limited"
|
|
50
|
+
|| value === "quota_exhausted"
|
|
51
|
+
|| value === "invalid_input"
|
|
52
|
+
|| value === "permission_denied"
|
|
53
|
+
|| value === "upstream_error";
|
|
54
|
+
}
|
|
55
|
+
function parseRemoteToolEnvelope(text) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(text);
|
|
58
|
+
if (parsed.ok === true && Object.prototype.hasOwnProperty.call(parsed, "data")) {
|
|
59
|
+
return { ok: true, data: parsed.data };
|
|
60
|
+
}
|
|
61
|
+
if (parsed.ok === false
|
|
62
|
+
&& isRemoteToolErrorType(parsed.error_type)
|
|
63
|
+
&& typeof parsed.user_message === "string"
|
|
64
|
+
&& typeof parsed.retryable === "boolean") {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error_type: parsed.error_type,
|
|
68
|
+
user_message: parsed.user_message,
|
|
69
|
+
retryable: parsed.retryable,
|
|
70
|
+
...(Object.prototype.hasOwnProperty.call(parsed, "upstream") ? { upstream: parsed.upstream } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function extractRemoteTextContent(content) {
|
|
80
|
+
if (!Array.isArray(content)) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return content.flatMap((item) => {
|
|
84
|
+
if (item
|
|
85
|
+
&& typeof item === "object"
|
|
86
|
+
&& item.type === "text"
|
|
87
|
+
&& typeof item.text === "string") {
|
|
88
|
+
return [item.text];
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
});
|
|
92
|
+
}
|
|
46
93
|
// ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
|
|
47
94
|
async function fetchRemoteTools(token) {
|
|
48
95
|
try {
|
|
@@ -74,7 +121,15 @@ async function callRemoteTool(name, args, token) {
|
|
|
74
121
|
try {
|
|
75
122
|
await client.connect(transport);
|
|
76
123
|
const result = await client.callTool({ name, arguments: args });
|
|
77
|
-
|
|
124
|
+
const textBlocks = extractRemoteTextContent(result.content);
|
|
125
|
+
const rawText = textBlocks.length > 0
|
|
126
|
+
? textBlocks.join("\n\n")
|
|
127
|
+
: JSON.stringify(result.content, null, 2);
|
|
128
|
+
const envelope = textBlocks
|
|
129
|
+
.map((text) => parseRemoteToolEnvelope(text))
|
|
130
|
+
.find((candidate) => candidate !== null)
|
|
131
|
+
?? parseRemoteToolEnvelope(rawText);
|
|
132
|
+
return { rawText, envelope };
|
|
78
133
|
}
|
|
79
134
|
finally {
|
|
80
135
|
await client.close();
|
|
@@ -108,10 +163,15 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
108
163
|
const tools = [
|
|
109
164
|
{
|
|
110
165
|
name: "authorize",
|
|
111
|
-
description: "
|
|
166
|
+
description: "检查 ScoreHub OAuth 授权。已有有效授权时直接返回;仅在传入 force=true 时重新打开浏览器登录。",
|
|
112
167
|
inputSchema: {
|
|
113
168
|
type: "object",
|
|
114
|
-
properties: {
|
|
169
|
+
properties: {
|
|
170
|
+
force: {
|
|
171
|
+
type: "boolean",
|
|
172
|
+
description: "是否强制清除当前端点的本地 token 并重新登录",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
115
175
|
required: [],
|
|
116
176
|
},
|
|
117
177
|
},
|
|
@@ -136,8 +196,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
136
196
|
const { name, arguments: args = {} } = request.params;
|
|
137
197
|
switch (name) {
|
|
138
198
|
case "authorize": {
|
|
139
|
-
|
|
140
|
-
|
|
199
|
+
const force = args.force === true;
|
|
200
|
+
if (!force) {
|
|
201
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
202
|
+
if (tokenResult) {
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text: `✅ 已授权(${tokenResult.source === "cache" ? "使用本地缓存" : "已自动刷新"}),可以直接使用 ScoreHub 工具。` }],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
210
|
+
}
|
|
141
211
|
// Port 0 lets the OS assign a random available port,
|
|
142
212
|
// avoiding conflicts when multiple MCP clients run simultaneously
|
|
143
213
|
let actualPort = 0;
|
|
@@ -262,8 +332,32 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
262
332
|
};
|
|
263
333
|
}
|
|
264
334
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
335
|
+
const remoteResult = await callRemoteTool(name, args, tokenResult.access_token);
|
|
336
|
+
if (remoteResult.envelope?.ok === false && remoteResult.envelope.error_type === "oauth_invalid") {
|
|
337
|
+
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
338
|
+
const authUrl = generateAuthUrl();
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: `⚠️ 授权已失效,已清除本地缓存。请调用 "authorize" 工具重新登录 ScoreHub。\n授权地址: ${authUrl}`,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (remoteResult.rawText.includes("ScoreHub OAuth 授权已过期或无效")) {
|
|
349
|
+
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
350
|
+
const authUrl = generateAuthUrl();
|
|
351
|
+
return {
|
|
352
|
+
content: [
|
|
353
|
+
{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: `⚠️ 授权已失效,已清除本地缓存。请调用 "authorize" 工具重新登录 ScoreHub。\n授权地址: ${authUrl}`,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { content: [{ type: "text", text: remoteResult.rawText }] };
|
|
267
361
|
}
|
|
268
362
|
catch (e) {
|
|
269
363
|
return {
|