✨ feat(kebab-mcp): handler skeleton + initialize handshake (fb-30)
KebabHandler implements rmcp::ServerHandler::get_info — returns serverInfo (name="kebab", version from CARGO_PKG_VERSION) and capabilities.tools. KebabAppState wraps Config in Arc for cheap clone into per-request task scope. serve_stdio entry builds a multi-thread tokio runtime and runs the server until client closes the stream. rmcp 1.6 API used: - rmcp::ServerHandler trait (re-exported from handler::server) - ServerInfo::new(caps).with_server_info(impl) builder (not struct-init: InitializeResult/Implementation are #[non_exhaustive]) - ServerCapabilities::builder().enable_tools().build() — builder macro generated, confirms the plan-literal pattern works - Implementation::new(name, version) — non-exhaustive constructor - rmcp::transport::stdio() returns (tokio::io::Stdin, tokio::io::Stdout) tuple; tuple impls IntoTransport via AsyncRead+AsyncWrite blanket - handler.serve(transport).await → RunningService<RoleServer, H> (ServiceExt::serve, returns Result<_, ServerInitializeError>) - service.waiting().await → Result<QuitReason, JoinError> - serve_stdio is plain fn wrapping a manually-built tokio runtime (avoids nested-runtime hazard if kebab-cli ever gains its own rt) Tools wire-up lands in subsequent tasks (one tool per task). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,52 @@
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use rmcp::ServerHandler;
|
||||
use rmcp::model::{Implementation, ServerCapabilities, ServerInfo};
|
||||
use rmcp::service::ServiceExt;
|
||||
use rmcp::transport::stdio;
|
||||
|
||||
use kebab_config::Config;
|
||||
|
||||
pub mod state;
|
||||
pub use state::KebabAppState;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KebabHandler {
|
||||
state: KebabAppState,
|
||||
}
|
||||
|
||||
impl KebabHandler {
|
||||
pub fn new(state: KebabAppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &KebabAppState {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerHandler for KebabHandler {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
.with_server_info(Implementation::new("kebab", env!("CARGO_PKG_VERSION")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the MCP server on stdio JSON-RPC. Blocks until the client closes
|
||||
/// the stream (typically when the agent host exits).
|
||||
pub fn serve_stdio(_cfg: Config) -> Result<()> {
|
||||
// Skeleton — actual rmcp wiring lands in Task 3.
|
||||
anyhow::bail!("kebab-mcp: serve_stdio not yet implemented")
|
||||
pub fn serve_stdio(cfg: Config) -> Result<()> {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
runtime.block_on(serve_stdio_async(cfg))
|
||||
}
|
||||
|
||||
async fn serve_stdio_async(cfg: Config) -> Result<()> {
|
||||
tracing::info!("kebab-mcp: starting stdio server");
|
||||
let state = KebabAppState::new(cfg);
|
||||
let handler = KebabHandler::new(state);
|
||||
let service = handler.serve(stdio()).await?;
|
||||
service.waiting().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
21
crates/kebab-mcp/src/state.rs
Normal file
21
crates/kebab-mcp/src/state.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! Long-lived server state — holds Config so per-request handlers don't
|
||||
//! reload from disk. Future: cache opened SqliteStore / Lance handles
|
||||
//! here so first tool call pays the cost, subsequent calls hit warm
|
||||
//! state.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use kebab_config::Config;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KebabAppState {
|
||||
pub config: Arc<Config>,
|
||||
}
|
||||
|
||||
impl KebabAppState {
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
config: Arc::new(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
19
crates/kebab-mcp/tests/initialize.rs
Normal file
19
crates/kebab-mcp/tests/initialize.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! Integration: KebabHandler::get_info returns correct kebab serverInfo.
|
||||
//! Doesn't exercise full transport — that lands when we have at least
|
||||
//! one tool to call (Task 4+).
|
||||
|
||||
use kebab_config::Config;
|
||||
use kebab_mcp::{KebabAppState, KebabHandler};
|
||||
use rmcp::ServerHandler;
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_returns_kebab_server_info() {
|
||||
let cfg = Config::defaults();
|
||||
let state = KebabAppState::new(cfg);
|
||||
let handler = KebabHandler::new(state);
|
||||
|
||||
let info = handler.get_info();
|
||||
assert_eq!(info.server_info.name, "kebab");
|
||||
assert!(!info.server_info.version.is_empty());
|
||||
assert!(info.capabilities.tools.is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user