next.js/packages/next/src/cli/internal/query-trace.ts
query-trace.ts107 lines3.0 KB
/**
 * CLI client for querying a running turbopack trace server via its MCP endpoint.
 * Sends a JSON-RPC `tools/call` request and prints the response to stdout.
 * Use --json for machine-readable JSON output (default: markdown).
 *
 * Usage: next internal query-trace [options]
 */

const DEFAULT_MCP_PORT = 5748 // Keep in sync with turbo-trace-server.ts

interface QueryTraceOptions {
  port: number | undefined
  parent: string | undefined
  aggregated: boolean | undefined
  sort: string | undefined
  search: string | undefined
  page: number | undefined
  json: boolean | undefined
}

export async function queryTraceCli(options: QueryTraceOptions): Promise<void> {
  const port = options.port ?? DEFAULT_MCP_PORT

  // Build arguments — only include values explicitly set by the user.
  const args: Record<string, unknown> = {}
  if (options.parent !== undefined) args.parent = options.parent
  if (options.aggregated !== undefined) args.aggregated = options.aggregated
  if (options.sort !== undefined) args.sort = options.sort
  if (options.search !== undefined) args.search = options.search
  if (options.page !== undefined) args.page = options.page
  if (options.json) args.outputType = 'json'

  const requestBody = JSON.stringify({
    jsonrpc: '2.0',
    method: 'tools/call',
    params: {
      name: 'query_spans',
      arguments: args,
    },
    id: 1,
  })

  let res: Response
  try {
    res = await fetch(`http://127.0.0.1:${port}/mcp`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json, text/event-stream',
      },
      body: requestBody,
    })
  } catch {
    console.error(
      [
        `Error: Could not connect to trace server on port ${port}.`,
        '',
        'Make sure the trace server is running in the background:',
        '',
        `  next internal trace <file> --mcp-port ${port}`,
        '',
        'Then run your query in another terminal. For all available options, see:',
        '',
        '  next internal query-trace --help',
      ].join('\n')
    )
    process.exit(1)
  }

  if (!res.ok) {
    console.error(`Error: MCP server responded with HTTP ${res.status}`)
    process.exit(1)
  }

  const body = await res.text()

  // Parse SSE stream: each event looks like:
  //   event: message
  //   data: <JSON-RPC response JSON>
  //
  // We scan for "data: " lines, parse the JSON, and extract the text content.
  for (const line of body.split('\n')) {
    if (!line.startsWith('data: ')) continue
    let msg: {
      result?: { content?: Array<{ type: string; text?: string }> }
      error?: unknown
    }
    try {
      msg = JSON.parse(line.slice('data: '.length))
    } catch {
      continue
    }
    if (msg.error) {
      console.error(`Error: MCP tool error: ${JSON.stringify(msg.error)}`)
      process.exit(1)
    }
    const text = msg.result?.content?.find((c) => c.type === 'text')?.text
    if (text !== undefined) {
      process.stdout.write(text)
      return
    }
  }

  console.error(`Error: No text content in MCP response:\n${body}`)
  process.exit(1)
}
Quest for Codev2.0.0
/
SIGN IN