searchTickets
Search WordPress Trac tickets using keywords or filter expressions to find development issues, track progress, and monitor code changes.
Instructions
Search for WordPress Trac tickets by keyword or filter expression. Returns ticket summaries with basic info.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search query for tickets (keywords or filter expressions like 'summary~=keyword') | |
| limit | No | Maximum number of results to return (default: 10, max: 50) | |
| status | No | Filter by ticket status (e.g., 'open', 'closed', 'new') | |
| component | No | Filter by component name (e.g., 'Administration', 'Posts, Post Types') |
Implementation Reference
- src/index.ts:165-377 (handler)Main execution handler for searchTickets tool. Builds Trac query URL, fetches CSV data, handles fallback filtering, parses CSV with custom parser handling quoted fields, formats ticket summaries with metadata, and returns structured results.case "searchTickets": { const { query, limit = 10, status, component } = args; try { // Build Trac query URL const queryUrl = new URL('https://core.trac.wordpress.org/query'); queryUrl.searchParams.set('format', 'csv'); queryUrl.searchParams.set('max', Math.min(limit, 50).toString()); // Add keyword search if (query.includes('=') || query.includes('~')) { // User provided a direct filter queryUrl.searchParams.set('summary', query); } else { // Search in summary with keyword queryUrl.searchParams.set('summary', `~${query}`); } // Add status filter if (status) { queryUrl.searchParams.set('status', status); } // Add component filter if (component) { queryUrl.searchParams.set('component', component); } // Query tickets with proper headers const response = await fetch(queryUrl.toString(), { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; WordPress-Trac-MCP-Server/1.0)', 'Accept': 'text/csv,text/plain,*/*', 'Accept-Language': 'en-US,en;q=0.9', } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const csvData = await response.text(); // Check if we got HTML instead of CSV (403 error) if (csvData.includes('<html>') || csvData.includes('403 Forbidden')) { // Fallback: try without search parameters const fallbackUrl = new URL('https://core.trac.wordpress.org/query'); fallbackUrl.searchParams.set('format', 'csv'); fallbackUrl.searchParams.set('max', Math.min(limit * 3, 100).toString()); // Get more to filter const fallbackResponse = await fetch(fallbackUrl.toString(), { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; WordPress-Trac-MCP-Server/1.0)', 'Accept': 'text/csv,text/plain,*/*', 'Accept-Language': 'en-US,en;q=0.9', } }); if (!fallbackResponse.ok) { throw new Error(`Fallback query failed: ${fallbackResponse.status} ${fallbackResponse.statusText}`); } const fallbackData = await fallbackResponse.text(); if (fallbackData.includes('<html>') || fallbackData.includes('403 Forbidden')) { throw new Error('Access denied - both search and fallback queries returned HTML'); } // Filter results client-side const allLines = fallbackData.trim().split('\n'); const filteredLines = [allLines[0]]; // Keep header for (let i = 1; i < allLines.length && i < allLines.length; i++) { const line = allLines[i]; if (line && line.toLowerCase().includes(query.toLowerCase())) { filteredLines.push(line); if (filteredLines.length > limit) break; } } const result = { csvData: filteredLines.join('\n'), wasFiltered: true }; // Parse CSV data const lines = result.csvData.trim().split('\n'); if (lines.length < 2) { throw new Error('No tickets found matching search criteria'); } const tickets = []; for (let i = 1; i < lines.length; i++) { const line = lines[i]?.trim(); if (!line) continue; // Simple CSV parsing - handle quoted fields const values = []; let currentField = ''; let inQuotes = false; for (let j = 0; j < line.length; j++) { const char = line[j]; if (char === '"' && (j === 0 || line[j-1] === ',')) { inQuotes = true; } else if (char === '"' && inQuotes && (j === line.length - 1 || line[j+1] === ',')) { inQuotes = false; } else if (char === ',' && !inQuotes) { values.push(currentField.trim()); currentField = ''; } else { currentField += char; } } values.push(currentField.trim()); if (values.length >= 2 && values[0] && !isNaN(parseInt(values[0]))) { const ticket = { id: parseInt(values[0]), title: values[1] || '', text: `#${values[0]}: ${values[1] || 'No summary'}\nStatus: ${values[2] || 'unknown'}\nOwner: ${values[3] || 'unassigned'}\nType: ${values[4] || 'unknown'}\nPriority: ${values[5] || 'unknown'}\nMilestone: ${values[6] || 'none'}`, url: `https://core.trac.wordpress.org/ticket/${values[0]}`, metadata: { status: values[2] || 'unknown', owner: values[3] || 'unassigned', type: values[4] || 'unknown', priority: values[5] || 'unknown', milestone: values[6] || 'none', }, }; tickets.push(ticket); } } return { results: tickets, query, totalFound: tickets.length, returned: tickets.length, note: result.wasFiltered ? 'Results filtered client-side due to search API limitations' : undefined, }; } const queryResult = { csvData, wasFiltered: false }; // Parse CSV data const lines = queryResult.csvData.trim().split('\n'); if (lines.length < 2) { throw new Error('No tickets found or invalid CSV response'); } const tickets = []; for (let i = 1; i < lines.length; i++) { const line = lines[i]?.trim(); if (!line) continue; // Simple CSV parsing - handle quoted fields const values = []; let currentField = ''; let inQuotes = false; for (let j = 0; j < line.length; j++) { const char = line[j]; if (char === '"' && (j === 0 || line[j-1] === ',')) { inQuotes = true; } else if (char === '"' && inQuotes && (j === line.length - 1 || line[j+1] === ',')) { inQuotes = false; } else if (char === ',' && !inQuotes) { values.push(currentField.trim()); currentField = ''; } else { currentField += char; } } values.push(currentField.trim()); if (values.length >= 2 && values[0] && !isNaN(parseInt(values[0]))) { const ticket = { id: parseInt(values[0]), title: values[1] || '', text: `#${values[0]}: ${values[1] || 'No summary'}\nStatus: ${values[4] || 'unknown'}\nOwner: ${values[2] || 'unassigned'}\nType: ${values[3] || 'unknown'}\nPriority: ${values[5] || 'unknown'}\nMilestone: ${values[6] || 'none'}`, url: `https://core.trac.wordpress.org/ticket/${values[0]}`, metadata: { status: values[4] || 'unknown', owner: values[2] || 'unassigned', type: values[3] || 'unknown', priority: values[5] || 'unknown', milestone: values[6] || 'none', }, }; tickets.push(ticket); } } result = { results: tickets, query, totalFound: tickets.length, returned: tickets.length, note: queryResult.wasFiltered ? 'Results filtered client-side due to search API limitations' : undefined, }; } catch (error) { result = { results: [], query, error: error instanceof Error ? error.message : 'Unknown error', }; } break; }
- src/index.ts:45-71 (registration)Tool registration in tools/list endpoint, defining name, description, and full input schema with properties for query (required), limit, status, and component.{ name: "searchTickets", description: "Search for WordPress Trac tickets by keyword or filter expression. Returns ticket summaries with basic info.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for tickets (keywords or filter expressions like 'summary~=keyword')", }, limit: { type: "number", description: "Maximum number of results to return (default: 10, max: 50)", default: 10, }, status: { type: "string", description: "Filter by ticket status (e.g., 'open', 'closed', 'new')", }, component: { type: "string", description: "Filter by component name (e.g., 'Administration', 'Posts, Post Types')", }, }, required: ["query"], }, },
- src/index.ts:48-70 (schema)Input schema definition for searchTickets tool, specifying object with required 'query' string and optional numeric limit, string status, and string component.inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for tickets (keywords or filter expressions like 'summary~=keyword')", }, limit: { type: "number", description: "Maximum number of results to return (default: 10, max: 50)", default: 10, }, status: { type: "string", description: "Filter by ticket status (e.g., 'open', 'closed', 'new')", }, component: { type: "string", description: "Filter by component name (e.g., 'Administration', 'Posts, Post Types')", }, }, required: ["query"], },
- src/index.ts:1365-1404 (helper)Custom CSV line parser used in searchTickets handler (and others) to handle quoted fields and escapes properly during ticket data parsing.function parseCSVLine(line: string): string[] { const values = []; let currentField = ''; let inQuotes = false; let escapeNext = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (escapeNext) { currentField += char; escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { if (inQuotes) { if (i + 1 < line.length && line[i + 1] === '"') { currentField += '"'; i++; } else { inQuotes = false; } } else { inQuotes = true; } } else if (char === ',' && !inQuotes) { values.push(currentField.trim()); currentField = ''; } else { currentField += char; } } values.push(currentField.trim()); return values;