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'),
|
JARVISJR_BACKEND_URL: z.string().default('http://localhost:4012'),
|
||||||
CHRONOMIND_BACKEND_URL: z.string().default('http://localhost:4011'),
|
CHRONOMIND_BACKEND_URL: z.string().default('http://localhost:4011'),
|
||||||
NOMGAP_BACKEND_URL: z.string().default('http://localhost:4013'),
|
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'),
|
PEAKPULSE_BACKEND_URL: z.string().default('http://localhost:4010'),
|
||||||
BYTELYST_NOTES_BACKEND_URL: z.string().default('http://localhost:4016'),
|
BYTELYST_NOTES_BACKEND_URL: z.string().default('http://localhost:4016'),
|
||||||
/** Max items returned per tool call query (hard cap) */
|
/** 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)
|
* jarvis.* — agents, sessions, memory (JarvisJr coaching platform)
|
||||||
* chronomind.* — timers, routines, syncStatus
|
* chronomind.* — timers, routines, syncStatus
|
||||||
* nomgap.* — fasting sessions, push triggers
|
* nomgap.* — fasting sessions, push triggers
|
||||||
|
* notelett.* — notes, workspaces, tasks, artifacts, summarize
|
||||||
* peakpulse.* — adventure sessions, GPS routes, stats
|
* peakpulse.* — adventure sessions, GPS routes, stats
|
||||||
* notes.* — notes, search, draft creation
|
* notes.* — notes, search, draft creation
|
||||||
* tracker.* — items, votes, comments, public roadmap
|
* tracker.* — items, votes, comments, public roadmap
|
||||||
@ -67,6 +68,7 @@ import './modules/lysnrai/lysnrai-tools.js';
|
|||||||
import './modules/jarvis/jarvis-tools.js';
|
import './modules/jarvis/jarvis-tools.js';
|
||||||
import './modules/chronomind/chronomind-tools.js';
|
import './modules/chronomind/chronomind-tools.js';
|
||||||
import './modules/nomgap/nomgap-tools.js';
|
import './modules/nomgap/nomgap-tools.js';
|
||||||
|
import './modules/notelett/notelett-tools.js';
|
||||||
import './modules/peakpulse/peakpulse-tools.js';
|
import './modules/peakpulse/peakpulse-tools.js';
|
||||||
import './modules/notes/notes-tools.js';
|
import './modules/notes/notes-tools.js';
|
||||||
import './modules/tracker/tracker-tools.js';
|
import './modules/tracker/tracker-tools.js';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user