@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.
- package/README.md +30 -11
- package/dist/index.js +27 -14
- 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,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;
|
|
@@ -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)(
|
|
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
|
-
|
|
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)(
|
|
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)(
|
|
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)(
|
|
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)(
|
|
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)(
|
|
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.`);
|