From 7bc730b3043ba03c76a119a2258cf00447202397 Mon Sep 17 00:00:00 2001 From: cafalchio Date: Thu, 27 Aug 2026 17:13:16 +0100 Subject: [PATCH 1/2] Added complete/completion Signed-off-by: cafalchio --- .../src/gateway/identifier_routing.rs | 16 +++++- .../src/gateway/mcp_service/completion.rs | 54 ++++++++++++++++--- tests/conformance/expected-failures.yml | 1 - 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index ef98ac1a..041d8566 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -1,5 +1,5 @@ use contextforge_data_plane_apis::user_store::VirtualHost; -use rmcp::{ErrorData, model::ErrorCode, service::ServiceError}; +use rmcp::{ErrorData, model::{ErrorCode, Reference}, service::ServiceError}; use tracing::warn; /// Preserves identifiers for a single backend. For multiple backends, splits a @@ -73,6 +73,20 @@ pub(super) fn resolve_prompt_route<'a, N: AsRef>( alias.or_else(|| route_identifier(name, backend_names)) } +pub(super) fn resolve_complete_route<'a, N: AsRef>( + virtual_host: &'a VirtualHost, + name: &'a str, + rref: &Reference, + backend_names: &'a [N], +) -> Option<(&'a str, &'a str)> { + let reference = match rref { + Reference::Prompt(_) => resolve_prompt_route(virtual_host, name, backend_names), + Reference::Resource(_) => resolve_resources_route(virtual_host, name, backend_names), + _ => None, + }; + reference +} + /// Returns the control-plane alias for an upstream tool when configured. Without an alias, /// single-backend hosts preserve the upstream name and multi-backend hosts use the legacy prefix. #[allow(dead_code)] diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs index c8d83e14..ebf735e4 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs @@ -3,18 +3,58 @@ use rmcp::{ model::{CompleteRequestParams, CompleteResult, ErrorCode}, service::RequestContext, }; +use tracing::info; + +use crate::gateway::{ + identifier_routing::{backend_forward_error, resolve_complete_route}, + mcp_call_validator::AuthorizedCallValidator, + mcp_service::initialization::connect_backend_for_request, +}; use super::McpService; #[allow(clippy::unused_async)] pub(super) async fn complete( - _: &McpService, - _: CompleteRequestParams, - _: RequestContext, + mcp_service: &McpService, + request: CompleteRequestParams, + cx: RequestContext, ) -> Result { - Err(ErrorData { - code: ErrorCode::INVALID_REQUEST, - message: "Fan out not supported at the moment. Go to control plane".into(), + info!("\n\n\n---------- request ----- {:?}", &request); + let mcp_call_validator = AuthorizedCallValidator::new("complete", &cx); + let (virtual_host, _claims) = mcp_call_validator.validate_stateless()?; + + let backend_names: Vec<&str> = virtual_host.backends.keys().map(String::as_str).collect(); + let Some((backend_name, completion_name)) = + resolve_complete_route(virtual_host, &request.argument.name, &request.r#ref, &backend_names) + else { + return Err(ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: "Routing problem... completion not found".into(), + data: None, + }); + }; + let backend_name = backend_name.to_owned(); + let completion_name = completion_name.to_owned(); + + let backend = virtual_host.backends.get(&backend_name).ok_or_else(|| ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: "Routing problem... backend not found".into(), data: None, - }) + })?; + + let service_name = backend_name.clone(); + let mut backend_service = connect_backend_for_request(mcp_service, &backend_name, backend, &cx).await?; + + let mut routed_request = request; + routed_request.argument.name = completion_name; + + let response = backend_service.complete(routed_request).await; + if let Err(error) = backend_service.close().await { + tracing::warn!("complete: backend cleanup failed backend_name = {service_name} error = {error:?}"); + } + let response = response.map_err(|error| backend_forward_error("complete", &service_name, &error))?; + + info!("read_resource: backend {service_name} returned {} contents", response.completion.values.len()); + + Ok(response.into()) } diff --git a/tests/conformance/expected-failures.yml b/tests/conformance/expected-failures.yml index f39c3734..6e8dca15 100644 --- a/tests/conformance/expected-failures.yml +++ b/tests/conformance/expected-failures.yml @@ -7,7 +7,6 @@ server: - caching:sep-2549-resources-templates-list-caching-hints - caching:sep-2549-tools-list-caching-hints - caching:sep-2549-ttl-non-negative - - completion-complete:completion-complete - input-required-result-basic-elicitation:sep-2322-elicitation-incomplete - input-required-result-basic-list-roots:sep-2322-list-roots-incomplete - input-required-result-basic-sampling:sep-2322-sampling-incomplete From c424ce46d1086b96b9d2ce0d9fb9ee15bd08d942 Mon Sep 17 00:00:00 2001 From: cafalchio Date: Fri, 28 Aug 2026 00:16:36 +0100 Subject: [PATCH 2/2] fmt clippy and small cleanup Signed-off-by: cafalchio --- .../src/gateway/identifier_routing.rs | 11 +++++++---- .../src/gateway/mcp_service/completion.rs | 5 ++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index 041d8566..48462d3b 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -1,5 +1,9 @@ use contextforge_data_plane_apis::user_store::VirtualHost; -use rmcp::{ErrorData, model::{ErrorCode, Reference}, service::ServiceError}; +use rmcp::{ + ErrorData, + model::{ErrorCode, Reference}, + service::ServiceError, +}; use tracing::warn; /// Preserves identifiers for a single backend. For multiple backends, splits a @@ -79,12 +83,11 @@ pub(super) fn resolve_complete_route<'a, N: AsRef>( rref: &Reference, backend_names: &'a [N], ) -> Option<(&'a str, &'a str)> { - let reference = match rref { + match rref { Reference::Prompt(_) => resolve_prompt_route(virtual_host, name, backend_names), Reference::Resource(_) => resolve_resources_route(virtual_host, name, backend_names), _ => None, - }; - reference + } } /// Returns the control-plane alias for an upstream tool when configured. Without an alias, diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs index ebf735e4..58a79c92 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/completion.rs @@ -19,7 +19,6 @@ pub(super) async fn complete( request: CompleteRequestParams, cx: RequestContext, ) -> Result { - info!("\n\n\n---------- request ----- {:?}", &request); let mcp_call_validator = AuthorizedCallValidator::new("complete", &cx); let (virtual_host, _claims) = mcp_call_validator.validate_stateless()?; @@ -54,7 +53,7 @@ pub(super) async fn complete( } let response = response.map_err(|error| backend_forward_error("complete", &service_name, &error))?; - info!("read_resource: backend {service_name} returned {} contents", response.completion.values.len()); + info!("complete: backend {service_name} returned {} contents", response.completion.values.len()); - Ok(response.into()) + Ok(response) }