index.tsā¢13.9 kB
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import * as dotenv from "dotenv";
import { JiraClient } from "./jira-client.js";
dotenv.config();
// Validate required environment variables
const requiredEnvVars = ["JIRA_HOST", "JIRA_EMAIL", "JIRA_API_TOKEN"];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
// Initialize Jira client
const jiraClient = new JiraClient({
host: process.env.JIRA_HOST!,
email: process.env.JIRA_EMAIL!,
apiToken: process.env.JIRA_API_TOKEN!,
});
// Define available tools
const TOOLS: Tool[] = [
{
name: "jira_search_issues",
description:
"Search for Jira issues using JQL (Jira Query Language). Returns a list of issues matching the query.",
inputSchema: {
type: "object",
properties: {
jql: {
type: "string",
description:
'JQL query string (e.g., "project = PROJ AND status = Open")',
},
maxResults: {
type: "number",
description: "Maximum number of results to return (default: 50)",
default: 50,
},
startAt: {
type: "number",
description: "Index of the first result to return (default: 0)",
default: 0,
},
},
required: ["jql"],
},
},
{
name: "jira_get_issue",
description:
"Get detailed information about a specific Jira issue by its key.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: 'The issue key (e.g., "PROJ-123")',
},
},
required: ["issueKey"],
},
},
{
name: "jira_create_issue",
description: "Create a new Jira issue in a specified project.",
inputSchema: {
type: "object",
properties: {
projectKey: {
type: "string",
description: 'The project key (e.g., "PROJ")',
},
summary: {
type: "string",
description: "Brief summary of the issue",
},
issueType: {
type: "string",
description: 'Type of issue (e.g., "Bug", "Task", "Story")',
},
description: {
type: "string",
description: "Detailed description of the issue",
},
priority: {
type: "string",
description: 'Priority name (e.g., "High", "Medium", "Low")',
},
},
required: ["projectKey", "summary", "issueType"],
},
},
{
name: "jira_update_issue",
description: "Update fields of an existing Jira issue.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: 'The issue key (e.g., "PROJ-123")',
},
summary: {
type: "string",
description: "Updated summary",
},
description: {
type: "string",
description: "Updated description",
},
priority: {
type: "string",
description: "Updated priority name",
},
},
required: ["issueKey"],
},
},
{
name: "jira_add_comment",
description: "Add a comment to a Jira issue.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: 'The issue key (e.g., "PROJ-123")',
},
comment: {
type: "string",
description: "The comment text to add",
},
},
required: ["issueKey", "comment"],
},
},
{
name: "jira_get_transitions",
description:
"Get available transitions for a Jira issue (e.g., what statuses it can move to).",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: 'The issue key (e.g., "PROJ-123")',
},
},
required: ["issueKey"],
},
},
{
name: "jira_transition_issue",
description: "Transition a Jira issue to a new status.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: 'The issue key (e.g., "PROJ-123")',
},
transitionId: {
type: "string",
description:
"The ID of the transition to execute (get from jira_get_transitions)",
},
},
required: ["issueKey", "transitionId"],
},
},
{
name: "jira_assign_issue",
description: "Assign a Jira issue to a user.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: 'The issue key (e.g., "PROJ-123")',
},
accountId: {
type: "string",
description:
"The account ID of the user to assign (use null to unassign)",
},
},
required: ["issueKey", "accountId"],
},
},
{
name: "jira_get_projects",
description:
"Get a list of all Jira projects accessible to the authenticated user.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "jira_search_users",
description: "Search for Jira users by name or email.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (name or email)",
},
},
required: ["query"],
},
},
{
name: "jira_get_comments",
description: "Get all comments for a specific Jira issue.",
inputSchema: {
type: "object",
properties: {
issueKey: {
type: "string",
description: 'The issue key (e.g., "PROJ-123")',
},
},
required: ["issueKey"],
},
},
{
name: "jira_get_boards",
description:
"Get a list of all Jira boards (Scrum and Kanban) accessible to the authenticated user.",
inputSchema: {
type: "object",
properties: {
startAt: {
type: "number",
description: "Index of the first result to return (default: 0)",
default: 0,
},
maxResults: {
type: "number",
description: "Maximum number of results to return (default: 50)",
default: 50,
},
},
},
},
{
name: "jira_get_board_issues",
description:
"Get issues (stories, tasks, bugs, etc.) from a specific Jira board. Use this to get stories based on a board ID.",
inputSchema: {
type: "object",
properties: {
boardId: {
type: "number",
description: "The ID of the board to retrieve issues from",
},
startAt: {
type: "number",
description: "Index of the first result to return (default: 0)",
default: 0,
},
maxResults: {
type: "number",
description: "Maximum number of results to return (default: 50)",
default: 50,
},
jql: {
type: "string",
description:
'Optional JQL to filter board issues (e.g., "type = Story AND status = "In Progress"")',
},
},
required: ["boardId"],
},
},
];
// Initialize MCP server
const server = new Server(
{
name: "jira-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
switch (name) {
case "jira_search_issues": {
const { jql, maxResults = 50, startAt = 0 } = args as any;
const result = await jiraClient.searchIssues(jql, maxResults, startAt);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
case "jira_get_issue": {
const { issueKey } = args as any;
const issue = await jiraClient.getIssue(issueKey);
return {
content: [
{
type: "text",
text: JSON.stringify(issue, null, 2),
},
],
};
}
case "jira_create_issue": {
const { projectKey, summary, issueType, description, priority } =
args as any;
const additionalFields: any = {};
if (priority) {
additionalFields.priority = { name: priority };
}
const issue = await jiraClient.createIssue(
projectKey,
summary,
issueType,
description,
additionalFields
);
return {
content: [
{
type: "text",
text: JSON.stringify(issue, null, 2),
},
],
};
}
case "jira_update_issue": {
const { issueKey, summary, description, priority } = args as any;
const fields: any = {};
if (summary) fields.summary = summary;
if (priority) fields.priority = { name: priority };
if (description) {
fields.description = {
type: "doc",
version: 1,
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: description,
},
],
},
],
};
}
await jiraClient.updateIssue(issueKey, fields);
return {
content: [
{
type: "text",
text: `Successfully updated issue ${issueKey}`,
},
],
};
}
case "jira_add_comment": {
const { issueKey, comment } = args as any;
const result = await jiraClient.addComment(issueKey, comment);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
case "jira_get_transitions": {
const { issueKey } = args as any;
const transitions = await jiraClient.getTransitions(issueKey);
return {
content: [
{
type: "text",
text: JSON.stringify(transitions, null, 2),
},
],
};
}
case "jira_transition_issue": {
const { issueKey, transitionId } = args as any;
await jiraClient.transitionIssue(issueKey, transitionId);
return {
content: [
{
type: "text",
text: `Successfully transitioned issue ${issueKey}`,
},
],
};
}
case "jira_assign_issue": {
const { issueKey, accountId } = args as any;
await jiraClient.assignIssue(issueKey, accountId);
return {
content: [
{
type: "text",
text: `Successfully assigned issue ${issueKey}`,
},
],
};
}
case "jira_get_projects": {
const projects = await jiraClient.getProjects();
return {
content: [
{
type: "text",
text: JSON.stringify(projects, null, 2),
},
],
};
}
case "jira_search_users": {
const { query } = args as any;
const users = await jiraClient.getUsers(query);
return {
content: [
{
type: "text",
text: JSON.stringify(users, null, 2),
},
],
};
}
case "jira_get_comments": {
const { issueKey } = args as any;
const comments = await jiraClient.getIssueComments(issueKey);
return {
content: [
{
type: "text",
text: JSON.stringify(comments, null, 2),
},
],
};
}
case "jira_get_boards": {
const { startAt = 0, maxResults = 50 } = args as any;
const result = await jiraClient.getBoards(startAt, maxResults);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
case "jira_get_board_issues": {
const { boardId, startAt = 0, maxResults = 50, jql } = args as any;
const result = await jiraClient.getBoardIssues(
boardId,
startAt,
maxResults,
jql
);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}\n${
error.response?.data
? JSON.stringify(error.response.data, null, 2)
: ""
}`,
},
],
isError: true,
};
}
});
// Start the server
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:", error);
process.exit(1);
});