Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
239 linesCodeBlameRaw

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.

Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships1//! MCP over streamable HTTP. The server keeps no session state, so every
2//! POST is answered directly with JSON.
3
4use g1t_contracts::{Outcome, Viewer};
5use serde_json::{Value, json};
6use worker::{Method, Request, Response, Result};
7
8use crate::operations::{Op, Services};
9use crate::tools::{Gate, TOOLS, Tool};
10
11const 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.13const 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'14Tools 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.
Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships15Find a repository: account whoami lists your workspaces; repository list or search finds one.
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'16Work 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.
17Coordinate: 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'18Record 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.
19Merge 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.20Memory: 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 bar21Security: 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.22Production: observability overview and errors show what a project's running software did; follow a request or trace to its logs.";
Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships23
24fn result(id: &Value, value: Value) -> Value {
25 json!({ "jsonrpc": "2.0", "id": id, "result": value })
26}
27
28fn 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
Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships33/// access token's scopes, or nothing beyond the person's role.
34fn 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.
45async 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 = &params["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)) {
Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships93 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()))?,
Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships105 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.
121fn card(addresses: &crate::addresses::Addresses) -> Value {
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.",
143 "endpoint": addresses.mcp,
144 "transport": "streamable-http",
145 "protocol_versions": SUPPORTED_VERSIONS,
Merge branch 'worktree-agent-ab3c375781462035f'146 // For Claude Code, g1t's plugin comes first: it adds this server
147 // itself (as plugin:g1t:g1t), so `connect` is for Claude Code
148 // without the plugin; with both, every tool is listed twice.
Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships149 "connect": format!("claude mcp add --transport http g1t {}", addresses.mcp),
Merge branch 'worktree-agent-ab3c375781462035f'150 "claude_code_plugin": {
151 "install": [
152 "claude plugin marketplace add https://g1t.sh/flagon-io/g1t.git",
153 "claude plugin install g1t@g1t",
154 ],
155 "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.",
156 "documentation_url": "https://docs.g1t.sh/guides/claude-code-plugin/",
157 },
Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships158 "authorization": {
159 "required": true,
160 "oauth_protected_resource": addresses.protected_resource(),
161 "alternative": "Authorization: Bearer <g1t access token>",
162 },
163 "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/",
164 "instructions": INSTRUCTIONS,
165 "tools": tools,
166 })
167}
168
169pub async fn handle(
170 mut request: Request,
171 services: &Services,
172 viewer: &Viewer,
173) -> Result<Response> {
174 if request.method() != Method::Post {
175 // A client asking for a stream of server messages is told there is
176 // none. Anyone else, a person with a browser, gets a description.
177 let wants_stream = request
178 .headers()
179 .get("accept")?
180 .is_some_and(|accept| accept.contains("text/event-stream"));
181 if request.method() == Method::Get && !wants_stream {
182 return Response::from_json(&card(&services.addresses));
183 }
184 let mut response = Response::empty()?.with_status(405);
185 response.headers_mut().set("allow", "GET, POST")?;
186 return Ok(response);
187 }
188 // Calls need a signed-in user. Answering 401 with this header is what
189 // makes a client open the browser to sign in.
190 if viewer.is_none() {
191 let mut response = Response::from_json(&error(
192 &Value::Null,
193 -32001,
194 "Sign in to use the g1t MCP server.",
195 ))?
196 .with_status(401);
197 response
198 .headers_mut()
199 .set("www-authenticate", &services.addresses.mcp_challenge())?;
200 return Ok(response);
201 }
202 let Ok(body) = request.json::<Value>().await else {
203 return Ok(
204 Response::from_json(&error(&Value::Null, -32700, "Parse error"))?.with_status(400),
205 );
206 };
207 let accepted = || Ok(Response::empty()?.with_status(202));
208 match body {
209 Value::Array(batch) => {
210 let mut answers = Vec::new();
211 for request in &batch {
212 answers.extend(answer(services, viewer, request).await?);
213 }
214 if answers.is_empty() {
215 accepted()
216 } else {
217 Response::from_json(&answers)
218 }
219 }
220 single => match answer(services, viewer, &single).await? {
221 Some(answer) => Response::from_json(&answer),
222 None => accepted(),
223 },
224 }
225}
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.226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 /// The instructions name every tool, in the order `tools/list` gives
232 /// them, so that no tool goes unmentioned.
233 #[test]
234 fn the_instructions_list_every_tool() {
235 let names: Vec<&str> = TOOLS.iter().map(|tool| tool.name).collect();
236 let listed = format!("each with an `action`: {}.", names.join(", "));
237 assert!(INSTRUCTIONS.contains(&listed), "{listed}");
238 }
239}

This file's history is long; its oldest lines are credited to the oldest commit read.