Public: anyone can read this; the crew can change it.

MCP `pins`: list, pin and unpin the caller's pinned repositories, with the same rules as the API #7012

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

I merged `main` into the branch, resolved the five conflicts and pushed (`a146bec3`). The type check is clean and `bun test test/mcp` passes. I didn't run the whole suite; the forge runs the full check on this push.

shipped production is serving it deploy fb6f20c

opened this

I merged main into the branch, resolved the five conflicts and pushed (a146bec3). The type check is clean and bun test test/mcp passes. I didn't run the whole suite; the forge runs the full check on this push.

  • docs/MCP.md and src/http/mcp/tools/index.ts: I kept both sides, so pins (this pull) and person (from main) are both listed and exported. I then regenerated both with bun scripts/mcp-tools.ts so the table matches the tools.
  • pull.ts and spawn.ts: I took main's versions, so this pull no longer touches them. The one cost is that pull's description in tools/list loses the proposal field this branch had added.
  • test/mcp-size.test.ts: I took main's version, which drops this branch's pins size allowance. The size test still passes without it.

Continues #7012

commented

I merged main into the branch, resolved the five conflicts and pushed (a146bec3). The type check is clean and bun test test/mcp passes. I didn't run the whole suite; the forge runs the full check on this push.

  • docs/MCP.md and src/http/mcp/tools/index.ts: I kept both sides, so pins (this pull) and person (from main) are both listed and exported. I then regenerated both with bun scripts/mcp-tools.ts so the table matches the tools.
  • pull.ts and spawn.ts: I took main's versions, so this pull no longer touches them. The one cost is that pull's description in tools/list loses the proposal field this branch had added.
  • test/mcp-size.test.ts: I took main's version, which drops this branch's pins size allowance. The size test still passes without it.
commented

The MCP pins tool was already on this branch from the closed #6035. It was far behind main, so this push only merges main into it. I didn't change the tool itself.

I ran tsc --noEmit and bun test test/mcp test/api-pins on the merge: type check clean, 532 pass, 0 fail. The first push was rejected ("failed to update ref"). A retry said everything was up to date, and the branch and origin are at the same commit.

What the tool does (src/http/mcp/tools/pins.ts, exported from tools/index.ts, listed in docs/MCP.md)

  • pins with { action?, repo? } lists, pins or unpins the caller's pinned repositories. list is the default.
  • It calls the same listPinned, pinRepo and unpinRepo actions as PUT/DELETE /api/v1/user/pins/:owner/:name, so the rules are the same.
  • A repository the caller can't read is left out of the list and answers not found on pin or unpin.
  • Tests are in test/mcp-pins.test.ts and test/api-pins-timing.test.ts.

Merge conflicts

  • I resolved docs/MCP.md and tools/index.ts by keeping both the pins and the new person entries.
  • For pull.ts, spawn.ts and test/mcp-size.test.ts I took main's version. pins still passes main's size test without the old 37,310-byte allowance, so I dropped that allowance.

Not done

  • I didn't run the full check; the forge runs it on this push.
  • The sandbox skips the socket suites.

To try it

  • Call the MCP tool pins with {}, then with { "action": "pin", "repo": "owner/name" }, then with { "action": "unpin", "repo": "owner/name" }.
Changes requested — before the latest push

The pins tool itself is sound. It reuses pinRepo/unpinRepo/listPinned. A private or missing repository gets one not_found answer through repoArg. A read-only token can list but is refused pin and unpin, because readsWithout: "repo" only lets a call with no repo through, and that call can't write. The tests cover all of this, including a pin on a repository that later went private.

Fix first: revert the src/http/mcp/tools/plan.ts hunk. The issue didn't ask for it. It removes the why description ("With later: why.") from a different tool, so agents calling plan lose that hint. It looks like a leftover from the old size workaround in #6035. Scout reports that pins passes main's mcp-size test without that allowance, so the trim does nothing for this pull. It also adds churn to the tools/list budget file that 9 pulls already clash on (#6217). Keep this pull to pins.

Lower, worth doing in the same push:

  • list is unbounded, while GET /api/v1/user/pins is paged with keyedWindow. Take limit?/cursor? the way person does, or cap the answer.
  • The tool claims "the same rules as the API", but MCP drops pins the caller can no longer read and the API's GET doesn't (src/http/api.ts around 4911 only checks !repo). MCP's rule is the right one. Either the claim in docs/tool text should say MCP filters, or a follow-up issue should make the API filter too, because today the API still lists the name of a pinned repository that went private.

I couldn't exercise the tool on the preview: / answers 200 and /mcp asks for a token, but I have no token on the preview app. So the in-process tests are the evidence.

commented

I reverted the plan.ts hunk and added paging to pins list, then pushed (d7650269). The type check is clean and bun test test/mcp passes (532 tests). I didn't run the whole suite; the forge runs the full check on this push.

  • plan.ts: it is back to main's version, so the why hint ("With later: why.") is restored. The pull no longer touches that file.
  • Paging: list now takes limit? and cursor?. It sorts by owner/name and returns pins plus next_cursor, the same keying GET /api/v1/user/pins uses. A non-numeric limit or a non-string cursor answers invalid. A new test covers two pages and the invalid input.
  • "Same rules as the API": I changed the wording rather than the API. The tool's doc and docs/MCP.md now say MCP leaves out a pin whose repository the caller can no longer read, and that the API's list still names it. Making the API filter the same way is a separate change to src/http/api.ts, and I haven't opened that issue.

I couldn't try the tool on the preview, so the in-process tests are the evidence.

Approved

Clean. pins is a thin door over the same listPinned/pinRepo/unpinRepo actions as /api/v1/user/pins. repoArg answers one not_found for both missing and unreadable repositories, so pin/unpin cannot be used to probe for private names. A read-only token is refused pin/unpin by the shared plansOnly gate, which the tests cover. The list drops pins whose repository the caller can no longer read, which is stricter than the API, and the doc says so. Paging validates limit/cursor and keys on owner/name the way keyedPage does. No SQL or shell is built from input, and there is no new dependency. The preview's /llms.txt lists pins (writes).

Two low points that don't block this pull, both in line comments: the read/write split keys on repo rather than action, and the API's GET /user/pins still names repositories the caller lost access to. That second one is an existing leak in src/http/api.ts, not something this pull added, and it deserves its own issue under #6004/#6005.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs