@scorehub/mcp-server 0.4.0 → 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/README.en.md +4 -4
- package/README.md +4 -4
- package/dist/index.js +7 -89
- package/dist/pages/callback-pages.js +20 -9
- package/dist/proxy.d.ts +59 -0
- package/dist/proxy.js +155 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -62,7 +62,7 @@ Common configuration locations are:
|
|
|
62
62
|
| Other MCP clients | The MCP configuration file used by that client |
|
|
63
63
|
| WorkBuddy | `mcp.json` |
|
|
64
64
|
|
|
65
|
-
On the first launch, `npx` downloads `@scorehub/mcp-server` and connects it to the remote endpoint. When you first call a remote tool, invoke `authorize` when prompted by your client. A browser opens the ScoreHub login page; after authorization it returns to a random `127.0.0.1` port and stores the token locally. Later calls reuse the cached token and do not require another login.
|
|
65
|
+
On the first launch, `npx` downloads `@scorehub/mcp-server` and connects it to the remote endpoint. When you first call a remote tool, invoke `authorize` when prompted by your client. A browser opens the ScoreHub login page; after authorization it returns to a random `127.0.0.1` port and stores the token locally. In WorkBuddy, the successful callback page can launch Tiky through “Return to WorkBuddy.” Later calls reuse the cached token and do not require another login.
|
|
66
66
|
|
|
67
67
|
> After you install Tiky through the WorkBuddy share link, Tiky's first-turn bootstrap gate writes or repairs this MCP configuration after the user's one-time confirmation. Manual editing is not required.
|
|
68
68
|
|
|
@@ -78,7 +78,7 @@ The command reports the package version, Node.js version, and executable entrypo
|
|
|
78
78
|
|
|
79
79
|
## Windows
|
|
80
80
|
|
|
81
|
-
Windows uses the same
|
|
81
|
+
For manually configured MCP clients, Windows uses the same `"command": "npx"` configuration shown above as macOS and Linux; you do not need to switch it to `npx.cmd` or install the package globally. Tiky's public WorkBuddy installation path generates a managed configuration instead: it uses WorkBuddy's bundled `node.exe` to load the matching npm `npx-cli.js` directly, avoiding host process-launch compatibility problems with `.cmd` / `.bat` files.
|
|
82
82
|
|
|
83
83
|
1. After installing Node.js 18+, reopen PowerShell or Windows Terminal and run the three version commands from the prerequisites section.
|
|
84
84
|
2. If `node`, `npm`, or `npx` cannot be found, Node.js is not available on the current process PATH. Close and reopen the client and terminal; if the problem remains, verify the Node.js installation.
|
|
@@ -86,7 +86,7 @@ Windows uses the same MCP JSON configuration as macOS and Linux. You do not need
|
|
|
86
86
|
4. Authorization uses PowerShell `Start-Process` to open your default browser, so OAuth URL parameters such as `redirect_uri` are preserved. If system policy blocks it, copy the authorization URL printed in the terminal and open it on the **same Windows computer that runs mcp-server**.
|
|
87
87
|
5. The callback always uses a random `127.0.0.1` port. Do not open the authorization URL on another computer. If local security software blocks the loopback callback, allow local loopback connections and try again.
|
|
88
88
|
|
|
89
|
-
If WorkBuddy uses its bundled Node.js, install Tiky through the share link first, then let Tiky run `npx -y @scorehub/creator-outreach@latest` to complete the local bootstrap. That flow
|
|
89
|
+
If WorkBuddy uses its bundled Node.js, install Tiky through the share link first, then let Tiky run `npx -y @scorehub/creator-outreach@latest` to complete the local bootstrap. That flow resolves the managed `node.exe`, `npx-cli.js`, and PATH automatically; no wrapper script or manual `mcp.json` edit is required.
|
|
90
90
|
|
|
91
91
|
## Optional: Global Installation
|
|
92
92
|
|
|
@@ -183,7 +183,7 @@ Local mcp-server ScoreHub Remote MCP
|
|
|
183
183
|
| `rate_limited` / `quota_exhausted` | Wait, split the operation into batches, or narrow the request. Do not reauthorize for rate limits. |
|
|
184
184
|
| `invalid_input` | Correct the tool parameters; do not start authorization. |
|
|
185
185
|
| Browser does not open automatically | Use the authorization URL printed in the terminal and complete the callback on the same machine that runs mcp-server. |
|
|
186
|
-
| The authorization page says the link is invalid, or the browser URL has no `redirect_uri` | Fully quit and restart WorkBuddy,
|
|
186
|
+
| The authorization page says the link is invalid, or the browser URL has no `redirect_uri` | Fully quit and restart WorkBuddy, then confirm the `scorehub` entry in `~/.workbuddy/mcp.json` still includes `@scorehub/mcp-server@latest` and is managed by `@scorehub/creator-outreach`. If not, rerun Tiky's bootstrap repair. Newer versions open the OAuth URL through PowerShell to avoid truncating query parameters. |
|
|
187
187
|
|
|
188
188
|
Key `status` fields:
|
|
189
189
|
|
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ npx --version
|
|
|
62
62
|
| 其他 MCP 客户端 | 该客户端使用的 MCP 配置文件 |
|
|
63
63
|
| WorkBuddy | `mcp.json` |
|
|
64
64
|
|
|
65
|
-
首次启动时,`npx` 会下载 `@scorehub/mcp-server` 并连接远程端点。首次调用远程工具时,按客户端提示调用 `authorize`:浏览器会打开 ScoreHub 登录页,授权完成后回调到 `127.0.0.1` 的随机端口并写入本地 Token
|
|
65
|
+
首次启动时,`npx` 会下载 `@scorehub/mcp-server` 并连接远程端点。首次调用远程工具时,按客户端提示调用 `authorize`:浏览器会打开 ScoreHub 登录页,授权完成后回调到 `127.0.0.1` 的随机端口并写入本地 Token。在 WorkBuddy 中,成功回调页可通过“切换到终端继续”唤起 Tiky;之后会复用缓存,不需要每次登录。
|
|
66
66
|
|
|
67
67
|
> 通过 WorkBuddy 分享链接安装 Tiky 后,Tiky 会在首轮 bootstrap 门禁中经用户首次确认后自动写入或修复这份 MCP 配置;无需手动编辑。
|
|
68
68
|
|
|
@@ -78,7 +78,7 @@ npx -y @scorehub/mcp-server@latest --self-check --json
|
|
|
78
78
|
|
|
79
79
|
## Windows 使用说明
|
|
80
80
|
|
|
81
|
-
Windows 与 macOS、Linux
|
|
81
|
+
手工配置普通 MCP 客户端时,Windows 与 macOS、Linux 一样使用前文的 `"command": "npx"`,不需要手工改成 `npx.cmd` 或另行全局安装。WorkBuddy 的公开安装路径由 Tiky 自动生成配置:它会使用 WorkBuddy 托管的 `node.exe` 直接加载同版本 npm 的 `npx-cli.js`,避免宿主原生进程 API 直接启动 `.cmd` / `.bat` 的兼容问题。
|
|
82
82
|
|
|
83
83
|
1. 安装 Node.js 18+ 后,重新打开 PowerShell 或 Windows Terminal,运行前置条件中的三个版本命令。
|
|
84
84
|
2. 如果提示找不到 `node`、`npm` 或 `npx`,说明 Node.js 尚未加入当前进程的 PATH:关闭并重新打开客户端和终端;仍无法识别时,检查 Node.js 安装是否完成。
|
|
@@ -86,7 +86,7 @@ Windows 与 macOS、Linux 使用同一份 MCP JSON 配置,不需要改成 `npx
|
|
|
86
86
|
4. 授权时会优先使用 `cmd.exe /c start` 打开默认浏览器。被系统策略拦截时,复制终端输出的授权 URL,在**运行 mcp-server 的同一台 Windows 电脑**上打开并完成授权。
|
|
87
87
|
5. 回调固定使用 `127.0.0.1` 随机端口。不要在另一台电脑打开授权 URL;本机安全软件若拦截回环回调,应允许本地回环连接后重试。
|
|
88
88
|
|
|
89
|
-
WorkBuddy 若使用其捆绑的 Node.js,建议先通过分享链接安装 Tiky,再由 Tiky 执行 `npx -y @scorehub/creator-outreach@latest`
|
|
89
|
+
WorkBuddy 若使用其捆绑的 Node.js,建议先通过分享链接安装 Tiky,再由 Tiky 执行 `npx -y @scorehub/creator-outreach@latest` 完成本地引导;该流程会自动解析托管的 `node.exe`、`npx-cli.js` 与 PATH,不需要创建包装脚本或手工编辑 `mcp.json`。
|
|
90
90
|
|
|
91
91
|
## 可选:全局安装
|
|
92
92
|
|
|
@@ -183,7 +183,7 @@ Token 持久化在本机文件中;调用默认 ScoreHub 生产端点时,mcp-
|
|
|
183
183
|
| `rate_limited` / `quota_exhausted` | 等待、分批执行或缩小请求范围;不要为了限流重新授权。 |
|
|
184
184
|
| `invalid_input` | 修正工具参数,不走授权流程。 |
|
|
185
185
|
| 浏览器无法自动打开 | 使用终端输出的授权 URL,并在运行 mcp-server 的同一台机器上完成回调。 |
|
|
186
|
-
| Windows 授权页提示授权链接无效,或浏览器地址栏缺少 `redirect_uri` | 完全退出并重启 WorkBuddy,确认 `~/.workbuddy/mcp.json`
|
|
186
|
+
| Windows 授权页提示授权链接无效,或浏览器地址栏缺少 `redirect_uri` | 完全退出并重启 WorkBuddy,确认 `~/.workbuddy/mcp.json` 的 `scorehub` 参数仍包含 `@scorehub/mcp-server@latest`,且配置由 `@scorehub/creator-outreach` 托管;若不符合,重新运行 Tiky bootstrap 修复。新版会通过 PowerShell 打开授权 URL,避免 `cmd.exe start` 截断带 `&` 的参数。 |
|
|
187
187
|
|
|
188
188
|
`status` 的关键字段说明:
|
|
189
189
|
|
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;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildCallbackPage = buildCallbackPage;
|
|
4
4
|
const logo_data_js_1 = require("./logo-data.js");
|
|
5
|
+
const WORKBUDDY_LAUNCH_URL = "workbuddy://launch?expertname=Tiky%20%C2%B7%20TikTok%E8%BE%BE%E4%BA%BA%E8%90%A5%E9%94%80%E4%B8%93%E5%AE%B6&buddy_type=workbuddy";
|
|
5
6
|
const CALLBACK_BASE_STYLES = `
|
|
6
7
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
7
8
|
body {
|
|
@@ -191,6 +192,9 @@ const CALLBACK_BASE_STYLES = `
|
|
|
191
192
|
margin-top:22px;
|
|
192
193
|
}
|
|
193
194
|
.btn {
|
|
195
|
+
display:flex;
|
|
196
|
+
align-items:center;
|
|
197
|
+
justify-content:center;
|
|
194
198
|
min-height:44px;
|
|
195
199
|
padding:10px 16px;
|
|
196
200
|
border-radius:12px;
|
|
@@ -198,6 +202,7 @@ const CALLBACK_BASE_STYLES = `
|
|
|
198
202
|
font-weight:800;
|
|
199
203
|
cursor:pointer;
|
|
200
204
|
border:1px solid transparent;
|
|
205
|
+
text-decoration:none;
|
|
201
206
|
transition:background 0.15s, border-color 0.15s, box-shadow 0.15s, transform 0.15s;
|
|
202
207
|
}
|
|
203
208
|
.btn:hover { transform:translateY(-1px); }
|
|
@@ -267,6 +272,19 @@ function escapeHtml(value) {
|
|
|
267
272
|
}
|
|
268
273
|
function buildCallbackPage(page) {
|
|
269
274
|
const statusClass = page.tone === "success" ? "status-badge-success" : "status-badge-error";
|
|
275
|
+
const primaryAction = page.tone === "success"
|
|
276
|
+
? `<a class="btn btn-primary" id="workbuddy-launch-link" href="${WORKBUDDY_LAUNCH_URL}">切换到终端继续</a>`
|
|
277
|
+
: `<button class="btn btn-primary" type="button" id="focus-button">切回终端继续</button>`;
|
|
278
|
+
const focusScript = page.tone === "success"
|
|
279
|
+
? ""
|
|
280
|
+
: `
|
|
281
|
+
const focusButton = document.getElementById('focus-button');
|
|
282
|
+
focusButton?.addEventListener('click', function() {
|
|
283
|
+
window.close();
|
|
284
|
+
setTimeout(function() {
|
|
285
|
+
window.focus();
|
|
286
|
+
}, 60);
|
|
287
|
+
});`;
|
|
270
288
|
const detailBlock = page.detail
|
|
271
289
|
? `<div class="detail-card">
|
|
272
290
|
<span class="section-title">错误详情</span>
|
|
@@ -326,7 +344,7 @@ function buildCallbackPage(page) {
|
|
|
326
344
|
</div>
|
|
327
345
|
|
|
328
346
|
<div class="actions">
|
|
329
|
-
|
|
347
|
+
${primaryAction}
|
|
330
348
|
<button class="btn btn-secondary" type="button" id="close-button">${escapeHtml(page.closeLabel ?? "关闭当前页面")}</button>
|
|
331
349
|
</div>
|
|
332
350
|
<div class="helper-note">${escapeHtml(page.helperText ?? "如果浏览器无法自动关闭,请手动切回终端继续。")}</div>
|
|
@@ -334,18 +352,11 @@ function buildCallbackPage(page) {
|
|
|
334
352
|
</div>
|
|
335
353
|
<script>
|
|
336
354
|
const closeButton = document.getElementById('close-button');
|
|
337
|
-
const focusButton = document.getElementById('focus-button');
|
|
338
355
|
|
|
339
356
|
closeButton?.addEventListener('click', function() {
|
|
340
357
|
window.close();
|
|
341
358
|
});
|
|
342
|
-
|
|
343
|
-
focusButton?.addEventListener('click', function() {
|
|
344
|
-
window.close();
|
|
345
|
-
setTimeout(function() {
|
|
346
|
-
window.focus();
|
|
347
|
-
}, 60);
|
|
348
|
-
});
|
|
359
|
+
${focusScript}
|
|
349
360
|
</script>
|
|
350
361
|
</body>
|
|
351
362
|
</html>`;
|
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;
|