Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ that may never merge. They are not releases and are not listed here.

## Unreleased

- `mapbox mcp list`/`mapbox mcp install`: registers a Mapbox MCP server
(direct tool-calling access to Mapbox's APIs, not just guidance about
them) with a coding agent's own CLI or config. Claude Code, Codex, VS Code
and Cursor are supported today, against the hosted Mapbox MCP endpoints.
An existing server with the same name is left alone rather than replaced.
VS Code and Cursor currently register for every project regardless of
`--global`, since neither has a working way to scope it to one project;
Codex may report a server `installed, login incomplete` when its own
OAuth step fails against Mapbox's hosted MCP server, a known
incompatibility between the two rather than something this command
controls.

- `install.sh`/`install.ps1`: when the install finds a coding agent on the
machine, it now asks, once, whether to write this CLI's own skill and
install the Mapbox Agent Skills library for it, naming the agent and what
Expand Down
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ base64 = "0.22"
rand = "0.10"
open = "5"
dirs = "5"
# VS Code's and Cursor's own config files are JSONC (comments, trailing
# commas), which `serde_json` alone refuses — `mcp.rs` reads them to check
# whether a server is already registered before ever writing to them, so a
# normal, commented file must not misread as corrupt. String-aware, so a
# `//` inside a URL value is never mistaken for a comment — both features
# are needed for `parse_to_serde_value`, which decodes straight into a
# `serde_json::Value`.
jsonc-parser = { version = "0.34.0", features = ["serde", "serde_json"] }

