Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ tracing-appender = "0.2"
agent-client-protocol = "0.14.0"

# MCP and API clients
rmcp = { version = "^1.7.0", default-features = false }
rmcp = { version = "^3.0.0", default-features = false }
async-openai = { version = "^0.41.0", features = ["byot", "chat-completion", "responses"] }
reqwest = { version = "^0.13.4", default-features = false, features = ["json", "query", "rustls", "http2", "stream"] }
oauth2 = "5.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/acp-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ pub mod testing;
// Re-export rmcp elicitation schema types so downstream crates (e.g. wisp)
// don't need a direct rmcp dependency.
pub use rmcp::model::{
ConstTitle, CreateElicitationRequestParams, ElicitationSchema, EnumSchema, MultiSelectEnumSchema, PrimitiveSchema,
ConstTitle, ElicitRequestParams, ElicitationSchema, EnumSchema, MultiSelectEnumSchema, PrimitiveSchemaDefinition,
SingleSelectEnumSchema,
};
8 changes: 4 additions & 4 deletions crates/acp-utils/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::PathBuf;
use agent_client_protocol::schema::AuthMethod;
use agent_client_protocol::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
pub use mcp_utils::display_meta::{ToolDisplayMeta, ToolResultMeta};
pub use rmcp::model::CreateElicitationRequestParams;
pub use rmcp::model::ElicitRequestParams;
use serde::{Deserialize, Serialize, de::DeserializeOwned};

