agent: Improve compatibility when using MCP servers with Gemini models (#28700)

WIP

Release Notes:

- agent: Improve compatibility when using MCPs with Gemini models
This commit is contained in:
Bennet Bo Fenner 2025-04-14 13:55:25 -06:00 committed by GitHub
parent 6c93d107c2
commit 2603f36737
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 307 additions and 103 deletions

View file

@ -844,13 +844,20 @@ impl Thread {
if model.supports_tools() { if model.supports_tools() {
request.tools = { request.tools = {
let mut tools = Vec::new(); let mut tools = Vec::new();
tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| { tools.extend(
LanguageModelRequestTool { self.tools()
name: tool.name(), .enabled_tools(cx)
description: tool.description(), .into_iter()
input_schema: tool.input_schema(model.tool_input_format()), .filter_map(|tool| {
} // Skip tools that cannot be supported
})); let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
}),
);
tools tools
}; };

View file

@ -1,5 +1,6 @@
mod action_log; mod action_log;
mod tool_registry; mod tool_registry;
mod tool_schema;
mod tool_working_set; mod tool_working_set;
use std::fmt; use std::fmt;
@ -16,6 +17,7 @@ use project::Project;
pub use crate::action_log::*; pub use crate::action_log::*;
pub use crate::tool_registry::*; pub use crate::tool_registry::*;
pub use crate::tool_schema::*;
pub use crate::tool_working_set::*; pub use crate::tool_working_set::*;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
@ -51,8 +53,8 @@ pub trait Tool: 'static + Send + Sync {
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool; fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
/// Returns the JSON schema that describes the tool's input. /// Returns the JSON schema that describes the tool's input.
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
serde_json::Value::Object(serde_json::Map::default()) Ok(serde_json::Value::Object(serde_json::Map::default()))
} }
/// Returns markdown to be displayed in the UI for this tool. /// Returns markdown to be displayed in the UI for this tool.

View file

