Integrations

Connect Xal to local usage dashboards, language servers for semantic code intelligence, and MCP servers for external tools, resources, and prompts.

Local usage dashboards

Xal writes one prompt-free usage record after each provider request that reports token counts. Records are JSONL files under $XAL_HOME/usage/, or ~/.xal/usage/ by default. Each Xal process owns a separate file so concurrent sessions never contend for the same log.

{
  "type": "provider_usage",
  "version": 2,
  "id": "…",
  "timestamp": "2026-08-22T12:34:56.000Z",
  "session": "4d2a…",
  "provider": "openai-chatgpt",
  "model": "gpt-5.6-sol",
  "phase": "turn",
  "outcome": "completed",
  "usage": { "totalInputTokens": 120, "cacheReadInputTokens": 80, "cacheWriteInputTokens": 0, "outputTokens": 15 }
}

totalInputTokens includes cached input. The cache-read and cache-write fields identify the subsets that may be priced differently. outputTokens is the provider-reported output total. phase distinguishes normal turns from compaction and goal-evaluation requests, while outcome preserves requests that reported usage before failing or being interrupted. session is a one-way fingerprint used to group records without storing the real session ID. Version-1 records remain valid and contribute to rolling, lifetime, and chart totals, but they cannot be attributed to a session.

The built-in /usage dashboard reads this ledger directly. Its default value is totalInputTokens + outputTokens, so cache-heavy requests contribute their full provider-processed token count. The uncached lifetime total and cache-read share remain visible, and M switches the chart and primary totals to max(0, totalInputTokens - cacheReadInputTokens) + outputTokens. Today, Yesterday, and daily chart buckets use the machine's local calendar dates, Last 7d is a rolling seven-day window, and Lifetime spans every local ledger file across all Xal projects. The openai filter includes both openai Platform API records and openai-chatgpt subscription records; openai-api and chatgpt select those sources individually. These are local processing records, not provider account quotas, invoices, or usage that occurred outside Xal.

The ledger contains no prompts, responses, working directories, real session IDs, profile names, credentials, or account identifiers. Files and directories are created with user-only permissions. Xal flushes pending records during shutdown and exits with an error if the ledger could not be written. Existing session transcripts are not backfilled, so collection starts with the first run of a Xal version that supports the ledger.

Usage dashboards should read this native ledger instead of treating Xal as Codex or asking Xal to write into another tool's home directory. The provider field lets a dashboard attribute usage to each supported provider without coupling to Xal's full session format.

Profiler records

Launch Xal with --profile to write an execution profile under $XAL_HOME/profiler/, or ~/.xal/profiler/ by default. Profiles use one JSON object per line and measure elapsed time as atMs from the start of the process. Session, request, tool, provider, model, mode, profile, hook, batch, and job values are replaced with run-local labels such as session-1 and request-2.

Provider request lifecycle records preserve the session kind, request phase, attempt number, time to first event, total provider time, outcome, and provider-reported usage. Each attempt also receives one provider_request_shape record with numeric item counts and estimated tokens split across user, assistant, reasoning, tool-call, and tool-result inputs, plus instruction bytes, tool count, schema bytes, and the total estimated request size. Retries use distinct request labels.

tool_output_shape records report original and model-visible byte counts, the visible token estimate, and whether the normal output bound changed the result. compaction_shape records report the trigger, strategy, outcome, measured and estimated context sizes, retained authored-user counts and tokens, summary estimate, and numeric removal counts by item type. Both primary and subagent sessions carry their kind on these records.

Profiler records never contain prompt, response, summary, tool-output, schema, path, working-directory, argument, call-ID, real session-ID, provider-profile, or credential content. The profiler logs a redacted error and stops recording if its writer fails; a profiler failure does not alter or retry the provider request. Use bun benchmark:context -- --fixture scripts/fixtures/context-efficiency-v1.json --policy legacy to replay the committed content-free numeric baseline. Regenerate a fixture only from explicit local inputs with bun benchmark:context -- --generate --profiler PROFILE_DIRECTORY --sessions SESSION_DIRECTORY --context-window TOKENS --output FIXTURE_JSON; generation derives all workload events and the legacy baseline from matching profiler and session observations, fails when they cannot be matched uniquely, and never copies a template. Live results retain anonymous provider/model labels, a connection/model/thinking configuration fingerprint, and a per-scenario workload fingerprint so paired comparisons reject changed configuration while release runs may include additional scenarios. Paired release comparisons gate the sum of provider-reported totalInputTokens without cache credit because provider cache attribution is best-effort. They retain cache-adjusted input as diagnostic evidence and enforce latency and continuation per workload. Use bun scripts/prompt-budget.ts to report how many characters the composed system prompt and the available tool schemas occupy for a given session shape under the current configuration, including configured plugins; it accepts --mode, --kind, and --headless, and is the measurement behind any claim about prompt or tool-surface size.

