@scorehub/mcp-server 0.2.1 → 0.2.3

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/dist/auth.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface TokenStore {
13
13
  export interface AuthorizationCodeResult {
14
14
  code: string;
15
15
  state: string;
16
+ port: number;
16
17
  }
17
18
  export interface AuthorizeParams {
18
19
  authEndpoint: string;
package/dist/auth.js CHANGED
@@ -137,8 +137,9 @@ function startCallbackServer(port, timeoutMs = 120_000) {
137
137
  return new Promise((resolve, reject) => {
138
138
  let timeoutId;
139
139
  const server = (0, node_http_2.createServer)((req, res) => {
140
+ const actualPort = server.address()?.port || port;
140
141
  const requestUrl = req.url ?? "/";
141
- const url = new URL(requestUrl, `http://127.0.0.1:${port}`);
142
+ const url = new URL(requestUrl, `http://127.0.0.1:${actualPort}`);
142
143
  const code = url.searchParams.get("code");
143
144
  const state = url.searchParams.get("state");
144
145
  if (code) {
@@ -161,7 +162,7 @@ function startCallbackServer(port, timeoutMs = 120_000) {
161
162
  clearTimeout(timeoutId);
162
163
  server.close();
163
164
  if (code) {
164
- resolve({ code, state: state ?? "" });
165
+ resolve({ code, state: state ?? "", port: actualPort });
165
166
  }
166
167
  else {
167
168
  reject(new Error("No authorization code in callback"));
package/dist/index.js CHANGED
@@ -16,15 +16,16 @@ 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 REMOTE_MCP_URL = process.env.SCOREHUB_REMOTE_MCP_URL || "https://mcp.scorehub.cn";
20
- const TOKEN_ENDPOINT = `${REMOTE_MCP_URL}/token`;
21
- const AUTHORIZE_URL = `${REMOTE_MCP_URL}/authorize`;
19
+ const SCOREHUB_BASE_URL = process.env.SCOREHUB_REMOTE_MCP_URL || "https://mcp.scorehub.cn";
20
+ // OAuth endpoints are at the root path
21
+ const TOKEN_ENDPOINT = `${SCOREHUB_BASE_URL}/token`;
22
+ const AUTHORIZE_URL = `${SCOREHUB_BASE_URL}/authorize`;
23
+ // MCP Streamable HTTP transport endpoints are at /mcp
24
+ const REMOTE_MCP_URL = `${SCOREHUB_BASE_URL}/mcp`;
22
25
  // ── Auth State ──────────────────────────────────────────────
23
26
  const auth_js_1 = require("./auth.js");
27
+ const node_http_1 = require("node:http");
24
28
  const node_child_process_1 = require("node:child_process");
25
- const node_os_1 = require("node:os");
26
- const node_path_1 = require("node:path");
27
- const node_fs_1 = require("node:fs");
28
29
  const HOST = new URL(REMOTE_MCP_URL).host;
29
30
  const CLIENT_ID = "scorehub-mcp";
30
31
  // ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
@@ -44,7 +45,9 @@ async function fetchRemoteTools(token) {
44
45
  }
45
46
  }
46
47
  catch (e) {
47
- console.error("Warning: fetchRemoteTools failed:", e instanceof Error ? e.message : String(e));
48
+ const msg = e instanceof Error ? e.message : String(e);
49
+ lastFetchError = msg;
50
+ console.error("Warning: fetchRemoteTools failed:", msg);
48
51
  return [];
49
52
  }
50
53
  }
@@ -63,22 +66,30 @@ async function callRemoteTool(name, args, token) {
63
66
  }
64
67
  }
65
68
  // ── OAuth authorize helper ──────────────────────────────────
66
- function generateAuthUrl() {
69
+ function generateAuthUrl(port = 19836) {
67
70
  // Generate random state for CSRF protection
68
71
  const state = Math.random().toString(36).substring(2);
69
- return `${AUTHORIZE_URL}?client_id=scorehub-mcp&redirect_uri=http://127.0.0.1:19836/callback&state=${state}`;
72
+ return `${AUTHORIZE_URL}?client_id=scorehub-mcp&redirect_uri=http://127.0.0.1:${port}/callback&state=${state}`;
70
73
  }
71
74
  // ── Fetch remote tools on startup ───────────────────────────
72
75
  let remoteTools = [];
76
+ let lastFetchError = null;
73
77
  // ── MCP Server ──────────────────────────────────────────────
74
78
  const server = new index_js_1.Server({ name: "scorehub-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
75
79
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
76
80
  // Refresh remote tools on each list
77
81
  try {
78
82
  const tokenResult = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
79
- remoteTools = await fetchRemoteTools(tokenResult?.access_token);
83
+ const freshTools = await fetchRemoteTools(tokenResult?.access_token);
84
+ if (freshTools.length > 0 || remoteTools.length === 0) {
85
+ remoteTools = freshTools;
86
+ }
87
+ // On success, clear any previous error
88
+ if (freshTools.length > 0) {
89
+ lastFetchError = null;
90
+ }
80
91
  }
81
- catch { /* keep stale */ }
92
+ catch { /* keep stale tools and lastError */ }
82
93
  const tools = [
83
94
  {
84
95
  name: "authorize",
@@ -112,44 +123,91 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
112
123
  case "authorize": {
113
124
  // Always force fresh OAuth flow when user explicitly authorizes
114
125
  (0, auth_js_1.clearTokens)(HOST);
115
- const authUrl = generateAuthUrl();
116
- const os = process.platform;
117
- const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
118
- // Start callback server BEFORE opening browser
119
- const callbackPromise = (0, auth_js_1.startCallbackServer)(19836);
120
- try {
121
- (0, node_child_process_1.execSync)(`${cmd} "${authUrl}"`);
122
- }
123
- catch { /* headless env: user opens manually */ }
124
- try {
125
- const { code } = await callbackPromise;
126
- const baseUrl = new URL(REMOTE_MCP_URL).origin;
127
- const tokenResp = await fetch(`${baseUrl}/token`, {
128
- method: "POST",
129
- headers: { "Content-Type": "application/json" },
130
- body: JSON.stringify({ grant_type: "authorization_code", code }),
126
+ // Port 0 lets the OS assign a random available port,
127
+ // avoiding conflicts when multiple MCP clients run simultaneously
128
+ let actualPort = 0;
129
+ return new Promise((resolve) => {
130
+ let timeoutId;
131
+ const callbackServer = (0, node_http_1.createServer)((req, res) => {
132
+ if (timeoutId)
133
+ clearTimeout(timeoutId);
134
+ const requestUrl = req.url ?? "/";
135
+ const url = new URL(requestUrl, `http://127.0.0.1:${actualPort}`);
136
+ const code = url.searchParams.get("code");
137
+ if (code) {
138
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
139
+ res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Complete</title></head>" +
140
+ "<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
141
+ "<h2>✅ Authorization Successful</h2>" +
142
+ "<p>You may close this window and return to the terminal.</p>" +
143
+ "</body></html>");
144
+ callbackServer.close();
145
+ const baseUrl = new URL(REMOTE_MCP_URL).origin;
146
+ fetch(`${baseUrl}/token`, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify({ grant_type: "authorization_code", code }),
150
+ })
151
+ .then((r) => r.json())
152
+ .then((tokenData) => {
153
+ if (!tokenData.access_token) {
154
+ resolve({
155
+ content: [{ type: "text", text: `❌ Token 获取失败: ${JSON.stringify(tokenData)}` }],
156
+ });
157
+ return;
158
+ }
159
+ (0, auth_js_1.saveTokens)(HOST, {
160
+ access_token: tokenData.access_token,
161
+ refresh_token: tokenData.refresh_token ?? "",
162
+ expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
163
+ updated_at: new Date().toISOString(),
164
+ });
165
+ resolve({
166
+ content: [{ type: "text", text: "✅ 授权成功!可以开始使用工具了。" }],
167
+ });
168
+ })
169
+ .catch((e) => resolve({
170
+ content: [{ type: "text", text: `❌ Token 获取失败: ${String(e)}` }],
171
+ }));
172
+ }
173
+ else {
174
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
175
+ res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Failed</title></head>" +
176
+ "<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
177
+ "<h2>❌ Authorization Failed</h2>" +
178
+ "<p>No authorization code received from the provider.</p>" +
179
+ "</body></html>");
180
+ callbackServer.close();
181
+ resolve({
182
+ content: [{ type: "text", text: "❌ 授权失败: No authorization code in callback" }],
183
+ });
184
+ }
131
185
  });
132
- const tokenData = await tokenResp.json();
133
- if (!tokenData.access_token) {
134
- return {
135
- content: [{ type: "text", text: `❌ Token 获取失败: ${JSON.stringify(tokenData)}` }],
136
- };
137
- }
138
- (0, auth_js_1.saveTokens)(HOST, {
139
- access_token: tokenData.access_token,
140
- refresh_token: tokenData.refresh_token ?? "",
141
- expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
142
- updated_at: new Date().toISOString(),
186
+ callbackServer.on("error", (err) => {
187
+ if (timeoutId)
188
+ clearTimeout(timeoutId);
189
+ resolve({
190
+ content: [{ type: "text", text: `❌ 授权失败: ${String(err)}` }],
191
+ });
143
192
  });
144
- return {
145
- content: [{ type: "text", text: "✅ 授权成功!可以开始使用工具了。" }],
146
- };
147
- }
148
- catch (e) {
149
- return {
150
- content: [{ type: "text", text: `❌ 授权失败: ${String(e)}` }],
151
- };
152
- }
193
+ callbackServer.listen(0, "127.0.0.1", () => {
194
+ actualPort = callbackServer.address().port;
195
+ callbackServer.unref();
196
+ const authUrl = generateAuthUrl(actualPort);
197
+ const os = process.platform;
198
+ const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
199
+ try {
200
+ (0, node_child_process_1.execSync)(`${cmd} "${authUrl}"`);
201
+ }
202
+ catch { /* headless env: user opens manually */ }
203
+ timeoutId = setTimeout(() => {
204
+ callbackServer.close();
205
+ resolve({
206
+ content: [{ type: "text", text: "❌ 授权超时 (120s)。请重试。" }],
207
+ });
208
+ }, 120_000);
209
+ });
210
+ });
153
211
  }
154
212
  case "status": {
155
213
  const tokenResult = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
@@ -161,6 +219,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
161
219
  remote_url: REMOTE_MCP_URL,
162
220
  authorized: !!tokenResult,
163
221
  tools_count: remoteTools.length,
222
+ ...(lastFetchError ? { error: lastFetchError } : {}),
164
223
  }, null, 2),
165
224
  },
166
225
  ],
@@ -194,32 +253,22 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
194
253
  });
195
254
  // ── Start ───────────────────────────────────────────────────
196
255
  async function main() {
197
- // ── Clean up orphaned previous instance ────────────────────
198
- const pidFile = (0, node_path_1.join)((0, node_os_1.homedir)(), ".scorehub", HOST, "pid");
199
- try {
200
- if ((0, node_fs_1.existsSync)(pidFile)) {
201
- const oldPid = parseInt((0, node_fs_1.readFileSync)(pidFile, "utf-8").trim(), 10);
202
- if (oldPid && oldPid !== process.pid) {
203
- try {
204
- process.kill(oldPid, 0); // check if process exists (cross-platform)
205
- process.kill(oldPid); // SIGTERM on POSIX, TerminateProcess on Windows
206
- }
207
- catch { /* process already dead */ }
208
- }
209
- }
210
- }
211
- catch { /* ignore PID file read errors */ }
212
- // Write current PID
213
- try {
214
- const dir = (0, node_path_1.dirname)(pidFile);
215
- if (!(0, node_fs_1.existsSync)(dir))
216
- (0, node_fs_1.mkdirSync)(dir, { recursive: true });
217
- (0, node_fs_1.writeFileSync)(pidFile, String(process.pid), "utf-8");
218
- }
219
- catch { /* ignore PID file write errors */ }
256
+ // Set process title for easy identification in ps/task manager
257
+ process.title = 'scorehub-mcp';
258
+ // Gracefully exit when parent AI agent closes stdin (normal shutdown or crash).
259
+ // stdio transport ties the child lifecycle to the parent — when the parent exits,
260
+ // the OS closes the pipe, stdin emits 'end', and we exit cleanly.
261
+ // This replaces the previous shared-PID-file approach which killed other instances.
262
+ process.stdin.on('end', () => {
263
+ console.error('ScoreHub MCP Server: stdin closed, exiting');
264
+ process.exit(0);
265
+ });
220
266
  try {
221
267
  const tokenResult = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
222
268
  remoteTools = await fetchRemoteTools(tokenResult?.access_token);
269
+ if (remoteTools.length === 0 && lastFetchError) {
270
+ console.error(`Warning: Could not fetch remote tools on startup: ${lastFetchError}. Retrying on first ListTools request.`);
271
+ }
223
272
  }
224
273
  catch {
225
274
  console.error("Warning: Could not fetch remote tools on startup. 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.1",
3
+ "version": "0.2.3",
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": {