@lessie/mcp-server 0.1.0 → 0.1.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/auth.js +107 -10
- package/dist/cli.js +2 -0
- 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 +2 -2
- package/mcpb/.mcpbignore +0 -4
- package/mcpb/README.md +0 -47
- package/mcpb/lessie-mcp-20260320-221003.mcpb +0 -0
- package/mcpb/lessie-mcp-20260320-232505.mcpb +0 -0
- package/mcpb/lessie-mcp-20260320-232558.mcpb +0 -0
- package/mcpb/manifest.json +0 -45
- package/mcpb/pack.sh +0 -36
package/dist/auth.js
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
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
|
const STORAGE_DIR = join(homedir(), ".lessie");
|
|
@@ -56,6 +58,10 @@ export class LessieAuthProvider {
|
|
|
56
58
|
_httpServer = null;
|
|
57
59
|
_callbackTimer = null;
|
|
58
60
|
_callbackPort;
|
|
61
|
+
_authCompletion = null;
|
|
62
|
+
_authFetchFn;
|
|
63
|
+
_onAuthorizationComplete = null;
|
|
64
|
+
_onAuthorizationFailure = null;
|
|
59
65
|
/** 等待用户访问的授权 URL;授权完成后清除 */
|
|
60
66
|
pendingAuthUrl = null;
|
|
61
67
|
/** 回调服务器开始监听的时间戳,用于计算等待时长 */
|
|
@@ -68,6 +74,24 @@ export class LessieAuthProvider {
|
|
|
68
74
|
get callbackPort() {
|
|
69
75
|
return this._callbackPort;
|
|
70
76
|
}
|
|
77
|
+
/** 注册 completion 成功回调(授权码已换 token) */
|
|
78
|
+
setOnAuthorizationComplete(cb) {
|
|
79
|
+
this._onAuthorizationComplete = cb;
|
|
80
|
+
}
|
|
81
|
+
/** 注册 completion 失败回调(超时、拒绝授权、回调服务错误等) */
|
|
82
|
+
setOnAuthorizationFailure(cb) {
|
|
83
|
+
this._onAuthorizationFailure = cb;
|
|
84
|
+
}
|
|
85
|
+
/** 设置 auth() 的可选 fetch 实现(用于调试日志等) */
|
|
86
|
+
setAuthFetchFn(fetchFn) {
|
|
87
|
+
this._authFetchFn = fetchFn;
|
|
88
|
+
}
|
|
89
|
+
/** 等待 provider 内部 completion 结束(成功或失败) */
|
|
90
|
+
async waitForAuthorizationCompletion() {
|
|
91
|
+
if (!this._authCompletion)
|
|
92
|
+
return;
|
|
93
|
+
await this._authCompletion.catch(() => { });
|
|
94
|
+
}
|
|
71
95
|
/**
|
|
72
96
|
* 扫描端口范围,创建 HTTP 服务器占住可用端口。
|
|
73
97
|
* redirectToAuthorization 直接复用该服务器,无需释放重听。
|
|
@@ -131,13 +155,26 @@ export class LessieAuthProvider {
|
|
|
131
155
|
}
|
|
132
156
|
/**
|
|
133
157
|
* SDK 在需要用户授权时调用此方法。
|
|
134
|
-
*
|
|
158
|
+
* 在监听中的 HTTP 服务器上挂载回调处理逻辑,进入 pending 授权状态。
|
|
135
159
|
*
|
|
136
|
-
*
|
|
160
|
+
* 为兼容 SDK 在普通工具调用路径中触发的重授权,这里会在必要时自动启动回调服务器,
|
|
161
|
+
* 并启动 provider 内部 completion 任务(等待 code -> 换 token)。
|
|
137
162
|
*/
|
|
138
163
|
async redirectToAuthorization(url) {
|
|
164
|
+
// 并发 auth() 调用可能重复进入 redirect。若已有 pending 授权,复用现有流程,
|
|
165
|
+
// 避免覆盖 _callbackPromise 导致原 completion 永久等待。
|
|
166
|
+
if (this._authCompletion) {
|
|
167
|
+
if (this.pendingAuthUrl && this._callbackPromise) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await this.waitForAuthorizationCompletion();
|
|
171
|
+
}
|
|
139
172
|
if (!this._httpServer?.listening) {
|
|
140
|
-
|
|
173
|
+
await this.prepareCallbackServer();
|
|
174
|
+
}
|
|
175
|
+
const server = this._httpServer;
|
|
176
|
+
if (!server?.listening) {
|
|
177
|
+
throw new Error("Callback server is not listening");
|
|
141
178
|
}
|
|
142
179
|
this._expectedState = url.searchParams.get("state") || "";
|
|
143
180
|
this.waitingSince = null;
|
|
@@ -147,13 +184,15 @@ export class LessieAuthProvider {
|
|
|
147
184
|
resolveCode = resolve;
|
|
148
185
|
rejectCode = reject;
|
|
149
186
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
server.removeAllListeners("request");
|
|
188
|
+
server.removeAllListeners("error");
|
|
189
|
+
server.on("error", (err) => {
|
|
153
190
|
this._cleanupServer();
|
|
154
|
-
|
|
191
|
+
this.pendingAuthUrl = null;
|
|
192
|
+
const authError = new Error(`Callback server error: ${err.message}`);
|
|
193
|
+
rejectCode(authError);
|
|
155
194
|
});
|
|
156
|
-
|
|
195
|
+
server.on("request", (req, res) => {
|
|
157
196
|
const reqUrl = new URL(req.url, `http://127.0.0.1:${this._callbackPort}`);
|
|
158
197
|
if (reqUrl.pathname !== "/callback") {
|
|
159
198
|
res.writeHead(404);
|
|
@@ -180,21 +219,26 @@ export class LessieAuthProvider {
|
|
|
180
219
|
}
|
|
181
220
|
else {
|
|
182
221
|
const error = reqUrl.searchParams.get("error") ?? "unknown_error";
|
|
183
|
-
|
|
222
|
+
const authError = new Error(`Authorization denied: ${error}`);
|
|
223
|
+
rejectCode(authError);
|
|
184
224
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
185
225
|
res.end("<!DOCTYPE html><html><body>" +
|
|
186
226
|
"<h1>Authorization Failed</h1>" +
|
|
187
227
|
`<p>${escapeHtml(error)}</p>` +
|
|
188
228
|
"</body></html>");
|
|
189
229
|
this._cleanupServer();
|
|
230
|
+
this.pendingAuthUrl = null;
|
|
190
231
|
}
|
|
191
232
|
});
|
|
192
233
|
this.waitingSince = Date.now();
|
|
193
234
|
this._callbackTimer = setTimeout(() => {
|
|
194
235
|
this._cleanupServer();
|
|
195
|
-
|
|
236
|
+
this.pendingAuthUrl = null;
|
|
237
|
+
const timeoutError = new Error("Authorization timed out — no callback received within 2 minutes");
|
|
238
|
+
rejectCode(timeoutError);
|
|
196
239
|
}, CALLBACK_TIMEOUT_MS);
|
|
197
240
|
this.pendingAuthUrl = url.toString();
|
|
241
|
+
this._ensureAuthorizationCompletion();
|
|
198
242
|
}
|
|
199
243
|
_cleanupServer() {
|
|
200
244
|
if (this._callbackTimer) {
|
|
@@ -208,6 +252,59 @@ export class LessieAuthProvider {
|
|
|
208
252
|
setTimeout(() => server.close(), 500);
|
|
209
253
|
}
|
|
210
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* provider 内部 completion 状态机:
|
|
257
|
+
* - 等待浏览器回调 code
|
|
258
|
+
* - 用 code 交换 token
|
|
259
|
+
* - 通过 Complete/Error 事件通知外层
|
|
260
|
+
*/
|
|
261
|
+
_ensureAuthorizationCompletion() {
|
|
262
|
+
if (this._authCompletion)
|
|
263
|
+
return;
|
|
264
|
+
const serverUrl = REMOTE_MCP_URL;
|
|
265
|
+
if (!serverUrl) {
|
|
266
|
+
const err = new Error("LESSIE_REMOTE_MCP_URL is not configured");
|
|
267
|
+
this.pendingAuthUrl = null;
|
|
268
|
+
this._notifyAuthorizationFailure(err);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this._authCompletion = (async () => {
|
|
272
|
+
try {
|
|
273
|
+
const code = await this.waitForCallback();
|
|
274
|
+
await auth(this, { serverUrl, authorizationCode: code, fetchFn: this._authFetchFn });
|
|
275
|
+
this.pendingAuthUrl = null;
|
|
276
|
+
this._notifyAuthorizationComplete();
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
this.pendingAuthUrl = null;
|
|
280
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
281
|
+
this._notifyAuthorizationFailure(err);
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
this._callbackPromise = null;
|
|
286
|
+
this._authCompletion = null;
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
// completion 主要由外层 wait/event 消费,这里兜底避免未处理拒绝告警
|
|
290
|
+
void this._authCompletion.catch(() => { });
|
|
291
|
+
}
|
|
292
|
+
_notifyAuthorizationComplete() {
|
|
293
|
+
try {
|
|
294
|
+
this._onAuthorizationComplete?.();
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
console.warn("[lessie] onAuthorizationComplete callback failed:", err);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
_notifyAuthorizationFailure(error) {
|
|
301
|
+
try {
|
|
302
|
+
this._onAuthorizationFailure?.(error);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
console.warn("[lessie] onAuthorizationFailure callback failed:", err);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
211
308
|
/**
|
|
212
309
|
* 等待用户在浏览器中完成授权后回调,返回授权码。
|
|
213
310
|
* 必须在 redirectToAuthorization 之后调用。
|
package/dist/cli.js
ADDED
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": {
|
package/package.json
CHANGED
package/mcpb/.mcpbignore
DELETED
package/mcpb/README.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# MCPB 打包
|
|
2
|
-
|
|
3
|
-
将 Lessie MCP Server 打包为 `.mcpb` 文件,供 Claude Desktop 用户一键安装。
|
|
4
|
-
|
|
5
|
-
## 打包
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# 在项目根目录执行
|
|
9
|
-
npm run build
|
|
10
|
-
npm run mcpb:pack
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
生成的 `.mcpb` 文件在当前目录下。
|
|
14
|
-
|
|
15
|
-
## 安装方式
|
|
16
|
-
|
|
17
|
-
用户拿到 `.mcpb` 文件后:
|
|
18
|
-
|
|
19
|
-
- **双击**文件自动打开 Claude Desktop
|
|
20
|
-
- **拖拽**到 Claude Desktop 窗口
|
|
21
|
-
- Claude Desktop 菜单:Developer → Extensions → Install Extension
|
|
22
|
-
|
|
23
|
-
## manifest.json 说明
|
|
24
|
-
|
|
25
|
-
| 字段 | 说明 |
|
|
26
|
-
|---|---|
|
|
27
|
-
| `server.type` | `node` — Claude Desktop 自带 Node.js 运行时,用户无需额外安装 |
|
|
28
|
-
| `server.entry_point` | 指向编译产物 `dist/index.js` |
|
|
29
|
-
| `tools` | 声明本地工具(`authorize`) |
|
|
30
|
-
| `tools_generated` | `true` — 授权后会动态暴露远程工具 |
|
|
31
|
-
| `user_config` | 安装时 Claude Desktop 自动弹出配置界面,用户可自定义远程服务地址 |
|
|
32
|
-
|
|
33
|
-
## 添加图标
|
|
34
|
-
|
|
35
|
-
准备一个 512×512 的 `icon.png` 放到本目录,然后在 `manifest.json` 中添加:
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
"icons": {
|
|
39
|
-
"default": "icon.png"
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## 参考
|
|
44
|
-
|
|
45
|
-
- [MCPB 规范](https://github.com/modelcontextprotocol/mcpb)
|
|
46
|
-
- [Manifest 字段说明](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md)
|
|
47
|
-
- [提交到 Anthropic Directory](https://support.claude.com/en/articles/12922832-local-mcp-server-submission-guide)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/mcpb/manifest.json
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"manifest_version": "0.3",
|
|
3
|
-
"name": "lessie-mcp-server",
|
|
4
|
-
"display_name": "Lessie",
|
|
5
|
-
"version": "0.0.8",
|
|
6
|
-
"description": "Connect Claude to your Lessie account. Authorize via OAuth and access remote Lessie tools directly in conversations.",
|
|
7
|
-
"author": {
|
|
8
|
-
"name": "Lessie"
|
|
9
|
-
},
|
|
10
|
-
"server": {
|
|
11
|
-
"type": "node",
|
|
12
|
-
"entry_point": "dist/index.js",
|
|
13
|
-
"mcp_config": {
|
|
14
|
-
"command": "node",
|
|
15
|
-
"args": ["${__dirname}/dist/index.js"],
|
|
16
|
-
"env": {
|
|
17
|
-
"LESSIE_REMOTE_MCP_URL": "${user_config.remote_mcp_url}"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"tools": [
|
|
22
|
-
{
|
|
23
|
-
"name": "authorize",
|
|
24
|
-
"description": "Connect to the remote Lessie service. Returns an authorization link on first use or when authorization expires; returns current status when already connected."
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"name": "use_lessie",
|
|
28
|
-
"description": "Call any remote Lessie tool. Omit the 'tool' parameter to list all available remote tools with their schemas; provide 'tool' and 'arguments' to invoke a specific tool. Requires prior authorization via the 'authorize' tool."
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"tools_generated": true,
|
|
32
|
-
"user_config": {
|
|
33
|
-
"remote_mcp_url": {
|
|
34
|
-
"type": "string",
|
|
35
|
-
"title": "Remote MCP Server URL",
|
|
36
|
-
"description": "The URL of the remote Lessie MCP server",
|
|
37
|
-
"required": false,
|
|
38
|
-
"default": "https://www.lessie.ai/mcp-server/mcp"
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
"compatibility": {
|
|
42
|
-
"platforms": ["darwin", "win32"]
|
|
43
|
-
},
|
|
44
|
-
"license": "MIT"
|
|
45
|
-
}
|
package/mcpb/pack.sh
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
-
|
|
7
|
-
cd "$PROJECT_ROOT"
|
|
8
|
-
|
|
9
|
-
echo "Bundling with esbuild..."
|
|
10
|
-
rm -rf "$SCRIPT_DIR/dist"
|
|
11
|
-
mkdir -p "$SCRIPT_DIR/dist"
|
|
12
|
-
npx esbuild src/index.ts \
|
|
13
|
-
--bundle \
|
|
14
|
-
--platform=node \
|
|
15
|
-
--format=esm \
|
|
16
|
-
--target=node18 \
|
|
17
|
-
--outfile="$SCRIPT_DIR/dist/index.js" \
|
|
18
|
-
--banner:js='#!/usr/bin/env node'
|
|
19
|
-
|
|
20
|
-
echo "Copying runtime assets..."
|
|
21
|
-
cp package.json "$SCRIPT_DIR/package.json"
|
|
22
|
-
cp SKILL.md "$SCRIPT_DIR/SKILL.md" 2>/dev/null || true
|
|
23
|
-
|
|
24
|
-
echo "Packing mcpb..."
|
|
25
|
-
cd "$SCRIPT_DIR"
|
|
26
|
-
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
|
27
|
-
OUTPUT="lessie-mcp-${TIMESTAMP}.mcpb"
|
|
28
|
-
npx @anthropic-ai/mcpb pack . "$OUTPUT"
|
|
29
|
-
|
|
30
|
-
echo "Cleaning up..."
|
|
31
|
-
rm -rf "$SCRIPT_DIR/dist"
|
|
32
|
-
rm -f "$SCRIPT_DIR/package.json"
|
|
33
|
-
rm -f "$SCRIPT_DIR/SKILL.md"
|
|
34
|
-
rm -f "$SCRIPT_DIR/lessie-mcp-server-"*.mcpb
|
|
35
|
-
|
|
36
|
-
echo "Done! Output: $SCRIPT_DIR/$OUTPUT"
|