Add optional MCP disablement for prediction forks and samples (#55463)

## What changed

Add `experimentalDisablePredictionMcp` to `thread/fork` and `disableMcp` to `thread/sample`. Disable MCP servers when requested and the inherited history contains incremental tools on Responses Lite. The fork option requires `experimentalPredictionMode: true`.

Apply disablement through a thread-local policy that excludes configured MCP servers and runtime contributions, including after MCP refresh. Preserve the inherited history prefix and append tool-removal instructions without changing the parent's tools.

## Testing

Add coverage for prediction forks with legacy and paginated history, flag validation, conditional disablement, MCP refresh, and sample/fork request parity. Add a request-history snapshot covering preserved history, tool removal across child turns, and unchanged parent tools.

GitOrigin-RevId: e31d602480e6550ed130d8152f538b8668280fa0
This commit is contained in:
liame-oai
2026-10-20 05:25:43 +00:00
committed by copyberry
parent 18795fc125
commit 014489466a
12 changed files with 495 additions and 18 deletions
@@ -553,6 +553,12 @@ pub struct ThreadForkParams {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub experimental_prediction_mode: bool,
/// Disable MCP servers in prediction mode when the inherited window uses incremental tools on Responses Lite.
/// Requires `experimentalPredictionMode: true`.
#[experimental("thread/fork.experimentalDisablePredictionMcp")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub experimental_disable_prediction_mcp: bool,
/// Optional last turn id to fork through, inclusive.
///
/// When specified, turns after `last_turn_id` are omitted from the fork.
@@ -20,6 +20,9 @@ pub struct ThreadSampleParams {
pub effort: Option<ReasoningEffort>,
#[ts(optional = nullable)]
pub turn_trigger: Option<String>,
/// Disable MCP servers when the inherited window uses incremental tools on Responses Lite.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub disable_mcp: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
@@ -5135,6 +5135,11 @@ impl ThreadRequestProcessor {
app_server_client_version: Option<String>,
client_mcp_extensions: ClientMcpExtensions,
) -> Result<(), JSONRPCErrorError> {
if params.experimental_disable_prediction_mcp && !params.experimental_prediction_mode {
return Err(invalid_request(
"`experimentalDisablePredictionMcp` requires `experimentalPredictionMode: true`",
));
}
let has_config_overrides = params.model.is_some()
|| params.model_provider.is_some()
|| params.service_tier.is_some()
@@ -5163,6 +5168,7 @@ impl ThreadRequestProcessor {
let ThreadForkParams {
thread_id,
experimental_prediction_mode,
experimental_disable_prediction_mcp,
last_turn_id,
before_turn_id,
path,
@@ -5203,6 +5209,11 @@ impl ThreadRequestProcessor {
/*include_history*/ false,
)
.await?;
let loaded_parent = self
.thread_manager
.get_thread(source_thread.thread_id)
.await
.ok();
let inherited_fork = if experimental_prediction_mode {
// Keep inherited state and its preparation off the fork handler's stack.
let (mut options, settings) = Box::pin(async {
@@ -5236,6 +5247,25 @@ impl ThreadRequestProcessor {
options
.thread_extension_init
.insert(codex_core::compact::AutoCompactionDisabled);
if experimental_disable_prediction_mcp
&& let Some(parent) = loaded_parent.as_ref()
&& parent
.conversation_history_snapshot()
.await
.items()
.any(|item| matches!(item, ResponseItem::AdditionalTools { .. }))
&& let Some(model) = options.config.model.as_deref()
&& self
.thread_manager
.get_models_manager()
.get_model_info(model, &options.config.to_models_manager_config())
.await
.use_responses_lite
{
options
.thread_extension_init
.insert(codex_core::McpServersDisabled);
}
options
.config
.features
@@ -5375,7 +5405,6 @@ impl ThreadRequestProcessor {
!has_permission_override(request_overrides.as_ref(), &typesafe_overrides);
let needs_latest_settings =
restore_approval_policy || restore_approvals_reviewer || restore_permission_profile;
let loaded_parent = self.thread_manager.get_thread(source_thread_id).await.ok();
let loaded_parent_settings = if paginated_source && needs_latest_settings {
if let Some(parent) = loaded_parent.as_ref() {
let snapshot = parent.thread_settings_snapshot().await;
+12 -4
View File
@@ -123,10 +123,8 @@ impl SampleTask {
"sample source must be a persistent root thread",
));
}
let revision = parent
.conversation_history_snapshot()
.await
.history_version();
let parent_history = parent.conversation_history_snapshot().await;
let revision = parent_history.history_version();
parent
.flush_rollout()
.await
@@ -191,6 +189,16 @@ impl SampleTask {
options
.thread_extension_init
.insert(codex_core::compact::AutoCompactionDisabled);
if params.disable_mcp
&& model.use_responses_lite
&& parent_history
.items()
.any(|item| matches!(item, ResponseItem::AdditionalTools { .. }))
{
options
.thread_extension_init
.insert(codex_core::McpServersDisabled);
}
options.thread_extension_init.insert(SampleThread);
let effort = params.effort.filter(|_| {
model.supports_reasoning_effort_updates && options.config.model_provider.is_openai()
@@ -21,6 +21,7 @@ use codex_app_server_protocol::DynamicToolSpec;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::McpServerRefreshResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxMode;
use codex_app_server_protocol::SandboxPolicy;
@@ -61,7 +62,9 @@ use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::ConfigBuilder;
use codex_core::test_support::all_model_presets;
use codex_core::test_support::construct_model_info_offline;
use codex_features::Feature;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_protocol::ThreadId;
@@ -2922,6 +2925,175 @@ async fn inheriting_fork_preserves_parent_config(history_mode: ThreadHistoryMode
Ok(())
}
#[test_case(ThreadHistoryMode::Legacy, false, true, true; "prediction fork retains MCPs")]
#[test_case(ThreadHistoryMode::Legacy, true, true, true; "disable MCPs in prediction fork")]
#[test_case(ThreadHistoryMode::Paginated, true, true, true; "disable MCPs in paginated fork")]
#[test_case(ThreadHistoryMode::Legacy, true, false, true; "retain MCPs without incremental tools")]
#[test_case(ThreadHistoryMode::Legacy, true, true, false; "retain MCPs without Responses Lite")]
#[tokio::test]
async fn fork_option_disables_mcp_servers(
history_mode: ThreadHistoryMode,
disable_mcp: bool,
incremental_tools: bool,
responses_lite: bool,
) -> Result<()> {
let server = responses::start_mock_server().await;
let mock = responses::mount_sse_sequence(
&server,
["parent", "child", "refreshed-child", "continued-parent"]
.into_iter()
.map(|id| responses::sse(vec![responses::ev_completed(id)]))
.collect(),
)
.await;
let mcp_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/mcp"))
.respond_with(|request: &wiremock::Request| {
let request: Value =
serde_json::from_slice(&request.body).expect("MCP request must be valid JSON");
let result = match request["method"].as_str() {
Some("initialize") => json!({
"protocolVersion": "2025-11-25",
"capabilities": { "tools": {} },
"serverInfo": { "name": "fixture", "version": "1.0.0" }
}),
Some("tools/list") => json!({ "tools": [{
"name": "prediction_mcp_lookup",
"description": "Look up a test value.",
"inputSchema": { "type": "object", "properties": {} }
}] }),
Some("resources/list") => json!({ "resources": [] }),
Some("resources/templates/list") => json!({ "resourceTemplates": [] }),
_ => return ResponseTemplate::new(202),
};
ResponseTemplate::new(200).set_body_json(json!({
"jsonrpc": "2.0", "id": request["id"], "result": result
}))
})
.mount(&mcp_server)
.await;
let codex_home = TempDir::new()?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.loader_overrides(codex_config::LoaderOverrides::without_managed_config_for_tests())
.build()
.await?;
let mut model = construct_model_info_offline("gpt-5.4", &config);
model.use_responses_lite = responses_lite;
model.supports_search_tool = false;
model.tool_mode = Some(codex_protocol::openai_models::ToolMode::Direct);
let catalog = codex_home.path().join("catalog.json");
std::fs::write(
&catalog,
serde_json::to_string(&json!({"models": [model]}))?,
)?;
MockResponsesConfig::new(&server.uri())
.with_model("gpt-5.4")
.with_root_config(&format!(
"model_catalog_json = {}\nfeatures.incremental_tools = {incremental_tools}",
serde_json::to_string(&catalog)?
))
.with_extra_config(&format!(
"[mcp_servers.fixture]\nurl = \"{}/mcp\"\nrequired = true\n",
mcp_server.uri()
))
.write(codex_home.path())?;
let mut app = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let parent = app
.start_thread(ThreadStartParams {
history_mode: Some(history_mode),
..Default::default()
})
.await?;
app.start_turn_and_wait_for_completion(TurnStartParams {
thread_id: parent.thread.id.clone(),
input: vec![UserInput::Text {
text: "Initial turn".into(),
text_elements: vec![],
}],
..Default::default()
})
.await?;
let child: ThreadForkResponse = app
.request(|request_id| ClientRequest::ThreadFork {
request_id,
params: ThreadForkParams {
thread_id: parent.thread.id.clone(),
experimental_prediction_mode: true,
experimental_disable_prediction_mcp: disable_mcp,
ephemeral: true,
exclude_turns: true,
..Default::default()
},
})
.await?;
let removes_mcp = disable_mcp && incremental_tools && responses_lite;
for (index, thread_id) in [&child.thread.id, &child.thread.id, &parent.thread.id]
.into_iter()
.enumerate()
{
if index == 1 {
let initializations = mcp_server
.received_requests()
.await
.expect("MCP request recording must be enabled")
.iter()
.filter(|request| request.method.as_str() == "POST")
.filter(|request| {
serde_json::from_slice::<Value>(&request.body)
.expect("MCP request must be valid JSON")["method"]
== "initialize"
})
.count();
if removes_mcp {
assert_eq!(initializations, 1, "disabled fork must not connect MCPs");
}
let _: McpServerRefreshResponse = app
.request(|request_id| ClientRequest::McpServerRefresh {
request_id,
params: None,
})
.await?;
}
app.start_turn_and_wait_for_completion(TurnStartParams {
thread_id: thread_id.clone(),
input: vec![UserInput::Text {
text: "Continue".into(),
text_elements: vec![],
}],
..Default::default()
})
.await?;
}
let requests = mock.requests();
assert!(
requests[0]
.body_json()
.to_string()
.contains("prediction_mcp_lookup")
);
assert_eq!(
requests
.iter()
.map(|request| request.body_json()["input"]
.as_array()
.expect("input")
.iter()
.any(|item| {
let text = item.to_string();
text.contains("namespaces are no longer available")
&& text.contains("mcp__fixture")
}))
.collect::<Vec<_>>(),
vec![false, removes_mcp, removes_mcp, false],
);
Ok(())
}
#[test_case(ThreadHistoryMode::Legacy, false; "fresh persistent legacy")]
#[test_case(ThreadHistoryMode::Legacy, true; "ephemeral legacy")]
#[test_case(ThreadHistoryMode::Paginated, false; "fresh persistent paginated")]
@@ -3020,8 +3192,14 @@ async fn inheriting_fork_requires_loaded_parent() -> Result<()> {
Ok(())
}
#[test_case(true, false, "`experimentalPredictionMode` requires `ephemeral: true`"; "ephemeral child")]
#[test_case(false, true, "`experimentalDisablePredictionMcp` requires `experimentalPredictionMode: true`"; "MCP disablement requires prediction mode")]
#[tokio::test]
async fn inheriting_fork_requires_ephemeral_child() -> Result<()> {
async fn inheriting_fork_rejects_invalid_prediction_flags(
prediction_mode: bool,
disable_mcp: bool,
expected_error: &str,
) -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -3030,7 +3208,8 @@ async fn inheriting_fork_requires_ephemeral_child() -> Result<()> {
let fork_id = mcp
.send_thread_fork_request(ThreadForkParams {
thread_id: "not-loaded".into(),
experimental_prediction_mode: true,
experimental_prediction_mode: prediction_mode,
experimental_disable_prediction_mcp: disable_mcp,
..Default::default()
})
.await?;
@@ -3039,10 +3218,7 @@ async fn inheriting_fork_requires_ephemeral_child() -> Result<()> {
mcp.read_stream_until_error_message(RequestId::Integer(fork_id)),
)
.await??;
assert_eq!(
error.error.message,
"`experimentalPredictionMode` requires `ephemeral: true`",
);
assert_eq!(error.error.message, expected_error);
Ok(())
}
@@ -264,6 +264,7 @@ async fn sample_preserves_context_and_keeps_source_unchanged(
prompt: "Continue identically".to_string(),
effort: Some(ReasoningEffort::Medium),
turn_trigger: Some("composer_predictions".to_string()),
..Default::default()
},
)
.await?;
@@ -589,8 +590,14 @@ async fn sample_returns_null_for_custom_tool_calls() -> Result<()> {
Ok(())
}
#[test_case(false, true; "inherit MCPs by default")]
#[test_case(true, true; "disable MCPs with incremental tools")]
#[test_case(true, false; "retain MCPs without incremental tools")]
#[tokio::test]
async fn sample_blocks_mcp_execution_without_changing_parent_tools() -> Result<()> {
async fn sample_blocks_mcp_execution_without_changing_parent_tools(
disable_mcp: bool,
incremental_tools: bool,
) -> Result<()> {
let server = responses::start_mock_server().await;
let mcp = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("POST"))
@@ -654,13 +661,24 @@ async fn sample_blocks_mcp_execution_without_changing_parent_tools() -> Result<(
&extra_config,
)
.await?;
let (parent_id, turn_id) = ready_parent(&mut client, ThreadStartParams::default()).await?;
let (parent_id, turn_id) = ready_parent(
&mut client,
ThreadStartParams {
config: Some(std::collections::HashMap::from([(
"features.incremental_tools".to_string(),
json!(incremental_tools),
)])),
..Default::default()
},
)
.await?;
let response = sample(
&mut client,
ThreadSampleParams {
thread_id: parent_id.clone(),
expected_turn_id: turn_id,
prompt: "Call the tool again".to_string(),
disable_mcp,
..Default::default()
},
)
@@ -669,20 +687,40 @@ async fn sample_blocks_mcp_execution_without_changing_parent_tools() -> Result<(
(response.text, response.response_id),
(None, "sample-tool".to_string())
);
let fork = prediction_fork(&mut client, &parent_id).await?;
let fork_id = client
.send_thread_fork_request(ThreadForkParams {
thread_id: parent_id,
experimental_prediction_mode: true,
experimental_disable_prediction_mcp: disable_mcp,
ephemeral: true,
exclude_turns: true,
..Default::default()
})
.await?;
let fork: ThreadForkResponse = timeout(WAIT, client.read_response(fork_id)).await??;
client
.start_turn_and_wait_for_completion(TurnStartParams {
thread_id: fork,
thread_id: fork.thread.id,
input: input("Call the tool again"),
..Default::default()
})
.await?;
let requests = mock.requests();
assert_eq!(requests.len(), 4);
let parent = comparable_request(requests[1].body_json());
let sample = comparable_request(requests[2].body_json());
let prefix = parent["input"].as_array().expect("parent input");
let sample_input = sample["input"].as_array().expect("sample input");
assert_eq!(&sample_input[..prefix.len()], prefix);
let removes_mcp = disable_mcp && incremental_tools;
assert_eq!(
comparable_request(requests[2].body_json()),
comparable_request(requests[3].body_json())
sample_input[prefix.len()..].iter().any(|item| {
let text = item.to_string();
text.contains("namespaces are no longer available") && text.contains("mcp__sample_test")
}),
removes_mcp
);
assert_eq!(sample, comparable_request(requests[3].body_json()));
assert_eq!(
requests[2].body_json()["tools"],
requests[1].body_json()["tools"]
+1
View File
@@ -108,6 +108,7 @@ mod mcp_tool_approval_templates;
mod mcp_tool_exposure;
mod network_policy_decision;
pub use mcp::McpManager;
pub use mcp::McpServersDisabled;
mod original_image_detail;
pub use codex_mcp::CodexAppsToolsCache;
pub use codex_mcp::SandboxState;
+17
View File
@@ -42,6 +42,10 @@ use codex_protocol::protocol::TurnEnvironmentSelection;
const LEGACY_CODEX_APPS_REGISTRATION_ID: &str = "legacy_codex_apps";
/// Thread-local policy that excludes all MCP servers, including runtime contributions.
/// Insert into `StartThreadOptions::thread_extension_init` before starting the thread.
pub struct McpServersDisabled;
/// MCP configuration and capability availability derived from the same inputs.
#[derive(Clone)]
pub(crate) struct McpRuntimeProjection {
@@ -263,6 +267,19 @@ impl McpManager {
disabled_plugin_ids: &[String],
) -> McpRuntimeProjection {
let config = context.config();
if context
.thread_init()
.is_some_and(|init| init.get::<McpServersDisabled>().is_some())
{
let mut mcp_config = config.to_mcp_config_with_loaded_plugins(&Default::default(), []);
mcp_config.mcp_server_catalog = Default::default();
mcp_config.apps_enabled = false;
return McpRuntimeProjection {
config: mcp_config,
plugins_available: false,
selected_plugins: Default::default(),
};
}
let plugins_config = &config.plugins;
let mut selected_plugin_connector_sources = Vec::new();
let mut selected_plugin_registrations = Vec::new();
+7 -1
View File
@@ -141,7 +141,13 @@ impl Session {
mut projection: McpRuntimeProjection,
) -> BoxFuture<'a, McpRuntimeProjection> {
Box::pin(async move {
if self.isolation == codex_extension_api::SessionIsolation::Isolated {
if self.isolation == codex_extension_api::SessionIsolation::Isolated
|| self
.services
.mcp_thread_init
.get::<crate::mcp::McpServersDisabled>()
.is_some()
{
return projection;
}
@@ -1,6 +1,9 @@
//! Request-history coverage for incremental tools on Responses Lite.
use anyhow::Result;
use codex_core::ForkSnapshot;
use codex_core::McpServersDisabled;
use codex_core::compact::AutoCompactionDisabled;
use codex_features::Feature;
use codex_otel::MetricsClient;
use codex_otel::MetricsConfig;
@@ -33,6 +36,106 @@ use test_case::test_case;
use super::super::rmcp_client::remote_aware_environment_id;
use super::super::rmcp_client::remote_aware_stdio_server_bin;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn prediction_fork_appends_mcp_removal_without_rewriting_history() -> Result<()> {
skip_if_wine_exec!(
Ok(()),
"requires a Windows test_stdio_server in the Wine-exec environment"
);
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let mcp = serde_json::from_value(json!({
"command": remote_aware_stdio_server_bin()?,
"environment_id": remote_aware_environment_id(),
"enabled_tools": ["echo"],
"required": true,
}))?;
let mut test = test_codex()
.with_model_info_override("gpt-5.4", |model| {
model.use_responses_lite = true;
model.supports_search_tool = false;
model.tool_mode = Some(ToolMode::Direct);
})
.with_config(move |config| {
config.workspace_roots = vec![config.cwd.clone()];
config.base_instructions = Some("Use the available tools to help the user.".into());
config
.features
.enable(Feature::IncrementalTools)
.expect("enable incremental tools");
config
.mcp_servers
.set(std::collections::HashMap::from([(
"echo_service".into(),
mcp,
)]))
.expect("set MCP server");
})
.build_with_auto_env(&server)
.await?;
wait_for_mcp_server(&test.codex, "echo_service").await?;
let mock = responses::mount_sse_sequence(
&server,
["parent", "child", "child-again", "parent-again"]
.into_iter()
.map(|id| responses::sse(vec![responses::ev_completed(id)]))
.collect(),
)
.await;
test.submit_text_turn("Start with the echo service.")
.await?;
let parent = std::sync::Arc::clone(&test.codex);
parent.flush_rollout().await?;
let (mut options, settings) = test
.thread_manager
.fork_options_from_parent(test.session_configured.thread_id)
.await?;
// Match experimentalPredictionMode with experimentalDisablePredictionMcp enabled.
options.config.ephemeral = true;
options
.config
.features
.enable(Feature::ReasoningEffortOverride)?;
options.thread_extension_init.insert(AutoCompactionDisabled);
options.thread_extension_init.insert(McpServersDisabled);
let child = test
.thread_manager
.fork_legacy_thread(
ForkSnapshot::Interrupted,
options,
parent.rollout_path().expect("parent rollout"),
)
.await?;
child.thread.restore_thread_settings(settings).await?;
test.codex = child.thread;
test.submit_text_turn("Predict the next message.").await?;
test.submit_text_turn("Predict again.").await?;
test.codex = parent;
test.submit_text_turn("Continue using the echo service.")
.await?;
let requests = mock.requests();
let parent = requests[0].body_json();
let child = requests[1].body_json();
let prefix = parent["input"].as_array().expect("parent input");
assert_eq!(
&child["input"].as_array().expect("child input")[..prefix.len()],
prefix
);
insta::assert_snapshot!(
"prediction_fork_mcp_removal",
context_snapshot::format_request_history_snapshot(
"The prediction fork preserves the incremental parent's complete prefix, then appends MCP removal. Its second turn retains that update; continuing the parent keeps its original tools.",
&requests,
&ContextSnapshotOptions::default()
.rewrite_known_segments()
.include_request_settings(),
)
);
Ok(())
}
#[test_case(true; "responses_lite")]
#[test_case(false; "responses_api")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -0,0 +1,90 @@
---
source: core/tests/suite/scenarios_incremental_tools.rs
expression: "context_snapshot::format_request_history_snapshot(\"The prediction fork preserves the incremental parent's complete prefix, then appends MCP removal. Its second turn retains that update; continuing the parent keeps its original tools.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
---
Scenario: The prediction fork preserves the incremental parent's complete prefix, then appends MCP removal. Its second turn retains that update; continuing the parent keeps its original tools.
## Window 1
Settings:
include: ["reasoning.encrypted_content"]
model: "gpt-5.4"
parallel_tool_calls: false
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
reasoning: {"context":"all_turns","effort":"medium"}
store: false
stream: true
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:additional_tools/developer (3; hash=94348ab8527021ff):
- namespace/functions; hash=124fee69a69ed6b2
- function/exec_command
- function/write_stdin
- function/list_mcp_resources
- function/list_mcp_resource_templates
- function/read_mcp_resource
- function/request_user_input
- custom/apply_patch
- function/view_image
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
- function/close_agent
- function/resume_agent
- function/send_input
- function/spawn_agent
- function/wait_agent
- namespace/mcp__echo_service: Use these tools to exercise the rmcp test server.; hash=6650953285437e87
- function/echo
01:message/developer:
<MODEL_INSTRUCTIONS>
02:message/developer:
<PERMISSIONS_INSTRUCTIONS>
03:message/user:
<ENVIRONMENT_CONTEXT>
04:message/user:
Start with the echo service.
-- request 2 (turn) --
05:message/developer:
The following namespaces are no longer available. Do not call tools in them unless those tools are declared in a later update:
- mcp__echo_service
The following tools are no longer available. Do not call them:
- functions.list_mcp_resource_templates
- functions.list_mcp_resources
- functions.read_mcp_resource
06:message/user:
Predict the next message.
-- request 3 (turn) --
07:message/user:
Predict again.
## Window 2 (after request 3: input diverged at item 05)
Settings: same as window 1
-- request 4 (turn) --
00:additional_tools/developer (3; hash=94348ab8527021ff):
- namespace/functions; hash=124fee69a69ed6b2
- function/exec_command
- function/write_stdin
- function/list_mcp_resources
- function/list_mcp_resource_templates
- function/read_mcp_resource
- function/request_user_input
- custom/apply_patch
- function/view_image
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
- function/close_agent
- function/resume_agent
- function/send_input
- function/spawn_agent
- function/wait_agent
- namespace/mcp__echo_service: Use these tools to exercise the rmcp test server.; hash=6650953285437e87
- function/echo
01:message/developer:
<MODEL_INSTRUCTIONS>
02:message/developer:
<PERMISSIONS_INSTRUCTIONS>
03:message/user:
<ENVIRONMENT_CONTEXT>
04:message/user:
Start with the echo service.
05:message/user:
Continue using the echo service.