@scorehub/mcp-server 0.2.4 → 0.2.6
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 +30 -11
- package/dist/index.js +47 -13
- 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
|
|
|
@@ -158,7 +158,7 @@ These tools are **dynamically loaded** from the remote MCP endpoint at startup a
|
|
|
158
158
|
├─ POST /token ───────────────────▶│
|
|
159
159
|
│◀── access_token + refresh_token │
|
|
160
160
|
│ │
|
|
161
|
-
|
|
161
|
+
├─ saves to ~/.scorehub/<endpoint>/oauth.json
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
### Token lifecycle
|
|
@@ -169,12 +169,31 @@ These tools are **dynamically loaded** from the remote MCP endpoint at startup a
|
|
|
169
169
|
| **Subsequent calls** | Read cached file → check expiry with 5-minute buffer |
|
|
170
170
|
| **Near expiry** | Auto-refresh with `refresh_token` → write new token back to file |
|
|
171
171
|
| **Refresh fails** | Returns an error message with the authorization URL — call `authorize` again |
|
|
172
|
-
| **Manual logout** | Delete `~/.scorehub/<
|
|
172
|
+
| **Manual logout** | Delete `~/.scorehub/<endpoint>/oauth.json` or the entire `~/.scorehub/` directory |
|
|
173
173
|
|
|
174
174
|
### Headless environments
|
|
175
175
|
|
|
176
176
|
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
177
|
|
|
178
|
+
### Local remote development
|
|
179
|
+
|
|
180
|
+
When testing a local `mcp-remote`, point this local proxy at the same local base URL:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
SCOREHUB_REMOTE_MCP_URL=http://127.0.0.1:5201 npm start -w packages/mcp-server
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Start `mcp-remote` with a matching `BASE_URL`:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
cd packages/mcp-remote
|
|
190
|
+
PORT=5201 BASE_URL=http://127.0.0.1:5201 npm start
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`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.
|
|
194
|
+
|
|
195
|
+
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`.
|
|
196
|
+
|
|
178
197
|
## Architecture
|
|
179
198
|
|
|
180
199
|
```
|
|
@@ -189,13 +208,13 @@ If you're running on a server or in a headless environment where a browser canno
|
|
|
189
208
|
│ │ client) │ │ • Tool forwarding │ │
|
|
190
209
|
│ └──────────────┘ └─────────┬───────────┘ │
|
|
191
210
|
│ │ │
|
|
192
|
-
│ ~/.scorehub/<
|
|
211
|
+
│ ~/.scorehub/<endpoint>/oauth.json │
|
|
193
212
|
└────────────────────────────────────────┼──────────────────────────┘
|
|
194
213
|
│ Streamable HTTP
|
|
195
214
|
▼
|
|
196
215
|
┌─────────────────────┐
|
|
197
216
|
│ ScoreHub Remote MCP │
|
|
198
|
-
│ (
|
|
217
|
+
│ (app.scorehub.cn/mcp)│
|
|
199
218
|
└─────────┬───────────┘
|
|
200
219
|
│
|
|
201
220
|
▼
|
|
@@ -227,7 +246,7 @@ Yes, absolutely. `@scorehub/mcp-server` is a standalone MCP server. Any MCP-comp
|
|
|
227
246
|
Delete the token file:
|
|
228
247
|
|
|
229
248
|
```bash
|
|
230
|
-
rm ~/.scorehub/
|
|
249
|
+
rm ~/.scorehub/app.scorehub.cn_mcp/oauth.json
|
|
231
250
|
```
|
|
232
251
|
|
|
233
252
|
Or delete the entire directory to clear all hosts:
|
|
@@ -238,7 +257,7 @@ rm -rf ~/.scorehub/
|
|
|
238
257
|
|
|
239
258
|
### How do I switch between staging and production?
|
|
240
259
|
|
|
241
|
-
Set different `SCOREHUB_REMOTE_MCP_URL` values in separate MCP client configurations. Tokens are isolated per
|
|
260
|
+
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
261
|
|
|
243
262
|
### What happens if the remote MCP is unreachable?
|
|
244
263
|
|
package/dist/index.js
CHANGED
|
@@ -16,18 +16,33 @@ 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";
|
|
44
|
+
// Proactively refresh token every 4 hours to prevent expiry during long sessions
|
|
45
|
+
const TOKEN_KEEPALIVE_INTERVAL = 4 * 60 * 60 * 1000;
|
|
31
46
|
// ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
|
|
32
47
|
async function fetchRemoteTools(token) {
|
|
33
48
|
try {
|
|
@@ -79,7 +94,7 @@ const server = new index_js_1.Server({ name: "scorehub-mcp", version: "0.1.0" },
|
|
|
79
94
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
80
95
|
// Refresh remote tools on each list
|
|
81
96
|
try {
|
|
82
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
97
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
83
98
|
const freshTools = await fetchRemoteTools(tokenResult?.access_token);
|
|
84
99
|
if (freshTools.length > 0 || remoteTools.length === 0) {
|
|
85
100
|
remoteTools = freshTools;
|
|
@@ -122,7 +137,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
122
137
|
switch (name) {
|
|
123
138
|
case "authorize": {
|
|
124
139
|
// Always force fresh OAuth flow when user explicitly authorizes
|
|
125
|
-
(0, auth_js_1.clearTokens)(
|
|
140
|
+
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
126
141
|
// Port 0 lets the OS assign a random available port,
|
|
127
142
|
// avoiding conflicts when multiple MCP clients run simultaneously
|
|
128
143
|
let actualPort = 0;
|
|
@@ -148,8 +163,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
148
163
|
"<p>You may close this window and return to the terminal.</p>" +
|
|
149
164
|
"</body></html>");
|
|
150
165
|
callbackServer.close();
|
|
151
|
-
|
|
152
|
-
fetch(`${baseUrl}/token`, {
|
|
166
|
+
fetch(TOKEN_ENDPOINT, {
|
|
153
167
|
method: "POST",
|
|
154
168
|
headers: { "Content-Type": "application/json" },
|
|
155
169
|
body: JSON.stringify({ grant_type: "authorization_code", code }),
|
|
@@ -162,7 +176,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
162
176
|
});
|
|
163
177
|
return;
|
|
164
178
|
}
|
|
165
|
-
(0, auth_js_1.saveTokens)(
|
|
179
|
+
(0, auth_js_1.saveTokens)(TOKEN_STORE_KEY, {
|
|
166
180
|
access_token: tokenData.access_token,
|
|
167
181
|
refresh_token: tokenData.refresh_token ?? "",
|
|
168
182
|
expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
|
|
@@ -217,13 +231,14 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
217
231
|
});
|
|
218
232
|
}
|
|
219
233
|
case "status": {
|
|
220
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
234
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
221
235
|
return {
|
|
222
236
|
content: [
|
|
223
237
|
{
|
|
224
238
|
type: "text",
|
|
225
239
|
text: JSON.stringify({
|
|
226
240
|
remote_url: REMOTE_MCP_URL,
|
|
241
|
+
token_store_key: TOKEN_STORE_KEY,
|
|
227
242
|
authorized: !!tokenResult,
|
|
228
243
|
tools_count: remoteTools.length,
|
|
229
244
|
...(lastFetchError ? { error: lastFetchError } : {}),
|
|
@@ -234,7 +249,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
234
249
|
}
|
|
235
250
|
default: {
|
|
236
251
|
// Check authorization for remote tools — re-validates on every call
|
|
237
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
252
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
238
253
|
if (!tokenResult) {
|
|
239
254
|
const authUrl = generateAuthUrl();
|
|
240
255
|
return {
|
|
@@ -258,6 +273,24 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
275
|
});
|
|
276
|
+
// ── Token Keep-Alive ────────────────────────────────────────
|
|
277
|
+
// Proactively refresh the access token on a fixed interval so long-running
|
|
278
|
+
// sessions never hit an expiry mid-call. Uses timer.unref() so the keep-alive
|
|
279
|
+
// loop does not prevent the process from exiting normally.
|
|
280
|
+
function startTokenKeepAlive() {
|
|
281
|
+
const timer = setInterval(async () => {
|
|
282
|
+
try {
|
|
283
|
+
const result = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
284
|
+
if (result?.source === "refresh") {
|
|
285
|
+
console.error("[keep-alive] access token refreshed proactively");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Non-fatal: will retry on next interval
|
|
290
|
+
}
|
|
291
|
+
}, TOKEN_KEEPALIVE_INTERVAL);
|
|
292
|
+
timer.unref(); // Don't block process exit when no other work remains
|
|
293
|
+
}
|
|
261
294
|
// ── Start ───────────────────────────────────────────────────
|
|
262
295
|
async function main() {
|
|
263
296
|
// Set process title for easy identification in ps/task manager
|
|
@@ -271,7 +304,7 @@ async function main() {
|
|
|
271
304
|
process.exit(0);
|
|
272
305
|
});
|
|
273
306
|
try {
|
|
274
|
-
const tokenResult = await (0, auth_js_1.getValidAccessToken)(
|
|
307
|
+
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
275
308
|
remoteTools = await fetchRemoteTools(tokenResult?.access_token);
|
|
276
309
|
if (remoteTools.length === 0 && lastFetchError) {
|
|
277
310
|
console.error(`Warning: Could not fetch remote tools on startup: ${lastFetchError}. Retrying on first ListTools request.`);
|
|
@@ -283,6 +316,7 @@ async function main() {
|
|
|
283
316
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
284
317
|
await server.connect(transport);
|
|
285
318
|
console.error(`ScoreHub MCP Server started (remote: ${REMOTE_MCP_URL})`);
|
|
319
|
+
startTokenKeepAlive();
|
|
286
320
|
}
|
|
287
321
|
main().catch((err) => {
|
|
288
322
|
console.error("Fatal:", err);
|