use anyhow::{Context, Result};
use std::io::{self, BufRead, Write};
use std::time::Duration;
use reqwest::blocking::Client;
use serde_json::{json, Value};
use tracing::{error, info};
fn build_client() -> Result<Client> {
Client::builder()
.timeout(Duration::from_secs(60))
.build()
.context("failed to build HTTP client for remote MCP proxy")
}
pub fn run(url: &str, api_key: &str) -> Result<()> {
let client = build_client()?;
info!(endpoint = %url, "Palace MCP server starting in remote (proxy) mode");
let stdin = io::stdin();
let stdout = io::stdout();
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
let line = line.trim().to_string();
if line.is_empty() {
continue;
}
let request: Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(e) => {
write_line(
&stdout,
&json!({
"jsonrpc": "2.0",
"id": Value::Null,
"error": {"code": -32700, "message": format!("Parse error: {e}")}
}),
)?;
continue;
}
};
let req_id = request.get("id").cloned();
let is_notification = req_id.is_none();
let method = request
.get("method")
.and_then(|m| m.as_str())
.unwrap_or("")
.to_string();
match forward(&client, url, api_key, &line) {
Ok(Some(response)) if !is_notification => {
write_line(&stdout, &adapt_response(&method, response))?
}
Ok(_) => {} Err(e) => {
error!(error = %e, "remote MCP forward failed");
if !is_notification {
write_line(
&stdout,
&json!({
"jsonrpc": "2.0",
"id": req_id.unwrap_or(Value::Null),
"error": {
"code": -32000,
"message": format!("Remote palace-server error: {e}")
}
}),
)?;
}
}
}
}
Ok(())
}
fn forward(client: &Client, url: &str, api_key: &str, body: &str) -> Result<Option<Value>> {
let resp = client
.post(url)
.header("Authorization", format!("Bearer {api_key}"))
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/event-stream")
.body(body.to_string())
.send()
.context("request to remote palace-server failed")?;
let status = resp.status();
let content_type = resp
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let text = resp.text().context("failed to read remote response body")?;
if !status.is_success() {
let detail = text.trim();
if status.as_u16() == 401 {
anyhow::bail!("HTTP 401 Unauthorized — check the API key (`palace remote set`)");
}
anyhow::bail!("HTTP {}: {}", status.as_u16(), detail);
}
if text.trim().is_empty() {
return Ok(None);
}
if content_type.contains("text/event-stream") {
Ok(parse_sse(&text))
} else {
let value: Value =
serde_json::from_str(text.trim()).context("invalid JSON from remote palace-server")?;
Ok(Some(value))
}
}
fn adapt_response(method: &str, mut resp: Value) -> Value {
if method != "initialize" && method != "tools/list" {
return resp;
}
let inner = resp
.get("result")
.and_then(|r| r.get("content"))
.and_then(|c| c.as_array())
.and_then(|a| a.first())
.and_then(|c0| c0.get("text"))
.and_then(|t| t.as_str())
.and_then(|s| serde_json::from_str::<Value>(s).ok());
if let Some(inner) = inner {
if let Some(obj) = resp.as_object_mut() {
obj.insert("result".to_string(), inner);
}
}
resp
}
fn parse_sse(body: &str) -> Option<Value> {
let mut last = None;
for line in body.lines() {
let line = line.trim();
if let Some(data) = line.strip_prefix("data:") {
let data = data.trim();
if data.is_empty() || data == "[DONE]" {
continue;
}
if let Ok(v) = serde_json::from_str::<Value>(data) {
last = Some(v);
}
}
}
last
}
fn write_line(stdout: &io::Stdout, value: &Value) -> Result<()> {
let mut out = stdout.lock();
writeln!(out, "{value}")?;
out.flush()?;
Ok(())
}
pub fn probe(url: &str, api_key: &str) -> Result<usize> {
let client = build_client()?;
let init = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "palace-cli", "version": env!("CARGO_PKG_VERSION")}
}
});
forward(&client, url, api_key, &init.to_string()).context("initialize handshake failed")?;
let list = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}});
let resp = forward(&client, url, api_key, &list.to_string())
.context("tools/list failed")?
.ok_or_else(|| anyhow::anyhow!("empty tools/list response from remote"))?;
let resp = adapt_response("tools/list", resp);
if let Some(err) = resp.get("error") {
anyhow::bail!("remote returned error: {err}");
}
let count = resp
.get("result")
.and_then(|r| r.get("tools"))
.and_then(|t| t.as_array())
.map(|a| a.len())
.unwrap_or(0);
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sse_returns_last_data_payload() {
let body =
"event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"ok\":true}}\n\n";
let v = parse_sse(body).expect("parsed");
assert_eq!(v["result"]["ok"], serde_json::json!(true));
}
#[test]
fn sse_skips_done_and_blank() {
let body = "data: \ndata: [DONE]\n";
assert!(parse_sse(body).is_none());
}
#[test]
fn adapt_unwraps_initialize_envelope() {
let raw = serde_json::json!({
"jsonrpc": "2.0", "id": 1,
"result": {"content": [{"type": "text",
"text": "{\"capabilities\":{\"tools\":{\"listChanged\":false}},\"protocolVersion\":\"2024-11-05\",\"serverInfo\":{\"name\":\"palace-server\",\"version\":\"0.3.0\"}}"}]}
});
let out = adapt_response("initialize", raw);
assert_eq!(out["result"]["protocolVersion"], "2024-11-05");
assert_eq!(out["result"]["serverInfo"]["name"], "palace-server");
assert!(out["result"].get("content").is_none());
}
#[test]
fn adapt_unwraps_tools_list_envelope() {
let raw = serde_json::json!({
"jsonrpc": "2.0", "id": 2,
"result": {"content": [{"type": "text",
"text": "{\"tools\":[{\"name\":\"palace_status\"},{\"name\":\"palace_search\"}]}"}]}
});
let out = adapt_response("tools/list", raw);
let tools = out["result"]["tools"].as_array().expect("tools array");
assert_eq!(tools.len(), 2);
assert_eq!(tools[0]["name"], "palace_status");
}
#[test]
fn adapt_leaves_tools_call_unchanged() {
let raw = serde_json::json!({
"jsonrpc": "2.0", "id": 3,
"result": {"content": [{"type": "text", "text": "{\"licensed\":true}"}]}
});
let out = adapt_response("tools/call", raw.clone());
assert_eq!(out, raw);
}
#[test]
fn adapt_passes_through_compliant_server() {
let raw = serde_json::json!({
"jsonrpc": "2.0", "id": 2,
"result": {"tools": [{"name": "palace_status"}]}
});
let out = adapt_response("tools/list", raw.clone());
assert_eq!(out, raw);
}
#[test]
fn adapt_passes_through_error_response() {
let raw = serde_json::json!({
"jsonrpc": "2.0", "id": 2,
"error": {"code": -32001, "message": "license inactive"}
});
let out = adapt_response("tools/list", raw.clone());
assert_eq!(out, raw);
}
}