Task evaluations

bun eval runs the task suite in scripts/evals/cases. Each case is a directory holding a seed repo/, a prompt.md, and a check.ts that default-exports a predicate over the mutated workspace. Every run copies the seed into a temporary Git repository, drives it with xal run --format jsonl, and scores the result programmatically.

The runner bounds its own work, because the primary agent loop has no round limit: --max-rounds (default 40) and --timeout seconds (default 300) kill a run that overruns and score it as a failure, and a run whose xal run exits unsuccessfully is scored as a failure before its check runs. Other options are --case NAME to select cases, --runs N for repeats per case (default 3), --mode, --provider, --model, --connection, --output FILE, --baseline FILE, and --min-pass-rate. A malformed --baseline report or a --min-pass-rate outside 0 to 1 stops the evaluation before any case runs, and a run that writes anything other than JSONL events to stdout or a check that returns a malformed verdict stops it with an error instead of being scored.

The report carries the pass rate, the spread across repeats, and uncached input tokens. Spread matters: a difference no larger than the run-to-run spread is noise, and --baseline says so explicitly rather than reporting a delta. Uncached input tokens are the signal for whether a change to the prompt or the tool surface is costing more in cache misses than it saves.

bun eval needs a connected provider profile and spends real tokens, so it is deliberately excluded from bun checks. A headless run builds its session with interactive: false, so task, wait_agent, request_user_input, and submit_plan are unavailable; evaluations measure the core loop, not the interactive one.

Language servers

Xal includes lazy language-server recipes for common languages:

IDFile suffixesCommandInstallation
typescript.ts, .tsx, .mts, .cts, .js, .jsx, .mjs, .cjstypescript-language-servernpm install --global typescript-language-server typescript
python.py, .pyipyright-langservernpm install --global pyright
rust.rsrust-analyzerrustup component add rust-analyzer
go.gogoplsgo install golang.org/x/tools/gopls@latest

Xal checks for these commands on PATH, but never downloads or installs them. An installed recipe remains idle until the model queries a matching file. /lsp reports missing commands as unavailable with their installation guidance.

Configure servers

Configure built-in overrides and custom servers under pluginConfig.lsp.servers. A built-in entry inherits every omitted recipe field, and enabled: false disables it. Custom server names must begin with a lower-case letter and contain only lower-case letters, numbers, hyphens, and underscores.

{
  "pluginConfig": {
    "lsp": {
      "servers": {
        "typescript": {
          "rootMarkers": ["tsconfig.json", "jsconfig.json", "package.json", ".git"],
          "timeoutMs": 45000
        },
        "python": {
          "enabled": false
        },
        "lua": {
          "command": "lua-language-server",
          "fileTypes": {
            ".lua": "lua"
          },
          "rootMarkers": [".luarc.json", ".git"]
        }
      }
    }
  }
}

An enabled custom server requires command and a non-empty fileTypes object mapping filename suffixes to LSP language IDs. Commands must be executable names resolved through PATH or absolute paths. Relative executable paths are rejected because servers run from detected project roots. A suffix can belong to only one enabled server, so disable a built-in recipe before assigning its suffixes to a differently named replacement.

args and env are optional, custom rootMarkers default to [".git"], timeoutMs defaults to 30000, and enabled defaults to true. Supplying args, fileTypes, or rootMarkers on a built-in replaces that recipe field. initializationOptions are passed during the LSP handshake; settings are sent with workspace/didChangeConfiguration after initialization. ${NAME} references in the command, arguments, and environment values expand from Xal's environment, and secret-like environment values enter its redaction set.

Runtime behavior

The read-only lsp model tool supports definitions, references, hover information, document and workspace symbols, implementations, incoming and outgoing calls, and diagnostics. It starts one server lazily for each matching server and project root. Before every request, Xal reads the current file from disk and synchronizes changed content through the notifications supported by the server. The diagnostics operation uses pull diagnostics when supported and otherwise waits briefly for published diagnostics.

For each file, Xal searches upward for the nearest configured root marker. If none is found, it uses the session working directory for files inside that workspace and the file's directory for external files. /lsp shows disabled, unavailable, idle, ready, and failed servers. /lsp restart [server] closes matching instances; the next semantic query starts them again. The model-facing tool is available when at least one enabled server command resolves. Language-server commands run as trusted local processes with the server root as their working directory, so only configure executables you trust. Xal closes every started server during shutdown.

MCP servers

MCP servers are configured under pluginConfig.mcp.servers. Server names must begin with a lower-case letter and contain only lower-case letters, numbers, hyphens, and underscores.

