@lessie/mcp-server 0.0.3 → 0.0.6
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/SKILL.md +130 -0
- package/dist/index.js +48 -12
- package/package.json +1 -1
package/SKILL.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lessie-api
|
|
3
|
+
description: >-
|
|
4
|
+
调用 Lessie API 的标准流程:API Key 换取 JWT、JWT 缓存刷新、通用请求封装。
|
|
5
|
+
当需要对接 Lessie 后端、调用 Lessie API、或实现鉴权流程时使用。
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Lessie API 调用流程
|
|
9
|
+
|
|
10
|
+
## 鉴权:API Key → JWT
|
|
11
|
+
|
|
12
|
+
环境变量:
|
|
13
|
+
|
|
14
|
+
- `LESSIE_BASE_URL` — 后端地址,默认 `https://www.lessie.ai/prod-api`
|
|
15
|
+
- `LESSIE_API_KEY` — 用户的 API Key(`sk-live-v1-xxx`)
|
|
16
|
+
|
|
17
|
+
### 换取 JWT
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
const res = await fetch(`${LESSIE_BASE_URL}/auth/token`, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
body: JSON.stringify({ apiKey: LESSIE_API_KEY }),
|
|
24
|
+
});
|
|
25
|
+
const body = await res.json();
|
|
26
|
+
// body 结构: { code: number, msg: string, data: { token: string, expiresIn: number } }
|
|
27
|
+
const { token, expiresIn } = body.data;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `expiresIn` 单位为秒(通常 3600 = 1 小时)
|
|
31
|
+
- 错误统一返回 401,不区分"不存在"和"已吊销"
|
|
32
|
+
|
|
33
|
+
### JWT 缓存与刷新
|
|
34
|
+
|
|
35
|
+
在内存中缓存 `{ token, expiry }`,每次请求前检查:
|
|
36
|
+
|
|
37
|
+
- 若缓存不存在,或距过期不足 **60 秒**,重新调用 `/auth/token` 换取
|
|
38
|
+
- 进程重启后需重新获取
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
interface JwtCache {
|
|
42
|
+
token: string;
|
|
43
|
+
expiry: number; // Unix timestamp (ms)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let jwtCache: JwtCache | null = null;
|
|
47
|
+
|
|
48
|
+
async function getJwt(): Promise<string> {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (jwtCache && jwtCache.expiry - now > 60_000) {
|
|
51
|
+
return jwtCache.token;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const res = await fetch(`${LESSIE_BASE_URL}/auth/token`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({ apiKey: LESSIE_API_KEY }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`Failed to exchange API key for JWT: ${res.status}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const body = await res.json() as {
|
|
65
|
+
code: number; msg: string;
|
|
66
|
+
data: { token: string; expiresIn: number };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
jwtCache = {
|
|
70
|
+
token: body.data.token,
|
|
71
|
+
expiry: now + body.data.expiresIn * 1000,
|
|
72
|
+
};
|
|
73
|
+
return jwtCache.token;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 通用请求封装
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
async function api<T = unknown>(
|
|
81
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
82
|
+
path: string,
|
|
83
|
+
body?: unknown,
|
|
84
|
+
): Promise<T> {
|
|
85
|
+
const token = await getJwt();
|
|
86
|
+
|
|
87
|
+
const res = await fetch(`${LESSIE_BASE_URL}${path}`, {
|
|
88
|
+
method,
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: token,
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
},
|
|
93
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const text = await res.text().catch(() => "");
|
|
98
|
+
throw new Error(`API error ${res.status}${text ? `: ${text}` : ""}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (res.status === 204) return undefined as T;
|
|
102
|
+
|
|
103
|
+
const wrapper = await res.json() as { code: number; msg: string; data: T };
|
|
104
|
+
return wrapper.data;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
要点:
|
|
109
|
+
|
|
110
|
+
- Authorization header 直接传 token(不加 `Bearer ` 前缀)
|
|
111
|
+
- 后端统一返回 `{ code, msg, data }`,`api()` 自动解包返回 `data`
|
|
112
|
+
- 所有 Agent 端点路径以 `/agent/` 开头
|
|
113
|
+
|
|
114
|
+
## 调用示例
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// GET 无参数
|
|
118
|
+
const info = await api("GET", "/agent/account/info");
|
|
119
|
+
|
|
120
|
+
// GET 带查询参数
|
|
121
|
+
const params = new URLSearchParams({ page: "1", status: "active" });
|
|
122
|
+
const list = await api("GET", `/agent/projects?${params}`);
|
|
123
|
+
|
|
124
|
+
// POST 带请求体
|
|
125
|
+
const result = await api("POST", "/agent/projects", {
|
|
126
|
+
name: "新项目",
|
|
127
|
+
description: "项目描述",
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const pkg = require("../package.json");
|
|
8
|
+
function loadInstructions() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = readFileSync(new URL("../SKILL.md", import.meta.url), "utf-8");
|
|
11
|
+
return raw.replace(/^---[\s\S]*?---\n*/, "").trim() || undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
4
17
|
// ── 环境变量 ──────────────────────────────────────────────────────────────────
|
|
5
|
-
const BASE_URL = process.env.
|
|
6
|
-
const API_KEY = process.env.
|
|
18
|
+
const BASE_URL = process.env.LESSIE_BASE_URL || 'https://www.lessie.ai/prod-api';
|
|
19
|
+
const API_KEY = process.env.LESSIE_API_KEY;
|
|
7
20
|
if (!BASE_URL) {
|
|
8
|
-
console.error("Error:
|
|
21
|
+
console.error("Error: LESSIE_BASE_URL is not set");
|
|
9
22
|
process.exit(1);
|
|
10
23
|
}
|
|
11
24
|
if (!API_KEY) {
|
|
12
|
-
console.error("Error:
|
|
25
|
+
console.error("Error: LESSIE_API_KEY is not set");
|
|
13
26
|
process.exit(1);
|
|
14
27
|
}
|
|
15
28
|
let jwtCache = null;
|
|
@@ -31,20 +44,22 @@ async function getJwt() {
|
|
|
31
44
|
if (!res.ok) {
|
|
32
45
|
throw new Error(`Failed to exchange API key for JWT: ${res.status} ${res.statusText} ${BASE_URL}/auth/token`);
|
|
33
46
|
}
|
|
34
|
-
const
|
|
47
|
+
const body = (await res.json());
|
|
48
|
+
if (!body.data?.token) {
|
|
49
|
+
throw new Error(`/auth/token response missing data.token. Got: ${JSON.stringify(body)}`);
|
|
50
|
+
}
|
|
35
51
|
jwtCache = {
|
|
36
|
-
token: data.token,
|
|
37
|
-
expiry: now + data.expiresIn * 1000,
|
|
52
|
+
token: body.data.token,
|
|
53
|
+
expiry: now + body.data.expiresIn * 1000,
|
|
38
54
|
};
|
|
39
55
|
return jwtCache.token;
|
|
40
56
|
}
|
|
41
|
-
// ── 通用请求封装 ───────────────────────────────────────────────────────────────
|
|
42
57
|
async function api(method, path, body) {
|
|
43
58
|
const token = await getJwt();
|
|
44
59
|
const res = await fetch(`${BASE_URL}${path}`, {
|
|
45
60
|
method,
|
|
46
61
|
headers: {
|
|
47
|
-
"Authorization":
|
|
62
|
+
"Authorization": token,
|
|
48
63
|
"Content-Type": "application/json",
|
|
49
64
|
},
|
|
50
65
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
@@ -55,7 +70,8 @@ async function api(method, path, body) {
|
|
|
55
70
|
}
|
|
56
71
|
if (res.status === 204)
|
|
57
72
|
return undefined;
|
|
58
|
-
|
|
73
|
+
const wrapper = (await res.json());
|
|
74
|
+
return wrapper.data;
|
|
59
75
|
}
|
|
60
76
|
// ── 工具结果辅助函数 ───────────────────────────────────────────────────────────
|
|
61
77
|
function textResult(data) {
|
|
@@ -66,12 +82,21 @@ function textResult(data) {
|
|
|
66
82
|
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
67
83
|
const server = new McpServer({
|
|
68
84
|
name: "lessie-mcp",
|
|
69
|
-
version:
|
|
85
|
+
version: pkg.version,
|
|
86
|
+
}, {
|
|
87
|
+
instructions: loadInstructions(),
|
|
70
88
|
});
|
|
71
89
|
// ── 工具定义 ──────────────────────────────────────────────────────────────────
|
|
72
90
|
// 命名规范:下划线分隔,动词开头(如 list_xxx、get_xxx、create_xxx)。
|
|
73
91
|
// 写入类工具在 description 中注明"执行前请向用户确认"。
|
|
74
|
-
|
|
92
|
+
server.tool("get_jwt", "获取当前有效的 JWT token,可用于直接调用 Lessie API。返回 token 字符串和剩余有效时间。", {}, async () => {
|
|
93
|
+
const token = await getJwt();
|
|
94
|
+
const remainingMs = jwtCache ? jwtCache.expiry - Date.now() : 0;
|
|
95
|
+
return textResult({
|
|
96
|
+
token,
|
|
97
|
+
remainingSeconds: Math.max(0, Math.floor(remainingMs / 1000)),
|
|
98
|
+
});
|
|
99
|
+
});
|
|
75
100
|
server.tool("get_account_info", "查看当前 Lessie 账号的详细信息,包括用户名、邮箱、账号状态、角色、邀请码等。", {}, async () => {
|
|
76
101
|
const data = await api("GET", "/agent/account/info");
|
|
77
102
|
return textResult(data);
|
|
@@ -79,3 +104,14 @@ server.tool("get_account_info", "查看当前 Lessie 账号的详细信息,包
|
|
|
79
104
|
// ── 启动 ──────────────────────────────────────────────────────────────────────
|
|
80
105
|
const transport = new StdioServerTransport();
|
|
81
106
|
await server.connect(transport);
|
|
107
|
+
await getJwt();
|
|
108
|
+
server.sendLoggingMessage({
|
|
109
|
+
level: "info",
|
|
110
|
+
logger: "lessie",
|
|
111
|
+
data: "Token acquired successfully",
|
|
112
|
+
});
|
|
113
|
+
server.sendLoggingMessage({
|
|
114
|
+
level: "info",
|
|
115
|
+
logger: "lessie",
|
|
116
|
+
data: `Lessie MCP Server v${pkg.version} started`,
|
|
117
|
+
});
|