@ -0,0 +1,236 @@
use anyhow::Result;
use serde_json::Value;
use crate::LanguageModelToolSchemaFormat;
/// Tries to adapt a JSON schema representation to be compatible with the specified format.
///
/// If the json cannot be made compatible with the specified format, an error is returned.
pub fn adapt_schema_to_format(
json: &mut Value,
format: LanguageModelToolSchemaFormat,
) -> Result<()> {
match format {
LanguageModelToolSchemaFormat::JsonSchema => Ok(()),
LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
}
}
/// Tries to adapt the json schema so that it is compatible with https://ai.google.dev/api/caching#Schema
fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
if let Value::Object(obj) = json {
const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"];
for key in UNSUPPORTED_KEYS {
if obj.contains_key(key) {
return Err(anyhow::anyhow!(
"Schema cannot be made compatible because it contains \"{}\" ",
key
));
}
}
const KEYS_TO_REMOVE: [&str; 2] = ["format", "$schema"];
for key in KEYS_TO_REMOVE {
obj.remove(key);
}
if let Some(default) = obj.get("default") {
let is_null = default.is_null();
// Default is not supported, so we need to remove it
obj.remove("default");
if is_null {
obj.insert("nullable".to_string(), Value::Bool(true));
}
}
// If a type is not specified for an input parameter, add a default type
if obj.contains_key("description")
&& !obj.contains_key("type")
&& !(obj.contains_key("anyOf")
|| obj.contains_key("oneOf")
|| obj.contains_key("allOf"))
{
obj.insert("type".to_string(), Value::String("string".to_string()));
}
// Handle oneOf -> anyOf conversion
if let Some(subschemas) = obj.get_mut("oneOf") {
if subschemas.is_array() {
let subschemas_clone = subschemas.clone();
obj.remove("oneOf");
obj.insert("anyOf".to_string(), subschemas_clone);
}
}
// Recursively process all nested objects and arrays
for (_, value) in obj.iter_mut() {
if let Value::Object(_) | Value::Array(_) = value {
adapt_to_json_schema_subset(value)?;
}
}
} else if let Value::Array(arr) = json {
for item in arr.iter_mut() {
adapt_to_json_schema_subset(item)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_transform_default_null_to_nullable() {
let mut json = json!({
"description": "A test field",
"type": "string",
"default": null
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "string",
"nullable": true
})
);
}
#[test]
fn test_transform_adds_type_when_missing() {
let mut json = json!({
"description": "A test field without type"
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field without type",
"type": "string"
})
);
}
#[test]
fn test_transform_removes_format() {
let mut json = json!({
"description": "A test field",
"type": "integer",
"format": "uint32"
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "integer"
})
);
}
#[test]
fn test_transform_one_of_to_any_of() {
let mut json = json!({
"description": "A test field",
"oneOf": [
{ "type": "string" },
{ "type": "integer" }
]
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"anyOf": [
{ "type": "string" },
{ "type": "integer" }
]
})
);
}
#[test]
fn test_transform_nested_objects() {
let mut json = json!({
"type": "object",
"properties": {
"nested": {
"oneOf": [
{ "type": "string" },
{ "type": "null" }
],
"format": "email"
}
}
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"type": "object",
"properties": {
"nested": {
"anyOf": [
{ "type": "string" },
{ "type": "null" }
]
}
}
})
);
}
#[test]
fn test_transform_fails_if_unsupported_keys_exist() {
let mut json = json!({
"type": "object",
"properties": {
"$ref": "#/definitions/User",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
let mut json = json!({
"type": "object",
"properties": {
"if": "...",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
let mut json = json!({
"type": "object",
"properties": {
"then": "...",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
let mut json = json!({
"type": "object",
"properties": {
"else": "...",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
}
}

View file

@ -84,7 +84,7 @@ mod tests {
use super::*; use super::*;
#[gpui::test] #[gpui::test]
fn test_tool_schema_compatibility(cx: &mut App) { fn test_builtin_tool_schema_compatibility(cx: &mut App) {
crate::init( crate::init(
Arc::new(http_client::HttpClientWithUrl::new( Arc::new(http_client::HttpClientWithUrl::new(
FakeHttpClient::with_200_response(), FakeHttpClient::with_200_response(),
@ -95,18 +95,23 @@ mod tests {
); );
for tool in ToolRegistry::global(cx).tools() { for tool in ToolRegistry::global(cx).tools() {
let schema = let actual_schema = tool
tool.input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset); .input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset)
assert!(schema.is_object()); .unwrap();
if schema.as_object().unwrap().contains_key("$schema") { let mut expected_schema = actual_schema.clone();
let error_message = format!( assistant_tool::adapt_schema_to_format(
"Tool schema for `{}` is not compatible with `language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset` (Gemini Models).\n\ &mut expected_schema,
Are you using `schema::json_schema_for<T>(format)` to generate the schema?", language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset,
tool.name() )
); .unwrap();
panic!("{}", error_message) let error_message = format!(
} "Tool schema for `{}` is not compatible with `language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset` (Gemini Models).\n\
Are you using `schema::json_schema_for<T>(format)` to generate the schema?",
tool.name(),
);
assert_eq!(actual_schema, expected_schema, "{}", error_message)
} }
} }
} }

View file

@ -172,7 +172,7 @@ impl Tool for BatchTool {
IconName::Cog IconName::Cog
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<BatchToolInput>(format) json_schema_for::<BatchToolInput>(format)
} }

View file

@ -2,7 +2,7 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16}; use language::{self, Anchor, Buffer, ToPointUtf16};
use language_model::LanguageModelRequestMessage; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{self, LspAction, Project}; use project::{self, LspAction, Project};
use regex::Regex; use regex::Regex;
use schemars::JsonSchema; use schemars::JsonSchema;
@ -97,10 +97,7 @@ impl Tool for CodeActionTool {
IconName::Wand IconName::Wand
} }
fn input_schema( fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
&self,
format: language_model::LanguageModelToolSchemaFormat,
) -> serde_json::Value {
json_schema_for::<CodeActionToolInput>(format) json_schema_for::<CodeActionToolInput>(format)
} }

View file

@ -91,7 +91,7 @@ impl Tool for CodeSymbolsTool {
IconName::Code IconName::Code
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<CodeSymbolsInput>(format) json_schema_for::<CodeSymbolsInput>(format)
} }

View file

@ -55,7 +55,7 @@ impl Tool for CopyPathTool {
IconName::Clipboard IconName::Clipboard
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<CopyPathToolInput>(format) json_schema_for::<CopyPathToolInput>(format)
} }

View file

@ -45,7 +45,7 @@ impl Tool for CreateDirectoryTool {
IconName::Folder IconName::Folder
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<CreateDirectoryToolInput>(format) json_schema_for::<CreateDirectoryToolInput>(format)
} }

View file

@ -52,7 +52,7 @@ impl Tool for CreateFileTool {
IconName::FileCreate IconName::FileCreate
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<CreateFileToolInput>(format) json_schema_for::<CreateFileToolInput>(format)
} }

View file

@ -45,7 +45,7 @@ impl Tool for DeletePathTool {
IconName::FileDelete IconName::FileDelete
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<DeletePathToolInput>(format) json_schema_for::<DeletePathToolInput>(format)
} }

View file

@ -58,7 +58,7 @@ impl Tool for DiagnosticsTool {
IconName::XCircle IconName::XCircle
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<DiagnosticsToolInput>(format) json_schema_for::<DiagnosticsToolInput>(format)
} }

View file

@ -128,7 +128,7 @@ impl Tool for FetchTool {
IconName::Globe IconName::Globe
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<FetchToolInput>(format) json_schema_for::<FetchToolInput>(format)
} }

View file

@ -151,7 +151,7 @@ impl Tool for FindReplaceFileTool {
IconName::Pencil IconName::Pencil
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<FindReplaceFileToolInput>(format) json_schema_for::<FindReplaceFileToolInput>(format)
} }

View file

@ -56,7 +56,7 @@ impl Tool for ListDirectoryTool {
IconName::Folder IconName::Folder
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<ListDirectoryToolInput>(format) json_schema_for::<ListDirectoryToolInput>(format)
} }

