chadtmiller.com/knotless
git that lives in your Atmosphere account
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.
| Tools | What they do |
|---|---|
list_repos, refs, list_tree, read_file, search, log, diff | read a repository; diff answers No differences: … when head changes nothing since base |
blame | who last changed each line of path at rev (the default branch if not given), as git blame finds it in the cache mirror: runs of lines, each {sha, start, count, author: {name, email, date}, summary, previous}, start counted from 1, date in UTC, and previous, where the commit's parent had the file, that parent and the file's path there |
search_owners | the accounts owning public repositories whose handle holds text, as the appview's search finds them, at most limit (10 by default, at most 50), as {owners: [{owner: {did, handle}, repoCount}]} |
storage | what a repository takes up on its PDS: its packs, their objects and bytes, and the bytes in blobs |
compact | as the owner, fold a repository's older packs into one and drop old claims, as knotless compact does, answering {before, removed, claims} |
delete_repo | delete a repository you own and everything in it, with what you wrote about it, as knotless delete; confirm is its name again, or it refuses |
delete_account_data | without confirm, what knotless data your account holds, as getAccountData answers it; with confirm, your handle, deletes it all as knotless delete-data, answering {deleted, loggedOut} |
commit | change files on a branch, optionally against a base commit, and with batch, as the owner, through the gateway |
publish, releases, read_release | releases (packages.md): publish publishes a version of a package from a repository you own, its files each {name, content} or {name, base64}, with optional commit, check and notes, answering {uri}; releases lists a repository's, of one package if given, as [{package, version, latest, commit, check, notes, createdAt, files: [{name, size, sha256}]}]; read_release reads one file of a version (the latest by default), checked, as {version, content} or {version, base64}, refusing a file over 1 MB |
create_branch, delete_branch, rename_branch | make, remove or rename a branch; a rename takes the open pulls on it along |
create_tag, delete_tag | make or remove a tag, such as a release's version, at a commit, branch or tag |
update_repo | as knotless describe does: change a repository's description (empty drops it), its links ([{url, title?}], which replace those it had; empty drops them), its topics (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} |
org | an 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, as public: true shows them to anyone. add invites an account (admin: true for an admin) or changes a member's role, keeping how it shows and when they joined, remove takes a member out or withdraws an invitation, both to yours or, with org, to one you are an admin of; join with visibility (public or hidden) accepts an invitation or changes how it shows; leave leaves; delete: true ends yours, as knotless org does. Answers {org, name, organization, inside, gateway?, entries: [{did, handle?, role, invitedAt, expiresAt?, joinedAt?, standing}], changed}: changed null, or for add {org, who, role, was?, had?, first, private} (first when it made the account an organization, private when the invitation is in the organization's space rather than its public records), for remove {org, who, was?, organization}, for join {org, role, visibility}, for leave {left, warning}, for delete {removed: [entries]}. create_repo answers the members who may push to a new repository |
create_repo, rename_repo, share, members, delegate, protect_branch, list_rules, transfer_repo, accept_transfer, archive_repo | manage repositories and access; archive_repo makes one you own read only, as knotless archive does, or with undo writable again, answering {archivedAt} (null once unarchived); delegate hands your writes to a gateway (the appview's, by default) so collaborators can push, and share with write does it first when you have not, approved in the browser as delegate is, then answers who may read and write it and, as a second text, Delegated your writes to the gateway <did> first.; protect_branch holds them to a branch's rules, and list_rules lists them, a rule that asks for code owners with warnings when 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_repo takes links ([{url, title?}], its website first) and a default_branch (main if not given) and names the runners given (none, for an empty list), or else the appview's default runner, as knotless create does, and returns them as runners: a private one's are members, invited, with a warning (null otherwise) if one could not be |
template | what a repository asks a pull's or issue's title and body to follow: read before open_pull or open_issue, which refuse one with no body when there is a template |
notifications | what others did that concerns you, from the appview (a mention reason for a pull, issue or comment that names you as @handle, invite for an organization's invitation you have yet to answer, its about the role it offers, and checks-failed and ejected for checks failing at your pull's latest run and the merge queue taking it out, from the runner, with checks those that failed, as {workflow, status, check}), the latest limit (50 by default, 1 to 100), as {notifications: [{reason, author, about, repo, uri, subject, verdict, mentioned, lineComments, checks, new, createdAt}], markedSeen}: repo the repository as handle/name, for show_item; subject what 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); verdict a review's; mentioned true 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 a comment; lineComments how many line comments a review was sent with; and new whether it was new before this listing; marks them all seen after, unless seen: false, and markedSeen says it did, so the next listing has them new: false. kind lists only mentions, reviews, comments, pulls-and-issues, checks or stars-and-follows, and marks none seen |
follow, list_follows | follow follows an account on knotless, or with following: false unfollows it, answering {following, uri}; list_follows lists whom an account follows, yours by default, as {follows: [{handle, uri, createdAt}]}, or with followers: true who follows it, from the appview, as {followers: [{did, handle}]} |
theme, list_themes | theme with a palette alone 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; with name publishes it as a dev.knotless.theme, answering {uri}, and with use draws your pages in it (palette), or in a theme named (system, light, dark, latte, mocha, dracula, pckt, plyr, blacksky, or a published one's at:// URI), answering {using}, the name of what they are drawn in; a palette that falls short is neither published nor used. delete deletes one of yours by URI, answering {deleted}. list_themes lists themes, everyone's or an account's, the latest first, or with find those whose name starts so, as {themes: [{uri, name, creator, palette, createdAt}]} |
react | reacts to a pull, issue or comment (uri) on repo (in its space, for a private one) with a content among GitHub's eight, or with reacted: false takes it back, answering {reacted, uri} |
star, list_stars | star stars a public repository, or with starred: false takes the star back, answering {starred, uri}; list_stars lists the repositories an account starred, yours by default, as {stars: [{repo, uri, createdAt}]} |
fork, open_pull, open_issue, list_items, show_item, pull_diff, comment, set_state, merge, retarget_pull | collaborate; list_items takes a query in GitHub's search words (collaboration.md), and says of each pull draft, true or false; show_item on a pull says what it brings in and whether it merges cleanly, and on an issue gives mentionedBy, the pulls whose title or body names it, oldest first, each {pull, closes}, closes true where it names it after a closing keyword |
revert | as knotless revert does: undoes a merged pull (uri) in one commit on revert/<rkey> and opens a pull of it into the same branch, saying reason if given, answering {pull, branch, commit} (reverting a pull) |
list_labels, define_label, label_item, triagers, eval_triager | a 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_comment | change 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_queue | list the merge queue that merge with queue puts a pull in, or take a pull out |
checks | what the repository's runners report of its workflows, waiting for runs to finish if asked; each log is its last 8000 bytes; images returns a check's pictures, at most 12 at once |
runners | the 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 |
secret | the secrets a repository's owner gives its runners, as knotless secret does: set a name with its value, refs to limit it to some refs' pushes and runners to some of the repository's runners, or remove one; 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, run knotless secret <url> set <name> with it on stdin |
agents | the accounts whose pulls a repository's runners check as they move, as code no one vouches for; as owner, name or drop one |
minutes | the 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 |
rerun | as owner, ask the runners to run a ref or a pull again, or one workflow of either |
workflows | the workflows at a ref (the default branch), and what runs each there: {ref, workflows: [{name, push, pull, asked, error}]} |
hide_item, block_account, report | moderate your repositories, and report to the appview; block_account with blocked: false unblocks |
list_blocks | the accounts an account blocks, yours by default, the latest first: each did, handle, repo (the one repository, by its at:// URI as when blocked, or null for all) and createdAt |
set_profile | how you show on knotless, over your Bluesky profile, and, with bot, whether the account is a bot, which the feed leaves out |
set_email | claim an address as yours, or with claimed: false take it back, answering {changed, claimed}, how many you claim now; with no address, only {claimed} |
notification_settings | which notifications you hear of, kept by the appview: on and off, lists of reasons (star, follow, pull, issue, comment, mention, review-requested, invite, checks-failed, ejected), and followed_only, change them first; answers {reasons: [{reason, enabled}], followed_only} |
takedown, moderation_status, list_reports, stats, logs | as the appview's operator: an account its KNOTLESS_ADMIN_DIDS names |
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).
| Tool | Parameters | Answer |
|---|---|---|
commit | changes | each {path} with one of content, base64, edits or delete: true; edits is a list of {old, new} made in turn in the file as the branch holds it, each old there exactly once, the file keeping its mode. A miss fails edit <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) |
search | path, context | only files under path, a directory, file or glob; with context, runs of lines around the matches instead of lines, each {path, start, end, matches, text}, matches the line numbers that match and text the run's lines |
read_file | start, end | only those lines of a text file, from 1, end by default its last, as {start, end, lines, text}, lines the file's count |
blame | start, end | only the runs for those lines, end by default start |
merge_queue | repo | each pull waiting, in order, with its place and the batch it is in while that is checked or landing, or the preflight workflows it waits for, then the open pulls ejected |
leave_queue | repo, uri | {"left": <uri>}; the owner and collaborators may |
open_pull, open_issue, comment, edit_item | images | 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); with edit_item, only beside a body |
open_pull | draft, reviewers | 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_item | draft, add_reviewers, remove_reviewers | for your pull: true makes it a draft, false marks it ready; ask more accounts to review it, or stop asking some |
list_items | query | also is:draft, -is:draft and review-requested:<handle, did or @me> |
pull_diff | repo, uri | 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 |, as comment's lines takes them: a + line is side: new at its new number, a - line side: old at its old number, an unchanged line side: new at its new number. File headers, @@ lines and \ No newline are not numbered |
comment | lines | comments on lines of a pull's diff at its head, each {path, line, start_line?, side?, body} (side old for lines it removes), each starting a thread: with a body or verdict, as that review's, all sent at once, otherwise on their own; at most 100. A comment's URI as uri answers 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 reviews |
set_state | state | resolved or open for a comment's URI that starts a thread on a pull's lines: its author, the pull's, the owner or a collaborator |
show_item | repo, uri | item.draft, true or false for a pull, and item.reviewers, those it asks to review it, {did, handle, pending}, pending while the pull is open and they have not reviewed it, and false once it is merged or closed; for a pull, item.reviews: each reviewer's standing review, {by, handle, verdict, commit, uri, outdated, maintainer, uncounted}, outdated once the open pull has moved past commit, maintainer for the owner or a collaborator, and uncounted, when its branch asks for reviews, why an approval does not count; on each comment, its author's handle and facets, the accounts it mentions; on a comment on lines, line ({path, side, line, startLine}, side new and startLine the same as line where its record leaves them out) and review, 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, also compare, which for a merged pull is what its merge brought in (base where the branch was before it, head the 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} |
checks | ref | 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 by queue |
checks | queue | only the runs of that branch's merge-queue batches, by its short name (main) |
checks | commit | only the runs of that commit: its hex ID, or at least its first 4 digits |
checks | wait | 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
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.