zeph_commands/handlers/
mcp.rs1use std::future::Future;
13use std::pin::Pin;
14
15use crate::context::CommandContext;
16use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
17
18pub struct McpCommand;
26
27impl CommandHandler<CommandContext<'_>> for McpCommand {
28 fn name(&self) -> &'static str {
29 "/mcp"
30 }
31
32 fn description(&self) -> &'static str {
33 "Manage MCP server connections"
34 }
35
36 fn args_hint(&self) -> &'static str {
37 "add|list|tools|remove"
38 }
39
40 fn category(&self) -> SlashCategory {
41 SlashCategory::Integration
42 }
43
44 fn requires_auth(&self) -> bool {
45 true
46 }
47
48 fn handle<'a>(
49 &'a self,
50 ctx: &'a mut CommandContext<'_>,
51 args: &'a str,
52 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
53 use tracing::Instrument as _;
54 let span = tracing::info_span!("commands.mcp.handle");
55 Box::pin(
56 async move {
57 let output = ctx.agent.handle_mcp(args).await?;
58 Ok(CommandOutput::Message(output))
59 }
60 .instrument(span),
61 )
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::CommandRegistry;
69 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
70 use crate::sink::NullSink;
71 use std::assert_matches;
72
73 #[test]
74 fn mcp_name_and_description() {
75 assert_eq!(McpCommand.name(), "/mcp");
76 assert!(!McpCommand.description().is_empty());
77 }
78
79 #[tokio::test]
80 async fn mcp_returns_message() {
81 let mut sink = NullSink;
82 let mut debug = MockDebug;
83 let mut messages = MockMessages;
84 let session = MockSession;
85 let mut agent = crate::NullAgent;
86 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
87 let out = McpCommand.handle(&mut ctx, "list").await.unwrap();
88 assert_matches!(out, CommandOutput::Message(_));
89 }
90
91 #[tokio::test]
92 async fn mcp_dispatch_allowed_when_trusted() {
93 let mut sink = NullSink;
94 let mut debug = MockDebug;
95 let mut messages = MockMessages;
96 let session = MockSession;
97 let mut agent = crate::NullAgent;
98 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
99
100 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
101 reg.register(McpCommand);
102
103 let result = reg.dispatch(&mut ctx, "/mcp list", true).await;
104 assert!(result.unwrap().is_ok());
105 }
106
107 #[tokio::test]
108 async fn mcp_dispatch_rejected_when_untrusted() {
109 let mut sink = NullSink;
110 let mut debug = MockDebug;
111 let mut messages = MockMessages;
112 let session = MockSession;
113 let mut agent = crate::NullAgent;
114 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
115
116 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
117 reg.register(McpCommand);
118
119 let result = reg
120 .dispatch(&mut ctx, "/mcp add pwn python3 -c evil", false)
121 .await;
122 let err = result.unwrap().unwrap_err();
123 assert!(err.0.contains("trusted"));
124 }
125}