Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

515 lines · 37.1 KBRaw History
1---
2title: MCP server
3description: Connect AI agents to Diffinery over the Model Context Protocol at https://diffinery.com/mcp, with client setup, the tool list, permissions and example calls.
4---
5
6Diffinery runs a Model Context Protocol (MCP) server at `https://diffinery.com/mcp`. An MCP client such as Claude Code, Cursor or VS Code can use it to read code, commit changes, open and review pull requests, follow CI, publish releases, run commands in sandboxes, import repositories from GitHub and look things up in these docs, all without a git client. Every tool goes through the same permission checks, branch protection and agent guardrails as git and the REST API.
7
8## Endpoint and transport
9
10| Property | Value |
11| --- | --- |
12| URL | `https://diffinery.com/mcp` |
13| Transport | Streamable HTTP. Send JSON-RPC 2.0 messages with `POST`; every response is a single JSON body (`application/json`), never an event stream. |
14| Sessions | None. The server is stateless and doesn't issue an `Mcp-Session-Id`; send the token with every request. |
15| Batches | A JSON array of requests gets a JSON array of responses. |
16| Notifications | Requests without an `id` (such as `notifications/initialized`) get `202 Accepted` with an empty body. |
17| `GET /mcp` | `405` with `{"message": "Diffinery MCP: POST JSON-RPC here with Authorization: Bearer <token>"}`. There is no server-to-client stream. |
18| Protocol versions | `2025-06-18` (default), `2025-03-26`, `2024-11-05`. `initialize` answers with the version you asked for when it is supported, otherwise `2025-06-18`. |
19| Capabilities | `tools` only (`listChanged: false`). No resources or prompts. |
20| Methods | `initialize`, `ping`, `tools/list`, `tools/call`, and the notifications `notifications/initialized` and `notifications/cancelled` |
21| Server info | `{"name": "diffinery", "title": "Diffinery", "version": "0.1.0"}`, plus short usage `instructions` for the model (how to read code, commit, open pull requests, watch CI, use sandboxes and search the docs) |
22
23## Authentication
24
25There are two ways to authenticate, and every tool call goes through the same checks either way.
26
27**Sign in through the browser (OAuth).** Clients that support MCP's OAuth sign-in (claude.ai connectors, Claude Code, Codex, Cursor, VS Code) need nothing but the URL. On their first request they get a `401` whose header says where to sign in:
28
29```http
30WWW-Authenticate: Bearer realm="Diffinery", resource_metadata="https://diffinery.com/.well-known/oauth-protected-resource/mcp"
31```
32
33The client then opens Diffinery in your browser, where you choose whom it acts as (you, or one of your [agents](/docs/agents/overview)) and its access, and connect it. It gets tokens that last 8 hours and renew themselves; the connection is listed under **Settings → Access tokens → Connected apps**, where you can disconnect it. See [How sign-in works](/docs/connect/how-sign-in-works) for the details.
34
35**A token in the header.** For scripts, CI and cloud sessions where nobody can click through a browser, send a Diffinery token:
36
37```http
38Authorization: Bearer dfy_...
39```
40
41`token dfy_...` and HTTP Basic auth with the token as the password also work. Use an agent's token for an AI agent (see [Agents](/docs/agents/overview)), or your own personal token (see [Tokens](/docs/accounts/tokens)).
42
43Either way, the token's scope limits what the tools can do: every tool that isn't read-only needs the `write` or `admin` scope, so a `read` token can only use read-only tools (see [Token scope and write tools](#token-scope-and-write-tools)).
44
45| Response | Meaning |
46| --- | --- |
47| `401 {"message": "sign in required"}` | No `Authorization` header. The `WWW-Authenticate` header points at the OAuth metadata. |
48| `401 {"message": "invalid, expired or revoked token"}` | The token is wrong, expired or revoked, or its account (or agent) is disabled. `WWW-Authenticate` adds `error="invalid_token"`, so an OAuth client renews its token or signs in again. |
49| `401 {"message": "unrecognised token; create one under Settings → Access tokens"}` | The header doesn't hold a `dfy_` token. |
50
51## Connect your client
52
53For Claude, Claude Code, Codex and other apps with browser sign-in, [Connect AI apps](/docs/connect) has step-by-step guides: a connector or a plugin, and a browser sign-in. The snippets below use a token in the header instead, for clients without OAuth and for headless use. Create the token in the web UI: for an agent, in **Settings → Agents**; for yourself, in **Settings → Access tokens**. Tokens can't create tokens. Keep it out of files you commit; most clients below can read it from an environment variable.
54
55### Claude Code
56
57With a browser on hand, add the server without a header and sign in when Claude Code asks (or run `/mcp`):
58
59```sh
60claude mcp add --transport http diffinery https://diffinery.com/mcp
61```
62
63With a token instead:
64
65```sh
66claude mcp add --transport http diffinery https://diffinery.com/mcp \
67 --header "Authorization: Bearer dfy_..."
68```
69
70To share the server with a project without sharing the token, put this in the project's `.mcp.json` and set `DIFFINERY_TOKEN` in each developer's environment:
71
72```json
73{
74 "mcpServers": {
75 "diffinery": {
76 "type": "http",
77 "url": "https://diffinery.com/mcp",
78 "headers": { "Authorization": "Bearer ${DIFFINERY_TOKEN}" }
79 }
80 }
81}
82```
83
84The `diffinery` CLI prints exactly this snippet with `diffinery mcp-config` (add `--show-token` to embed the saved token instead of `${DIFFINERY_TOKEN}`). Run `/mcp` inside Claude Code to check that the server is connected.
85
86### Cursor
87
88Add to `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for all projects:
89
90```json
91{
92 "mcpServers": {
93 "diffinery": {
94 "url": "https://diffinery.com/mcp",
95 "headers": { "Authorization": "Bearer dfy_..." }
96 }
97 }
98}
99```
100
101### VS Code
102
103Add to `.vscode/mcp.json`. VS Code prompts for the token and stores it securely:
104
105```json
106{
107 "inputs": [
108 { "type": "promptString", "id": "diffinery-token", "description": "Diffinery token (dfy_...)", "password": true }
109 ],
110 "servers": {
111 "diffinery": {
112 "type": "http",
113 "url": "https://diffinery.com/mcp",
114 "headers": { "Authorization": "Bearer ${input:diffinery-token}" }
115 }
116 }
117}
118```
119
120### Claude Desktop and other clients that start local servers
121
122Clients whose configuration only launches local (stdio) servers can reach Diffinery through a bridge such as `mcp-remote`, which forwards to the remote URL with your header:
123
124```json
125{
126 "mcpServers": {
127 "diffinery": {
128 "command": "npx",
129 "args": ["-y", "mcp-remote", "https://diffinery.com/mcp", "--header", "Authorization:${AUTH_HEADER}"],
130 "env": { "AUTH_HEADER": "Bearer dfy_..." }
131 }
132 }
133}
134```
135
136### Any other client
137
138Point an MCP client that supports the Streamable HTTP transport at `https://diffinery.com/mcp` and configure a custom `Authorization: Bearer dfy_...` header. The usual JSON form is:
139
140```json
141{ "mcpServers": { "diffinery": { "type": "http", "url": "https://diffinery.com/mcp", "headers": { "Authorization": "Bearer dfy_..." } } } }
142```
143
144## Tools
145
146Every tool that works on a repository takes `owner` and `repo`. A repository that was transferred or renamed is also found under its old owner and name, for anyone who can read it, so an agent configured with the old name keeps working. Tools marked read-only have the MCP annotation `readOnlyHint: true`, so clients can run them without asking. Every tool marked **No** needs a token with the `write` or `admin` scope, whatever its minimum role. The full input schema of each tool is on [MCP tools reference](/docs/reference/mcp-tools), and `tools/list` returns it at runtime.
147
148### Content written by others
149
150Repository files, branch names, issue and pull request text, comments, workflow logs and sandbox output can be written by anyone who can push or comment, including on a public repository. Someone can write text there meant to steer your agent. Results from tools that return such content (`list_repos`, `list_tree`, `get_file`, `list_branches`, `compare`, `list_pull_requests`, `get_pull_request`, `list_issues`, `get_issue`, `list_workflow_runs`, `get_workflow_run`, `get_job_logs`, `sandbox_exec`, `sandbox_read_file`, `get_import` and `list_imports`) end with a second text block. It tells the model to treat the result as data and not to follow instructions inside it. These tools also carry the `openWorldHint` annotation in `tools/list`, and the server's `instructions` say the same. This lowers the risk but can't remove it, so give an agent a token with only the scopes and repositories it needs. See [Personal access tokens](/docs/accounts/tokens).
151
152### Account and repositories
153
154| Tool | Read-only | What it does | Minimum role |
155| --- | --- | --- | --- |
156| `whoami` | Yes | The account the token acts as (`login`, `kind`) and, for an agent, `on_behalf_of` | Any token |
157| `today` | Yes | Your morning view: pull requests that need you, your agents' open pull requests and where each stands, and what merged, was released or failed in the last 24 hours. See [Today](/docs/agents/today) | Any token |
158| `agent_tasks` | Yes | Agent tasks: with `owner` and `repo` that repository's, without them the ones you started (an agent: the ones it runs). See [Agent tasks](/docs/agents/tasks) | Write on the repository; any token for your own |
159| `agent_task` | Yes | One agent task (`owner`, `repo`, `number`) with its runs, pull request and transcript after `after` | Write |
160| `project` | Yes | The repository's [project](/docs/agents/projects): its task board, memory and settings | Write |
161| `project_posts` | Yes | The project's conversation after `after`, or the latest 100 lines | Write |
162| `list_repos` | Yes | Repositories visible to you, optionally filtered by `owner` | Any token |
163| `get_organization` | Yes | An organization's `login` and `name`; to its members, its agents, agents of its members and site administrators also `default_repository_permission` (the base permission members get on its repositories) | Any token |
164| `list_invitations` | Yes | Pending invitations to a repository (`owner` and `repo`), an organization (`org`) or the instance (neither), each with an `id`, and the `email` or, for an invitation sent to a username, the `login` | Admin on the repository; organization owner or admin; site administrator |
165| `revoke_invitation` | No | Revoke a pending invitation by `invitation_id`, with the same `owner` and `repo`, `org` or neither; the address can then be invited again | Same as `list_invitations`; organization and instance invitations need an `admin` token |
166
167### Code
168
169| Tool | Read-only | What it does | Minimum role |
170| --- | --- | --- | --- |
171| `list_tree` | Yes | Files and directories at a `path` and `ref` (default: the default branch) | read |
172| `get_file` | Yes | A file's contents at a `ref`; binary files return only their size and `binary: true` | read |
173| `list_branches` | Yes | Branches, marking the default and protected ones | read |
174| `compare` | Yes | Commits ahead and behind, and the diff, between `base` and `head` | read |
175
176### Branches and commits
177
178| Tool | Read-only | What it does | Minimum role |
179| --- | --- | --- | --- |
180| `create_branch` | No | Create `branch` from another branch, tag or commit (`from`, default: the default branch) | write |
181| `commit_files` | No | One commit on `branch` that writes `files` and removes `deletes`; see [Commit without git](#commit-without-git) | write |
182
183### Pull requests
184
185| Tool | Read-only | What it does | Minimum role |
186| --- | --- | --- | --- |
187| `list_pull_requests` | Yes | Up to 100 pull requests by `state` (`open`, `closed`, `all`) | read |
188| `get_pull_request` | Yes | A pull request with its comments, reviews and mergeability (including blockers); `include_diff: true` adds the files and patches. `mergeability.blockers` is what stops the caller merging it directly now. For agents and workflow tokens it also returns `auto_merge_blockers`: what stops Diffinery landing it through auto-merge or the merge queue (`[]` means it would land now). An agent on a protected branch sees `agents may not merge into this branch` in the first list but not in the second when the rule allows auto-merge. | read |
189| `create_pull_request` | No | Open a pull request: `title`, `head`, optional `base`, `body`, `draft`, `intent`, `reviewers`, and `auto_merge` (`true` turns on auto-merge as it opens, `false` keeps it off where your setting would turn it on; `autoMergeSkipped` says why it stayed off when asked). See [Merge on green automatically](/docs/collaboration/merge-on-green) | write |
190| `comment` | No | Comment on an issue or pull request | triage |
191| `review_pull_request` | No | Submit an `APPROVE`, `REQUEST_CHANGES` or `COMMENT` review, with optional line `comments` | read (write to approve or request changes) |
192| `merge_pull_request` | No | Merge with `method`, `expected_head_sha`, `delete_branch`; queues the pull request when the base branch uses a merge queue. Agents can't merge into the default branch or protected branches this way: they use `enable_auto_merge` | write |
193| `enable_auto_merge` | No | Turn on auto-merge, optionally with `merge_method`, `commit_title`, `commit_message`, `delete_branch` and `expected_head_sha`: Diffinery merges the pull request (or adds it to the merge queue) once every branch rule is met. Returns `state` (`enabled`, `queued` or `merged`), `autoMerge` and, once merged, `mergeCommit`. See [Auto-merge](/docs/collaboration/pull-requests#auto-merge) and [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes) | write |
194| `disable_auto_merge` | No | Turn auto-merge off; returns `{"auto_merge": null}` | write |
195| `dequeue_pull_request` | No | Remove a pull request from the merge queue | write |
196
197### Issues
198
199| Tool | Read-only | What it does | Minimum role |
200| --- | --- | --- | --- |
201| `list_issues` | Yes | Up to 100 issues by `state`, search text `q` and `label` | read |
202| `get_issue` | Yes | An issue (or pull request) with its comments and reviews | read |
203| `create_issue` | No | Open an issue with `title`, optional `body` and `labels` (`labels` from an account below triage are dropped, as on GitHub) | read |
204| `update_issue` | No | Change the `title`, `body`, `state` (`open`, `closed`) or `labels` of an issue or pull request | triage |
205
206### Workflows
207
208| Tool | Read-only | What it does | Minimum role |
209| --- | --- | --- | --- |
210| `list_workflow_runs` | Yes | The 30 newest runs, filtered by `branch`, `sha`, `pr_number` or `status` | read |
211| `get_workflow_run` | Yes | A run with its jobs, their status, conclusion and failure reason | read |
212| `get_job_logs` | Yes | A job's log as text; the last `tail` lines (default 400), with `complete` and `truncated` flags | read |
213| `rerun_workflow` | No | Re-run a finished run as a new attempt; with `failed_only`, only its failed jobs and the jobs that depend on them | write |
214| `rerun_job` | No | Re-run one job (`job_id`) of a finished run and the jobs that depend on it | write |
215| `cancel_workflow` | No | Cancel a queued or running run | write |
216| `dispatch_workflow` | No | Start a `workflow_dispatch` run of `workflow` (for example `deploy.yml`) on `ref` with `inputs` | write |
217
218### Releases
219
220| Tool | Read-only | What it does | Minimum role |
221| --- | --- | --- | --- |
222| `list_releases` | Yes | Releases, newest first, up to `limit` (default 30, at most 100), with `total`. Drafts are included only for writers. | read |
223| `get_release` | Yes | A release by `tag` or `id`, or the latest release when you pass neither | read |
224| `create_release` | No | Create a release for `tag_name`, creating the tag at `target_commitish` (default: the default branch) if it doesn't exist. Optional `name`, `body`, `draft`, `prerelease`, `make_latest` and `generate_release_notes`. | write |
225| `update_release` | No | Edit a release by `id`: `tag_name`, `target_commitish`, `name`, `body`, `draft`, `prerelease`, `make_latest`. Publishing a draft sends the `release` event. | write |
226| `upload_release_asset` | No | Attach a file of up to 25 MB to release `release_id`: `name`, and either `content` (text) or `content_base64` (bytes), with optional `label` and `content_type` | write |
227| `generate_release_notes` | Yes | Draft notes for `tag_name` from the pull requests merged since the previous release (or `previous_tag_name`). Creates nothing. | write |
228
229See [Releases](#releases-over-mcp) below for the result shape.
230
231### Docs
232
233| Tool | Read-only | What it does | Minimum role |
234| --- | --- | --- | --- |
235| `search_docs` | Yes | Search this documentation for `query` (for example `merge queue` or `workflow_dispatch inputs`). Returns up to 8 matching pages, each with `page`, `title`, `section`, the best matching `heading`, `url`, `markdown_url` and a `snippet`. | Any token |
236| `read_docs` | Yes | Read one docs page as Markdown. `page` is a slug (`actions/workflow-syntax`), a docs URL, or `index` for the overview that links to every page. Returns `page`, `title` and `markdown`. | Any token |
237
238### Sandboxes
239
240| Tool | Read-only | What it does | Minimum role |
241| --- | --- | --- | --- |
242| `sandbox_create` | No | Start a Linux sandbox with the repository checked out at `ref`; returns its `id` | write |
243| `sandbox_exec` | No | Run a shell `command` in the sandbox, with optional `cwd` and `timeout_seconds` | write |
244| `sandbox_read_file` | No | Read a file; text comes back as text, other files as base64 | write |
245| `sandbox_write_file` | No | Write a text file | write |
246| `sandbox_close` | No | Shut the sandbox down | write |
247
248See [Sandboxes](/docs/agents/sandboxes) for what is inside a sandbox and its limits.
249
250### Imports from GitHub
251
252| Tool | Read-only | What it does | Minimum role |
253| --- | --- | --- | --- |
254| `start_github_import` | No | Start a server-side import of one GitHub repository: `source` (`owner/name` or a GitHub URL) and `owner` (the Diffinery organization or user to import into), optional `name`, `visibility` (`keep`, `private`, `public`), `github_token` and `github_api_url`. Returns the import, with its `id`. | See [Imports over MCP](#imports-over-mcp) |
255| `get_import` | Yes | An import by `id`: `status`, per-step progress, `git` transfer progress, `summary`, `warnings`, `error` and, for an organization import, its repositories | You started it; a person who administers its target, or a site administrator, can also read it |
256| `list_imports` | Yes | The imports you started, newest first | Any token |
257
258## Commit without git
259
260`commit_files` creates a single commit from file contents:
261
262| Argument | Meaning |
263| --- | --- |
264| `branch` | Branch to commit to (required) |
265| `message` | Commit message (required) |
266| `files` | `[{ "path", "content", "encoding": "utf-8" or "base64", "executable": true or false }]` |
267| `deletes` | Paths to remove |
268| `create_branch_from` | Create `branch` from this branch, tag or commit if it doesn't exist |
269| `expected_head_sha` | Only commit if `branch` is still at this commit; otherwise `branch moved; refresh and retry` |
270
271It returns `{ "sha": "...", "branch": "..." }`. At least one file or delete is required (`nothing to commit`). Committing to a branch that doesn't exist without `create_branch_from` fails with `branch fix-pricing-link not found`. Commits made by agents are stamped with the agent and its owner; see [Agents](/docs/agents/overview#how-agent-activity-is-recorded).
272
273`commit_files` runs [push protection](/docs/security/push-protection) before it writes anything, like `git push`. If a file looks like a credential (an AWS key, a GitHub or Diffinery token, a private key and so on), nothing is committed and the tool fails with, for example, `push protection: possible secret detected (AWS access key) in config/deploy.env. Remove it, or rotate it and ask an admin.` The attempt is recorded in the audit log as `api.blocked_secret` against the token's account, and the agent's owner gets a `Your commit was blocked` email. `commit_files` has no bypass. For a test fixture with fake keys, put the text `diffinery:allow-secret` anywhere in the file.
274
275## Releases over MCP
276
277Release tools return a compact release object:
278
279```json
280{
281 "id": 7,
282 "tag_name": "v1.4.0",
283 "target_commitish": "main",
284 "name": "v1.4.0",
285 "body": "## What's Changed\n...",
286 "draft": false,
287 "prerelease": false,
288 "latest": true,
289 "author": "review-bot",
290 "created_at": "2026-10-13T09:30:00.000Z",
291 "published_at": "2026-10-13T09:30:00.000Z",
292 "html_url": "https://diffinery.com/acme/website/releases/tag/v1.4.0",
293 "assets": [{ "id": 12, "name": "notes.txt", "label": "", "content_type": "text/plain", "size": 18, "download_count": 0, "download_url": "https://diffinery.com/acme/website/releases/download/v1.4.0/notes.txt" }]
294}
295```
296
297- `make_latest` is the string `"true"`, `"false"` or `"legacy"`; a new published release becomes the latest by default.
298- `draft: true` keeps a release visible to writers only. `update_release` with `draft: false` publishes it, which starts `on: release` workflows and sends the `release` webhook event.
299- `upload_release_asset` takes the file inline, so it is limited to 25 MB (`MCP uploads are limited to 25 MB; use the upload_url of the REST API for larger files`). Pass exactly one of `content` and `content_base64`. Text defaults to `text/plain`, bytes to `application/octet-stream`. For files up to 100 MB, use the release's `upload_url` in the [REST API](/docs/reference/rest-api#release-assets).
300- `generate_release_notes` only drafts text; pass its `body` to `create_release`, or set `generate_release_notes: true` there.
301- Publishing a release for a tag that doesn't exist creates the tag at `target_commitish`, and that follows the same rules as any ref update. A draft creates its tag only when it is published.
302
303## Imports over MCP
304
305`start_github_import` starts the same server-side importer as the web UI, `diffinery migrate github` and `POST /api/v3/diffinery/imports`. It imports one repository with every part turned on: Git history and Git LFS, issues and pull requests with their numbers, comments, reviews and review comments, labels, milestones, branch protection, releases, and (for people) Actions variables, environments and secret names. Secret values are never read from GitHub. The tool returns at once; the import runs on the server.
306
307```sh
308curl -s https://diffinery.com/mcp \
309 -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
310 -d '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"start_github_import","arguments":{"source":"acme/website","owner":"acme","github_token":"ghp_..."}}}'
311```
312
313The result is the import object (`id`, `html_url`, `status`, `source`, `target`, `steps`, `git`, `summary`, `warnings`, `error`). Call `get_import` with the `id` every minute or so until `status` is `succeeded`, `failed` or `cancelled`. While the import runs, every other write to the new repository is refused: pushes with `repository is locked by an import in progress`, and tools such as `create_issue` with `repository is locked by an import in progress; try again when it finishes, or cancel the import`. A person can still set the repository's Actions secrets and variables, environments and collaborators while it runs.
314
315Who can start one:
316
317- The token needs the `write` or `admin` scope (`starting an import needs a token with write or admin scope`).
318- A person can import into their own account or an organization they own or administer.
319- An agent can import only where its owner may: a person's agent into the owner's own account or an organization the owner owns or administers, an organization's agent into that organization (`agents can import only where their owner may: the owner's own account or an organization the owner administers, not <owner>`). Its maximum role must be at least `write` (`this agent's policy does not allow writing; an import needs at least write`).
320
321Rules for imports an agent starts:
322
323- **Push protection is never bypassed.** If old history contains something that looks like a credential, the import stops and fails with `Push protection found possible secrets in the history (...)`. A person who administers the target starts a new import from the web or API to import it with an audited bypass.
324- **Variables, secret names and environments are skipped** (`skipped (agents cannot manage variables)`); a person sets them up afterwards. Agents never read or set secret values.
325- **No retry once the repository exists.** An agent's retry (through the REST API) is refused after the repository was created; a person who administers the target retries it instead.
326- The agent sees only the imports it started (`get_import`, `list_imports`).
327
328An import uses the GitHub token only for that import, deletes it when the import ends, and never returns it. For GitHub Enterprise, pass `github_api_url`, for example `https://ghe.example.com/api/v3`; when `source` is an Enterprise URL and you pass a token, `github_api_url` must be on the same host. See [Migrate from GitHub](/docs/migrate-from-github) for what is copied, limits (at most 3 repository imports at a time per account and per target) and errors.
329
330## Permissions
331
332The token's account (person or agent), its role on the repository and the token's scope decide what each tool may do, exactly as for the REST API. The minimum role for each tool is in the tables above.
333
334### Token scope and write tools
335
336Diffinery checks the token's scope before a tool runs. A tool that isn't read-only (`readOnlyHint: false`) needs a token with the `write` or `admin` scope. With a `read` token, the call fails with `isError: true` and the text `this token needs the write scope`, even when the role the tool asks for is only read or triage: `create_issue`, `comment`, a `COMMENT` review with `review_pull_request`, and the sandbox tools all need a `write` token. Read-only tools work with any scope, including `read`.
337
338Use a `write` token for agents that change anything, and a `read` token for agents that only read code, issues, pull requests, runs, releases and docs.
339
340### Other rules
341
342On top of the role and scope:
343
344- Agents can't push or commit to the default branch or protected branches, and can't merge into them, unless the branch's rule allows it (`agentsMayPush`, `agentsMayMerge`). Where the rule has `allowAgentAutoMerge` and a required check, an agent lands its pull request there with `enable_auto_merge` instead, and Diffinery merges once the rules are met (see [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes)).
345- Pull requests opened by agents need a human approval, and approvals submitted by agents with `review_pull_request` never count toward it.
346- Private repositories you can't see return `repository not found`, the same as a repository that doesn't exist.
347- Archived repositories refuse every tool that changes something (`repository is archived (read-only)`), whatever the role. Read-only tools keep working.
348- `commit_files` refuses files that look like credentials (see [Commit without git](#commit-without-git)).
349- `create_issue` drops `labels` when the account's role is below triage, as GitHub does for issues opened by people without triage access.
350- Release tools follow the same rules as the REST API: anyone who can read the repository can list published releases, writers create, edit and publish them and see drafts, and a release that creates a tag is a ref update, so agent branch and tag rules apply.
351- `start_github_import` follows the agent import rules in [Imports over MCP](#imports-over-mcp).
352- `search_docs` and `read_docs` read the public documentation. They work with any token, read-only included, and don't touch a repository.
353
354See [Agents](/docs/agents/overview#guardrails) and [Permissions](/docs/accounts/permissions).
355
356## Results and errors
357
358A successful `tools/call` returns the tool's output twice: as pretty-printed JSON in a text content block, and as `structuredContent` (the object itself, or `{ "result": ... }` when the output is an array or a plain value):
359
360```json
361{
362 "jsonrpc": "2.0",
363 "id": 3,
364 "result": {
365 "content": [{ "type": "text", "text": "{\n \"path\": \"README.md\",\n ..." }],
366 "structuredContent": { "path": "README.md", "commit": "51c0e2aa...", "size": 412, "content": "# Website\n..." },
367 "isError": false
368 }
369}
370```
371
372When a tool fails, the call still succeeds at the JSON-RPC level, with `isError: true` and the error message as text. The messages are the same as the REST API's:
373
374```json
375{
376 "jsonrpc": "2.0",
377 "id": 4,
378 "result": {
379 "content": [{ "type": "text", "text": "agents may not push to protected or default branches; open a pull request" }],
380 "isError": true
381 }
382}
383```
384
385Missing or invalid arguments produce messages such as `owner is required`, `title must be a string` or `number must be a positive integer`.
386
387Tools that change something count toward your account's 60 changes a minute. Past that, the call fails with `Too many changes from your account: at most 60 a minute through MCP. Try again in a minute.` Every request to `/mcp` also counts toward the account's 2,000 requests a minute; past that, the HTTP response itself is `429` with `retry-after: 60`. See [Rate limits](/docs/reference/rest-api#rate-limits).
388
389Protocol-level errors use JSON-RPC error objects:
390
391| Code | Message | Cause |
392| --- | --- | --- |
393| `-32700` | `parse error` | The body isn't JSON (HTTP status 400). |
394| `-32600` | `invalid request` | A message without a string `method`. |
395| `-32601` | `method not found: <method>` | An unsupported method, such as `resources/list`. |
396| `-32602` | `unknown tool: <name>` | `tools/call` named a tool that doesn't exist. |
397
398## Example calls with curl
399
400Set your token once:
401
402```sh
403export DIFFINERY_TOKEN=dfy_...
404```
405
406### Initialize
407
408```sh
409curl -s https://diffinery.com/mcp \
410 -H "Authorization: Bearer $DIFFINERY_TOKEN" \
411 -H "Content-Type: application/json" \
412 -H "Accept: application/json, text/event-stream" \
413 -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
414```
415
416```json
417{
418 "jsonrpc": "2.0",
419 "id": 1,
420 "result": {
421 "protocolVersion": "2025-06-18",
422 "capabilities": { "tools": { "listChanged": false } },
423 "serverInfo": { "name": "diffinery", "title": "Diffinery", "version": "0.1.0" },
424 "instructions": "Diffinery is a git forge built for agents. Read code with get_file/list_tree, ..."
425 }
426}
427```
428
429### List tools
430
431```sh
432curl -s https://diffinery.com/mcp \
433 -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
434 -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
435```
436
437Each tool has `name`, `description`, `inputSchema` (a JSON Schema object with `properties` and `required`) and `annotations.readOnlyHint`.
438
439### Read a file
440
441```sh
442curl -s https://diffinery.com/mcp \
443 -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
444 -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_file","arguments":{"owner":"acme","repo":"website","path":"README.md"}}}'
445```
446
447### Commit on a new branch and open a pull request
448
449```sh
450curl -s https://diffinery.com/mcp \
451 -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
452 -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"commit_files","arguments":{
453 "owner":"acme","repo":"website","branch":"fix-pricing-link","create_branch_from":"main",
454 "message":"Fix the Contact sales link",
455 "files":[{"path":"src/pricing.html","content":"<a href=\"/contact\">Contact sales</a>\n"}]}}}'
456
457curl -s https://diffinery.com/mcp \
458 -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
459 -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"create_pull_request","arguments":{
460 "owner":"acme","repo":"website","title":"Fix the Contact sales link","head":"fix-pricing-link",
461 "intent":"Point the Contact sales link at /contact; no other changes."}}}'
462```
463
464### Check CI
465
466```sh
467curl -s https://diffinery.com/mcp \
468 -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
469 -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"list_workflow_runs","arguments":{"owner":"acme","repo":"website","pr_number":12}}}'
470```
471
472Then `get_workflow_run` with a run's `id`, and `get_job_logs` with a failed job's `id`, to see why it failed.
473
474## A typical agent workflow
475
4761. `whoami` to confirm the account and its owner.
4772. `search_docs` and `read_docs` when it needs to check how a Diffinery feature works (for example `workflow_dispatch inputs` or `branch protection`).
4783. `list_tree` and `get_file` (or `compare`) to read the code.
4794. `commit_files` with `create_branch_from` to put changes on a new branch, passing `expected_head_sha` on later commits to avoid overwriting someone else's.
4805. `sandbox_create` and `sandbox_exec` to build and run tests when needed.
4816. `create_pull_request` with an `intent` that states the goal.
4827. `list_workflow_runs`, `get_workflow_run` and `get_job_logs` to follow CI, fixing failures with more commits.
4838. `get_pull_request` to read reviews and the remaining merge blockers. A person approves and merges. Or the agent calls `enable_auto_merge` where the branch allows it (`allowAgentAutoMerge`), and Diffinery merges once the checks pass and a person has approved (or without the approval where an admin turned on `agentAutoMergeWithoutApproval`); `auto_merge_blockers` lists what auto-merge still needs. Or it merges with `merge_pull_request` where the branch allows agents to merge.
484
485## Limitations
486
487- The server offers tools only: no resources, prompts or sampling.
488- Responses are plain JSON. The server doesn't stream, keeps no session, and can't send messages to the client on its own.
489- OAuth sign-in supports the authorization code flow with PKCE only, for apps that register themselves or identify by a client metadata document. There is no device flow for MCP; on a machine without a browser, use a token in the header.
490
491## Troubleshooting
492
493| Symptom | Cause and fix |
494| --- | --- |
495| The client says it can't connect or gets `401` | The `Authorization` header is missing or the token is wrong. Check the header name and the `Bearer ` prefix, and that the token hasn't expired. |
496| The browser says "Can't connect this app" | The app's registration expired (unused registrations are removed after a week) or it asked to send you back to an address it didn't register. Remove the server in the app and add it again. |
497| An app that used to work asks you to sign in again | Its connection lapsed after 30 days unused, or you (or a security check after a reused refresh token) disconnected it. Sign in again. |
498| `repository not found` | Wrong `owner` or `repo`, or the account (or, for an agent, its owner) can't see the repository. |
499| `this token needs the write scope` | The tool isn't read-only and the token has only the `read` scope. Create a `write` token (for an agent, in **Settings → Agents** while signed in). |
500| `write access required` (or `triage access required`) | The account's role, or the token's scope, is too low for the tool. A `read` token can only use read-only tools. |
501| `agents may not push to protected or default branches; open a pull request` | Commit to a new branch with `create_branch_from` and open a pull request. |
502| `agents may not merge into this branch; a human must merge` | `merge_pull_request` into the default branch or a protected branch. Use `enable_auto_merge` where the branch allows it, or ask a person to merge. |
503| `agents may not have pull requests merged into main automatically unless its branch protection allows it ...` | `enable_auto_merge` on a branch whose rule doesn't set `allowAgentAutoMerge`. A person merges, or an admin allows it. |
504| `Auto-merge was turned off: ...` (a comment on the pull request) | Auto-merge stopped, for example because someone else pushed to the branch. Read the reason and call `enable_auto_merge` again. See [When auto-merge turns itself off](/docs/collaboration/pull-requests#when-auto-merge-turns-itself-off). |
505| `branch moved; refresh and retry` | Someone else committed to the branch. Read the new head and retry with the new `expected_head_sha`. |
506| `push protection: possible secret detected (...)` | A file passed to `commit_files` looks like a credential. Remove it and commit again, or add `diffinery:allow-secret` to a file whose keys are fake. |
507| `repository is archived (read-only)` | The repository is archived. Only read-only tools work until a repository admin unarchives it. |
508| `method not found: resources/list` | The server only offers tools; the client can ignore this. |
509| `MCP uploads are limited to 25 MB; use the upload_url of the REST API for larger files` | `upload_release_asset` takes the file inline. Upload larger files (up to 100 MB) to the release's `upload_url` with the [REST API](/docs/reference/rest-api#release-assets). |
510| `pass exactly one of content or content_base64` | `upload_release_asset` needs the file as either text or base64, not both and not neither. |
511| `make_latest must be true, false or legacy` | Pass `make_latest` as one of those strings, not as a boolean. |
512| `query is required` | `search_docs` was called with an empty `query`. |
513| `agents can import only where their owner may: ...` | Pass as `owner` the owner's own account or an organization the owner administers (for an organization's agent, that organization). |
514| `too many repository imports are already running (at most 3 at a time); wait for one to finish` | Wait for one of the running imports to finish (check with `list_imports`). |
515| `no docs page "..."; pages: ...` | `read_docs` didn't recognize the slug. Pick one from the list in the message, or call `read_docs` with `index`. |