@scorehub/mcp-server 0.2.5 → 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 +27 -14
  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,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 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";
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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;
@@ -124,7 +137,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
124
137
  switch (name) {
125
138
  case "authorize": {
126
139
  // Always force fresh OAuth flow when user explicitly authorizes
127
- (0, auth_js_1.clearTokens)(HOST);
140
+ (0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
128
141
  // Port 0 lets the OS assign a random available port,
129
142
  // avoiding conflicts when multiple MCP clients run simultaneously
130
143
  let actualPort = 0;
@@ -150,8 +163,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
150
163
  "<p>You may close this window and return to the terminal.</p>" +
151
164
  "</body></html>");
152
165
  callbackServer.close();
153
- const baseUrl = new URL(REMOTE_MCP_URL).origin;
154
- fetch(`${baseUrl}/token`, {
166
+ fetch(TOKEN_ENDPOINT, {
155
167
  method: "POST",
156
168
  headers: { "Content-Type": "application/json" },
157
169
  body: JSON.stringify({ grant_type: "authorization_code", code }),
@@ -164,7 +176,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
164
176
  });
165
177
  return;
166
178
  }
167
- (0, auth_js_1.saveTokens)(HOST, {
179
+ (0, auth_js_1.saveTokens)(TOKEN_STORE_KEY, {
168
180
  access_token: tokenData.access_token,
169
181
  refresh_token: tokenData.refresh_token ?? "",
170
182
  expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
@@ -219,13 +231,14 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
219
231
  });
220
232
  }
221
233
  case "status": {
222
- 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);
223
235
  return {
224
236
  content: [
225
237
  {
226
238
  type: "text",
227
239
  text: JSON.stringify({
228
240
  remote_url: REMOTE_MCP_URL,
241
+ token_store_key: TOKEN_STORE_KEY,
229
242
  authorized: !!tokenResult,
230
243
  tools_count: remoteTools.length,
231
244
  ...(lastFetchError ? { error: lastFetchError } : {}),
@@ -236,7 +249,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
236
249
  }
237
250
  default: {
238
251
  // Check authorization for remote tools — re-validates on every call
239
- 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);
240
253
  if (!tokenResult) {
241
254
  const authUrl = generateAuthUrl();
242
255
  return {
@@ -267,7 +280,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
267
280
  function startTokenKeepAlive() {
268
281
  const timer = setInterval(async () => {
269
282
  try {
270
- const result = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
283
+ const result = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
271
284
  if (result?.source === "refresh") {
272
285
  console.error("[keep-alive] access token refreshed proactively");
273
286
  }
@@ -291,7 +304,7 @@ async function main() {
291
304
  process.exit(0);
292
305
  });
293
306
  try {
294
- 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);
295
308
  remoteTools = await fetchRemoteTools(tokenResult?.access_token);
296
309
  if (remoteTools.length === 0 && lastFetchError) {
297
310
  console.error(`Warning: Could not fetch remote tools on startup: ${lastFetchError}. Retrying on first ListTools request.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorehub/mcp-server",
3
- "version": "0.2.5",
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": {