MCP tools
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.
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, 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
sluginsidedatathat disagrees with theslugargument answersINVALID_SLUG. Omit it fromdata. - An executable MDX body is refused before it is stored. See 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), 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. 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.
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 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.
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.
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:
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.
Next steps
- Connecting agents covers transports, identity, and setup.
- MCP resources & prompts covers the rest of the surface.
- Agents overview explains what agents can do.
- Error reference lists every code a tool can return.