Skip to main content
Glama
jira-comments.ts10.2 kB
/** * Consolidated Jira Comments Tool. * Combines all comment-related operations into a single action-based tool. * @module tools/consolidated/jira-comments */ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { getComments, addComment, updateComment, deleteComment, getComment, } from '../../jira/endpoints/comments.js'; import { encodeToon, simplifyComment } from '../../formatters/toon.js'; import { adfToMarkdown, isAdfDocument } from '../../utils/adf.js'; import { createLogger } from '../../utils/logger.js'; import { logAudit, isDryRunMode, validateConfirmation, createDryRunSummary, } from '../../utils/audit.js'; import type { JiraComment } from '../../jira/types.js'; const logger = createLogger('tool-jira-comments'); /** * Schema for the jira_comments tool. */ const jiraCommentsSchema = z.object({ action: z .enum(['list', 'get', 'add', 'update', 'delete']) .describe('The action to perform'), // Issue identification issueKey: z.string().describe('Issue key (e.g., "PROJ-123")'), // Comment identification commentId: z .string() .optional() .describe('Comment ID - required for get, update, delete'), // Get options full: z .boolean() .optional() .default(false) .describe('Return full data instead of minimal fields (default: false)'), // Safety options dryRun: z .boolean() .optional() .default(false) .describe('Preview changes without executing (default: false)'), confirm: z .boolean() .optional() .default(false) .describe('Confirm destructive action (required for update/delete)'), // List pagination maxResults: z .number() .optional() .default(50) .describe('Maximum results (default: 50)'), startAt: z .number() .optional() .default(0) .describe('Starting index for pagination'), // Add/Update fields body: z .string() .optional() .describe('Comment body (supports markdown) - required for add/update'), // Visibility restriction visibility: z .object({ type: z.enum(['group', 'role']), value: z.string(), }) .optional() .describe('Visibility restriction for add'), }); type JiraCommentsInput = z.infer<typeof jiraCommentsSchema>; /** * Formats a single comment. */ function formatComment( comment: JiraComment, full: boolean ): Record<string, unknown> { if (full) { return { ...comment, body: isAdfDocument(comment.body) ? adfToMarkdown(comment.body) : comment.body, }; } return simplifyComment(comment); } /** * Handler for the jira_comments tool. */ async function handleJiraComments(input: JiraCommentsInput): Promise<string> { const { action, issueKey } = input; const isDryRun = input.dryRun || isDryRunMode(); switch (action) { case 'list': { const response = await getComments( issueKey, input.startAt ?? 0, input.maxResults ?? 50 ); if (input.full) { const formatted = response.values.map((c) => formatComment(c, true)); return JSON.stringify( { comments: formatted, total: response.total }, null, 2 ); } const simplified = response.values.map((c) => simplifyComment(c)); return encodeToon({ comments: simplified, total: response.total, hasMore: !response.isLast, }); } case 'get': { if (!input.commentId) { throw new Error('commentId is required for get action'); } const comment = await getComment(issueKey, input.commentId); if (input.full) { return JSON.stringify(formatComment(comment, true), null, 2); } return encodeToon(simplifyComment(comment)); } case 'add': { if (!input.body) { throw new Error('body is required for add action'); } const addInput = { issueKey, body: input.body, visibility: input.visibility, }; // Dry-run mode if (isDryRun) { logAudit({ action: 'create', resource: 'comment', resourceId: issueKey, input: addInput, result: 'dry-run', }); return createDryRunSummary('create', 'comment', issueKey, addInput); } try { const comment = await addComment( issueKey, input.body, input.visibility ); logAudit({ action: 'create', resource: 'comment', resourceId: `${issueKey}/${comment.id}`, input: addInput, result: 'success', }); return encodeToon({ success: true, comment: { id: comment.id, author: comment.author.displayName, created: comment.created.split('T')[0], }, }); } catch (err) { logAudit({ action: 'create', resource: 'comment', resourceId: issueKey, input: addInput, result: 'failure', error: err instanceof Error ? err.message : String(err), }); throw err; } } case 'update': { if (!input.commentId) { throw new Error('commentId is required for update action'); } if (!input.body) { throw new Error('body is required for update action'); } // Check confirmation if (!isDryRun) { const confirmation = validateConfirmation('update', input.confirm); if (!confirmation.valid) { return encodeToon({ error: 'Confirmation required', message: confirmation.message, hint: 'Set confirm: true to proceed, or use dryRun: true to preview.', }); } } const updateInput = { issueKey, commentId: input.commentId, body: input.body, }; // Dry-run mode if (isDryRun) { logAudit({ action: 'update', resource: 'comment', resourceId: `${issueKey}/${input.commentId}`, input: updateInput, result: 'dry-run', }); return createDryRunSummary( 'update', 'comment', `${issueKey}/${input.commentId}`, updateInput ); } try { const comment = await updateComment( issueKey, input.commentId, input.body ); logAudit({ action: 'update', resource: 'comment', resourceId: `${issueKey}/${input.commentId}`, input: updateInput, result: 'success', }); return encodeToon({ success: true, comment: { id: comment.id, updated: comment.updated.split('T')[0], }, }); } catch (err) { logAudit({ action: 'update', resource: 'comment', resourceId: `${issueKey}/${input.commentId}`, input: updateInput, result: 'failure', error: err instanceof Error ? err.message : String(err), }); throw err; } } case 'delete': { if (!input.commentId) { throw new Error('commentId is required for delete action'); } // Check confirmation if (!isDryRun) { const confirmation = validateConfirmation('delete', input.confirm); if (!confirmation.valid) { return encodeToon({ error: 'Confirmation required', message: confirmation.message, hint: 'Set confirm: true to proceed, or use dryRun: true to preview.', }); } } const deleteInput = { issueKey, commentId: input.commentId, }; // Dry-run mode if (isDryRun) { logAudit({ action: 'delete', resource: 'comment', resourceId: `${issueKey}/${input.commentId}`, input: deleteInput, result: 'dry-run', }); return createDryRunSummary( 'delete', 'comment', `${issueKey}/${input.commentId}`, deleteInput ); } try { await deleteComment(issueKey, input.commentId); logAudit({ action: 'delete', resource: 'comment', resourceId: `${issueKey}/${input.commentId}`, input: deleteInput, result: 'success', }); return encodeToon({ success: true, message: `Comment ${input.commentId} deleted`, }); } catch (err) { logAudit({ action: 'delete', resource: 'comment', resourceId: `${issueKey}/${input.commentId}`, input: deleteInput, result: 'failure', error: err instanceof Error ? err.message : String(err), }); throw err; } } default: throw new Error(`Unknown action: ${action}`); } } /** * Registers the jira_comments tool with the MCP server. */ export function registerJiraCommentsTool(server: McpServer): void { server.tool( 'jira_comments', `Manage Jira issue comments. Actions: - list: List comments on an issue - get: Get a specific comment - add: Add a new comment (supports markdown) - update: Update comment body (requires confirm=true or dryRun=true) - delete: Delete a comment (requires confirm=true or dryRun=true) Safety: Use dryRun=true to preview changes.`, jiraCommentsSchema.shape, async (params) => { try { const input = jiraCommentsSchema.parse(params); const result = await handleJiraComments(input); return { content: [{ type: 'text', text: result }] }; } catch (err) { logger.error( 'jira_comments error', err instanceof Error ? err : new Error(String(err)) ); const message = err instanceof Error ? err.message : 'Unknown error'; return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true, }; } } ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/icy-r/jira-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server