pub use mcp_utils::status::{McpServerAuthCapability, McpServerStatus, McpServerStatusEntry};
Expand Down Expand Up @@ -99,7 +99,7 @@ pub struct AuthMethodsUpdatedParams {
#[request(method = "_aether/elicitation", response = ElicitationResponse)]
pub struct ElicitationParams {
pub server_name: String,
pub request: CreateElicitationRequestParams,
pub request: ElicitRequestParams,
}

pub use rmcp::model::ElicitationAction;
Expand Down Expand Up @@ -519,7 +519,7 @@ mod tests {

let params = ElicitationParams {
server_name: "github".to_string(),
request: CreateElicitationRequestParams::FormElicitationParams {
request: ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Pick a color".to_string(),
requested_schema: ElicitationSchema::builder()
Expand All @@ -542,7 +542,7 @@ mod tests {
fn elicitation_params_url_variant_has_mode_field() {
let params = ElicitationParams {
server_name: "github".to_string(),
request: CreateElicitationRequestParams::UrlElicitationParams {
request: ElicitRequestParams::UrlElicitationParams {
meta: None,
message: "Authorize GitHub".to_string(),
url: "https://github.com/login/oauth".to_string(),
Expand Down
4 changes: 2 additions & 2 deletions crates/acp-utils/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use agent_client_protocol::schema::SessionNotification;
use agent_client_protocol::{
self as acp, Agent, Builder, ByteStreams, Client, ConnectionTo, HandleDispatchFrom, NullRun, Responder,
};
use rmcp::model::{CreateElicitationRequestParams, ElicitationSchema};
use rmcp::model::{ElicitRequestParams, ElicitationSchema};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use tokio::io::DuplexStream;
Expand Down Expand Up @@ -196,7 +196,7 @@ pub async fn test_connection() -> (ConnectionTo<Client>, TestPeer) {
fn placeholder_params() -> ElicitationParams {
ElicitationParams {
server_name: String::new(),
request: CreateElicitationRequestParams::FormElicitationParams {
request: ElicitRequestParams::FormElicitationParams {
meta: None,
message: String::new(),
requested_schema: ElicitationSchema::builder().build().expect("empty schema is valid"),
Expand Down
2 changes: 1 addition & 1 deletion crates/aether-auth/src/mcp/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub async fn perform_oauth_flow(
);
}

let metadata = manager.discover_metadata().await.map_err(rmcp_err("OAuth metadata discovery failed"))?;
let metadata = manager.resolve_metadata().await.map_err(rmcp_err("OAuth metadata discovery failed"))?.metadata;
manager.set_metadata(metadata);

let scopes = manager.select_scopes(None, &[]);
Expand Down
67 changes: 39 additions & 28 deletions crates/aether-auth/tests/mcp_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,36 +70,47 @@ impl OAuthServer {
captured_requests.lock().unwrap().push(request_line.clone());
let path = request_line.split_whitespace().nth(1).unwrap();

let body = if path.contains("oauth-protected-resource") {
serde_json::json!({
"resource": format!("{origin}/mcp"),
"authorization_servers": [&origin]
})
} else if path == "/token" {
serde_json::json!({
"access_token": "access-token",
"token_type": "Bearer",
"expires_in": 3600
})
} else if path == "/register" {
serde_json::json!({
"client_id": "registered-client",
"redirect_uris": ["http://localhost:3118/"]
})
let (status, headers, body) = if path == "/mcp" {
(
"401 Unauthorized",
format!(
"WWW-Authenticate: Bearer resource_metadata=\"{origin}/.well-known/oauth-protected-resource/mcp\"\r\n"
),
String::new(),
)
} else {
serde_json::json!({
"issuer": origin,
"authorization_endpoint": format!("{origin}/authorize"),
"token_endpoint": format!("{origin}/token"),
"registration_endpoint": format!("{origin}/register"),
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid"]
})
}
.to_string();
let body = if path.contains("oauth-protected-resource") {
serde_json::json!({
"resource": format!("{origin}/mcp"),
"authorization_servers": [&origin]
})
} else if path == "/token" {
serde_json::json!({
"access_token": "access-token",
"token_type": "Bearer",
"expires_in": 3600
})
} else if path == "/register" {
serde_json::json!({
"client_id": "registered-client",
"redirect_uris": ["http://localhost:3118/"]
})
} else {
serde_json::json!({
"issuer": origin,
"authorization_endpoint": format!("{origin}/authorize"),
"token_endpoint": format!("{origin}/token"),
"registration_endpoint": format!("{origin}/register"),
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid"]
})
}
.to_string();
("200 OK", String::new(), body)
};
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\n{headers}Content-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
Expand Down
12 changes: 6 additions & 6 deletions crates/aether-cli/src/acp/fake_prompt_mcp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use rmcp::model::{
GetPromptRequestParams, GetPromptResult, Implementation, ListPromptsResult, PaginatedRequestParams,
Prompt as McpPrompt, PromptMessage, PromptMessageRole, ServerCapabilities, ServerInfo,
GetPromptRequestParams, GetPromptResponse, Implementation, ListPromptsResult, PaginatedRequestParams,
Prompt as McpPrompt, PromptMessage, Role, ServerCapabilities, ServerInfo,
};
use rmcp::service::{DynService, RequestContext};
use rmcp::{ErrorData as McpError, RoleServer, ServerHandler};
Expand Down Expand Up @@ -33,18 +33,18 @@ impl ServerHandler for FakePromptMcp {
_context: RequestContext<RoleServer>,
) -> Result<ListPromptsResult, McpError> {
let prompt = McpPrompt::new(&self.prompt_name, Some(format!("{} command", self.prompt_name)), None);
Ok(ListPromptsResult { prompts: vec![prompt], next_cursor: None, meta: None })
Ok(ListPromptsResult::with_all_items(vec![prompt]))
}

async fn get_prompt(
&self,
request: GetPromptRequestParams,
_context: RequestContext<RoleServer>,
) -> Result<GetPromptResult, McpError> {
) -> Result<GetPromptResponse, McpError> {
if request.name.as_str() != self.prompt_name {
return Err(McpError::invalid_params(format!("Prompt '{}' not found", request.name), None));
}
let messages = vec![PromptMessage::new_text(PromptMessageRole::User, format!("expanded {}", self.prompt_name))];
Ok(GetPromptResult::new(messages))
let messages = vec![PromptMessage::new_text(Role::User, format!("expanded {}", self.prompt_name))];
Ok(rmcp::model::GetPromptResult::new(messages).into())
}
}
28 changes: 15 additions & 13 deletions crates/aether-cli/src/acp/session_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use llm::catalog::LlmModel;
use llm::parser::ModelProviderParser;
use llm::{ChatMessage, ContentBlock, ProviderConnectionOverrides, ReasoningEffort};
use mcp_utils::client::{ElicitationRequest, McpClientEvent, McpServerStatusEntry, cancel_result};
use rmcp::model::{CreateElicitationRequestParams, CreateElicitationResult};
use rmcp::model::{ElicitRequestParams, ElicitResult};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{mpsc, watch};
Expand Down Expand Up @@ -592,7 +592,7 @@ async fn on_elicitation_request(connection: &ConnectionTo<Client>, elicitation:
.map_err(|e| AcpServerError::protocol("_aether/elicitation", e))
{
Ok(response) => {
let mut result = CreateElicitationResult::new(response.action);
let mut result = ElicitResult::new(response.action);
result.content = response.content;
result
}
Expand All @@ -617,7 +617,7 @@ fn spawn_elicitation_request(connection: &ConnectionTo<Client>, elicitation: Eli
}
}

fn build_elicitation_params(server_name: &str, request: &CreateElicitationRequestParams) -> ElicitationParams {
fn build_elicitation_params(server_name: &str, request: &ElicitRequestParams) -> ElicitationParams {
ElicitationParams { server_name: server_name.to_string(), request: request.clone() }
}

Expand Down Expand Up @@ -787,7 +787,7 @@ mod tests {

#[test]
fn test_build_elicitation_params_from_form() {
let elicitation = CreateElicitationRequestParams::FormElicitationParams {
let elicitation = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Pick a color".to_string(),
requested_schema: rmcp::model::ElicitationSchema::builder().required_bool("approved").build().unwrap(),
Expand All @@ -796,18 +796,19 @@ mod tests {
let params = build_elicitation_params("test-server", &elicitation);
assert_eq!(params.server_name, "test-server");
match &params.request {
CreateElicitationRequestParams::FormElicitationParams { message, requested_schema, .. } => {
ElicitRequestParams::FormElicitationParams { message, requested_schema, .. } => {
assert_eq!(message, "Pick a color");
assert_eq!(requested_schema.properties.len(), 1);
assert!(requested_schema.properties.contains_key("approved"));
}
CreateElicitationRequestParams::UrlElicitationParams { .. } => panic!("Expected Form, got Url"),
ElicitRequestParams::UrlElicitationParams { .. } => panic!("Expected Form, got Url"),
_ => panic!("Expected Form elicitation"),
}
}

#[test]
fn test_build_elicitation_params_from_url() {
let elicitation = CreateElicitationRequestParams::UrlElicitationParams {
let elicitation = ElicitRequestParams::UrlElicitationParams {
meta: None,
message: "Authorize GitHub".to_string(),
url: "https://github.com/login/oauth".to_string(),
Expand All @@ -817,12 +818,13 @@ mod tests {
let params = build_elicitation_params("github", &elicitation);
assert_eq!(params.server_name, "github");
match &params.request {
CreateElicitationRequestParams::UrlElicitationParams { message, url, elicitation_id, .. } => {
ElicitRequestParams::UrlElicitationParams { message, url, elicitation_id, .. } => {
assert_eq!(message, "Authorize GitHub");
assert_eq!(url, "https://github.com/login/oauth");
assert_eq!(elicitation_id, "el-123");
}
CreateElicitationRequestParams::FormElicitationParams { .. } => panic!("Expected Url, got Form"),
ElicitRequestParams::FormElicitationParams { .. } => panic!("Expected Url, got Form"),
_ => panic!("Expected URL elicitation"),
}
}

Expand Down Expand Up @@ -953,7 +955,7 @@ mod tests {
let (tx, rx) = oneshot::channel();
let elicitation = ElicitationRequest {
server_name: "test-server".to_string(),
request: CreateElicitationRequestParams::FormElicitationParams {
request: ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Pick a color".to_string(),
requested_schema: rmcp::model::ElicitationSchema::builder()
Expand Down Expand Up @@ -985,7 +987,7 @@ mod tests {
let (tx, rx) = oneshot::channel();
let elicitation = ElicitationRequest {
server_name: "github".to_string(),
request: CreateElicitationRequestParams::UrlElicitationParams {
request: ElicitRequestParams::UrlElicitationParams {
meta: None,
message: "Authorize".to_string(),
url: "https://example.com/oauth".to_string(),
Expand Down Expand Up @@ -1034,7 +1036,7 @@ mod tests {
let (tx, rx) = oneshot::channel();
let elicitation = ElicitationRequest {
server_name: "test-server".to_string(),
request: CreateElicitationRequestParams::FormElicitationParams {
request: ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Pick a color".to_string(),
requested_schema: rmcp::model::ElicitationSchema::builder()
Expand Down Expand Up @@ -1067,7 +1069,7 @@ mod tests {
let (tx, rx) = oneshot::channel();
let elicitation = ElicitationRequest {
server_name: "test-server".to_string(),
request: CreateElicitationRequestParams::UrlElicitationParams {
request: ElicitRequestParams::UrlElicitationParams {
meta: None,
message: "Authorize".to_string(),
url: "https://example.com".to_string(),
Expand Down
4 changes: 2 additions & 2 deletions crates/aether-cli/src/slash_commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use aether_core::mcp::run_mcp_task::McpCommand;
use agent_client_protocol::schema::AvailableCommand;
use rmcp::model::{GetPromptResult, Prompt as McpPrompt, PromptMessageContent};
use rmcp::model::{ContentBlock, GetPromptResult, Prompt as McpPrompt};
use std::collections::HashSet;
use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
Expand Down Expand Up @@ -93,7 +93,7 @@ fn prompt_result_text(prompt_result: &GetPromptResult) -> Result<String, SlashCo
.messages
.first()
.and_then(|message| match &message.content {
PromptMessageContent::Text { text } => Some(text.clone()),
ContentBlock::Text(text) => Some(text.text.clone()),
_ => None,
})
.ok_or(SlashCommandError::NoTextContent)
Expand Down
10 changes: 5 additions & 5 deletions crates/aether-core/src/events/trace_context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rmcp::model::Meta;
use rmcp::model::RequestMetaObject;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

Expand All @@ -21,15 +21,15 @@ pub struct TraceContext {
impl TraceContext {
/// Reads the trace context a caller attached to MCP request metadata via
/// [`to_meta`](Self::to_meta), if any.
pub fn from_meta(meta: &Meta) -> Option<Self> {
pub fn from_meta(meta: &RequestMetaObject) -> Option<Self> {
let traceparent = meta.0.get(TRACEPARENT_KEY)?.as_str()?.to_string();
let tracestate = meta.0.get(TRACESTATE_KEY).and_then(|value| value.as_str()).map(str::to_string);
Some(Self { traceparent, tracestate })
}

/// Attaches the trace context to MCP request metadata.
pub fn to_meta(&self) -> Meta {
let mut meta = Meta::new();
pub fn to_meta(&self) -> RequestMetaObject {
let mut meta = RequestMetaObject::new();
meta.0.insert(TRACEPARENT_KEY.to_string(), self.traceparent.clone().into());
if let Some(tracestate) = &self.tracestate {
meta.0.insert(TRACESTATE_KEY.to_string(), tracestate.clone().into());
Expand Down Expand Up @@ -67,6 +67,6 @@ mod tests {

#[test]
fn from_meta_returns_none_without_a_traceparent() {
assert_eq!(TraceContext::from_meta(&Meta::new()), None);
assert_eq!(TraceContext::from_meta(&RequestMetaObject::new()), None);
}
}
Loading