Skip to content

Commit 1f121c5

Browse files
committed
dynamically return mcp hostname for citations
1 parent 2e574ea commit 1f121c5

1 file changed

Lines changed: 111 additions & 14 deletions

File tree

src/mcp/server.rs

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use axum::{
22
Json, Router,
33
body::Body,
44
extract::Extension,
5-
http::{Request, StatusCode},
5+
http::{HeaderMap, Request, StatusCode, Uri},
66
middleware::{self, Next},
77
response::{IntoResponse, Response},
88
routing::{get, post},
@@ -167,7 +167,7 @@ struct ToolCallParams {
167167
arguments: Value,
168168
}
169169

170-
async fn mcp_rpc(Json(req): Json<JsonRpcRequest>) -> Response {
170+
async fn mcp_rpc(headers: HeaderMap, uri: Uri, Json(req): Json<JsonRpcRequest>) -> Response {
171171
let raw_request =
172172
serde_json::to_string(&req).unwrap_or_else(|_| "<serialize_error>".to_string());
173173
tracing::trace!(
@@ -190,16 +190,9 @@ async fn mcp_rpc(Json(req): Json<JsonRpcRequest>) -> Response {
190190

191191
match req.method.as_str() {
192192
"initialize" => {
193-
let result = json!({
194-
"protocolVersion": "2024-11-05",
195-
"capabilities": {
196-
"tools": { "listChanged": false }
197-
},
198-
"serverInfo": {
199-
"name": "pointer-mcp",
200-
"version": env!("CARGO_PKG_VERSION"),
201-
},
202-
"instructions": r#"Use MCP tools for indexed repository inspection. Start with `repositories` to discover the exact repo key, then `repo_branches` when branch selection matters. Use `path_search` for fuzzy path lookup, `file_list` for directory enumeration, `file_content` for targeted snippet reads, `search` for structured content search, and `symbol_insights` for symbol definitions and references.
193+
let public_base_url = mcp_public_base_url(&headers, &uri);
194+
let instructions = format!(
195+
r#"Use MCP tools for indexed repository inspection. Start with `repositories` to discover the exact repo key, then `repo_branches` when branch selection matters. Use `path_search` for fuzzy path lookup, `file_list` for directory enumeration, `file_content` for targeted snippet reads, `search` for structured content search, and `symbol_insights` for symbol definitions and references.
203196
204197
Tool requirements:
205198
- `search` accepts structured JSON fields only. Do not send a free-form `query` string.
@@ -217,7 +210,19 @@ async fn mcp_rpc(Json(req): Json<JsonRpcRequest>) -> Response {
217210
- If branch recency or version differences matter, call `repo_branches` first and compare explicit branch results.
218211
219212
Citation requirement:
220-
- When citing code, use the MCP server UI hyperlink format: https://{mcp_server_host}/repo/{repo}/tree/{branch|commit}/{path}#L{Line number}"#,
213+
- When citing code, use the MCP server UI hyperlink format: {mcp_server_host}/repo/{{repo}}/tree/{{branch|commit}}/{{path}}#L{{Line number}}"#,
214+
mcp_server_host = public_base_url
215+
);
216+
let result = json!({
217+
"protocolVersion": "2024-11-05",
218+
"capabilities": {
219+
"tools": { "listChanged": false }
220+
},
221+
"serverInfo": {
222+
"name": "pointer-mcp",
223+
"version": env!("CARGO_PKG_VERSION"),
224+
},
225+
"instructions": instructions,
221226
});
222227
jsonrpc_result(req.id, result)
223228
}
@@ -275,6 +280,55 @@ async fn mcp_rpc(Json(req): Json<JsonRpcRequest>) -> Response {
275280
}
276281
}
277282

283+
fn mcp_public_base_url(headers: &HeaderMap, uri: &Uri) -> String {
284+
if let Some((proto, host)) = forwarded_proto_and_host(headers) {
285+
return format!("{proto}://{host}");
286+
}
287+
288+
let host = header_value(headers, "x-forwarded-host")
289+
.or_else(|| header_value(headers, "host"))
290+
.unwrap_or_else(|| "localhost".to_string());
291+
let proto = header_value(headers, "x-forwarded-proto")
292+
.or_else(|| uri.scheme_str().map(str::to_string))
293+
.unwrap_or_else(|| "http".to_string());
294+
295+
format!("{proto}://{host}")
296+
}
297+
298+
fn forwarded_proto_and_host(headers: &HeaderMap) -> Option<(String, String)> {
299+
let forwarded = header_value(headers, "forwarded")?;
300+
301+
forwarded.split(',').find_map(|entry| {
302+
let mut proto = None;
303+
let mut host = None;
304+
305+
for pair in entry.split(';') {
306+
let (key, value) = pair.trim().split_once('=')?;
307+
let value = value.trim().trim_matches('"');
308+
309+
if key.eq_ignore_ascii_case("proto") && !value.is_empty() {
310+
proto = Some(value.to_string());
311+
} else if key.eq_ignore_ascii_case("host") && !value.is_empty() {
312+
host = Some(value.to_string());
313+
}
314+
}
315+
316+
match (proto, host) {
317+
(Some(proto), Some(host)) => Some((proto, host)),
318+
_ => None,
319+
}
320+
})
321+
}
322+
323+
fn header_value(headers: &HeaderMap, name: &str) -> Option<String> {
324+
headers
325+
.get(name)
326+
.and_then(|value| value.to_str().ok())
327+
.map(str::trim)
328+
.filter(|value| !value.is_empty())
329+
.map(str::to_string)
330+
}
331+
278332
async fn execute_tool_call(name: &str, arguments: Value) -> Result<Value, String> {
279333
match name {
280334
"search" => {
@@ -510,7 +564,9 @@ fn mcp_tools() -> Vec<Value> {
510564

511565
#[cfg(test)]
512566
mod tests {
513-
use super::{mcp_docs_payload, mcp_tools};
567+
use axum::http::{HeaderMap, HeaderValue, Uri};
568+
569+
use super::{mcp_docs_payload, mcp_public_base_url, mcp_tools};
514570

515571
#[test]
516572
fn docs_payload_uses_structured_search_key() {
@@ -542,4 +598,45 @@ mod tests {
542598
.expect("search examples must be present");
543599
assert!(!examples.is_empty());
544600
}
601+
602+
#[test]
603+
fn public_base_url_prefers_forwarded_header() {
604+
let mut headers = HeaderMap::new();
605+
headers.insert(
606+
"forwarded",
607+
HeaderValue::from_static("for=1.2.3.4;proto=https;host=pointer.example.com"),
608+
);
609+
610+
assert_eq!(
611+
mcp_public_base_url(&headers, &Uri::from_static("/mcp/v1")),
612+
"https://pointer.example.com"
613+
);
614+
}
615+
616+
#[test]
617+
fn public_base_url_falls_back_to_x_forwarded_headers() {
618+
let mut headers = HeaderMap::new();
619+
headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
620+
headers.insert(
621+
"x-forwarded-host",
622+
HeaderValue::from_static("pointer.example.com"),
623+
);
624+
headers.insert("host", HeaderValue::from_static("127.0.0.1:3000"));
625+
626+
assert_eq!(
627+
mcp_public_base_url(&headers, &Uri::from_static("/mcp/v1")),
628+
"https://pointer.example.com"
629+
);
630+
}
631+
632+
#[test]
633+
fn public_base_url_uses_host_header_without_proxy_headers() {
634+
let mut headers = HeaderMap::new();
635+
headers.insert("host", HeaderValue::from_static("127.0.0.1:3000"));
636+
637+
assert_eq!(
638+
mcp_public_base_url(&headers, &Uri::from_static("/mcp/v1")),
639+
"http://127.0.0.1:3000"
640+
);
641+
}
545642
}

0 commit comments

Comments
 (0)