@scorehub/mcp-server 0.2.5 → 0.2.7
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 +33 -12
- package/dist/auth.js +4 -1
- package/dist/index.js +57 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ In `claude_desktop_config.json` or `.claude/mcp.json`:
|
|
|
51
51
|
"command": "npx",
|
|
52
52
|
"args": ["-y", "@scorehub/mcp-server"],
|
|
53
53
|
"env": {
|
|
54
|
-
"SCOREHUB_REMOTE_MCP_URL": "https://
|
|
54
|
+
"SCOREHUB_REMOTE_MCP_URL": "https://app.scorehub.cn/mcp"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -69,7 +69,7 @@ In `mcp.json`:
|
|
|
69
69
|
"command": "npx",
|
|
70
70
|
"args": ["-y", "@scorehub/mcp-server"],
|
|
71
71
|
"env": {
|
|
72
|
-
"SCOREHUB_REMOTE_MCP_URL": "https://
|
|
72
|
+
"SCOREHUB_REMOTE_MCP_URL": "https://app.scorehub.cn/mcp"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -91,17 +91,17 @@ In `mcp.json`:
|
|
|
91
91
|
|
|
92
92
|
| Variable | Required | Default | Description |
|
|
93
93
|
|---|---|---|---|
|
|
94
|
-
| `SCOREHUB_REMOTE_MCP_URL` | No | `https://
|
|
94
|
+
| `SCOREHUB_REMOTE_MCP_URL` | No | `https://app.scorehub.cn/mcp` | Base URL of the remote ScoreHub MCP endpoint |
|
|
95
95
|
|
|
96
96
|
### Token storage
|
|
97
97
|
|
|
98
98
|
OAuth tokens are stored at:
|
|
99
99
|
|
|
100
100
|
```
|
|
101
|
-
~/.scorehub/<
|
|
101
|
+
~/.scorehub/<endpoint>/oauth.json
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
The `<
|
|
104
|
+
The `<endpoint>` is derived from the `SCOREHUB_REMOTE_MCP_URL` host and path (for example, `app.scorehub.cn_mcp`). This means you can run staging and production side-by-side by pointing each configuration at a different URL — tokens are isolated per endpoint.
|
|
105
105
|
|
|
106
106
|
**Token file structure:**
|
|
107
107
|
|
|
@@ -116,6 +116,8 @@ The `<host>` is derived from the `SCOREHUB_REMOTE_MCP_URL` (e.g., `mcp.scorehub.
|
|
|
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
|
|
@@ -158,7 +160,7 @@ These tools are **dynamically loaded** from the remote MCP endpoint at startup a
|
|
|
158
160
|
├─ POST /token ───────────────────▶│
|
|
159
161
|
│◀── access_token + refresh_token │
|
|
160
162
|
│ │
|
|
161
|
-
|
|
163
|
+
├─ saves to ~/.scorehub/<endpoint>/oauth.json
|
|
162
164
|
```
|
|
163
165
|
|
|
164
166
|
### Token lifecycle
|
|
@@ -169,12 +171,31 @@ These tools are **dynamically loaded** from the remote MCP endpoint at startup a
|
|
|
169
171
|
| **Subsequent calls** | Read cached file → check expiry with 5-minute buffer |
|
|
170
172
|
| **Near expiry** | Auto-refresh with `refresh_token` → write new token back to file |
|
|
171
173
|
| **Refresh fails** | Returns an error message with the authorization URL — call `authorize` again |
|
|
172
|
-
| **Manual logout** | Delete `~/.scorehub/<
|
|
174
|
+
| **Manual logout** | Delete `~/.scorehub/<endpoint>/oauth.json` or the entire `~/.scorehub/` directory |
|
|
173
175
|
|
|
174
176
|
### Headless environments
|
|
175
177
|
|
|
176
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:19836/callback` — make sure port 19836 is accessible on the machine running mcp-server.
|
|
177
179
|
|
|
180
|
+
### Local remote development
|
|
181
|
+
|
|
182
|
+
When testing a local `mcp-remote`, point this local proxy at the same local base URL:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
SCOREHUB_REMOTE_MCP_URL=http://127.0.0.1:5201 npm start -w packages/mcp-server
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Start `mcp-remote` with a matching `BASE_URL`:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
cd packages/mcp-remote
|
|
192
|
+
PORT=5201 BASE_URL=http://127.0.0.1:5201 npm start
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`BASE_URL` controls where `mcp-remote` sends the browser after login, for example `http://127.0.0.1:5201/consent?...`. `SCOREHUB_REMOTE_MCP_URL` controls which remote endpoint this proxy uses for `/authorize`, `/token`, and `/mcp`. In production, `https://app.scorehub.cn/mcp` maps to externally visible `/mcp/authorize`, `/mcp/token`, and `/mcp/mcp` paths.
|
|
196
|
+
|
|
197
|
+
OAuth tokens are isolated by endpoint under `~/.scorehub/<endpoint>/oauth.json`, so local tokens for `127.0.0.1:5201` do not overwrite production tokens for `app.scorehub.cn/mcp`.
|
|
198
|
+
|
|
178
199
|
## Architecture
|
|
179
200
|
|
|
180
201
|
```
|
|
@@ -189,13 +210,13 @@ If you're running on a server or in a headless environment where a browser canno
|
|
|
189
210
|
│ │ client) │ │ • Tool forwarding │ │
|
|
190
211
|
│ └──────────────┘ └─────────┬───────────┘ │
|
|
191
212
|
│ │ │
|
|
192
|
-
│ ~/.scorehub/<
|
|
213
|
+
│ ~/.scorehub/<endpoint>/oauth.json │
|
|
193
214
|
└────────────────────────────────────────┼──────────────────────────┘
|
|
194
215
|
│ Streamable HTTP
|
|
195
216
|
▼
|
|
196
217
|
┌─────────────────────┐
|
|
197
218
|
│ ScoreHub Remote MCP │
|
|
198
|
-
│ (
|
|
219
|
+
│ (app.scorehub.cn/mcp)│
|
|
199
220
|
└─────────┬───────────┘
|
|
200
221
|
│
|
|
201
222
|
▼
|
|
@@ -227,7 +248,7 @@ Yes, absolutely. `@scorehub/mcp-server` is a standalone MCP server. Any MCP-comp
|
|
|
227
248
|
Delete the token file:
|
|
228
249
|
|
|
229
250
|
```bash
|
|
230
|
-
rm ~/.scorehub/
|
|
251
|
+
rm ~/.scorehub/app.scorehub.cn_mcp/oauth.json
|
|
231
252
|
```
|
|
232
253
|
|
|
233
254
|
Or delete the entire directory to clear all hosts:
|
|
@@ -238,7 +259,7 @@ rm -rf ~/.scorehub/
|
|
|
238
259
|
|
|
239
260
|
### How do I switch between staging and production?
|
|
240
261
|
|
|
241
|
-
Set different `SCOREHUB_REMOTE_MCP_URL` values in separate MCP client configurations. Tokens are isolated per
|
|
262
|
+
Set different `SCOREHUB_REMOTE_MCP_URL` values in separate MCP client configurations. Tokens are isolated per endpoint, so staging and production credentials won't interfere with each other.
|
|
242
263
|
|
|
243
264
|
### What happens if the remote MCP is unreachable?
|
|
244
265
|
|
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
|
@@ -16,17 +16,30 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
16
16
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
17
17
|
const index_js_2 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
18
18
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
19
|
-
const
|
|
19
|
+
const DEFAULT_SCOREHUB_BASE_URL = "https://app.scorehub.cn/mcp";
|
|
20
|
+
const SCOREHUB_BASE_URL = normalizeBaseUrl(process.env.SCOREHUB_REMOTE_MCP_URL || DEFAULT_SCOREHUB_BASE_URL);
|
|
20
21
|
// OAuth endpoints are at the root path
|
|
21
|
-
const TOKEN_ENDPOINT =
|
|
22
|
-
const AUTHORIZE_URL =
|
|
22
|
+
const TOKEN_ENDPOINT = joinUrl(SCOREHUB_BASE_URL, "/token");
|
|
23
|
+
const AUTHORIZE_URL = joinUrl(SCOREHUB_BASE_URL, "/authorize");
|
|
23
24
|
// MCP Streamable HTTP transport endpoints are at /mcp
|
|
24
|
-
const REMOTE_MCP_URL =
|
|
25
|
+
const REMOTE_MCP_URL = joinUrl(SCOREHUB_BASE_URL, "/mcp");
|
|
26
|
+
function normalizeBaseUrl(value) {
|
|
27
|
+
return value.replace(/\/+$/, "");
|
|
28
|
+
}
|
|
29
|
+
function joinUrl(baseUrl, path) {
|
|
30
|
+
return `${baseUrl}/${path.replace(/^\/+/, "")}`;
|
|
31
|
+
}
|
|
32
|
+
function endpointTokenKey(baseUrl) {
|
|
33
|
+
const url = new URL(baseUrl);
|
|
34
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
35
|
+
const endpoint = path ? `${url.host}/${path}` : url.host;
|
|
36
|
+
return endpoint.replace(/[^A-Za-z0-9.-]+/g, "_");
|
|
37
|
+
}
|
|
25
38
|
// ── Auth State ──────────────────────────────────────────────
|
|
26
39
|
const auth_js_1 = require("./auth.js");
|
|
27
40
|
const node_http_1 = require("node:http");
|
|
28
41
|
const node_child_process_1 = require("node:child_process");
|
|
29
|
-
const
|
|
42
|
+
const TOKEN_STORE_KEY = endpointTokenKey(SCOREHUB_BASE_URL);
|
|
30
43
|
const CLIENT_ID = "scorehub-mcp";
|
|
31
44
|
// Proactively refresh token every 4 hours to prevent expiry during long sessions
|
|
32
45
|
const TOKEN_KEEPALIVE_INTERVAL = 4 * 60 * 60 * 1000;
|
|
@@ -81,7 +94,7 @@ const server = new index_js_1.Server({ name: "scorehub-mcp", version: "0.1.0" },
|
|
|
81
94
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
82
95
|
// Refresh remote tools on each list
|
|
83
96
|
try {
|
|
84
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
97
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
85
98
|
const freshTools = await fetchRemoteTools(tokenResult?.access_token);
|
|
86
99
|
if (freshTools.length > 0 || remoteTools.length === 0) {
|
|
87
100
|
remoteTools = freshTools;
|
|
@@ -95,10 +108,15 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
95
108
|
const tools = [
|
|
96
109
|
{
|
|
97
110
|
name: "authorize",
|
|
98
|
-
description: "
|
|
111
|
+
description: "检查 ScoreHub OAuth 授权。已有有效授权时直接返回;仅在传入 force=true 时重新打开浏览器登录。",
|
|
99
112
|
inputSchema: {
|
|
100
113
|
type: "object",
|
|
101
|
-
properties: {
|
|
114
|
+
properties: {
|
|
115
|
+
force: {
|
|
116
|
+
type: "boolean",
|
|
117
|
+
description: "是否强制清除当前端点的本地 token 并重新登录",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
102
120
|
required: [],
|
|
103
121
|
},
|
|
104
122
|
},
|
|
@@ -123,8 +141,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
123
141
|
const { name, arguments: args = {} } = request.params;
|
|
124
142
|
switch (name) {
|
|
125
143
|
case "authorize": {
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
const force = args.force === true;
|
|
145
|
+
if (!force) {
|
|
146
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
147
|
+
if (tokenResult) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: `✅ 已授权(${tokenResult.source === "cache" ? "使用本地缓存" : "已自动刷新"}),可以直接使用 ScoreHub 工具。` }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
155
|
+
}
|
|
128
156
|
// Port 0 lets the OS assign a random available port,
|
|
129
157
|
// avoiding conflicts when multiple MCP clients run simultaneously
|
|
130
158
|
let actualPort = 0;
|
|
@@ -150,8 +178,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
150
178
|
"<p>You may close this window and return to the terminal.</p>" +
|
|
151
179
|
"</body></html>");
|
|
152
180
|
callbackServer.close();
|
|
153
|
-
|
|
154
|
-
fetch(`${baseUrl}/token`, {
|
|
181
|
+
fetch(TOKEN_ENDPOINT, {
|
|
155
182
|
method: "POST",
|
|
156
183
|
headers: { "Content-Type": "application/json" },
|
|
157
184
|
body: JSON.stringify({ grant_type: "authorization_code", code }),
|
|
@@ -164,7 +191,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
164
191
|
});
|
|
165
192
|
return;
|
|
166
193
|
}
|
|
167
|
-
(0, auth_js_1.saveTokens)(
|
|
194
|
+
(0, auth_js_1.saveTokens)(TOKEN_STORE_KEY, {
|
|
168
195
|
access_token: tokenData.access_token,
|
|
169
196
|
refresh_token: tokenData.refresh_token ?? "",
|
|
170
197
|
expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
|
|
@@ -219,13 +246,14 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
219
246
|
});
|
|
220
247
|
}
|
|
221
248
|
case "status": {
|
|
222
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
249
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
223
250
|
return {
|
|
224
251
|
content: [
|
|
225
252
|
{
|
|
226
253
|
type: "text",
|
|
227
254
|
text: JSON.stringify({
|
|
228
255
|
remote_url: REMOTE_MCP_URL,
|
|
256
|
+
token_store_key: TOKEN_STORE_KEY,
|
|
229
257
|
authorized: !!tokenResult,
|
|
230
258
|
tools_count: remoteTools.length,
|
|
231
259
|
...(lastFetchError ? { error: lastFetchError } : {}),
|
|
@@ -236,7 +264,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
236
264
|
}
|
|
237
265
|
default: {
|
|
238
266
|
// Check authorization for remote tools — re-validates on every call
|
|
239
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
267
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
240
268
|
if (!tokenResult) {
|
|
241
269
|
const authUrl = generateAuthUrl();
|
|
242
270
|
return {
|
|
@@ -250,6 +278,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
250
278
|
}
|
|
251
279
|
try {
|
|
252
280
|
const result = await callRemoteTool(name, args, tokenResult.access_token);
|
|
281
|
+
if (result.includes("ScoreHub OAuth 授权已过期或无效")) {
|
|
282
|
+
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
283
|
+
const authUrl = generateAuthUrl();
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `⚠️ 授权已失效,已清除本地缓存。请调用 "authorize" 工具重新登录 ScoreHub。\n授权地址: ${authUrl}`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
253
293
|
return { content: [{ type: "text", text: result }] };
|
|
254
294
|
}
|
|
255
295
|
catch (e) {
|
|
@@ -267,7 +307,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
267
307
|
function startTokenKeepAlive() {
|
|
268
308
|
const timer = setInterval(async () => {
|
|
269
309
|
try {
|
|
270
|
-
const result = await (0, auth_js_1.getValidAccessToken)(
|
|
310
|
+
const result = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
271
311
|
if (result?.source === "refresh") {
|
|
272
312
|
console.error("[keep-alive] access token refreshed proactively");
|
|
273
313
|
}
|
|
@@ -291,7 +331,7 @@ async function main() {
|
|
|
291
331
|
process.exit(0);
|
|
292
332
|
});
|
|
293
333
|
try {
|
|
294
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
334
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
295
335
|
remoteTools = await fetchRemoteTools(tokenResult?.access_token);
|
|
296
336
|
if (remoteTools.length === 0 && lastFetchError) {
|
|
297
337
|
console.error(`Warning: Could not fetch remote tools on startup: ${lastFetchError}. Retrying on first ListTools request.`);
|