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:
parent
bb85bf6176
commit
6997dff8d9
@ -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) */
|
||||
|
||||
152
services/mcp-server/src/lib/notelett-client.ts
Normal file
152
services/mcp-server/src/lib/notelett-client.ts
Normal 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);
|
||||
}
|
||||
183
services/mcp-server/src/modules/notelett/notelett-tools.ts
Normal file
183
services/mcp-server/src/modules/notelett/notelett-tools.ts
Normal 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 });
|
||||
},
|
||||
});
|
||||
@ -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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user