/** * LysnrAI MCP tools — lysnrai.transcripts.*, lysnrai.stt.*, lysnrai.sessions.*, * lysnrai.orgs.*, lysnrai.apiTokens.* * * Backed by: lysnrai-backend (port 4015). * All tools require admin role. */ import { z } from 'zod'; import { registerTool } from '../tools/registry.js'; import { config } from '../../lib/config.js'; import { lysnraiTranscriptsList, lysnraiTranscriptGet, lysnraiTranscriptRunExtraction, lysnraiSttGetBackendStatus, lysnraiSessionGet, lysnraiSessionsList, lysnraiOrgsList, lysnraiOrgGet, lysnraiApiTokensList, lysnraiApiTokenCreate, lysnraiApiTokenRevoke, lysnraiSessionsStats, lysnraiTranscriptsExportBatch, } from '../../lib/lysnrai-client.js'; import type { McpToolRequest } from '../tools/types.js'; const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7); // ── lysnrai.transcripts.get ─────────────────────────────────────────────── registerTool({ name: 'lysnrai.transcripts.get', description: 'Get a single transcript by ID, including extractions[] and extractionMetadata if extraction has run. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ transcriptId: z.string().min(1).describe('Transcript ID'), }), async execute(args, req) { return lysnraiTranscriptGet(args.transcriptId, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.transcripts.list ────────────────────────────────────────────── registerTool({ name: 'lysnrai.transcripts.list', description: 'List dictation transcripts for the authenticated user. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ 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 lysnraiTranscriptsList(args, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.transcripts.runExtraction ──────────────────────────────────── registerTool({ name: 'lysnrai.transcripts.runExtraction', description: 'Run extraction-service enrichment on a transcript — populates extractions[] and extractedAt. Best-effort: returns partial if extraction-service is unavailable. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ transcriptId: z.string().min(1).describe('Transcript ID (trx_... prefix)'), }), async execute(args, req) { return lysnraiTranscriptRunExtraction(args.transcriptId, { token: tokenOf(req), requestId: req.id, }); }, }); // ── lysnrai.stt.getBackendStatus ────────────────────────────────────────── registerTool({ name: 'lysnrai.stt.getBackendStatus', description: 'Report which speech-to-text backend is active: azure (Azure Speech SDK) or whisper (local Whisper model). Useful for ops health checks. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({}), async execute(_args, req) { return lysnraiSttGetBackendStatus({ token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.sessions.get ────────────────────────────────────────────────── registerTool({ name: 'lysnrai.sessions.get', description: 'Get a single dictation session by ID including its entries[], composedText, compositionHistory, and status. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ sessionId: z.string().min(1).describe('Session ID (ses_… prefix)'), }), async execute(args, req) { return lysnraiSessionGet(args.sessionId, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.sessions.list ───────────────────────────────────────────────── registerTool({ name: 'lysnrai.sessions.list', description: 'List dictation sessions for the authenticated user. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ status: z .enum(['active', 'composed', 'archived']) .optional() .describe('Filter by session status'), 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 lysnraiSessionsList(args, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.orgs.list ───────────────────────────────────────────────────── registerTool({ name: 'lysnrai.orgs.list', description: 'List organisations for the authenticated user. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ 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 lysnraiOrgsList(args, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.orgs.get ───────────────────────────────────────────────────── registerTool({ name: 'lysnrai.orgs.get', description: 'Get a single organisation by ID including member count, vocabulary, and shared instructions. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ orgId: z.string().min(1).describe('Organisation ID'), }), async execute(args, req) { return lysnraiOrgGet(args.orgId, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.apiTokens.revoke ─────────────────────────────────────────────── registerTool({ name: 'lysnrai.apiTokens.revoke', description: 'Revoke (soft-delete) an API token by ID — sets status to "revoked" so it can no longer authenticate. Irreversible. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ tokenId: z.string().min(1).describe('API token ID (tok_… prefix)'), }), async execute(args, req) { return lysnraiApiTokenRevoke(args.tokenId, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.apiTokens.list ───────────────────────────────────────────────── registerTool({ name: 'lysnrai.apiTokens.list', description: 'List API tokens for the authenticated user. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ 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 lysnraiApiTokensList(args, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.sessions.stats ──────────────────────────────────────────────── registerTool({ name: 'lysnrai.sessions.stats', description: 'Get aggregated dictation session statistics for the authenticated user: total sessions, composed count, average word count, and last session date. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({}), async execute(_args, req) { return lysnraiSessionsStats({ token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.transcripts.exportBatch ────────────────────────────────────── registerTool({ name: 'lysnrai.transcripts.exportBatch', description: 'Batch-export transcripts for the authenticated user as JSON or plain text. Returns a signed export URL, item count, and format. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ format: z.enum(['json', 'txt']).default('json').describe('Export format'), limit: z.coerce .number() .min(1) .max(config.QUERY_MAX_LIMIT) .default(config.QUERY_DEFAULT_LIMIT) .describe('Max transcripts to include'), }), async execute(args, req) { return lysnraiTranscriptsExportBatch(args, { token: tokenOf(req), requestId: req.id }); }, }); // ── lysnrai.apiTokens.rotate ────────────────────────────────────────────── registerTool({ name: 'lysnrai.apiTokens.rotate', description: 'Rotate an API token: revokes the existing token and issues a fresh one with the same name. Returns the new raw token (shown once only). Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ tokenId: z.string().min(1).describe('Existing API token ID to rotate'), name: z.string().min(1).describe('Name for the replacement token'), }), async execute(args, req) { const opts = { token: tokenOf(req), requestId: req.id }; await lysnraiApiTokenRevoke(args.tokenId, opts); return lysnraiApiTokenCreate({ name: args.name }, opts); }, });