A lot of work now starts in a conversation with an AI assistant. Someone describes a bug to Claude, works out what is going on, and then has to leave that conversation to file a ticket somewhere else and wait for it to be picked up. We wanted the assistant to be able to hand the work over itself: create the task, get the spec drafted and approved, start the run, and later trigger the deploy.
The Model Context Protocol is the obvious way to do that, because the assistants people already use speak it. So CodeLynq has an MCP endpoint, and an OAuth 2.1 server in front of it. This post is about how that works, what limits it, and the two serious mistakes we found in it before anyone else did.
What an assistant can do
The endpoint offers fifteen tools that follow the life of a task:
- Read: list projects, repositories and branches, check a run's status, read the architecture map, list releases.
- Plan: create a task, generate its technical spec, approve the spec.
- Build and ship: start a run, generate documentation, trigger a deploy target.
- Investigate: open a read-only troubleshooting session on a task, ask follow-up questions, read the diagnosis.
The design decision that matters most is invisible: every tool calls the same route handler the web app calls. Starting a run over MCP goes through the same function as pressing the button, so the same plan limits, concurrency caps, approval rules, project membership checks and audit entries apply. There is no second implementation that can drift from the first, and no way to reach a project over MCP that you could not reach in the browser.
Tokens are API tokens, with scopes
We did not invent a new kind of credential. A token issued through the OAuth flow is an ordinary CodeLynq API token, named after the app that asked for it, shown in your security settings with a "Connected app" badge, and revoked the same way. It passes through the same checks as a token you created yourself, including an organisation's IP allowlist and MFA policy if one is set.
Every token has a scope: read or write. Each tool declares whether it writes, and a write tool called with a read token returns an error instead of acting. That needed one deliberate exception. For the rest of the API we gate on the HTTP method, where a GET is a read. MCP sends every call, read or write, as a POST, so the method tells you nothing and a read-only token would have been refused before reaching any tool. The MCP endpoint is exempt from the method check and enforces the scope per tool instead.
The OAuth side is authorization code with PKCE, and nothing else. S256 is mandatory and plain challenges are refused, redirect URIs must match exactly, codes are stored only as a hash, are single-use and expire after ten minutes, and the access tokens they produce expire after 90 days. You cannot authorise an app while a platform admin is impersonating you, or before your email address is verified, and the authorisation itself is written to the audit log.
What our own review found
After building it we reviewed the public endpoints the way an attacker would. Most of it held: every tool enforces project and organisation membership because the handler it calls does. Two things did not.
An authorization code could be used twice. The token endpoint checked whether a code had been used, then marked it used, as two separate steps. Two requests with the same code arriving at the same moment could both pass the check and both get a token. It is now a single conditional update that succeeds only while the code is unused; exactly one request wins, and the other is rejected as a replay.
Anyone could register a client that asked for write access. MCP clients register themselves without logging in. That is how the protocol is meant to work, and we allowed it. But we also gave those anonymous clients whatever scope they asked for, with any https redirect. An attacker could register a client with a plausible name, send a victim through our real consent screen, and receive a write token at their own address. Anonymous registration is now capped at read-only, and the scope granted can never exceed what the client was registered with, whatever the consent request says.
We also fixed two smaller issues in the same pass: a fallback that could add a scope the client was not allowed, and a state parameter that was copied into the redirect without encoding.
The limits, stated plainly
- Because of the fix above, an assistant that connects through the OAuth flow gets read access. To let it create tasks or start runs, you create a write-scoped API token yourself and give it to the client. There is no self-service way yet to register an app that may ask for write access through OAuth.
- The assistant acts as you. It can do what you can do in those projects, and no more, but also no less. Give it a token with the narrowest scope that does the job.
- Nothing about MCP skips a gate. A spec still needs approving, a run still shows its cost, and the pull request still waits for a human reviewer.
If you want to try it, start with a read-only connection and ask the assistant to list your projects. Setup steps are on the MCP integration page, and the rest of how we handle access is on security.