Linear MCP Server
- linear_mcp
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
from .linear_client import LinearMCPClient
# Find and load .env file
env_path = Path(__file__).parent.parent / ".env"
if env_path.exists():
load_dotenv(env_path)
else:
print(f"Warning: No .env file found at {env_path}", file=sys.stderr)
# Initialize FastMCP server
mcp = FastMCP("linear")
# Initialize global client
linear_client = None
@mcp.tool()
async def linear_create_issue(
title: str,
team_id: str,
description: Optional[str] = None,
priority: Optional[int] = None,
status: Optional[str] = None,
) -> str:
"""Create a new Linear issue.
Args:
title: Issue title
team_id: Team ID to create issue in
description: Issue description (markdown supported)
priority: Priority level (1=urgent, 4=low)
status: Initial status name
"""
global linear_client
if not linear_client:
return "Error: Linear client not initialized"
try:
issue = linear_client.create_issue(
title=title,
team_id=team_id,
description=description,
priority=priority,
status=status,
)
if not issue:
return "Error: Failed to create issue"
return json.dumps(
{
"message": f"Created issue {issue.get('identifier')}: {issue.get('title')}",
"issue": issue,
}
)
except Exception as e:
return f"Error: Failed to create issue - {str(e)}"
@mcp.tool()
async def linear_update_issue(
id: str,
title: Optional[str] = None,
description: Optional[str] = None,
priority: Optional[int] = None,
status: Optional[str] = None,
) -> str:
"""Update an existing Linear issue.
Args:
id: Issue ID to update
title: New title
description: New description
priority: New priority (1=urgent, 4=low)
status: New status name
"""
global linear_client
if not linear_client:
return "Error: Linear client not initialized"
try:
issue = linear_client.update_issue(
issue_id=id,
title=title,
description=description,
priority=priority,
status=status,
)
if not issue:
return "Error: Failed to update issue"
return json.dumps(
{"message": f"Updated issue {issue.get('identifier')}", "issue": issue}
)
except Exception as e:
return f"Error: Failed to update issue - {str(e)}"
@mcp.tool()
async def linear_search_issues(
query: Optional[str] = None,
team_id: Optional[str] = None,
status: Optional[str] = None,
assignee_id: Optional[str] = None,
labels: Optional[List[str]] = None,
priority: Optional[int] = None,
estimate: Optional[int] = None,
include_archived: Optional[bool] = False,
limit: int = 10,
) -> str:
"""Search issues with flexible filtering.
Args:
query: Text to search in title/description
team_id: Filter by team
status: Filter by status
assignee_id: Filter by assignee
labels: Filter by labels
priority: Filter by priority
estimate: Filter by estimate points
include_archived: Include archived issues
limit: Max results (default: 10)
"""
global linear_client
if not linear_client:
return "Error: Linear client not initialized"
try:
issues = linear_client.search_issues(
query=query,
team_id=team_id,
status=status,
assignee_id=assignee_id,
labels=labels,
priority=priority,
limit=limit,
)
issue_list = "\n".join(
[
f"- {issue.get('identifier')}: {issue.get('title')}\n "
f"Priority: {issue.get('priority') or 'None'}, "
f"Status: {issue.get('state') or 'None'}\n "
f"{issue.get('url')}"
for issue in issues
]
)
return json.dumps(
{
"message": f"Found {len(issues)} matching issues",
"issues": issues,
"text": f"Found {len(issues)} issues:\n{issue_list}",
}
)
except Exception as e:
return f"Error: Failed to search issues - {str(e)}"
@mcp.tool()
async def linear_get_user_issues(
user_id: Optional[str] = None,
include_archived: bool = False,
limit: int = 50,
) -> str:
"""Get issues assigned to a user.
Args:
user_id: User ID (omit for authenticated user)
include_archived: Include archived issues
limit: Max results (default: 50)
"""
global linear_client
if not linear_client:
return "Error: Linear client not initialized"
try:
issues = linear_client.get_user_issues(
user_id=user_id,
include_archived=include_archived,
limit=limit,
)
issue_list = "\n".join(
[
f"- {issue.get('identifier')}: {issue.get('title')}\n "
f"Priority: {issue.get('priority') or 'None'}, "
f"Status: {issue.get('state') or 'Unknown'}\n "
f"{issue.get('url')}"
for issue in issues
]
)
return json.dumps(
{
"message": f"Found {len(issues)} assigned issues",
"issues": issues,
"text": f"Found {len(issues)} issues:\n{issue_list}",
}
)
except Exception as e:
return f"Error: Failed to get user issues - {str(e)}"
@mcp.tool()
async def linear_add_comment(
issue_id: str,
body: str,
create_as_user: Optional[str] = None,
display_icon_url: Optional[str] = None,
) -> str:
"""Add a comment to an issue.
Args:
issue_id: Issue ID to comment on
body: Comment text (markdown supported)
create_as_user: Custom username
display_icon_url: Custom avatar URL
"""
global linear_client
if not linear_client:
return "Error: Linear client not initialized"
try:
comment = linear_client.add_comment(
issue_id=issue_id,
body=body,
create_as_user=create_as_user,
display_icon_url=display_icon_url,
)
if not comment:
return "Error: Failed to add comment"
return json.dumps(
{
"message": f"Added comment to issue {comment.get('issue', {}).get('identifier')}",
"comment": comment,
}
)
except Exception as e:
return f"Error: Failed to add comment - {str(e)}"
@mcp.resource("linear-issue:///{issue_id}")
async def get_issue(issue_id: str) -> Dict:
"""Get a Linear issue by ID.
Args:
issue_id: Issue ID
Returns:
Issue details
"""
global linear_client
if not linear_client:
return {"error": "Linear client not initialized"}
try:
issue = linear_client.get_issue(issue_id)
return {"mimeType": "application/json", "data": issue}
except Exception as e:
return {"error": f"Failed to get issue: {str(e)}"}
@mcp.resource("linear-team:///{team_id}/issues")
async def get_team_issues(team_id: str) -> Dict:
"""Get issues for a Linear team.
Args:
team_id: Team ID
Returns:
Team issues
"""
global linear_client
if not linear_client:
return {"error": "Linear client not initialized"}
try:
issues = linear_client.get_team_issues(team_id)
return {"mimeType": "application/json", "data": issues}
except Exception as e:
return {"error": f"Failed to get team issues: {str(e)}"}
@mcp.resource("linear-user:///{user_id}/assigned")
async def get_user_assigned(user_id: str) -> Dict:
"""Get issues assigned to a user.
Args:
user_id: User ID (use 'me' for authenticated user)
Returns:
User's assigned issues
"""
global linear_client
if not linear_client:
return {"error": "Linear client not initialized"}
try:
# Handle 'me' special case
actual_user_id = None if user_id == "me" else user_id
issues = linear_client.get_user_issues(user_id=actual_user_id)
return {"mimeType": "application/json", "data": issues}
except Exception as e:
return {"error": f"Failed to get user issues: {str(e)}"}
@mcp.resource("linear-organization:")
async def get_organization() -> Dict:
"""Get the Linear organization.
Returns:
Organization details
"""
global linear_client
if not linear_client:
return {"error": "Linear client not initialized"}
try:
org = linear_client.get_organization()
return {"mimeType": "application/json", "data": org}
except Exception as e:
return {"error": f"Failed to get organization: {str(e)}"}
@mcp.resource("linear-viewer:")
async def get_viewer() -> Dict:
"""Get the authenticated user (viewer).
Returns:
User details
"""
global linear_client
if not linear_client:
return {"error": "Linear client not initialized"}
try:
viewer = linear_client.get_viewer()
return {"mimeType": "application/json", "data": viewer}
except Exception as e:
return {"error": f"Failed to get viewer: {str(e)}"}
@mcp.prompt("default")
def get_default_prompt() -> str:
"""Get the default prompt for the Linear MCP server."""
return """This server provides access to Linear, a project management tool. Use it to manage issues, track work, and coordinate with teams.
Key capabilities:
- Create and update issues: Create new tickets or modify existing ones with titles, descriptions, priorities, and team assignments.
- Search functionality: Find issues across the organization using flexible search queries with team and user filters.
- Team coordination: Access team-specific issues and manage work distribution within teams.
- Issue tracking: Add comments and track progress through status updates and assignments.
- Organization overview: View team structures and user assignments across the organization.
Tool Usage:
- linear_create_issue:
- use teamId from linear-organization: resource
- priority levels: 1=urgent, 2=high, 3=normal, 4=low
- status must match exact Linear workflow state names (e.g., "In Progress", "Done")
- linear_update_issue:
- get issue IDs from search_issues or linear-issue:/// resources
- only include fields you want to change
- status changes must use valid state IDs from the team's workflow
- linear_search_issues:
- combine multiple filters for precise results
- use labels array for multiple tag filtering
- query searches both title and description
- returns max 10 results by default
- linear_get_user_issues:
- omit userId to get authenticated user's issues
- useful for workload analysis and sprint planning
- returns most recently updated issues first
- linear_add_comment:
- supports full markdown formatting
- use displayIconUrl for bot/integration avatars
- createAsUser for custom comment attribution
Best practices:
- When creating issues:
- Write clear, actionable titles that describe the task well (e.g., "Implement user authentication for mobile app")
- Include concise but appropriately detailed descriptions in markdown format with context and acceptance criteria
- Set appropriate priority based on the context (1=critical to 4=nice-to-have)
- Always specify the correct team ID (default to the user's team if possible)
- When searching:
- Use specific, targeted queries for better results (e.g., "auth mobile app" rather than just "auth")
- Apply relevant filters when asked or when you can infer the appropriate filters to narrow results
- When adding comments:
- Use markdown formatting to improve readability and structure
- Keep content focused on the specific issue and relevant updates
- Include action items or next steps when appropriate
- General best practices:
- Fetch organization data first to get valid team IDs
- Use search_issues to find issues for bulk operations
- Include markdown formatting in descriptions and comments
Resource patterns:
- linear-issue:///{issueId} - Single issue details (e.g., linear-issue:///c2b318fb-95d2-4a81-9539-f3268f34af87)
- linear-team:///{teamId}/issues - Team's issue list (e.g., linear-team:///OPS/issues)
- linear-user:///{userId}/assigned - User assignments (e.g., linear-user:///USER-123/assigned)
- linear-organization: - Organization for the current user
- linear-viewer: - Current user context
The server uses the authenticated user's permissions for all operations."""
def initialize_client(api_key: str) -> None:
"""Initialize the Linear client.
Args:
api_key: Linear API key
"""
global linear_client
try:
linear_client = LinearMCPClient(api_key)
except Exception as e:
print(f"Error initializing Linear client: {str(e)}", file=sys.stderr)
sys.exit(1)
def main():
api_key = os.environ.get("LINEAR_API_KEY")
if not api_key:
print(
"Error: LINEAR_API_KEY not found in environment variables or .env file",
file=sys.stderr,
)
sys.exit(1)
initialize_client(api_key)
mcp.run(transport="stdio")
if __name__ == "__main__":
main()