MCP tools
The tools knotless-mcp offers over stdio, and a public knotless-serve
offers at /mcp. They name a repository as handle/name. Reads answer from
a local cache mirror, writes go through the same code as the CLI, and
errors come back as tool errors for the agent to read. One a PDS refused
for want of scope, as a login from before knotless asked for more is, adds
that the user signs in again: reconnecting knotless in their assistant, or
running knotless login for a knotless on their machine.
Agents says how to connect a client.
Over stdio, knotless-mcp asks its appview about versions once a day, as
the CLI does (Command reference). With a newer release
out, the first tool’s answer ends with a text saying so. Older than the
appview asks for, every tool fails, saying why and to run knotless upgrade, unless KNOTLESS_IGNORE_MINIMUM is set. /mcp on a server runs
that server’s own build and does neither.
knotless-mcp --read-only offers only the tools that read, as
list_repos, refs, list_tree, read_file, blame, search, log,
diff, pull_diff, show_item, list_items, checks and workflows
(READS in crates/knotless-mcp/src/lib.rs lists them all), and runs no
other, whatever login it has: for an agent that may read but must change
nothing, as the review agent is.
list_repos,refs,list_tree,read_file,search,log,diffread a repository;
diffanswersNo differences: …whenheadchanges nothing sincebaseblamewho last changed each line of
pathatrev(the default branch if not given), asgit blamefinds it in the cache mirror: runs of lines, each{sha, start, count, author: {name, email, date}, summary, previous},startcounted from 1,datein UTC, andprevious, where the commit’s parent had the file, that parent and the file’s path theresearch_ownersthe accounts owning public repositories whose handle holds
text, as the appview’s search finds them, at mostlimit(10 by default, at most 50), as{owners: [{owner: {did, handle}, repoCount}]}storagewhat a repository takes up on its PDS: its packs, their objects and bytes, and the bytes in blobs
compactas the owner, fold a repository’s older packs into one and drop old claims, as
knotless compactdoes, answering{before, removed, claims}delete_repodelete a repository you own and everything in it, with what you wrote about it, as
knotless delete;confirmis its name again, or it refusesdelete_account_datawithout
confirm, what knotless data your account holds, asgetAccountDataanswers it; withconfirm, your handle, deletes it all asknotless delete-data, answering{deleted, loggedOut}commitchange files on a branch, optionally against a
basecommit, and withbatch, as the owner, through the gatewaypublish,releases,read_releasereleases (packages.md):
publishpublishes aversionof apackagefrom a repository you own, itsfileseach{name, content}or{name, base64}, with optionalcommit,checkandnotes, answering{uri};releaseslists a repository’s, of onepackageif given, as[{package, version, latest, commit, check, notes, createdAt, files: [{name, size, sha256}]}];read_releasereads onefileof aversion(the latest by default), checked, as{version, content}or{version, base64}, refusing a file over 1 MBcreate_branch,delete_branch,rename_branchmake, remove or rename a branch; a rename takes the open pulls on it along
create_tag,delete_tagmake or remove a tag, such as a release’s version, at a commit, branch or tag
update_repoas
knotless describedoes: change a repository’s description (empty drops it), itslinks([{url, title?}], which replace those it had; empty drops them), itstopics(names, which replace those it had; empty drops them), and the branch its HEAD names, one of its branches once it has any, answering{description, links, topics, defaultBranch}organ organization’s members as you may see them, yours or an account’s (
org): the organization and its members see invitations and hidden members, anyone else public members, aspublic: trueshows them to anyone.addinvites an account (admin: truefor an admin) or changes a member’s role, keeping how it shows and when they joined,removetakes a member out or withdraws an invitation, both to yours or, withorg, to one you are an admin of;joinwithvisibility(publicorhidden) accepts an invitation or changes how it shows;leaveleaves;delete: trueends yours, asknotless orgdoes. Answers{org, name, organization, inside, gateway?, entries: [{did, handle?, role, invitedAt, expiresAt?, joinedAt?, standing}], changed}:changednull, or foradd{org, who, role, was?, had?, first, private}(firstwhen it made the account an organization,privatewhen the invitation is in the organization’s space rather than its public records), forremove{org, who, was?, organization}, forjoin{org, role, visibility}, forleave{left, warning}, fordelete{removed: [entries]}.create_repoanswers thememberswho may push to a new repositorycreate_repo,rename_repo,share,members,delegate,protect_branch,list_rules,transfer_repo,accept_transfer,archive_repomanage repositories and access;
archive_repomakes one you own read only, asknotless archivedoes, or withundowritable again, answering{archivedAt}(null once unarchived);delegatehands your writes to a gateway (the appview’s, by default) so collaborators can push, andsharewithwritedoes it first when you have not, approved in the browser asdelegateis, then answers who may read and write it and, as a second text,Delegated your writes to the gateway <did> first.;protect_branchholds them to a branch’s rules, andlist_ruleslists them, a rule that asks for code owners withwarningswhen CODEOWNERS on its branch gives you, the owner, a path alone (your own pull changing it never has a code owner’s approval, as an author’s never counts, and merges only by force);create_repotakeslinks([{url, title?}], its website first) and adefault_branch(mainif not given) and names therunnersgiven (none, for an empty list), or else the appview’s default runner, asknotless createdoes, and returns them asrunners: a private one’s are members, invited, with awarning(null otherwise) if one could not betemplatewhat a repository asks a pull’s or issue’s title and body to follow: read before
open_pulloropen_issue, which refuse one with no body when there is a templatenotificationswhat others did that concerns you, from the appview (a
mentionreason for a pull, issue or comment that names you as@handle,invitefor an organization’s invitation you have yet to answer, itsaboutthe role it offers, andchecks-failedandejectedfor checks failing at your pull’s latest run and the merge queue taking it out, from the runner, withchecksthose that failed, as{workflow, status, check}), the latestlimit(50 by default, 1 to 100), as{notifications: [{reason, author, about, repo, uri, subject, verdict, mentioned, lineComments, checks, new, createdAt}], markedSeen}:repothe repository ashandle/name, forshow_item;subjectwhat it is about, the pull or issue a comment is on, else the pull or issue itself, or for a star the repository’s record (null for a follow or an invitation);verdicta review’s;mentionedtrue when it names you though it is heard of for another reason, as a review of your pull that names you is heard of once, as acomment;lineCommentshow many line comments a review was sent with; andnewwhether it was new before this listing; marks them all seen after, unlessseen: false, andmarkedSeensays it did, so the next listing has themnew: false.kindlists onlymentions,reviews,comments,pulls-and-issues,checksorstars-and-follows, and marks none seenfollow,list_followsfollowfollows anaccounton knotless, or withfollowing: falseunfollows it, answering{following, uri};list_followslists whom anaccountfollows, yours by default, as{follows: [{handle, uri, createdAt}]}, or withfollowers: truewho follows it, from the appview, as{followers: [{did, handle}]}theme,list_themesthemewith apalettealone checks it, answering{readable, shortfalls: [{text, on, ratio}]}, each text that would not read at 4.5 to 1 on what it is drawn on; withnamepublishes it as adev.knotless.theme, answering{uri}, and withusedraws your pages in it (palette), or in a theme named (system,light,dark,latte,mocha,dracula,pckt,plyr,blacksky, or a published one’sat://URI), answering{using}, the name of what they are drawn in; a palette that falls short is neither published nor used.deletedeletes one of yours by URI, answering{deleted}.list_themeslists themes, everyone’s or anaccount’s, the latest first, or withfindthose whose name starts so, as{themes: [{uri, name, creator, palette, createdAt}]}reactreacts to a pull, issue or comment (
uri) onrepo(in its space, for a private one) with acontentamong GitHub’s eight, or withreacted: falsetakes it back, answering{reacted, uri}star,list_starsstarstars a public repository, or withstarred: falsetakes the star back, answering{starred, uri};list_starslists the repositories anaccountstarred, yours by default, as{stars: [{repo, uri, createdAt}]}fork,open_pull,open_issue,list_items,show_item,pull_diff,comment,set_state,merge,retarget_pullcollaborate;
list_itemstakes aqueryin GitHub’s search words (collaboration.md), and says of each pulldraft,trueorfalse;show_itemon a pull says what it brings in and whether it merges cleanly, and on an issue givesmentionedBy, the pulls whose title or body names it, oldest first, each{pull, closes},closestrue where it names it after a closing keywordrevertas
knotless revertdoes: undoes a merged pull (uri) in one commit onrevert/<rkey>and opens a pull of it into the same branch, sayingreasonif given, answering{pull, branch, commit}(reverting a pull)list_labels,define_label,label_item,triagers,eval_triagera repository’s labels: what it defines, defining one as owner, putting them on pulls and issues as owner, collaborator or triager, naming the triagers, and how an
account’s labels agree with everyone else’s, per scope, as[{scope, labelled, compared, agreed}]edit_item,delete_commentchange your pull’s, issue’s or comment’s text, whether your pull is a draft and whom it asks to review it, or take your comment back
merge_queue,leave_queuelist the merge queue that
mergewithqueueputs a pull in, or take a pull outcheckswhat the repository’s runners report of its workflows, waiting for runs to finish if asked; each log is its last 8000 bytes;
imagesreturns a check’s pictures, at most 12 at oncerunnersthe accounts whose checks a repository shows; as owner, name or drop one: a private repository’s is made a member and invited, again each time it is added
secretthe secrets a repository’s owner gives its runners, as
knotless secretdoes:seta name with itsvalue,refsto limit it to some refs’ pushes andrunnersto some of the repository’s runners, orremoveone; answers each, one per runner,name,runner,refs,key,updatedAt, never a value. A value given here passes through the conversation: for one that should not, runknotless secret <url> set <name>with it on stdinagentsthe accounts whose pulls a repository’s runners check as they move, as code no one vouches for; as owner, name or drop one
minutesthe minutes each of a repository’s runners counts for its owner’s public repositories in 30 days, against its allowance, and when minutes free up
rerunas owner, ask the runners to run a ref or a pull again, or one workflow of either
workflowsthe workflows at a
ref(the default branch), and what runs each there:{ref, workflows: [{name, push, pull, asked, error}]}hide_item,block_account,reportmoderate your repositories, and report to the appview;
block_accountwithblocked: falseunblockslist_blocksthe accounts an account blocks, yours by default, the latest first: each
did,handle,repo(the one repository, by itsat://URI as when blocked, or null for all) andcreatedAtset_profilehow you show on knotless, over your Bluesky profile, and, with
bot, whether the account is a bot, which the feed leaves outset_emailclaim an
addressas yours, or withclaimed: falsetake it back, answering{changed, claimed}, how many you claim now; with no address, only{claimed}notification_settingswhich notifications you hear of, kept by the appview:
onandoff, lists of reasons (star,follow,pull,issue,comment,mention,review-requested,invite,checks-failed,ejected), andfollowed_only, change them first; answers{reasons: [{reason, enabled}], followed_only}takedown,moderation_status,list_reports,stats,logsas the appview’s operator: an account its
KNOTLESS_ADMIN_DIDSnames
merge takes wait (seconds, at most 300), force and keep_branch, as
the CLI’s merge takes --wait, --force and --keep-branch. Merged, it
answers {sha, method, deleted, kept, closed}: the commit its branch is
at, how (fastForward, merge or squash), the head branch it deleted,
or else why it kept it (asked to, in another repository such as a fork,
the branch merged into or the default branch, or named by another open
pull), and the issues it closed.
With queue: true, merge puts the pull in its branch’s merge queue
instead and answers {"queued": {base, position, of}} at once; it then
ignores wait, force and keep_branch. Its description tells an
agent to queue a pull once it builds, to fix what an ejection says and
queue again, and to stop after a second ejection for the same reason.
Into a branch protected with queue (protect_branch takes queue, and
list_rules shows it), merge without force queues the pull instead,
answering {"queued": {base, position, of}, "why": "<base> takes pulls only through the merge queue"}; see queue-only
branches. With check: true, it merges
nothing and answers whether it would now, as knotless merge --check
says: {state, reason, conflicts, checks, method, deletes, base, head, may: {merge, force, queue, base, head, rerun}, queueOnly, reviews, reviewsMissing, reviewsUncounted, codeOwnersWarnings}: reviews is
passed, blocked, or none when the branch asks for none, and
reviewsMissing, reviewsUncounted (the approvals that do not count, and
why) and codeOwnersWarnings are [{text}]. may is what the caller’s
part in the repository lets it do, not whether that would work now:
state and reason say that, and a blocked pull’s merge is refused, the
owner’s too, unless the owner forces it. When the caller may not merge
the pull, reason says so too. Over MCP, a refusal for reviews names the
comment tool with verdict: "approve", and merge with force, where
the CLI names its commands. protect_branch takes reviews (how
many approvals, 0 to 10), code_owners and dismiss_stale beside merges
and queue; into such a branch merge refuses a pull not reviewed as it
asks, naming what is missing, unless the owner forces it (see required
reviews).
commitAnswer each
{path}with one ofcontent,base64,editsordelete: true;editsis a list of{old, new}made in turn in the file as the branch holds it, eacholdthere exactly once, the file keeping its mode. A miss failsedit <n> of <path>: old is not in the file, or says how many times it is there; a path with no regular file fails<path>: no file there to edit. On a branch that moved, edits are made in the file as it is there (long files)searchAnswer only files under
path, a directory, file or glob; withcontext, runs of lines around the matches instead of lines, each{path, start, end, matches, text},matchesthe line numbers that match andtextthe run’s linesread_fileAnswer only those lines of a text file, from 1,
endby default its last, as{start, end, lines, text},linesthe file’s countblameAnswer only the runs for those lines,
endby defaultstartmerge_queueAnswer each pull waiting, in order, with its
placeand thebatchit is in while that is checked or landing, or thepreflightworkflows it waits for, then the open pulls ejectedleave_queueAnswer
{"left": <uri>}; the owner and collaborators mayopen_pull,open_issue,comment,edit_itemAnswer pictures to show after the body, each
{base64, alt}, a PNG, JPEG, WebP or GIF: uploaded, made smaller when over 1 MB, and attached (collaboration.md); withedit_item, only beside abodyopen_pullAnswer open it as a draft, which neither merges nor queues until marked ready; ask these accounts, by handle or DID, to review it (drafts and requested reviewers)
edit_itemAnswer for your pull:
truemakes it a draft,falsemarks it ready; ask more accounts to review it, or stop asking somelist_itemsAnswer also
is:draft,-is:draftandreview-requested:<handle, did or @me>pull_diffAnswer the pull’s unified diff (for a merged pull, what its merge brought in, from the commits its state records, so it answers once its branch is gone), each line of a hunk led by its line in the old file and in the new (blank on the side it is not on) and
|, ascomment’slinestakes them: a+line isside: newat its new number, a-lineside: oldat its old number, an unchanged lineside: newat its new number. File headers,@@lines and\ No newlineare not numberedcommentAnswer comments on lines of a pull’s diff at its head, each
{path, line, start_line?, side?, body}(sideoldfor lines it removes), each starting a thread: with abodyorverdict, as that review’s, all sent at once, otherwise on their own; at most 100. A comment’s URI asurianswers it in its thread. Answers{uri, written}: the first it wrote and all of them, and for an approval from someone who may not write to the repository,note, that it does not count toward a protected branch’s reviewsset_stateAnswer
resolvedoropenfor a comment’s URI that starts a thread on a pull’s lines: its author, the pull’s, the owner or a collaboratorshow_itemAnswer
item.draft,trueorfalsefor a pull, anditem.reviewers, those it asks to review it,{did, handle, pending},pendingwhile the pull is open and they have not reviewed it, andfalseonce it is merged or closed; for a pull,item.reviews: each reviewer’s standing review,{by, handle, verdict, commit, uri, outdated, maintainer, uncounted},outdatedonce the open pull has moved pastcommit,maintainerfor the owner or a collaborator, anduncounted, when its branch asks for reviews, why an approval does not count; on each comment, its author’shandleandfacets, the accounts it mentions; on a comment on lines,line({path, side, line, startLine},sidenewandstartLinethe same aslinewhere its record leaves them out) andreview, which a review’s own comment names too; on each comment of a thread on lines, and on no other,resolved; on a review its reviewer’s later one stands in place of,superseded: true; and on an answer,reply;reactions, on it and on each comment, as{"<content>": count}or null, as the appview counts them, or for a private repository as its space has them; for a pull, alsocompare, which for a merged pull is what its merge brought in (basewhere the branch was before it,headthe merge), whether or not its branch is still there; for a merged pull a merged revert undid,item.revertedBy, that revert, and on a revert,item.reverts, the pull it undoes; for an open pull in the queue or ejected from it,standing:{said, place, batch}checksAnswer only the runs of pushes to that ref, in full, and those asked for there; a pull’s are found by
pull, a batch’s byqueuechecksAnswer only the runs of that branch’s merge-queue batches, by its short name (
main)checksAnswer only the runs of that commit: its hex ID, or at least its first 4 digits
checksAnswer seconds to wait, at most 300, for a check to show and none to be queued or running; finding none by then is an error naming what was asked for
standing.said reads as Queued, 3rd of 5, Queued, 1st of 2, waiting for its pre-flight: check, In a batch of 4 being checked, Landing,
or Ejected: <reason>, with the failed checks.
Long files
Section titled “Long files”An agent pays for what it sends and reads as tokens, so the tools let it
change part of a long file without moving all of it: search with
path and context finds the lines and what is around them, and
commit with edits sends only what changes. read_file with start
and end reads a part when the search shows too little.
The test an_agent_changes_a_line_of_a_long_file
(crates/knotless-mcp/tests/mcp.rs) benches one change both ways, and
fails if editing ever costs more than a fiftieth of rewriting:
| One line of a 138 KB file | Sent | Received | About | Time |
|---|---|---|---|---|
Before: read_file, then commit with content | 143,115 B | 138,207 B | 70,330 tokens | 473 ms |
Now: search with context, then commit with edits | 313 B | 634 B | 236 tokens | 351 ms |
Tokens are bytes over four. The run behind these numbers is the test’s
own, against the reference PDS on one machine; cargo test -p knotless-mcp --test mcp an_agent_changes_a_line_of_a_long_file -- --nocapture prints
them again. The saving grows with the file: what an edit sends does not.