[dev-dependencies]
# Integration tests build fake Mapbox tokens; same crate the CLI already uses.
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mapbox styles list
- [For AI agents](#for-ai-agents)
- [Agent skills](#agent-skills)
- [Generate skills](#generate-skills)
- [MCP servers](#mcp-servers)
- [Global options](#global-options)
- [Dry runs](#dry-runs)
- [Timeouts](#timeouts)
Expand Down Expand Up @@ -340,6 +341,26 @@ Codex. `--agent`, `--global`, `--dir` and `--service` narrow it, and
mapbox agent-skills uninstall mapbox-cli
```

### MCP servers

```sh
mapbox mcp list
mapbox mcp install
```

Different kind of "install" from `agent-skills`/`generate-skills` above:
those write a directory this CLI owns, this registers an MCP server —
direct tool-calling access to Mapbox's APIs, not just guidance about them —
with a coding agent's *own* config, since that config belongs to the agent
and may already list other servers. Claude Code, Codex, VS Code and Cursor
are supported today, against the hosted Mapbox MCP endpoints: no token, no
npm package, no Node version to manage. An existing server with the same
name is left alone rather than replaced. VS Code and Cursor currently
register for every project regardless of `--global`, since neither has a
working way to scope it to one; Codex may report a server as installed with
its own login incomplete, an OAuth incompatibility between Codex and
Mapbox's hosted MCP server rather than something this command controls.

## Global options

These apply to every command, not just the API ones.
Expand Down
199 changes: 196 additions & 3 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ leaving the account as it was found.

Every API command's **Outputs** block below is that snapshot rather than a
live reading, and is re-taken by hand — nothing schedules it and nothing
enforces it. The auth, `completion`, `generate-skills` and tilesets-cli blocks
are not captures: those are the CLI's own rendering, which the test suite does
cover.
enforces it. The auth, `completion`, `generate-skills`, `mcp` and
tilesets-cli blocks are not captures: those are the CLI's own rendering,
which the test suite does cover.
Nothing re-checks the captures in between, because what
the Mapbox APIs return is not this repo's to monitor. What *is* ours — the
commands and the flags they take — is held to `mapbox --schema` on every
Expand Down Expand Up @@ -58,6 +58,9 @@ nests, and is typed `mapbox styles draft get`.
**[Generate skills](#generate-skills)** —
[generate-skills](#mapbox-generate-skills)

**[MCP servers](#mcp-servers)** — [mcp.list](#mapbox-mcp-list) ·
[mcp.install](#mapbox-mcp-install)

**[Uninstall](#uninstall)** — [uninstall](#mapbox-uninstall)

**[Config](#config)** — [config.get](#mapbox-config-get) ·
Expand Down Expand Up @@ -3190,6 +3193,196 @@ until `--force`.

---

## MCP servers

Registers a Mapbox MCP server — direct tool-calling access to Mapbox's APIs,
not just guidance about them — with a coding agent's own CLI.

**Not the same kind of "install" as [`agent-skills`](#agent-skills) or
[`generate-skills`](#generate-skills).** Those write a directory this CLI
fully owns. An MCP server has to be added to a config store that belongs to
the *client*, which may already list other servers, so this shells out to
the client's own tooling rather than editing that store directly — the same
reasoning [`tilesets-cli`](#tilesets-cli) has for exec-ing `tilesets` rather
than reimplementing it.

Against the hosted Mapbox MCP endpoints only — no token, no npm package, no
Node version to manage. Four clients today, two different ways of driving
them:

- **Claude Code and Codex** each have their own `mcp add`/`mcp get`, so this
runs that rather than touching either one's config file. The two need
different argv (`claude mcp add --transport http <name> <url>` vs. `codex
mcp add <name> --url <url>`), and Codex has a real quirk worth knowing:
registering a server that advertises OAuth support starts a login flow as
*part of* `add`, and if that login fails — which it currently does
against the real Mapbox hosted endpoint, an incompatibility between
Codex's OAuth client and this server, not something this command can fix
— the config entry is written anyway. That's reported as `installed, login
incomplete` (`"status": "installed_login_incomplete"` in JSON) rather
than either a flat success or a flat failure.
- **VS Code and Cursor** have no `mcp` subcommand at all, but both expose a
top-level `--add-mcp '<json>'` flag. Neither refuses a duplicate name —
both would silently overwrite an existing entry under the same name if
asked to — so this reads each client's own config file directly first
rather than ever calling that flag for a server already there. Where that
file lives is genuinely different per client: VS Code keeps a dedicated
`mcp.json`; Cursor keeps the same data inside `settings.json` under an
`"mcp"` key. Neither client currently has a working way to register a
server for one project rather than every one — confirmed directly, not
assumed — so both always register for every project, and this says so
rather than pretending otherwise.

An existing server with the same name is left alone rather than replaced,
whichever of the two mechanisms above applies — the same
never-overwrite-what-you-didn't-write rule `agent-skills` follows for a
skill directory someone has edited. A config file that exists but can't be
parsed is reported as such (`config unreadable` /
`"status": "config_unreadable"`) rather than guessed past, since guessing
wrong could mean silently discarding whatever was in it.

### `mapbox mcp list`

Every known server and client, and whether each is already registered.

#### Examples

```sh
mapbox mcp list
```

#### Outputs

<table>
<tr><th width="50%"><code>text</code></th><th width="50%"><code>json</code></th></tr>
<tr><td>

```
mapbox claude-code not installed
mapbox-devkit claude-code not installed
mapbox codex not installed
mapbox-devkit codex not installed
mapbox vscode client not found
mapbox-devkit vscode client not found
mapbox cursor not installed
mapbox-devkit cursor not installed
```

</td><td>

```json
{
"servers": [
{ "server": "mapbox", "client": "claude-code", "status": "not_installed" },
{ "server": "mapbox-devkit", "client": "claude-code", "status": "not_installed" },
{ "server": "mapbox", "client": "codex", "status": "not_installed" },
{ "server": "mapbox-devkit", "client": "codex", "status": "not_installed" },
{ "server": "mapbox", "client": "vscode", "status": "client_not_found" },
{ "server": "mapbox-devkit", "client": "vscode", "status": "client_not_found" },
{ "server": "mapbox", "client": "cursor", "status": "not_installed" },
{ "server": "mapbox-devkit", "client": "cursor", "status": "not_installed" }
]
}
```

</td></tr>
</table>

`client not found` in place of a status is that client's own CLI not being on
`PATH` at all — see [`mcp install`](#mapbox-mcp-install) below for what that
means for installing. `config unreadable` means VS Code's or Cursor's own
config file exists but didn't parse (checked as JSONC, so an ordinary
commented file is not what triggers this). `-o json` spells every status in
snake_case (`not_installed`, `client_not_found`, `config_unreadable`); this
page's prose and the `text` column above use the spaced form for reading,
never for matching against.

---

### `mapbox mcp install`

Registers every known server for every detected client. With no `--client`
named and no known client's CLI reachable at all, this is an error —
`mcp_client_not_found`, exit 1 — rather than a silent no-op; see the end of
this section for its exact shape. A client named explicitly, or detected,
but whose CLI goes unreachable partway through (or whose config can't be
read) is different: that one pair is skipped and named, the rest of the run
continues.

#### Parameters

| Parameter | Effect |
| --- | --- |
| `--server <SERVER>` | Repeatable. Defaults to every known server (`mapbox`, `mapbox-devkit`). |
| `--client <CLIENT>` | Repeatable. Defaults to whichever clients are detected. One of `claude-code`, `codex`, `vscode`, `cursor`. |
| `--global` | Register for every project rather than just this one. Only Claude Code (`--scope user`) draws that distinction — the other three currently register for every project regardless. |
| `--dry-run` | Report what would be installed, then exit without installing it. |

#### Examples

```sh
mapbox mcp install

mapbox mcp install --server mapbox --client vscode

mapbox mcp install --server mapbox --global

mapbox mcp install --dry-run
```

#### Outputs

<table>
<tr><th width="50%"><code>text</code></th><th width="50%"><code>json</code></th></tr>
<tr><td>

```
Mapbox MCP: https://mcp.mapbox.com/mcp for Claude Code — installed.
Mapbox DevKit MCP: https://mcp-devkit.mapbox.com/mcp for Claude Code — installed.
Mapbox MCP: https://mcp.mapbox.com/mcp for Codex — installed, login incomplete.
Mapbox MCP: https://mcp.mapbox.com/mcp for VS Code — installed.
```

</td><td>

```json
{
"results": [
{ "server": "mapbox", "client": "claude-code", "status": "installed" },
{ "server": "mapbox-devkit", "client": "claude-code", "status": "installed" },
{ "server": "mapbox", "client": "codex", "status": "installed_login_incomplete", "error": "..." },
{ "server": "mapbox", "client": "vscode", "status": "installed" }
]
}
```

</td></tr>
</table>

Run again, `installed` reads `already installed` (`already_installed` in
JSON) for each — the server is left exactly as it is, nothing is re-written.
`--dry-run` reads `would install` (`would_install`) instead of attempting
anything. A client whose CLI isn't reachable reads `is not on PATH, skipped`
(`"status": "client_not_found"`) rather than stopping the rest of the run,
and one whose config exists but couldn't be parsed (VS Code, Cursor) reads
`'s config could not be read, skipped` (`"status": "config_unreadable"`),
also without stopping the rest of the run. A registration that actually
fails reads `failed`, with the detail in `"error"`, and makes the whole
command exit 1 once everything has been attempted and reported — a skipped
pair (client not found, config unreadable) does not count toward that,
since nothing was attempted there to call a failure.

With no `--client` named and no known client's CLI reachable at all, this is
an error rather than a silent no-op — `mcp_client_not_found` in `-o json`,
naming the clients it looked for:

```
Error: No supported coding-agent CLI was found: claude-code, codex, vscode, cursor.
Fix: Install one of these CLIs, or pass --client to name one anyway (claude-code, codex, vscode, cursor).
```

---

## Uninstall

### `mapbox uninstall`
Expand Down
13 changes: 13 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod generate_skills;
mod history;
mod http;
mod link;
mod mcp;
mod output;
mod remedy;
mod run_history;
Expand Down Expand Up @@ -612,6 +613,13 @@ fn build_app(specs: &[ServiceSpec]) -> Command {
// same agent directories, which is why they share `skill_dest`.
app = app.subcommand(agent_skills::command());

// The other half of "make a coding agent Mapbox-aware": the two skills
// commands above write guidance an agent reads, this registers an MCP
// server an agent can actually call. It shells out to the agent's own
// CLI rather than sharing `skill_dest`, since it edits a config store
// that CLI owns rather than a directory this one does.
app = app.subcommand(mcp::command());

// Between the other two hand-written leaves, so the help lists the three
// that make no request together and in the order someone meets them.
// What it prints is built from `app` itself, which is why nothing here
Expand Down Expand Up @@ -1099,6 +1107,11 @@ fn run(app: &Command, specs: &[ServiceSpec], matches: &ArgMatches, mode: Mode) -
agent_skills::RunFlags { debug, assume_yes },
mode,
)?,
// Ahead of the generic service arm for the same reason as its
// neighbors: it makes no Mapbox request and needs no token. It does
// shell out to another process (the agent's own CLI), but that is
// local, not a network call.
Some((mcp::COMMAND, mcp_matches)) => mcp::run(mcp_matches, mode)?,
// Ahead of the generic service arm for the same reason again, and
// handed `app` for the same reason `generate-skills` is: the script
// it prints is a rendering of the command tree already in memory.
Expand Down
Loading
Loading