Building an MCP Server - Part 3: Advanced Features

We have 24 Plane tools. Now let's add intelligence: tools that analyze tickets, parse dependencies, and even operate Git and GitHub. This is where MCP becomes truly powerful - AI doesn't just call APIs, it understands your workflow.

By the end, you'll have 35 total tools including complexity analysis, Git operations, and GitHub PR management.

Intelligence Tools: Making AI Smarter

These 4 tools help AI understand project complexity:

  1. Complexity Analysis - Score tickets 0-100
  2. Should Split - Recommend breaking down large tasks
  3. Decompose - Generate sub-task suggestions
  4. Parse Dependencies - Extract dependency graphs

Why Intelligence Tools Matter

Without intelligence:

Claude: "I'll create this ticket for you."

With intelligence:

Claude: "This ticket scores 87/100 complexity. I recommend splitting it into 3 sub-tasks:
1. Database schema changes
2. API endpoint implementation
3. Frontend integration
Would you like me to create these sub-issues?"

Complexity Analysis Algorithm

Create src/utils/complexity.py:

"""
Ticket Complexity Analysis
Adapted from project-team autonomous agent
"""
import re
from typing import Dict, List

def analyze_ticket_complexity(ticket: Dict) -> Dict:
    """
    Score ticket complexity 0-100 based on:
    - Description length
    - Number of technical keywords
    - File/component mentions
    - Estimated scope
    """
    score = 0
    factors = {}

    description = ticket.get('description', '') or ''

    # Factor 1: Description length (0-30 points)
    length = len(description)
    if length > 1000:
        desc_score = 30
    elif length > 500:
        desc_score = 20
    elif length > 200:
        desc_score = 10
    else:
        desc_score = 5
    score += desc_score
    factors['description_length'] = desc_score

    # Factor 2: Technical keywords (0-25 points)
    keywords = [
        r'\bapi\b', r'\bdatabase\b', r'\bschema\b', r'\bmigration\b',
        r'\bauth\b', r'\bsecurity\b', r'\bperformance\b',
        r'\brefactor\b', r'\barchitecture\b', r'\bintegration\b'
    ]
    keyword_count = sum(1 for kw in keywords if re.search(kw, description, re.I))
    keyword_score = min(keyword_count * 5, 25)
    score += keyword_score
    factors['technical_keywords'] = keyword_score

    # Factor 3: File/component mentions (0-20 points)
    file_patterns = [
        r'\.py\b', r'\.js\b', r'\.ts\b', r'\.tsx\b',
        r'\.css\b', r'\.html\b', r'\.sql\b'
    ]
    file_count = sum(1 for pattern in file_patterns if re.search(pattern, description, re.I))
    file_score = min(file_count * 5, 20)
    score += file_score
    factors['file_mentions'] = file_score

    # Factor 4: Multiple components (0-25 points)
    component_keywords = [
        'frontend', 'backend', 'database', 'api',
        'ui', 'ux', 'deployment', 'testing', 'docs'
    ]
    component_count = sum(1 for comp in component_keywords if comp in description.lower())
    component_score = min(component_count * 5, 25)
    score += component_score
    factors['components'] = component_score

    return {
        'score': min(score, 100),
        'factors': factors,
        'recommendation': _get_recommendation(score)
    }

def _get_recommendation(score: int) -> str:
    """Get recommendation based on complexity score"""
    if score >= 70:
        return "High complexity - strongly recommend splitting into sub-tasks"
    elif score >= 50:
        return "Medium complexity - consider breaking down"
    elif score >= 30:
        return "Low-medium complexity - manageable as single task"
    else:
        return "Low complexity - can be completed as-is"

Dependency Parsing

Add to complexity.py:

def parse_ticket_dependencies(ticket: Dict) -> List[str]:
    """
    Parse ticket description to find dependencies

    Looks for patterns like:
    - "depends on PROJ-123"
    - "requires PROJ-456"
    - "blocked by PROJ-789"
    """
    dependencies = []
    description = ticket.get('description', '')

    patterns = [
        r'depends\s+on\s+([A-Z]+-\d+)',
        r'requires?\s+([A-Z]+-\d+)',
        r'blocked\s+by\s+([A-Z]+-\d+)',
        r'needs?\s+([A-Z]+-\d+)',
    ]

    for pattern in patterns:
        matches = re.findall(pattern, description, re.IGNORECASE)
        dependencies.extend(matches)

    # Remove duplicates
    return list(set(dep.upper() for dep in dependencies))

