@scorehub/mcp-server 0.4.2 → 0.4.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 +5 -0
- package/dist/auth.js +8 -3
- package/dist/cli.d.ts +32 -0
- package/dist/cli.js +110 -0
- package/dist/index.js +12 -0
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -43,6 +43,11 @@ export interface AuthorizeParams {
|
|
|
43
43
|
callbackPath?: string;
|
|
44
44
|
preserveExistingTokensOnFailure?: boolean;
|
|
45
45
|
authorizationUrlFactory?: (request: AuthorizationRequest) => Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* CLI 前置认证(preAuth: "cli")模式:把授权 URL 交给调用方打开浏览器,
|
|
48
|
+
* 本进程只保留本地回环回调监听。未传时保持原有自动开浏览器行为。
|
|
49
|
+
*/
|
|
50
|
+
onAuthorizationUrl?: (url: string) => void;
|
|
46
51
|
}
|
|
47
52
|
export interface AuthorizationRequest {
|
|
48
53
|
clientId: string;
|
package/dist/auth.js
CHANGED
|
@@ -285,7 +285,7 @@ function openBrowser(url) {
|
|
|
285
285
|
}
|
|
286
286
|
// ── Full OAuth Flow ──────────────────────────────────────────
|
|
287
287
|
async function authorize(params) {
|
|
288
|
-
const { authEndpoint, tokenEndpoint, clientId, redirectUri, scope, timeoutMs = 120_000, host, preserveExistingTokensOnFailure = false, authorizationUrlFactory, } = params;
|
|
288
|
+
const { authEndpoint, tokenEndpoint, clientId, redirectUri, scope, timeoutMs = 120_000, host, preserveExistingTokensOnFailure = false, authorizationUrlFactory, onAuthorizationUrl, } = params;
|
|
289
289
|
const redirectUrl = redirectUri ? new URL(redirectUri) : null;
|
|
290
290
|
const callbackHost = params.callbackHost ?? redirectUrl?.hostname ?? DEFAULT_CALLBACK_HOST;
|
|
291
291
|
const callbackPath = normalizeCallbackPath(params.callbackPath ?? redirectUrl?.pathname ?? DEFAULT_CALLBACK_PATH);
|
|
@@ -485,8 +485,13 @@ async function authorize(params) {
|
|
|
485
485
|
authUrl.searchParams.set("scope", scope);
|
|
486
486
|
authorizationUrl = authUrl.toString();
|
|
487
487
|
}
|
|
488
|
-
|
|
489
|
-
|
|
488
|
+
if (onAuthorizationUrl) {
|
|
489
|
+
onAuthorizationUrl(authorizationUrl);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.error(`[scorehub] Opening authorization URL: ${authorizationUrl}`);
|
|
493
|
+
openBrowser(authorizationUrl);
|
|
494
|
+
}
|
|
490
495
|
timeoutId = setTimeout(() => {
|
|
491
496
|
if (settled)
|
|
492
497
|
return;
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI authentication commands used by connector/cli.json (preAuth: "cli").
|
|
3
|
+
*
|
|
4
|
+
* WorkBuddy runs these before starting the stdio MCP server. The authorization
|
|
5
|
+
* URL is printed to stdout for WorkBuddy to open the browser; this process keeps
|
|
6
|
+
* the 127.0.0.1 callback listener alive until the flow completes.
|
|
7
|
+
*
|
|
8
|
+
* See packages/mcp-server/docs/PREAUTH_CLI.md for the full contract.
|
|
9
|
+
*/
|
|
10
|
+
import { type AuthorizeParams, type AuthorizeResult, type TokenStore } from "./auth.js";
|
|
11
|
+
export interface TokenStorage {
|
|
12
|
+
load(host: string): TokenStore | null;
|
|
13
|
+
clear(host: string): void;
|
|
14
|
+
}
|
|
15
|
+
export interface CliContext {
|
|
16
|
+
tokenStoreKey: string;
|
|
17
|
+
tokenEndpoint: string;
|
|
18
|
+
authorizeUrl: string;
|
|
19
|
+
clientId: string;
|
|
20
|
+
/** Injectable so tests never touch the real ~/.scorehub token file. */
|
|
21
|
+
storage?: TokenStorage;
|
|
22
|
+
authorizeFn?: (params: AuthorizeParams) => Promise<AuthorizeResult>;
|
|
23
|
+
}
|
|
24
|
+
export interface CliIO {
|
|
25
|
+
stdout(text: string): void;
|
|
26
|
+
stderr(text: string): void;
|
|
27
|
+
}
|
|
28
|
+
/** Only these argv forms are CLI commands; everything else keeps the MCP server behaviour. */
|
|
29
|
+
export declare function isCliInvocation(args: string[]): boolean;
|
|
30
|
+
/** Pure, side-effect-free authorization check for the status command. */
|
|
31
|
+
export declare function isAuthorized(tokens: TokenStore | null, now?: number): boolean;
|
|
32
|
+
export declare function runCli(args: string[], ctx: CliContext, io?: CliIO): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isCliInvocation = isCliInvocation;
|
|
4
|
+
exports.isAuthorized = isAuthorized;
|
|
5
|
+
exports.runCli = runCli;
|
|
6
|
+
/**
|
|
7
|
+
* CLI authentication commands used by connector/cli.json (preAuth: "cli").
|
|
8
|
+
*
|
|
9
|
+
* WorkBuddy runs these before starting the stdio MCP server. The authorization
|
|
10
|
+
* URL is printed to stdout for WorkBuddy to open the browser; this process keeps
|
|
11
|
+
* the 127.0.0.1 callback listener alive until the flow completes.
|
|
12
|
+
*
|
|
13
|
+
* See packages/mcp-server/docs/PREAUTH_CLI.md for the full contract.
|
|
14
|
+
*/
|
|
15
|
+
const auth_js_1 = require("./auth.js");
|
|
16
|
+
/** Matches authWaitForExit: WorkBuddy waits up to 5 minutes for the CLI to exit. */
|
|
17
|
+
const AUTH_TIMEOUT_MS = 300_000;
|
|
18
|
+
/** Treat a token expiring within a minute as unauthenticated. */
|
|
19
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
20
|
+
const defaultStorage = { load: auth_js_1.loadTokens, clear: auth_js_1.clearTokens };
|
|
21
|
+
const defaultIO = {
|
|
22
|
+
stdout: (text) => { process.stdout.write(text); },
|
|
23
|
+
stderr: (text) => { process.stderr.write(text); },
|
|
24
|
+
};
|
|
25
|
+
const USAGE = `ScoreHub MCP Server - CLI authentication commands
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
scorehub-mcp auth login Start browser authorization (prints URL, waits for callback)
|
|
29
|
+
scorehub-mcp auth status Check persisted authorization (exit 0 = logged in)
|
|
30
|
+
scorehub-mcp auth status --json Machine-readable status
|
|
31
|
+
scorehub-mcp auth logout Clear persisted authorization
|
|
32
|
+
scorehub-mcp --help Show this help
|
|
33
|
+
|
|
34
|
+
With no recognized command the process starts the stdio MCP server as usual.
|
|
35
|
+
`;
|
|
36
|
+
/** Only these argv forms are CLI commands; everything else keeps the MCP server behaviour. */
|
|
37
|
+
function isCliInvocation(args) {
|
|
38
|
+
const first = args[0];
|
|
39
|
+
return first === "auth" || first === "--help" || first === "-h";
|
|
40
|
+
}
|
|
41
|
+
/** Pure, side-effect-free authorization check for the status command. */
|
|
42
|
+
function isAuthorized(tokens, now = Date.now()) {
|
|
43
|
+
if (!tokens || !tokens.access_token)
|
|
44
|
+
return false;
|
|
45
|
+
return typeof tokens.expires_at === "number" && tokens.expires_at > now + EXPIRY_SKEW_MS;
|
|
46
|
+
}
|
|
47
|
+
async function runCli(args, ctx, io = defaultIO) {
|
|
48
|
+
const [group, sub, ...rest] = args;
|
|
49
|
+
if (group === "--help" || group === "-h" || sub === "--help" || sub === "-h") {
|
|
50
|
+
io.stdout(USAGE);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const storage = ctx.storage ?? defaultStorage;
|
|
54
|
+
switch (sub) {
|
|
55
|
+
case "login": {
|
|
56
|
+
const authorizeFn = ctx.authorizeFn ?? auth_js_1.authorize;
|
|
57
|
+
try {
|
|
58
|
+
const result = await authorizeFn({
|
|
59
|
+
authEndpoint: ctx.authorizeUrl,
|
|
60
|
+
tokenEndpoint: ctx.tokenEndpoint,
|
|
61
|
+
clientId: ctx.clientId,
|
|
62
|
+
host: ctx.tokenStoreKey,
|
|
63
|
+
port: 0,
|
|
64
|
+
timeoutMs: AUTH_TIMEOUT_MS,
|
|
65
|
+
// WorkBuddy extracts this URL from stdout and opens the browser itself.
|
|
66
|
+
// Must be a bare URL line: no quotes, no angle brackets, whitespace-delimited.
|
|
67
|
+
onAuthorizationUrl: (url) => { io.stdout(`${url}\n`); },
|
|
68
|
+
});
|
|
69
|
+
io.stderr(`[scorehub] Authorization complete\n${(0, auth_js_1.formatAuthorizationDetails)(result.account, result.selectedShop)}\n`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
io.stderr(`[scorehub] Authorization failed: ${message}\n`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
case "status": {
|
|
79
|
+
// Deliberately read-only: never call getValidAccessToken (it refreshes tokens).
|
|
80
|
+
const tokens = storage.load(ctx.tokenStoreKey);
|
|
81
|
+
const ok = isAuthorized(tokens);
|
|
82
|
+
if (rest.includes("--json")) {
|
|
83
|
+
io.stdout(`${JSON.stringify({
|
|
84
|
+
authenticated: ok,
|
|
85
|
+
account: tokens?.account?.phone ?? null,
|
|
86
|
+
shop: tokens?.selected_shop?.name ?? null,
|
|
87
|
+
expires_at: tokens?.expires_at ?? null,
|
|
88
|
+
})}\n`);
|
|
89
|
+
}
|
|
90
|
+
else if (ok && tokens) {
|
|
91
|
+
io.stdout(`Logged in as ${tokens.account?.phone ?? "ScoreHub account"} ` +
|
|
92
|
+
`(shop: ${tokens.selected_shop?.name ?? "unknown"})\n`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
io.stdout("Not logged in\n");
|
|
96
|
+
}
|
|
97
|
+
return ok ? 0 : 1;
|
|
98
|
+
}
|
|
99
|
+
case "logout": {
|
|
100
|
+
storage.clear(ctx.tokenStoreKey);
|
|
101
|
+
io.stdout("Logged out\n");
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
default: {
|
|
105
|
+
io.stderr(`[scorehub] Unknown auth command: ${sub ?? "(none)"}\n`);
|
|
106
|
+
io.stdout(USAGE);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,7 @@ function endpointTokenKey(baseUrl) {
|
|
|
46
46
|
// ── Auth State ──────────────────────────────────────────────
|
|
47
47
|
const auth_js_1 = require("./auth.js");
|
|
48
48
|
const status_js_1 = require("./status.js");
|
|
49
|
+
const cli_js_1 = require("./cli.js");
|
|
49
50
|
const TOKEN_STORE_KEY = endpointTokenKey(SCOREHUB_BASE_URL);
|
|
50
51
|
const CLIENT_ID = "scorehub-mcp";
|
|
51
52
|
// Proactively refresh token every 4 hours to prevent expiry during long sessions
|
|
@@ -314,6 +315,17 @@ async function main() {
|
|
|
314
315
|
process.exitCode = 1;
|
|
315
316
|
return;
|
|
316
317
|
}
|
|
318
|
+
// CLI 前置认证命令(cli.json 的 auth / status / unAuth)。未命中时仍是原 stdio MCP 服务。
|
|
319
|
+
const cliArgs = process.argv.slice(2);
|
|
320
|
+
if ((0, cli_js_1.isCliInvocation)(cliArgs)) {
|
|
321
|
+
process.exitCode = await (0, cli_js_1.runCli)(cliArgs, {
|
|
322
|
+
tokenStoreKey: TOKEN_STORE_KEY,
|
|
323
|
+
tokenEndpoint: TOKEN_ENDPOINT,
|
|
324
|
+
authorizeUrl: AUTHORIZE_URL,
|
|
325
|
+
clientId: CLIENT_ID,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
317
329
|
// Set process title for easy identification in ps/task manager
|
|
318
330
|
process.title = 'scorehub-mcp';
|
|
319
331
|
// Gracefully exit when parent AI agent closes stdin (normal shutdown or crash).
|