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.

162 linesCodeBlameRaw
1//! Where this installation is reached: the site, the REST API and the MCP
2//! server. Hosted g1t sets none of the variables and gets g1t.sh's
3//! addresses; a self-hosted one sets them from its PUBLIC_URL
4//! (deploy/self-host/configs.mjs).
5//!
6//! The MCP server is on its own host hosted (`mcp.g1t.sh`). Self-hosted it
7//! can be a path on the API's host instead (`http://localhost:8789/mcp`):
8//! a request is for MCP when its host starts with `mcp.`, or when MCP_URL
9//! has a path and the request is under it.
10
11use worker::{Env, Url};
12
13pub const HOSTED_SITE: &str = "https://g1t.sh";
14pub const HOSTED_API: &str = "https://api.g1t.sh";
15pub const HOSTED_MCP: &str = "https://mcp.g1t.sh";
16pub const HOSTED_USERCONTENT: &str = "https://g1tusercontent.com";
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct Addresses {
20 /// SITE_URL: where people sign in and approve, and where git remotes are.
21 pub site: String,
22 /// API_URL: the REST API, and the OAuth issuer.
23 pub api: String,
24 /// MCP_URL: the MCP server, and the OAuth protected resource.
25 pub mcp: String,
26 /// USERCONTENT_URL: where attachments are served (apps/web/workers/usercontent.ts),
27 /// as the site reads it (apps/web/app/lib/addresses.ts): unset, g1tusercontent.com
28 /// for g1t.sh, and a path on the site (`SITE_URL/-/usercontent`) for any other.
29 pub usercontent: String,
30}
31
32impl Default for Addresses {
33 fn default() -> Self {
34 Addresses {
35 site: HOSTED_SITE.to_owned(),
36 api: HOSTED_API.to_owned(),
37 mcp: HOSTED_MCP.to_owned(),
38 usercontent: HOSTED_USERCONTENT.to_owned(),
39 }
40 }
41}
42
43fn setting(value: Option<String>, default: &str) -> String {
44 let value = value.unwrap_or_default();
45 let value = value.trim().trim_end_matches('/');
46 if value.is_empty() { default.to_owned() } else { value.to_owned() }
47}
48
49impl Addresses {
50 pub fn from_settings(site: Option<String>, api: Option<String>, mcp: Option<String>) -> Addresses {
51 let site = setting(site, HOSTED_SITE);
52 let usercontent = if site == HOSTED_SITE { HOSTED_USERCONTENT.to_owned() } else { format!("{site}/-/usercontent") };
53 Addresses {
54 api: setting(api, HOSTED_API),
55 mcp: setting(mcp, HOSTED_MCP),
56 usercontent,
57 site,
58 }
59 }
60
61 /// With USERCONTENT_URL, when it is set: the site's own origin is
62 /// never the usercontent origin, a path on it is.
63 pub fn with_usercontent(mut self, usercontent: Option<String>) -> Addresses {
64 let given = setting(usercontent, "");
65 if !given.is_empty() {
66 self.usercontent = if given == self.site { format!("{}/-/usercontent", self.site) } else { given };
67 }
68 self
69 }
70
71 pub fn from_env(env: &Env) -> Addresses {
72 let var = |name: &str| env.var(name).ok().map(|value| value.to_string());
73 Addresses::from_settings(var("SITE_URL"), var("API_URL"), var("MCP_URL")).with_usercontent(var("USERCONTENT_URL"))
74 }
75
76 /// What a client is told when it must sign in first (RFC 9728).
77 pub fn mcp_challenge(&self) -> String {
78 format!("Bearer resource_metadata=\"{}\"", self.protected_resource())
79 }
80
81 /// The protected resource metadata, at the MCP server's origin with
82 /// its path appended, as RFC 9728 places it.
83 pub fn protected_resource(&self) -> String {
84 match Url::parse(&self.mcp) {
85 Ok(url) if url.path() != "/" => {
86 let origin = url.origin().ascii_serialization();
87 format!("{origin}/.well-known/oauth-protected-resource{}", url.path().trim_end_matches('/'))
88 }
89 _ => format!("{}/.well-known/oauth-protected-resource", self.mcp),
90 }
91 }
92
93 /// A repository's git remote.
94 pub fn git_remote(&self, owner: &str, name: &str) -> String {
95 format!("{}/{owner}/{name}.git", self.site)
96 }
97
98 /// Whether a request to `url` is for the MCP server, and the path it
99 /// asks for there.
100 pub fn mcp_path(&self, url: &Url) -> Option<String> {
101 if url.host_str().is_some_and(|host| host.starts_with("mcp.")) {
102 return Some(url.path().to_owned());
103 }
104 // By path alone: behind a proxy the host a request names need not
105 // be the one people use, and no REST route starts with it.
106 let mcp = Url::parse(&self.mcp).ok()?;
107 let base = mcp.path().trim_end_matches('/');
108 if base.is_empty() {
109 return None;
110 }
111 let rest = url.path().strip_prefix(base)?;
112 (rest.is_empty() || rest.starts_with('/')).then(|| if rest.is_empty() { "/".to_owned() } else { rest.to_owned() })
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn hosted_is_the_default() {
122 let hosted = Addresses::from_settings(None, Some(String::new()), Some(" ".into()));
123 assert_eq!(hosted, Addresses::default());
124 assert_eq!(
125 hosted.mcp_challenge(),
126 "Bearer resource_metadata=\"https://mcp.g1t.sh/.well-known/oauth-protected-resource\""
127 );
128 assert_eq!(hosted.git_remote("acme", "rocket"), "https://g1t.sh/acme/rocket.git");
129 assert_eq!(hosted.usercontent, "https://g1tusercontent.com");
130 let on_mcp = Url::parse("https://mcp.g1t.sh/").unwrap();
131 assert_eq!(hosted.mcp_path(&on_mcp).as_deref(), Some("/"));
132 let on_api = Url::parse("https://api.g1t.sh/user").unwrap();
133 assert_eq!(hosted.mcp_path(&on_api), None);
134 }
135
136 #[test]
137 fn self_hosted_mcp_is_a_path_on_the_api() {
138 let own = Addresses::from_settings(
139 Some("http://localhost:8787/".into()),
140 Some("http://localhost:8789".into()),
141 Some("http://localhost:8789/mcp".into()),
142 );
143 assert_eq!(own.site, "http://localhost:8787");
144 assert_eq!(
145 own.protected_resource(),
146 "http://localhost:8789/.well-known/oauth-protected-resource/mcp"
147 );
148 let url = |text: &str| Url::parse(text).unwrap();
149 assert_eq!(own.mcp_path(&url("http://localhost:8789/mcp")).as_deref(), Some("/"));
150 assert_eq!(own.mcp_path(&url("http://localhost:8789/mcp/")).as_deref(), Some("/"));
151 assert_eq!(own.mcp_path(&url("http://localhost:8789/mcpx")), None);
152 assert_eq!(own.mcp_path(&url("http://localhost:8789/user")), None);
153 assert_eq!(own.mcp_path(&url("http://127.0.0.1:8789/mcp")).as_deref(), Some("/"), "by path, whatever the host");
154 assert_eq!(own.git_remote("acme", "rocket"), "http://localhost:8787/acme/rocket.git");
155 assert_eq!(own.usercontent, "http://localhost:8787/-/usercontent");
156 let files = own.clone().with_usercontent(Some("https://files.example.net/".into()));
157 assert_eq!(files.usercontent, "https://files.example.net");
158 let same = own.clone().with_usercontent(Some("http://localhost:8787".into()));
159 assert_eq!(same.usercontent, "http://localhost:8787/-/usercontent");
160 assert_eq!(own.clone().with_usercontent(Some(" ".into())), own);
161 }
162}