Add mapbox mcp: register a Mapbox MCP server with a coding agent - #69
mattpodwysocki wants to merge 5 commits into
Conversation
AGI-1150 Phase 2 MVP. Shells out to `claude mcp add`/`claude mcp get` rather than editing an agent's own config file directly, since that file may already list other servers. Supports the hosted Mapbox MCP and DevKit MCP endpoints for Claude Code; other clients and the local npm install path are left as documented follow-up work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codex has its own mcp add/get, close to Claude Code's shape but not identical: different argv, and registering a server that advertises OAuth support starts a login flow as part of add itself. That flow fails against the real Mapbox hosted endpoint (an incompatibility between Codex's OAuth client and this server), but the config entry is written regardless of add's exit code - now reported as "installed, login incomplete" rather than a flat failure. VS Code and Cursor have no mcp subcommand, but both expose a top-level --add-mcp flag. Neither refuses a duplicate name, so this reads each client's own config file directly before ever calling that flag - and where that file lives is genuinely different per client (VS Code: a dedicated mcp.json; Cursor: settings.json under an "mcp" key), verified live rather than assumed. Neither has a working per-project scope via their CLI, so both always register for every project regardless of --global, disclosed rather than silently ignored. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
zmofei
left a comment
There was a problem hiding this comment.
Thanks for this! I built this branch (c17ab1c) and ran it. I could reproduce 6 of the 7 issues below. I could not test #4 (Windows). cargo test passes, so the current tests do not catch any of them.
How I tested
codexandcursorare not installed on my Mac. I usedMAPBOX_CODEX_CLIandMAPBOX_CODE_CLIto point to small fake scripts that log every call.MAPBOX_CODE_CONFIG_DIRpointed to a temp folder, so no real config was touched.- To compare with the real VS Code (1.136.1), I ran
code --add-mcpwith--user-data-dirset to a temp folder.
Summary
| # | Problem | Where | Reproduced |
|---|---|---|---|
| 1 | JSON status is installed__login_incomplete (two _) |
src/mcp.rs:694 |
✅ |
| 2 | Config file with comments or trailing commas → config unreadable |
src/mcp.rs:346 |
✅ |
| 3 | Any read error counts as "not installed", then --add-mcp runs |
src/mcp.rs:344 |
✅ |
| 4 | .cmd CLIs may not be found on Windows |
src/mcp.rs:228 |
❌ not tested |
| 5 | Codex output during add (for example an auth URL) is hidden |
src/mcp.rs:441 |
✅ with a fake codex |
| 6 | list and install spell JSON status values differently |
src/mcp.rs:623 |
✅ |
| 7 | Docs say "no client found" is not an error, but it is | docs/commands.md:3242 |
✅ |
A question (not a change request): install exits 0 even when every add fails:
$ MAPBOX_CODE_CLI=./bin/fail mapbox mcp install --client vscode -o json
{"results":[{"client":"vscode","error":"VS Code could not register Mapbox MCP: boom","server":"mapbox","status":"failed"},{"client":"vscode","error":"VS Code could not register Mapbox DevKit MCP: boom","server":"mapbox-devkit","status":"failed"}]}
exit=0
Is this on purpose? A script that only checks the exit code will think it worked. If it is on purpose, please say so in the docs.
Not checked: Windows, and the real Codex and Cursor CLIs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- JSON status values are now a fixed snake_case table (Status::json()), not derived from the display text one character at a time — that was the actual bug behind "installed__login_incomplete" (two underscores, one per character replaced in the comma-and-space in "installed, login incomplete"). list and install now share the same table, so the same state spells the same way in both. - VS Code's/Cursor's config files are parsed as JSONC (jsonc-parser crate), not plain JSON. Both allow comments and a trailing comma, so an ordinary, already-edited file was misread as "config unreadable" before this. - Only io::ErrorKind::NotFound means "not installed" now. Every other read error (no permission, the path is a directory, invalid UTF-8) is "config unreadable" instead of silently falling through to an install attempt over a file this command had never actually read. - Every spawn now tries .cmd/.bat/.exe in turn on Windows for a bare name with no extension of its own (command_candidates/spawn/ spawn_live) — CreateProcessW does not probe PATHEXT the way cmd.exe does, so an npm-installed CLI (or VS Code's/Cursor's own launcher, both .cmd there) would otherwise read as "not found". Not verified live, no Windows machine here, but the gap and the fix are both well-documented Rust/Windows behavior. - Codex's add is now run via spawn_live, which forwards both of the child's streams to this process's own stderr as they arrive (one on a second thread, so neither pipe can block behind the other filling up) rather than only capturing them — an OAuth URL it prints as part of add was previously never shown. - install now exits 1 once a registration actually fails, after reporting the full result — a skipped pair (client not found, config unreadable) doesn't count, only a real attempted-and-failed one does. Previously this always exited 0 regardless. - Fixed documentation that said a totally unreachable client makes install report nothing was done; it's actually an error, exit 1, which is what the code already did. Also merged main in (CHANGELOG.md/README.md conflicts, additive). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for the thorough testing, especially catching the VS Code JSONC and Windows PATHEXT gaps, neither occurred to me. Replied inline on each of the 7. Pushed two commits: one merging main (a couple of conflicts, additive, CHANGELOG and README), and one with all the fixes plus new tests for each. On your question, that wasn't on purpose, it should exit non-zero. Fixed: install now exits 1 once a registration actually fails, after the full result is still printed. A skipped pair (client not found, config unreadable) doesn't count toward that, only a real attempted-and-failed one does, since nothing was attempted there to call a failure. Everything passes cargo build/fmt/clippy/test, including 7 new tests across the fixes (4 unit, 3 integration). |
|
Update on the Codex OAuth incompatibility this PR disclosed: a fix for the issuer/authorization-endpoint mismatch Codex was rejecting is in review upstream, and already tested working in staging for Codex (along with Claude Code, MCP Inspector and ChatGPT). Nothing to change here yet since it hasn't shipped, and the |
# Conflicts: # CHANGELOG.md
What
AGI-1150 (Phase 2 of the coding-agent-setup epic): a
mapbox mcpcommandthat registers a Mapbox MCP server with a coding agent's own CLI or config,
so the agent can call Mapbox's tools directly rather than only reading
guidance about them (
mapbox agent-skills) or about this CLI (mapbox generate-skills).Started as Claude Code only (first commit), then extended to Codex, VS Code
and Cursor (second commit) once live testing showed each is a genuinely
different shape, not a drop-in row in the same table. Both commits are on
this PR since the second is a direct continuation of the same command, not
separate scope.
Research
research from the original PR description still applies (both Mapbox MCP
and Mapbox DevKit MCP publish an npm package and a hosted, OAuth
endpoint).
codex mcp add/get/list/remove, close toClaude Code's shape but not identical: different argv (
codex mcp add <name> --url <url>, name positional, url a flag, vs Claude Code'sadd --transport http <name> <url>), and registering a server that advertisesOAuth support starts a login flow as part of
additself, not aseparate step. Tested live against the real
https://mcp.mapbox.com/mcp: that login fails (OAuth authorization endpoint origin does not match the authorization server origin without issuer-bound callbacks), an incompatibility between Codex's OAuth clientand this server, not something this CLI can fix. The config entry is
written regardless of that exit code, confirmed via a follow-up
codex mcp get.codex mcp addalso doesn't refuse a duplicate name on its own,unlike Claude Code's
add.mcpsubcommand, but both expose atop-level
--add-mcp '<json>'flag, confirmed via--help, not assumed.Tested both safely with
--user-data-dirpointed at a scratch directory.Correctly merges with an existing, differently-named entry; silently
overwrites an entry under the same name if its content differs, no
refusal, no warning. Where the result lands is genuinely different per
client: VS Code writes a dedicated
User/mcp.json; Cursor writes intoUser/settings.jsonunder an"mcp"key instead. Both editors'--mcp-workspace-style per-project flag was also tested and foundnon-functional in the versions used here (Cursor's is not a recognized
CLI option at all; VS Code has no equivalent flag), so both always
register in the user profile regardless of
--global.--add-mcpwas outright broken inthe version first tested (2.5.20, garbled JSON parse error on every
input, including its own
--helpexample). Updating Cursor to 3.22.12fixed it, and also moved its storage from a dedicated
mcp.jsontosettings.json, exactly the kind of version driftAddMcpConfignamesexplicitly per client rather than assuming.
isolate a Codex test via a
CODEX_HOMEenv var override didn't work(Codex ignored it), and a test entry briefly landed in the real
~/.codex/config.toml. Caught immediately viacodex mcp list --jsonand removed with
codex mcp remove, confirmed gone. No other real entrywas touched. Every test after that point uses the same fake-stub
technique as Claude Code rather than writing to any real client config.
Design
Two
ClientKinds insrc/mcp.rs:Verb(Claude Code, Codex): each gets its ownverb_addmatch arm,since argv shape and error handling differ enough that a shared function
would be wrong for one of them. Codex's arm checks
codex mcp getagainafter
add, regardless ofadd's own exit code, and reportsinstalled, login incomplete("status": "installed_login_incomplete") rather thaneither a flat success or failure when the entry was written but the OAuth
step wasn't.
AddMcpFlag(VS Code, Cursor): a newAddMcpConfigper client nameswhere its config actually lives (
app_dir_name,file_name,servers_pathfor the nested JSON key, an env-var override for tests).file_getreads that file directly, never writes it, and treats anunparseable file as its own
Unreadable/config_unreadablestatusrather than guessing past it, since guessing wrong could mean silently
discarding whatever
--add-mcpwould otherwise have clobbered.A stable
mcp_client_not_founderror code is unchanged in shape but nownames all four clients when none was specified and none is reachable.
Tests
tests/mcp_proxy.rsgrew from 8 to 17 cases. New: a Codex stub whoseaddalways "writes" to a state file regardless of its configured exit code
(modeling the real OAuth-failure-but-still-written behavior), covering a
clean install, the login-incomplete path, a genuine write failure (nothing
written, correctly reported as failed), and already-installed. A shared
AddMcpFlagstub for VS Code and Cursor covering a fresh install, analready-installed server never reaching
--add-mcp(the entry point of theseeded config file is the real shape each client writes, including
Cursor's nested
settings.jsonpath), an unreadable config reported ratherthan guessed past, and the no-per-project-scope notice. Every test now
isolates all four clients' binary-override variables by default, so a real
code/cursor/codex/claudeinstalled on the machine running the suitenever leaks into a scenario that isn't testing it (this is what the
a_client_not_on_path_is_skipped_and_namedcase needed fixing for, oncethis machine had all four for real).
Also live-verified by hand (read-only:
mcp list,install --dry-run)against this machine's real Claude Code, Codex and Cursor, which already
have
mapbox/mapbox-devkitregistered as personal dev servers, so thosethree correctly report
already installed; VS Code reportsclient not foundhere since this machine'scodeis a shell alias tocode-insiders, invisible to a directCommand::new("code")spawn, nota bug in the detection logic.
Verification
cargo build/fmt/clippy --all-targets -- -D warnings/testall clean,689 tests passing including the 17
mcp_proxyones.cargo test --test docs_contractconfirms the docs section and flags forall four clients are checked against
mapbox --schema.🤖 Generated with Claude Code