g1t/apps/api/src/mcp.rs

209 lines9,519 bytesCodeBlame

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.

API and MCP server in Rust; a public index at the API root1//! 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};
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step9use crate::tools::{Gate, TOOLS, Tool};
API and MCP server in Rust; a public index at the API root10
11const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"];
12
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step13const 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.
14Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, webhook, access, workspace, account. 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.
15Find a repository: account whoami lists your workspaces; repository list or search finds one.
Fast pages, required checks on the branch, self-hosted runners, honest incidents16Work 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.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step17Hand 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.
18When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret.";
API and MCP server in Rust; a public index at the API root19
20fn result(id: &Value, value: Value) -> Value {
21 json!({ "jsonrpc": "2.0", "id": id, "result": value })
22}
23
24fn error(id: &Value, code: i32, message: &str) -> Value {
25 json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
26}
27
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step28/// What decides the actions a caller sees: an agent's run scope, an
29/// access token's scopes, or nothing beyond the person's role.
30fn gate<'a>(services: &'a Services, viewer: &'a Viewer) -> Gate<'a> {
31 if let Some(scope) = &services.scope {
32 return Gate::Agent(scope);
33 }
34 match viewer.as_ref().and_then(|user| user.token.as_deref()) {
35 Some(access) => Gate::Token(access),
36 None => Gate::Everything,
37 }
38}
39
API and MCP server in Rust; a public index at the API root40/// Answers one JSON-RPC request, or `None` for a notification.
41async fn answer(services: &Services, viewer: &Viewer, request: &Value) -> Result<Option<Value>> {
42 // Notifications carry no id and get no response.
43 let Some(id) = request.get("id") else {
44 return Ok(None);
45 };
46 let params = &request["params"];
47 let answer = match request["method"].as_str().unwrap_or_default() {
48 "initialize" => {
49 let requested = params["protocolVersion"].as_str().unwrap_or_default();
50 let version = SUPPORTED_VERSIONS
51 .into_iter()
52 .find(|version| *version == requested)
53 .unwrap_or(SUPPORTED_VERSIONS[0]);
54 result(
55 id,
56 json!({
57 "protocolVersion": version,
58 "capabilities": { "tools": {} },
59 "serverInfo": { "name": "g1t", "version": "0.1.0" },
60 "instructions": INSTRUCTIONS,
61 }),
62 )
63 }
64 "ping" => result(id, json!({})),
65 "tools/list" => {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step66 // A caller sees the tools, and the actions of each, that its
67 // token may use.
68 let gate = gate(services, viewer);
69 let tools: Vec<Value> = TOOLS.iter().filter_map(|tool| tool.listed(&gate)).collect();
API and MCP server in Rust; a public index at the API root70 result(id, json!({ "tools": tools }))
71 }
72 "tools/call" => {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step73 let name = params["name"].as_str().unwrap_or_default();
74 let arguments = &params["arguments"];
75 let op = match Tool::by_name(name) {
76 Some(tool) => match crate::tools::resolve(tool, arguments) {
77 Ok(op) => op,
78 Err(problem) => {
79 return Ok(Some(result(
80 id,
81 json!({ "content": [{ "type": "text", "text": problem }], "isError": true }),
82 )));
83 }
84 },
85 // A tool per operation, as the server had before its
86 // resource tools. Still answered, no longer listed.
87 None => match Op::by_name(name) {
88 Some(op) => op,
89 None => return Ok(Some(error(id, -32602, "Unknown tool."))),
90 },
API and MCP server in Rust; a public index at the API root91 };
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step92 let outcome = crate::audit::run(op, services, viewer, arguments).await?;
API and MCP server in Rust; a public index at the API root93 // A failed operation is a tool result the model can read and
94 // act on, not a protocol error.
95 let (text, failed) = match outcome {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API96 // In `snake_case`, as the REST API answers; the protocol's
97 // own envelope keeps MCP's spelling.
98 Outcome::Ok(value) => (
99 serde_json::to_string_pretty(&g1t_kit::wire::snake_case(value))?,
100 false,
101 ),
API and MCP server in Rust; a public index at the API root102 Outcome::Fail(failure) => (failure.message, true),
103 };
104 result(
105 id,
106 json!({ "content": [{ "type": "text", "text": text }], "isError": failed }),
107 )
108 }
109 method => error(id, -32601, &format!("Method not found: {method}")),
110 };
111 Ok(Some(answer))
112}
113
114/// What someone sees when they open the server's address in a browser:
115/// what this is, how to connect, and what it offers.
Merge branch 'worktree-agent-aaf03bdceac799c89'116fn card(addresses: &crate::addresses::Addresses) -> Value {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step117 let tools: Vec<Value> = TOOLS
118 .iter()
119 .map(|tool| {
120 let actions: Vec<&crate::tools::Action> = tool.actions.iter().collect();
121 json!({
122 "name": tool.name,
123 "title": tool.title,
124 "description": tool.description,
125 "actions": tool.actions.iter().map(|action| json!({
126 "name": action.name,
127 "description": action.summary,
128 "operation": action.op.name(),
129 "scope": g1t_contracts::scopes::scope_for(action.op.name()).map(|scope| scope.as_str()),
130 })).collect::<Vec<_>>(),
131 "input_schema": tool.discriminated(&actions),
132 })
133 })
API and MCP server in Rust; a public index at the API root134 .collect();
135 json!({
136 "name": "g1t",
137 "description": "The g1t MCP server: issues, pull requests and sessions for agents.",
Merge branch 'worktree-agent-aaf03bdceac799c89'138 "endpoint": addresses.mcp,
API and MCP server in Rust; a public index at the API root139 "transport": "streamable-http",
140 "protocol_versions": SUPPORTED_VERSIONS,
Merge branch 'worktree-agent-aaf03bdceac799c89'141 "connect": format!("claude mcp add --transport http g1t {}", addresses.mcp),
API and MCP server in Rust; a public index at the API root142 "authorization": {
143 "required": true,
Merge branch 'worktree-agent-aaf03bdceac799c89'144 "oauth_protected_resource": addresses.protected_resource(),
API and MCP server in Rust; a public index at the API root145 "alternative": "Authorization: Bearer <g1t access token>",
146 },
147 "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/",
148 "instructions": INSTRUCTIONS,
149 "tools": tools,
150 })
151}
152
153pub async fn handle(
154 mut request: Request,
155 services: &Services,
156 viewer: &Viewer,
157) -> Result<Response> {
158 if request.method() != Method::Post {
159 // A client asking for a stream of server messages is told there is
160 // none. Anyone else, a person with a browser, gets a description.
161 let wants_stream = request
162 .headers()
163 .get("accept")?
164 .is_some_and(|accept| accept.contains("text/event-stream"));
165 if request.method() == Method::Get && !wants_stream {
Merge branch 'worktree-agent-aaf03bdceac799c89'166 return Response::from_json(&card(&services.addresses));
API and MCP server in Rust; a public index at the API root167 }
168 let mut response = Response::empty()?.with_status(405);
169 response.headers_mut().set("allow", "GET, POST")?;
170 return Ok(response);
171 }
172 // Calls need a signed-in user. Answering 401 with this header is what
173 // makes a client open the browser to sign in.
174 if viewer.is_none() {
175 let mut response = Response::from_json(&error(
176 &Value::Null,
177 -32001,
178 "Sign in to use the g1t MCP server.",
179 ))?
180 .with_status(401);
181 response
182 .headers_mut()
Merge branch 'worktree-agent-aaf03bdceac799c89'183 .set("www-authenticate", &services.addresses.mcp_challenge())?;
API and MCP server in Rust; a public index at the API root184 return Ok(response);
185 }
186 let Ok(body) = request.json::<Value>().await else {
187 return Ok(
188 Response::from_json(&error(&Value::Null, -32700, "Parse error"))?.with_status(400),
189 );
190 };
191 let accepted = || Ok(Response::empty()?.with_status(202));
192 match body {
193 Value::Array(batch) => {
194 let mut answers = Vec::new();
195 for request in &batch {
196 answers.extend(answer(services, viewer, request).await?);
197 }
198 if answers.is_empty() {
199 accepted()
200 } else {
201 Response::from_json(&answers)
202 }
203 }
204 single => match answer(services, viewer, &single).await? {
205 Some(answer) => Response::from_json(&answer),
206 None => accepted(),
207 },
208 }
209}

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