Skip to content

Commit 08bbf61

Browse files
authored
Merge pull request #98 from ovh/feature/agent-config
easier agent configuration
2 parents 4c221b3 + fe17081 commit 08bbf61

File tree

10 files changed

+133
-51
lines changed

10 files changed

+133
-51
lines changed

.ovh.config

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
{
22
"name": "ovh",
33
"description": "OVH agent with OVH MCP server for cloud management and API calls",
4-
"llm_provider": {
5-
"provider": "ovhcloud",
6-
"env_vars": {
7-
"OVH_BASE_URL": "https://gpt-oss-120b.endpoints.kepler.ai.cloud.ovh.net/api/openai_compat/v1"
8-
},
9-
"model": "gpt-oss-120b",
10-
"tool_method": "FunctionCall"
11-
},
124
"tools": {
135
"builtin": ["*"],
146
"mcp": {
@@ -17,7 +9,10 @@
179
"type": "http",
1810
"url": "https://mcp.eu.ovhcloud.com/mcp"
1911
},
20-
"enabled_tools": ["*"]
12+
"enabled_tools": ["*"],
13+
"excluded_tools": [
14+
"get-cloud-project-flavor-list"
15+
]
2116
}
2217
}
2318
},

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,17 @@ Instead of a single global configuration, you can create custom agent in a separ
137137

138138
[`.ovh.config`](./.ovh.config) contains an example of a custom configuration with an remote MCP server configured.
139139

140-
Place this file in `~/.config/shai/agents/example.config`, you can then list the agents available with:
140+
Place this file in `~/.config/shai/agents/ovh.config`, you can then list the agents available with:
141141

142142
```bash
143+
curl https://raw.githubusercontent.com/ovh/shai/refs/heads/main/.ovh.config -o ˜/.config/shai/agents/ovh.config
143144
shai agent list
144145
```
145146

146147
You can run shai with this specific agent with the `agent` subcommand:
147148

148149
```bash
149-
shai agent example
150+
shai agent ovh
150151
```
151152

152153
### OVHCloud Endpoints

