Add output_schema to code mode render (#17210)

This updates code-mode tool rendering so MCP tools can surface
structured output types from their `outputSchema`.

What changed:
- Detect MCP tool-call result wrappers from the output schema shape
instead of relying on tool-name parsing or provenance flags.
- Render shared TypeScript aliases once for MCP tool results
(`CallToolResult`, `ContentBlock`, etc.) so multiple MCP tool
declarations stay compact.
- Type `structuredContent` from the tool definition's `outputSchema`
instead of rendering it as `unknown`.
- Update the shared MCP aliases to match the MCP draft `CallToolResult`
schema more closely.

Example:
- Before: `declare const tools: { mcp__rmcp__echo(args: { env_var?:
string; message: string; }): Promise<{ _meta?: unknown; content:
Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };`
- After: `declare const tools: { mcp__rmcp__echo(args: { env_var?:
string; message: string; }): Promise<CallToolResult<{ echo: string; env:
string | null; }>>; };`
This commit is contained in:
Vivian Fang
2026-04-10 04:41:44 -07:00
committed by GitHub
parent 1de0085418
commit 7bbe3b6011
12 changed files with 516 additions and 49 deletions

View File

@@ -39,6 +39,83 @@ const WAIT_DESCRIPTION_TEMPLATE: &str = r#"- Use `wait` only after `exec` return
- `wait` returns only the new output since the last yield, or the final completion or termination result for that cell.
- If the cell is still running, `wait` may yield again with the same `cell_id`.
- If the cell has already finished, `wait` returns the completed result and closes the cell."#;
// Based off of https://modelcontextprotocol.io/specification/draft/schema#calltoolresult
const MCP_TYPESCRIPT_PREAMBLE: &str = r#"type Role = "user" | "assistant";
type MetaObject = Record<string, unknown>;
type Annotations = {
audience?: Role[];
priority?: number;
lastModified?: string;
};
type Icon = {
src: string;
mimeType?: string;
sizes?: string[];
theme?: "light" | "dark";
};
type TextResourceContents = {
uri: string;
mimeType?: string;
_meta?: MetaObject;
text: string;
};
type BlobResourceContents = {
uri: string;
mimeType?: string;
_meta?: MetaObject;
blob: string;
};
type TextContent = {
type: "text";
text: string;
annotations?: Annotations;
_meta?: MetaObject;
};
type ImageContent = {
type: "image";
data: string;
mimeType: string;
annotations?: Annotations;
_meta?: MetaObject;
};
type AudioContent = {
type: "audio";
data: string;
mimeType: string;
annotations?: Annotations;
_meta?: MetaObject;
};
type ResourceLink = {
icons?: Icon[];
name: string;
title?: string;
uri: string;
description?: string;
mimeType?: string;
annotations?: Annotations;
size?: number;
_meta?: MetaObject;
type: "resource_link";
};
type EmbeddedResource = {
type: "resource";
resource: TextResourceContents | BlobResourceContents;
annotations?: Annotations;
_meta?: MetaObject;
};
type ContentBlock =
| TextContent
| ImageContent
| AudioContent
| ResourceLink
| EmbeddedResource;
type CallToolResult<TStructured = { [key: string]: unknown }> = {
_meta?: MetaObject;
content: ContentBlock[];
isError?: boolean;
structuredContent?: TStructured;
[key: string]: unknown;
};"#;
pub const CODE_MODE_PRAGMA_PREFIX: &str = "// @exec:";
@@ -169,7 +246,7 @@ pub fn is_code_mode_nested_tool(tool_name: &str) -> bool {
}
pub fn build_exec_tool_description(
enabled_tools: &[(String, String)],
enabled_tools: &[ToolDefinition],
namespace_descriptions: &BTreeMap<String, ToolNamespaceDescription>,
code_mode_only: bool,
) -> String {
@@ -185,8 +262,13 @@ pub fn build_exec_tool_description(
if !enabled_tools.is_empty() {
let mut current_namespace: Option<&str> = None;
let mut nested_tool_sections = Vec::with_capacity(enabled_tools.len());
let has_mcp_tools = enabled_tools
.iter()
.any(|tool| mcp_structured_content_schema(tool.output_schema.as_ref()).is_some());
for (name, nested_description) in enabled_tools {
for tool in enabled_tools {
let name = tool.name.as_str();
let nested_description = render_code_mode_sample_for_definition(tool);
let next_namespace = namespace_descriptions
.get(name)
.map(|namespace_description| namespace_description.name.as_str());
@@ -206,14 +288,20 @@ pub fn build_exec_tool_description(
let global_name = normalize_code_mode_identifier(name);
let nested_description = nested_description.trim();
if nested_description.is_empty() {
nested_tool_sections.push(format!("### `{global_name}` (`{name}`)"));
nested_tool_sections.push(render_tool_heading(&global_name, name));
} else {
nested_tool_sections.push(format!(
"### `{global_name}` (`{name}`)\n{nested_description}"
"{}\n{nested_description}",
render_tool_heading(&global_name, name)
));
}
}
if has_mcp_tools {
sections.push(format!(
"Shared MCP Types:\n```ts\n{MCP_TYPESCRIPT_PREAMBLE}\n```"
));
}
let nested_tool_reference = nested_tool_sections.join("\n\n");
sections.push(nested_tool_reference);
}
@@ -251,7 +339,7 @@ pub fn normalize_code_mode_identifier(tool_key: &str) -> String {
pub fn augment_tool_definition(mut definition: ToolDefinition) -> ToolDefinition {
if definition.name != PUBLIC_TOOL_NAME {
definition.description = append_code_mode_sample_for_definition(&definition);
definition.description = render_code_mode_sample_for_definition(&definition);
}
definition
}
@@ -273,7 +361,7 @@ pub struct EnabledToolMetadata {
pub kind: CodeModeToolKind,
}
pub fn append_code_mode_sample(
pub fn render_code_mode_sample(
description: &str,
tool_name: &str,
input_name: &str,
@@ -287,7 +375,7 @@ pub fn append_code_mode_sample(
format!("{description}\n\nexec tool declaration:\n```ts\n{declaration}\n```")
}
fn append_code_mode_sample_for_definition(definition: &ToolDefinition) -> String {
fn render_code_mode_sample_for_definition(definition: &ToolDefinition) -> String {
let input_name = match definition.kind {
CodeModeToolKind::Function => "args",
CodeModeToolKind::Freeform => "input",
@@ -300,12 +388,23 @@ fn append_code_mode_sample_for_definition(definition: &ToolDefinition) -> String
.unwrap_or_else(|| "unknown".to_string()),
CodeModeToolKind::Freeform => "string".to_string(),
};
let output_type = definition
.output_schema
.as_ref()
.map(render_json_schema_to_typescript)
.unwrap_or_else(|| "unknown".to_string());
append_code_mode_sample(
let output_type = if let Some(structured_content_schema) =
mcp_structured_content_schema(definition.output_schema.as_ref())
{
let structured_content_type = render_json_schema_to_typescript(structured_content_schema);
if structured_content_type == "unknown" {
"CallToolResult".to_string()
} else {
format!("CallToolResult<{structured_content_type}>")
}
} else {
definition
.output_schema
.as_ref()
.map(render_json_schema_to_typescript)
.unwrap_or_else(|| "unknown".to_string())
};
render_code_mode_sample(
&definition.description,
&definition.name,
input_name,
@@ -324,10 +423,59 @@ fn render_code_mode_tool_declaration(
format!("{tool_name}({input_name}: {input_type}): Promise<{output_type}>;")
}
fn render_tool_heading(global_name: &str, raw_name: &str) -> String {
if global_name == raw_name {
format!("### `{global_name}`")
} else {
format!("### `{global_name}` (`{raw_name}`)")
}
}
pub fn render_json_schema_to_typescript(schema: &JsonValue) -> String {
render_json_schema_to_typescript_inner(schema)
}
fn mcp_structured_content_schema(output_schema: Option<&JsonValue>) -> Option<&JsonValue> {
let output_schema = output_schema?;
let properties = output_schema
.get("properties")
.and_then(JsonValue::as_object)?;
let content_schema = properties.get("content").and_then(JsonValue::as_object)?;
if content_schema.get("type").and_then(JsonValue::as_str) != Some("array") {
return None;
}
if content_schema
.get("items")
.and_then(JsonValue::as_object)
.is_none_or(|items| items.get("type").and_then(JsonValue::as_str) != Some("object"))
{
return None;
}
if properties
.get("isError")
.and_then(JsonValue::as_object)
.is_none_or(|schema| schema.get("type").and_then(JsonValue::as_str) != Some("boolean"))
{
return None;
}
if properties
.get("_meta")
.and_then(JsonValue::as_object)
.is_none_or(|schema| schema.get("type").and_then(JsonValue::as_str) != Some("object"))
{
return None;
}
Some(
properties
.get("structuredContent")
.unwrap_or(&JsonValue::Bool(true)),
)
}
fn render_json_schema_to_typescript_inner(schema: &JsonValue) -> String {
match schema {
JsonValue::Bool(true) => "unknown".to_string(),
@@ -559,9 +707,29 @@ mod tests {
use super::normalize_code_mode_identifier;
use super::parse_exec_source;
use pretty_assertions::assert_eq;
use serde_json::Value as JsonValue;
use serde_json::json;
use std::collections::BTreeMap;
fn mcp_call_tool_result_schema(structured_content_schema: JsonValue) -> JsonValue {
json!({
"type": "object",
"properties": {
"content": {
"type": "array",
"items": {
"type": "object"
}
},
"structuredContent": structured_content_schema,
"isError": { "type": "boolean" },
"_meta": { "type": "object" }
},
"required": ["content"],
"additionalProperties": false
})
}
#[test]
fn parse_exec_source_without_pragma() {
assert_eq!(
@@ -676,11 +844,20 @@ mod tests {
#[test]
fn code_mode_only_description_includes_nested_tools() {
let description = build_exec_tool_description(
&[("foo".to_string(), "bar".to_string())],
&[ToolDefinition {
name: "foo".to_string(),
description: "bar".to_string(),
kind: CodeModeToolKind::Function,
input_schema: None,
output_schema: None,
}],
&BTreeMap::new(),
/*code_mode_only*/ true,
);
assert!(description.contains("### `foo` (`foo`)"));
assert!(description.contains(
"### `foo`
bar"
));
}
#[test]
@@ -711,22 +888,47 @@ mod tests {
]);
let description = build_exec_tool_description(
&[
("mcp__sample__alpha".to_string(), "First tool".to_string()),
("mcp__sample__beta".to_string(), "Second tool".to_string()),
ToolDefinition {
name: "mcp__sample__alpha".to_string(),
description: "First tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: Some(json!({
"type": "object",
"properties": {},
"additionalProperties": false
})),
output_schema: Some(mcp_call_tool_result_schema(json!({
"type": "object",
"properties": {},
"additionalProperties": false
}))),
},
ToolDefinition {
name: "mcp__sample__beta".to_string(),
description: "Second tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: Some(json!({
"type": "object",
"properties": {},
"additionalProperties": false
})),
output_schema: Some(mcp_call_tool_result_schema(json!({
"type": "object",
"properties": {},
"additionalProperties": false
}))),
},
],
&namespace_descriptions,
/*code_mode_only*/ true,
);
assert_eq!(description.matches("## mcp__sample").count(), 1);
assert!(description.contains("## mcp__sample\nShared namespace guidance."));
assert!(description.contains(
r#"## mcp__sample
Shared namespace guidance.
### `mcp__sample__alpha` (`mcp__sample__alpha`)
First tool
### `mcp__sample__beta` (`mcp__sample__beta`)
Second tool"#
"declare const tools: { mcp__sample__alpha(args: {}): Promise<CallToolResult<{}>>; };"
));
assert!(description.contains(
"declare const tools: { mcp__sample__beta(args: {}): Promise<CallToolResult<{}>>; };"
));
}
@@ -740,12 +942,125 @@ Second tool"#
},
)]);
let description = build_exec_tool_description(
&[("mcp__sample__alpha".to_string(), "First tool".to_string())],
&[ToolDefinition {
name: "mcp__sample__alpha".to_string(),
description: "First tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: Some(json!({
"type": "object",
"properties": {},
"additionalProperties": false
})),
output_schema: Some(mcp_call_tool_result_schema(json!({
"type": "object",
"properties": {},
"additionalProperties": false
}))),
}],
&namespace_descriptions,
/*code_mode_only*/ true,
);
assert!(!description.contains("## mcp__sample"));
assert!(description.contains("### `mcp__sample__alpha` (`mcp__sample__alpha`)"));
assert!(description.contains("### `mcp__sample__alpha`"));
}
#[test]
fn code_mode_only_description_renders_shared_mcp_types_once() {
let first_tool = augment_tool_definition(ToolDefinition {
name: "mcp__sample__alpha".to_string(),
description: "First tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: Some(json!({
"type": "object",
"properties": {},
"additionalProperties": false
})),
output_schema: Some(json!({
"type": "object",
"properties": {
"content": {
"type": "array",
"items": {
"type": "object"
}
},
"structuredContent": {
"type": "object",
"properties": {
"echo": { "type": "string" }
},
"required": ["echo"],
"additionalProperties": false
},
"isError": { "type": "boolean" },
"_meta": { "type": "object" }
},
"required": ["content"],
"additionalProperties": false
})),
});
let second_tool = augment_tool_definition(ToolDefinition {
name: "mcp__sample__beta".to_string(),
description: "Second tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: Some(json!({
"type": "object",
"properties": {},
"additionalProperties": false
})),
output_schema: Some(json!({
"type": "object",
"properties": {
"content": {
"type": "array",
"items": {
"type": "object"
}
},
"structuredContent": {
"type": "object",
"properties": {
"count": { "type": "integer" }
},
"required": ["count"],
"additionalProperties": false
},
"isError": { "type": "boolean" },
"_meta": { "type": "object" }
},
"required": ["content"],
"additionalProperties": false
})),
});
let description = build_exec_tool_description(
&[
ToolDefinition {
name: first_tool.name,
description: "First tool".to_string(),
kind: first_tool.kind,
input_schema: first_tool.input_schema,
output_schema: first_tool.output_schema,
},
ToolDefinition {
name: second_tool.name,
description: "Second tool".to_string(),
kind: second_tool.kind,
input_schema: second_tool.input_schema,
output_schema: second_tool.output_schema,
},
],
&BTreeMap::new(),
/*code_mode_only*/ true,
);
assert_eq!(
description
.matches("type CallToolResult<TStructured = { [key: string]: unknown }>")
.count(),
1
);
assert_eq!(description.matches("Shared MCP Types:").count(), 1);
}
}

