Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| API and MCP server in Rust; a public index at the API root | 1 | //! MCP over streamable HTTP. The server keeps no session state, so every |
| 2 | //! POST is answered directly with JSON. | |
| 3 | ||
| 4 | use g1t_contracts::{Outcome, Viewer}; | |
| 5 | use serde_json::{Value, json}; | |
| 6 | use worker::{Method, Request, Response, Result}; | |
| 7 | ||
| 8 | use crate::oauth::MCP_CHALLENGE; | |
| 9 | use crate::operations::{Op, Services}; | |
| 10 | ||
| 11 | const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"]; | |
| 12 | ||
| 13 | const INSTRUCTIONS: &str = "g1t is a git forge with issues and pull requests, built so that many agents can work on the same issue at once. | |
| 14 | To work on an issue: get_issue to read it and see the pull requests already made for it, then create_pull_request with the issue's number. You get a draft pull request with its own fork to clone and push to. Call record_session as you work so people can see your reasoning, push your commits, and call mark_pull_request_ready with a summary. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 15 | Before going far, read `overlaps` on get_pull_request: other pull requests in progress that change the same files. One for a different issue will conflict with yours, so narrow your change or say so. `behind` means main has moved; pull it into your fork and push. Once your pull request is ready, the issue's acceptance checks are run for you in a clean sandbox; read their output from get_pull_request and push a fix if they fail. |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 16 | Before you start, recall what the project and its workspace remember; when you learn something the next agent would need, remember it (scope project for this codebase, workspace for what holds across projects). Never a secret. |
| API and MCP server in Rust; a public index at the API root | 17 | Issues and pull requests are named by repository (\"owner/name\") and number, and share one sequence of numbers."; |
| 18 | ||
| 19 | fn result(id: &Value, value: Value) -> Value { | |
| 20 | json!({ "jsonrpc": "2.0", "id": id, "result": value }) | |
| 21 | } | |
| 22 | ||
| 23 | fn error(id: &Value, code: i32, message: &str) -> Value { | |
| 24 | json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }) | |
| 25 | } | |
| 26 | ||
| 27 | /// Answers one JSON-RPC request, or `None` for a notification. | |
| 28 | async fn answer(services: &Services, viewer: &Viewer, request: &Value) -> Result<Option<Value>> { | |
| 29 | // Notifications carry no id and get no response. | |
| 30 | let Some(id) = request.get("id") else { | |
| 31 | return Ok(None); | |
| 32 | }; | |
| 33 | let params = &request["params"]; | |
| 34 | let answer = match request["method"].as_str().unwrap_or_default() { | |
| 35 | "initialize" => { | |
| 36 | let requested = params["protocolVersion"].as_str().unwrap_or_default(); | |
| 37 | let version = SUPPORTED_VERSIONS | |
| 38 | .into_iter() | |
| 39 | .find(|version| *version == requested) | |
| 40 | .unwrap_or(SUPPORTED_VERSIONS[0]); | |
| 41 | result( | |
| 42 | id, | |
| 43 | json!({ | |
| 44 | "protocolVersion": version, | |
| 45 | "capabilities": { "tools": {} }, | |
| 46 | "serverInfo": { "name": "g1t", "version": "0.1.0" }, | |
| 47 | "instructions": INSTRUCTIONS, | |
| 48 | }), | |
| 49 | ) | |
| 50 | } | |
| 51 | "ping" => result(id, json!({})), | |
| 52 | "tools/list" => { | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 53 | // An agent sees only the tools its token may use. |
| API and MCP server in Rust; a public index at the API root | 54 | let tools: Vec<Value> = Op::ALL |
| 55 | .into_iter() | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 56 | .filter(|op| services.scope.as_ref().is_none_or(|scope| op.allowed_by(scope))) |
| API and MCP server in Rust; a public index at the API root | 57 | .map(|op| { |
| 58 | json!({ | |
| 59 | "name": op.name(), | |
| 60 | "description": op.description(), | |
| 61 | "inputSchema": op.input(), | |
| 62 | }) | |
| 63 | }) | |
| 64 | .collect(); | |
| 65 | result(id, json!({ "tools": tools })) | |
| 66 | } | |
| 67 | "tools/call" => { | |
| 68 | let Some(op) = Op::by_name(params["name"].as_str().unwrap_or_default()) else { | |
| 69 | return Ok(Some(error(id, -32602, "Unknown tool."))); | |
| 70 | }; | |
| 71 | let outcome = op.run(services, viewer, ¶ms["arguments"]).await?; | |
| 72 | // A failed operation is a tool result the model can read and | |
| 73 | // act on, not a protocol error. | |
| 74 | let (text, failed) = match outcome { | |
| 75 | Outcome::Ok(value) => (serde_json::to_string_pretty(&value)?, false), | |
| 76 | Outcome::Fail(failure) => (failure.message, true), | |
| 77 | }; | |
| 78 | result( | |
| 79 | id, | |
| 80 | json!({ "content": [{ "type": "text", "text": text }], "isError": failed }), | |
| 81 | ) | |
| 82 | } | |
| 83 | method => error(id, -32601, &format!("Method not found: {method}")), | |
| 84 | }; | |
| 85 | Ok(Some(answer)) | |
| 86 | } | |
| 87 | ||
| 88 | /// What someone sees when they open the server's address in a browser: | |
| 89 | /// what this is, how to connect, and what it offers. | |
| 90 | fn card() -> Value { | |
| 91 | let tools: Vec<Value> = Op::ALL | |
| 92 | .into_iter() | |
| 93 | .map(|op| json!({ "name": op.name(), "description": op.description() })) | |
| 94 | .collect(); | |
| 95 | json!({ | |
| 96 | "name": "g1t", | |
| 97 | "description": "The g1t MCP server: issues, pull requests and sessions for agents.", | |
| 98 | "endpoint": "https://mcp.g1t.sh", | |
| 99 | "transport": "streamable-http", | |
| 100 | "protocol_versions": SUPPORTED_VERSIONS, | |
| 101 | "connect": "claude mcp add --transport http g1t https://mcp.g1t.sh", | |
| 102 | "authorization": { | |
| 103 | "required": true, | |
| 104 | "oauth_protected_resource": "https://mcp.g1t.sh/.well-known/oauth-protected-resource", | |
| 105 | "alternative": "Authorization: Bearer <g1t access token>", | |
| 106 | }, | |
| 107 | "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/", | |
| 108 | "instructions": INSTRUCTIONS, | |
| 109 | "tools": tools, | |
| 110 | }) | |
| 111 | } | |
| 112 | ||
| 113 | pub async fn handle( | |
| 114 | mut request: Request, | |
| 115 | services: &Services, | |
| 116 | viewer: &Viewer, | |
| 117 | ) -> Result<Response> { | |
| 118 | if request.method() != Method::Post { | |
| 119 | // A client asking for a stream of server messages is told there is | |
| 120 | // none. Anyone else, a person with a browser, gets a description. | |
| 121 | let wants_stream = request | |
| 122 | .headers() | |
| 123 | .get("accept")? | |
| 124 | .is_some_and(|accept| accept.contains("text/event-stream")); | |
| 125 | if request.method() == Method::Get && !wants_stream { | |
| 126 | return Response::from_json(&card()); | |
| 127 | } | |
| 128 | let mut response = Response::empty()?.with_status(405); | |
| 129 | response.headers_mut().set("allow", "GET, POST")?; | |
| 130 | return Ok(response); | |
| 131 | } | |
| 132 | // Calls need a signed-in user. Answering 401 with this header is what | |
| 133 | // makes a client open the browser to sign in. | |
| 134 | if viewer.is_none() { | |
| 135 | let mut response = Response::from_json(&error( | |
| 136 | &Value::Null, | |
| 137 | -32001, | |
| 138 | "Sign in to use the g1t MCP server.", | |
| 139 | ))? | |
| 140 | .with_status(401); | |
| 141 | response | |
| 142 | .headers_mut() | |
| 143 | .set("www-authenticate", MCP_CHALLENGE)?; | |
| 144 | return Ok(response); | |
| 145 | } | |
| 146 | let Ok(body) = request.json::<Value>().await else { | |
| 147 | return Ok( | |
| 148 | Response::from_json(&error(&Value::Null, -32700, "Parse error"))?.with_status(400), | |
| 149 | ); | |
| 150 | }; | |
| 151 | let accepted = || Ok(Response::empty()?.with_status(202)); | |
| 152 | match body { | |
| 153 | Value::Array(batch) => { | |
| 154 | let mut answers = Vec::new(); | |
| 155 | for request in &batch { | |
| 156 | answers.extend(answer(services, viewer, request).await?); | |
| 157 | } | |
| 158 | if answers.is_empty() { | |
| 159 | accepted() | |
| 160 | } else { | |
| 161 | Response::from_json(&answers) | |
| 162 | } | |
| 163 | } | |
| 164 | single => match answer(services, viewer, &single).await? { | |
| 165 | Some(answer) => Response::from_json(&answer), | |
| 166 | None => accepted(), | |
| 167 | }, | |
| 168 | } | |
| 169 | } |