feat: env and secrets configuration for mcp server (#565)

This commit is contained in:
Salman Mohammed
2025-01-13 14:11:42 -05:00
committed by GitHub
parent ec4672d3ab
commit a258b76d42
8 changed files with 84 additions and 17 deletions
+8 -4
View File
@@ -88,12 +88,16 @@ impl Capabilities {
// TODO IMPORTANT need to ensure this times out if the system command is broken!
pub async fn add_system(&mut self, config: SystemConfig) -> SystemResult<()> {
let mut client: McpClient = match config {
SystemConfig::Sse { ref uri } => {
let transport = SseTransport::new(uri);
SystemConfig::Sse { ref uri, ref envs } => {
let transport = SseTransport::new(uri, envs.get_env());
McpClient::new(transport.start().await?)
}
SystemConfig::Stdio { ref cmd, ref args } => {
let transport = StdioTransport::new(cmd, args.to_vec());
SystemConfig::Stdio {
ref cmd,
ref args,
ref envs,
} => {
let transport = StdioTransport::new(cmd, args.to_vec(), envs.get_env());
McpClient::new(transport.start().await?)
}
};
+47 -6
View File
@@ -1,3 +1,5 @@
use std::collections::HashMap;
use mcp_client::client::Error as ClientError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -15,25 +17,63 @@ pub enum SystemError {
pub type SystemResult<T> = Result<T, SystemError>;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Envs {
/// A map of environment variables to set, e.g. API_KEY -> some_secret, HOST -> host
#[serde(default)]
#[serde(flatten)]
map: HashMap<String, String>,
}
impl Envs {
pub fn new(map: HashMap<String, String>) -> Self {
Self { map }
}
pub fn default() -> Self {
Self::new(HashMap::new())
}
pub fn get_env(&self) -> HashMap<String, String> {
self.map
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
}
/// Represents the different types of MCP systems that can be added to the manager
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum SystemConfig {
/// Server-sent events client with a URI endpoint
Sse { uri: String },
Sse {
uri: String,
#[serde(default)]
envs: Envs,
},
/// Standard I/O client with command and arguments
Stdio { cmd: String, args: Vec<String> },
Stdio {
cmd: String,
args: Vec<String>,
#[serde(default)]
envs: Envs,
},
}
impl SystemConfig {
pub fn sse<S: Into<String>>(uri: S) -> Self {
Self::Sse { uri: uri.into() }
Self::Sse {
uri: uri.into(),
envs: Envs::default(),
}
}
pub fn stdio<S: Into<String>>(cmd: S) -> Self {
Self::Stdio {
cmd: cmd.into(),
args: vec![],
envs: Envs::default(),
}
}
@@ -43,8 +83,9 @@ impl SystemConfig {
S: Into<String>,
{
match self {
Self::Stdio { cmd, .. } => Self::Stdio {
Self::Stdio { cmd, envs, .. } => Self::Stdio {
cmd,
envs,
args: args.into_iter().map(Into::into).collect(),
},
other => other,
@@ -55,8 +96,8 @@ impl SystemConfig {
impl std::fmt::Display for SystemConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SystemConfig::Sse { uri } => write!(f, "SSE({})", uri),
SystemConfig::Stdio { cmd, args } => write!(f, "Stdio({} {})", cmd, args.join(" ")),
SystemConfig::Sse { uri, .. } => write!(f, "SSE({})", uri),
SystemConfig::Stdio { cmd, args, .. } => write!(f, "Stdio({} {})", cmd, args.join(" ")),
}
}
}
+3 -3
View File
@@ -4,8 +4,8 @@ use mcp_client::{
};
use rand::Rng;
use rand::SeedableRng;
use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, sync::Arc};
use tracing_subscriber::EnvFilter;
#[tokio::main]
@@ -122,7 +122,7 @@ async fn create_stdio_client(
_name: &str,
_version: &str,
) -> Result<McpClient, Box<dyn std::error::Error>> {
let transport = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()]);
let transport = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());
Ok(McpClient::new(transport.start().await?))
}
@@ -130,6 +130,6 @@ async fn create_sse_client(
_name: &str,
_version: &str,
) -> Result<McpClient, Box<dyn std::error::Error>> {
let transport = SseTransport::new("http://localhost:8000/sse");
let transport = SseTransport::new("http://localhost:8000/sse", HashMap::new());
Ok(McpClient::new(transport.start().await?))
}
+2 -1
View File
@@ -1,6 +1,7 @@
use anyhow::Result;
use mcp_client::client::{ClientCapabilities, ClientInfo, McpClient};
use mcp_client::transport::{SseTransport, Transport};
use std::collections::HashMap;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
@@ -16,7 +17,7 @@ async fn main() -> Result<()> {
.init();
// Create the base transport
let transport = SseTransport::new("http://localhost:8000/sse");
let transport = SseTransport::new("http://localhost:8000/sse", HashMap::new());
// Start transport
let handle = transport.start().await?;
+3 -1
View File
@@ -1,3 +1,5 @@
use std::collections::HashMap;
use anyhow::Result;
use mcp_client::client::{ClientCapabilities, ClientInfo, Error as ClientError, McpClient};
use mcp_client::transport::{StdioTransport, Transport};
@@ -15,7 +17,7 @@ async fn main() -> Result<(), ClientError> {
.init();
// 1) Create the transport
let transport = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()]);
let transport = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());
// 2) Start the transport to get a handle
let transport_handle = transport.start().await?;
@@ -1,3 +1,5 @@
use std::collections::HashMap;
// This example shows how to use the mcp-client crate to interact with a server that has a simple counter tool.
// The server is started by running `cargo run -p mcp-server` in the root of the mcp-server crate.
use anyhow::Result;
@@ -23,6 +25,7 @@ async fn main() -> Result<(), ClientError> {
.into_iter()
.map(|s| s.to_string())
.collect(),
HashMap::new(),
);
// Start the transport to get a handle
+9 -1
View File
@@ -4,6 +4,7 @@ use eventsource_client::{Client, SSE};
use futures::TryStreamExt;
use mcp_core::protocol::{JsonRpcMessage, JsonRpcRequest};
use reqwest::Client as HttpClient;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tokio::time::{timeout, Duration};
@@ -205,13 +206,15 @@ impl SseActor {
#[derive(Clone)]
pub struct SseTransport {
sse_url: String,
env: HashMap<String, String>,
}
/// The SSE transport spawns an `SseActor` on `start()`.
impl SseTransport {
pub fn new<S: Into<String>>(sse_url: S) -> Self {
pub fn new<S: Into<String>>(sse_url: S, env: HashMap<String, String>) -> Self {
Self {
sse_url: sse_url.into(),
env: env,
}
}
@@ -238,6 +241,11 @@ impl SseTransport {
#[async_trait]
impl Transport for SseTransport {
async fn start(&self) -> Result<TransportHandle, Error> {
// Set environment variables
for (key, value) in &self.env {
std::env::set_var(key, value);
}
// Create a channel for outgoing TransportMessages
let (tx, rx) = mpsc::channel(32);
+9 -1
View File
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
@@ -103,18 +104,25 @@ impl StdioActor {
pub struct StdioTransport {
command: String,
args: Vec<String>,
env: HashMap<String, String>,
}
impl StdioTransport {
pub fn new<S: Into<String>>(command: S, args: Vec<String>) -> Self {
pub fn new<S: Into<String>>(
command: S,
args: Vec<String>,
env: HashMap<String, String>,
) -> Self {
Self {
command: command.into(),
args,
env: env,
}
}
async fn spawn_process(&self) -> Result<(Child, ChildStdin, ChildStdout), Error> {
let mut process = Command::new(&self.command)
.envs(&self.env)
.args(&self.args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())