@dinoxx/dinox-cli 1.0.15 → 1.0.17

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 (42) hide show
  1. package/README.md +102 -6
  2. package/dist/cli.js +8 -0
  3. package/dist/commands/graph/index.d.ts +2 -0
  4. package/dist/commands/graph/index.js +113 -0
  5. package/dist/commands/graph/repo.d.ts +40 -0
  6. package/dist/commands/graph/repo.js +126 -0
  7. package/dist/commands/mcp/auth.d.ts +4 -0
  8. package/dist/commands/mcp/auth.js +75 -0
  9. package/dist/commands/mcp/index.d.ts +2 -0
  10. package/dist/commands/mcp/index.js +85 -0
  11. package/dist/commands/mcp/server.d.ts +31 -0
  12. package/dist/commands/mcp/server.js +392 -0
  13. package/dist/commands/mcp/tools.d.ts +2 -0
  14. package/dist/commands/mcp/tools.js +104 -0
  15. package/dist/commands/mcp/types.d.ts +28 -0
  16. package/dist/commands/mcp/types.js +1 -0
  17. package/dist/commands/meta/index.d.ts +2 -0
  18. package/dist/commands/meta/index.js +45 -0
  19. package/dist/commands/meta/repo.d.ts +27 -0
  20. package/dist/commands/meta/repo.js +71 -0
  21. package/dist/commands/notes/index.d.ts +13 -3
  22. package/dist/commands/notes/index.js +135 -6
  23. package/dist/commands/notes/repo.d.ts +26 -0
  24. package/dist/commands/notes/repo.js +99 -0
  25. package/dist/commands/notes/searchOutput.d.ts +6 -0
  26. package/dist/commands/notes/searchOutput.js +41 -0
  27. package/dist/daemon/client.d.ts +2 -0
  28. package/dist/daemon/client.js +86 -0
  29. package/dist/daemon/ensure.d.ts +1 -0
  30. package/dist/daemon/ensure.js +23 -0
  31. package/dist/daemon/index.d.ts +2 -0
  32. package/dist/daemon/index.js +151 -0
  33. package/dist/daemon/pid.d.ts +10 -0
  34. package/dist/daemon/pid.js +55 -0
  35. package/dist/daemon/server.d.ts +10 -0
  36. package/dist/daemon/server.js +140 -0
  37. package/dist/daemon/types.d.ts +19 -0
  38. package/dist/daemon/types.js +1 -0
  39. package/dist/dinox.js +6 -14
  40. package/dist/utils/errors.d.ts +22 -5
  41. package/dist/utils/errors.js +29 -0
  42. 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
- ## 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
@@ -2,7 +2,11 @@ import { Command } from 'commander';
2
2
  import { registerAuthCommands } from './commands/auth/index.js';
3
3
  import { registerBoxesCommand } from './commands/boxes/index.js';
4
4
  import { registerConfigCommands } from './commands/config/index.js';
5
+ import { registerDaemonCommands } from './daemon/index.js';
6
+ import { registerGraphCommands } from './commands/graph/index.js';
5
7
  import { registerInfoCommand } from './commands/info/index.js';
8
+ import { registerMCPCommands } from './commands/mcp/index.js';
9
+ import { registerMetaCommands } from './commands/meta/index.js';
6
10
  import { registerNoteCommands } from './commands/notes/index.js';
7
11
  import { registerPromptCommands } from './commands/prompt/index.js';
8
12
  import { registerSyncCommand } from './commands/sync.js';
