This PR introduces a single integration test for `cargo mcp`, though it also introduces a number of reusable components so that it should be easier to introduce more integration tests going forward. The new test is introduced in `codex-rs/mcp-server/tests/elicitation.rs` and the reusable pieces are in `codex-rs/mcp-server/tests/common`. The test itself verifies new functionality around elicitations introduced in https://github.com/openai/codex/pull/1623 (and the fix introduced in https://github.com/openai/codex/pull/1629) by doing the following: - starts a mock model provider with canned responses for `/v1/chat/completions` - starts the MCP server with a `config.toml` to use that model provider (and `approval_policy = "untrusted"`) - sends the `codex` tool call which causes the mock model provider to request a shell call for `git init` - the MCP server sends an elicitation to the client to approve the request - the client replies to the elicitation with `"approved"` - the MCP server runs the command and re-samples the model, getting a `"finish_reason": "stop"` - in turn, the MCP server sends the final response to the original `codex` tool call - verifies that `git init` ran as expected To test: ``` cargo test shell_command_approval_triggers_elicitation ``` In writing this test, I discovered that `ExecApprovalResponse` does not conform to `ElicitResult`, so I added a TODO to fix that, since I think that should be updated in a separate PR. As it stands, this PR does not update any business logic, though it does make a number of members of the `mcp-server` crate `pub` so they can be used in the test. One additional learning from this PR is that `std::process::Command::cargo_bin()` from the `assert_cmd` trait is only available for `std::process::Command`, but we really want to use `tokio::process::Command` so that everything is async and we can leverage utilities like `tokio::time::timeout()`. The trick I came up with was to use `cargo_bin()` to locate the program, and then to use `std::process::Command::get_program()` when constructing the `tokio::process::Command`.
60 lines
1.5 KiB
Rust
60 lines
1.5 KiB
Rust
use serde_json::json;
|
|
use std::path::Path;
|
|
|
|
pub fn create_shell_sse_response(
|
|
command: Vec<String>,
|
|
workdir: Option<&Path>,
|
|
timeout_ms: Option<u64>,
|
|
call_id: &str,
|
|
) -> anyhow::Result<String> {
|
|
// The `arguments`` for the `shell` tool is a serialized JSON object.
|
|
let tool_call_arguments = serde_json::to_string(&json!({
|
|
"command": command,
|
|
"workdir": workdir.map(|w| w.to_string_lossy()),
|
|
"timeout": timeout_ms
|
|
}))?;
|
|
let tool_call = json!({
|
|
"choices": [
|
|
{
|
|
"delta": {
|
|
"tool_calls": [
|
|
{
|
|
"id": call_id,
|
|
"function": {
|
|
"name": "shell",
|
|
"arguments": tool_call_arguments
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"finish_reason": "tool_calls"
|
|
}
|
|
]
|
|
});
|
|
|
|
let sse = format!(
|
|
"data: {}\n\ndata: DONE\n\n",
|
|
serde_json::to_string(&tool_call)?
|
|
);
|
|
Ok(sse)
|
|
}
|
|
|
|
pub fn create_final_assistant_message_sse_response(message: &str) -> anyhow::Result<String> {
|
|
let assistant_message = json!({
|
|
"choices": [
|
|
{
|
|
"delta": {
|
|
"content": message
|
|
},
|
|
"finish_reason": "stop"
|
|
}
|
|
]
|
|
});
|
|
|
|
let sse = format!(
|
|
"data: {}\n\ndata: DONE\n\n",
|
|
serde_json::to_string(&assistant_message)?
|
|
);
|
|
Ok(sse)
|
|
}
|