note-mcp-server 1.0.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.
Files changed (4) hide show
  1. package/.env.example +11 -0
  2. package/README.md +144 -0
  3. package/index.js +350 -0
  4. package/package.json +37 -0
package/.env.example ADDED
@@ -0,0 +1,11 @@
1
+ # 筆記 API 基底(擇一方式設定)
2
+
3
+ # 方式 A:拆開(推薦,與 Laravel Socialite 路徑一致)
4
+ notes_url=https://notes.example.com
5
+ socialite=github
6
+ # 路徑上加密 token(與 key 擇一即可)
7
+ key=
8
+ # socialite_id=
9
+
10
+ # 方式 B:整段 API base(若設定則優先於方式 A,勿結尾斜線)
11
+ # NOTES_API_BASE=https://notes.example.com/api/notes/github/your-encrypted-token
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # note-mcp-server
2
+
3
+ 以 [Model Context Protocol](https://modelcontextprotocol.io/)(stdio)連接 **Notes API v2**,讓 Cursor 等客戶端透過工具列出/讀寫筆記、上傳圖片、發送 Web Push。
4
+
5
+ ---
6
+
7
+ ## 需求
8
+
9
+ - Node.js **18+**
10
+ - 已註冊之 Notes API(路徑形如:`{網站}/api/notes/{socialite}/{加密 token}`)
11
+
12
+ ---
13
+
14
+ ## 安裝
15
+
16
+ ```bash
17
+ cd /path/to/note-mcp-server
18
+ npm install
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 設定 `.env`
24
+
25
+ 在 **`index.js` 同一目錄**建立 `.env`(本專案已將 `.env` 列入 `.gitignore`,請勿提交私密內容)。
26
+
27
+ ### 方式 A:拆開設定(建議)
28
+
29
+ 與 Laravel 端 **Socialite 提供者代號** 及 **加密後的路徑 token** 對齊,由程式組出完整 API base:
30
+
31
+ | 變數 | 說明 |
32
+ |------|------|
33
+ | `notes_url` 或 `NOTES_URL` | 站台根網址,例如 `https://notes.example.com`(**不要**結尾斜線) |
34
+ | `socialite` 或 `SOCIALITE` | 路徑中的提供者代號,例如 `github`(依你後端路由為準) |
35
+ | `key` 或 `KEY` | 路徑最後一段的 **加密 token** |
36
+ | `socialite_id` 或 `SOCIALITE_ID` | 與 **`key` 擇一即可**;語意相同,都是該段加密字串 |
37
+
38
+ 實際請求的 base 為:
39
+
40
+ ```text
41
+ {notes_url}/api/notes/{socialite}/{key 或 socialite_id}
42
+ ```
43
+
44
+ **範例:**
45
+
46
+ ```env
47
+ notes_url=https://notes.example.com
48
+ socialite=github
49
+ key=eyJpdiI6I…(你的加密 token,整段貼上)
50
+ ```
51
+
52
+ 若你習慣用「加密 socialite id」命名,可改為:
53
+
54
+ ```env
55
+ notes_url=https://notes.example.com
56
+ socialite=github
57
+ socialite_id=eyJpdiI6I…
58
+ ```
59
+
60
+ ### 方式 B:整段 API base(選用)
61
+
62
+ 若已持有完整 API URL(到 token 為止、**無結尾斜線**),可只設:
63
+
64
+ | 變數 | 說明 |
65
+ |------|------|
66
+ | `NOTES_API_BASE` | 例如 `https://notes.example.com/api/notes/github/eyJ…` |
67
+
68
+ **優先序:** 只要設了 `NOTES_API_BASE`,就會 **忽略** 方式 A 的 `notes_url` / `socialite` / `key`。
69
+
70
+ ---
71
+
72
+ ## 參考檔案
73
+
74
+ - **`.env.example`**:不含密鑰的範本,可複製為 `.env` 再填入。
75
+ - 啟動時會執行 `dotenv`,從 **`index.js` 所在目錄** 讀取 `.env`,與終端機目前工作目錄無關(方便 Cursor 從任意 cwd 啟動 MCP)。
76
+
77
+ ---
78
+
79
+ ## 在 Cursor 中掛載
80
+
81
+ 於 **使用者** 或 **專案** 的 `.cursor/mcp.json` 加入(路徑請改成你的本機路徑):
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "note-assistant": {
87
+ "command": "node",
88
+ "args": ["/path/to/note-mcp-server/index.js"],
89
+ "env": {}
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ - **不必**在 `mcp.json` 重複貼 token**:放在 `note-mcp-server` 目錄下的 `.env` 即可。
96
+ - 若仍想由 Cursor 注入環境變數,可在 `env` 內設定 `NOTES_API_BASE` 或 `notes_url` / `socialite` / `key`(會覆寫/補齊系統環境;`.env` 仍會載入,順序依 `dotenv` 與既有 `process.env` 行為)。
97
+
98
+ 儲存後在 **Settings → Tools & MCPs** 確認 `note-assistant` 為啟用狀態;必要時 **Reload Window**。
99
+
100
+ > **提示:** 在 Agent 對話裡,內部工具伺服器名稱可能顯示為 `user-note-assistant`(`user-` 前綴),與設定裡的顯示名稱不同屬正常現象。
101
+
102
+ ---
103
+
104
+ ## 提供的 MCP 工具
105
+
106
+ | 工具 | 說明 |
107
+ |------|------|
108
+ | `list_notes` | GET 列表,可選 `limit`(預設 3) |
109
+ | `read_note` | GET 單筆,`c_id` |
110
+ | `create_note` | POST 新增,`content` 必填,`title` 可選 |
111
+ | `update_note` | POST 修改,`c_id`、`content` 必填,`title` 可選 |
112
+ | `delete_note` | POST 刪除,`c_id` + `action=delete` |
113
+ | `upload_image` | multipart,`image_base64`、可選 `filename` |
114
+ | `send_push` | POST 推播,`message` 必填,`title` / `url` 可選 |
115
+
116
+ 完整 REST 行為請對你的 API 網址加上 **`/help`** 查官方說明。
117
+
118
+ ---
119
+
120
+ ## 常見問題
121
+
122
+ ### 啟動失敗、訊息請建立 `.env`
123
+
124
+ 代表未滿足任一組條件:請設 **`NOTES_API_BASE`**,或同時設 **`notes_url` + `socialite` +(`key` 或 `socialite_id`)**。
125
+
126
+ ### 不要用終端機手動當 MCP 用
127
+
128
+ MCP 須由 **Cursor 子程序** 以 **stdio** 啟動;在終端機長駐執行 `node index.js` **不會**自動掛進 Cursor。
129
+
130
+ ### 手動除錯(可選)
131
+
132
+ 僅供本機檢查流程,**不是**正式掛載 MCP:
133
+
134
+ ```bash
135
+ node /path/to/note-mcp-server/index.js
136
+ ```
137
+
138
+ 程序會卡在 stdio 等待,除錯完請 Ctrl+C 結束。
139
+
140
+ ---
141
+
142
+ ## 授權
143
+
144
+ ISC(見 `package.json`)。
package/index.js ADDED
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { config as loadEnv } from "dotenv";
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import {
9
+ CallToolRequestSchema,
10
+ ListToolsRequestSchema,
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import axios from "axios";
13
+ import FormData from "form-data";
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ loadEnv({ path: path.join(__dirname, ".env") });
18
+
19
+ /**
20
+ * MCP 須由 Cursor 依 mcp.json 啟動;勿在終端機手動跑本檔當成「掛載 MCP」。
21
+ *
22
+ * 設定優先序:
23
+ * 1) NOTES_API_BASE — 完整 API base(含 /api/notes/{socialite}/{key})
24
+ * 2) notes_url + socialite + key(或 socialite_id,與 key 同義:加密路徑 token)
25
+ *
26
+ * 變數名支援常見大小寫(見 resolveNotesApiBase)。
27
+ */
28
+
29
+ function stripTrailingSlashes(value) {
30
+ return value.replace(/\/+$/, "");
31
+ }
32
+
33
+ function firstEnv(...keys) {
34
+ for (const key of keys) {
35
+ const raw = process.env[key];
36
+ if (raw === undefined) {
37
+ continue;
38
+ }
39
+ const trimmed = String(raw).trim();
40
+ if (trimmed !== "") {
41
+ return trimmed;
42
+ }
43
+ }
44
+ return "";
45
+ }
46
+
47
+ function resolveNotesApiBase() {
48
+ const direct = firstEnv("NOTES_API_BASE");
49
+ if (direct) {
50
+ return stripTrailingSlashes(direct);
51
+ }
52
+
53
+ const notesUrl = stripTrailingSlashes(
54
+ firstEnv("notes_url", "NOTES_URL")
55
+ );
56
+ const socialite = firstEnv("socialite", "SOCIALITE");
57
+ const encryptedToken = firstEnv(
58
+ "key",
59
+ "KEY",
60
+ "socialite_id",
61
+ "SOCIALITE_ID"
62
+ );
63
+
64
+ if (notesUrl && socialite && encryptedToken) {
65
+ return `${notesUrl}/api/notes/${socialite}/${encryptedToken}`;
66
+ }
67
+
68
+ throw new Error(
69
+ "note-mcp-server:請在專案目錄建立 .env,設定 notes_url、socialite、key(或 socialite_id),或設定 NOTES_API_BASE。詳見 .env.example。"
70
+ );
71
+ }
72
+
73
+ const BASE_URL = resolveNotesApiBase();
74
+
75
+ const server = new Server(
76
+ {
77
+ name: "note-mcp-server",
78
+ version: "1.0.0",
79
+ },
80
+ {
81
+ capabilities: {
82
+ tools: {},
83
+ },
84
+ }
85
+ );
86
+
87
+ function formatAxiosError(err) {
88
+ if (axios.isAxiosError(err) && err.response?.data !== undefined) {
89
+ const body =
90
+ typeof err.response.data === "string"
91
+ ? err.response.data
92
+ : JSON.stringify(err.response.data);
93
+ return `${err.message} | 回應: ${body}`;
94
+ }
95
+ return err instanceof Error ? err.message : String(err);
96
+ }
97
+
98
+ function normalizeListPayload(data) {
99
+ if (Array.isArray(data)) {
100
+ return data;
101
+ }
102
+ if (data && Array.isArray(data.notes)) {
103
+ return data.notes;
104
+ }
105
+ if (data && Array.isArray(data.data)) {
106
+ return data.data;
107
+ }
108
+ return [];
109
+ }
110
+
111
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
112
+ return {
113
+ tools: [
114
+ {
115
+ name: "list_notes",
116
+ description: "列出筆記(GET 列表),可依 limit 截斷前 N 筆。",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ limit: {
121
+ type: "number",
122
+ description: "最多回傳幾筆(預設 3)",
123
+ default: 3,
124
+ },
125
+ },
126
+ },
127
+ },
128
+ {
129
+ name: "read_note",
130
+ description: "讀取單筆筆記(GET ?c_id=)。",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ c_id: {
135
+ type: "number",
136
+ description: "筆記 ID(c_id)",
137
+ },
138
+ },
139
+ required: ["c_id"],
140
+ },
141
+ },
142
+ {
143
+ name: "create_note",
144
+ description:
145
+ "新增筆記(POST:title、content;title 可省略由 API 自動產生)。內容建議符合 AdminLTE 4 / PrismJS 約定。",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ content: { type: "string", description: "HTML 或純文字內容" },
150
+ title: { type: "string", description: "標題(可選)" },
151
+ },
152
+ required: ["content"],
153
+ },
154
+ },
155
+ {
156
+ name: "update_note",
157
+ description: "修改筆記(POST:c_id、title、content)。",
158
+ inputSchema: {
159
+ type: "object",
160
+ properties: {
161
+ c_id: { type: "number", description: "筆記 ID" },
162
+ content: { type: "string", description: "新內容" },
163
+ title: { type: "string", description: "新標題(可選)" },
164
+ },
165
+ required: ["c_id", "content"],
166
+ },
167
+ },
168
+ {
169
+ name: "delete_note",
170
+ description: "刪除筆記(POST:c_id + action=delete)。",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ c_id: { type: "number", description: "要刪除的筆記 ID" },
175
+ },
176
+ required: ["c_id"],
177
+ },
178
+ },
179
+ {
180
+ name: "upload_image",
181
+ description:
182
+ "上傳圖片(multipart:action=upload_image、image=檔案)。請傳 image_base64 與可選 filename。",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {
186
+ image_base64: {
187
+ type: "string",
188
+ description: "圖檔 Base64(可含 data:image/...;base64, 前綴)",
189
+ },
190
+ filename: {
191
+ type: "string",
192
+ description: "檔名,例如 note.png(預設 upload.bin)",
193
+ },
194
+ },
195
+ required: ["image_base64"],
196
+ },
197
+ },
198
+ {
199
+ name: "send_push",
200
+ description: "發送 Web Push(POST:action=push、message;可選 title、url)。",
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ message: { type: "string", description: "推播主文" },
205
+ title: { type: "string", description: "推播標題(可選)" },
206
+ url: { type: "string", description: "點擊後開啟的網址(可選)" },
207
+ },
208
+ required: ["message"],
209
+ },
210
+ },
211
+ ],
212
+ };
213
+ });
214
+
215
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
216
+ const { name, arguments: rawArgs } = request.params;
217
+ const args = rawArgs ?? {};
218
+
219
+ try {
220
+ switch (name) {
221
+ case "list_notes": {
222
+ const limit = Number(args.limit) > 0 ? Number(args.limit) : 3;
223
+ const response = await axios.get(BASE_URL);
224
+ const list = normalizeListPayload(response.data).slice(0, limit);
225
+ return {
226
+ content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
227
+ };
228
+ }
229
+
230
+ case "read_note": {
231
+ const cId = Number(args.c_id);
232
+ const response = await axios.get(BASE_URL, {
233
+ params: { c_id: cId },
234
+ });
235
+ return {
236
+ content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
237
+ };
238
+ }
239
+
240
+ case "create_note": {
241
+ const payload = { content: args.content };
242
+ if (args.title !== undefined && args.title !== "") {
243
+ payload.title = args.title;
244
+ }
245
+ const response = await axios.post(BASE_URL, payload, {
246
+ headers: { "Content-Type": "application/json" },
247
+ });
248
+ return {
249
+ content: [
250
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
251
+ ],
252
+ };
253
+ }
254
+
255
+ case "update_note": {
256
+ const payload = {
257
+ c_id: Number(args.c_id),
258
+ content: args.content,
259
+ };
260
+ if (args.title !== undefined) {
261
+ payload.title = args.title;
262
+ }
263
+ const response = await axios.post(BASE_URL, payload, {
264
+ headers: { "Content-Type": "application/json" },
265
+ });
266
+ return {
267
+ content: [
268
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
269
+ ],
270
+ };
271
+ }
272
+
273
+ case "delete_note": {
274
+ const response = await axios.post(
275
+ BASE_URL,
276
+ { c_id: Number(args.c_id), action: "delete" },
277
+ { headers: { "Content-Type": "application/json" } }
278
+ );
279
+ return {
280
+ content: [
281
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
282
+ ],
283
+ };
284
+ }
285
+
286
+ case "upload_image": {
287
+ let b64 = String(args.image_base64).replace(/\s/g, "");
288
+ const dataUrl = /^data:[^;]+;base64,(.+)$/i.exec(b64);
289
+ if (dataUrl) {
290
+ b64 = dataUrl[1];
291
+ }
292
+ const buffer = Buffer.from(b64, "base64");
293
+ const form = new FormData();
294
+ form.append("action", "upload_image");
295
+ form.append("image", buffer, {
296
+ filename: args.filename || "upload.png",
297
+ });
298
+ const response = await axios.post(BASE_URL, form, {
299
+ headers: form.getHeaders(),
300
+ maxBodyLength: Infinity,
301
+ maxContentLength: Infinity,
302
+ });
303
+ return {
304
+ content: [
305
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
306
+ ],
307
+ };
308
+ }
309
+
310
+ case "send_push": {
311
+ const body = { action: "push", message: args.message };
312
+ if (args.title !== undefined) {
313
+ body.title = args.title;
314
+ }
315
+ if (args.url !== undefined) {
316
+ body.url = args.url;
317
+ }
318
+ const response = await axios.post(BASE_URL, body, {
319
+ headers: { "Content-Type": "application/json" },
320
+ });
321
+ return {
322
+ content: [
323
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
324
+ ],
325
+ };
326
+ }
327
+
328
+ default:
329
+ throw new Error(`未知工具: ${name}`);
330
+ }
331
+ } catch (error) {
332
+ return {
333
+ isError: true,
334
+ content: [{ type: "text", text: `API 錯誤: ${formatAxiosError(error)}` }],
335
+ };
336
+ }
337
+ });
338
+
339
+ async function runServer() {
340
+ const transport = new StdioServerTransport();
341
+ await server.connect(transport);
342
+ console.error(
343
+ `Personal Note Assistant MCP on stdio (base: ${BASE_URL.replace(/\/[^/]+$/, "/…")})`
344
+ );
345
+ }
346
+
347
+ runServer().catch((err) => {
348
+ console.error(err);
349
+ process.exit(1);
350
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "note-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP (stdio) server for Notes API v2 — list/read/write notes, upload images, Web Push from Cursor and compatible clients.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "note-mcp-server": "index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ ".env.example",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1",
16
+ "prepublishOnly": "node --check index.js"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "notes",
22
+ "cursor",
23
+ "stdio"
24
+ ],
25
+ "author": "",
26
+ "license": "ISC",
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
+ "axios": "^1.16.0",
34
+ "dotenv": "^16.4.5",
35
+ "form-data": "^4.0.0"
36
+ }
37
+ }