ticket_search
Search project tickets by keywords in title or description, with filters for project, status, priority, and tags to find relevant issues.
Instructions
PROJECT MANAGEMENT (TPM): Search tickets by keyword.
USE THIS TOOL WHEN:
User asks "find tickets about X" or "search for Y"
Looking for tickets by keywords in title or description
Need to discover relevant tickets across projects
Searches title and description. Supports prefix matching (e.g., "org" matches "organization"). Case-insensitive. All filters are combinable.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search query (keywords to search in title/description) | |
| project_id | No | Filter by project ID (optional, case-insensitive) | |
| status | No | Filter by status (optional) | |
| priority | No | Filter by priority (optional) | |
| tags | No | Filter by tags - ticket must have all specified tags (optional) | |
| limit | No | Maximum results to return (default: 20, max: 100) |
Implementation Reference
- src/tpm_mcp/server.py:210-255 (registration)Registration of the 'ticket_search' tool in the list_tools() function, including name, description, and JSON inputSchema.Tool( name="ticket_search", description="""PROJECT MANAGEMENT (TPM): Search tickets by keyword. USE THIS TOOL WHEN: - User asks "find tickets about X" or "search for Y" - Looking for tickets by keywords in title or description - Need to discover relevant tickets across projects Searches title and description. Supports prefix matching (e.g., "org" matches "organization"). Case-insensitive. All filters are combinable.""", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "Search query (keywords to search in title/description)", }, "project_id": { "type": "string", "description": "Filter by project ID (optional, case-insensitive)", }, "status": { "type": "string", "enum": ["backlog", "planned", "in-progress", "done", "blocked"], "description": "Filter by status (optional)", }, "priority": { "type": "string", "enum": ["critical", "high", "medium", "low"], "description": "Filter by priority (optional)", }, "tags": { "type": "array", "items": {"type": "string"}, "description": "Filter by tags - ticket must have all specified tags (optional)", }, "limit": { "type": "integer", "description": "Maximum results to return (default: 20, max: 100)", "default": 20, }, }, "required": ["query"], }, ),
- src/tpm_mcp/server.py:560-579 (handler)Handler logic in _handle_tool() that processes arguments, normalizes them, calls db.search_tickets(), and formats the JSON response.if name == "ticket_search": limit = min(args.get("limit", 20), 100) status = TicketStatus(args["status"]) if args.get("status") else None priority = Priority(args["priority"]) if args.get("priority") else None results = db.search_tickets( query=args["query"], project_id=args.get("project_id"), status=status, priority=priority, tags=args.get("tags"), limit=limit, ) return _json({ "results": results, "total": len(results), "query": args["query"], })
- src/tpm_mcp/db.py:438-524 (helper)Core implementation of ticket search using SQLite FTS5 full-text search on titles/descriptions, with dynamic filters for project, status, priority, tags, and limit. Returns highlighted snippets.def search_tickets( self, query: str, project_id: str | None = None, status: TicketStatus | None = None, priority: Priority | None = None, tags: list[str] | None = None, limit: int = 20, ) -> list[dict]: """Search tickets using full-text search with optional filters. Args: query: Search query (supports prefix matching) project_id: Filter by project ID (case-insensitive) status: Filter by ticket status priority: Filter by priority level tags: Filter by tags (ticket must have all specified tags) limit: Maximum results to return (default 20) Returns: List of dicts with: id, title, project_id, status, priority, tags, snippet """ if not query or not query.strip(): return [] # Build FTS5 query with prefix matching for each term terms = query.strip().split() fts_query = " ".join(f"{term}*" for term in terms) # Build the SQL query with joins and filters sql = """ SELECT t.id, t.title, t.project_id, t.status, t.priority, t.tags, snippet(tickets_fts, 1, '<b>', '</b>', '...', 32) as snippet FROM tickets_fts JOIN tickets t ON tickets_fts.ticket_id = t.id WHERE tickets_fts MATCH ? """ params: list = [fts_query] # Add filters if project_id: project_id = self._normalize_id(project_id) sql += " AND LOWER(t.project_id) = ?" params.append(project_id) if status: sql += " AND t.status = ?" params.append(status.value) if priority: sql += " AND t.priority = ?" params.append(priority.value) if tags: # Check that ticket has all specified tags using json_each for tag in tags: sql += " AND EXISTS (SELECT 1 FROM json_each(t.tags) WHERE value = ?)" params.append(tag) # Order by relevance (FTS5 rank) and limit sql += " ORDER BY rank LIMIT ?" params.append(limit) try: rows = self.conn.execute(sql, params).fetchall() return [ { "id": r["id"], "title": r["title"], "project_id": r["project_id"], "status": r["status"], "priority": r["priority"], "tags": _from_json(r["tags"]), "snippet": r["snippet"], } for r in rows ] except Exception: # Handle FTS5 syntax errors gracefully return []