🧪 test(kebab-mcp): tools/list returns 4 tools (fb-30)
Approach: extracted `pub fn build_tools_vec() -> Vec<Tool>` from the inline `list_tools` trait impl body — `RequestContext<RoleServer>` is non-constructible from outside rmcp (Peer::new is pub(crate)), so a direct trait-method call was not viable without an in-memory transport rmcp 1.6 does not expose. The helper is the single source of truth; `list_tools` now delegates to it. Three test cases in tests/tools_list.rs: - 4 tools present with correct names - search inputSchema has "required": ["query"] - schema/doctor tools accept empty input (type=object, no required) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,35 @@ pub mod state;
|
||||
pub mod tools;
|
||||
pub use state::KebabAppState;
|
||||
|
||||
/// Build the canonical list of tools exposed by the MCP server.
|
||||
///
|
||||
/// Extracted from [`ServerHandler::list_tools`] so it can be called
|
||||
/// directly in tests without constructing a `RequestContext`.
|
||||
pub fn build_tools_vec() -> Vec<Tool> {
|
||||
vec![
|
||||
Tool::new(
|
||||
"schema",
|
||||
"Introspection — wire schemas, capabilities, model versions, index stats.",
|
||||
schema_for_empty_input(),
|
||||
),
|
||||
Tool::new(
|
||||
"doctor",
|
||||
"Health check — verifies config, storage, models, and Ollama connectivity.",
|
||||
schema_for_empty_input(),
|
||||
),
|
||||
Tool::new(
|
||||
"search",
|
||||
"Full-text / vector / hybrid search over the knowledge base. Returns search_hit.v1 array.",
|
||||
schema_for_type::<tools::search::SearchInput>(),
|
||||
),
|
||||
Tool::new(
|
||||
"ask",
|
||||
"RAG question answering over the knowledge base. Returns answer.v1 JSON. Pass session_id for multi-turn context.",
|
||||
schema_for_type::<tools::ask::AskInput>(),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KebabHandler {
|
||||
state: KebabAppState,
|
||||
@@ -49,28 +78,7 @@ impl ServerHandler for KebabHandler {
|
||||
_request: Option<rmcp::model::PaginatedRequestParams>,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<ListToolsResult, ErrorData> {
|
||||
Ok(ListToolsResult::with_all_items(vec![
|
||||
Tool::new(
|
||||
"schema",
|
||||
"Introspection — wire schemas, capabilities, model versions, index stats.",
|
||||
schema_for_empty_input(),
|
||||
),
|
||||
Tool::new(
|
||||
"doctor",
|
||||
"Health check — verifies config, storage, models, and Ollama connectivity.",
|
||||
schema_for_empty_input(),
|
||||
),
|
||||
Tool::new(
|
||||
"search",
|
||||
"Full-text / vector / hybrid search over the knowledge base. Returns search_hit.v1 array.",
|
||||
schema_for_type::<tools::search::SearchInput>(),
|
||||
),
|
||||
Tool::new(
|
||||
"ask",
|
||||
"RAG question answering over the knowledge base. Returns answer.v1 JSON. Pass session_id for multi-turn context.",
|
||||
schema_for_type::<tools::ask::AskInput>(),
|
||||
),
|
||||
]))
|
||||
Ok(ListToolsResult::with_all_items(build_tools_vec()))
|
||||
}
|
||||
|
||||
async fn call_tool(
|
||||
|
||||
68
crates/kebab-mcp/tests/tools_list.rs
Normal file
68
crates/kebab-mcp/tests/tools_list.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Integration: `build_tools_vec` returns 4 tools with correct names and
|
||||
//! inputSchema. Uses the extracted `pub fn build_tools_vec()` helper — no
|
||||
//! transport or RequestContext needed.
|
||||
|
||||
use kebab_mcp::build_tools_vec;
|
||||
|
||||
#[test]
|
||||
fn tools_list_returns_four_tools() {
|
||||
let tools = build_tools_vec();
|
||||
assert_eq!(tools.len(), 4, "expected exactly 4 tools, got {}", tools.len());
|
||||
|
||||
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
|
||||
assert!(names.contains(&"schema"), "missing 'schema' tool");
|
||||
assert!(names.contains(&"doctor"), "missing 'doctor' tool");
|
||||
assert!(names.contains(&"search"), "missing 'search' tool");
|
||||
assert!(names.contains(&"ask"), "missing 'ask' tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_input_schema_has_required_query() {
|
||||
let tools = build_tools_vec();
|
||||
let search = tools
|
||||
.iter()
|
||||
.find(|t| t.name.as_ref() == "search")
|
||||
.expect("search tool must be present");
|
||||
|
||||
// input_schema is Arc<JsonObject> (serde_json::Map<String, Value>).
|
||||
let schema_val = serde_json::Value::Object(search.input_schema.as_ref().clone());
|
||||
|
||||
let required = schema_val
|
||||
.get("required")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("search inputSchema must have a 'required' array");
|
||||
|
||||
assert!(
|
||||
required.iter().any(|v| v.as_str() == Some("query")),
|
||||
"search inputSchema 'required' must contain 'query', got: {required:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_and_doctor_tools_accept_empty_input() {
|
||||
let tools = build_tools_vec();
|
||||
|
||||
for name in &["schema", "doctor"] {
|
||||
let tool = tools
|
||||
.iter()
|
||||
.find(|t| t.name.as_ref() == *name)
|
||||
.unwrap_or_else(|| panic!("{name} tool must be present"));
|
||||
|
||||
let schema_val = serde_json::Value::Object(tool.input_schema.as_ref().clone());
|
||||
// An empty-input schema has type "object" and no required fields
|
||||
// (or no 'required' key at all).
|
||||
let ty = schema_val.get("type").and_then(|v| v.as_str());
|
||||
assert_eq!(
|
||||
ty,
|
||||
Some("object"),
|
||||
"{name} inputSchema 'type' must be 'object', got {ty:?}"
|
||||
);
|
||||
|
||||
if let Some(required) = schema_val.get("required").and_then(|v| v.as_array()) {
|
||||
assert!(
|
||||
required.is_empty(),
|
||||
"{name} inputSchema 'required' must be empty, got: {required:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user