@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
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
2
|
-
import pool from "../db.js";
|
|
3
|
-
import { hashApiKey, signJwt } from "../auth.js";
|
|
4
|
-
const router = Router();
|
|
5
|
-
// POST /auth/token — API Key 换取 JWT(无需登录态)
|
|
6
|
-
router.post("/", async (req, res) => {
|
|
7
|
-
try {
|
|
8
|
-
const { apiKey } = req.body;
|
|
9
|
-
if (!apiKey || typeof apiKey !== "string") {
|
|
10
|
-
res.status(401).json({ error: "Invalid credentials" });
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
const keyHash = hashApiKey(apiKey);
|
|
14
|
-
const result = await pool.query(`SELECT id, user_id, scopes, expires_at
|
|
15
|
-
FROM api_keys
|
|
16
|
-
WHERE key_hash = $1 AND revoked_at IS NULL`, [keyHash]);
|
|
17
|
-
if (result.rowCount === 0) {
|
|
18
|
-
res.status(401).json({ error: "Invalid credentials" });
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const row = result.rows[0];
|
|
22
|
-
if (row.expires_at && new Date(row.expires_at) < new Date()) {
|
|
23
|
-
res.status(401).json({ error: "Invalid credentials" });
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
// 异步更新 last_used_at,不阻塞响应
|
|
27
|
-
pool.query("UPDATE api_keys SET last_used_at = now() WHERE id = $1", [row.id]).catch((err) => console.error("Failed to update last_used_at:", err));
|
|
28
|
-
const { token, expiresIn } = signJwt({
|
|
29
|
-
sub: row.user_id,
|
|
30
|
-
scopes: row.scopes ?? ["read"],
|
|
31
|
-
via: "api_key",
|
|
32
|
-
kid: row.id,
|
|
33
|
-
});
|
|
34
|
-
res.json({ token, expiresIn });
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
console.error("POST /auth/token error:", err);
|
|
38
|
-
res.status(500).json({ error: "Internal server error" });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
export default router;
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
# Lessie MCP OAuth 2.1 接入指南
|
|
2
|
-
|
|
3
|
-
本文档面向 MCP Client 开发者,描述如何通过 OAuth 2.1 授权流程获取 access_token 并调用 Lessie MCP 接口。
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 授权流程概览
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
MCP Client Lessie 授权服务器
|
|
11
|
-
│ │
|
|
12
|
-
│ ① POST /mcp (无 token) │
|
|
13
|
-
│──────────────────────────────────────────────►│
|
|
14
|
-
│◄──────────────── 401 Unauthorized ────────────│
|
|
15
|
-
│ │
|
|
16
|
-
│ ② GET /.well-known/oauth-authorization-server │
|
|
17
|
-
│──────────────────────────────────────────────►│
|
|
18
|
-
│◄──── 元数据 JSON (各端点地址) ─────────────────│
|
|
19
|
-
│ │
|
|
20
|
-
│ ③ POST /register (动态客户端注册) │
|
|
21
|
-
│──────────────────────────────────────────────►│
|
|
22
|
-
│◄──── { client_id, client_secret } ───────────│
|
|
23
|
-
│ │
|
|
24
|
-
│ ④ 打开浏览器 → /authorize?... │
|
|
25
|
-
│ 用户登录 → 点击"授权" │
|
|
26
|
-
│◄──── 302 → callback?code=xxx&state=yyy ───────│
|
|
27
|
-
│ │
|
|
28
|
-
│ ⑤ POST /token (code + code_verifier) │
|
|
29
|
-
│──────────────────────────────────────────────►│
|
|
30
|
-
│◄──── { access_token, token_type, expires_in } │
|
|
31
|
-
│ │
|
|
32
|
-
│ ⑥ POST /mcp (Authorization: Bearer xxx) │
|
|
33
|
-
│──────────────────────────────────────────────►│
|
|
34
|
-
│◄──── 业务数据 ────────────────────────────────│
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
> 本服务实现了 OAuth 2.1 + PKCE (S256) 授权码流程,遵循 RFC 8414、RFC 7591、RFC 7636 标准。
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 基础地址
|
|
42
|
-
|
|
43
|
-
| 环境 | Base URL |
|
|
44
|
-
|------|----------|
|
|
45
|
-
| 生产 | `https://www.lessie.ai/prod-api` |
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## 1. 授权服务器元数据发现
|
|
50
|
-
|
|
51
|
-
> RFC 8414
|
|
52
|
-
|
|
53
|
-
**请求**
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
GET /.well-known/oauth-authorization-server
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**响应 200**
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{
|
|
63
|
-
"issuer": "https://www.lessie.ai/prod-api",
|
|
64
|
-
"authorization_endpoint": "https://www.lessie.ai/prod-api/authorize",
|
|
65
|
-
"token_endpoint": "https://www.lessie.ai/prod-api/token",
|
|
66
|
-
"registration_endpoint": "https://www.lessie.ai/prod-api/register",
|
|
67
|
-
"response_types_supported": ["code"],
|
|
68
|
-
"grant_types_supported": ["authorization_code"],
|
|
69
|
-
"code_challenge_methods_supported": ["S256"],
|
|
70
|
-
"token_endpoint_auth_methods_supported": [
|
|
71
|
-
"client_secret_basic",
|
|
72
|
-
"client_secret_post",
|
|
73
|
-
"none"
|
|
74
|
-
],
|
|
75
|
-
"scopes_supported": ["read", "write"]
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
客户端应通过此端点**动态发现**各接口地址,不要硬编码。
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 2. 动态客户端注册
|
|
84
|
-
|
|
85
|
-
> RFC 7591 — 首次连接时调用,无需用户操作
|
|
86
|
-
|
|
87
|
-
**请求**
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
POST /register
|
|
91
|
-
Content-Type: application/json
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"redirect_uris": ["http://127.0.0.1:3000/callback"],
|
|
97
|
-
"grant_types": ["authorization_code"],
|
|
98
|
-
"response_types": ["code"],
|
|
99
|
-
"client_name": "My MCP Client",
|
|
100
|
-
"token_endpoint_auth_method": "client_secret_basic"
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
| 字段 | 类型 | 必填 | 说明 |
|
|
105
|
-
|------|------|------|------|
|
|
106
|
-
| `redirect_uris` | string[] | 是 | 回调地址列表,仅允许 loopback(`127.0.0.1` / `localhost` / `[::1]`)或 `https` |
|
|
107
|
-
| `grant_types` | string[] | 否 | 默认 `["authorization_code"]` |
|
|
108
|
-
| `response_types` | string[] | 否 | 默认 `["code"]` |
|
|
109
|
-
| `client_name` | string | 否 | 默认 `"MCP Client"`,显示在授权页面上 |
|
|
110
|
-
| `token_endpoint_auth_method` | string | 否 | `"client_secret_basic"`(默认)、`"client_secret_post"` 或 `"none"`(public client) |
|
|
111
|
-
|
|
112
|
-
**响应 201 Created**
|
|
113
|
-
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"client_id": "lessie-aBcDeFgHiJkLmNoP",
|
|
117
|
-
"client_secret": "sk-xXxXxXxXxXxXxXxX...",
|
|
118
|
-
"client_name": "My MCP Client",
|
|
119
|
-
"redirect_uris": ["http://127.0.0.1:3000/callback"],
|
|
120
|
-
"grant_types": ["authorization_code"]
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
> **重要**:`client_secret` 仅在注册响应中返回一次,请安全存储。后续无法再次获取。
|
|
125
|
-
|
|
126
|
-
**错误响应**
|
|
127
|
-
|
|
128
|
-
| HTTP 状态码 | error | 说明 |
|
|
129
|
-
|-------------|-------|------|
|
|
130
|
-
| 400 | `invalid_redirect_uri` | redirect_uri 格式不合法(必须是 loopback 或 https) |
|
|
131
|
-
| 403 | `client_limit_exceeded` | 客户端注册数量已达上限 |
|
|
132
|
-
| 429 | `too_many_requests` | 注册频率超限,请稍后重试 |
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## 3. 授权请求
|
|
137
|
-
|
|
138
|
-
客户端在本地生成 PKCE 参数后,打开浏览器跳转到授权页面。
|
|
139
|
-
|
|
140
|
-
### 3.1 生成 PKCE 参数
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
code_verifier = 随机生成 43-128 字符的 URL-safe 字符串
|
|
144
|
-
code_challenge = BASE64URL(SHA256(code_verifier))
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### 3.2 打开浏览器
|
|
148
|
-
|
|
149
|
-
```
|
|
150
|
-
GET /authorize
|
|
151
|
-
?response_type=code
|
|
152
|
-
&client_id={client_id}
|
|
153
|
-
&redirect_uri={redirect_uri}
|
|
154
|
-
&code_challenge={code_challenge}
|
|
155
|
-
&code_challenge_method=S256
|
|
156
|
-
&state={随机字符串}
|
|
157
|
-
&scope=read write
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
| 参数 | 必填 | 说明 |
|
|
161
|
-
|------|------|------|
|
|
162
|
-
| `response_type` | 是 | 固定为 `code` |
|
|
163
|
-
| `client_id` | 是 | 注册时获取的 client_id |
|
|
164
|
-
| `redirect_uri` | 是 | 必须与注册时提交的地址匹配(loopback 忽略端口) |
|
|
165
|
-
| `code_challenge` | 是 | PKCE challenge (S256) |
|
|
166
|
-
| `code_challenge_method` | 是 | 固定为 `S256` |
|
|
167
|
-
| `state` | 建议 | 防 CSRF 的随机字符串,会原样回传 |
|
|
168
|
-
| `scope` | 否 | 空格分隔,可选值:`read`、`write`。默认 `read write` |
|
|
169
|
-
|
|
170
|
-
### 3.3 用户操作
|
|
171
|
-
|
|
172
|
-
1. 用户在浏览器中登录 Lessie 账号(如未登录会自动跳转登录页)
|
|
173
|
-
2. 用户在授权页面看到客户端名称,点击"授权"
|
|
174
|
-
|
|
175
|
-
### 3.4 授权回调
|
|
176
|
-
|
|
177
|
-
授权成功后,浏览器 302 重定向到 `redirect_uri`:
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
http://127.0.0.1:3000/callback?code=xxxx&state=yyyy
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
| 参数 | 说明 |
|
|
184
|
-
|------|------|
|
|
185
|
-
| `code` | 授权码,10 分钟有效,一次性使用 |
|
|
186
|
-
| `state` | 与请求时一致,客户端应验证其值 |
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## 4. 令牌交换
|
|
191
|
-
|
|
192
|
-
用授权码 + PKCE code_verifier 换取 access_token。
|
|
193
|
-
|
|
194
|
-
**请求**
|
|
195
|
-
|
|
196
|
-
```
|
|
197
|
-
POST /token
|
|
198
|
-
Content-Type: application/x-www-form-urlencoded
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### 4.1 客户端认证方式
|
|
202
|
-
|
|
203
|
-
**方式 A — Basic Auth(推荐)**
|
|
204
|
-
|
|
205
|
-
```
|
|
206
|
-
Authorization: Basic base64(client_id:client_secret)
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
表单参数:
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
grant_type=authorization_code
|
|
213
|
-
&code={授权码}
|
|
214
|
-
&redirect_uri={与授权请求一致的 redirect_uri}
|
|
215
|
-
&code_verifier={PKCE code_verifier}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
**方式 B — Form Body**
|
|
219
|
-
|
|
220
|
-
```
|
|
221
|
-
grant_type=authorization_code
|
|
222
|
-
&code={授权码}
|
|
223
|
-
&redirect_uri={与授权请求一致的 redirect_uri}
|
|
224
|
-
&code_verifier={PKCE code_verifier}
|
|
225
|
-
&client_id={client_id}
|
|
226
|
-
&client_secret={client_secret}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
**方式 C — Public Client(无 secret)**
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
grant_type=authorization_code
|
|
233
|
-
&code={授权码}
|
|
234
|
-
&redirect_uri={与授权请求一致的 redirect_uri}
|
|
235
|
-
&code_verifier={PKCE code_verifier}
|
|
236
|
-
&client_id={client_id}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
**响应 200**
|
|
240
|
-
|
|
241
|
-
```json
|
|
242
|
-
{
|
|
243
|
-
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
244
|
-
"token_type": "bearer",
|
|
245
|
-
"expires_in": 3600,
|
|
246
|
-
"scope": "read write"
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
| 字段 | 说明 |
|
|
251
|
-
|------|------|
|
|
252
|
-
| `access_token` | JWT 格式的访问令牌 |
|
|
253
|
-
| `token_type` | 固定为 `bearer` |
|
|
254
|
-
| `expires_in` | 有效期(秒),默认 3600(1 小时) |
|
|
255
|
-
| `scope` | 实际授予的权限范围,空格分隔 |
|
|
256
|
-
|
|
257
|
-
**错误响应**
|
|
258
|
-
|
|
259
|
-
| HTTP 状态码 | error | 说明 |
|
|
260
|
-
|-------------|-------|------|
|
|
261
|
-
| 400 | `unsupported_grant_type` | 仅支持 `authorization_code` |
|
|
262
|
-
| 400 | `invalid_request` | 缺少必要参数(code / redirect_uri / code_verifier) |
|
|
263
|
-
| 400 | `invalid_grant` | 授权码无效、已过期、已使用,或 PKCE 验证失败 |
|
|
264
|
-
| 401 | `invalid_client` | 客户端认证失败(client_id 不存在或 secret 错误) |
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## 5. 调用 MCP 接口
|
|
269
|
-
|
|
270
|
-
在每次请求中携带 Bearer Token:
|
|
271
|
-
|
|
272
|
-
```
|
|
273
|
-
POST /mcp
|
|
274
|
-
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
|
275
|
-
Content-Type: application/json
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### 401 响应处理
|
|
279
|
-
|
|
280
|
-
| WWW-Authenticate 值 | 含义 | 处理方式 |
|
|
281
|
-
|---------------------|------|----------|
|
|
282
|
-
| `Bearer` | 未携带 token | 触发完整 OAuth 授权流程 |
|
|
283
|
-
| `Bearer error="invalid_token"` | token 无效或已过期 | 重新走授权流程获取新 token |
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## 6. 可用 Scope
|
|
288
|
-
|
|
289
|
-
| Scope | 说明 |
|
|
290
|
-
|-------|------|
|
|
291
|
-
| `read` | 读取数据 |
|
|
292
|
-
| `write` | 写入数据 |
|
|
293
|
-
|
|
294
|
-
默认授予 `read write`。
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## 7. 完整接入示例(伪代码)
|
|
299
|
-
|
|
300
|
-
```python
|
|
301
|
-
# 1. 发现元数据
|
|
302
|
-
metadata = GET("/.well-known/oauth-authorization-server")
|
|
303
|
-
|
|
304
|
-
# 2. 动态注册(仅首次)
|
|
305
|
-
registration = POST(metadata.registration_endpoint, {
|
|
306
|
-
"redirect_uris": ["http://127.0.0.1:9876/callback"],
|
|
307
|
-
"grant_types": ["authorization_code"],
|
|
308
|
-
"client_name": "My Agent"
|
|
309
|
-
})
|
|
310
|
-
client_id = registration.client_id
|
|
311
|
-
client_secret = registration.client_secret
|
|
312
|
-
|
|
313
|
-
# 3. 生成 PKCE
|
|
314
|
-
code_verifier = random_url_safe_string(64)
|
|
315
|
-
code_challenge = base64url(sha256(code_verifier))
|
|
316
|
-
|
|
317
|
-
# 4. 打开浏览器授权
|
|
318
|
-
open_browser(f"{metadata.authorization_endpoint}"
|
|
319
|
-
f"?response_type=code"
|
|
320
|
-
f"&client_id={client_id}"
|
|
321
|
-
f"&redirect_uri=http://127.0.0.1:9876/callback"
|
|
322
|
-
f"&code_challenge={code_challenge}"
|
|
323
|
-
f"&code_challenge_method=S256"
|
|
324
|
-
f"&state={random_state}")
|
|
325
|
-
|
|
326
|
-
# 5. 本地 HTTP 服务器等待回调,获取 code
|
|
327
|
-
code = wait_for_callback()
|
|
328
|
-
|
|
329
|
-
# 6. 用 code 换 token
|
|
330
|
-
token_response = POST(metadata.token_endpoint, {
|
|
331
|
-
"grant_type": "authorization_code",
|
|
332
|
-
"code": code,
|
|
333
|
-
"redirect_uri": "http://127.0.0.1:9876/callback",
|
|
334
|
-
"code_verifier": code_verifier,
|
|
335
|
-
"client_id": client_id,
|
|
336
|
-
"client_secret": client_secret
|
|
337
|
-
})
|
|
338
|
-
access_token = token_response.access_token
|
|
339
|
-
|
|
340
|
-
# 7. 调用 MCP
|
|
341
|
-
response = POST("/mcp", headers={"Authorization": f"Bearer {access_token}"}, ...)
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
## 8. 注意事项
|
|
347
|
-
|
|
348
|
-
- `redirect_uri` 仅允许 loopback 地址(`127.0.0.1` / `localhost` / `[::1]`,任意端口)或 `https` 地址
|
|
349
|
-
- loopback 回调时端口可以与注册时不同(符合 [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3))
|
|
350
|
-
- `client_secret` 仅注册时返回一次,请妥善保管
|
|
351
|
-
- 授权码 10 分钟有效,且只能使用一次
|
|
352
|
-
- access_token 有效期 1 小时,过期后需重新走授权流程
|
|
353
|
-
- PKCE (S256) 为强制要求,不支持 plain 方式
|
|
354
|
-
- `state` 参数建议始终使用,用于防止 CSRF 攻击
|