@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 +102 -6
- package/dist/cli.js +2 -0
- package/dist/commands/graph/index.js +16 -1
- package/dist/commands/graph/repo.d.ts +7 -0
- package/dist/commands/graph/repo.js +50 -0
- package/dist/commands/mcp/index.js +3 -2
- package/dist/commands/mcp/server.d.ts +4 -3
- package/dist/commands/mcp/server.js +123 -55
- package/dist/commands/mcp/tools.js +14 -0
- package/dist/commands/mcp/types.d.ts +6 -2
- package/dist/commands/mcp/types.js +1 -1
- package/dist/commands/meta/index.d.ts +2 -0
- package/dist/commands/meta/index.js +45 -0
- package/dist/commands/meta/repo.d.ts +27 -0
- package/dist/commands/meta/repo.js +71 -0
- package/dist/commands/notes/index.d.ts +13 -3
- package/dist/commands/notes/index.js +135 -6
- package/dist/commands/notes/repo.d.ts +26 -0
- package/dist/commands/notes/repo.js +99 -0
- package/dist/commands/notes/searchOutput.d.ts +6 -0
- package/dist/commands/notes/searchOutput.js +41 -0
- package/dist/daemon/server.js +4 -1
- package/dist/dinox.js +6 -14
- package/dist/utils/errors.d.ts +22 -5
- package/dist/utils/errors.js +29 -0
- package/package.json +1 -1
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
|
-
##
|
|
774
|
+
## 11. GitHub Action 自动发布 npm
|
|
679
775
|
|
|
680
776
|
仓库已内置工作流:`/Users/shanks/Documents/GitHub/dinox-cli/.github/workflows/publish-npm.yml`
|
|
681
777
|
|
|
682
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
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>',
|
|
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
|
|
5
|
+
id?: JsonRpcId;
|
|
5
6
|
method: string;
|
|
6
7
|
params?: any;
|
|
7
8
|
};
|
|
8
9
|
type MCPResponse = {
|
|
9
10
|
jsonrpc: '2.0';
|
|
10
|
-
id:
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
120
|
-
name: 'dinox-mcp-server',
|
|
121
|
-
version: getPackageVersion(),
|
|
189
|
+
const result = {
|
|
122
190
|
protocolVersion: '2024-11-05',
|
|
123
191
|
capabilities: {
|
|
124
|
-
tools:
|
|
125
|
-
|
|
192
|
+
tools: {},
|
|
193
|
+
},
|
|
194
|
+
serverInfo: {
|
|
195
|
+
name: 'dinox-mcp-server',
|
|
196
|
+
version: getPackageVersion(),
|
|
126
197
|
},
|
|
127
198
|
};
|
|
128
|
-
return
|
|
199
|
+
return buildResponse(req, { result });
|
|
129
200
|
}
|
|
130
201
|
handleToolsList(req) {
|
|
131
|
-
return
|
|
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
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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 =
|
|
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
|
|
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?:
|
|
22
|
-
resources?:
|
|
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;
|