# MCP tools

> All eighteen tools, what each one needs, and the annotations and output schemas a client can rely on.

*Every tool an agent can call, with its inputs and results.*

Use this page to look up a tool. The project MCP server offers eighteen tools,
grouped below by what they do. To connect an agent, see
[Connecting agents](/docs/connecting-agents).

Tools give an agent no extra powers. `run_function` goes through the same
handler as `POST /api/fn/<name>`, so access rules, the audit log, rate limits,
and approvals apply exactly as they do for your app's users.

## Discovery

| Tool | Input | Returns |
| --- | --- | --- |
| `list_collections` | none | Every registered collection on this branch |
| `describe_schema` | none | Collections with full field descriptors, plus the project's functions |
| `list_functions` | none | Every typed function |
| `describe_function` | `name` | One function's input fields, kind, and flags |

`describe_schema` returns the same description that Studio's schema view and the
field tables in these docs are built from, so all three always agree.

## Reading content

| Tool | Input | Notes |
| --- | --- | --- |
| `list_content` | `collection` | Reads the authored MDX files, not the index |
| `get_content` | `collection`, `slug` | Returns data, body, and `sourcePath` |
| `search_content` | `query`, `collection?`, `limit?` | Reads the compiled index; default limit 20 |

`list_content` and `get_content` read your files, which are the source of truth,
so they show your latest edits even before you compile.

