@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.
Files changed (3) hide show
  1. package/README.md +30 -11
  2. package/dist/index.js +47 -13
  3. 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://mcp.scorehub.cn"
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://mcp.scorehub.cn"
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://mcp.scorehub.cn` | Base URL of the remote ScoreHub MCP endpoint |
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/<host>/oauth.json
101
+ ~/.scorehub/<endpoint>/oauth.json
102
102
  ```
103
103
 
104
- The `<host>` is derived from the `SCOREHUB_REMOTE_MCP_URL` (e.g., `mcp.scorehub.cn`). This means you can run staging and production side-by-side by pointing each configuration at a different URL — tokens are isolated per host.
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
- ├─ saves to ~/.scorehub/<host>/oauth.json
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/<host>/oauth.json` or the entire `~/.scorehub/` directory |
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/<host>/oauth.json │
211
+ │ ~/.scorehub/<endpoint>/oauth.json │
193
212
  └────────────────────────────────────────┼──────────────────────────┘
194
213
  │ Streamable HTTP
195
214
  ▼
196
215
  ┌─────────────────────┐
197
216
  │ ScoreHub Remote MCP │
198
- │ (mcp.scorehub.cn) │
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/mcp.scorehub.cn/oauth.json
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 host, so staging and production credentials won't interfere with each other.
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 SCOREHUB_BASE_URL = process.env.SCOREHUB_REMOTE_MCP_URL || "https://mcp.scorehub.cn";
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 = `${SCOREHUB_BASE_URL}/token`;
22
- const AUTHORIZE_URL = `${SCOREHUB_BASE_URL}/authorize`;
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 = `${SCOREHUB_BASE_URL}/mcp`;
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 HOST = new URL(REMOTE_MCP_URL).host;
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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)(HOST);
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
- const baseUrl = new URL(REMOTE_MCP_URL).origin;
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)(HOST, {
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorehub/mcp-server",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "ScoreHub local MCP proxy — OAuth login, token caching, and tool forwarding to remote MCP endpoint",
5
5
  "main": "dist/index.js",
6
6
  "bin": {