@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 CHANGED
@@ -2,11 +2,70 @@
2
2
 
3
3
  将 [Lessie](https://lessie.com) 接入 Claude Desktop,通过自然语言操作你的 Lessie 账号。
4
4
 
5
+ ## 架构
6
+
7
+ 本地 MCP Server 同时充当**代理网关**:通过 stdio 与 Claude Desktop 通信,
8
+ 连接远程 MCP Server,将其工具合并暴露给 AI Agent。
9
+
10
+ ```
11
+ Claude Desktop
12
+ │ stdio
13
+
14
+ ┌──────────────────────────────┐
15
+ │ 本地 MCP Server │
16
+ │ │
17
+ │ 本地工具(authorize) │
18
+ │ + │ Streamable HTTP / SSE
19
+ │ 远程工具代理 ───────────────│──────► 远程 MCP Server
20
+ │ │ Authorization: Bearer <token>
21
+ └──────────────────────────────┘
22
+ OAuth 2.1 Authorization Code + PKCE
23
+ ```
24
+
25
+ ### 鉴权机制
26
+
27
+ 采用 **OAuth 2.1 Authorization Code + PKCE** 流程,实现了 MCP SDK 的 `OAuthClientProvider` 接口(`src/auth.ts`)。
28
+
29
+ **首次连接时:**
30
+
31
+ 1. MCP Client 向远程服务器发送请求,收到 401
32
+ 2. SDK 发现 OAuth 元数据(`/.well-known/oauth-authorization-server`)
33
+ 3. SDK 动态注册客户端(`POST /register`,RFC 7591)
34
+ 4. 自动打开浏览器,引导用户到 SaaS 登录页面
35
+ 5. 用户登录并授权后,浏览器重定向到本地 `http://127.0.0.1:19836/callback`
36
+ 6. SDK 用授权码交换 access_token(`POST /token`)
37
+ 7. 令牌持久化到 `~/.lessie/oauth.json`
38
+
39
+ **后续连接时:**
40
+
41
+ - 自动读取本地缓存的令牌,无需再次登录
42
+ - 令牌过期时 SDK 自动触发 re-auth
43
+
44
+ ### 模块结构
45
+
46
+ ```
47
+ src/
48
+ ├── config.ts 环境变量与静态配置(REMOTE_MCP_URL)
49
+ ├── auth.ts OAuth 鉴权:OAuthClientProvider(Authorization Code + PKCE + 持久化)
50
+ ├── remote.ts 远程 MCP 代理客户端:连接、授权重试、工具发现、工具转发
51
+ ├── tools.ts 本地工具注册:工具元数据 + handler
52
+ └── index.ts 入口:创建 Server、注册路由(合并本地/远程工具)、启动
53
+ ```
54
+
55
+ **依赖方向**(单向,无循环):
56
+
57
+ ```
58
+ config ← auth ← tools
59
+ ↑ ↑
60
+ └─ remote ┘
61
+
62
+ index(汇总所有模块)
63
+ ```
64
+
5
65
  ## 前置条件
6
66
 
7
67
  - [Claude Desktop](https://claude.ai/download)
8
68
  - [Node.js](https://nodejs.org) 18+
9
- - Lessie API Key(登录 Lessie → 设置 → 开发者 → API Keys)
10
69
 
11
70
  ## 安装
12
71
 
@@ -17,24 +76,29 @@
17
76
  | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
18
77
  | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
19
78
 
20
- 在 `mcpServers` 中添加以下内容,将 `sk-live-v1-你的Key` 替换为你的 API Key:
79
+ 添加以下配置:
21
80
 
22
81
  ```json
23
82
  {
24
83
  "mcpServers": {
25
84
  "lessie": {
26
85
  "command": "npx",
27
- "args": ["-y", "lessie-mcp"],
86
+ "args": ["-y", "@lessie/mcp-server"],
28
87
  "env": {
29
- "SAAS_BASE_URL": "https://api.lessie.com",
30
- "SAAS_API_KEY": "sk-live-v1-你的Key"
88
+ "LESSIE_REMOTE_MCP_URL": "https://your-remote-mcp-server.com/mcp"
31
89
  }
32
90
  }
33
91
  }
34
92
  }
35
93
  ```
36
94
 
37
- 保存后**完全退出并重新打开** Claude Desktop
95
+ 保存后**完全退出并重新打开** Claude Desktop。首次使用时会自动打开浏览器引导登录。
96
+
97
+ ## 环境变量
98
+
99
+ | 变量 | 必填 | 说明 |
100
+ | ---------------------- | ---- | -------------------- |
101
+ | `LESSIE_REMOTE_MCP_URL`| 否 | 远程 MCP Server 地址 |
38
102
 
39
103
  ## 验证
40
104
 
@@ -42,16 +106,13 @@
42
106
 
43
107
  > 查看我的 Lessie 账号信息
44
108
 
45
- ## 可用工具
46
-
47
- | 工具 | 说明 |
48
- | ----------------- | ---------------------------------------- |
49
- | `get_account_info` | 查看当前账号详情(用户名、邮箱、状态、角色等) |
50
-
51
109
  ## 常见问题
52
110
 
53
111
  **工具列表中没有 lessie?**
54
112
  检查配置文件 JSON 格式是否正确,然后完全退出并重启 Claude Desktop。
55
113
 
56
- **报错 `Failed to exchange API key for JWT: 401`?**
57
- API Key 无效或已被吊销,请在 Lessie 设置页重新生成。
114
+ **连接远程服务器失败?**
115
+ 确认 `LESSIE_REMOTE_MCP_URL` 地址正确,且远程服务器已实现 OAuth 2.1 端点。
116
+
117
+ **如何重新登录?**
118
+ 删除 `~/.lessie/oauth.json` 后重启 Claude Desktop,会重新打开浏览器授权。
package/SKILL.md CHANGED
@@ -1,130 +1,51 @@
1
1
  ---
2
- name: lessie-api
2
+ name: lessie-mcp
3
3
  description: >-
4
- 调用 Lessie API 的标准流程:API Key 换取 JWT、JWT 缓存刷新、通用请求封装。
5
- 当需要对接 Lessie 后端、调用 Lessie API、或实现鉴权流程时使用。
4
+ Lessie MCP 使用指南:授权流程与工具调用。
5
+ 当需要连接 Lessie 服务或使用 Lessie 工具时参考。
6
6
  ---
7
7
 
8
- # Lessie API 调用流程
8
+ # Lessie MCP
9
9
 
10
- ## 鉴权:API Key JWT
11
-
12
- 环境变量:
10
+ Lessie MCP 连接远程 Lessie 服务并暴露其工具,使用前需完成 OAuth 授权。
13
11
 
14
- - `LESSIE_BASE_URL` — 后端地址,默认 `https://www.lessie.ai/prod-api`
15
- - `LESSIE_API_KEY` — 用户的 API Key(`sk-live-v1-xxx`)
12
+ ## 首次使用
16
13
 
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
- ```
14
+ 首次使用时,Agent 应主动调用 `authorize` 工具发起 OAuth 授权:
130
15
 
16
+ 1. 调用 `authorize` → 获取授权链接
17
+ 2. 将授权链接展示给用户,请用户在浏览器中打开完成登录
18
+ 3. 用户完成授权后,再次调用 `authorize` 确认连接状态
19
+ 4. 授权成功后,通过 `use_lessie` 工具调用远程功能
20
+
21
+ 如果 `authorize` 返回已授权状态,则跳过上述流程,直接使用 `use_lessie` 或已暴露的远程工具。
22
+
23
+ ## 异常处理
24
+
25
+ `authorize` 工具会返回详细的诊断信息和修复建议,Agent 应按照提示操作:
26
+
27
+ - **端口占用**(`port_in_use`):回调服务器端口被占用。按提示帮用户排查占用进程,然后重新调用 `authorize`。
28
+ - **授权超时**(`timeout`):2 分钟内未收到浏览器回调。重新调用 `authorize` 生成新链接,提醒用户及时完成授权。
29
+ - **用户拒绝授权**(`auth_denied`):重新调用 `authorize` 并引导用户在浏览器中允许授权。
30
+ - **等待中**(`waiting`):授权流程进行中。提醒用户在浏览器中完成操作,无需重新发起。
31
+ - **其他错误**:按返回的具体建议操作,通常重新调用 `authorize` 即可重试。
32
+
33
+ 当收到 logging 级别的授权失败通知时,应主动告知用户并建议重新调用 `authorize`。
34
+
35
+ ## 使用远程工具
36
+
37
+ 授权完成后,通过 `use_lessie` 工具调用所有远程功能:
38
+
39
+ 1. 调用 `use_lessie`(不传参数)→ 列出所有可用的远程工具及参数说明
40
+ 2. 调用 `use_lessie(tool="工具名", arguments={...})` → 调用指定的远程工具
41
+
42
+ 如果远程工具直接出现在工具列表中(已授权的回访用户),也可以直接调用,无需通过 `use_lessie`。
43
+
44
+ 收到"远程 MCP 服务器未连接"错误时,说明授权已过期,需重新调用 `authorize` 完成授权。
45
+
46
+ ## 本地工具
47
+
48
+ | 工具 | 说明 |
49
+ | ----------- | ------------------------------------------------------------------------------------------ |
50
+ | `authorize` | 发起授权或检查连接状态。返回连接状态、授权链接、等待状态或错误诊断信息(含修复建议) |
51
+ | `use_lessie` | 远程工具统一入口。不传 `tool` 列出所有远程工具;传入 `tool` 和 `arguments` 调用指定工具 |
package/dist/auth.js ADDED
@@ -0,0 +1,256 @@
1
+ /**
2
+ * OAuth 鉴权(Authorization Code + PKCE + 动态客户端注册)。
3
+ *
4
+ * 实现 MCP SDK 的 OAuthClientProvider 接口:
5
+ * 1. 首次连接时通过 RFC 7591 动态注册客户端(POST /register)
6
+ * 2. 打开浏览器引导用户到 SaaS 页面登录并授权
7
+ * 3. 在 localhost HTTP 服务器上接收授权码回调
8
+ * 4. SDK 自动用授权码交换 access_token(POST /token)
9
+ *
10
+ * 端口策略:prepareCallbackServer() 扫描端口范围并创建 HTTP 服务器占住可用端口,
11
+ * redirectToAuthorization() 直接在该服务器上挂载回调处理逻辑,避免竞争窗口。
12
+ *
13
+ * 客户端信息和令牌持久化到 ~/.lessie/oauth.json,进程重启后复用,
14
+ * 避免每次都打开浏览器。
15
+ */
16
+ import { createServer } from "node:http";
17
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { homedir } from "node:os";
20
+ export const DEFAULT_CALLBACK_PORT = 19836;
21
+ export const PORT_SCAN_RANGE = 10;
22
+ const STORAGE_DIR = join(homedir(), ".lessie");
23
+ const STORAGE_FILE = join(STORAGE_DIR, "oauth.json");
24
+ const CALLBACK_TIMEOUT_MS = 120_000;
25
+ function tryListenHttp(port) {
26
+ return new Promise((resolve) => {
27
+ const srv = createServer((_req, res) => {
28
+ res.writeHead(404);
29
+ res.end();
30
+ });
31
+ srv.once("error", () => resolve(null));
32
+ srv.once("listening", () => resolve(srv));
33
+ srv.listen(port, "127.0.0.1");
34
+ });
35
+ }
36
+ // JSON 损坏时返回空对象,用户需重新授权(损坏数据无法恢复)
37
+ function loadStorage() {
38
+ try {
39
+ return JSON.parse(readFileSync(STORAGE_FILE, "utf-8"));
40
+ }
41
+ catch {
42
+ return {};
43
+ }
44
+ }
45
+ function persistStorage(data) {
46
+ mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 });
47
+ writeFileSync(STORAGE_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
48
+ // writeFileSync mode 仅在创建新文件时生效;已存在文件需 chmod 确保权限正确
49
+ chmodSync(STORAGE_FILE, 0o600);
50
+ }
51
+ export class LessieAuthProvider {
52
+ _storage;
53
+ _codeVerifier = "";
54
+ _expectedState = "";
55
+ _callbackPromise = null;
56
+ _httpServer = null;
57
+ _callbackTimer = null;
58
+ _callbackPort;
59
+ /** 等待用户访问的授权 URL;授权完成后清除 */
60
+ pendingAuthUrl = null;
61
+ /** 回调服务器开始监听的时间戳,用于计算等待时长 */
62
+ waitingSince = null;
63
+ constructor() {
64
+ this._storage = loadStorage();
65
+ this._codeVerifier = this._storage.codeVerifier || "";
66
+ this._callbackPort = this._storage.callbackPort ?? DEFAULT_CALLBACK_PORT;
67
+ }
68
+ get callbackPort() {
69
+ return this._callbackPort;
70
+ }
71
+ /**
72
+ * 扫描端口范围,创建 HTTP 服务器占住可用端口。
73
+ * redirectToAuthorization 直接复用该服务器,无需释放重听。
74
+ * 若端口与上次注册时不同,自动清除客户端信息以触发重新注册。
75
+ */
76
+ async prepareCallbackServer() {
77
+ if (this._httpServer?.listening)
78
+ return;
79
+ this._cleanupServer();
80
+ for (let offset = 0; offset < PORT_SCAN_RANGE; offset++) {
81
+ const port = DEFAULT_CALLBACK_PORT + offset;
82
+ const server = await tryListenHttp(port);
83
+ if (server) {
84
+ if (this._storage.clientInfo && this._storage.callbackPort != null && this._storage.callbackPort !== port) {
85
+ delete this._storage.clientInfo;
86
+ }
87
+ this._httpServer = server;
88
+ this._callbackPort = port;
89
+ this._storage.callbackPort = port;
90
+ persistStorage(this._storage);
91
+ return;
92
+ }
93
+ }
94
+ throw new Error(`No available port found in range ${DEFAULT_CALLBACK_PORT}–${DEFAULT_CALLBACK_PORT + PORT_SCAN_RANGE - 1}`);
95
+ }
96
+ get redirectUrl() {
97
+ return `http://127.0.0.1:${this._callbackPort}/callback`;
98
+ }
99
+ get clientMetadata() {
100
+ return {
101
+ redirect_uris: [this.redirectUrl],
102
+ grant_types: ["authorization_code"],
103
+ response_types: ["code"],
104
+ client_name: "Lessie MCP",
105
+ token_endpoint_auth_method: "client_secret_basic",
106
+ };
107
+ }
108
+ clientInformation() {
109
+ return this._storage.clientInfo;
110
+ }
111
+ async saveClientInformation(info) {
112
+ this._storage.clientInfo = info;
113
+ persistStorage(this._storage);
114
+ }
115
+ async tokens() {
116
+ return this._storage.tokens;
117
+ }
118
+ async saveTokens(tokens) {
119
+ this._storage.tokens = tokens;
120
+ this._storage.tokensSavedAt = Date.now();
121
+ delete this._storage.codeVerifier;
122
+ this._codeVerifier = "";
123
+ persistStorage(this._storage);
124
+ }
125
+ /** 当前 access_token 的剩余有效秒数(基于本地时钟估算) */
126
+ remainingSeconds() {
127
+ if (!this._storage.tokens?.expires_in || !this._storage.tokensSavedAt)
128
+ return 0;
129
+ const elapsed = (Date.now() - this._storage.tokensSavedAt) / 1000;
130
+ return Math.max(0, Math.floor(this._storage.tokens.expires_in - elapsed));
131
+ }
132
+ /**
133
+ * SDK 在需要用户授权时调用此方法。
134
+ * 在 prepareCallbackServer() 已监听的 HTTP 服务器上挂载回调处理逻辑,等待浏览器回调。
135
+ *
136
+ * 必须先调用 prepareCallbackServer(),否则抛出异常。
137
+ */
138
+ async redirectToAuthorization(url) {
139
+ if (!this._httpServer?.listening) {
140
+ throw new Error("No listening server — prepareCallbackServer() must be called before redirectToAuthorization()");
141
+ }
142
+ this._expectedState = url.searchParams.get("state") || "";
143
+ this.waitingSince = null;
144
+ let resolveCode;
145
+ let rejectCode;
146
+ this._callbackPromise = new Promise((resolve, reject) => {
147
+ resolveCode = resolve;
148
+ rejectCode = reject;
149
+ });
150
+ this._httpServer.removeAllListeners("request");
151
+ this._httpServer.removeAllListeners("error");
152
+ this._httpServer.on("error", (err) => {
153
+ this._cleanupServer();
154
+ rejectCode(new Error(`Callback server error: ${err.message}`));
155
+ });
156
+ this._httpServer.on("request", (req, res) => {
157
+ const reqUrl = new URL(req.url, `http://127.0.0.1:${this._callbackPort}`);
158
+ if (reqUrl.pathname !== "/callback") {
159
+ res.writeHead(404);
160
+ res.end();
161
+ return;
162
+ }
163
+ if (this._expectedState) {
164
+ const callbackState = reqUrl.searchParams.get("state");
165
+ if (callbackState !== this._expectedState) {
166
+ res.writeHead(403, { "Content-Type": "text/plain" });
167
+ res.end("Invalid state parameter");
168
+ return;
169
+ }
170
+ }
171
+ const code = reqUrl.searchParams.get("code");
172
+ if (code) {
173
+ resolveCode(code);
174
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
175
+ res.end("<!DOCTYPE html><html><body>" +
176
+ "<h1>Authorization Successful</h1>" +
177
+ "<p>You may close this page and return to your agent.</p>" +
178
+ "</body></html>");
179
+ this._cleanupServer();
180
+ }
181
+ else {
182
+ const error = reqUrl.searchParams.get("error") ?? "unknown_error";
183
+ rejectCode(new Error(`Authorization denied: ${error}`));
184
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
185
+ res.end("<!DOCTYPE html><html><body>" +
186
+ "<h1>Authorization Failed</h1>" +
187
+ `<p>${escapeHtml(error)}</p>` +
188
+ "</body></html>");
189
+ this._cleanupServer();
190
+ }
191
+ });
192
+ this.waitingSince = Date.now();
193
+ this._callbackTimer = setTimeout(() => {
194
+ this._cleanupServer();
195
+ rejectCode(new Error("Authorization timed out — no callback received within 2 minutes"));
196
+ }, CALLBACK_TIMEOUT_MS);
197
+ this.pendingAuthUrl = url.toString();
198
+ }
199
+ _cleanupServer() {
200
+ if (this._callbackTimer) {
201
+ clearTimeout(this._callbackTimer);
202
+ this._callbackTimer = null;
203
+ }
204
+ if (this._httpServer) {
205
+ const server = this._httpServer;
206
+ this._httpServer = null;
207
+ this.waitingSince = null;
208
+ setTimeout(() => server.close(), 500);
209
+ }
210
+ }
211
+ /**
212
+ * 等待用户在浏览器中完成授权后回调,返回授权码。
213
+ * 必须在 redirectToAuthorization 之后调用。
214
+ */
215
+ async waitForCallback() {
216
+ if (!this._callbackPromise)
217
+ throw new Error("No pending authorization flow");
218
+ return this._callbackPromise;
219
+ }
220
+ async saveCodeVerifier(codeVerifier) {
221
+ this._codeVerifier = codeVerifier;
222
+ this._storage.codeVerifier = codeVerifier;
223
+ persistStorage(this._storage);
224
+ }
225
+ async codeVerifier() {
226
+ return this._codeVerifier || this._storage.codeVerifier || "";
227
+ }
228
+ async saveDiscoveryState(state) {
229
+ this._storage.discoveryState = state;
230
+ persistStorage(this._storage);
231
+ }
232
+ discoveryState() {
233
+ return this._storage.discoveryState;
234
+ }
235
+ async invalidateCredentials(scope) {
236
+ if (scope === "all" || scope === "client") {
237
+ delete this._storage.clientInfo;
238
+ }
239
+ if (scope === "all" || scope === "tokens") {
240
+ delete this._storage.tokens;
241
+ delete this._storage.tokensSavedAt;
242
+ }
243
+ if (scope === "all" || scope === "verifier") {
244
+ this._codeVerifier = "";
245
+ }
246
+ if (scope === "all" || scope === "discovery") {
247
+ delete this._storage.discoveryState;
248
+ }
249
+ persistStorage(this._storage);
250
+ }
251
+ }
252
+ function escapeHtml(s) {
253
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
254
+ }
255
+ /** 全局单例,供 remote / tools 共享 */
256
+ export const authProvider = new LessieAuthProvider();
package/dist/config.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 环境变量与静态配置。
3
+ *
4
+ * 所有外部可配置项在此集中读取,其余模块通过 import 获取,
5
+ * 避免散落的 process.env 访问。
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ function readPkgVersion() {
9
+ try {
10
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf-8");
11
+ return JSON.parse(raw).version;
12
+ }
13
+ catch {
14
+ return "0.0.0";
15
+ }
16
+ }
17
+ export const pkg = { version: readPkgVersion() };
18
+ /** 远程 MCP Server 地址 */
19
+ export const REMOTE_MCP_URL = process.env.LESSIE_REMOTE_MCP_URL || "https://www.lessie.ai/mcp-server/mcp";
20
+ /** 读取 SKILL.md 作为 MCP instructions,跳过 YAML front-matter */
21
+ export function loadInstructions() {
22
+ try {
23
+ const raw = readFileSync(new URL("../SKILL.md", import.meta.url), "utf-8");
24
+ return raw.replace(/^---[\s\S]*?---\n*/, "").trim() || undefined;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }