Skip to content

Commit 0099f76

Browse files
committed
reformats sign in messages
1 parent da747e1 commit 0099f76

File tree

4 files changed

+123
-28
lines changed

4 files changed

+123
-28
lines changed

crates/chat-cli/src/cli/agent/mod.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,15 @@ impl Default for Agent {
181181
set.extend(default_approve);
182182
set
183183
},
184-
resources: vec!["file://AmazonQ.md", "file://AGENTS.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
185-
.into_iter()
186-
.map(Into::into)
187-
.collect::<Vec<_>>(),
184+
resources: vec![
185+
"file://AmazonQ.md",
186+
"file://AGENTS.md",
187+
"file://README.md",
188+
"file://.amazonq/rules/**/*.md",
189+
]
190+
.into_iter()
191+
.map(Into::into)
192+
.collect::<Vec<_>>(),
188193
hooks: Default::default(),
189194
tools_settings: Default::default(),
190195
use_legacy_mcp_json: true,

crates/chat-cli/src/cli/chat/tool_manager.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ enum LoadingMsg {
138138
/// This is sent when all tool initialization is complete or when the application is shutting
139139
/// down.
140140
Terminate { still_loading: Vec<String> },
141+
/// Indicates that a server requires user authentication and provides a sign-in link.
142+
/// This message is used to notify the user about authentication requirements for MCP servers
143+
/// that need OAuth or other authentication methods. Contains the server name and the
144+
/// authentication message (typically a URL or instructions).
145+
SignInNotice { name: String },
141146
}
142147

143148
/// Used to denote the loading outcome associated with a server.
@@ -1183,6 +1188,15 @@ fn spawn_display_task(
11831188
execute!(output, style::Print("\n"),)?;
11841189
break;
11851190
},
1191+
LoadingMsg::SignInNotice { name } => {
1192+
execute!(
1193+
output,
1194+
cursor::MoveToColumn(0),
1195+
cursor::MoveUp(1),
1196+
terminal::Clear(terminal::ClearType::CurrentLine),
1197+
)?;
1198+
queue_oauth_message(&name, &mut output)?;
1199+
},
11861200
},
11871201
Err(_e) => {
11881202
spinner_logo_idx = (spinner_logo_idx + 1) % SPINNER_CHARS.len();
@@ -1611,7 +1625,7 @@ fn spawn_orchestrator_task(
16111625
UpdateEventMessage::OauthLink { server_name, link } => {
16121626
let mut buf_writer = BufWriter::new(&mut *record_temp_buf);
16131627
let msg = eyre::eyre!(link);
1614-
let _ = queue_oauth_message(server_name.as_str(), &msg, &mut buf_writer);
1628+
let _ = queue_oauth_message_with_link(server_name.as_str(), &msg, &mut buf_writer);
16151629
let _ = buf_writer.flush();
16161630
drop(buf_writer);
16171631
let record_str = String::from_utf8_lossy(record_temp_buf).to_string();
@@ -1625,10 +1639,8 @@ fn spawn_orchestrator_task(
16251639
})
16261640
.or_insert(vec![record]);
16271641
if let Some(sender) = &loading_status_sender {
1628-
let msg = LoadingMsg::Warn {
1642+
let msg = LoadingMsg::SignInNotice {
16291643
name: server_name.clone(),
1630-
msg: eyre::eyre!("{}", record_str),
1631-
time: "0.0".to_string(),
16321644
};
16331645
if let Err(e) = sender.send(msg).await {
16341646
warn!(
@@ -1920,18 +1932,31 @@ fn queue_failure_message(
19201932
)?)
19211933
}
19221934

1923-
fn queue_oauth_message(name: &str, msg: &eyre::Report, output: &mut impl Write) -> eyre::Result<()> {
1935+
fn queue_oauth_message(name: &str, output: &mut impl Write) -> eyre::Result<()> {
1936+
Ok(queue!(
1937+
output,
1938+
style::SetForegroundColor(style::Color::Yellow),
1939+
style::Print("⚠ "),
1940+
style::SetForegroundColor(style::Color::Blue),
1941+
style::Print(name),
1942+
style::ResetColor,
1943+
style::Print(" requires OAuth authentication. Use /mcp to see the auth link\n"),
1944+
)?)
1945+
}
1946+
1947+
fn queue_oauth_message_with_link(name: &str, msg: &eyre::Report, output: &mut impl Write) -> eyre::Result<()> {
19241948
Ok(queue!(
19251949
output,
19261950
style::SetForegroundColor(style::Color::Yellow),
19271951
style::Print("⚠ "),
19281952
style::SetForegroundColor(style::Color::Blue),
19291953
style::Print(name),
19301954
style::ResetColor,
1931-
style::Print(" requires OAuth authentication. Please visit:\n"),
1932-
style::SetForegroundColor(style::Color::Cyan),
1955+
style::Print(" requires OAuth authentication. Follow this link to proceed: \n"),
1956+
style::SetForegroundColor(style::Color::Yellow),
19331957
style::Print(msg),
19341958
style::ResetColor,
1959+
style::Print("\n")
19351960
)?)
19361961
}
19371962

crates/chat-cli/src/mcp_client/client.rs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@ use std::process::Stdio;
55
use regex::Regex;
66
use reqwest::Client;
77
use rmcp::model::{
8-
CallToolRequestParam, CallToolResult, ErrorCode, GetPromptRequestParam, GetPromptResult, Implementation, InitializeRequestParam, ListPromptsResult, ListToolsResult, LoggingLevel, LoggingMessageNotificationParam, PaginatedRequestParam, ServerNotification, ServerRequest
8+
CallToolRequestParam,
9+
CallToolResult,
10+
ErrorCode,
11+
GetPromptRequestParam,
12+
GetPromptResult,
13+
Implementation,
14+
InitializeRequestParam,
15+
ListPromptsResult,
16+
ListToolsResult,
17+
LoggingLevel,
18+
LoggingMessageNotificationParam,
19+
PaginatedRequestParam,
20+
ServerNotification,
21+
ServerRequest,
922
};
1023
use rmcp::service::{
1124
ClientInitializeError,
@@ -31,6 +44,7 @@ use tokio::process::{
3144
};
3245
use tokio::task::JoinHandle;
3346
use tracing::{
47+
debug,
3448
error,
3549
info,
3650
};
@@ -152,7 +166,7 @@ macro_rules! decorate_with_auth_retry {
152166
// TODO: discern error type prior to retrying
153167
// Not entirely sure what is thrown when auth is required
154168
if let Some(auth_client) = self.get_auth_client() {
155-
let refresh_result = auth_client.auth_manager.lock().await.refresh_token().await;
169+
let refresh_result = auth_client.get_access_token().await;
156170
match refresh_result {
157171
Ok(_) => {
158172
// Retry the operation after token refresh
@@ -163,8 +177,11 @@ macro_rules! decorate_with_auth_retry {
163177
},
164178
Err(_) => {
165179
// If refresh fails, return the original error
180+
// Currently our event loop just does not allow us easy ways to
181+
// reauth entirely once a session starts since this would mean
182+
// swapping of transport (which also means swapping of client)
166183
Err(e)
167-
}
184+
},
168185
}
169186
} else {
170187
// No auth client available, return original error
@@ -176,6 +193,11 @@ macro_rules! decorate_with_auth_retry {
176193
};
177194
}
178195

196+
/// Wrapper around rmcp service types to enable cloning.
197+
///
198+
/// This exists because `rmcp::service::RunningService` is not directly cloneable as it is a
199+
/// pointer type to `Peer<C>`. This enum allows us to hold either the original service or its
200+
/// peer representation, enabling cloning by converting the original service to a peer when needed.
179201
pub enum InnerService {
180202
Original(rmcp::service::RunningService<RoleClient, Box<dyn DynService<RoleClient>>>),
181203
Peer(rmcp::service::Peer<RoleClient>),
@@ -194,11 +216,22 @@ impl Clone for InnerService {
194216
fn clone(&self) -> Self {
195217
match self {
196218
InnerService::Original(rs) => InnerService::Peer((*rs).clone()),
197-
InnerService::Peer(peer) => InnerService::Peer(peer.clone())
219+
InnerService::Peer(peer) => InnerService::Peer(peer.clone()),
198220
}
199221
}
200222
}
201223

224+
/// A wrapper around MCP (Model Context Protocol) service instances that manages
225+
/// authentication and enables cloning functionality.
226+
///
227+
/// This struct holds either an original `RunningService` or its peer representation,
228+
/// along with an optional authentication drop guard for managing OAuth tokens.
229+
/// The authentication drop guard handles token lifecycle and cleanup when the
230+
/// service is dropped.
231+
///
232+
/// # Fields
233+
/// * `inner_service` - The underlying MCP service instance (original or peer)
234+
/// * `auth_dropguard` - Optional authentication manager for OAuth token handling
202235
#[derive(Debug)]
203236
pub struct RunningService {
204237
pub inner_service: InnerService,
@@ -215,18 +248,19 @@ impl Clone for RunningService {
215248

216249
RunningService {
217250
inner_service: self.inner_service.clone(),
218-
auth_dropguard
251+
auth_dropguard,
219252
}
220253
}
221254
}
222255

223256
impl RunningService {
257+
decorate_with_auth_retry!(CallToolRequestParam, call_tool, CallToolResult);
258+
259+
decorate_with_auth_retry!(GetPromptRequestParam, get_prompt, GetPromptResult);
260+
224261
pub fn get_auth_client(&self) -> Option<AuthClient<Client>> {
225262
self.auth_dropguard.as_ref().map(|a| a.auth_client.clone())
226263
}
227-
228-
decorate_with_auth_retry!(CallToolRequestParam, call_tool, CallToolResult);
229-
decorate_with_auth_retry!(GetPromptRequestParam, get_prompt, GetPromptResult);
230264
}
231265

232266
pub type StdioTransport = (TokioChildProcess, Option<ChildStderr>);
@@ -304,16 +338,17 @@ impl McpClientService {
304338
let service = match self.into_dyn().serve(transport).await.map_err(Box::new) {
305339
Ok(service) => service,
306340
Err(e) if matches!(*e, ClientInitializeError::ConnectionClosed(_)) => {
341+
debug!("## mcp: first hand shake attempt failed: {:?}", e);
307342
let refresh_res =
308-
auth_dg.auth_client.auth_manager.lock().await.refresh_token().await;
343+
auth_dg.auth_client.get_access_token().await;
309344
let new_self = McpClientService::new(
310345
server_name.clone(),
311346
backup_config,
312347
messenger_clone.clone(),
313348
);
314349

315350
let new_transport =
316-
get_http_transport(&os_clone, true, &url, &*messenger_dup).await?;
351+
get_http_transport(&os_clone, true, &url, Some(auth_dg.auth_client.clone()), &*messenger_dup).await?;
317352

318353
match new_transport {
319354
HttpTransport::WithAuth((new_transport, new_auth_dg)) => {
@@ -325,13 +360,14 @@ impl McpClientService {
325360
new_self.into_dyn().serve(new_transport).await.map_err(Box::new)?
326361
},
327362
Err(e) => {
363+
error!("## mcp: token refresh attempt failed: {:?}", e);
328364
info!("Retry for http transport failed {e}. Possible reauth needed");
329365
// This could be because the refresh token is expired, in which
330366
// case we would need to have user go through the auth flow
331367
// again
332368
let new_transport =
333-
get_http_transport(&os_clone, true, &url, &*messenger_dup).await?;
334-
369+
get_http_transport(&os_clone, true, &url, None, &*messenger_dup).await?;
370+
335371
match new_transport {
336372
HttpTransport::WithAuth((new_transport, new_auth_dg)) => {
337373
auth_dg = new_auth_dg;
@@ -345,7 +381,7 @@ impl McpClientService {
345381
},
346382
}
347383
},
348-
HttpTransport::WithoutAuth(new_transport) =>
384+
HttpTransport::WithoutAuth(new_transport) =>
349385
new_self.into_dyn().serve(new_transport).await.map_err(Box::new)?,
350386
}
351387
},
@@ -487,7 +523,7 @@ impl McpClientService {
487523
Ok(Transport::Stdio((tokio_child_process, child_stderr)))
488524
},
489525
TransportType::Http => {
490-
let http_transport = get_http_transport(os, false, url, messenger).await?;
526+
let http_transport = get_http_transport(os, false, url, None, messenger).await?;
491527

492528
Ok(Transport::Http(http_transport))
493529
},

crates/chat-cli/src/mcp_client/oauth_util.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use sha2::{
3333
use tokio::sync::oneshot::Sender;
3434
use tokio_util::sync::CancellationToken;
3535
use tracing::{
36+
debug,
3637
error,
3738
info,
3839
};
@@ -78,6 +79,13 @@ impl Drop for LoopBackDropGuard {
7879
}
7980
}
8081

82+
/// A guard that manages the lifecycle of an authenticated MCP client and automatically
83+
/// persists OAuth credentials when dropped.
84+
///
85+
/// This struct wraps an `AuthClient` and ensures that OAuth tokens are written to disk
86+
/// when the guard goes out of scope, unless explicitly disabled via `should_write`.
87+
/// This provides automatic credential caching for MCP server connections that require
88+
/// OAuth authentication.
8189
#[derive(Clone, Debug)]
8290
pub struct AuthClientDropGuard {
8391
pub should_write: bool,
@@ -136,6 +144,16 @@ impl Drop for AuthClientDropGuard {
136144
}
137145
}
138146

147+
/// HTTP transport wrapper that handles both authenticated and non-authenticated MCP connections.
148+
///
149+
/// This enum provides two variants for different authentication scenarios:
150+
/// - `WithAuth`: Used when the MCP server requires OAuth authentication, containing both the
151+
/// transport worker and an auth client guard that manages credential persistence
152+
/// - `WithoutAuth`: Used for servers that don't require authentication, containing only the basic
153+
/// transport worker
154+
///
155+
/// The appropriate variant is automatically selected based on the server's response to
156+
/// an initial probe request during transport creation.
139157
pub enum HttpTransport {
140158
WithAuth(
141159
(
@@ -150,6 +168,7 @@ pub async fn get_http_transport(
150168
os: &Os,
151169
delete_cache: bool,
152170
url: &str,
171+
auth_client: Option<AuthClient<Client>>,
153172
messenger: &dyn Messenger,
154173
) -> Result<HttpTransport, OauthUtilError> {
155174
let cred_dir = get_mcp_auth_dir(os)?;
@@ -165,8 +184,14 @@ pub async fn get_http_transport(
165184
let probe_resp = reqwest_client.get(url.clone()).send().await?;
166185
match probe_resp.status() {
167186
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
168-
let am = get_auth_manager(url.clone(), cred_full_path.clone(), messenger).await?;
169-
let auth_client = AuthClient::new(reqwest_client, am);
187+
debug!("## mcp: requires auth, auth client passed in is {:?}", auth_client);
188+
let auth_client = match auth_client {
189+
Some(auth_client) => auth_client,
190+
None => {
191+
let am = get_auth_manager(url.clone(), cred_full_path.clone(), messenger).await?;
192+
AuthClient::new(reqwest_client, am)
193+
},
194+
};
170195
let transport =
171196
StreamableHttpClientTransport::with_client(auth_client.clone(), StreamableHttpClientTransportConfig {
172197
uri: url.as_str().into(),
@@ -175,6 +200,7 @@ pub async fn get_http_transport(
175200
});
176201

177202
let auth_dg = AuthClientDropGuard::new(cred_full_path, auth_client);
203+
debug!("## mcp: transport obtained");
178204

179205
Ok(HttpTransport::WithAuth((transport, auth_dg)))
180206
},
@@ -191,7 +217,7 @@ async fn get_auth_manager(
191217
cred_full_path: PathBuf,
192218
messenger: &dyn Messenger,
193219
) -> Result<AuthorizationManager, OauthUtilError> {
194-
let content_as_bytes = tokio::fs::read(cred_full_path).await;
220+
let content_as_bytes = tokio::fs::read(&cred_full_path).await;
195221
let mut oauth_state = OAuthState::new(url, None).await?;
196222

197223
match content_as_bytes {
@@ -200,12 +226,15 @@ async fn get_auth_manager(
200226

201227
oauth_state.set_credentials("id", token).await?;
202228

229+
debug!("## mcp: credentials set with cache");
230+
203231
Ok(oauth_state
204232
.into_authorization_manager()
205233
.ok_or(OauthUtilError::MissingAuthorizationManager)?)
206234
},
207235
Err(e) => {
208236
info!("Error reading cached credentials: {e}");
237+
debug!("## mcp: cache read failed. constructing auth manager from scratch");
209238
get_auth_manager_impl(oauth_state, messenger).await
210239
},
211240
}

0 commit comments

Comments
 (0)