@@ -25,7 +29,11 @@ export function createCli() {
25
29
  registerAuthCommands(program);
26
30
  registerBoxesCommand(program);
27
31
  registerConfigCommands(program);
32
+ registerDaemonCommands(program);
33
+ registerGraphCommands(program);
28
34
  registerInfoCommand(program);
35
+ registerMCPCommands(program);
36
+ registerMetaCommands(program);
29
37
  registerSyncCommand(program);
30
38
  registerUpdateCommand(program);
31
39
  registerPromptCommands(program);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerGraphCommands(program: Command): void;
@@ -0,0 +1,113 @@
1
+ import { printYaml } from '../../utils/output.js';
2
+ import { sendDaemonRequest } from '../../daemon/client.js';
3
+ import { ensureDaemonRunning } from '../../daemon/ensure.js';
4
+ import { DinoxError } from '../../utils/errors.js';
5
+ import { loadConfig } from '../../config/store.js';
6
+ import { resolveConfig } from '../../config/resolve.js';
7
+ import { connectPowerSync } from '../../powersync/runtime.js';
8
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
9
+ import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from './repo.js';
10
+ function parseDepth(value) {
11
+ if (value === undefined || value === null || value === '') {
12
+ return 2;
13
+ }
14
+ if (typeof value !== 'string') {
15
+ throw new DinoxError('--depth must be a positive integer');
16
+ }
17
+ const trimmed = value.trim();
18
+ if (!/^\d+$/.test(trimmed)) {
19
+ throw new DinoxError('--depth must be a positive integer');
20
+ }
21
+ const parsed = Number(trimmed);
22
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 5) {
23
+ throw new DinoxError('--depth must be between 1 and 5');
24
+ }
25
+ return Math.trunc(parsed);
26
+ }
27
+ async function withOfflineDb(command, fn) {
28
+ const globals = command.optsWithGlobals?.() ?? {};
29
+ const offline = Boolean(globals.offline);
30
+ if (!offline) {
31
+ throw new DinoxError('withOfflineDb called without --offline');
32
+ }
33
+ const config = resolveConfig(await loadConfig());
34
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
35
+ const { db } = await connectPowerSync({ config, offline: true, timeoutMs });
36
+ try {
37
+ return await fn(db);
38
+ }
39
+ finally {
40
+ await db.close().catch(() => undefined);
41
+ }
42
+ }
43
+ export function registerGraphCommands(program) {
44
+ const graph = program.command('graph').description('Query knowledge graph');
45
+ graph
46
+ .command('backlinks')
47
+ .description('Get notes that link to this note')
48
+ .argument('<id>', 'note id')
49
+ .action(async (id, _options, command) => {
50
+ const globals = command.optsWithGlobals?.() ?? {};
51
+ const offline = Boolean(globals.offline);
52
+ if (offline) {
53
+ const result = await withOfflineDb(command, (db) => getBacklinks(db, id));
54
+ printYaml(result);
55
+ return;
56
+ }
57
+ await ensureDaemonRunning();
58
+ const result = await sendDaemonRequest('graph.backlinks', { id });
59
+ printYaml(result);
60
+ });
61
+ graph
62
+ .command('outlinks')
63
+ .description('Get notes that this note links to')
64
+ .argument('<id>', 'note id')
65
+ .action(async (id, _options, command) => {
66
+ const globals = command.optsWithGlobals?.() ?? {};
67
+ const offline = Boolean(globals.offline);
68
+ if (offline) {
69
+ const result = await withOfflineDb(command, (db) => getOutlinks(db, id));
70
+ printYaml(result);
71
+ return;
72
+ }
73
+ await ensureDaemonRunning();
74
+ const result = await sendDaemonRequest('graph.outlinks', { id });
75
+ printYaml(result);
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
+ });
92
+ graph
93
+ .command('related')
94
+ .description('Get related notes within N degrees of separation')
95
+ .argument('<id>', 'note id')
96
+ .option('--depth <n>', 'degrees of separation (1-5, default: 2)')
97
+ .action(async (id, options, command) => {
98
+ const depth = parseDepth(options.depth);
99
+ const globals = command.optsWithGlobals?.() ?? {};
100
+ const offline = Boolean(globals.offline);
101
+ if (offline) {
102
+ const result = await withOfflineDb(command, (db) => getRelatedNotes(db, id, depth));
103
+ printYaml(result);
104
+ return;
105
+ }
106
+ await ensureDaemonRunning();
107
+ const result = await sendDaemonRequest('graph.related', { id, depth });
108
+ printYaml(result);
109
+ });
110
+ graph.action((_options, command) => {
111
+ command.outputHelp();
112
+ });
113
+ }
@@ -0,0 +1,40 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ export type NoteLinkRecord = {
3
+ id: string;
4
+ src_note_id: string;
5
+ dst_note_id: string;
6
+ relation: string | null;
7
+ created_at: string;
8
+ };
9
+ export type LinkedNote = {
10
+ note_id: string;
11
+ title: string;
12
+ relation: string | null;
13
+ link_created_at: string;
14
+ };
15
+ export type GraphBacklinksResult = {
16
+ note_id: string;
17
+ backlinks: LinkedNote[];
18
+ count: number;
19
+ };
20
+ export type GraphOutlinksResult = {
21
+ note_id: string;
22
+ outlinks: LinkedNote[];
23
+ count: number;
24
+ };
25
+ export type GraphRelatedResult = {
26
+ note_id: string;
27
+ related: LinkedNote[];
28
+ count: number;
29
+ depth: number;
30
+ };
31
+ export type GraphStatsResult = {
32
+ notes_count: number;
33
+ links_count: number;
34
+ connected_notes_count: number;
35
+ isolated_notes_count: number;
36
+ };
37
+ export declare function getBacklinks(db: AbstractPowerSyncDatabase, noteId: string): Promise<GraphBacklinksResult>;
38
+ export declare function getOutlinks(db: AbstractPowerSyncDatabase, noteId: string): Promise<GraphOutlinksResult>;
39
+ export declare function getRelatedNotes(db: AbstractPowerSyncDatabase, noteId: string, depth?: number): Promise<GraphRelatedResult>;
40
+ export declare function getGraphStats(db: AbstractPowerSyncDatabase): Promise<GraphStatsResult>;
@@ -0,0 +1,126 @@
1
+ export async function getBacklinks(db, noteId) {
2
+ const rows = await db.getAll(`SELECT
3
+ l.id,
4
+ l.src_note_id,
5
+ l.dst_note_id,
6
+ l.relation,
7
+ l.created_at,
8
+ n.title
9
+ FROM c_note_link l
10
+ LEFT JOIN c_note n ON l.src_note_id = n.id
11
+ WHERE l.dst_note_id = ? AND l.is_del = 0 AND (n.is_del = 0 OR n.is_del IS NULL)
12
+ ORDER BY l.created_at DESC`, [noteId]);
13
+ const backlinks = rows.map((row) => ({
14
+ note_id: row.src_note_id,
15
+ title: row.title || '',
16
+ relation: row.relation,
17
+ link_created_at: row.created_at,
18
+ }));
19
+ return {
20
+ note_id: noteId,
21
+ backlinks,
22
+ count: backlinks.length,
23
+ };
24
+ }
25
+ export async function getOutlinks(db, noteId) {
26
+ const rows = await db.getAll(`SELECT
27
+ l.id,
28
+ l.src_note_id,
29
+ l.dst_note_id,
30
+ l.relation,
31
+ l.created_at,
32
+ n.title
33
+ FROM c_note_link l
34
+ LEFT JOIN c_note n ON l.dst_note_id = n.id
35
+ WHERE l.src_note_id = ? AND l.is_del = 0 AND (n.is_del = 0 OR n.is_del IS NULL)
36
+ ORDER BY l.created_at DESC`, [noteId]);
37
+ const outlinks = rows.map((row) => ({
38
+ note_id: row.dst_note_id,
39
+ title: row.title || '',
40
+ relation: row.relation,
41
+ link_created_at: row.created_at,
42
+ }));
43
+ return {
44
+ note_id: noteId,
45
+ outlinks,
46
+ count: outlinks.length,
47
+ };
48
+ }
49
+ export async function getRelatedNotes(db, noteId, depth = 2) {
50
+ const visited = new Set([noteId]);
51
+ const related = new Map();
52
+ async function traverse(currentId, currentDepth) {
53
+ if (currentDepth > depth)
54
+ return;
55
+ const [backlinks, outlinks] = await Promise.all([
56
+ getBacklinks(db, currentId),
57
+ getOutlinks(db, currentId),
58
+ ]);
59
+ for (const link of [...backlinks.backlinks, ...outlinks.outlinks]) {
60
+ if (!visited.has(link.note_id)) {
61
+ visited.add(link.note_id);
62
+ related.set(link.note_id, link);
63
+ if (currentDepth < depth) {
64
+ await traverse(link.note_id, currentDepth + 1);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ await traverse(noteId, 1);
70
+ return {
71
+ note_id: noteId,
72
+ related: Array.from(related.values()),
73
+ count: related.size,
74
+ depth,
75
+ };
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
+ }
@@ -0,0 +1,4 @@
1
+ import type { IncomingHttpHeaders } from 'node:http';
2
+ export declare function isLoopbackHost(host: string): boolean;
3
+ export declare function resolveMCPAuthToken(tokenOption: unknown, envToken?: string | undefined): string | undefined;
4
+ export declare function isAuthorizedMCPRequest(headers: IncomingHttpHeaders, expectedToken?: string): boolean;
@@ -0,0 +1,75 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { DinoxError } from '../../utils/errors.js';
3
+ const AUTH_HEADER_RE = /^Bearer\s+(.+)$/i;
4
+ export function isLoopbackHost(host) {
5
+ const normalized = host.trim().toLowerCase();
6
+ return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1';
7
+ }
8
+ export function resolveMCPAuthToken(tokenOption, envToken = process.env.DINOX_MCP_TOKEN) {
9
+ if (tokenOption !== undefined && tokenOption !== null) {
10
+ if (typeof tokenOption !== 'string') {
11
+ throw new DinoxError('--token must be a non-empty string');
12
+ }
13
+ const parsed = tokenOption.trim();
14
+ if (!parsed) {
15
+ throw new DinoxError('--token must be a non-empty string');
16
+ }
17
+ return parsed;
18
+ }
19
+ const fromEnv = envToken?.trim() ?? '';
20
+ return fromEnv || undefined;
21
+ }
22
+ function readHeaderTokenValue(value) {
23
+ if (typeof value === 'string') {
24
+ const parsed = value.trim();
25
+ return parsed || undefined;
26
+ }
27
+ if (!Array.isArray(value)) {
28
+ return undefined;
29
+ }
30
+ for (const item of value) {
31
+ const parsed = item.trim();
32
+ if (parsed) {
33
+ return parsed;
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+ function readBearerToken(headers) {
39
+ const authorization = readHeaderTokenValue(headers.authorization);
40
+ if (!authorization) {
41
+ return undefined;
42
+ }
43
+ const match = authorization.match(AUTH_HEADER_RE);
44
+ if (!match) {
45
+ return undefined;
46
+ }
47
+ const token = match[1]?.trim() ?? '';
48
+ return token || undefined;
49
+ }
50
+ function readMCPToken(headers) {
51
+ const bearer = readBearerToken(headers);
52
+ if (bearer) {
53
+ return bearer;
54
+ }
55
+ const raw = readHeaderTokenValue(headers['x-dinox-token']);
56
+ return raw?.trim() || undefined;
57
+ }
58
+ function secureTokenEquals(left, right) {
59
+ const leftBuffer = Buffer.from(left);
60
+ const rightBuffer = Buffer.from(right);
61
+ if (leftBuffer.length !== rightBuffer.length) {
62
+ return false;
63
+ }
64
+ return timingSafeEqual(leftBuffer, rightBuffer);
65
+ }
66
+ export function isAuthorizedMCPRequest(headers, expectedToken) {
67
+ if (!expectedToken) {
68
+ return true;
69
+ }
70
+ const provided = readMCPToken(headers);
71
+ if (!provided) {
72
+ return false;
73
+ }
74
+ return secureTokenEquals(provided, expectedToken);
75
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerMCPCommands(program: Command): void;
@@ -0,0 +1,85 @@
1
+ import { startMCPServer } from './server.js';
2
+ import { mcpTools } from './tools.js';
3
+ import { isLoopbackHost, resolveMCPAuthToken } from './auth.js';
4
+ import { DEFAULT_MCP_PORT } from './types.js';
5
+ import { DinoxError } from '../../utils/errors.js';
6
+ function parsePort(value) {
7
+ if (value === undefined || value === null || value === '') {
8
+ return DEFAULT_MCP_PORT;
9
+ }
10
+ if (typeof value !== 'string') {
11
+ throw new DinoxError('--port must be a valid port number');
12
+ }
13
+ const trimmed = value.trim();
14
+ if (!/^\d+$/.test(trimmed)) {
15
+ throw new DinoxError('--port must be a valid port number');
16
+ }
17
+ const parsed = Number(trimmed);
18
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
19
+ throw new DinoxError('--port must be between 1 and 65535');
20
+ }
21
+ return Math.trunc(parsed);
22
+ }
23
+ function parseHost(value) {
24
+ if (value === undefined || value === null || value === '') {
25
+ return '127.0.0.1';
26
+ }
27
+ if (typeof value !== 'string') {
28
+ throw new DinoxError('--host must be a valid hostname or IP address');
29
+ }
30
+ const trimmed = value.trim();
31
+ if (!trimmed) {
32
+ throw new DinoxError('--host must be a valid hostname or IP address');
33
+ }
34
+ return trimmed;
35
+ }
36
+ export function registerMCPCommands(program) {
37
+ const mcp = program.command('mcp').description('MCP (Model Context Protocol) server');
38
+ mcp
39
+ .command('serve')
40
+ .description('Start MCP server for AI agent integration')
41
+ .option('--host <string>', 'bind address (default: 127.0.0.1)')
42
+ .option('--port <number>', `server port (default: ${DEFAULT_MCP_PORT})`)
43
+ .option('--token <string>', 'auth token (or DINOX_MCP_TOKEN env var)')
44
+ .action(async (options) => {
45
+ const port = parsePort(options.port);
46
+ const host = parseHost(options.host);
47
+ const token = resolveMCPAuthToken(options.token);
48
+ const loopback = isLoopbackHost(host);
49
+ if (!loopback && !token) {
50
+ throw new DinoxError('--token (or DINOX_MCP_TOKEN) is required when --host is non-loopback');
51
+ }
52
+ console.log('Starting Dinox MCP server...');
53
+ console.log(`Host: ${host}`);
54
+ console.log(`Port: ${port}`);
55
+ console.log(`Auth: ${token ? 'enabled' : 'disabled (loopback only)'}`);
56
+ console.log('Protocol: Model Context Protocol (MCP)');
57
+ if (!loopback) {
58
+ console.log('warning: binding to a non-loopback host may expose your local data; use firewall/auth as needed');
59
+ }
60
+ console.log('');
61
+ console.log('Available tools:');
62
+ for (const tool of mcpTools) {
63
+ console.log(` - ${tool.name}`);
64
+ }
65
+ console.log('');
66
+ const { server, mcpServer } = await startMCPServer(port, host, token);
67
+ let shuttingDown = false;
68
+ const shutdown = async () => {
69
+ if (shuttingDown)
70
+ return;
71
+ shuttingDown = true;
72
+ await new Promise((resolve) => server.close(() => resolve()));
73
+ await mcpServer.close();
74
+ };
75
+ process.on('SIGINT', () => {
76
+ void shutdown().finally(() => process.exit(0));
77
+ });
78
+ process.on('SIGTERM', () => {
79
+ void shutdown().finally(() => process.exit(0));
80
+ });
81
+ });
82
+ mcp.action((_options, command) => {
83
+ command.outputHelp();
84
+ });
85
+ }
@@ -0,0 +1,31 @@
1
+ import type { IncomingMessage, Server } from 'node:http';
2
+ type JsonRpcId = string | number | null;
3
+ type MCPRequest = {
4
+ jsonrpc: '2.0';
5
+ id?: JsonRpcId;
6
+ method: string;
7
+ params?: any;
8
+ };
9
+ type MCPResponse = {
10
+ jsonrpc: '2.0';
11
+ id: JsonRpcId;
12
+ result?: any;
13
+ error?: {
14
+ code: number;
15
+ message: string;
16
+ };
17
+ };
18
+ export declare class MCPServer {
19
+ private db;
20
+ initialize(): Promise<void>;
21
+ handleRequest(req: MCPRequest): Promise<MCPResponse | null>;
22
+ private handleInitialize;
23
+ private handleToolsList;
24
+ private handleToolsCall;
25
+ close(): Promise<void>;
26
+ }
27
+ export declare function startMCPServer(port?: number, host?: string, authToken?: string): Promise<{
28
+ server: Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
29
+ mcpServer: MCPServer;
30
+ }>;
31
+ export {};