@scorehub/mcp-server 0.2.13 → 0.3.0

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.en.md CHANGED
@@ -120,9 +120,9 @@ OAuth tokens are stored under the current user's home directory:
120
120
 
121
121
  Tokens persist in a local file. When calling the default ScoreHub production endpoint, mcp-server sends the access token to the remote MCP server in an HTTPS `Authorization` header. Do not share or commit files under `~/.scorehub/`.
122
122
 
123
- To sign out or switch accounts, delete the directory for the relevant endpoint, or all of `~/.scorehub/`, then invoke `authorize` again. On Windows, delete the directory in File Explorer; on macOS and Linux, you can also delete it from a terminal.
123
+ To sign out, delete the directory for the relevant endpoint, or all of `~/.scorehub/`, then invoke `authorize` again. On Windows, delete the directory in File Explorer; on macOS and Linux, you can also delete it from a terminal.
124
124
 
125
- By default, `authorize` reuses a valid access token or refresh token and does not open a browser again. Only an explicit `force: true` clears the cached token for the current endpoint and starts a new login.
125
+ By default, `authorize` reuses a valid access token or refresh token and does not open a browser again. Pass `force: true` to clear the current cache and log in as another account. Pass `switch_shop: true` to open the current account's shop picker; confirming a shop replaces the local token without another ScoreHub login. The arguments are mutually exclusive, and a cancelled or failed shop switch keeps the original shop token.
126
126
 
127
127
  ## Available Tools
128
128
 
@@ -130,8 +130,8 @@ By default, `authorize` reuses a valid access token or refresh token and does no
130
130
 
131
131
  | Tool | Description |
132
132
  |---|---|
133
- | `authorize` | Checks the ScoreHub OAuth state. Reuses the cache by default; passing `force: true` opens the browser for a new login. |
134
- | `status` | Returns the remote address, authorization state, and current number of available tools. |
133
+ | `authorize` | Checks ScoreHub OAuth. Use `force: true` to log in as another account or `switch_shop: true` to open the current account's shop picker. |
134
+ | `status` | Returns the remote address, authorization state, current account/shop, and available tool count. |
135
135
 
136
136
  ### Remote Tools
137
137
 
package/README.md CHANGED
@@ -12,7 +12,7 @@ English version: [README.en.md](./README.en.md)
12
12
 
13
13
  `@scorehub/mcp-server` 运行在你的电脑上,位于 Claude Code、Claude Desktop、WorkBuddy 等 MCP 客户端与 ScoreHub 远程 MCP 之间,负责:
14
14
 
15
- - **OAuth 登录**:首次需要授权时打开浏览器,登录 ScoreHub 后将 Token 缓存到本机。
15
+ - **账号与店铺切换**:`authorize(force=true)` 可重新登录切换账号;`authorize(switch_shop=true)` 可在当前账号下切换店铺,无需重新登录。
16
16
  - **Token 生命周期**:调用前检查 Token 有效期;距过期不足 5 分钟时自动刷新,并为长会话每 4 小时主动保活一次。
17
17
  - **工具转发**:通过 Streamable HTTP 将 TikTok Shop 达人建联相关工具请求转发到 ScoreHub 远程 MCP。
18
18
 
@@ -120,9 +120,9 @@ OAuth Token 保存在当前用户目录下:
120
120
 
121
121
  Token 持久化在本机文件中;调用默认 ScoreHub 生产端点时,mcp-server 会通过 HTTPS 的 `Authorization` 请求头将访问 Token 提供给远程 MCP。不要共享或提交 `~/.scorehub/` 下的文件。
122
122
 
123
- 如需退出登录或切换账号,删除对应端点目录或整个 `~/.scorehub/` 目录,然后重新调用 `authorize`。在 Windows 资源管理器中删除该目录即可;macOS/Linux 也可用终端删除。
123
+ 如需退出登录,删除对应端点目录或整个 `~/.scorehub/` 目录,然后重新调用 `authorize`。在 Windows 资源管理器中删除该目录即可;macOS/Linux 也可用终端删除。
124
124
 
