@scorehub/mcp-server 0.4.1 → 0.4.2

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/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
  }
@@ -51,68 +50,11 @@ const TOKEN_STORE_KEY = endpointTokenKey(SCOREHUB_BASE_URL);
51
50
  const CLIENT_ID = "scorehub-mcp";
52
51
  // Proactively refresh token every 4 hours to prevent expiry during long sessions
53
52
  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
53
  // ── Remote Tool Proxy (MCP Streamable HTTP) ─────────────────
54
+ const remoteProxy = new proxy_js_1.RemoteToolProxy(new URL(REMOTE_MCP_URL), SERVER_VERSION);
102
55
  async function fetchRemoteTools(token) {
103
56
  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
- }
57
+ return await remoteProxy.listTools(token);
116
58
  }
117
59
  catch (e) {
118
60
  const msg = e instanceof Error ? e.message : String(e);
@@ -121,27 +63,8 @@ async function fetchRemoteTools(token) {
121
63
  return [];
122
64
  }
123
65
  }
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
- }
66
+ function callRemoteTool(name, args, token) {
67
+ return remoteProxy.callTool(name, args, token);
145
68
  }
146
69
  async function startShopSwitch(accessToken, request) {
147
70
  const response = await fetch(SHOP_SWITCH_ENDPOINT, {
@@ -182,7 +105,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
182
105
  }
183
106
  }
184
107
  catch { /* keep stale tools and lastError */ }
185
- const tools = [
108
+ const localTools = [
186
109
  {
187
110
  name: "authorize",
188
111
  description: "检查 ScoreHub OAuth 授权。force=true 重新登录以切换账号;switch_shop=true 打开当前账号的店铺选择页,无需重新登录。",
@@ -210,13 +133,8 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
210
133
  required: [],
211
134
  },
212
135
  },
213
- ...remoteTools.map((t) => ({
214
- name: t.name,
215
- description: t.description,
216
- inputSchema: t.inputSchema,
217
- })),
218
136
  ];
219
- return { tools };
137
+ return { tools: (0, proxy_js_1.mergeRemoteTools)(localTools, remoteTools) };
220
138
  });
221
139
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
222
140
  const { name, arguments: args = {} } = request.params;
@@ -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.2",
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"