@lessie/mcp-server 0.0.6 → 0.1.0
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 +75 -14
- package/SKILL.md +43 -122
- package/dist/auth.js +256 -0
- package/dist/config.js +29 -0
- package/dist/index.js +66 -104
- package/dist/process-handlers.js +10 -0
- package/dist/remote.js +267 -0
- package/dist/schema.js +71 -0
- package/dist/tools.js +145 -0
- package/mcpb/.mcpbignore +4 -0
- package/mcpb/README.md +47 -0
- package/mcpb/lessie-mcp-20260320-221003.mcpb +0 -0
- package/mcpb/lessie-mcp-20260320-232505.mcpb +0 -0
- package/mcpb/lessie-mcp-20260320-232558.mcpb +0 -0
- package/mcpb/manifest.json +45 -0
- package/mcpb/pack.sh +36 -0
- package/package.json +7 -2
- 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/mcpb/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# MCPB 打包
|
|
2
|
+
|
|
3
|
+
将 Lessie MCP Server 打包为 `.mcpb` 文件,供 Claude Desktop 用户一键安装。
|
|
4
|
+
|
|
5
|
+
## 打包
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 在项目根目录执行
|
|
9
|
+
npm run build
|
|
10
|
+
npm run mcpb:pack
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
生成的 `.mcpb` 文件在当前目录下。
|
|
14
|
+
|
|
15
|
+
## 安装方式
|
|
16
|
+
|
|
17
|
+
用户拿到 `.mcpb` 文件后:
|
|
18
|
+
|
|
19
|
+
- **双击**文件自动打开 Claude Desktop
|
|
20
|
+
- **拖拽**到 Claude Desktop 窗口
|
|
21
|
+
- Claude Desktop 菜单:Developer → Extensions → Install Extension
|
|
22
|
+
|
|
23
|
+
## manifest.json 说明
|
|
24
|
+
|
|
25
|
+
| 字段 | 说明 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `server.type` | `node` — Claude Desktop 自带 Node.js 运行时,用户无需额外安装 |
|
|
28
|
+
| `server.entry_point` | 指向编译产物 `dist/index.js` |
|
|
29
|
+
| `tools` | 声明本地工具(`authorize`) |
|
|
30
|
+
| `tools_generated` | `true` — 授权后会动态暴露远程工具 |
|
|
31
|
+
| `user_config` | 安装时 Claude Desktop 自动弹出配置界面,用户可自定义远程服务地址 |
|
|
32
|
+
|
|
33
|
+
## 添加图标
|
|
34
|
+
|
|
35
|
+
准备一个 512×512 的 `icon.png` 放到本目录,然后在 `manifest.json` 中添加:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
"icons": {
|
|
39
|
+
"default": "icon.png"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 参考
|
|
44
|
+
|
|
45
|
+
- [MCPB 规范](https://github.com/modelcontextprotocol/mcpb)
|
|
46
|
+
- [Manifest 字段说明](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md)
|
|
47
|
+
- [提交到 Anthropic Directory](https://support.claude.com/en/articles/12922832-local-mcp-server-submission-guide)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "0.3",
|
|
3
|
+
"name": "lessie-mcp-server",
|
|
4
|
+
"display_name": "Lessie",
|
|
5
|
+
"version": "0.0.8",
|
|
6
|
+
"description": "Connect Claude to your Lessie account. Authorize via OAuth and access remote Lessie tools directly in conversations.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Lessie"
|
|
9
|
+
},
|
|
10
|
+
"server": {
|
|
11
|
+
"type": "node",
|
|
12
|
+
"entry_point": "dist/index.js",
|
|
13
|
+
"mcp_config": {
|
|
14
|
+
"command": "node",
|
|
15
|
+
"args": ["${__dirname}/dist/index.js"],
|
|
16
|
+
"env": {
|
|
17
|
+
"LESSIE_REMOTE_MCP_URL": "${user_config.remote_mcp_url}"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"tools": [
|
|
22
|
+
{
|
|
23
|
+
"name": "authorize",
|
|
24
|
+
"description": "Connect to the remote Lessie service. Returns an authorization link on first use or when authorization expires; returns current status when already connected."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "use_lessie",
|
|
28
|
+
"description": "Call any remote Lessie tool. Omit the 'tool' parameter to list all available remote tools with their schemas; provide 'tool' and 'arguments' to invoke a specific tool. Requires prior authorization via the 'authorize' tool."
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"tools_generated": true,
|
|
32
|
+
"user_config": {
|
|
33
|
+
"remote_mcp_url": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"title": "Remote MCP Server URL",
|
|
36
|
+
"description": "The URL of the remote Lessie MCP server",
|
|
37
|
+
"required": false,
|
|
38
|
+
"default": "https://www.lessie.ai/mcp-server/mcp"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"compatibility": {
|
|
42
|
+
"platforms": ["darwin", "win32"]
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|
package/mcpb/pack.sh
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
cd "$PROJECT_ROOT"
|
|
8
|
+
|
|
9
|
+
echo "Bundling with esbuild..."
|
|
10
|
+
rm -rf "$SCRIPT_DIR/dist"
|
|
11
|
+
mkdir -p "$SCRIPT_DIR/dist"
|
|
12
|
+
npx esbuild src/index.ts \
|
|
13
|
+
--bundle \
|
|
14
|
+
--platform=node \
|
|
15
|
+
--format=esm \
|
|
16
|
+
--target=node18 \
|
|
17
|
+
--outfile="$SCRIPT_DIR/dist/index.js" \
|
|
18
|
+
--banner:js='#!/usr/bin/env node'
|
|
19
|
+
|
|
20
|
+
echo "Copying runtime assets..."
|
|
21
|
+
cp package.json "$SCRIPT_DIR/package.json"
|
|
22
|
+
cp SKILL.md "$SCRIPT_DIR/SKILL.md" 2>/dev/null || true
|
|
23
|
+
|
|
24
|
+
echo "Packing mcpb..."
|
|
25
|
+
cd "$SCRIPT_DIR"
|
|
26
|
+
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
|
27
|
+
OUTPUT="lessie-mcp-${TIMESTAMP}.mcpb"
|
|
28
|
+
npx @anthropic-ai/mcpb pack . "$OUTPUT"
|
|
29
|
+
|
|
30
|
+
echo "Cleaning up..."
|
|
31
|
+
rm -rf "$SCRIPT_DIR/dist"
|
|
32
|
+
rm -f "$SCRIPT_DIR/package.json"
|
|
33
|
+
rm -f "$SCRIPT_DIR/SKILL.md"
|
|
34
|
+
rm -f "$SCRIPT_DIR/lessie-mcp-server-"*.mcpb
|
|
35
|
+
|
|
36
|
+
echo "Done! Output: $SCRIPT_DIR/$OUTPUT"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessie/mcp-server",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,14 +9,19 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsc --watch",
|
|
12
|
-
"start": "node dist/index.js"
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
14
|
+
"mcpb:pack": "bash mcpb/pack.sh"
|
|
13
15
|
},
|
|
14
16
|
"dependencies": {
|
|
15
17
|
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
16
18
|
"zod": "^3.23.0"
|
|
17
19
|
},
|
|
18
20
|
"devDependencies": {
|
|
21
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
22
|
+
"@modelcontextprotocol/inspector": "^0.21.1",
|
|
19
23
|
"@types/node": "^20.0.0",
|
|
24
|
+
"esbuild": "^0.27.4",
|
|
20
25
|
"typescript": "^5.4.0"
|
|
21
26
|
},
|
|
22
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;
|
|
@@ -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;
|