View File

@@ -7,13 +7,13 @@ pub use description::CODE_MODE_PRAGMA_PREFIX;
pub use description::CodeModeToolKind;
pub use description::ToolDefinition;
pub use description::ToolNamespaceDescription;
pub use description::append_code_mode_sample;
pub use description::augment_tool_definition;
pub use description::build_exec_tool_description;
pub use description::build_wait_tool_description;
pub use description::is_code_mode_nested_tool;
pub use description::normalize_code_mode_identifier;
pub use description::parse_exec_source;
pub use description::render_code_mode_sample;
pub use description::render_json_schema_to_typescript;
pub use response::FunctionCallOutputContentItem;
pub use response::ImageDetail;

View File

@@ -2280,7 +2280,14 @@ text(JSON.stringify(tool));
parsed,
serde_json::json!({
"name": "mcp__rmcp__echo",
"description": "Echo back the provided message and include environment data.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__rmcp__echo(args: { env_var?: string; message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };\n```",
"description": concat!(
"Echo back the provided message and include environment data.\n\n",
"exec tool declaration:\n",
"```ts\n",
"declare const tools: { mcp__rmcp__echo(args: { env_var?: string; message: string; }): ",
"Promise<CallToolResult<{ echo: string; env: string | null; }>>; };\n",
"```",
),
})
);

View File

@@ -45,11 +45,29 @@ impl TestToolServer {
}))
.expect("echo tool schema should deserialize");
Tool::new(
let mut tool = Tool::new(
Cow::Borrowed("echo"),
Cow::Borrowed("Echo back the provided message and include environment data."),
Arc::new(schema),
)
);
#[expect(clippy::expect_used)]
let output_schema: JsonObject = serde_json::from_value(json!({
"type": "object",
"properties": {
"echo": { "type": "string" },
"env": {
"anyOf": [
{ "type": "string" },
{ "type": "null" }
]
}
},
"required": ["echo", "env"],
"additionalProperties": false
}))
.expect("echo tool output schema should deserialize");
tool.output_schema = Some(Arc::new(output_schema));
tool
}
}

View File

@@ -91,6 +91,23 @@ impl TestToolServer {
Cow::Borrowed(description),
Arc::new(schema),
);
#[expect(clippy::expect_used)]
let output_schema: JsonObject = serde_json::from_value(json!({
"type": "object",
"properties": {
"echo": { "type": "string" },
"env": {
"anyOf": [
{ "type": "string" },
{ "type": "null" }
]
}
},
"required": ["echo", "env"],
"additionalProperties": false
}))
.expect("echo tool output schema should deserialize");
tool.output_schema = Some(Arc::new(output_schema));
tool.annotations = Some(ToolAnnotations::new().read_only(true));
tool
}

