feat(http): implement to_mcp 4-tool gateway projection (rmcp ServerHandler, StreamableHttpService at /mcp)
to_mcp is the MCP-direction gateway projection (ADR-041): exposes 4 fixed gateway tools (search, schema, call, batch) over rmcp StreamableHttpService nested into the axum Router at /mcp, not one MCP tool per registry operation. The LLM discovers operations on demand via search+schema. - ToMcpGateway implements rmcp ServerHandler (call_tool, list_tools, get_info) - tools/list returns the 4 fixed gateway tools, never the registry's ops - search dispatches services/list via GatewayDispatch::invoke, excludes Subscription ops (ADR-041 §2), returns names + descriptions - schema dispatches services/schema, returns the full OperationSpec - call dispatches via GatewayDispatch::invoke (shared spine), maps ResponseEnvelope -> CallToolResult::structured (Ok) / CallToolResult::structured_error (Err(CallError)) - batch loops over invoke, returns an array of results - Bearer auth via shared bearer_auth_middleware applied around nest_service (rmcp simple_auth_streamhttp pattern); Identity read from RequestContext.extensions -> http::request::Parts.extensions (research §6 #2 identity-survives-framing assumption, confirmed via test) - to_mcp is a pure projection (consumes registry, produces no entries) - Feature-gated behind mcp; stdio NOT built (ADR-037) - /mcp route wired in adapter.rs replacing the placeholder 501 cargo test -p alknet-http --features mcp: 172 passed cargo clippy -p alknet-http --features mcp --all-targets: clean cargo check -p alknet-http (no mcp): clean
This commit is contained in:
@@ -17,7 +17,7 @@ use async_trait::async_trait;
|
||||
use axum::http::StatusCode;
|
||||
use axum::middleware::from_fn_with_state;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{any, get, post};
|
||||
use axum::routing::{any, get};
|
||||
use axum::Router;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper_util::server::conn::auto::Builder as HyperBuilder;
|
||||
@@ -32,6 +32,10 @@ use alknet_core::types::{Connection, HandlerError, ProtocolHandler, StreamError}
|
||||
use super::auth::bearer_auth_middleware;
|
||||
use crate::server::decoy::decoy_fallback;
|
||||
use crate::server::healthz::healthz;
|
||||
#[cfg(feature = "mcp")]
|
||||
use crate::adapters::to_mcp_service;
|
||||
#[cfg(feature = "mcp")]
|
||||
use crate::gateway::GatewayDispatch;
|
||||
|
||||
const ALPN_HTTP1: &[u8] = b"http/1.1";
|
||||
const ALPN_H2: &[u8] = b"h2";
|
||||
@@ -135,6 +139,20 @@ impl HttpAdapter {
|
||||
|
||||
fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
|
||||
let auth_state = Arc::clone(&state.identity_provider);
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
let mcp_router: Router<RouterState> = {
|
||||
let dispatch = Arc::new(GatewayDispatch::new(
|
||||
Arc::clone(&state.registry),
|
||||
Arc::clone(&state.identity_provider),
|
||||
));
|
||||
Router::new()
|
||||
.nest_service("/mcp", to_mcp_service(dispatch))
|
||||
.layer(from_fn_with_state(auth_state.clone(), bearer_auth_middleware))
|
||||
};
|
||||
#[cfg(not(feature = "mcp"))]
|
||||
let mcp_router: Router<RouterState> = Router::new();
|
||||
|
||||
let default: Router<RouterState> = Router::new()
|
||||
.route("/search", any(not_implemented))
|
||||
.route("/schema", any(not_implemented))
|
||||
@@ -142,10 +160,10 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
|
||||
.route("/batch", any(not_implemented))
|
||||
.route("/subscribe", any(not_implemented))
|
||||
.route("/openapi.json", get(not_implemented))
|
||||
.route("/mcp", post(not_implemented))
|
||||
.route_layer(from_fn_with_state(auth_state.clone(), bearer_auth_middleware))
|
||||
.route("/healthz", get(healthz))
|
||||
.fallback(decoy_fallback);
|
||||
.fallback(decoy_fallback)
|
||||
.merge(mcp_router);
|
||||
|
||||
let with_extras = match extra_routes {
|
||||
Some(extra) => {
|
||||
@@ -257,6 +275,7 @@ impl AsyncWrite for QuicStream {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::routing::post;
|
||||
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
struct NoopProvider;
|
||||
|
||||
Reference in New Issue
Block a user