@scorehub/mcp-server 0.2.2 → 0.2.4
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 +1 -0
- package/dist/auth.js +3 -2
- package/dist/index.js +103 -64
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
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:${
|
|
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
|
|
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,98 @@ 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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
const requestUrl = req.url ?? "/";
|
|
133
|
+
const url = new URL(requestUrl, `http://127.0.0.1:${actualPort}`);
|
|
134
|
+
// 只处理 /callback 路径的请求,忽略浏览器探测请求(favicon、安全检查等)
|
|
135
|
+
if (url.pathname !== "/callback") {
|
|
136
|
+
res.writeHead(204);
|
|
137
|
+
res.end();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (timeoutId)
|
|
141
|
+
clearTimeout(timeoutId);
|
|
142
|
+
const code = url.searchParams.get("code");
|
|
143
|
+
if (code) {
|
|
144
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
145
|
+
res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Complete</title></head>" +
|
|
146
|
+
"<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
|
|
147
|
+
"<h2>✅ Authorization Successful</h2>" +
|
|
148
|
+
"<p>You may close this window and return to the terminal.</p>" +
|
|
149
|
+
"</body></html>");
|
|
150
|
+
callbackServer.close();
|
|
151
|
+
const baseUrl = new URL(REMOTE_MCP_URL).origin;
|
|
152
|
+
fetch(`${baseUrl}/token`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "Content-Type": "application/json" },
|
|
155
|
+
body: JSON.stringify({ grant_type: "authorization_code", code }),
|
|
156
|
+
})
|
|
157
|
+
.then((r) => r.json())
|
|
158
|
+
.then((tokenData) => {
|
|
159
|
+
if (!tokenData.access_token) {
|
|
160
|
+
resolve({
|
|
161
|
+
content: [{ type: "text", text: `❌ Token 获取失败: ${JSON.stringify(tokenData)}` }],
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
(0, auth_js_1.saveTokens)(HOST, {
|
|
166
|
+
access_token: tokenData.access_token,
|
|
167
|
+
refresh_token: tokenData.refresh_token ?? "",
|
|
168
|
+
expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
|
|
169
|
+
updated_at: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
resolve({
|
|
172
|
+
content: [{ type: "text", text: "✅ 授权成功!可以开始使用工具了。" }],
|
|
173
|
+
});
|
|
174
|
+
})
|
|
175
|
+
.catch((e) => resolve({
|
|
176
|
+
content: [{ type: "text", text: `❌ Token 获取失败: ${String(e)}` }],
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// 到达 /callback 但没有 code —— 真正的错误
|
|
181
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
182
|
+
res.end("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Authorization Failed</title></head>" +
|
|
183
|
+
"<body style=\"font-family:sans-serif;text-align:center;padding-top:80px;\">" +
|
|
184
|
+
"<h2>❌ Authorization Failed</h2>" +
|
|
185
|
+
"<p>No authorization code received from the provider.</p>" +
|
|
186
|
+
"</body></html>");
|
|
187
|
+
callbackServer.close();
|
|
188
|
+
resolve({
|
|
189
|
+
content: [{ type: "text", text: "❌ 授权失败: No authorization code in callback" }],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
144
192
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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(),
|
|
193
|
+
callbackServer.on("error", (err) => {
|
|
194
|
+
if (timeoutId)
|
|
195
|
+
clearTimeout(timeoutId);
|
|
196
|
+
resolve({
|
|
197
|
+
content: [{ type: "text", text: `❌ 授权失败: ${String(err)}` }],
|
|
198
|
+
});
|
|
156
199
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
200
|
+
callbackServer.listen(0, "127.0.0.1", () => {
|
|
201
|
+
actualPort = callbackServer.address().port;
|
|
202
|
+
callbackServer.unref();
|
|
203
|
+
const authUrl = generateAuthUrl(actualPort);
|
|
204
|
+
const os = process.platform;
|
|
205
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
206
|
+
try {
|
|
207
|
+
(0, node_child_process_1.execSync)(`${cmd} "${authUrl}"`);
|
|
208
|
+
}
|
|
209
|
+
catch { /* headless env: user opens manually */ }
|
|
210
|
+
timeoutId = setTimeout(() => {
|
|
211
|
+
callbackServer.close();
|
|
212
|
+
resolve({
|
|
213
|
+
content: [{ type: "text", text: "❌ 授权超时 (120s)。请重试。" }],
|
|
214
|
+
});
|
|
215
|
+
}, 120_000);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
166
218
|
}
|
|
167
219
|
case "status": {
|
|
168
220
|
const tokenResult = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
|
|
@@ -208,29 +260,16 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
208
260
|
});
|
|
209
261
|
// ── Start ───────────────────────────────────────────────────
|
|
210
262
|
async function main() {
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 */ }
|
|
263
|
+
// Set process title for easy identification in ps/task manager
|
|
264
|
+
process.title = 'scorehub-mcp';
|
|
265
|
+
// Gracefully exit when parent AI agent closes stdin (normal shutdown or crash).
|
|
266
|
+
// stdio transport ties the child lifecycle to the parent — when the parent exits,
|
|
267
|
+
// the OS closes the pipe, stdin emits 'end', and we exit cleanly.
|
|
268
|
+
// This replaces the previous shared-PID-file approach which killed other instances.
|
|
269
|
+
process.stdin.on('end', () => {
|
|
270
|
+
console.error('ScoreHub MCP Server: stdin closed, exiting');
|
|
271
|
+
process.exit(0);
|
|
272
|
+
});
|
|
234
273
|
try {
|
|
235
274
|
const tokenResult = await (0, auth_js_1.getValidAccessToken)(HOST, TOKEN_ENDPOINT, CLIENT_ID);
|
|
236
275
|
remoteTools = await fetchRemoteTools(tokenResult?.access_token);
|