# How to use MCP for project management (Jira, Linear)

- Tool: MCP
- Difficulty: Intermediate
- Time required: 20-30 min
- Compatibility: MCP TypeScript SDK v1.x, Jira Cloud or Linear, Claude Desktop / Cursor
- Last updated: March 2026

## TL;DR

Use MCP servers for Jira and Linear to let AI assistants manage project tickets through natural language. The AI creates issues, updates statuses, assigns team members, and queries sprint progress — all without leaving your IDE or chat interface. Configure community or custom MCP servers that wrap the Jira REST API or Linear GraphQL API and expose project management actions as MCP tools.

## Managing Jira and Linear Tickets via MCP

Switching between your code editor and project management tools breaks flow. MCP solves this by bringing ticket management into your AI assistant. Connect a Jira or Linear MCP server, and you can create bugs from error logs, update ticket statuses after completing features, and check sprint progress — all through natural language conversation. This tutorial covers configuring community servers and building a custom Linear MCP server.

## Before you start

- A Jira Cloud or Linear account with project access
- API token for Jira or API key for Linear
- Claude Desktop or Cursor with MCP support
- Node.js 18+ for custom server development

## Step-by-step guide

### 1. Configure a Jira MCP server for ticket management

Set up a Jira MCP server that connects to your Jira Cloud instance. The server provides tools to search issues with JQL, create new issues, update fields, add comments, and transition statuses. You need a Jira API token (create one at id.atlassian.com/manage-profile/security/api-tokens) and your Jira site URL.

```
// Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "jira": {
      "command": "npx",
      "args": ["-y", "mcp-server-jira"],
      "env": {
        "JIRA_URL": "https://your-company.atlassian.net",
        "JIRA_EMAIL": "you@company.com",
        "JIRA_API_TOKEN": "your-jira-api-token"
      }
    }
  }
}

// Tools typically available:
// - search_issues: Search with JQL queries
// - create_issue: Create a new ticket
// - update_issue: Update fields on existing ticket
// - add_comment: Add a comment to a ticket
// - transition_issue: Change ticket status
```

**Expected result:** Jira MCP server connected with tools for searching, creating, and updating tickets.

### 2. Configure a Linear MCP server for modern project tracking

Linear's GraphQL API is well-suited for MCP integration. Several community MCP servers exist for Linear. Configure one with your Linear API key (Settings > API > Personal API keys). Linear's data model is cleaner than Jira's, making AI interactions more straightforward.

```
// Claude Desktop config:
{
  "mcpServers": {
    "linear": {
      "command": "npx",
      "args": ["-y", "mcp-server-linear"],
      "env": {
        "LINEAR_API_KEY": "lin_api_your_key_here"
      }
    }
  }
}

// Common natural language interactions:
// "Create a bug ticket: Login page returns 500 when password is empty"
// "What issues are assigned to me this sprint?"
// "Mark LIN-423 as done"
// "Add a comment to LIN-150: Fixed in PR #78, deployed to staging"
```

**Expected result:** Linear MCP server connected with issue CRUD tools and sprint query capabilities.

### 3. Build a custom Linear MCP server with typed tools

For maximum control, build a custom MCP server that wraps Linear's GraphQL API. This lets you tailor tools to your team's specific workflows — creating issues with custom fields, filtering by team or label, and managing cycles. The Linear GraphQL API is simple and well-documented.

