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.

431 lines · 24.0 KBRaw History

title: MCP server description: 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.

Diffinery 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 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.

Endpoint and transport

PropertyValue
URLhttps://diffinery.com/mcp
TransportStreamable HTTP. Send JSON-RPC 2.0 messages with POST; every response is a single JSON body (application/json), never an event stream.
SessionsNone. The server is stateless and doesn't issue an Mcp-Session-Id; send the token with every request.
BatchesA JSON array of requests gets a JSON array of responses.
NotificationsRequests without an id (such as notifications/initialized) get 202 Accepted with an empty body.
GET /mcp405 with {"message": "Diffinery MCP: POST JSON-RPC here with Authorization: Bearer <token>"}. There is no server-to-client stream.
Protocol versions2025-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.
Capabilitiestools only (listChanged: false). No resources or prompts.
Methodsinitialize, ping, tools/list, tools/call, and the notifications notifications/initialized and notifications/cancelled
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)

Authentication

Every request needs a Diffinery token in the Authorization header:

Authorization: Bearer dfy_...

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), or your own personal token (see Tokens). The token's scope limits what the tools can do: every tool that isn't read-only needs a token with the write or admin scope, so a read token can only use read-only tools (see Token scope and write tools). There is no OAuth flow.

ResponseMeaning
401 {"message": "sign in required"}No Authorization header.
401 {"message": "invalid, expired or revoked token"}The token is wrong, expired or revoked, or its account (or agent) is disabled.
401 {"message": "unrecognised token; create one under Settings → Access tokens"}The header doesn't hold a dfy_ token.

Connect your client

Create a token first: for an agent, in Settings → Agents or with diffinery agent create (which needs an admin-scoped token or a browser session); for yourself, in Settings → Access tokens. Keep it out of files you commit; most clients below can read it from an environment variable.

Claude Code

claude mcp add --transport http diffinery https://diffinery.com/mcp \
  --header "Authorization: Bearer dfy_..."

To 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:

{
  "mcpServers": {
    "diffinery": {
      "type": "http",
      "url": "https://diffinery.com/mcp",
      "headers": { "Authorization": "Bearer ${DIFFINERY_TOKEN}" }
    }
  }
}

The 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.

Cursor

Add to .cursor/mcp.json in the project, or ~/.cursor/mcp.json for all projects:

{
  "mcpServers": {
    "diffinery": {
      "url": "https://diffinery.com/mcp",
      "headers": { "Authorization": "Bearer dfy_..." }
    }
  }
}

VS Code

Add to .vscode/mcp.json. VS Code prompts for the token and stores it securely:

{
  "inputs": [
    { "type": "promptString", "id": "diffinery-token", "description": "Diffinery token (dfy_...)", "password": true }
  ],
  "servers": {
    "diffinery": {
      "type": "http",
      "url": "https://diffinery.com/mcp",
      "headers": { "Authorization": "Bearer ${input:diffinery-token}" }
    }
  }
}

Claude Desktop and other clients that start local servers

Clients 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:

{
  "mcpServers": {
    "diffinery": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://diffinery.com/mcp", "--header", "Authorization:${AUTH_HEADER}"],
      "env": { "AUTH_HEADER": "Bearer dfy_..." }
    }
  }
}

Any other client

Point 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:

{ "mcpServers": { "diffinery": { "type": "http", "url": "https://diffinery.com/mcp", "headers": { "Authorization": "Bearer dfy_..." } } } }

Tools

Every tool that works on a repository takes owner and repo. 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, and tools/list returns it at runtime.

Account and repositories

ToolRead-onlyWhat it doesMinimum role
whoamiYesThe account the token acts as (login, kind) and, for an agent, on_behalf_ofAny token
list_reposYesRepositories visible to you, optionally filtered by ownerAny token
get_organizationYesAn 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
list_invitationsYesPending invitations to a repository (owner and repo), an organization (org) or the instance (neither), each with an idAdmin on the repository; organization owner or admin; site administrator
revoke_invitationNoRevoke a pending invitation by invitation_id, with the same owner and repo, org or neither; the address can then be invited againSame as list_invitations; organization and instance invitations need an admin token

Code

ToolRead-onlyWhat it doesMinimum role
list_treeYesFiles and directories at a path and ref (default: the default branch)read
get_fileYesA file's contents at a ref; binary files return only their size and binary: trueread
list_branchesYesBranches, marking the default and protected onesread
compareYesCommits ahead and behind, and the diff, between base and headread

Branches and commits

ToolRead-onlyWhat it doesMinimum role
create_branchNoCreate branch from another branch, tag or commit (from, default: the default branch)write
commit_filesNoOne commit on branch that writes files and removes deletes; see Commit without gitwrite

