|
@@ -41,15 +41,16 @@ use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
|
|
use runtime::{
|
|
use runtime::{
|
|
|
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
|
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
|
|
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials,
|
|
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials,
|
|
|
- ApiClient, ApiRequest, AssistantEvent,
|
|
|
|
|
- CompactionConfig, ConfigLoader, ConfigSource, ContentBlock, ConversationMessage,
|
|
|
|
|
- ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig,
|
|
|
|
|
- OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent,
|
|
|
|
|
- ResolvedPermissionMode, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
|
|
|
|
|
- UsageTracker, ModelPricing, format_usd, pricing_for_model,
|
|
|
|
|
|
|
+ ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource,
|
|
|
|
|
+ ContentBlock, ConversationMessage, ConversationRuntime, McpServerManager, McpTool,
|
|
|
|
|
+ MessageRole, ModelPricing, OAuthAuthorizationRequest, OAuthConfig,
|
|
|
|
|
+ OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext,
|
|
|
|
|
+ PromptCacheEvent, ResolvedPermissionMode, RuntimeError, Session, TokenUsage, ToolError,
|
|
|
|
|
+ ToolExecutor, UsageTracker, format_usd, pricing_for_model,
|
|
|
};
|
|
};
|
|
|
|
|
+use serde::Deserialize;
|
|
|
use serde_json::json;
|
|
use serde_json::json;
|
|
|
-use tools::GlobalToolRegistry;
|
|
|
|
|
|
|
+use tools::{GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput};
|
|
|
|
|
|
|
|
const DEFAULT_MODEL: &str = "claude-opus-4-6";
|
|
const DEFAULT_MODEL: &str = "claude-opus-4-6";
|
|
|
fn max_tokens_for_model(model: &str) -> u32 {
|
|
fn max_tokens_for_model(model: &str) -> u32 {
|
|
@@ -594,11 +595,17 @@ fn current_tool_registry() -> Result<GlobalToolRegistry, String> {
|
|
|
let cwd = env::current_dir().map_err(|error| error.to_string())?;
|
|
let cwd = env::current_dir().map_err(|error| error.to_string())?;
|
|
|
let loader = ConfigLoader::default_for(&cwd);
|
|
let loader = ConfigLoader::default_for(&cwd);
|
|
|
let runtime_config = loader.load().map_err(|error| error.to_string())?;
|
|
let runtime_config = loader.load().map_err(|error| error.to_string())?;
|
|
|
- let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
|
|
|
|
|
- let plugin_tools = plugin_manager
|
|
|
|
|
- .aggregated_tools()
|
|
|
|
|
|
|
+ let state = build_runtime_plugin_state_with_loader(&cwd, &loader, &runtime_config)
|
|
|
.map_err(|error| error.to_string())?;
|
|
.map_err(|error| error.to_string())?;
|
|
|
- GlobalToolRegistry::with_plugin_tools(plugin_tools)
|
|
|
|
|
|
|
+ let registry = state.tool_registry.clone();
|
|
|
|
|
+ if let Some(mcp_state) = state.mcp_state {
|
|
|
|
|
+ mcp_state
|
|
|
|
|
+ .lock()
|
|
|
|
|
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
+ .shutdown()
|
|
|
|
|
+ .map_err(|error| error.to_string())?;
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(registry)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn parse_permission_mode_arg(value: &str) -> Result<PermissionMode, String> {
|
|
fn parse_permission_mode_arg(value: &str) -> Result<PermissionMode, String> {
|
|
@@ -1587,23 +1594,35 @@ struct RuntimePluginState {
|
|
|
feature_config: runtime::RuntimeFeatureConfig,
|
|
feature_config: runtime::RuntimeFeatureConfig,
|
|
|
tool_registry: GlobalToolRegistry,
|
|
tool_registry: GlobalToolRegistry,
|
|
|
plugin_registry: PluginRegistry,
|
|
plugin_registry: PluginRegistry,
|
|
|
|
|
+ mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+struct RuntimeMcpState {
|
|
|
|
|
+ runtime: tokio::runtime::Runtime,
|
|
|
|
|
+ manager: McpServerManager,
|
|
|
|
|
+ pending_servers: Vec<String>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct BuiltRuntime {
|
|
struct BuiltRuntime {
|
|
|
runtime: Option<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>>,
|
|
runtime: Option<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>>,
|
|
|
plugin_registry: PluginRegistry,
|
|
plugin_registry: PluginRegistry,
|
|
|
plugins_active: bool,
|
|
plugins_active: bool,
|
|
|
|
|
+ mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
|
|
|
|
|
+ mcp_active: bool,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
impl BuiltRuntime {
|
|
impl BuiltRuntime {
|
|
|
fn new(
|
|
fn new(
|
|
|
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
|
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
|
|
plugin_registry: PluginRegistry,
|
|
plugin_registry: PluginRegistry,
|
|
|
|
|
+ mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
|
|
|
) -> Self {
|
|
) -> Self {
|
|
|
Self {
|
|
Self {
|
|
|
runtime: Some(runtime),
|
|
runtime: Some(runtime),
|
|
|
plugin_registry,
|
|
plugin_registry,
|
|
|
plugins_active: true,
|
|
plugins_active: true,
|
|
|
|
|
+ mcp_state,
|
|
|
|
|
+ mcp_active: true,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1623,6 +1642,19 @@ impl BuiltRuntime {
|
|
|
}
|
|
}
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ fn shutdown_mcp(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
+ if self.mcp_active {
|
|
|
|
|
+ if let Some(mcp_state) = &self.mcp_state {
|
|
|
|
|
+ mcp_state
|
|
|
|
|
+ .lock()
|
|
|
|
|
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
+ .shutdown()?;
|
|
|
|
|
+ }
|
|
|
|
|
+ self.mcp_active = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
impl Deref for BuiltRuntime {
|
|
impl Deref for BuiltRuntime {
|
|
@@ -1645,10 +1677,284 @@ impl DerefMut for BuiltRuntime {
|
|
|
|
|
|
|
|
impl Drop for BuiltRuntime {
|
|
impl Drop for BuiltRuntime {
|
|
|
fn drop(&mut self) {
|
|
fn drop(&mut self) {
|
|
|
|
|
+ let _ = self.shutdown_mcp();
|
|
|
let _ = self.shutdown_plugins();
|
|
let _ = self.shutdown_plugins();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+#[derive(Debug, Deserialize)]
|
|
|
|
|
+struct ToolSearchRequest {
|
|
|
|
|
+ query: String,
|
|
|
|
|
+ max_results: Option<usize>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Deserialize)]
|
|
|
|
|
+struct McpToolRequest {
|
|
|
|
|
+ #[serde(rename = "qualifiedName")]
|
|
|
|
|
+ qualified_name: Option<String>,
|
|
|
|
|
+ tool: Option<String>,
|
|
|
|
|
+ arguments: Option<serde_json::Value>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Deserialize)]
|
|
|
|
|
+struct ListMcpResourcesRequest {
|
|
|
|
|
+ server: Option<String>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Deserialize)]
|
|
|
|
|
+struct ReadMcpResourceRequest {
|
|
|
|
|
+ server: String,
|
|
|
|
|
+ uri: String,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl RuntimeMcpState {
|
|
|
|
|
+ fn new(
|
|
|
|
|
+ runtime_config: &runtime::RuntimeConfig,
|
|
|
|
|
+ ) -> Result<Option<(Self, runtime::McpToolDiscoveryReport)>, Box<dyn std::error::Error>> {
|
|
|
|
|
+ let mut manager = McpServerManager::from_runtime_config(runtime_config);
|
|
|
|
|
+ if manager.server_names().is_empty() && manager.unsupported_servers().is_empty() {
|
|
|
|
|
+ return Ok(None);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let runtime = tokio::runtime::Runtime::new()?;
|
|
|
|
|
+ let discovery = runtime.block_on(manager.discover_tools_best_effort());
|
|
|
|
|
+ let pending_servers = discovery
|
|
|
|
|
+ .failed_servers
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|failure| failure.server_name.clone())
|
|
|
|
|
+ .chain(
|
|
|
|
|
+ discovery
|
|
|
|
|
+ .unsupported_servers
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|server| server.server_name.clone()),
|
|
|
|
|
+ )
|
|
|
|
|
+ .collect::<BTreeSet<_>>()
|
|
|
|
|
+ .into_iter()
|
|
|
|
|
+ .collect::<Vec<_>>();
|
|
|
|
|
+
|
|
|
|
|
+ Ok(Some((
|
|
|
|
|
+ Self {
|
|
|
|
|
+ runtime,
|
|
|
|
|
+ manager,
|
|
|
|
|
+ pending_servers,
|
|
|
|
|
+ },
|
|
|
|
|
+ discovery,
|
|
|
|
|
+ )))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn shutdown(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
+ self.runtime.block_on(self.manager.shutdown())?;
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn pending_servers(&self) -> Option<Vec<String>> {
|
|
|
|
|
+ (!self.pending_servers.is_empty()).then(|| self.pending_servers.clone())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn server_names(&self) -> Vec<String> {
|
|
|
|
|
+ self.manager.server_names()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn call_tool(
|
|
|
|
|
+ &mut self,
|
|
|
|
|
+ qualified_tool_name: &str,
|
|
|
|
|
+ arguments: Option<serde_json::Value>,
|
|
|
|
|
+ ) -> Result<String, ToolError> {
|
|
|
|
|
+ let response = self
|
|
|
|
|
+ .runtime
|
|
|
|
|
+ .block_on(self.manager.call_tool(qualified_tool_name, arguments))
|
|
|
|
|
+ .map_err(|error| ToolError::new(error.to_string()))?;
|
|
|
|
|
+ if let Some(error) = response.error {
|
|
|
|
|
+ return Err(ToolError::new(format!(
|
|
|
|
|
+ "MCP tool `{qualified_tool_name}` returned JSON-RPC error: {} ({})",
|
|
|
|
|
+ error.message, error.code
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let result = response.result.ok_or_else(|| {
|
|
|
|
|
+ ToolError::new(format!(
|
|
|
|
|
+ "MCP tool `{qualified_tool_name}` returned no result payload"
|
|
|
|
|
+ ))
|
|
|
|
|
+ })?;
|
|
|
|
|
+ serde_json::to_string_pretty(&result).map_err(|error| ToolError::new(error.to_string()))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn list_resources_for_server(&mut self, server_name: &str) -> Result<String, ToolError> {
|
|
|
|
|
+ let result = self
|
|
|
|
|
+ .runtime
|
|
|
|
|
+ .block_on(self.manager.list_resources(server_name))
|
|
|
|
|
+ .map_err(|error| ToolError::new(error.to_string()))?;
|
|
|
|
|
+ serde_json::to_string_pretty(&json!({
|
|
|
|
|
+ "server": server_name,
|
|
|
|
|
+ "resources": result.resources,
|
|
|
|
|
+ }))
|
|
|
|
|
+ .map_err(|error| ToolError::new(error.to_string()))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn list_resources_for_all_servers(&mut self) -> Result<String, ToolError> {
|
|
|
|
|
+ let mut resources = Vec::new();
|
|
|
|
|
+ let mut failures = Vec::new();
|
|
|
|
|
+
|
|
|
|
|
+ for server_name in self.server_names() {
|
|
|
|
|
+ match self
|
|
|
|
|
+ .runtime
|
|
|
|
|
+ .block_on(self.manager.list_resources(&server_name))
|
|
|
|
|
+ {
|
|
|
|
|
+ Ok(result) => resources.push(json!({
|
|
|
|
|
+ "server": server_name,
|
|
|
|
|
+ "resources": result.resources,
|
|
|
|
|
+ })),
|
|
|
|
|
+ Err(error) => failures.push(json!({
|
|
|
|
|
+ "server": server_name,
|
|
|
|
|
+ "error": error.to_string(),
|
|
|
|
|
+ })),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if resources.is_empty() && !failures.is_empty() {
|
|
|
|
|
+ let message = failures
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter_map(|failure| failure.get("error").and_then(serde_json::Value::as_str))
|
|
|
|
|
+ .collect::<Vec<_>>()
|
|
|
|
|
+ .join("; ");
|
|
|
|
|
+ return Err(ToolError::new(message));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ serde_json::to_string_pretty(&json!({
|
|
|
|
|
+ "resources": resources,
|
|
|
|
|
+ "failures": failures,
|
|
|
|
|
+ }))
|
|
|
|
|
+ .map_err(|error| ToolError::new(error.to_string()))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn read_resource(&mut self, server_name: &str, uri: &str) -> Result<String, ToolError> {
|
|
|
|
|
+ let result = self
|
|
|
|
|
+ .runtime
|
|
|
|
|
+ .block_on(self.manager.read_resource(server_name, uri))
|
|
|
|
|
+ .map_err(|error| ToolError::new(error.to_string()))?;
|
|
|
|
|
+ serde_json::to_string_pretty(&json!({
|
|
|
|
|
+ "server": server_name,
|
|
|
|
|
+ "contents": result.contents,
|
|
|
|
|
+ }))
|
|
|
|
|
+ .map_err(|error| ToolError::new(error.to_string()))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn build_runtime_mcp_state(
|
|
|
|
|
+ runtime_config: &runtime::RuntimeConfig,
|
|
|
|
|
+) -> Result<
|
|
|
|
|
+ (
|
|
|
|
|
+ Option<Arc<Mutex<RuntimeMcpState>>>,
|
|
|
|
|
+ Vec<RuntimeToolDefinition>,
|
|
|
|
|
+ ),
|
|
|
|
|
+ Box<dyn std::error::Error>,
|
|
|
|
|
+> {
|
|
|
|
|
+ let Some((mcp_state, discovery)) = RuntimeMcpState::new(runtime_config)? else {
|
|
|
|
|
+ return Ok((None, Vec::new()));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let mut runtime_tools = discovery
|
|
|
|
|
+ .tools
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(mcp_runtime_tool_definition)
|
|
|
|
|
+ .collect::<Vec<_>>();
|
|
|
|
|
+ if !mcp_state.server_names().is_empty() {
|
|
|
|
|
+ runtime_tools.extend(mcp_wrapper_tool_definitions());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Ok((Some(Arc::new(Mutex::new(mcp_state))), runtime_tools))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn mcp_runtime_tool_definition(tool: &runtime::ManagedMcpTool) -> RuntimeToolDefinition {
|
|
|
|
|
+ RuntimeToolDefinition {
|
|
|
|
|
+ name: tool.qualified_name.clone(),
|
|
|
|
|
+ description: Some(
|
|
|
|
|
+ tool.tool
|
|
|
|
|
+ .description
|
|
|
|
|
+ .clone()
|
|
|
|
|
+ .unwrap_or_else(|| format!("Invoke MCP tool `{}`.", tool.qualified_name)),
|
|
|
|
|
+ ),
|
|
|
|
|
+ input_schema: tool
|
|
|
|
|
+ .tool
|
|
|
|
|
+ .input_schema
|
|
|
|
|
+ .clone()
|
|
|
|
|
+ .unwrap_or_else(|| json!({ "type": "object", "additionalProperties": true })),
|
|
|
|
|
+ required_permission: permission_mode_for_mcp_tool(&tool.tool),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn mcp_wrapper_tool_definitions() -> Vec<RuntimeToolDefinition> {
|
|
|
|
|
+ vec![
|
|
|
|
|
+ RuntimeToolDefinition {
|
|
|
|
|
+ name: "MCPTool".to_string(),
|
|
|
|
|
+ description: Some(
|
|
|
|
|
+ "Call a configured MCP tool by its qualified name and JSON arguments.".to_string(),
|
|
|
|
|
+ ),
|
|
|
|
|
+ input_schema: json!({
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "qualifiedName": { "type": "string" },
|
|
|
|
|
+ "arguments": {}
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["qualifiedName"],
|
|
|
|
|
+ "additionalProperties": false
|
|
|
|
|
+ }),
|
|
|
|
|
+ required_permission: PermissionMode::DangerFullAccess,
|
|
|
|
|
+ },
|
|
|
|
|
+ RuntimeToolDefinition {
|
|
|
|
|
+ name: "ListMcpResourcesTool".to_string(),
|
|
|
|
|
+ description: Some(
|
|
|
|
|
+ "List MCP resources from one configured server or from every connected server."
|
|
|
|
|
+ .to_string(),
|
|
|
|
|
+ ),
|
|
|
|
|
+ input_schema: json!({
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "server": { "type": "string" }
|
|
|
|
|
+ },
|
|
|
|
|
+ "additionalProperties": false
|
|
|
|
|
+ }),
|
|
|
|
|
+ required_permission: PermissionMode::ReadOnly,
|
|
|
|
|
+ },
|
|
|
|
|
+ RuntimeToolDefinition {
|
|
|
|
|
+ name: "ReadMcpResourceTool".to_string(),
|
|
|
|
|
+ description: Some("Read a specific MCP resource from a configured server.".to_string()),
|
|
|
|
|
+ input_schema: json!({
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "server": { "type": "string" },
|
|
|
|
|
+ "uri": { "type": "string" }
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["server", "uri"],
|
|
|
|
|
+ "additionalProperties": false
|
|
|
|
|
+ }),
|
|
|
|
|
+ required_permission: PermissionMode::ReadOnly,
|
|
|
|
|
+ },
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn permission_mode_for_mcp_tool(tool: &McpTool) -> PermissionMode {
|
|
|
|
|
+ let read_only = mcp_annotation_flag(tool, "readOnlyHint");
|
|
|
|
|
+ let destructive = mcp_annotation_flag(tool, "destructiveHint");
|
|
|
|
|
+ let open_world = mcp_annotation_flag(tool, "openWorldHint");
|
|
|
|
|
+
|
|
|
|
|
+ if read_only && !destructive && !open_world {
|
|
|
|
|
+ PermissionMode::ReadOnly
|
|
|
|
|
+ } else if destructive || open_world {
|
|
|
|
|
+ PermissionMode::DangerFullAccess
|
|
|
|
|
+ } else {
|
|
|
|
|
+ PermissionMode::WorkspaceWrite
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn mcp_annotation_flag(tool: &McpTool, key: &str) -> bool {
|
|
|
|
|
+ tool.annotations
|
|
|
|
|
+ .as_ref()
|
|
|
|
|
+ .and_then(|annotations| annotations.get(key))
|
|
|
|
|
+ .and_then(serde_json::Value::as_bool)
|
|
|
|
|
+ .unwrap_or(false)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
struct HookAbortMonitor {
|
|
struct HookAbortMonitor {
|
|
|
stop_tx: Option<Sender<()>>,
|
|
stop_tx: Option<Sender<()>>,
|
|
|
join_handle: Option<JoinHandle<()>>,
|
|
join_handle: Option<JoinHandle<()>>,
|
|
@@ -3561,11 +3867,14 @@ fn build_runtime_plugin_state_with_loader(
|
|
|
.feature_config()
|
|
.feature_config()
|
|
|
.clone()
|
|
.clone()
|
|
|
.with_hooks(runtime_config.hooks().merged(&plugin_hook_config));
|
|
.with_hooks(runtime_config.hooks().merged(&plugin_hook_config));
|
|
|
- let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_registry.aggregated_tools()?)?;
|
|
|
|
|
|
|
+ let (mcp_state, runtime_tools) = build_runtime_mcp_state(runtime_config)?;
|
|
|
|
|
+ let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_registry.aggregated_tools()?)?
|
|
|
|
|
+ .with_runtime_tools(runtime_tools)?;
|
|
|
Ok(RuntimePluginState {
|
|
Ok(RuntimePluginState {
|
|
|
feature_config,
|
|
feature_config,
|
|
|
tool_registry,
|
|
tool_registry,
|
|
|
plugin_registry,
|
|
plugin_registry,
|
|
|
|
|
+ mcp_state,
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -3987,6 +4296,7 @@ fn build_runtime_with_plugin_state(
|
|
|
feature_config,
|
|
feature_config,
|
|
|
tool_registry,
|
|
tool_registry,
|
|
|
plugin_registry,
|
|
plugin_registry,
|
|
|
|
|
+ mcp_state,
|
|
|
} = runtime_plugin_state;
|
|
} = runtime_plugin_state;
|
|
|
plugin_registry.initialize()?;
|
|
plugin_registry.initialize()?;
|
|
|
let policy = permission_policy(permission_mode, &feature_config, &tool_registry)
|
|
let policy = permission_policy(permission_mode, &feature_config, &tool_registry)
|
|
@@ -4002,7 +4312,12 @@ fn build_runtime_with_plugin_state(
|
|
|
tool_registry.clone(),
|
|
tool_registry.clone(),
|
|
|
progress_reporter,
|
|
progress_reporter,
|
|
|
)?,
|
|
)?,
|
|
|
- CliToolExecutor::new(allowed_tools.clone(), emit_output, tool_registry),
|
|
|
|
|
|
|
+ CliToolExecutor::new(
|
|
|
|
|
+ allowed_tools.clone(),
|
|
|
|
|
+ emit_output,
|
|
|
|
|
+ tool_registry.clone(),
|
|
|
|
|
+ mcp_state.clone(),
|
|
|
|
|
+ ),
|
|
|
policy,
|
|
policy,
|
|
|
system_prompt,
|
|
system_prompt,
|
|
|
&feature_config,
|
|
&feature_config,
|
|
@@ -4010,7 +4325,7 @@ fn build_runtime_with_plugin_state(
|
|
|
if emit_output {
|
|
if emit_output {
|
|
|
runtime = runtime.with_hook_progress_reporter(Box::new(CliHookProgressReporter));
|
|
runtime = runtime.with_hook_progress_reporter(Box::new(CliHookProgressReporter));
|
|
|
}
|
|
}
|
|
|
- Ok(BuiltRuntime::new(runtime, plugin_registry))
|
|
|
|
|
|
|
+ Ok(BuiltRuntime::new(runtime, plugin_registry, mcp_state))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct CliHookProgressReporter;
|
|
struct CliHookProgressReporter;
|
|
@@ -4949,6 +5264,7 @@ struct CliToolExecutor {
|
|
|
emit_output: bool,
|
|
emit_output: bool,
|
|
|
allowed_tools: Option<AllowedToolSet>,
|
|
allowed_tools: Option<AllowedToolSet>,
|
|
|
tool_registry: GlobalToolRegistry,
|
|
tool_registry: GlobalToolRegistry,
|
|
|
|
|
+ mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
impl CliToolExecutor {
|
|
impl CliToolExecutor {
|
|
@@ -4956,12 +5272,72 @@ impl CliToolExecutor {
|
|
|
allowed_tools: Option<AllowedToolSet>,
|
|
allowed_tools: Option<AllowedToolSet>,
|
|
|
emit_output: bool,
|
|
emit_output: bool,
|
|
|
tool_registry: GlobalToolRegistry,
|
|
tool_registry: GlobalToolRegistry,
|
|
|
|
|
+ mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
|
|
|
) -> Self {
|
|
) -> Self {
|
|
|
Self {
|
|
Self {
|
|
|
renderer: TerminalRenderer::new(),
|
|
renderer: TerminalRenderer::new(),
|
|
|
emit_output,
|
|
emit_output,
|
|
|
allowed_tools,
|
|
allowed_tools,
|
|
|
tool_registry,
|
|
tool_registry,
|
|
|
|
|
+ mcp_state,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn execute_search_tool(&self, value: serde_json::Value) -> Result<String, ToolError> {
|
|
|
|
|
+ let input: ToolSearchRequest = serde_json::from_value(value)
|
|
|
|
|
+ .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
|
|
|
|
+ let pending_mcp_servers = self.mcp_state.as_ref().and_then(|state| {
|
|
|
|
|
+ state
|
|
|
|
|
+ .lock()
|
|
|
|
|
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
+ .pending_servers()
|
|
|
|
|
+ });
|
|
|
|
|
+ serde_json::to_string_pretty(&self.tool_registry.search(
|
|
|
|
|
+ &input.query,
|
|
|
|
|
+ input.max_results.unwrap_or(5),
|
|
|
|
|
+ pending_mcp_servers,
|
|
|
|
|
+ ))
|
|
|
|
|
+ .map_err(|error| ToolError::new(error.to_string()))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn execute_runtime_tool(
|
|
|
|
|
+ &self,
|
|
|
|
|
+ tool_name: &str,
|
|
|
|
|
+ value: serde_json::Value,
|
|
|
|
|
+ ) -> Result<String, ToolError> {
|
|
|
|
|
+ let Some(mcp_state) = &self.mcp_state else {
|
|
|
|
|
+ return Err(ToolError::new(format!(
|
|
|
|
|
+ "runtime tool `{tool_name}` is unavailable without configured MCP servers"
|
|
|
|
|
+ )));
|
|
|
|
|
+ };
|
|
|
|
|
+ let mut mcp_state = mcp_state
|
|
|
|
|
+ .lock()
|
|
|
|
|
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
+
|
|
|
|
|
+ match tool_name {
|
|
|
|
|
+ "MCPTool" => {
|
|
|
|
|
+ let input: McpToolRequest = serde_json::from_value(value)
|
|
|
|
|
+ .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
|
|
|
|
+ let qualified_name = input
|
|
|
|
|
+ .qualified_name
|
|
|
|
|
+ .or(input.tool)
|
|
|
|
|
+ .ok_or_else(|| ToolError::new("missing required field `qualifiedName`"))?;
|
|
|
|
|
+ mcp_state.call_tool(&qualified_name, input.arguments)
|
|
|
|
|
+ }
|
|
|
|
|
+ "ListMcpResourcesTool" => {
|
|
|
|
|
+ let input: ListMcpResourcesRequest = serde_json::from_value(value)
|
|
|
|
|
+ .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
|
|
|
|
+ match input.server {
|
|
|
|
|
+ Some(server_name) => mcp_state.list_resources_for_server(&server_name),
|
|
|
|
|
+ None => mcp_state.list_resources_for_all_servers(),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ "ReadMcpResourceTool" => {
|
|
|
|
|
+ let input: ReadMcpResourceRequest = serde_json::from_value(value)
|
|
|
|
|
+ .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
|
|
|
|
+ mcp_state.read_resource(&input.server, &input.uri)
|
|
|
|
|
+ }
|
|
|
|
|
+ _ => mcp_state.call_tool(tool_name, Some(value)),
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -4979,7 +5355,16 @@ impl ToolExecutor for CliToolExecutor {
|
|
|
}
|
|
}
|
|
|
let value = serde_json::from_str(input)
|
|
let value = serde_json::from_str(input)
|
|
|
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
|
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
|
|
- match self.tool_registry.execute(tool_name, &value) {
|
|
|
|
|
|
|
+ let result = if tool_name == "ToolSearch" {
|
|
|
|
|
+ self.execute_search_tool(value)
|
|
|
|
|
+ } else if self.tool_registry.has_runtime_tool(tool_name) {
|
|
|
|
|
+ self.execute_runtime_tool(tool_name, value)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ self.tool_registry
|
|
|
|
|
+ .execute(tool_name, &value)
|
|
|
|
|
+ .map_err(ToolError::new)
|
|
|
|
|
+ };
|
|
|
|
|
+ match result {
|
|
|
Ok(output) => {
|
|
Ok(output) => {
|
|
|
if self.emit_output {
|
|
if self.emit_output {
|
|
|
let markdown = format_tool_result(tool_name, &output, false);
|
|
let markdown = format_tool_result(tool_name, &output, false);
|
|
@@ -4991,12 +5376,12 @@ impl ToolExecutor for CliToolExecutor {
|
|
|
}
|
|
}
|
|
|
Err(error) => {
|
|
Err(error) => {
|
|
|
if self.emit_output {
|
|
if self.emit_output {
|
|
|
- let markdown = format_tool_result(tool_name, &error, true);
|
|
|
|
|
|
|
+ let markdown = format_tool_result(tool_name, &error.to_string(), true);
|
|
|
self.renderer
|
|
self.renderer
|
|
|
.stream_markdown(&markdown, &mut io::stdout())
|
|
.stream_markdown(&markdown, &mut io::stdout())
|
|
|
.map_err(|stream_error| ToolError::new(stream_error.to_string()))?;
|
|
.map_err(|stream_error| ToolError::new(stream_error.to_string()))?;
|
|
|
}
|
|
}
|
|
|
- Err(ToolError::new(error))
|
|
|
|
|
|
|
+ Err(error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -5199,8 +5584,9 @@ mod tests {
|
|
|
resolve_model_alias, resolve_session_reference, response_to_events,
|
|
resolve_model_alias, resolve_session_reference, response_to_events,
|
|
|
resume_supported_slash_commands, run_resume_command,
|
|
resume_supported_slash_commands, run_resume_command,
|
|
|
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
|
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
|
|
- CliAction, CliOutputFormat, GitWorkspaceSummary, InternalPromptProgressEvent,
|
|
|
|
|
- InternalPromptProgressState, LiveCli, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
|
|
|
|
|
|
+ write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
|
|
|
|
+ InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, SlashCommand,
|
|
|
|
|
+ StatusUsage, DEFAULT_MODEL,
|
|
|
};
|
|
};
|
|
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
|
|
use plugins::{
|
|
use plugins::{
|
|
@@ -5208,7 +5594,7 @@ mod tests {
|
|
|
};
|
|
};
|
|
|
use runtime::{
|
|
use runtime::{
|
|
|
AssistantEvent, ConfigLoader, ContentBlock, ConversationMessage, MessageRole,
|
|
AssistantEvent, ConfigLoader, ContentBlock, ConversationMessage, MessageRole,
|
|
|
- PermissionMode, Session,
|
|
|
|
|
|
|
+ PermissionMode, Session, ToolExecutor,
|
|
|
};
|
|
};
|
|
|
use serde_json::json;
|
|
use serde_json::json;
|
|
|
use std::fs;
|
|
use std::fs;
|
|
@@ -6516,7 +6902,11 @@ UU conflicted.rs",
|
|
|
|
|
|
|
|
#[test]
|
|
#[test]
|
|
|
fn init_template_mentions_detected_rust_workspace() {
|
|
fn init_template_mentions_detected_rust_workspace() {
|
|
|
- let rendered = crate::init::render_init_claude_md(std::path::Path::new("."));
|
|
|
|
|
|
|
+ let _guard = cwd_lock()
|
|
|
|
|
+ .lock()
|
|
|
|
|
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
+ let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
|
|
|
|
+ let rendered = crate::init::render_init_claude_md(&workspace_root);
|
|
|
assert!(rendered.contains("# CLAUDE.md"));
|
|
assert!(rendered.contains("# CLAUDE.md"));
|
|
|
assert!(rendered.contains("cargo clippy --workspace --all-targets -- -D warnings"));
|
|
assert!(rendered.contains("cargo clippy --workspace --all-targets -- -D warnings"));
|
|
|
}
|
|
}
|
|
@@ -6907,6 +7297,111 @@ UU conflicted.rs",
|
|
|
let _ = fs::remove_dir_all(source_root);
|
|
let _ = fs::remove_dir_all(source_root);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn build_runtime_plugin_state_discovers_mcp_tools_and_surfaces_pending_servers() {
|
|
|
|
|
+ let config_home = temp_dir();
|
|
|
|
|
+ let workspace = temp_dir();
|
|
|
|
|
+ fs::create_dir_all(&config_home).expect("config home");
|
|
|
|
|
+ fs::create_dir_all(&workspace).expect("workspace");
|
|
|
|
|
+ let script_path = workspace.join("fixture-mcp.py");
|
|
|
|
|
+ write_mcp_server_fixture(&script_path);
|
|
|
|
|
+ fs::write(
|
|
|
|
|
+ config_home.join("settings.json"),
|
|
|
|
|
+ format!(
|
|
|
|
|
+ r#"{{
|
|
|
|
|
+ "mcpServers": {{
|
|
|
|
|
+ "alpha": {{
|
|
|
|
|
+ "command": "python3",
|
|
|
|
|
+ "args": ["{}"]
|
|
|
|
|
+ }},
|
|
|
|
|
+ "broken": {{
|
|
|
|
|
+ "command": "python3",
|
|
|
|
|
+ "args": ["-c", "import sys; sys.exit(0)"]
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}"#,
|
|
|
|
|
+ script_path.to_string_lossy()
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ .expect("write mcp settings");
|
|
|
|
|
+
|
|
|
|
|
+ let loader = ConfigLoader::new(&workspace, &config_home);
|
|
|
|
|
+ let runtime_config = loader.load().expect("runtime config should load");
|
|
|
|
|
+ let state = build_runtime_plugin_state_with_loader(&workspace, &loader, &runtime_config)
|
|
|
|
|
+ .expect("runtime plugin state should load");
|
|
|
|
|
+
|
|
|
|
|
+ let allowed = state
|
|
|
|
|
+ .tool_registry
|
|
|
|
|
+ .normalize_allowed_tools(&["mcp__alpha__echo".to_string(), "MCPTool".to_string()])
|
|
|
|
|
+ .expect("mcp tools should be allow-listable")
|
|
|
|
|
+ .expect("allow-list should exist");
|
|
|
|
|
+ assert!(allowed.contains("mcp__alpha__echo"));
|
|
|
|
|
+ assert!(allowed.contains("MCPTool"));
|
|
|
|
|
+
|
|
|
|
|
+ let mut executor = CliToolExecutor::new(
|
|
|
|
|
+ None,
|
|
|
|
|
+ false,
|
|
|
|
|
+ state.tool_registry.clone(),
|
|
|
|
|
+ state.mcp_state.clone(),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ let tool_output = executor
|
|
|
|
|
+ .execute("mcp__alpha__echo", r#"{"text":"hello"}"#)
|
|
|
|
|
+ .expect("discovered mcp tool should execute");
|
|
|
|
|
+ let tool_json: serde_json::Value =
|
|
|
|
|
+ serde_json::from_str(&tool_output).expect("tool output should be json");
|
|
|
|
|
+ assert_eq!(tool_json["structuredContent"]["echoed"], "hello");
|
|
|
|
|
+
|
|
|
|
|
+ let wrapped_output = executor
|
|
|
|
|
+ .execute(
|
|
|
|
|
+ "MCPTool",
|
|
|
|
|
+ r#"{"qualifiedName":"mcp__alpha__echo","arguments":{"text":"wrapped"}}"#,
|
|
|
|
|
+ )
|
|
|
|
|
+ .expect("generic mcp wrapper should execute");
|
|
|
|
|
+ let wrapped_json: serde_json::Value =
|
|
|
|
|
+ serde_json::from_str(&wrapped_output).expect("wrapped output should be json");
|
|
|
|
|
+ assert_eq!(wrapped_json["structuredContent"]["echoed"], "wrapped");
|
|
|
|
|
+
|
|
|
|
|
+ let search_output = executor
|
|
|
|
|
+ .execute("ToolSearch", r#"{"query":"alpha echo","max_results":5}"#)
|
|
|
|
|
+ .expect("tool search should execute");
|
|
|
|
|
+ let search_json: serde_json::Value =
|
|
|
|
|
+ serde_json::from_str(&search_output).expect("search output should be json");
|
|
|
|
|
+ assert_eq!(search_json["matches"][0], "mcp__alpha__echo");
|
|
|
|
|
+ assert_eq!(search_json["pending_mcp_servers"][0], "broken");
|
|
|
|
|
+
|
|
|
|
|
+ let listed = executor
|
|
|
|
|
+ .execute("ListMcpResourcesTool", r#"{"server":"alpha"}"#)
|
|
|
|
|
+ .expect("resources should list");
|
|
|
|
|
+ let listed_json: serde_json::Value =
|
|
|
|
|
+ serde_json::from_str(&listed).expect("resource output should be json");
|
|
|
|
|
+ assert_eq!(listed_json["resources"][0]["uri"], "file://guide.txt");
|
|
|
|
|
+
|
|
|
|
|
+ let read = executor
|
|
|
|
|
+ .execute(
|
|
|
|
|
+ "ReadMcpResourceTool",
|
|
|
|
|
+ r#"{"server":"alpha","uri":"file://guide.txt"}"#,
|
|
|
|
|
+ )
|
|
|
|
|
+ .expect("resource should read");
|
|
|
|
|
+ let read_json: serde_json::Value =
|
|
|
|
|
+ serde_json::from_str(&read).expect("resource read output should be json");
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ read_json["contents"][0]["text"],
|
|
|
|
|
+ "contents for file://guide.txt"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(mcp_state) = state.mcp_state {
|
|
|
|
|
+ mcp_state
|
|
|
|
|
+ .lock()
|
|
|
|
|
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
+ .shutdown()
|
|
|
|
|
+ .expect("mcp shutdown should succeed");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let _ = fs::remove_dir_all(config_home);
|
|
|
|
|
+ let _ = fs::remove_dir_all(workspace);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
#[test]
|
|
#[test]
|
|
|
fn build_runtime_runs_plugin_lifecycle_init_and_shutdown() {
|
|
fn build_runtime_runs_plugin_lifecycle_init_and_shutdown() {
|
|
|
let config_home = temp_dir();
|
|
let config_home = temp_dir();
|
|
@@ -6965,6 +7460,105 @@ UU conflicted.rs",
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn write_mcp_server_fixture(script_path: &Path) {
|
|
|
|
|
+ let script = [
|
|
|
|
|
+ "#!/usr/bin/env python3",
|
|
|
|
|
+ "import json, sys",
|
|
|
|
|
+ "",
|
|
|
|
|
+ "def read_message():",
|
|
|
|
|
+ " header = b''",
|
|
|
|
|
+ r" while not header.endswith(b'\r\n\r\n'):",
|
|
|
|
|
+ " chunk = sys.stdin.buffer.read(1)",
|
|
|
|
|
+ " if not chunk:",
|
|
|
|
|
+ " return None",
|
|
|
|
|
+ " header += chunk",
|
|
|
|
|
+ " length = 0",
|
|
|
|
|
+ r" for line in header.decode().split('\r\n'):",
|
|
|
|
|
+ r" if line.lower().startswith('content-length:'):",
|
|
|
|
|
+ " length = int(line.split(':', 1)[1].strip())",
|
|
|
|
|
+ " payload = sys.stdin.buffer.read(length)",
|
|
|
|
|
+ " return json.loads(payload.decode())",
|
|
|
|
|
+ "",
|
|
|
|
|
+ "def send_message(message):",
|
|
|
|
|
+ " payload = json.dumps(message).encode()",
|
|
|
|
|
+ r" sys.stdout.buffer.write(f'Content-Length: {len(payload)}\r\n\r\n'.encode() + payload)",
|
|
|
|
|
+ " sys.stdout.buffer.flush()",
|
|
|
|
|
+ "",
|
|
|
|
|
+ "while True:",
|
|
|
|
|
+ " request = read_message()",
|
|
|
|
|
+ " if request is None:",
|
|
|
|
|
+ " break",
|
|
|
|
|
+ " method = request['method']",
|
|
|
|
|
+ " if method == 'initialize':",
|
|
|
|
|
+ " send_message({",
|
|
|
|
|
+ " 'jsonrpc': '2.0',",
|
|
|
|
|
+ " 'id': request['id'],",
|
|
|
|
|
+ " 'result': {",
|
|
|
|
|
+ " 'protocolVersion': request['params']['protocolVersion'],",
|
|
|
|
|
+ " 'capabilities': {'tools': {}, 'resources': {}},",
|
|
|
|
|
+ " 'serverInfo': {'name': 'fixture', 'version': '1.0.0'}",
|
|
|
|
|
+ " }",
|
|
|
|
|
+ " })",
|
|
|
|
|
+ " elif method == 'tools/list':",
|
|
|
|
|
+ " send_message({",
|
|
|
|
|
+ " 'jsonrpc': '2.0',",
|
|
|
|
|
+ " 'id': request['id'],",
|
|
|
|
|
+ " 'result': {",
|
|
|
|
|
+ " 'tools': [",
|
|
|
|
|
+ " {",
|
|
|
|
|
+ " 'name': 'echo',",
|
|
|
|
|
+ " 'description': 'Echo from MCP fixture',",
|
|
|
|
|
+ " 'inputSchema': {",
|
|
|
|
|
+ " 'type': 'object',",
|
|
|
|
|
+ " 'properties': {'text': {'type': 'string'}},",
|
|
|
|
|
+ " 'required': ['text'],",
|
|
|
|
|
+ " 'additionalProperties': False",
|
|
|
|
|
+ " },",
|
|
|
|
|
+ " 'annotations': {'readOnlyHint': True}",
|
|
|
|
|
+ " }",
|
|
|
|
|
+ " ]",
|
|
|
|
|
+ " }",
|
|
|
|
|
+ " })",
|
|
|
|
|
+ " elif method == 'tools/call':",
|
|
|
|
|
+ " args = request['params'].get('arguments') or {}",
|
|
|
|
|
+ " send_message({",
|
|
|
|
|
+ " 'jsonrpc': '2.0',",
|
|
|
|
|
+ " 'id': request['id'],",
|
|
|
|
|
+ " 'result': {",
|
|
|
|
|
+ " 'content': [{'type': 'text', 'text': f\"echo:{args.get('text', '')}\"}],",
|
|
|
|
|
+ " 'structuredContent': {'echoed': args.get('text', '')},",
|
|
|
|
|
+ " 'isError': False",
|
|
|
|
|
+ " }",
|
|
|
|
|
+ " })",
|
|
|
|
|
+ " elif method == 'resources/list':",
|
|
|
|
|
+ " send_message({",
|
|
|
|
|
+ " 'jsonrpc': '2.0',",
|
|
|
|
|
+ " 'id': request['id'],",
|
|
|
|
|
+ " 'result': {",
|
|
|
|
|
+ " 'resources': [{'uri': 'file://guide.txt', 'name': 'guide', 'mimeType': 'text/plain'}]",
|
|
|
|
|
+ " }",
|
|
|
|
|
+ " })",
|
|
|
|
|
+ " elif method == 'resources/read':",
|
|
|
|
|
+ " uri = request['params']['uri']",
|
|
|
|
|
+ " send_message({",
|
|
|
|
|
+ " 'jsonrpc': '2.0',",
|
|
|
|
|
+ " 'id': request['id'],",
|
|
|
|
|
+ " 'result': {",
|
|
|
|
|
+ " 'contents': [{'uri': uri, 'mimeType': 'text/plain', 'text': f'contents for {uri}'}]",
|
|
|
|
|
+ " }",
|
|
|
|
|
+ " })",
|
|
|
|
|
+ " else:",
|
|
|
|
|
+ " send_message({",
|
|
|
|
|
+ " 'jsonrpc': '2.0',",
|
|
|
|
|
+ " 'id': request['id'],",
|
|
|
|
|
+ " 'error': {'code': -32601, 'message': method}",
|
|
|
|
|
+ " })",
|
|
|
|
|
+ "",
|
|
|
|
|
+ ]
|
|
|
|
|
+ .join("\n");
|
|
|
|
|
+ fs::write(script_path, script).expect("mcp fixture script should write");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
#[cfg(test)]
|
|
#[cfg(test)]
|
|
|
mod sandbox_report_tests {
|
|
mod sandbox_report_tests {
|
|
|
use super::{format_sandbox_report, HookAbortMonitor};
|
|
use super::{format_sandbox_report, HookAbortMonitor};
|