@lessie/mcp-server 0.0.8 → 0.1.1
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.md +1 -1
- package/SKILL.md +29 -40
- package/dist/auth.js +129 -90
- package/dist/cli.js +2 -0
- package/dist/config.js +11 -12
- package/dist/index.js +30 -24
- package/dist/process-handlers.js +10 -0
- package/dist/remote.js +103 -28
- package/dist/schema.js +71 -0
- package/dist/tools.js +139 -58
- package/package.json +7 -3
- package/dist/server/auth.js +0 -36
- package/dist/server/db.js +0 -5
- package/dist/server/index.js +0 -11
- package/dist/server/routes/api-keys.js +0 -78
- package/dist/server/routes/auth-token.js +0 -41
- package/docs/oauth-client-guide.md +0 -354
package/README.md
CHANGED
package/SKILL.md
CHANGED
|
@@ -1,62 +1,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lessie-mcp
|
|
3
3
|
description: >-
|
|
4
|
-
Lessie MCP
|
|
5
|
-
|
|
4
|
+
Lessie MCP 使用指南:授权流程与工具调用。
|
|
5
|
+
当需要连接 Lessie 服务或使用 Lessie 工具时参考。
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Lessie MCP
|
|
9
9
|
|
|
10
|
-
Lessie MCP
|
|
10
|
+
Lessie MCP 连接远程 Lessie 服务并暴露其工具,使用前需完成 OAuth 授权。
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## 首次使用
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
首次使用时,Agent 应主动调用 `authorize` 工具发起 OAuth 授权:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
1. 调用 `authorize` → 获取授权链接
|
|
17
|
+
2. 将授权链接展示给用户,请用户在浏览器中打开完成登录
|
|
18
|
+
3. 用户完成授权后,再次调用 `authorize` 确认连接状态
|
|
19
|
+
4. 授权成功后,通过 `use_lessie` 工具调用远程功能
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
2. 将返回的授权链接展示给用户,请用户在浏览器中打开
|
|
20
|
-
3. 用户在浏览器中完成登录授权
|
|
21
|
-
4. 授权完成后,远程工具自动可用,无需再次调用 `authorize`
|
|
21
|
+
如果 `authorize` 返回已授权状态,则跳过上述流程,直接使用 `use_lessie` 或已暴露的远程工具。
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
## 异常处理
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
`authorize` 工具会返回详细的诊断信息和修复建议,Agent 应按照提示操作:
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
- **端口占用**(`port_in_use`):回调服务器端口被占用。按提示帮用户排查占用进程,然后重新调用 `authorize`。
|
|
28
|
+
- **授权超时**(`timeout`):2 分钟内未收到浏览器回调。重新调用 `authorize` 生成新链接,提醒用户及时完成授权。
|
|
29
|
+
- **用户拒绝授权**(`auth_denied`):重新调用 `authorize` 并引导用户在浏览器中允许授权。
|
|
30
|
+
- **等待中**(`waiting`):授权流程进行中。提醒用户在浏览器中完成操作,无需重新发起。
|
|
31
|
+
- **其他错误**:按返回的具体建议操作,通常重新调用 `authorize` 即可重试。
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
当收到 logging 级别的授权失败通知时,应主动告知用户并建议重新调用 `authorize`。
|
|
30
34
|
|
|
31
|
-
##
|
|
32
|
-
|
|
33
|
-
| 工具 | 说明 |
|
|
34
|
-
| ------------------ | ------------------------------------------------------------------ |
|
|
35
|
-
| `authorize` | 发起授权或检查连接状态。未授权时返回授权链接,已连接时返回状态信息 |
|
|
36
|
-
| `get_access_token` | 获取当前 OAuth access token 及剩余有效时间 |
|
|
37
|
-
|
|
38
|
-
## 项目架构(开发参考)
|
|
39
|
-
|
|
40
|
-
### 鉴权:OAuth Authorization Code + PKCE
|
|
35
|
+
## 使用远程工具
|
|
41
36
|
|
|
42
|
-
|
|
37
|
+
授权完成后,通过 `use_lessie` 工具调用所有远程功能:
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- `waitForCallback()` 在 `127.0.0.1:19836` 监听回调,等待用户完成授权
|
|
39
|
+
1. 调用 `use_lessie`(不传参数)→ 列出所有可用的远程工具及参数说明
|
|
40
|
+
2. 调用 `use_lessie(tool="工具名", arguments={...})` → 调用指定的远程工具
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
如果远程工具直接出现在工具列表中(已授权的回访用户),也可以直接调用,无需通过 `use_lessie`。
|
|
49
43
|
|
|
50
|
-
`
|
|
44
|
+
收到"远程 MCP 服务器未连接"错误时,说明授权已过期,需重新调用 `authorize` 完成授权。
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
- `initiateAuth()` — 发起 OAuth 流程,返回授权 URL,后台等待回调自动完成连接
|
|
54
|
-
- `callRemoteTool()` — 授权进行中自动等待;未连接时返回错误引导用户调用 `authorize`
|
|
55
|
-
|
|
56
|
-
传输层优先使用 Streamable HTTP,不可用时自动降级到 SSE。
|
|
57
|
-
|
|
58
|
-
### 环境变量
|
|
46
|
+
## 本地工具
|
|
59
47
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| `
|
|
48
|
+
| 工具 | 说明 |
|
|
49
|
+
| ----------- | ------------------------------------------------------------------------------------------ |
|
|
50
|
+
| `authorize` | 发起授权或检查连接状态。返回连接状态、授权链接、等待状态或错误诊断信息(含修复建议) |
|
|
51
|
+
| `use_lessie` | 远程工具统一入口。不传 `tool` 列出所有远程工具;传入 `tool` 和 `arguments` 调用指定工具 |
|
package/dist/auth.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* 实现 MCP SDK 的 OAuthClientProvider 接口:
|
|
5
5
|
* 1. 首次连接时通过 RFC 7591 动态注册客户端(POST /register)
|
|
6
6
|
* 2. 打开浏览器引导用户到 SaaS 页面登录并授权
|
|
7
|
-
* 3. 在 localhost
|
|
7
|
+
* 3. 在 localhost HTTP 服务器上接收授权码回调
|
|
8
8
|
* 4. SDK 自动用授权码交换 access_token(POST /token)
|
|
9
9
|
*
|
|
10
|
+
* 端口策略:prepareCallbackServer() 扫描端口范围并创建 HTTP 服务器占住可用端口,
|
|
11
|
+
* redirectToAuthorization() 直接在该服务器上挂载回调处理逻辑,避免竞争窗口。
|
|
12
|
+
*
|
|
10
13
|
* 客户端信息和令牌持久化到 ~/.lessie/oauth.json,进程重启后复用,
|
|
11
14
|
* 避免每次都打开浏览器。
|
|
12
15
|
*/
|
|
@@ -14,12 +17,22 @@ import { createServer } from "node:http";
|
|
|
14
17
|
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
15
18
|
import { join } from "node:path";
|
|
16
19
|
import { homedir } from "node:os";
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const REDIRECT_URL = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
|
|
20
|
+
export const DEFAULT_CALLBACK_PORT = 19836;
|
|
21
|
+
export const PORT_SCAN_RANGE = 10;
|
|
20
22
|
const STORAGE_DIR = join(homedir(), ".lessie");
|
|
21
23
|
const STORAGE_FILE = join(STORAGE_DIR, "oauth.json");
|
|
22
24
|
const CALLBACK_TIMEOUT_MS = 120_000;
|
|
25
|
+
function tryListenHttp(port) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const srv = createServer((_req, res) => {
|
|
28
|
+
res.writeHead(404);
|
|
29
|
+
res.end();
|
|
30
|
+
});
|
|
31
|
+
srv.once("error", () => resolve(null));
|
|
32
|
+
srv.once("listening", () => resolve(srv));
|
|
33
|
+
srv.listen(port, "127.0.0.1");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
23
36
|
// JSON 损坏时返回空对象,用户需重新授权(损坏数据无法恢复)
|
|
24
37
|
function loadStorage() {
|
|
25
38
|
try {
|
|
@@ -38,24 +51,57 @@ function persistStorage(data) {
|
|
|
38
51
|
export class LessieAuthProvider {
|
|
39
52
|
_storage;
|
|
40
53
|
_codeVerifier = "";
|
|
54
|
+
_expectedState = "";
|
|
41
55
|
_callbackPromise = null;
|
|
42
56
|
_httpServer = null;
|
|
43
|
-
|
|
57
|
+
_callbackTimer = null;
|
|
58
|
+
_callbackPort;
|
|
44
59
|
/** 等待用户访问的授权 URL;授权完成后清除 */
|
|
45
60
|
pendingAuthUrl = null;
|
|
61
|
+
/** 回调服务器开始监听的时间戳,用于计算等待时长 */
|
|
62
|
+
waitingSince = null;
|
|
46
63
|
constructor() {
|
|
47
64
|
this._storage = loadStorage();
|
|
48
|
-
this.
|
|
65
|
+
this._codeVerifier = this._storage.codeVerifier || "";
|
|
66
|
+
this._callbackPort = this._storage.callbackPort ?? DEFAULT_CALLBACK_PORT;
|
|
67
|
+
}
|
|
68
|
+
get callbackPort() {
|
|
69
|
+
return this._callbackPort;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 扫描端口范围,创建 HTTP 服务器占住可用端口。
|
|
73
|
+
* redirectToAuthorization 直接复用该服务器,无需释放重听。
|
|
74
|
+
* 若端口与上次注册时不同,自动清除客户端信息以触发重新注册。
|
|
75
|
+
*/
|
|
76
|
+
async prepareCallbackServer() {
|
|
77
|
+
if (this._httpServer?.listening)
|
|
78
|
+
return;
|
|
79
|
+
this._cleanupServer();
|
|
80
|
+
for (let offset = 0; offset < PORT_SCAN_RANGE; offset++) {
|
|
81
|
+
const port = DEFAULT_CALLBACK_PORT + offset;
|
|
82
|
+
const server = await tryListenHttp(port);
|
|
83
|
+
if (server) {
|
|
84
|
+
if (this._storage.clientInfo && this._storage.callbackPort != null && this._storage.callbackPort !== port) {
|
|
85
|
+
delete this._storage.clientInfo;
|
|
86
|
+
}
|
|
87
|
+
this._httpServer = server;
|
|
88
|
+
this._callbackPort = port;
|
|
89
|
+
this._storage.callbackPort = port;
|
|
90
|
+
persistStorage(this._storage);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`No available port found in range ${DEFAULT_CALLBACK_PORT}–${DEFAULT_CALLBACK_PORT + PORT_SCAN_RANGE - 1}`);
|
|
49
95
|
}
|
|
50
96
|
get redirectUrl() {
|
|
51
|
-
return
|
|
97
|
+
return `http://127.0.0.1:${this._callbackPort}/callback`;
|
|
52
98
|
}
|
|
53
99
|
get clientMetadata() {
|
|
54
100
|
return {
|
|
55
|
-
redirect_uris: [
|
|
101
|
+
redirect_uris: [this.redirectUrl],
|
|
56
102
|
grant_types: ["authorization_code"],
|
|
57
103
|
response_types: ["code"],
|
|
58
|
-
client_name: "
|
|
104
|
+
client_name: "Lessie MCP",
|
|
59
105
|
token_endpoint_auth_method: "client_secret_basic",
|
|
60
106
|
};
|
|
61
107
|
}
|
|
@@ -72,6 +118,8 @@ export class LessieAuthProvider {
|
|
|
72
118
|
async saveTokens(tokens) {
|
|
73
119
|
this._storage.tokens = tokens;
|
|
74
120
|
this._storage.tokensSavedAt = Date.now();
|
|
121
|
+
delete this._storage.codeVerifier;
|
|
122
|
+
this._codeVerifier = "";
|
|
75
123
|
persistStorage(this._storage);
|
|
76
124
|
}
|
|
77
125
|
/** 当前 access_token 的剩余有效秒数(基于本地时钟估算) */
|
|
@@ -83,61 +131,83 @@ export class LessieAuthProvider {
|
|
|
83
131
|
}
|
|
84
132
|
/**
|
|
85
133
|
* SDK 在需要用户授权时调用此方法。
|
|
86
|
-
*
|
|
134
|
+
* 在 prepareCallbackServer() 已监听的 HTTP 服务器上挂载回调处理逻辑,等待浏览器回调。
|
|
135
|
+
*
|
|
136
|
+
* 必须先调用 prepareCallbackServer(),否则抛出异常。
|
|
87
137
|
*/
|
|
88
138
|
async redirectToAuthorization(url) {
|
|
89
|
-
if (this._httpServer) {
|
|
90
|
-
|
|
91
|
-
this._httpServer = null;
|
|
139
|
+
if (!this._httpServer?.listening) {
|
|
140
|
+
throw new Error("No listening server — prepareCallbackServer() must be called before redirectToAuthorization()");
|
|
92
141
|
}
|
|
142
|
+
this._expectedState = url.searchParams.get("state") || "";
|
|
143
|
+
this.waitingSince = null;
|
|
144
|
+
let resolveCode;
|
|
145
|
+
let rejectCode;
|
|
93
146
|
this._callbackPromise = new Promise((resolve, reject) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
147
|
+
resolveCode = resolve;
|
|
148
|
+
rejectCode = reject;
|
|
149
|
+
});
|
|
150
|
+
this._httpServer.removeAllListeners("request");
|
|
151
|
+
this._httpServer.removeAllListeners("error");
|
|
152
|
+
this._httpServer.on("error", (err) => {
|
|
153
|
+
this._cleanupServer();
|
|
154
|
+
rejectCode(new Error(`Callback server error: ${err.message}`));
|
|
155
|
+
});
|
|
156
|
+
this._httpServer.on("request", (req, res) => {
|
|
157
|
+
const reqUrl = new URL(req.url, `http://127.0.0.1:${this._callbackPort}`);
|
|
158
|
+
if (reqUrl.pathname !== "/callback") {
|
|
159
|
+
res.writeHead(404);
|
|
160
|
+
res.end();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (this._expectedState) {
|
|
164
|
+
const callbackState = reqUrl.searchParams.get("state");
|
|
165
|
+
if (callbackState !== this._expectedState) {
|
|
166
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
167
|
+
res.end("Invalid state parameter");
|
|
105
168
|
return;
|
|
106
169
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
: `Callback server error: ${err.message}`;
|
|
129
|
-
reject(new Error(msg));
|
|
130
|
-
});
|
|
131
|
-
server.listen(CALLBACK_PORT, "127.0.0.1");
|
|
132
|
-
this._httpServer = server;
|
|
133
|
-
timer = setTimeout(() => {
|
|
134
|
-
this._httpServer = null;
|
|
135
|
-
server.close();
|
|
136
|
-
reject(new Error("Authorization timed out — no callback received within 2 minutes"));
|
|
137
|
-
}, CALLBACK_TIMEOUT_MS);
|
|
170
|
+
}
|
|
171
|
+
const code = reqUrl.searchParams.get("code");
|
|
172
|
+
if (code) {
|
|
173
|
+
resolveCode(code);
|
|
174
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
175
|
+
res.end("<!DOCTYPE html><html><body>" +
|
|
176
|
+
"<h1>Authorization Successful</h1>" +
|
|
177
|
+
"<p>You may close this page and return to your agent.</p>" +
|
|
178
|
+
"</body></html>");
|
|
179
|
+
this._cleanupServer();
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const error = reqUrl.searchParams.get("error") ?? "unknown_error";
|
|
183
|
+
rejectCode(new Error(`Authorization denied: ${error}`));
|
|
184
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
185
|
+
res.end("<!DOCTYPE html><html><body>" +
|
|
186
|
+
"<h1>Authorization Failed</h1>" +
|
|
187
|
+
`<p>${escapeHtml(error)}</p>` +
|
|
188
|
+
"</body></html>");
|
|
189
|
+
this._cleanupServer();
|
|
190
|
+
}
|
|
138
191
|
});
|
|
192
|
+
this.waitingSince = Date.now();
|
|
193
|
+
this._callbackTimer = setTimeout(() => {
|
|
194
|
+
this._cleanupServer();
|
|
195
|
+
rejectCode(new Error("Authorization timed out — no callback received within 2 minutes"));
|
|
196
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
139
197
|
this.pendingAuthUrl = url.toString();
|
|
140
198
|
}
|
|
199
|
+
_cleanupServer() {
|
|
200
|
+
if (this._callbackTimer) {
|
|
201
|
+
clearTimeout(this._callbackTimer);
|
|
202
|
+
this._callbackTimer = null;
|
|
203
|
+
}
|
|
204
|
+
if (this._httpServer) {
|
|
205
|
+
const server = this._httpServer;
|
|
206
|
+
this._httpServer = null;
|
|
207
|
+
this.waitingSince = null;
|
|
208
|
+
setTimeout(() => server.close(), 500);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
141
211
|
/**
|
|
142
212
|
* 等待用户在浏览器中完成授权后回调,返回授权码。
|
|
143
213
|
* 必须在 redirectToAuthorization 之后调用。
|
|
@@ -149,16 +219,18 @@ export class LessieAuthProvider {
|
|
|
149
219
|
}
|
|
150
220
|
async saveCodeVerifier(codeVerifier) {
|
|
151
221
|
this._codeVerifier = codeVerifier;
|
|
222
|
+
this._storage.codeVerifier = codeVerifier;
|
|
223
|
+
persistStorage(this._storage);
|
|
152
224
|
}
|
|
153
225
|
async codeVerifier() {
|
|
154
|
-
return this._codeVerifier;
|
|
226
|
+
return this._codeVerifier || this._storage.codeVerifier || "";
|
|
155
227
|
}
|
|
156
228
|
async saveDiscoveryState(state) {
|
|
157
229
|
this._storage.discoveryState = state;
|
|
158
230
|
persistStorage(this._storage);
|
|
159
231
|
}
|
|
160
232
|
discoveryState() {
|
|
161
|
-
return this.
|
|
233
|
+
return this._storage.discoveryState;
|
|
162
234
|
}
|
|
163
235
|
async invalidateCredentials(scope) {
|
|
164
236
|
if (scope === "all" || scope === "client") {
|
|
@@ -180,38 +252,5 @@ export class LessieAuthProvider {
|
|
|
180
252
|
function escapeHtml(s) {
|
|
181
253
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
182
254
|
}
|
|
183
|
-
/**
|
|
184
|
-
* 从环境变量构建 OAuthDiscoveryState。
|
|
185
|
-
* 至少需要配置 authorization_endpoint 和 token_endpoint 才会生效;
|
|
186
|
-
* 未配置则返回 undefined,由 SDK 自动发现。
|
|
187
|
-
*/
|
|
188
|
-
function buildEnvDiscoveryState() {
|
|
189
|
-
if (!OAUTH_AUTHORIZATION_ENDPOINT || !OAUTH_TOKEN_ENDPOINT)
|
|
190
|
-
return undefined;
|
|
191
|
-
const authorizationServerUrl = OAUTH_SERVER_URL || REMOTE_MCP_URL;
|
|
192
|
-
if (!authorizationServerUrl)
|
|
193
|
-
return undefined;
|
|
194
|
-
return {
|
|
195
|
-
authorizationServerUrl,
|
|
196
|
-
authorizationServerMetadata: {
|
|
197
|
-
issuer: authorizationServerUrl,
|
|
198
|
-
authorization_endpoint: OAUTH_AUTHORIZATION_ENDPOINT,
|
|
199
|
-
token_endpoint: OAUTH_TOKEN_ENDPOINT,
|
|
200
|
-
registration_endpoint: OAUTH_REGISTRATION_ENDPOINT,
|
|
201
|
-
response_types_supported: ["code"],
|
|
202
|
-
grant_types_supported: ["authorization_code"],
|
|
203
|
-
code_challenge_methods_supported: ["S256"],
|
|
204
|
-
token_endpoint_auth_methods_supported: [
|
|
205
|
-
"client_secret_basic",
|
|
206
|
-
"client_secret_post",
|
|
207
|
-
"none",
|
|
208
|
-
],
|
|
209
|
-
},
|
|
210
|
-
resourceMetadata: {
|
|
211
|
-
resource: REMOTE_MCP_URL,
|
|
212
|
-
authorization_servers: [authorizationServerUrl],
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
255
|
/** 全局单例,供 remote / tools 共享 */
|
|
217
256
|
export const authProvider = new LessieAuthProvider();
|
package/dist/cli.js
ADDED
package/dist/config.js
CHANGED
|
@@ -5,19 +5,18 @@
|
|
|
5
5
|
* 避免散落的 process.env 访问。
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function readPkgVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf-8");
|
|
11
|
+
return JSON.parse(raw).version;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return "0.0.0";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const pkg = { version: readPkgVersion() };
|
|
11
18
|
/** 远程 MCP Server 地址 */
|
|
12
|
-
export const REMOTE_MCP_URL = process.env.LESSIE_REMOTE_MCP_URL || "https://
|
|
13
|
-
/**
|
|
14
|
-
* OAuth 端点配置(可选)。
|
|
15
|
-
* 未设置时 SDK 通过 RFC 8414 / RFC 9728 自动发现。
|
|
16
|
-
*/
|
|
17
|
-
export const OAUTH_SERVER_URL = process.env.LESSIE_OAUTH_SERVER_URL || "https://s1.jennie.im/sit-api/oauth";
|
|
18
|
-
export const OAUTH_AUTHORIZATION_ENDPOINT = process.env.LESSIE_OAUTH_AUTHORIZATION_ENDPOINT || "https://s1.jennie.im/sit-api/oauth/authorize";
|
|
19
|
-
export const OAUTH_TOKEN_ENDPOINT = process.env.LESSIE_OAUTH_TOKEN_ENDPOINT || "https://s1.jennie.im/sit-api/oauth/token";
|
|
20
|
-
export const OAUTH_REGISTRATION_ENDPOINT = process.env.LESSIE_OAUTH_REGISTRATION_ENDPOINT || "https://s1.jennie.im/sit-api/oauth/register";
|
|
19
|
+
export const REMOTE_MCP_URL = process.env.LESSIE_REMOTE_MCP_URL || "https://www.lessie.ai/mcp-server/mcp";
|
|
21
20
|
/** 读取 SKILL.md 作为 MCP instructions,跳过 YAML front-matter */
|
|
22
21
|
export function loadInstructions() {
|
|
23
22
|
try {
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,28 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
|
-
* 入口:创建 MCP Server
|
|
4
|
-
*
|
|
5
|
-
* 使用低级 Server 而非 McpServer,以便手动控制 tools/list 和 tools/call 路由,
|
|
6
|
-
* 将本地工具与远程代理工具合并到同一个工具列表中返回。
|
|
2
|
+
* 入口:创建 MCP Server,注册工具,启动 stdio 传输。
|
|
7
3
|
*
|
|
8
4
|
* 工具路由策略:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* authorize → 发起 OAuth 授权流程
|
|
6
|
+
* use_lessie → 代理调用远程 MCP Server 上的工具
|
|
11
7
|
*/
|
|
12
|
-
import
|
|
8
|
+
import "./process-handlers.js";
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
15
11
|
import { REMOTE_MCP_URL, pkg, loadInstructions } from "./config.js";
|
|
16
|
-
import { connectToRemote, listRemoteTools,
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
import { connectToRemote, listRemoteTools, isRemoteConnected, setOnAuthComplete, setOnAuthError } from "./remote.js";
|
|
13
|
+
import { registerTools } from "./tools.js";
|
|
14
|
+
console.error("[lessie] Modules loaded");
|
|
15
|
+
const server = new McpServer({ name: "lessie-mcp", version: pkg.version }, {
|
|
16
|
+
capabilities: { tools: { listChanged: true } },
|
|
20
17
|
instructions: loadInstructions(),
|
|
21
18
|
});
|
|
22
|
-
server
|
|
23
|
-
const remoteTools = await listRemoteTools();
|
|
24
|
-
return { tools: [...LOCAL_TOOLS, ...remoteTools] };
|
|
25
|
-
});
|
|
26
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
27
|
-
const { name, arguments: args } = request.params;
|
|
28
|
-
const handler = localHandlers.get(name);
|
|
29
|
-
if (handler)
|
|
30
|
-
return handler(args ?? {});
|
|
31
|
-
return callRemoteTool(name, args);
|
|
32
|
-
});
|
|
19
|
+
registerTools(server);
|
|
33
20
|
// ── 启动 ──────────────────────────────────────────────────────────────────────
|
|
21
|
+
console.error("[lessie] Creating transport...");
|
|
34
22
|
const transport = new StdioServerTransport();
|
|
23
|
+
console.error("[lessie] Connecting server...");
|
|
35
24
|
await server.connect(transport);
|
|
25
|
+
console.error("[lessie] Server connected, setting up auth callback...");
|
|
36
26
|
setOnAuthComplete((toolCount) => {
|
|
37
27
|
server.sendLoggingMessage({
|
|
38
28
|
level: "info",
|
|
@@ -41,8 +31,22 @@ setOnAuthComplete((toolCount) => {
|
|
|
41
31
|
});
|
|
42
32
|
server.sendToolListChanged();
|
|
43
33
|
});
|
|
34
|
+
setOnAuthError(({ code, message }) => {
|
|
35
|
+
const hint = code === "timeout"
|
|
36
|
+
? "Please call the 'authorize' tool again to generate a new authorization link."
|
|
37
|
+
: code === "port_in_use"
|
|
38
|
+
? `Port conflict detected. Check for processes using the callback port, then call 'authorize' to retry.`
|
|
39
|
+
: "Please call the 'authorize' tool to retry.";
|
|
40
|
+
server.sendLoggingMessage({
|
|
41
|
+
level: "error",
|
|
42
|
+
logger: "lessie",
|
|
43
|
+
data: `Authorization failed: ${message}. ${hint}`,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
console.error("[lessie] Connecting to remote...");
|
|
44
47
|
try {
|
|
45
48
|
await connectToRemote();
|
|
49
|
+
console.error("[lessie] connectToRemote() completed, connected:", isRemoteConnected());
|
|
46
50
|
if (isRemoteConnected()) {
|
|
47
51
|
const remoteTools = await listRemoteTools();
|
|
48
52
|
server.sendLoggingMessage({
|
|
@@ -60,12 +64,14 @@ try {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
catch (err) {
|
|
67
|
+
console.error("[lessie] connectToRemote() threw:", err);
|
|
63
68
|
server.sendLoggingMessage({
|
|
64
69
|
level: "error",
|
|
65
70
|
logger: "lessie",
|
|
66
71
|
data: `Failed to connect to remote MCP server: ${err}`,
|
|
67
72
|
});
|
|
68
73
|
}
|
|
74
|
+
console.error("[lessie] Startup complete");
|
|
69
75
|
server.sendLoggingMessage({
|
|
70
76
|
level: "info",
|
|
71
77
|
logger: "lessie",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
process.on("uncaughtException", (err) => {
|
|
2
|
+
console.error("[lessie] Uncaught exception:", err);
|
|
3
|
+
});
|
|
4
|
+
process.on("unhandledRejection", (reason) => {
|
|
5
|
+
console.error("[lessie] Unhandled rejection:", reason);
|
|
6
|
+
});
|
|
7
|
+
process.on("exit", (code) => {
|
|
8
|
+
console.error(`[lessie] Process exiting with code ${code}`);
|
|
9
|
+
});
|
|
10
|
+
export {};
|