MCP tools

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

Agents & MCP9 min read

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

ToolInputReturns
list_collectionsnoneEvery registered collection on this branch
describe_schemanoneCollections with full field descriptors, plus the project's functions
list_functionsnoneEvery typed function
describe_functionnameOne 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

ToolInputNotes
list_contentcollectionReads the authored MDX files, not the index
get_contentcollection, slugReturns data, body, and sourcePath
search_contentquery, 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

ToolScopeNotes
write_contentcontent:writeValidates, writes <contentDir>/<collection>/<slug>.mdx, compiles
delete_contentcontent:writeDestructive and human-gated
put_assetcontent:writeUploads a binary for field.asset
list_draftsnoneHosted on GitHub only. This connection's unpublished changes
publish_draftscontent:writeHosted on GitHub only. A pull request, or a commit with content:publish
discard_draftscontent:writeHosted 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.

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

ToolInputNotes
run_functionname, 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

ToolInputNotes
setup_statusnoneLocal 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_branchesnoneRegistered branches and their backends
list_compilationsbranch?, limit?Recent projection trail rows; default 20, max 100
list_approvalsnonePending approvals, the same data as the Studio's list
decide_approvalid, decisionApprove 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

ToolInputNotes
list_packagesframework?, tier?, includeIndirect?Which @usegraft/* package to install, and when
list_registrynoneOwned primitives available to graft add
describe_itemnameOne primitive's files and dependencies
explain_errorcode?, 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 setTools
Read-onlyThe four discovery tools, the three content reads, list_branches, list_compilations, list_approvals, list_packages, list_registry, describe_item, explain_error
Write, not destructivewrite_content, publish_drafts
Destructivedelete_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.

ScopeTools
content:writewrite_content, delete_content, put_asset, publish_drafts, discard_drafts
approvals:decidedecide_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.

ToolCommand
list_collections, list_contentgraft content list [collection]
get_contentgraft content get <collection>/<slug>
search_contentgraft content search <query>
write_contentgraft content write <collection>/<slug>
delete_contentgraft content delete <collection>/<slug>
list_drafts, publish_drafts, discard_draftsgraft drafts, graft drafts publish, graft drafts discard
describe_schemagraft schema
list_functions, describe_function, run_functiongraft fn, graft fn describe, graft fn run
explain_errorgraft explain <code>

See Script the CLI.

Next steps