@dinoxx/dinox-cli 1.0.14 → 1.0.16

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 (52) hide show
  1. package/dist/auth/userInfo.d.ts +1 -0
  2. package/dist/auth/userInfo.js +29 -10
  3. package/dist/cli.js +6 -0
  4. package/dist/commands/auth/index.js +16 -19
  5. package/dist/commands/boxes/index.js +1 -10
  6. package/dist/commands/graph/index.d.ts +2 -0
  7. package/dist/commands/graph/index.js +98 -0
  8. package/dist/commands/graph/repo.d.ts +33 -0
  9. package/dist/commands/graph/repo.js +76 -0
  10. package/dist/commands/mcp/auth.d.ts +4 -0
  11. package/dist/commands/mcp/auth.js +75 -0
  12. package/dist/commands/mcp/index.d.ts +2 -0
  13. package/dist/commands/mcp/index.js +84 -0
  14. package/dist/commands/mcp/server.d.ts +30 -0
  15. package/dist/commands/mcp/server.js +324 -0
  16. package/dist/commands/mcp/tools.d.ts +2 -0
  17. package/dist/commands/mcp/tools.js +90 -0
  18. package/dist/commands/mcp/types.d.ts +24 -0
  19. package/dist/commands/mcp/types.js +1 -0
  20. package/dist/commands/notes/index.d.ts +39 -0
  21. package/dist/commands/notes/index.js +61 -12
  22. package/dist/commands/prompt/index.js +1 -10
  23. package/dist/commands/sync.js +1 -11
  24. package/dist/commands/tags/index.js +1 -10
  25. package/dist/commands/todo/index.js +1 -10
  26. package/dist/config/resolve.js +3 -2
  27. package/dist/config/store.js +17 -2
  28. package/dist/config/types.d.ts +1 -2
  29. package/dist/daemon/client.d.ts +2 -0
  30. package/dist/daemon/client.js +86 -0
  31. package/dist/daemon/ensure.d.ts +1 -0
  32. package/dist/daemon/ensure.js +23 -0
  33. package/dist/daemon/index.d.ts +2 -0
  34. package/dist/daemon/index.js +151 -0
  35. package/dist/daemon/pid.d.ts +10 -0
  36. package/dist/daemon/pid.js +55 -0
  37. package/dist/daemon/server.d.ts +10 -0
  38. package/dist/daemon/server.js +137 -0
  39. package/dist/daemon/types.d.ts +19 -0
  40. package/dist/daemon/types.js +1 -0
  41. package/dist/powersync/connector.d.ts +2 -1
  42. package/dist/powersync/connector.js +27 -9
  43. package/dist/powersync/runtime.js +4 -9
  44. package/dist/powersync/uploader.d.ts +1 -0
  45. package/dist/powersync/uploader.js +36 -13
  46. package/dist/utils/abort.d.ts +6 -0
  47. package/dist/utils/abort.js +23 -0
  48. package/dist/utils/authToken.d.ts +2 -0
  49. package/dist/utils/authToken.js +23 -0
  50. package/dist/utils/syncTimeout.d.ts +1 -0
  51. package/dist/utils/syncTimeout.js +19 -0
  52. package/package.json +1 -1
