@@ -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+
278332async 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) ]
512566mod 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