@dinoxx/dinox-cli 1.0.16 → 1.0.18

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
@@ -656,7 +656,103 @@ sync.timeoutMs (必须为正整数)
656
656
 
657
657
  ---
658
658
 
659
- ## 9. 查看帮助
659
+ ## 9. MCP Server 与 Cursor 配置
660
+
661
+ `dino mcp serve` 会启动一个 HTTP JSON-RPC 服务,提供 Dinox 的 MCP tools 给 AI 客户端调用。
662
+
663
+ ### 9.1 启动 MCP 服务
664
+
665
+ 默认监听本机 `127.0.0.1:45137`:
666
+
667
+ ```bash
668
+ dino mcp serve
669
+ ```
670
+
671
+ 手动指定端口:
672
+
673
+ ```bash
674
+ dino mcp serve --host 127.0.0.1 --port 45137
675
+ ```
676
+
677
+ ### 9.2 局域网访问与鉴权
678
+
679
+ 当 `--host` 为非回环地址(例如 `0.0.0.0`)时,必须提供 token:
680
+
681
+ ```bash
682
+ dino mcp serve --host 0.0.0.0 --port 45137 --token "<your-token>"
683
+ ```
684
+
685
+ 也可以使用环境变量:
686
+
687
+ ```bash
688
+ export DINOX_MCP_TOKEN="<your-token>"
689
+ dino mcp serve --host 0.0.0.0 --port 45137
690
+ ```
691
+
692
+ 请求头支持:
693
+
694
+ 1. `Authorization: Bearer <token>`
695
+ 2. `x-dinox-token: <token>`
696
+
697
+ ### 9.3 快速连通性验证(curl)
698
+
699
+ 初始化握手:
700
+
701
+ ```bash
702
+ curl -X POST http://127.0.0.1:45137 \
703
+ -H 'content-type: application/json' \
704
+ --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
705
+ ```
706
+
707
+ 列出 tools:
708
+
709
+ ```bash
710
+ curl -X POST http://127.0.0.1:45137 \
711
+ -H 'content-type: application/json' \
712
+ --data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
713
+ ```
714
+
715
+ 调用 tools(示例:搜索笔记):
716
+
717
+ ```bash
718
+ curl -X POST http://127.0.0.1:45137 \
719
+ -H 'content-type: application/json' \
720
+ --data '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"dinox_search_notes","arguments":{"query":"dinox","limit":5}}}'
721
+ ```
722
+
723
+ ### 9.4 Cursor 配置示例
724
+
725
+ 可在以下位置创建 MCP 配置文件(任选其一):
726
+
727
+ 1. 项目级:`.cursor/mcp.json`
728
+ 2. 全局:`~/.cursor/mcp.json`
729
+
730
+ 推荐配置(带 token):
731
+
732
+ ```json
733
+ {
734
+ "mcpServers": {
735
+ "dinox": {
736
+ "url": "http://127.0.0.1:45137",
737
+ "headers": {
738
+ "Authorization": "Bearer ${env:DINOX_MCP_TOKEN}"
739
+ }
740
+ }
741
+ }
742
+ }
743
+ ```
744
+
745
+ 若本地仅回环访问且不启用 token,可去掉 `headers` 字段。
746
+
747
+ ### 9.5 常见问题
748
+
749
+ - 报错 `--token (or DINOX_MCP_TOKEN) is required when --host is non-loopback`:表示你使用了非回环 host,但没提供 token。
750
+ - 返回 `401 Unauthorized`:检查请求头 token 是否与启动服务时一致。
751
+ - 端口冲突:改用 `--port` 指定其他高位端口。
752
+
753
+ ---
754
+
755
+ ## 10. 查看帮助
660
756
 
661
757
  随时可以看帮助:
662
758
 
@@ -675,17 +771,17 @@ dino note create --help
675
771
 
676
772
  ---
677
773
 
678
- ## 10. GitHub Action 自动发布 npm
774
+ ## 11. GitHub Action 自动发布 npm
679
775
 