@@ -8,6 +8,7 @@ export type AuthIdentity = {
8
8
  export declare function fetchUserInfoWithAuthorization(input: {
9
9
  authorization: string;
10
10
  config: ResolvedDinoxConfig;
11
+ timeoutMs?: number;
11
12
  signal?: AbortSignal;
12
13
  }): Promise<AuthIdentity>;
13
14
  export declare function refreshAndPersistAuthIdentity(rawConfig: DinoxConfig, resolvedConfig: ResolvedDinoxConfig): Promise<AuthIdentity>;
@@ -1,5 +1,7 @@
1
1
  import { saveConfig } from '../config/store.js';
2
2
  import { DinoxError } from '../utils/errors.js';
3
+ import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
4
+ import { toAuthorizationHeaderValue } from '../utils/authToken.js';
3
5
  const AUTH_CODE_OK = '000000';
4
6
  function normalizeOptionalString(value) {
5
7
  if (typeof value !== 'string') {
@@ -59,17 +61,34 @@ function resolveUserInfoEndpoint(config) {
59
61
  export async function fetchUserInfoWithAuthorization(input) {
60
62
  const authorization = normalizeOptionalString(input.authorization);
61
63
  if (!authorization) {
62
- throw new DinoxError('Authorization is required for user info');
64
+ throw new DinoxError('Token is required for user info');
63
65
  }
64
66
  const endpoint = resolveUserInfoEndpoint(input.config);
65
- const response = await fetch(endpoint, {
66
- method: 'GET',
67
- headers: {
68
- Authorization: authorization,
69
- 'Content-Type': 'application/json',
70
- },
71
- signal: input.signal,
72
- });
67
+ const effectiveTimeoutMs = input.timeoutMs ?? input.config.sync.timeoutMs;
68
+ const timeout = typeof effectiveTimeoutMs === 'number' && Number.isFinite(effectiveTimeoutMs) && effectiveTimeoutMs > 0
69
+ ? createTimeoutSignal(effectiveTimeoutMs)
70
+ : null;
71
+ const requestSignal = mergeAbortSignals([input.signal, timeout?.signal]);
72
+ let response;
73
+ try {
74
+ response = await fetch(endpoint, {
75
+ method: 'GET',
76
+ headers: {
77
+ Authorization: toAuthorizationHeaderValue(authorization),
78
+ 'Content-Type': 'application/json',
79
+ },
80
+ signal: requestSignal,
81
+ });
82
+ }
83
+ catch (error) {
84
+ if (timeout?.signal.aborted && !input.signal?.aborted) {
85
+ throw new DinoxError(`User info request timed out after ${effectiveTimeoutMs}ms`);
86
+ }
87
+ throw error;
88
+ }
89
+ finally {
90
+ timeout?.cancel();
91
+ }
73
92
  if (!response.ok) {
74
93
  const body = await response.text().catch(() => '');
75
94
  throw new DinoxError(`User info request failed (${response.status}): ${body || endpoint}`);
@@ -80,7 +99,7 @@ export async function fetchUserInfoWithAuthorization(input) {
80
99
  export async function refreshAndPersistAuthIdentity(rawConfig, resolvedConfig) {
81
100
  const authorization = normalizeOptionalString(resolvedConfig.auth.authorization);
82
101
  if (!authorization) {
83
- throw new DinoxError('Authorization is required to fetch user info');
102
+ throw new DinoxError('Token is required to fetch user info');
84
103
  }
85
104
  const identity = await fetchUserInfoWithAuthorization({
86
105
  authorization,
package/dist/cli.js CHANGED
@@ -2,7 +2,10 @@ 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';
6
9
  import { registerNoteCommands } from './commands/notes/index.js';
7
10
  import { registerPromptCommands } from './commands/prompt/index.js';
8
11
  import { registerSyncCommand } from './commands/sync.js';
@@ -25,7 +28,10 @@ export function createCli() {
25
28
  registerAuthCommands(program);
26
29
  registerBoxesCommand(program);
27
30
  registerConfigCommands(program);
31
+ registerDaemonCommands(program);
32
+ registerGraphCommands(program);
28
33
  registerInfoCommand(program);
34
+ registerMCPCommands(program);
29
35
  registerSyncCommand(program);
30
36
  registerUpdateCommand(program);
31
37
  registerPromptCommands(program);
@@ -5,20 +5,12 @@ import { getPowerSyncDbPath } from '../../config/paths.js';
5
5
  import { redactAuthorization } from '../../utils/redact.js';
6
6
  import { DinoxError } from '../../utils/errors.js';
7
7
  import { printYaml } from '../../utils/output.js';
8
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
9
+ import { normalizeAuthToken } from '../../utils/authToken.js';
8
10
  import { refreshAndPersistAuthIdentity } from '../../auth/userInfo.js';
9
11
  import { connectPowerSync, getStatusSnapshot } from '../../powersync/runtime.js';
10
12
  import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
11
13
  import { runSingleSyncPass } from '../../powersync/syncPass.js';
12
- function parseSyncTimeoutMs(value) {
13
- if (typeof value !== 'string') {
14
- return undefined;
15
- }
16
- const parsed = Number(value);
17
- if (!Number.isFinite(parsed) || parsed <= 0) {
18
- throw new DinoxError('--sync-timeout must be a positive integer');
19
- }
20
- return Math.trunc(parsed);
21
- }
22
14
  async function removeSqliteArtifacts(dbPath) {
23
15
  const artifacts = ['', '-wal', '-shm'];
24
16
  for (const suffix of artifacts) {
@@ -31,15 +23,15 @@ export function registerAuthCommands(program) {
31
23
  .description('Authentication and PowerSync connectivity commands');
32
24
  auth
33
25
  .command('login')
34
- .description('Save authorization token and verify PowerSync connectivity')
35
- .argument('<authorization>', 'full Authorization header value (e.g. "Bearer <token>")')
36
- .action(async (authorization, _options, command) => {
37
- const auth = authorization?.trim();
38
- if (!auth) {
39
- throw new DinoxError('Authorization token is required');
26
+ .description('Save login token and verify PowerSync connectivity')
27
+ .argument('<token>', 'login token (pass the raw token string, without Bearer prefix)')
28
+ .action(async (token, _options, command) => {
29
+ const rawToken = normalizeAuthToken(token);
30
+ if (!rawToken) {
31
+ throw new DinoxError('Token is required');
40
32
  }
41
33
  const config = await loadConfig();
42
- config.auth = { ...(config.auth ?? {}), authorization: auth };
34
+ config.auth = { ...(config.auth ?? {}), authorization: rawToken };
43
35
  await saveConfig(config);
44
36
  const resolved = resolveConfig(config);
45
37
  const globals = command.optsWithGlobals?.() ?? {};
@@ -47,9 +39,10 @@ export function registerAuthCommands(program) {
47
39
  const jsonOutput = Boolean(globals.json);
48
40
  // 1) Token exchange sanity check.
49
41
  await fetchCredentialsWithAuthorization({
50
- authorization: auth,
42
+ authorization: rawToken,
51
43
  endpoint: resolved.powersync.endpoint,
52
44
  credentialsUrl: resolved.powersync.tokenEndpoint,
45
+ timeoutMs,
53
46
  });
54
47
  // 1.5) Fetch and persist user identity.
55
48
  const identity = await refreshAndPersistAuthIdentity(config, resolved);
@@ -93,12 +86,16 @@ export function registerAuthCommands(program) {
93
86
  });
94
87
  auth
95
88
  .command('logout')
96
- .description('Clear saved authorization token')
89
+ .description('Clear saved login token')
97
90
  .option('--clear-local-db', 'delete the local PowerSync SQLite database')
98
91
  .action(async (options) => {
99
92
  const config = await loadConfig();
100
93
  if (config.auth) {
101
94
  config.auth.authorization = undefined;
95
+ config.auth.userId = undefined;
96
+ config.auth.username = undefined;
97
+ config.auth.email = undefined;
98
+ config.auth.userInfoSyncedAt = undefined;
102
99
  }
103
100
  await saveConfig(config);
104
101
  if (options.clearLocalDb) {
@@ -2,20 +2,11 @@ import { loadConfig } from '../../config/store.js';
2
2
  import { resolveConfig } from '../../config/resolve.js';
3
3
  import { DinoxError } from '../../utils/errors.js';
4
4
  import { printYaml } from '../../utils/output.js';
5
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
5
6
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
6
7
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
7
8
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
8
9
  import { addBox, listBoxes } from './repo.js';
9
- function parseSyncTimeoutMs(value) {
10
- if (typeof value !== 'string') {
11
- return undefined;
12
- }
13
- const parsed = Number(value);
14
- if (!Number.isFinite(parsed) || parsed <= 0) {
15
- throw new DinoxError('--sync-timeout must be a positive integer');
16
- }
17
- return Math.trunc(parsed);
18
- }
19
10
  async function syncBeforeBoxCommand(db, offline, timeoutMs) {
20
11
  if (offline) {
21
12
  return;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerGraphCommands(program: Command): void;
@@ -0,0 +1,98 @@
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, 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('related')
79
+ .description('Get related notes within N degrees of separation')
80
+ .argument('<id>', 'note id')
81
+ .option('--depth <n>', 'degrees of separation (1-5, default: 2)')
82
+ .action(async (id, options, command) => {
83
+ const depth = parseDepth(options.depth);
84
+ const globals = command.optsWithGlobals?.() ?? {};
85
+ const offline = Boolean(globals.offline);
86
+ if (offline) {
87
+ const result = await withOfflineDb(command, (db) => getRelatedNotes(db, id, depth));
88
+ printYaml(result);
89
+ return;
90
+ }
91
+ await ensureDaemonRunning();
92
+ const result = await sendDaemonRequest('graph.related', { id, depth });
93
+ printYaml(result);
94
+ });
95
+ graph.action((_options, command) => {
96
+ command.outputHelp();
97
+ });
98
+ }
@@ -0,0 +1,33 @@
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 declare function getBacklinks(db: AbstractPowerSyncDatabase, noteId: string): Promise<GraphBacklinksResult>;
32
+ export declare function getOutlinks(db: AbstractPowerSyncDatabase, noteId: string): Promise<GraphOutlinksResult>;
33
+ export declare function getRelatedNotes(db: AbstractPowerSyncDatabase, noteId: string, depth?: number): Promise<GraphRelatedResult>;
@@ -0,0 +1,76 @@
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
+ }
@@ -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,84 @@
1
+ import { startMCPServer } from './server.js';
2
+ import { mcpTools } from './tools.js';
3
+ import { isLoopbackHost, resolveMCPAuthToken } from './auth.js';
4
+ import { DinoxError } from '../../utils/errors.js';
5
+ function parsePort(value) {
6
+ if (value === undefined || value === null || value === '') {
7
+ return 3000;
8
+ }
9
+ if (typeof value !== 'string') {
10
+ throw new DinoxError('--port must be a valid port number');
11
+ }
12
+ const trimmed = value.trim();
13
+ if (!/^\d+$/.test(trimmed)) {
14
+ throw new DinoxError('--port must be a valid port number');
15
+ }
16
+ const parsed = Number(trimmed);
17
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
18
+ throw new DinoxError('--port must be between 1 and 65535');
19
+ }
20
+ return Math.trunc(parsed);
21
+ }
22
+ function parseHost(value) {
23
+ if (value === undefined || value === null || value === '') {
24
+ return '127.0.0.1';
25
+ }
26
+ if (typeof value !== 'string') {
27
+ throw new DinoxError('--host must be a valid hostname or IP address');
28
+ }
29
+ const trimmed = value.trim();
30
+ if (!trimmed) {
31
+ throw new DinoxError('--host must be a valid hostname or IP address');
32
+ }
33
+ return trimmed;
34
+ }
35
+ export function registerMCPCommands(program) {
36
+ const mcp = program.command('mcp').description('MCP (Model Context Protocol) server');
37
+ mcp
38
+ .command('serve')
39
+ .description('Start MCP server for AI agent integration')
40
+ .option('--host <string>', 'bind address (default: 127.0.0.1)')
41
+ .option('--port <number>', 'server port (default: 3000)')
42
+ .option('--token <string>', 'auth token (or DINOX_MCP_TOKEN env var)')
43
+ .action(async (options) => {
44
+ const port = parsePort(options.port);
45
+ const host = parseHost(options.host);
46
+ const token = resolveMCPAuthToken(options.token);
47
+ const loopback = isLoopbackHost(host);
48
+ if (!loopback && !token) {
49
+ throw new DinoxError('--token (or DINOX_MCP_TOKEN) is required when --host is non-loopback');
50
+ }
51
+ console.log('Starting Dinox MCP server...');
52
+ console.log(`Host: ${host}`);
53
+ console.log(`Port: ${port}`);
54
+ console.log(`Auth: ${token ? 'enabled' : 'disabled (loopback only)'}`);
55
+ console.log('Protocol: Model Context Protocol (MCP)');
56
+ if (!loopback) {
57
+ console.log('warning: binding to a non-loopback host may expose your local data; use firewall/auth as needed');
58
+ }
59
+ console.log('');
60
+ console.log('Available tools:');
61
+ for (const tool of mcpTools) {
62
+ console.log(` - ${tool.name}`);
63
+ }
64
+ console.log('');
65
+ const { server, mcpServer } = await startMCPServer(port, host, token);
66
+ let shuttingDown = false;
67
+ const shutdown = async () => {
68
+ if (shuttingDown)
69
+ return;
70
+ shuttingDown = true;
71
+ await new Promise((resolve) => server.close(() => resolve()));
72
+ await mcpServer.close();
73
+ };
74
+ process.on('SIGINT', () => {
75
+ void shutdown().finally(() => process.exit(0));
76
+ });
77
+ process.on('SIGTERM', () => {
78
+ void shutdown().finally(() => process.exit(0));
79
+ });
80
+ });
81
+ mcp.action((_options, command) => {
82
+ command.outputHelp();
83
+ });
84
+ }
@@ -0,0 +1,30 @@
1
+ import type { IncomingMessage, Server } from 'node:http';
2
+ type MCPRequest = {
3
+ jsonrpc: '2.0';
4
+ id: string | number;
5
+ method: string;
6
+ params?: any;
7
+ };
8
+ type MCPResponse = {
9
+ jsonrpc: '2.0';
10
+ id: string | number;
11
+ result?: any;
12
+ error?: {
13
+ code: number;
14
+ message: string;
15
+ };
16
+ };
17
+ export declare class MCPServer {
18
+ private db;
19
+ initialize(): Promise<void>;
20
+ handleRequest(req: MCPRequest): Promise<MCPResponse>;
21
+ private handleInitialize;
22
+ private handleToolsList;
23
+ private handleToolsCall;
24
+ close(): Promise<void>;
25
+ }
26
+ export declare function startMCPServer(port?: number, host?: string, authToken?: string): Promise<{
27
+ server: Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
28
+ mcpServer: MCPServer;
29
+ }>;
30
+ export {};