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::operations::{Op, Services}; | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 9 | use crate::tools::{Gate, TOOLS, Tool}; |
| API and MCP server in Rust; a public index at the API root | 10 | |
| 11 | const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"]; | |
| 12 | ||
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 13 | const INSTRUCTIONS: &str = "g1t is a git forge where people and agents work through issues and pull requests. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers. |
| 14 | Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, webhook, access, workspace, account. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs. | |
| 15 | Find a repository: account whoami lists your workspaces; repository list or search finds one. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 16 | Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again. |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 17 | Hand work to g1t's agent: agent delegate opens an issue and starts it in one step; agent assign starts it on an existing issue. Each costs the workspace money. |
| 18 | When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret."; | |
| API and MCP server in Rust; a public index at the API root | 19 | |
| 20 | fn result(id: &Value, value: Value) -> Value { | |
| 21 | json!({ "jsonrpc": "2.0", "id": id, "result": value }) | |
| 22 | } | |
| 23 | ||
| 24 | fn error(id: &Value, code: i32, message: &str) -> Value { | |
| 25 | json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }) | |
| 26 | } | |
| 27 | ||
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 28 | /// What decides the actions a caller sees: an agent's run scope, an |
| 29 | /// access token's scopes, or nothing beyond the person's role. | |
| 30 | fn gate<'a>(services: &'a Services, viewer: &'a Viewer) -> Gate<'a> { | |
| 31 | if let Some(scope) = &services.scope { | |
| 32 | return Gate::Agent(scope); | |
| 33 | } | |
| 34 | match viewer.as_ref().and_then(|user| user.token.as_deref()) { | |
| 35 | Some(access) => Gate::Token(access), | |
| 36 | None => Gate::Everything, | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| API and MCP server in Rust; a public index at the API root | 40 | /// Answers one JSON-RPC request, or `None` for a notification. |
| 41 | async fn answer(services: &Services, viewer: &Viewer, request: &Value) -> Result<Option<Value>> { | |
| 42 | // Notifications carry no id and get no response. | |
| 43 | let Some(id) = request.get("id") else { | |
| 44 | return Ok(None); | |
| 45 | }; | |
| 46 | let params = &request["params"]; | |
| 47 | let answer = match request["method"].as_str().unwrap_or_default() { | |
| 48 | "initialize" => { | |
| 49 | let requested = params["protocolVersion"].as_str().unwrap_or_default(); | |
| 50 | let version = SUPPORTED_VERSIONS | |
| 51 | .into_iter() | |
| 52 | .find(|version| *version == requested) | |
| 53 | .unwrap_or(SUPPORTED_VERSIONS[0]); | |
| 54 | result( | |
| 55 | id, | |
| 56 | json!({ | |
| 57 | "protocolVersion": version, | |
| 58 | "capabilities": { "tools": {} }, | |
| 59 | "serverInfo": { "name": "g1t", "version": "0.1.0" }, | |
| 60 | "instructions": INSTRUCTIONS, | |
| 61 | }), | |
| 62 | ) | |
| 63 | } | |
| 64 | "ping" => result(id, json!({})), | |
| 65 | "tools/list" => { | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 66 | // A caller sees the tools, and the actions of each, that its |
| 67 | // token may use. | |
| 68 | let gate = gate(services, viewer); | |
| 69 | let tools: Vec<Value> = TOOLS.iter().filter_map(|tool| tool.listed(&gate)).collect(); | |
| API and MCP server in Rust; a public index at the API root | 70 | result(id, json!({ "tools": tools })) |
| 71 | } | |
| 72 | "tools/call" => { | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 73 | let name = params["name"].as_str().unwrap_or_default(); |
| 74 | let arguments = ¶ms["arguments"]; | |
| 75 | let op = match Tool::by_name(name) { | |
| 76 | Some(tool) => match crate::tools::resolve(tool, arguments) { | |
| 77 | Ok(op) => op, | |
| 78 | Err(problem) => { | |
| 79 | return Ok(Some(result( | |
| 80 | id, | |
| 81 | json!({ "content": [{ "type": "text", "text": problem }], "isError": true }), | |
| 82 | ))); | |
| 83 | } | |
| 84 | }, | |
| 85 | // A tool per operation, as the server had before its | |
| 86 | // resource tools. Still answered, no longer listed. | |
| 87 | None => match Op::by_name(name) { | |
| 88 | Some(op) => op, | |
| 89 | None => return Ok(Some(error(id, -32602, "Unknown tool."))), | |
| 90 | }, | |
| API and MCP server in Rust; a public index at the API root | 91 | }; |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 92 | let outcome = crate::audit::run(op, services, viewer, arguments).await?; |
| API and MCP server in Rust; a public index at the API root | 93 | // A failed operation is a tool result the model can read and |
| 94 | // act on, not a protocol error. | |
| 95 | let (text, failed) = match outcome { | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 96 | // In `snake_case`, as the REST API answers; the protocol's |
| 97 | // own envelope keeps MCP's spelling. | |
| 98 | Outcome::Ok(value) => ( | |
| 99 | serde_json::to_string_pretty(&g1t_kit::wire::snake_case(value))?, | |
| 100 | false, | |
| 101 | ), | |
| API and MCP server in Rust; a public index at the API root | 102 | Outcome::Fail(failure) => (failure.message, true), |
| 103 | }; | |
| 104 | result( | |
| 105 | id, | |
| 106 | json!({ "content": [{ "type": "text", "text": text }], "isError": failed }), | |
| 107 | ) | |
| 108 | } | |
| 109 | method => error(id, -32601, &format!("Method not found: {method}")), | |
| 110 | }; | |
| 111 | Ok(Some(answer)) | |
| 112 | } | |
| 113 | ||
| 114 | /// What someone sees when they open the server's address in a browser: | |
| 115 | /// what this is, how to connect, and what it offers. | |
| Self-hosting: the clone pack cache on S3 (MinIO, expiring), the API and MCP on their own port with PUBLIC_URL-derived addresses across the site, API and mail, scheduler --once, and smoke.sh covering pull requests from branches and forks and the merge queue | 116 | fn card(addresses: &crate::addresses::Addresses) -> Value { |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 117 | let tools: Vec<Value> = TOOLS |
| 118 | .iter() | |
| 119 | .map(|tool| { | |
| 120 | let actions: Vec<&crate::tools::Action> = tool.actions.iter().collect(); | |
| 121 | json!({ | |
| 122 | "name": tool.name, | |
| 123 | "title": tool.title, | |
| 124 | "description": tool.description, | |
| 125 | "actions": tool.actions.iter().map(|action| json!({ | |
| 126 | "name": action.name, | |
| 127 | "description": action.summary, | |
| 128 | "operation": action.op.name(), | |
| 129 | "scope": g1t_contracts::scopes::scope_for(action.op.name()).map(|scope| scope.as_str()), | |
| 130 | })).collect::<Vec<_>>(), | |
| 131 | "input_schema": tool.discriminated(&actions), | |
| 132 | }) | |
| 133 | }) | |
| API and MCP server in Rust; a public index at the API root | 134 | .collect(); |
| 135 | json!({ | |
| 136 | "name": "g1t", | |
| 137 | "description": "The g1t MCP server: issues, pull requests and sessions for agents.", | |
| Self-hosting: the clone pack cache on S3 (MinIO, expiring), the API and MCP on their own port with PUBLIC_URL-derived addresses across the site, API and mail, scheduler --once, and smoke.sh covering pull requests from branches and forks and the merge queue | 138 | "endpoint": addresses.mcp, |
| API and MCP server in Rust; a public index at the API root | 139 | "transport": "streamable-http", |
| 140 | "protocol_versions": SUPPORTED_VERSIONS, | |
| Self-hosting: the clone pack cache on S3 (MinIO, expiring), the API and MCP on their own port with PUBLIC_URL-derived addresses across the site, API and mail, scheduler --once, and smoke.sh covering pull requests from branches and forks and the merge queue | 141 | "connect": format!("claude mcp add --transport http g1t {}", addresses.mcp), |
| API and MCP server in Rust; a public index at the API root | 142 | "authorization": { |
| 143 | "required": true, | |
| Self-hosting: the clone pack cache on S3 (MinIO, expiring), the API and MCP on their own port with PUBLIC_URL-derived addresses across the site, API and mail, scheduler --once, and smoke.sh covering pull requests from branches and forks and the merge queue | 144 | "oauth_protected_resource": addresses.protected_resource(), |
| API and MCP server in Rust; a public index at the API root | 145 | "alternative": "Authorization: Bearer <g1t access token>", |
| 146 | }, | |
| 147 | "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/", | |
| 148 | "instructions": INSTRUCTIONS, | |
| 149 | "tools": tools, | |
| 150 | }) | |
| 151 | } | |
| 152 | ||
| 153 | pub async fn handle( | |
| 154 | mut request: Request, | |
| 155 | services: &Services, | |
| 156 | viewer: &Viewer, | |
| 157 | ) -> Result<Response> { | |
| 158 | if request.method() != Method::Post { | |
| 159 | // A client asking for a stream of server messages is told there is | |
| 160 | // none. Anyone else, a person with a browser, gets a description. | |
| 161 | let wants_stream = request | |
| 162 | .headers() | |
| 163 | .get("accept")? | |
| 164 | .is_some_and(|accept| accept.contains("text/event-stream")); | |
| 165 | if request.method() == Method::Get && !wants_stream { | |
| Self-hosting: the clone pack cache on S3 (MinIO, expiring), the API and MCP on their own port with PUBLIC_URL-derived addresses across the site, API and mail, scheduler --once, and smoke.sh covering pull requests from branches and forks and the merge queue | 166 | return Response::from_json(&card(&services.addresses)); |
| API and MCP server in Rust; a public index at the API root | 167 | } |
| 168 | let mut response = Response::empty()?.with_status(405); | |
| 169 | response.headers_mut().set("allow", "GET, POST")?; | |
| 170 | return Ok(response); | |
| 171 | } | |
| 172 | // Calls need a signed-in user. Answering 401 with this header is what | |
| 173 | // makes a client open the browser to sign in. | |
| 174 | if viewer.is_none() { | |
| 175 | let mut response = Response::from_json(&error( | |
| 176 | &Value::Null, | |
| 177 | -32001, | |
| 178 | "Sign in to use the g1t MCP server.", | |
| 179 | ))? | |
| 180 | .with_status(401); | |
| 181 | response | |
| 182 | .headers_mut() | |
| Self-hosting: the clone pack cache on S3 (MinIO, expiring), the API and MCP on their own port with PUBLIC_URL-derived addresses across the site, API and mail, scheduler --once, and smoke.sh covering pull requests from branches and forks and the merge queue | 183 | .set("www-authenticate", &services.addresses.mcp_challenge())?; |
| API and MCP server in Rust; a public index at the API root | 184 | return Ok(response); |
| 185 | } | |
| 186 | let Ok(body) = request.json::<Value>().await else { | |
| 187 | return Ok( | |
| 188 | Response::from_json(&error(&Value::Null, -32700, "Parse error"))?.with_status(400), | |
| 189 | ); | |
| 190 | }; | |
| 191 | let accepted = || Ok(Response::empty()?.with_status(202)); | |
| 192 | match body { | |
| 193 | Value::Array(batch) => { | |
| 194 | let mut answers = Vec::new(); | |
| 195 | for request in &batch { | |
| 196 | answers.extend(answer(services, viewer, request).await?); | |
| 197 | } | |
| 198 | if answers.is_empty() { | |
| 199 | accepted() | |
| 200 | } else { | |
| 201 | Response::from_json(&answers) | |
| 202 | } | |
| 203 | } | |
| 204 | single => match answer(services, viewer, &single).await? { | |
| 205 | Some(answer) => Response::from_json(&answer), | |
| 206 | None => accepted(), | |
| 207 | }, | |
| 208 | } | |
| 209 | } |