@scorehub/mcp-server 0.4.1 → 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 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
- console.error(`[scorehub] Opening authorization URL: ${authorizationUrl}`);
489
- openBrowser(authorizationUrl);
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
@@ -14,11 +14,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
15
15
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
16
16
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
17
- const index_js_2 = require("@modelcontextprotocol/sdk/client/index.js");
18
- const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
19
17
  const node_fs_1 = require("node:fs");
20
18
  const node_path_1 = require("node:path");
21
19
  const self_check_js_1 = require("./self-check.js");
20
+ const proxy_js_1 = require("./proxy.js");
22
21
  function readPackageMetadata() {
23
22
  return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "../package.json"), "utf-8"));
24
23
  }
@@ -47,72 +46,16 @@ function endpointTokenKey(baseUrl) {
47
46
  // ── Auth State ──────────────────────────────────────────────
48
47
  const auth_js_1 = require("./auth.js");
49
48
  const status_js_1 = require("./status.js");
49
+ const cli_js_1 = require("./cli.js");
50
50
  const TOKEN_STORE_KEY = endpointTokenKey(SCOREHUB_BASE_URL);
51
51
  const CLIENT_ID = "scorehub-mcp";
52
52
  // Proactively refresh token every 4 hours to prevent expiry during long sessions
53
53
  const TOKEN_KEEPALIVE_INTERVAL = 4 * 60 * 60 * 1000;
54
- function isRemoteToolErrorType(value) {
55
- return value === "oauth_invalid"
56
- || value === "shop_auth_invalid"
57
- || value === "rate_limited"
58
- || value === "quota_exhausted"
59
- || value === "invalid_input"
60
- || value === "permission_denied"
61
- || value === "upstream_error";
62
- }
63
- function parseRemoteToolEnvelope(text) {
64
- try {
65
- const parsed = JSON.parse(text);
66
- if (parsed.ok === true && Object.prototype.hasOwnProperty.call(parsed, "data")) {
67
- return { ok: true, data: parsed.data };
68
- }
69
- if (parsed.ok === false
70
- && isRemoteToolErrorType(parsed.error_type)
71
- && typeof parsed.user_message === "string"
72
- && typeof parsed.retryable === "boolean") {
73
- return {
74
- ok: false,
75
- error_type: parsed.error_type,
76
- user_message: parsed.user_message,
77
- retryable: parsed.retryable,
78
- ...(Object.prototype.hasOwnProperty.call(parsed, "upstream") ? { upstream: parsed.upstream } : {}),
79
- };
80
- }
81
- return null;
82
- }
83
- catch {
84
- return null;
85
- }
86
- }
87
- function extractRemoteTextContent(content) {
88
- if (!Array.isArray(content)) {
89
- return [];
90
- }
91
- return content.flatMap((item) => {
92
- if (item
93
- && typeof item === "object"
94
- && item.type === "text"
95
- && typeof item.text === "string") {
96
- return [item.text];
97
- }
98
- return [];
99
- });
100
- }
101
54
  // ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
55
+ const remoteProxy = new proxy_js_1.RemoteToolProxy(new URL(REMOTE_MCP_URL), SERVER_VERSION);
102
56
  async function fetchRemoteTools(token) {
103
57
  try {
104
- const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(REMOTE_MCP_URL), {
105
- requestInit: token ? { headers: { Authorization: `Bearer ${token}` } } : undefined,
106
- });
107
- const client = new index_js_2.Client({ name: "scorehub-mcp", version: SERVER_VERSION }, { capabilities: {} });
108
- try {
109
- await client.connect(transport);
110
- const result = await client.listTools();
111
- return result.tools;
112
- }
113
- finally {
114
- await client.close();
115
- }
58
+ return await remoteProxy.listTools(token);
116
59
  }
117
60
  catch (e) {
118
61
  const msg = e instanceof Error ? e.message : String(e);
@@ -121,27 +64,8 @@ async function fetchRemoteTools(token) {
121
64
  return [];
122
65
  }
123
66
  }
124
- async function callRemoteTool(name, args, token) {
125
- const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(REMOTE_MCP_URL), {
126
- requestInit: { headers: { Authorization: `Bearer ${token}` } },
127
- });
128
- const client = new index_js_2.Client({ name: "scorehub-mcp", version: SERVER_VERSION }, { capabilities: {} });
129
- try {
130
- await client.connect(transport);
131
- const result = await client.callTool({ name, arguments: args });
132
- const textBlocks = extractRemoteTextContent(result.content);
133
- const rawText = textBlocks.length > 0
134
- ? textBlocks.join("\n\n")
135
- : JSON.stringify(result.content, null, 2);
136
- const envelope = textBlocks
137
- .map((text) => parseRemoteToolEnvelope(text))
138
- .find((candidate) => candidate !== null)
139
- ?? parseRemoteToolEnvelope(rawText);
140
- return { rawText, envelope };
141
- }
142
- finally {
143
- await client.close();
144
- }
67
+ function callRemoteTool(name, args, token) {
68
+ return remoteProxy.callTool(name, args, token);
145
69
  }
146
70
  async function startShopSwitch(accessToken, request) {
147
71
  const response = await fetch(SHOP_SWITCH_ENDPOINT, {
@@ -182,7 +106,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
182
106
  }
183
107
  }
184
108
  catch { /* keep stale tools and lastError */ }
185
- const tools = [
109
+ const localTools = [
186
110
  {
187
111
  name: "authorize",
188
112
  description: "检查 ScoreHub OAuth 授权。force=true 重新登录以切换账号;switch_shop=true 打开当前账号的店铺选择页,无需重新登录。",
@@ -210,13 +134,8 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
210
134
  required: [],
211
135
  },
212
136
  },
213
- ...remoteTools.map((t) => ({
214
- name: t.name,
215
- description: t.description,
216
- inputSchema: t.inputSchema,
217
- })),
218
137
  ];
219
- return { tools };
138
+ return { tools: (0, proxy_js_1.mergeRemoteTools)(localTools, remoteTools) };
220
139
  });