680
776
  仓库已内置工作流:`/Users/shanks/Documents/GitHub/dinox-cli/.github/workflows/publish-npm.yml`
681
777
 
682
- ### 10.1 先配置 Secret
778
+ ### 11.1 先配置 Secret
683
779
 
684
780
  在 GitHub 仓库设置里添加:
685
781
 
686
782
  - `NPM_TOKEN`:npm 的 publish token(建议使用 granular token,并开启 bypass 2FA)
687
783
 
688
- ### 10.2 手动触发发布
784
+ ### 11.2 手动触发发布
689
785
 
690
786
  1. 打开 GitHub 仓库 → **Actions** → **Publish NPM**
691
787
  2. 点击 **Run workflow**
@@ -693,13 +789,13 @@ dino note create --help
693
789
  - `dry_run`:`true` 只做演练,不真正发布
694
790
  - `run_tests`:`true` 时会先执行测试
695
791
 
696
- ### 10.3 Push 自动触发
792
+ ### 11.3 Push 自动触发
697
793
 
698
794
  - 每次 push 到默认分支(当前为 `main`)会自动触发发布流程
699
795
  - 为兼容分支命名迁移,workflow 也监听 `master`,但发布阶段会只允许默认分支执行
700
796
  - 为防止递归触发,workflow 会跳过 `github-actions[bot]` 自己提交的版本 bump commit
701
797
 
702
- ### 10.4 工作流行为
798
+ ### 11.4 工作流行为
703
799
 
704
800
  - 只允许在仓库默认分支执行发布(默认分支改名后无需改 workflow 逻辑)
705
801
  - 调用仓库脚本 `publish-npm.sh` 自动递增 patch 版本并发布
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { registerDaemonCommands } from './daemon/index.js';
6
6
  import { registerGraphCommands } from './commands/graph/index.js';
7
7
  import { registerInfoCommand } from './commands/info/index.js';
8
8
  import { registerMCPCommands } from './commands/mcp/index.js';
9
+ import { registerMetaCommands } from './commands/meta/index.js';
9
10
  import { registerNoteCommands } from './commands/notes/index.js';
10
11
  import { registerPromptCommands } from './commands/prompt/index.js';
11
12
  import { registerSyncCommand } from './commands/sync.js';