```
// src/linear-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const LINEAR_API = "https://api.linear.app/graphql";
const API_KEY = process.env.LINEAR_API_KEY!;

async function linearQuery(query: string, variables: Record<string, unknown> = {}) {
  const res = await fetch(LINEAR_API, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: API_KEY,
    },
    body: JSON.stringify({ query, variables }),
  });
  return res.json();
}

const server = new McpServer({ name: "linear-pm", version: "1.0.0" });

server.tool("search_issues", "Search Linear issues with filters", {
  query: z.string().optional().describe("Text search query"),
  status: z.string().optional().describe("Filter by status: Todo, In Progress, Done"),
  assignee: z.string().optional().describe("Filter by assignee name"),
  limit: z.number().default(20),
}, async ({ query: q, status, assignee, limit }) => {
  const filters: string[] = [];
  if (q) filters.push(`{ searchableContent: { contains: "${q}" } }`);
  if (status) filters.push(`{ state: { name: { eq: "${status}" } } }`);
  if (assignee) filters.push(`{ assignee: { name: { contains: "${assignee}" } } }`);

  const filterStr = filters.length > 0 ? `filter: { and: [${filters.join(",")}] },` : "";
  const result = await linearQuery(`{
    issues(${filterStr} first: ${limit}, orderBy: updatedAt) {
      nodes { identifier title state { name } assignee { name } priority createdAt }
    }
  }`);
  return { content: [{ type: "text", text: JSON.stringify(result.data.issues.nodes, null, 2) }] };
});

server.tool("create_issue", "Create a new Linear issue", {
  title: z.string(),
  description: z.string().optional(),
  teamKey: z.string().describe("Team key, e.g. ENG"),
  priority: z.number().min(0).max(4).default(3).describe("0=none, 1=urgent, 2=high, 3=medium, 4=low"),
  labelNames: z.array(z.string()).optional().describe("Labels to apply"),
}, async ({ title, description, teamKey, priority, labelNames }) => {
  const teamResult = await linearQuery(`{ teams(filter: { key: { eq: "${teamKey}" } }) { nodes { id } } }`);
  const teamId = teamResult.data.teams.nodes[0]?.id;
  if (!teamId) return { content: [{ type: "text", text: `Error: Team ${teamKey} not found` }], isError: true };

  const result = await linearQuery(`mutation($input: IssueCreateInput!) {
    issueCreate(input: $input) { success issue { identifier title url } }
  }`, { input: { title, description, teamId, priority } });

  const issue = result.data.issueCreate.issue;
  return { content: [{ type: "text", text: `Created ${issue.identifier}: ${issue.title}\n${issue.url}` }] };
});
```

**Expected result:** Custom Linear MCP server with search and create tools tailored to your team's workflow.

### 4. Integrate project management into your development workflow

The highest-value pattern is using project management MCP tools alongside code tools. When you finish a feature, tell the AI to update the ticket. When you hit a bug, create a ticket with the error details directly from your conversation. This keeps project tracking up to date without context switching. RapidDev teams use this pattern extensively to keep tickets synchronized with actual development progress.

```
// Workflow examples:

// After fixing a bug:
// "Update LIN-423 status to Done and add a comment: 
//  Fixed the null pointer in UserService.getProfile(). 
//  Root cause was missing null check on the session token. 
//  PR #78 deployed to staging."

// After finding a bug while coding:
// "Create a bug ticket for the ENG team: 
//  Title: API returns 500 when email contains special characters
//  Description: The /api/users/search endpoint throws an unhandled
//  error when the email query parameter contains + or & characters.
//  Priority: high"

// Sprint planning:
// "Show me all In Progress issues assigned to me"
// "What high-priority bugs are unassigned in the ENG team?"
// "How many story points are done vs remaining in the current sprint?"
```

**Expected result:** Seamless project management integrated into your AI-assisted development workflow.

## Complete code example

File: `src/linear-server.ts`

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const API_KEY = process.env.LINEAR_API_KEY!;

async function gql(query: string, variables?: Record<string, unknown>) {
  const res = await fetch("https://api.linear.app/graphql", {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: API_KEY },
    body: JSON.stringify({ query, variables }),
  });
  return res.json();
}

const server = new McpServer({ name: "linear-pm", version: "1.0.0" });

server.tool("search_issues", "Search Linear issues", {
  query: z.string().optional(), status: z.string().optional(),
  assignee: z.string().optional(), limit: z.number().default(20),
}, async ({ query: q, status, assignee, limit }) => {
  const f: string[] = [];
  if (q) f.push(`searchableContent: { contains: "${q}" }`);
  if (status) f.push(`state: { name: { eq: "${status}" } }`);
  if (assignee) f.push(`assignee: { name: { contains: "${assignee}" } }`);
  const filter = f.length ? `filter: { ${f.join(", ")} },` : "";
  const r = await gql(`{ issues(${filter} first: ${limit}) { nodes { identifier title state { name } assignee { name } priority url } } }`);
  return { content: [{ type: "text", text: JSON.stringify(r.data.issues.nodes, null, 2) }] };
});