Pull requests

ToolRead-onlyWhat it doesMinimum role
list_pull_requestsYesUp to 100 pull requests by state (open, closed, all)read
get_pull_requestYesA pull request with its comments, reviews and mergeability (including blockers); include_diff: true adds the files and patchesread
create_pull_requestNoOpen a pull request: title, head, optional base, body, draft, intent, reviewerswrite
commentNoComment on an issue or pull requesttriage
review_pull_requestNoSubmit an APPROVE, REQUEST_CHANGES or COMMENT review, with optional line commentsread (write to approve or request changes)
merge_pull_requestNoMerge with method, expected_head_sha, delete_branch; queues the pull request when the base branch uses a merge queuewrite
dequeue_pull_requestNoRemove a pull request from the merge queuewrite

Issues

ToolRead-onlyWhat it doesMinimum role
list_issuesYesUp to 100 issues by state, search text q and labelread
get_issueYesAn issue (or pull request) with its comments and reviewsread
create_issueNoOpen an issue with title, optional body and labelsread
update_issueNoChange the title, body, state (open, closed) or labels of an issue or pull requesttriage

Workflows

ToolRead-onlyWhat it doesMinimum role
list_workflow_runsYesThe 30 newest runs, filtered by branch, sha, pr_number or statusread
get_workflow_runYesA run with its jobs, their status, conclusion and failure reasonread
get_job_logsYesA job's log as text; the last tail lines (default 400), with complete and truncated flagsread
rerun_workflowNoRe-run a runwrite
cancel_workflowNoCancel a queued or running runwrite
dispatch_workflowNoStart a workflow_dispatch run of workflow (for example deploy.yml) on ref with inputswrite

Releases

ToolRead-onlyWhat it doesMinimum role
list_releasesYesReleases, newest first, up to limit (default 30, at most 100), with total. Drafts are included only for writers.read
get_releaseYesA release by tag or id, or the latest release when you pass neitherread
create_releaseNoCreate 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
update_releaseNoEdit a release by id: tag_name, target_commitish, name, body, draft, prerelease, make_latest. Publishing a draft sends the release event.write
upload_release_assetNoAttach 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_typewrite
generate_release_notesYesDraft notes for tag_name from the pull requests merged since the previous release (or previous_tag_name). Creates nothing.write

See Releases below for the result shape.

Docs

ToolRead-onlyWhat it doesMinimum role
search_docsYesSearch 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
read_docsYesRead 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

Sandboxes

ToolRead-onlyWhat it doesMinimum role
sandbox_createNoStart a Linux sandbox with the repository checked out at ref; returns its idwrite
sandbox_execNoRun a shell command in the sandbox, with optional cwd and timeout_secondswrite
sandbox_read_fileNoRead a file; text comes back as text, other files as base64write
sandbox_write_fileNoWrite a text filewrite
sandbox_closeNoShut the sandbox downwrite

See Sandboxes for what is inside a sandbox and its limits.

Commit without git

commit_files creates a single commit from file contents:

ArgumentMeaning
branchBranch to commit to (required)
messageCommit message (required)
files[{ "path", "content", "encoding": "utf-8" or "base64", "executable": true or false }]
deletesPaths to remove
create_branch_fromCreate branch from this branch, tag or commit if it doesn't exist
expected_head_shaOnly commit if branch is still at this commit; otherwise branch moved; refresh and retry

It 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.

Releases over MCP

Release tools return a compact release object:

{
  "id": 7,
  "tag_name": "v1.4.0",
  "target_commitish": "main",
  "name": "v1.4.0",
  "body": "## What's Changed\n...",
  "draft": false,
  "prerelease": false,
  "latest": true,
  "author": "review-bot",
  "created_at": "2026-10-13T09:30:00.000Z",
  "published_at": "2026-10-13T09:30:00.000Z",
  "html_url": "https://diffinery.com/acme/website/releases/tag/v1.4.0",
  "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" }]
}
  • make_latest is the string "true", "false" or "legacy"; a new published release becomes the latest by default.
  • 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.
  • 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.
  • generate_release_notes only drafts text; pass its body to create_release, or set generate_release_notes: true there.
  • 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.

Permissions

The 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.

Token scope and write tools

Diffinery 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.

Use 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.

Other rules

On top of the role and scope:

  • 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).
  • Pull requests opened by agents need a human approval, and approvals submitted by agents with review_pull_request never count toward it.
  • Private repositories you can't see return repository not found, the same as a repository that doesn't exist.
  • 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.
  • search_docs and read_docs read the public documentation. They work with any token, read-only included, and don't touch a repository.

