feat: dashboard Second Brain, essai 7 jours et vérification e-mail
Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,7 +6,7 @@ Model Context Protocol (MCP) server for integrating Memento note-taking app with
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **22 Tools** for notes, notebooks, labels, and reminders
|
||||
- ✅ **29 Tools** for notes, notebooks, labels, reminders, semantic similarity, Memory Echo insights, and statistics
|
||||
- 🔒 **API Key Authentication** with secure storage
|
||||
- 🚀 **Performance Optimized** with connection pooling and caching
|
||||
- 📊 **Observability** with Prometheus metrics export
|
||||
@@ -61,7 +61,7 @@ Generate API keys from the Memento web UI: **Settings > MCP**.
|
||||
curl -H "x-api-key: mcp_sk_xxx" http://localhost:3001/health
|
||||
```
|
||||
|
||||
## Available Tools (22)
|
||||
## Available Tools (29)
|
||||
|
||||
### Notes (13)
|
||||
|
||||
@@ -81,7 +81,7 @@ curl -H "x-api-key: mcp_sk_xxx" http://localhost:3001/health
|
||||
| `batch_move_notes` | Move multiple notes at once |
|
||||
| `batch_delete_notes` | Delete multiple notes at once |
|
||||
|
||||
### Notebooks (6)
|
||||
### Notebooks (7)
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
@@ -102,11 +102,28 @@ curl -H "x-api-key: mcp_sk_xxx" http://localhost:3001/health
|
||||
| `update_label` | Update a label |
|
||||
| `delete_label` | Delete a label |
|
||||
|
||||
### Reminders (1)
|
||||
### Reminders (3)
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_due_reminders` | Get due reminders |
|
||||
| `get_upcoming_reminders` | Get reminders due in the next N hours |
|
||||
| `update_reminder` | Set, update, or clear a note reminder |
|
||||
|
||||
### Semantic & AI (3)
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `find_similar_notes` | Find notes semantically similar to a given note |
|
||||
| `get_memory_echo_insights` | List AI-discovered note connections |
|
||||
| `dismiss_memory_echo_insight` | Mark a Memory Echo insight as viewed/dismissed |
|
||||
|
||||
### Filters & Statistics (2)
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_notes_by_label` | List notes by label/tag |
|
||||
| `get_note_statistics` | Get counts and breakdowns for notes, notebooks, labels |
|
||||
|
||||
### Utilities (2)
|
||||
|
||||
|
||||
@@ -197,8 +197,9 @@ export function validateConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Port validation
|
||||
const portValidation = validatePort(config.port);
|
||||
// Port validation (check raw value before clamping)
|
||||
const rawPort = Number.parseInt(env('PORT', '3001'), 10);
|
||||
const portValidation = validatePort(Number.isNaN(rawPort) ? config.port : rawPort);
|
||||
if (!portValidation.valid) {
|
||||
errors.push({ key: 'PORT', message: portValidation.error, critical: true });
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ app.get('/', (req, res) => {
|
||||
sessions: '/sessions',
|
||||
},
|
||||
auth: { enabled: config.requireAuth },
|
||||
tools: 22,
|
||||
tools: 29,
|
||||
uptime: process.uptime(),
|
||||
});
|
||||
});
|
||||
@@ -445,25 +445,23 @@ app.all(
|
||||
}
|
||||
|
||||
// Validate tool input if present
|
||||
if (req.body?.method) {
|
||||
const toolName = req.body.method;
|
||||
if (req.body?.params) {
|
||||
const validation = validateAndSanitize(toolName, req.body.params);
|
||||
if (!validation.success) {
|
||||
log('warn', `Validation failed for ${toolName}:`, validation.errors);
|
||||
return res
|
||||
.status(400)
|
||||
.json(
|
||||
mcpError(McpErrors.INVALID_PARAMS.code, {
|
||||
detail: 'Input validation failed',
|
||||
field: validation.errors[0]?.field,
|
||||
context: { toolName, errors: validation.errors },
|
||||
})
|
||||
);
|
||||
}
|
||||
// Update request with sanitized data
|
||||
req.body.params = validation.data;
|
||||
if (req.body?.method === 'tools/call' && req.body?.params?.name && req.body?.params?.arguments) {
|
||||
const toolName = req.body.params.name;
|
||||
const validation = validateAndSanitize(toolName, req.body.params.arguments);
|
||||
if (!validation.success) {
|
||||
log('warn', `Validation failed for ${toolName}:`, validation.errors);
|
||||
return res
|
||||
.status(400)
|
||||
.json(
|
||||
mcpError(McpErrors.INVALID_PARAMS.code, {
|
||||
detail: 'Input validation failed',
|
||||
field: validation.errors[0]?.field,
|
||||
context: { toolName, errors: validation.errors },
|
||||
})
|
||||
);
|
||||
}
|
||||
// Update request with sanitized data
|
||||
req.body.params.arguments = validation.data;
|
||||
}
|
||||
|
||||
const ctx = { userId: req.userSession?.userId || null };
|
||||
@@ -539,7 +537,7 @@ async function main() {
|
||||
Auth: ${config.requireAuth ? 'ENABLED' : 'DISABLED (dev)'}
|
||||
Timeout: ${config.requestTimeout}ms
|
||||
Database: ${isPostgres ? 'PostgreSQL' : 'SQLite'}
|
||||
Tools: 22
|
||||
Tools: 29
|
||||
Features: ${config.enableMetrics ? 'Metrics' : ''}${config.enableAuditLog ? ', Audit Log' : ''}
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ async function main() {
|
||||
Database: ${isPostgres ? 'PostgreSQL' : 'SQLite'}
|
||||
User: ${config.userId || 'all'}
|
||||
Log Level: ${config.logLevel}
|
||||
Tools: 22
|
||||
Tools: 29
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "memento-mcp-server",
|
||||
"version": "3.2.0",
|
||||
"description": "MCP Server for Memento - AI-powered note-taking app. Enhanced with error handling, metrics, rate limiting, and input validation. Provides 22 tools for notes, notebooks, labels, and reminders.",
|
||||
"description": "MCP Server for Memento - AI-powered note-taking app. Enhanced with error handling, metrics, rate limiting, and input validation. Provides 29 tools for notes, notebooks, labels, reminders, semantic similarity, Memory Echo insights, and statistics.",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Run with: npm test
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
@@ -124,9 +124,8 @@ describe('MCP Server - Input Validation', () => {
|
||||
|
||||
it('should allow safe HTML', () => {
|
||||
const xss = checkXSS({ content: 'Hello <em>world</em>' });
|
||||
// This will be true because we check for any HTML tags
|
||||
// In production, you might want more sophisticated checking
|
||||
expect(xss).toBe(true);
|
||||
// Safe formatting tags are not flagged as XSS
|
||||
expect(xss).toBe(false);
|
||||
});
|
||||
|
||||
it('should sanitize input', () => {
|
||||
@@ -206,11 +205,18 @@ describe('MCP Server - Tool Definitions', () => {
|
||||
'update_label',
|
||||
'delete_label',
|
||||
'get_due_reminders',
|
||||
'get_upcoming_reminders',
|
||||
'update_reminder',
|
||||
'find_similar_notes',
|
||||
'get_memory_echo_insights',
|
||||
'dismiss_memory_echo_insight',
|
||||
'get_notes_by_label',
|
||||
'get_note_statistics',
|
||||
'export_notes',
|
||||
'import_notes',
|
||||
];
|
||||
|
||||
it('should have all expected tools with schemas', () => {
|
||||
it('should have all expected tools with schemas', async () => {
|
||||
const { toolSchemas } = await import('../validation.js');
|
||||
|
||||
for (const toolName of toolNames) {
|
||||
|
||||
@@ -19,7 +19,7 @@ const DEFAULT_NOTES_LIMIT = 100;
|
||||
const MAX_NOTES_LIMIT = 500;
|
||||
|
||||
const NOTE_COLORS = 'default, red, orange, yellow, green, teal, blue, purple, pink, gray';
|
||||
const LABEL_COLORS = 'red, orange, yellow, green, teal, blue, purple, pink, gray';
|
||||
const LABEL_COLORS = ['red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'gray'];
|
||||
|
||||
export function parseNote(dbNote) {
|
||||
if (!dbNote) return null;
|
||||
@@ -463,6 +463,100 @@ const toolDefinitions = [
|
||||
description: 'Get notes with due reminders. Designed for cron/automation.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'get_upcoming_reminders',
|
||||
description: 'Get notes with reminders due in the next N hours (default 24).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hours: { type: 'number', description: 'Number of hours ahead to look', default: 24 },
|
||||
includeDone: { type: 'boolean', description: 'Include already-done reminders', default: false },
|
||||
limit: { type: 'number', description: 'Max results', default: 100 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'update_reminder',
|
||||
description: 'Set, update, or clear a reminder on a note.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Note ID' },
|
||||
reminder: { type: 'string', description: 'ISO 8601 datetime, or null to clear', nullable: true },
|
||||
isReminderDone: { type: 'boolean' },
|
||||
reminderRecurrence: { type: 'string', description: 'daily, weekly, monthly, yearly', nullable: true },
|
||||
reminderLocation: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
|
||||
// ═══ SEMANTIC / AI ═══
|
||||
{
|
||||
name: 'find_similar_notes',
|
||||
description: 'Find notes semantically similar to a given note using its vector embedding. Requires the note to have an embedding.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Source note ID' },
|
||||
limit: { type: 'number', description: 'Max results', default: 10 },
|
||||
threshold: { type: 'number', description: 'Minimum cosine similarity (0-1)', default: 0.7 },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_memory_echo_insights',
|
||||
description: 'List Memory Echo insights: AI-discovered connections between notes.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeViewed: { type: 'boolean', description: 'Include already viewed insights', default: false },
|
||||
includeDismissed: { type: 'boolean', description: 'Include dismissed insights', default: false },
|
||||
limit: { type: 'number', description: 'Max results', default: 20 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dismiss_memory_echo_insight',
|
||||
description: 'Mark a Memory Echo insight as viewed or dismissed.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Insight ID' },
|
||||
viewed: { type: 'boolean', description: 'Mark as viewed', default: true },
|
||||
dismissed: { type: 'boolean', description: 'Mark as dismissed', default: true },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
|
||||
// ═══ FILTERS & STATISTICS ═══
|
||||
{
|
||||
name: 'get_notes_by_label',
|
||||
description: 'List notes that have a specific label/tag.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: { type: 'string', description: 'Label text to match' },
|
||||
notebookId: { type: 'string', description: 'Filter by notebook', nullable: true },
|
||||
includeArchived: { type: 'boolean', default: false },
|
||||
limit: { type: 'number', description: 'Max results', default: 100 },
|
||||
},
|
||||
required: ['label'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_note_statistics',
|
||||
description: 'Get statistics about notes, notebooks, labels, reminders, and trash.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeArchived: { type: 'boolean', description: 'Include archived notes in counts', default: false },
|
||||
includeTrashed: { type: 'boolean', description: 'Include trashed notes in counts', default: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Tool Handlers ──────────────────────────────────────────────────────────
|
||||
@@ -502,7 +596,7 @@ export function registerTools(server, prisma) {
|
||||
title: args.title || null,
|
||||
content: args.content,
|
||||
color: args.color || 'default',
|
||||
type: args.type || 'text',
|
||||
type: args.type || 'richtext',
|
||||
checkItems: args.checkItems ?? null,
|
||||
labels: args.labels ?? null,
|
||||
isPinned: args.isPinned || false,
|
||||
@@ -1003,6 +1097,201 @@ export function registerTools(server, prisma) {
|
||||
return textResult({ count: reminders.length, reminders });
|
||||
}
|
||||
|
||||
case 'get_upcoming_reminders': {
|
||||
const now = new Date();
|
||||
const horizon = new Date(now.getTime() + (args.hours || 24) * 60 * 60 * 1000);
|
||||
const extra = {
|
||||
reminder: { not: null, gte: now, lte: horizon },
|
||||
isArchived: false,
|
||||
};
|
||||
if (!args.includeDone) extra.isReminderDone = false;
|
||||
|
||||
const reminders = await prisma.note.findMany({
|
||||
where: noteWhere(uid, extra),
|
||||
select: { id: true, title: true, content: true, reminder: true, isReminderDone: true, notebookId: true },
|
||||
orderBy: { reminder: 'asc' },
|
||||
take: args.limit || 100,
|
||||
});
|
||||
|
||||
return textResult({ count: reminders.length, horizon, reminders });
|
||||
}
|
||||
|
||||
case 'update_reminder': {
|
||||
const d = { updatedAt: new Date() };
|
||||
if ('reminder' in args) d.reminder = args.reminder ? new Date(args.reminder) : null;
|
||||
if ('isReminderDone' in args) d.isReminderDone = args.isReminderDone;
|
||||
if ('reminderRecurrence' in args) d.reminderRecurrence = args.reminderRecurrence || null;
|
||||
if ('reminderLocation' in args) d.reminderLocation = args.reminderLocation || null;
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id: args.id, userId: uid, trashedAt: null },
|
||||
data: d,
|
||||
});
|
||||
return textResult(parseNote(note));
|
||||
}
|
||||
|
||||
// ═══ SEMANTIC / AI ═══
|
||||
case 'find_similar_notes': {
|
||||
const source = await prisma.note.findUnique({
|
||||
where: { id: args.id, userId: uid, trashedAt: null },
|
||||
select: { id: true, title: true },
|
||||
});
|
||||
if (!source) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
|
||||
|
||||
const sourceEmbedding = await prisma.noteEmbedding.findUnique({
|
||||
where: { noteId: args.id },
|
||||
select: { embedding: true },
|
||||
});
|
||||
if (!sourceEmbedding) {
|
||||
return textResult({
|
||||
noteId: args.id,
|
||||
error: 'Source note has no embedding. Index it first via the Memento app.',
|
||||
results: [],
|
||||
});
|
||||
}
|
||||
|
||||
const embeddingArray = Array.isArray(sourceEmbedding.embedding)
|
||||
? sourceEmbedding.embedding
|
||||
: String(sourceEmbedding.embedding).replace(/^\[|\]$/g, '').split(',').map(Number);
|
||||
const vecStr = `[${embeddingArray.join(',')}]`;
|
||||
const limit = Math.min(args.limit || 10, 50);
|
||||
const threshold = args.threshold ?? 0.7;
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe(
|
||||
`SELECT n.id, n.title, n.content, n.color, n.type, n."isPinned", n."isArchived",
|
||||
n."notebookId", n."createdAt", n."updatedAt",
|
||||
1 - (e."embedding"::vector <=> $1::vector) AS similarity
|
||||
FROM "Note" n
|
||||
INNER JOIN "NoteEmbedding" e ON e."noteId" = n.id
|
||||
WHERE n."trashedAt" IS NULL
|
||||
AND n."isArchived" = false
|
||||
AND n."userId" = $2
|
||||
AND n.id != $3
|
||||
AND 1 - (e."embedding"::vector <=> $1::vector) >= $4
|
||||
ORDER BY e."embedding"::vector <=> $1::vector ASC
|
||||
LIMIT $5`,
|
||||
vecStr,
|
||||
uid,
|
||||
args.id,
|
||||
threshold,
|
||||
limit,
|
||||
);
|
||||
|
||||
return textResult({
|
||||
noteId: args.id,
|
||||
sourceTitle: source.title || 'Untitled',
|
||||
count: rows.length,
|
||||
results: rows.map(r => ({ ...parseNoteLightweight(r), similarity: Number(r.similarity) })),
|
||||
});
|
||||
}
|
||||
|
||||
case 'get_memory_echo_insights': {
|
||||
const where = { userId: uid };
|
||||
if (!args.includeViewed) where.viewed = false;
|
||||
if (!args.includeDismissed) where.dismissed = false;
|
||||
|
||||
const insights = await prisma.memoryEchoInsight.findMany({
|
||||
where,
|
||||
orderBy: { similarityScore: 'desc' },
|
||||
take: Math.min(args.limit || 20, 100),
|
||||
include: {
|
||||
note1: { select: { id: true, title: true } },
|
||||
note2: { select: { id: true, title: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return textResult({
|
||||
count: insights.length,
|
||||
insights: insights.map(i => ({
|
||||
id: i.id,
|
||||
insight: i.insight,
|
||||
similarityScore: i.similarityScore,
|
||||
insightDate: i.insightDate,
|
||||
viewed: i.viewed,
|
||||
dismissed: i.dismissed,
|
||||
note1: i.note1,
|
||||
note2: i.note2,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
case 'dismiss_memory_echo_insight': {
|
||||
const d = {};
|
||||
if ('viewed' in args) d.viewed = args.viewed;
|
||||
if ('dismissed' in args) d.dismissed = args.dismissed;
|
||||
|
||||
const insight = await prisma.memoryEchoInsight.update({
|
||||
where: { id: args.id, userId: uid },
|
||||
data: d,
|
||||
});
|
||||
return textResult({ success: true, id: insight.id, viewed: insight.viewed, dismissed: insight.dismissed });
|
||||
}
|
||||
|
||||
// ═══ FILTERS & STATISTICS ═══
|
||||
case 'get_notes_by_label': {
|
||||
const label = args.label.trim();
|
||||
const extra = {};
|
||||
if (!args.includeArchived) extra.isArchived = false;
|
||||
if (args.notebookId) {
|
||||
extra.notebookId = args.notebookId === 'inbox' ? null : args.notebookId;
|
||||
}
|
||||
|
||||
// Labels are stored as a JSON string like ["tag1","tag2"].
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
...noteWhere(uid, extra),
|
||||
labels: { contains: label },
|
||||
},
|
||||
orderBy: [{ isPinned: 'desc' }, { updatedAt: 'desc' }],
|
||||
take: Math.min(args.limit || 100, MAX_NOTES_LIMIT),
|
||||
});
|
||||
|
||||
return textResult({ label, count: notes.length, notes: notes.map(parseNoteLightweight) });
|
||||
}
|
||||
|
||||
case 'get_note_statistics': {
|
||||
const baseWhere = { userId: uid };
|
||||
const activeWhere = { ...baseWhere, trashedAt: null };
|
||||
|
||||
const [
|
||||
totalNotes,
|
||||
archivedNotes,
|
||||
trashedNotes,
|
||||
notebookCount,
|
||||
labelCount,
|
||||
notesWithReminders,
|
||||
notesWithImages,
|
||||
notesWithCheckItems,
|
||||
notesByType,
|
||||
notesByColor,
|
||||
] = await Promise.all([
|
||||
prisma.note.count({ where: activeWhere }),
|
||||
prisma.note.count({ where: { ...activeWhere, isArchived: true } }),
|
||||
prisma.note.count({ where: { ...baseWhere, trashedAt: { not: null } } }),
|
||||
prisma.notebook.count({ where: { userId: uid } }),
|
||||
prisma.label.count({ where: { notebook: { userId: uid } } }),
|
||||
prisma.note.count({ where: { ...activeWhere, reminder: { not: null } } }),
|
||||
prisma.note.count({ where: { ...activeWhere, images: { not: null } } }),
|
||||
prisma.note.count({ where: { ...activeWhere, checkItems: { not: null } } }),
|
||||
prisma.note.groupBy({ by: ['type'], where: activeWhere, _count: { type: true } }),
|
||||
prisma.note.groupBy({ by: ['color'], where: activeWhere, _count: { color: true } }),
|
||||
]);
|
||||
|
||||
return textResult({
|
||||
totalNotes,
|
||||
archivedNotes,
|
||||
trashedNotes,
|
||||
activeNotes: totalNotes - archivedNotes,
|
||||
notebookCount,
|
||||
labelCount,
|
||||
notesWithReminders,
|
||||
notesWithImages,
|
||||
notesWithCheckItems,
|
||||
byType: notesByType.map(t => ({ type: t.type, count: t._count.type })),
|
||||
byColor: notesByColor.map(c => ({ color: c.color, count: c._count.color })),
|
||||
});
|
||||
}
|
||||
|
||||
// ═══ TRASH ═══
|
||||
case 'trash_note': {
|
||||
const note = await prisma.note.update({
|
||||
|
||||
@@ -179,7 +179,6 @@ export const getNotesSchema = z.object({
|
||||
notebookId: idSchema.optional().nullable(),
|
||||
fullDetails: boolSchema(false),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -225,7 +224,6 @@ export const deleteNoteSchema = z.object({
|
||||
*/
|
||||
export const searchNotesSchema = z.object({
|
||||
query: z.string().min(1).max(500),
|
||||
limit: z.number().int().min(1).max(100).default(50),
|
||||
notebookId: idSchema.optional().nullable(),
|
||||
includeArchived: boolSchema(false),
|
||||
});
|
||||
@@ -243,7 +241,6 @@ export const moveNoteSchema = z.object({
|
||||
*/
|
||||
export const togglePinSchema = z.object({
|
||||
id: idSchema,
|
||||
pinned: z.boolean().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -251,14 +248,13 @@ export const togglePinSchema = z.object({
|
||||
*/
|
||||
export const toggleArchiveSchema = z.object({
|
||||
id: idSchema,
|
||||
archived: z.boolean().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* batch_move_notes input schema
|
||||
*/
|
||||
export const batchMoveNotesSchema = z.object({
|
||||
noteIds: z.array(idSchema).min(1).max(100),
|
||||
ids: z.array(idSchema).min(1).max(100),
|
||||
notebookId: idSchema.optional().nullable(),
|
||||
});
|
||||
|
||||
@@ -266,7 +262,7 @@ export const batchMoveNotesSchema = z.object({
|
||||
* batch_delete_notes input schema
|
||||
*/
|
||||
export const batchDeleteNotesSchema = z.object({
|
||||
noteIds: z.array(idSchema).min(1).max(100),
|
||||
ids: z.array(idSchema).min(1).max(100),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -274,18 +270,16 @@ export const batchDeleteNotesSchema = z.object({
|
||||
*/
|
||||
export const createNotebookSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
color: colorSchema.default('default'),
|
||||
color: z.string().max(50).default('#3B82F6'),
|
||||
icon: z.string().max(50).optional().nullable(),
|
||||
parentId: idSchema.optional().nullable(),
|
||||
order: z.number().int().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* get_notebooks input schema
|
||||
*/
|
||||
export const getNotebooksSchema = z.object({
|
||||
includeHierarchy: boolSchema(false),
|
||||
includeTrashed: boolSchema(false),
|
||||
});
|
||||
export const getNotebooksSchema = z.object({});
|
||||
|
||||
/**
|
||||
* get_notebook input schema
|
||||
@@ -300,8 +294,9 @@ export const getNotebookSchema = z.object({
|
||||
export const updateNotebookSchema = z.object({
|
||||
id: idSchema,
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
color: colorSchema.optional(),
|
||||
color: z.string().max(50).optional().nullable(),
|
||||
icon: z.string().max(50).optional().nullable(),
|
||||
order: z.number().int().optional(),
|
||||
parentId: idSchema.optional().nullable(),
|
||||
});
|
||||
|
||||
@@ -332,14 +327,15 @@ export const getNotebookHierarchySchema = z.object({
|
||||
*/
|
||||
export const createLabelSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
color: colorSchema.default('default'),
|
||||
color: z.string().max(50).optional().nullable(),
|
||||
notebookId: idSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* get_labels input schema
|
||||
*/
|
||||
export const getLabelsSchema = z.object({
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
notebookId: idSchema.optional().nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -361,37 +357,90 @@ export const deleteLabelSchema = z.object({
|
||||
/**
|
||||
* get_due_reminders input schema
|
||||
*/
|
||||
export const getDueRemindersSchema = z.object({
|
||||
before: isoDateSchema.optional().nullable(),
|
||||
after: isoDateSchema.optional().nullable(),
|
||||
includeDone: boolSchema(false),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
});
|
||||
export const getDueRemindersSchema = z.object({});
|
||||
|
||||
/**
|
||||
* export_notes input schema
|
||||
*/
|
||||
export const exportNotesSchema = z.object({
|
||||
notebookId: idSchema.optional().nullable(),
|
||||
includeArchived: boolSchema(false),
|
||||
format: z.enum(['json', 'markdown']).default('json'),
|
||||
});
|
||||
export const exportNotesSchema = z.object({});
|
||||
|
||||
/**
|
||||
* import_notes input schema
|
||||
*/
|
||||
export const importNotesSchema = z.object({
|
||||
notes: z.array(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
content: z.string(),
|
||||
color: colorSchema.optional(),
|
||||
labels: labelsSchema,
|
||||
notebookId: idSchema.optional().nullable(),
|
||||
})
|
||||
).min(1).max(100),
|
||||
data: z.object({
|
||||
version: z.string().optional(),
|
||||
data: z.object({
|
||||
notes: z.array(z.any()).optional(),
|
||||
labels: z.array(z.any()).optional(),
|
||||
notebooks: z.array(z.any()).optional(),
|
||||
}).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* find_similar_notes input schema
|
||||
*/
|
||||
export const findSimilarNotesSchema = z.object({
|
||||
id: idSchema,
|
||||
limit: z.number().int().min(1).max(50).default(10),
|
||||
threshold: z.number().min(0).max(1).default(0.7),
|
||||
});
|
||||
|
||||
/**
|
||||
* get_memory_echo_insights input schema
|
||||
*/
|
||||
export const getMemoryEchoInsightsSchema = z.object({
|
||||
includeViewed: boolSchema(false),
|
||||
includeDismissed: boolSchema(false),
|
||||
limit: z.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
/**
|
||||
* dismiss_memory_echo_insight input schema
|
||||
*/
|
||||
export const dismissMemoryEchoInsightSchema = z.object({
|
||||
id: idSchema,
|
||||
viewed: boolSchema(true),
|
||||
dismissed: boolSchema(true),
|
||||
});
|
||||
|
||||
/**
|
||||
* get_notes_by_label input schema
|
||||
*/
|
||||
export const getNotesByLabelSchema = z.object({
|
||||
label: z.string().min(1).max(100),
|
||||
notebookId: idSchema.optional().nullable(),
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
includeArchived: boolSchema(false),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
});
|
||||
|
||||
/**
|
||||
* get_note_statistics input schema
|
||||
*/
|
||||
export const getNoteStatisticsSchema = z.object({
|
||||
includeArchived: boolSchema(false),
|
||||
includeTrashed: boolSchema(false),
|
||||
});
|
||||
|
||||
/**
|
||||
* get_upcoming_reminders input schema
|
||||
*/
|
||||
export const getUpcomingRemindersSchema = z.object({
|
||||
hours: z.number().int().min(1).max(168).default(24),
|
||||
includeDone: boolSchema(false),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
});
|
||||
|
||||
/**
|
||||
* update_reminder input schema
|
||||
*/
|
||||
export const updateReminderSchema = z.object({
|
||||
id: idSchema,
|
||||
reminder: isoDateSchema,
|
||||
isReminderDone: z.boolean().optional(),
|
||||
reminderRecurrence: recurrenceSchema,
|
||||
reminderLocation: z.string().max(500).optional().nullable(),
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@@ -427,6 +476,13 @@ export const toolSchemas = {
|
||||
get_due_reminders: getDueRemindersSchema,
|
||||
export_notes: exportNotesSchema,
|
||||
import_notes: importNotesSchema,
|
||||
find_similar_notes: findSimilarNotesSchema,
|
||||
get_memory_echo_insights: getMemoryEchoInsightsSchema,
|
||||
dismiss_memory_echo_insight: dismissMemoryEchoInsightSchema,
|
||||
get_notes_by_label: getNotesByLabelSchema,
|
||||
get_note_statistics: getNoteStatisticsSchema,
|
||||
get_upcoming_reminders: getUpcomingRemindersSchema,
|
||||
update_reminder: updateReminderSchema,
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@@ -457,7 +513,7 @@ export function validateToolInput(toolName, input) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
errors: error.errors.map((e) => ({
|
||||
errors: error.issues.map((e) => ({
|
||||
field: e.path.join('.'),
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
|
||||
7
mcp-server/vitest.config.js
Normal file
7
mcp-server/vitest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['test/test.js', 'test/**/*.test.js'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user