View file

@ -54,7 +54,7 @@ impl Tool for MovePathTool {
IconName::ArrowRightLeft IconName::ArrowRightLeft
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<MovePathToolInput>(format) json_schema_for::<MovePathToolInput>(format)
} }

View file

@ -45,7 +45,7 @@ impl Tool for NowTool {
IconName::Info IconName::Info
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<NowToolInput>(format) json_schema_for::<NowToolInput>(format)
} }

View file

@ -35,7 +35,7 @@ impl Tool for OpenTool {
IconName::ArrowUpRight IconName::ArrowUpRight
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<OpenToolInput>(format) json_schema_for::<OpenToolInput>(format)
} }

View file

@ -53,7 +53,7 @@ impl Tool for PathSearchTool {
IconName::SearchCode IconName::SearchCode
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<PathSearchToolInput>(format) json_schema_for::<PathSearchToolInput>(format)
} }

View file

@ -63,7 +63,7 @@ impl Tool for ReadFileTool {
IconName::FileSearch IconName::FileSearch
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<ReadFileToolInput>(format) json_schema_for::<ReadFileToolInput>(format)
} }

View file

@ -60,7 +60,7 @@ impl Tool for RegexSearchTool {
IconName::Regex IconName::Regex
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<RegexSearchToolInput>(format) json_schema_for::<RegexSearchToolInput>(format)
} }

View file

@ -2,7 +2,7 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use language::{self, Buffer, ToPointUtf16}; use language::{self, Buffer, ToPointUtf16};
use language_model::LanguageModelRequestMessage; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project; use project::Project;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -68,10 +68,7 @@ impl Tool for RenameTool {
IconName::Pencil IconName::Pencil
} }
fn input_schema( fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
&self,
format: language_model::LanguageModelToolSchemaFormat,
) -> serde_json::Value {
json_schema_for::<RenameToolInput>(format) json_schema_for::<RenameToolInput>(format)
} }

