This PR adds support for streamable HTTP MCP servers when the `experimental_use_rmcp_client` is enabled. To set one up, simply add a new mcp server config with the url: ``` [mcp_servers.figma] url = "http://127.0.0.1:3845/mcp" ``` It also supports an optional `bearer_token` which will be provided in an authorization header. The full oauth flow is not supported yet. The config parsing will throw if it detects that the user mixed and matched config fields (like command + bearer token or url + env). The best way to review it is to review `core/src` and then `rmcp-client/src/rmcp_client.rs` first. The rest is tests and propagating the `Transport` struct around the codebase. Example with the Figma MCP: <img width="5084" height="1614" alt="CleanShot 2025-09-26 at 13 35 40" src="https://github.com/user-attachments/assets/eaf2771e-df3e-4300-816b-184d7dec5a28" />
105 lines
3.0 KiB
Rust
105 lines
3.0 KiB
Rust
use std::path::Path;
|
|
|
|
use anyhow::Result;
|
|
use predicates::str::contains;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::Value as JsonValue;
|
|
use serde_json::json;
|
|
use tempfile::TempDir;
|
|
|
|
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
|
|
let mut cmd = assert_cmd::Command::cargo_bin("codex")?;
|
|
cmd.env("CODEX_HOME", codex_home);
|
|
Ok(cmd)
|
|
}
|
|
|
|
#[test]
|
|
fn list_shows_empty_state() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
|
|
let mut cmd = codex_command(codex_home.path())?;
|
|
let output = cmd.args(["mcp", "list"]).output()?;
|
|
assert!(output.status.success());
|
|
let stdout = String::from_utf8(output.stdout)?;
|
|
assert!(stdout.contains("No MCP servers configured yet."));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn list_and_get_render_expected_output() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
|
|
let mut add = codex_command(codex_home.path())?;
|
|
add.args([
|
|
"mcp",
|
|
"add",
|
|
"docs",
|
|
"--env",
|
|
"TOKEN=secret",
|
|
"--",
|
|
"docs-server",
|
|
"--port",
|
|
"4000",
|
|
])
|
|
.assert()
|
|
.success();
|
|
|
|
let mut list_cmd = codex_command(codex_home.path())?;
|
|
let list_output = list_cmd.args(["mcp", "list"]).output()?;
|
|
assert!(list_output.status.success());
|
|
let stdout = String::from_utf8(list_output.stdout)?;
|
|
assert!(stdout.contains("Name"));
|
|
assert!(stdout.contains("docs"));
|
|
assert!(stdout.contains("docs-server"));
|
|
assert!(stdout.contains("TOKEN=secret"));
|
|
|
|
let mut list_json_cmd = codex_command(codex_home.path())?;
|
|
let json_output = list_json_cmd.args(["mcp", "list", "--json"]).output()?;
|
|
assert!(json_output.status.success());
|
|
let stdout = String::from_utf8(json_output.stdout)?;
|
|
let parsed: JsonValue = serde_json::from_str(&stdout)?;
|
|
assert_eq!(
|
|
parsed,
|
|
json!([
|
|
{
|
|
"name": "docs",
|
|
"transport": {
|
|
"type": "stdio",
|
|
"command": "docs-server",
|
|
"args": [
|
|
"--port",
|
|
"4000"
|
|
],
|
|
"env": {
|
|
"TOKEN": "secret"
|
|
}
|
|
},
|
|
"startup_timeout_sec": null,
|
|
"tool_timeout_sec": null
|
|
}
|
|
]
|
|
)
|
|
);
|
|
|
|
let mut get_cmd = codex_command(codex_home.path())?;
|
|
let get_output = get_cmd.args(["mcp", "get", "docs"]).output()?;
|
|
assert!(get_output.status.success());
|
|
let stdout = String::from_utf8(get_output.stdout)?;
|
|
assert!(stdout.contains("docs"));
|
|
assert!(stdout.contains("transport: stdio"));
|
|
assert!(stdout.contains("command: docs-server"));
|
|
assert!(stdout.contains("args: --port 4000"));
|
|
assert!(stdout.contains("env: TOKEN=secret"));
|
|
assert!(stdout.contains("remove: codex mcp remove docs"));
|
|
|
|
let mut get_json_cmd = codex_command(codex_home.path())?;
|
|
get_json_cmd
|
|
.args(["mcp", "get", "docs", "--json"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("\"name\": \"docs\""));
|
|
|
|
Ok(())
|
|
}
|