Skip to content
Open
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
13 changes: 9 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 @@ -44,7 +44,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-beta.2", 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
8 changes: 6 additions & 2 deletions crates/aether-auth/src/mcp/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ pub async fn perform_oauth_flow(
);
}

let metadata = manager.discover_metadata().await.map_err(rmcp_err("OAuth metadata discovery failed"))?;
manager.set_metadata(metadata);
// rmcp 3 resolves metadata through protected-resource metadata (RFC 9728),
// then authorization-server metadata, then a legacy-endpoint fallback. The
// returned metadata must be handed back via `set_metadata` before any client
// is configured.
let resolution = manager.resolve_metadata().await.map_err(rmcp_err("OAuth metadata discovery failed"))?;
manager.set_metadata(resolution.metadata);

let scopes = manager.select_scopes(None, &[]);
let scope_refs = scopes.iter().map(String::as_str).collect::<Vec<_>>();
Expand Down
77 changes: 51 additions & 26 deletions crates/aether-auth/tests/mcp_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,36 +70,61 @@ 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]
})
let (status, reason, body) = if path.contains("oauth-protected-resource") {
(
200,
"OK",
serde_json::json!({
"resource": format!("{origin}/mcp"),
"authorization_servers": [&origin]
})
.to_string(),
)
} else if path == "/token" {
serde_json::json!({
"access_token": "access-token",
"token_type": "Bearer",
"expires_in": 3600
})
(
200,
"OK",
serde_json::json!({
"access_token": "access-token",
"token_type": "Bearer",
"expires_in": 3600
})
.to_string(),
)
} else if path == "/register" {
serde_json::json!({
"client_id": "registered-client",
"redirect_uris": ["http://localhost:3118/"]
})
(
200,
"OK",
serde_json::json!({
"client_id": "registered-client",
"redirect_uris": ["http://localhost:3118/"]
})
.to_string(),
)
} else if path == "/mcp" {
// The MCP endpoint is a protected resource: an
// unauthenticated GET is rejected, which drives rmcp's
// RFC 9728 protected-resource-metadata discovery through
// the oauth-protected-resource well-known lookup.
(401, "Unauthorized", 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();
(
200,
"OK",
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 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} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
Expand Down
2 changes: 1 addition & 1 deletion crates/aether-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ tui = { package = "aether-tui", path = "../tui", default-features = false, featu
acp_utils = { package = "aether-acp-utils", path = "../acp-utils", features = ["server", "testing"], version = "0.3.33" }
agent-client-protocol = { workspace = true }
aether-lspd = { path = "../aether-lspd", version = "0.1.25" }
rmcp = { workspace = true, features = ["client", "elicitation", "server", "transport-streamable-http-client-reqwest"] }
rmcp = { workspace = true, features = ["client", "elicitation", "request-state", "server", "transport-streamable-http-client-reqwest"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros", "sync", "io-util", "net", "fs", "time"] }
tokio-util = { workspace = true, features = ["compat"] }
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, GetPromptResult, 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(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!("unexpected elicitation request variant"),
}
}

#[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!("unexpected elicitation request variant"),
}
}

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_content) => Some(text_content.text.clone()),
_ => None,
})
.ok_or(SlashCommandError::NoTextContent)
Expand Down
Loading
Loading