125
- `authorize` 默认复用有效的 access token 或 refresh token,不会重复打开浏览器。只有显式传入 `force: true` 时,才会清除当前端点缓存并要求重新登录。
125
+ `authorize` 默认复用有效的 access token 或 refresh token,不会重复打开浏览器。传入 `force: true` 会清除当前端点缓存并要求重新登录,可用于切换账号;传入 `switch_shop: true` 会打开当前账号的店铺选择页,确认后直接替换本地 token,不要求再次登录。两个参数不能同时使用;店铺切换取消或失败时会保留原店铺 token。
126
126
 
127
127
  ## 可用工具
128
128
 
@@ -130,8 +130,8 @@ Token 持久化在本机文件中;调用默认 ScoreHub 生产端点时,mcp-
130
130
 
131
131
  | 工具 | 说明 |
132
132
  |---|---|
133
- | `authorize` | 检查 ScoreHub OAuth 状态;默认复用缓存,传入 `force: true` 才重新打开浏览器登录。 |
134
- | `status` | 返回远程地址、授权状态和当前可用工具数量。 |
133
+ | `authorize` | 检查 ScoreHub OAuth 状态;`force: true` 重新登录以切换账号,`switch_shop: true` 打开当前账号的店铺选择页。 |
134
+ | `status` | 返回远程地址、授权状态、当前账号/店铺和当前可用工具数量。 |
135
135
 
136
136
  ### 远程工具
137
137
 
package/dist/auth.d.ts CHANGED
@@ -3,12 +3,26 @@ export interface OAuthTokens {
3
3
  refresh_token: string;
4
4
  expires_in: number;
5
5
  scope?: string;
6
+ account?: OAuthAccount;
7
+ selected_shop?: OAuthShop;
8
+ }
9
+ export interface OAuthAccount {
10
+ id: string;
11
+ email: string;
12
+ }
13
+ export interface OAuthShop {
14
+ id: string;
15
+ name: string;
16
+ code: string;
17
+ region: string;
6
18
  }
7
19
  export interface TokenStore {
8
20
  access_token: string;
9
21
  refresh_token: string;
10
22
  expires_at: number;
11
23
  updated_at: string;
24
+ account?: OAuthAccount;
25
+ selected_shop?: OAuthShop;
12
26
  }