View file

@ -5,23 +5,20 @@ use schemars::{
schema::{RootSchema, Schema, SchemaObject}, schema::{RootSchema, Schema, SchemaObject},
}; };
pub fn json_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> serde_json::Value { pub fn json_schema_for<T: JsonSchema>(
format: LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> {
let schema = root_schema_for::<T>(format); let schema = root_schema_for::<T>(format);
schema_to_json(&schema, format).expect("Failed to convert tool calling schema to JSON") schema_to_json(&schema, format)
} }
pub fn schema_to_json( fn schema_to_json(
schema: &RootSchema, schema: &RootSchema,
format: LanguageModelToolSchemaFormat, format: LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> { ) -> Result<serde_json::Value> {
let mut value = serde_json::to_value(schema)?; let mut value = serde_json::to_value(schema)?;
match format { assistant_tool::adapt_schema_to_format(&mut value, format)?;
LanguageModelToolSchemaFormat::JsonSchema => Ok(value), Ok(value)
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
transform_fields_to_json_schema_subset(&mut value);
Ok(value)
}
}
} }
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema { fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema {
@ -79,42 +76,3 @@ impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
schemars::visit::visit_schema_object(self, schema) schemars::visit::visit_schema_object(self, schema)
} }
} }
fn transform_fields_to_json_schema_subset(json: &mut serde_json::Value) {
if let serde_json::Value::Object(obj) = json {
if let Some(default) = obj.get("default") {
let is_null = default.is_null();
//Default is not supported, so we need to remove it.
obj.remove("default");
if is_null {
obj.insert("nullable".to_string(), serde_json::Value::Bool(true));
}
}
// If a type is not specified for an input parameter we need to add it.
if obj.contains_key("description")
&& !obj.contains_key("type")
&& !(obj.contains_key("anyOf")
|| obj.contains_key("oneOf")
|| obj.contains_key("allOf"))
{
obj.insert(
"type".to_string(),
serde_json::Value::String("string".to_string()),
);
}
//Format field is only partially supported (e.g. not uint compatibility)
obj.remove("format");
for (_, value) in obj.iter_mut() {
if let serde_json::Value::Object(_) | serde_json::Value::Array(_) = value {
transform_fields_to_json_schema_subset(value);
}
}
} else if let serde_json::Value::Array(arr) = json {
for item in arr.iter_mut() {
transform_fields_to_json_schema_subset(item);
}
}
}

View file

@ -84,7 +84,7 @@ impl Tool for SymbolInfoTool {
IconName::Code IconName::Code
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<SymbolInfoToolInput>(format) json_schema_for::<SymbolInfoToolInput>(format)
} }

View file

@ -44,7 +44,7 @@ impl Tool for TerminalTool {
IconName::Terminal IconName::Terminal
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<TerminalToolInput>(format) json_schema_for::<TerminalToolInput>(format)
} }

View file

@ -36,7 +36,7 @@ impl Tool for ThinkingTool {
IconName::LightBulb IconName::LightBulb
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<ThinkingToolInput>(format) json_schema_for::<ThinkingToolInput>(format)
} }

View file

@ -53,16 +53,18 @@ impl Tool for ContextServerTool {
true true
} }
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> serde_json::Value { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
match &self.tool.input_schema { let mut schema = self.tool.input_schema.clone();
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema {
serde_json::Value::Null => { serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] }) serde_json::json!({ "type": "object", "properties": [] })
} }
serde_json::Value::Object(map) if map.is_empty() => { serde_json::Value::Object(map) if map.is_empty() => {
serde_json::json!({ "type": "object", "properties": [] }) serde_json::json!({ "type": "object", "properties": [] })
} }
_ => self.tool.input_schema.clone(), _ => schema,
} })
} }
fn ui_text(&self, _input: &serde_json::Value) -> String { fn ui_text(&self, _input: &serde_json::Value) -> String {