From 61eef9bc82bf30b6a14fddb75f6f8ecc629bbb5f Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Thu, 7 May 2026 16:04:42 +0900
Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20test(kebab-mcp):=20tools/list=20?=
=?UTF-8?q?returns=204=20tools=20(fb-30)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Approach: extracted `pub fn build_tools_vec() -> Vec` from the
inline `list_tools` trait impl body — `RequestContext` 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)
---
crates/kebab-mcp/src/lib.rs | 52 ++++++++++++---------
crates/kebab-mcp/tests/tools_list.rs | 68 ++++++++++++++++++++++++++++
2 files changed, 98 insertions(+), 22 deletions(-)
create mode 100644 crates/kebab-mcp/tests/tools_list.rs
diff --git a/crates/kebab-mcp/src/lib.rs b/crates/kebab-mcp/src/lib.rs
index 1bca86b..9cbafa9 100644
--- a/crates/kebab-mcp/src/lib.rs
+++ b/crates/kebab-mcp/src/lib.rs
@@ -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 {
+ 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::(),
+ ),
+ Tool::new(
+ "ask",
+ "RAG question answering over the knowledge base. Returns answer.v1 JSON. Pass session_id for multi-turn context.",
+ schema_for_type::(),
+ ),
+ ]
+}
+
#[derive(Clone)]
pub struct KebabHandler {
state: KebabAppState,
@@ -49,28 +78,7 @@ impl ServerHandler for KebabHandler {
_request: Option,
_context: RequestContext,
) -> Result {
- 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::(),
- ),
- Tool::new(
- "ask",
- "RAG question answering over the knowledge base. Returns answer.v1 JSON. Pass session_id for multi-turn context.",
- schema_for_type::(),
- ),
- ]))
+ Ok(ListToolsResult::with_all_items(build_tools_vec()))
}
async fn call_tool(
diff --git a/crates/kebab-mcp/tests/tools_list.rs b/crates/kebab-mcp/tests/tools_list.rs
new file mode 100644
index 0000000..f7c0cd4
--- /dev/null
+++ b/crates/kebab-mcp/tests/tools_list.rs
@@ -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 (serde_json::Map).
+ 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:?}"
+ );
+ }
+ }
+}