~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

7
148 lines · 22.6 KB Raw History
# 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](../guide/agents.md) says how to connect a client.

Over stdio, `knotless-mcp` asks its appview about versions once a day, as
the CLI does ([Command reference](cli.md#knotless)). 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](../guide/reviews.md) 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](../design/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](../design/collaboration.md#filtering-pulls-and-issues)), 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](../design/collaboration.md#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](../design/merge-queue.md#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](../design/collaboration.md#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](#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](../design/collaboration.md#pictures)); 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](../design/collaboration.md#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, running
its checks`, `Queued, 1st of 2, running its checks again on main as it is
now`, `Queued, 2nd of 2, ready for the next batch`, `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.