@scorehub/mcp-server 0.2.14 → 0.3.1
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 +4 -4
- package/README.md +5 -5
- package/dist/auth.d.ts +33 -0
- package/dist/auth.js +114 -34
- package/dist/index.js +70 -3
- package/package.json +3 -2
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
|
|
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.
|
|
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
|
|
134
|
-
| `status` | Returns the remote address, authorization state,
|
|
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
|
-
-
|
|
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
|
-
|
|
123
|
+
如需退出登录,删除对应端点目录或整个 `~/.scorehub/` 目录,然后重新调用 `authorize`。在 Windows 资源管理器中删除该目录即可;macOS/Linux 也可用终端删除。
|
|
124
124
|
|
|
125
|
-
`authorize` 默认复用有效的 access token 或 refresh token
|
|
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
|
|
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,32 @@ 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;
|
|
18
|
+
country?: string;
|
|
19
|
+
brands?: OAuthShopBrand[];
|
|
20
|
+
}
|
|
21
|
+
export interface OAuthShopBrand {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
6
24
|
}
|
|
7
25
|
export interface TokenStore {
|
|
8
26
|
access_token: string;
|
|
9
27
|
refresh_token: string;
|
|
10
28
|
expires_at: number;
|
|
11
29
|
updated_at: string;
|
|
30
|
+
account?: OAuthAccount;
|
|
31
|
+
selected_shop?: OAuthShop;
|
|
12
32
|
}
|
|
13
33
|
export interface AuthorizeParams {
|
|
14
34
|
authEndpoint: string;
|
|
@@ -21,16 +41,29 @@ export interface AuthorizeParams {
|
|
|
21
41
|
host?: string;
|
|
22
42
|
callbackHost?: string;
|
|
23
43
|
callbackPath?: string;
|
|
44
|
+
preserveExistingTokensOnFailure?: boolean;
|
|
45
|
+
authorizationUrlFactory?: (request: AuthorizationRequest) => Promise<string>;
|
|
46
|
+
}
|
|
47
|
+
export interface AuthorizationRequest {
|
|
48
|
+
clientId: string;
|
|
49
|
+
redirectUri: string;
|
|
50
|
+
state: string;
|
|
51
|
+
codeChallenge: string;
|
|
52
|
+
codeChallengeMethod: "S256";
|
|
53
|
+
scope?: string;
|
|
24
54
|
}
|
|
25
55
|
export interface AuthorizeResult {
|
|
26
56
|
accessToken: string;
|
|
27
57
|
refreshToken: string;
|
|
28
58
|
expiresIn: number;
|
|
29
59
|
scope?: string;
|
|
60
|
+
account?: OAuthAccount;
|
|
61
|
+
selectedShop?: OAuthShop;
|
|
30
62
|
}
|
|
31
63
|
export declare function loadTokens(host: string): TokenStore | null;
|
|
32
64
|
export declare function saveTokens(host: string, tokens: TokenStore): void;
|
|
33
65
|
export declare function clearTokens(host: string): void;
|
|
66
|
+
export declare function formatShopAuthorizationDetails(shop?: OAuthShop): string;
|
|
34
67
|
export declare function exchangeCodeForTokens(tokenEndpoint: string, code: string, codeVerifier: string, clientId: string, redirectUri: string): Promise<OAuthTokens>;
|
|
35
68
|
export declare function refreshAccessToken(tokenEndpoint: string, refreshToken: string, clientId: string): Promise<OAuthTokens>;
|
|
36
69
|
export declare function getValidAccessToken(host: string, tokenEndpoint: string, clientId: string): Promise<{
|
package/dist/auth.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.loadTokens = loadTokens;
|
|
7
7
|
exports.saveTokens = saveTokens;
|
|
8
8
|
exports.clearTokens = clearTokens;
|
|
9
|
+
exports.formatShopAuthorizationDetails = formatShopAuthorizationDetails;
|
|
9
10
|
exports.exchangeCodeForTokens = exchangeCodeForTokens;
|
|
10
11
|
exports.refreshAccessToken = refreshAccessToken;
|
|
11
12
|
exports.getValidAccessToken = getValidAccessToken;
|
|
@@ -96,16 +97,72 @@ function parseOAuthTokens(payload) {
|
|
|
96
97
|
if (typeof record.access_token === "string"
|
|
97
98
|
&& typeof record.refresh_token === "string"
|
|
98
99
|
&& typeof record.expires_in === "number") {
|
|
100
|
+
const account = parseOAuthAccount(record.account);
|
|
101
|
+
const selectedShop = parseOAuthShop(record.selected_shop);
|
|
99
102
|
return {
|
|
100
103
|
access_token: record.access_token,
|
|
101
104
|
refresh_token: record.refresh_token,
|
|
102
105
|
expires_in: record.expires_in,
|
|
103
106
|
...(typeof record.scope === "string" ? { scope: record.scope } : {}),
|
|
107
|
+
...(account ? { account } : {}),
|
|
108
|
+
...(selectedShop ? { selected_shop: selectedShop } : {}),
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
throw new Error(getTokenErrorMessage(payload));
|
|
108
113
|
}
|
|
114
|
+
function parseOAuthAccount(value) {
|
|
115
|
+
if (value && typeof value === "object"
|
|
116
|
+
&& typeof value.id === "string"
|
|
117
|
+
&& typeof value.email === "string") {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
function parseOAuthShop(value) {
|
|
123
|
+
const shop = value;
|
|
124
|
+
if (shop && typeof shop === "object"
|
|
125
|
+
&& typeof shop.id === "string"
|
|
126
|
+
&& typeof shop.name === "string"
|
|
127
|
+
&& typeof shop.code === "string"
|
|
128
|
+
&& typeof shop.region === "string") {
|
|
129
|
+
const brands = Array.isArray(shop.brands)
|
|
130
|
+
? shop.brands.filter((brand) => (Boolean(brand)
|
|
131
|
+
&& typeof brand.id === "string"
|
|
132
|
+
&& typeof brand.name === "string"))
|
|
133
|
+
: undefined;
|
|
134
|
+
return {
|
|
135
|
+
id: shop.id,
|
|
136
|
+
name: shop.name,
|
|
137
|
+
code: shop.code,
|
|
138
|
+
region: shop.region,
|
|
139
|
+
...(typeof shop.country === "string" ? { country: shop.country } : {}),
|
|
140
|
+
...(brands !== undefined ? { brands } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
function formatShopAuthorizationDetails(shop) {
|
|
146
|
+
if (!shop)
|
|
147
|
+
return "";
|
|
148
|
+
const country = shop.country?.trim() || "未获取";
|
|
149
|
+
const brands = shop.brands === undefined
|
|
150
|
+
? "未获取"
|
|
151
|
+
: shop.brands.length === 0
|
|
152
|
+
? "未绑定"
|
|
153
|
+
: shop.brands.map((brand) => brand.name).join("、");
|
|
154
|
+
return `店铺名称:${shop.name}\n国家:${country}\n绑定品牌:${brands}`;
|
|
155
|
+
}
|
|
156
|
+
function tokenStoreFromOAuthTokens(tokens, previous) {
|
|
157
|
+
return {
|
|
158
|
+
access_token: tokens.access_token,
|
|
159
|
+
refresh_token: tokens.refresh_token ?? previous?.refresh_token ?? "",
|
|
160
|
+
expires_at: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
161
|
+
updated_at: new Date().toISOString(),
|
|
162
|
+
...(tokens.account ? { account: tokens.account } : previous?.account ? { account: previous.account } : {}),
|
|
163
|
+
...(tokens.selected_shop ? { selected_shop: tokens.selected_shop } : previous?.selected_shop ? { selected_shop: previous.selected_shop } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
109
166
|
function createCallbackPageModel(input) {
|
|
110
167
|
return {
|
|
111
168
|
tone: input.tone,
|
|
@@ -180,12 +237,7 @@ async function getValidAccessToken(host, tokenEndpoint, clientId) {
|
|
|
180
237
|
try {
|
|
181
238
|
const result = await refreshAccessToken(tokenEndpoint, cached.refresh_token, clientId);
|
|
182
239
|
if (result.access_token) {
|
|
183
|
-
saveTokens(host,
|
|
184
|
-
access_token: result.access_token,
|
|
185
|
-
refresh_token: result.refresh_token ?? cached.refresh_token,
|
|
186
|
-
expires_at: Date.now() + (result.expires_in ?? 3600) * 1000,
|
|
187
|
-
updated_at: new Date().toISOString(),
|
|
188
|
-
});
|
|
240
|
+
saveTokens(host, tokenStoreFromOAuthTokens(result, cached));
|
|
189
241
|
return { access_token: result.access_token, source: "refresh" };
|
|
190
242
|
}
|
|
191
243
|
}
|
|
@@ -218,7 +270,7 @@ function openBrowser(url) {
|
|
|
218
270
|
}
|
|
219
271
|
// ── Full OAuth Flow ──────────────────────────────────────────
|
|
220
272
|
async function authorize(params) {
|
|
221
|
-
const { authEndpoint, tokenEndpoint, clientId, redirectUri, scope, timeoutMs = 120_000, host, } = params;
|
|
273
|
+
const { authEndpoint, tokenEndpoint, clientId, redirectUri, scope, timeoutMs = 120_000, host, preserveExistingTokensOnFailure = false, authorizationUrlFactory, } = params;
|
|
222
274
|
const redirectUrl = redirectUri ? new URL(redirectUri) : null;
|
|
223
275
|
const callbackHost = params.callbackHost ?? redirectUrl?.hostname ?? DEFAULT_CALLBACK_HOST;
|
|
224
276
|
const callbackPath = normalizeCallbackPath(params.callbackPath ?? redirectUrl?.pathname ?? DEFAULT_CALLBACK_PATH);
|
|
@@ -324,12 +376,7 @@ async function authorize(params) {
|
|
|
324
376
|
}
|
|
325
377
|
const actualRedirectUri = redirectUri ?? callbackUrl;
|
|
326
378
|
const tokens = await exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, clientId, actualRedirectUri);
|
|
327
|
-
saveTokens(storageHost,
|
|
328
|
-
access_token: tokens.access_token,
|
|
329
|
-
refresh_token: tokens.refresh_token,
|
|
330
|
-
expires_at: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
331
|
-
updated_at: new Date().toISOString(),
|
|
332
|
-
});
|
|
379
|
+
saveTokens(storageHost, tokenStoreFromOAuthTokens(tokens));
|
|
333
380
|
settled = true;
|
|
334
381
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
335
382
|
res.end((0, callback_pages_js_1.buildCallbackPage)(createCallbackPageModel({
|
|
@@ -352,11 +399,14 @@ async function authorize(params) {
|
|
|
352
399
|
refreshToken: tokens.refresh_token,
|
|
353
400
|
expiresIn: tokens.expires_in,
|
|
354
401
|
scope: tokens.scope,
|
|
402
|
+
account: tokens.account,
|
|
403
|
+
selectedShop: tokens.selected_shop,
|
|
355
404
|
});
|
|
356
405
|
}
|
|
357
406
|
catch (error) {
|
|
358
407
|
settled = true;
|
|
359
|
-
|
|
408
|
+
if (!preserveExistingTokensOnFailure)
|
|
409
|
+
clearTokens(storageHost);
|
|
360
410
|
const err = error instanceof CallbackError
|
|
361
411
|
? error
|
|
362
412
|
: new CallbackError(error instanceof Error ? error.message : String(error), createCallbackPageModel({
|
|
@@ -385,7 +435,8 @@ async function authorize(params) {
|
|
|
385
435
|
if (settled)
|
|
386
436
|
return;
|
|
387
437
|
settled = true;
|
|
388
|
-
|
|
438
|
+
if (!preserveExistingTokensOnFailure)
|
|
439
|
+
clearTokens(storageHost);
|
|
389
440
|
stopServer(server);
|
|
390
441
|
reject(err);
|
|
391
442
|
});
|
|
@@ -393,25 +444,54 @@ async function authorize(params) {
|
|
|
393
444
|
server.unref();
|
|
394
445
|
const actualPort = server.address().port;
|
|
395
446
|
const actualRedirectUri = redirectUri ?? buildLocalRedirectUri(callbackHost, actualPort, callbackPath);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
447
|
+
void (async () => {
|
|
448
|
+
try {
|
|
449
|
+
const authorizationRequest = {
|
|
450
|
+
clientId,
|
|
451
|
+
redirectUri: actualRedirectUri,
|
|
452
|
+
state,
|
|
453
|
+
codeChallenge,
|
|
454
|
+
codeChallengeMethod: "S256",
|
|
455
|
+
...(scope ? { scope } : {}),
|
|
456
|
+
};
|
|
457
|
+
let authorizationUrl;
|
|
458
|
+
if (authorizationUrlFactory) {
|
|
459
|
+
authorizationUrl = await authorizationUrlFactory(authorizationRequest);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
const authUrl = new URL(authEndpoint);
|
|
463
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
464
|
+
authUrl.searchParams.set("redirect_uri", actualRedirectUri);
|
|
465
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
466
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
467
|
+
authUrl.searchParams.set("state", state);
|
|
468
|
+
authUrl.searchParams.set("response_type", "code");
|
|
469
|
+
if (scope)
|
|
470
|
+
authUrl.searchParams.set("scope", scope);
|
|
471
|
+
authorizationUrl = authUrl.toString();
|
|
472
|
+
}
|
|
473
|
+
console.error(`[scorehub] Opening authorization URL: ${authorizationUrl}`);
|
|
474
|
+
openBrowser(authorizationUrl);
|
|
475
|
+
timeoutId = setTimeout(() => {
|
|
476
|
+
if (settled)
|
|
477
|
+
return;
|
|
478
|
+
settled = true;
|
|
479
|
+
if (!preserveExistingTokensOnFailure)
|
|
480
|
+
clearTokens(storageHost);
|
|
481
|
+
stopServer(server);
|
|
482
|
+
reject(new Error(`Authorization timed out after ${timeoutMs / 1000}s. Please try again.`));
|
|
483
|
+
}, timeoutMs);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (settled)
|
|
487
|
+
return;
|
|
488
|
+
settled = true;
|
|
489
|
+
if (!preserveExistingTokensOnFailure)
|
|
490
|
+
clearTokens(storageHost);
|
|
491
|
+
stopServer(server);
|
|
492
|
+
reject(error);
|
|
493
|
+
}
|
|
494
|
+
})();
|
|
415
495
|
});
|
|
416
496
|
});
|
|
417
497
|
}
|
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
|
|
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: `✅ 已切换店铺。\n${(0, auth_js_1.formatShopAuthorizationDetails)(result.selectedShop)}\n可以继续使用 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) {
|
|
@@ -208,7 +269,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
208
269
|
(0, auth_js_1.clearTokens)(TOKEN_STORE_KEY);
|
|
209
270
|
}
|
|
210
271
|
try {
|
|
211
|
-
await (0, auth_js_1.authorize)({
|
|
272
|
+
const result = await (0, auth_js_1.authorize)({
|
|
212
273
|
authEndpoint: AUTHORIZE_URL,
|
|
213
274
|
tokenEndpoint: TOKEN_ENDPOINT,
|
|
214
275
|
clientId: CLIENT_ID,
|
|
@@ -216,7 +277,10 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
216
277
|
port: 0,
|
|
217
278
|
});
|
|
218
279
|
return {
|
|
219
|
-
content: [{
|
|
280
|
+
content: [{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: `✅ 授权成功!\n${(0, auth_js_1.formatShopAuthorizationDetails)(result.selectedShop)}\n可以直接继续使用 ScoreHub 工具。`,
|
|
283
|
+
}],
|
|
220
284
|
};
|
|
221
285
|
}
|
|
222
286
|
catch (error) {
|
|
@@ -227,6 +291,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
227
291
|
}
|
|
228
292
|
case "status": {
|
|
229
293
|
const tokenResult = await (0, auth_js_1.getValidAccessToken)(TOKEN_STORE_KEY, TOKEN_ENDPOINT, CLIENT_ID);
|
|
294
|
+
const tokenStore = (0, auth_js_1.loadTokens)(TOKEN_STORE_KEY);
|
|
230
295
|
return {
|
|
231
296
|
content: [
|
|
232
297
|
{
|
|
@@ -235,6 +300,8 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
235
300
|
remote_url: REMOTE_MCP_URL,
|
|
236
301
|
token_store_key: TOKEN_STORE_KEY,
|
|
237
302
|
authorized: !!tokenResult,
|
|
303
|
+
...(tokenStore?.account ? { account: tokenStore.account } : {}),
|
|
304
|
+
...(tokenStore?.selected_shop ? { selected_shop: tokenStore.selected_shop } : {}),
|
|
238
305
|
tools_count: remoteTools.length,
|
|
239
306
|
...(lastFetchError ? { error: lastFetchError } : {}),
|
|
240
307
|
}, null, 2),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scorehub/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|