mnote-mcp 1.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.
@@ -0,0 +1,101 @@
1
+ export interface NowenApiConfig {
2
+ baseUrl: string;
3
+ username?: string;
4
+ password?: string;
5
+ token?: string;
6
+ }
7
+ export declare class NowenApiClient {
8
+ private baseUrl;
9
+ private username;
10
+ private password;
11
+ private token;
12
+ private readonly isApiToken;
13
+ constructor(config: NowenApiConfig);
14
+ private login;
15
+ private ensureAuth;
16
+ private get authHeader();
17
+ private request;
18
+ listNotebooks(): Promise<any[]>;
19
+ createNotebook(params: {
20
+ name: string;
21
+ parentId?: string;
22
+ icon?: string;
23
+ color?: string;
24
+ }): Promise<any>;
25
+ updateNotebook(id: string, params: {
26
+ name?: string;
27
+ icon?: string;
28
+ color?: string;
29
+ parentId?: string;
30
+ }): Promise<any>;
31
+ deleteNotebook(id: string): Promise<any>;
32
+ listNotes(params?: {
33
+ notebookId?: string;
34
+ isFavorite?: string;
35
+ isTrashed?: string;
36
+ tagId?: string;
37
+ search?: string;
38
+ dateFrom?: string;
39
+ dateTo?: string;
40
+ }): Promise<any[]>;
41
+ getNote(id: string): Promise<any>;
42
+ createNote(params: {
43
+ notebookId: string;
44
+ title?: string;
45
+ content?: string;
46
+ contentText?: string;
47
+ }): Promise<any>;
48
+ updateNote(id: string, params: {
49
+ title?: string;
50
+ content?: string;
51
+ contentText?: string;
52
+ notebookId?: string;
53
+ isPinned?: number;
54
+ isFavorite?: number;
55
+ isLocked?: number;
56
+ isTrashed?: number;
57
+ version?: number;
58
+ }): Promise<any>;
59
+ deleteNote(id: string): Promise<any>;
60
+ listTags(): Promise<any[]>;
61
+ createTag(params: {
62
+ name: string;
63
+ color?: string;
64
+ }): Promise<any>;
65
+ addTagToNote(noteId: string, tagId: string): Promise<any>;
66
+ removeTagFromNote(noteId: string, tagId: string): Promise<any>;
67
+ search(q: string): Promise<any[]>;
68
+ askKnowledge(question: string): Promise<{
69
+ answer: string;
70
+ references: {
71
+ id: string;
72
+ title: string;
73
+ }[];
74
+ }>;
75
+ aiChat(params: {
76
+ action: string;
77
+ text: string;
78
+ context?: string;
79
+ customPrompt?: string;
80
+ }): Promise<string>;
81
+ knowledgeStats(): Promise<any>;
82
+ listPlugins(): Promise<any[]>;
83
+ executePlugin(name: string, params: Record<string, any>): Promise<any>;
84
+ listWebhooks(): Promise<any[]>;
85
+ createWebhook(params: {
86
+ url: string;
87
+ events?: string[];
88
+ description?: string;
89
+ }): Promise<any>;
90
+ queryAuditLogs(params?: Record<string, string>): Promise<any>;
91
+ getAuditStats(): Promise<any>;
92
+ listBackups(): Promise<any[]>;
93
+ createBackup(type?: string): Promise<any>;
94
+ listApiTokens(): Promise<any>;
95
+ createApiToken(params: {
96
+ name: string;
97
+ scopes?: string[];
98
+ expiresInDays?: number;
99
+ }): Promise<any>;
100
+ deleteApiToken(id: string): Promise<any>;
101
+ }
@@ -0,0 +1,234 @@
1
+ export class NowenApiClient {
2
+ baseUrl;
3
+ username;
4
+ password;
5
+ token = null;
6
+ isApiToken;
7
+ constructor(config) {
8
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
9
+ this.username = config.username || "";
10
+ this.password = config.password || "";
11
+ this.isApiToken = !!config.token;
12
+ if (config.token) {
13
+ this.token = config.token;
14
+ }
15
+ }
16
+ async login() {
17
+ const res = await fetch(`${this.baseUrl}/api/auth/login`, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ username: this.username, password: this.password }),
21
+ });
22
+ if (!res.ok) {
23
+ const err = await res.text();
24
+ throw new Error(`登录失败 (${res.status}): ${err}`);
25
+ }
26
+ const data = await res.json();
27
+ this.token = data.token;
28
+ }
29
+ async ensureAuth() {
30
+ if (!this.token) {
31
+ if (this.isApiToken) {
32
+ throw new Error("API Token 未设置,请检查 NOWEN_TOKEN 环境变量");
33
+ }
34
+ await this.login();
35
+ }
36
+ }
37
+ get authHeader() {
38
+ return `Bearer ${this.token}`;
39
+ }
40
+ async request(path, options = {}) {
41
+ await this.ensureAuth();
42
+ let url = `${this.baseUrl}${path}`;
43
+ if (options.query) {
44
+ const params = new URLSearchParams();
45
+ for (const [k, v] of Object.entries(options.query)) {
46
+ if (v !== undefined && v !== null && v !== "") {
47
+ params.set(k, v);
48
+ }
49
+ }
50
+ const qs = params.toString();
51
+ if (qs)
52
+ url += `?${qs}`;
53
+ }
54
+ const headers = {
55
+ "Authorization": this.authHeader,
56
+ };
57
+ if (options.body) {
58
+ headers["Content-Type"] = "application/json";
59
+ }
60
+ const res = await fetch(url, {
61
+ method: options.method || "GET",
62
+ headers,
63
+ body: options.body ? JSON.stringify(options.body) : undefined,
64
+ });
65
+ if (res.status === 401 && !this.isApiToken) {
66
+ this.token = null;
67
+ await this.login();
68
+ headers["Authorization"] = this.authHeader;
69
+ const retryRes = await fetch(url, {
70
+ method: options.method || "GET",
71
+ headers,
72
+ body: options.body ? JSON.stringify(options.body) : undefined,
73
+ });
74
+ if (!retryRes.ok) {
75
+ const err = await retryRes.text();
76
+ throw new Error(`API 请求失败 (${retryRes.status}): ${err}`);
77
+ }
78
+ return retryRes.json();
79
+ }
80
+ if (!res.ok) {
81
+ const err = await res.text();
82
+ throw new Error(`API 请求失败 (${res.status}): ${err}`);
83
+ }
84
+ return res.json();
85
+ }
86
+ async listNotebooks() {
87
+ return this.request("/api/notebooks");
88
+ }
89
+ async createNotebook(params) {
90
+ return this.request("/api/notebooks", { method: "POST", body: params });
91
+ }
92
+ async updateNotebook(id, params) {
93
+ return this.request(`/api/notebooks/${id}`, { method: "PUT", body: params });
94
+ }
95
+ async deleteNotebook(id) {
96
+ return this.request(`/api/notebooks/${id}`, { method: "DELETE" });
97
+ }
98
+ async listNotes(params) {
99
+ return this.request("/api/notes", { query: params });
100
+ }
101
+ async getNote(id) {
102
+ return this.request(`/api/notes/${id}`);
103
+ }
104
+ async createNote(params) {
105
+ return this.request("/api/notes", { method: "POST", body: params });
106
+ }
107
+ async updateNote(id, params) {
108
+ return this.request(`/api/notes/${id}`, { method: "PUT", body: params });
109
+ }
110
+ async deleteNote(id) {
111
+ return this.request(`/api/notes/${id}`, { method: "DELETE" });
112
+ }
113
+ async listTags() {
114
+ return this.request("/api/tags");
115
+ }
116
+ async createTag(params) {
117
+ return this.request("/api/tags", { method: "POST", body: params });
118
+ }
119
+ async addTagToNote(noteId, tagId) {
120
+ return this.request(`/api/tags/note/${noteId}/tag/${tagId}`, { method: "POST" });
121
+ }
122
+ async removeTagFromNote(noteId, tagId) {
123
+ return this.request(`/api/tags/note/${noteId}/tag/${tagId}`, { method: "DELETE" });
124
+ }
125
+ async search(q) {
126
+ return this.request("/api/search", { query: { q } });
127
+ }
128
+ async askKnowledge(question) {
129
+ await this.ensureAuth();
130
+ const res = await fetch(`${this.baseUrl}/api/ai/ask`, {
131
+ method: "POST",
132
+ headers: {
133
+ "Content-Type": "application/json",
134
+ "Authorization": this.authHeader,
135
+ },
136
+ body: JSON.stringify({ question }),
137
+ });
138
+ if (!res.ok) {
139
+ const err = await res.text();
140
+ throw new Error(`AI 请求失败 (${res.status}): ${err}`);
141
+ }
142
+ const text = await res.text();
143
+ const lines = text.split("\n");
144
+ let answer = "";
145
+ let references = [];
146
+ for (const line of lines) {
147
+ if (line.startsWith("event: references")) {
148
+ continue;
149
+ }
150
+ if (line.startsWith("data: ")) {
151
+ const data = line.slice(6);
152
+ if (data === "[DONE]")
153
+ break;
154
+ try {
155
+ const parsed = JSON.parse(data);
156
+ if (Array.isArray(parsed) && parsed[0]?.id && parsed[0]?.title) {
157
+ references = parsed;
158
+ continue;
159
+ }
160
+ }
161
+ catch {
162
+ }
163
+ answer += data;
164
+ }
165
+ }
166
+ return { answer, references };
167
+ }
168
+ async aiChat(params) {
169
+ await this.ensureAuth();
170
+ const res = await fetch(`${this.baseUrl}/api/ai/chat`, {
171
+ method: "POST",
172
+ headers: {
173
+ "Content-Type": "application/json",
174
+ "Authorization": this.authHeader,
175
+ },
176
+ body: JSON.stringify(params),
177
+ });
178
+ if (!res.ok) {
179
+ const err = await res.text();
180
+ throw new Error(`AI 请求失败 (${res.status}): ${err}`);
181
+ }
182
+ const text = await res.text();
183
+ const lines = text.split("\n");
184
+ let result = "";
185
+ for (const line of lines) {
186
+ if (line.startsWith("data: ")) {
187
+ const data = line.slice(6);
188
+ if (data === "[DONE]")
189
+ break;
190
+ result += data;
191
+ }
192
+ }
193
+ return result;
194
+ }
195
+ async knowledgeStats() {
196
+ return this.request("/api/ai/knowledge-stats");
197
+ }
198
+ async listPlugins() {
199
+ return this.request("/api/plugins");
200
+ }
201
+ async executePlugin(name, params) {
202
+ return this.request("/api/plugins/" + name + "/execute", {
203
+ method: "POST",
204
+ body: params,
205
+ });
206
+ }
207
+ async listWebhooks() {
208
+ return this.request("/api/webhooks");
209
+ }
210
+ async createWebhook(params) {
211
+ return this.request("/api/webhooks", { method: "POST", body: params });
212
+ }
213
+ async queryAuditLogs(params) {
214
+ return this.request("/api/audit", { query: params });
215
+ }
216
+ async getAuditStats() {
217
+ return this.request("/api/audit/stats");
218
+ }
219
+ async listBackups() {
220
+ return this.request("/api/backups");
221
+ }
222
+ async createBackup(type = "db-only") {
223
+ return this.request("/api/backups", { method: "POST", body: { type } });
224
+ }
225
+ async listApiTokens() {
226
+ return this.request("/api/tokens");
227
+ }
228
+ async createApiToken(params) {
229
+ return this.request("/api/tokens", { method: "POST", body: params });
230
+ }
231
+ async deleteApiToken(id) {
232
+ return this.request(`/api/tokens/${id}`, { method: "DELETE" });
233
+ }
234
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Nowen Note MCP Server
4
+ *
5
+ * 让 Codex CLI / Claude Desktop / Cursor 等 AI 工具直接操作 Nowen Note 笔记系统。
6
+ *
7
+ * 环境变量:
8
+ * NOWEN_URL — Nowen Note 后端地址(默认 http://localhost:3001)
9
+ * NOWEN_USERNAME — 登录用户名(默认 admin)
10
+ * NOWEN_PASSWORD — 登录密码(默认 admin123)
11
+ */
12
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Nowen Note MCP Server
4
+ *
5
+ * 让 Codex CLI / Claude Desktop / Cursor 等 AI 工具直接操作 Nowen Note 笔记系统。
6
+ *
7
+ * 环境变量:
8
+ * NOWEN_URL — Nowen Note 后端地址(默认 http://localhost:3001)
9
+ * NOWEN_USERNAME — 登录用户名(默认 admin)
10
+ * NOWEN_PASSWORD — 登录密码(默认 admin123)
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+ import { NowenApiClient } from "./api-client.js";
16
+ // ===== 从环境变量读取配置 =====
17
+ const config = {
18
+ baseUrl: process.env.NOWEN_URL || "http://localhost:3001",
19
+ };
20
+ // NOWEN_TOKEN 优先,其次 USERNAME/PASSWORD
21
+ if (process.env.NOWEN_TOKEN) {
22
+ config.token = process.env.NOWEN_TOKEN;
23
+ }
24
+ else {
25
+ config.username = process.env.NOWEN_USERNAME || "admin";
26
+ config.password = process.env.NOWEN_PASSWORD || "admin123";
27
+ }
28
+ const api = new NowenApiClient(config);
29
+ // ===== 创建 MCP Server =====
30
+ const server = new McpServer({
31
+ name: "nowen-note",
32
+ version: "1.0.0",
33
+ });
34
+ // ==================== 笔记本工具 ====================
35
+ server.tool("nowen_list_notebooks", "获取 Nowen Note 中的所有笔记本列表(支持树形结构),返回每个笔记本的 id、名称、图标、颜色和笔记数量", {}, async () => {
36
+ try {
37
+ const notebooks = await api.listNotebooks();
38
+ const summary = notebooks.map((nb) => ({
39
+ id: nb.id,
40
+ name: nb.name,
41
+ icon: nb.icon,
42
+ color: nb.color,
43
+ parentId: nb.parentId,
44
+ noteCount: nb.noteCount,
45
+ }));
46
+ return {
47
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
48
+ };
49
+ }
50
+ catch (err) {
51
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
52
+ }
53
+ });
54
+ server.tool("nowen_create_notebook", "在 Nowen Note 中创建一个新笔记本", {
55
+ name: z.string().describe("笔记本名称"),
56
+ parentId: z.string().optional().describe("父笔记本 ID(可选,创建子笔记本时使用)"),
57
+ icon: z.string().optional().describe("笔记本图标 emoji(默认 📒)"),
58
+ }, async ({ name, parentId, icon }) => {
59
+ try {
60
+ const notebook = await api.createNotebook({ name, parentId, icon });
61
+ return {
62
+ content: [{ type: "text", text: `笔记本创建成功:\n${JSON.stringify(notebook, null, 2)}` }],
63
+ };
64
+ }
65
+ catch (err) {
66
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
67
+ }
68
+ });
69
+ // ==================== 笔记工具 ====================
70
+ server.tool("nowen_list_notes", "获取 Nowen Note 中的笔记列表。可按笔记本、标签、收藏状态、日期范围等筛选。返回笔记概要(不含完整内容)", {
71
+ notebookId: z.string().optional().describe("按笔记本 ID 筛选"),
72
+ tagId: z.string().optional().describe("按标签 ID 筛选"),
73
+ isFavorite: z.boolean().optional().describe("是否只返回收藏笔记"),
74
+ isTrashed: z.boolean().optional().describe("是否只返回回收站笔记"),
75
+ search: z.string().optional().describe("全文搜索关键词"),
76
+ dateFrom: z.string().optional().describe("开始日期 YYYY-MM-DD"),
77
+ dateTo: z.string().optional().describe("结束日期 YYYY-MM-DD"),
78
+ }, async ({ notebookId, tagId, isFavorite, isTrashed, search, dateFrom, dateTo }) => {
79
+ try {
80
+ const query = {};
81
+ if (notebookId)
82
+ query.notebookId = notebookId;
83
+ if (tagId)
84
+ query.tagId = tagId;
85
+ if (isFavorite)
86
+ query.isFavorite = "1";
87
+ if (isTrashed)
88
+ query.isTrashed = "1";
89
+ if (search)
90
+ query.search = search;
91
+ if (dateFrom)
92
+ query.dateFrom = dateFrom;
93
+ if (dateTo)
94
+ query.dateTo = dateTo;
95
+ const notes = await api.listNotes(query);
96
+ const summary = notes.map((n) => ({
97
+ id: n.id,
98
+ title: n.title,
99
+ notebookId: n.notebookId,
100
+ isPinned: n.isPinned,
101
+ isFavorite: n.isFavorite,
102
+ isLocked: n.isLocked,
103
+ updatedAt: n.updatedAt,
104
+ contentPreview: n.contentText?.slice(0, 100),
105
+ }));
106
+ return {
107
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
108
+ };
109
+ }
110
+ catch (err) {
111
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
112
+ }
113
+ });
114
+ server.tool("nowen_read_note", "读取 Nowen Note 中指定笔记的完整内容(包括标题、正文、标签等全部信息)", {
115
+ noteId: z.string().describe("笔记 ID"),
116
+ }, async ({ noteId }) => {
117
+ try {
118
+ const note = await api.getNote(noteId);
119
+ // 提取纯文本内容供 AI 阅读
120
+ const result = {
121
+ id: note.id,
122
+ title: note.title,
123
+ notebookId: note.notebookId,
124
+ contentText: note.contentText,
125
+ isPinned: note.isPinned,
126
+ isFavorite: note.isFavorite,
127
+ isLocked: note.isLocked,
128
+ version: note.version,
129
+ tags: note.tags?.map((t) => ({ id: t.id, name: t.name })),
130
+ createdAt: note.createdAt,
131
+ updatedAt: note.updatedAt,
132
+ };
133
+ return {
134
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
135
+ };
136
+ }
137
+ catch (err) {
138
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
139
+ }
140
+ });
141
+ server.tool("nowen_create_note", "在 Nowen Note 中创建一篇新笔记。需要指定目标笔记本 ID", {
142
+ notebookId: z.string().describe("目标笔记本 ID"),
143
+ title: z.string().optional().describe("笔记标题(默认为'无标题笔记')"),
144
+ content: z.string().optional().describe("笔记内容(Markdown 纯文本)"),
145
+ }, async ({ notebookId, title, content }) => {
146
+ try {
147
+ const body = { notebookId };
148
+ if (title)
149
+ body.title = title;
150
+ if (content) {
151
+ // 将纯文本包装为 TipTap JSON 格式
152
+ body.content = JSON.stringify({
153
+ type: "doc",
154
+ content: [{ type: "paragraph", content: [{ type: "text", text: content }] }],
155
+ });
156
+ body.contentText = content;
157
+ }
158
+ const note = await api.createNote(body);
159
+ return {
160
+ content: [{ type: "text", text: `笔记创建成功:\n${JSON.stringify({ id: note.id, title: note.title }, null, 2)}` }],
161
+ };
162
+ }
163
+ catch (err) {
164
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
165
+ }
166
+ });
167
+ server.tool("nowen_update_note", "更新 Nowen Note 中已有笔记的标题或内容", {
168
+ noteId: z.string().describe("笔记 ID"),
169
+ title: z.string().optional().describe("新标题"),
170
+ content: z.string().optional().describe("新内容(Markdown 纯文本)"),
171
+ }, async ({ noteId, title, content }) => {
172
+ try {
173
+ const body = {};
174
+ if (title)
175
+ body.title = title;
176
+ if (content) {
177
+ body.content = JSON.stringify({
178
+ type: "doc",
179
+ content: [{ type: "paragraph", content: [{ type: "text", text: content }] }],
180
+ });
181
+ body.contentText = content;
182
+ }
183
+ const note = await api.updateNote(noteId, body);
184
+ return {
185
+ content: [{ type: "text", text: `笔记更新成功:\n${JSON.stringify({ id: note.id, title: note.title, version: note.version }, null, 2)}` }],
186
+ };
187
+ }
188
+ catch (err) {
189
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
190
+ }
191
+ });
192
+ server.tool("nowen_delete_note", "将 Nowen Note 中指定笔记移入回收站(软删除),或从回收站永久删除", {
193
+ noteId: z.string().describe("笔记 ID"),
194
+ permanent: z.boolean().optional().describe("是否永久删除(默认 false,仅移入回收站)"),
195
+ }, async ({ noteId, permanent }) => {
196
+ try {
197
+ if (permanent) {
198
+ await api.deleteNote(noteId);
199
+ return { content: [{ type: "text", text: `笔记已永久删除: ${noteId}` }] };
200
+ }
201
+ else {
202
+ await api.updateNote(noteId, { isTrashed: 1 });
203
+ return { content: [{ type: "text", text: `笔记已移入回收站: ${noteId}` }] };
204
+ }
205
+ }
206
+ catch (err) {
207
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
208
+ }
209
+ });
210
+ // ==================== 搜索工具 ====================
211
+ server.tool("nowen_search", "在 Nowen Note 中全文搜索笔记。使用 FTS5 全文索引,支持模糊匹配,返回匹配的笔记摘要和高亮片段", {
212
+ query: z.string().describe("搜索关键词"),
213
+ }, async ({ query }) => {
214
+ try {
215
+ const results = await api.search(query);
216
+ const summary = results.map((r) => ({
217
+ id: r.id,
218
+ title: r.title,
219
+ snippet: r.snippet,
220
+ notebookId: r.notebookId,
221
+ updatedAt: r.updatedAt,
222
+ }));
223
+ return {
224
+ content: [{ type: "text", text: `找到 ${results.length} 条结果:\n${JSON.stringify(summary, null, 2)}` }],
225
+ };
226
+ }
227
+ catch (err) {
228
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
229
+ }
230
+ });
231
+ // ==================== 标签工具 ====================
232
+ server.tool("nowen_list_tags", "获取 Nowen Note 中的所有标签列表,包含每个标签的颜色和关联笔记数", {}, async () => {
233
+ try {
234
+ const tags = await api.listTags();
235
+ const summary = tags.map((t) => ({
236
+ id: t.id,
237
+ name: t.name,
238
+ color: t.color,
239
+ noteCount: t.noteCount,
240
+ }));
241
+ return {
242
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
243
+ };
244
+ }
245
+ catch (err) {
246
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
247
+ }
248
+ });
249
+ server.tool("nowen_manage_tags", "管理 Nowen Note 中笔记的标签:创建标签、给笔记添加标签、或移除笔记标签", {
250
+ action: z.enum(["create", "add_to_note", "remove_from_note"]).describe("操作类型: create=创建标签, add_to_note=给笔记添加标签, remove_from_note=移除笔记标签"),
251
+ tagName: z.string().optional().describe("标签名称(创建时必填)"),
252
+ tagColor: z.string().optional().describe("标签颜色(创建时可选,默认蓝色)"),
253
+ tagId: z.string().optional().describe("标签 ID(添加/移除时必填)"),
254
+ noteId: z.string().optional().describe("笔记 ID(添加/移除时必填)"),
255
+ }, async ({ action, tagName, tagColor, tagId, noteId }) => {
256
+ try {
257
+ switch (action) {
258
+ case "create": {
259
+ if (!tagName)
260
+ throw new Error("创建标签时 tagName 必填");
261
+ const tag = await api.createTag({ name: tagName, color: tagColor });
262
+ return { content: [{ type: "text", text: `标签创建成功:\n${JSON.stringify(tag, null, 2)}` }] };
263
+ }
264
+ case "add_to_note": {
265
+ if (!noteId || !tagId)
266
+ throw new Error("添加标签时 noteId 和 tagId 必填");
267
+ await api.addTagToNote(noteId, tagId);
268
+ return { content: [{ type: "text", text: `已为笔记 ${noteId} 添加标签 ${tagId}` }] };
269
+ }
270
+ case "remove_from_note": {
271
+ if (!noteId || !tagId)
272
+ throw new Error("移除标签时 noteId 和 tagId 必填");
273
+ await api.removeTagFromNote(noteId, tagId);
274
+ return { content: [{ type: "text", text: `已移除笔记 ${noteId} 的标签 ${tagId}` }] };
275
+ }
276
+ }
277
+ }
278
+ catch (err) {
279
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
280
+ }
281
+ });
282
+ // ==================== AI 工具 ====================
283
+ server.tool("nowen_ai_ask", "向 Nowen Note 知识库提问。系统会自动检索相关笔记内容,结合 AI 生成回答,并标注信息来源", {
284
+ question: z.string().describe("要提问的问题"),
285
+ }, async ({ question }) => {
286
+ try {
287
+ const { answer, references } = await api.askKnowledge(question);
288
+ let text = answer;
289
+ if (references.length > 0) {
290
+ text += "\n\n📌 参考笔记:\n";
291
+ text += references.map((r, i) => ` ${i + 1}. [${r.title}] (id: ${r.id})`).join("\n");
292
+ }
293
+ return { content: [{ type: "text", text }] };
294
+ }
295
+ catch (err) {
296
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
297
+ }
298
+ });
299
+ server.tool("nowen_ai_process", "使用 AI 处理笔记文本。支持续写、改写、润色、精简、扩展、翻译、摘要、解释、纠错、格式化等操作", {
300
+ action: z.enum([
301
+ "continue", "rewrite", "polish", "shorten", "expand",
302
+ "translate_en", "translate_zh", "summarize", "explain",
303
+ "fix_grammar", "format_markdown", "format_code", "custom",
304
+ ]).describe("处理类型"),
305
+ text: z.string().describe("要处理的文本内容"),
306
+ customPrompt: z.string().optional().describe("自定义指令(action 为 custom 时必填)"),
307
+ }, async ({ action, text, customPrompt }) => {
308
+ try {
309
+ const result = await api.aiChat({ action, text, customPrompt });
310
+ return { content: [{ type: "text", text: result }] };
311
+ }
312
+ catch (err) {
313
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
314
+ }
315
+ });
316
+ server.tool("nowen_knowledge_stats", "获取 Nowen Note 知识库的统计信息,包括笔记数、笔记本数、标签数、FTS 索引状态等", {}, async () => {
317
+ try {
318
+ const stats = await api.knowledgeStats();
319
+ return {
320
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
321
+ };
322
+ }
323
+ catch (err) {
324
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
325
+ }
326
+ });
327
+ // ==================== 插件工具 ====================
328
+ server.tool("nowen_list_plugins", "获取 Nowen Note 中已加载的插件列表,每个插件声明了它支持的能力(action)", {}, async () => {
329
+ try {
330
+ const plugins = await api.listPlugins();
331
+ return { content: [{ type: "text", text: JSON.stringify(plugins, null, 2) }] };
332
+ }
333
+ catch (err) {
334
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
335
+ }
336
+ });
337
+ server.tool("nowen_execute_plugin", "执行 Nowen Note 中指定名称的插件,传入参数并获取处理结果", {
338
+ pluginName: z.string().describe("插件名称"),
339
+ params: z.record(z.string(), z.any()).describe("传给插件的参数对象"),
340
+ }, async ({ pluginName, params }) => {
341
+ try {
342
+ const result = await api.executePlugin(pluginName, params);
343
+ if (result.text) {
344
+ return { content: [{ type: "text", text: result.text }] };
345
+ }
346
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
347
+ }
348
+ catch (err) {
349
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
350
+ }
351
+ });
352
+ // ==================== Webhook 工具 ====================
353
+ server.tool("nowen_list_webhooks", "获取 Nowen Note 中已配置的 Webhook 列表", {}, async () => {
354
+ try {
355
+ const webhooks = await api.listWebhooks();
356
+ return { content: [{ type: "text", text: JSON.stringify(webhooks, null, 2) }] };
357
+ }
358
+ catch (err) {
359
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
360
+ }
361
+ });
362
+ server.tool("nowen_create_webhook", "在 Nowen Note 中创建新的 Webhook,当指定事件发生时自动推送 HTTP 通知", {
363
+ url: z.string().describe("Webhook 回调 URL"),
364
+ events: z.array(z.string()).optional().describe("监听的事件列表,如 ['note.created', 'note.updated'],默认为 ['*'] 接收所有"),
365
+ description: z.string().optional().describe("描述"),
366
+ }, async ({ url, events, description }) => {
367
+ try {
368
+ const webhook = await api.createWebhook({ url, events, description });
369
+ return { content: [{ type: "text", text: `Webhook 创建成功!\nID: ${webhook.id}\nSecret: ${webhook.secret}\n\n请妥善保存 Secret,后续不会再显示。` }] };
370
+ }
371
+ catch (err) {
372
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
373
+ }
374
+ });
375
+ // ==================== 审计日志工具 ====================
376
+ server.tool("nowen_audit_stats", "获取 Nowen Note 的操作审计统计(按分类、级别统计,以及最近操作记录)", {}, async () => {
377
+ try {
378
+ const stats = await api.getAuditStats();
379
+ let text = `📊 审计统计\n总记录: ${stats.total} | 今日: ${stats.todayCount}\n\n`;
380
+ text += `按分类:\n`;
381
+ for (const c of stats.byCategory || []) {
382
+ text += ` ${c.category}: ${c.count}\n`;
383
+ }
384
+ text += `\n最近操作:\n`;
385
+ for (const r of (stats.recent || []).slice(0, 5)) {
386
+ text += ` [${r.level}] ${r.category}.${r.action} — ${r.createdAt}\n`;
387
+ }
388
+ return { content: [{ type: "text", text }] };
389
+ }
390
+ catch (err) {
391
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
392
+ }
393
+ });
394
+ // ==================== 备份工具 ====================
395
+ server.tool("nowen_list_backups", "获取 Nowen Note 的数据备份列表", {}, async () => {
396
+ try {
397
+ const backups = await api.listBackups();
398
+ if (backups.length === 0) {
399
+ return { content: [{ type: "text", text: "暂无备份" }] };
400
+ }
401
+ const text = backups.map(b => `📦 ${b.filename}\n 大小: ${(b.size / 1024).toFixed(1)}KB | 类型: ${b.type} | 笔记: ${b.noteCount} | 时间: ${b.createdAt}`).join("\n\n");
402
+ return { content: [{ type: "text", text }] };
403
+ }
404
+ catch (err) {
405
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
406
+ }
407
+ });
408
+ server.tool("nowen_create_backup", "创建 Nowen Note 数据备份。db-only 仅备份数据库,full 备份所有数据", {
409
+ type: z.enum(["db-only", "full"]).optional().describe("备份类型,默认 db-only"),
410
+ }, async ({ type }) => {
411
+ try {
412
+ const backup = await api.createBackup(type || "db-only");
413
+ return { content: [{ type: "text", text: `✅ 备份创建成功!\n文件: ${backup.filename}\n大小: ${(backup.size / 1024).toFixed(1)}KB\n笔记: ${backup.noteCount} 篇\n校验: ${backup.checksum}` }] };
414
+ }
415
+ catch (err) {
416
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
417
+ }
418
+ });
419
+ // ==================== API Token 管理 ====================
420
+ server.tool("nowen_list_api_tokens", "列出当前用户的所有 API Token(不返回明文),包含可用 scope 列表", {}, async () => {
421
+ try {
422
+ const data = await api.listApiTokens();
423
+ const lines = data.tokens.map((t) => `${t.revokedAt ? "❌ [已吊销]" : "✅ [活跃]"} ${t.name} (${t.id.slice(0, 8)}...)\n scope: ${t.scopes?.join(", ") || "全部"} | 过期: ${t.expiresAt || "永不过期"} | 最近使用: ${t.lastUsedAt || "从未"}`);
424
+ const text = [`可用 scope: ${data.availableScopes?.join(", ") || "全部"}`, "", ...lines].join("\n");
425
+ return { content: [{ type: "text", text }] };
426
+ }
427
+ catch (err) {
428
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
429
+ }
430
+ });
431
+ server.tool("nowen_create_api_token", "创建一个新的长期 API Token(nkn_xxx 格式)。注意:明文只返回一次,请妥善保存。\n创建需要登录 JWT 凭证,不能用 API Token 创建 API Token", {
432
+ name: z.string().describe("Token 名称,例如 'claude-mcp'"),
433
+ scopes: z.array(z.string()).optional().describe("scope 列表,空数组或留空表示全部权限。可选值: notes:read, notes:write, notebooks:read, notebooks:write, attachments:write, tags:read, tags:write, export:import"),
434
+ expiresInDays: z.number().optional().describe("过期天数,不传则永不过期。推荐: 30/90/365"),
435
+ }, async ({ name, scopes, expiresInDays }) => {
436
+ try {
437
+ const result = await api.createApiToken({ name, scopes, expiresInDays });
438
+ return {
439
+ content: [{ type: "text", text: `✅ API Token 创建成功!
440
+ 名称: ${result.name}
441
+ 明文: ${result.token}
442
+ scope: ${result.scopes?.join(", ") || "全部"}
443
+ 过期: ${result.expiresAt || "永不过期"}
444
+
445
+ ⚠️ 该 token 只会显示这一次,请立即保存!
446
+ 设置环境变量: NOWEN_TOKEN=${result.token}` }],
447
+ };
448
+ }
449
+ catch (err) {
450
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
451
+ }
452
+ });
453
+ server.tool("nowen_delete_api_token", "吊销指定 ID 的 API Token,吊销后不可恢复", {
454
+ tokenId: z.string().describe("要吊销的 Token ID(可用 nowen_list_api_tokens 查看)"),
455
+ }, async ({ tokenId }) => {
456
+ try {
457
+ await api.deleteApiToken(tokenId);
458
+ return { content: [{ type: "text", text: `✅ Token 已吊销: ${tokenId}` }] };
459
+ }
460
+ catch (err) {
461
+ return { content: [{ type: "text", text: `错误: ${err.message}` }], isError: true };
462
+ }
463
+ });
464
+ // ==================== 资源:笔记本列表 ====================
465
+ server.resource("notebooks", "nowen://notebooks", async (uri) => {
466
+ const notebooks = await api.listNotebooks();
467
+ return {
468
+ contents: [{
469
+ uri: uri.href,
470
+ mimeType: "application/json",
471
+ text: JSON.stringify(notebooks, null, 2),
472
+ }],
473
+ };
474
+ });
475
+ // ==================== 启动 ====================
476
+ async function main() {
477
+ const transport = new StdioServerTransport();
478
+ await server.connect(transport);
479
+ console.error(`🚀 Nowen Note MCP Server 已启动`);
480
+ console.error(` 连接目标: ${config.baseUrl}`);
481
+ if (config.token) {
482
+ console.error(` 认证方式: NOWEN_TOKEN (静态 Token)`);
483
+ }
484
+ else {
485
+ console.error(` 用户: ${config.username}`);
486
+ }
487
+ }
488
+ main().catch((err) => {
489
+ console.error("MCP Server 启动失败:", err);
490
+ process.exit(1);
491
+ });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "mnote-mcp",
3
+ "version": "1.0.1",
4
+ "description": "mnote MCP Server — 让 AI 工具直接操作笔记系统",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mnote-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx watch src/index.ts",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.29.0",
20
+ "zod": "^4.4.3"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "latest",
24
+ "tsx": "latest",
25
+ "typescript": "latest"
26
+ }
27
+ }