Trov's MCP server is https://trov.dev/mcp (Streamable HTTP). It gives 44 tools to every member, plus 1 to an organization's admins (45 in all). This page is written from the server's own tool list at every build, so the names, descriptions and arguments below are exactly what your client receives from tools/list. To connect a client: Connect an MCP client. What staged and live mean: Concepts.
Common arguments
These decide which organization a call acts in (see get_connection), and the tool itself never sees them. Every tool below that lists them takes them, the same way:
repo(string, optional): The GitHub repository you are working in, as owner/name (fromgit remote get-url origin). A connection that follows the repository acts in the organization that has it connected, and reads and writes nothing without it; any other connection ignores it. With no checkout (a chat on the web), ask the person which repository they mean — never guess one.org(string, optional): An organization's slug, to act there for THIS call only — one this connection is allowed to use (see get_connection). Omit it to act in the connection's organization.
Read
Read-only. Orient with these before working an existing area; every result says whether it is settled (live) or only proposed.
query
Retrieve assembled context from the team brain (Trov): whole authoritative bodies for the top hits plus ranked pointers to the rest, over docs, decisions, feed, sprints and artifacts (an artifact's id is its slug — open it with artifact_get; its body starts Status: <draft|published|ratified> · v<n>). Each result is flagged live / staged_pending / unpromoted / draft — treat anything not 'live' as not-yet-settled. Use this to orient before working an existing area and ALWAYS before proposing a doc change. Read-only and safe to call freely.
Arguments:
q(string, optional)types(array ofdoc|decision|feed|sprint|artifact, optional)section(string, optional)space(technical|product, optional)include_staged(boolean, optional)limit(number, optional)pointer_limit(number, optional)
Also takes repo and org (see Common arguments).
get_doc
Get a doc and all its versions by slug.
Arguments:
slug(string, required)
Also takes repo and org (see Common arguments).
list_docs
List docs, optionally filtered by section.
Arguments:
section(string, optional)
Also takes repo and org (see Common arguments).
get_feed
Read the feed with optional author/tags/since/limit filters. Each entry carries its tags; tags filters to entries that carry ANY of the given ones (names from list_tags).
Arguments:
author(string, optional)tags(array of string, optional)since(string, optional)limit(number, optional)
Also takes repo and org (see Common arguments).
list_tags
List this organization's tags: the only tags a feed entry may carry. Each has a name and a one-line description of what it is for. The list is the organization's own (its admins add, rename, merge and retire tags in Org settings), so read it rather than assuming one — and pick from it. Read-only.
No arguments of its own.
Also takes repo and org (see Common arguments).
get_roadmap
Read the roadmap plan: admin narrative + sprints in target-date order with their progress — progress is the sprint's TICKETS (done + declined over total), issues the cached GitHub issue counts behind it (no live GitHub). Each sprint carries label, summary, phase, dates, due, status, active, urgency, lead and domain.
No arguments of its own.
Also takes repo and org (see Common arguments).
get_my_work
Your personal My Work projection (D1 only, no live GitHub): previous-activity (your most recent summarized merged/closed PRs), to-do (your open assigned GitHub issues), and tickets (your open assigned tickets, native AND mirrored from GitHub issues — source: "github" marks a mirrored one, whose issue may also be in to-do; capped, with ticketsTotal the full count). Read-only.
No arguments of its own.
Also takes repo and org (see Common arguments).
get_events
Recent captured GitHub events (raw log behind My Work and roadmap progress). Filter by type/subject. Read-only.
Arguments:
type(pr_merged|pr_closed|issue, optional)subject(string, optional)limit(number, optional)
Also takes repo and org (see Common arguments).
get_repo_dashboard
The Repo dashboard for the org's main repository: environments (each part's newest deploy) and deploy strips, CI, code activity, usage (requests and errors summed over each environment's web parts, active users), each hosting provider part's deploys / traffic / resources (providers: one entry per part of each environment, whatever its host — Cloudflare, Railway, Vercel, Render, Netlify, Fly.io — with its status, tone, last_poll and the metrics the provider cannot read in unavailable), the app's product metrics, and planning — read from Trov's own database, never live GitHub or a live provider. Every section is ok, empty (connected, nothing to show) or not_connected (never captured): treat anything not ok as unknown, never as zero. The same holds INSIDE an ok section: a null figure (usage[].requests / errorRate / users, a providers[].traffic figure or resources.cpu / mem_mb, a product value, contributors[].reviews, ciFailures.rate, prs.openCount (open PRs not captured yet), a null or empty delta, a null sha or checks) is unknown / not captured — never zero — and usage[].seen / providers[].seen say whether that source has EVER reported (null + not seen = not connected; null + seen = no recent reading). Optional tab (overview | code | ci | usage | planning) returns only the sections that tab shows; range (24h | 7d | 30d, default 7d) picks the one view the usage / product sections and each provider part's traffic return; include_trends (default false) adds the sparkline trend arrays, the full drift breakdown and every provider deploy — without it drift.groups is the first 20 groups, each with a commitCount instead of its commits, and drift.groupCount is the full number, and each providers[] entry lists its newest 2 deploys with deployCount the full number; with include_trends: true every group is returned with its commits, and every part with all its deploys (up to 10). Leave it off unless you need the series. Optional repo (one of the organization's repositories; omitted, or one that is not the organization's, reads its main one — the output's repo says which) and app (an app key of it, or __repo for files outside every app; unknown = every app) narrow the read: the per-part sections (environments' parts, deploys, usage, providers) and the facts. With tab overview or no tab it also returns facts — the Overview's facts: apps, environments that have a part of them, each part (app × environment) with its serving / latest deploy and history, health (stale = over 30 minutes old), previews matched to open PRs, not shipped (count: null = not computed for what serves now), out of step, the promotion-chain drift, CI per environment, week figures (value / delta null = unknown, never zero) and structured attention items — and layout, the opener those facts decide (the same rule the screen applies). Without include_trends each commit group in facts carries commitCount instead of its commits (first 20 groups, groupCount the full number) and each part's history its newest 2 deploys (historyCount the full number). Returns { repo, generatedAt, degraded, tab, range, sections, facts?, layout? }; degraded: true means a database read failed and the sections fell back. Read-only and safe to call freely.
Arguments:
tab(overview|code|ci|usage|planning, optional)range(24h|7d|30d, optional)include_trends(boolean, optional)repo(string, optional): The GitHub repository as owner/name — the one you are working in (git remote get-url origin). A connection that follows the repository acts in the organization that has it connected; on any connection it is the repository whose dashboard is read when it is one of the organization's repositories — omitted, or one that is not, reads the organization's main repository.app(string, optional): Narrow to one app: an app key of the repository, or__repofor files outside every app. Unknown reads as every app.
Also takes org (see Common arguments).
list_people
Read-only: every person in the org as { handle, name, role, responsibilities }. Call this BEFORE choosing assignees on create_ticket: match the work to each person's role and responsibilities and use their handle. role / responsibilities may be null — that means unknown, never guess what someone owns from their name or handle; if nobody clearly fits, file the ticket unassigned (or ask the person you are working for) rather than pick someone. Returns { people }.
No arguments of its own.
Also takes repo and org (see Common arguments).
Contribute
Knowledge an agent proposes. Each goes through the gate and is staged; a person promotes it in the web app.
append_feed
Append a feed entry through the gate. tags must come from THIS organization's tag list — read it with list_tags; every organization has its own. A tag that is not on the list does not hold the entry back: the entry is written with the tags that are, and the other is returned under suggested_tags and waits for an admin to accept, map or dismiss it (you cannot add a tag to the list). summary is the one-line title. brief is what PEOPLE read in the Feed: 1–2 plain sentences (≤ 280 characters) on the problem solved and who it helps — no file names, PR/issue numbers or jargon. body is the full record agents read (what changed, why, evidence, follow-ups). Optional prs/commits/issues record the artifacts (PR urls, commit shas, GitHub issue numbers) this session observed.
Arguments:
summary(string, required)brief(string, optional)body(string, optional)tags(array of string, optional)prs(array of string, optional)commits(array of string, optional)issues(array of number, optional)
Also takes repo and org (see Common arguments).
propose_doc_update
Propose a doc version through the reconciling gate. Images: embed only uploaded doc images, as  — upload each first with upload_asset (destination "doc"); a body with a not-yet-uploaded /img ref or any other image source (external URL, data: URI) is REFUSED ({ outcome: "refused", reason }), nothing staged. Out-of-vocab section or low confidence on a NEW slug routes to needs_triage; an unchanged body is dropped; otherwise staged non-destructively (current_version untouched) and classified new/edit/rewrite. Pass base_version (the current_version you read) so a stale edit is flagged, space ('technical' for engineering docs or 'product' for product docs; defaults 'technical') to place a new doc, and force to stage an identical body.
Arguments:
slug(string, required)section(string, required)title(string, optional)body(string, required)change_summary(string, required)confidence(high|low, required)space(technical|product, optional)base_version(number, optional)force(boolean, optional)
Also takes repo and org (see Common arguments).
record_session
Record a whole Claude Code session into Trov in ONE reconciled batch: pass a full IngestPayload (session + feed_entries / doc_proposals / adr_drafts / needs_triage, and optional artifact_links). A doc proposal may embed only uploaded doc images (, uploaded with upload_asset destination "doc" BEFORE this call); one that breaks that rule is listed under refused with its reason and is not ledgered, so re-sending the same batch after uploading stages it. Routes through the SAME gate as /ingest — drops no-ops, stages real deltas, classifies each doc change, and is replay-safe on session.id. The author is your authenticated bearer principal; session.author is advisory and ignored. Returns per-type outcome counts. A feed entry's tags must come from the organization's list (list_tags): an entry with a tag that is not on it is still written, without that tag, and the tag is listed under suggested_tags for an admin to decide. artifact_links ([{slug, target_type: ticket|sprint|pr|issue, target_ref}], for artifacts this session produced) are NOT staged: after the batch is reconciled each is linked directly, as you, and reported in artifact_links as linked / not_found / error (idempotent — a replay re-links nothing). Used by the record-session skill at session end; you only ever stage knowledge — humans confirm.
Arguments:
session(object { id, author, ended_at, skill_version }, required)feed_entries(array of object { summary, brief, body, tags, artifacts }, optional)doc_proposals(array of object { slug, section, title, body, change_summary, confidence, space, base_version, force }, optional)adr_drafts(array of object { title, context, decision, rationale, confidence }, optional)needs_triage(array of object { raw, reason }, optional)artifact_links(array of object { slug, target_type, target_ref }, optional)
Also takes repo and org (see Common arguments).
Tickets
The ticket queue. Reading and filing are open; every other write needs the ticket to be assigned to you already.
list_tickets
Read-only: the org's ticket queue. Tickets are Trov D1 rows the whole org files into (ADR-007, amended): a ticket may LINK to GitHub or Figma work, and may be SOURCED from a GitHub issue, but is never the issue itself. A mirrored ticket has source 'github' and source_ref 'owner/repo#n'; its title/body/assignees were copied at import and are Trov's since, while closing or reopening the issue on GitHub closes or reopens it. Its source link is locked (never removable). Filter with seg ('open' = submitted + in_progress + testing, the default / 'closed' = done + declined / 'all'), assignee ('anyone' default, 'me' = you, the bearer principal, 'unassigned') and category. Newest-updated first; each row carries its assignees, link/sub-ticket counts and sprint label. Reading is unscoped: you see the whole org's queue. WRITING is scoped to your own lane — see create_ticket and transition_ticket.
Arguments:
seg(open|closed|all, optional)assignee(anyone|me|unassigned, optional)category(bug|request|question|access|other, optional)
Also takes repo and org (see Common arguments).
get_ticket
Read-only: one whole ticket by id — a ticket's id is its NUMBER within your organization (#12), here and in every ticket tool's arguments and results (parent_id, sub-tickets, ticket_id) — body, category, priority, status, requester, assignees, linked work, comments, the full status history, its parent and sub-tickets, its sprint, and artifacts ([{slug, title, kind, status, version}] — the artifact pages linked to it that you can see; open one with artifact_get). A ticket is a Trov D1 row that may be sourced from a GitHub issue but is never the issue itself (ADR-007, amended) — source, source_ref and each link's locked say whether it is mirrored and which link is its source. Read this BEFORE any write: its assignees tell you whether the ticket is in your lane at all.
Arguments:
id(number, required)
Also takes repo and org (see Common arguments).
create_ticket
File a ticket. THE ONE UNSCOPED WRITE — you may file freely; every other ticket write requires the ticket to be assigned to you already. The requester is YOU (the bearer principal); a client-supplied requester is ignored. assignees (person handles) — call list_people first and match the work to each person's role and responsibilities. After filing, assign_ticket adds or removes one assignee (as the ticket's requester you may, so you can route it later). Optional link takes a bare issue number ('#214'), a GitHub/Figma URL, or any URL. sprint_id omitted = the backlog. Returns the whole ticket. Confirm the exact fields with the person before calling — a ticket is org-visible the moment it exists.
Arguments:
title(string, required)body(string, optional)category(bug|request|question|access|other, optional)priority(low|normal|high, optional)assignees(array of string, optional)sprint_id(number or null, optional)link(string, optional)
Also takes repo and org (see Common arguments).
edit_ticket
Edit a ticket's title and/or body (markdown). SCOPED: only on a ticket already assigned to you, else forbidden and nothing is written. Pass at least one of title / body; the other is left as it is. A ticket mirrored from a GitHub issue is editable too — its title and body were copied from the issue when it was imported and are Trov's from then on (GitHub edits never overwrite them). Records no history row (history is status moves). Returns the whole ticket.
Arguments:
id(number, required)title(string, optional)body(string, optional)
Also takes repo and org (see Common arguments).
transition_ticket
Move a ticket's status. SCOPED: only on a ticket already assigned to you, else forbidden and nothing is written. Any status may move to any other — done and declined included, so a resolved ticket can be moved back (only a move to the status it already has is conflict, and writes nothing). done/declined resolve the ticket for the whole org, so confirm with the person first. Appends a ticket_events row attributed to you.
Arguments:
id(number, required)to(submitted|in_progress|testing|done|declined, required)
Also takes repo and org (see Common arguments).
add_ticket_comment
Append a comment to a ticket. SCOPED: only on a ticket already assigned to you. Raw text — mentions are a rendering concern, not a write one. Bumps the ticket's updated_at (the queue's sort key), and is attributed to you with nothing marking it as agent-written, so say so in the text if the team wants that.
Arguments:
id(number, required)body(string, required)
Also takes repo and org (see Common arguments).
add_ticket_link
Attach linked work to a ticket (GitHub issue/PR, Figma file, or any URL). SCOPED: only on a ticket already assigned to you. raw is parsed by the same parser the web UI uses: a bare '#214' or '214' resolves against the org's primary repository, github.com and figma.com URLs are labelled by kind, anything else is a plain link. An unusable input is bad_request.
Arguments:
id(number, required)raw(string, required)
Also takes repo and org (see Common arguments).
set_ticket_sprint
Move a ticket into a sprint, or back to the backlog with sprint_id null. SCOPED to a ticket assigned to you — with ONE exception: an ADMIN may re-home any ticket, because composing a sprint is sprint management. This is the only ticket verb an admin may use outside their own lane; it moves the ticket and nothing else. An unknown sprint is not_found, and nothing is written.
Arguments:
id(number, required)sprint_id(number or null, required)
Also takes repo and org (see Common arguments).
set_ticket_parent
Nest child_id under ticket id. SCOPED on BOTH tickets — the call re-homes the child and changes the parent's shape, so both must already be assigned to you. Tickets nest EXACTLY ONE level: it is a conflict (writing nothing) if the parent already has a parent, the child already has a parent, the child is done/declined, or the child has sub-tickets of its own.
Arguments:
id(number, required)child_id(number, required)
Also takes repo and org (see Common arguments).
assign_ticket
Add (on: true) or remove (on: false) ONE assignee on an existing ticket. login is a person HANDLE — take it from list_people, matching the work to each person's role and responsibilities; an unknown handle is bad_request. SCOPED by its own rule, not the lane: only an ADMIN, the ticket's REQUESTER or one of its CURRENT assignees may (re)assign it, else forbidden and nothing is written. Idempotent: adding someone already assigned, or removing someone who isn't, succeeds and writes nothing. It never changes status (assigning a submitted ticket does not start it — use transition_ticket), and records no history row — history is status moves, so nothing records who (un)assigned, exactly as with the web UI's picker. Works on a ticket mirrored from a GitHub issue — its assignees are Trov's after import. Removing yourself takes the ticket out of your lane. Returns the whole ticket; read assignees back. Confirm who with the person before calling.
Arguments:
id(number, required)login(string, required)on(boolean, required)
Also takes repo and org (see Common arguments).
Sprints
The roadmap's sprints. Direct writes, open to every member; a sprint is finished only when a person says so.
list_sprints
Read-only: every sprint in roadmap order. Sprints are the roadmap's containers — a sprint holds tickets, and its progress is its TICKETS only (closed/total/pct, where closed = done + declined). The GitHub issues behind a sprint are a separate issues field, the cached closed/total from its github_ref (null when it has no cache row); no live GitHub at read time. Each carries label, summary, phase, dates (a free-text label), start and due (YYYY-MM-DD or null), status/active, urgency, lead, domain and members (the handles assigned to its tickets). Sprint writes (create_sprint / set_sprint_active / complete_sprint / add_sprint_resource / delete_sprint) are open to every principal; only the bulk plan write update_plan is admin-only.
No arguments of its own.
Also takes repo and org (see Common arguments).
get_sprint
Read-only: one sprint by id, with its tickets ordered roots-then-sub-tickets and its resources (the sprint's own links merged with its tickets', deduped by url), on top of everything list_sprints returns including the tickets-only progress and the separate cached issues counts. A sprint is completed by a person (complete_sprint), never inferred from its tickets resolving.
Arguments:
id(number, required)
Also takes repo and org (see Common arguments).
create_sprint
Create a sprint. It lands INACTIVE and unscheduled — status 'upcoming', phase 'Unscheduled' unless you pass one, and no due stores an empty target date that reads back as due: null (those sort last on the Roadmap). start and due are real calendar days written YYYY-MM-DD, start on or before due — anything else ("Oct 17", 2026-02-30) is refused and nothing is written. dates is only an optional free-text display label. label is the sprint name; lead is a person handle. Which TICKETS are in the sprint is not set here — that is set_ticket_sprint. Direct promote-class write, not staged.
Arguments:
label(string, required)dates(string or null, optional)summary(string or null, optional)description(string or null, optional)urgency(low|normal|high, optional)start(string or null, optional)due(string or null, optional)lead(string or null, optional)domain(notifications|tickets|gate|feed|search|infraor null, optional)phase(string or null, optional)
Also takes repo and org (see Common arguments).
set_sprint_active
Move a sprint between the Roadmap's In Progress and Upcoming groups. active is DERIVED from status, never stored: true → 'in_progress' (from ANY status, including 'done' — that is re-opening a sprint that turned out not to be finished); false → 'upcoming', EXCEPT on a done sprint where it is a NO-OP, because clearing 'active' must never un-finish a sprint.
Arguments:
id(number, required)active(boolean, required)
Also takes repo and org (see Common arguments).
complete_sprint
Flip a sprint to 'done'. A sprint is completed by a PERSON — 'done' is NEVER inferred from its tickets resolving or its GitHub issues closing, not by the cron, not by the webhook, not by this tool being available. Confirm with the person before calling: it is how the Roadmap reports the sprint finished. Already-done is an error, not a silent no-op.
Arguments:
id(number, required)
Also takes repo and org (see Common arguments).
add_sprint_resource
Attach a resource link to the sprint itself (as opposed to one of its tickets). raw goes through the SAME parser as ticket links, so '#214' means the same thing wherever it is typed. Idempotent on url. The sprint's read model merges these with its tickets' links, deduped by url.
Arguments:
id(number, required)raw(string, required)
Also takes repo and org (see Common arguments).
delete_sprint
Delete a sprint for good (hard delete). Its tickets are NOT deleted — they move to the backlog (no sprint) and keep all their history; the sprint's own resources go with it. Answers with the label and how many tickets moved. Confirm with the person before calling: the Roadmap loses the sprint and it cannot be undone.
Arguments:
id(number, required)
Also takes repo and org (see Common arguments).
Handoffs and prompts
Notes from one session to the next, and the team's reusable prompts. A saved prompt is staged until a person publishes it.
send_handoff
Leave a handoff for the next session: where a task stands when you stop mid-way (context running out, switching person, ending the session). Send exactly ONE per session and tell the person its id as #N. recipient is a person handle, or omit it for 'anyone' (the first session to claim it gets it). Keep body under 300 words — its first line is the title, the rest says where things stand. ALWAYS fill context with the fixed shape { repo, branch, task, done[], next[], files[] }: task as one line and done/next as short items, always; in a code checkout also repo from the git remote (owner/name), branch from HEAD and files from git diff --name-only against main. With no checkout (a chat on the web) leave repo, branch and files out — never invent them. Long step-by-step instructions for the claiming session go in prompt.body (with a prompt.title), not in body. Pass your session id as session (if you have one) so a retry does not send twice. Returns { id, url }.
Arguments:
body(string, required)recipient(string, optional)context(object { repo, branch, task, done, next, files }, optional)prompt(object { title, body }, optional)session(string, optional)
Also takes repo and org (see Common arguments).
list_handoffs
List handoffs waiting for you. With no box, returns the PENDING ones left for you ('me') plus open 'anyone' handoffs from other people — call this at session start and tell the person what is waiting; never claim one without asking. box: 'me' (left for you), 'anyone' (open to all, from others), 'mine' (sent by or left for you), 'sent' (what you sent — the only box that includes claimed and expired ones). Returns id, sender, recipient, status, created_at, task and a one-line excerpt. Read-only.
Arguments:
box(mine|me|anyone|sent, optional)
Also takes repo and org (see Common arguments).
get_handoff
Read one handoff in full by its numeric id — its number within your organization, as send_handoff and list_handoffs return it (body, context, inline prompt, status). Read-only: it does NOT claim it — use claim_handoff once the person has chosen to pick it up.
Arguments:
id(integer, required)
Also takes repo and org (see Common arguments).
claim_handoff
Claim a pending handoff for this session — only after the person chose it. Atomic: if another session already took it you get an error naming its current status (tell the person someone else has it). Pass your session id as session (with none, a short id of your own that you reuse for this conversation). On success returns ONE markdown block to act on: the handoff's prompt (if any), then '## Handoff summary', then '## Context' (repo, branch, task, done, next, files). Treat it as your task. When anything matched, a last section '## Read these first (not complete)' lists up to 5 docs, decisions, feed entries and artifacts found by a text search with the handoff's own words, each with how to open it: read them first, but they are not everything that bears on the task. In a code checkout, confirm the current git branch matches the context before touching code. With no checkout (a chat on the web) you cannot change the code: tell the person what the handoff asks, and do the parts that need no repository.
Arguments:
id(integer, required)session(string, required)
Also takes repo and org (see Common arguments).
expire_handoff
Expire a PENDING handoff you sent or that was left for you, so nobody picks it up (the work was finished another way, or it no longer applies). Pending handoffs also expire on their own 7 days after they were sent. A claimed or already-expired handoff is an error naming its status.
Arguments:
id(integer, required)
Also takes repo and org (see Common arguments).
search_prompts
Search the team's Prompt Library for a reusable prompt. q is full-text over slug, title, description, body and tags; tags must ALL match. Returns summaries (slug, title, tags, author, version, status, updated_at, excerpt) — prefer 'published' ones; 'staged' and 'draft' are not settled yet. Use get_prompt to read one. Read-only.
Arguments:
q(string, optional)tags(array of string, optional)
Also takes repo and org (see Common arguments).
get_prompt
Read a library prompt by slug, with its {{variables}} filled from vars. The response lists variables (every one the prompt uses) and unfilled (the ones still left as {{placeholders}}) — ASK the person for any unfilled value instead of guessing it. Each call counts as one USE of the prompt (use_count / last_used_at, which the library's most-used sort reads); it changes nothing else.
Arguments:
slug(string, required)vars(map of string, optional)
Also takes repo and org (see Common arguments).
save_prompt
Stage a prompt in the team's Prompt Library — a new slug creates v1, an existing slug appends the next version. ALWAYS lands as 'staged' (whatever you intend): a human must publish it in Trov before it is settled, and you cannot rename a slug. Only save instructions you have had to write out twice; the slug is 2–60 chars of a-z, 0-9 and '-'. Write {{name}} for anything the caller fills in. Pass branch (your git branch, when you are in a checkout) for the default version note. Returns { slug, version, status }.
Arguments:
slug(string, required)title(string, required)body(string, required)tags(array of string, optional)summary(string, optional)branch(string, optional)
Also takes repo and org (see Common arguments).
Artifacts and doc images
Versioned pages and files the team keeps, and the images a doc embeds. Only a person ratifies an artifact.
upload_asset
Put something into Trov: an ARTIFACT page, or an IMAGE for a doc. destination picks which (default "artifact").
destination "doc" — an image a doc embeds. Pass sha256 (hex, shasum -a 256 img.png), size_bytes and content_type (image/png | image/jpeg | image/gif | image/webp; ≤ 10 MB); kind may be omitted (it is always image) and page fields (title, area, …) are refused. → { destination, ref: "/img/<sha256>", markdown, sha256, uploaded }. uploaded: true = that exact image is already stored, nothing to PUT. Otherwise also { upload_url, expires_at }: PUT the exact bytes (single use, 5 minutes, e.g. curl -X PUT --data-binary @img.png -H "Content-Type: image/png" "<upload_url>"). THEN reference it in the doc body as  and propose the doc (propose_doc_update / record_session). The doc gate REFUSES a body whose image is not uploaded yet, and any other image source (an external URL, a data: URI) — upload first, then propose. Images are immutable: a new picture is a new sha256.
destination "artifact" (default) — create an artifact page (v1, status draft): a rendered HTML page, markdown doc, SVG, mermaid diagram, image, PDF or file the team keeps and versions. Needs title, kind, area, repo, visibility. Text kinds (html | markdown | svg | mermaid; ≤ 750 KB): pass content → { id, slug, url, version }. Binary kinds (image | pdf | file; ≤ 10 MB): pass size_bytes and sha256, NOT content → { id, slug, url, upload_url, expires_at }; then PUT the exact bytes to upload_url (single use, 5 minutes) — the page does not exist to anyone until that PUT lands. area is one of auth | architecture | infra | api | ui | data; repo is owner/repo or ""; visibility org (the whole org) or private (only you). Optional links ([{target_type: ticket|sprint|pr|issue, target_ref}]) and summary. You author it; a PERSON ratifies it on the web — there is no ratify tool. Contract: docs/artifact-contract.md.
With no shell (a chat on the web), a binary file — for either destination — goes INSIDE the call instead: pass content_base64 (the file's bytes, base64, ≤ 1 MB; content_type still required for an image) and NOT size_bytes / sha256 → the same result with version (an artifact) or uploaded: true (a doc image) and no upload_url: nothing is left to PUT. Use it only for bytes you actually hold; a larger file needs the PUT. Every result has warnings — non-empty when artifact content calls something only claude.ai has (window.claude, window.storage, api.anthropic.com); the page is still created.
Arguments:
destination(artifact|doc, optional)title(string, optional)kind(html|markdown|svg|mermaid|image|pdf|file, optional)area(auth|architecture|infra|api|ui|data, optional)repo(string, optional)visibility(org|private, optional)content(string, optional)links(array of object { target_type, target_ref }, optional)summary(string, optional)size_bytes(integer, optional)sha256(string, optional)content_type(string, optional)filename(string, optional)content_base64(string, optional)
Also takes org (see Common arguments).
artifact_update
Add a version to an artifact you can see (any org page, or your own private one). A new version is published and clears any ratification. Text kinds: pass content (the whole new body) OR old_str + new_str (an exact edit of the latest version — old_str must occur EXACTLY once) → { id, slug, url, version, unchanged } (unchanged: true = identical to the latest, nothing written). Binary kinds: pass size_bytes + sha256 (+ optional content_type / filename) → { id, slug, url, upload_url, expires_at }, then PUT the bytes (single use, 5 minutes); with no shell, pass content_base64 instead (≤ 1 MB) → { id, slug, url, version, unchanged }, nothing to PUT. summary says what changed. Every result has warnings (claude.ai-only calls in the content — a warning, never a rejection). An unknown or private-to-someone-else slug is { error: "not_found", code: "not_found" }.
Arguments:
slug(string, required)summary(string, required)content(string, optional)old_str(string, optional)new_str(string, optional)size_bytes(integer, optional)sha256(string, optional)content_type(string, optional)filename(string, optional)content_base64(string, optional)
Also takes repo and org (see Common arguments).
artifact_get
Read one artifact: metadata (title, kind, area, repo, author, status draft | published | ratified, visibility, versions, links, ratified_version) plus, for text kinds, content of the requested version (binary kinds: content is null). A text version over 64 KB is NOT inlined: content is null and content_omitted is true — pull it with download_url and grep / read the slices you need, or pass include_content: true only when you genuinely need the whole text in context (content_omitted is false on every other result). To get the FILE — any kind, binary included — use download_url: absolute, signed for you, reusable for 5 minutes (download_expires_at), no header needed: curl -fsSL "$download_url" -o <path> returns the exact stored bytes as an attachment named download_filename. Verify it against sha256 / size_bytes (this version's; shasum -a 256 <path>). Expired → HTTP 410: call artifact_get again. With no shell (a chat on the web): read a text kind from content; for a binary kind pass include_file: true — the file itself comes back beside the metadata (an image you can look at; a PDF or other file as an embedded resource) when it is ≤ 1 MB, and file says what happened: "included", "too_large" (give the person url), "text" (it is in content) or "unavailable". Leave it off when you can use download_url: the file lands whole in your context. raw_url is the browser view (signed-in session only — it does not take your bearer). slug may name a version (slug@v3 or slug/v3), or pass version; default the latest. url is the page in the Trov web app — share that when a person just wants the link. warnings flags claude.ai-only calls in the content. Only ratified is team-confirmed; draft / published are one person's word. An unknown, private-to-someone-else or not-yet-uploaded slug is { error: "not_found", code: "not_found" }.
Arguments:
slug(string, required)version(integer, optional)include_content(boolean, optional)include_file(boolean, optional)
Also takes repo and org (see Common arguments).
artifact_list
Read-only: the artifact pages you can see (every org page, plus your own private ones), newest first — the same filters as the web library. All optional: q (full text over title / summary / body, or a title / slug substring), kind (html | markdown | svg | mermaid | image | pdf | file), area (auth | architecture | infra | api | ui | data), author (a handle), status (draft | published | ratified), ticket / sprint (an id — pages linked to it), limit (default 25, max 100). → { artifacts: [{ slug, title, kind, status, version, updated_at, url, area, author, visibility }], total, truncated }. Open one with artifact_get (its download_url fetches the file). Only ratified is team-confirmed.
Arguments:
q(string, optional)kind(html|markdown|svg|mermaid|image|pdf|file, optional)area(auth|architecture|infra|api|ui|data, optional)author(string, optional)status(draft|published|ratified, optional)ticket(integer or string, optional)sprint(integer or string, optional)limit(integer, optional)
Also takes repo and org (see Common arguments).
Connection
About the connection itself, not an organization's data: who you are, and which organization a call acts in.
get_connection
Who and WHERE this connection is — call it first in a session and tell the person which organization you are in. Returns { handle, mode, organization, role, current, organizations, unresolved }. mode is "manual" (the connection acts in its current organization; organizations are the ones it is allowed to use — switch with switch_org, or pass org on one call) or "repo" (it follows the repository: every call passes repo, and acts in the organization that has that repository connected; organizations are the ones it could reach). organization ({ slug, name }) is where a call with THESE arguments would act, with your role there — pass the same repo / org you will pass to the other tools. organization: null means such a call would read and write NOTHING; unresolved ({ code, message }) says why and what to do. Read-only: it touches no organization's data and changes nothing.
No arguments of its own.
Also takes repo and org (see Common arguments).
switch_org
Change the organization a MANUAL connection acts in — its current organization — to another one it is ALREADY allowed to use (organizations from get_connection). It lasts until switched again and applies to EVERY session using this connection, so tell the person, and for a single call in another organization pass org on that call instead. It cannot add an organization: a person does that, signed in, in Trov › Settings › MCP access. A connection that follows the repository has nothing to switch (pass repo), and one bound to a single organization cannot. Returns { current: { slug, name }, organizations }.
Arguments:
org(string, required): The slug of the organization to switch to — one oforganizationsfrom get_connection.
Admin
Listed only for an organization's admins.
update_plan
Admin only: listed for a person who is an admin or owner of an organization the connection reaches.
ADMIN plan write: replace the roadmap narrative and create/update sprints (including status 'done') in one direct, non-destructively versioned write — same authored-write class as promote, NOT the ingestion gate. Sprints not listed are untouched. label is the sprint name, start its start date and due its target date — each a real calendar day written YYYY-MM-DD (due may be "" for unscheduled; start omitted keeps the stored one, null clears it), start on or before due, else the whole call is refused and nothing is written. dates is only a free-text display label. Which tickets are IN a sprint is set from the Tickets UI, not here. The narrative is SHORT — 2–3 sentences up to two short paragraphs (Now / Next / Later), at most 800 characters after trimming; leave out sprint-by-sprint detail, issue lists, dated status logs and ops steps (the sprints and the timeline carry those). Over the cap the whole call is refused and nothing is written — plan, versions and sprints alike. The narrative is always a full replacement, so an over-cap narrative already stored must be shortened, not resent. Use via the update-plan skill.
Arguments:
narrative(string, required): The whole roadmap narrative (markdown), replacing the current one. SHORT: 2–3 sentences to two short paragraphs — what is happening now, next, later — at most 800 characters after trimming. No sprint-by-sprint detail; the sprints and the timeline carry it.sprints(array of object { id, label, summary, description, phase, dates, start, due, status, urgency, lead, domain, github_ref }, optional)
Also takes repo and org (see Common arguments).
Errors
A tool that fails answers isError: true with one text block holding JSON, and nothing was written unless the error says otherwise:
{ "error": "what went wrong, in a sentence", "code": "not_found", "hint": "what to do next" }code is one of these; hint is always the sentence beside it (a refusal about which organization a call acts in also lists the organizations you may use, as orgs):
not_found: Nothing with that id or slug that you can see here. A missing item and one you may not see answer the same, on purpose. A ticket's or handoff's id is its number in the organization (#12): list first (list_tickets, list_sprints, list_handoffs, artifact_list, search_prompts, query) and use an id from the result.forbidden: Outside what you may do, so nothing was written. Ticket writes need the ticket to be assigned to you already (filing with create_ticket is open; assign_ticket is for an admin, the requester or a current assignee), and update_plan is for admins. Ask the person you work for to assign it to you, or to do it in the Trov web app.conflict: The item's current state does not allow this (it is already in that state, someone else claimed it, or the move would break a rule such as one level of sub-tickets). Nothing was written. Read it again (get_ticket, get_sprint, get_handoff) before deciding what to do.bad_request: An argument is not usable; the error says which. Nothing was written. Fix that argument and call again.too_large: Over the size limit the error names. For a file, pass size_bytes and sha256 and PUT the exact bytes to the returned upload_url instead of sending them inline.gone: The upload or download link has expired (they last 5 minutes) or was already used. Ask for a fresh one: upload_asset or artifact_update for an upload, artifact_get for a download.plan_limit: The organization's plan does not allow more of this; retrying will not help and nothing was written. Tell the person: an owner can upgrade or make room in Trov › Org settings (plans: https://trov.dev/pricing).plan_feature: The organization's plan does not include this; retrying will not help and nothing was written. Tell the person: an owner can change the plan in Trov › Org settings (plans: https://trov.dev/pricing).repo_required: This connection follows the repository you work in: passrepoas owner/name (fromgit remote get-url origin) on every call. With no checkout, ask the person which repository they mean. Nothing was read or written.not_connected: That repository is not connected to an organization this connection can reach, or the name is wrong. Nothing was read or written. Tell the person; an admin connects it in Trov › Org settings › Repositories. Never retry with another repository just to get an answer.ambiguous_org: More than one of your organizations has that repository. Passorgwith one of the slugs inorgs; ask the person which one they mean.org_unavailable: The connection's current organization is no longer available to you. Passorgwith one oforgson the call, or call switch_org.org_not_allowed: This connection may not act there. Use one oforgs; a person adds an organization to the connection in Trov › Settings › MCP access.
An error with no code (an unexpected failure) carries this hint: The call failed and nothing is known to have been written. Check the arguments against the tool's input schema and try once more; if it fails the same way, tell the person.
Arguments that do not match a tool's input schema are refused by the MCP SDK before the tool runs: the text starts MCP error -32602: Input validation error: Invalid arguments for tool <name> and lists each wrong argument with what was expected. Fix those arguments and call again.
Not signed in. /mcp answers 401 with WWW-Authenticate: Bearer resource_metadata="https://trov.dev/.well-known/oauth-protected-resource" (and error="invalid_token" when a token was sent and refused), and a JSON body, { "error": "unauthorized", "hint": "…" }. With no token, the hint is:
Sign in to use Trov's MCP server: it uses OAuth 2.1 with PKCE. The WWW-Authenticate header points at its metadata (RFC 9728), so an MCP client that supports OAuth (Claude Code: run /mcp, choose trov, then Authenticate) opens the browser sign-in itself. Setup for each client: https://trov.dev/docs/connect
With a refused token:
The bearer token was not accepted: it is mistyped, revoked or expired, or its person has no organization left that it can act in. Sign in again through your MCP client (Claude Code: /mcp, choose trov, then Authenticate); a pasted trov_mcp_ token that was revoked cannot be renewed. Setup: https://trov.dev/docs/connect
Plan limits come back as code: "plan_limit", with the limit in words in error. The MCP server has no rate limit of its own.
The HTTP API (the web app's, behind its session cookie) answers its JSON errors the same way: the status and the error code, any message, and a hint chosen by that code and status alone.