shai-core/examples/oauth_test.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@ use shai_core::tools::mcp::mcp_oauth::signin_oauth;
33
#[tokio::main]
44
async fn main() {
55
println!("🚀 Starting OAuth flow test...");
6-
6+
77
match signin_oauth("https://mcp.eu.ovhcloud.com/").await {
8-
Ok(access_token) => {
8+
Ok(token) => {
99
println!("✅ OAuth flow completed successfully!");
10-
println!("🎫 Access Token: {}", access_token);
11-
println!("🔑 Token length: {} characters", access_token.len());
10+
println!("🎫 Access Token: {}", token.access_token);
11+
println!("🔑 Token length: {} characters", token.access_token.len());
12+
if let Some(expires_at) = token.expires_at {
13+
let now = std::time::SystemTime::now()
14+
.duration_since(std::time::UNIX_EPOCH)
15+
.unwrap()
16+
.as_secs() as i64;
17+
let seconds_until_expiry = expires_at - now;
18+
println!("⏰ Token expires in {} seconds", seconds_until_expiry);
19+
} else {
20+
println!("⏰ Token has no expiration");
21+
}
1222
}
1323
Err(e) => {
1424
println!("❌ OAuth flow failed: {}", e);

shai-core/src/agent/builder.rs

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ impl AgentBuilder {
146146
LlmClient::create_provider(&config.llm_provider.provider, &config.llm_provider.env_vars)
147147
.map_err(|e| AgentError::LlmError(e.to_string()))?
148148
);
149-
149+
150150
// Create brain with custom system prompt and temperature
151151
let brain = Box::new(CoderBrain::with_custom_prompt(
152152
llm_client.clone(),
@@ -275,43 +275,57 @@ impl AgentBuilder {
275275
/// Handle OAuth flow for MCP connections if needed
276276
async fn mcp_check_oauth(mcp_name: &str, mcp_config: &mut McpConfig) -> Result<bool, AgentError> {
277277
use crate::tools::mcp::McpConfig;
278-
279278
let mut config_changed = false;
280-
279+
281280
// Only handle HTTP configs that might need OAuth
282-
if let McpConfig::Http { url, bearer_token } = mcp_config {
283-
// Test connection with current config
284-
let test_config = McpConfig::Http {
285-
url: url.clone(),
286-
bearer_token: bearer_token.clone()
287-
};
288-
let mut test_client = create_mcp_client(test_config);
289-
match test_client.connect().await {
290-
Ok(_) => {
291-
if bearer_token.is_some() {
281+
if let McpConfig::Http { url, auth } = mcp_config {
282+
let needs_new_token = match auth {
283+
Some(token) if token.is_expired() => {
284+
eprintln!("\x1b[2m░ MCP '{}' token expired, refreshing...\x1b[0m", mcp_name);
285+
true
286+
}
287+
Some(_) => {
288+
// Test connection with existing token
289+
let test_config = McpConfig::Http { url: url.clone(), auth: auth.clone() };
290+
let mut test_client = create_mcp_client(test_config);
291+
if test_client.connect().await.is_ok() {
292292
eprintln!("\x1b[2m░ MCP '{}' connected (authenticated)\x1b[0m", mcp_name);
293+
false
293294
} else {
294-
eprintln!("\x1b[2m░ MCP '{}' connected (no auth)\x1b[0m", mcp_name);
295+
eprintln!("\x1b[2m░ MCP '{}' authentication failed, refreshing token...\x1b[0m", mcp_name);
296+
true
295297
}
296298
}
297-
Err(_) => {
298-
eprintln!("\x1b[2m░ MCP '{}' connection failed, starting OAuth flow...\x1b[0m", mcp_name);
299-
let url_clone = url.clone();
300-
match signin_oauth(&url_clone).await {
301-
Ok(token) => {
302-
eprintln!("\x1b[2m░ MCP '{}' connected (OAuth successful)\x1b[0m", mcp_name);
303-
*bearer_token = Some(token);
304-
config_changed = true;
305-
}
306-
Err(e) => {
307-
return Err(AgentError::ConfigurationError(format!("OAuth failed for MCP '{}': {}", mcp_name, e)));
308-
}
299+
None => {
300+
// Test connection without auth
301+
let test_config = McpConfig::Http { url: url.clone(), auth: None };
302+
let mut test_client = create_mcp_client(test_config);
303+
if test_client.connect().await.is_ok() {
304+
eprintln!("\x1b[2m░ MCP '{}' connected (no auth required)\x1b[0m", mcp_name);
305+
false
306+
} else {
307+
eprintln!("\x1b[2m░ MCP '{}' requires authentication, starting OAuth flow...\x1b[0m", mcp_name);
308+
true
309+
}
310+
}
311+
};
312+
313+
if needs_new_token {
314+
let url_clone = url.clone();
315+
match signin_oauth(&url_clone).await {
316+
Ok(token) => {
317+
eprintln!("\x1b[2m░ MCP '{}' OAuth successful\x1b[0m", mcp_name);
318+
*auth = Some(token);
319+
config_changed = true;
320+
}
321+
Err(e) => {
322+
return Err(AgentError::ConfigurationError(format!("OAuth failed for MCP '{}': {}", mcp_name, e)));
309323
}
310324
}
311325
}
312326
}
313327
// SSE and Stdio don't need OAuth handling for now
314-
328+
315329
Ok(config_changed)
316330
}
317331
}

shai-core/src/config/agent.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::PathBuf;
33
use serde::{Serialize, Deserialize};
44
use shai_llm::ToolCallMethod;
55
use crate::tools::mcp::McpConfig;
6+
use super::config::ShaiConfig;
67

78
#[derive(Debug, Clone, Serialize, Deserialize)]
89
pub struct AgentProviderConfig {
@@ -35,6 +36,7 @@ pub struct AgentTools {
3536
pub struct AgentConfig {
3637
pub name: String,
3738
pub description: String,
39+
#[serde(default = "default_llm_provider")]
3840
pub llm_provider: AgentProviderConfig,
3941
#[serde(default)]
4042
pub tools: AgentTools,
@@ -46,6 +48,23 @@ pub struct AgentConfig {
4648
pub temperature: f32,
4749
}
4850

51+
fn default_llm_provider() -> AgentProviderConfig {
52+
// Load the default provider from ShaiConfig
53+
let shai_config = ShaiConfig::load()
54+
.unwrap_or_else(|_| ShaiConfig::default());
55+
56+
let provider_config = shai_config
57+
.get_selected_provider()
58+
.expect("No provider configured in default config");
59+
60+
AgentProviderConfig {
61+
provider: provider_config.provider.clone(),
62+
env_vars: provider_config.env_vars.clone(),
63+
model: provider_config.model.clone(),
64+
tool_method: provider_config.tool_method.clone(),
65+
}
66+
}
67+
4968
fn default_system_prompt() -> String {
5069
"{{CODER_BASE_PROMPT}}".to_string()
5170
}

shai-core/src/config/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ impl ShaiConfig {
199199
.map(|(name, config)| {
200200
let description = match config {
201201
McpConfig::Stdio { command, .. } => format!("stdio: {}", command),
202-
McpConfig::Http { url, bearer_token } => {
203-
if bearer_token.is_some() {
202+
McpConfig::Http { url, auth } => {
203+
if auth.is_some() {
204204
format!("http: {} (authenticated)", url)
205205
} else {
206206
format!("http: {}", url)

shai-core/src/tools/mcp/mcp_config.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,54 @@ use serde::{Serialize, Deserialize};
33

44
use super::{StdioClient, HttpClient, SseClient};
55

6+
#[derive(Debug, Clone, Serialize, Deserialize)]
7+
pub struct OAuthToken {
8+
pub access_token: String,
9+
/// Unix timestamp (seconds since epoch) when the token expires
10+
pub expires_at: Option<i64>,
11+
}
12+
613
#[derive(Debug, Clone, Serialize, Deserialize)]
714
#[serde(tag = "type")]
815
pub enum McpConfig {
916
#[serde(rename = "stdio")]
1017
Stdio { command: String, args: Vec<String> },
1118
#[serde(rename = "http")]
12-
Http { url: String, bearer_token: Option<String> },
19+
Http {
20+
url: String,
21+
#[serde(flatten)]
22+
auth: Option<OAuthToken>
23+
},
1324
#[serde(rename = "sse")]
1425
Sse { url: String },
1526
}
1627

28+
impl OAuthToken {
29+
/// Check if the token is expired or will expire within the next 60 seconds
30+
pub fn is_expired(&self) -> bool {
31+
if let Some(expires_at) = self.expires_at {
32+
let now = std::time::SystemTime::now()
33+
.duration_since(std::time::UNIX_EPOCH)
34+
.unwrap()
35+
.as_secs() as i64;
36+
37+
// Consider expired if it expires within the next 60 seconds (safety margin)
38+
expires_at <= now + 60
39+
} else {
40+
// If no expiration time, assume it's still valid
41+
false
42+
}
43+
}
44+
}
45+
1746
/// Factory function to create an MCP client from configuration
1847
pub fn create_mcp_client(config: McpConfig) -> Box<dyn McpClient> {
1948
match config {
2049
McpConfig::Stdio { command, args } => {
2150
Box::new(StdioClient::new(command, args))
2251
}
23-
McpConfig::Http { url, bearer_token } => {
52+
McpConfig::Http { url, auth } => {
53+
let bearer_token = auth.map(|t| t.access_token);
2454
Box::new(HttpClient::new_with_auth(url, bearer_token))
2555
}
2656
McpConfig::Sse { url } => {

shai-core/src/tools/mcp/mcp_oauth.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use oauth2::{
22
AuthUrl, TokenUrl, ClientId, ClientSecret, RedirectUrl, CsrfToken,
3-
AuthorizationCode, PkceCodeChallenge, Scope,
3+
AuthorizationCode, PkceCodeChallenge, Scope,
44
basic::BasicClient, reqwest::async_http_client, TokenResponse,
55
AuthType, url::Url,
66
};
@@ -9,6 +9,7 @@ use std::sync::{Arc, Mutex};
99
use reqwest;
1010
use serde::{Deserialize, Serialize};
1111
use tokio::net::TcpListener;
12+
use super::mcp_config::OAuthToken;
1213

1314
#[derive(Serialize)]
1415
struct ClientRegistrationRequest {
@@ -24,7 +25,7 @@ struct ClientRegistrationResponse {
2425
client_secret: Option<String>,
2526
}
2627

27-
pub async fn signin_oauth(base_url: &str) -> anyhow::Result<String> {
28+
pub async fn signin_oauth(base_url: &str) -> anyhow::Result<OAuthToken> {
2829
// Extract the root domain for .well-known endpoint (OAuth standard)
2930
let url = Url::parse(base_url)?;
3031
let root_url = format!("{}://{}", url.scheme(), url.host_str().unwrap_or(""));
@@ -221,5 +222,17 @@ pub async fn signin_oauth(base_url: &str) -> anyhow::Result<String> {
221222
.request_async(async_http_client)
222223
.await?;
223224

224-
Ok(token_response.access_token().secret().to_string())
225+
// Calculate expiration time
226+
let expires_at = token_response.expires_in().map(|duration| {
227+
let now = std::time::SystemTime::now()
228+
.duration_since(std::time::UNIX_EPOCH)
229+
.unwrap()
230+
.as_secs() as i64;
231+
now + duration.as_secs() as i64
232+
});
233+
234+
Ok(OAuthToken {
235+
access_token: token_response.access_token().secret().to_string(),
236+
expires_at,
237+
})
225238
}

shai-core/src/tools/mcp/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub mod mcp_oauth;
99
mod tests;
1010

1111
pub use mcp::{McpClient, McpToolDescription, get_mcp_tools};
12-
pub use mcp_config::{McpConfig, create_mcp_client};
12+
pub use mcp_config::{McpConfig, OAuthToken, create_mcp_client};
1313
pub use mcp_stdio::StdioClient;
1414
pub use mcp_http::HttpClient;
1515
pub use mcp_sse::SseClient;

shai-core/src/tools/mcp/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ mod tests {
148148
// Test HTTP config
149149
let http_config = McpConfig::Http {
150150
url: "http://localhost:8080".to_string(),
151-
bearer_token: None
151+
auth: None
152152
};
153153
let _http_client = create_mcp_client(http_config);
154154
println!("✅ Successfully created HttpClient via factory");

0 commit comments

Comments
 (0)