def topological_sort(tickets: List[Dict]) -> List[Dict]:
    """
    Sort tickets by dependencies using Kahn's algorithm
    Dependencies come before dependents
    """
    # Build identifier map
    ticket_map = {}
    for ticket in tickets:
        identifier = f"{ticket['project']}-{ticket['sequence_id']}"
        ticket_map[identifier] = ticket

    # Build dependency graph
    graph = {tid: [] for tid in ticket_map}
    in_degree = {tid: 0 for tid in ticket_map}

    for ticket_id, ticket in ticket_map.items():
        deps = parse_ticket_dependencies(ticket)
        for dep in deps:
            if dep in ticket_map:
                graph[dep].append(ticket_id)
                in_degree[ticket_id] += 1

    # Kahn's algorithm
    queue = [tid for tid, degree in in_degree.items() if degree == 0]
    sorted_ids = []

    while queue:
        queue.sort()  # Maintain consistent ordering
        current = queue.pop(0)
        sorted_ids.append(current)

        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    # Return sorted tickets
    return [ticket_map[tid] for tid in sorted_ids if tid in ticket_map]

Registering Intelligence Tools

Add to src/server.py:

from utils.complexity import (
    analyze_ticket_complexity,
    parse_ticket_dependencies,
    topological_sort
)

@mcp.tool()
def plane_analyze_complexity(project_id: str, issue_id: str) -> str:
    """
    Analyze ticket complexity and get recommendations.
    Returns score 0-100 with breakdown of factors.
    """
    try:
        issue = plane_tools.get_issue({'project_id': project_id, 'issue_id': issue_id})
        issue_data = json.loads(issue)

        analysis = analyze_ticket_complexity(issue_data)

        return json.dumps({
            'issue_id': issue_id,
            'complexity_score': analysis['score'],
            'factors': analysis['factors'],
            'recommendation': analysis['recommendation']
        }, indent=2)
    except Exception as e:
        return f"Error analyzing complexity: {str(e)}"

@mcp.tool()
def plane_should_split(project_id: str, issue_id: str) -> str:
    """
    Determine if ticket should be split into sub-tasks.
    Returns boolean recommendation with reasoning.
    """
    try:
        issue = plane_tools.get_issue({'project_id': project_id, 'issue_id': issue_id})
        issue_data = json.loads(issue)

        analysis = analyze_ticket_complexity(issue_data)
        should_split = analysis['score'] >= 70

        return json.dumps({
            'should_split': should_split,
            'complexity_score': analysis['score'],
            'reason': analysis['recommendation'],
            'suggestion': 'Create 2-4 sub-tasks focusing on separate concerns' if should_split else 'Can be completed as single task'
        }, indent=2)
    except Exception as e:
        return f"Error: {str(e)}"

@mcp.tool()
def plane_parse_dependencies(project_id: str, issue_id: str) -> str:
    """
    Parse ticket description to extract dependencies.
    Returns list of ticket IDs this issue depends on.
    """
    try:
        issue = plane_tools.get_issue({'project_id': project_id, 'issue_id': issue_id})
        issue_data = json.loads(issue)

        dependencies = parse_ticket_dependencies(issue_data)

        return json.dumps({
            'issue_id': issue_id,
            'dependencies': dependencies,
            'count': len(dependencies),
            'message': f"Found {len(dependencies)} dependencies" if dependencies else "No dependencies found"
        }, indent=2)
    except Exception as e:
        return f"Error parsing dependencies: {str(e)}"

Git Integration (8 Tools)

Now let's add Git operations. Create src/tools/git_tools.py:

"""
Git MCP Tools
"""
import subprocess
import json
from typing import Dict, Any, Tuple

class GitTools:
    """Git operations via subprocess"""

    def _git_command(self, repo_path: str, command: List[str]) -> Tuple[bool, str]:
        """Execute git command and return (success, output)"""
        try:
            result = subprocess.run(
                ['git'] + command,
                cwd=repo_path,
                capture_output=True,
                text=True,
                timeout=60
            )
            output = result.stdout + result.stderr
            return result.returncode == 0, output.strip()
        except subprocess.TimeoutExpired:
            return False, "Git command timed out after 60 seconds"
        except Exception as e:
            return False, f"Error: {str(e)}"

    def git_status(self, arguments: Dict[str, Any]) -> str:
        """Show working tree status"""
        repo_path = arguments.get('repo_path', '.')
        success, output = self._git_command(repo_path, ['status', '--short'])
        return json.dumps({'success': success, 'output': output}, indent=2)

    def git_diff(self, arguments: Dict[str, Any]) -> str:
        """Show changes in working directory"""
        repo_path = arguments.get('repo_path', '.')
        file_path = arguments.get('file_path')

        cmd = ['diff']
        if file_path:
            cmd.append(file_path)

        success, output = self._git_command(repo_path, cmd)
        return json.dumps({'success': success, 'diff': output}, indent=2)

    def git_commit(self, arguments: Dict[str, Any]) -> str:
        """Create commit with message"""
        repo_path = arguments.get('repo_path', '.')
        message = arguments.get('message')

        success, output = self._git_command(repo_path, ['commit', '-m', message])
        return json.dumps({
            'success': success,
            'message': message if success else 'Commit failed',
            'output': output
        }, indent=2)

    # ... Add: git_push, git_pull, git_checkout, git_log, git_current_branch
    # See GitHub for complete implementation

