mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
fix(mcp): emit explicit answer input schema
Avoid advertising fabro_run_interact.answer as the boolean JSON Schema true, which some MCP clients reject during tool discovery.
This commit is contained in:
parent
503601f8be
commit
6d016d69a1
2 changed files with 90 additions and 5 deletions
|
|
@ -426,12 +426,33 @@ async fn stdio_server_initializes_and_lists_run_tools() {
|
|||
"fabro_run_interact",
|
||||
"fabro_run_search",
|
||||
]);
|
||||
for (_, _, schema) in tools {
|
||||
for (name, _, schema) in &tools {
|
||||
assert!(
|
||||
schema.is_object(),
|
||||
"tool should have input schema: {schema}"
|
||||
);
|
||||
let properties = schema
|
||||
.get("properties")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.expect("tool input schema should have properties");
|
||||
for (property, property_schema) in properties {
|
||||
assert!(
|
||||
property_schema.is_object(),
|
||||
"{name}.{property} should use an object JSON Schema, got {property_schema}"
|
||||
);
|
||||
}
|
||||
}
|
||||
let interact_schema = tools
|
||||
.iter()
|
||||
.find(|(name, _, _)| name == "fabro_run_interact")
|
||||
.map(|(_, _, schema)| schema)
|
||||
.expect("fabro_run_interact tool should be listed");
|
||||
assert!(
|
||||
interact_schema
|
||||
.pointer("/properties/answer")
|
||||
.is_some_and(serde_json::Value::is_object),
|
||||
"fabro_run_interact.answer should have an object JSON Schema: {interact_schema}"
|
||||
);
|
||||
client
|
||||
.shutdown()
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_api::types;
|
||||
use fabro_client::Client;
|
||||
use fabro_types::RunId;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -30,7 +31,70 @@ pub(crate) struct FabroRunInteractParams {
|
|||
pub(crate) message: Option<String>,
|
||||
pub(crate) interrupt: Option<bool>,
|
||||
pub(crate) question_id: Option<String>,
|
||||
pub(crate) answer: Option<Value>,
|
||||
pub(crate) answer: Option<AnswerValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub(crate) struct AnswerValue(Value);
|
||||
|
||||
impl From<Value> for AnswerValue {
|
||||
fn from(value: Value) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AnswerValue {
|
||||
fn into_inner(self) -> Value {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AnswerValue {
|
||||
fn inline_schema() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"AnswerValue".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "Answer payload for a pending Fabro question. Use a boolean for yes/no, a string or {\"text\": \"...\"} for freeform text, {\"option\": \"key\"} for a single choice, or {\"options\": [\"key\"]} for multi-select.",
|
||||
"anyOf": [
|
||||
{ "type": "boolean" },
|
||||
{ "type": "string" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"option": { "type": "string" }
|
||||
},
|
||||
"required": ["option"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["options"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": { "type": "string" }
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -116,7 +180,7 @@ impl TryFrom<FabroRunInteractParams> for ValidatedInteractRun {
|
|||
};
|
||||
ValidatedInteractAction::Answer {
|
||||
question_id: question_id.to_string(),
|
||||
body: answer_to_submit_request(answer)?,
|
||||
body: answer_to_submit_request(answer.into_inner())?,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -326,7 +390,7 @@ mod tests {
|
|||
message: None,
|
||||
interrupt: None,
|
||||
question_id: Some("question-1".to_string()),
|
||||
answer: Some(json!({ "value": "yes" })),
|
||||
answer: Some(json!({ "value": "yes" }).into()),
|
||||
})
|
||||
.unwrap_err();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue