feat(mcp-server): register NoteLett tools (notes, workspaces, tasks, artifacts, summarize)

Adds notelett-client.ts HTTP wrapper, notelett-tools.ts with 10 MCP tool registrations,
and NOTELETT_BACKEND_URL config entry.

Made-with: Cursor
This commit is contained in:
Saravana Achu Mac 2026-03-29 20:56:52 -07:00
parent bb85bf6176
commit 6997dff8d9
4 changed files with 338 additions and 0 deletions

View File

@ -15,6 +15,7 @@ const envSchema = z.object({
JARVISJR_BACKEND_URL: z.string().default('http://localhost:4012'),
CHRONOMIND_BACKEND_URL: z.string().default('http://localhost:4011'),
NOMGAP_BACKEND_URL: z.string().default('http://localhost:4013'),
NOTELETT_BACKEND_URL: z.string().default('http://localhost:4016'),
PEAKPULSE_BACKEND_URL: z.string().default('http://localhost:4010'),
BYTELYST_NOTES_BACKEND_URL: z.string().default('http://localhost:4016'),
/** Max items returned per tool call query (hard cap) */

View File

@ -0,0 +1,152 @@
/**
* NoteLett backend client typed HTTP wrappers for notelett-backend (port 4016).
*
* Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service).
*/
import { config } from './config.js';
export interface NoteLettClientOptions {
token?: string;
requestId?: string;
}
async function noteFetch<T>(
path: string,
init: RequestInit,
opts: NoteLettClientOptions,
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-product-id': 'notelett',
...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}),
...(opts.requestId ? { 'x-request-id': opts.requestId } : {}),
};
const res = await fetch(`${config.NOTELETT_BACKEND_URL}/api${path}`, {
...init,
headers: { ...((init.headers as Record<string, string>) ?? {}), ...headers },
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`notelett-backend ${init.method ?? 'GET'} ${path}${res.status}: ${body}`);
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
}
// ── Notes ────────────────────────────────────────────────────────────────
export interface NoteDoc {
id: string;
workspaceId: string;
title: string;
content: string;
status: string;
createdAt: string;
updatedAt: string;
}
export function noteLettNotesList(
params: { workspaceId: string; limit?: number; offset?: number; search?: string },
opts: NoteLettClientOptions,
): Promise<{ items: NoteDoc[]; total: number }> {
const qs = new URLSearchParams();
qs.set('workspaceId', params.workspaceId);
if (params.limit !== undefined) qs.set('limit', String(params.limit));
if (params.offset !== undefined) qs.set('offset', String(params.offset));
if (params.search) qs.set('search', params.search);
return noteFetch(`/notes?${qs}`, { method: 'GET' }, opts);
}
export function noteLettNoteGet(
noteId: string,
opts: NoteLettClientOptions,
): Promise<NoteDoc> {
return noteFetch(`/notes/${noteId}`, { method: 'GET' }, opts);
}
export function noteLettNoteCreate(
input: { workspaceId: string; title: string; content?: string },
opts: NoteLettClientOptions,
): Promise<NoteDoc> {
return noteFetch('/notes', { method: 'POST', body: JSON.stringify(input) }, opts);
}
export function noteLettNoteUpdate(
noteId: string,
input: { title?: string; content?: string },
opts: NoteLettClientOptions,
): Promise<NoteDoc> {
return noteFetch(`/notes/${noteId}`, { method: 'PATCH', body: JSON.stringify(input) }, opts);
}
export function noteLettNoteDelete(
noteId: string,
opts: NoteLettClientOptions,
): Promise<void> {
return noteFetch(`/notes/${noteId}`, { method: 'DELETE' }, opts);
}
export function noteLettNoteSummarize(
noteId: string,
opts: NoteLettClientOptions,
): Promise<{ summary: string }> {
return noteFetch(`/notes/${noteId}/summarize`, { method: 'POST' }, opts);
}
// ── Workspaces ───────────────────────────────────────────────────────────
export interface WorkspaceDoc {
id: string;
name: string;
description?: string;
createdAt: string;
}
export function noteLettWorkspacesList(
opts: NoteLettClientOptions,
): Promise<{ items: WorkspaceDoc[]; total: number }> {
return noteFetch('/workspaces', { method: 'GET' }, opts);
}
export function noteLettWorkspaceCreate(
input: { name: string; description?: string },
opts: NoteLettClientOptions,
): Promise<WorkspaceDoc> {
return noteFetch('/workspaces', { method: 'POST', body: JSON.stringify(input) }, opts);
}
// ── Note tasks ───────────────────────────────────────────────────────────
export interface NoteTaskDoc {
id: string;
noteId: string;
title: string;
status: string;
createdAt: string;
}
export function noteLettNoteTasksList(
noteId: string,
opts: NoteLettClientOptions,
): Promise<{ items: NoteTaskDoc[]; total: number }> {
return noteFetch(`/note-tasks?noteId=${noteId}`, { method: 'GET' }, opts);
}
// ── Note artifacts ───────────────────────────────────────────────────────
export interface NoteArtifactDoc {
id: string;
noteId: string;
type: string;
content: string;
createdAt: string;
}
export function noteLettNoteArtifactsList(
noteId: string,
opts: NoteLettClientOptions,
): Promise<{ items: NoteArtifactDoc[]; total: number }> {
return noteFetch(`/note-artifacts?noteId=${noteId}`, { method: 'GET' }, opts);
}

View File

