@lessie/mcp-server 0.1.1 → 0.1.3
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.js +117 -11
- package/dist/config.js +1 -1
- package/dist/index.js +4 -4
- package/dist/remote.js +123 -72
- package/dist/tools.js +1 -1
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -13,13 +13,24 @@
|
|
|
13
13
|
* 客户端信息和令牌持久化到 ~/.lessie/oauth.json,进程重启后复用,
|
|
14
14
|
* 避免每次都打开浏览器。
|
|
15
15
|
*/
|
|
16
|
+
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
16
17
|
import { createServer } from "node:http";
|
|
17
18
|
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
18
19
|
import { join } from "node:path";
|
|
19
20
|
import { homedir } from "node:os";
|
|
21
|
+
import { REMOTE_MCP_URL } from "./config.js";
|
|
20
22
|
export const DEFAULT_CALLBACK_PORT = 19836;
|
|
21
23
|
export const PORT_SCAN_RANGE = 10;
|
|
22
|
-
|
|
24
|
+
function getStorageDir() {
|
|
25
|
+
try {
|
|
26
|
+
const host = new URL(REMOTE_MCP_URL).hostname;
|
|
27
|
+
return join(homedir(), ".lessie", host);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return join(homedir(), ".lessie");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const STORAGE_DIR = getStorageDir();
|
|
23
34
|
const STORAGE_FILE = join(STORAGE_DIR, "oauth.json");
|
|
24
35
|
const CALLBACK_TIMEOUT_MS = 120_000;
|
|
25
36
|
function tryListenHttp(port) {
|
|
@@ -56,6 +67,10 @@ export class LessieAuthProvider {
|
|
|
56
67
|
_httpServer = null;
|
|
57
68
|
_callbackTimer = null;
|
|
58
69
|
_callbackPort;
|
|
70
|
+
_authCompletion = null;
|
|
71
|
+
_authFetchFn;
|
|
72
|
+
_onAuthorizationComplete = null;
|
|
73
|
+
_onAuthorizationFailure = null;
|
|
59
74
|
/** 等待用户访问的授权 URL;授权完成后清除 */
|
|
60
75
|
pendingAuthUrl = null;
|
|
61
76
|
/** 回调服务器开始监听的时间戳,用于计算等待时长 */
|
|
@@ -68,6 +83,24 @@ export class LessieAuthProvider {
|
|
|
68
83
|
get callbackPort() {
|
|
69
84
|
return this._callbackPort;
|
|
70
85
|
}
|
|
86
|
+
/** 注册 completion 成功回调(授权码已换 token) */
|
|
87
|
+
setOnAuthorizationComplete(cb) {
|
|
88
|
+
this._onAuthorizationComplete = cb;
|
|
89
|
+
}
|
|
90
|
+
/** 注册 completion 失败回调(超时、拒绝授权、回调服务错误等) */
|
|
91
|
+
setOnAuthorizationFailure(cb) {
|
|
92
|
+
this._onAuthorizationFailure = cb;
|
|
93
|
+
}
|
|
94
|
+
/** 设置 auth() 的可选 fetch 实现(用于调试日志等) */
|
|
95
|
+
setAuthFetchFn(fetchFn) {
|
|
96
|
+
this._authFetchFn = fetchFn;
|
|
97
|
+
}
|
|
98
|
+
/** 等待 provider 内部 completion 结束(成功或失败) */
|
|
99
|
+
async waitForAuthorizationCompletion() {
|
|
100
|
+
if (!this._authCompletion)
|
|
101
|
+
return;
|
|
102
|
+
await this._authCompletion.catch(() => { });
|
|
103
|
+
}
|
|
71
104
|
/**
|
|
72
105
|
* 扫描端口范围,创建 HTTP 服务器占住可用端口。
|
|
73
106
|
* redirectToAuthorization 直接复用该服务器,无需释放重听。
|
|
@@ -131,13 +164,26 @@ export class LessieAuthProvider {
|
|
|
131
164
|
}
|
|
132
165
|
/**
|
|
133
166
|
* SDK 在需要用户授权时调用此方法。
|
|
134
|
-
*
|
|
167
|
+
* 在监听中的 HTTP 服务器上挂载回调处理逻辑,进入 pending 授权状态。
|
|
135
168
|
*
|
|
136
|
-
*
|
|
169
|
+
* 为兼容 SDK 在普通工具调用路径中触发的重授权,这里会在必要时自动启动回调服务器,
|
|
170
|
+
* 并启动 provider 内部 completion 任务(等待 code -> 换 token)。
|
|
137
171
|
*/
|
|
138
172
|
async redirectToAuthorization(url) {
|
|
173
|
+
// 并发 auth() 调用可能重复进入 redirect。若已有 pending 授权,复用现有流程,
|
|
174
|
+
// 避免覆盖 _callbackPromise 导致原 completion 永久等待。
|
|
175
|
+
if (this._authCompletion) {
|
|
176
|
+
if (this.pendingAuthUrl && this._callbackPromise) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await this.waitForAuthorizationCompletion();
|
|
180
|
+
}
|
|
139
181
|
if (!this._httpServer?.listening) {
|
|
140
|
-
|
|
182
|
+
await this.prepareCallbackServer();
|
|
183
|
+
}
|
|
184
|
+
const server = this._httpServer;
|
|
185
|
+
if (!server?.listening) {
|
|
186
|
+
throw new Error("Callback server is not listening");
|
|
141
187
|
}
|
|
142
188
|
this._expectedState = url.searchParams.get("state") || "";
|
|
143
189
|
this.waitingSince = null;
|
|
@@ -147,13 +193,15 @@ export class LessieAuthProvider {
|
|
|
147
193
|
resolveCode = resolve;
|
|
148
194
|
rejectCode = reject;
|
|
149
195
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
196
|
+
server.removeAllListeners("request");
|
|
197
|
+
server.removeAllListeners("error");
|
|
198
|
+
server.on("error", (err) => {
|
|
153
199
|
this._cleanupServer();
|
|
154
|
-
|
|
200
|
+
this.pendingAuthUrl = null;
|
|
201
|
+
const authError = new Error(`Callback server error: ${err.message}`);
|
|
202
|
+
rejectCode(authError);
|
|
155
203
|
});
|
|
156
|
-
|
|
204
|
+
server.on("request", (req, res) => {
|
|
157
205
|
const reqUrl = new URL(req.url, `http://127.0.0.1:${this._callbackPort}`);
|
|
158
206
|
if (reqUrl.pathname !== "/callback") {
|
|
159
207
|
res.writeHead(404);
|
|
@@ -180,21 +228,26 @@ export class LessieAuthProvider {
|
|
|
180
228
|
}
|
|
181
229
|
else {
|
|
182
230
|
const error = reqUrl.searchParams.get("error") ?? "unknown_error";
|
|
183
|
-
|
|
231
|
+
const authError = new Error(`Authorization denied: ${error}`);
|
|
232
|
+
rejectCode(authError);
|
|
184
233
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
185
234
|
res.end("<!DOCTYPE html><html><body>" +
|
|
186
235
|
"<h1>Authorization Failed</h1>" +
|
|
187
236
|
`<p>${escapeHtml(error)}</p>` +
|
|
188
237
|
"</body></html>");
|
|
189
238
|
this._cleanupServer();
|
|
239
|
+
this.pendingAuthUrl = null;
|
|
190
240
|
}
|
|
191
241
|
});
|
|
192
242
|
this.waitingSince = Date.now();
|
|
193
243
|
this._callbackTimer = setTimeout(() => {
|
|
194
244
|
this._cleanupServer();
|
|
195
|
-
|
|
245
|
+
this.pendingAuthUrl = null;
|
|
246
|
+
const timeoutError = new Error("Authorization timed out — no callback received within 2 minutes");
|
|
247
|
+
rejectCode(timeoutError);
|
|
196
248
|
}, CALLBACK_TIMEOUT_MS);
|
|
197
249
|
this.pendingAuthUrl = url.toString();
|
|
250
|
+
this._ensureAuthorizationCompletion();
|
|
198
251
|
}
|
|
199
252
|
_cleanupServer() {
|
|
200
253
|
if (this._callbackTimer) {
|
|
@@ -208,6 +261,59 @@ export class LessieAuthProvider {
|
|
|
208
261
|
setTimeout(() => server.close(), 500);
|
|
209
262
|
}
|
|
210
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* provider 内部 completion 状态机:
|
|
266
|
+
* - 等待浏览器回调 code
|
|
267
|
+
* - 用 code 交换 token
|
|
268
|
+
* - 通过 Complete/Error 事件通知外层
|
|
269
|
+
*/
|
|
270
|
+
_ensureAuthorizationCompletion() {
|
|
271
|
+
if (this._authCompletion)
|
|
272
|
+
return;
|
|
273
|
+
const serverUrl = REMOTE_MCP_URL;
|
|
274
|
+
if (!serverUrl) {
|
|
275
|
+
const err = new Error("LESSIE_REMOTE_MCP_URL is not configured");
|
|
276
|
+
this.pendingAuthUrl = null;
|
|
277
|
+
this._notifyAuthorizationFailure(err);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this._authCompletion = (async () => {
|
|
281
|
+
try {
|
|
282
|
+
const code = await this.waitForCallback();
|
|
283
|
+
await auth(this, { serverUrl, authorizationCode: code, fetchFn: this._authFetchFn });
|
|
284
|
+
this.pendingAuthUrl = null;
|
|
285
|
+
this._notifyAuthorizationComplete();
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
this.pendingAuthUrl = null;
|
|
289
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
290
|
+
this._notifyAuthorizationFailure(err);
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
this._callbackPromise = null;
|
|
295
|
+
this._authCompletion = null;
|
|
296
|
+
}
|
|
297
|
+
})();
|
|
298
|
+
// completion 主要由外层 wait/event 消费,这里兜底避免未处理拒绝告警
|
|
299
|
+
void this._authCompletion.catch(() => { });
|
|
300
|
+
}
|
|
301
|
+
_notifyAuthorizationComplete() {
|
|
302
|
+
try {
|
|
303
|
+
this._onAuthorizationComplete?.();
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
console.warn("[lessie] onAuthorizationComplete callback failed:", err);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
_notifyAuthorizationFailure(error) {
|
|
310
|
+
try {
|
|
311
|
+
this._onAuthorizationFailure?.(error);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
console.warn("[lessie] onAuthorizationFailure callback failed:", err);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
211
317
|
/**
|
|
212
318
|
* 等待用户在浏览器中完成授权后回调,返回授权码。
|
|
213
319
|
* 必须在 redirectToAuthorization 之后调用。
|
package/dist/config.js
CHANGED
|
@@ -16,7 +16,7 @@ function readPkgVersion() {
|
|
|
16
16
|
}
|
|
17
17
|
export const pkg = { version: readPkgVersion() };
|
|
18
18
|
/** 远程 MCP Server 地址 */
|
|
19
|
-
export const REMOTE_MCP_URL = process.env.LESSIE_REMOTE_MCP_URL || "https://
|
|
19
|
+
export const REMOTE_MCP_URL = process.env.LESSIE_REMOTE_MCP_URL || "https://app.lessie.ai/mcp-server/mcp";
|
|
20
20
|
/** 读取 SKILL.md 作为 MCP instructions,跳过 YAML front-matter */
|
|
21
21
|
export function loadInstructions() {
|
|
22
22
|
try {
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import "./process-handlers.js";
|
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
11
|
import { REMOTE_MCP_URL, pkg, loadInstructions } from "./config.js";
|
|
12
|
-
import { connectToRemote, listRemoteTools,
|
|
12
|
+
import { connectToRemote, listRemoteTools, setOnAuthComplete, setOnAuthError } from "./remote.js";
|
|
13
13
|
import { registerTools } from "./tools.js";
|
|
14
14
|
console.error("[lessie] Modules loaded");
|
|
15
15
|
const server = new McpServer({ name: "lessie-mcp", version: pkg.version }, {
|
|
@@ -45,9 +45,9 @@ setOnAuthError(({ code, message }) => {
|
|
|
45
45
|
});
|
|
46
46
|
console.error("[lessie] Connecting to remote...");
|
|
47
47
|
try {
|
|
48
|
-
await connectToRemote();
|
|
49
|
-
console.error("[lessie] connectToRemote() completed, connected:",
|
|
50
|
-
if (
|
|
48
|
+
const connected = await connectToRemote();
|
|
49
|
+
console.error("[lessie] connectToRemote() completed, connected:", connected);
|
|
50
|
+
if (connected) {
|
|
51
51
|
const remoteTools = await listRemoteTools();
|
|
52
52
|
server.sendLoggingMessage({
|
|
53
53
|
level: "info",
|
package/dist/remote.js
CHANGED
|
@@ -34,8 +34,8 @@ const debugFetch = async (input, init) => {
|
|
|
34
34
|
const fetchFn = DEBUG ? debugFetch : undefined;
|
|
35
35
|
let client = null;
|
|
36
36
|
let cachedTools = [];
|
|
37
|
-
|
|
38
|
-
let
|
|
37
|
+
// 授权码换 token 完成后,remote 侧需要做一次连接同步(重连 + 刷新工具)。
|
|
38
|
+
let postAuthSync = null;
|
|
39
39
|
let onAuthComplete = null;
|
|
40
40
|
let onAuthError = null;
|
|
41
41
|
let lastAuthError = null;
|
|
@@ -50,25 +50,36 @@ export function setOnAuthError(cb) {
|
|
|
50
50
|
export function isRemoteConnected() {
|
|
51
51
|
return client !== null;
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
53
|
+
/** 等待授权 provider 完成,并等待 remote 侧连接同步完成。 */
|
|
54
54
|
export async function waitForAuthCompletion() {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
await authProvider.waitForAuthorizationCompletion();
|
|
56
|
+
if (postAuthSync)
|
|
57
|
+
await postAuthSync;
|
|
58
|
+
}
|
|
59
|
+
/** 获取当前已连接客户端的工具列表并刷新缓存。 */
|
|
60
|
+
async function listConnectedTools() {
|
|
61
|
+
if (!client)
|
|
62
|
+
throw new Error("Remote client is not connected");
|
|
63
|
+
const { tools } = await client.listTools();
|
|
64
|
+
cachedTools = tools;
|
|
65
|
+
return tools;
|
|
57
66
|
}
|
|
58
67
|
/** 启动时静默连接:有缓存令牌才尝试,无令牌或连接失败则跳过 */
|
|
59
68
|
export async function connectToRemote() {
|
|
60
69
|
if (!REMOTE_MCP_URL)
|
|
61
|
-
return;
|
|
70
|
+
return false;
|
|
62
71
|
if (!(await authProvider.tokens()))
|
|
63
|
-
return;
|
|
72
|
+
return false;
|
|
64
73
|
const url = new URL(REMOTE_MCP_URL);
|
|
65
74
|
try {
|
|
66
75
|
client = await tryConnect(url);
|
|
67
76
|
const { tools } = await client.listTools();
|
|
68
77
|
cachedTools = tools;
|
|
78
|
+
return true;
|
|
69
79
|
}
|
|
70
80
|
catch {
|
|
71
81
|
client = null;
|
|
82
|
+
return false;
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
/**
|
|
@@ -81,15 +92,12 @@ export async function connectToRemote() {
|
|
|
81
92
|
* error — 授权流程出错(附带错误码和引导信息)
|
|
82
93
|
*/
|
|
83
94
|
export async function initiateAuth() {
|
|
84
|
-
if (client) {
|
|
85
|
-
const tools = await listRemoteTools();
|
|
86
|
-
return { status: "connected", toolCount: tools.length };
|
|
87
|
-
}
|
|
88
95
|
if (!REMOTE_MCP_URL) {
|
|
89
96
|
return { status: "error", errorCode: "not_configured", message: "LESSIE_REMOTE_MCP_URL is not configured" };
|
|
90
97
|
}
|
|
98
|
+
const serverUrl = REMOTE_MCP_URL;
|
|
91
99
|
// 授权流程进行中:告诉 Agent 当前等待状态
|
|
92
|
-
if (authProvider.pendingAuthUrl
|
|
100
|
+
if (authProvider.pendingAuthUrl) {
|
|
93
101
|
const elapsed = authProvider.waitingSince
|
|
94
102
|
? Date.now() - authProvider.waitingSince
|
|
95
103
|
: 0;
|
|
@@ -99,62 +107,93 @@ export async function initiateAuth() {
|
|
|
99
107
|
elapsedMs: elapsed,
|
|
100
108
|
};
|
|
101
109
|
}
|
|
110
|
+
// 若上一次授权流程刚完成,先等待 remote 侧连接同步,避免与当前 authorize 并发竞争。
|
|
111
|
+
await waitForAuthCompletion();
|
|
102
112
|
// 上次异步错误(超时、授权拒绝等),记录后清除
|
|
103
113
|
const prevError = lastAuthError;
|
|
104
114
|
lastAuthError = null;
|
|
105
|
-
const serverUrl = REMOTE_MCP_URL;
|
|
106
115
|
try {
|
|
107
|
-
await authProvider.prepareCallbackServer();
|
|
108
116
|
const result = await auth(authProvider, { serverUrl, fetchFn });
|
|
109
|
-
if (result === "
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return {
|
|
117
|
+
if (result === "REDIRECT") {
|
|
118
|
+
const authUrl = authProvider.pendingAuthUrl;
|
|
119
|
+
if (!authUrl) {
|
|
120
|
+
return { status: "error", errorCode: "no_auth_url", message: "OAuth flow initiated but no authorization URL was generated" };
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
status: "auth_url",
|
|
124
|
+
authUrl,
|
|
125
|
+
...(prevError ? { previousError: prevError.message } : {}),
|
|
126
|
+
};
|
|
115
127
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return { status: "error", errorCode: "no_auth_url", message: "OAuth flow initiated but no authorization URL was generated" };
|
|
128
|
+
if (result !== "AUTHORIZED") {
|
|
129
|
+
return { status: "error", errorCode: "unexpected_auth_result", message: `Unexpected auth result: ${result}` };
|
|
119
130
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
// 只有在 auth() 已确认 AUTHORIZED 后,才认为当前会话可用。
|
|
132
|
+
if (client) {
|
|
133
|
+
try {
|
|
134
|
+
const tools = await listConnectedTools();
|
|
135
|
+
return { status: "connected", toolCount: tools.length };
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// 连接对象已失效:清理后重建连接。
|
|
139
|
+
const old = client;
|
|
140
|
+
client = null;
|
|
141
|
+
await old?.close().catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const nextClient = await tryConnect(new URL(serverUrl));
|
|
145
|
+
client = nextClient;
|
|
146
|
+
const tools = await listConnectedTools();
|
|
147
|
+
return { status: "connected", toolCount: tools.length };
|
|
126
148
|
}
|
|
127
149
|
catch (err) {
|
|
128
150
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
129
151
|
return { status: "error", errorCode: categorizeAuthError(error), message: error.message };
|
|
130
152
|
}
|
|
131
153
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
// provider 负责 OAuth completion;remote 只监听完成/失败事件并同步连接状态。
|
|
155
|
+
authProvider.setAuthFetchFn(fetchFn);
|
|
156
|
+
authProvider.setOnAuthorizationComplete(() => {
|
|
157
|
+
syncRemoteAfterAuthorization();
|
|
158
|
+
});
|
|
159
|
+
authProvider.setOnAuthorizationFailure((error) => {
|
|
160
|
+
const code = categorizeAuthError(error);
|
|
161
|
+
lastAuthError = { code, message: error.message };
|
|
162
|
+
onAuthError?.({ code, message: error.message });
|
|
163
|
+
});
|
|
164
|
+
function syncRemoteAfterAuthorization() {
|
|
165
|
+
if (postAuthSync)
|
|
135
166
|
return;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
167
|
+
if (!REMOTE_MCP_URL)
|
|
168
|
+
return;
|
|
169
|
+
const url = new URL(REMOTE_MCP_URL);
|
|
170
|
+
postAuthSync = (async () => {
|
|
139
171
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
172
|
+
const old = client;
|
|
173
|
+
let next = null;
|
|
174
|
+
try {
|
|
175
|
+
next = await tryConnect(url);
|
|
176
|
+
const { tools } = await next.listTools();
|
|
177
|
+
client = next;
|
|
178
|
+
next = null;
|
|
179
|
+
cachedTools = tools;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
// 新连接初始化失败时,关闭半初始化连接,避免泄漏。
|
|
183
|
+
await next?.close().catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
await old?.close().catch(() => { });
|
|
146
186
|
onAuthComplete?.(cachedTools.length);
|
|
147
187
|
}
|
|
148
188
|
catch (e) {
|
|
149
|
-
authProvider.pendingAuthUrl = null;
|
|
150
189
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
151
190
|
const code = categorizeAuthError(error);
|
|
152
191
|
lastAuthError = { code, message: error.message };
|
|
153
192
|
onAuthError?.({ code, message: error.message });
|
|
154
|
-
console.error("[lessie]
|
|
193
|
+
console.error("[lessie] Post-authorization sync failed:", e);
|
|
155
194
|
}
|
|
156
195
|
finally {
|
|
157
|
-
|
|
196
|
+
postAuthSync = null;
|
|
158
197
|
}
|
|
159
198
|
})();
|
|
160
199
|
}
|
|
@@ -191,8 +230,7 @@ export async function listRemoteTools() {
|
|
|
191
230
|
}
|
|
192
231
|
/** 转发工具调用到远程 MCP Server;失败时引导用户重新授权 */
|
|
193
232
|
export async function callRemoteTool(name, args) {
|
|
194
|
-
|
|
195
|
-
await authCompletion;
|
|
233
|
+
await waitForAuthCompletion();
|
|
196
234
|
if (!client) {
|
|
197
235
|
return {
|
|
198
236
|
content: [
|
|
@@ -208,32 +246,65 @@ export async function callRemoteTool(name, args) {
|
|
|
208
246
|
return (await client.callTool({ name, arguments: args }));
|
|
209
247
|
}
|
|
210
248
|
catch (err) {
|
|
211
|
-
if (
|
|
249
|
+
if (authProvider.pendingAuthUrl) {
|
|
212
250
|
return {
|
|
213
251
|
content: [
|
|
214
252
|
{
|
|
215
253
|
type: "text",
|
|
216
|
-
text:
|
|
254
|
+
text: buildReauthPrompt(name),
|
|
217
255
|
},
|
|
218
256
|
],
|
|
219
257
|
isError: true,
|
|
220
258
|
};
|
|
221
259
|
}
|
|
222
|
-
|
|
223
|
-
if (!client) {
|
|
260
|
+
if (!isAuthError(err)) {
|
|
224
261
|
return {
|
|
225
262
|
content: [
|
|
226
263
|
{
|
|
227
264
|
type: "text",
|
|
228
|
-
text: `工具 ${name}
|
|
265
|
+
text: `工具 ${name} 调用失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
229
266
|
},
|
|
230
267
|
],
|
|
231
268
|
isError: true,
|
|
232
269
|
};
|
|
233
270
|
}
|
|
234
|
-
return
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: buildReauthPrompt(name),
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
isError: true,
|
|
279
|
+
};
|
|
235
280
|
}
|
|
236
281
|
}
|
|
282
|
+
function buildReauthPrompt(toolName) {
|
|
283
|
+
const authUrl = authProvider.pendingAuthUrl;
|
|
284
|
+
if (!authUrl) {
|
|
285
|
+
return [
|
|
286
|
+
`工具 ${toolName} 需要重新授权。`,
|
|
287
|
+
"",
|
|
288
|
+
"请按以下步骤操作:",
|
|
289
|
+
"1. 调用 authorize 工具,生成新的授权链接",
|
|
290
|
+
"2. 在浏览器中完成授权流程",
|
|
291
|
+
"3. 授权完成后,重新调用当前工具",
|
|
292
|
+
].join("\n");
|
|
293
|
+
}
|
|
294
|
+
return [
|
|
295
|
+
`工具 ${toolName} 需要重新授权。`,
|
|
296
|
+
"",
|
|
297
|
+
"请按以下步骤操作:",
|
|
298
|
+
"1. 在浏览器中打开并完成以下授权链接:",
|
|
299
|
+
"",
|
|
300
|
+
authUrl,
|
|
301
|
+
"",
|
|
302
|
+
"2. 完成授权后,调用 authorize 工具确认连接状态",
|
|
303
|
+
"3. 重新调用当前工具",
|
|
304
|
+
"",
|
|
305
|
+
"如果链接失效,请再次调用 authorize 工具生成新链接。",
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
237
308
|
function isAuthError(err) {
|
|
238
309
|
const obj = err;
|
|
239
310
|
const status = obj?.status ?? obj?.statusCode;
|
|
@@ -245,23 +316,3 @@ function isAuthError(err) {
|
|
|
245
316
|
}
|
|
246
317
|
return false;
|
|
247
318
|
}
|
|
248
|
-
/**
|
|
249
|
-
* 重连,关闭旧连接;并发调用共享同一个 Promise 避免竞争。
|
|
250
|
-
* 静默尝试连接,若令牌已失效则 client 保持 null。
|
|
251
|
-
*/
|
|
252
|
-
async function reconnect() {
|
|
253
|
-
if (reconnecting)
|
|
254
|
-
return reconnecting;
|
|
255
|
-
reconnecting = (async () => {
|
|
256
|
-
try {
|
|
257
|
-
const old = client;
|
|
258
|
-
client = null;
|
|
259
|
-
await old?.close().catch(() => { });
|
|
260
|
-
await connectToRemote();
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
reconnecting = null;
|
|
264
|
-
}
|
|
265
|
-
})();
|
|
266
|
-
return reconnecting;
|
|
267
|
-
}
|
package/dist/tools.js
CHANGED
|
@@ -51,7 +51,7 @@ function formatAuthResult(result) {
|
|
|
51
51
|
return {
|
|
52
52
|
content: [{
|
|
53
53
|
type: "text",
|
|
54
|
-
text: `已连接到远程 MCP 服务器,共发现 ${result.toolCount}
|
|
54
|
+
text: `已连接到远程 MCP 服务器,共发现 ${result.toolCount} 个远程工具。\n\n可通过 use_lessie 工具调用远程功能:不传 tool 参数可列出所有工具,传入 tool 和 arguments 可调用指定工具。若后续调用提示未授权,请重新执行 authorize。`,
|
|
55
55
|
}],
|
|
56
56
|
};
|
|
57
57
|
case "auth_url": {
|