altis-cli-mcp
v0.2.2
Published
MCP server that wraps altis-cli to fetch Altis DXP logs and AWS X-Ray traces.
Maintainers
Readme
altis-cli-mcp
Ask Claude Code or Codex for Altis DXP logs or AWS X-Ray traces. Each comes back as a file plus a preview. The server runs altis-cli app logs, so it covers access logs, which altis-cli php-logs does not.
It fetches access, PHP, nginx, cron, email, database, Elasticsearch and Node.js logs. It also lists and fetches X-Ray traces through altis-cli app xray. We wrap altis-cli rather than call the Altis Dashboard API directly. The server reuses altis-cli's login, so it needs no OAuth setup of its own.
Requirements
- Node.js 24. We stay on 24 because altis-cli does not work on Node 26.
- altis-cli, installed and logged in (steps 1–2 below).
- Access to the stacks in the Altis Dashboard. The server sees only the stacks the altis-cli login can see.
- Claude Code or the Codex CLI. Other MCP clients that run local stdio servers are untested.
Installing
- Install altis-cli:
npm install -g altis-cli - Log altis-cli in by running
altis-cli app list. It opens the Altis Dashboard login in a browser and stores a token. - Install the server from npm:
npm install -g altis-cli-mcp - Register it with Claude Code or Codex, as below.
To install a specific npm version, use npm install -g altis-cli-mcp@<version> (for example, [email protected], once published). Check the releases page for release notes. A new release is available on npm after its Release workflow succeeds.
GitHub installation remains available: npm install -g --install-links github:humanmade/altis-cli-mcp#v0.2.2. Use a release tag, since main has no built dist/. Keep --install-links for GitHub installs: without it, npm 11 links the global package to its temporary git clone, deletes the clone once the install ends, and leaves altis-cli-mcp pointing at nothing. npm registry installs do not need this flag.
Do step 2 before the first fetch. Without a stored token, altis-cli opens the browser login from inside the server, and the call times out after 5 minutes if nobody completes it.
Registering with Claude Code
- Run
claude mcp add --scope user altis -- altis-cli-mcp - Run
/mcpin Claude Code and check thataltislists five tools.
We register at --scope user so the server works in every project. The default scope, local, limits it to the folder where the command ran.
Registering with Codex
Run
codex mcp add altis -- altis-cli-mcpIn
~/.codex/config.toml, addtool_timeout_sec = 300to the[mcp_servers.altis]table that step 1 wrote:[mcp_servers.altis] command = "altis-cli-mcp" tool_timeout_sec = 300Run
/mcpin Codex and check thataltisis listed.
Codex cancels a tool call after 60 seconds by default. The server gives altis-cli 5 minutes, so without step 2 Codex cuts off a slow access-log download first. codex mcp add has no timeout flag, which is why step 2 edits the file.
For a local checkout, follow Running from source, including the altis-dev config example. After editing config.toml, restart Codex and check /mcp again.
Updating to a new release
- Install the latest npm release:
npm install -g altis-cli-mcp@latest - Run
/mcpand reconnectaltis, so the client starts the new build.
When migrating from a GitHub install, first run npm uninstall -g altis-cli-mcp, even when the current install works. A GitHub install made without --install-links leaves a link that npm cannot install over, and the npm install can then fail with ENOTDIR. Later npm updates do not need the uninstall step.
To keep installing from GitHub, uninstall the old copy and install the new tag with npm install -g --install-links github:humanmade/altis-cli-mcp#<tag>.
The registration names the altis-cli-mcp command rather than a version, so an update does not need it changed.
Remove the registration only to change it, such as its scope or command, or when mcp add reports that altis already exists. Then register again:
- Claude Code: run
claude mcp remove altis -s user, then the steps in "Registering with Claude Code". - Codex: run
codex mcp remove altis, then all three steps in "Registering with Codex", including the timeout.
Fetching logs
Ask in plain language. Claude Code or Codex picks the stack, log type and date range, then calls get_logs.
Get the last hour of access logs for example-dev-01
Now the same for example-prod-01
Show the PHP logs for example-dev-01 from the last 24 hours
Each result opens with the altis-cli commands it ran:
Ran: altis-cli app logs example-dev-01 --type=access --after=2026-10-25T23:37:00Z --before=2026-10-26T00:00:00Z --format=csv --output=/var/folders/…/T/altis-cli-mcp/example-dev-01-access-2026-10-26-103700.csv.part
Ran: altis-cli app logs example-dev-01 --type=access --after=2026-10-26T00:00:00Z --before=2026-10-26T00:37:00Z --format=csv --output=/var/folders/…/T/altis-cli-mcp/example-dev-01-access-2026-10-26-103700.csv.part
Saved 3 lines (0.0 MB) to /var/folders/…/T/altis-cli-mcp/example-dev-01-access-2026-10-26-103700.csv
Preview (3 of 3 lines):
timestamp,status,uri
…To change the stack, log type or date range, say so in the conversation. The assistant calls get_logs again with the new values, and the Ran: line shows what changed.
Each Ran: line is shell-quoted, so it also runs as pasted into a terminal. A pasted access-log command writes to the --output path it names: the saved file when the range had no after, otherwise a .part file holding that one hour.
Access logs longer than an hour
The Altis API returns access logs only for the UTC hour that after falls in, however late before is. Asked for 03:30–04:30 UTC, it returned entries up to 03:59:32 and nothing later. So when after is set, get_logs runs altis-cli once per UTC hour and merges the hours into one file, which is why the example above has two Ran: lines.
- To split the range, the server parses
afterandbeforeitself. It accepts an ISO 8601 time,<n> <unit>s agowith units from seconds to weeks, ornow.beforedefaults to now. - The server stops starting new hours after 2 minutes, to finish inside Codex's 300-second tool timeout. The result then says
Stopped before <time>, and the assistant calls again from that time. - The budget is checked before each hour, so a slow final hour can still run past it. On a dev stack each hour took about 4 seconds and one call covered 30 hours. A busy production stack takes longer per hour.
- Without
after, the server makes one request and the API picks the window. When tested, that was about the last 20 minutes.
Finding slow or failing requests with X-Ray
Ask for traces the way you ask for logs. The assistant calls list_traces to find requests, then get_trace for the one worth reading.
List the 5xx traces on example-prod-01 from the last hour
Show me requests slower than 2 seconds on example-dev-01 today
Get the detail for trace 1-6adeddd6-26dd231f7f472afc0c1f9604
list_traces saves the summaries newest first. Each preview line gives the ID, start time, status, method and URL. It ends with the response time, any X-Ray flags and the first root-cause exception.
Ran: altis-cli app xray list-traces example-dev-01 '--after=1 hour ago' '--filter=(fault OR http.status >= 500)' --json --output=/var/folders/…/T/altis-cli-mcp/example-dev-01-traces-2026-10-26-154500.jsonl.part
Saved 1 traces (0.0 MB) to /var/folders/…/T/altis-cli-mcp/example-dev-01-traces-2026-10-26-154500.jsonl
Preview (1 of 1 traces):
1-6adeddd6-26dd231f7f472afc0c1f9604 2026-10-26T04:57:58+00:00 500 GET https://example.com/ 0.004s [error] E_USER_ERROR: Invalid value given for argument `lru_cache_max_items`.get_trace saves the whole trace and returns a digest of it. The digest covers:
- the request, status, duration and flags
- annotations, and object cache hits and misses
- exceptions
- the slowest SQL queries and remote calls
A one-second production request came to 0.9 MB of JSON. Most of it was an xhprof call tree of about 870 frames, which the digest leaves out. The assistant reads the file for anything else.
- Filters combine with AND. The server builds the X-Ray filter expression itself. altis-cli's filter flags join without parentheses, so its
--faultsplus another flag givesfault OR http.status >= 500 AND …. - One call returns at most 100 traces. On a busy production stack, that covered about the last 2 minutes. altis-cli prints the page token only in table mode, so
--jsonloses it. A full page ends with its earliest start time instead. The assistant then calls again with that time asbefore. errorsandfaultsfollow X-Ray's flags, not status codes. X-Ray defineserroras 4xx andfaultas 5xx. Yet Altis flagged a 500 with a PHPE_USER_ERRORaserror, notfault. Sofaultsalso matches any status of 500 or above, as altis-cli's--faultsdoes.- A pasted
list_tracescommand writes a.partfile. altis-cli writes the page there as one JSON array. The server rewrites it as JSON Lines and deletes the.partfile.
Where the saved files go
Every tool that fetches writes to $TMPDIR/altis-cli-mcp/. A name already taken gets -2 before the extension, the next -3, and so on. Saving to a file keeps a large download out of the model's context. The assistant reads the file when the preview is not enough.
| Tool or log type | File name | File format |
| ---------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| get_logs, access | <stack>-access-<YYYY-MM-DD-HHMMSS>.<ext> | .csv by default, .json with format: json. With after, the hours are merged: CSV keeps one header row, and JSON stays one array with one entry per line. Without after, the file is saved exactly as altis-cli downloads it |
| get_logs, every other type | <stack>-<type>-<YYYY-MM-DD-HHMMSS>.jsonl | JSON Lines, one entry per line, so the line count is the entry count |
| list_traces | <stack>-traces-<YYYY-MM-DD-HHMMSS>.jsonl | JSON Lines, one trace summary per line, newest first |
| get_trace | <stack>-trace-<trace ID>.json | The trace as altis-cli writes it, pretty-printed |
The timestamp is the local time of the fetch, so names sort by when they were fetched. get_logs returns the path, the size, the line count and up to the first 20 lines. Its preview defaults to a 16 KiB size limit; set preview_kib to change it, from 1 to 256 KiB. The preview stops at whichever limit comes first: preview_lines or preview_kib. The size limit includes the notice Preview truncated. when output is cut short. A line may be cut short, but UTF-8 characters stay intact. The saved file is always complete.
Files stay until cleanup_logs deletes them, and the assistant calls it only when asked. Ask it to "delete the saved Altis logs and traces", or to delete only those older than a day.
Reference
Tools
| Tool | Runs | Returns |
| -------------- | ----------------------------------------- | ------------------------------------------------------------------ |
| list_stacks | altis-cli app list | Each stack's ID, repo, branch, latest commit and Altis version |
| get_logs | altis-cli app logs | The command, the file path, the line count and a preview |
| list_traces | altis-cli app xray list-traces | The command, the file path, the trace count and one line per trace |
| get_trace | altis-cli app xray trace | The command, the file path and a digest of the trace |
| cleanup_logs | Deletes files in $TMPDIR/altis-cli-mcp/ | How many files it deleted and the space freed |
get_logs parameters
| Parameter | Applies to | Default | Meaning |
| --------------- | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- |
| stack | All types | Required | Application ID, such as example-dev-01. Lowercase letters, digits and hyphens only |
| type | All types | Required | One of the log types below |
| after | All types | None | Start of the range. Relative values such as 1 hour ago work. For access, an ISO 8601 time, <n> <unit>s ago or now |
| before | All types | None | End of the range. For access with after, defaults to now |
| format | access | csv | csv or json |
| filter | access | None | Field/value pairs, each sent as --filter key=value |
| search | Every type except access | None | CloudWatch Logs filter pattern |
| status | cron, email | None | fail, success, or both |
| next_token | Every type except access | None | Page token from a previous result's "More entries exist" line |
| preview_lines | All types | 20 | Maximum lines returned inline, 0–200. 0 returns no preview |
| preview_kib | All types | 16 | Maximum preview size in KiB, an integer from 1–256, including its truncation notice. Both preview limits apply |
Log types: access, php, nginx, cron, raw-cron, email, db-general, db-error, db-slowquery, es-search, es-index, es-error, nodejs.
list_traces parameters
| Parameter | Default | Meaning |
| ------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| stack | Required | Application ID, as for get_logs |
| after | 15 minutes ago | Start of the range. An ISO 8601 time or a relative value such as 1 hour ago |
| before | Now | End of the range |
| errors | None | Only traces X-Ray flags as error |
| faults | None | Only traces flagged fault, or with status 500 or above |
| min_response_time | None | Only traces whose response took at least this many seconds |
| status | None | Only traces with exactly this HTTP status, 100–599 |
| url_contains | None | Only traces whose URL contains this text. Double quotes and backslashes are rejected |
| method | None | GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS |
| filter | None | A raw X-Ray filter expression, such as annotation[fpmQueueTime] > 0.5. Wrapped in parentheses and combined with the others by AND |
| preview_lines | 20 | Traces listed inline, 0–100 |
get_trace parameters
| Parameter | Default | Meaning |
| ------------ | -------- | ---------------------------------------------------------------------------------------- |
| stack | Required | Application ID, as for get_logs |
| id | Required | X-Ray trace ID, such as 1-6adeddd6-26dd231f7f472afc0c1f9604 |
| list_limit | 10 | How many exceptions, slowest SQL queries and slowest remote calls the digest lists, 0–50 |
cleanup_logs parameters
| Parameter | Default | Meaning |
| ------------------ | ------- | ------------------------------------------------------------------------------------------ |
| older_than_hours | 0 | Delete only files last modified at least this many hours ago. 0 deletes every saved file |
It deletes files directly inside $TMPDIR/altis-cli-mcp/ and nothing else.
Known limits
- Absolute dates are confirmed for access logs only. ISO 8601 times such as
2026-10-26T04:00:00Zwork there. Other types passafterandbeforeto the API unparsed, and only relative values such as1 hour agohave been tested. filterfields are undocumented. altis-cli does not list which access-log fields the API filters on.- Access logs have no search or page token. Narrow them with
after,beforeandfilter. Ranges past one hour are split by hour, as in "Access logs longer than an hour". - No live tail. The server does not expose altis-cli's
--tail. When we tested it, the Dashboard stream replayed 100 lines from 14 days earlier and sent nothing new in 20 seconds. A tool call also returns only once, so ask forget_logswithaftera few minutes back instead. - X-Ray paging can repeat or skip a trace. Stepping back with
beforeworks at one-second precision. Traces that started in the boundary second can land on both pages or on neither. - No X-Ray statistics or service graph. altis-cli fetches both only inside its interactive dashboard. No command prints them.
- The remote-call part of the
get_tracedigest is untested. No trace we fetched had a remote HTTP call. The digest readshttp.request.urlon subsegments, as the X-Ray segment format places it. - The server and altis-cli must run under Node 24. nvm keeps global packages per Node version, so under another version the client may not find
altis-cli-mcpor altis-cli. Start the client from a Node 24 shell, or set the server's Node executable andPATHexplicitly, as in "Running from source".
When a call fails
| Message | Cause | Fix |
| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| altis-cli was not found on PATH | altis-cli is missing, or the MCP client did not inherit the shell's PATH | Install altis-cli. Claude Desktop may not see the shell's PATH; Claude Code started from a terminal does |
| altis-cli timed out after 300s | altis-cli is waiting for a browser login, or the download ran past 5 minutes | Run altis-cli app list in a terminal to log in, then retry |
| Codex reports a tool timeout after about 60 seconds | tool_timeout_sec is missing from the Codex config | Add tool_timeout_sec = 300, as in "Registering with Codex" |
| after "…" is not a time | An access-log after or before the server cannot parse | Use an ISO 8601 time, <n> <unit>s ago, or now |
| after (…) must be earlier than before (…) | The access-log range is empty or reversed | Swap the two values, or widen the range |
| … has a line that is not one JSON entry | The API changed how it lays out access logs as JSON, so the hours cannot be merged | Use format: csv, and report the message as a bug |
| altis-cli app logs … failed: or altis-cli app xray … failed: followed by a message | altis-cli or the Altis API rejected the request | Read the message after failed:. It comes from altis-cli unchanged |
| … failed: followed by Error: Error Location: … Invalid input symbol | X-Ray could not parse the filter expression | Check the expression against the X-Ray filter syntax linked in the list_traces parameters |
| … printed JSON that is not a list of traces or … not a trace with segments | The X-Ray API returned a shape the server does not expect | Report the message as a bug |
| /mcp in Claude Code reports Failed to reconnect to <name>: ENOENT after a GitHub install | npm installed without --install-links, so $(npm prefix -g)/lib/node_modules/altis-cli-mcp links to a deleted git-clone folder | Run npm uninstall -g altis-cli-mcp, then install step 3 again with --install-links |
| npm install -g fails with ENOTDIR: not a directory, rename '…/altis-cli-mcp' | The existing install is a link left by an install without --install-links | Run npm uninstall -g altis-cli-mcp, then install again |
| /mcp in Claude Code reports Failed to reconnect to <name>: CONNECTION_CLOSED | The server exited before connecting. A development registration pointing at the project folder does this | Register the file by full path, as in "Running from source". For any other cause, run the registered command in a terminal and read its error |
Running from source
- Switch to Node 24. The project's
.nvmrcpins it. - Install dependencies:
npm install - Register the TypeScript entry point. Node 24 runs it without a build:
claude mcp add altis-dev -- node /path/to/altis-cli-mcp/src/server.ts
codex mcp add altis-dev -- node /path/to/altis-cli-mcp/src/server.tsGive node the file, not the project folder. package.json has no main field, so node /path/to/altis-cli-mcp exits without starting the server. The dist/server.js that npm run build writes works in place of src/server.ts.
For Codex, add the timeout to [mcp_servers.altis-dev] in ~/.codex/config.toml. If Codex inherits a different Node version, pin both the executable and PATH:
[mcp_servers.altis-dev]
command = "/path/to/node24/bin/node"
args = ["/path/to/altis-cli-mcp/src/server.ts"]
tool_timeout_sec = 300
env = { PATH = "/path/to/node24/bin:/usr/local/bin:/usr/bin:/bin" }Replace the example paths with absolute paths on your machine. Keep Node 24's bin directory first on PATH so altis-cli also uses Node 24, and include the directory containing altis-cli if it is installed elsewhere. Restart Codex after changing the config, then check that /mcp lists altis-dev.
To run the installed altis-cli-mcp command, as an npm package or release tag provides it, build and install the working copy:
npm run build
npm install -g .npm links the global command to this folder rather than copying it, so a later npm run build takes effect without reinstalling. Register it as in "Installing".
Code quality
Run these checks under Node 24 before submitting a change:
| Command | Checks |
| ---------------------- | --------------------------------------------------------- |
| npm run lint | Type-aware ESLint and Markdownlint |
| npm run lint:js:fix | Applies ESLint fixes |
| npm run format:check | WordPress formatting, including spaces inside parentheses |
| npm run format | Applies formatting |
| npm run build | TypeScript 7 type checking and compilation |
GitHub Actions runs linting, formatting checks and the build on pull requests and pushes to main. Generated dist/ files are excluded from linting and formatting.
The build stays on TypeScript 7. ESLint still needs the TypeScript 6 compiler API, so the dependencies follow Microsoft's side-by-side setup: @typescript/native supplies TypeScript 7, and typescript aliases @typescript/typescript6 for linting. npm run build invokes the TypeScript 7 compiler explicitly because the compatibility package's dependencies also install a tsc binary. This avoids forcing an unsupported TypeScript version into typescript-eslint.
Releasing
- Raise the version in
package.json, in theMcpServerconstructor insrc/server.ts, and in the pinned npm and GitHub installation examples above. Updatepackage-lock.jsonwithnpm install --package-lock-only. Merge tomainafter CI passes. - Wait for the Build to "release" branch workflow to finish. On every push to
mainit mergesmaininto thereleasebranch, builds under Node 24 and commitsdist/there. - Run the Tag and Release workflow from the repository's Actions tab, giving a stable version tag such as
v0.2.2. - Review the draft on the releases page, then publish it in the GitHub UI. This triggers the Release workflow in
.github/workflows/release.yml, based on Altis CLI's npm release workflow. - Wait for Release to succeed before announcing npm availability. It checks out the release tag, verifies both version fields match it, installs dependencies, runs linting, formatting and the build under Node 24, runs an advisory security audit, and publishes to npm with provenance.
Tag and Release works from the release branch. It stops if the tag already exists, if either version differs from the tag, or if dist/ is missing. Otherwise it tags the checked commit and creates a draft release targeting release. The tag carries built dist/ for GitHub installs. Its notes open with npm install and update commands, followed by notes generated from merged pull requests. Running it before step 2 finishes tags the previous build, which the version check catches when the version was raised.
Publish the draft as a maintainer: a release published by a workflow using GITHUB_TOKEN does not trigger another workflow. Unlike Altis CLI, this project keeps committed versions and v-prefixed tags; the npm workflow validates them rather than stamping a version at publish time. It accepts stable vX.Y.Z tags only, so prereleases cannot accidentally become npm's latest release.
If npm publishing fails, fix the publishing configuration and re-run the failed Release job. Do not rerun Tag and Release, which rejects existing tags. npm versions cannot be overwritten; if the package was already published, make any code changes in a new version.
One-time npm setup
An npm package maintainer must establish the altis-cli-mcp package with an initial authenticated publish from a reviewed release commit. Under Node 24, run npm ci, npm run lint, npm run format:check and npm run build, inspect npm pack --dry-run, then run npm login and npm publish --access public. This is a one-time bootstrap before configuring the package's trusted publisher.
In the npm package's Settings → Trusted Publisher → GitHub Actions, configure:
- Organization or user:
humanmade - Repository:
altis-cli-mcp - Workflow filename:
release.yml - Environment: leave blank
- Allowed actions: enable direct publishing with
npm publish
The Release workflow authenticates using npm trusted publishing through OIDC, with id-token: write and npm 11.5.1 or newer. No npm publishing token or repository secret is needed for subsequent releases. The repository URL in package.json matches this repository, and public releases include provenance.
License
Licensed under the MIT license, matching Altis CLI. Copyright 2026 Human Made.
