UNPKG

@nakedved/ano

Version:

Claude Code plugin for collaborative annotation and review

228 lines (227 loc) 8.48 kB
#!/usr/bin/env node /** * Ano MCP Server * * Exposes annotation tools to Claude Code via MCP protocol. * * Tools: * - read_annotations: Get annotations for a file * - add_annotation: Add a new annotation * - resolve_annotation: Mark annotation as resolved * - list_approvals: Get approvals for a file * - approve_file: Add approval to a file */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { resolve } from 'node:path'; import { listAnnotations, addAnnotation, resolveAnnotation, getApprovals, setApproval, readAnnotationFile, } from '../core/annotations.js'; import { getAuthorString } from '../core/config.js'; // ============================================ // Create MCP Server // ============================================ const server = new McpServer({ name: 'ano', version: '0.1.0', }); // ============================================ // Tool: read_annotations // ============================================ server.tool('read_annotations', 'Get all annotations for a file. Returns annotations with their line numbers, types, authors, and content.', { file: z.string().describe('Path to the file to read annotations from'), status: z.enum(['open', 'resolved', 'all']).optional().describe('Filter by status (default: all)'), }, async ({ file, status }) => { try { const filePath = resolve(file); // Build filter const filter = {}; if (status && status !== 'all') { filter.status = status; } const annotations = await listAnnotations(filePath, filter); const approvals = await getApprovals(filePath); // Format for Claude const summary = { file: filePath, totalAnnotations: annotations.length, openCount: annotations.filter(a => a.status === 'open').length, resolvedCount: annotations.filter(a => a.status === 'resolved').length, approvalCount: approvals.filter(a => a.status === 'approved').length, annotations: annotations.map(a => ({ id: a.id, line: a.anchor.line, endLine: a.anchor.endLine, type: a.type, author: a.author, content: a.content, status: a.status, replies: a.replies, })), approvals: approvals, }; return { content: [ { type: 'text', text: JSON.stringify(summary, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error reading annotations: ${error.message}`, }, ], isError: true, }; } }); // ============================================ // Tool: add_annotation // ============================================ server.tool('add_annotation', 'Add a new annotation to a specific line in a file. Use this to add feedback, questions, or concerns about code or plans.', { file: z.string().describe('Path to the file to annotate'), line: z.number().describe('Line number to annotate (1-indexed)'), content: z.string().describe('The annotation content/comment'), type: z.enum(['concern', 'question', 'suggestion', 'blocker']).optional() .describe('Type of annotation: concern (risk/issue), question (needs clarification), suggestion (improvement idea), blocker (must resolve)'), }, async ({ file, line, content, type }) => { try { const filePath = resolve(file); const author = await getAuthorString(); const annotation = await addAnnotation({ file: filePath, line, type: (type || 'suggestion'), author: `Claude (via ${author})`, content, }); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: 'Annotation added successfully', annotation: { id: annotation.id, line: annotation.anchor.line, type: annotation.type, content: annotation.content, }, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error adding annotation: ${error.message}`, }, ], isError: true, }; } }); // ============================================ // Tool: resolve_annotation // ============================================ server.tool('resolve_annotation', 'Mark an annotation as resolved. Use this after addressing feedback or answering a question.', { file: z.string().describe('Path to the file containing the annotation'), annotationId: z.string().describe('The annotation ID (can be partial, e.g., first 8 characters)'), }, async ({ file, annotationId }) => { try { const filePath = resolve(file); // Find full ID from partial const annotationFile = await readAnnotationFile(filePath); if (!annotationFile) { throw new Error('No annotations found for this file'); } const annotation = annotationFile.annotations.find(a => a.id.startsWith(annotationId) || a.id === annotationId); if (!annotation) { throw new Error(`Annotation not found: ${annotationId}`); } const success = await resolveAnnotation(filePath, annotation.id); return { content: [ { type: 'text', text: JSON.stringify({ success, message: success ? 'Annotation resolved' : 'Failed to resolve annotation', annotation: { id: annotation.id, line: annotation.anchor.line, content: annotation.content, }, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error resolving annotation: ${error.message}`, }, ], isError: true, }; } }); // ============================================ // Tool: approve_file // ============================================ server.tool('approve_file', 'Add an approval to a file. Use this to sign off on a plan or document.', { file: z.string().describe('Path to the file to approve'), status: z.enum(['approved', 'changes_requested']).optional() .describe('Approval status (default: approved)'), comment: z.string().optional().describe('Optional comment with the approval'), }, async ({ file, status, comment }) => { try { const filePath = resolve(file); const author = await getAuthorString(); const approval = await setApproval(filePath, `Claude (via ${author})`, (status || 'approved'), 'AI Assistant', comment); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `File ${status || 'approved'}`, approval, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error approving file: ${error.message}`, }, ], isError: true, }; } }); // ============================================ // Start Server // ============================================ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Ano MCP server running...'); } main().catch(console.error);