Skip to content
238 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.

Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP1//! 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-a4babc5b5815f6f3a'14Tools 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 MCP15Find a repository: account whoami lists your workspaces; repository list or search finds one.
Merge branch 'worktree-agent-a4babc5b5815f6f3a'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), 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'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.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar20Security: 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.21Production: 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 MCP22
23fn result(id: &Value, value: Value) -> Value {
24 json!({ "jsonrpc": "2.0", "id": id, "result": value })
25}
26
27fn 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 MCP32/// access token's scopes, or nothing beyond the person's role.
33fn 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.
44async 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 = &params["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 MCP92 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 MCP104 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.
120fn 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 MCP148 "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 MCP157 "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
168pub 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)]
227mod 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.