13
27
  export interface AuthorizeParams {
14
28
  authEndpoint: string;
@@ -21,12 +35,24 @@ export interface AuthorizeParams {
21
35
  host?: string;
22
36
  callbackHost?: string;
23
37
  callbackPath?: string;
38
+ preserveExistingTokensOnFailure?: boolean;
39
+ authorizationUrlFactory?: (request: AuthorizationRequest) => Promise<string>;
40
+ }
41
+ export interface AuthorizationRequest {
42
+ clientId: string;
43
+ redirectUri: string;
44
+ state: string;
45
+ codeChallenge: string;
46
+ codeChallengeMethod: "S256";
47
+ scope?: string;
24
48
  }
25
49
  export interface AuthorizeResult {
26
50
  accessToken: string;
27
51
  refreshToken: string;
28
52
  expiresIn: number;
29
53
  scope?: string;
54
+ account?: OAuthAccount;
55
+ selectedShop?: OAuthShop;
30
56
  }
31
57
  export declare function loadTokens(host: string): TokenStore | null;
32
58
  export declare function saveTokens(host: string, tokens: TokenStore): void;
package/dist/auth.js CHANGED
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.loadTokens = loadTokens;
4
7
  exports.saveTokens = saveTokens;
@@ -17,7 +20,7 @@ const node_fs_1 = require("node:fs");
17
20
  const node_path_1 = require("node:path");
18
21
  const node_os_1 = require("node:os");
19
22
  const node_crypto_1 = require("node:crypto");
20
- const node_child_process_1 = require("node:child_process");
23
+ const open_1 = __importDefault(require("open"));
21
24
  const callback_pages_js_1 = require("./pages/callback-pages.js");
22
25
  class CallbackError extends Error {
23
26
  page;
@@ -93,16 +96,48 @@ function parseOAuthTokens(payload) {
93
96
  if (typeof record.access_token === "string"
94
97
  && typeof record.refresh_token === "string"
95
98
  && typeof record.expires_in === "number") {
99
+ const account = parseOAuthAccount(record.account);
100
+ const selectedShop = parseOAuthShop(record.selected_shop);
96
101
  return {
97
102
  access_token: record.access_token,
98
103
  refresh_token: record.refresh_token,
99
104
  expires_in: record.expires_in,
100
105
  ...(typeof record.scope === "string" ? { scope: record.scope } : {}),
106
+ ...(account ? { account } : {}),
107
+ ...(selectedShop ? { selected_shop: selectedShop } : {}),
101
108
  };
102
109
  }
103
110
  }
104
111
  throw new Error(getTokenErrorMessage(payload));
105
112
  }
113
+ function parseOAuthAccount(value) {
114
+ if (value && typeof value === "object"
115
+ && typeof value.id === "string"
116
+ && typeof value.email === "string") {
117
+ return value;
118
+ }
119
+ return undefined;
120
+ }
121
+ function parseOAuthShop(value) {
122
+ if (value && typeof value === "object"
123
+ && typeof value.id === "string"
124
+ && typeof value.name === "string"
125
+ && typeof value.code === "string"
126
+ && typeof value.region === "string") {
127
+ return value;
128
+ }
129
+ return undefined;
130
+ }
131
+ function tokenStoreFromOAuthTokens(tokens, previous) {
132
+ return {
133
+ access_token: tokens.access_token,
134
+ refresh_token: tokens.refresh_token ?? previous?.refresh_token ?? "",
135
+ expires_at: Date.now() + (tokens.expires_in ?? 3600) * 1000,
136
+ updated_at: new Date().toISOString(),
137
+ ...(tokens.account ? { account: tokens.account } : previous?.account ? { account: previous.account } : {}),
138
+ ...(tokens.selected_shop ? { selected_shop: tokens.selected_shop } : previous?.selected_shop ? { selected_shop: previous.selected_shop } : {}),
139
+ };
140
+ }
106
141
  function createCallbackPageModel(input) {
107
142
  return {
108
143
  tone: input.tone,
@@ -177,12 +212,7 @@ async function getValidAccessToken(host, tokenEndpoint, clientId) {
177
212
  try {
178
213
  const result = await refreshAccessToken(tokenEndpoint, cached.refresh_token, clientId);
179
214
  if (result.access_token) {
180
- saveTokens(host, {
181
- access_token: result.access_token,
182
- refresh_token: result.refresh_token ?? cached.refresh_token,
183
- expires_at: Date.now() + (result.expires_in ?? 3600) * 1000,
184
- updated_at: new Date().toISOString(),
185
- });
215
+ saveTokens(host, tokenStoreFromOAuthTokens(result, cached));
186
216
  return { access_token: result.access_token, source: "refresh" };
187
217
  }
188
218
  }
@@ -204,39 +234,18 @@ function generateState(length = 16) {
204
234
  }
205
235
  // ── Browser Launch ───────────────────────────────────────────
206
236
  function openBrowser(url) {
207
- try {
208
- const platform = process.platform;
209
- if (platform === "darwin") {
210
- (0, node_child_process_1.execFileSync)("open", [url], { stdio: "ignore", timeout: 3000 });
211
- }
212
- else if (platform === "win32") {
213
- try {
214
- (0, node_child_process_1.execFileSync)("powershell.exe", [
215
- "-NoProfile",
216
- "-NonInteractive",
217
- "-ExecutionPolicy",
218
- "Bypass",
219
- "-Command",
220
- "Start-Process -FilePath ([string]$args[0])",
221
- url,
222
- ], { stdio: "ignore", timeout: 3000 });
223
- }
224
- catch {
225
- (0, node_child_process_1.execFileSync)("cmd.exe", ["/c", "start", "", `"${url}"`], { stdio: "ignore", timeout: 3000 });
226
- }
227
- }
228
- else {
229
- (0, node_child_process_1.execFileSync)("xdg-open", [url], { stdio: "ignore", timeout: 3000 });
230
- }
231
- }
232
- catch {
237
+ // Delegate to `open`, which spawns the platform launcher with the URL as a
238
+ // single argument. This avoids shell interpretation of `&`/`?` — the source
239
+ // of the earlier Windows bugs (cmd `start` truncating at `&`, PowerShell
240
+ // `$args` not populating under -Command and treating the URL as a file path).
241
+ void (0, open_1.default)(url).catch(() => {
233
242
  console.error("[scorehub] Could not open browser automatically.");
234
243
  console.error(`[scorehub] Please visit this URL manually:\n ${url}`);
235
- }
244
+ });
236
245
  }
237
246
  // ── Full OAuth Flow ──────────────────────────────────────────
238
247
  async function authorize(params) {
239
- const { authEndpoint, tokenEndpoint, clientId, redirectUri, scope, timeoutMs = 120_000, host, } = params;
248
+ const { authEndpoint, tokenEndpoint, clientId, redirectUri, scope, timeoutMs = 120_000, host, preserveExistingTokensOnFailure = false, authorizationUrlFactory, } = params;
240
249
  const redirectUrl = redirectUri ? new URL(redirectUri) : null;
241
250
  const callbackHost = params.callbackHost ?? redirectUrl?.hostname ?? DEFAULT_CALLBACK_HOST;
242
251
  const callbackPath = normalizeCallbackPath(params.callbackPath ?? redirectUrl?.pathname ?? DEFAULT_CALLBACK_PATH);
@@ -342,12 +351,7 @@ async function authorize(params) {
342
351
  }
343
352
  const actualRedirectUri = redirectUri ?? callbackUrl;
344
353
  const tokens = await exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, clientId, actualRedirectUri);
345
- saveTokens(storageHost, {
346
- access_token: tokens.access_token,
347
- refresh_token: tokens.refresh_token,
348
- expires_at: Date.now() + (tokens.expires_in ?? 3600) * 1000,
349
- updated_at: new Date().toISOString(),
350
- });
354
+ saveTokens(storageHost, tokenStoreFromOAuthTokens(tokens));
351
355
  settled = true;
352
356
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
353
357
  res.end((0, callback_pages_js_1.buildCallbackPage)(createCallbackPageModel({
@@ -370,11 +374,14 @@ async function authorize(params) {
370
374
  refreshToken: tokens.refresh_token,
371
375
  expiresIn: tokens.expires_in,
372
376
  scope: tokens.scope,
377
+ account: tokens.account,
378
+ selectedShop: tokens.selected_shop,
373
379
  });
374
380
  }
375
381
  catch (error) {
376
382
  settled = true;
377
- clearTokens(storageHost);
383
+ if (!preserveExistingTokensOnFailure)
384
+ clearTokens(storageHost);
378
385
  const err = error instanceof CallbackError
379
386
  ? error
380
387
  : new CallbackError(error instanceof Error ? error.message : String(error), createCallbackPageModel({
@@ -403,7 +410,8 @@ async function authorize(params) {
403
410
  if (settled)
404
411
  return;
405
412
  settled = true;
406
- clearTokens(storageHost);
413
+ if (!preserveExistingTokensOnFailure)
414
+ clearTokens(storageHost);
407
415
  stopServer(server);
408
416
  reject(err);
409
417
  });
@@ -411,25 +419,54 @@ async function authorize(params) {
411
419
  server.unref();
412
420
  const actualPort = server.address().port;
413
421
  const actualRedirectUri = redirectUri ?? buildLocalRedirectUri(callbackHost, actualPort, callbackPath);
414
- const authUrl = new URL(authEndpoint);
415
- authUrl.searchParams.set("client_id", clientId);
416
- authUrl.searchParams.set("redirect_uri", actualRedirectUri);
417
- authUrl.searchParams.set("code_challenge", codeChallenge);
418
- authUrl.searchParams.set("code_challenge_method", "S256");
419
- authUrl.searchParams.set("state", state);
420
- authUrl.searchParams.set("response_type", "code");
421
- if (scope)
422
- authUrl.searchParams.set("scope", scope);
423
- console.error(`[scorehub] Opening authorization URL: endpoint=${authEndpoint}, redirect_uri=${actualRedirectUri}`);
424
- openBrowser(authUrl.toString());
425
- timeoutId = setTimeout(() => {
426
- if (settled)
427
- return;
428
- settled = true;
429
- clearTokens(storageHost);
430
- stopServer(server);
431
- reject(new Error(`Authorization timed out after ${timeoutMs / 1000}s. Please try again.`));
432
- }, timeoutMs);
422
+ void (async () => {
423
+ try {
424
+ const authorizationRequest = {
425
+ clientId,
426
+ redirectUri: actualRedirectUri,
427
+ state,
428
+ codeChallenge,
429
+ codeChallengeMethod: "S256",
430
+ ...(scope ? { scope } : {}),
431
+ };
432
+ let authorizationUrl;
433
+ if (authorizationUrlFactory) {
434
+ authorizationUrl = await authorizationUrlFactory(authorizationRequest);
435
+ }
436
+ else {
437
+ const authUrl = new URL(authEndpoint);
438
+ authUrl.searchParams.set("client_id", clientId);
439
+ authUrl.searchParams.set("redirect_uri", actualRedirectUri);
440
+ authUrl.searchParams.set("code_challenge", codeChallenge);
441
+ authUrl.searchParams.set("code_challenge_method", "S256");
442
+ authUrl.searchParams.set("state", state);
443
+ authUrl.searchParams.set("response_type", "code");
444
+ if (scope)
445
+ authUrl.searchParams.set("scope", scope);
446
+ authorizationUrl = authUrl.toString();
447
+ }
448
+ console.error(`[scorehub] Opening authorization URL: ${authorizationUrl}`);
449
+ openBrowser(authorizationUrl);
450
+ timeoutId = setTimeout(() => {
451
+ if (settled)
452
+ return;
453
+ settled = true;
454
+ if (!preserveExistingTokensOnFailure)
455
+ clearTokens(storageHost);
456
+ stopServer(server);
457
+ reject(new Error(`Authorization timed out after ${timeoutMs / 1000}s. Please try again.`));
458
+ }, timeoutMs);
459
+ }
460
+ catch (error) {
461
+ if (settled)
462
+ return;
463
+ settled = true;
464
+ if (!preserveExistingTokensOnFailure)
465
+ clearTokens(storageHost);
466
+ stopServer(server);
467
+ reject(error);
468
+ }
469
+ })();
433
470
  });
434
471
  });
435
472
  }
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ const SCOREHUB_BASE_URL = normalizeBaseUrl(process.env.SCOREHUB_REMOTE_MCP_URL |
28
28
  // OAuth endpoints are at the root path
29
29
  const TOKEN_ENDPOINT = joinUrl(SCOREHUB_BASE_URL, "/token");
30
30
  const AUTHORIZE_URL = joinUrl(SCOREHUB_BASE_URL, "/authorize");
31
+ const SHOP_SWITCH_ENDPOINT = joinUrl(SCOREHUB_BASE_URL, "/authorize/switch-shop");
31
32
  // MCP Streamable HTTP transport endpoints are at /mcp
32
33
  const REMOTE_MCP_URL = joinUrl(SCOREHUB_BASE_URL, "/mcp");
33
34
  function normalizeBaseUrl(value) {
@@ -140,6 +141,26 @@ async function callRemoteTool(name, args, token) {
140
141
  await client.close();
141
142
  }
142
143
  }
144
+ async function startShopSwitch(accessToken, request) {
145
+ const response = await fetch(SHOP_SWITCH_ENDPOINT, {
146
+ method: "POST",
147
+ headers: {
148
+ Authorization: `Bearer ${accessToken}`,
149
+ "Content-Type": "application/json",
150
+ },
151
+ body: JSON.stringify({
152
+ client_id: request.clientId,
153
+ redirect_uri: request.redirectUri,
154
+ state: request.state,
155
+ }),
156
+ });
157
+ const payload = await response.json().catch(() => null);
158
+ if (!response.ok || typeof payload?.redirect !== "string") {
159
+ const message = typeof payload?.error === "string" ? payload.error : `远端返回 HTTP ${response.status}`;
160
+ throw new Error(`无法发起店铺切换:${message}`);
161
+ }
162
+ return payload.redirect;
163
+ }
143
164
  // ── Fetch remote tools on startup ───────────────────────────
144
165
  let remoteTools = [];
145
166
  let lastFetchError = null;
@@ -162,7 +183,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
162
183
  const tools = [
163
184
  {
164
185
  name: "authorize",
165
- description: "检查 ScoreHub OAuth 授权。已有有效授权时直接返回;仅在传入 force=true 时重新打开浏览器登录。",
186
+ description: "检查 ScoreHub OAuth 授权。force=true 重新登录以切换账号;switch_shop=true 打开当前账号的店铺选择页,无需重新登录。",
166
187
  inputSchema: {
167
188
  type: "object",
168
189
  properties: {
@@ -170,6 +191,10 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
170
191
  type: "boolean",
171
192
  description: "是否强制清除当前端点的本地 token 并重新登录",
172
193
  },
194
+ switch_shop: {
195
+ type: "boolean",
196
+ description: "在当前已登录账号下打开店铺选择页;成功后切换本地 token 绑定的店铺,不要求重新登录",
197
+ },
173
198
  },
174
199
  required: [],
175
200
  },
@@ -196,6 +221,42 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
196
221
  switch (name) {
197
222
  case "authorize": {
198
223
  const force = args.force === true;
224
+ const switchShop = args.switch_shop === true;
225
+ if (force && switchShop) {
226
+ return {
227
+ content: [{ type: "text", text: "⚠️ 参数冲突:切换账号请只传 force=true;切换当前账号的店铺请只传 switch_shop=true。" }],
228
+ };
229
+ }
230
+ if (switchShop) {
231
+ const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
232
+ if (!tokenResult) {
233
+ return {
234
+ content: [{ type: "text", text: "⚠️ 当前没有有效授权,无法切换店铺。请先调用 authorize 完成 ScoreHub 登录。" }],
235
+ };
236
+ }
237
+ try {
238
+ const result = await (0, auth_js_1.authorize)({
239
+ authEndpoint: AUTHORIZE_URL,
240
+ tokenEndpoint: TOKEN_ENDPOINT,
241
+ clientId: CLIENT_ID,
242
+ host: TOKEN_STORE_KEY,
243
+ port: 0,
244
+ preserveExistingTokensOnFailure: true,
245
+ authorizationUrlFactory: (request) => startShopSwitch(tokenResult.access_token, request),
246
+ });
247
+ return {
248
+ content: [{
249
+ type: "text",
250
+ text: `✅ 已切换到店铺${result.selectedShop ? `「${result.selectedShop.name}」` : ""},可以继续使用 ScoreHub 工具。`,
251
+ }],
252
+ };
253
+ }
254
+ catch (error) {
255
+ return {
256
+ content: [{ type: "text", text: `❌ 店铺切换未完成,已保留原店铺授权:${error instanceof Error ? error.message : String(error)}` }],
257
+ };
258
+ }
259
+ }
199
260
  if (!force) {
200
261
  const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
201
262
  if (tokenResult) {
@@ -227,6 +288,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
227
288
  }
228
289
  case "status": {
229
290
  const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
291
+ const tokenStore = (0, auth_js_1.loadTokens)(TOKEN_STORE_KEY);
230
292
  return {
231
293
  content: [
232
294
  {
@@ -235,6 +297,8 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
235
297
  remote_url: REMOTE_MCP_URL,
236
298
  token_store_key: TOKEN_STORE_KEY,
237
299
  authorized: !!tokenResult,
300
+ ...(tokenStore?.account ? { account: tokenStore.account } : {}),
301
+ ...(tokenStore?.selected_shop ? { selected_shop: tokenStore.selected_shop } : {}),
238
302
  tools_count: remoteTools.length,
239
303
  ...(lastFetchError ? { error: lastFetchError } : {}),
240
304
  }, null, 2),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorehub/mcp-server",
3
- "version": "0.2.13",
3
+ "version": "0.3.0",
4
4
  "description": "ScoreHub local MCP proxy — OAuth login, token caching, and tool forwarding to remote MCP endpoint",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -20,7 +20,8 @@
20
20
  "scripts": {
21
21
  "build": "tsc && node -e \"const fs=require('fs');const f='dist/index.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'));fs.chmodSync(f,0o755)\"",
22
22
  "dev": "tsx src/index.ts",
23
- "start": "node dist/index.js"
23
+ "start": "node dist/index.js",
24
+ "test": "tsx --test test/*.test.ts"
24
25
  },
25
26
  "keywords": [
26
27
  "mcp",
@@ -30,7 +31,8 @@
30
31
  ],
31
32
  "license": "MIT",
32
33
  "dependencies": {
33
- "@modelcontextprotocol/sdk": "^1.29.0"
34
+ "@modelcontextprotocol/sdk": "^1.29.0",
35
+ "open": "^8.4.2"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@types/node": "^22.0.0",