server.tool("create_issue", "Create a new issue", {
  title: z.string(), description: z.string().optional(),
  teamKey: z.string(), priority: z.number().min(0).max(4).default(3),
}, async ({ title, description, teamKey, priority }) => {
  const t = await gql(`{ teams(filter: { key: { eq: "${teamKey}" } }) { nodes { id } } }`);
  const teamId = t.data.teams.nodes[0]?.id;
  if (!teamId) return { content: [{ type: "text", text: "Team not found" }], isError: true };
  const r = await gql(`mutation($i: IssueCreateInput!) { issueCreate(input: $i) { success issue { identifier title url } } }`,
    { i: { title, description, teamId, priority } });
  const issue = r.data.issueCreate.issue;
  return { content: [{ type: "text", text: `Created ${issue.identifier}: ${issue.title}\n${issue.url}` }] };
});

server.tool("update_status", "Update issue status", {
  issueId: z.string().describe("Issue identifier like ENG-123"),
  status: z.string().describe("New status: Todo, In Progress, Done, Canceled"),
}, async ({ issueId, status }) => {
  const i = await gql(`{ issueSearch(query: "${issueId}") { nodes { id } } }`);
  const id = i.data.issueSearch.nodes[0]?.id;
  if (!id) return { content: [{ type: "text", text: "Issue not found" }], isError: true };
  const states = await gql(`{ workflowStates { nodes { id name } } }`);
  const stateId = states.data.workflowStates.nodes.find((s: any) => s.name === status)?.id;
  if (!stateId) return { content: [{ type: "text", text: `Status '${status}' not found` }], isError: true };
  await gql(`mutation { issueUpdate(id: "${id}", input: { stateId: "${stateId}" }) { success } }`);
  return { content: [{ type: "text", text: `Updated ${issueId} to ${status}` }] };
});

async function main() {
  await server.connect(new StdioServerTransport());
  console.error("Linear MCP server running");
}
main().catch(e => { console.error(e); process.exit(1); });
```

## Common mistakes

- **Using personal API tokens instead of service account tokens for shared MCP servers** — undefined Fix: Create a dedicated service account or bot user for the MCP server. Personal tokens stop working when the user leaves the organization.
- **Not validating team keys before creating issues, causing cryptic API errors** — undefined Fix: Look up the team ID first and return a clear error if the team key does not exist.
- **Hardcoding status names that differ between projects or teams** — undefined Fix: Query available workflow states dynamically and match by name. Different teams may have different status names.

## Best practices

- Use service account API tokens for MCP servers, not personal tokens
- Include issue URLs in creation and update responses for easy access
- Query workflow states dynamically instead of hardcoding status names
- Add comment tools so the AI can document changes directly on tickets
- Log all project management operations for audit purposes
- Validate team keys and project IDs before creating issues
- Return structured data so the AI can present it clearly to the user

## Frequently asked questions

### Should I use Jira or Linear MCP for my team?

Linear has a cleaner GraphQL API that is easier to work with from MCP. Jira has broader enterprise adoption and more complex workflows. Choose based on what your team already uses.

### Can the AI automatically create tickets from errors?

Yes. Combine error monitoring (Sentry MCP) with project management (Linear MCP) to automatically create bug tickets when errors exceed a threshold.

### How do I prevent the AI from making unwanted changes to tickets?

Configure the MCP server with read-only tools for querying, and require explicit confirmation before executing create or update operations.

### Can RapidDev build custom project management MCP integrations?

Yes. RapidDev builds MCP integrations for Jira, Linear, Asana, Notion, and other project management tools, including custom workflows and automated ticket creation pipelines.

### Does this work with Asana or Notion?

The pattern works with any project management tool that has an API. Community MCP servers exist for Notion, and Asana can be integrated with a custom MCP server wrapping their REST API.

---

Source: https://www.rapidevelopers.com/mcp-tutorial/how-to-use-mcp-for-project-management
© RapidDev — https://www.rapidevelopers.com/mcp-tutorial/how-to-use-mcp-for-project-management
