Skip to content

Add mapbox mcp: register a Mapbox MCP server with a coding agent - #69

Open
mattpodwysocki wants to merge 5 commits into
mainfrom
agi-1150-mcp-install
Open

mattpodwysocki wants to merge 5 commits into
mainfrom
agi-1150-mcp-install

Conversation

@mattpodwysocki

@mattpodwysocki mattpodwysocki commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What

AGI-1150 (Phase 2 of the coding-agent-setup epic): a mapbox mcp command
that 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

  • All four target npm/hosted-endpoint distribution and per-client setup
    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 has its own codex mcp add/get/list/remove, close to
    Claude Code's shape but not identical: different argv (codex mcp add <name> --url <url>, name positional, url a flag, vs Claude Code's add --transport http <name> <url>), and registering a server that advertises
    OAuth support starts a login flow as part of add itself, not a
    separate 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 client
    and 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 add also doesn't refuse a duplicate name on its own,
    unlike Claude Code's add.
  • VS Code and Cursor have no mcp subcommand, but both expose a
    top-level --add-mcp '<json>' flag, confirmed via --help, not assumed.
    Tested both safely with --user-data-dir pointed 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 into
    User/settings.json under an "mcp" key instead. Both editors'
    --mcp-workspace-style per-project flag was also tested and found
    non-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.
  • One real mid-session finding: Cursor's --add-mcp was outright broken in
    the version first tested (2.5.20, garbled JSON parse error on every
    input, including its own --help example). Updating Cursor to 3.22.12
    fixed it, and also moved its storage from a dedicated mcp.json to
    settings.json, exactly the kind of version drift AddMcpConfig names
    explicitly per client rather than assuming.
  • One live-testing mistake, caught and corrected: an early attempt to
    isolate a Codex test via a CODEX_HOME env var override didn't work
    (Codex ignored it), and a test entry briefly landed in the real
    ~/.codex/config.toml. Caught immediately via codex mcp list --json
    and removed with codex mcp remove, confirmed gone. No other real entry
    was 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 in src/mcp.rs:

  • Verb (Claude Code, Codex): each gets its own verb_add match 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 get again
    after add, regardless of add's own exit code, and reports installed, login incomplete ("status": "installed_login_incomplete") rather than
    either a flat success or failure when the entry was written but the OAuth
    step wasn't.
  • AddMcpFlag (VS Code, Cursor): a new AddMcpConfig per client names
    where its config actually lives (app_dir_name, file_name,
    servers_path for the nested JSON key, an env-var override for tests).
    file_get reads that file directly, never writes it, and treats an
    unparseable file as its own Unreadable/config_unreadable status
    rather than guessing past it, since guessing wrong could mean silently
    discarding whatever --add-mcp would otherwise have clobbered.

A stable mcp_client_not_found error code is unchanged in shape but now
names all four clients when none was specified and none is reachable.

Tests

tests/mcp_proxy.rs grew from 8 to 17 cases. New: a Codex stub whose add
always "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
AddMcpFlag stub for VS Code and Cursor covering a fresh install, an
already-installed server never reaching --add-mcp (the entry point of the
seeded config file is the real shape each client writes, including
Cursor's nested settings.json path), an unreadable config reported rather
than 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/claude installed on the machine running the suite
never leaks into a scenario that isn't testing it (this is what the
a_client_not_on_path_is_skipped_and_named case needed fixing for, once
this 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-devkit registered as personal dev servers, so those
three correctly report already installed; VS Code reports client not found here since this machine's code is a shell alias to
code-insiders, invisible to a direct Command::new("code") spawn, not
a bug in the detection logic.

Verification

cargo build/fmt/clippy --all-targets -- -D warnings/test all clean,
689 tests passing including the 17 mcp_proxy ones.
cargo test --test docs_contract confirms the docs section and flags for
all four clients are checked against mapbox --schema.

🤖 Generated with Claude Code

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>
@mattpodwysocki
mattpodwysocki requested a review from a team as a code owner September 29, 2026 15:21
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>
@mattpodwysocki mattpodwysocki changed the title Add mapbox mcp: register a Mapbox MCP server with Claude Code Add mapbox mcp: register a Mapbox MCP server with a coding agent Sep 29, 2026

@zmofei zmofei left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • codex and cursor are not installed on my Mac. I used MAPBOX_CODEX_CLI and MAPBOX_CODE_CLI to point to small fake scripts that log every call.
  • MAPBOX_CODE_CONFIG_DIR pointed to a temp folder, so no real config was touched.
  • To compare with the real VS Code (1.136.1), I ran code --add-mcp with --user-data-dir set 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.

Comment thread src/mcp.rs Outdated
Comment thread src/mcp.rs Outdated
Comment thread src/mcp.rs Outdated
Comment thread src/mcp.rs
Comment thread src/mcp.rs Outdated
Comment thread src/mcp.rs Outdated
Comment thread docs/commands.md Outdated
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>
@mattpodwysocki

Copy link
Copy Markdown
Contributor Author

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).

@mattpodwysocki

Copy link
Copy Markdown
Contributor Author

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 installed_login_incomplete handling is still the right behavior regardless (OAuth can still fail for other reasons later), but once it lands the login step should actually succeed for Codex rather than falling into that path.

@mattpodwysocki
mattpodwysocki requested a review from zmofei October 1, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants