gestura_core_mcp/
lib.rs

1//! Model Context Protocol implementation for Gestura.
2//!
3//! This crate owns Gestura's MCP protocol layer for protocol version
4//! `2025-11-25`, including the protocol data model, client/server plumbing,
5//! discovery and caching, connection lifecycle, notifications, prompt registry,
6//! and local integration helpers.
7//!
8//! ## Responsibilities
9//!
10//! - MCP client and server implementations
11//! - service discovery, caching, and registry helpers
12//! - session lifecycle and notification delivery
13//! - prompt resources and prompt registry access
14//! - MCP-specific configuration, errors, and inspection helpers
15//!
16//! ## Boundary with `gestura-core`
17//!
18//! Higher-level agent orchestration stays in `gestura-core`. This crate focuses
19//! on implementing the MCP protocol surface itself so it can evolve as a
20//! cohesive domain.
21//!
22//! Most application code should import MCP items through `gestura_core::mcp::*`
23//! unless it specifically needs to depend on this domain crate directly.
24
25pub mod client;
26pub mod cmd_utils;
27pub mod config;
28pub mod discovery;
29pub mod integrator;
30pub mod lifecycle;
31pub mod notifications;
32pub mod prompts;
33pub mod provision;
34pub mod registry;
35pub mod server;
36pub mod types;
37
38// Compatibility-style local re-exports used by this domain.
39// (These keep internal module paths stable while the workspace is modularized.)
40pub mod error;
41pub mod execution_mode;
42pub mod tool_inspection;
43
44// Re-export commonly used types (mirrors the historical `gestura_core::mcp::*` surface).
45pub use client::{McpClient, McpClientRegistry, get_mcp_client_registry};
46pub use config::{
47    McpJsonFile, McpScope, McpServerEntry, McpTool, McpTransportType, import_claude_desktop_servers,
48};
49pub use discovery::{
50    CacheStats as McpCacheStats, CachedTool, McpDiscoveryManager, McpServerConfig,
51    ServerInfo as McpServerInfo, ServerState,
52};
53pub use integrator::{LocalMcp, McpIntegrator, MdhResource, TokenInfo, get_mcp, mdh_translate};
54pub use lifecycle::{SessionManager, create_session_manager};
55pub use notifications::{
56    McpLogger, McpNotification, NotificationReceiver, NotificationSender, OperationProgress,
57    ProgressTracker, create_notification_channel,
58};
59pub use prompts::{PromptRegistry, RegisteredPrompt};
60pub use provision::{ProvisionResult, ProvisionStatus, provision_mcp_server};
61pub use registry::{
62    PopularMcpServer, RegistryBrowseEntry, RegistryBrowsePage, browse_mcp_registry,
63    list_popular_mcp_servers, normalize_mcp_server_name,
64};
65pub use server::{
66    JsonRpcError, JsonRpcRequest, JsonRpcResponse, McpRequestContext, McpResourceHandler,
67    McpServer, McpToolHandler,
68};
69pub use types::{
70    CancelledNotification, ClientCapabilities, ClientInfo, EmbeddedResource, InitializeParams,
71    InitializeResult, LogLevel, LoggingCapability, LoggingMessage, PROTOCOL_VERSION, PingParams,
72    PingResult, ProgressNotification, ProgressToken, Prompt, PromptArgument, PromptContent,
73    PromptMessage, PromptRole, PromptsCapability, PromptsGetParams, PromptsGetResult,
74    PromptsListParams, PromptsListResult, Resource, ResourceAnnotations, ResourceContent,
75    ResourceReference, ResourcesCapability, ResourcesListParams, ResourcesListResult,
76    ResourcesReadParams, ResourcesReadResult, ServerCapabilities, ServerInfo, SessionState,
77    TextContent, Tool, ToolAnnotations, ToolResultContent, ToolsCallParams, ToolsCallResult,
78    ToolsCapability, ToolsListParams, ToolsListResult, error_codes, mcp_error_codes,
79};
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_protocol_version() {
87        assert_eq!(PROTOCOL_VERSION, "2025-11-25");
88    }
89
90    #[test]
91    fn test_server_info_default() {
92        let info = ServerInfo::default();
93        assert_eq!(info.name, "gestura");
94        assert!(!info.version.is_empty());
95    }
96
97    #[test]
98    fn test_session_lifecycle() {
99        let session = SessionManager::new();
100        assert_eq!(session.state(), SessionState::Uninitialized);
101
102        let params = InitializeParams {
103            protocol_version: PROTOCOL_VERSION.to_string(),
104            capabilities: ClientCapabilities::default(),
105            client_info: ClientInfo {
106                name: "test-client".to_string(),
107                version: "1.0.0".to_string(),
108            },
109        };
110
111        let result = session.initialize(params).unwrap();
112        assert_eq!(result.protocol_version, PROTOCOL_VERSION);
113        assert_eq!(session.state(), SessionState::Initializing);
114
115        session.initialized().unwrap();
116        assert_eq!(session.state(), SessionState::Ready);
117        assert!(session.is_ready());
118
119        session.shutdown().unwrap();
120        assert_eq!(session.state(), SessionState::Closed);
121    }
122
123    #[test]
124    fn test_prompt_registry() {
125        let registry = PromptRegistry::new();
126        let prompts = registry.list();
127        assert!(!prompts.is_empty());
128        assert!(registry.contains("voice-command"));
129
130        let mut args = std::collections::HashMap::new();
131        args.insert("command".to_string(), "hello world".to_string());
132        args.insert("context".to_string(), "testing".to_string());
133
134        let result = registry.get("voice-command", Some(&args)).unwrap();
135        assert!(
136            result.messages[0]
137                .content
138                .as_text()
139                .map(|t| t.text.contains("hello world"))
140                .unwrap_or(false)
141        );
142    }
143
144    #[test]
145    fn test_progress_tracker() {
146        let (sender, _receiver) = create_notification_channel();
147        let tracker = ProgressTracker::new(sender);
148
149        let id = tracker.start_operation("test-op".to_string(), Some(100.0));
150        assert!(!tracker.is_cancelled(&id));
151
152        tracker.update_progress(&id, 50.0, Some("Halfway".to_string()));
153        tracker.cancel_operation(&id, Some("User cancelled".to_string()));
154        assert!(tracker.is_cancelled(&id));
155    }
156}