@lessie/mcp-server 0.0.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 ADDED
@@ -0,0 +1,57 @@
1
+ # Lessie MCP
2
+
3
+ 将 [Lessie](https://lessie.com) 接入 Claude Desktop,通过自然语言操作你的 Lessie 账号。
4
+
5
+ ## 前置条件
6
+
7
+ - [Claude Desktop](https://claude.ai/download)
8
+ - [Node.js](https://nodejs.org) 18+
9
+ - Lessie API Key(登录 Lessie → 设置 → 开发者 → API Keys)
10
+
11
+ ## 安装
12
+
13
+ 打开 Claude Desktop 配置文件:
14
+
15
+ | 系统 | 路径 |
16
+ | ------- | ----------------------------------------------------------- |
17
+ | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
18
+ | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
19
+
20
+ 在 `mcpServers` 中添加以下内容,将 `sk-live-v1-你的Key` 替换为你的 API Key:
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "lessie": {
26
+ "command": "npx",
27
+ "args": ["-y", "lessie-mcp"],
28
+ "env": {
29
+ "SAAS_BASE_URL": "https://api.lessie.com",
30
+ "SAAS_API_KEY": "sk-live-v1-你的Key"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ 保存后**完全退出并重新打开** Claude Desktop。
38
+
39
+ ## 验证
40
+
41
+ 在对话中发送:
42
+
43
+ > 查看我的 Lessie 账号信息
44
+
45
+ ## 可用工具
46
+
47
+ | 工具 | 说明 |
48
+ | ----------------- | ---------------------------------------- |
49
+ | `get_account_info` | 查看当前账号详情(用户名、邮箱、状态、角色等) |
50
+
51
+ ## 常见问题
52
+
53
+ **工具列表中没有 lessie?**
54
+ 检查配置文件 JSON 格式是否正确,然后完全退出并重启 Claude Desktop。
55
+
56
+ **报错 `Failed to exchange API key for JWT: 401`?**
57
+ API Key 无效或已被吊销,请在 Lessie 设置页重新生成。
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ // ── 环境变量 ──────────────────────────────────────────────────────────────────
4
+ const BASE_URL = process.env.SAAS_BASE_URL;
5
+ const API_KEY = process.env.SAAS_API_KEY;
6
+ if (!BASE_URL) {
7
+ console.error("Error: SAAS_BASE_URL is not set");
8
+ process.exit(1);
9
+ }
10
+ if (!API_KEY) {
11
+ console.error("Error: SAAS_API_KEY is not set");
12
+ process.exit(1);
13
+ }
14
+ let jwtCache = null;
15
+ /**
16
+ * 获取有效的 JWT token。
17
+ * 若缓存不存在或距过期不足 60 秒,则重新向 /auth/token 换取。
18
+ */
19
+ async function getJwt() {
20
+ const now = Date.now();
21
+ const REFRESH_BEFORE_MS = 60 * 1000;
22
+ if (jwtCache && jwtCache.expiry - now > REFRESH_BEFORE_MS) {
23
+ return jwtCache.token;
24
+ }
25
+ const res = await fetch(`${BASE_URL}/auth/token`, {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ apiKey: API_KEY }),
29
+ });
30
+ if (!res.ok) {
31
+ throw new Error(`Failed to exchange API key for JWT: ${res.status} ${res.statusText}`);
32
+ }
33
+ const data = (await res.json());
34
+ jwtCache = {
35
+ token: data.token,
36
+ expiry: now + data.expiresIn * 1000,
37
+ };
38
+ return jwtCache.token;
39
+ }
40
+ // ── 通用请求封装 ───────────────────────────────────────────────────────────────
41
+ async function api(method, path, body) {
42
+ const token = await getJwt();
43
+ const res = await fetch(`${BASE_URL}${path}`, {
44
+ method,
45
+ headers: {
46
+ "Authorization": `Bearer ${token}`,
47
+ "Content-Type": "application/json",
48
+ },
49
+ body: body !== undefined ? JSON.stringify(body) : undefined,
50
+ });
51
+ if (!res.ok) {
52
+ const text = await res.text().catch(() => "");
53
+ throw new Error(`API error ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
54
+ }
55
+ if (res.status === 204)
56
+ return undefined;
57
+ return res.json();
58
+ }
59
+ // ── 工具结果辅助函数 ───────────────────────────────────────────────────────────
60
+ function textResult(data) {
61
+ return {
62
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
63
+ };
64
+ }
65
+ // ── MCP Server ────────────────────────────────────────────────────────────────
66
+ const server = new McpServer({
67
+ name: "lessie-mcp",
68
+ version: "1.0.0",
69
+ });
70
+ // ── 工具定义 ──────────────────────────────────────────────────────────────────
71
+ // 命名规范:下划线分隔,动词开头(如 list_xxx、get_xxx、create_xxx)。
72
+ // 写入类工具在 description 中注明"执行前请向用户确认"。
73
+ // 账号信息
74
+ server.tool("get_account_info", "查看当前 Lessie 账号的详细信息,包括用户名、邮箱、账号状态、角色、邀请码等。", {}, async () => {
75
+ const data = await api("GET", "/agent/account/info");
76
+ return textResult(data);
77
+ });
78
+ // ── 启动 ──────────────────────────────────────────────────────────────────────
79
+ const transport = new StdioServerTransport();
80
+ await server.connect(transport);
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ import pg from "pg";
2
+ const pool = new pg.Pool({
3
+ connectionString: process.env.DATABASE_URL,
4
+ });
5
+ export default pool;
@@ -0,0 +1,11 @@
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
+ });
@@ -0,0 +1,78 @@
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;
@@ -0,0 +1,41 @@
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;
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@lessie/mcp-server",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "dev": "tsc --watch",
9
+ "start": "node dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.10.0",
13
+ "zod": "^3.23.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.0.0",
17
+ "typescript": "^5.4.0"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ }
22
+ }