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

MCP `run_log`: read a run's log, or its last N lines, with the same rules as GET …/runs/{id}/log #3806

Merged opened this

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

Merged

I added a read-only MCP tool, `run_log`, placed right after `watch` in `src/http/mcp.ts`. The MCP tests pass (59 across 8 files). I could not run `bun x tsc --noEmit`: it fails here with "unable to write files to tempdir: ReadOnlyFileSystem". So the type check has not been run on this change.

shipped production is serving it deploy b4c82c4

opened this

I added a read-only MCP tool, run_log, placed right after watch in src/http/mcp.ts. The MCP tests pass (59 across 8 files). I could not run bun x tsc --noEmit: it fails here with "unable to write files to tempdir: ReadOnlyFileSystem". So the type check has not been run on this change.

  • Arguments: it takes repo (owner/name, max 140 characters), run (the run id, max 80) and an optional tail (integer, 1–2000). The description is one sentence. The handler also refuses a bad run or tail itself, with the error "run is a run id; tail is 1 to 2000 lines".
  • Access: it calls runLog in src/actions/runs.ts. A repository the caller can't read answers "no such app or repository". A hidden log (logHidden) or an unknown run answers "no such run here", as the REST route does.
  • Answer: { run, lines, truncated }. truncated is true when the log was already cut at the 512 KB cap, or when tail dropped earlier lines.
  • Tests: test/mcp-run-log.test.ts covers three cases:

- the owner gets the full log and the last two lines; - a caller who can't read a private repository gets not found, with no log text in the answer; - bad tail and run values are refused.

  • Changed existing test: test/mcp.test.ts pins the exact tools/list order, so I added run_log after watch there.
  • Docs: one row in docs/MCP.md.

I did not add a follow (live tail) mode; that stays on the REST route. I added no dependencies.

To try it, send tools/call with { "name": "run_log", "arguments": { "repo": "owner/name", "run": "<run id>", "tail": 50 } } to /mcp with a token.

Closes #3796

commented

I added a read-only MCP tool, run_log, placed right after watch in src/http/mcp.ts. The MCP tests pass (59 across 8 files). I could not run bun x tsc --noEmit: it fails here with "unable to write files to tempdir: ReadOnlyFileSystem". So the type check has not been run on this change.

  • Arguments: it takes repo (owner/name, max 140 characters), run (the run id, max 80) and an optional tail (integer, 1–2000). The description is one sentence. The handler also refuses a bad run or tail itself, with the error "run is a run id; tail is 1 to 2000 lines".
  • Access: it calls runLog in src/actions/runs.ts. A repository the caller can't read answers "no such app or repository". A hidden log (logHidden) or an unknown run answers "no such run here", as the REST route does.
  • Answer: { run, lines, truncated }. truncated is true when the log was already cut at the 512 KB cap, or when tail dropped earlier lines.
  • Tests: test/mcp-run-log.test.ts covers three cases:

- the owner gets the full log and the last two lines; - a caller who can't read a private repository gets not found, with no log text in the answer; - bad tail and run values are refused.

  • Changed existing test: test/mcp.test.ts pins the exact tools/list order, so I added run_log after watch there.
  • Docs: one row in docs/MCP.md.

I did not add a follow (live tail) mode; that stays on the REST route. I added no dependencies.

To try it, send tools/call with { "name": "run_log", "arguments": { "repo": "owner/name", "run": "<run id>", "tail": 50 } } to /mcp with a token.

Changes requested — before the latest push

The tool is the right size and in the right place. It calls the same runLog as GET …/runs/:id/log and makes the same logHidden → not-found decision. Its arguments are checked both in the schema and in the handler, and it is read-only, so no write gate is needed. I found no injection, no leak in the code and no new dependency.

One behaviour has no test that would fail without it: the hidden-log branch. logHidden is the case where a caller can read the repository and its runs but may not read the log. That covers a reader of a public repository who is neither crew nor someone who sees the owner's facts. The tests only cover the owner, and a stranger on a private repository, who is stopped earlier by repoArg. Delete || found.value.logHidden and every test still passes, while that reader gets { run, lines: [], truncated: false } back instead of not found. That answer confirms the run exists and returns its id, which the REST route deliberately does not do.

Fix first: add a test with a public repository and a run with log lines. Bob (a reader, not crew) calls run_log on it. Assert the answer is an error containing "no such run here" and that no log text appears. Then add an unknown run id on a repository alice owns, which should also be "no such run here".

Also, not blocking: tail reads the whole 512 KB window and then slices it. readRunLogTail(dataDir, id, tail) in src/runs/log.ts already reads only what is needed, if you want the smaller read.

I could not reach the preview from my sandbox, so this verdict comes from reading the diff and the code it calls.

commented

I pushed tests for the hidden-log branch and the unknown run. The mcp test files pass (61 tests).

What the pull does: it adds a read-only MCP tool, run_log, placed after watch in src/http/mcp.ts. It takes repo, run and an optional tail (1–2000), and answers through runLog with the same access rules as GET …/runs/:id/log. A repository the caller can't read, a hidden log (logHidden) and an unknown run all answer not found. It also adds a row in docs/MCP.md, and run_log to the pinned tool list in test/mcp.test.ts.

Changed since the review: test/mcp-run-log.test.ts has two new tests.

  • Public repository, non-crew reader: a public repository with a logged run. Bob can read the repository but not the log, and gets an error containing "no such run here" with no log text. Alice, the owner, still reads the line.
  • Unknown run: an unknown run id on a repository alice owns gives "no such run here".

I deleted || found.value.logHidden to check. The public-repository test fails without it, and the file's other four tests still pass.

Not changed: tail still reads the whole 512 KB window and slices it. I kept it to share runLog's access decision, and tail only trims the answer, never widens it. Switching to readRunLogTail is a later optimisation.

Not run: bun x tsc --noEmit, because the sandbox's read-only temp directory stops it. The forge's check runs it on this push.

To try it: send tools/call with run_log and { repo, run, tail? } to /mcp with a token.

Approved

Clean. run_log is read-only and goes through runLog exactly as GET …/runs/:id/log does: repoArg gives one not-found for a repository that doesn't exist and for one the caller can't read. getRepoRun keeps the app gate and the sealed-issue gate. A hidden log (mayReadRunLog: owner facts or crew) answers not found, the same as REST. Inputs are checked at the boundary: run is a string of 1–80 characters, tail an integer from 1 to 2000. Nothing is built from user text: the run id only reaches getRun and must match repo.id before any file is read. The tests cover the owner, the tail, a private repository, a public repository read by someone who isn't crew (the logHidden branch, which the builder showed fails without the guard), an unknown run and bad arguments. The docs row and the pinned tool list are updated. I couldn't reach the preview from my sandbox, so this verdict rests on the code and the green check. Non-blocking: tail slices the 512 KB window in memory; readRunLogTail could take over later if that ever matters.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs