Skip to main content
Glama
Jameswlepage

WordPress Trac MCP Server

by Jameswlepage

getTicket

Retrieve detailed WordPress Trac ticket information including descriptions, comments, and metadata to track development issues and discussions.

Instructions

Get detailed information about a specific WordPress Trac ticket including description, comments, and metadata.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
idYesTrac ticket ID number
includeCommentsNoInclude ticket comments and discussion (default: true)
commentLimitNoMaximum number of comments to return (default: 10, max: 50)

Implementation Reference

  • src/index.ts:72-96 (registration)
    Registration of the 'getTicket' tool in the tools/list MCP method response, including its input schema definition.
    {
      name: "getTicket",
      description: "Get detailed information about a specific WordPress Trac ticket including description, comments, and metadata.",
      inputSchema: {
        type: "object",
        properties: {
          id: {
            type: "number",
            description: "Trac ticket ID number",
          },
          includeComments: {
            type: "boolean",
            description: "Include ticket comments and discussion (default: true)",
            default: true,
          },
          commentLimit: {
            type: "number",
            description: "Maximum number of comments to return (default: 10, max: 50)",
            default: 10,
          },
        },
        required: ["id"],
      },
    },
    {
  • Primary handler implementation for the 'getTicket' tool within the handleMcpRequest function's tools/call switch statement. Fetches ticket data using Trac's CSV query API, parses the response, extracts metadata, handles comments placeholder, and formats the output.
    case "getTicket": {
      const { id, includeComments = true } = args;
      
      try {
        // Use search approach since CSV parsing is problematic
        const searchUrl = new URL('https://core.trac.wordpress.org/query');
        searchUrl.searchParams.set('format', 'csv');
        searchUrl.searchParams.set('id', id.toString());
        searchUrl.searchParams.set('max', '1');
        
        const response = await fetch(searchUrl.toString(), {
          headers: {
            'User-Agent': 'Mozilla/5.0 (compatible; WordPress-Trac-MCP-Server/1.0)',
            'Accept': 'text/csv,text/plain,*/*',
          }
        });
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const csvData = await response.text();
        
        // Parse CSV data similar to searchTickets
        const lines = csvData.replace(/^\uFEFF/, '').trim().split(/\r?\n/);
        if (lines.length < 2) {
          throw new Error(`Ticket ${id} not found`);
        }
        
        // Parse each line like in searchTickets
        for (let i = 1; i < lines.length; i++) {
          const line = lines[i]?.trim();
          if (!line) continue;
          
          // Better CSV parsing - handle quoted fields properly
          const values = [];
          let currentField = '';
          let inQuotes = false;
          let escapeNext = false;
          
          for (let j = 0; j < line.length; j++) {
            const char = line[j];
            
            if (escapeNext) {
              currentField += char;
              escapeNext = false;
              continue;
            }
            
            if (char === '\\') {
              escapeNext = true;
              continue;
            }
            
            if (char === '"') {
              if (inQuotes) {
                // Check if this is an escaped quote
                if (j + 1 < line.length && line[j + 1] === '"') {
                  currentField += '"';
                  j++; // Skip the next quote
                } else {
                  inQuotes = false;
                }
              } else {
                inQuotes = true;
              }
            } 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 ticketId = parseInt(values[0]);
            if (ticketId === id) {
              // Map fields based on actual headers from search query
              // Headers: id,Summary,Owner,Type,Status,Priority,Milestone
              const ticket = {
                id: parseInt(values[0]),
                summary: values[1] || '',
                owner: values[2] || '',
                type: values[3] || '',
                status: values[4] || '',
                priority: values[5] || '',
                milestone: values[6] || '',
                reporter: '', // Not available in search query
                description: 'Full description not available in search query. Visit the ticket URL for complete details.',
                component: '', // Not available in search query
                version: '',
                severity: '',
                resolution: '',
                keywords: '',
                cc: '',
                focuses: '',
              };
    
              // Note: Comments are not available through the CSV API
              let comments: any[] = [];
              
              if (includeComments) {
                comments = [{
                  author: 'system',
                  timestamp: new Date().toISOString(),
                  comment: 'Comment history not available through CSV API. Visit the ticket URL for full discussion.',
                }];
              }
    
              result = {
                id: id,
                title: `#${id}: ${ticket.summary}`,
                text: `Ticket #${id}: ${ticket.summary}\n\nStatus: ${ticket.status}\nComponent: ${ticket.component}\nPriority: ${ticket.priority}\nType: ${ticket.type}\nReporter: ${ticket.reporter}\nOwner: ${ticket.owner}\nMilestone: ${ticket.milestone}\nVersion: ${ticket.version}\nKeywords: ${ticket.keywords}\n\nDescription:\n${ticket.description}\n\nFor full discussion and comments, visit: https://core.trac.wordpress.org/ticket/${id}`,
                url: `https://core.trac.wordpress.org/ticket/${id}`,
                metadata: {
                  ticket,
                  comments,
                  totalComments: comments.length,
                },
              };
              break; // Found the ticket, exit the loop
            }
          }
        }
        
        // If we didn't find the ticket, result will be undefined
        if (!result) {
          throw new Error(`Ticket ${id} not found`);
        }
      } catch (error) {
        result = {
          id: id,
          title: `Error loading ticket ${id}`,
          text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
          url: `https://core.trac.wordpress.org/ticket/${id}`,
          metadata: { error: true },
        };
      }
      break;
    }
  • Helper function getTicketForChatGPT used in the ChatGPT-specific MCP handler for fetching and formatting ticket data, similar logic to main handler but simplified for ChatGPT's search/fetch tools.
    async function getTicketForChatGPT(ticketId: number, includeComments: boolean) {
      try {
        const searchUrl = new URL('https://core.trac.wordpress.org/query');
        searchUrl.searchParams.set('format', 'csv');
        searchUrl.searchParams.set('id', ticketId.toString());
        searchUrl.searchParams.set('max', '1');
        
        const response = await fetch(searchUrl.toString(), {
          headers: {
            'User-Agent': 'Mozilla/5.0 (compatible; WordPress-Trac-MCP-Server/1.0)',
            'Accept': 'text/csv,text/plain,*/*',
          }
        });
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const csvData = await response.text();
        const lines = csvData.replace(/^\uFEFF/, '').trim().split(/\r?\n/);
        if (lines.length < 2) {
          throw new Error(`Ticket ${ticketId} not found`);
        }
        
        const values = parseCSVLine(lines[1] || '');
        if (values.length >= 2 && values[0] && parseInt(values[0]) === ticketId) {
          const ticket = {
            id: parseInt(values[0]),
            summary: values[1] || '',
            owner: values[2] || '',
            type: values[3] || '',
            status: values[4] || '',
            priority: values[5] || '',
            milestone: values[6] || '',
          };
    
          // Cache the ticket
          chatgptCache.set(ticketId.toString(), ticket);
    
          const commentNote = includeComments 
            ? "\n\nNote: Full comments and description available on the ticket page."
            : "";
    
          return {
            id: ticketId.toString(),
            title: `#${ticketId}: ${ticket.summary}`,
            text: `Ticket #${ticketId}: ${ticket.summary}\n\nStatus: ${ticket.status}\nType: ${ticket.type}\nPriority: ${ticket.priority}\nOwner: ${ticket.owner}\nMilestone: ${ticket.milestone}${commentNote}`,
            url: `https://core.trac.wordpress.org/ticket/${ticketId}`,
            metadata: { ticket },
          };
        } else {
          throw new Error(`Ticket ${ticketId} not found`);
        }
      } catch (error) {
        return {
          id: ticketId.toString(),
          title: `Error loading ticket ${ticketId}`,
          text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
          url: `https://core.trac.wordpress.org/ticket/${ticketId}`,
          metadata: { error: true },
        };
      }
    }

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