Register Git Tools

from tools.git_tools import GitTools

git_tools = GitTools()

@mcp.tool()
def git_status(repo_path: str = ".") -> str:
    """Show git working tree status"""
    return git_tools.git_status({'repo_path': repo_path})

@mcp.tool()
def git_commit(message: str, repo_path: str = ".") -> str:
    """Create git commit with message"""
    return git_tools.git_commit({'message': message, 'repo_path': repo_path})

@mcp.tool()
def git_diff(file_path: str = None, repo_path: str = ".") -> str:
    """Show git diff for file or entire repo"""
    return git_tools.git_diff({'file_path': file_path, 'repo_path': repo_path})

# ... Register remaining 5 Git tools

GitHub Integration (3 Tools)

Create src/tools/github_tools.py:

"""
GitHub MCP Tools
"""
import os
import requests
import json
from typing import Dict, Any

class GitHubTools:
    """GitHub API operations"""

    def __init__(self):
        self.token = os.getenv('GITHUB_TOKEN')
        self.headers = {
            'Authorization': f'token {self.token}',
            'Accept': 'application/vnd.github.v3+json'
        }

    def create_pr(self, arguments: Dict[str, Any]) -> str:
        """Create pull request"""
        owner = arguments.get('owner')
        repo = arguments.get('repo')
        title = arguments.get('title')
        head = arguments.get('head')  # branch name
        base = arguments.get('base', 'main')
        body = arguments.get('body', '')

        url = f'https://api.github.com/repos/{owner}/{repo}/pulls'
        data = {
            'title': title,
            'head': head,
            'base': base,
            'body': body
        }

        try:
            response = requests.post(url, headers=self.headers, json=data)
            response.raise_for_status()
            pr = response.json()

            return json.dumps({
                'success': True,
                'pr_number': pr['number'],
                'url': pr['html_url'],
                'message': f"Created PR #{pr['number']}: {title}"
            }, indent=2)
        except Exception as e:
            return f"Error creating PR: {str(e)}"

    # ... Add: get_pr, list_prs

Register GitHub Tools

from tools.github_tools import GitHubTools

github_tools = GitHubTools()

@mcp.tool()
def github_create_pr(
    owner: str,
    repo: str,
    title: str,
    head: str,
    base: str = "main",
    body: str = ""
) -> str:
    """
    Create GitHub pull request.

    Args:
        owner: Repository owner
        repo: Repository name
        title: PR title
        head: Branch to merge from
        base: Branch to merge into (default: main)
        body: PR description (markdown)
    """
    return github_tools.create_pr({
        'owner': owner,
        'repo': repo,
        'title': title,
        'head': head,
        'base': base,
        'body': body
    })

Complete Tool Count

We now have 35 total tools:

  • Plane Tools: 24 (from Part 2)
  • Intelligence Tools: 4 (new)
  • Git Tools: 8 (new)
  • GitHub Tools: 3 (new)

Real-World Workflow Example

Now Claude can handle complete project workflows:

User: "Create a feature branch for the new auth system, analyze complexity of PORTF-45, and if it's complex, split it into sub-tasks"

Claude's actions:

  1. git_current_branch() - Check current branch
  2. git_checkout(branch="feature/auth-system", create=True) - Create branch
  3. plane_analyze_complexity(project_id="PORTF", issue_id="45") - Analyze
  4. If score > 70:
    • plane_should_split(...) - Confirm recommendation
    • plane_create_sub_issue(...) × 3 - Create sub-tasks
    • plane_parse_dependencies(...) - Check dependencies
  5. Respond with summary and next steps

This is autonomous project management powered by MCP!

Code Reuse Pattern

Notice we adapted complexity.py from an existing autonomous agent system (project-team). This is a key pattern:

✅ Do:

  • Extract proven algorithms from existing projects
  • Generalize them for MCP tools
  • Maintain error handling and edge cases
  • Keep business logic separate from MCP registration

Example structure:

src/
├── utils/           # Pure business logic (reusable)
│   ├── complexity.py
│   └── parsers.py
├── tools/           # MCP tool wrappers
│   ├── plane_tools.py
│   ├── git_tools.py
│   └── github_tools.py
└── server.py        # MCP registration only

This separation means:

  • Business logic can be unit tested
  • Tools can be reused in other projects
  • MCP layer stays thin and focused

What We've Built

✅ 4 intelligence tools for ticket analysis ✅ 8 Git tools for version control ✅ 3 GitHub tools for PR management ✅ 35 total tools covering complete project workflow ✅ Code reuse patterns from autonomous agents

Try this: Ask Claude to analyze all your backlog tickets and sort them by dependency order!

Next: Docker & Traefik

In Part 4, we'll deploy this to production:

  • Dual-mode transport (stdio + SSE)
  • Docker containerization
  • Traefik reverse proxy with SSL
  • Environment-based configuration
  • Health checks and monitoring

→ Continue to Part 4: Docker & Traefik Deployment

Resources