{
  "pluginConfig": {
    "mcp": {
      "servers": {
        "local-tools": {
          "transport": "stdio",
          "command": "node",
          "args": ["/absolute/path/to/server.js"],
          "cwd": "/absolute/path/to/project",
          "env": {
            "SERVICE_TOKEN": "${SERVICE_TOKEN}"
          },
          "timeoutMs": 30000
        },
        "remote-tools": {
          "transport": "http",
          "url": "https://example.com/mcp",
          "headers": {
            "Authorization": "Bearer ${MCP_TOKEN}"
          }
        }
      }
    }
  }
}

Each server supports enabled, which defaults to true, and timeoutMs, which defaults to 30000. A stdio server requires command, accepts optional args, cwd, and env, and inherits Xal's process environment with the configured values applied. Relative cwd values resolve from the directory where Xal starts. Xal bounds stdio messages, captures recent server stderr for failures, and terminates the server process tree during shutdown. An HTTP server requires url and accepts optional headers; Xal tries Streamable HTTP first and falls back to legacy SSE at the same URL only when the initial Streamable HTTP request receives a 4xx response. HTTP redirects are rejected so authorization and custom headers cannot be replayed to another destination, and response bodies and SSE events are size-bounded.

If a higher-priority configuration changes an existing server's transport, fields inherited for the inactive transport are ignored. Unknown field names still fail configuration.

${NAME} references in commands, arguments, working directories, environment values, URLs, and headers expand from Xal's environment. A missing variable makes the MCP configuration fail instead of starting with an incomplete value. Values in secret-like environment variables and headers are added to Xal's redaction set.

Project .mcp.json discovery

On an interactive launch, Xal checks for .mcp.json at the detected project root after the workspace is trusted and before plugins or the session start. The file uses the common mcpServers object:

{
  "mcpServers": {
    "local-tools": {
      "type": "stdio",
      "command": "node",
      "args": ["server.js"],
      "env": {
        "SERVICE_TOKEN": "${SERVICE_TOKEN}"
      }
    },
    "remote-tools": {
      "type": "http",
      "url": "https://example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_TOKEN}"
      }
    }
  }
}

The type field may be omitted for stdio servers. http and streamable-http both import as Xal's HTTP transport. Stdio entries accept command, args, cwd, env, enabled, and timeoutMs. HTTP entries accept url, headers, enabled, and timeoutMs. Names follow the same lower-case rules as native Xal MCP configuration. Unknown fields and malformed values fail startup.

When the file contains names that are not already configured in Xal, the launch chooser offers four actions:

Existing Xal server names always win and are never overwritten by .mcp.json. After every discovered name has been imported, Xal does not prompt again. Environment references are copied without expansion so secrets are not written into configuration. Noninteractive commands do not use unapproved discovered servers and print a message explaining how to approve them interactively.

MCP runtime behavior

Servers connect in parallel during plugin bootstrap. One unavailable server is reported as failed without hiding capabilities from healthy servers. MCP tool definitions and server instructions are deferred instead of sending every remote method and operating guide to the model up front. The model receives mcp_tool_search, which searches cached tool metadata and loads only matching definitions for its next call. That next request also includes system instructions from the servers that own those matches. Loaded tools use names such as mcp__local-tools__count, retain their remote input schemas, and remain available for that session. Remote MCP calls pass through normal permission handling and run without confirmation in normal mode unless an explicit permission rule asks or denies them. They are still treated as unsandboxed mutations and invalidate workspace redo history because server annotations are untrusted hints and the tool's effects are external or unknown. Reading a remote resource or resolving a remote prompt follows the same policy; listing their already-cached catalogs remains read-only.

Connected resource catalogs, resource templates, and prompts are exposed through mcp_resources, mcp_read_resource, mcp_prompts, and mcp_get_prompt. Binary resource and image or audio content is summarized with its media type and byte size because Xal's tool-result boundary is text-only. Catalog pagination is bounded by page count, item count, cursor size, and the server timeout. Tools whose output schema uses an unsupported dialect are skipped and reported in status. Xal does not currently advertise the MCP Tasks extension. Tools with the obsolete execution.taskSupport: "required" field still appear in the catalog, but calls fail if the server refuses a synchronous result because it requires Tasks support.

Tool-list change notifications refresh registered tools, and /mcp reconnect [server] reconnects one server or all servers. Run /mcp to open a searchable server list with transport, status, and capability counts. Selecting a server offers reconnect and delete actions. Delete requires confirmation, disconnects the server, unregisters its tools, and removes its definition from the project or global Xal configuration file that supplied it. A session-only discovered server is removed only from the current process. If deleting a project override reveals a same-name global definition, that global server becomes effective on the next launch.