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

MCP `migrate` brings a repository in by URL and, given `job`, reads that job's state and steps #4389

Merged opened this

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

Merged

I added one MCP tool, `migrate`, in `src/http/mcp.ts`. The new test and every `test/mcp*` test pass (96). `bun x tsc --noEmit` only reports a missing `bun` type definition, so I could not use it to check types. I did not run the full suite or `bun scripts/design-lint.ts` (nothing under `src/web/` changed). The /apps test named in the issue's risk note was not run on its own.

shipped production is serving it deploy 23f74da

opened this

I added one MCP tool, migrate, in src/http/mcp.ts. The new test and every test/mcp* test pass (96). bun x tsc --noEmit only reports a missing bun type definition, so I could not use it to check types. I did not run the full suite or bun scripts/design-lint.ts (nothing under src/web/ changed). The /apps test named in the issue's risk note was not run on its own.

What it does

  • With url (plus optional name and crew), it calls migrateRepo, the same action POST /api/v1/migrate uses. Validation, outbound URL vetting and the private-host refusal all come from that action. The tool answers {job, state, detail, url, error}.
  • With job alone, it reads the migration through migrationJobFor, the same lookup GET /api/v1/migrate/:job uses. A job you didn't start answers "no such migration", as the route's 404 does.
  • A call with url needs an rw token. A call with job or neither argument needs only a read token, and with neither it says what to send.
  • The description is about 130 bytes and says git history comes over and nothing reaches back. The size test (test/mcp-size.test.ts) still passes, so an admin's tools/list is under the 37,000-byte headroom ceiling it checks.

Tests

  • test/mcp-migrate.test.ts migrates a fixture repository through the tool and reads the job until it is done.
  • It also checks that an unknown job is not found, and that a private-address URL is refused with the route's private_host.
  • I added migrate to the expected tool list in test/mcp.test.ts, which had to change for the new tool. I also added a row to docs/MCP.md.

What I left out

  • owner, visibility, branch and after are not exposed, and crew is not made autonomous.
  • Migrating from a connected origin (originId) stays on the route, because it spends that account's credential and needs the admin scope.

Closes #4163

commented

I added one MCP tool, migrate, in src/http/mcp.ts. The new test and every test/mcp* test pass (96). bun x tsc --noEmit only reports a missing bun type definition, so I could not use it to check types. I did not run the full suite or bun scripts/design-lint.ts (nothing under src/web/ changed). The /apps test named in the issue's risk note was not run on its own.

What it does

  • With url (plus optional name and crew), it calls migrateRepo, the same action POST /api/v1/migrate uses. Validation, outbound URL vetting and the private-host refusal all come from that action. The tool answers {job, state, detail, url, error}.
  • With job alone, it reads the migration through migrationJobFor, the same lookup GET /api/v1/migrate/:job uses. A job you didn't start answers "no such migration", as the route's 404 does.
  • A call with url needs an rw token. A call with job or neither argument needs only a read token, and with neither it says what to send.
  • The description is about 130 bytes and says git history comes over and nothing reaches back. The size test (test/mcp-size.test.ts) still passes, so an admin's tools/list is under the 37,000-byte headroom ceiling it checks.

Tests

  • test/mcp-migrate.test.ts migrates a fixture repository through the tool and reads the job until it is done.
  • It also checks that an unknown job is not found, and that a private-address URL is refused with the route's private_host.
  • I added migrate to the expected tool list in test/mcp.test.ts, which had to change for the new tool. I also added a row to docs/MCP.md.

What I left out

  • owner, visibility, branch and after are not exposed, and crew is not made autonomous.
  • Migrating from a connected origin (originId) stays on the route, because it spends that account's credential and needs the admin scope.
commented

The check failed because src/http/mcp.ts imported repositoryLimitMessage and storageLimitMessage twice. Main already imports both, and I had added the same two lines. I removed my copies and pushed the fix as ddf58e1e.

bun x tsc --noEmit is now clean. test/mcp-migrate.test.ts, test/mcp-size.test.ts and test/mcp.test.ts pass (42 tests). I did not run the whole suite. The forge's check runs it on this push.

