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.
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 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}; | |
| 9 | use crate::tools::{Gate, TOOLS, Tool}; | |
| 10 | ||
| 11 | const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"]; | |
| 12 | ||
| The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused. | 13 | const INSTRUCTIONS: &str = "g1t is where your code, its history and the work around it live: repositories, issues, pull requests, workflows, deployments and packages. You bring your own agent; these tools are how it reads and changes what is on g1t, as the person or token it acts for. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers. |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 14 | Tools are resources, each with an `action`: search, repository, issue, pull_request, session, memory, workflow, observability, package, secret, webhook, access, team, workspace, billing, security, notifications, account, artifact. 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. |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 15 | Find a repository: account whoami lists your workspaces; repository list or search finds one. |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 16 | Work on an issue: issue work (everything about it: what done means, who is on it, its pull requests, what it waits on), issue claim (advisory: it tells you who else is on 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. Push your commits, 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. |
| 17 | Coordinate: session start once per run of your harness (harness_session_id makes it idempotent), session update as a heartbeat, session record your prompts and tool calls, session end when done; issue release or handoff (with a note) when you stop. | |
| Merge branch 'worktree-agent-ab8033ab973f8d99a' | 18 | Record your session: as you work, pull_request record_session appends your prompts, reasoning and the tools you ran to the pull request's session, so reviewers see why the change is what it is; pull_request read_session reads it back. pull_request record_decision keeps a decision (why the change is made this way, or what fixed a failure) and list_decisions reads them; repository why says why a line of code is the way it is. |
| 19 | Merge queue: when your pull request fails combined with the ones ahead of it, g1t says which of them it breaks with and the check that failed (on the pull request, its issue and its session, and as integration.failed). Fix it and push: once its checks pass, it rejoins the queue by itself. | |
| The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused. | 20 | Memory: memory recall before you start to learn how the project builds and what it has decided; when you learn something the next agent needs, memory remember it, one short fact at a time (scope project or workspace). Never a secret. |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 21 | Security: security code_alerts, vulnerability_alerts and secret_alerts show what to fix; fix it in your pull request, which the Code scanning and Dependency review checks judge. |
| The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused. | 22 | Production: observability overview and errors show what a project's running software did; follow a request or trace to its logs."; |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 23 | |
| 24 | fn result(id: &Value, value: Value) -> Value { | |
| 25 | json!({ "jsonrpc": "2.0", "id": id, "result": value }) | |
| 26 | } | |
| 27 | ||
| 28 | fn error(id: &Value, code: i32, message: &str) -> Value { | |
| 29 | json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }) | |
| 30 | } | |
| 31 | ||
| The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused. | 32 | /// What decides the actions a caller sees: a run's scope, an |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 33 | /// access token's scopes, or nothing beyond the person's role. |
| 34 | fn gate<'a>(services: &'a Services, viewer: &'a Viewer) -> Gate<'a> { | |
| 35 | if let Some(scope) = &services.scope { | |
| 36 | return Gate::Agent(scope); | |
| 37 | } | |
| 38 | match viewer.as_ref().and_then(|user| user.token.as_deref()) { | |
| 39 | Some(access) => Gate::Token(access), | |
| 40 | None => Gate::Everything, | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | /// Answers one JSON-RPC request, or `None` for a notification. | |
| 45 | async fn answer(services: &Services, viewer: &Viewer, request: &Value) -> Result<Option<Value>> { | |
| 46 | // Notifications carry no id and get no response. | |
| 47 | let Some(id) = request.get("id") else { | |
| 48 | return Ok(None); | |
| 49 | }; | |
| 50 | let params = &request["params"]; | |
| 51 | let answer = match request["method"].as_str().unwrap_or_default() { | |
| 52 | "initialize" => { | |
| 53 | let requested = params["protocolVersion"].as_str().unwrap_or_default(); | |
| 54 | let version = SUPPORTED_VERSIONS | |
| 55 | .into_iter() | |
| 56 | .find(|version| *version == requested) | |
| 57 | .unwrap_or(SUPPORTED_VERSIONS[0]); | |
| 58 | result( | |
| 59 | id, | |
| 60 | json!({ | |
| 61 | "protocolVersion": version, | |
| 62 | "capabilities": { "tools": {} }, | |
| 63 | "serverInfo": { "name": "g1t", "version": "0.1.0" }, | |
| 64 | "instructions": INSTRUCTIONS, | |
| 65 | }), | |
| 66 | ) | |
| 67 | } | |
| 68 | "ping" => result(id, json!({})), | |
| 69 | "tools/list" => { | |
| 70 | // A caller sees the tools, and the actions of each, that its | |
| 71 | // token may use. | |
| 72 | let gate = gate(services, viewer); | |
| 73 | let tools: Vec<Value> = TOOLS.iter().filter_map(|tool| tool.listed(&gate)).collect(); | |
| 74 | result(id, json!({ "tools": tools })) | |
| 75 | } | |
| 76 | "tools/call" => { | |
| 77 | let name = params["name"].as_str().unwrap_or_default(); | |
| 78 | let arguments = ¶ms["arguments"]; | |
| 79 | let op = match Tool::by_name(name) { | |
| 80 | Some(tool) => match crate::tools::resolve(tool, arguments) { | |
| 81 | Ok(op) => op, | |
| 82 | Err(problem) => { | |
| 83 | return Ok(Some(result( | |
| 84 | id, | |
| 85 | json!({ "content": [{ "type": "text", "text": problem }], "isError": true }), | |
| 86 | ))); | |
| 87 | } | |
| 88 | }, | |
| 89 | // A tool per operation, as the server had before its | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 90 | // resource tools. Still answered, no longer listed; never |
| 91 | // one offered over REST alone. | |
| 92 | None => match Op::by_name(name).filter(|op| !crate::tools::REST_ONLY.contains(op)) { | |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 93 | Some(op) => op, |
| 94 | None => return Ok(Some(error(id, -32602, "Unknown tool."))), | |
| 95 | }, | |
| 96 | }; | |
| 97 | let outcome = crate::audit::run(op, services, viewer, arguments).await?; | |
| 98 | // A failed operation is a tool result the model can read and | |
| 99 | // act on, not a protocol error. | |
| 100 | let (text, failed) = match outcome { | |
| 101 | // In `snake_case`, as the REST API answers; the protocol's | |
| 102 | // own envelope keeps MCP's spelling. | |
| 103 | Outcome::Ok(value) => ( | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 104 | serde_json::to_string_pretty(&g1t_kit::wire::snake_case_keeping(value, op.as_given()))?, |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 105 | false, |
| 106 | ), | |
| 107 | Outcome::Fail(failure) => (failure.message, true), | |
| 108 | }; | |
| 109 | result( | |
| 110 | id, | |
| 111 | json!({ "content": [{ "type": "text", "text": text }], "isError": failed }), | |
| 112 | ) | |
| 113 | } | |
| 114 | method => error(id, -32601, &format!("Method not found: {method}")), | |
| 115 | }; | |
| 116 | Ok(Some(answer)) | |
| 117 | } | |
| 118 | ||
| 119 | /// What someone sees when they open the server's address in a browser: | |
| 120 | /// what this is, how to connect, and what it offers. | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 121 | fn card(addresses: &crate::addresses::Addresses) -> Value { |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 122 | let tools: Vec<Value> = TOOLS |
| 123 | .iter() | |
| 124 | .map(|tool| { | |
| 125 | let actions: Vec<&crate::tools::Action> = tool.actions.iter().collect(); | |
| 126 | json!({ | |
| 127 | "name": tool.name, | |
| 128 | "title": tool.title, | |
| 129 | "description": tool.description, | |
| 130 | "actions": tool.actions.iter().map(|action| json!({ | |
| 131 | "name": action.name, | |
| 132 | "description": action.summary, | |
| 133 | "operation": action.op.name(), | |
| 134 | "scope": g1t_contracts::scopes::scope_for(action.op.name()).map(|scope| scope.as_str()), | |
| 135 | })).collect::<Vec<_>>(), | |
| 136 | "input_schema": tool.discriminated(&actions), | |
| 137 | }) | |
| 138 | }) | |
| 139 | .collect(); | |
| 140 | json!({ | |
| 141 | "name": "g1t", | |
| 142 | "description": "The g1t MCP server: issues, pull requests and sessions for agents.", | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 143 | "endpoint": addresses.mcp, |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 144 | "transport": "streamable-http", |
| 145 | "protocol_versions": SUPPORTED_VERSIONS, | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 146 | "connect": format!("claude mcp add --transport http g1t {}", addresses.mcp), |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 147 | "authorization": { |
| 148 | "required": true, | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 149 | "oauth_protected_resource": addresses.protected_resource(), |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 150 | "alternative": "Authorization: Bearer <g1t access token>", |
| 151 | }, | |
| 152 | "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/", | |
| 153 | "instructions": INSTRUCTIONS, | |
| 154 | "tools": tools, | |
| 155 | }) | |
| 156 | } | |
| 157 | ||
| 158 | pub async fn handle( | |
| 159 | mut request: Request, | |
| 160 | services: &Services, | |
| 161 | viewer: &Viewer, | |
| 162 | ) -> Result<Response> { | |
| 163 | if request.method() != Method::Post { | |
| 164 | // A client asking for a stream of server messages is told there is | |
| 165 | // none. Anyone else, a person with a browser, gets a description. | |
| 166 | let wants_stream = request | |
| 167 | .headers() | |
| 168 | .get("accept")? | |
| 169 | .is_some_and(|accept| accept.contains("text/event-stream")); | |
| 170 | if request.method() == Method::Get && !wants_stream { | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 171 | return Response::from_json(&card(&services.addresses)); |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 172 | } |
| 173 | let mut response = Response::empty()?.with_status(405); | |
| 174 | response.headers_mut().set("allow", "GET, POST")?; | |
| 175 | return Ok(response); | |
| 176 | } | |
| 177 | // Calls need a signed-in user. Answering 401 with this header is what | |
| 178 | // makes a client open the browser to sign in. | |
| 179 | if viewer.is_none() { | |
| 180 | let mut response = Response::from_json(&error( | |
| 181 | &Value::Null, | |
| 182 | -32001, | |
| 183 | "Sign in to use the g1t MCP server.", | |
| 184 | ))? | |
| 185 | .with_status(401); | |
| 186 | response | |
| 187 | .headers_mut() | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 188 | .set("www-authenticate", &services.addresses.mcp_challenge())?; |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 189 | return Ok(response); |
| 190 | } | |
| 191 | let Ok(body) = request.json::<Value>().await else { | |
| 192 | return Ok( | |
| 193 | Response::from_json(&error(&Value::Null, -32700, "Parse error"))?.with_status(400), | |
| 194 | ); | |
| 195 | }; | |
| 196 | let accepted = || Ok(Response::empty()?.with_status(202)); | |
| 197 | match body { | |
| 198 | Value::Array(batch) => { | |
| 199 | let mut answers = Vec::new(); | |
| 200 | for request in &batch { | |
| 201 | answers.extend(answer(services, viewer, request).await?); | |
| 202 | } | |
| 203 | if answers.is_empty() { | |
| 204 | accepted() | |
| 205 | } else { | |
| 206 | Response::from_json(&answers) | |
| 207 | } | |
| 208 | } | |
| 209 | single => match answer(services, viewer, &single).await? { | |
| 210 | Some(answer) => Response::from_json(&answer), | |
| 211 | None => accepted(), | |
| 212 | }, | |
| 213 | } | |
| 214 | } | |
| The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused. | 215 | |
| 216 | #[cfg(test)] | |
| 217 | mod tests { | |
| 218 | use super::*; | |
| 219 | ||
| 220 | /// The instructions name every tool, in the order `tools/list` gives | |
| 221 | /// them, so that no tool goes unmentioned. | |
| 222 | #[test] | |
| 223 | fn the_instructions_list_every_tool() { | |
| 224 | let names: Vec<&str> = TOOLS.iter().map(|tool| tool.name).collect(); | |
| 225 | let listed = format!("each with an `action`: {}.", names.join(", ")); | |
| 226 | assert!(INSTRUCTIONS.contains(&listed), "{listed}"); | |
| 227 | } | |
| 228 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.