pagedex 0.1.4 → 0.1.6
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 +29 -9
- package/bin/pagedex.js +73 -5
- package/dist/assets/index-A_l6j1w5.css +1 -0
- package/dist/assets/index-vKzhpzX9.js +185 -0
- package/dist/index.html +3 -2
- package/docs/MCP.md +107 -0
- package/docs/README.md +2 -1
- package/docs/getting-started.md +6 -0
- package/docs/images/desktop-edit.png +0 -0
- package/docs/images/desktop-preview.png +0 -0
- package/docs/images/desktop-switcher.png +0 -0
- package/docs/images/mobile-preview.png +0 -0
- package/docs/mcp-and-api.md +7 -6
- package/package.json +4 -1
- package/src/server/index.js +4 -0
- package/src/server/mcp.js +423 -65
- package/dist/assets/TiptapEditor-OFHNcjy3.js +0 -1
- package/dist/assets/index-BXGU3AiE.css +0 -1
- package/dist/assets/index-ZVjt99Mq.js +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pagedex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Local-first Markdown knowledge viewer, editor, Git bridge, and MCP v2 server for any directory.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"start": "node --no-warnings src/server/index.js",
|
|
30
30
|
"prepublishOnly": "vite build",
|
|
31
31
|
"test": "node --no-warnings --test test/*.test.js",
|
|
32
|
+
"test:playwright": "node --no-warnings --test test/playwright/*.spec.js",
|
|
32
33
|
"smoke": "node --no-warnings test/smoke.js all",
|
|
33
34
|
"smoke:api": "node --no-warnings test/smoke.js api",
|
|
34
35
|
"smoke:mcp": "node --no-warnings test/smoke.js mcp"
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"@tiptap/starter-kit": "^3.34.0",
|
|
43
44
|
"highlight.js": "^11.12.0",
|
|
44
45
|
"hono": "^4.7.0",
|
|
46
|
+
"lucide-react": "^1.73.0",
|
|
45
47
|
"marked": "^15.0.0",
|
|
46
48
|
"react": "^19.1.0",
|
|
47
49
|
"react-dom": "^19.1.0",
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@vitejs/plugin-react": "^4.4.0",
|
|
55
|
+
"playwright-core": "^1.64.1",
|
|
53
56
|
"vite": "^6.3.0"
|
|
54
57
|
}
|
|
55
58
|
}
|
package/src/server/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { registerEventRoutes, startVaultWatcher } from './routes/events.js';
|
|
|
12
12
|
|
|
13
13
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
14
14
|
const PORT = Number(process.env.PORT || 4321);
|
|
15
|
+
const READ_ONLY = process.env.PAGEDEX_READ_ONLY === '1' || process.env.PAGEDEX_READ_ONLY === 'true';
|
|
15
16
|
|
|
16
17
|
try {
|
|
17
18
|
syncIndex();
|
|
@@ -27,6 +28,9 @@ const app = new Hono();
|
|
|
27
28
|
app.use('/api/v1/*', async (c, next) => {
|
|
28
29
|
const method = c.req.method.toUpperCase();
|
|
29
30
|
if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
|
|
31
|
+
if (READ_ONLY) {
|
|
32
|
+
return c.json({ error: 'Server is running in read-only mode' }, 403);
|
|
33
|
+
}
|
|
30
34
|
if (c.req.header('origin') === 'null') {
|
|
31
35
|
return c.json({ error: 'Forbidden origin' }, 403);
|
|
32
36
|
}
|
package/src/server/mcp.js
CHANGED
|
@@ -1,9 +1,57 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { McpServer, createMcpHandler, parseJSONRPCMessage } from '@modelcontextprotocol/server';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
deleteNote,
|
|
7
|
+
discardGitChange,
|
|
8
|
+
editNote,
|
|
9
|
+
getCachedVault,
|
|
10
|
+
getGitChanges,
|
|
11
|
+
getGitDiff,
|
|
12
|
+
gitCommitAndPush,
|
|
13
|
+
renameDir,
|
|
14
|
+
renameNote,
|
|
15
|
+
resolveSafe,
|
|
16
|
+
writeNote
|
|
17
|
+
} from './vault.js';
|
|
18
|
+
import { removeFileFromIndex, searchFts, syncIndex, upsertFileInIndex } from './search-index.js';
|
|
19
|
+
|
|
20
|
+
const VALID_MCP_MODES = new Set(['read', 'write', 'full', 'off']);
|
|
21
|
+
|
|
22
|
+
export function getMcpConfig() {
|
|
23
|
+
const readOnly = process.env.PAGEDEX_READ_ONLY === '1' || process.env.PAGEDEX_READ_ONLY === 'true';
|
|
24
|
+
const rawMode = (process.env.PAGEDEX_MCP_MODE || 'full').toLowerCase();
|
|
25
|
+
const normalized = VALID_MCP_MODES.has(rawMode) ? rawMode : 'full';
|
|
26
|
+
const mode = normalized === 'off' ? 'off' : readOnly ? 'read' : normalized;
|
|
27
|
+
const allowWrite = mode === 'write' || mode === 'full';
|
|
28
|
+
const allowDelete = mode === 'full';
|
|
29
|
+
const allowGit =
|
|
30
|
+
!readOnly &&
|
|
31
|
+
(process.env.PAGEDEX_MCP_GIT === '1' || process.env.PAGEDEX_MCP_GIT === 'true') &&
|
|
32
|
+
allowWrite;
|
|
33
|
+
return { mode, readOnly, allowWrite, allowDelete, allowGit };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readMcpToken() {
|
|
37
|
+
const tokenFile = process.env.PAGEDEX_MCP_TOKEN_FILE;
|
|
38
|
+
if (tokenFile) {
|
|
39
|
+
return fs.readFileSync(tokenFile, 'utf8').trim();
|
|
40
|
+
}
|
|
41
|
+
return (process.env.PAGEDEX_MCP_TOKEN || '').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isAuthorizedMcpRequest(req) {
|
|
45
|
+
const expected = readMcpToken();
|
|
46
|
+
if (!expected) return true;
|
|
47
|
+
const auth = req.headers.get('authorization') || '';
|
|
48
|
+
const match = auth.match(/^Bearer\s+(.+)$/i);
|
|
49
|
+
if (!match) return false;
|
|
50
|
+
const provided = match[1].trim();
|
|
51
|
+
const a = Buffer.from(provided, 'utf8');
|
|
52
|
+
const b = Buffer.from(expected, 'utf8');
|
|
53
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
54
|
+
}
|
|
7
55
|
|
|
8
56
|
const sseSessions = new Map();
|
|
9
57
|
const encoder = new TextEncoder();
|
|
@@ -88,7 +136,7 @@ class WebStandardSSEServerTransport {
|
|
|
88
136
|
}
|
|
89
137
|
}
|
|
90
138
|
|
|
91
|
-
function createVaultMcpServer() {
|
|
139
|
+
function createVaultMcpServer(config = getMcpConfig()) {
|
|
92
140
|
const server = new McpServer({
|
|
93
141
|
name: 'vault-mcp',
|
|
94
142
|
version: '0.1.0'
|
|
@@ -233,68 +281,142 @@ function createVaultMcpServer() {
|
|
|
233
281
|
);
|
|
234
282
|
|
|
235
283
|
server.registerTool(
|
|
236
|
-
'
|
|
284
|
+
'get_vault_info',
|
|
237
285
|
{
|
|
238
|
-
title: '
|
|
239
|
-
description:
|
|
240
|
-
'Create/overwrite/append a note (with content), or partially edit an existing note (with target + replacement).',
|
|
286
|
+
title: 'Get Vault Info',
|
|
287
|
+
description: 'Return vault summary: total indexed files and the most recently modified files.',
|
|
241
288
|
inputSchema: z.object({
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
289
|
+
recentLimit: z.number().int().min(1).max(50).optional()
|
|
290
|
+
}),
|
|
291
|
+
outputSchema: z.object({
|
|
292
|
+
totalFiles: z.number(),
|
|
293
|
+
recentFiles: z.array(
|
|
294
|
+
z.object({
|
|
295
|
+
path: z.string(),
|
|
296
|
+
mtimeMs: z.number()
|
|
297
|
+
})
|
|
298
|
+
)
|
|
299
|
+
})
|
|
300
|
+
},
|
|
301
|
+
async ({ recentLimit }) => {
|
|
302
|
+
const { flatFiles, fileStats } = getCachedVault();
|
|
303
|
+
const limit = recentLimit ?? 15;
|
|
304
|
+
const recentFiles = [...(fileStats || [])]
|
|
305
|
+
.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0))
|
|
306
|
+
.slice(0, limit)
|
|
307
|
+
.map((f) => ({ path: f.path, mtimeMs: f.mtimeMs || 0 }));
|
|
308
|
+
const lines = recentFiles.map((f) => f.path);
|
|
309
|
+
return {
|
|
310
|
+
structuredContent: {
|
|
311
|
+
totalFiles: flatFiles.length,
|
|
312
|
+
recentFiles
|
|
313
|
+
},
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: 'text',
|
|
317
|
+
text: `Total files: ${flatFiles.length}\nRecent files:\n${lines.length ? lines.join('\n') : '(none)'}`
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
server.registerTool(
|
|
325
|
+
'get_note_links',
|
|
326
|
+
{
|
|
327
|
+
title: 'Get Note Links and Outline',
|
|
328
|
+
description: 'Return outgoing [[wikilinks]], incoming backlinks, and Markdown headings for a note.',
|
|
329
|
+
inputSchema: z.object({
|
|
330
|
+
path: z.string()
|
|
248
331
|
}),
|
|
249
332
|
outputSchema: z.object({
|
|
250
|
-
ok: z.boolean(),
|
|
251
333
|
path: z.string(),
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
334
|
+
outgoingLinks: z.array(z.string()),
|
|
335
|
+
backlinks: z.array(z.string()),
|
|
336
|
+
headings: z.array(
|
|
337
|
+
z.object({
|
|
338
|
+
level: z.number(),
|
|
339
|
+
text: z.string()
|
|
340
|
+
})
|
|
341
|
+
)
|
|
255
342
|
})
|
|
256
343
|
},
|
|
257
|
-
async ({ path: relPath
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return { isError: true, content: [{ type: 'text', text: result.error }] };
|
|
262
|
-
}
|
|
263
|
-
upsertFileInIndex(result.path);
|
|
264
|
-
return {
|
|
265
|
-
structuredContent: {
|
|
266
|
-
ok: true,
|
|
267
|
-
path: result.path,
|
|
268
|
-
mode: 'edit',
|
|
269
|
-
replacements: result.replacements
|
|
270
|
-
},
|
|
271
|
-
content: [
|
|
272
|
-
{
|
|
273
|
-
type: 'text',
|
|
274
|
-
text: `Edited ${result.path} (${result.replacements} replacement${result.replacements === 1 ? '' : 's'})`
|
|
275
|
-
}
|
|
276
|
-
]
|
|
277
|
-
};
|
|
344
|
+
async ({ path: relPath }) => {
|
|
345
|
+
const full = resolveSafe(relPath);
|
|
346
|
+
if (!full || !fs.existsSync(full) || !fs.statSync(full).isFile()) {
|
|
347
|
+
return { isError: true, content: [{ type: 'text', text: `Not found: ${relPath}` }] };
|
|
278
348
|
}
|
|
279
|
-
|
|
280
|
-
|
|
349
|
+
const raw = fs.readFileSync(full, 'utf8');
|
|
350
|
+
const outgoingSet = new Set();
|
|
351
|
+
for (const m of raw.matchAll(/\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g)) {
|
|
352
|
+
const target = m[1].trim();
|
|
353
|
+
if (target) outgoingSet.add(target);
|
|
281
354
|
}
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
355
|
+
const headings = [];
|
|
356
|
+
for (const line of raw.split('\n')) {
|
|
357
|
+
const hm = line.match(/^(#{1,6})\s+(.+?)\s*#*$/);
|
|
358
|
+
if (hm) headings.push({ level: hm[1].length, text: hm[2].trim() });
|
|
285
359
|
}
|
|
286
|
-
|
|
360
|
+
const { backlinks } = getCachedVault();
|
|
361
|
+
const outgoingLinks = [...outgoingSet];
|
|
362
|
+
const incoming = backlinks[relPath] || [];
|
|
363
|
+
return {
|
|
364
|
+
structuredContent: {
|
|
365
|
+
path: relPath,
|
|
366
|
+
outgoingLinks,
|
|
367
|
+
backlinks: incoming,
|
|
368
|
+
headings
|
|
369
|
+
},
|
|
370
|
+
content: [
|
|
371
|
+
{
|
|
372
|
+
type: 'text',
|
|
373
|
+
text: [
|
|
374
|
+
`# ${relPath}`,
|
|
375
|
+
`Outgoing: ${outgoingLinks.length ? outgoingLinks.join(', ') : 'none'}`,
|
|
376
|
+
`Backlinks: ${incoming.length ? incoming.join(', ') : 'none'}`,
|
|
377
|
+
`Headings: ${headings.length ? headings.map((h) => `${'#'.repeat(h.level)} ${h.text}`).join(' | ') : 'none'}`
|
|
378
|
+
].join('\n')
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
server.registerTool(
|
|
386
|
+
'git_status',
|
|
387
|
+
{
|
|
388
|
+
title: 'Git Status',
|
|
389
|
+
description: 'List uncommitted modified, added, or untracked files in the vault.',
|
|
390
|
+
inputSchema: z.object({}),
|
|
391
|
+
outputSchema: z.object({
|
|
392
|
+
isGit: z.boolean(),
|
|
393
|
+
count: z.number(),
|
|
394
|
+
changes: z.array(
|
|
395
|
+
z.object({
|
|
396
|
+
status: z.string(),
|
|
397
|
+
path: z.string(),
|
|
398
|
+
mtimeMs: z.number()
|
|
399
|
+
})
|
|
400
|
+
)
|
|
401
|
+
})
|
|
402
|
+
},
|
|
403
|
+
async () => {
|
|
404
|
+
const { isGit, changes } = getGitChanges();
|
|
405
|
+
const lines = changes.map((c) => `[${c.status}] ${c.path}`);
|
|
287
406
|
return {
|
|
288
407
|
structuredContent: {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
bytes: result.bytes
|
|
408
|
+
isGit,
|
|
409
|
+
count: changes.length,
|
|
410
|
+
changes
|
|
293
411
|
},
|
|
294
412
|
content: [
|
|
295
413
|
{
|
|
296
414
|
type: 'text',
|
|
297
|
-
text:
|
|
415
|
+
text: !isGit
|
|
416
|
+
? 'Not a git repository'
|
|
417
|
+
: lines.length
|
|
418
|
+
? lines.join('\n')
|
|
419
|
+
: 'Working tree clean'
|
|
298
420
|
}
|
|
299
421
|
]
|
|
300
422
|
};
|
|
@@ -302,40 +424,276 @@ function createVaultMcpServer() {
|
|
|
302
424
|
);
|
|
303
425
|
|
|
304
426
|
server.registerTool(
|
|
305
|
-
'
|
|
427
|
+
'git_diff',
|
|
306
428
|
{
|
|
307
|
-
title: '
|
|
308
|
-
description: '
|
|
429
|
+
title: 'Git Diff',
|
|
430
|
+
description: 'Get the unified git diff for a specific file in the vault.',
|
|
309
431
|
inputSchema: z.object({
|
|
310
432
|
path: z.string()
|
|
311
433
|
}),
|
|
312
434
|
outputSchema: z.object({
|
|
313
|
-
|
|
314
|
-
|
|
435
|
+
path: z.string(),
|
|
436
|
+
untracked: z.boolean(),
|
|
437
|
+
raw: z.string()
|
|
315
438
|
})
|
|
316
439
|
},
|
|
317
440
|
async ({ path: relPath }) => {
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
return { isError: true, content: [{ type: 'text', text:
|
|
441
|
+
const diff = getGitDiff(relPath);
|
|
442
|
+
if (diff.error) {
|
|
443
|
+
return { isError: true, content: [{ type: 'text', text: diff.error }] };
|
|
321
444
|
}
|
|
322
|
-
removeFileFromIndex(result.path);
|
|
323
445
|
return {
|
|
324
446
|
structuredContent: {
|
|
325
|
-
|
|
326
|
-
|
|
447
|
+
path: diff.path,
|
|
448
|
+
untracked: Boolean(diff.untracked),
|
|
449
|
+
raw: diff.raw || ''
|
|
327
450
|
},
|
|
328
|
-
content: [
|
|
451
|
+
content: [
|
|
452
|
+
{
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: diff.raw ? diff.raw : `No changes in ${diff.path}`
|
|
455
|
+
}
|
|
456
|
+
]
|
|
329
457
|
};
|
|
330
458
|
}
|
|
331
459
|
);
|
|
332
460
|
|
|
461
|
+
if (config.allowWrite) {
|
|
462
|
+
server.registerTool(
|
|
463
|
+
'write_note',
|
|
464
|
+
{
|
|
465
|
+
title: 'Write or Edit Note',
|
|
466
|
+
description:
|
|
467
|
+
'Create/overwrite/append a note (with content), or partially edit an existing note (with target + replacement).',
|
|
468
|
+
inputSchema: z.object({
|
|
469
|
+
path: z.string(),
|
|
470
|
+
content: z.string().optional(),
|
|
471
|
+
append: z.boolean().optional(),
|
|
472
|
+
target: z.string().optional(),
|
|
473
|
+
replacement: z.string().optional(),
|
|
474
|
+
replaceAll: z.boolean().optional()
|
|
475
|
+
}),
|
|
476
|
+
outputSchema: z.object({
|
|
477
|
+
ok: z.boolean(),
|
|
478
|
+
path: z.string(),
|
|
479
|
+
mode: z.enum(['write', 'append', 'edit']),
|
|
480
|
+
bytes: z.number().optional(),
|
|
481
|
+
replacements: z.number().optional()
|
|
482
|
+
})
|
|
483
|
+
},
|
|
484
|
+
async ({ path: relPath, content, append, target, replacement, replaceAll }) => {
|
|
485
|
+
if (typeof target === 'string' && typeof replacement === 'string') {
|
|
486
|
+
const result = editNote(relPath, target, replacement, { replaceAll: Boolean(replaceAll) });
|
|
487
|
+
if (result.error) {
|
|
488
|
+
return { isError: true, content: [{ type: 'text', text: result.error }] };
|
|
489
|
+
}
|
|
490
|
+
upsertFileInIndex(result.path);
|
|
491
|
+
return {
|
|
492
|
+
structuredContent: {
|
|
493
|
+
ok: true,
|
|
494
|
+
path: result.path,
|
|
495
|
+
mode: 'edit',
|
|
496
|
+
replacements: result.replacements
|
|
497
|
+
},
|
|
498
|
+
content: [
|
|
499
|
+
{
|
|
500
|
+
type: 'text',
|
|
501
|
+
text: `Edited ${result.path} (${result.replacements} replacement${result.replacements === 1 ? '' : 's'})`
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (typeof content !== 'string') {
|
|
507
|
+
return { isError: true, content: [{ type: 'text', text: 'Provide either content or target + replacement' }] };
|
|
508
|
+
}
|
|
509
|
+
const result = writeNote(relPath, content, { append: Boolean(append) });
|
|
510
|
+
if (!result) {
|
|
511
|
+
return { isError: true, content: [{ type: 'text', text: `Invalid or protected path: ${relPath}` }] };
|
|
512
|
+
}
|
|
513
|
+
upsertFileInIndex(result.path);
|
|
514
|
+
return {
|
|
515
|
+
structuredContent: {
|
|
516
|
+
ok: true,
|
|
517
|
+
path: result.path,
|
|
518
|
+
mode: append ? 'append' : 'write',
|
|
519
|
+
bytes: result.bytes
|
|
520
|
+
},
|
|
521
|
+
content: [
|
|
522
|
+
{
|
|
523
|
+
type: 'text',
|
|
524
|
+
text: `${append ? 'Appended' : 'Wrote'} ${result.path} (${result.bytes} bytes)`
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
server.registerTool(
|
|
532
|
+
'move_path',
|
|
533
|
+
{
|
|
534
|
+
title: 'Move or Rename File/Folder',
|
|
535
|
+
description: 'Rename or move a file or folder within the vault.',
|
|
536
|
+
inputSchema: z.object({
|
|
537
|
+
oldPath: z.string(),
|
|
538
|
+
newPath: z.string(),
|
|
539
|
+
type: z.enum(['auto', 'file', 'dir']).optional()
|
|
540
|
+
}),
|
|
541
|
+
outputSchema: z.object({
|
|
542
|
+
ok: z.boolean(),
|
|
543
|
+
type: z.enum(['file', 'dir']),
|
|
544
|
+
oldPath: z.string(),
|
|
545
|
+
newPath: z.string()
|
|
546
|
+
})
|
|
547
|
+
},
|
|
548
|
+
async ({ oldPath, newPath, type = 'auto' }) => {
|
|
549
|
+
const full = resolveSafe(oldPath);
|
|
550
|
+
if (!full || !fs.existsSync(full)) {
|
|
551
|
+
return { isError: true, content: [{ type: 'text', text: `Source not found: ${oldPath}` }] };
|
|
552
|
+
}
|
|
553
|
+
const isDir = type === 'dir' || (type === 'auto' && fs.statSync(full).isDirectory());
|
|
554
|
+
const result = isDir ? renameDir(oldPath, newPath) : renameNote(oldPath, newPath);
|
|
555
|
+
if (result.error) {
|
|
556
|
+
return { isError: true, content: [{ type: 'text', text: result.error }] };
|
|
557
|
+
}
|
|
558
|
+
if (isDir) {
|
|
559
|
+
syncIndex();
|
|
560
|
+
} else {
|
|
561
|
+
removeFileFromIndex(result.oldPath);
|
|
562
|
+
upsertFileInIndex(result.newPath);
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
structuredContent: {
|
|
566
|
+
ok: true,
|
|
567
|
+
type: isDir ? 'dir' : 'file',
|
|
568
|
+
oldPath: result.oldPath,
|
|
569
|
+
newPath: result.newPath
|
|
570
|
+
},
|
|
571
|
+
content: [{ type: 'text', text: `Moved ${result.oldPath} -> ${result.newPath}` }]
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (config.allowDelete) {
|
|
578
|
+
server.registerTool(
|
|
579
|
+
'delete_note',
|
|
580
|
+
{
|
|
581
|
+
title: 'Delete Note',
|
|
582
|
+
description: 'Delete a note/file from the vault by relative path.',
|
|
583
|
+
inputSchema: z.object({
|
|
584
|
+
path: z.string()
|
|
585
|
+
}),
|
|
586
|
+
outputSchema: z.object({
|
|
587
|
+
ok: z.boolean(),
|
|
588
|
+
path: z.string()
|
|
589
|
+
})
|
|
590
|
+
},
|
|
591
|
+
async ({ path: relPath }) => {
|
|
592
|
+
const result = deleteNote(relPath);
|
|
593
|
+
if (!result) {
|
|
594
|
+
return { isError: true, content: [{ type: 'text', text: `Not found or protected path: ${relPath}` }] };
|
|
595
|
+
}
|
|
596
|
+
removeFileFromIndex(result.path);
|
|
597
|
+
return {
|
|
598
|
+
structuredContent: {
|
|
599
|
+
ok: true,
|
|
600
|
+
path: result.path
|
|
601
|
+
},
|
|
602
|
+
content: [{ type: 'text', text: `Deleted ${result.path}` }]
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (config.allowGit) {
|
|
609
|
+
server.registerTool(
|
|
610
|
+
'git_commit',
|
|
611
|
+
{
|
|
612
|
+
title: 'Git Commit',
|
|
613
|
+
description: 'Stage and commit vault changes locally (never pushes).',
|
|
614
|
+
inputSchema: z.object({
|
|
615
|
+
message: z.string(),
|
|
616
|
+
files: z.array(z.string()).optional()
|
|
617
|
+
}),
|
|
618
|
+
outputSchema: z.object({
|
|
619
|
+
ok: z.boolean(),
|
|
620
|
+
output: z.string()
|
|
621
|
+
})
|
|
622
|
+
},
|
|
623
|
+
async ({ message, files }) => {
|
|
624
|
+
const result = gitCommitAndPush({ message, files, push: false });
|
|
625
|
+
if (result.error) {
|
|
626
|
+
return { isError: true, content: [{ type: 'text', text: result.error }] };
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
structuredContent: {
|
|
630
|
+
ok: true,
|
|
631
|
+
output: result.output || ''
|
|
632
|
+
},
|
|
633
|
+
content: [{ type: 'text', text: result.output || 'Committed' }]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
server.registerTool(
|
|
639
|
+
'git_discard',
|
|
640
|
+
{
|
|
641
|
+
title: 'Git Discard File Changes',
|
|
642
|
+
description: 'Discard all uncommitted changes to a file (or remove an untracked file).',
|
|
643
|
+
inputSchema: z.object({
|
|
644
|
+
path: z.string()
|
|
645
|
+
}),
|
|
646
|
+
outputSchema: z.object({
|
|
647
|
+
ok: z.boolean(),
|
|
648
|
+
path: z.string(),
|
|
649
|
+
discarded: z.string()
|
|
650
|
+
})
|
|
651
|
+
},
|
|
652
|
+
async ({ path: relPath }) => {
|
|
653
|
+
const result = discardGitChange({ path: relPath });
|
|
654
|
+
if (result.error) {
|
|
655
|
+
return { isError: true, content: [{ type: 'text', text: result.error }] };
|
|
656
|
+
}
|
|
657
|
+
syncIndex();
|
|
658
|
+
return {
|
|
659
|
+
structuredContent: {
|
|
660
|
+
ok: true,
|
|
661
|
+
path: result.path,
|
|
662
|
+
discarded: result.discarded || 'file'
|
|
663
|
+
},
|
|
664
|
+
content: [{ type: 'text', text: `Discarded changes in ${result.path} (${result.discarded})` }]
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
333
670
|
return server;
|
|
334
671
|
}
|
|
335
672
|
|
|
336
|
-
const
|
|
673
|
+
const mcpHandlers = new Map();
|
|
674
|
+
|
|
675
|
+
function getMcpHandler(config) {
|
|
676
|
+
const key = `${config.mode}:${config.allowGit}`;
|
|
677
|
+
let handler = mcpHandlers.get(key);
|
|
678
|
+
if (!handler) {
|
|
679
|
+
handler = createMcpHandler(() => createVaultMcpServer(config));
|
|
680
|
+
mcpHandlers.set(key, handler);
|
|
681
|
+
}
|
|
682
|
+
return handler;
|
|
683
|
+
}
|
|
337
684
|
|
|
338
685
|
export async function handleMcpRequest(req) {
|
|
686
|
+
const config = getMcpConfig();
|
|
687
|
+
if (config.mode === 'off') {
|
|
688
|
+
return Response.json({ error: 'MCP endpoint is disabled' }, { status: 404 });
|
|
689
|
+
}
|
|
690
|
+
if (!isAuthorizedMcpRequest(req)) {
|
|
691
|
+
return Response.json(
|
|
692
|
+
{ error: 'Unauthorized' },
|
|
693
|
+
{ status: 401, headers: { 'WWW-Authenticate': 'Bearer' } }
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
339
697
|
const url = new URL(req.url, 'http://localhost');
|
|
340
698
|
const sessionId = url.searchParams.get('sessionId');
|
|
341
699
|
const isStreamableHttpGet =
|
|
@@ -345,7 +703,7 @@ export async function handleMcpRequest(req) {
|
|
|
345
703
|
if (req.method === 'GET' && !sessionId && !isStreamableHttpGet) {
|
|
346
704
|
const transport = new WebStandardSSEServerTransport(url.pathname);
|
|
347
705
|
sseSessions.set(transport.sessionId, transport);
|
|
348
|
-
const server = createVaultMcpServer();
|
|
706
|
+
const server = createVaultMcpServer(config);
|
|
349
707
|
await server.connect(transport);
|
|
350
708
|
return transport.createResponse(req.signal);
|
|
351
709
|
}
|
|
@@ -360,5 +718,5 @@ export async function handleMcpRequest(req) {
|
|
|
360
718
|
}
|
|
361
719
|
|
|
362
720
|
// 3. Official MCP v2 handler: supports BOTH Modern (2026-07-28 server/discover) and Legacy Streamable HTTP (2025-11-25 initialize)
|
|
363
|
-
return
|
|
721
|
+
return getMcpHandler(config).fetch(req);
|
|
364
722
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as d,j as e}from"./vendor-react-E4kvVwMA.js";import{u as p,E as b,s as h,M as x}from"./vendor-tiptap-ChgD7Piv.js";import"./vendor-markdown-C94ubdL7.js";function N({content:n,fileKey:u,fontSize:m,lineHeight:f,maxWidth:g,saving:c,dirty:o,onChange:l,onSave:v}){const t=p({extensions:[h,x],content:n});return d.useEffect(()=>{var s,i;if(!t)return;(((i=(s=t.storage.markdown)==null?void 0:s.getMarkdown)==null?void 0:i.call(s))??"")!==n&&!o&&t.commands.setContent(n)},[u,n,t,o]),d.useEffect(()=>{if(!t)return;const a=()=>{var i,r;const s=((r=(i=t.storage.markdown)==null?void 0:i.getMarkdown)==null?void 0:r.call(i))??"";l(s)};return t.on("update",a),()=>t.off("update",a)},[t,l]),t?e.jsxs("div",{className:"tiptap-wrap",style:{maxWidth:`${g}px`},children:[e.jsxs("div",{className:"tiptap-toolbar",children:[e.jsx("button",{className:`btn${t.isActive("bold")?" active":""}`,onClick:()=>t.chain().focus().toggleBold().run(),children:"B"}),e.jsx("button",{className:`btn${t.isActive("italic")?" active":""}`,onClick:()=>t.chain().focus().toggleItalic().run(),children:"I"}),e.jsx("button",{className:`btn${t.isActive("heading",{level:1})?" active":""}`,onClick:()=>t.chain().focus().toggleHeading({level:1}).run(),children:"H1"}),e.jsx("button",{className:`btn${t.isActive("heading",{level:2})?" active":""}`,onClick:()=>t.chain().focus().toggleHeading({level:2}).run(),children:"H2"}),e.jsx("button",{className:`btn${t.isActive("bulletList")?" active":""}`,onClick:()=>t.chain().focus().toggleBulletList().run(),children:"List"}),e.jsx("button",{className:`btn${t.isActive("codeBlock")?" active":""}`,onClick:()=>t.chain().focus().toggleCodeBlock().run(),children:"Code"}),e.jsx("button",{className:`btn${o?" primary":""}`,style:{marginLeft:"auto"},disabled:c,onClick:()=>{var a,s;return v(((s=(a=t.storage.markdown)==null?void 0:a.getMarkdown)==null?void 0:s.call(a))??"")},children:c?"Saving...":o?"Save *":"Saved"})]}),e.jsx("div",{className:"doc tiptap-body",style:{fontSize:`${m}px`,lineHeight:f},children:e.jsx(b,{editor:t})})]}):null}export{N as default};
|