Skip to main content
Glama
Jameswlepage

WordPress Trac MCP Server

by Jameswlepage

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

Implementation Reference

  • 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"],
      },
    },
  • 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"],
    },
  • 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;
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It mentions the return format ('ticket summaries with basic info') but lacks critical behavioral details: it doesn't specify if results are paginated, sorted, or limited beyond the 'limit' parameter; doesn't mention authentication needs, rate limits, or error handling; and doesn't clarify what 'basic info' includes. For a search tool with no annotation coverage, this is insufficient.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose and key details. Every word earns its place: it specifies the action, target, method, and return format without redundancy or fluff. This is optimally concise for a search tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description is moderately complete for a search tool: it covers the basic purpose and return format. However, it lacks details on behavioral traits (e.g., pagination, errors) and doesn't fully compensate for the missing output schema by explaining what 'ticket summaries' entail. This is adequate but has clear gaps, fitting a score of 3.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents all parameters. The description adds minimal value beyond the schema by implying the query can use 'keyword or filter expression' and that results include 'ticket summaries', but it doesn't provide additional syntax examples, format details, or constraints. This meets the baseline of 3 when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb ('Search for') and resource ('WordPress Trac tickets') with the method ('by keyword or filter expression'). It distinguishes from siblings like getTicket (single ticket retrieval) and getChangeset (code changes) by focusing on multi-ticket search. However, it doesn't explicitly contrast with getTimeline or getTracInfo, keeping it at 4 rather than 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives like getTicket for single tickets or getTimeline for history. It mentions the search capability but offers no context about prerequisites, typical use cases, or exclusions. This leaves the agent without explicit direction for tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other 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