@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 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
 
@@ -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` | Opens your browser for ScoreHub OAuth login. Forces a fresh authorization flow every time. |
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
- ├─ saves to ~/.scorehub/<host>/oauth.json
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/<host>/oauth.json` or the entire `~/.scorehub/` directory |
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/<host>/oauth.json │
213
+ │ ~/.scorehub/<endpoint>/oauth.json │
193
214
  └────────────────────────────────────────┼──────────────────────────┘
194
215
  │ Streamable HTTP
195
216
  ▼
196
217
  ┌─────────────────────┐
197
218
  │ ScoreHub Remote MCP │
198
- │ (mcp.scorehub.cn) │
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/mcp.scorehub.cn/oauth.json
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 host, so staging and production credentials won't interfere with each other.
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 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;
@@ -95,10 +108,15 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
95
108
  const tools = [
96
109
  {
97
110
  name: "authorize",
98
- description: "通过浏览器 OAuth 登录 ScoreHub。首次连接时自动调用,打开浏览器引导登录。已登录时可直接使用其他工具。",
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
- // Always force fresh OAuth flow when user explicitly authorizes
127
- (0, auth_js_1.clearTokens)(HOST);
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
- const baseUrl = new URL(REMOTE_MCP_URL).origin;
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)(HOST, {
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
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.`);
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.7",
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": {