@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/dist/remote.js
CHANGED
|
@@ -37,13 +37,24 @@ let cachedTools = [];
|
|
|
37
37
|
let reconnecting = null;
|
|
38
38
|
let authCompletion = null;
|
|
39
39
|
let onAuthComplete = null;
|
|
40
|
+
let onAuthError = null;
|
|
41
|
+
let lastAuthError = null;
|
|
40
42
|
/** 注册授权完成回调(用于发送 logging 通知) */
|
|
41
43
|
export function setOnAuthComplete(cb) {
|
|
42
44
|
onAuthComplete = cb;
|
|
43
45
|
}
|
|
46
|
+
/** 注册授权失败回调(用于主动通知 Agent) */
|
|
47
|
+
export function setOnAuthError(cb) {
|
|
48
|
+
onAuthError = cb;
|
|
49
|
+
}
|
|
44
50
|
export function isRemoteConnected() {
|
|
45
51
|
return client !== null;
|
|
46
52
|
}
|
|
53
|
+
/** 若授权流程正在进行中,等待其完成(成功或失败) */
|
|
54
|
+
export async function waitForAuthCompletion() {
|
|
55
|
+
if (authCompletion)
|
|
56
|
+
await authCompletion;
|
|
57
|
+
}
|
|
47
58
|
/** 启动时静默连接:有缓存令牌才尝试,无令牌或连接失败则跳过 */
|
|
48
59
|
export async function connectToRemote() {
|
|
49
60
|
if (!REMOTE_MCP_URL)
|
|
@@ -61,37 +72,62 @@ export async function connectToRemote() {
|
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
/**
|
|
64
|
-
* 发起 OAuth
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
75
|
+
* 发起 OAuth 授权流程,返回结构化结果。
|
|
76
|
+
*
|
|
77
|
+
* 状态说明:
|
|
78
|
+
* connected — 已连接远程服务器
|
|
79
|
+
* auth_url — 生成了授权链接,等待用户浏览器完成登录
|
|
80
|
+
* waiting — 授权流程进行中,正在等待浏览器回调
|
|
81
|
+
* error — 授权流程出错(附带错误码和引导信息)
|
|
68
82
|
*/
|
|
69
83
|
export async function initiateAuth() {
|
|
70
|
-
if (client)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
if (client) {
|
|
85
|
+
const tools = await listRemoteTools();
|
|
86
|
+
return { status: "connected", toolCount: tools.length };
|
|
87
|
+
}
|
|
88
|
+
if (!REMOTE_MCP_URL) {
|
|
89
|
+
return { status: "error", errorCode: "not_configured", message: "LESSIE_REMOTE_MCP_URL is not configured" };
|
|
90
|
+
}
|
|
91
|
+
// 授权流程进行中:告诉 Agent 当前等待状态
|
|
92
|
+
if (authProvider.pendingAuthUrl && authCompletion) {
|
|
93
|
+
const elapsed = authProvider.waitingSince
|
|
94
|
+
? Date.now() - authProvider.waitingSince
|
|
95
|
+
: 0;
|
|
96
|
+
return {
|
|
97
|
+
status: "waiting",
|
|
98
|
+
authUrl: authProvider.pendingAuthUrl,
|
|
99
|
+
elapsedMs: elapsed,
|
|
100
|
+
};
|
|
78
101
|
}
|
|
102
|
+
// 上次异步错误(超时、授权拒绝等),记录后清除
|
|
103
|
+
const prevError = lastAuthError;
|
|
104
|
+
lastAuthError = null;
|
|
79
105
|
const serverUrl = REMOTE_MCP_URL;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
try {
|
|
107
|
+
await authProvider.prepareCallbackServer();
|
|
108
|
+
const result = await auth(authProvider, { serverUrl, fetchFn });
|
|
109
|
+
if (result === "AUTHORIZED") {
|
|
110
|
+
const url = new URL(serverUrl);
|
|
111
|
+
client = await tryConnect(url);
|
|
112
|
+
const { tools } = await client.listTools();
|
|
113
|
+
cachedTools = tools;
|
|
114
|
+
return { status: "connected", toolCount: tools.length };
|
|
115
|
+
}
|
|
116
|
+
const authUrl = authProvider.pendingAuthUrl;
|
|
117
|
+
if (!authUrl) {
|
|
118
|
+
return { status: "error", errorCode: "no_auth_url", message: "OAuth flow initiated but no authorization URL was generated" };
|
|
119
|
+
}
|
|
120
|
+
ensureAuthCompletion();
|
|
121
|
+
return {
|
|
122
|
+
status: "auth_url",
|
|
123
|
+
authUrl,
|
|
124
|
+
...(prevError ? { previousError: prevError.message } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
129
|
+
return { status: "error", errorCode: categorizeAuthError(error), message: error.message };
|
|
88
130
|
}
|
|
89
|
-
// 'REDIRECT' — 用户需要访问授权 URL
|
|
90
|
-
const authUrl = authProvider.pendingAuthUrl;
|
|
91
|
-
if (!authUrl)
|
|
92
|
-
throw new Error("OAuth flow initiated but no authorization URL was generated");
|
|
93
|
-
ensureAuthCompletion();
|
|
94
|
-
return { authUrl };
|
|
95
131
|
}
|
|
96
132
|
/** 启动后台任务:等待用户浏览器回调 → 交换令牌 → 连接远程服务器 */
|
|
97
133
|
function ensureAuthCompletion() {
|
|
@@ -111,6 +147,10 @@ function ensureAuthCompletion() {
|
|
|
111
147
|
}
|
|
112
148
|
catch (e) {
|
|
113
149
|
authProvider.pendingAuthUrl = null;
|
|
150
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
151
|
+
const code = categorizeAuthError(error);
|
|
152
|
+
lastAuthError = { code, message: error.message };
|
|
153
|
+
onAuthError?.({ code, message: error.message });
|
|
114
154
|
console.error("[lessie] Authorization completion failed:", e);
|
|
115
155
|
}
|
|
116
156
|
finally {
|
|
@@ -118,6 +158,19 @@ function ensureAuthCompletion() {
|
|
|
118
158
|
}
|
|
119
159
|
})();
|
|
120
160
|
}
|
|
161
|
+
function categorizeAuthError(err) {
|
|
162
|
+
const msg = err.message.toLowerCase();
|
|
163
|
+
const code = err.code;
|
|
164
|
+
if (code === "EADDRINUSE" || msg.includes("already in use") || msg.includes("no available port"))
|
|
165
|
+
return "port_in_use";
|
|
166
|
+
if (msg.includes("timed out") || msg.includes("timeout"))
|
|
167
|
+
return "timeout";
|
|
168
|
+
if (msg.includes("authorization denied") || msg.includes("access_denied"))
|
|
169
|
+
return "auth_denied";
|
|
170
|
+
if (msg.includes("callback server"))
|
|
171
|
+
return "server_error";
|
|
172
|
+
return "unknown";
|
|
173
|
+
}
|
|
121
174
|
async function tryConnect(url) {
|
|
122
175
|
const c = new Client({ name: "lessie-mcp-proxy", version: pkg.version });
|
|
123
176
|
await c.connect(new StreamableHTTPClientTransport(url, { authProvider, fetch: fetchFn }));
|
|
@@ -154,14 +207,25 @@ export async function callRemoteTool(name, args) {
|
|
|
154
207
|
try {
|
|
155
208
|
return (await client.callTool({ name, arguments: args }));
|
|
156
209
|
}
|
|
157
|
-
catch {
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (!isAuthError(err)) {
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: `工具 ${name} 调用失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
isError: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
158
222
|
await reconnect();
|
|
159
223
|
if (!client) {
|
|
160
224
|
return {
|
|
161
225
|
content: [
|
|
162
226
|
{
|
|
163
227
|
type: "text",
|
|
164
|
-
text: `工具 ${name}
|
|
228
|
+
text: `工具 ${name} 调用失败(认证过期),请使用 authorize 工具重新登录。`,
|
|
165
229
|
},
|
|
166
230
|
],
|
|
167
231
|
isError: true,
|
|
@@ -170,6 +234,17 @@ export async function callRemoteTool(name, args) {
|
|
|
170
234
|
return (await client.callTool({ name, arguments: args }));
|
|
171
235
|
}
|
|
172
236
|
}
|
|
237
|
+
function isAuthError(err) {
|
|
238
|
+
const obj = err;
|
|
239
|
+
const status = obj?.status ?? obj?.statusCode;
|
|
240
|
+
if (status === 401 || status === 403)
|
|
241
|
+
return true;
|
|
242
|
+
if (err instanceof Error) {
|
|
243
|
+
const msg = err.message.toLowerCase();
|
|
244
|
+
return msg.includes("unauthorized") || msg.includes("forbidden");
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
173
248
|
/**
|
|
174
249
|
* 重连,关闭旧连接;并发调用共享同一个 Promise 避免竞争。
|
|
175
250
|
* 静默尝试连接,若令牌已失效则 client 保持 null。
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema (MCP Tool inputSchema) → Zod shape 转换。
|
|
3
|
+
*
|
|
4
|
+
* MCP 工具的 inputSchema 是 JSON Schema 的受限子集(type: "object"),
|
|
5
|
+
* 这里只处理 properties 中可能出现的常见类型。
|
|
6
|
+
* 不支持的类型回退为 z.unknown()。
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
function convertProperty(prop) {
|
|
10
|
+
const types = Array.isArray(prop.type) ? prop.type : prop.type ? [prop.type] : [];
|
|
11
|
+
const hasNull = types.includes("null");
|
|
12
|
+
const nonNullTypes = types.filter((t) => t !== "null");
|
|
13
|
+
const primaryType = nonNullTypes[0];
|
|
14
|
+
let schema;
|
|
15
|
+
if (prop.enum && Array.isArray(prop.enum)) {
|
|
16
|
+
const values = prop.enum.filter((v) => v !== null);
|
|
17
|
+
schema = values.length > 0 ? z.enum(values) : z.unknown();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
switch (primaryType) {
|
|
21
|
+
case "string":
|
|
22
|
+
schema = z.string();
|
|
23
|
+
break;
|
|
24
|
+
case "number":
|
|
25
|
+
case "integer":
|
|
26
|
+
schema = z.number();
|
|
27
|
+
break;
|
|
28
|
+
case "boolean":
|
|
29
|
+
schema = z.boolean();
|
|
30
|
+
break;
|
|
31
|
+
case "array":
|
|
32
|
+
schema = prop.items ? z.array(convertProperty(prop.items)) : z.array(z.unknown());
|
|
33
|
+
break;
|
|
34
|
+
case "object":
|
|
35
|
+
if (prop.properties) {
|
|
36
|
+
schema = z.object(convertShape(prop.properties, prop.required));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
schema = z.record(z.unknown());
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
schema = z.unknown();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (hasNull)
|
|
47
|
+
schema = schema.nullable();
|
|
48
|
+
if (prop.description)
|
|
49
|
+
schema = schema.describe(prop.description);
|
|
50
|
+
return schema;
|
|
51
|
+
}
|
|
52
|
+
function convertShape(properties, required) {
|
|
53
|
+
const requiredSet = new Set(required ?? []);
|
|
54
|
+
const shape = {};
|
|
55
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
56
|
+
let field = convertProperty(prop);
|
|
57
|
+
if (!requiredSet.has(key))
|
|
58
|
+
field = field.optional();
|
|
59
|
+
shape[key] = field;
|
|
60
|
+
}
|
|
61
|
+
return shape;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 将 MCP Tool 的 inputSchema(JSON Schema object)转换为 Zod raw shape。
|
|
65
|
+
* 返回的 shape 可以直接传给 McpServer.registerTool() 的 inputSchema 参数。
|
|
66
|
+
*/
|
|
67
|
+
export function jsonSchemaToZodShape(inputSchema) {
|
|
68
|
+
if (!inputSchema.properties || Object.keys(inputSchema.properties).length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
return convertShape(inputSchema.properties, inputSchema.required);
|
|
71
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -2,63 +2,144 @@
|
|
|
2
2
|
* 本地工具注册。
|
|
3
3
|
*
|
|
4
4
|
* 定义不依赖远程 MCP Server 的工具(如鉴权辅助工具)。
|
|
5
|
-
* 新增工具时在
|
|
5
|
+
* 新增工具时在 registerTools() 中通过 server.registerTool() 注册。
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { initiateAuth, listRemoteTools } from "./remote.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { initiateAuth, listRemoteTools, callRemoteTool, isRemoteConnected, waitForAuthCompletion } from "./remote.js";
|
|
9
|
+
import { DEFAULT_CALLBACK_PORT, PORT_SCAN_RANGE } from "./auth.js";
|
|
10
|
+
function portInUseGuidance() {
|
|
11
|
+
const endPort = DEFAULT_CALLBACK_PORT + PORT_SCAN_RANGE - 1;
|
|
12
|
+
return [
|
|
13
|
+
`授权回调服务器启动失败:端口 ${DEFAULT_CALLBACK_PORT}–${endPort} 均被占用。`,
|
|
14
|
+
"",
|
|
15
|
+
"可能原因:上次授权流程未正常关闭,或其他程序占用了这些端口。",
|
|
16
|
+
`请执行 \`lsof -i :${DEFAULT_CALLBACK_PORT}-${endPort}\` 查看占用进程并终止,然后重新调用此工具重试。`,
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
const ERROR_GUIDANCE = {
|
|
20
|
+
port_in_use: portInUseGuidance,
|
|
21
|
+
timeout: [
|
|
22
|
+
"授权超时:2 分钟内未收到浏览器回调。",
|
|
23
|
+
"",
|
|
24
|
+
"可能原因:",
|
|
25
|
+
" - 用户未在浏览器中完成授权流程",
|
|
26
|
+
" - 浏览器页面已关闭或未正确加载",
|
|
27
|
+
" - 网络连接问题导致回调未到达",
|
|
28
|
+
"",
|
|
29
|
+
"请重新调用此工具生成新的授权链接,并提醒用户在浏览器中完成授权。",
|
|
30
|
+
].join("\n"),
|
|
31
|
+
auth_denied: [
|
|
32
|
+
"用户拒绝了授权请求。",
|
|
33
|
+
"",
|
|
34
|
+
"如果用户需要使用远程功能,请重新调用此工具生成新的授权链接,",
|
|
35
|
+
"并请用户在浏览器中点击「允许」完成授权。",
|
|
36
|
+
].join("\n"),
|
|
37
|
+
server_error: [
|
|
38
|
+
"授权回调服务器运行异常。",
|
|
39
|
+
"",
|
|
40
|
+
"请重新调用此工具重试。如果问题持续出现,可能需要检查系统网络配置或防火墙设置。",
|
|
41
|
+
].join("\n"),
|
|
42
|
+
not_configured: [
|
|
43
|
+
"远程 MCP 服务器地址未配置。",
|
|
44
|
+
"",
|
|
45
|
+
"请检查环境变量 LESSIE_REMOTE_MCP_URL 是否正确设置。",
|
|
46
|
+
].join("\n"),
|
|
47
|
+
};
|
|
48
|
+
function formatAuthResult(result) {
|
|
49
|
+
switch (result.status) {
|
|
50
|
+
case "connected":
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `已连接到远程 MCP 服务器,共发现 ${result.toolCount} 个远程工具,无需重新授权。\n\n可通过 use_lessie 工具调用远程功能:不传 tool 参数可列出所有工具,传入 tool 和 arguments 可调用指定工具。`,
|
|
55
|
+
}],
|
|
56
|
+
};
|
|
57
|
+
case "auth_url": {
|
|
58
|
+
const lines = [];
|
|
59
|
+
if (result.previousError) {
|
|
60
|
+
lines.push(`⚠ 上次授权尝试失败:${result.previousError}`);
|
|
61
|
+
lines.push("已自动发起新的授权流程。");
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
lines.push("需要授权登录。请在浏览器中打开以下链接完成授权:", "", result.authUrl, "", "授权完成后,使用 use_lessie 工具调用远程功能(不传 tool 参数可查看所有可用工具)。", `如果 2 分钟内未完成授权,流程将超时,届时请重新调用此工具重试。`);
|
|
65
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
66
|
+
}
|
|
67
|
+
case "waiting": {
|
|
68
|
+
const elapsedSec = Math.floor(result.elapsedMs / 1000);
|
|
69
|
+
const remainingSec = Math.max(0, 120 - elapsedSec);
|
|
70
|
+
return {
|
|
71
|
+
content: [{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: [
|
|
74
|
+
`授权流程进行中,正在等待用户在浏览器中完成登录。`,
|
|
75
|
+
`已等待 ${elapsedSec} 秒,剩余超时时间约 ${remainingSec} 秒。`,
|
|
76
|
+
"",
|
|
77
|
+
"请提醒用户在浏览器中完成授权。如果用户已关闭页面,可以重新访问以下链接:",
|
|
78
|
+
"",
|
|
79
|
+
result.authUrl,
|
|
80
|
+
].join("\n"),
|
|
81
|
+
}],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
case "error": {
|
|
85
|
+
const entry = ERROR_GUIDANCE[result.errorCode];
|
|
86
|
+
const guidance = typeof entry === "function" ? entry()
|
|
87
|
+
: entry ?? `授权失败:${result.message}\n\n请重新调用此工具重试。`;
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: guidance }],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
34
93
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
}
|
|
95
|
+
export function registerTools(server) {
|
|
96
|
+
server.registerTool("authorize", {
|
|
97
|
+
description: "连接到远程 Lessie 服务。首次使用或授权过期时返回授权链接,用户需在浏览器中打开完成登录。已连接时返回当前状态。授权异常时返回诊断信息和修复建议。",
|
|
98
|
+
}, async () => {
|
|
99
|
+
const result = await initiateAuth();
|
|
100
|
+
return formatAuthResult(result);
|
|
101
|
+
});
|
|
102
|
+
server.registerTool("use_lessie", {
|
|
103
|
+
description: "调用远程 Lessie 工具的统一入口。不传 tool 参数时列出所有可用的远程工具及其参数说明;传入 tool 和 arguments 时调用指定的远程工具。需先通过 authorize 完成授权。",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
tool: z.string().optional().describe("要调用的远程工具名称,留空则列出所有可用工具"),
|
|
106
|
+
arguments: z.record(z.unknown()).optional().describe("传递给远程工具的参数"),
|
|
107
|
+
},
|
|
108
|
+
}, async (args) => {
|
|
109
|
+
const toolName = args.tool;
|
|
110
|
+
await waitForAuthCompletion();
|
|
111
|
+
if (!isRemoteConnected()) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: "远程 MCP 服务器未连接。请先调用 authorize 工具完成授权。",
|
|
116
|
+
}],
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (!toolName) {
|
|
121
|
+
const tools = await listRemoteTools();
|
|
122
|
+
if (tools.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: "当前没有可用的远程工具。" }],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const listing = tools.map((t) => {
|
|
128
|
+
const params = t.inputSchema?.properties
|
|
129
|
+
? Object.entries(t.inputSchema.properties)
|
|
130
|
+
.map(([k, v]) => ` - ${k} (${v.type ?? "any"}): ${v.description ?? ""}`)
|
|
131
|
+
.join("\n")
|
|
132
|
+
: " (无参数)";
|
|
133
|
+
const required = t.inputSchema?.required?.join(", ") ?? "";
|
|
134
|
+
return `## ${t.name}\n${t.description ?? ""}\n 参数:\n${params}${required ? `\n 必填: ${required}` : ""}`;
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: `共 ${tools.length} 个远程工具可用:\n\n${listing.join("\n\n")}`,
|
|
140
|
+
}],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return callRemoteTool(toolName, args.arguments ?? {});
|
|
144
|
+
});
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessie/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"lessie-mcp-server": "dist/
|
|
7
|
+
"lessie-mcp-server": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsc --watch",
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
|
-
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
13
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
14
|
+
"mcpb:pack": "bash mcpb/pack.sh"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
17
18
|
"zod": "^3.23.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
21
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
22
|
+
"@modelcontextprotocol/inspector": "^0.21.1",
|
|
20
23
|
"@types/node": "^20.0.0",
|
|
24
|
+
"esbuild": "^0.27.4",
|
|
21
25
|
"typescript": "^5.4.0"
|
|
22
26
|
},
|
|
23
27
|
"publishConfig": {
|
package/dist/server/auth.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import jwt from "jsonwebtoken";
|
|
3
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
4
|
-
const JWT_EXPIRES_IN = 3600; // 1 hour
|
|
5
|
-
// ── API Key 生成 ─────────────────────────────────────────────────────────────
|
|
6
|
-
export function generateApiKey() {
|
|
7
|
-
const rawKey = "sk-live-v1-" + randomBytes(24).toString("base64url");
|
|
8
|
-
const keyHash = hashApiKey(rawKey);
|
|
9
|
-
const keyHint = rawKey.slice(0, 20) + "...";
|
|
10
|
-
return { rawKey, keyHash, keyHint };
|
|
11
|
-
}
|
|
12
|
-
export function hashApiKey(rawKey) {
|
|
13
|
-
return createHash("sha256").update(rawKey).digest("hex");
|
|
14
|
-
}
|
|
15
|
-
// ── JWT 签发 ─────────────────────────────────────────────────────────────────
|
|
16
|
-
export function signJwt(payload) {
|
|
17
|
-
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
18
|
-
return { token, expiresIn: JWT_EXPIRES_IN };
|
|
19
|
-
}
|
|
20
|
-
// ── JWT 验证中间件(登录态保护) ──────────────────────────────────────────────
|
|
21
|
-
export function requireAuth(req, res, next) {
|
|
22
|
-
const header = req.headers.authorization;
|
|
23
|
-
if (!header?.startsWith("Bearer ")) {
|
|
24
|
-
res.status(401).json({ error: "Missing or invalid Authorization header" });
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const token = header.slice(7);
|
|
28
|
-
try {
|
|
29
|
-
const decoded = jwt.verify(token, JWT_SECRET);
|
|
30
|
-
req.user = decoded;
|
|
31
|
-
next();
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
res.status(401).json({ error: "Invalid or expired token" });
|
|
35
|
-
}
|
|
36
|
-
}
|
package/dist/server/db.js
DELETED
package/dist/server/index.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import apiKeysRouter from "./routes/api-keys.js";
|
|
3
|
-
import authTokenRouter from "./routes/auth-token.js";
|
|
4
|
-
const app = express();
|
|
5
|
-
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
6
|
-
app.use(express.json());
|
|
7
|
-
app.use("/api-keys", apiKeysRouter);
|
|
8
|
-
app.use("/auth/token", authTokenRouter);
|
|
9
|
-
app.listen(PORT, () => {
|
|
10
|
-
console.log(`Server listening on port ${PORT}`);
|
|
11
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
2
|
-
import pool from "../db.js";
|
|
3
|
-
import { generateApiKey, requireAuth } from "../auth.js";
|
|
4
|
-
const router = Router();
|
|
5
|
-
router.use(requireAuth);
|
|
6
|
-
const MAX_KEYS_PER_USER = 10;
|
|
7
|
-
// POST /api-keys — 创建 API Key
|
|
8
|
-
router.post("/", async (req, res) => {
|
|
9
|
-
try {
|
|
10
|
-
const userId = req.user.sub;
|
|
11
|
-
const { name, scopes } = req.body;
|
|
12
|
-
if (!name || typeof name !== "string" || name.trim().length === 0) {
|
|
13
|
-
res.status(400).json({ error: "name is required" });
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
const validScopes = scopes ?? ["read"];
|
|
17
|
-
const countResult = await pool.query("SELECT COUNT(*) FROM api_keys WHERE user_id = $1 AND revoked_at IS NULL", [userId]);
|
|
18
|
-
if (parseInt(countResult.rows[0].count, 10) >= MAX_KEYS_PER_USER) {
|
|
19
|
-
res.status(400).json({ error: `Maximum ${MAX_KEYS_PER_USER} active API keys allowed` });
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const { rawKey, keyHash, keyHint } = generateApiKey();
|
|
23
|
-
await pool.query(`INSERT INTO api_keys (user_id, name, key_hash, key_hint, scopes)
|
|
24
|
-
VALUES ($1, $2, $3, $4, $5)`, [userId, name.trim(), keyHash, keyHint, validScopes]);
|
|
25
|
-
res.status(201).json({
|
|
26
|
-
key: rawKey,
|
|
27
|
-
hint: keyHint,
|
|
28
|
-
name: name.trim(),
|
|
29
|
-
scopes: validScopes,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
console.error("POST /api-keys error:", err);
|
|
34
|
-
res.status(500).json({ error: "Internal server error" });
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
// GET /api-keys — 列出 API Keys
|
|
38
|
-
router.get("/", async (req, res) => {
|
|
39
|
-
try {
|
|
40
|
-
const userId = req.user.sub;
|
|
41
|
-
const result = await pool.query(`SELECT id, name, key_hint, scopes, last_used_at, created_at
|
|
42
|
-
FROM api_keys
|
|
43
|
-
WHERE user_id = $1 AND revoked_at IS NULL
|
|
44
|
-
ORDER BY created_at DESC`, [userId]);
|
|
45
|
-
res.json(result.rows.map((row) => ({
|
|
46
|
-
id: row.id,
|
|
47
|
-
name: row.name,
|
|
48
|
-
keyHint: row.key_hint,
|
|
49
|
-
scopes: row.scopes,
|
|
50
|
-
lastUsedAt: row.last_used_at,
|
|
51
|
-
createdAt: row.created_at,
|
|
52
|
-
})));
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
console.error("GET /api-keys error:", err);
|
|
56
|
-
res.status(500).json({ error: "Internal server error" });
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
// DELETE /api-keys/:id — 吊销 API Key(软删除)
|
|
60
|
-
router.delete("/:id", async (req, res) => {
|
|
61
|
-
try {
|
|
62
|
-
const userId = req.user.sub;
|
|
63
|
-
const { id } = req.params;
|
|
64
|
-
const result = await pool.query(`UPDATE api_keys SET revoked_at = now()
|
|
65
|
-
WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
|
|
66
|
-
RETURNING id`, [id, userId]);
|
|
67
|
-
if (result.rowCount === 0) {
|
|
68
|
-
res.status(404).json({ error: "API key not found" });
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
res.status(204).send();
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
console.error("DELETE /api-keys/:id error:", err);
|
|
75
|
-
res.status(500).json({ error: "Internal server error" });
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
export default router;
|