Skip to content

Add MCP write tools: updates, joining, my projects - #1944

Open
plusky wants to merge 16 commits into
SUSE:masterfrom
plusky:mcp-writes
Open

plusky wants to merge 16 commits into
SUSE:masterfrom
plusky:mcp-writes

Conversation

@plusky

@plusky plusky commented Oct 13, 2026

Copy link
Copy Markdown

State-changing MCP tools on top of #1943: posting project updates and joining projects (both enforce the write scope inside the tool plus the Ability check, denying uniformly), and my_projects, which proves the authenticated user is carried end to end. Includes the README section documenting the endpoint and token flow. Split from #1939, where this code was already reviewed.

To verify:

  • bundle exec rspec and bundle exec rubocop
  • With a write-scoped token: create_update on your project creates an Update; join_project adds membership; a read-scoped token gets the same denial as a guest
  • No database migration in this stack.

@plusky
plusky force-pushed the mcp-writes branch 4 times, most recently from dbdaf3f to 1b717df Compare October 16, 2026 13:44
plusky added 16 commits October 16, 2026 17:00
The official Ruby SDK for the Model Context Protocol powers the new machine interface. Only the server half is used, over the per-request controller transport, so no Rails version pin is needed.

Assisted-by: opencode/GLM-5.3-Flash
POST /mcp speaks Streamable HTTP through a per-request server, so multiple Puma workers need no shared session state. The caller authenticates with the normal login identity; Bearer tokens arrive with the token stack. Tools receive the user as server context and answer guests without leaking email.

Assisted-by: opencode/GLM-5.3-Flash
The episode listing tool authorizes through the same Ability as the HTML pages, and without a read rule every non-admin is denied. This grants Episode read alongside the other public listings, which also makes the episode index and show pages - already authentication-skipped in the controller - actually readable. Split out from the endpoint commit on review feedback.

Assisted-by: opencode/GLM-5.3-Flash
List, fetch, LIKE-search and episode listing go through the same Ability as the HTML pages, with a uniform denial that reveals nothing about missing versus forbidden records. Search stays clear of Sphinx so the default suite needs no searchd.

Assisted-by: opencode/GLM-5.3-Flash
The search tool runs a full-table LIKE per request, so unbounded clients can saturate the database. Throttle to 300 requests per 5 minutes per IP; production shares the file cache across Puma workers, the spec swaps in a memory store because test runs null_store.

Assisted-by: opencode/GLM-5.3-Flash
The only tool without a bound; episodes are few today but the endpoint should not serialize unbounded relations.

Assisted-by: opencode/GLM-5.3-Flash
Satisfies Style/ClassAndModuleChildren; no behavior change.

Assisted-by: opencode/GLM-5.3-Flash
Headless MCP clients cannot complete the browser login, so users mint per-client Bearer secrets in the web UI. Only a SHA256 digest hits the database; the raw secret renders once at mint time and is never stored or logged. Scopes separate read clients from write clients; revocation is an instant timestamp.

Assisted-by: opencode/GLM-5.3-Flash
Token management lives in profile settings and binds strictly to the requester, so even admins cannot mint credentials for somebody else. Creation caps at 50 tokens per user and the minted secret page carries Cache-Control: no-store.

Assisted-by: opencode/GLM-5.3-Flash
A present Bearer header owns the request: the session is ignored, so a junk secret degrades to guest instead of inheriting the cookie identity. Forgery protection is skipped only for authenticated tokens, authentication runs once per request, and last_used_at refreshes at most once a minute. Scope travels in the server context for the write tools of the next stack.

Assisted-by: opencode/GLM-5.3-Flash
A stolen bearer was valid until manual revoke. New tokens expire a year after mint; nil means never for tokens minted before this change. The UI shows the date; authentication rejects the expired.

Assisted-by: opencode/GLM-5.3-Flash
With forgery protection on, a junk bearer plus session cookie raised an HTML error instead of the uniform guest denial, fingerprinting sessions. Re-run the dispatch with a guest identity so the bytes match. Also drops two dead McpAuth wrappers nobody calls.

Assisted-by: opencode/GLM-5.3-Flash
Left over from extracting the dispatch helper; no behavior change.

Assisted-by: opencode/GLM-5.3-Flash
Posting updates and joining projects are the state-changing calls, so both enforce the write scope inside the tool on top of the Ability check, denying uniformly like the read tools. my_projects answers which projects the caller originates or belongs to, proving the authenticated user is carried end to end.

Assisted-by: opencode/GLM-5.3-Flash
Gives operators the endpoint URL, the token minting flow and a client configuration pointer, so the feature is discoverable without reading the code.

Assisted-by: opencode/GLM-5.3-Flash
Write calls accept unbounded TEXT, inviting table bloat through the API. Model-side bound covers the MCP tools and every other writer; existing copy stays orders of magnitude below it.

Assisted-by: opencode/GLM-5.3-Flash
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants