#![deny(clippy::unwrap_used)]
use std::time::Duration;
use rmcp::ServiceExt;
use rmcp::transport::child_process::TokioChildProcess;
const GITLAB_MCP_URL: &str = "https://gitlab.com/api/v4/mcp";
const TOOL_PREFIX_HEADER: &str = "X-Gitlab-Mcp-Server-Tool-Name-Prefix: gitlab-";
const TOOL_PREFIX: &str = "gitlab-";
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(300);
const TOOLS_LIST_TIMEOUT: Duration = Duration::from_secs(60);
#[tokio::test]
#[ignore = "hits gitlab.com; may open a browser for OAuth on first run"]
async fn e2e_gitlab_oauth_lists_tools() {
let bin = env!("CARGO_BIN_EXE_hyper-mcp-remote");
eprintln!("e2e: spawning {bin} -> {GITLAB_MCP_URL}");
let mut cmd = tokio::process::Command::new(bin);
cmd.arg(GITLAB_MCP_URL)
.arg("--header")
.arg(TOOL_PREFIX_HEADER);
let proc = TokioChildProcess::new(cmd).expect("failed to spawn hyper-mcp-remote binary");
let pid = proc.id();
eprintln!("e2e: proxy pid={pid:?}");
let client = tokio::time::timeout(HANDSHAKE_TIMEOUT, ().serve(proc))
.await
.expect("MCP handshake timed out (did you complete OAuth in the browser?)")
.expect("MCP handshake failed");
let info = client
.peer_info()
.expect("peer_info should be set after handshake");
eprintln!(
"e2e: connected; proxy reports server_info={} v{}",
info.server_info.name, info.server_info.version
);
assert_eq!(
info.server_info.name, "hyper-mcp-remote",
"proxy must rewrite server_info.name to its own identity"
);
assert_eq!(
info.server_info.version,
env!("CARGO_PKG_VERSION"),
"proxy server_info.version should match the crate version"
);
assert!(
info.capabilities.tools.is_some(),
"GitLab MCP advertises tools; proxy must surface that capability"
);
let tools = tokio::time::timeout(TOOLS_LIST_TIMEOUT, client.peer().list_tools(None))
.await
.expect("list_tools timed out")
.expect("list_tools returned a protocol error");
assert!(
!tools.tools.is_empty(),
"GitLab MCP server should expose at least one tool; got an empty list"
);
eprintln!("e2e: received {} tools from GitLab:", tools.tools.len());
for tool in &tools.tools {
eprintln!(
" - {} \u{2014} {}",
tool.name,
tool.description.as_deref().unwrap_or("(no description)")
);
}
for tool in &tools.tools {
assert_eq!(
tool.input_schema.get("type").and_then(|v| v.as_str()),
Some("object"),
"tool '{}' must have inputSchema.type == \"object\"",
tool.name
);
}
let bad: Vec<&str> = tools
.tools
.iter()
.map(|t| t.name.as_ref())
.filter(|name| !name.starts_with(TOOL_PREFIX))
.collect();
assert!(
bad.is_empty(),
"every tool name should start with {TOOL_PREFIX:?} when the \
X-Gitlab-Mcp-Server-Tool-Name-Prefix header is set; offenders: {bad:?}"
);
if let Err(e) = client.cancel().await {
eprintln!("e2e: client.cancel() returned {e}; ignoring");
}
}