Honor explicit plugin mentions during MCP startup (#42406)

## Why

Selected plugin MCP servers may be skipped after the optional startup grace
period. When a user explicitly mentions a plugin or MCP server, the turn should
continue waiting so the requested tools are available to the model.

## What changed

- Track explicitly mentioned plugin IDs and MCP servers across later samples,
  steered input, and same-turn restarts.
- Require matching selected-plugin MCP servers to finish startup before
  capturing the tool catalog, while leaving unmentioned servers optional.
- Match `plugin://` identities independently of display names and ignore
  targeting query parameters such as `app` and `browserFamily`.

## Testing

Added coverage for linked and structured plugin mentions, direct and batched
capability discovery, delayed environment attachment, steering, and same-turn
restarts.

GitOrigin-RevId: c509ef8df45df1a7edb0da6afdb2682dc53a4de8
This commit is contained in:
Matthew Zeng authored and copyberry committed 2026-09-03 01:33:32 +00:00
1 parent b7f710273e
commit 460b63e5f4
14 files changed
+617 -106

No files matched your search

+35 -10
View File
@@ -6,6 +6,7 @@
//! [`crate::connection_manager`].
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
@@ -335,20 +336,29 @@ impl McpRuntime {
/// Captures the latest published configuration and live client handles.
pub async fn current_binding(&self) -> Option<Arc<McpBinding>> {
self.current_binding_with_required_servers(&[]).await
self.current_binding_with_requirements(&[], &HashSet::new())
.await
}
/// Captures the latest runtime, waiting for servers explicitly required by this turn.
pub async fn current_binding_with_required_servers(
/// Captures one runtime, waiting for explicitly required servers and selected plugins.
/// Plugin IDs are resolved by the captured connection set, even if a refresh publishes later.
pub async fn current_binding_with_requirements(
&self,
required_servers: &[String],
required_plugins: &HashSet<String>,
) -> Option<Arc<McpBinding>> {
Self::binding_from_published_runtime(self.current.load_full(), required_servers).await
Self::binding_from_published_runtime(
self.current.load_full(),
required_servers,
required_plugins,
)
.await
}
async fn binding_from_published_runtime(
current: Arc<PublishedMcpRuntime>,
required_servers: &[String],
required_plugins: &HashSet<String>,
) -> Option<Arc<McpBinding>> {
let config = Arc::clone(current.config.as_ref()?);
let stable_catalog_revision = current.connections.stable_catalog_revision().await;
@@ -367,7 +377,12 @@ impl McpRuntime {
let binding = Arc::new(
current
.connections
.capture_binding_with_metadata(config, current.plugins_available, required_servers)
.capture_binding_with_metadata(
config,
current.plugins_available,
required_servers,
required_plugins,
)
.await,
);
if let Some(catalog_revision) = stable_catalog_revision
@@ -445,7 +460,12 @@ impl McpRuntime {
if !current.connections.wait_for_server_startup(server).await {
return None;
}
Self::binding_from_published_runtime(current, /*required_servers*/ &[]).await
Self::binding_from_published_runtime(
current,
/*required_servers*/ &[],
/*required_plugins*/ &HashSet::new(),
)
.await
}
/// Returns the latest published configuration without waiting for clients.
@@ -877,12 +897,14 @@ mod tests {
let first = McpRuntime::binding_from_published_runtime(
Arc::clone(&published),
/*required_servers*/ &[],
/*required_plugins*/ &HashSet::new(),
)
.await
.expect("first binding");
let repeated = McpRuntime::binding_from_published_runtime(
Arc::clone(&published),
/*required_servers*/ &[],
/*required_plugins*/ &HashSet::new(),
)
.await
.expect("repeated binding");
@@ -893,10 +915,13 @@ mod tests {
cached_binding: Mutex::new(None),
..previous
});
let refreshed =
McpRuntime::binding_from_published_runtime(republished, /*required_servers*/ &[])
.await
.expect("republished binding");
let refreshed = McpRuntime::binding_from_published_runtime(
republished,
/*required_servers*/ &[],
/*required_plugins*/ &HashSet::new(),
)
.await
.expect("republished binding");
assert!(!Arc::ptr_eq(&first, &refreshed));
}