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