@dinoxx/dinox-cli 1.0.15 → 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.
- package/dist/cli.js +6 -0
- package/dist/commands/graph/index.d.ts +2 -0
- package/dist/commands/graph/index.js +98 -0
- package/dist/commands/graph/repo.d.ts +33 -0
- package/dist/commands/graph/repo.js +76 -0
- package/dist/commands/mcp/auth.d.ts +4 -0
- package/dist/commands/mcp/auth.js +75 -0
- package/dist/commands/mcp/index.d.ts +2 -0
- package/dist/commands/mcp/index.js +84 -0
- package/dist/commands/mcp/server.d.ts +30 -0
- package/dist/commands/mcp/server.js +324 -0
- package/dist/commands/mcp/tools.d.ts +2 -0
- package/dist/commands/mcp/tools.js +90 -0
- package/dist/commands/mcp/types.d.ts +24 -0
- package/dist/commands/mcp/types.js +1 -0
- package/dist/daemon/client.d.ts +2 -0
- package/dist/daemon/client.js +86 -0
- package/dist/daemon/ensure.d.ts +1 -0
- package/dist/daemon/ensure.js +23 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +151 -0
- package/dist/daemon/pid.d.ts +10 -0
- package/dist/daemon/pid.js +55 -0
- package/dist/daemon/server.d.ts +10 -0
- package/dist/daemon/server.js +137 -0
- package/dist/daemon/types.d.ts +19 -0
- package/dist/daemon/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { mcpTools } from './tools.js';
|
|
3
|
+
import { isAuthorizedMCPRequest } from './auth.js';
|
|
4
|
+
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
5
|
+
import { loadConfig } from '../../config/store.js';
|
|
6
|
+
import { resolveConfig } from '../../config/resolve.js';
|
|
7
|
+
import { connectPowerSync } from '../../powersync/runtime.js';
|
|
8
|
+
import { parseCreatedAtRange } from '../notes/searchTime.js';
|
|
9
|
+
import { createNote, getNoteDetails, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, searchNotes, validateTagsForCreate, } from '../notes/repo.js';
|
|
10
|
+
import { getBacklinks, getOutlinks, getRelatedNotes } from '../graph/repo.js';
|
|
11
|
+
import { searchTodoTasks } from '../todo/repo.js';
|
|
12
|
+
import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
|
|
13
|
+
import { generateId } from '../../utils/id.js';
|
|
14
|
+
import { getPackageVersion } from '../../utils/version.js';
|
|
15
|
+
const MAX_MCP_REQUEST_BYTES = 1 * 1024 * 1024;
|
|
16
|
+
const DEFAULT_SEARCH_LIMIT = 50;
|
|
17
|
+
const MAX_SEARCH_LIMIT = 500;
|
|
18
|
+
function normalizeLimit(value) {
|
|
19
|
+
if (value === undefined || value === null) {
|
|
20
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
21
|
+
}
|
|
22
|
+
const raw = typeof value === 'number'
|
|
23
|
+
? value
|
|
24
|
+
: typeof value === 'string'
|
|
25
|
+
? Number(value)
|
|
26
|
+
: NaN;
|
|
27
|
+
if (!Number.isFinite(raw)) {
|
|
28
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
29
|
+
}
|
|
30
|
+
const int = Math.trunc(raw);
|
|
31
|
+
if (int <= 0) {
|
|
32
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
33
|
+
}
|
|
34
|
+
return Math.min(int, MAX_SEARCH_LIMIT);
|
|
35
|
+
}
|
|
36
|
+
function normalizeStringArray(value) {
|
|
37
|
+
if (!Array.isArray(value)) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
function parseBoxFilter(value) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return { names: normalizeStringArray(value), matchEmpty: false };
|
|
45
|
+
}
|
|
46
|
+
if (typeof value !== 'string') {
|
|
47
|
+
return { names: [], matchEmpty: false };
|
|
48
|
+
}
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (!trimmed) {
|
|
51
|
+
return { names: [], matchEmpty: false };
|
|
52
|
+
}
|
|
53
|
+
if (trimmed === '[]') {
|
|
54
|
+
return { names: [], matchEmpty: true };
|
|
55
|
+
}
|
|
56
|
+
const names = trimmed
|
|
57
|
+
.split(/[,\n]/)
|
|
58
|
+
.map((name) => name.trim())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
return { names, matchEmpty: false };
|
|
61
|
+
}
|
|
62
|
+
async function readJsonBody(req) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
let totalBytes = 0;
|
|
66
|
+
req.on('data', (chunk) => {
|
|
67
|
+
totalBytes += chunk.length;
|
|
68
|
+
if (totalBytes > MAX_MCP_REQUEST_BYTES) {
|
|
69
|
+
req.destroy(new Error('Request body too large'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
chunks.push(chunk);
|
|
73
|
+
});
|
|
74
|
+
req.on('end', () => {
|
|
75
|
+
try {
|
|
76
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
77
|
+
resolve(JSON.parse(body));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
reject(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
req.on('error', reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export class MCPServer {
|
|
87
|
+
db = null;
|
|
88
|
+
async initialize() {
|
|
89
|
+
const config = resolveConfig(await loadConfig());
|
|
90
|
+
const { db } = await connectPowerSync({ config, offline: false, timeoutMs: config.sync.timeoutMs });
|
|
91
|
+
this.db = db;
|
|
92
|
+
}
|
|
93
|
+
async handleRequest(req) {
|
|
94
|
+
try {
|
|
95
|
+
switch (req.method) {
|
|
96
|
+
case 'initialize':
|
|
97
|
+
return this.handleInitialize(req);
|
|
98
|
+
case 'tools/list':
|
|
99
|
+
return this.handleToolsList(req);
|
|
100
|
+
case 'tools/call':
|
|
101
|
+
return await this.handleToolsCall(req);
|
|
102
|
+
default:
|
|
103
|
+
return {
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
id: req.id,
|
|
106
|
+
error: { code: -32601, message: 'Method not found' },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
jsonrpc: '2.0',
|
|
113
|
+
id: req.id,
|
|
114
|
+
error: { code: -32603, message: error.message || 'Internal error' },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
handleInitialize(req) {
|
|
119
|
+
const serverInfo = {
|
|
120
|
+
name: 'dinox-mcp-server',
|
|
121
|
+
version: getPackageVersion(),
|
|
122
|
+
protocolVersion: '2024-11-05',
|
|
123
|
+
capabilities: {
|
|
124
|
+
tools: true,
|
|
125
|
+
resources: false,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
return { jsonrpc: '2.0', id: req.id, result: serverInfo };
|
|
129
|
+
}
|
|
130
|
+
handleToolsList(req) {
|
|
131
|
+
return { jsonrpc: '2.0', id: req.id, result: { tools: mcpTools } };
|
|
132
|
+
}
|
|
133
|
+
async handleToolsCall(req) {
|
|
134
|
+
if (!this.db) {
|
|
135
|
+
throw new Error('Database not initialized');
|
|
136
|
+
}
|
|
137
|
+
const name = req.params?.name;
|
|
138
|
+
const args = req.params?.arguments ?? {};
|
|
139
|
+
if (typeof name !== 'string') {
|
|
140
|
+
throw new Error('Invalid tools/call: missing tool name');
|
|
141
|
+
}
|
|
142
|
+
switch (name) {
|
|
143
|
+
case 'dinox_search_notes': {
|
|
144
|
+
const keyword = typeof args.query === 'string' ? args.query : '';
|
|
145
|
+
const tagsExpression = typeof args.tags === 'string' ? args.tags.trim() : '';
|
|
146
|
+
const { names: boxNames, matchEmpty: matchEmptyBoxes } = parseBoxFilter(args.box);
|
|
147
|
+
const zettelBoxIds = await resolveZettelBoxIdsForSearch(this.db, boxNames);
|
|
148
|
+
const daysInput = typeof args.days === 'number' ? String(args.days) : args.days;
|
|
149
|
+
const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
|
|
150
|
+
from: args.from,
|
|
151
|
+
to: args.to,
|
|
152
|
+
days: daysInput,
|
|
153
|
+
});
|
|
154
|
+
const limit = normalizeLimit(args.limit);
|
|
155
|
+
const rows = await searchNotes(this.db, keyword, {
|
|
156
|
+
includeDeleted: false,
|
|
157
|
+
tagsExpression: tagsExpression || undefined,
|
|
158
|
+
createdAtFrom,
|
|
159
|
+
createdAtTo,
|
|
160
|
+
zettelBoxIds,
|
|
161
|
+
matchEmptyZettelBoxes: matchEmptyBoxes,
|
|
162
|
+
});
|
|
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
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case 'dinox_get_note': {
|
|
172
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
173
|
+
throw new Error('dinox_get_note: id is required');
|
|
174
|
+
}
|
|
175
|
+
const notes = await getNoteDetails(this.db, [args.id.trim()]);
|
|
176
|
+
const text = JSON.stringify(notes[0] ?? null);
|
|
177
|
+
return {
|
|
178
|
+
jsonrpc: '2.0',
|
|
179
|
+
id: req.id,
|
|
180
|
+
result: { content: [{ type: 'text', text }] },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
case 'dinox_create_note': {
|
|
184
|
+
const title = typeof args.title === 'string' ? args.title.trim() : '';
|
|
185
|
+
const contentMd = typeof args.content === 'string' ? args.content : '';
|
|
186
|
+
if (!title) {
|
|
187
|
+
throw new Error('dinox_create_note: title is required');
|
|
188
|
+
}
|
|
189
|
+
if (!contentMd) {
|
|
190
|
+
throw new Error('dinox_create_note: content is required');
|
|
191
|
+
}
|
|
192
|
+
const rawConfig = await loadConfig();
|
|
193
|
+
const config = resolveConfig(rawConfig);
|
|
194
|
+
const identity = await resolveAuthIdentity(rawConfig, config);
|
|
195
|
+
const { contentJson, contentText } = convertMarkdownToNoteContent(contentMd);
|
|
196
|
+
const tags = await validateTagsForCreate(this.db, normalizeStringArray(args.tags));
|
|
197
|
+
const zettelBoxIds = await resolveZettelBoxIdsForCreate(this.db, normalizeStringArray(args.boxes));
|
|
198
|
+
const id = generateId();
|
|
199
|
+
await createNote(this.db, {
|
|
200
|
+
id,
|
|
201
|
+
title,
|
|
202
|
+
noteType: 'crawl',
|
|
203
|
+
contentMd,
|
|
204
|
+
contentText,
|
|
205
|
+
contentJson,
|
|
206
|
+
tags,
|
|
207
|
+
zettelBoxIds,
|
|
208
|
+
userId: identity.userId,
|
|
209
|
+
});
|
|
210
|
+
const text = JSON.stringify({ id });
|
|
211
|
+
return {
|
|
212
|
+
jsonrpc: '2.0',
|
|
213
|
+
id: req.id,
|
|
214
|
+
result: { content: [{ type: 'text', text }] },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
case 'dinox_graph_backlinks': {
|
|
218
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
219
|
+
throw new Error('dinox_graph_backlinks: id is required');
|
|
220
|
+
}
|
|
221
|
+
const result = await getBacklinks(this.db, args.id.trim());
|
|
222
|
+
const text = JSON.stringify(result);
|
|
223
|
+
return {
|
|
224
|
+
jsonrpc: '2.0',
|
|
225
|
+
id: req.id,
|
|
226
|
+
result: { content: [{ type: 'text', text }] },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
case 'dinox_graph_outlinks': {
|
|
230
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
231
|
+
throw new Error('dinox_graph_outlinks: id is required');
|
|
232
|
+
}
|
|
233
|
+
const result = await getOutlinks(this.db, args.id.trim());
|
|
234
|
+
const text = JSON.stringify(result);
|
|
235
|
+
return {
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id: req.id,
|
|
238
|
+
result: { content: [{ type: 'text', text }] },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
case 'dinox_graph_related': {
|
|
242
|
+
if (typeof args.id !== 'string' || !args.id.trim()) {
|
|
243
|
+
throw new Error('dinox_graph_related: id is required');
|
|
244
|
+
}
|
|
245
|
+
const depthRaw = typeof args.depth === 'number' && Number.isFinite(args.depth)
|
|
246
|
+
? Math.trunc(args.depth)
|
|
247
|
+
: 2;
|
|
248
|
+
const depth = Math.min(Math.max(depthRaw, 1), 5);
|
|
249
|
+
const result = await getRelatedNotes(this.db, args.id.trim(), depth);
|
|
250
|
+
const text = JSON.stringify(result);
|
|
251
|
+
return {
|
|
252
|
+
jsonrpc: '2.0',
|
|
253
|
+
id: req.id,
|
|
254
|
+
result: { content: [{ type: 'text', text }] },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
case 'dinox_search_todos': {
|
|
258
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
259
|
+
const status = typeof args.status === 'string' && ['all', 'completed', 'uncompleted'].includes(args.status)
|
|
260
|
+
? args.status
|
|
261
|
+
: 'all';
|
|
262
|
+
const limit = normalizeLimit(args.limit);
|
|
263
|
+
const result = await searchTodoTasks(this.db, query, {
|
|
264
|
+
includeDeleted: false,
|
|
265
|
+
taskStatus: status || 'all',
|
|
266
|
+
taskTagFilter: normalizeStringArray(args.tags),
|
|
267
|
+
limit,
|
|
268
|
+
scanLimit: 200,
|
|
269
|
+
});
|
|
270
|
+
const text = JSON.stringify(result);
|
|
271
|
+
return {
|
|
272
|
+
jsonrpc: '2.0',
|
|
273
|
+
id: req.id,
|
|
274
|
+
result: { content: [{ type: 'text', text }] },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async close() {
|
|
282
|
+
if (this.db) {
|
|
283
|
+
await this.db.close().catch(() => undefined);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export async function startMCPServer(port = 3000, host = '127.0.0.1', authToken) {
|
|
288
|
+
const mcpServer = new MCPServer();
|
|
289
|
+
await mcpServer.initialize();
|
|
290
|
+
const server = createServer(async (req, res) => {
|
|
291
|
+
if (req.method === 'POST') {
|
|
292
|
+
if (!isAuthorizedMCPRequest(req.headers, authToken)) {
|
|
293
|
+
res.writeHead(401, {
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
'WWW-Authenticate': 'Bearer realm="dinox-mcp"',
|
|
296
|
+
});
|
|
297
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const mcpReq = await readJsonBody(req);
|
|
302
|
+
const mcpRes = await mcpServer.handleRequest(mcpReq);
|
|
303
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
304
|
+
res.end(JSON.stringify(mcpRes));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
const statusCode = message.includes('too large') ? 413 : 400;
|
|
309
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
310
|
+
res.end(JSON.stringify({ error: message }));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
res.writeHead(405);
|
|
315
|
+
res.end();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
server.headersTimeout = 5_000;
|
|
319
|
+
server.requestTimeout = 10_000;
|
|
320
|
+
server.listen(port, host, () => {
|
|
321
|
+
console.log(`MCP server listening on http://${host}:${port}`);
|
|
322
|
+
});
|
|
323
|
+
return { server, mcpServer };
|
|
324
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const mcpTools = [
|
|
2
|
+
{
|
|
3
|
+
name: 'dinox_search_notes',
|
|
4
|
+
description: 'Search notes by keyword, tags, date range, or SQL expression',
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
query: { type: 'string', description: 'Search keyword' },
|
|
9
|
+
tags: { type: 'string', description: 'Tag expression (AND/OR/NOT)' },
|
|
10
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
11
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
12
|
+
days: { type: 'number', description: 'Recent N days' },
|
|
13
|
+
box: { type: 'string', description: 'Zettel box name' },
|
|
14
|
+
limit: { type: 'number', description: 'Max results (default: 50)' },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'dinox_get_note',
|
|
20
|
+
description: 'Get note details by ID',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
id: { type: 'string', description: 'Note ID' },
|
|
25
|
+
},
|
|
26
|
+
required: ['id'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'dinox_create_note',
|
|
31
|
+
description: 'Create a new note',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
title: { type: 'string', description: 'Note title' },
|
|
36
|
+
content: { type: 'string', description: 'Markdown content' },
|
|
37
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
|
|
38
|
+
boxes: { type: 'array', items: { type: 'string' }, description: 'Zettel boxes' },
|
|
39
|
+
},
|
|
40
|
+
required: ['title', 'content'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'dinox_graph_backlinks',
|
|
45
|
+
description: 'Get notes that link to this note',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
id: { type: 'string', description: 'Note ID' },
|
|
50
|
+
},
|
|
51
|
+
required: ['id'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'dinox_graph_outlinks',
|
|
56
|
+
description: 'Get notes that this note links to',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
id: { type: 'string', description: 'Note ID' },
|
|
61
|
+
},
|
|
62
|
+
required: ['id'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'dinox_graph_related',
|
|
67
|
+
description: 'Get related notes within N degrees',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
id: { type: 'string', description: 'Note ID' },
|
|
72
|
+
depth: { type: 'number', description: 'Degrees (1-5, default: 2)' },
|
|
73
|
+
},
|
|
74
|
+
required: ['id'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'dinox_search_todos',
|
|
79
|
+
description: 'Search todo tasks',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
query: { type: 'string', description: 'Task text query' },
|
|
84
|
+
status: { type: 'string', enum: ['all', 'completed', 'uncompleted'], description: 'Task status' },
|
|
85
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Task tags' },
|
|
86
|
+
limit: { type: 'number', description: 'Max results (default: 50)' },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type MCPTool = {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: 'object';
|
|
6
|
+
properties: Record<string, any>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type MCPResource = {
|
|
11
|
+
uri: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
};
|
|
16
|
+
export type MCPServerInfo = {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
protocolVersion: string;
|
|
20
|
+
capabilities: {
|
|
21
|
+
tools?: boolean;
|
|
22
|
+
resources?: boolean;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { request as httpRequest } from 'node:http';
|
|
2
|
+
import { DEFAULT_DAEMON_PORT } from './types.js';
|
|
3
|
+
import { readPid } from './pid.js';
|
|
4
|
+
const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 2_000;
|
|
5
|
+
const MAX_DAEMON_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
export async function sendDaemonRequest(method, params) {
|
|
7
|
+
const pidInfo = readPid();
|
|
8
|
+
const port = pidInfo?.port || DEFAULT_DAEMON_PORT;
|
|
9
|
+
const request = {
|
|
10
|
+
id: Date.now().toString(),
|
|
11
|
+
method,
|
|
12
|
+
params,
|
|
13
|
+
};
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let settled = false;
|
|
16
|
+
const finalize = (error, value) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
if (error) {
|
|
21
|
+
reject(error);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
resolve(value);
|
|
25
|
+
};
|
|
26
|
+
const data = JSON.stringify(request);
|
|
27
|
+
const options = {
|
|
28
|
+
hostname: '127.0.0.1',
|
|
29
|
+
port,
|
|
30
|
+
path: '/',
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Content-Length': Buffer.byteLength(data),
|
|
35
|
+
},
|
|
36
|
+
timeout: DEFAULT_DAEMON_REQUEST_TIMEOUT_MS,
|
|
37
|
+
};
|
|
38
|
+
const req = httpRequest(options, (res) => {
|
|
39
|
+
const statusCode = res.statusCode ?? 0;
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let totalBytes = 0;
|
|
42
|
+
res.on('data', (chunk) => {
|
|
43
|
+
totalBytes += chunk.length;
|
|
44
|
+
if (totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
|
|
45
|
+
req.destroy(new Error('Daemon response too large'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
chunks.push(chunk);
|
|
49
|
+
});
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
53
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
54
|
+
finalize(new Error(`Daemon request failed (HTTP ${statusCode || 'unknown'}): ${body || '(empty)'}`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const response = JSON.parse(body);
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
finalize(undefined, response.result);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
finalize(new Error(response.error || 'Unknown error'));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
finalize(error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
req.on('timeout', () => {
|
|
71
|
+
req.destroy(new Error('Daemon request timed out'));
|
|
72
|
+
});
|
|
73
|
+
req.on('error', (error) => finalize(error));
|
|
74
|
+
req.write(data);
|
|
75
|
+
req.end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function isDaemonRunning() {
|
|
79
|
+
try {
|
|
80
|
+
await sendDaemonRequest('ping');
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensureDaemonRunning(): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { isDaemonRunning } from './client.js';
|
|
3
|
+
import { DEFAULT_DAEMON_PORT } from './types.js';
|
|
4
|
+
export async function ensureDaemonRunning() {
|
|
5
|
+
if (await isDaemonRunning()) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
console.log('Starting daemon...');
|
|
9
|
+
const args = ['daemon', 'start', '--port', String(DEFAULT_DAEMON_PORT), '--no-detach'];
|
|
10
|
+
const child = spawn(process.argv[0], [process.argv[1], ...args], {
|
|
11
|
+
detached: true,
|
|
12
|
+
stdio: 'ignore',
|
|
13
|
+
});
|
|
14
|
+
child.unref();
|
|
15
|
+
for (let i = 0; i < 10; i++) {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
17
|
+
if (await isDaemonRunning()) {
|
|
18
|
+
console.log('Daemon started');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Failed to start daemon');
|
|
23
|
+
}
|