`search_content` searches the index, so it reflects the last compile. Writes
through `write_content` compile as they go, so they are searchable at once. The
query syntax is the familiar web-search one: words, `"quoted phrases"`, `or`,
and `-exclusions`. On a preview branch, results include the documents the
branch inherits from its parent chain (the branch it was forked from, and that
branch's parent, up to `main`), with the nearest branch's version taking
priority.

All three include a link to each document's
[resource](/docs/mcp-resources-and-prompts), so a client can attach a document
without another call.

## Writing content

| Tool | Scope | Notes |
| --- | --- | --- |
| `write_content` | `content:write` | Validates, writes `<contentDir>/<collection>/<slug>.mdx`, compiles |
| `delete_content` | `content:write` | Destructive and human-gated |
| `put_asset` | `content:write` | Uploads a binary for `field.asset` |
| `list_drafts` | none | Hosted on GitHub only. This connection's unpublished changes |
| `publish_drafts` | `content:write` | Hosted on GitHub only. A pull request, or a commit with `content:publish` |
| `discard_drafts` | `content:write` | Hosted on GitHub only. Throws unpublished changes away |

### write_content

Takes `collection`, `slug`, `data`, and an optional `body`. It replaces the
document rather than patching it.

Three refusals worth knowing:

- A db-authoritative collection answers `AUTHORITY_MISMATCH`. Those rows are
  written through typed functions.
- A `slug` inside `data` that disagrees with the `slug` argument answers
  `INVALID_SLUG`. Omit it from `data`.
- An executable MDX body is refused before it is stored. See
  [MDX safety](/docs/mdx-safety).

Frontmatter keys you do not change come back byte-identical. Only a real data
change re-serializes the file.

### delete_content

The first call files an approval and fails with its id. A human decides, then
the caller retries the same collection and slug with `approval: "<id>"`.
Approvals are one-shot and bound to that exact input.

A missing document fails before an approval is filed, so nobody reviews a
request for a file that was never there.

On a server that opted into elicited approvals, and a client that supports
elicitation, the confirmation happens in-band and the call completes in one
step. The gate is the same either way.

### Drafts on a hosted server

When `graft serve` writes through GitHub (`GRAFT_GITHUB_REPO` is set, see
[Studio hosting](/docs/studio-hosting)), a deployment's files are read-only and
nothing is written to them. `write_content` and `delete_content` then commit to
a draft branch of the connection's own, `graft-studio/drafts/<identity>`, and
return `gitSha: null`, because nothing was compiled: a draft must not reach the
live site's index.

`list_drafts` shows those changes, and `publish_drafts` sends them toward
production. It opens a pull request unless the credential carries
`content:publish` and the server publishes directly
(`GRAFT_STUDIO_PUBLISH=commit`, the default), in which case it commits to the
production branch. Either way the site changes when the change reaches that
branch and the host redeploys: at once for a commit, after the merge for a pull
request. A commit also calls the site's
[deploy hooks](/docs/studio#site-and-deploy-hooks). A document the
production branch also changed since the draft began is refused with
`CONTENT_CONFLICT`; send `resolve: { "<path>": "mine" }` or `"theirs"` to
decide. This is the same draft model a Studio editor uses, so an agent and a
person never take different routes to the live site.

### put_asset

Pass exactly one of `path` or `base64`. `path` only exists on a local mount and
resolves inside the project root; a remote server does not offer it at all and
tells the caller to send bytes instead.

`key` is required with `base64` and defaults to `assets/<filename>` with `path`.
Existing keys refuse unless `overwrite: true`, because the store keeps no
version history.

## Functions

| Tool | Input | Notes |
| --- | --- | --- |
| `run_function` | `name`, `input?`, `approval?`, `authorization?` | Runs a typed function through the ordinary handler |

There is no MCP-level scope on `run_function`. The function's own `access` rule,
the anonymous-mutation default, its rate limit, and its approval gate all
decide, exactly as they would over HTTP.

`authorization` overrides the connection's bearer for one call, which is how an
agent acts as someone else. On stdio the server holds `GRAFT_DEV_TOKEN` as the
default authorization, so the secret never enters the agent's context.

## Operations

| Tool | Input | Notes |
| --- | --- | --- |
| `setup_status` | none | Local `graft mcp` only. The checklist `graft doctor` prints: each check's state, what was found, the fix, and the one thing to do `next` |
| `list_branches` | none | Registered branches and their backends |
| `list_compilations` | `branch?`, `limit?` | Recent projection trail rows; default 20, max 100 |
| `list_approvals` | none | Pending approvals, the same data as the Studio's list |
| `decide_approval` | `id`, `decision` | Approve or deny |

`decide_approval` needs the `approvals:decide` scope **and** an authenticated
connection. The decision is attributed to the identity the connection
authenticated as, and there is no argument to name a different decider. A
requester can never decide its own approval.

It also needs a database role that may `UPDATE approvals`. A hardened runtime
role cannot, by design. See [Hardening](/docs/hardening).

`graft mcp` does **not** have `approvals:decide`, on purpose. The agent files
the approval and a person decides it with `graft approve`, and those have to
be different identities.

## Packages, registry and errors

| Tool | Input | Notes |
| --- | --- | --- |
| `list_packages` | `framework?`, `tier?`, `includeIndirect?` | Which `@usegraft/*` package to install, and when |
| `list_registry` | none | Owned primitives available to `graft add` |
| `describe_item` | `name` | One primitive's files and dependencies |
| `explain_error` | `code?`, `error?` | What a GraftError means and how to recover |

`list_packages` answers the question that comes before a project exists: an
agent that knows the user is on SvelteKit can name `@usegraft/sdk-sveltekit`
rather than guess. Filter by `framework` and you get that adapter plus the
packages every project needs, never a competing adapter. Filter by storage engine (`tier`) and a
static project is never told to install something that cannot work without
Postgres.

Distinct from `list_registry`: packages come from npm, primitives are copied
into your repository by `graft add`.

`explain_error` with no arguments lists every code this build knows. See the
[Error reference](/docs/errors) for the same content as a page.

## Annotations

Every tool declares MCP annotations, so a client can tell a read from a deletion
without parsing prose.

| Annotation set | Tools |
| --- | --- |
| Read-only | The four discovery tools, the three content reads, `list_branches`, `list_compilations`, `list_approvals`, `list_packages`, `list_registry`, `describe_item`, `explain_error` |
| Write, not destructive | `write_content`, `publish_drafts` |
| Destructive | `delete_content`, `discard_drafts`, `put_asset`, `decide_approval`, `run_function` |

`write_content` is marked non-destructive, because Git can undo a content
change once the earlier version is committed. Uncommitted edits it overwrites,
and new files never committed, cannot be recovered from Git. `put_asset` is
not, because the asset store keeps no history.
`run_function` is marked destructive, because it runs whatever your function
does.

`openWorldHint` is false on every tool except `run_function`. The others work
inside your own project: its content, its registry, and its error catalog.
`run_function` runs a function you wrote, and that function can call any
service, so it sets `openWorldHint: true`.

> **Annotations are hints, not security.**
> Annotations are hints. The MCP spec says a client must not make an
> authorization decision on annotations from a server it does not trust, and
> that is right. Graft's real gates are the scope check, the one-shot
> input-bound approval, and the Postgres role separation.

## Output schemas

Every tool except two declares an `outputSchema`. The SDK validates every
result against it before the answer leaves the server, so a tool that quietly
changes shape fails there rather than inside whatever the agent did next.
Results also carry `structuredContent` alongside the text block.

The two without one are `run_function`, which returns whatever your function
returns, and `delete_content`, whose result includes the deleting function's
own return value. Their shape depends on your code, so a fixed schema could not
describe them.

## Scopes

These tools check a scope at the MCP layer. `publish_drafts` also reads `content:publish` to decide between a pull request and a commit.

| Scope | Tools |
| --- | --- |
| `content:write` | `write_content`, `delete_content`, `put_asset`, `publish_drafts`, `discard_drafts` |
| `approvals:decide` | `decide_approval` |

Everything else is ungated at this layer, and `run_function` defers to the
function.

A token without the scope gets `UNAUTHORIZED`, naming the scope required and
the scopes it has. A server that allows anonymous callers checks no scopes at
all, which is why anonymous MCP is only allowed on your own machine by default.

A server given an identity resolver but no `connectionActor` refuses to start
with `CONFIG_INVALID`. Without that check, it would pass every scope check
without verifying anything.

## Static projects

Tools that need Postgres stay listed on a static project and answer
`NEEDS_DATABASE` with the upgrade rather than vanishing. `delete_content` and
`list_compilations` refuse for reasons covered in
[Storage engines](/docs/tiers#agents-on-the-static-engine).

## Call a tool from the terminal

Every tool also runs from the terminal, on the same server `graft mcp` gives
an agent and with the same checks. `graft mcp tools` lists the tools, and
`graft mcp call` runs one by name and prints its answer:

```sh
graft mcp call list_content --input '{"collection":"pages"}'
```

The tools you reach for most have their own commands. With `--json`, each
prints exactly what the tool returns.

| Tool | Command |
| --- | --- |
| `list_collections`, `list_content` | `graft content list [collection]` |
| `get_content` | `graft content get <collection>/<slug>` |
| `search_content` | `graft content search <query>` |
| `write_content` | `graft content write <collection>/<slug>` |
| `delete_content` | `graft content delete <collection>/<slug>` |
| `list_drafts`, `publish_drafts`, `discard_drafts` | `graft drafts`, `graft drafts publish`, `graft drafts discard` |
| `describe_schema` | `graft schema` |
| `list_functions`, `describe_function`, `run_function` | `graft fn`, `graft fn describe`, `graft fn run` |
| `explain_error` | `graft explain <code>` |

See [Script the CLI](/docs/cli#read-and-write-content-from-the-terminal).

## Next steps

- [Connecting agents](/docs/connecting-agents) covers transports, identity, and setup.
- [MCP resources & prompts](/docs/mcp-resources-and-prompts) covers the rest of the surface.
- [Agents overview](/docs/agent-surface) explains what agents can do.
- [Error reference](/docs/errors) lists every code a tool can return.
