diff --git a/services/mcp-server/src/lib/config.ts b/services/mcp-server/src/lib/config.ts index a00db066..db49ff55 100644 --- a/services/mcp-server/src/lib/config.ts +++ b/services/mcp-server/src/lib/config.ts @@ -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) */ diff --git a/services/mcp-server/src/lib/notelett-client.ts b/services/mcp-server/src/lib/notelett-client.ts new file mode 100644 index 00000000..94b27f96 --- /dev/null +++ b/services/mcp-server/src/lib/notelett-client.ts @@ -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( + path: string, + init: RequestInit, + opts: NoteLettClientOptions, +): Promise { + const headers: Record = { + '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) ?? {}), ...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; +} + +// ── 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 { + return noteFetch(`/notes/${noteId}`, { method: 'GET' }, opts); +} + +export function noteLettNoteCreate( + input: { workspaceId: string; title: string; content?: string }, + opts: NoteLettClientOptions, +): Promise { + return noteFetch('/notes', { method: 'POST', body: JSON.stringify(input) }, opts); +} + +export function noteLettNoteUpdate( + noteId: string, + input: { title?: string; content?: string }, + opts: NoteLettClientOptions, +): Promise { + return noteFetch(`/notes/${noteId}`, { method: 'PATCH', body: JSON.stringify(input) }, opts); +} + +export function noteLettNoteDelete( + noteId: string, + opts: NoteLettClientOptions, +): Promise { + 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 { + 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); +} diff --git a/services/mcp-server/src/modules/notelett/notelett-tools.ts b/services/mcp-server/src/modules/notelett/notelett-tools.ts new file mode 100644 index 00000000..8ce9b685 --- /dev/null +++ b/services/mcp-server/src/modules/notelett/notelett-tools.ts @@ -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 }); + }, +}); diff --git a/services/mcp-server/src/server.ts b/services/mcp-server/src/server.ts index 55b01561..5a3ff9db 100644 --- a/services/mcp-server/src/server.ts +++ b/services/mcp-server/src/server.ts @@ -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';