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

g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1//! 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
13const INSTRUCTIONS: &str = "g1t is a git forge where people and agents work through issues and pull requests. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r914Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, security, webhook, access, workspace, customize, account, notifications. 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.
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights15Find a repository: account whoami lists your workspaces; repository list or search finds one.
16Work 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. Record your reasoning with pull_request record_session as you go, push, 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.
17Hand work to g1t's agent: agent delegate opens an issue and starts it in one step; agent assign starts it on an existing issue. Each costs the workspace money.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar18Security: 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.
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights19When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret.";
20
21fn result(id: &Value, value: Value) -> Value {
22 json!({ "jsonrpc": "2.0", "id": id, "result": value })
23}
24
25fn error(id: &Value, code: i32, message: &str) -> Value {
26 json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
27}
28
29/// What decides the actions a caller sees: an agent's run scope, an
30/// access token's scopes, or nothing beyond the person's role.
31fn gate<'a>(services: &'a Services, viewer: &'a Viewer) -> Gate<'a> {
32 if let Some(scope) = &services.scope {
33 return Gate::Agent(scope);
34 }
35 match viewer.as_ref().and_then(|user| user.token.as_deref()) {
36 Some(access) => Gate::Token(access),
37 None => Gate::Everything,
38 }
39}
40
41/// Answers one JSON-RPC request, or `None` for a notification.
42async fn answer(services: &Services, viewer: &Viewer, request: &Value) -> Result<Option<Value>> {
43 // Notifications carry no id and get no response.
44 let Some(id) = request.get("id") else {
45 return Ok(None);
46 };
47 let params = &request["params"];
48 let answer = match request["method"].as_str().unwrap_or_default() {
49 "initialize" => {
50 let requested = params["protocolVersion"].as_str().unwrap_or_default();
51 let version = SUPPORTED_VERSIONS
52 .into_iter()
53 .find(|version| *version == requested)
54 .unwrap_or(SUPPORTED_VERSIONS[0]);
55 result(
56 id,
57 json!({
58 "protocolVersion": version,
59 "capabilities": { "tools": {} },
60 "serverInfo": { "name": "g1t", "version": "0.1.0" },
61 "instructions": INSTRUCTIONS,
62 }),
63 )
64 }
65 "ping" => result(id, json!({})),
66 "tools/list" => {
67 // A caller sees the tools, and the actions of each, that its
68 // token may use.
69 let gate = gate(services, viewer);
70 let tools: Vec<Value> = TOOLS.iter().filter_map(|tool| tool.listed(&gate)).collect();
71 result(id, json!({ "tools": tools }))
72 }
73 "tools/call" => {
74 let name = params["name"].as_str().unwrap_or_default();
75 let arguments = &params["arguments"];
76 let op = match Tool::by_name(name) {
77 Some(tool) => match crate::tools::resolve(tool, arguments) {
78 Ok(op) => op,
79 Err(problem) => {
80 return Ok(Some(result(
81 id,
82 json!({ "content": [{ "type": "text", "text": problem }], "isError": true }),
83 )));
84 }
85 },
86 // 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.87 // resource tools. Still answered, no longer listed; never
88 // one offered over REST alone.
89 None => match Op::by_name(name).filter(|op| !crate::tools::REST_ONLY.contains(op)) {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights90 Some(op) => op,
91 None => return Ok(Some(error(id, -32602, "Unknown tool."))),
92 },
93 };
94 let outcome = crate::audit::run(op, services, viewer, arguments).await?;
95 // A failed operation is a tool result the model can read and
96 // act on, not a protocol error.
97 let (text, failed) = match outcome {
98 // In `snake_case`, as the REST API answers; the protocol's
99 // own envelope keeps MCP's spelling.
100 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.101 serde_json::to_string_pretty(&g1t_kit::wire::snake_case_keeping(value, op.as_given()))?,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights102 false,
103 ),
104 Outcome::Fail(failure) => (failure.message, true),
105 };
106 result(
107 id,
108 json!({ "content": [{ "type": "text", "text": text }], "isError": failed }),
109 )
110 }
111 method => error(id, -32601, &format!("Method not found: {method}")),
112 };
113 Ok(Some(answer))
114}
115
116/// What someone sees when they open the server's address in a browser:
117/// what this is, how to connect, and what it offers.
Merge branch 'worktree-agent-aaf03bdceac799c89'118fn card(addresses: &crate::addresses::Addresses) -> Value {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights119 let tools: Vec<Value> = TOOLS
120 .iter()
121 .map(|tool| {
122 let actions: Vec<&crate::tools::Action> = tool.actions.iter().collect();
123 json!({
124 "name": tool.name,
125 "title": tool.title,
126 "description": tool.description,
127 "actions": tool.actions.iter().map(|action| json!({
128 "name": action.name,
129 "description": action.summary,
130 "operation": action.op.name(),
131 "scope": g1t_contracts::scopes::scope_for(action.op.name()).map(|scope| scope.as_str()),
132 })).collect::<Vec<_>>(),
133 "input_schema": tool.discriminated(&actions),
134 })
135 })
136 .collect();
137 json!({
138 "name": "g1t",
139 "description": "The g1t MCP server: issues, pull requests and sessions for agents.",
Merge branch 'worktree-agent-aaf03bdceac799c89'140 "endpoint": addresses.mcp,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights141 "transport": "streamable-http",
142 "protocol_versions": SUPPORTED_VERSIONS,
Merge branch 'worktree-agent-aaf03bdceac799c89'143 "connect": format!("claude mcp add --transport http g1t {}", addresses.mcp),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights144 "authorization": {
145 "required": true,
Merge branch 'worktree-agent-aaf03bdceac799c89'146 "oauth_protected_resource": addresses.protected_resource(),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights147 "alternative": "Authorization: Bearer <g1t access token>",
148 },
149 "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/",
150 "instructions": INSTRUCTIONS,
151 "tools": tools,
152 })
153}
154
155pub async fn handle(
156 mut request: Request,
157 services: &Services,
158 viewer: &Viewer,
159) -> Result<Response> {
160 if request.method() != Method::Post {
161 // A client asking for a stream of server messages is told there is
162 // none. Anyone else, a person with a browser, gets a description.
163 let wants_stream = request
164 .headers()
165 .get("accept")?
166 .is_some_and(|accept| accept.contains("text/event-stream"));
167 if request.method() == Method::Get && !wants_stream {
Merge branch 'worktree-agent-aaf03bdceac799c89'168 return Response::from_json(&card(&services.addresses));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights169 }
170 let mut response = Response::empty()?.with_status(405);
171 response.headers_mut().set("allow", "GET, POST")?;
172 return Ok(response);
173 }
174 // Calls need a signed-in user. Answering 401 with this header is what
175 // makes a client open the browser to sign in.
176 if viewer.is_none() {
177 let mut response = Response::from_json(&error(
178 &Value::Null,
179 -32001,
180 "Sign in to use the g1t MCP server.",
181 ))?
182 .with_status(401);
183 response
184 .headers_mut()
Merge branch 'worktree-agent-aaf03bdceac799c89'185 .set("www-authenticate", &services.addresses.mcp_challenge())?;
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights186 return Ok(response);
187 }
188 let Ok(body) = request.json::<Value>().await else {
189 return Ok(
190 Response::from_json(&error(&Value::Null, -32700, "Parse error"))?.with_status(400),
191 );
192 };
193 let accepted = || Ok(Response::empty()?.with_status(202));
194 match body {
195 Value::Array(batch) => {
196 let mut answers = Vec::new();
197 for request in &batch {
198 answers.extend(answer(services, viewer, request).await?);
199 }
200 if answers.is_empty() {
201 accepted()
202 } else {
203 Response::from_json(&answers)
204 }
205 }
206 single => match answer(services, viewer, &single).await? {
207 Some(answer) => Response::from_json(&answer),
208 None => accepted(),
209 },
210 }
211}

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