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.

Merge 1e251dd626abe397ca935eb2af5cb00c1dcecf03 into 629b6aba6d52f56ed5cb049c8a8802e851efaea1

Diffinery authored 2 parents 629b6ab1e251ddcommit d76219148878a9b129804a02a224076e93b16c2dBrowse files
7 files changed+87−0
Files (7)
modifiedapps/diffinery/docs/agents/mcp.md+22−0View file
@@ -482,6 +482,28 @@
482482 7. `list_workflow_runs`, `get_workflow_run` and `get_job_logs` to follow CI, fixing failures with more commits.
483483 8. `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.
484484
485+## List your instance in the MCP registry
486+
487+The [official MCP registry](https://registry.modelcontextprotocol.io) lists Diffinery's MCP server as `com.diffinery/diffinery`, with the URL `https://{host}/mcp`: clients that read the registry ask for your instance's host name. To list your own instance under your domain as well, prove that you own the domain with a key:
488+
489+1. Make a key pair and its proof record (OpenSSL 3):
490+
491+ ```sh
492+ openssl genpkey -algorithm Ed25519 -out mcp-registry-key.pem
493+ echo "v=MCPv1; k=ed25519; p=$(openssl pkey -in mcp-registry-key.pem -pubout -outform DER | tail -c 32 | base64)"
494+ ```
495+
496+2. Add the printed line as `"MCP_REGISTRY_AUTH"` under `vars` in `wrangler.jsonc` and deploy. Check that `https://<your host>/.well-known/mcp-registry-auth` returns it.
497+3. Sign in to the registry with [`mcp-publisher`](https://github.com/modelcontextprotocol/registry) and publish a `server.json` named after your domain in reverse (`com.example/diffinery` for `git.example.com`), with `remotes` set to `[{"type": "streamable-http", "url": "https://git.example.com/mcp"}]`:
498+
499+ ```sh
500+ mcp-publisher login http --domain git.example.com \
501+ --private-key "$(openssl pkey -in mcp-registry-key.pem -noout -text | grep -A3 'priv:' | tail -n +2 | tr -d ' :\n')"
502+ mcp-publisher publish server.json
503+ ```
504+
505+The record holds only the public key. Keep the `.pem` file to publish updates; if you lose it, make a new pair and replace the record.
506+
485507 ## Limitations
486508
487509 - The server offers tools only: no resources, prompts or sampling.
modifiedapps/diffinery/docs/self-hosting/configuration.md+1−0View file
@@ -43,6 +43,7 @@
4343 | `EGRESS` | Not set | No | `web` limits hosted Actions jobs and agent sandboxes (including tasks, reviews and terminals) to HTTP and HTTPS (ports 80 and 443) and DNS. Any other value, or none, leaves them open to the whole internet. Accounts run by a site administrator stay open. See [EGRESS](#egress). |
4444 | `RUNNER_IMAGE` | Set by the deploy | No | The runner image's name and tag (for example `diffinery-runner:96dbbe31790f`), as jobs report it in `runner.image`. `scripts/render-config.mjs` sets it when the deploy uses a prebuilt runner image, and `GET /api/v3/meta` returns it as `runner_image` (`null` when not set). Don't set it by hand. |
4545 | `SECURITY_CONTACT` | Not set | No | More places people can report a vulnerability in your instance, listed in `/.well-known/security.txt` after the instance's own report form (`/security/report`): an email address or an `https://` URL, several separated by commas. See [Security contact](#security-contact). |
46+| `MCP_REGISTRY_AUTH` | Not set | No | The proof record served at `/.well-known/mcp-registry-auth`, such as `v=MCPv1; k=ed25519; p=<public key>`, so you can publish your instance's MCP server to the official MCP registry under your domain. Not set, that path is a 404. See [List your instance in the MCP registry](/docs/agents/mcp#list-your-instance-in-the-mcp-registry). |
4647 | `INDEXNOW` | Not set | No | `off` stops telling search engines about new, changed and removed public pages through IndexNow. Not set, or anything else, it is on whenever `PUBLIC_URL` is a public `https` origin and `MASTER_KEY` is set. Turn it off for an instance search engines can't reach. See [Search engines](/docs/admin/site-administration#search-engines). |
4748 | `TERMINALS` | Not set | No | `off` switches [terminals](/docs/agents/terminal) off: nobody can open one, and open ones end at their next check, within a minute. Anything else, or not set, leaves them on. |
4849
modifiedapps/diffinery/src/app.ts+2−0View file
@@ -36,6 +36,7 @@
3636 import { mailQueue } from "./mail/send.js";
3737 import { mailEvents, mailEventsQueue } from "./mail/events.js";
3838 import { securityTxtRoutes } from "./http/security-txt.js";
39+import { mcpRegistryAuthRoutes } from "./http/mcp-registry-auth.js";
3940 import { securityReportsApi } from "./http/security-reports.js";
4041 import { earlyAccessApi } from "./http/early-access.js";
4142 import { statusApi } from "./http/status.js";
@@ -123,6 +124,7 @@
123124 app.route("/", oauthApi);
124125 app.route("/", wellKnown);
125126 app.route("/", securityTxtRoutes);
127+app.route("/", mcpRegistryAuthRoutes);
126128 app.route("/", git);
127129 app.route("/", runner);
128130 app.route("/", mailApi);
modifiedapps/diffinery/src/docs/reference.ts+1−0View file
@@ -269,6 +269,7 @@
269269 ["GET /.well-known/openid-configuration", "OIDC discovery for workflow ID tokens. See [OIDC](/docs/actions/oidc)."],
270270 ["GET /.well-known/jwks", "Public keys that sign workflow ID tokens."],
271271 ["GET /.well-known/security.txt", "Where to report a vulnerability (RFC 9116): this instance's report form first, then any `SECURITY_CONTACT` entries. See [Reporting a vulnerability](/docs/security/disclosure)."],
272+ ["GET /.well-known/mcp-registry-auth", "The proof record that lets this instance's MCP server be published to the official MCP registry under its domain; 404 until `MCP_REGISTRY_AUTH` is set. See [MCP](/docs/agents/mcp#list-your-instance-in-the-mcp-registry)."],
272273 ["GET, POST /security/report", "The private security report form: signed in, or anonymously after Cloudflare Turnstile. Only site administrators can read reports. See [Reporting a vulnerability](/docs/security/disclosure)."],
273274 ["POST /-/email/events", "Signed bounce and complaint intake for the email suppression list; 404 until `MAIL_EVENTS_SECRET` is set. See [Site administration](/docs/admin/site-administration#bounces-complaints-and-the-suppression-list)."],
274275 ["GET, POST /-/unsubscribe/{token}", "One-click unsubscribe from a notification email (RFC 8058): a POST with `List-Unsubscribe=One-Click` turns that kind of email off. See [Notifications](/docs/accounts/notifications)."],
modifiedapps/diffinery/src/env.ts+5−0View file
@@ -91,6 +91,11 @@
9191 * separated by commas.
9292 */
9393 SECURITY_CONTACT?: string;
94+ /**
95+ * The proof record served at /.well-known/mcp-registry-auth (`v=MCPv1; k=ed25519; p=...`), so the
96+ * instance's MCP server can be published to the official MCP registry under its own domain.
97+ */
98+ MCP_REGISTRY_AUTH?: string;
9499 /** Comma-separated emails that become site admins on first sign-in (logins are ignored). Deployed by scripts/bootstrap-admins.sh. */
95100 BOOTSTRAP_ADMINS: string;
96101 /** e.g. example.cloudflareaccess.com — empty disables Access verification. */
addedapps/diffinery/src/http/mcp-registry-auth.ts+36−0View file
@@ -1,0 +1,36 @@
1+import { Hono } from "hono";
2+import type { AppVars } from "../auth/principal.js";
3+import type { Env } from "../env.js";
4+
5+/**
6+ * /.well-known/mcp-registry-auth: the proof record that lets an instance's owner publish its MCP
7+ * server to the official MCP registry (registry.modelcontextprotocol.io) under the instance's own
8+ * domain, such as com.diffinery/diffinery. The registry fetches this file and checks that the key
9+ * `mcp-publisher login http` signs with is the one named here.
10+ *
11+ * The record comes from MCP_REGISTRY_AUTH, or for diffinery.com from PROOFS below, so the project's
12+ * own key is never served by another instance (it would let that key publish under that instance's
13+ * domain). Anything that isn't one well-formed record is a 404, like an unset one.
14+ */
15+
16+/** Built-in records by host. Rotating a key means changing it here; the private key stays with whoever publishes. */
17+const PROOFS: Record<string, string> = {
18+ "diffinery.com": "v=MCPv1; k=ed25519; p=HbHhQYSP5tE1guSK2Df9MqxTJ8F+ojzjAesiFpgABTU=",
19+};
20+
21+const RECORD = /^v=MCPv1; k=(ed25519|ecdsap384); p=[A-Za-z0-9+/]+={0,2}$/;
22+
23+/** This instance's proof record, or null when it has none. */
24+export function mcpRegistryAuth(env: Pick<Env, "PUBLIC_URL" | "MCP_REGISTRY_AUTH">): string | null {
25+ const own = env.MCP_REGISTRY_AUTH?.trim();
26+ if (own) return RECORD.test(own) ? own : null;
27+ return PROOFS[new URL(env.PUBLIC_URL).hostname] ?? null;
28+}
29+
30+export const mcpRegistryAuthRoutes = new Hono<{ Bindings: Env; Variables: AppVars }>();
31+
32+mcpRegistryAuthRoutes.get("/.well-known/mcp-registry-auth", (c) => {
33+ const record = mcpRegistryAuth(c.env);
34+ if (!record) return c.text("Not found", 404);
35+ return c.body(`${record}\n`, 200, { "content-type": "text/plain; charset=utf-8", "cache-control": "public, max-age=300" });
36+});
addedapps/diffinery/test/unit/mcp-registry-auth.test.ts+20−0View file
@@ -1,0 +1,20 @@
1+import { describe, expect, it } from "vitest";
2+import { mcpRegistryAuth } from "../../src/http/mcp-registry-auth.js";
3+
4+describe("mcp-registry-auth", () => {
5+ const record = "v=MCPv1; k=ed25519; p=HbHhQYSP5tE1guSK2Df9MqxTJ8F+ojzjAesiFpgABTU=";
6+
7+ it("serves the instance's own record, and nothing when it is missing or malformed", () => {
8+ expect(mcpRegistryAuth({ PUBLIC_URL: "https://git.example.com", MCP_REGISTRY_AUTH: ` ${record} ` })).toBe(record);
9+ expect(mcpRegistryAuth({ PUBLIC_URL: "https://git.example.com", MCP_REGISTRY_AUTH: "v=MCPv1; k=ecdsap384; p=AbC+/9==" })).toBe("v=MCPv1; k=ecdsap384; p=AbC+/9==");
10+ for (const MCP_REGISTRY_AUTH of [undefined, "", " ", "v=MCPv1; k=rsa; p=AAAA", "v=MCPv1; k=ed25519; p=", `${record}\nextra`, "<html>"]) {
11+ expect(mcpRegistryAuth({ PUBLIC_URL: "https://git.example.com", MCP_REGISTRY_AUTH })).toBeNull();
12+ }
13+ });
14+
15+ it("serves diffinery.com's built-in record only on diffinery.com", () => {
16+ expect(mcpRegistryAuth({ PUBLIC_URL: "https://diffinery.com" })).toMatch(/^v=MCPv1; k=ed25519; p=[A-Za-z0-9+/]{43}=$/);
17+ expect(mcpRegistryAuth({ PUBLIC_URL: "https://diffinery.com.evil.example" })).toBeNull();
18+ expect(mcpRegistryAuth({ PUBLIC_URL: "http://localhost:8787" })).toBeNull();
19+ });
20+});