@@ -32,6 +33,7 @@ export function createCli() {
32
33
  registerGraphCommands(program);
33
34
  registerInfoCommand(program);
34
35
  registerMCPCommands(program);
36
+ registerMetaCommands(program);
35
37
  registerSyncCommand(program);
36
38
  registerUpdateCommand(program);
37
39
  registerPromptCommands(program);
@@ -6,7 +6,7 @@ import { loadConfig } from '../../config/store.js';
6
6
  import { resolveConfig } from '../../config/resolve.js';
7
7
  import { connectPowerSync } from '../../powersync/runtime.js';
8
8
  import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
9
- import { getBacklinks, getOutlinks, getRelatedNotes } from './repo.js';
9
+ import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from './repo.js';
10
10
  function parseDepth(value) {
11
11
  if (value === undefined || value === null || value === '') {
12
12
  return 2;
@@ -74,6 +74,21 @@ export function registerGraphCommands(program) {
74
74
  const result = await sendDaemonRequest('graph.outlinks', { id });
75
75
  printYaml(result);
76
76
  });
77
+ graph
78
+ .command('stats')
79
+ .description('Get graph statistics')
80
+ .action(async (_options, command) => {
81
+ const globals = command.optsWithGlobals?.() ?? {};
82
+ const offline = Boolean(globals.offline);
83
+ if (offline) {
84
+ const result = await withOfflineDb(command, (db) => getGraphStats(db));
85
+ printYaml(result);
86
+ return;
87
+ }
88
+ await ensureDaemonRunning();
89
+ const result = await sendDaemonRequest('graph.stats');
90
+ printYaml(result);
91
+ });
77
92
  graph
78
93
  .command('related')
79
94
  .description('Get related notes within N degrees of separation')
@@ -28,6 +28,13 @@ export type GraphRelatedResult = {
28
28
  count: number;
29
29
  depth: number;
30
30
  };
31
+ export type GraphStatsResult = {
32
+ notes_count: number;
33
+ links_count: number;
34
+ connected_notes_count: number;
35
+ isolated_notes_count: number;
36
+ };
31
37
  export declare function getBacklinks(db: AbstractPowerSyncDatabase, noteId: string): Promise<GraphBacklinksResult>;
32
38
  export declare function getOutlinks(db: AbstractPowerSyncDatabase, noteId: string): Promise<GraphOutlinksResult>;
33
39
  export declare function getRelatedNotes(db: AbstractPowerSyncDatabase, noteId: string, depth?: number): Promise<GraphRelatedResult>;
40
+ export declare function getGraphStats(db: AbstractPowerSyncDatabase): Promise<GraphStatsResult>;
@@ -74,3 +74,53 @@ export async function getRelatedNotes(db, noteId, depth = 2) {
74
74
  depth,
75
75
  };
76
76
  }
77
+ export async function getGraphStats(db) {
78
+ const [notesCountRow, linksCountRow, connectedCountRow] = await Promise.all([
79
+ db.getOptional(`
80
+ SELECT COUNT(1) AS count
81
+ FROM c_note n
82
+ WHERE n.is_del IS NULL OR n.is_del = 0
83
+ `, []),
84
+ db.getOptional(`
85
+ SELECT COUNT(1) AS count
86
+ FROM c_note_link l
87
+ JOIN c_note src ON src.id = l.src_note_id
88
+ JOIN c_note dst ON dst.id = l.dst_note_id
89
+ WHERE l.is_del = 0
90
+ AND (src.is_del IS NULL OR src.is_del = 0)
91
+ AND (dst.is_del IS NULL OR dst.is_del = 0)
92
+ `, []),
93
+ db.getOptional(`
94
+ SELECT COUNT(1) AS count
95
+ FROM (
96
+ SELECT DISTINCT note_id
97
+ FROM (
98
+ SELECT l.src_note_id AS note_id
99
+ FROM c_note_link l
100
+ JOIN c_note src ON src.id = l.src_note_id
101
+ JOIN c_note dst ON dst.id = l.dst_note_id
102
+ WHERE l.is_del = 0
103
+ AND (src.is_del IS NULL OR src.is_del = 0)
104
+ AND (dst.is_del IS NULL OR dst.is_del = 0)
105
+ UNION
106
+ SELECT l.dst_note_id AS note_id
107
+ FROM c_note_link l
108
+ JOIN c_note src ON src.id = l.src_note_id
109
+ JOIN c_note dst ON dst.id = l.dst_note_id
110
+ WHERE l.is_del = 0
111
+ AND (src.is_del IS NULL OR src.is_del = 0)
112
+ AND (dst.is_del IS NULL OR dst.is_del = 0)
113
+ ) all_linked
114
+ ) linked_notes
115
+ `, []),
116
+ ]);
117
+ const notesCount = Number(notesCountRow?.count ?? 0);
118
+ const linksCount = Number(linksCountRow?.count ?? 0);
119
+ const connectedNotesCount = Number(connectedCountRow?.count ?? 0);
120
+ return {
121
+ notes_count: notesCount,
122
+ links_count: linksCount,
123
+ connected_notes_count: connectedNotesCount,
124
+ isolated_notes_count: Math.max(notesCount - connectedNotesCount, 0),
125
+ };
126
+ }
@@ -1,10 +1,11 @@
1
1
  import { startMCPServer } from './server.js';
2
2
  import { mcpTools } from './tools.js';
3
3
  import { isLoopbackHost, resolveMCPAuthToken } from './auth.js';
4
+ import { DEFAULT_MCP_PORT } from './types.js';
4
5
  import { DinoxError } from '../../utils/errors.js';
5
6
  function parsePort(value) {
6
7
  if (value === undefined || value === null || value === '') {
7
- return 3000;
8
+ return DEFAULT_MCP_PORT;
8
9
  }
9
10
  if (typeof value !== 'string') {
10
11
  throw new DinoxError('--port must be a valid port number');
@@ -38,7 +39,7 @@ export function registerMCPCommands(program) {
38
39
  .command('serve')
39
40
  .description('Start MCP server for AI agent integration')
40
41
  .option('--host <string>', 'bind address (default: 127.0.0.1)')
41
- .option('--port <number>', 'server port (default: 3000)')
42
+ .option('--port <number>', `server port (default: ${DEFAULT_MCP_PORT})`)
42
43
  .option('--token <string>', 'auth token (or DINOX_MCP_TOKEN env var)')
43
44
  .action(async (options) => {
44
45
  const port = parsePort(options.port);
@@ -1,13 +1,14 @@
1
1
  import type { IncomingMessage, Server } from 'node:http';
2
+ type JsonRpcId = string | number | null;
2
3
  type MCPRequest = {
3
4
  jsonrpc: '2.0';
4
- id: string | number;
5
+ id?: JsonRpcId;
5
6
  method: string;
6
7
  params?: any;
7
8
  };
8
9
  type MCPResponse = {
9
10
  jsonrpc: '2.0';
10
- id: string | number;
11
+ id: JsonRpcId;
11
12
  result?: any;
12
13
  error?: {
13
14
  code: number;
@@ -17,7 +18,7 @@ type MCPResponse = {
17
18
  export declare class MCPServer {
18
19
  private db;
19
20
  initialize(): Promise<void>;
20
- handleRequest(req: MCPRequest): Promise<MCPResponse>;
21
+ handleRequest(req: MCPRequest): Promise<MCPResponse | null>;
21
22
  private handleInitialize;
22
23
  private handleToolsList;
23
24
  private handleToolsCall;
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
+ import { DEFAULT_MCP_PORT } from './types.js';
2
3
  import { mcpTools } from './tools.js';
3
4
  import { isAuthorizedMCPRequest } from './auth.js';
4
5
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
@@ -6,8 +7,9 @@ import { loadConfig } from '../../config/store.js';
6
7
  import { resolveConfig } from '../../config/resolve.js';
7
8
  import { connectPowerSync } from '../../powersync/runtime.js';
8
9
  import { parseCreatedAtRange } from '../notes/searchTime.js';
10
+ import { parseNoteSearchFields, projectNoteSearchFields } from '../notes/searchOutput.js';
9
11
  import { createNote, getNoteDetails, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, searchNotes, validateTagsForCreate, } from '../notes/repo.js';
10
- import { getBacklinks, getOutlinks, getRelatedNotes } from '../graph/repo.js';
12
+ import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from '../graph/repo.js';
11
13
  import { searchTodoTasks } from '../todo/repo.js';
12
14
  import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
13
15
  import { generateId } from '../../utils/id.js';
@@ -33,6 +35,24 @@ function normalizeLimit(value) {
33
35
  }
34
36
  return Math.min(int, MAX_SEARCH_LIMIT);
35
37
  }
38
+ function normalizeOffset(value) {
39
+ if (value === undefined || value === null) {
40
+ return 0;
41
+ }
42
+ const raw = typeof value === 'number'
43
+ ? value
44
+ : typeof value === 'string'
45
+ ? Number(value)
46
+ : NaN;
47
+ if (!Number.isFinite(raw)) {
48
+ return 0;
49
+ }
50
+ const int = Math.trunc(raw);
51
+ if (int < 0) {
52
+ return 0;
53
+ }
54
+ return int;
55
+ }
36
56
  function normalizeStringArray(value) {
37
57
  if (!Array.isArray(value)) {
38
58
  return [];
@@ -59,6 +79,40 @@ function parseBoxFilter(value) {
59
79
  .filter(Boolean);
60
80
  return { names, matchEmpty: false };
61
81
  }
82
+ function isJsonRpcId(value) {
83
+ return value === null || typeof value === 'string' || typeof value === 'number';
84
+ }
85
+ function parseMCPRequest(value) {
86
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
87
+ throw new Error('Invalid JSON-RPC request');
88
+ }
89
+ const record = value;
90
+ if (record.jsonrpc !== '2.0') {
91
+ throw new Error('Invalid JSON-RPC request: jsonrpc must be "2.0"');
92
+ }
93
+ if (typeof record.method !== 'string' || !record.method.trim()) {
94
+ throw new Error('Invalid JSON-RPC request: method is required');
95
+ }
96
+ if (record.id !== undefined && !isJsonRpcId(record.id)) {
97
+ throw new Error('Invalid JSON-RPC request: id must be a string, number, or null');
98
+ }
99
+ return {
100
+ jsonrpc: '2.0',
101
+ id: record.id,
102
+ method: record.method.trim(),
103
+ params: record.params,
104
+ };
105
+ }
106
+ function hasRequestId(req) {
107
+ return req.id !== undefined;
108
+ }
109
+ function buildResponse(req, payload) {
110
+ return {
111
+ jsonrpc: '2.0',
112
+ id: req.id ?? null,
113
+ ...payload,
114
+ };
115
+ }
62
116
  async function readJsonBody(req) {
63
117
  return new Promise((resolve, reject) => {
64
118
  const chunks = [];
@@ -91,44 +145,61 @@ export class MCPServer {
91
145
  this.db = db;
92
146
  }
93
147
  async handleRequest(req) {
148
+ if (req.method === 'notifications/initialized') {
149
+ return hasRequestId(req) ? buildResponse(req, { result: {} }) : null;
150
+ }
151
+ const shouldRespond = hasRequestId(req);
94
152
  try {
95
153
  switch (req.method) {
96
154
  case 'initialize':
155
+ if (!shouldRespond) {
156
+ return null;
157
+ }
97
158
  return this.handleInitialize(req);
98
159
  case 'tools/list':
160
+ if (!shouldRespond) {
161
+ return null;
162
+ }
99
163
  return this.handleToolsList(req);
100
164
  case 'tools/call':
165
+ if (!shouldRespond) {
166
+ await this.handleToolsCall(req);
167
+ return null;
168
+ }
101
169
  return await this.handleToolsCall(req);
102
170
  default:
103
- return {
104
- jsonrpc: '2.0',
105
- id: req.id,
171
+ if (!shouldRespond) {
172
+ return null;
173
+ }
174
+ return buildResponse(req, {
106
175
  error: { code: -32601, message: 'Method not found' },
107
- };
176
+ });
108
177
  }
109
178
  }
110
179
  catch (error) {
111
- return {
112
- jsonrpc: '2.0',
113
- id: req.id,
180
+ if (!shouldRespond) {
181
+ return null;
182
+ }
183
+ return buildResponse(req, {
114
184
  error: { code: -32603, message: error.message || 'Internal error' },
115
- };
185
+ });
116
186
  }
117
187
  }
118
188
  handleInitialize(req) {
119
- const serverInfo = {
120
- name: 'dinox-mcp-server',
121
- version: getPackageVersion(),
189
+ const result = {
122
190
  protocolVersion: '2024-11-05',
123
191
  capabilities: {
124
- tools: true,
125
- resources: false,
192
+ tools: {},
193
+ },
194
+ serverInfo: {
195
+ name: 'dinox-mcp-server',
196
+ version: getPackageVersion(),
126
197
  },
127
198
  };
128
- return { jsonrpc: '2.0', id: req.id, result: serverInfo };
199
+ return buildResponse(req, { result });
129
200
  }
130
201
  handleToolsList(req) {
131
- return { jsonrpc: '2.0', id: req.id, result: { tools: mcpTools } };
202
+ return buildResponse(req, { result: { tools: mcpTools } });
132
203
  }
133
204
  async handleToolsCall(req) {
134
205
  if (!this.db) {
@@ -152,6 +223,8 @@ export class MCPServer {
152
223
  days: daysInput,
153
224
  });
154
225
  const limit = normalizeLimit(args.limit);
226
+ const offset = normalizeOffset(args.offset);
227
+ const fields = parseNoteSearchFields(args.fields, 'dinox_search_notes.fields');
155
228
  const rows = await searchNotes(this.db, keyword, {
156
229
  includeDeleted: false,
157
230
  tagsExpression: tagsExpression || undefined,
@@ -160,13 +233,21 @@ export class MCPServer {
160
233
  zettelBoxIds,
161
234
  matchEmptyZettelBoxes: matchEmptyBoxes,
162
235
  });
163
- const result = rows.slice(0, limit);
164
- const text = JSON.stringify(result);
165
- return {
166
- jsonrpc: '2.0',
167
- id: req.id,
168
- result: { content: [{ type: 'text', text }] },
169
- };
236
+ const totalCount = rows.length;
237
+ const paged = rows.slice(offset, offset + limit);
238
+ const result = projectNoteSearchFields(paged, fields);
239
+ const text = JSON.stringify({
240
+ meta: {
241
+ total_count: totalCount,
242
+ returned_count: result.length,
243
+ limit,
244
+ offset,
245
+ has_more: offset + result.length < totalCount,
246
+ fields: fields ?? null,
247
+ },
248
+ notes: result,
249
+ });
250
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
170
251
  }
171
252
  case 'dinox_get_note': {
172
253
  if (typeof args.id !== 'string' || !args.id.trim()) {
@@ -174,11 +255,7 @@ export class MCPServer {
174
255
  }
175
256
  const notes = await getNoteDetails(this.db, [args.id.trim()]);
176
257
  const text = JSON.stringify(notes[0] ?? null);
177
- return {
178
- jsonrpc: '2.0',
179
- id: req.id,
180
- result: { content: [{ type: 'text', text }] },
181
- };
258
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
182
259
  }
183
260
  case 'dinox_create_note': {
184
261
  const title = typeof args.title === 'string' ? args.title.trim() : '';
@@ -208,11 +285,7 @@ export class MCPServer {
208
285
  userId: identity.userId,
209
286
  });
210
287
  const text = JSON.stringify({ id });
211
- return {
212
- jsonrpc: '2.0',
213
- id: req.id,
214
- result: { content: [{ type: 'text', text }] },
215
- };
288
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
216
289
  }
217
290
  case 'dinox_graph_backlinks': {
218
291
  if (typeof args.id !== 'string' || !args.id.trim()) {
@@ -220,11 +293,7 @@ export class MCPServer {
220
293
  }
221
294
  const result = await getBacklinks(this.db, args.id.trim());
222
295
  const text = JSON.stringify(result);
223
- return {
224
- jsonrpc: '2.0',
225
- id: req.id,
226
- result: { content: [{ type: 'text', text }] },
227
- };
296
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
228
297
  }
229
298
  case 'dinox_graph_outlinks': {
230
299
  if (typeof args.id !== 'string' || !args.id.trim()) {
@@ -232,11 +301,7 @@ export class MCPServer {
232
301
  }
233
302
  const result = await getOutlinks(this.db, args.id.trim());
234
303
  const text = JSON.stringify(result);
235
- return {
236
- jsonrpc: '2.0',
237
- id: req.id,
238
- result: { content: [{ type: 'text', text }] },
239
- };
304
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
240
305
  }
241
306
  case 'dinox_graph_related': {
242
307
  if (typeof args.id !== 'string' || !args.id.trim()) {
@@ -248,11 +313,12 @@ export class MCPServer {
248
313
  const depth = Math.min(Math.max(depthRaw, 1), 5);
249
314
  const result = await getRelatedNotes(this.db, args.id.trim(), depth);
250
315
  const text = JSON.stringify(result);
251
- return {
252
- jsonrpc: '2.0',
253
- id: req.id,
254
- result: { content: [{ type: 'text', text }] },
255
- };
316
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
317
+ }
318
+ case 'dinox_graph_stats': {
319
+ const result = await getGraphStats(this.db);
320
+ const text = JSON.stringify(result);
321
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
256
322
  }
257
323
  case 'dinox_search_todos': {
258
324
  const query = typeof args.query === 'string' ? args.query : '';
@@ -268,11 +334,7 @@ export class MCPServer {
268
334
  scanLimit: 200,
269
335
  });
270
336
  const text = JSON.stringify(result);
271
- return {
272
- jsonrpc: '2.0',
273
- id: req.id,
274
- result: { content: [{ type: 'text', text }] },
275
- };
337
+ return buildResponse(req, { result: { content: [{ type: 'text', text }] } });
276
338
  }
277
339
  default:
278
340
  throw new Error(`Unknown tool: ${name}`);
@@ -284,7 +346,7 @@ export class MCPServer {
284
346
  }
285
347
  }
286
348
  }
287
- export async function startMCPServer(port = 3000, host = '127.0.0.1', authToken) {
349
+ export async function startMCPServer(port = DEFAULT_MCP_PORT, host = '127.0.0.1', authToken) {
288
350
  const mcpServer = new MCPServer();
289
351
  await mcpServer.initialize();
290
352
  const server = createServer(async (req, res) => {
@@ -298,8 +360,14 @@ export async function startMCPServer(port = 3000, host = '127.0.0.1', authToken)
298
360
  return;
299
361
  }
300
362
  try {
301
- const mcpReq = await readJsonBody(req);
363
+ const body = await readJsonBody(req);
364
+ const mcpReq = parseMCPRequest(body);
302
365
  const mcpRes = await mcpServer.handleRequest(mcpReq);
366
+ if (!mcpRes) {
367
+ res.writeHead(204);
368
+ res.end();
369
+ return;
370
+ }
303
371
  res.writeHead(200, { 'Content-Type': 'application/json' });
304
372
  res.end(JSON.stringify(mcpRes));
305
373
  }
@@ -12,6 +12,12 @@ export const mcpTools = [
12
12
  days: { type: 'number', description: 'Recent N days' },
13
13
  box: { type: 'string', description: 'Zettel box name' },
14
14
  limit: { type: 'number', description: 'Max results (default: 50)' },
15
+ offset: { type: 'number', description: 'Result offset (default: 0)' },
16
+ fields: {
17
+ type: 'array',
18
+ items: { type: 'string' },
19
+ description: 'Returned fields: id,title,summary,tags,created_at,zettel_boxes',
20
+ },
15
21
  },
16
22
  },
17
23
  },
@@ -74,6 +80,14 @@ export const mcpTools = [
74
80
  required: ['id'],
75
81
  },
76
82
  },
83
+ {
84
+ name: 'dinox_graph_stats',
85
+ description: 'Get graph statistics for notes and links',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {},
89
+ },
90
+ },
77
91
  {
78
92
  name: 'dinox_search_todos',
79
93
  description: 'Search todo tasks',
@@ -16,9 +16,13 @@ export type MCPResource = {
16
16
  export type MCPServerInfo = {
17
17
  name: string;
18
18
  version: string;
19
+ };
20
+ export type MCPInitializeResult = {
19
21
  protocolVersion: string;
20
22
  capabilities: {
21
- tools?: boolean;
22
- resources?: boolean;
23
+ tools?: Record<string, never>;
24
+ resources?: Record<string, never>;
23
25
  };
26
+ serverInfo: MCPServerInfo;
24
27
  };
28
+ export declare const DEFAULT_MCP_PORT = 45137;
@@ -1 +1 @@
1
- export {};
1
+ export const DEFAULT_MCP_PORT = 45137;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerMetaCommands(program: Command): void;