agentleFS
vish288vish288/mcp-coda/AGENTS.md

mcp-coda

AGENTS.md2 starsChanged 8 months agoMITView on GitHub

MCP server for the Coda v1 API. It provides 54 tools, 12 resources (2 live data + 5 rules + 5 guides), and 5 prompts. They cover docs, pages, tables, rows, formulas, controls, permissions, folders, publishing, automations, and analytics. mcp-coda runs on FastMCP. PyPI hosts it as the mcp-coda package. Install it with uvx mcp-coda.

  • Reads credentials

We split this AGENTS.md into the rules it gives an agent

12 rules. About 0 are common, 4 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Write tools are declared @tool_result(write=True), which runs _check_write(ctx) before the body

    line 34
  2. Very specific:

    Every tool MUST pass annotations={} with at least readOnlyHint and openWorldHint.

    line 51
  3. Distinctive:

    Every optional parameter has a default

    line 71
  4. Distinctive:

    Never expose stack traces, tokens, or internal paths

    line 77
  5. Very specific:

    Add it to the matching server module, or create a new module under servers/ — every non-underscore module there is imported automatically

    line 85
  6. Very specific:

    Import helpers from ._helpers — never re-implement _get_client, _ok, _err, _check_write

    line 86
  7. Very specific:

    Add unit tests in tests/unit/test_tools_<module>.py

    line 88
  8. Distinctive:

    Update the docs listed under "Documentation freshness" in the same commit

    line 89
  9. Very specific:

    Never edit the pyproject.toml version directly — the workflow owns it

    line 183
  10. Distinctive:

    Never create tags manually

    line 184
  11. Very specific:

    Commit messages must follow conventional commits (feat:, fix:, docs:, …) so the changelog generates

    line 185
  12. Very specific:

    Check: uv run pytest tests/test_doc_parity.py re-derives every count, tool name and version string from the source and fails on drift.

    line 200

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

mcp-coda — Agent Context

MCP server for the Coda v1 API. It provides 54 tools, 12 resources (2 live data + 5 rules + 5 guides), and 5 prompts. They cover docs, pages, tables, rows, formulas, controls, permissions, folders, publishing, automations, and analytics.

mcp-coda runs on FastMCP. PyPI hosts it as the mcp-coda package. Install it with uvx mcp-coda.

Protocol support

  • Implements the MCP 2026-07-28 specification (MCP 2.0). Stays compatible with 2025-11-25 clients.
  • Built on FastMCP 4.x and the MCP Python SDK 2.x.
  • Transports: stdio (default) and streamable-http (recommended for remote). sse still works, but the 2026-07-28 spec deprecates it, so the server prints a warning.
  • Uses tools, resources, and prompts only. No Roots, Sampling, Logging, elicitation, or resource subscriptions, so the 2026-07-28 deprecations do not affect it.

