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.
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 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-a4babc5b5815f6f3a' | 14 | Tools are resources, each with an `action`: search, repository, issue, pull_request, session, 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. |
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 15 | Find a repository: account whoami lists your workspaces; repository list or search finds one. |
| Merge branch 'worktree-agent-a4babc5b5815f6f3a' | 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), 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. |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 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. | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 20 | 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. | 21 | Production: observability overview and errors show what a project's running software did; follow a request or trace to its logs."; |
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 22 | |
| 23 | fn result(id: &Value, value: Value) -> Value { | |
| 24 | json!({ "jsonrpc": "2.0", "id": id, "result": value }) | |
| 25 | } | |
| 26 | ||
| 27 | fn error(id: &Value, code: i32, message: &str) -> Value { | |
| 28 | json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }) | |
| 29 | } | |
| 30 | ||
| 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. | 31 | /// What decides the actions a caller sees: a run's scope, an |
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 32 | /// access token's scopes, or nothing beyond the person's role. |
| 33 | fn gate<'a>(services: &'a Services, viewer: &'a Viewer) -> Gate<'a> { | |
| 34 | if let Some(scope) = &services.scope { | |
| 35 | return Gate::Agent(scope); | |
| 36 | } | |
| 37 | match viewer.as_ref().and_then(|user| user.token.as_deref()) { | |
| 38 | Some(access) => Gate::Token(access), | |
| 39 | None => Gate::Everything, | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | /// Answers one JSON-RPC request, or `None` for a notification. | |
| 44 | async fn answer(services: &Services, viewer: &Viewer, request: &Value) -> Result<Option<Value>> { | |
| 45 | // Notifications carry no id and get no response. | |
| 46 | let Some(id) = request.get("id") else { | |
| 47 | return Ok(None); | |
| 48 | }; | |
| 49 | let params = &request["params"]; | |
| 50 | let answer = match request["method"].as_str().unwrap_or_default() { | |
| 51 | "initialize" => { | |
| 52 | let requested = params["protocolVersion"].as_str().unwrap_or_default(); | |
| 53 | let version = SUPPORTED_VERSIONS | |
| 54 | .into_iter() | |
| 55 | .find(|version| *version == requested) | |
| 56 | .unwrap_or(SUPPORTED_VERSIONS[0]); | |
| 57 | result( | |
| 58 | id, | |
| 59 | json!({ | |
| 60 | "protocolVersion": version, | |
| 61 | "capabilities": { "tools": {} }, | |
| 62 | "serverInfo": { "name": "g1t", "version": "0.1.0" }, | |
| 63 | "instructions": INSTRUCTIONS, | |
| 64 | }), | |
| 65 | ) | |
| 66 | } | |
| 67 | "ping" => result(id, json!({})), | |
| 68 | "tools/list" => { | |
| 69 | // A caller sees the tools, and the actions of each, that its | |
| 70 | // token may use. | |
| 71 | let gate = gate(services, viewer); | |
| 72 | let tools: Vec<Value> = TOOLS.iter().filter_map(|tool| tool.listed(&gate)).collect(); | |
| 73 | result(id, json!({ "tools": tools })) | |
| 74 | } | |
| 75 | "tools/call" => { | |
| 76 | let name = params["name"].as_str().unwrap_or_default(); | |
| 77 | let arguments = ¶ms["arguments"]; | |
| 78 | let op = match Tool::by_name(name) { | |
| 79 | Some(tool) => match crate::tools::resolve(tool, arguments) { | |
| 80 | Ok(op) => op, | |
| 81 | Err(problem) => { | |
| 82 | return Ok(Some(result( | |
| 83 | id, | |
| 84 | json!({ "content": [{ "type": "text", "text": problem }], "isError": true }), | |
| 85 | ))); | |
| 86 | } | |
| 87 | }, | |
| 88 | // 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. | 89 | // resource tools. Still answered, no longer listed; never |
| 90 | // one offered over REST alone. | |
| 91 | None => match Op::by_name(name).filter(|op| !crate::tools::REST_ONLY.contains(op)) { | |
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 92 | Some(op) => op, |
| 93 | None => return Ok(Some(error(id, -32602, "Unknown tool."))), | |
| 94 | }, | |
| 95 | }; | |
| 96 | let outcome = crate::audit::run(op, services, viewer, arguments).await?; | |
| 97 | // A failed operation is a tool result the model can read and | |
| 98 | // act on, not a protocol error. | |
| 99 | let (text, failed) = match outcome { | |
| 100 | // In `snake_case`, as the REST API answers; the protocol's | |
| 101 | // own envelope keeps MCP's spelling. | |
| 102 | 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. | 103 | serde_json::to_string_pretty(&g1t_kit::wire::snake_case_keeping(value, op.as_given()))?, |
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 104 | false, |
| 105 | ), | |
| 106 | Outcome::Fail(failure) => (failure.message, true), | |
| 107 | }; | |
| 108 | result( | |
| 109 | id, | |
| 110 | json!({ "content": [{ "type": "text", "text": text }], "isError": failed }), | |
| 111 | ) | |
| 112 | } | |
| 113 | method => error(id, -32601, &format!("Method not found: {method}")), | |
| 114 | }; | |
| 115 | Ok(Some(answer)) | |
| 116 | } | |
| 117 | ||
| 118 | /// What someone sees when they open the server's address in a browser: | |
| 119 | /// what this is, how to connect, and what it offers. | |
| 120 | fn card(addresses: &crate::addresses::Addresses) -> Value { | |
| 121 | let tools: Vec<Value> = TOOLS | |
| 122 | .iter() | |
| 123 | .map(|tool| { | |
| 124 | let actions: Vec<&crate::tools::Action> = tool.actions.iter().collect(); | |
| 125 | json!({ | |
| 126 | "name": tool.name, | |
| 127 | "title": tool.title, | |
| 128 | "description": tool.description, | |
| 129 | "actions": tool.actions.iter().map(|action| json!({ | |
| 130 | "name": action.name, | |
| 131 | "description": action.summary, | |
| 132 | "operation": action.op.name(), | |
| 133 | "scope": g1t_contracts::scopes::scope_for(action.op.name()).map(|scope| scope.as_str()), | |
| 134 | })).collect::<Vec<_>>(), | |
| 135 | "input_schema": tool.discriminated(&actions), | |
| 136 | }) | |
| 137 | }) | |
| 138 | .collect(); | |
| 139 | json!({ | |
| 140 | "name": "g1t", | |
| 141 | "description": "The g1t MCP server: issues, pull requests and sessions for agents.", | |
| 142 | "endpoint": addresses.mcp, | |
| 143 | "transport": "streamable-http", | |
| 144 | "protocol_versions": SUPPORTED_VERSIONS, | |
| Merge branch 'worktree-agent-ab3c375781462035f' | 145 | // For Claude Code, g1t's plugin comes first: it adds this server |
| 146 | // itself (as plugin:g1t:g1t), so `connect` is for Claude Code | |
| 147 | // without the plugin; with both, every tool is listed twice. | |
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 148 | "connect": format!("claude mcp add --transport http g1t {}", addresses.mcp), |
| Merge branch 'worktree-agent-ab3c375781462035f' | 149 | "claude_code_plugin": { |
| 150 | "install": [ | |
| 151 | "claude plugin marketplace add https://g1t.sh/flagon-io/g1t.git", | |
| 152 | "claude plugin install g1t@g1t", | |
| 153 | ], | |
| 154 | "note": "The plugin adds this server (listed in /mcp as plugin:g1t:g1t) and records your sessions on g1t. With it installed, skip `connect`, or each tool is listed twice.", | |
| 155 | "documentation_url": "https://docs.g1t.sh/guides/claude-code-plugin/", | |
| 156 | }, | |
| Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP | 157 | "authorization": { |
| 158 | "required": true, | |
| 159 | "oauth_protected_resource": addresses.protected_resource(), | |
| 160 | "alternative": "Authorization: Bearer <g1t access token>", | |
| 161 | }, | |
| 162 | "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/", | |
| 163 | "instructions": INSTRUCTIONS, | |
| 164 | "tools": tools, | |
| 165 | }) | |
| 166 | } | |
| 167 | ||
| 168 | pub async fn handle( | |
| 169 | mut request: Request, | |
| 170 | services: &Services, | |
| 171 | viewer: &Viewer, | |
| 172 | ) -> Result<Response> { | |
| 173 | if request.method() != Method::Post { | |
| 174 | // A client asking for a stream of server messages is told there is | |
| 175 | // none. Anyone else, a person with a browser, gets a description. | |
| 176 | let wants_stream = request | |
| 177 | .headers() | |
| 178 | .get("accept")? | |
| 179 | .is_some_and(|accept| accept.contains("text/event-stream")); | |
| 180 | if request.method() == Method::Get && !wants_stream { | |
| 181 | return Response::from_json(&card(&services.addresses)); | |
| 182 | } | |
| 183 | let mut response = Response::empty()?.with_status(405); | |
| 184 | response.headers_mut().set("allow", "GET, POST")?; | |
| 185 | return Ok(response); | |
| 186 | } | |
| 187 | // Calls need a signed-in user. Answering 401 with this header is what | |
| 188 | // makes a client open the browser to sign in. | |
| 189 | if viewer.is_none() { | |
| 190 | let mut response = Response::from_json(&error( | |
| 191 | &Value::Null, | |
| 192 | -32001, | |
| 193 | "Sign in to use the g1t MCP server.", | |
| 194 | ))? | |
| 195 | .with_status(401); | |
| 196 | response | |
| 197 | .headers_mut() | |
| 198 | .set("www-authenticate", &services.addresses.mcp_challenge())?; | |
| 199 | return Ok(response); | |
| 200 | } | |
| 201 | let Ok(body) = request.json::<Value>().await else { | |
| 202 | return Ok( | |
| 203 | Response::from_json(&error(&Value::Null, -32700, "Parse error"))?.with_status(400), | |
| 204 | ); | |
| 205 | }; | |
| 206 | let accepted = || Ok(Response::empty()?.with_status(202)); | |
| 207 | match body { | |
| 208 | Value::Array(batch) => { | |
| 209 | let mut answers = Vec::new(); | |
| 210 | for request in &batch { | |
| 211 | answers.extend(answer(services, viewer, request).await?); | |
| 212 | } | |
| 213 | if answers.is_empty() { | |
| 214 | accepted() | |
| 215 | } else { | |
| 216 | Response::from_json(&answers) | |
| 217 | } | |
| 218 | } | |
| 219 | single => match answer(services, viewer, &single).await? { | |
| 220 | Some(answer) => Response::from_json(&answer), | |
| 221 | None => accepted(), | |
| 222 | }, | |
| 223 | } | |
| 224 | } | |
| 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. | 225 | |
| 226 | #[cfg(test)] | |
| 227 | mod tests { | |
| 228 | use super::*; | |
| 229 | ||
| 230 | /// The instructions name every tool, in the order `tools/list` gives | |
| 231 | /// them, so that no tool goes unmentioned. | |
| 232 | #[test] | |
| 233 | fn the_instructions_list_every_tool() { | |
| 234 | let names: Vec<&str> = TOOLS.iter().map(|tool| tool.name).collect(); | |
| 235 | let listed = format!("each with an `action`: {}.", names.join(", ")); | |
| 236 | assert!(INSTRUCTIONS.contains(&listed), "{listed}"); | |
| 237 | } | |
| 238 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.