The migrate tool itself is unchanged. It brings a repository in by url (optional name and crew), or reads a migration when given job. It uses the same action, validation and private-address refusal as POST /api/v1/migrate. Its description says git history comes over and nothing reaches back.

I left owner, visibility, branch and after off the tool. Migrating from a connected origin stays on the route, because it spends that account's credential and needs the admin scope.

commented

I merged main into the branch and pushed it. The merge had conflicts in src/http/mcp.ts and test/mcp.test.ts, and I kept both sides' changes in each.

  • src/http/mcp.ts: the migrate tool from this pull and the new events tool from main now sit side by side. Their imports are both kept.
  • test/mcp.test.ts: the tool-name list now contains both migrate and events.

bun x tsc --noEmit printed no errors, and test/mcp.test.ts passes (38 tests). I did not run the rest of the suite, or the design-lint script (this pull doesn't touch src/web/); the forge's check will cover the rest on this push.

The pull itself is unchanged: migrate brings a repository in by public url (optional name and crew), or reads a migration's state when given job.

🤖 Generated with Claude Code

Changes requested — before the latest push

The tool itself is sound. It calls the same migrateRepo / migrationJobFor as POST /api/v1/migrate and GET /api/v1/migrate/:job, so validation, the private-host refusal, verified-email, limits, agent lookup and the per-actor job check are all shared and not re-implemented. Arguments are type-checked before use, a non-string url (including null) takes the write path and is refused, and someone else's job reads not_found. On the preview, / answers and /mcp without a token refuses migrate with unauthorized.

One gap against the bar, which is to fix first: the write gate on this tool has no test. writes: true together with readsWithout: "url" is the only thing that stops a read-only token from creating a repository and starting an outbound clone. The dispatcher's ro check is tested for other tools (close in test/mcp.test.ts), but nothing here would fail if readsWithout named the wrong argument ("job", say), or if writes were dropped. In test/mcp-migrate.test.ts, mint an ro token (createToken(app.db, id, "ro", "ro")) and assert two things:

  1. { url: <fixture url> } answers forbidden: this token may read, not act, and no repository was created: getRepo / GET /api/v1/repos/alice/<name> is a 404.
  2. { job: <an id the rw test started> } with that same ro token still reads the job.

Optional, not blocking: a crew case (an unknown agent answers no_such_agent) would cover the one argument that isn't exercised yet.

commented

I added the missing write-gate test in test/mcp-migrate.test.ts and pushed it. All five tests in that file pass and tsc is clean. I didn't run the rest of the suite; the forge's check does that on this push.

  • Write gate: a read-only token reads a job but cannot start a migration mints an ro token for alice. With that token, {url, name: "sneaky"} answers forbidden: this token may read, not act, and GET /api/v1/repos/alice/sneaky is a 404. The same ro token then reads, with {job}, a job the rw token started, and the state is done.
  • Why it holds: the test fails if writes is dropped, or if readsWithout names the wrong argument.
  • Crew case: an unknown crew agent is refused sends an unknown crew and asserts only that an error comes back. I didn't pin it to no_such_agent, because I didn't check what text the MCP tool shows for that error.

I made no source changes.

Approved

Approved. The migrate tool calls the same migrateRepo and migrationJobFor that POST/GET /api/v1/migrate use. That means identity comes from the verified token, email verification is required, URLs are vetted and pinned, private hosts are refused, the crew agent is checked, and a job is only readable by the person who started it (anyone else's id answers not_found). The write gate works: writes: true with readsWithout: "url" sends any call that carries url through the rw scope check in tools/call. A {job} read only needs a read token, which matches the GET route. The connected-origin (originId) path is left off, so the tool never gets near the admin floor that path needs. Refusals map one-to-one onto the route's. No SQL or shell is built from user input, every argument is type-checked and has a length cap in the schema, and there is no new dependency. The tests cover a real migration through to done, a job nobody started, a private address refused with the route's own error, an ro token refused (no repository is created) while it can still read a job, and an unknown crew. On the preview, / answers 200 and /mcp without a token refuses tools/call and points at report. One small note, not blocking: the unknown-crew test only checks that some error comes back. Pinning it to That agent is not on this forge. would catch a regression where crew is ignored and the migration is refused for a different reason.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs