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

Commit

fix(api): include OAuth discovery metadata in MCP server card

syntaqxcommitted Parent10aafd8Browse files
2 files+48−90/2 viewed
+36−2
137137 })
138138 })
139139 .collect();
140− json!({
140+ let mut card = json!({
141141 "name": "g1t",
142142 "description": "The g1t MCP server: issues, pull requests and sessions for agents.",
143143 "endpoint": addresses.mcp,
163163 "documentation_url": "https://docs.g1t.sh/guides/bring-your-own-agent/",
164164 "instructions": INSTRUCTIONS,
165165 "tools": tools,
166− })
166+ });
167+ // Codex also reads the endpoint itself as protected resource metadata.
168+ // Keep the public card useful to people and valid for OAuth discovery.
169+ if let (Some(card), Value::Object(metadata)) = (
170+ card.as_object_mut(),
171+ crate::oauth::protected_resource_metadata(addresses),
172+ ) {
173+ card.extend(metadata);
174+ }
175+ card
167176 }
168177
169178 pub async fn handle(
228237 mod tests {
229238 use super::*;
230239
240+ #[test]
241+ fn server_card_supports_oauth_discovery() {
242+ use crate::addresses::Addresses;
243+
244+ for addresses in [
245+ Addresses::default(),
246+ Addresses::from_settings(
247+ Some("https://forge.example".into()),
248+ Some("https://api.forge.example".into()),
249+ Some("https://api.forge.example/mcp".into()),
250+ ),
251+ ] {
252+ let card = card(&addresses);
253+ assert_eq!(card["resource"], addresses.mcp);
254+ assert_eq!(card["authorization_servers"], json!([addresses.api]));
255+ assert_eq!(card["bearer_methods_supported"], json!(["header"]));
256+ assert!(card["scopes_supported"].as_array().is_some_and(|scopes| !scopes.is_empty()));
257+ for (key, value) in crate::oauth::protected_resource_metadata(&addresses).as_object().unwrap() {
258+ assert_eq!(&card[key], value, "discovery field {key}");
259+ }
260+ assert_eq!(card["endpoint"], addresses.mcp);
261+ assert!(card["tools"].as_array().is_some_and(|tools| !tools.is_empty()));
262+ }
263+ }
264+
231265 /// The instructions name every tool, in the order `tools/list` gives
232266 /// them, so that no tool goes unmentioned.
233267 #[test]
+12−7
228228
229229 /// Answers the request if it is for an OAuth endpoint. These are served on
230230 /// both hosts: an MCP client looks for the metadata next to the MCP server.
231+/// Shared by well-known discovery and the MCP endpoint's server card.
232+pub(crate) fn protected_resource_metadata(addresses: &Addresses) -> Value {
233+ json!({
234+ "resource": addresses.mcp,
235+ "authorization_servers": [addresses.api],
236+ "bearer_methods_supported": ["header"],
237+ "resource_documentation": "https://docs.g1t.sh/guides/bring-your-own-agent/",
238+ "scopes_supported": g1t_contracts::scopes::offered_scopes().iter().map(|scope| scope.as_str()).collect::<Vec<_>>(),
239+ })
240+}
241+
231242 pub async fn handle(
232243 request: &mut Request,
233244 services: &Services,
240251 }
241252 // Asked for with or without the MCP server's path appended.
242253 ("GET", path) if path.starts_with("/.well-known/oauth-protected-resource") => {
243− crate::reply(&json!({
244− "resource": services.addresses.mcp,
245− "authorization_servers": [services.addresses.api],
246− "bearer_methods_supported": ["header"],
247− "resource_documentation": "https://docs.g1t.sh/guides/bring-your-own-agent/",
248− "scopes_supported": g1t_contracts::scopes::offered_scopes().iter().map(|scope| scope.as_str()).collect::<Vec<_>>(),
249− }))?
254+ crate::reply(&protected_resource_metadata(&services.addresses))?
250255 }
251256 ("POST", "/oauth/register") => register(request).await?,
252257 ("POST", "/oauth/token") => token(request, services).await?,

Comments

Sign in to comment.