Skip to main content
Glama
server.py26.4 kB
"""MCP Server for project tracking.""" import json from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import TextContent, Tool from .db import DEFAULT_DB_PATH, TrackerDB from .models import ( Complexity, NoteCreate, OrgCreate, Priority, ProjectCreate, TaskCreate, TaskStatus, TaskUpdate, TicketCreate, TicketStatus, TicketUpdate, ) # Initialize server and database server = Server("technical-project-manager") db = TrackerDB() def _json(obj) -> str: """Convert model to JSON string.""" if hasattr(obj, "model_dump"): return json.dumps(obj.model_dump(), default=str, indent=2) return json.dumps(obj, default=str, indent=2) # --- Tool Definitions --- @server.list_tools() async def list_tools() -> list[Tool]: return [ # Roadmap view - PRIMARY TOOL for checking status Tool( name="roadmap_view", description="""PROJECT MANAGEMENT (TPM): Get the full project roadmap showing all work status. USE THIS TOOL WHEN: - User asks "what's in progress?" or "what are we working on?" - User asks "TPM status", ":TPM:" prefix, or "show me the roadmap" - User asks about pending/blocked/completed work - User asks "what features are we working on this sprint?" - You need to check what features or tasks exist before updating status - Starting a work session to see current state - After git operations to map changes to tracked tasks - User completes work and you need to find related tasks to mark done This replaces the project-tracking-pm agent. Returns all organizations, projects, features (with status/priority), and their tasks.""", inputSchema={ "type": "object", "properties": { "org_id": { "type": "string", "description": "Filter by organization ID (optional)", }, "format": { "type": "string", "enum": ["json", "summary"], "description": "Output format: 'json' for full data, 'summary' for readable overview (default: summary)", }, }, }, ), # Ticket operations Tool( name="ticket_create", description="""PROJECT MANAGEMENT (TPM): Create a new ticket, epic, or issue to track. USE THIS TOOL WHEN: - User says ":TPM: Add X feature to the roadmap" - User wants to add a new feature/ticket/issue - User says "add ticket for X" or "create feature for Y" - Breaking down work into trackable items - User asks to scope out or define new work - User discusses new work that should be tracked Use roadmap_view first to get the project_id. Tickets are high-level work items (like Jira epics/stories). TICKET ID NAMING CONVENTIONS: When providing a custom 'id', use this format: PREFIX-NNN where: - PREFIX: 2-8 uppercase letters indicating ticket type and/or project - NNN: Sequential 3-digit number (001, 002, etc.) Standard prefixes by type: - FEAT-NNN: New features or capabilities - ISSUE-NNN: Bugs, problems, or issues to fix - TASK-NNN: General tasks or chores - INFRA-NNN: Infrastructure or DevOps work - DOC-NNN: Documentation tasks Project-specific prefixes (use project name or abbreviation): - BE-NNN: Backend project tickets - FE-NNN: Frontend project tickets - SENTRY-NNN: Sentry project tickets - API-NNN: API-related tickets Examples: FEAT-001, ISSUE-042, SENTRY-003, BE-15 If no id is provided, auto-generates {PROJECT_ID}-{NNN} sequentially (e.g., BACKEND-001, SENTRY-042).""", inputSchema={ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID (use project_list to find)", }, "id": { "type": "string", "description": "Optional custom ticket ID (e.g., FEAT-001, SENTRY-003). Use PREFIX-NNN format. If omitted, auto-generates {PROJECT_ID}-{NNN} sequentially.", }, "title": {"type": "string", "description": "Ticket title"}, "description": { "type": "string", "description": "Detailed description of the ticket", }, "status": { "type": "string", "enum": ["backlog", "planned", "in-progress", "done", "blocked"], "description": "Ticket status (default: backlog)", }, "priority": { "type": "string", "enum": ["critical", "high", "medium", "low"], "description": "Priority level (default: medium)", }, "tags": { "type": "array", "items": {"type": "string"}, "description": "Tags for categorization", }, "assignees": { "type": "array", "items": {"type": "string"}, "description": "Who is working on this", }, }, "required": ["project_id", "title"], }, ), Tool( name="ticket_update", description="""PROJECT MANAGEMENT (TPM): Update a ticket's status, priority, or details. USE THIS TOOL WHEN: - User says "I just finished implementing X" - mark related ticket as done - User says "I've pushed commits for X" - update status based on progress - Marking work as in-progress, done, or blocked - Changing priority of a ticket - User completes a ticket and needs to update status - Adding/updating tags or assignees Use roadmap_view first to find the ticket_id.""", inputSchema={ "type": "object", "properties": { "ticket_id": {"type": "string", "description": "Ticket ID (e.g., FEAT-001)"}, "title": {"type": "string", "description": "New title"}, "description": {"type": "string", "description": "New description"}, "status": { "type": "string", "enum": ["backlog", "planned", "in-progress", "done", "blocked"], "description": "New status", }, "priority": { "type": "string", "enum": ["critical", "high", "medium", "low"], "description": "New priority", }, "tags": { "type": "array", "items": {"type": "string"}, "description": "Updated tags", }, "assignees": { "type": "array", "items": {"type": "string"}, "description": "Updated assignees", }, }, "required": ["ticket_id"], }, ), Tool( name="ticket_list", description="PROJECT MANAGEMENT: List tickets filtered by project or status. Use roadmap_view for full overview.", inputSchema={ "type": "object", "properties": { "project_id": {"type": "string", "description": "Filter by project ID"}, "status": { "type": "string", "enum": ["backlog", "planned", "in-progress", "done", "blocked"], "description": "Filter by status", }, }, }, ), Tool( name="ticket_get", description="""PROJECT MANAGEMENT: Get info about a ticket and its tasks. IMPORTANT: Do NOT pass detail='full' unless explicitly asked for full/all details. The default 'summary' is sufficient for most queries. Only use 'full' when user specifically asks for implementation details, metadata, or complete task information.""", inputSchema={ "type": "object", "properties": { "ticket_id": {"type": "string", "description": "Ticket ID (e.g., FEAT-001)"}, "detail": { "type": "string", "enum": ["minimal", "summary", "full"], "description": "OMIT this param for most requests (defaults to 'summary'). Only use 'full' if user explicitly asks for all details/metadata.", "default": "summary", }, }, "required": ["ticket_id"], }, ), Tool( name="task_get", description="""PROJECT MANAGEMENT: Get full details of ONE specific task. Use this to drill into a single task's implementation details (metadata, files_to_modify, technical_notes). Prefer ticket_get for overview, use this only when you need deep task details.""", inputSchema={ "type": "object", "properties": { "task_id": { "type": "string", "description": "Task ID (e.g., SUBTASK-007-1 or TASK-abc123-1)", } }, "required": ["task_id"], }, ), # Task operations Tool( name="task_create", description="""PROJECT MANAGEMENT (TPM): Create a task (sub-item) under a ticket. USE THIS TOOL WHEN: - Breaking down a ticket into smaller tasks - User asks to add implementation steps - Creating a work breakdown structure""", inputSchema={ "type": "object", "properties": { "ticket_id": {"type": "string", "description": "Parent ticket ID"}, "title": {"type": "string", "description": "Task title"}, "details": { "type": "string", "description": "Task details/implementation notes", }, "status": { "type": "string", "enum": ["pending", "in-progress", "done", "blocked"], "description": "Task status (default: pending)", }, "priority": { "type": "string", "enum": ["critical", "high", "medium", "low"], "description": "Priority (default: medium)", }, "complexity": { "type": "string", "enum": ["simple", "medium", "complex"], "description": "Complexity estimate (default: medium)", }, }, "required": ["ticket_id", "title"], }, ), Tool( name="task_update", description="PROJECT MANAGEMENT (TPM): Update a task's status or details. Use when completing or updating task progress.", inputSchema={ "type": "object", "properties": { "task_id": {"type": "string", "description": "Task ID (e.g., TASK-001-1)"}, "title": {"type": "string", "description": "New title"}, "details": {"type": "string", "description": "New details"}, "status": { "type": "string", "enum": ["pending", "in-progress", "done", "blocked"], "description": "New status", }, "priority": { "type": "string", "enum": ["critical", "high", "medium", "low"], "description": "New priority", }, "complexity": { "type": "string", "enum": ["simple", "medium", "complex"], "description": "New complexity", }, }, "required": ["task_id"], }, ), Tool( name="task_list", description="PROJECT MANAGEMENT (TPM): List tasks filtered by ticket or status.", inputSchema={ "type": "object", "properties": { "ticket_id": {"type": "string", "description": "Filter by ticket ID"}, "status": { "type": "string", "enum": ["pending", "in-progress", "done", "blocked"], "description": "Filter by status", }, }, }, ), # Notes Tool( name="note_add", description="PROJECT MANAGEMENT (TPM): Add a note/comment to a ticket or task for context or decisions.", inputSchema={ "type": "object", "properties": { "entity_type": { "type": "string", "enum": ["org", "project", "ticket", "task"], "description": "Type of entity", }, "entity_id": {"type": "string", "description": "ID of the entity"}, "content": {"type": "string", "description": "Note content"}, }, "required": ["entity_type", "entity_id", "content"], }, ), # Org/Project operations (less frequently used) Tool( name="org_list", description="PROJECT MANAGEMENT: List all organizations. Usually only one org exists.", inputSchema={"type": "object", "properties": {}}, ), Tool( name="org_create", description="PROJECT MANAGEMENT: Create a new organization (rarely needed).", inputSchema={ "type": "object", "properties": {"name": {"type": "string", "description": "Organization name"}}, "required": ["name"], }, ), Tool( name="project_list", description="PROJECT MANAGEMENT: List projects in an organization.", inputSchema={ "type": "object", "properties": { "org_id": {"type": "string", "description": "Filter by organization ID"} }, }, ), Tool( name="project_create", description="PROJECT MANAGEMENT: Create a new project under an organization.", inputSchema={ "type": "object", "properties": { "org_id": {"type": "string", "description": "Organization ID"}, "name": {"type": "string", "description": "Project name"}, "repo_path": {"type": "string", "description": "Path to git repo"}, "description": {"type": "string", "description": "Project description"}, }, "required": ["org_id", "name"], }, ), # Info tool Tool( name="info", description="Get information about the tracker MCP server: database location, stats, and usage.", inputSchema={"type": "object", "properties": {}}, ), ] # --- Tool Handlers --- @server.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: try: result = await _handle_tool(name, arguments) return [TextContent(type="text", text=result)] except Exception as e: return [TextContent(type="text", text=f"Error: {str(e)}")] async def _handle_tool(name: str, args: dict) -> str: # Orgs if name == "org_create": org = db.create_org(OrgCreate(name=args["name"])) return f"Created org: {_json(org)}" if name == "org_list": orgs = db.list_orgs() return _json([o.model_dump() for o in orgs]) # Projects if name == "project_create": project = db.create_project( ProjectCreate( org_id=args["org_id"], name=args["name"], repo_path=args.get("repo_path"), description=args.get("description"), ) ) return f"Created project: {_json(project)}" if name == "project_list": projects = db.list_projects(args.get("org_id")) return _json([p.model_dump() for p in projects]) # Tickets if name == "ticket_create": ticket = db.create_ticket( TicketCreate( project_id=args["project_id"], title=args["title"], id=args.get("id"), description=args.get("description"), status=TicketStatus(args.get("status", "backlog")), priority=Priority(args.get("priority", "medium")), tags=args.get("tags"), assignees=args.get("assignees"), ) ) return f"Created ticket: {_json(ticket)}" if name == "ticket_list": status = TicketStatus(args["status"]) if args.get("status") else None tickets = db.list_tickets(args.get("project_id"), status) return _json([t.model_dump() for t in tickets]) if name == "ticket_update": update = TicketUpdate( title=args.get("title"), description=args.get("description"), status=TicketStatus(args["status"]) if args.get("status") else None, priority=Priority(args["priority"]) if args.get("priority") else None, tags=args.get("tags"), assignees=args.get("assignees"), ) ticket = db.update_ticket(args["ticket_id"], update) if ticket: return f"Updated ticket: {_json(ticket)}" return f"Ticket {args['ticket_id']} not found" if name == "ticket_get": ticket = db.get_ticket(args["ticket_id"]) if not ticket: return f"Ticket {args['ticket_id']} not found" detail = args.get("detail", "summary") tasks = db.list_tasks(args["ticket_id"]) if detail == "minimal": # Just the essentials - very small response return _json( { "ticket": { "id": ticket.id, "title": ticket.title, "status": ticket.status.value, "priority": ticket.priority.value, "task_count": len(tasks), "tasks_done": sum( 1 for t in tasks if t.status.value in ("done", "completed") ), } } ) elif detail == "full": # Everything - can be large return _json({"ticket": ticket.model_dump(), "tasks": [t.model_dump() for t in tasks]}) else: # summary (default) - balanced response desc = ticket.description if desc and len(desc) > 300: desc = desc[:300] + "..." return _json( { "ticket": { "id": ticket.id, "title": ticket.title, "description": desc, "status": ticket.status.value, "priority": ticket.priority.value, "tags": ticket.tags, "assignees": ticket.assignees, "acceptance_criteria": ticket.acceptance_criteria, }, "tasks": [ { "id": t.id, "title": t.title, "status": t.status.value, "priority": t.priority.value, } for t in tasks ], } ) if name == "task_get": task = db.get_task(args["task_id"]) if not task: return f"Task {args['task_id']} not found" return _json(task.model_dump()) # Tasks if name == "task_create": task = db.create_task( TaskCreate( ticket_id=args["ticket_id"], title=args["title"], details=args.get("details"), status=TaskStatus(args.get("status", "pending")), priority=Priority(args.get("priority", "medium")), complexity=Complexity(args.get("complexity", "medium")), ) ) return f"Created task: {_json(task)}" if name == "task_list": status = TaskStatus(args["status"]) if args.get("status") else None tasks = db.list_tasks(args.get("ticket_id"), status) return _json([t.model_dump() for t in tasks]) if name == "task_update": update = TaskUpdate( title=args.get("title"), details=args.get("details"), status=TaskStatus(args["status"]) if args.get("status") else None, priority=Priority(args["priority"]) if args.get("priority") else None, complexity=Complexity(args["complexity"]) if args.get("complexity") else None, ) task = db.update_task(args["task_id"], update) if task: return f"Updated task: {_json(task)}" return f"Task {args['task_id']} not found" # Notes if name == "note_add": note = db.add_note( NoteCreate( entity_type=args["entity_type"], entity_id=args["entity_id"], content=args["content"], ) ) return f"Added note: {_json(note)}" # Roadmap view if name == "roadmap_view": roadmap = db.get_roadmap(args.get("org_id")) fmt = args.get("format", "summary") if fmt == "json": return _json(roadmap) # Summary format lines = ["# Roadmap Summary\n"] lines.append( f"**Stats**: {roadmap.stats['tickets_done']}/{roadmap.stats['total_tickets']} tickets, " f"{roadmap.stats['tasks_done']}/{roadmap.stats['total_tasks']} tasks " f"({roadmap.stats['completion_pct']}% complete)\n" ) for org in roadmap.orgs: lines.append(f"## {org.name}") for proj in org.projects: lines.append(f"\n### {proj.name}") if proj.description: lines.append(f"_{proj.description}_\n") lines.append(f"Tickets: {proj.tickets_done}/{proj.ticket_count} done\n") for ticket in proj.tickets: status_icon = { "backlog": "[ ]", "planned": "[P]", "in-progress": "[~]", "done": "[x]", "blocked": "[!]", }.get(ticket.status.value, "[ ]") prio = ( f"({ticket.priority.value})" if ticket.priority.value in ["critical", "high"] else "" ) lines.append(f"- {status_icon} **{ticket.id}**: {ticket.title} {prio}") lines.append(f" Tasks: {ticket.tasks_done}/{ticket.task_count}") # Show incomplete tasks incomplete = [t for t in ticket.tasks if t.status.value != "done"] for task in incomplete[:5]: # Show max 5 t_icon = {"pending": "[ ]", "in-progress": "[~]", "blocked": "[!]"}.get( task.status.value, "[ ]" ) lines.append(f" - {t_icon} {task.id}: {task.title}") if len(incomplete) > 5: lines.append(f" - ... and {len(incomplete) - 5} more") return "\n".join(lines) # Info if name == "info": import os roadmap = db.get_roadmap() db_size = os.path.getsize(DEFAULT_DB_PATH) if DEFAULT_DB_PATH.exists() else 0 db_size_mb = db_size / (1024 * 1024) info = f"""# Tracker MCP Server ## Database - **Location**: `{DEFAULT_DB_PATH}` - **Size**: {db_size_mb:.2f} MB - **Mode**: SQLite with WAL (concurrent read/write safe) ## Current Stats - **Organizations**: {len(roadmap.orgs)} - **Projects**: {sum(len(o.projects) for o in roadmap.orgs)} - **Tickets**: {roadmap.stats.get("total_tickets", 0)} ({roadmap.stats.get("tickets_done", 0)} done) - **Tasks**: {roadmap.stats.get("total_tasks", 0)} ({roadmap.stats.get("tasks_done", 0)} done) - **Completion**: {roadmap.stats.get("completion_pct", 0)}% ## Installation Add to Claude Code with: ```bash claude mcp add tracker --scope user -- uv run --directory /path/to/tpm-mcp tpm-mcp ``` ## Available Tools - `roadmap_view` - Get full project roadmap - `ticket_create/update/list/get` - Manage tickets (features, issues, epics) - `task_create/update/list` - Manage tasks under tickets - `note_add` - Add notes to any entity - `org_list/create` - Manage organizations - `project_list/create` - Manage projects - `info` - This info page ## Migration To import from JSON project tracker: ```bash uv run python -m tpm_mcp.migrate /path/to/project-tracker ``` """ return info return f"Unknown tool: {name}" async def run_server(): """Run the MCP server with stdio transport.""" async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options()) def main(): """Run the MCP server.""" import asyncio asyncio.run(run_server()) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/urjitbhatia/tpm-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server