MCP Tools
Overview of Flywheel MCP tool families.
Flywheel MCP tools are grouped by workflow area:
- Graph and node creation.
- Node inspection and summarization.
- Artifact publish and retrieval.
- Hook and automation management.
- Managed compute workflows.
- Import, export, and sharing workflows.
For conceptual hook guidance, see
Hooks And Automations. For hook semantics, call
flywheel_get_contract_section with the hooks section or inspect the
generated hook API reference. Hook definitions declare one or more events in
workflow_yaml.on; supported events are artifact.finalized and
node.published.
Every tool's inputs and results are described by schemas your MCP host
receives with the tool list: inputSchema for its arguments and
outputSchema for its result. The result schemas are generated from the
server's response models, so they name each field the tool returns and its
type. If your host does not show them, call flywheel_get_contract_section
with tool_results/<tool_name> (for example
tool_results/flywheel_get_node). Call flywheel_get_contract for the
overview, and flywheel_get_contract_section for focused sections such as
graph, artifacts, hooks, compute, and campaign workflows.
What each tool says about itself
Every tool lists a short title and four behavior flags that hosts use to
decide when to ask you first:
readOnlyHint: the call changes nothing you can see. Reading audit records and refreshing your session do not count as changes.destructiveHint: for a tool that is not read-only, whether it deletes or overwrites content or configuration, revokes access, or tears down resources.idempotentHint: whether repeating the same call has no further effect.openWorldHint: whether the call itself reaches outside Flywheel (public URLs, compute providers or an LLM provider).
flywheel_get_contract lists which operations are read-only and which change
something from these same flags. Summary exports (flywheel_export_summary,
flywheel_export_summary_stream, flywheel_export_summary_pdf) are not
read-only: they call an outside model and charge your credits.
What each tool says about itself
Every tool lists a short title and four behavior flags that hosts use to
decide when to ask you first:
readOnlyHint: the call changes nothing you can see. Reading audit records and refreshing your session do not count as changes.destructiveHint: for a tool that is not read-only, whether it deletes or overwrites content or configuration, revokes access, or tears down resources.idempotentHint: whether repeating the same call has no further effect.openWorldHint: whether the call itself reaches outside Flywheel (public URLs, compute providers or an LLM provider).
flywheel_get_contract lists which operations are read-only and which change
something from these same flags. Summary exports (flywheel_export_summary,
flywheel_export_summary_stream, flywheel_export_summary_pdf) are not
read-only: they call an outside model and charge your credits.
Edit a committed node
MCP node-body editing uses a stage lease and a full payload, not the repository
CLI's operation staging area. Start with a committed node titled
Documentation MCP example, empty content and summary, null repository context
fields, and revision 0 (for example, one just created with
flywheel_commit_new_node). For another node, read its current revision and
preserve its other body fields before editing.
Replace ${NODE_ID} with that node's ID and ${STAGE_SESSION_ID} with one
unique editing-session ID. These are documentation placeholders, not literal
MCP values. Reuse the same session ID in both requests.
Call flywheel_acquire_stage_lease with:
{
"node_id": "${NODE_ID}",
"stage_session_id": "${STAGE_SESSION_ID}",
"base_committed_revision": 0
}
After acquisition succeeds, call flywheel_commit_node with:
{
"node_id": "${NODE_ID}",
"stage_session_id": "${STAGE_SESSION_ID}",
"base_committed_revision": 0,
"staged_payload": {
"title": "Documentation MCP example",
"content": "",
"summary": "Documentation MCP commit",
"repo_context": {
"repo_url": null,
"branch_name": null,
"head_commit_sha": null,
"origin_host": null,
"updated_by": null,
"external_transcript_ref": null
}
}
}
The commit advances the node to revision 1, saves the summary
Documentation MCP commit, and releases the lease. It replaces the node's
title, content and summary with the payload's values (nothing is merged),
so send the full text. Skipping acquisition fails without changing the node. A
lease lasts 45 seconds by default: for a longer edit, call
flywheel_heartbeat_stage_lease about every 15 seconds, and release an
abandoned edit with flywheel_release_stage_lease. Another session's live
lease, or a stale base_committed_revision, is refused with 409. Consult the
tool contract for revision-conflict handling.
Delete and restore nodes
flywheel_delete_node deletes a node and its descendants without a revision
check; you must be its owner or an admin. The default delete_mode,
detach_shared, keeps a descendant that has another parent outside the
deleted set; cascade deletes the whole subtree. Nodes are tombstoned, not
purged: each node and its history stay in the repository, but its parent and
child edges are removed. The result's tombstoned_node_ids lists every node
tombstoned, with the node you named first.
flywheel_bulk_delete_nodes applies the same delete_mode to several nodes.
It skips nodes you may not delete or that do not exist and lists them in
skipped_non_owned. It is not all-or-nothing: repositories are changed one at
a time, so a failure part-way can leave earlier repositories changed.
deleted_count counts the requested nodes deleted, not their descendants;
tombstoned_node_ids includes every node tombstoned, descendants included.
Call flywheel_restore_node with node_id to restore one of those nodes
from its ID alone. It brings back the title, content and history, but not
parent or child edges; add those again with flywheel_add_parent. Restore
only brings back the node you name, so call it once per ID in
tombstoned_node_ids after a delete that includes descendants.
Restore requires write access (editor, admin or owner) to the node's
repository. Without that access, an unknown ID and a purged node all answer
404. A live node answers 409. The result contains success, node_id,
repository_id, commit_id and graph_event_id. The restore tool is
mutating, full-surface only, and requires the write scope; it calls
POST /nodes/{node_id}/restore.
Share a node with an MCP collaborator
Named grants use an existing account's email; removal also accepts a user
ID. Sharing modes are private, unlisted, and public; collaborator roles
are viewer, editor, and admin. sharing_mode is required, and omitting
it is refused without a write. Node sharing is separate from
repository membership
below.
Replace one node's named people with writer@example.test as an editor:
flywheel_set_sharing_for_node({
"node_id": "<node-id>",
"sharing_mode": "private",
"collaborators": [{"email": "writer@example.test", "role": "editor"}]
})Immediately confirm the grant with the creator's own readback:
flywheel_get_node_sharing({"node_id": "<node-id>"})Apply the same visibility and explicit collaborator list to several nodes:
flywheel_set_sharing_for_nodes({
"node_ids": ["<node-a>", "<node-b>"],
"sharing_mode": "unlisted",
"collaborators": [{"email": "reader@example.test", "role": "viewer"}]
})For node and node-list setters, leaving collaborators out keeps every node's
current people and repository roles, even when the nodes have different
lists. An explicit list replaces them on each target; [] removes everyone
except the creator. No read-then-write list reconstruction is needed.
To change only named people, use people instead of collaborators:
flywheel_set_sharing_for_nodes({
"node_ids": ["<node-a>", "<node-b>"],
"sharing_mode": "unlisted",
"people": {
"add": [{"email": "new@example.test", "write": true}],
"remove": ["former@example.test", {"user_id": "<another-user-id>"}],
"set": [{"email": "reader@example.test", "write": false, "admin": false}]
}
})add only raises access; set assigns the exact role, including lowering;
remove accepts email or {user_id}. Everyone not named keeps their access.
Overlapping identities and combining people with collaborators are refused
before a write. So are keys these tools do not accept: a misspelled action
(remvoe), or role in an add or set entry, where write and admin set
the role. The error names the key and nothing changes. This holds for all three
sharing tools; the raw HTTP API still ignores unknown keys in people.
One call changes at most 1000 distinct nodes; repeated node_ids count once.
A larger list is refused with 422 and nothing changes. To share a larger
graph, use flywheel_set_sharing_for_graph, not split node-list calls.
Each node-list call saves all or nothing. Nodes the caller may not change are
skipped and listed in skipped with a reason; do not report them as updated.
A node, node-list or whole-graph save that fits in one call (see below) answers
success once committed, even if a later
result read or notification fails. An explicit save error means nothing
changed. If the connection drops without a reply, the outcome is unknown:
read sharing back before retrying. If idempotency recording failed on the
server, retrying re-applies the policy and may overwrite intervening changes.
Only the node's creator or an existing admin collaborator can read or change
its sharing policy; a plain viewer or editor collaborator cannot call
flywheel_get_node_sharing or either sharing setter for that node.
Share a whole graph
flywheel_set_sharing_for_graph shares every node of a graph you may
change, by any one of its nodes whose sharing you may change, however large
the graph is:
flywheel_set_sharing_for_graph({
"node_id": "<a node of the graph you may share>",
"sharing_mode": "public",
"collaborators": [{"email": "reader@example.test", "role": "viewer"}]
})collaborators only adds people: everyone already named on a node keeps
their access, and leaving collaborators out changes only the visibility.
Nodes you may not change are skipped and counted by reason.
The graph tool also accepts people instead of collaborators. Exact-role
set and remove work on graphs of at most 1000 nodes. Above that size the
server refuses nonempty set or remove with 422 before writing; it does
not convert them to additions or apply only part of the change.
A graph of at most 1000 nodes is saved in that call, all or nothing, and the
answer is final (applied_count, skipped). A larger graph is saved by a
sharing job on the server, a few hundred nodes at a time: the answer is the
job (job_id, state, node_count, done_count, skipped_by_reason).
While it runs, part of the graph has the new setting and the rest keeps the
old one. Follow it until state is done, stopped or failed:
flywheel_get_graph_sharing_job({"job_id": "<job id>"})Stop a specific job between member steps, then resume its unsaved work:
flywheel_stop_graph_sharing_job({"job_id": "<job id>"})
flywheel_resume_graph_sharing_job({"job_id": "<job id>"})stopping is not a completed stop: cache updates owed by committed steps
are still retrying. Only stopped or failed can resume; finished steps are
not applied again. Status keeps counts, reason and requested visibility
(null for an approval that preserves each node's visibility). A later
whole-graph share's setting survives Resume; skipped counts name
later_share. The creator or someone who may manage the original requested
node can read, stop or resume the job. Owning a newly attached root alone
does not grant these permissions.
Nodes added to the graph while it is being saved get the new setting too: the call reads the graph again before it saves, and a job reads it again before it ends and saves the nodes it gained. They are counted in the answer or the job's counts.
While a job will still save nodes of a graph, sharing that graph again is
refused with 409 naming the job. Graphs shared separately can be joined
while a job runs; each node then ends with the setting of the last-started
share that saved it. A job never overwrites a node that a share started
after it has saved, but it still saves the nodes that share skipped because
its caller may not change them.
Remove one collaborator without disturbing the rest
Use people.remove to remove only the named person; do not rebuild and
replace the other collaborators. sharing_mode is still required. If the
visibility should stay unchanged, read its current mode with
flywheel_get_node_sharing and send that mode, without copying the
collaborator list.
For a node that is currently unlisted:
flywheel_set_sharing_for_node({
"node_id": "<node-id>",
"sharing_mode": "unlisted",
"people": {"remove": ["former@example.test"]}
})For an account without an email, use
"people": {"remove": [{"user_id": "<account-id>"}]} instead. Everyone not
named keeps their access, including repository roles. Read
flywheel_get_node_sharing afterward to confirm the person is absent and
the retained collaborators and visibility are unchanged.
Explicit collaborators: [] removes every named collaborator except the
creator. Use it only when that is the intent, never as a shortcut for removing
one person.
Repository membership uses a different identity
Repository membership is a separate hosted control surface from node
sharing. It identifies a collaborator by principal UUID, not email, and
every write carries a fresh UUIDv4 membership_change_id plus the
expected_roles_digest from a just-completed member read. It has no MCP
tool binding; it is an authenticated HTTP control-plane surface. Do not
describe a repository membership call as a node sharing call. Membership
uses principal_id; the account-ID form of node removal uses user_id in
people.remove and does not add repository membership.
Read current membership and its digest:
GET /v1/repositories/{repository_id}/members
-> {
"repository_id": "...",
"roles": [{"principal_id": "...", "role": "..."}],
"roles_digest": "..."
}Grant or change a member's role, reusing the digest from that read:
PUT /v1/repositories/{repository_id}/members/{principal_id}
Idempotency-Key: <membership_change_id>
{
"wire_version": 1,
"membership_change_id": "<fresh-uuidv4>",
"expected_roles_digest": "<digest-from-member-read>",
"role": "editor"
}Remove a member the same way, with its own request shape:
POST /v1/repositories/{repository_id}/membership-removals
Idempotency-Key: <membership_change_id>
{
"wire_version": 1,
"membership_change_id": "<fresh-uuidv4>",
"expected_roles_digest": "<digest-from-member-read>",
"principal_id": "<principal-id>"
}Campaign submissions
When participating in a campaign, treat metadata.campaign_role = "submission"
as the marker for the artifact that should be accepted by the campaign. Read
the campaign config before finalizing it. If the config declares
submission_policy.required_visibility = "public", make the attempt node public
first; private drafts should remain ordinary artifacts without the submission
role.
After a valid submission artifact is finalized, Flywheel creates a campaign
submission lifecycle record. Use flywheel_get_artifact_campaign_submission to
check the status for a known node/artifact pair,
flywheel_list_node_campaign_submissions to list statuses for an attempt node,
or flywheel_get_campaign_submission when you already have the submission id.
Invalid submissions return 422 during finalization and do not create lifecycle
records. forwarded means Flywheel handed the submission to campaign
automation; terminal states are scored, rejected, and failed.
Mutating tools can return HTTP 429 when a user exceeds write limits. See
Usage Limits for the current limits and retry
behavior.