#!/usr/bin/env node
import 'dotenv/config';
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
// Jira configuration from environment variables
const JIRA_BASE_URL = process.env.JIRA_BASE_URL || "https://cryptobplatform.atlassian.net";
const JIRA_EMAIL = process.env.JIRA_EMAIL;
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
const JIRA_ASSIGNEE_ACCOUNT_ID = process.env.JIRA_ASSIGNEE_ACCOUNT_ID; // Your account ID
// Validate configuration
if (!JIRA_EMAIL || !JIRA_API_TOKEN) {
console.error("Error: JIRA_EMAIL and JIRA_API_TOKEN environment variables are required");
process.exit(1);
}
// Create Jira API client
const jiraClient = axios.create({
baseURL: `${JIRA_BASE_URL}/rest/api/3`,
auth: {
username: JIRA_EMAIL,
password: JIRA_API_TOKEN,
},
headers: {
"Content-Type": "application/json",
},
});
// Helper function to get my tasks
async function getMyTasks(projectKey = "ART", status = "In Progress") {
try {
let jql = `project = ${projectKey}`;
if (JIRA_ASSIGNEE_ACCOUNT_ID) {
jql += ` AND assignee = ${JIRA_ASSIGNEE_ACCOUNT_ID}`;
}
if (status) {
jql += ` AND status = "${status}"`;
}
jql += " ORDER BY updated DESC";
const response = await jiraClient.post("/search/jql", {
jql,
fields: ["summary", "description", "status", "issuetype", "priority", "created", "updated", "assignee"],
maxResults: 50,
});
return response.data.issues.map(issue => ({
key: issue.key,
id: issue.id,
summary: issue.fields.summary,
description: issue.fields.description,
status: issue.fields.status.name,
type: issue.fields.issuetype.name,
priority: issue.fields.priority?.name,
created: issue.fields.created,
updated: issue.fields.updated,
assignee: issue.fields.assignee?.displayName,
url: `${JIRA_BASE_URL}/browse/${issue.key}`,
}));
} catch (error) {
throw new Error(`Failed to fetch tasks: ${error.response?.data?.errorMessages || error.message}`);
}
}
// Helper function to create a task
async function createTask(projectKey, summary, description, issueType = "Задача") {
try {
const payload = {
fields: {
project: {
key: projectKey,
},
summary,
description: {
type: "doc",
version: 1,
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: description || "",
},
],
},
],
},
issuetype: {
name: issueType,
},
},
};
// Add assignee if provided
if (JIRA_ASSIGNEE_ACCOUNT_ID) {
payload.fields.assignee = {
id: JIRA_ASSIGNEE_ACCOUNT_ID,
};
}
const response = await jiraClient.post("/issue", payload);
return {
key: response.data.key,
id: response.data.id,
url: `${JIRA_BASE_URL}/browse/${response.data.key}`,
};
} catch (error) {
throw new Error(`Failed to create task: ${error.response?.data?.errors ? JSON.stringify(error.response.data.errors) : error.message}`);
}
}
// Helper function to get task details
async function getTaskDetails(issueKey) {
try {
const response = await jiraClient.get(`/issue/${issueKey}`, {
params: {
fields: ["summary", "description", "status", "issuetype", "priority", "created", "updated", "assignee", "comment"],
},
});
const issue = response.data;
return {
key: issue.key,
id: issue.id,
summary: issue.fields.summary,
description: issue.fields.description,
status: issue.fields.status.name,
type: issue.fields.issuetype.name,
priority: issue.fields.priority?.name,
created: issue.fields.created,
updated: issue.fields.updated,
assignee: issue.fields.assignee?.displayName,
url: `${JIRA_BASE_URL}/browse/${issue.key}`,
comments: issue.fields.comment?.comments?.map(c => ({
author: c.author.displayName,
created: c.created,
body: c.body,
})) || [],
};
} catch (error) {
throw new Error(`Failed to fetch task details: ${error.response?.data?.errorMessages || error.message}`);
}
}
// Helper function to get available transitions for a task
async function getAvailableTransitions(issueKey) {
try {
const response = await jiraClient.get(`/issue/${issueKey}/transitions`);
return response.data.transitions.map(t => ({
id: t.id,
name: t.name,
to: t.to.name,
}));
} catch (error) {
throw new Error(`Failed to fetch transitions: ${error.response?.data?.errorMessages || error.message}`);
}
}
// Helper function to change task status
async function changeTaskStatus(issueKey, statusName) {
try {
// First, get available transitions
const transitions = await getAvailableTransitions(issueKey);
// Find the transition that matches the desired status
const transition = transitions.find(
t => t.to.toLowerCase() === statusName.toLowerCase() ||
t.name.toLowerCase() === statusName.toLowerCase()
);
if (!transition) {
const availableStatuses = transitions.map(t => `${t.name} (to: ${t.to})`).join(', ');
throw new Error(
`Cannot transition to "${statusName}". Available transitions: ${availableStatuses}`
);
}
// Perform the transition
await jiraClient.post(`/issue/${issueKey}/transitions`, {
transition: {
id: transition.id,
},
});
return {
key: issueKey,
newStatus: transition.to,
url: `${JIRA_BASE_URL}/browse/${issueKey}`,
};
} catch (error) {
throw new Error(`Failed to change task status: ${error.response?.data?.errorMessages || error.message}`);
}
}
// Create MCP server
const server = new Server(
{
name: "jira-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_my_tasks",
description: "Get my Jira tasks. Can filter by project and status. By default returns tasks assigned to me in 'In Progress' status from ART project.",
inputSchema: {
type: "object",
properties: {
projectKey: {
type: "string",
description: "Project key (e.g., 'ART', 'DIS'). Default: 'ART'",
default: "ART",
},
status: {
type: "string",
description: "Task status (e.g., 'In Progress', 'To Do', 'Done'). Leave empty to get all statuses.",
},
},
},
},
{
name: "get_task_details",
description: "Get detailed information about a specific Jira task by its key (e.g., 'ART-114')",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: "Task key (e.g., 'ART-114')",
},
},
required: ["issueKey"],
},
},
{
name: "create_task",
description: "Create a new Jira task in specified project",
inputSchema: {
type: "object",
properties: {
projectKey: {
type: "string",
description: "Project key where to create the task (e.g., 'ART')",
},
summary: {
type: "string",
description: "Task title/summary",
},
description: {
type: "string",
description: "Task description (optional)",
},
issueType: {
type: "string",
description: "Issue type: 'Задача', 'Баг', 'История'. Default: 'Задача'",
default: "Задача",
},
},
required: ["projectKey", "summary"],
},
},
{
name: "get_available_transitions",
description: "Get all available status transitions for a specific task. Use this to see what statuses a task can be moved to.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: "Task key (e.g., 'ART-114')",
},
},
required: ["issueKey"],
},
},
{
name: "change_task_status",
description: "Change the status of a Jira task (e.g., from 'In Progress' to 'Done'). The task will be transitioned to the specified status if the transition is available.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: "Task key (e.g., 'ART-114')",
},
statusName: {
type: "string",
description: "Target status name (e.g., 'Done', 'In Progress', 'To Do', 'В работе', 'Готово'). You can also use the transition name.",
},
},
required: ["issueKey", "statusName"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "get_my_tasks": {
const tasks = await getMyTasks(args.projectKey, args.status);
return {
content: [
{
type: "text",
text: JSON.stringify(tasks, null, 2),
},
],
};
}
case "get_task_details": {
const task = await getTaskDetails(args.issueKey);
return {
content: [
{
type: "text",
text: JSON.stringify(task, null, 2),
},
],
};
}
case "create_task": {
const result = await createTask(
args.projectKey,
args.summary,
args.description,
args.issueType
);
return {
content: [
{
type: "text",
text: `Task created successfully!\nKey: ${result.key}\nURL: ${result.url}`,
},
],
};
}
case "get_available_transitions": {
const transitions = await getAvailableTransitions(args.issueKey);
return {
content: [
{
type: "text",
text: JSON.stringify(transitions, null, 2),
},
],
};
}
case "change_task_status": {
const result = await changeTaskStatus(args.issueKey, args.statusName);
return {
content: [
{
type: "text",
text: `Status changed successfully!\nTask: ${result.key}\nNew Status: ${result.newStatus}\nURL: ${result.url}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Jira MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});