feat(mcp-server): add notes tool integration

This commit is contained in:
saravanakumardb1 2026-03-10 09:39:07 -07:00
parent 925e9b6b0f
commit ec3dd4bd66
4 changed files with 330 additions and 1 deletions

View File

@ -16,6 +16,7 @@ const envSchema = z.object({
CHRONOMIND_BACKEND_URL: z.string().default('http://localhost:4011'),
NOMGAP_BACKEND_URL: z.string().default('http://localhost:4013'),
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) */
QUERY_MAX_LIMIT: z.coerce.number().default(100),
/** Default items per tool call query */

View File

@ -0,0 +1,144 @@
import { config } from './config.js';
export interface NotesClientOptions {
token?: string;
requestId?: string;
}
async function notesFetch<T>(
path: string,
init: RequestInit,
opts: NotesClientOptions
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}),
...(opts.requestId ? { 'x-request-id': opts.requestId } : {}),
};
const res = await fetch(`${config.BYTELYST_NOTES_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(
`bytelyst-notes-backend ${init.method ?? 'GET'} ${path}${res.status}: ${body}`
);
}
return res.json() as Promise<T>;
}
export interface NoteDoc {
id: string;
productId: string;
workspaceId: string;
userId: string;
title: string;
body: string;
status: 'draft' | 'active' | 'archived';
tags: string[];
links: string[];
sourceType?: string;
sourceUri?: string;
createdAt: string;
updatedAt: string;
createdBy: string;
updatedBy: string;
agentId?: string;
}
export interface NoteListResponse {
items: NoteDoc[];
total: number;
limit: number;
offset: number;
}
export function notesList(
params: {
workspaceId: string;
status?: 'draft' | 'active' | 'archived';
tag?: string;
limit?: number;
offset?: number;
},
opts: NotesClientOptions
): Promise<NoteListResponse> {
const qs = new URLSearchParams();
qs.set('workspaceId', params.workspaceId);
if (params.status) qs.set('status', params.status);
if (params.tag) qs.set('tag', params.tag);
if (params.limit !== undefined) qs.set('limit', String(params.limit));
if (params.offset !== undefined) qs.set('offset', String(params.offset));
return notesFetch(`/notes?${qs.toString()}`, { method: 'GET' }, opts);
}
export function notesSearch(
params: {
workspaceId: string;
query: string;
status?: 'draft' | 'active' | 'archived';
tag?: string;
limit?: number;
offset?: number;
},
opts: NotesClientOptions
): Promise<NoteListResponse & { query: string | null }> {
const qs = new URLSearchParams();
qs.set('workspaceId', params.workspaceId);
qs.set('search', params.query);
if (params.status) qs.set('status', params.status);
if (params.tag) qs.set('tag', params.tag);
if (params.limit !== undefined) qs.set('limit', String(params.limit));
if (params.offset !== undefined) qs.set('offset', String(params.offset));
return notesFetch(`/notes/search?${qs.toString()}`, { method: 'GET' }, opts);
}
export function notesGet(
noteId: string,
workspaceId: string,
opts: NotesClientOptions
): Promise<NoteDoc> {
const qs = new URLSearchParams({ workspaceId });
return notesFetch(`/notes/${noteId}?${qs.toString()}`, { method: 'GET' }, opts);
}
export function notesCreateDraft(
input: {
id: string;
workspaceId: string;
title: string;
body: string;
tags?: string[];
links?: string[];
sourceType?: string;
sourceUri?: string;
agentId?: string;
},
opts: NotesClientOptions
): Promise<NoteDoc> {
return notesFetch('/notes', { method: 'POST', body: JSON.stringify(input) }, opts);
}
export function notesCreateAgentAction(
input: {
id: string;
workspaceId: string;
noteId: string;
actorId: string;
actorType: 'agent' | 'human';
toolName: string;
actionType: 'create' | 'update' | 'summarize' | 'extract_tasks' | 'attach_citation';
state?: 'draft' | 'proposed' | 'approved' | 'rejected' | 'applied';
reason?: string;
beforeSummary?: string;
afterSummary?: string;
idempotencyKey?: string;
correlationId?: string;
workflowId?: string;
},
opts: NotesClientOptions
): Promise<{ id: string; noteId: string; state: string; toolName: string }> {
return notesFetch('/note-agent-actions', { method: 'POST', body: JSON.stringify(input) }, opts);
}

View File

@ -0,0 +1,182 @@
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import { registerTool } from '../tools/registry.js';
import { config } from '../../lib/config.js';
import {
notesCreateAgentAction,
notesCreateDraft,
notesGet,
notesList,
notesSearch,
} from '../../lib/notes-client.js';
import type { McpToolRequest } from '../tools/types.js';
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
function requireProductScope(req: McpToolRequest): void {
const requestProductId = req.jwtPayload?.productId;
if (requestProductId && requestProductId !== 'bytelyst-notes') {
throw new Error("Product scope mismatch: expected 'bytelyst-notes'");
}
}
function requireUserId(req: McpToolRequest): string {
requireProductScope(req);
if (!req.jwtPayload?.sub) {
throw new Error('Authentication required');
}
return req.jwtPayload.sub;
}
registerTool({
name: 'notes.notes.list',
description: 'List notes in a workspace with optional status and tag filters.',
requiredRole: 'viewer',
inputSchema: z.object({
workspaceId: z.string().min(1).max(128),
status: z.enum(['draft', 'active', 'archived']).optional(),
tag: z.string().min(1).max(64).optional(),
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) {
requireUserId(req);
return notesList(args, { token: tokenOf(req), requestId: req.id });
},
});
registerTool({
name: 'notes.notes.get',
description: 'Get a single note by note ID and workspace scope.',
requiredRole: 'viewer',
inputSchema: z.object({
noteId: z.string().min(1).max(128),
workspaceId: z.string().min(1).max(128),
}),
async execute(args, req) {
requireUserId(req);
return notesGet(args.noteId, args.workspaceId, { token: tokenOf(req), requestId: req.id });
},
});
registerTool({
name: 'notes.notes.search',
description: 'Search notes in a workspace using lexical query plus optional filters.',
requiredRole: 'viewer',
inputSchema: z.object({
workspaceId: z.string().min(1).max(128),
query: z.string().min(1).max(200),
status: z.enum(['draft', 'active', 'archived']).optional(),
tag: z.string().min(1).max(64).optional(),
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) {
requireUserId(req);
return notesSearch(args, { token: tokenOf(req), requestId: req.id });
},
});
registerTool({
name: 'notes.notes.create_draft',
description:
'Create a note draft in a workspace. Supports dry-run, idempotency, and correlation metadata.',
requiredRole: 'admin',
inputSchema: z.object({
workspaceId: z.string().min(1).max(128),
title: z.string().min(1).max(500),
body: z.string().min(1).max(50000),
tags: z.array(z.string().min(1).max(64)).default([]),
links: z.array(z.string().min(1).max(512)).default([]),
sourceType: z.string().max(128).optional(),
sourceUri: z.string().max(2000).optional(),
agentId: z.string().max(128).optional(),
dryRun: z.boolean().default(false),
idempotencyKey: z.string().max(255).optional(),
correlationId: z.string().max(255).optional(),
}),
async execute(args, req) {
const userId = requireUserId(req);
const noteId = randomUUID();
const correlationId = args.correlationId ?? req.id;
if (args.dryRun) {
const now = new Date().toISOString();
return {
dryRun: true,
state: 'proposed',
note: {
id: noteId,
workspaceId: args.workspaceId,
title: args.title,
body: args.body,
status: 'draft',
tags: args.tags,
links: args.links,
sourceType: args.sourceType,
sourceUri: args.sourceUri,
agentId: args.agentId,
createdAt: now,
updatedAt: now,
},
idempotencyKey: args.idempotencyKey,
correlationId,
};
}
const note = await notesCreateDraft(
{
id: noteId,
workspaceId: args.workspaceId,
title: args.title,
body: args.body,
tags: args.tags,
links: args.links,
sourceType: args.sourceType,
sourceUri: args.sourceUri,
agentId: args.agentId,
},
{ token: tokenOf(req), requestId: req.id }
);
await notesCreateAgentAction(
{
id: randomUUID(),
workspaceId: args.workspaceId,
noteId: note.id,
actorId: args.agentId ?? userId,
actorType: args.agentId ? 'agent' : 'human',
toolName: 'notes.notes.create_draft',
actionType: 'create',
state: 'proposed',
reason: 'Created via shared MCP create_draft tool',
afterSummary: `${note.title}\n\n${note.body}`.slice(0, 4000),
idempotencyKey: args.idempotencyKey,
correlationId,
workflowId: req.id,
},
{ token: tokenOf(req), requestId: req.id }
);
return {
dryRun: false,
state: 'draft',
note: {
id: note.id,
workspaceId: note.workspaceId,
title: note.title,
body: note.body,
status: note.status,
tags: note.tags,
links: note.links,
sourceType: note.sourceType,
sourceUri: note.sourceUri,
agentId: note.agentId,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
},
idempotencyKey: args.idempotencyKey,
correlationId,
};
},
});

View File

@ -12,6 +12,7 @@
* chronomind.* timers, routines, syncStatus
* nomgap.* fasting sessions, push triggers
* peakpulse.* adventure sessions, GPS routes, stats
* notes.* notes, search, draft creation
* tracker.* items, votes, comments, public roadmap
* flags.* feature flag CRUD + kill switch
* jobs.* background job list, trigger, run history
@ -67,6 +68,7 @@ import './modules/jarvis/jarvis-tools.js';
import './modules/chronomind/chronomind-tools.js';
import './modules/nomgap/nomgap-tools.js';
import './modules/peakpulse/peakpulse-tools.js';
import './modules/notes/notes-tools.js';
import './modules/tracker/tracker-tools.js';
import './modules/platform/ops-tools.js';
import './modules/platform/webhooks-tools.js';
@ -80,7 +82,7 @@ const app = await createServiceApp({
name: 'mcp-server',
version: '0.1.0',
description:
'ByteLyst MCP Server — platform.*, extraction.*, support.*, mindlyst.*, lysnrai.*, jarvis.*, chronomind.*, nomgap.*, peakpulse.*, tracker.*, flags.*, jobs.*, maintenance.*, settings.*, webhooks.*',
'ByteLyst MCP Server — platform.*, extraction.*, support.*, mindlyst.*, lysnrai.*, jarvis.*, chronomind.*, nomgap.*, peakpulse.*, notes.*, tracker.*, flags.*, jobs.*, maintenance.*, settings.*, webhooks.*',
corsOrigin: config.CORS_ORIGIN,
logLevel: config.LOG_LEVEL,
});