View File

@@ -90,6 +90,23 @@ impl TestToolServer {
Cow::Borrowed("Echo back the provided message and include environment data."),
Arc::new(schema),
);
#[expect(clippy::expect_used)]
let output_schema: JsonObject = serde_json::from_value(json!({
"type": "object",
"properties": {
"echo": { "type": "string" },
"env": {
"anyOf": [
{ "type": "string" },
{ "type": "null" }
]
}
},
"required": ["echo", "env"],
"additionalProperties": false
}))
.expect("echo tool output schema should deserialize");
tool.output_schema = Some(Arc::new(output_schema));
tool.annotations = Some(ToolAnnotations::new().read_only(true));
tool
}

View File

@@ -49,6 +49,19 @@ pub fn collect_code_mode_tool_definitions<'a>(
tool_definitions
}
pub fn collect_code_mode_exec_prompt_tool_definitions<'a>(
specs: impl IntoIterator<Item = &'a ToolSpec>,
) -> Vec<CodeModeToolDefinition> {
let mut tool_definitions = specs
.into_iter()
.filter_map(code_mode_tool_definition_for_spec)
.filter(|definition| codex_code_mode::is_code_mode_nested_tool(&definition.name))
.collect::<Vec<_>>();
tool_definitions.sort_by(|left, right| left.name.cmp(&right.name));
tool_definitions.dedup_by(|left, right| left.name == right.name);
tool_definitions
}
pub fn create_wait_tool() -> ToolSpec {
let properties = BTreeMap::from([
(
@@ -95,7 +108,7 @@ pub fn create_wait_tool() -> ToolSpec {
}
pub fn create_code_mode_tool(
enabled_tools: &[(String, String)],
enabled_tools: &[CodeModeToolDefinition],
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
code_mode_only_enabled: bool,
) -> ToolSpec {

View File

@@ -182,7 +182,13 @@ fn create_wait_tool_matches_expected_spec() {
#[test]
fn create_code_mode_tool_matches_expected_spec() {
let enabled_tools = vec![("update_plan".to_string(), "Update the plan".to_string())];
let enabled_tools = vec![codex_code_mode::ToolDefinition {
name: "update_plan".to_string(),
description: "Update the plan".to_string(),
kind: codex_code_mode::CodeModeToolKind::Function,
input_schema: None,
output_schema: None,
}];
assert_eq!(
create_code_mode_tool(

View File

@@ -44,6 +44,7 @@ pub use apply_patch_tool::ApplyPatchToolArgs;
pub use apply_patch_tool::create_apply_patch_freeform_tool;
pub use apply_patch_tool::create_apply_patch_json_tool;
pub use code_mode::augment_tool_spec_for_code_mode;
pub use code_mode::collect_code_mode_exec_prompt_tool_definitions;
pub use code_mode::collect_code_mode_tool_definitions;
pub use code_mode::create_code_mode_tool;
pub use code_mode::create_wait_tool;

View File

@@ -42,13 +42,17 @@ pub fn mcp_call_tool_result_output_schema(structured_content_schema: JsonValue)
"properties": {
"content": {
"type": "array",
"items": {}
"items": {
"type": "object"
}
},
"structuredContent": structured_content_schema,
"isError": {
"type": "boolean"
},
"_meta": {}
"_meta": {
"type": "object"
}
},
"required": ["content"],
"additionalProperties": false

View File

@@ -13,7 +13,7 @@ use crate::ToolSpec;
use crate::ToolsConfig;
use crate::ViewImageToolOptions;
use crate::WebSearchToolOptions;
use crate::collect_code_mode_tool_definitions;
use crate::collect_code_mode_exec_prompt_tool_definitions;
use crate::collect_tool_search_source_infos;
use crate::collect_tool_suggest_entries;
use crate::create_apply_patch_freeform_tool;
@@ -93,17 +93,14 @@ pub fn build_tool_registry_plan(
..params
},
);
let mut enabled_tools = collect_code_mode_tool_definitions(
let mut enabled_tools = collect_code_mode_exec_prompt_tool_definitions(
nested_plan
.specs
.iter()
.map(|configured_tool| &configured_tool.spec),
)
.into_iter()
.map(|tool| (tool.name, tool.description))
.collect::<Vec<_>>();
enabled_tools.sort_by(|(left_name, _), (right_name, _)| {
compare_code_mode_tool_names(left_name, right_name, &namespace_descriptions)
);
enabled_tools.sort_by(|left, right| {
compare_code_mode_tool_names(&left.name, &right.name, &namespace_descriptions)
});
plan.push_spec(
create_code_mode_tool(

View File

@@ -1608,7 +1608,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
exec tool declaration:
```ts
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<CallToolResult>; };
```"#
);
}
@@ -1694,7 +1694,7 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
assert!(description.contains(
r#"exec tool declaration:
```ts
declare const tools: { mcp__sample__fn(args: { open?: Array<{ lineno?: number | null; ref_id: string; }> | null; response_length?: "short" | "medium" | "long"; tagged_list?: Array<{ kind: "tagged"; scope: "one" | "two"; variant: "alpha" | "beta"; }> | null; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };
declare const tools: { mcp__sample__fn(args: { open?: Array<{ lineno?: number | null; ref_id: string; }> | null; response_length?: "short" | "medium" | "long"; tagged_list?: Array<{ kind: "tagged"; scope: "one" | "two"; variant: "alpha" | "beta"; }> | null; }): Promise<CallToolResult>; };
```"#
));
}
@@ -1769,8 +1769,8 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() {
assert!(description.starts_with(
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
));
assert!(description.contains("### `update_plan` (`update_plan`)"));
assert!(description.contains("### `view_image` (`view_image`)"));
assert!(description.contains("### `update_plan`"));
assert!(description.contains("### `view_image`"));
}
#[test]
@@ -1804,8 +1804,8 @@ fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only(
assert!(!description.starts_with(
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
));
assert!(!description.contains("### `update_plan` (`update_plan`)"));
assert!(!description.contains("### `view_image` (`view_image`)"));
assert!(!description.contains("### `update_plan`"));
assert!(!description.contains("### `view_image`"));
}
fn model_info() -> ModelInfo {
@@ -1919,6 +1919,78 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
}
}
#[test]
fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() {
let model_info = model_info();
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::CodeModeOnly);
features.enable(Feature::UnifiedExec);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let mut tool = mcp_tool(
"echo",
"Echo text",
serde_json::json!({
"type": "object",
"properties": {
"message": {"type": "string"}
},
"required": ["message"],
"additionalProperties": false
}),
);
tool.output_schema = Some(std::sync::Arc::new(rmcp::model::object(
serde_json::json!({
"type": "object",
"properties": {
"echo": {"type": "string"},
"env": {
"anyOf": [
{"type": "string"},
{"type": "null"}
]
}
},
"required": ["echo", "env"],
"additionalProperties": false
}),
)));
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::from([("mcp__sample__echo".to_string(), tool)])),
/*deferred_mcp_tools*/ None,
&[],
);
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
&find_tool(&tools, "mcp__sample__echo").spec
else {
panic!("expected function tool");
};
assert_eq!(
description,
r#"Echo text
exec tool declaration:
```ts
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<CallToolResult<{ echo: string; env: string | null; }>>; };
```"#
);
}
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
let slug = name.replace(' ', "-").to_lowercase();
DiscoverableTool::Connector(Box::new(AppInfo {