diff --git a/Cargo.lock b/Cargo.lock index 3213cce..8413627 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -94,7 +94,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1026,7 +1026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1329,9 +1329,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -2188,7 +2188,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2646,7 +2646,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3322,7 +3322,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3379,7 +3379,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3751,7 +3751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4036,7 +4036,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4898,7 +4898,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/crates/rustapi-mcp/README.md b/crates/rustapi-mcp/README.md new file mode 100644 index 0000000..0a1b7e4 --- /dev/null +++ b/crates/rustapi-mcp/README.md @@ -0,0 +1,91 @@ +# rustapi-mcp + +Native [Model Context Protocol (MCP)](https://modelcontextprotocol.io) integration for RustAPI. + +This crate allows you to expose your existing RustAPI routes as **discoverable tools** for LLMs and AI agents (Claude, Cursor, custom agents, etc.) with full respect for your middleware, validation, auth, and observability layers. + +## Status + +**Core implementation complete** (discovery, `tools/list`, real `tools/call` via proxy into normal RustAPI pipeline, HTTP JSON-RPC transport, concurrent runner with `run_rustapi_and_mcp`). + +HTTP transport enforces `McpConfig::admin_token` when set (`Authorization: Bearer`, +`X-MCP-Token`, or `?token=`). Non-path tool arguments are forwarded as query +parameters on safe HTTP methods. + +## Goals (v1) + +- Opt-in via `protocol-mcp` feature +- Automatic tool discovery from your route metadata + OpenAPI schemas +- Tool invocations execute through the **normal** RustAPI request pipeline (no bypass) +- Support for the recommended HTTP + SSE transport (stdio later) +- Strong security defaults (explicit exposure control, never auto-expose everything) + +## Usage (planned shape) + +```rust +use rustapi_rs::prelude::*; +use rustapi_rs::protocol::mcp::{McpConfig, McpServer}; + +#[rustapi_rs::get("/users/{id}")] +async fn get_user(Path(id): Path) -> Json { /* ... */ } + +#[tokio::main] +async fn main() -> Result<(), Box> { + let app = RustApi::auto(); + + // Run your normal HTTP API + MCP side-by-side (similar to gRPC) + let mcp = McpServer::new( + McpConfig::new() + .name("my-api") + .version("1.0.0") + .enable_tools(true) + // Control what gets exposed as tools + .allowed_tags(["public", "agent"]) + // When set, every MCP HTTP request must present this token + .admin_token("secret-for-mcp-clients"), + ); + + rustapi_mcp::run_rustapi_and_mcp(app, "0.0.0.0:8080", mcp, "0.0.0.0:9090").await?; + Ok(()) +} +``` + +Clients send the token as: + +```http +Authorization: Bearer secret-for-mcp-clients +``` + +or `X-MCP-Token: secret-for-mcp-clients`, or `?token=secret-for-mcp-clients`. + +## Feature Flag + +In `rustapi-rs`: + +```toml +[dependencies] +rustapi-rs = { version = "...", features = ["protocol-mcp"] } +``` + +Or with the meta feature: + +```toml +rustapi-rs = { version = "...", features = ["protocol-all"] } +``` + +## Design Principles + +- Follows the same facade contract as the rest of RustAPI. +- New internal crate (`rustapi-mcp`) behind the `protocol-mcp` feature. +- Tool calls **must** go through existing `Router`, layers, interceptors, extractors, and validation. +- Explicit exposure model by default (no accidental leakage of internal routes). + +## Related + +- Main roadmap: see top-level `README.md` +- Detailed plan: `memories/native_mcp_orchestration_plan.md` +- Master task list: `memories/TASKLIST.md` + +## License + +MIT OR Apache-2.0 (same as the rest of the workspace) diff --git a/crates/rustapi-rs/examples/status_demo.rs b/crates/rustapi-rs/examples/status_demo.rs index 9e0140f..a2ae26f 100644 --- a/crates/rustapi-rs/examples/status_demo.rs +++ b/crates/rustapi-rs/examples/status_demo.rs @@ -25,7 +25,7 @@ async fn main() -> Result<(), Box> { } // An endpoint that sometimes fails - async fn flaky_handler() -> Result<&'static str, rustapi_rs::Response> { + async fn flaky_handler() -> rustapi_rs::Response { use std::sync::atomic::{AtomicBool, Ordering}; static FAILURE: AtomicBool = AtomicBool::new(false); @@ -33,9 +33,9 @@ async fn main() -> Result<(), Box> { let fail = FAILURE.fetch_xor(true, Ordering::Relaxed); if !fail { - Ok("Success!") + "Success!".into_response() } else { - Err(rustapi_rs::StatusCode::INTERNAL_SERVER_ERROR.into_response()) + rustapi_rs::StatusCode::INTERNAL_SERVER_ERROR.into_response() } }