note-mcp-server 2.4.1 → 2.5.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/index.js +94 -33
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -12,73 +12,101 @@ import {
|
|
|
12
12
|
import axios from "axios";
|
|
13
13
|
import FormData from "form-data";
|
|
14
14
|
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { parse as parseEnv } from "dotenv";
|
|
17
|
+
|
|
15
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
19
|
|
|
17
|
-
// 優先載入專案目錄 (cwd) 的 .env,以支援不同專案自由切換
|
|
18
|
-
loadEnv({ path: path.join(process.cwd(), ".env") });
|
|
19
|
-
// 如果專案目錄沒有設定,則退回讀取全域的 .env
|
|
20
|
-
loadEnv({ path: path.join(__dirname, ".env") });
|
|
21
20
|
/**
|
|
22
21
|
* MCP 須由 Cursor 依 mcp.json 啟動;勿在終端機手動跑本檔當成「掛載 MCP」。
|
|
23
22
|
*
|
|
24
23
|
* 設定優先序:
|
|
25
|
-
* 1)
|
|
26
|
-
* 2)
|
|
24
|
+
* 1) 專案目錄 (cwd) 的 .env 優先(支援不同專案自由切換)
|
|
25
|
+
* 2) 全域的 note-mcp-server/.env 作為預設退路
|
|
26
|
+
*
|
|
27
|
+
* 變數組合:
|
|
28
|
+
* A) NOTES_API_BASE — 完整 API base(含 /api/notes/{socialite}/{key})
|
|
29
|
+
* B) notes_url + socialite + key(或 socialite_id,與 key 同義:加密路徑 token)
|
|
27
30
|
*
|
|
28
31
|
* 變數名支援常見大小寫(見 resolveNotesApiBase)。
|
|
29
32
|
*/
|
|
30
33
|
|
|
34
|
+
const cwdEnvPath = path.join(process.cwd(), ".env");
|
|
35
|
+
const cwdEnv = fs.existsSync(cwdEnvPath) ? parseEnv(fs.readFileSync(cwdEnvPath)) : {};
|
|
36
|
+
|
|
37
|
+
const globalEnvPath = path.join(__dirname, ".env");
|
|
38
|
+
const globalEnv = fs.existsSync(globalEnvPath) ? parseEnv(fs.readFileSync(globalEnvPath)) : {};
|
|
39
|
+
|
|
40
|
+
// 將載入的變數注入 process.env (cwd 優先蓋過 global)
|
|
41
|
+
for (const [k, v] of Object.entries(globalEnv)) {
|
|
42
|
+
if (process.env[k] === undefined) process.env[k] = v;
|
|
43
|
+
}
|
|
44
|
+
for (const [k, v] of Object.entries(cwdEnv)) {
|
|
45
|
+
process.env[k] = v; // cwd 總是能覆寫
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
function stripTrailingSlashes(value) {
|
|
32
49
|
return value.replace(/\/+$/, "");
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const trimmed = String(raw).trim();
|
|
42
|
-
if (trimmed !== "") {
|
|
43
|
-
return trimmed;
|
|
52
|
+
function extractBase(envObj) {
|
|
53
|
+
const get = (...keys) => {
|
|
54
|
+
for (const k of keys) {
|
|
55
|
+
if (envObj[k] !== undefined && String(envObj[k]).trim() !== "") {
|
|
56
|
+
return String(envObj[k]).trim();
|
|
57
|
+
}
|
|
44
58
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
59
|
+
return "";
|
|
60
|
+
};
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
const direct = firstEnv("NOTES_API_BASE");
|
|
62
|
+
const direct = get("NOTES_API_BASE");
|
|
51
63
|
if (direct) {
|
|
52
64
|
return stripTrailingSlashes(direct);
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
const notesUrl = stripTrailingSlashes(
|
|
56
|
-
|
|
57
|
-
);
|
|
58
|
-
const socialite = firstEnv("socialite", "SOCIALITE");
|
|
59
|
-
const encryptedToken = firstEnv(
|
|
60
|
-
"key",
|
|
61
|
-
"KEY",
|
|
62
|
-
"socialite_id",
|
|
63
|
-
"SOCIALITE_ID"
|
|
64
|
-
);
|
|
67
|
+
const notesUrl = stripTrailingSlashes(get("notes_url", "NOTES_URL"));
|
|
68
|
+
const socialite = get("socialite", "SOCIALITE");
|
|
69
|
+
const encryptedToken = get("key", "KEY", "socialite_id", "SOCIALITE_ID");
|
|
65
70
|
|
|
66
71
|
if (notesUrl && socialite && encryptedToken) {
|
|
67
72
|
return `${notesUrl}/api/notes/${socialite}/${encryptedToken}`;
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveNotesApiBase() {
|
|
79
|
+
let base = extractBase(cwdEnv);
|
|
80
|
+
if (base) return base;
|
|
81
|
+
|
|
82
|
+
base = extractBase(globalEnv);
|
|
83
|
+
if (base) return base;
|
|
84
|
+
|
|
85
|
+
base = extractBase(process.env);
|
|
86
|
+
if (base) return base;
|
|
87
|
+
|
|
70
88
|
throw new Error(
|
|
71
89
|
"note-mcp-server:請在專案目錄建立 .env,設定 notes_url、socialite、key(或 socialite_id),或設定 NOTES_API_BASE。詳見 .env.example。"
|
|
72
90
|
);
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
function firstEnv(...keys) {
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
if (process.env[key] !== undefined) {
|
|
96
|
+
const trimmed = String(process.env[key]).trim();
|
|
97
|
+
if (trimmed !== "") return trimmed;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
const BASE_URL = resolveNotesApiBase();
|
|
76
104
|
const APP_NAME = firstEnv("app_name", "APP_NAME", "notes_app_name", "NOTES_APP_NAME") || "Personal Note Assistant";
|
|
77
105
|
|
|
78
106
|
const server = new Server(
|
|
79
107
|
{
|
|
80
108
|
name: `note-mcp-server (${APP_NAME})`,
|
|
81
|
-
version: "2.4.
|
|
109
|
+
version: "2.4.2",
|
|
82
110
|
},
|
|
83
111
|
{
|
|
84
112
|
capabilities: {
|
|
@@ -164,6 +192,16 @@ function buildNoteUrl(cId) {
|
|
|
164
192
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
165
193
|
return {
|
|
166
194
|
tools: [
|
|
195
|
+
{
|
|
196
|
+
name: "block_list",
|
|
197
|
+
description: withSecurityNotice(
|
|
198
|
+
`列出 ${APP_NAME} 中當前用戶有權限建立內容的所有區塊(看板/筆記本等)。回傳的資料會包含每個區塊的名稱與可用的狀態標籤 (tags)。如果該區塊有設定狀態標籤(例如用於 Kanban 或是 Defect Flow),請在安排工作流程或建立內容時參考這些可用的標籤;若無狀態標籤,則通常為單純的 CMS 或企業知識庫,直接新增內容即可。回傳的 b_id 可用於 create_note 建立內容到指定看板。`
|
|
199
|
+
),
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
167
205
|
{
|
|
168
206
|
name: "list_notes",
|
|
169
207
|
description: withSecurityNotice(
|
|
@@ -199,18 +237,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
199
237
|
name: "create_note",
|
|
200
238
|
description: withCardGuide(
|
|
201
239
|
withSecurityNotice(
|
|
202
|
-
`在 ${APP_NAME}
|
|
240
|
+
`在 ${APP_NAME} 中新增筆記。未指定任何 b_id 的情況下,代表就是新增至「我的筆記」預設值。內容請優先使用 AdminLTE 4 可摺疊 card(見下方結構)。\nAI 行為:\n1. 除非用戶明確要求,否則不要傳 tags(勿自行推測、分類或補加標籤)。\n2. 若用戶僅提供區塊名稱(例如「看板」)而未提供 b_id,請先呼叫 block_list 查詢。若只有一個相符的名稱,直接使用該 b_id 建立;若有多個相符名稱,請列出選項並詢問用戶要加到哪一個 b_id。`
|
|
203
241
|
)
|
|
204
242
|
),
|
|
205
243
|
inputSchema: {
|
|
206
244
|
type: "object",
|
|
207
245
|
properties: {
|
|
246
|
+
b_id: { type: "number", description: "指定要建立的區塊 ID(可選,預設為個人筆記本)。可先用 block_list 查詢可用的 b_id" },
|
|
208
247
|
content: { type: "string", description: "HTML 或純文字內容" },
|
|
209
248
|
title: { type: "string", description: "標題(可選)" },
|
|
210
249
|
tags: {
|
|
211
250
|
type: "string",
|
|
212
251
|
description:
|
|
213
|
-
"
|
|
252
|
+
"標籤,多個以逗號分隔(可選;若指定 b_id 且該區塊有必填標籤時,不傳則會自動補上預設狀態標籤)",
|
|
214
253
|
},
|
|
215
254
|
},
|
|
216
255
|
required: ["content"],
|
|
@@ -226,6 +265,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
226
265
|
inputSchema: {
|
|
227
266
|
type: "object",
|
|
228
267
|
properties: {
|
|
268
|
+
b_id: { type: "number", description: "移動到指定的區塊 ID(可選)" },
|
|
229
269
|
c_id: { type: "number", description: "筆記 ID" },
|
|
230
270
|
content: { type: "string", description: "新內容(可選,若未傳則使用現有內容作為基底)" },
|
|
231
271
|
title: {
|
|
@@ -318,6 +358,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
318
358
|
|
|
319
359
|
try {
|
|
320
360
|
switch (name) {
|
|
361
|
+
case "block_list": {
|
|
362
|
+
const response = await axios.get(BASE_URL, {
|
|
363
|
+
params: { action: "list_blocks" },
|
|
364
|
+
});
|
|
365
|
+
const payload =
|
|
366
|
+
response.data && typeof response.data === "object"
|
|
367
|
+
? { ...response.data }
|
|
368
|
+
: response.data;
|
|
369
|
+
return {
|
|
370
|
+
content: [
|
|
371
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) },
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
321
376
|
case "list_notes": {
|
|
322
377
|
const params = {};
|
|
323
378
|
if (args.search) params.search = args.search;
|
|
@@ -368,6 +423,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
368
423
|
|
|
369
424
|
case "create_note": {
|
|
370
425
|
const payload = { content: args.content };
|
|
426
|
+
if (args.b_id !== undefined && args.b_id !== null) {
|
|
427
|
+
payload.b_id = Number(args.b_id);
|
|
428
|
+
}
|
|
371
429
|
if (args.title !== undefined && args.title !== "") {
|
|
372
430
|
payload.title = args.title;
|
|
373
431
|
}
|
|
@@ -388,6 +446,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
388
446
|
const payload = {
|
|
389
447
|
c_id: Number(args.c_id),
|
|
390
448
|
};
|
|
449
|
+
if (args.b_id !== undefined && args.b_id !== null) {
|
|
450
|
+
payload.b_id = Number(args.b_id);
|
|
451
|
+
}
|
|
391
452
|
if (args.content !== undefined) {
|
|
392
453
|
payload.content = args.content;
|
|
393
454
|
}
|
package/package.json
CHANGED