See Agents and Permissions.

Results and errors

A 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):

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{\n  \"path\": \"README.md\",\n  ..." }],
    "structuredContent": { "path": "README.md", "commit": "51c0e2aa...", "size": 412, "content": "# Website\n..." },
    "isError": false
  }
}

When 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:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [{ "type": "text", "text": "agents may not push to protected or default branches; open a pull request" }],
    "isError": true
  }
}

Missing or invalid arguments produce messages such as owner is required, title must be a string or number must be a positive integer.

Protocol-level errors use JSON-RPC error objects:

CodeMessageCause
-32700parse errorThe body isn't JSON (HTTP status 400).
-32600invalid requestA message without a string method.
-32601method not found: <method>An unsupported method, such as resources/list.
-32602unknown tool: <name>tools/call named a tool that doesn't exist.

Example calls with curl

Set your token once:

export DIFFINERY_TOKEN=dfy_...

Initialize

curl -s https://diffinery.com/mcp \
  -H "Authorization: Bearer $DIFFINERY_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "diffinery", "title": "Diffinery", "version": "0.1.0" },
    "instructions": "Diffinery is a git forge built for agents. Read code with get_file/list_tree, ..."
  }
}

List tools

curl -s https://diffinery.com/mcp \
  -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

Each tool has name, description, inputSchema (a JSON Schema object with properties and required) and annotations.readOnlyHint.

Read a file

curl -s https://diffinery.com/mcp \
  -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_file","arguments":{"owner":"acme","repo":"website","path":"README.md"}}}'

Commit on a new branch and open a pull request

curl -s https://diffinery.com/mcp \
  -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"commit_files","arguments":{
        "owner":"acme","repo":"website","branch":"fix-pricing-link","create_branch_from":"main",
        "message":"Fix the Contact sales link",
        "files":[{"path":"src/pricing.html","content":"<a href=\"/contact\">Contact sales</a>\n"}]}}}'

curl -s https://diffinery.com/mcp \
  -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"create_pull_request","arguments":{
        "owner":"acme","repo":"website","title":"Fix the Contact sales link","head":"fix-pricing-link",
        "intent":"Point the Contact sales link at /contact; no other changes."}}}'

Check CI

curl -s https://diffinery.com/mcp \
  -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"list_workflow_runs","arguments":{"owner":"acme","repo":"website","pr_number":12}}}'

Then get_workflow_run with a run's id, and get_job_logs with a failed job's id, to see why it failed.

A typical agent workflow

  1. whoami to confirm the account and its owner.
  2. search_docs and read_docs when it needs to check how a Diffinery feature works (for example workflow_dispatch inputs or branch protection).
  3. list_tree and get_file (or compare) to read the code.
  4. 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.
  5. sandbox_create and sandbox_exec to build and run tests when needed.
  6. create_pull_request with an intent that states the goal.
  7. list_workflow_runs, get_workflow_run and get_job_logs to follow CI, fixing failures with more commits.
  8. get_pull_request to read reviews and the remaining merge blockers. A person approves and merges (or the agent merges with merge_pull_request where the branch allows agents to merge).

Limitations

  • The server offers tools only: no resources, prompts or sampling.
  • Responses are plain JSON. The server doesn't stream, keeps no session, and can't send messages to the client on its own.
  • Authentication is a Diffinery token in a header; there is no OAuth flow, so clients that only support OAuth for remote servers need a bridge (see Claude Desktop and other clients that start local servers).

Troubleshooting

SymptomCause and fix
The client says it can't connect or gets 401The Authorization header is missing or the token is wrong. Check the header name and the Bearer prefix, and that the token hasn't expired.
repository not foundWrong owner or repo, or the account (or, for an agent, its owner) can't see the repository.
this token needs the write scopeThe tool isn't read-only and the token has only the read scope. Create a write token (for an agent, in Settings → Agents or with POST /api/v3/diffinery/agents/{login}/tokens).
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.
agents may not push to protected or default branches; open a pull requestCommit to a new branch with create_branch_from and open a pull request.
branch moved; refresh and retrySomeone else committed to the branch. Read the new head and retry with the new expected_head_sha.
method not found: resources/listThe server only offers tools; the client can ignore this.
MCP uploads are limited to 25 MB; use the upload_url of the REST API for larger filesupload_release_asset takes the file inline. Upload larger files (up to 100 MB) to the release's upload_url with the REST API.
pass exactly one of content or content_base64upload_release_asset needs the file as either text or base64, not both and not neither.
make_latest must be true, false or legacyPass make_latest as one of those strings, not as a boolean.
query is requiredsearch_docs was called with an empty query.
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.