221
140
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
222
141
  const { name, arguments: args = {} } = request.params;
@@ -396,6 +315,17 @@ async function main() {
396
315
  process.exitCode = 1;
397
316
  return;
398
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
+ }
399
329
  // Set process title for easy identification in ps/task manager
400
330
  process.title = 'scorehub-mcp';
401
331
  // Gracefully exit when parent AI agent closes stdin (normal shutdown or crash).
@@ -0,0 +1,59 @@
1
+ export interface RemoteToolDescriptor {
2
+ name: string;
3
+ description?: string;
4
+ inputSchema: Record<string, unknown>;
5
+ }
6
+ export type RemoteToolErrorType = "oauth_invalid" | "shop_auth_invalid" | "rate_limited" | "quota_exhausted" | "invalid_input" | "permission_denied" | "upstream_error";
7
+ export type RemoteToolEnvelope = {
8
+ ok: true;
9
+ data: unknown;
10
+ } | {
11
+ ok: false;
12
+ error_type: RemoteToolErrorType;
13
+ user_message: string;
14
+ retryable: boolean;
15
+ upstream?: unknown;
16
+ };
17
+ export interface RemoteToolCallResult {
18
+ rawText: string;
19
+ envelope: RemoteToolEnvelope | null;
20
+ }
21
+ export interface RemoteClientLike {
22
+ connect(): Promise<void>;
23
+ listTools(): Promise<{
24
+ tools: RemoteToolDescriptor[];
25
+ }>;
26
+ callTool(request: {
27
+ name: string;
28
+ arguments: Record<string, unknown>;
29
+ }): Promise<{
30
+ content: unknown;
31
+ }>;
32
+ close(): Promise<void>;
33
+ }
34
+ export type RemoteClientFactory = (endpoint: URL, token: string | undefined, serverVersion: string) => RemoteClientLike;
35
+ /** Parse the public QueryData/error envelope without interpreting business data. */
36
+ export declare function parseRemoteToolEnvelope(text: string): RemoteToolEnvelope | null;
37
+ /** Extract text blocks in remote order; non-text blocks are left untouched. */
38
+ export declare function extractRemoteTextContent(content: unknown): string[];
39
+ /** Build the same raw text and envelope the proxy returns to the MCP client. */
40
+ export declare function buildRemoteToolCallResult(content: unknown): RemoteToolCallResult;
41
+ /**
42
+ * Merge local tools with the remote list without filtering, renaming or
43
+ * rewriting schemas. Remote order is preserved after the two local tools.
44
+ */
45
+ export declare function mergeRemoteTools(localTools: readonly RemoteToolDescriptor[], remoteTools: readonly RemoteToolDescriptor[]): RemoteToolDescriptor[];
46
+ /** Default SDK-backed client factory. Tests inject a fake factory instead. */
47
+ export declare function createSdkRemoteClient(endpoint: URL, token: string | undefined, serverVersion: string): RemoteClientLike;
48
+ /**
49
+ * One short-lived remote client per list/call, matching the existing proxy
50
+ * lifecycle. The factory seam is the only test-specific difference.
51
+ */
52
+ export declare class RemoteToolProxy {
53
+ private readonly endpoint;
54
+ private readonly serverVersion;
55
+ private readonly factory;
56
+ constructor(endpoint: URL, serverVersion: string, factory?: RemoteClientFactory);
57
+ listTools(token?: string): Promise<RemoteToolDescriptor[]>;
58
+ callTool(name: string, args: Record<string, unknown>, token: string): Promise<RemoteToolCallResult>;
59
+ }
package/dist/proxy.js ADDED
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RemoteToolProxy = void 0;
4
+ exports.parseRemoteToolEnvelope = parseRemoteToolEnvelope;
5
+ exports.extractRemoteTextContent = extractRemoteTextContent;
6
+ exports.buildRemoteToolCallResult = buildRemoteToolCallResult;
7
+ exports.mergeRemoteTools = mergeRemoteTools;
8
+ exports.createSdkRemoteClient = createSdkRemoteClient;
9
+ /**
10
+ * mcp-server remote-tool proxy.
11
+ *
12
+ * The local MCP server is intentionally a transparent proxy: it discovers every
13
+ * tool from the remote /mcp endpoint and forwards calls without a local
14
+ * allowlist. This module keeps that behavior testable without starting stdio,
15
+ * connecting to the network, or touching OAuth state.
16
+ */
17
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
18
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
19
+ function isRemoteToolErrorType(value) {
20
+ return value === "oauth_invalid"
21
+ || value === "shop_auth_invalid"
22
+ || value === "rate_limited"
23
+ || value === "quota_exhausted"
24
+ || value === "invalid_input"
25
+ || value === "permission_denied"
26
+ || value === "upstream_error";
27
+ }
28
+ /** Parse the public QueryData/error envelope without interpreting business data. */
29
+ function parseRemoteToolEnvelope(text) {
30
+ try {
31
+ const parsed = JSON.parse(text);
32
+ if (parsed.ok === true && Object.prototype.hasOwnProperty.call(parsed, "data")) {
33
+ return { ok: true, data: parsed.data };
34
+ }
35
+ if (parsed.ok === false
36
+ && isRemoteToolErrorType(parsed.error_type)
37
+ && typeof parsed.user_message === "string"
38
+ && typeof parsed.retryable === "boolean") {
39
+ return {
40
+ ok: false,
41
+ error_type: parsed.error_type,
42
+ user_message: parsed.user_message,
43
+ retryable: parsed.retryable,
44
+ ...(Object.prototype.hasOwnProperty.call(parsed, "upstream") ? { upstream: parsed.upstream } : {}),
45
+ };
46
+ }
47
+ return null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /** Extract text blocks in remote order; non-text blocks are left untouched. */
54
+ function extractRemoteTextContent(content) {
55
+ if (!Array.isArray(content))
56
+ return [];
57
+ return content.flatMap((item) => {
58
+ if (item
59
+ && typeof item === "object"
60
+ && item.type === "text"
61
+ && typeof item.text === "string") {
62
+ return [item.text];
63
+ }
64
+ return [];
65
+ });
66
+ }
67
+ /** Build the same raw text and envelope the proxy returns to the MCP client. */
68
+ function buildRemoteToolCallResult(content) {
69
+ const textBlocks = extractRemoteTextContent(content);
70
+ const rawText = textBlocks.length > 0
71
+ ? textBlocks.join("\n\n")
72
+ : JSON.stringify(content, null, 2);
73
+ const envelope = textBlocks
74
+ .map((text) => parseRemoteToolEnvelope(text))
75
+ .find((candidate) => candidate !== null)
76
+ ?? parseRemoteToolEnvelope(rawText);
77
+ return { rawText, envelope };
78
+ }
79
+ function toDescriptor(tool) {
80
+ return {
81
+ name: tool.name,
82
+ ...(tool.description !== undefined ? { description: tool.description } : {}),
83
+ inputSchema: tool.inputSchema,
84
+ };
85
+ }
86
+ /**
87
+ * Merge local tools with the remote list without filtering, renaming or
88
+ * rewriting schemas. Remote order is preserved after the two local tools.
89
+ */
90
+ function mergeRemoteTools(localTools, remoteTools) {
91
+ return [
92
+ ...localTools.map((tool) => ({ ...tool })),
93
+ ...remoteTools.map((tool) => ({ ...tool })),
94
+ ];
95
+ }
96
+ /** Default SDK-backed client factory. Tests inject a fake factory instead. */
97
+ function createSdkRemoteClient(endpoint, token, serverVersion) {
98
+ const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(endpoint, {
99
+ requestInit: token ? { headers: { Authorization: `Bearer ${token}` } } : undefined,
100
+ });
101
+ const client = new index_js_1.Client({ name: "scorehub-mcp", version: serverVersion }, { capabilities: {} });
102
+ return {
103
+ connect: () => client.connect(transport),
104
+ listTools: async () => {
105
+ const result = await client.listTools();
106
+ return { tools: result.tools.map(toDescriptor) };
107
+ },
108
+ callTool: async (request) => {
109
+ const result = await client.callTool(request);
110
+ return { content: "content" in result ? result.content : result };
111
+ },
112
+ close: () => client.close(),
113
+ };
114
+ }
115
+ /**
116
+ * One short-lived remote client per list/call, matching the existing proxy
117
+ * lifecycle. The factory seam is the only test-specific difference.
118
+ */
119
+ class RemoteToolProxy {
120
+ endpoint;
121
+ serverVersion;
122
+ factory;
123
+ constructor(endpoint, serverVersion, factory = createSdkRemoteClient) {
124
+ this.endpoint = endpoint;
125
+ this.serverVersion = serverVersion;
126
+ this.factory = factory;
127
+ }
128
+ async listTools(token) {
129
+ const client = this.factory(this.endpoint, token, this.serverVersion);
130
+ try {
131
+ await client.connect();
132
+ const result = await client.listTools();
133
+ return result.tools.map((tool) => ({
134
+ name: tool.name,
135
+ ...(tool.description !== undefined ? { description: tool.description } : {}),
136
+ inputSchema: tool.inputSchema,
137
+ }));
138
+ }
139
+ finally {
140
+ await client.close();
141
+ }
142
+ }
143
+ async callTool(name, args, token) {
144
+ const client = this.factory(this.endpoint, token, this.serverVersion);
145
+ try {
146
+ await client.connect();
147
+ const result = await client.callTool({ name, arguments: args });
148
+ return buildRemoteToolCallResult(result.content);
149
+ }
150
+ finally {
151
+ await client.close();
152
+ }
153
+ }
154
+ }
155
+ exports.RemoteToolProxy = RemoteToolProxy;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorehub/mcp-server",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
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"