@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 +7 -89
- package/dist/proxy.d.ts +59 -0
- package/dist/proxy.js +155 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
|
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;
|
package/dist/proxy.d.ts
ADDED
|
@@ -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;
|