Skip to main content
Glama
jira-links.ts7.8 kB
/** * Consolidated Jira Links Tool. * Combines all issue linking operations into a single action-based tool. * @module tools/consolidated/jira-links */ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { getLinkTypes, createIssueLink, removeIssueLink, getIssueLinks, getRemoteLinks, createRemoteLink, removeRemoteLink, } from '../../jira/endpoints/links.js'; import { linkToEpic } from '../../jira/endpoints/issues.js'; import { encodeToon } from '../../formatters/toon.js'; import { createLogger } from '../../utils/logger.js'; const logger = createLogger('tool-jira-links'); /** * Schema for the jira_links tool. */ const jiraLinksSchema = z.object({ action: z .enum([ 'get_link_types', 'list', 'create', 'remove', 'link_to_epic', 'list_remote', 'create_remote', 'remove_remote', ]) .describe('The action to perform'), // Issue identification issueKey: z .string() .optional() .describe( 'Issue key - required for list, create, link_to_epic, remote link operations' ), // Link identification linkId: z.string().optional().describe('Link ID for remove action'), remoteLinkId: z .number() .optional() .describe('Remote link ID for remove_remote action'), // Create link fields targetIssueKey: z .string() .optional() .describe('Target issue key for create action'), linkType: z .string() .optional() .describe( 'Link type name (e.g., "Blocks", "Relates", "Duplicates") for create' ), comment: z.string().optional().describe('Comment to add with the link'), commentVisibility: z .object({ type: z.enum(['group', 'role']), value: z.string(), }) .optional() .describe( 'Visibility restriction for the comment (e.g., { type: "role", value: "Developers" })' ), // Epic link fields epicKey: z .string() .nullable() .optional() .describe('Epic key to link to (null to unlink) for link_to_epic'), // Remote link fields url: z.string().optional().describe('URL for remote link'), title: z.string().optional().describe('Title for remote link'), summary: z .string() .optional() .describe('Summary/description for remote link'), iconUrl: z.string().optional().describe('Icon URL for remote link'), // Get options full: z .boolean() .optional() .default(false) .describe('Return full data instead of minimal fields'), }); type JiraLinksInput = z.infer<typeof jiraLinksSchema>; /** * Handler for the jira_links tool. */ async function handleJiraLinks(input: JiraLinksInput): Promise<string> { const { action } = input; switch (action) { case 'get_link_types': { const types = await getLinkTypes(); if (input.full) { return JSON.stringify(types, null, 2); } const simplified = types.map((t) => ({ name: t.name, inward: t.inward, outward: t.outward, })); return encodeToon({ linkTypes: simplified }); } case 'list': { if (!input.issueKey) { throw new Error('issueKey is required for list action'); } const links = await getIssueLinks(input.issueKey); if (input.full) { return JSON.stringify(links, null, 2); } const simplified = links.map((link) => ({ id: link.id, type: link.type.name, direction: link.inwardIssue ? 'inward' : 'outward', linkedIssue: link.inwardIssue?.key || link.outwardIssue?.key, linkedSummary: link.inwardIssue?.fields?.summary || link.outwardIssue?.fields?.summary, })); return encodeToon({ links: simplified }); } case 'create': { if (!input.issueKey || !input.targetIssueKey || !input.linkType) { throw new Error( 'issueKey, targetIssueKey, and linkType are required for create action' ); } await createIssueLink( input.issueKey, input.targetIssueKey, input.linkType, input.comment, input.commentVisibility ); return encodeToon({ success: true, message: `Created "${input.linkType}" link from ${input.issueKey} to ${input.targetIssueKey}`, }); } case 'remove': { if (!input.linkId) { throw new Error('linkId is required for remove action'); } await removeIssueLink(input.linkId); return encodeToon({ success: true, message: `Removed link ${input.linkId}`, }); } case 'link_to_epic': { if (!input.issueKey) { throw new Error('issueKey is required for link_to_epic action'); } await linkToEpic(input.issueKey, input.epicKey ?? null); return encodeToon({ success: true, message: input.epicKey ? `Linked ${input.issueKey} to epic ${input.epicKey}` : `Unlinked ${input.issueKey} from epic`, }); } case 'list_remote': { if (!input.issueKey) { throw new Error('issueKey is required for list_remote action'); } const remoteLinks = await getRemoteLinks(input.issueKey); if (input.full) { return JSON.stringify(remoteLinks, null, 2); } const simplified = remoteLinks.map((link) => ({ id: link.id, url: link.object.url, title: link.object.title, summary: link.object.summary, })); return encodeToon({ remoteLinks: simplified }); } case 'create_remote': { if (!input.issueKey || !input.url || !input.title) { throw new Error( 'issueKey, url, and title are required for create_remote action' ); } const link = await createRemoteLink( input.issueKey, input.url, input.title, input.summary, input.iconUrl ); return encodeToon({ success: true, remoteLink: { id: link.id, url: link.object.url, title: link.object.title, }, }); } case 'remove_remote': { if (!input.issueKey || !input.remoteLinkId) { throw new Error( 'issueKey and remoteLinkId are required for remove_remote action' ); } await removeRemoteLink(input.issueKey, input.remoteLinkId); return encodeToon({ success: true, message: `Removed remote link ${input.remoteLinkId}`, }); } default: throw new Error(`Unknown action: ${action}`); } } /** * Registers the jira_links tool with the MCP server. */ export function registerJiraLinksTool(server: McpServer): void { server.tool( 'jira_links', `Manage Jira issue links. Actions: - get_link_types: Get available link types (Blocks, Relates, Duplicates, etc.) - list: List all links for an issue - create: Create a link between two issues - remove: Remove an issue link - link_to_epic: Link/unlink an issue to an epic (uses parent field) - list_remote: List remote/web links for an issue - create_remote: Add a remote link (URL) to an issue - remove_remote: Remove a remote link`, jiraLinksSchema.shape, async (params) => { try { const input = jiraLinksSchema.parse(params); const result = await handleJiraLinks(input); return { content: [{ type: 'text', text: result }] }; } catch (err) { logger.error( 'jira_links 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