@ -0,0 +1,183 @@
/**
* NoteLett MCP tools notelett.notes.*, notelett.workspaces.*
*
* Backed by: notelett-backend (port 4016).
* All tools require admin role.
*/
import { z } from 'zod';
import { registerTool } from '../tools/registry.js';
import { config } from '../../lib/config.js';
import {
noteLettNotesList,
noteLettNoteGet,
noteLettNoteCreate,
noteLettNoteUpdate,
noteLettNoteDelete,
noteLettNoteSummarize,
noteLettWorkspacesList,
noteLettWorkspaceCreate,
noteLettNoteTasksList,
noteLettNoteArtifactsList,
} from '../../lib/notelett-client.js';
import type { McpToolRequest } from '../tools/types.js';
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
// ── notelett.notes.list ─────────────────────────────────────────────────
registerTool({
name: 'notelett.notes.list',
description:
'List notes in a workspace. Supports text search, pagination. Returns id, title, status, createdAt. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
workspaceId: z.string().min(1).describe('Workspace ID'),
search: z.string().optional().describe('Full-text search query'),
limit: z.coerce.number().min(1).max(config.QUERY_MAX_LIMIT).default(config.QUERY_DEFAULT_LIMIT),
offset: z.coerce.number().min(0).default(0),
}),
async execute(args, req) {
return noteLettNotesList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.notes.get ──────────────────────────────────────────────────
registerTool({
name: 'notelett.notes.get',
description:
'Get a single note by ID including full content, status, and timestamps. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
noteId: z.string().min(1).describe('Note ID'),
}),
async execute(args, req) {
return noteLettNoteGet(args.noteId, { token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.notes.create ───────────────────────────────────────────────
registerTool({
name: 'notelett.notes.create',
description:
'Create a new note in a workspace. Returns the new note document. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
workspaceId: z.string().min(1).describe('Workspace ID'),
title: z.string().min(1).describe('Note title'),
content: z.string().optional().describe('Initial note content (plain text or HTML)'),
}),
async execute(args, req) {
return noteLettNoteCreate(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.notes.update ───────────────────────────────────────────────
registerTool({
name: 'notelett.notes.update',
description:
'Update an existing note (title and/or content). Returns the updated document. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
noteId: z.string().min(1).describe('Note ID'),
title: z.string().optional().describe('New title'),
content: z.string().optional().describe('New content'),
}),
async execute(args, req) {
const { noteId, ...updates } = args;
return noteLettNoteUpdate(noteId, updates, { token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.notes.delete ───────────────────────────────────────────────
registerTool({
name: 'notelett.notes.delete',
description:
'Soft-delete a note (sets status to archived). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
noteId: z.string().min(1).describe('Note ID'),
}),
async execute(args, req) {
await noteLettNoteDelete(args.noteId, { token: tokenOf(req), requestId: req.id });
return { success: true };
},
});
// ── notelett.notes.summarize ────────────────────────────────────────────
registerTool({
name: 'notelett.notes.summarize',
description:
'Generate an AI summary of a note using the extraction service. Returns { summary }. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
noteId: z.string().min(1).describe('Note ID'),
}),
async execute(args, req) {
return noteLettNoteSummarize(args.noteId, { token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.workspaces.list ────────────────────────────────────────────
registerTool({
name: 'notelett.workspaces.list',
description:
'List all workspaces accessible to the authenticated user. Returns id, name, description, createdAt. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({}),
async execute(_args, req) {
return noteLettWorkspacesList({ token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.workspaces.create ──────────────────────────────────────────
registerTool({
name: 'notelett.workspaces.create',
description:
'Create a new workspace. Returns the new workspace document. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
name: z.string().min(1).describe('Workspace name'),
description: z.string().optional().describe('Workspace description'),
}),
async execute(args, req) {
return noteLettWorkspaceCreate(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.notes.tasks ────────────────────────────────────────────────
registerTool({
name: 'notelett.notes.tasks',
description:
'List tasks extracted from a note. Returns id, title, status, createdAt. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
noteId: z.string().min(1).describe('Note ID'),
}),
async execute(args, req) {
return noteLettNoteTasksList(args.noteId, { token: tokenOf(req), requestId: req.id });
},
});
// ── notelett.notes.artifacts ────────────────────────────────────────────
registerTool({
name: 'notelett.notes.artifacts',
description:
'List artifacts associated with a note (file uploads, links, etc). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
noteId: z.string().min(1).describe('Note ID'),
}),
async execute(args, req) {
return noteLettNoteArtifactsList(args.noteId, { token: tokenOf(req), requestId: req.id });
},
});

View File

@ -11,6 +11,7 @@
* jarvis.* agents, sessions, memory (JarvisJr coaching platform)
* chronomind.* timers, routines, syncStatus
* nomgap.* fasting sessions, push triggers
* notelett.* notes, workspaces, tasks, artifacts, summarize
* peakpulse.* adventure sessions, GPS routes, stats
* notes.* notes, search, draft creation
* tracker.* items, votes, comments, public roadmap
@ -67,6 +68,7 @@ import './modules/lysnrai/lysnrai-tools.js';
import './modules/jarvis/jarvis-tools.js';
import './modules/chronomind/chronomind-tools.js';
import './modules/nomgap/nomgap-tools.js';
import './modules/notelett/notelett-tools.js';
import './modules/peakpulse/peakpulse-tools.js';
import './modules/notes/notes-tools.js';
import './modules/tracker/tracker-tools.js';