@scorehub/mcp-server 0.2.6 → 0.2.8

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
@@ -116,6 +116,8 @@ The `<endpoint>` is derived from the `SCOREHUB_REMOTE_MCP_URL` host and path (fo
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
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
@@ -43,6 +43,53 @@ const TOKEN_STORE_KEY = endpointTokenKey(SCOREHUB_BASE_URL);
43
43
  const CLIENT_ID = "scorehub-mcp";
44
44
  // Proactively refresh token every 4 hours to prevent expiry during long sessions
45
45
  const TOKEN_KEEPALIVE_INTERVAL = 4 * 60 * 60 * 1000;
46
+ function isRemoteToolErrorType(value) {
47
+ return value === "oauth_invalid"
48
+ || value === "shop_auth_invalid"
49
+ || value === "rate_limited"
50
+ || value === "quota_exhausted"
51
+ || value === "invalid_input"
52
+ || value === "permission_denied"
53
+ || value === "upstream_error";
54
+ }
55
+ function parseRemoteToolEnvelope(text) {
56
+ try {
57
+ const parsed = JSON.parse(text);
58
+ if (parsed.ok === true && Object.prototype.hasOwnProperty.call(parsed, "data")) {
59
+ return { ok: true, data: parsed.data };
60
+ }
61
+ if (parsed.ok === false
62
+ && isRemoteToolErrorType(parsed.error_type)
63
+ && typeof parsed.user_message === "string"
64
+ && typeof parsed.retryable === "boolean") {
65
+ return {
66
+ ok: false,
67
+ error_type: parsed.error_type,
68
+ user_message: parsed.user_message,
69
+ retryable: parsed.retryable,
70
+ ...(Object.prototype.hasOwnProperty.call(parsed, "upstream") ? { upstream: parsed.upstream } : {}),
71
+ };
72
+ }
73
+ return null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ function extractRemoteTextContent(content) {
80
+ if (!Array.isArray(content)) {
81
+ return [];
82
+ }
83
+ return content.flatMap((item) => {
84
+ if (item
85
+ && typeof item === "object"
86
+ && item.type === "text"
87
+ && typeof item.text === "string") {
88
+ return [item.text];
89
+ }
90
+ return [];
91
+ });
92
+ }
46
93
  // ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
47
94
  async function fetchRemoteTools(token) {
48
95
  try {
@@ -74,7 +121,15 @@ async function callRemoteTool(name, args, token) {
74
121
  try {
75
122
  await client.connect(transport);
76
123
  const result = await client.callTool({ name, arguments: args });
77
- return JSON.stringify(result.content, null, 2);
124
+ const textBlocks = extractRemoteTextContent(result.content);
125
+ const rawText = textBlocks.length > 0
126
+ ? textBlocks.join("\n\n")
127
+ : JSON.stringify(result.content, null, 2);
128
+ const envelope = textBlocks
129
+ .map((text) => parseRemoteToolEnvelope(text))
130
+ .find((candidate) => candidate !== null)
131
+ ?? parseRemoteToolEnvelope(rawText);
132
+ return { rawText, envelope };
78
133
  }
79
134
  finally {
80
135
  await client.close();
@@ -108,10 +163,15 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
108
163
  const tools = [
109
164
  {
110
165
  name: "authorize",
111
- description: "通过浏览器 OAuth 登录 ScoreHub。首次连接时自动调用,打开浏览器引导登录。已登录时可直接使用其他工具。",
166
+ description: "检查 ScoreHub OAuth 授权。已有有效授权时直接返回;仅在传入 force=true 时重新打开浏览器登录。",
112
167
  inputSchema: {
113
168
  type: "object",
114
- properties: {},
169
+ properties: {
170
+ force: {
171
+ type: "boolean",
172
+ description: "是否强制清除当前端点的本地 token 并重新登录",
173
+ },
174
+ },
115
175
  required: [],
116
176
  },
117
177
  },
@@ -136,8 +196,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
136
196
  const { name, arguments: args = {} } = request.params;
137
197
  switch (name) {
138
198
  case "authorize": {
139
- // Always force fresh OAuth flow when user explicitly authorizes
140
- (0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
199
+ const force = args.force === true;
200
+ if (!force) {
201
+ const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
202
+ if (tokenResult) {
203
+ return {
204
+ content: [{ type: "text", text: `✅ 已授权(${tokenResult.source === "cache" ? "使用本地缓存" : "已自动刷新"}),可以直接使用 ScoreHub 工具。` }],
205
+ };
206
+ }
207
+ }
208
+ else {
209
+ (0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
210
+ }
141
211
  // Port 0 lets the OS assign a random available port,
142
212
  // avoiding conflicts when multiple MCP clients run simultaneously
143
213
  let actualPort = 0;
@@ -262,8 +332,32 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
262
332
  };
263
333
  }
264
334
  try {
265
- const result = await callRemoteTool(name, args, tokenResult.access_token);
266
- return { content: [{ type: "text", text: result }] };
335
+ const remoteResult = await callRemoteTool(name, args, tokenResult.access_token);
336
+ if (remoteResult.envelope?.ok === false && remoteResult.envelope.error_type === "oauth_invalid") {
337
+ (0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
338
+ const authUrl = generateAuthUrl();
339
+ return {
340
+ content: [
341
+ {
342
+ type: "text",
343
+ text: `⚠️ 授权已失效,已清除本地缓存。请调用 "authorize" 工具重新登录 ScoreHub。\n授权地址: ${authUrl}`,
344
+ },
345
+ ],
346
+ };
347
+ }
348
+ if (remoteResult.rawText.includes("ScoreHub OAuth 授权已过期或无效")) {
349
+ (0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
350
+ const authUrl = generateAuthUrl();
351
+ return {
352
+ content: [
353
+ {
354
+ type: "text",
355
+ text: `⚠️ 授权已失效,已清除本地缓存。请调用 "authorize" 工具重新登录 ScoreHub。\n授权地址: ${authUrl}`,
356
+ },
357
+ ],
358
+ };
359
+ }
360
+ return { content: [{ type: "text", text: remoteResult.rawText }] };
267
361
  }
268
362
  catch (e) {
269
363
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorehub/mcp-server",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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": {