Skip to main content
Glama
Jameswlepage

WordPress Trac MCP Server

by Jameswlepage

searchTickets

Locate WordPress Trac tickets by keyword or filter criteria. Retrieve ticket summaries with essential details, filtered by status or component, for efficient WordPress development tracking.

Instructions

Search for WordPress Trac tickets by keyword or filter expression. Returns ticket summaries with basic info.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
componentNoFilter by component name (e.g., 'Administration', 'Posts, Post Types')
limitNoMaximum number of results to return (default: 10, max: 50)
queryYesSearch query for tickets (keywords or filter expressions like 'summary~=keyword')
statusNoFilter by ticket status (e.g., 'open', 'closed', 'new')

Implementation Reference

  • Main execution logic for the searchTickets tool. Constructs WordPress Trac query URL with parameters, fetches CSV data, implements fallback client-side filtering if server blocks search params, parses CSV into structured ticket objects with id, title, url, metadata.
    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 response, defining name, description, and input schema for searchTickets.
    { 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"], }, },
  • Input schema definition for searchTickets tool, specifying parameters like query (required), limit, status, component with types and descriptions.
    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"], },
  • CSV parsing utility function used by searchTickets to parse ticket data from Trac query CSV responses, handling quoted fields properly.
    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; }

Other Tools

Related Tools

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/Jameswlepage/trac-mcp'

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