Layout

  • src/mcp_coda/__init__.py — click CLI, loads .env via python-dotenv, runs the FastMCP server
  • src/mcp_coda/servers/__init__.py — the FastMCP instance, lifespan (builds the shared CodaClient), and _register_tools() which imports every tool module so decorators execute
  • src/mcp_coda/servers/*.py — one module per domain (docs, pages, tables, rows, …)
  • src/mcp_coda/servers/_helpers.py — shared _get_client, _ok, _ok_markdown, _err, _check_write, _format_list_as_markdown
  • src/mcp_coda/servers/resources.py / prompts.py — MCP resources and prompts
  • src/mcp_coda/resources/ — static markdown backing the rules/guides resources
  • src/mcp_coda/client.py — async httpx client wrapping every Coda API call. Transport (R2): a per-phase httpx.Timeout with the connect phase capped at 10s and AsyncHTTPTransport(retries=2) for connection errors; a single _send path adds GET-only status retries (502/503/504 with jitter, 429 when Retry-After ≤ 5s, max 2 attempts, writes never retried) and splits a timeout (CodaTimeoutError) from an unreachable host (CodaConnectionError; a ConnectTimeout counts as unreachable). Worst-case latency against a hard-down host is roughly 2 × (3 × connect-cap + timeout) + jitter — the connect cap is 10s, the two outer attempts are the GET status-retry and each wraps httpx's three connect tries (retries=2)
  • src/mcp_coda/config.py — CodaConfig dataclass built from env vars; CODA_TIMEOUT is validated with a named error
  • src/mcp_coda/exceptions.py — CodaApiError, CodaNotFoundError, CodaRateLimitError, CodaTimeoutError, CodaConnectionError, …
  • tests/unit/ — ~307 tool- and unit-level tests; tests/conftest.py holds shared fixtures
  • tests/test_doc_parity.py — re-derives tool/resource/prompt counts, tool names and version strings from the source and asserts the docs match; tests/test_links.py checks external URLs
  • evaluations/eval.xml — question / expected-answer / expected-tool pairs used to sanity-check tool selection

Key patterns

  • Every tool sits under @tool_result (from _helpers.py), below @mcp.tool. For an expected CodaError (401/403/404/429, other API statuses, read-only mode) it returns a ToolResult(is_error=True) whose text is the _err(e) JSON and no structuredContent. Any other exception is logged with its traceback and re-raised as ToolError, so the MCP result is also isError: true with TypeName: message text
  • Errors: expected failures are isError: true results whose error body ({"error", "hint", "status_code"?, "body"?, "retry_after"?}) is the text content; bugs are isError: true with TypeName: message text, logged with a traceback. No error result carries structuredContent — the TS MCP SDK validates it against the output schema even when isError is true, so an error payload there would make strict clients reject the result
  • Every tool returns a fastmcp ToolResult. Success: _ok/_page put the Coda JSON object in structuredContent beside the pretty text; the two markdown branches use _text (text only, no structuredContent). Expected failures come from _error_result(_err(e)). Tools advertise no outputSchema (payloads pass upstream fields through untyped); a typed schema is opt-in per tool via @mcp.tool(output_schema=…)
  • Write tools are declared @tool_result(write=True), which runs _check_write(ctx) before the body
  • List responses go through _page(data) and include {items, has_more, next_cursor, total_count}; most paginated endpoints default to limit=50 (coda_list_docs 25, coda_list_columns 100)
  • Rate-limit errors carry retry_after seconds; the client retries a 429 once when its Retry-After is ≤ 5s (jittered GET-only retries also cover 502/503/504), and otherwise surfaces retry_after without retrying. Writes are never retried
  • Row writes are async server-side: they return a requestId, poll it with coda_get_mutation_status

Access control (three layers)

  1. Server-level: CODA_READ_ONLY=true (or the shared MCP_READ_ONLY=true) locks the server read-only — write tools and write-workflow prompts are hidden from tools/list/prompts/list, calling a hidden tool is an Unknown tool error, and _check_write still refuses on any path that reaches it. The lock is computed once at startup and cannot be lifted at runtime; --read-only can only raise it. It is a deployment control, not a per-user one: a Coda token has no scopes and there is no per-request auth, so it only holds when the deployer owns the launch (managed MCP config env, container/k8s env, deployer-run HTTP)
  2. MCP annotations: readOnlyHint / destructiveHint / idempotentHint drive client-side permission prompts
  3. Coda token: Coda's sharing settings enforce doc-level access. Tokens have no scopes and reach everything their owner can reach

HTTP transports add a network layer: Host/Origin protection is not authentication, so binding to a non-loopback host requires CODA_HTTP_BEARER_TOKEN (min 16 chars, constant-time compared via a TokenVerifier subclass). The CLI refuses to start on a non-loopback host without it.

MCP compliance rules

Tool annotations (mandatory)

Every tool MUST pass annotations={} with at least readOnlyHint and openWorldHint.

  • Read: {"openWorldHint": True, "readOnlyHint": True, "idempotentHint": True}
  • Non-destructive write: {"openWorldHint": True, "readOnlyHint": False}
  • Destructive write: {"openWorldHint": True, "destructiveHint": True, "readOnlyHint": False}
  • Idempotent write (PUT/update): add "idempotentHint": True

Also pass tags={"coda", "<domain>", "read"|"write"}.

Tool descriptions

1–2 sentences, front-loading what it does and what it returns.

  • Bad: "This tool gets rows."
  • Good: "List rows in a Coda table with optional filtering. Returns row data with values, timestamps, and pagination cursor."

Parameters

  • Annotated[type, Field(description="...")] on every parameter
  • Literal[...] for known value sets instead of bare str
  • Every optional parameter has a default
  • Flat — no nested dicts unless genuinely required (e.g. cells)

Errors

  • Actionable text: what went wrong plus a suggested fix
  • Never expose stack traces, tokens, or internal paths

Naming

coda_{verb}_{resource}, snake_case. Verbs in use: create, get, list, update, delete, push, trigger, export, publish, unpublish, resolve.

Adding a tool

  1. Add it to the matching server module, or create a new module under servers/ — every non-underscore module there is imported automatically
  2. Import helpers from ._helpers — never re-implement _get_client, _ok, _err, _check_write
  3. Include annotations={} and tags={} on the @mcp.tool() decorator
  4. Add unit tests in tests/unit/test_tools_<module>.py
  5. Update the docs listed under "Documentation freshness" in the same commit

Development

uv sync --all-extras                    # install deps incl. dev group
uv run pytest --cov                     # tests (asyncio_mode = auto; respx mocks httpx)
uv run ruff check .
uv run ruff format --check .
uv run pyright                          # type check (src only, basic mode)

CI: tests.yml runs the suite on Python 3.10–3.13 and uploads coverage from 3.13; lint.yml runs the two ruff commands and pyright. Ruff config lives in ruff.toml (line-length 100, rule set E,F,B,W,I,N,UP,S,C4,EM,ISC; tests/** ignores S101,S105,S106,S104).

Running locally:

uvx mcp-coda                                              # stdio (default)
uvx mcp-coda --transport sse --host 127.0.0.1 --port 8000
uvx mcp-coda --transport streamable-http --port 9000
uvx mcp-coda --read-only                                  # CODA_API_TOKEN from env (--coda-token is deprecated, hidden, warns on stderr)

Tool inventory (54)

ModuleToolsTypeCovers
account3readcoda_whoami, coda_resolve_browser_link, coda_get_mutation_status
docs5read/writelist, get, create, update, delete docs
pages9read/writelist, get, create, update, delete, export pages; get/delete page content; export status
tables4readlist/get tables, list/get columns
rows7read/writelist, get, insert (upsert via key_columns), update, delete row/rows, push button
permissions6read/writesharing metadata, list/add/delete permissions, search principals, ACL settings
folders5read/writelist, get, create, update, delete folders
misc8read/writelist/get formulas, list/get controls, list categories, publish/unpublish, trigger automation
analytics7readdoc/page/pack analytics + summaries, last-updated day

Common workflows

  • Table data: coda_list_docs → coda_list_tables → coda_list_columns → coda_list_rows → coda_insert_rows / coda_update_row
  • Page editing: coda_resolve_browser_link → coda_get_page → coda_update_page
  • Doc setup: coda_create_doc → coda_create_page → coda_list_tables → coda_insert_rows
  • Access control: coda_list_permissions → coda_add_permission → coda_publish_doc
  • Automation: coda_list_docs → coda_trigger_automation → coda_get_mutation_status

Environment variables

VariableRequiredDefaultDescription
CODA_API_TOKENYes—Coda API token
CODA_READ_ONLYNofalseLock the server read-only: write tools and write-workflow prompts are not listed and are refused before any API call. Any value other than false/0/no/off/empty locks
MCP_READ_ONLYNofalseShared alias for CODA_READ_ONLY across the mcp-* servers; either locking wins, any value other than false/0/no/off/empty locks, nothing lifts it at runtime, and --read-only can only raise the lock
CODA_BASE_URLNohttps://coda.io/apis/v1API base URL (trailing slash stripped)
CODA_TIMEOUTNo30HTTP timeout, seconds
CODA_HTTP_BEARER_TOKENNo—Bearer token (min 16 chars) required to bind an HTTP transport to a non-loopback host; clients send Authorization: Bearer

The token is read from CODA_API_TOKEN, then CODA_TOKEN, then CODA_PAT — first non-empty wins. Tokens are generated at https://coda.io/account#apiSettings under "API settings". CodaConfig.validate() rejects non-ASCII tokens (httpx header encoding).

Coda API gotchas

  • Browser URLs are not API IDs — run coda_resolve_browser_link first to get doc/page/table/row IDs
  • The API cannot insert or execute pack formulas (DrawFlowchart, Mermaid, etc.)
  • Rate limits are per-token and vary by plan; a 429 returns retry_after seconds. The client retries a 429 once when Retry-After ≤ 5s (and 502/503/504 with jitter), GET-only; otherwise it surfaces retry_after without retrying
  • Analytics endpoints generally require doc ownership, not only Viewer access
  • Minimum doc roles: Viewer for reads; Editor for page/row writes, folder management, and triggering automations; Doc Owner for permissions and publishing

Release workflow

Releases run through GitHub Actions. Never bump versions or create tags by hand.

The six workflows (tests, lint, link-check, release, publish, reconcile) are thin stubs that call reusable workflow_call workflows in vish288/.github, pinned by commit SHA with a # vX.Y.Z comment so Dependabot bumps them. The shared scripts/ci/*.sh and scripts/derive_artifacts.py live there too; this repo no longer carries its own copies. The stubs are Copier-owned (.copier-answers.yml): change CI in vish288/.github and run copier update here, not by editing these files directly. A CI fix is one PR in vish288/.github plus a tag bump, not a change in each server.

gh workflow run release.yml -f bump=minor               # 0.3.0 → 0.4.0
gh workflow run release.yml -f bump=patch               # 0.4.0 → 0.4.1
gh workflow run release.yml -f bump=major               # 0.4.0 → 1.0.0
gh workflow run release.yml -f bump=minor -f dry_run=true   # preview changelog, no push
  1. release.yml (workflow_dispatch) runs tests.yml and lint.yml on the released commit first (needs: [test, lint]), bumps the pyproject.toml version, regenerates uv.lock, generates CHANGELOG.md, and creates the release commit + tag via the GitHub API. The commit is created with GITHUB_TOKEN and no author/committer, so GitHub signs it as github-actions[bot] (verification verified: true); only the main and tag ref updates use RELEASE_PAT (needed to bypass the branch ruleset); release.yml dispatches publish.yml, and the PAT's tag push no longer triggers publish (publish.yml is dispatch-only)
  2. publish.yml runs as split, idempotent jobs: the reusable publish-prepare.yml (status → build), then a local top-level pypi job with environment: pypi (PyPI trusted publishing checks the OIDC job_workflow_ref, so the pypa/gh-action-pypi-publish job must stay in this repo's publish.yml), then the reusable publish-finish.yml (confirm-pypi → github-release → registry). Each target is checked first and written only when not already done, so PyPI, the GitHub release and the MCP Registry each complete independently and recoverably
  3. Recovery of a failed publish is gh run rerun <id> --failed or gh workflow run publish.yml --ref refs/tags/vX.Y.Z; both are no-ops for targets that are already done
  4. reconcile.yml does this unattended — about 5 min after a failed publish run, then every 6 h on a schedule — scanning the last three tags and dispatching publish.yml as needed, capped at 8 failed attempts per tag before it asks for a human. A tag pushed by hand outside release.yml publishes only via a manual gh workflow run publish.yml --ref refs/tags/vX.Y.Z or the reconcile cron (there is no tag-push trigger).

PR commits land signed via a local signed rebase + fast-forward push of the branch to main, never GitHub's rebase-merge, squash, or merge-commit buttons; the chore(release) commit is GitHub-signed (github-actions[bot], Verified). main requires signed commits and linear history.

Rules:

  • Never edit the pyproject.toml version directly — the workflow owns it
  • Never create tags manually
  • Commit messages must follow conventional commits (feat:, fix:, docs:, …) so the changelog generates
  • github-actions[bot] authors the release commit with the message chore(release): X.Y.Z

Documentation freshness (mandatory)

Any changeset that adds, removes, or modifies tools, resources, or prompts MUST update all of these in the same commit:

  • README.md — tool count in heading and intro, tool table, full tool reference, usage examples, permissions table
  • llms.txt — tool count in tagline and documentation link
  • llms-full.txt — tool count in tagline, documentation link, full tool reference section
  • AGENTS.md — tool count in intro, tool inventory table
  • server.json — description field (≤100 chars)
  • gemini-extension.json — description field
  • src/mcp_coda/resources/**.md — the rules/guides shipped to clients; they must only name tools that exist

Check: uv run pytest tests/test_doc_parity.py re-derives every count, tool name and version string from the source and fails on drift. To count the total: grep -c '^async def coda_' src/mcp_coda/servers/*.py. Also confirm the category list is complete and new tools appear in the right sections with parameters and annotations.

llms.txt and gemini-extension.json are derived from llms-full.txt / server.json by derive_artifacts.py, which now lives in vish288/.github and is run by the release.yml pipeline (with coda's \n## Setup Examples split marker). Do not hand-edit them; to preview locally, run the script from a vish288/.github checkout against this repo root: LLMS_SPLIT_MARKER=$'\n## Setup Examples' python /path/to/.github/scripts/derive_artifacts.py.

Known limitations

  • Passthrough objects stay untyped. Tools return the raw Coda API JSON through _ok (wrapped in a ToolResult as structuredContent plus pretty text), so field names keep the API's camelCase and unknown fields survive; tools advertise no outputSchema. _ok shrinks oversized list envelopes by dropping items (never slicing the JSON) and caps single non-list payloads at 25k characters via _text (text only), and each list tool builds the {items, has_more, next_cursor, total_count} envelope inline. A typed output_schema= is opt-in per tool; blanket typed response models would rename every field and silently drop extras, so that is a breaking change, not a refactor.

Download now (ZIP)

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository2 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.