@scorehub/mcp-server 0.2.2 → 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
@@ -24,10 +24,8 @@ const AUTHORIZE_URL = `${SCOREHUB_BASE_URL}/authorize`;
24
24
  const REMOTE_MCP_URL = `${SCOREHUB_BASE_URL}/mcp`;
25
25
  // ── Auth State ──────────────────────────────────────────────
26
26
  const auth_js_1 = require("./auth.js");
27
+ const node_http_1 = require("node:http");
27
28
  const node_child_process_1 = require("node:child_process");
28
- const node_os_1 = require("node:os");
29
- const node_path_1 = require("node:path");
30
- const node_fs_1 = require("node:fs");
31
29
  const HOST = new URL(REMOTE_MCP_URL).host;
32
30
  const CLIENT_ID = "scorehub-mcp";
33
31
  // ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
@@ -68,10 +66,10 @@ async function callRemoteTool(name, args, token) {
68
66
  }
69
67
  }
70
68
  // ── OAuth authorize helper ──────────────────────────────────
71
- function generateAuthUrl() {
69
+ function generateAuthUrl(port = 19836) {
72
70
  // Generate random state for CSRF protection
73
71
  const state = Math.random().toString(36).substring(2);
74
- 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}`;
75
73
  }
76
74
  // ── Fetch remote tools on startup ───────────────────────────
77
75
  let remoteTools = [];
@@ -125,44 +123,91 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
125
123
  case "authorize": {
126
124
  // Always force fresh OAuth flow when user explicitly authorizes
127
125
  (0, auth_js_1.clearTokens)(HOST);
128
- const authUrl = generateAuthUrl();
129
- const os = process.platform;
130
- const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
131
- // Start callback server BEFORE opening browser
132
- const callbackPromise = (0, auth_js_1.startCallbackServer)(19836);
133
- try {
134
- (0, node_child_process_1.execSync)(`${cmd} "${authUrl}"`);
135
- }
136
- catch { /* headless env: user opens manually */ }
137
- try {
138
- const { code } = await callbackPromise;
139
- const baseUrl = new URL(REMOTE_MCP_URL).origin;
140
- const tokenResp = await fetch(`${baseUrl}/token`, {
141
- method: "POST",
142
- headers: { "Content-Type": "application/json" },
143
- 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
+ }
144
185
  });
145
- const tokenData = await tokenResp.json();
146
- if (!tokenData.access_token) {
147
- return {
148
- content: [{ type: "text", text: `❌ Token 获取失败: ${JSON.stringify(tokenData)}` }],
149
- };
150
- }
151
- (0, auth_js_1.saveTokens)(HOST, {
152
- access_token: tokenData.access_token,
153
- refresh_token: tokenData.refresh_token ?? "",
154
- expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
155
- 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
+ });
156
192
  });
157
- return {
158
- content: [{ type: "text", text: "✅ 授权成功!可以开始使用工具了。" }],
159
- };
160
- }
161
- catch (e) {
162
- return {
163
- content: [{ type: "text", text: `❌ 授权失败: ${String(e)}` }],
164
- };
165
- }
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
+ });
166
211
  }
167
212
  case "status": {
168
213
  const tokenResult = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
@@ -208,29 +253,16 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
208
253
  });
209
254
  // ── Start ───────────────────────────────────────────────────
210
255
  async function main() {
211
- // ── Clean up orphaned previous instance ────────────────────
212
- const pidFile = (0, node_path_1.join)((0, node_os_1.homedir)(), ".scorehub", HOST, "pid");
213
- try {
214
- if ((0, node_fs_1.existsSync)(pidFile)) {
215
- const oldPid = parseInt((0, node_fs_1.readFileSync)(pidFile, "utf-8").trim(), 10);
216
- if (oldPid && oldPid !== process.pid) {
217
- try {
218
- process.kill(oldPid, 0); // check if process exists (cross-platform)
219
- process.kill(oldPid); // SIGTERM on POSIX, TerminateProcess on Windows
220
- }
221
- catch { /* process already dead */ }
222
- }
223
- }
224
- }
225
- catch { /* ignore PID file read errors */ }
226
- // Write current PID
227
- try {
228
- const dir = (0, node_path_1.dirname)(pidFile);
229
- if (!(0, node_fs_1.existsSync)(dir))
230
- (0, node_fs_1.mkdirSync)(dir, { recursive: true });
231
- (0, node_fs_1.writeFileSync)(pidFile, String(process.pid), "utf-8");
232
- }
233
- 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
+ });
234
266
  try {
235
267
  const tokenResult = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
236
268
  remoteTools = await fetchRemoteTools(tokenResult?.access_token);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorehub/mcp-server",
3
- "version": "0.2.2",
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": {