diff --git a/Cargo.lock b/Cargo.lock index 44c8f883a4..998a6688bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4647,6 +4647,7 @@ dependencies = [ "anyhow", "assert2", "async-trait", + "base64 0.22.1", "bigdecimal", "bytes 1.11.0", "chrono", diff --git a/golem-common/src/base_model/agent.rs b/golem-common/src/base_model/agent.rs index 144dc79783..2560c089d6 100644 --- a/golem-common/src/base_model/agent.rs +++ b/golem-common/src/base_model/agent.rs @@ -547,15 +547,45 @@ pub enum UntypedDataValue { } #[derive(Debug, Clone, PartialEq)] -#[cfg_attr( - feature = "full", - derive(IntoValue, FromValue, desert_rust::BinaryCodec) -)] +#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))] pub struct UntypedNamedElementValue { pub name: String, pub value: UntypedElementValue, } +#[cfg(feature = "full")] +impl golem_wasm::FromValue for UntypedNamedElementValue { + fn from_value(value: Value) -> Result { + match value { + Value::Tuple(fields) if fields.len() == 2 => { + let mut iter = fields.into_iter(); + let name = String::from_value(iter.next().unwrap())?; + let value = UntypedElementValue::from_value(iter.next().unwrap())?; + Ok(UntypedNamedElementValue { name, value }) + } + _ => Err(format!( + "Expected Tuple with 2 fields for UntypedNamedElementValue, got {:?}", + value + )), + } + } +} + +#[cfg(feature = "full")] +impl golem_wasm::IntoValue for UntypedNamedElementValue { + fn into_value(self) -> Value { + Value::Tuple(vec![self.name.into_value(), self.value.into_value()]) + } + + fn get_type() -> AnalysedType { + AnalysedType::Tuple(golem_wasm::analysis::TypeTuple { + name: None, + owner: None, + items: vec![String::get_type(), UntypedElementValue::get_type()], + }) + } +} + #[derive(Debug, Clone, PartialEq)] #[cfg_attr( feature = "full", diff --git a/golem-worker-service/Cargo.toml b/golem-worker-service/Cargo.toml index 9c58f21ccd..20b96ef9a0 100644 --- a/golem-worker-service/Cargo.toml +++ b/golem-worker-service/Cargo.toml @@ -38,6 +38,7 @@ golem-wasm = { workspace = true, default-features = true } golem-wasm-derive = { workspace = true } anyhow = { workspace = true } +base64 = { workspace = true } async-trait = { workspace = true } bigdecimal = { workspace = true } bytes = { workspace = true } diff --git a/golem-worker-service/src/mcp/agent_mcp_capability.rs b/golem-worker-service/src/mcp/agent_mcp_capability.rs index 884f818052..87d44aea80 100644 --- a/golem-worker-service/src/mcp/agent_mcp_capability.rs +++ b/golem-worker-service/src/mcp/agent_mcp_capability.rs @@ -12,27 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::mcp::agent_mcp_resource::AgentMcpResource; +use crate::mcp::agent_mcp_resource::{AgentMcpResource, AgentMcpResourceKind}; use crate::mcp::agent_mcp_tool::AgentMcpTool; -use crate::mcp::schema::{McpToolSchema, get_mcp_schema, get_mcp_tool_schema}; +use crate::mcp::schema::{McpToolSchema, get_input_mcp_schema, get_mcp_tool_schema}; use golem_common::base_model::account::AccountId; -use golem_common::base_model::agent::{AgentMethod, AgentTypeName, DataSchema}; +use golem_common::base_model::agent::{ + AgentMethod, AgentTypeName, DataSchema, ElementSchema, NamedElementSchemas, +}; use golem_common::base_model::component::ComponentId; use golem_common::base_model::environment::EnvironmentId; use golem_common::model::agent::AgentConstructor; -use rmcp::model::Tool; +use rmcp::model::{Annotated, RawResource, RawResourceTemplate, Tool}; use std::borrow::Cow; use std::sync::Arc; #[derive(Clone)] pub enum McpAgentCapability { Tool(Box), - #[allow(unused)] - Resource(AgentMcpResource), + Resource(Box), } impl McpAgentCapability { - pub fn from( + pub fn from_agent_method( account_id: &AccountId, environment_id: &EnvironmentId, agent_type_name: &AgentTypeName, @@ -40,61 +41,106 @@ impl McpAgentCapability { constructor: &AgentConstructor, component_id: ComponentId, ) -> Self { - match &method.input_schema { - DataSchema::Tuple(schemas) => { - if !schemas.elements.is_empty() { - tracing::debug!( - "Method {} of agent type {} has input parameters, exposing as tool", - method.name, - agent_type_name.0 - ); - - let constructor_schema = get_mcp_schema(&constructor.input_schema); - - let McpToolSchema { - mut input_schema, - output_schema, - } = get_mcp_tool_schema(method); - - input_schema.prepend_schema(constructor_schema); - - let tool = Tool { - name: Cow::from(get_tool_name(agent_type_name, method)), + let schemas = match &method.input_schema { + DataSchema::Tuple(schemas) | DataSchema::Multimodal(schemas) => schemas, + }; + + if !schemas.elements.is_empty() { + tracing::debug!( + "Method {} of agent type {} has input parameters, exposing as tool", + method.name, + agent_type_name.0 + ); + + let constructor_schema = get_input_mcp_schema(&constructor.input_schema); + + let McpToolSchema { + mut input_schema, + output_schema, + } = get_mcp_tool_schema(method); + + input_schema.prepend_schema(constructor_schema); + + let tool = Tool { + name: Cow::from(get_tool_name(agent_type_name, method)), + title: None, + description: Some(method.description.clone().into()), + input_schema: Arc::new(rmcp::model::JsonObject::from(input_schema)), + output_schema: output_schema + .map(|internal| Arc::new(rmcp::model::JsonObject::from(internal))), + annotations: None, + execution: None, + icons: None, + meta: None, + }; + + Self::Tool(Box::new(AgentMcpTool { + environment_id: *environment_id, + account_id: *account_id, + constructor: constructor.clone(), + raw_method: method.clone(), + tool, + component_id, + agent_type_name: agent_type_name.clone(), + })) + } else { + tracing::debug!( + "Method {} of agent type {} has no input parameters, exposing as resource", + method.name, + agent_type_name.0 + ); + + let constructor_param_names = AgentMcpResource::constructor_param_names(constructor); + let name = AgentMcpResource::resource_name(agent_type_name, method); + + let mime_type = output_resource_mime_type(&method.output_schema); + + let kind = if constructor_param_names.is_empty() { + let uri = AgentMcpResource::static_uri(agent_type_name, method); + AgentMcpResourceKind::Static(Annotated::new( + RawResource { + uri, + name, title: None, - description: Some(method.description.clone().into()), - input_schema: Arc::new(rmcp::model::JsonObject::from(input_schema)), - output_schema: output_schema - .map(|internal| Arc::new(rmcp::model::JsonObject::from(internal))), - annotations: None, - execution: None, + description: Some(method.description.clone()), + mime_type, + size: None, icons: None, meta: None, - }; - - Self::Tool(Box::new(AgentMcpTool { - environment_id: *environment_id, - account_id: *account_id, - constructor: constructor.clone(), - raw_method: method.clone(), - tool, - component_id, - agent_type_name: agent_type_name.clone(), - })) - } else { - tracing::debug!( - "Method {} of agent type {} has no input parameters, exposing as resource", - method.name, - agent_type_name.0 - ); - - Self::Resource(AgentMcpResource { - resource: method.clone(), - }) + }, + None, + )) + } else { + let uri_template = AgentMcpResource::template_uri( + agent_type_name, + method, + &constructor_param_names, + ); + AgentMcpResourceKind::Template { + template: Annotated::new( + RawResourceTemplate { + uri_template, + name, + title: None, + description: Some(method.description.clone()), + mime_type, + icons: None, + }, + None, + ), + constructor_param_names, } - } - DataSchema::Multimodal(_) => { - todo!("Multimodal schema handling not implemented yet") - } + }; + + Self::Resource(Box::new(AgentMcpResource { + kind, + environment_id: *environment_id, + account_id: *account_id, + constructor: constructor.clone(), + raw_method: method.clone(), + component_id, + agent_type_name: agent_type_name.clone(), + })) } } } @@ -102,3 +148,21 @@ impl McpAgentCapability { fn get_tool_name(agent_type_name: &AgentTypeName, method: &AgentMethod) -> String { format!("{}-{}", agent_type_name.0, method.name) } + +fn output_resource_mime_type(output_schema: &DataSchema) -> Option { + match output_schema { + DataSchema::Tuple(NamedElementSchemas { elements }) => match elements.as_slice() { + [single] => match &single.schema { + ElementSchema::ComponentModel(_) => Some("application/json".to_string()), + ElementSchema::UnstructuredText(_) => Some("text/plain".to_string()), + // The actual mime type + ElementSchema::UnstructuredBinary(_) => None, + }, + _ => None, + }, + + // Each individual resource contents could have its own mime type, so we can't assign a single mime type to the whole output + // when it comes to multimodal output schemas. + DataSchema::Multimodal(_) => None, + } +} diff --git a/golem-worker-service/src/mcp/agent_mcp_prompt.rs b/golem-worker-service/src/mcp/agent_mcp_prompt.rs index 05e3ffe126..1b4fe5c49b 100644 --- a/golem-worker-service/src/mcp/agent_mcp_prompt.rs +++ b/golem-worker-service/src/mcp/agent_mcp_prompt.rs @@ -12,61 +12,512 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::mcp::GolemAgentMcpServer; -use futures::FutureExt; -use futures::future::BoxFuture; -use golem_common::base_model::agent::AgentMethod; -use rmcp::ErrorData; -use rmcp::handler::server::prompt::{GetPromptHandler, PromptContext}; -use rmcp::handler::server::router::prompt::{IntoPromptRoute, PromptRoute}; +use golem_common::base_model::agent::{ + AgentConstructor, AgentMethod, AgentTypeName, DataSchema, ElementSchema, NamedElementSchema, +}; use rmcp::model::{ GetPromptResult, Prompt, PromptMessage, PromptMessageContent, PromptMessageRole, }; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PromptName(pub String); -#[allow(unused)] #[derive(Clone)] pub struct AgentMcpPrompt { - pub agent_method: AgentMethod, - pub raw_prompt: Prompt, + pub name: PromptName, + pub prompt: Prompt, + pub prompt_text: String, +} + +impl AgentMcpPrompt { + pub fn from_constructor_hint( + agent_type_name: &AgentTypeName, + description: &str, + prompt_hint: &str, + ) -> Self { + let name = PromptName(agent_type_name.0.clone()); + + let prompt = Prompt::new(name.0.clone(), Some(description.to_string()), None); + + AgentMcpPrompt { + name, + prompt, + prompt_text: prompt_hint.to_string(), + } + } + + pub fn from_method_hint( + agent_type_name: &AgentTypeName, + method: &AgentMethod, + constructor: &AgentConstructor, + prompt_hint: &str, + ) -> Self { + let prompt_name = format!("{}-{}", agent_type_name.0, method.name); + let name = PromptName(prompt_name.clone()); + + let constructor_arg_names = constructor_arg_names(&constructor.input_schema); + let input_description = describe_input(&constructor_arg_names, &method.input_schema); + let output_description = describe_output(&method.output_schema); + + let mut text = prompt_hint.to_string(); + + if let Some(input_desc) = input_description { + text.push_str(&format!("\n\n{}", input_desc)); + } + + if let Some(output_desc) = output_description { + text.push_str(&format!("\n\n{}", output_desc)); + } + + let prompt = Prompt::new(prompt_name, Some(method.description.clone()), None); + + AgentMcpPrompt { + name, + prompt, + prompt_text: text, + } + } + + pub fn get_prompt_result(&self) -> GetPromptResult { + GetPromptResult { + description: self.prompt.description.clone(), + messages: vec![PromptMessage { + role: PromptMessageRole::User, + content: PromptMessageContent::Text { + text: self.prompt_text.clone(), + }, + }], + } + } +} + +fn constructor_arg_names(schema: &DataSchema) -> Vec { + match schema { + DataSchema::Tuple(schemas) => schemas.elements.iter().map(|e| e.name.clone()).collect(), + DataSchema::Multimodal(_) => vec![], + } } -impl GetPromptHandler for AgentMcpPrompt { - fn handle( - self, - context: PromptContext<'_, GolemAgentMcpServer>, - ) -> BoxFuture<'_, Result> { - async move { - let parameters = context - .arguments - .map(|x| { - x.iter() - .map(|(k, v)| format!("{}: {}", k, v)) +fn describe_input( + constructor_arg_names: &[String], + method_input_schema: &DataSchema, +) -> Option { + match method_input_schema { + DataSchema::Tuple(schemas) => { + let mut all_names: Vec<&str> = + constructor_arg_names.iter().map(|s| s.as_str()).collect(); + all_names.extend(schemas.elements.iter().map(|e| e.name.as_str())); + + if all_names.is_empty() { + None + } else { + Some(format!( + "Expected JSON input properties: {}", + all_names.join(", ") + )) + } + } + DataSchema::Multimodal(schemas) => { + let part_names: Vec<&str> = schemas.elements.iter().map(|e| e.name.as_str()).collect(); + + let mut all_property_names: Vec<&str> = + constructor_arg_names.iter().map(|s| s.as_str()).collect(); + + if !part_names.is_empty() { + all_property_names.push("parts"); + } + + let mut desc = String::new(); + + if !all_property_names.is_empty() { + desc.push_str(&format!( + "Expected JSON input properties: {}", + all_property_names.join(", ") + )); + } + + if !part_names.is_empty() { + desc.push_str(&format!( + "\n\nThe \"parts\" property is an array with elements named: {}", + part_names.join(", ") + )); + } + + if desc.is_empty() { None } else { Some(desc) } + } + } +} + +fn describe_output_element(element: &NamedElementSchema) -> String { + match &element.schema { + ElementSchema::ComponentModel(_) => "JSON".to_string(), + + ElementSchema::UnstructuredText(text_desc) => { + if let Some(desc) = &text_desc.restrictions { + if desc.is_empty() { + return "text".to_string(); + } + + return format!( + "text with with one of the following language codes: {}", + desc.iter() + .map(|x| x.language_code.clone()) .collect::>() .join(", ") - }) - .unwrap_or_else(|| "no parameters".to_string()); - - let result = GetPromptResult { - description: None, - messages: vec![PromptMessage { - role: PromptMessageRole::User, - content: PromptMessageContent::Text { - text: format!( - "{}, call {} with the following parameters: {}", - "developer-given prompt", self.agent_method.name, parameters - ), - }, - }], - }; + ); + } + + "text".to_string() + } + ElementSchema::UnstructuredBinary(binary) => { + if let Some(restrictions) = &binary.restrictions { + if restrictions.is_empty() { + return "binary".to_string(); + } + + return format!( + "binary with one of the following mime-types: {}", + restrictions + .iter() + .map(|x| x.mime_type.clone()) + .collect::>() + .join(", ") + ); + } + + "binary".to_string() + } + } +} - Ok(result) +fn describe_output(schema: &DataSchema) -> Option { + match schema { + DataSchema::Tuple(schemas) => match schemas.elements.as_slice() { + [] => None, + [single] => Some(format!("output hint: {}", describe_output_element(single))), + _ => None, + }, + DataSchema::Multimodal(schemas) => { + if schemas.elements.is_empty() { + return None; + } + let parts: Vec = schemas + .elements + .iter() + .map(describe_output_element) + .collect(); + Some(format!( + "output hint: multimodal response: {}", + parts.join(", ") + )) } - .boxed() } } -impl IntoPromptRoute for AgentMcpPrompt { - fn into_prompt_route(self) -> PromptRoute { - PromptRoute::new(self.raw_prompt.clone(), self) +#[derive(Clone, Default)] +pub struct PromptRegistry { + prompts: HashMap, +} + +impl PromptRegistry { + pub fn insert(&mut self, prompt: AgentMcpPrompt) { + self.prompts.insert(prompt.name.clone(), prompt); + } + + pub fn list_prompts(&self) -> Vec { + self.prompts.values().map(|p| p.prompt.clone()).collect() + } + + pub fn get_by_name(&self, name: &str) -> Option<&AgentMcpPrompt> { + self.prompts.get(&PromptName(name.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use golem_common::base_model::agent::{ + BinaryDescriptor, ComponentModelElementSchema, DataSchema, NamedElementSchema, + NamedElementSchemas, TextDescriptor, + }; + use golem_wasm::analysis::analysed_type::str; + use test_r::test; + + #[test] + fn creates_prompt_from_constructor_hint() { + let prompt = AgentMcpPrompt::from_constructor_hint( + &AgentTypeName("WeatherAgent".to_string()), + "A weather agent", + "You are a weather assistant. Help the user get weather information.", + ); + + assert_eq!(prompt.name, PromptName("WeatherAgent".to_string())); + assert_eq!(prompt.prompt.name, "WeatherAgent"); + assert_eq!( + prompt.prompt.description.as_deref(), + Some("A weather agent") + ); + assert!(prompt.prompt.arguments.is_none()); + assert_eq!( + prompt.prompt_text, + "You are a weather assistant. Help the user get weather information." + ); + } + + #[test] + fn get_prompt_result_returns_user_message_with_hint() { + let prompt = AgentMcpPrompt::from_constructor_hint( + &AgentTypeName("MyAgent".to_string()), + "desc", + "Do something useful", + ); + + let result = prompt.get_prompt_result(); + assert_eq!(result.messages.len(), 1); + assert!(matches!(result.messages[0].role, PromptMessageRole::User)); + match &result.messages[0].content { + PromptMessageContent::Text { text } => { + assert_eq!(text, "Do something useful"); + } + _ => panic!("expected text content"), + } + } + + #[test] + fn registry_insert_and_list() { + let mut registry = PromptRegistry::default(); + registry.insert(AgentMcpPrompt::from_constructor_hint( + &AgentTypeName("A".to_string()), + "Agent A", + "hint a", + )); + registry.insert(AgentMcpPrompt::from_constructor_hint( + &AgentTypeName("B".to_string()), + "Agent B", + "hint b", + )); + + let listed = registry.list_prompts(); + assert_eq!(listed.len(), 2); + + let names: Vec<&str> = listed.iter().map(|p| p.name.as_str()).collect(); + assert!(names.contains(&"A")); + assert!(names.contains(&"B")); + } + + #[test] + fn registry_get_by_name() { + let mut registry = PromptRegistry::default(); + registry.insert(AgentMcpPrompt::from_constructor_hint( + &AgentTypeName("X".to_string()), + "Agent X", + "hint x", + )); + + assert!(registry.get_by_name("X").is_some()); + assert!(registry.get_by_name("Y").is_none()); + } + + #[test] + fn method_prompt_name_is_agent_type_dash_method() { + let constructor = AgentConstructor { + name: None, + description: "ctor".to_string(), + prompt_hint: None, + input_schema: DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "city".to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }), + }], + }), + }; + let method = AgentMethod { + name: "get_weather".to_string(), + description: "Gets weather".to_string(), + prompt_hint: Some("Fetch the weather".to_string()), + input_schema: DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "location".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }], + }), + output_schema: DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "report".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }], + }), + http_endpoint: vec![], + }; + + let prompt = AgentMcpPrompt::from_method_hint( + &AgentTypeName("WeatherAgent".to_string()), + &method, + &constructor, + "Fetch the weather", + ); + + assert_eq!( + prompt.name, + PromptName("WeatherAgent-get_weather".to_string()) + ); + assert_eq!(prompt.prompt.name, "WeatherAgent-get_weather"); + assert_eq!(prompt.prompt.description.as_deref(), Some("Gets weather")); + } + + #[test] + fn method_prompt_text_includes_constructor_and_method_args_and_output() { + let constructor = AgentConstructor { + name: None, + description: "ctor".to_string(), + prompt_hint: None, + input_schema: DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "tenant".to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }), + }], + }), + }; + let method = AgentMethod { + name: "analyze".to_string(), + description: "Analyze data".to_string(), + prompt_hint: Some("Run analysis".to_string()), + input_schema: DataSchema::Tuple(NamedElementSchemas { + elements: vec![ + NamedElementSchema { + name: "query".to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }), + }, + NamedElementSchema { + name: "image".to_string(), + schema: ElementSchema::UnstructuredBinary(BinaryDescriptor { + restrictions: None, + }), + }, + ], + }), + output_schema: DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "result".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }], + }), + http_endpoint: vec![], + }; + + let prompt = AgentMcpPrompt::from_method_hint( + &AgentTypeName("DataAgent".to_string()), + &method, + &constructor, + "Run analysis", + ); + + assert!(prompt.prompt_text.starts_with("Run analysis")); + + assert!( + prompt + .prompt_text + .contains("Expected JSON input properties: tenant, query, image"), + "got: {}", + prompt.prompt_text + ); + + assert!(prompt.prompt_text.contains("text")); + } + + #[test] + fn method_prompt_multimodal_input_describes_parts_array() { + let constructor = AgentConstructor { + name: None, + description: "ctor".to_string(), + prompt_hint: None, + input_schema: DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "tenant".to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }), + }], + }), + }; + let method = AgentMethod { + name: "process".to_string(), + description: "Process multimodal".to_string(), + prompt_hint: Some("Process it".to_string()), + input_schema: DataSchema::Multimodal(NamedElementSchemas { + elements: vec![ + NamedElementSchema { + name: "description".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { + restrictions: None, + }), + }, + NamedElementSchema { + name: "photo".to_string(), + schema: ElementSchema::UnstructuredBinary(BinaryDescriptor { + restrictions: None, + }), + }, + ], + }), + output_schema: DataSchema::Tuple(NamedElementSchemas { elements: vec![] }), + http_endpoint: vec![], + }; + + let prompt = AgentMcpPrompt::from_method_hint( + &AgentTypeName("MultiAgent".to_string()), + &method, + &constructor, + "Process it", + ); + + assert!( + prompt + .prompt_text + .contains("Expected JSON input properties: tenant, parts"), + "got: {}", + prompt.prompt_text + ); + assert!( + prompt.prompt_text.contains( + "The \"parts\" property is an array with elements named: description, photo" + ), + "got: {}", + prompt.prompt_text + ); + } + + #[test] + fn method_prompt_omits_empty_sections() { + let constructor = AgentConstructor { + name: None, + description: "ctor".to_string(), + prompt_hint: None, + input_schema: DataSchema::Tuple(NamedElementSchemas { elements: vec![] }), + }; + let method = AgentMethod { + name: "ping".to_string(), + description: "Ping".to_string(), + prompt_hint: Some("Just ping".to_string()), + input_schema: DataSchema::Tuple(NamedElementSchemas { elements: vec![] }), + output_schema: DataSchema::Tuple(NamedElementSchemas { elements: vec![] }), + http_endpoint: vec![], + }; + + let prompt = AgentMcpPrompt::from_method_hint( + &AgentTypeName("PingAgent".to_string()), + &method, + &constructor, + "Just ping", + ); + + assert_eq!(prompt.prompt_text, "Just ping"); } } diff --git a/golem-worker-service/src/mcp/agent_mcp_resource.rs b/golem-worker-service/src/mcp/agent_mcp_resource.rs index 57aefc58f1..1c35b71ca6 100644 --- a/golem-worker-service/src/mcp/agent_mcp_resource.rs +++ b/golem-worker-service/src/mcp/agent_mcp_resource.rs @@ -12,10 +12,276 @@ // See the License for the specific language governing permissions and // limitations under the License. -use golem_common::base_model::agent::AgentMethod; +use golem_common::base_model::account::AccountId; +use golem_common::base_model::agent::{ + AgentMethod, AgentTypeName, DataSchema, NamedElementSchemas, +}; +use golem_common::base_model::component::ComponentId; +use golem_common::base_model::environment::EnvironmentId; +use golem_common::model::agent::AgentConstructor; +use rmcp::model::{Resource, ResourceTemplate}; +use std::collections::HashMap; #[derive(Clone)] pub struct AgentMcpResource { - #[allow(dead_code)] - pub resource: AgentMethod, + pub kind: AgentMcpResourceKind, + pub environment_id: EnvironmentId, + pub account_id: AccountId, + pub constructor: AgentConstructor, + pub raw_method: AgentMethod, + pub component_id: ComponentId, + pub agent_type_name: AgentTypeName, +} + +#[derive(Clone)] +pub enum AgentMcpResourceKind { + Static(Resource), + Template { + template: ResourceTemplate, + constructor_param_names: Vec, + }, +} + +pub struct ConstructorParam { + pub name: String, + pub value: String, +} + +pub struct McpResourceUri { + pub agent: String, + pub method: String, + pub tail_segments: Vec, +} + +impl McpResourceUri { + pub fn parse(uri: &str) -> Result { + let rest = uri + .strip_prefix("golem://") + .ok_or_else(|| format!("URI must start with golem://, got: {uri}"))?; + + let (agent, path) = rest + .split_once('/') + .ok_or_else(|| format!("URI must contain agent and method: {uri}"))?; + + if agent.is_empty() { + return Err(format!("URI agent name cannot be empty: {uri}")); + } + + let mut segments: Vec = path + .split('/') + .filter(|s| !s.is_empty()) + .map(percent_decode) + .collect(); + + if segments.is_empty() { + return Err(format!("URI must contain a method name: {uri}")); + } + + let method = segments.remove(0); + + Ok(McpResourceUri { + agent: agent.to_string(), + method, + tail_segments: segments, + }) + } +} + +fn percent_decode(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + let mut chars = s.bytes(); + while let Some(b) = chars.next() { + if b == b'%' { + let hi = chars.next(); + let lo = chars.next(); + if let (Some(hi), Some(lo)) = (hi, lo) + && let Ok(byte) = u8::from_str_radix(&format!("{}{}", hi as char, lo as char), 16) + { + result.push(byte as char); + continue; + } + result.push('%'); + } else { + result.push(b as char); + } + } + result +} + +type StaticResourceUri = String; + +#[derive(Hash, Eq, PartialEq)] +struct AgentMethodKey { + agent_type_name: String, + method_name: String, +} + +#[derive(Default)] +pub struct ResourceRegistry { + static_resources: HashMap, + template_resources: HashMap, +} + +impl ResourceRegistry { + pub fn insert(&mut self, resource: AgentMcpResource) { + match &resource.kind { + AgentMcpResourceKind::Static(res) => { + self.static_resources.insert(res.uri.clone(), resource); + } + AgentMcpResourceKind::Template { .. } => { + let key = AgentMethodKey { + agent_type_name: resource.agent_type_name.0.clone(), + method_name: resource.raw_method.name.clone(), + }; + self.template_resources.insert(key, resource); + } + } + } + + pub fn get_static(&self, uri: &str) -> Option<&AgentMcpResource> { + self.static_resources.get(uri) + } + + pub fn list_static_resources(&self) -> Vec { + self.static_resources + .values() + .filter_map(|r| match &r.kind { + AgentMcpResourceKind::Static(resource) => Some(resource.clone()), + AgentMcpResourceKind::Template { .. } => None, + }) + .collect() + } + + pub fn list_resource_templates(&self) -> Vec { + self.template_resources + .values() + .filter_map(|r| match &r.kind { + AgentMcpResourceKind::Template { template, .. } => Some(template.clone()), + AgentMcpResourceKind::Static(_) => None, + }) + .collect() + } + + pub fn extract_mcp_resource_with_input( + &self, + uri: &McpResourceUri, + ) -> Option<(&AgentMcpResource, Vec)> { + let key = AgentMethodKey { + agent_type_name: uri.agent.clone(), + method_name: uri.method.clone(), + }; + let resource = self.template_resources.get(&key)?; + + if let AgentMcpResourceKind::Template { + constructor_param_names, + .. + } = &resource.kind + && uri.tail_segments.len() == constructor_param_names.len() + { + let params = constructor_param_names + .iter() + .zip(uri.tail_segments.iter()) + .map(|(name, value)| ConstructorParam { + name: name.clone(), + value: value.clone(), + }) + .collect(); + return Some((resource, params)); + } + None + } +} + +impl AgentMcpResource { + pub fn resource_name(agent_type_name: &AgentTypeName, method: &AgentMethod) -> String { + format!("{}-{}", agent_type_name.0, method.name) + } + + pub fn static_uri(agent_type_name: &AgentTypeName, method: &AgentMethod) -> String { + // https://modelcontextprotocol.info/docs/concepts/resources + // The protocol and path structure is defined by the MCP server implementation. + // Servers can define their own custom URI schemes. + format!("golem://{}/{}", agent_type_name.0, method.name) + } + + pub fn template_uri( + agent_type_name: &AgentTypeName, + method: &AgentMethod, + param_names: &[String], + ) -> String { + // https://modelcontextprotocol.info/docs/concepts/resources + // The protocol and path structure is defined by the MCP server implementation. + // Servers can define their own custom URI schemes. + let base = format!("golem://{}/{}", agent_type_name.0, method.name); + let placeholders: Vec = param_names.iter().map(|n| format!("{{{}}}", n)).collect(); + format!("{}/{}", base, placeholders.join("/")) + } + + pub fn constructor_param_names(constructor: &AgentConstructor) -> Vec { + match &constructor.input_schema { + DataSchema::Tuple(NamedElementSchemas { elements }) => { + elements.iter().map(|e| e.name.clone()).collect() + } + DataSchema::Multimodal(_) => vec![], + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_r::test; + + #[test] + fn test_parse_static_uri() { + let uri = McpResourceUri::parse("golem://counter/get-value").unwrap(); + assert_eq!(uri.agent, "counter"); + assert_eq!(uri.method, "get-value"); + assert!(uri.tail_segments.is_empty()); + } + + #[test] + fn test_parse_template_uri_with_params() { + let uri = McpResourceUri::parse("golem://counter/get-value/my-counter").unwrap(); + assert_eq!(uri.agent, "counter"); + assert_eq!(uri.method, "get-value"); + assert_eq!(uri.tail_segments, vec!["my-counter"]); + } + + #[test] + fn test_parse_template_uri_with_multiple_params() { + let uri = McpResourceUri::parse("golem://counter/get-value/ns/my-counter").unwrap(); + assert_eq!(uri.agent, "counter"); + assert_eq!(uri.method, "get-value"); + assert_eq!(uri.tail_segments, vec!["ns", "my-counter"]); + } + + #[test] + fn test_parse_percent_encoded_uri() { + let uri = McpResourceUri::parse("golem://counter/get-value/my%20counter").unwrap(); + assert_eq!(uri.tail_segments, vec!["my counter"]); + } + + #[test] + fn test_parse_uri_trailing_slash() { + let uri = McpResourceUri::parse("golem://counter/get-value/").unwrap(); + assert_eq!(uri.agent, "counter"); + assert_eq!(uri.method, "get-value"); + assert!(uri.tail_segments.is_empty()); + } + + #[test] + fn test_parse_invalid_scheme() { + assert!(McpResourceUri::parse("http://counter/get-value").is_err()); + } + + #[test] + fn test_parse_missing_method() { + assert!(McpResourceUri::parse("golem://counter/").is_err()); + } + + #[test] + fn test_parse_empty_agent() { + assert!(McpResourceUri::parse("golem:///get-value").is_err()); + } } diff --git a/golem-worker-service/src/mcp/agent_mcp_server.rs b/golem-worker-service/src/mcp/agent_mcp_server.rs index f393f97d65..ec04c37c6a 100644 --- a/golem-worker-service/src/mcp/agent_mcp_server.rs +++ b/golem-worker-service/src/mcp/agent_mcp_server.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::mcp::McpCapabilityLookup; use crate::mcp::agent_mcp_capability::McpAgentCapability; +use crate::mcp::agent_mcp_prompt::{AgentMcpPrompt, PromptRegistry}; +use crate::mcp::agent_mcp_resource::{AgentMcpResource, McpResourceUri, ResourceRegistry}; use crate::mcp::agent_mcp_tool::AgentMcpTool; -use crate::mcp::invoke::agent_invoke; +use crate::mcp::{McpCapabilityLookup, invoke}; use crate::service::worker::WorkerService; use dashmap::DashMap; use golem_common::base_model::domain_registration::Domain; @@ -27,12 +28,13 @@ use rmcp::{ use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; -// Every client will get an instance of this #[derive(Clone)] pub struct GolemAgentMcpServer { processor: Arc>, tool_router: Arc>>>, tools: Arc>, + resources: Arc>, + prompts: Arc>, domain: Arc>>, mcp_definitions_lookup: Arc, worker_service: Arc, @@ -46,6 +48,8 @@ impl GolemAgentMcpServer { Self { tool_router: Arc::new(RwLock::new(None)), tools: Arc::new(DashMap::new()), + resources: Arc::new(RwLock::new(ResourceRegistry::default())), + prompts: Arc::new(RwLock::new(PromptRegistry::default())), processor: Arc::new(Mutex::new(OperationProcessor::new())), domain: Arc::new(RwLock::new(None)), mcp_definitions_lookup, @@ -53,40 +57,59 @@ impl GolemAgentMcpServer { } } - pub async fn invoke( + pub async fn invoke_tool( &self, args_map: JsonObject, mcp_tool: &AgentMcpTool, ) -> Result { - agent_invoke(&self.worker_service, args_map, mcp_tool).await + invoke::tool::invoke_tool(args_map, mcp_tool, &self.worker_service).await } - async fn tool_router(&self, domain: &Domain) -> ToolRouter { - let tool_handlers = get_agent_tool_and_handlers(domain, &self.mcp_definitions_lookup).await; + async fn build_capabilities( + &self, + domain: &Domain, + ) -> ( + ToolRouter, + Vec, + Vec, + ) { + let capabilities = get_agent_capabilities(domain, &self.mcp_definitions_lookup).await; let mut router = ToolRouter::::new(); - for tool in tool_handlers { + for tool in capabilities.tools { router = router.with_route(tool); } - router + (router, capabilities.resources, capabilities.prompts) } } -pub async fn get_agent_tool_and_handlers( +pub struct AgentCapabilities { + pub tools: Vec, + pub resources: Vec, + pub prompts: Vec, +} + +pub async fn get_agent_capabilities( domain: &Domain, mcp_definition_lookup: &Arc, -) -> Vec { +) -> AgentCapabilities { let compiled_mcp = match mcp_definition_lookup.get(domain).await { Ok(mcp) => mcp, Err(e) => { tracing::error!("Failed to get compiled MCP for domain {}: {}", domain.0, e); - return vec![]; + return AgentCapabilities { + tools: vec![], + resources: vec![], + prompts: vec![], + }; } }; let mut tools = vec![]; + let mut resources = vec![]; + let mut prompts = vec![]; let account_id = compiled_mcp.account_id; let environment_id = compiled_mcp.environment_id; @@ -123,9 +146,28 @@ pub async fn get_agent_tool_and_handlers( ); let agent_type = ®istered_agent_type.agent_type; + let component_id = registered_agent_type.implemented_by.component_id; + + if let Some(prompt_hint) = &agent_type.constructor.prompt_hint { + prompts.push(AgentMcpPrompt::from_constructor_hint( + &agent_type.type_name, + &agent_type.description, + prompt_hint, + )); + } + for method in &agent_type.methods { - let agent_method_mcp = McpAgentCapability::from( + if let Some(prompt_hint) = &method.prompt_hint { + prompts.push(AgentMcpPrompt::from_method_hint( + &agent_type.type_name, + method, + &agent_type.constructor, + prompt_hint, + )); + } + + let agent_method_mcp = McpAgentCapability::from_agent_method( &account_id, &environment_id, &agent_type.type_name, @@ -138,7 +180,9 @@ pub async fn get_agent_tool_and_handlers( McpAgentCapability::Tool(agent_mcp_tool) => { tools.push(*agent_mcp_tool); } - McpAgentCapability::Resource(_) => {} + McpAgentCapability::Resource(agent_mcp_resource) => { + resources.push(*agent_mcp_resource); + } } } } @@ -153,13 +197,19 @@ pub async fn get_agent_tool_and_handlers( } } - if tools.is_empty() { - tracing::warn!("No tools found for domain {}", domain.0); - } else { - tracing::info!("Found {} tools for domain {}", tools.len(), domain.0); - } + tracing::info!( + "Found {} tools, {} resources, and {} prompts for domain {}", + tools.len(), + resources.len(), + prompts.len(), + domain.0 + ); - tools + AgentCapabilities { + tools, + resources, + prompts, + } } #[allow(deprecated)] @@ -232,13 +282,21 @@ impl ServerHandler for GolemAgentMcpServer { } } - async fn read_resource( + async fn list_resources( &self, - ReadResourceRequestParams { meta: _, uri }: ReadResourceRequestParams, - _: RequestContext, - ) -> Result { - // TODO; Include Git tickets here - todo!("Resource support is not implemented yet. URI: {}", uri) + _request: Option, + _context: RequestContext, + ) -> Result { + let registry = self.resources.read().await; + let resource_list = registry.list_static_resources(); + + tracing::info!("Listing {} static resources", resource_list.len()); + + Ok(ListResourcesResult { + resources: resource_list, + next_cursor: None, + meta: None, + }) } async fn list_resource_templates( @@ -246,13 +304,83 @@ impl ServerHandler for GolemAgentMcpServer { _request: Option, _: RequestContext, ) -> Result { + let registry = self.resources.read().await; + let resource_templates = registry.list_resource_templates(); + + tracing::info!("Listing {} resource templates", resource_templates.len()); + Ok(ListResourceTemplatesResult { next_cursor: None, - resource_templates: Vec::new(), + resource_templates, + meta: None, + }) + } + + async fn list_prompts( + &self, + _request: Option, + _context: RequestContext, + ) -> Result { + let registry = self.prompts.read().await; + let prompt_list = registry.list_prompts(); + + tracing::info!("Listing {} prompts", prompt_list.len()); + + Ok(ListPromptsResult { + prompts: prompt_list, + next_cursor: None, meta: None, }) } + async fn get_prompt( + &self, + request: GetPromptRequestParams, + _context: RequestContext, + ) -> Result { + let registry = self.prompts.read().await; + + registry + .get_by_name(&request.name) + .map(|p| p.get_prompt_result()) + .ok_or_else(|| { + McpError::invalid_params(format!("Prompt not found: {}", request.name), None) + }) + } + + async fn read_resource( + &self, + ReadResourceRequestParams { meta: _, uri }: ReadResourceRequestParams, + _: RequestContext, + ) -> Result { + let resource_registry = self.resources.read().await; + + if let Some(resource) = resource_registry.get_static(&uri) { + return invoke::resource::invoke_resource(&self.worker_service, resource, &uri, None) + .await; + } + + let parsed_resource_uri = McpResourceUri::parse(&uri) + .map_err(|e| McpError::invalid_params(format!("Invalid resource URI: {e}"), None))?; + + if let Some((resource, params)) = + resource_registry.extract_mcp_resource_with_input(&parsed_resource_uri) + { + return invoke::resource::invoke_resource( + &self.worker_service, + resource, + &uri, + Some(params), + ) + .await; + } + + Err(McpError::invalid_params( + format!("Resource not found for URI: {}", uri), + None, + )) + } + async fn initialize( &self, _request: InitializeRequestParams, @@ -278,12 +406,25 @@ impl ServerHandler for GolemAgentMcpServer { if let Some(host) = parts.headers.get("host") { let domain = Domain(host.to_str().unwrap().to_string()); - let tool_router = self.tool_router(&domain).await; - for tool in tool_router.list_all() { + let (router, agent_resources, agent_prompts) = + self.build_capabilities(&domain).await; + + for tool in router.list_all() { self.tools.insert(tool.name.to_string(), tool); } + + let mut resources = self.resources.write().await; + for resource in agent_resources { + resources.insert(resource); + } + + let mut prompts = self.prompts.write().await; + for prompt in agent_prompts { + prompts.insert(prompt); + } + *self.domain.write().await = Some(domain); - *self.tool_router.write().await = Some(tool_router); + *self.tool_router.write().await = Some(router); } } diff --git a/golem-worker-service/src/mcp/agent_mcp_tool.rs b/golem-worker-service/src/mcp/agent_mcp_tool.rs index 050c5e4f27..9f92757128 100644 --- a/golem-worker-service/src/mcp/agent_mcp_tool.rs +++ b/golem-worker-service/src/mcp/agent_mcp_tool.rs @@ -43,7 +43,7 @@ impl CallToolHandler for AgentMcpTool { async move { context .service - .invoke(context.arguments.unwrap_or_default(), &self) + .invoke_tool(context.arguments.unwrap_or_default(), &self) .await } .boxed() diff --git a/golem-worker-service/src/mcp/invoke.rs b/golem-worker-service/src/mcp/invoke.rs deleted file mode 100644 index da0b0540be..0000000000 --- a/golem-worker-service/src/mcp/invoke.rs +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::mcp::agent_mcp_tool::AgentMcpTool; -use crate::service::worker::WorkerService; -use golem_common::base_model::AgentId; -use golem_common::base_model::agent::*; -use golem_common::model::agent::ParsedAgentId; -use golem_wasm::ValueAndType; -use golem_wasm::analysis::AnalysedType; -use golem_wasm::json::ValueAndTypeJsonExtensions; -use rmcp::ErrorData; -use rmcp::model::{CallToolResult, JsonObject}; -use serde_json::json; -use std::sync::Arc; - -pub async fn agent_invoke( - worker_service: &Arc, - args_map: JsonObject, - mcp_tool: &AgentMcpTool, -) -> Result { - let constructor_params = extract_parameters_by_schema( - &args_map, - &mcp_tool.constructor.input_schema, - |value_and_type| ComponentModelElementValue { - value: value_and_type, - }, - ) - .map_err(|e| { - tracing::error!("Failed to extract constructor parameters: {}", e); - ErrorData::invalid_params( - format!("Failed to extract constructor parameters: {}", e), - None, - ) - })?; - - let parsed_agent_id = ParsedAgentId::new( - mcp_tool.agent_type_name.clone(), - DataValue::Tuple(ElementValues { - elements: constructor_params - .into_iter() - .map(ElementValue::ComponentModel) - .collect(), - }), - None, - ) - .map_err(|e| { - tracing::error!("Failed to parse agent id: {}", e); - ErrorData::invalid_params(format!("Failed to parse agent id: {}", e), None) - })?; - - let method_params = - extract_parameters_by_schema(&args_map, &mcp_tool.raw_method.input_schema, |vat| { - UntypedElementValue::ComponentModel(vat.value) - }) - .map_err(|e| { - tracing::error!("Failed to extract method parameters: {}", e); - ErrorData::invalid_params(format!("Failed to extract method parameters: {}", e), None) - })?; - - let method_params_data_value = UntypedDataValue::Tuple(method_params); - - let proto_method_parameters: golem_api_grpc::proto::golem::component::UntypedDataValue = - method_params_data_value.into(); - - let principal = Principal::anonymous(); - let proto_principal: golem_api_grpc::proto::golem::component::Principal = principal.into(); - - let agent_id = AgentId { - component_id: mcp_tool.component_id, - agent_id: parsed_agent_id.to_string(), - }; - - let auth_ctx = golem_service_base::model::auth::AuthCtx::impersonated_user(mcp_tool.account_id); - - let agent_output = worker_service - .invoke_agent( - &agent_id, - mcp_tool.raw_method.name.clone(), - proto_method_parameters, - golem_api_grpc::proto::golem::workerexecutor::v1::AgentInvocationMode::Await as i32, - None, - None, - None, - auth_ctx, - proto_principal, - ) - .await - .map_err(|e| { - tracing::error!("Failed to invoke worker: {:?}", e); - ErrorData::internal_error(format!("Failed to invoke worker: {:?}", e), None) - })?; - - let agent_result = match agent_output.result { - golem_common::model::AgentInvocationResult::AgentMethod { output } => Some(output), - _ => None, - }; - - interpret_agent_response(agent_result, &mcp_tool.raw_method.output_schema) - .map(CallToolResult::structured) -} - -pub fn interpret_agent_response( - invoke_result: Option, - expected_type: &DataSchema, -) -> Result { - match invoke_result { - Some(untyped_data_value) => { - map_successful_agent_response(untyped_data_value, expected_type) - .map(|json_value| { - json!({ - "return-value": json_value, - }) - }) - .map_err(|e| { - tracing::error!("Failed to map successful agent response: {}", e); - ErrorData::internal_error( - format!("Failed to map successful agent response: {}", e), - None, - ) - }) - } - None => Ok(json!({})), - } -} - -fn map_successful_agent_response( - agent_response: UntypedDataValue, - expected_type: &DataSchema, -) -> Result { - let typed_value = - DataValue::try_from_untyped(agent_response, expected_type.clone()).map_err(|error| { - ErrorData::internal_error(format!("Agent response type mismatch: {error}"), None) - })?; - - match typed_value { - DataValue::Tuple(ElementValues { elements }) => match elements.len() { - 0 => Ok(json!({})), - 1 => map_single_element_agent_response(elements.into_iter().next().unwrap()).map_err( - |e| { - tracing::error!("Failed to map single element agent response: {}", e); - ErrorData::internal_error( - format!("Failed to map single element agent response: {}", e), - None, - ) - }, - ), - _ => Err(ErrorData::internal_error( - "Unexpected number of response tuple elements".to_string(), - None, - )), - }, - DataValue::Multimodal(_) => Err(ErrorData::internal_error( - "multi modal response not yet supported".to_string(), - None, - )), - } -} - -fn map_single_element_agent_response(element: ElementValue) -> Result { - match element { - ElementValue::ComponentModel(component_model_value) => { - component_model_value.value.to_json_value() - } - - ElementValue::UnstructuredBinary(_) => Err( - "Received unstructured binary response, which is not supported in this context" - .to_string(), - ), - - ElementValue::UnstructuredText(_) => Err( - "Received unstructured text response, which is not supported in this context" - .to_string(), - ), - } -} - -fn extract_parameters_by_schema( - args_map: &JsonObject, - schema: &DataSchema, - f: F, -) -> Result, String> -where - F: Fn(ValueAndType) -> A, -{ - match schema { - DataSchema::Tuple(named_schemas) => { - let mut params = Vec::new(); - - for NamedElementSchema { - name, - schema: elem_schema, - } in &named_schemas.elements - { - match elem_schema { - ElementSchema::ComponentModel(ComponentModelElementSchema { element_type }) => { - let json_value = match args_map.get(name) { - Some(value) => value.clone(), - None => { - if matches!(element_type, AnalysedType::Option(_)) { - serde_json::Value::Null - } else { - return Err(format!("Missing parameter: {}", name)); - } - } - }; - - let value_and_type = - golem_wasm::ValueAndType::parse_with_type(&json_value, element_type) - .map_err(|errs| { - format!( - "Failed to parse parameter '{}': {}", - name, - errs.join(", ") - ) - })?; - - params.push(f(value_and_type)); - } - _ => { - return Err(format!( - "Unsupported element schema type for parameter '{}'", - name - )); - } - } - } - - Ok(params) - } - DataSchema::Multimodal(_) => Err("Multimodal schema is not yet supported".to_string()), - } -} diff --git a/golem-worker-service/src/mcp/invoke/agent_method_input.rs b/golem-worker-service/src/mcp/invoke/agent_method_input.rs new file mode 100644 index 0000000000..2652e61d9b --- /dev/null +++ b/golem-worker-service/src/mcp/invoke/agent_method_input.rs @@ -0,0 +1,397 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::mcp::invoke::multimodal_params_extraction::extract_multimodal_element_value; +use base64::Engine; +use golem_common::base_model::agent::{ + BinaryReference, BinaryReferenceValue, BinarySource, BinaryType, ComponentModelElementSchema, + DataSchema, ElementSchema, NamedElementSchema, TextReference, TextReferenceValue, TextSource, + TextType, UntypedDataValue, UntypedElementValue, UntypedNamedElementValue, +}; +use golem_wasm::analysis::AnalysedType; +use golem_wasm::json::ValueAndTypeJsonExtensions; +use rmcp::model::JsonObject; + +pub fn get_agent_method_input( + mcp_args: &JsonObject, + schema: &DataSchema, +) -> Result { + match schema { + DataSchema::Tuple(named_schemas) => { + let elements = extract_element_values(mcp_args, &named_schemas.elements)?; + Ok(UntypedDataValue::Tuple(elements)) + } + DataSchema::Multimodal(named_schemas) => { + let parts_array = mcp_args + .get("parts") + .and_then(|v| v.as_array()) + .ok_or("Multimodal input requires a parts array field")?; + + let schema_map: std::collections::HashMap<&str, &ElementSchema> = named_schemas + .elements + .iter() + .map(|s| (s.name.as_str(), &s.schema)) + .collect(); + + let mut named_elements = Vec::new(); + for (i, part) in parts_array.iter().enumerate() { + let obj = part.as_object().ok_or_else(|| { + format!("parts[{}] must be an object with 'name' and 'value'", i) + })?; + + let name = obj + .get("name") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("parts[{}] is missing 'name' string field", i))?; + + let elem_schema = schema_map.get(name).ok_or_else(|| { + format!( + "parts[{}]: unknown element name '{}'. Expected one of: {}", + i, + name, + schema_map.keys().copied().collect::>().join(", ") + ) + })?; + + let value_json = obj + .get("value") + .ok_or_else(|| format!("parts[{}] is missing 'value' field", i))?; + + let element = extract_multimodal_element_value(name, value_json, elem_schema, i)?; + + named_elements.push(UntypedNamedElementValue { + name: name.to_string(), + value: element, + }); + } + Ok(UntypedDataValue::Multimodal(named_elements)) + } + } +} + +fn extract_element_values( + args_map: &JsonObject, + schemas: &[NamedElementSchema], +) -> Result, String> { + let mut params = Vec::new(); + for schema_element in schemas { + let element = + extract_single_element_value(args_map, &schema_element.name, &schema_element.schema)?; + params.push(element); + } + Ok(params) +} + +fn extract_single_element_value( + args_map: &JsonObject, + name: &str, + elem_schema: &ElementSchema, +) -> Result { + let json_value = args_map.get(name); + match elem_schema { + ElementSchema::ComponentModel(ComponentModelElementSchema { element_type }) => { + let json_value = match json_value { + Some(value) => value.clone(), + None => { + if matches!(element_type, AnalysedType::Option(_)) { + serde_json::Value::Null + } else { + return Err(format!("Missing parameter: {}", name)); + } + } + }; + + let value_and_type = + golem_wasm::ValueAndType::parse_with_type(&json_value, element_type).map_err( + |errs| format!("Failed to parse parameter '{}': {}", name, errs.join(", ")), + )?; + + Ok(UntypedElementValue::ComponentModel(value_and_type.value)) + } + ElementSchema::UnstructuredText(descriptor) => { + let obj = match json_value { + Some(serde_json::Value::Object(o)) => o, + Some(_) => { + return Err(format!( + "Parameter '{}' must be an object with 'data' and optional 'languageCode'", + name + )); + } + None => return Err(format!("Missing parameter: {}", name)), + }; + + let data = obj + .get("data") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("Parameter '{}' is missing 'data' string field", name))? + .to_string(); + + let language_code = obj.get("languageCode").and_then(|v| v.as_str()); + + if let Some(code) = language_code + && let Some(allowed) = &descriptor.restrictions + && !allowed.is_empty() + && !allowed.iter().any(|t| t.language_code == code) + { + let expected: Vec<&str> = + allowed.iter().map(|t| t.language_code.as_str()).collect(); + return Err(format!( + "Parameter '{}': language code '{}' is not allowed. Expected one of: {}", + name, + code, + expected.join(", ") + )); + } + + let text_type = language_code.map(|code| TextType { + language_code: code.to_string(), + }); + + Ok(UntypedElementValue::UnstructuredText(TextReferenceValue { + value: TextReference::Inline(TextSource { data, text_type }), + })) + } + ElementSchema::UnstructuredBinary(descriptor) => { + let obj = match json_value { + Some(serde_json::Value::Object(o)) => o, + Some(_) => { + return Err(format!( + "Parameter '{}' must be an object with 'data' and 'mimeType'", + name + )); + } + None => return Err(format!("Missing parameter: {}", name)), + }; + + let b64 = obj + .get("data") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("Parameter '{}' is missing 'data' string field", name))?; + + let mime_type = obj + .get("mimeType") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + format!("Parameter '{}' is missing 'mimeType' string field", name) + })?; + + if let Some(allowed) = &descriptor.restrictions + && !allowed.is_empty() + && !allowed.iter().any(|t| t.mime_type == mime_type) + { + let expected: Vec<&str> = allowed.iter().map(|t| t.mime_type.as_str()).collect(); + return Err(format!( + "Parameter '{}': MIME type '{}' is not allowed. Expected one of: {}", + name, + mime_type, + expected.join(", ") + )); + } + + let data = base64::engine::general_purpose::STANDARD + .decode(b64) + .map_err(|e| format!("Failed to decode base64 parameter '{}': {}", name, e))?; + + Ok(UntypedElementValue::UnstructuredBinary( + BinaryReferenceValue { + value: BinaryReference::Inline(BinarySource { + data, + binary_type: BinaryType { + mime_type: mime_type.to_string(), + }, + }), + }, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use golem_common::base_model::agent::{ + BinaryDescriptor, ComponentModelElementSchema, NamedElementSchemas, TextDescriptor, + }; + use golem_wasm::analysis::analysed_type::{option, str}; + use serde_json::json; + use test_r::test; + + fn string_schema(name: &str) -> NamedElementSchema { + NamedElementSchema { + name: name.to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }), + } + } + + fn text_schema(name: &str) -> NamedElementSchema { + NamedElementSchema { + name: name.to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + } + } + + fn binary_schema(name: &str) -> NamedElementSchema { + NamedElementSchema { + name: name.to_string(), + schema: ElementSchema::UnstructuredBinary(BinaryDescriptor { restrictions: None }), + } + } + + #[test] + fn tuple_extracts_component_model_param() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![string_schema("city")], + }); + let args: JsonObject = json!({"city": "Sydney"}).as_object().unwrap().clone(); + let result = get_agent_method_input(&args, &schema).unwrap(); + assert!(matches!(result, UntypedDataValue::Tuple(elems) if elems.len() == 1)); + } + + #[test] + fn tuple_extracts_unstructured_text() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![text_schema("report")], + }); + let args: JsonObject = json!({"report": {"data": "hello world"}}) + .as_object() + .unwrap() + .clone(); + let result = get_agent_method_input(&args, &schema).unwrap(); + match result { + UntypedDataValue::Tuple(elems) => match &elems[0] { + UntypedElementValue::UnstructuredText(t) => match &t.value { + TextReference::Inline(src) => assert_eq!(src.data, "hello world"), + _ => panic!("expected inline text"), + }, + _ => panic!("expected unstructured text"), + }, + _ => panic!("expected tuple"), + } + } + + #[test] + fn tuple_extracts_unstructured_binary() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![binary_schema("image")], + }); + // base64("abc") = "YWJj" + let args: JsonObject = json!({"image": {"data": "YWJj", "mimeType": "image/png"}}) + .as_object() + .unwrap() + .clone(); + let result = get_agent_method_input(&args, &schema).unwrap(); + match result { + UntypedDataValue::Tuple(elems) => match &elems[0] { + UntypedElementValue::UnstructuredBinary(b) => match &b.value { + BinaryReference::Inline(src) => { + assert_eq!(src.data, b"abc"); + assert_eq!(src.binary_type.mime_type, "image/png"); + } + _ => panic!("expected inline binary"), + }, + _ => panic!("expected unstructured binary"), + }, + _ => panic!("expected tuple"), + } + } + + #[test] + fn error_on_missing_required_param() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![string_schema("city")], + }); + let args: JsonObject = json!({}).as_object().unwrap().clone(); + let err = get_agent_method_input(&args, &schema).unwrap_err(); + assert!(err.contains("Missing parameter: city"), "got: {err}"); + } + + #[test] + fn error_on_invalid_base64() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![binary_schema("image")], + }); + let args: JsonObject = + json!({"image": {"data": "not-valid-b64!!!", "mimeType": "image/png"}}) + .as_object() + .unwrap() + .clone(); + let err = get_agent_method_input(&args, &schema).unwrap_err(); + assert!(err.contains("base64"), "got: {err}"); + } + + #[test] + fn error_on_binary_missing_mime_type() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![binary_schema("image")], + }); + let args: JsonObject = json!({"image": {"data": "YWJj"}}) + .as_object() + .unwrap() + .clone(); + let err = get_agent_method_input(&args, &schema).unwrap_err(); + assert!(err.contains("mimeType"), "got: {err}"); + } + + #[test] + fn multimodal_extracts_parts() { + let schema = DataSchema::Multimodal(NamedElementSchemas { + elements: vec![text_schema("description"), binary_schema("photo")], + }); + let args: JsonObject = json!({ + "parts": [ + {"name": "description", "value": {"data": "a photo"}}, + {"name": "photo", "value": {"data": "AQID", "mimeType": "image/png"}} + ] + }) + .as_object() + .unwrap() + .clone(); + let result = get_agent_method_input(&args, &schema).unwrap(); + assert!(matches!(result, UntypedDataValue::Multimodal(parts) if parts.len() == 2)); + } + + #[test] + fn multimodal_error_on_unknown_part_name() { + let schema = DataSchema::Multimodal(NamedElementSchemas { + elements: vec![text_schema("description")], + }); + let args: JsonObject = json!({ + "parts": [ + {"name": "unknown_field", "value": {"data": "hello"}} + ] + }) + .as_object() + .unwrap() + .clone(); + let err = get_agent_method_input(&args, &schema).unwrap_err(); + assert!(err.contains("unknown element name"), "got: {err}"); + } + + #[test] + fn tuple_missing_optional_param_defaults_to_none() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "note".to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: option(str()), + }), + }], + }); + let args: JsonObject = json!({}).as_object().unwrap().clone(); + let result = get_agent_method_input(&args, &schema).unwrap(); + assert!(matches!(result, UntypedDataValue::Tuple(elems) if elems.len() == 1)); + } +} diff --git a/golem-worker-service/src/mcp/invoke/constructor_param_extraction.rs b/golem-worker-service/src/mcp/invoke/constructor_param_extraction.rs new file mode 100644 index 0000000000..af3746148c --- /dev/null +++ b/golem-worker-service/src/mcp/invoke/constructor_param_extraction.rs @@ -0,0 +1,166 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use golem_common::base_model::agent::{ + ComponentModelElementSchema, ComponentModelElementValue, DataSchema, ElementSchema, + NamedElementSchema, +}; +use golem_wasm::analysis::AnalysedType; +use golem_wasm::json::ValueAndTypeJsonExtensions; +use rmcp::model::JsonObject; + +pub fn extract_constructor_input_values( + args_map: &JsonObject, + schema: &DataSchema, +) -> Result, String> { + match schema { + DataSchema::Tuple(named_schemas) => { + let mut params = Vec::new(); + + for NamedElementSchema { + name, + schema: elem_schema, + } in &named_schemas.elements + { + match elem_schema { + ElementSchema::ComponentModel(ComponentModelElementSchema { element_type }) => { + let json_value = match args_map.get(name) { + Some(value) => value.clone(), + None => { + if matches!(element_type, AnalysedType::Option(_)) { + serde_json::Value::Null + } else { + return Err(format!("Missing parameter: {}", name)); + } + } + }; + + let value_and_type = + golem_wasm::ValueAndType::parse_with_type(&json_value, element_type) + .map_err(|errs| { + format!( + "Failed to parse parameter '{}': {}", + name, + errs.join(", ") + ) + })?; + + params.push(ComponentModelElementValue { + value: value_and_type, + }); + } + ElementSchema::UnstructuredText(_) => { + return Err(format!( + "MCP cannot support unstructured-text constructor parameters like '{}'", + name + )); + } + + ElementSchema::UnstructuredBinary(_) => { + return Err(format!( + "MCP cannot support unstructured-binary constructor parameters like '{}'", + name + )); + } + } + } + + Ok(params) + } + DataSchema::Multimodal(_) => { + Err("MCP does not support multimodal constructor schemas".to_string()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use golem_common::base_model::agent::{NamedElementSchemas, TextDescriptor}; + use golem_wasm::analysis::analysed_type::{str, u32}; + use serde_json::json; + use test_r::test; + + fn string_schema(name: &str) -> NamedElementSchema { + NamedElementSchema { + name: name.to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }), + } + } + + fn u32_schema(name: &str) -> NamedElementSchema { + NamedElementSchema { + name: name.to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: u32(), + }), + } + } + + #[test] + fn extracts_string_param() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![string_schema("name")], + }); + let args: JsonObject = json!({"name": "alice"}).as_object().unwrap().clone(); + let result = extract_constructor_input_values(&args, &schema).unwrap(); + assert_eq!(result.len(), 1); + } + + #[test] + fn extracts_multiple_params() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![string_schema("name"), u32_schema("age")], + }); + let args: JsonObject = json!({"name": "alice", "age": 30}) + .as_object() + .unwrap() + .clone(); + let result = extract_constructor_input_values(&args, &schema).unwrap(); + assert_eq!(result.len(), 2); + } + + #[test] + fn error_on_missing_required_param() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![string_schema("name")], + }); + let args: JsonObject = json!({}).as_object().unwrap().clone(); + let err = extract_constructor_input_values(&args, &schema).unwrap_err(); + assert!(err.contains("Missing parameter: name"), "got: {err}"); + } + + #[test] + fn rejects_unstructured_text_constructor() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "desc".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }], + }); + let args: JsonObject = json!({"desc": "hello"}).as_object().unwrap().clone(); + let err = extract_constructor_input_values(&args, &schema).unwrap_err(); + assert!(err.contains("unstructured-text"), "got: {err}"); + } + + #[test] + fn rejects_multimodal_schema() { + let schema = DataSchema::Multimodal(NamedElementSchemas { elements: vec![] }); + let args: JsonObject = json!({}).as_object().unwrap().clone(); + let err = extract_constructor_input_values(&args, &schema).unwrap_err(); + assert!(err.contains("multimodal"), "got: {err}"); + } +} diff --git a/golem-worker-service/src/mcp/schema/mcp_schema_mapping.rs b/golem-worker-service/src/mcp/invoke/mod.rs similarity index 58% rename from golem-worker-service/src/mcp/schema/mcp_schema_mapping.rs rename to golem-worker-service/src/mcp/invoke/mod.rs index 6b6bf4fef3..ed8c7f2669 100644 --- a/golem-worker-service/src/mcp/schema/mcp_schema_mapping.rs +++ b/golem-worker-service/src/mcp/invoke/mod.rs @@ -12,14 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::mcp::schema::mcp_schema::McpSchema; -use golem_common::base_model::agent::DataSchema; - -pub fn get_mcp_schema(data_schema: &DataSchema) -> McpSchema { - match data_schema { - DataSchema::Tuple(schemas) => McpSchema::from_named_element_schemas(&schemas.elements), - DataSchema::Multimodal(_) => { - todo!("Multimodal schema is not supported in this example") - } - } -} +mod agent_method_input; +mod constructor_param_extraction; +mod multimodal_params_extraction; +pub mod resource; +pub mod tool; diff --git a/golem-worker-service/src/mcp/invoke/multimodal_params_extraction.rs b/golem-worker-service/src/mcp/invoke/multimodal_params_extraction.rs new file mode 100644 index 0000000000..3b8f12b08f --- /dev/null +++ b/golem-worker-service/src/mcp/invoke/multimodal_params_extraction.rs @@ -0,0 +1,264 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use base64::Engine; +use golem_common::base_model::agent::{ + BinaryReference, BinaryReferenceValue, BinarySource, BinaryType, ComponentModelElementSchema, + ElementSchema, TextReference, TextReferenceValue, TextSource, TextType, UntypedElementValue, +}; +use golem_wasm::json::ValueAndTypeJsonExtensions; + +pub fn extract_multimodal_element_value( + name: &str, + value_json: &serde_json::Value, + elem_schema: &ElementSchema, + index: usize, +) -> Result { + match elem_schema { + ElementSchema::ComponentModel(ComponentModelElementSchema { element_type }) => { + let value_and_type = + golem_wasm::ValueAndType::parse_with_type(value_json, element_type).map_err( + |errs| { + format!( + "parts[{}] '{}': failed to parse value: {}", + index, + name, + errs.join(", ") + ) + }, + )?; + Ok(UntypedElementValue::ComponentModel(value_and_type.value)) + } + ElementSchema::UnstructuredText(descriptor) => { + let obj = value_json.as_object().ok_or_else(|| { + format!( + "parts[{}] '{}': value must be an object with 'data' and optional 'languageCode'", + index, name + ) + })?; + + let data = obj + .get("data") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("parts[{}] '{}': missing 'data' string field", index, name))? + .to_string(); + + let language_code = obj.get("languageCode").and_then(|v| v.as_str()); + + if let Some(code) = language_code + && let Some(allowed) = &descriptor.restrictions + && !allowed.is_empty() + && !allowed.iter().any(|t| t.language_code == code) + { + let expected: Vec<&str> = + allowed.iter().map(|t| t.language_code.as_str()).collect(); + return Err(format!( + "parts[{}] '{}': language code '{}' is not allowed. Expected one of: {}", + index, + name, + code, + expected.join(", ") + )); + } + + let text_type = language_code.map(|code| TextType { + language_code: code.to_string(), + }); + + Ok(UntypedElementValue::UnstructuredText(TextReferenceValue { + value: TextReference::Inline(TextSource { data, text_type }), + })) + } + ElementSchema::UnstructuredBinary(descriptor) => { + let obj = value_json.as_object().ok_or_else(|| { + format!( + "parts[{}] '{}': value must be an object with 'data' and 'mimeType'", + index, name + ) + })?; + + let b64 = obj.get("data").and_then(|v| v.as_str()).ok_or_else(|| { + format!("parts[{}] '{}': missing 'data' string field", index, name) + })?; + + let mime_type = obj + .get("mimeType") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + format!( + "parts[{}] '{}': missing 'mimeType' string field", + index, name + ) + })?; + + if let Some(allowed) = &descriptor.restrictions + && !allowed.is_empty() + && !allowed.iter().any(|t| t.mime_type == mime_type) + { + let expected: Vec<&str> = allowed.iter().map(|t| t.mime_type.as_str()).collect(); + return Err(format!( + "parts[{}] '{}': MIME type '{}' is not allowed. Expected one of: {}", + index, + name, + mime_type, + expected.join(", ") + )); + } + + let data = base64::engine::general_purpose::STANDARD + .decode(b64) + .map_err(|e| { + format!( + "parts[{}] '{}': failed to decode base64: {}", + index, name, e + ) + })?; + + Ok(UntypedElementValue::UnstructuredBinary( + BinaryReferenceValue { + value: BinaryReference::Inline(BinarySource { + data, + binary_type: BinaryType { + mime_type: mime_type.to_string(), + }, + }), + }, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use golem_common::base_model::agent::{BinaryDescriptor, TextDescriptor, TextType}; + use golem_wasm::analysis::analysed_type::str; + use serde_json::json; + use test_r::test; + + #[test] + fn extracts_component_model_string() { + let schema = ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }); + let value = json!("hello"); + let result = extract_multimodal_element_value("msg", &value, &schema, 0).unwrap(); + assert!(matches!(result, UntypedElementValue::ComponentModel(_))); + } + + #[test] + fn extracts_text_without_language_code() { + let schema = ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }); + let value = json!({"data": "some text"}); + let result = extract_multimodal_element_value("note", &value, &schema, 0).unwrap(); + match result { + UntypedElementValue::UnstructuredText(t) => match t.value { + TextReference::Inline(src) => { + assert_eq!(src.data, "some text"); + assert!(src.text_type.is_none()); + } + _ => panic!("expected inline"), + }, + _ => panic!("expected text"), + } + } + + #[test] + fn extracts_text_with_language_code() { + let schema = ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }); + let value = json!({"data": "bonjour", "languageCode": "fr"}); + let result = extract_multimodal_element_value("note", &value, &schema, 0).unwrap(); + match result { + UntypedElementValue::UnstructuredText(t) => match t.value { + TextReference::Inline(src) => { + assert_eq!(src.data, "bonjour"); + assert_eq!(src.text_type.unwrap().language_code, "fr"); + } + _ => panic!("expected inline"), + }, + _ => panic!("expected text"), + } + } + + #[test] + fn rejects_disallowed_language_code() { + let schema = ElementSchema::UnstructuredText(TextDescriptor { + restrictions: Some(vec![TextType { + language_code: "en".to_string(), + }]), + }); + let value = json!({"data": "hola", "languageCode": "es"}); + let err = extract_multimodal_element_value("note", &value, &schema, 1).unwrap_err(); + assert!( + err.contains("language code 'es' is not allowed"), + "got: {err}" + ); + } + + #[test] + fn extracts_binary() { + let schema = ElementSchema::UnstructuredBinary(BinaryDescriptor { restrictions: None }); + let value = json!({"data": "AQID", "mimeType": "image/png"}); + let result = extract_multimodal_element_value("img", &value, &schema, 0).unwrap(); + match result { + UntypedElementValue::UnstructuredBinary(b) => match b.value { + BinaryReference::Inline(src) => { + assert_eq!(src.data, vec![1, 2, 3]); + assert_eq!(src.binary_type.mime_type, "image/png"); + } + _ => panic!("expected inline"), + }, + _ => panic!("expected binary"), + } + } + + #[test] + fn rejects_disallowed_mime_type() { + let schema = ElementSchema::UnstructuredBinary(BinaryDescriptor { + restrictions: Some(vec![BinaryType { + mime_type: "image/png".to_string(), + }]), + }); + let value = json!({"data": "AQID", "mimeType": "image/jpeg"}); + let err = extract_multimodal_element_value("img", &value, &schema, 0).unwrap_err(); + assert!( + err.contains("MIME type 'image/jpeg' is not allowed"), + "got: {err}" + ); + } + + #[test] + fn error_on_invalid_base64() { + let schema = ElementSchema::UnstructuredBinary(BinaryDescriptor { restrictions: None }); + let value = json!({"data": "!!!invalid!!!", "mimeType": "image/png"}); + let err = extract_multimodal_element_value("img", &value, &schema, 0).unwrap_err(); + assert!(err.contains("base64"), "got: {err}"); + } + + #[test] + fn error_on_missing_data_field_text() { + let schema = ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }); + let value = json!({"other": "stuff"}); + let err = extract_multimodal_element_value("note", &value, &schema, 0).unwrap_err(); + assert!(err.contains("missing 'data'"), "got: {err}"); + } + + #[test] + fn error_on_missing_mime_type() { + let schema = ElementSchema::UnstructuredBinary(BinaryDescriptor { restrictions: None }); + let value = json!({"data": "AQID"}); + let err = extract_multimodal_element_value("img", &value, &schema, 0).unwrap_err(); + assert!(err.contains("mimeType"), "got: {err}"); + } +} diff --git a/golem-worker-service/src/mcp/invoke/resource.rs b/golem-worker-service/src/mcp/invoke/resource.rs new file mode 100644 index 0000000000..558740f773 --- /dev/null +++ b/golem-worker-service/src/mcp/invoke/resource.rs @@ -0,0 +1,418 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::mcp::agent_mcp_resource::{AgentMcpResource, ConstructorParam}; +use crate::mcp::invoke::constructor_param_extraction::extract_constructor_input_values; +use crate::service::worker::WorkerService; +use base64::Engine; +use golem_common::base_model::AgentId; +use golem_common::base_model::agent::*; +use golem_common::model::agent::ParsedAgentId; +use golem_wasm::json::ValueAndTypeJsonExtensions; +use rmcp::ErrorData; +use rmcp::model::{JsonObject, ReadResourceResult, ResourceContents}; +use std::sync::Arc; + +pub async fn invoke_resource( + worker_service: &Arc, + mcp_resource: &AgentMcpResource, + uri: &str, + extracted_params: Option>, +) -> Result { + let constructor_params = match extracted_params { + None => { + vec![] + } + Some(params) => { + let mut args_map = JsonObject::default(); + for param in ¶ms { + args_map.insert( + param.name.clone(), + serde_json::Value::String(param.value.clone()), + ); + } + extract_constructor_input_values(&args_map, &mcp_resource.constructor.input_schema) + .map_err(|e| { + tracing::error!("Failed to extract constructor parameters from URI: {}", e); + ErrorData::invalid_params( + format!("Failed to extract constructor parameters from URI: {}", e), + None, + ) + })? + } + }; + + let parsed_agent_id = ParsedAgentId::new( + mcp_resource.agent_type_name.clone(), + DataValue::Tuple(ElementValues { + elements: constructor_params + .into_iter() + .map(ElementValue::ComponentModel) + .collect(), + }), + None, + ) + .map_err(|e| { + tracing::error!("Failed to parse agent id: {}", e); + ErrorData::invalid_params(format!("Failed to parse agent id: {}", e), None) + })?; + + let method_params_data_value = UntypedDataValue::Tuple(vec![]); + + let proto_method_parameters: golem_api_grpc::proto::golem::component::UntypedDataValue = + method_params_data_value.into(); + + let principal = Principal::anonymous(); + let proto_principal: golem_api_grpc::proto::golem::component::Principal = principal.into(); + + let agent_id = AgentId { + component_id: mcp_resource.component_id, + agent_id: parsed_agent_id.to_string(), + }; + + let auth_ctx = + golem_service_base::model::auth::AuthCtx::impersonated_user(mcp_resource.account_id); + + let agent_output = worker_service + .invoke_agent( + &agent_id, + mcp_resource.raw_method.name.clone(), + proto_method_parameters, + golem_api_grpc::proto::golem::workerexecutor::v1::AgentInvocationMode::Await as i32, + None, + None, + None, + auth_ctx, + proto_principal, + ) + .await + .map_err(|e| { + tracing::error!("Failed to invoke worker for resource: {:?}", e); + ErrorData::internal_error( + format!("Failed to invoke worker for resource: {:?}", e), + None, + ) + })?; + + let agent_result = match agent_output.result { + golem_common::model::AgentInvocationResult::AgentMethod { output } => Some(output), + _ => None, + }; + + let contents = map_agent_response_to_resource_contents( + agent_result, + &mcp_resource.raw_method.output_schema, + uri, + )?; + + Ok(ReadResourceResult { contents }) +} + +fn map_agent_response_to_resource_contents( + invoke_result: Option, + expected_type: &DataSchema, + uri: &str, +) -> Result, ErrorData> { + match invoke_result { + Some(untyped_data_value) => { + let typed_value = DataValue::try_from_untyped( + untyped_data_value, + expected_type.clone(), + ) + .map_err(|error| { + ErrorData::internal_error(format!("Agent response type mismatch: {error}"), None) + })?; + + data_value_to_resource_contents(typed_value, uri) + } + None => Ok(vec![]), + } +} + +fn data_value_to_resource_contents( + typed_value: DataValue, + uri: &str, +) -> Result, ErrorData> { + match typed_value { + DataValue::Tuple(ElementValues { elements }) => match elements.len() { + 0 => Ok(vec![]), + 1 => { + let element = elements.into_iter().next().unwrap(); + convert_to_resource_content(element, uri).map(|c| vec![c]) + } + _ => Err(ErrorData::internal_error( + "Unexpected number of response tuple elements".to_string(), + None, + )), + }, + DataValue::Multimodal(NamedElementValues { elements }) => elements + .into_iter() + .map(|named| convert_to_resource_content(named.value, uri)) + .collect(), + } +} + +fn convert_to_resource_content( + element: ElementValue, + uri: &str, +) -> Result { + match element { + ElementValue::ComponentModel(v) => { + let json_value = v.value.to_json_value().map_err(|e| { + ErrorData::internal_error( + format!("Failed to serialize component model response: {e}"), + None, + ) + })?; + Ok(ResourceContents::TextResourceContents { + uri: uri.to_string(), + mime_type: Some("application/json".to_string()), + text: json_value.to_string(), + meta: None, + }) + } + + ElementValue::UnstructuredText(UnstructuredTextElementValue { value, .. }) => { + match value { + TextReference::Inline(TextSource { data, .. }) => { + // Note that languageCode cannot be encoded in the output to MCP clients when they act as resources + // ResourceContents::text(text, uri) — first param is text content, second is URI + Ok(ResourceContents::text(data.to_string(), uri.to_string())) + } + TextReference::Url(url) => { + // This cannot be possible according to MCP spec + // A resource content must respond with either an actual text or blob + // https://modelcontextprotocol.info/docs/concepts/resources/#reading-resources + Err(ErrorData::internal_error( + format!( + "Received URL text reference, which cannot be part of resource output: {}", + url.value + ), + None, + )) + } + } + } + + ElementValue::UnstructuredBinary(UnstructuredBinaryElementValue { value, .. }) => { + match value { + BinaryReference::Inline(BinarySource { data, binary_type }) => { + let b64 = base64::engine::general_purpose::STANDARD.encode(&data); + + Ok(ResourceContents::BlobResourceContents { + uri: uri.to_string(), + mime_type: Some(binary_type.mime_type), + blob: b64, + meta: None, + }) + } + BinaryReference::Url(_) => Err(ErrorData::internal_error( + "Received URL binary reference, which cannot be part of resource output" + .to_string(), + None, + )), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use golem_common::base_model::agent::{ + BinaryDescriptor, ComponentModelElementSchema, ElementSchema, NamedElementSchema, + NamedElementSchemas, TextDescriptor, UntypedNamedElementValue, Url as AgentUrl, + }; + use golem_wasm::Value; + use golem_wasm::analysis::analysed_type::str; + use serde_json::json; + use test_r::test; + + const TEST_URI: &str = "golem://Agent/resource"; + + fn str_output_schema() -> DataSchema { + DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "result".to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: str(), + }), + }], + }) + } + + #[test] + fn component_model_to_text_resource_json() { + let response = UntypedDataValue::Tuple(vec![UntypedElementValue::ComponentModel( + Value::String("sunny".to_string()), + )]); + let contents = + map_agent_response_to_resource_contents(Some(response), &str_output_schema(), TEST_URI) + .unwrap(); + assert_eq!(contents.len(), 1); + match &contents[0] { + ResourceContents::TextResourceContents { + uri, + mime_type, + text, + .. + } => { + assert_eq!(uri, TEST_URI); + assert_eq!(mime_type.as_deref(), Some("application/json")); + let parsed: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(parsed, json!("sunny")); + } + _ => panic!("expected TextResourceContents"), + } + } + + #[test] + fn text_element_to_text_resource() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "report".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }], + }); + let response = UntypedDataValue::Tuple(vec![UntypedElementValue::UnstructuredText( + TextReferenceValue { + value: TextReference::Inline(TextSource { + data: "rainy day".to_string(), + text_type: None, + }), + }, + )]); + let contents = + map_agent_response_to_resource_contents(Some(response), &schema, TEST_URI).unwrap(); + assert_eq!(contents.len(), 1); + match &contents[0] { + ResourceContents::TextResourceContents { text, .. } => { + assert_eq!(text, "rainy day"); + } + _ => panic!("expected TextResourceContents"), + } + } + + #[test] + fn binary_element_to_blob_resource() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "image".to_string(), + schema: ElementSchema::UnstructuredBinary(BinaryDescriptor { restrictions: None }), + }], + }); + let response = UntypedDataValue::Tuple(vec![UntypedElementValue::UnstructuredBinary( + BinaryReferenceValue { + value: BinaryReference::Inline(BinarySource { + data: vec![1, 2, 3], + binary_type: BinaryType { + mime_type: "image/png".to_string(), + }, + }), + }, + )]); + let contents = + map_agent_response_to_resource_contents(Some(response), &schema, TEST_URI).unwrap(); + assert_eq!(contents.len(), 1); + match &contents[0] { + ResourceContents::BlobResourceContents { + blob, mime_type, .. + } => { + assert_eq!(blob, "AQID"); + assert_eq!(mime_type.as_deref(), Some("image/png")); + } + _ => panic!("expected BlobResourceContents"), + } + } + + #[test] + fn none_response_returns_empty() { + let contents = + map_agent_response_to_resource_contents(None, &str_output_schema(), TEST_URI).unwrap(); + assert!(contents.is_empty()); + } + + #[test] + fn multimodal_returns_multiple_contents() { + let schema = DataSchema::Multimodal(NamedElementSchemas { + elements: vec![ + NamedElementSchema { + name: "text".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }, + NamedElementSchema { + name: "img".to_string(), + schema: ElementSchema::UnstructuredBinary(BinaryDescriptor { + restrictions: None, + }), + }, + ], + }); + let response = UntypedDataValue::Multimodal(vec![ + UntypedNamedElementValue { + name: "text".to_string(), + value: UntypedElementValue::UnstructuredText(TextReferenceValue { + value: TextReference::Inline(TextSource { + data: "snow report".to_string(), + text_type: None, + }), + }), + }, + UntypedNamedElementValue { + name: "img".to_string(), + value: UntypedElementValue::UnstructuredBinary(BinaryReferenceValue { + value: BinaryReference::Inline(BinarySource { + data: vec![1, 2, 3], + binary_type: BinaryType { + mime_type: "image/png".to_string(), + }, + }), + }), + }, + ]); + let contents = + map_agent_response_to_resource_contents(Some(response), &schema, TEST_URI).unwrap(); + assert_eq!(contents.len(), 2); + assert!( + matches!(&contents[0], ResourceContents::TextResourceContents { text, .. } if text == "snow report") + ); + assert!( + matches!(&contents[1], ResourceContents::BlobResourceContents { blob, .. } if blob == "AQID") + ); + } + + #[test] + fn error_on_text_url_reference() { + let elem = ElementValue::UnstructuredText(UnstructuredTextElementValue { + value: TextReference::Url(AgentUrl { + value: "https://example.com".to_string(), + }), + descriptor: TextDescriptor { restrictions: None }, + }); + let err = convert_to_resource_content(elem, TEST_URI).unwrap_err(); + assert!(err.message.contains("URL"), "got: {}", err.message); + } + + #[test] + fn error_on_binary_url_reference() { + let elem = ElementValue::UnstructuredBinary(UnstructuredBinaryElementValue { + value: BinaryReference::Url(AgentUrl { + value: "https://example.com/img.png".to_string(), + }), + descriptor: BinaryDescriptor { restrictions: None }, + }); + let err = convert_to_resource_content(elem, TEST_URI).unwrap_err(); + assert!(err.message.contains("URL"), "got: {}", err.message); + } +} diff --git a/golem-worker-service/src/mcp/invoke/tool.rs b/golem-worker-service/src/mcp/invoke/tool.rs new file mode 100644 index 0000000000..c7b5ca8aa3 --- /dev/null +++ b/golem-worker-service/src/mcp/invoke/tool.rs @@ -0,0 +1,477 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::mcp::agent_mcp_tool::AgentMcpTool; +use crate::mcp::invoke::agent_method_input::get_agent_method_input; +use crate::mcp::invoke::constructor_param_extraction::extract_constructor_input_values; +use crate::service::worker::WorkerService; +use base64::Engine; +use golem_common::base_model::AgentId; +use golem_common::base_model::agent::*; +use golem_common::model::agent::ParsedAgentId; +use golem_wasm::json::ValueAndTypeJsonExtensions; +use rmcp::ErrorData; +use rmcp::model::{ + AnnotateAble, CallToolResult, Content, JsonObject, RawAudioContent, RawContent, + RawEmbeddedResource, ResourceContents, +}; +use serde_json::json; +use std::sync::Arc; + +pub async fn invoke_tool( + args_map: JsonObject, + mcp_tool: &AgentMcpTool, + worker_service: &Arc, +) -> Result { + let constructor_params = + extract_constructor_input_values(&args_map, &mcp_tool.constructor.input_schema).map_err( + |e| { + tracing::error!("Failed to extract constructor parameters: {}", e); + ErrorData::invalid_params( + format!("Failed to extract constructor parameters: {}", e), + None, + ) + }, + )?; + + let parsed_agent_id = ParsedAgentId::new( + mcp_tool.agent_type_name.clone(), + DataValue::Tuple(ElementValues { + elements: constructor_params + .into_iter() + .map(ElementValue::ComponentModel) + .collect(), + }), + None, + ) + .map_err(|e| { + tracing::error!("Failed to parse agent id: {}", e); + ErrorData::invalid_params(format!("Failed to parse agent id: {}", e), None) + })?; + + let method_params_data_value = + get_agent_method_input(&args_map, &mcp_tool.raw_method.input_schema).map_err(|e| { + tracing::error!("Failed to extract method parameters: {}", e); + ErrorData::invalid_params(format!("Failed to extract method parameters: {}", e), None) + })?; + + let proto_method_parameters: golem_api_grpc::proto::golem::component::UntypedDataValue = + method_params_data_value.into(); + + let principal = Principal::anonymous(); + let proto_principal: golem_api_grpc::proto::golem::component::Principal = principal.into(); + + let agent_id = AgentId { + component_id: mcp_tool.component_id, + agent_id: parsed_agent_id.to_string(), + }; + + let auth_ctx = golem_service_base::model::auth::AuthCtx::impersonated_user(mcp_tool.account_id); + + let agent_output = worker_service + .invoke_agent( + &agent_id, + mcp_tool.raw_method.name.clone(), + proto_method_parameters, + golem_api_grpc::proto::golem::workerexecutor::v1::AgentInvocationMode::Await as i32, + None, + None, + None, + auth_ctx, + proto_principal, + ) + .await + .map_err(|e| { + tracing::error!("Failed to invoke worker: {:?}", e); + ErrorData::internal_error(format!("Failed to invoke worker: {:?}", e), None) + })?; + + let agent_result = match agent_output.result { + golem_common::model::AgentInvocationResult::AgentMethod { output } => Some(output), + _ => None, + }; + + match agent_result { + Some(untyped_data_value) => map_agent_response_to_tool_result( + untyped_data_value, + &mcp_tool.raw_method.output_schema, + ), + None => Ok(CallToolResult::success(vec![])), + } +} + +pub fn map_agent_response_to_tool_result( + agent_response: UntypedDataValue, + expected_type: &DataSchema, +) -> Result { + let typed_value = + DataValue::try_from_untyped(agent_response, expected_type.clone()).map_err(|error| { + ErrorData::internal_error(format!("Agent response type mismatch: {error}"), None) + })?; + + // Note that, according to MCP specification, the output schema for a tool must be a JsonObject, + // And as part of tool result, we simply ensure to respond according to the advertised output schema. + // This is why even for multimodal response, we convert to structured format with "parts" array. + // See `element_value_to_mcp_json` for more info. + // We deal with actual content (text or binary) when it comes to "resource" results, where it doesn't + // need to adhere to `mcp-schema` + match typed_value { + DataValue::Tuple(ElementValues { elements }) => match elements.len() { + 0 => Ok(CallToolResult::success(vec![])), + 1 => { + let element_name = match expected_type { + DataSchema::Tuple(NamedElementSchemas { elements: schemas }) => { + schemas.first().map(|s| s.name.clone()) + } + _ => None, + }; + + let element = elements.into_iter().next().unwrap(); + let too_result = convert_elem_value_to_mcp_tool_response(&element)?; + + match too_result { + ToolResult::Default(value) => { + let json_value = value; + // Wrap in an object keyed by the schema element name to match the + // advertised outputSchema (which must be type: object per MCP spec). + let structured = match element_name { + Some(name) => json!({ name: json_value }), + None => json_value, + }; + + // Both contents and structured fields are populated here (apparently) + Ok(CallToolResult::structured(structured)) + } + ToolResult::Content(content) => { + // For content results, we put the content in the "content" field of the tool result, + // and still provide the structured JSON for the rest of the schema (if any). + Ok(CallToolResult { + content: vec![content], + structured_content: None, + is_error: Some(false), + meta: None, + }) + } + } + } + _ => Err(ErrorData::internal_error( + "Unexpected number of response tuple elements".to_string(), + None, + )), + }, + + // multimodal + DataValue::Multimodal(NamedElementValues { elements }) => { + let mut contents: Vec = vec![]; + + for named in elements { + let tool_result = convert_elem_value_to_mcp_tool_response(&named.value)?; + + match tool_result { + ToolResult::Default(json_value) => { + contents.push(Content::text(json_value.to_string())); + } + + // Mostly multimodal is a collection of binary or unstructured text data + ToolResult::Content(content) => { + contents.push(content); + } + } + } + + Ok(CallToolResult { + content: contents, + structured_content: None, + is_error: Some(false), + meta: None, + }) + } + } +} + +// https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result +// Unstructured types are part of content-array, but note that it's better of sending `none` outputSchema +// when the types are unstructured. + +#[derive(Debug)] +pub enum ToolResult { + Default(serde_json::Value), + Content(Content), +} + +// Mapping from ElementValue to the JSON format expected by MCP clients +// (based on the schema they learned from initialization) +// This is used only for tools, and not for resources. +// Any changes in this mapping should be carefully tested with actual MCP clients +fn convert_elem_value_to_mcp_tool_response( + element: &ElementValue, +) -> Result { + match element { + ElementValue::ComponentModel(component_model_value) => component_model_value + .value + .to_json_value() + .map_err(|e| { + ErrorData::internal_error( + format!("Failed to serialize component model response: {e}"), + None, + ) + }) + .map(ToolResult::Default), + + ElementValue::UnstructuredText(UnstructuredTextElementValue { value, .. }) => match value { + TextReference::Inline(TextSource { data, .. }) => Ok(ToolResult::Content( + RawContent::text(data.clone()).no_annotation(), + )), + TextReference::Url(_) => Err(ErrorData::internal_error( + "A text reference URL can only be part of tool input and not output".to_string(), + None, + )), + }, + + ElementValue::UnstructuredBinary(UnstructuredBinaryElementValue { value, .. }) => { + match value { + BinaryReference::Inline(BinarySource { data, binary_type }) => { + let mime_type = binary_type.mime_type.as_str(); + + match mime_type { + "image/png" | "image/jpeg" | "image/gif" | "image/webp" => { + let b64 = base64::engine::general_purpose::STANDARD.encode(data); + + Ok(ToolResult::Content( + RawContent::image(b64, mime_type.to_string()).no_annotation(), + )) + } + + "audio/mpeg" | "audio/wav" | "audio/ogg" => { + let b64 = base64::engine::general_purpose::STANDARD.encode(data); + + Ok(ToolResult::Content( + RawContent::Audio(RawAudioContent { + data: b64, + mime_type: mime_type.to_string(), + }) + .no_annotation(), + )) + } + + "text/plain" | "text/csv" | "application/pdf" => { + let data_str = String::from_utf8_lossy(data).to_string(); + Ok(ToolResult::Content( + RawContent::Resource(RawEmbeddedResource { + meta: None, + resource: ResourceContents::TextResourceContents { + uri: "data:".to_string(), + mime_type: Some(mime_type.to_string()), + text: data_str, + meta: None, + }, + }) + .no_annotation(), + )) + } + + _ => Ok(ToolResult::Content( + RawContent::Resource(RawEmbeddedResource { + meta: None, + resource: ResourceContents::BlobResourceContents { + uri: "data:".to_string(), + mime_type: Some(mime_type.to_string()), + blob: base64::engine::general_purpose::STANDARD.encode(data), + meta: None, + }, + }) + .no_annotation(), + )), + } + } + BinaryReference::Url(_) => Err(ErrorData::internal_error( + "A binary reference URL can only be part of tool input and not output" + .to_string(), + None, + )), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use golem_common::base_model::agent::{ + BinaryDescriptor, ComponentModelElementSchema, ElementSchema, NamedElementSchema, + NamedElementSchemas, TextDescriptor, TextType, UntypedNamedElementValue, Url, + }; + use golem_wasm::Value; + use golem_wasm::analysis::{AnalysedType, TypeStr}; + use serde_json::json; + use test_r::test; + + fn str_output_schema() -> DataSchema { + DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "result".to_string(), + schema: ElementSchema::ComponentModel(ComponentModelElementSchema { + element_type: AnalysedType::Str(TypeStr), + }), + }], + }) + } + + #[test] + fn tuple_single_component_model_to_structured_json() { + let response = UntypedDataValue::Tuple(vec![UntypedElementValue::ComponentModel( + Value::String("hello".to_string()), + )]); + let result = map_agent_response_to_tool_result(response, &str_output_schema()).unwrap(); + assert_eq!(result.structured_content, Some(json!({"result": "hello"}))); + assert_eq!(result.is_error, Some(false)); + } + + #[test] + fn tuple_empty_returns_success() { + let schema = DataSchema::Tuple(NamedElementSchemas { elements: vec![] }); + let response = UntypedDataValue::Tuple(vec![]); + let result = map_agent_response_to_tool_result(response, &schema).unwrap(); + assert!(result.content.is_empty()); + assert_eq!(result.is_error, Some(false)); + } + + #[test] + fn tuple_text_element_to_data_object() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "report".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }], + }); + let response = UntypedDataValue::Tuple(vec![UntypedElementValue::UnstructuredText( + TextReferenceValue { + value: TextReference::Inline(TextSource { + data: "weather is sunny".to_string(), + text_type: Some(TextType { + language_code: "en".to_string(), + }), + }), + }, + )]); + let result = map_agent_response_to_tool_result(response, &schema).unwrap(); + + let raw_content = &result.content[0].raw; + + assert_eq!(raw_content, &RawContent::text("weather is sunny")); + } + + #[test] + fn tuple_binary_element_to_base64_object() { + let schema = DataSchema::Tuple(NamedElementSchemas { + elements: vec![NamedElementSchema { + name: "image".to_string(), + schema: ElementSchema::UnstructuredBinary(BinaryDescriptor { restrictions: None }), + }], + }); + + let response = UntypedDataValue::Tuple(vec![UntypedElementValue::UnstructuredBinary( + BinaryReferenceValue { + value: BinaryReference::Inline(BinarySource { + data: vec![1, 2, 3], + binary_type: BinaryType { + mime_type: "image/png".to_string(), + }, + }), + }, + )]); + let result = map_agent_response_to_tool_result(response, &schema).unwrap(); + + let raw_content = &result.content[0].raw; + + assert_eq!( + raw_content, + &RawContent::image("AQID", "image/png".to_string()) + ); + } + + #[test] + fn multimodal_response_to_parts_array() { + let schema = DataSchema::Multimodal(NamedElementSchemas { + elements: vec![ + NamedElementSchema { + name: "desc".to_string(), + schema: ElementSchema::UnstructuredText(TextDescriptor { restrictions: None }), + }, + NamedElementSchema { + name: "photo".to_string(), + schema: ElementSchema::UnstructuredBinary(BinaryDescriptor { + restrictions: None, + }), + }, + ], + }); + let response = UntypedDataValue::Multimodal(vec![ + UntypedNamedElementValue { + name: "desc".to_string(), + value: UntypedElementValue::UnstructuredText(TextReferenceValue { + value: TextReference::Inline(TextSource { + data: "a photo".to_string(), + text_type: None, + }), + }), + }, + UntypedNamedElementValue { + name: "photo".to_string(), + value: UntypedElementValue::UnstructuredBinary(BinaryReferenceValue { + value: BinaryReference::Inline(BinarySource { + data: vec![1, 2, 3], + binary_type: BinaryType { + mime_type: "image/png".to_string(), + }, + }), + }), + }, + ]); + let result = map_agent_response_to_tool_result(response, &schema).unwrap(); + let contents = &result.content; + + assert_eq!(contents.len(), 2); + + assert_eq!(&contents[0].raw, &RawContent::text("a photo")); + assert_eq!( + &contents[1].raw, + &RawContent::image("AQID", "image/png".to_string()) + ); + } + + #[test] + fn error_on_text_url_reference() { + let elem = ElementValue::UnstructuredText(UnstructuredTextElementValue { + value: TextReference::Url(Url { + value: "https://example.com".to_string(), + }), + descriptor: TextDescriptor { restrictions: None }, + }); + let err = convert_elem_value_to_mcp_tool_response(&elem).unwrap_err(); + assert!(err.message.contains("URL"), "got: {}", err.message); + } + + #[test] + fn error_on_binary_url_reference() { + let elem = ElementValue::UnstructuredBinary(UnstructuredBinaryElementValue { + value: BinaryReference::Url(Url { + value: "https://example.com/img.png".to_string(), + }), + descriptor: BinaryDescriptor { restrictions: None }, + }); + let err = convert_elem_value_to_mcp_tool_response(&elem).unwrap_err(); + assert!(err.message.contains("URL"), "got: {}", err.message); + } +} diff --git a/golem-worker-service/src/mcp/schema/mcp_schema.rs b/golem-worker-service/src/mcp/schema/mcp_schema.rs index 3758b95dab..70c812be9b 100644 --- a/golem-worker-service/src/mcp/schema/mcp_schema.rs +++ b/golem-worker-service/src/mcp/schema/mcp_schema.rs @@ -19,13 +19,13 @@ use golem_wasm::analysis::AnalysedType; use serde_json::{Map, Value, json}; #[derive(Default)] -pub struct McpSchema { +pub struct McpInputSchema { pub properties: Map, pub required: Vec, } -impl From for rmcp::model::JsonObject { - fn from(value: McpSchema) -> Self { +impl From for rmcp::model::JsonObject { + fn from(value: McpInputSchema) -> Self { let json_value = json!({ "type": "object", "properties": value.properties, @@ -36,8 +36,8 @@ impl From for rmcp::model::JsonObject { } } -impl McpSchema { - pub fn prepend_schema(&mut self, mut new_schema: McpSchema) { +impl McpInputSchema { + pub fn prepend_schema(&mut self, mut new_schema: McpInputSchema) { new_schema .properties .extend(std::mem::take(&mut self.properties)); @@ -49,21 +49,102 @@ impl McpSchema { *self = new_schema; } - pub fn from_named_element_schemas(schemas: &[NamedElementSchema]) -> McpSchema { - let named_types: Vec<(&str, &AnalysedType)> = schemas - .iter() - .map(|s| match &s.schema { + pub fn from_named_element_schemas(schemas: &[NamedElementSchema]) -> McpInputSchema { + let mut properties: Map = Map::new(); + let mut required = Vec::new(); + + for s in schemas { + let schema = match &s.schema { ElementSchema::ComponentModel(ComponentModelElementSchema { element_type }) => { - (s.name.as_str(), element_type) + if !matches!(element_type, AnalysedType::Option(_)) { + required.push(s.name.clone()); + } + analysed_type_to_json_schema(element_type) + } + ElementSchema::UnstructuredText(descriptor) => { + required.push(s.name.to_string()); + + let language_code_description = match &descriptor.restrictions { + Some(types) if !types.is_empty() => { + let codes: Vec<&str> = + types.iter().map(|t| t.language_code.as_str()).collect(); + format!("Language code. Must be one of: {}", codes.join(", ")) + } + _ => "Language code".to_string(), + }; + json!({ + "type": "object", + "properties": { + "data": {"type": "string", "description": "Text content"}, + "languageCode": {"type": "string", "description": language_code_description} + }, + "required": ["data"] + }) + } + ElementSchema::UnstructuredBinary(descriptor) => { + required.push(s.name.clone()); + let mime_type_description = match &descriptor.restrictions { + Some(types) if !types.is_empty() => { + let mimes: Vec<&str> = + types.iter().map(|t| t.mime_type.as_str()).collect(); + format!("MIME type. Must be one of: {}", mimes.join(", ")) + } + _ => "MIME type".to_string(), + }; + + json!({ + "type": "object", + "properties": { + "data": {"type": "string", "description": "Base64-encoded binary data"}, + "mimeType": {"type": "string", "description": mime_type_description} + }, + "required": ["data", "mimeType"] + }) } - _ => todo!("Unsupported element schema type in MCP schema mapping"), + }; + properties.insert(s.name.clone(), schema); + } + + McpInputSchema { + properties, + required, + } + } + + pub fn from_multimodal_element_schemas(schemas: &[NamedElementSchema]) -> McpInputSchema { + let one_of: Vec = schemas + .iter() + .map(|s| { + let value_schema = element_schema_to_json_schema(&s.schema); + json!({ + "type": "object", + "properties": { + "name": {"type": "string", "const": s.name}, + "value": value_schema + }, + "required": ["name", "value"], + "additionalProperties": false + }) }) .collect(); - Self::from_record_fields(&named_types) + let array_schema = json!({ + "type": "array", + "items": { + "oneOf": one_of + } + }); + + let mut properties: Map = Map::new(); + properties.insert("parts".to_string(), array_schema); + + McpInputSchema { + properties, + required: vec!["parts".to_string()], + } } - pub fn from_record_fields(fields: &[(&str, &AnalysedType)]) -> McpSchema { + pub fn from_record_fields(fields: &[(&str, &AnalysedType)]) -> McpInputSchema { let mut properties: Map = Map::new(); let mut required = Vec::new(); @@ -74,7 +155,7 @@ impl McpSchema { } } - McpSchema { + McpInputSchema { properties, required, } @@ -84,6 +165,48 @@ impl McpSchema { pub type JsonTypeDescription = Value; pub type FieldName = String; +fn element_schema_to_json_schema(schema: &ElementSchema) -> JsonTypeDescription { + match schema { + ElementSchema::ComponentModel(ComponentModelElementSchema { element_type }) => { + analysed_type_to_json_schema(element_type) + } + ElementSchema::UnstructuredText(descriptor) => { + let language_code_description = match &descriptor.restrictions { + Some(types) if !types.is_empty() => { + let codes: Vec<&str> = types.iter().map(|t| t.language_code.as_str()).collect(); + format!("Language code. Must be one of: {}", codes.join(", ")) + } + _ => "Language code".to_string(), + }; + json!({ + "type": "object", + "properties": { + "data": {"type": "string", "description": "Text content"}, + "languageCode": {"type": "string", "description": language_code_description} + }, + "required": ["data"] + }) + } + ElementSchema::UnstructuredBinary(descriptor) => { + let mime_type_description = match &descriptor.restrictions { + Some(types) if !types.is_empty() => { + let mimes: Vec<&str> = types.iter().map(|t| t.mime_type.as_str()).collect(); + format!("MIME type. Must be one of: {}", mimes.join(", ")) + } + _ => "MIME type".to_string(), + }; + json!({ + "type": "object", + "properties": { + "data": {"type": "string", "description": "Base64-encoded binary data"}, + "mimeType": {"type": "string", "description": mime_type_description} + }, + "required": ["data", "mimeType"] + }) + } + } +} + // Based on https://modelcontextprotocol.io/specification/2025-11-25/server/tools and // https://json-schema.org/draft/2020-12/json-schema-core (Example: oneOf) // while ensuring how golem-wasm treats JSON values @@ -128,7 +251,7 @@ fn analysed_type_to_json_schema(analysed_type: &AnalysedType) -> JsonTypeDescrip .map(|f| (f.name.as_str(), &f.typ)) .collect(); - let schema = McpSchema::from_record_fields(&fields); + let schema = McpInputSchema::from_record_fields(&fields); json!({ "type": "object", diff --git a/golem-worker-service/src/mcp/schema/mcp_tool_schema.rs b/golem-worker-service/src/mcp/schema/mcp_tool_schema.rs index f225d93f42..57c79f6655 100644 --- a/golem-worker-service/src/mcp/schema/mcp_tool_schema.rs +++ b/golem-worker-service/src/mcp/schema/mcp_tool_schema.rs @@ -12,21 +12,68 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::mcp::schema::mcp_schema::McpSchema; -use crate::mcp::schema::mcp_schema_mapping::get_mcp_schema; -use golem_common::base_model::agent::AgentMethod; +use crate::mcp::schema::mcp_schema::McpInputSchema; +use golem_common::base_model::agent::{AgentMethod, DataSchema}; +use golem_common::model::agent::ElementSchema; pub struct McpToolSchema { - pub input_schema: McpSchema, - pub output_schema: Option, + pub input_schema: McpInputSchema, // for unstructured input, we will still have an MCP schema + // Many clients work if output schema is optional, where it can be null whenever the result is unstructured + pub output_schema: Option, } pub fn get_mcp_tool_schema(method: &AgentMethod) -> McpToolSchema { - let input_schema = get_mcp_schema(&method.input_schema); - let output_schema = get_mcp_schema(&method.output_schema); + let input_schema = get_input_mcp_schema(&method.input_schema); + let output_schema = get_output_mcp_schema(&method.output_schema); McpToolSchema { input_schema, - output_schema: Some(output_schema), + output_schema, + } +} + +pub fn get_input_mcp_schema(data_schema: &DataSchema) -> McpInputSchema { + match data_schema { + DataSchema::Tuple(schemas) => McpInputSchema::from_named_element_schemas(&schemas.elements), + + DataSchema::Multimodal(schemas) => { + McpInputSchema::from_multimodal_element_schemas(&schemas.elements) + } + } +} + +fn get_output_mcp_schema(data_schema: &DataSchema) -> Option { + match data_schema { + DataSchema::Tuple(schemas) => { + // This in reality will be just "{result_value: T}" + if schemas.elements.len() == 1 { + // If the output schema is structured (i.e. component model), we can represent it as MCP schema, + // otherwise we will just return None and let clients handle it as unstructured output. This is also in accordance + // with the MCP protocol, and probably the main reason why protocol says it's optional + // Setting any schema for any unstructured content is either imposing bad user experience, or indeterministic results + // Also says, `OutputSchema` is for structured output here: https://modelcontextprotocol.io/specification/2025-11-25/server/tools#output-schema + // although optional + let is_structured = + matches!(schemas.elements[0].schema, ElementSchema::ComponentModel(_)); + + if is_structured { + Some(McpInputSchema::from_named_element_schemas( + &schemas.elements, + )) + } else { + None + } + } else { + None + } + } + + // This is decided after testing with several MCP clients, and the actual MCP protocol also considers mainly just `inputSchema` + // If we set `outputSchema` (similar to input schema for multimodal), clients find it difficult to render the output + // and behaves inconsistently. Example: If the output multimodal schema is represented using `rmcp::model::JsonObject` (tool output schema) instead of `None`, + // then clients prefer to not render the image (and simply emit base64), or actual decode and fail due to large size b64, and at times succeeds, + // or worse case, it can go in circles (it decodes to image, but finds the output schema to be b64 and decides to encode it again) + // https://modelcontextprotocol.io/specification/2025-11-25/server/tools#listing-tools + DataSchema::Multimodal(_) => None, } } diff --git a/golem-worker-service/src/mcp/schema/mod.rs b/golem-worker-service/src/mcp/schema/mod.rs index 1958dc90e5..571986fbd5 100644 --- a/golem-worker-service/src/mcp/schema/mod.rs +++ b/golem-worker-service/src/mcp/schema/mod.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub use mcp_schema_mapping::*; pub use mcp_tool_schema::*; -mod mcp_schema; -mod mcp_schema_mapping; +pub(crate) mod mcp_schema; mod mcp_tool_schema; diff --git a/integration-tests/tests/custom_api/mcp.rs b/integration-tests/tests/custom_api/mcp.rs new file mode 100644 index 0000000000..85efd8b9bf --- /dev/null +++ b/integration-tests/tests/custom_api/mcp.rs @@ -0,0 +1,740 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use golem_client::api::RegistryServiceClient; +use golem_common::base_model::agent::AgentTypeName; +use golem_common::base_model::domain_registration::{Domain, DomainRegistrationCreation}; +use golem_common::base_model::mcp_deployment::{McpDeploymentAgentOptions, McpDeploymentCreation}; +use golem_test_framework::config::{EnvBasedTestDependencies, TestDependencies}; +use golem_test_framework::dsl::{EnvironmentOptions, TestDsl, TestDslExtended}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use std::fmt::{Debug, Formatter}; +use std::sync::atomic::{AtomicU64, Ordering}; +use test_r::test_dep; +use test_r::{inherit_test_dep, test}; + +inherit_test_dep!(EnvBasedTestDependencies); + +const MCP_PORT: u16 = 9007; + +static REQUEST_ID: AtomicU64 = AtomicU64::new(1); + +struct McpClient { + http: reqwest::Client, + url: String, + session_id: Option, +} + +fn parse_sse_json(body: &str) -> anyhow::Result { + for line in body.lines() { + if let Some(data) = line.strip_prefix("data:") { + let data = data.trim(); + if !data.is_empty() { + return Ok(serde_json::from_str(data)?); + } + } + } + anyhow::bail!("No data line found in SSE response: {}", body) +} + +impl McpClient { + fn next_id() -> u64 { + REQUEST_ID.fetch_add(1, Ordering::SeqCst) + } + + async fn new(url: String, host: &str) -> anyhow::Result { + let http = reqwest::Client::new(); + + // Send initialize request + let init_req = json!({ + "jsonrpc": "2.0", + "id": Self::next_id(), + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "golem-mcp-integration-test", + "version": "0.0.1" + } + } + }); + + let resp = http + .post(&url) + .header("Host", host) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/event-stream") + .json(&init_req) + .send() + .await?; + + let session_id = resp + .headers() + .get("mcp-session-id") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + + // Response is SSE, parse the JSON from it + let body = resp.text().await?; + let _init_result = parse_sse_json(&body)?; + + // Send initialized notification + let notif = json!({ + "jsonrpc": "2.0", + "method": "notifications/initialized" + }); + + let mut notif_req = http + .post(&url) + .header("Host", host) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/event-stream"); + + if let Some(sid) = &session_id { + notif_req = notif_req.header("mcp-session-id", sid.as_str()); + } + + notif_req.json(¬if).send().await?; + + Ok(McpClient { + http, + url, + session_id, + }) + } + + async fn request(&self, method: &str, params: Value) -> anyhow::Result { + let req_body = json!({ + "jsonrpc": "2.0", + "id": Self::next_id(), + "method": method, + "params": params + }); + + let mut builder = self + .http + .post(&self.url) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/event-stream"); + + if let Some(sid) = &self.session_id { + builder = builder.header("mcp-session-id", sid.as_str()); + } + + let resp = builder.json(&req_body).send().await?; + let body = resp.text().await?; + let json_body = parse_sse_json(&body)?; + + if let Some(error) = json_body.get("error") { + anyhow::bail!("MCP error: {}", error); + } + + Ok(json_body["result"].clone()) + } + + async fn list_tools(&self) -> anyhow::Result> { + let result = self.request("tools/list", json!({})).await?; + Ok(result["tools"].as_array().cloned().unwrap_or_default()) + } + + async fn call_tool(&self, name: &str, arguments: Value) -> anyhow::Result { + self.request( + "tools/call", + json!({ + "name": name, + "arguments": arguments + }), + ) + .await + } + + async fn list_resources(&self) -> anyhow::Result> { + let result = self.request("resources/list", json!({})).await?; + Ok(result["resources"].as_array().cloned().unwrap_or_default()) + } + + async fn list_resource_templates(&self) -> anyhow::Result> { + let result = self.request("resources/templates/list", json!({})).await?; + Ok(result["resourceTemplates"] + .as_array() + .cloned() + .unwrap_or_default()) + } + + async fn read_resource(&self, uri: &str) -> anyhow::Result { + self.request("resources/read", json!({ "uri": uri })).await + } + + async fn list_prompts(&self) -> anyhow::Result> { + let result = self.request("prompts/list", json!({})).await?; + Ok(result["prompts"].as_array().cloned().unwrap_or_default()) + } + + async fn get_prompt(&self, name: &str) -> anyhow::Result { + self.request("prompts/get", json!({ "name": name })).await + } +} + +pub struct McpTestContext { + pub domain: Domain, +} + +impl Debug for McpTestContext { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "McpTestContext") + } +} + +impl McpTestContext { + async fn connect_mcp_client(&self) -> anyhow::Result { + let url = format!("http://127.0.0.1:{}/mcp", MCP_PORT); + McpClient::new(url, &self.domain.0).await + } +} + +#[test_dep] +async fn test_context(deps: &EnvBasedTestDependencies) -> McpTestContext { + let user = deps.user().await.unwrap().with_auto_deploy(false); + let client = deps.registry_service().client(&user.token).await; + let (_, env) = user + .app_and_env_custom(&EnvironmentOptions { + security_overrides: true, + version_check: false, + compatibility_check: false, + }) + .await + .unwrap(); + + let domain = Domain(format!("{}.golem.cloud", env.id)); + + client + .create_domain_registration( + &env.id.0, + &DomainRegistrationCreation { + domain: domain.clone(), + }, + ) + .await + .unwrap(); + + user.component(&env.id, "golem_it_mcp_release") + .name("golem-it:mcp") + .store() + .await + .unwrap(); + + let mcp_deployment_creation = McpDeploymentCreation { + domain: domain.clone(), + agents: BTreeMap::from_iter(vec![ + ( + AgentTypeName("WeatherAgent".to_string()), + McpDeploymentAgentOptions::default(), + ), + ( + AgentTypeName("WeatherAgentSingleton".to_string()), + McpDeploymentAgentOptions::default(), + ), + ( + AgentTypeName("StaticResource".to_string()), + McpDeploymentAgentOptions::default(), + ), + ( + AgentTypeName("DynamicResource".to_string()), + McpDeploymentAgentOptions::default(), + ), + ]), + }; + + client + .create_mcp_deployment(&env.id.0, &mcp_deployment_creation) + .await + .unwrap(); + + user.deploy_environment(env.id).await.unwrap(); + + McpTestContext { domain } +} + +#[test] +#[tracing::instrument] +async fn list_tools(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + let tools = client.list_tools().await?; + + let tool_names: Vec = tools + .iter() + .filter_map(|t| t["name"].as_str().map(String::from)) + .collect(); + + // WeatherAgent tools (with constructor param "name") + assert!( + tool_names.contains(&"WeatherAgent-get_weather_report_for_city".to_string()), + "Expected WeatherAgent-get_weather_report_for_city in {:?}", + tool_names + ); + assert!( + tool_names.contains(&"WeatherAgent-get_weather_report_for_city_with_images".to_string()) + ); + assert!(tool_names.contains(&"WeatherAgent-get_weather_report_for_city_text".to_string())); + assert!(tool_names.contains(&"WeatherAgent-get_snow_fall_image_for_city".to_string())); + assert!(tool_names.contains(&"WeatherAgent-get_lat_long_for_city".to_string())); + + // WeatherAgentSingleton tools (no constructor params) + assert!(tool_names.contains(&"WeatherAgentSingleton-get_weather_report_for_city".to_string())); + assert!(tool_names + .contains(&"WeatherAgentSingleton-get_weather_report_for_city_with_images".to_string())); + assert!( + tool_names.contains(&"WeatherAgentSingleton-get_weather_report_for_city_text".to_string()) + ); + assert!(tool_names.contains(&"WeatherAgentSingleton-get_snow_fall_image_for_city".to_string())); + assert!(tool_names.contains(&"WeatherAgentSingleton-get_lat_long_for_city".to_string())); + + // StaticResource and DynamicResource methods have no input params -> exposed as resources, not tools + assert!(!tool_names.iter().any(|n| n.starts_with("StaticResource"))); + assert!(!tool_names.iter().any(|n| n.starts_with("DynamicResource"))); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn call_tool_weather_agent_string(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .call_tool( + "WeatherAgent-get_weather_report_for_city", + json!({ "name": "test-agent", "city": "Sydney" }), + ) + .await?; + + assert_eq!(result["isError"], json!(false)); + assert_eq!( + result["structuredContent"]["return_value"], + json!("Agent test-agent: This is a weather report for Sydney") + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn call_tool_weather_agent_multimodal(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .call_tool( + "WeatherAgent-get_weather_report_for_city_with_images", + json!({ "name": "test-agent", "city": "Sydney" }), + ) + .await?; + + assert_eq!(result["isError"], json!(false)); + + let multimodal_array = &result["content"].as_array().unwrap(); + + assert_eq!(multimodal_array.len(), 2); + + let unstructured_text_content = &multimodal_array[0]; + + assert!(unstructured_text_content["text"] + .as_str() + .unwrap() + .contains("snow fall in Sydney")); + + let image_content = &multimodal_array[1]; + + assert_eq!(image_content["data"].as_str().unwrap(), "AQID"); + assert_eq!(image_content["mimeType"].as_str().unwrap(), "image/png"); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn call_tool_weather_agent_unstructured_text(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .call_tool( + "WeatherAgent-get_weather_report_for_city_text", + json!({ "name": "test-agent", "city": "Sydney" }), + ) + .await?; + + assert_eq!(result["isError"], json!(false)); + + let unstructured_text = &result["content"].as_array().unwrap(); + + assert!(unstructured_text[0]["text"] + .as_str() + .unwrap() + .contains("unstructured weather report for Sydney"),); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn call_tool_weather_agent_unstructured_binary(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .call_tool( + "WeatherAgent-get_snow_fall_image_for_city", + json!({ "name": "test-agent", "city": "Sydney" }), + ) + .await?; + + assert_eq!(result["isError"], json!(false)); + + let contents = &result["content"]; + + // Binary data is base64 encoded: vec![1, 2, 3] -> "AQID" + assert_eq!(contents[0]["data"], "AQID"); + assert_eq!(contents[0]["mimeType"], "image/png"); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn call_tool_weather_agent_component_model(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .call_tool( + "WeatherAgent-get_lat_long_for_city", + json!({ "name": "test-agent", "city": "Sydney" }), + ) + .await?; + + assert_eq!(result["isError"], json!(false)); + let structured = &result["structuredContent"]["return_value"]; + + assert_eq!(structured["lat"], json!(0.0)); + assert_eq!(structured["long"], json!(0.0)); + assert_eq!(structured["country"], "Unknown"); + assert_eq!(structured["population"], 0); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn call_tool_singleton_string(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .call_tool( + "WeatherAgentSingleton-get_weather_report_for_city", + json!({ "city": "Darwin" }), + ) + .await?; + + assert_eq!(result["isError"], json!(false)); + assert_eq!( + result["structuredContent"]["return_value"], + json!("This is a weather report for Darwin.") + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn call_tool_singleton_component_model(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .call_tool( + "WeatherAgentSingleton-get_lat_long_for_city", + json!({ "city": "Darwin" }), + ) + .await?; + + assert_eq!(result["isError"], json!(false)); + + let location = &result["structuredContent"]["return_value"]; + + assert_eq!(location["lat"], json!(0.0)); + assert_eq!(location["long"], json!(0.0)); + assert_eq!(location["country"], "Unknown"); + assert_eq!(location["population"], 0); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn list_resources(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + let resources = client.list_resources().await?; + + let resource_uris: Vec = resources + .iter() + .filter_map(|r| r["uri"].as_str().map(String::from)) + .collect(); + + assert!( + resource_uris.contains(&"golem://StaticResource/get_static_weather_report".to_string()), + "Expected static weather report resource in {:?}", + resource_uris + ); + assert!(resource_uris + .contains(&"golem://StaticResource/get_static_weather_report_with_images".to_string())); + assert!(resource_uris + .contains(&"golem://StaticResource/get_static_weather_report_text".to_string())); + assert!(resource_uris.contains(&"golem://StaticResource/get_static_now_fall_image".to_string())); + + assert!(!resource_uris + .iter() + .any(|u| u.starts_with("golem://DynamicResource"))); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn list_resource_templates(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + let templates = client.list_resource_templates().await?; + + let template_uris: Vec = templates + .iter() + .filter_map(|t| t["uriTemplate"].as_str().map(String::from)) + .collect(); + + assert!( + template_uris.contains(&"golem://DynamicResource/get_weather_report/{name}".to_string()), + "Expected dynamic weather report template in {:?}", + template_uris + ); + assert!(template_uris + .contains(&"golem://DynamicResource/get_weather_report_with_images/{name}".to_string())); + assert!(template_uris + .contains(&"golem://DynamicResource/get_weather_report_text/{name}".to_string())); + assert!( + template_uris.contains(&"golem://DynamicResource/get_snow_fall_image/{name}".to_string()) + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn read_static_resource_string(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .read_resource("golem://StaticResource/get_static_weather_report") + .await?; + + let contents = result["contents"].as_array().unwrap(); + assert_eq!(contents.len(), 1); + + assert_eq!(contents[0]["mimeType"].as_str(), Some("application/json")); + let text = contents[0]["text"].as_str().unwrap(); + let json_value: Value = serde_json::from_str(text)?; + assert_eq!( + json_value, + json!("Sydney: Sunny, Darwin: Rainy, Hobart: Cloudy") + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn read_static_resource_unstructured_text(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .read_resource("golem://StaticResource/get_static_weather_report_text") + .await?; + + let contents = result["contents"].as_array().unwrap(); + assert_eq!(contents.len(), 1); + + let text = contents[0]["text"].as_str().unwrap(); + assert!( + text.contains("unstructured weather report") + || text == "golem://StaticResource/get_static_weather_report_text", + "Unexpected text content: {}", + text + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn read_static_resource_binary(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .read_resource("golem://StaticResource/get_static_now_fall_image") + .await?; + + let contents = result["contents"].as_array().unwrap(); + assert_eq!(contents.len(), 1); + + assert_eq!(contents[0]["mimeType"].as_str(), Some("image/png")); + + // vec![1, 2, 3] encoded as base64 = "AQID" + assert_eq!(contents[0]["blob"].as_str(), Some("AQID")); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn read_static_resource_multimodal(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .read_resource("golem://StaticResource/get_static_weather_report_with_images") + .await?; + + let contents = result["contents"].as_array().unwrap(); + assert_eq!(contents.len(), 2); + + let text = contents[0]["text"].as_str().unwrap(); + assert!( + text.contains("snow fall in Sydney") + || text == "golem://StaticResource/get_static_weather_report_with_images", + "Unexpected text: {}", + text + ); + + assert_eq!(contents[1]["mimeType"].as_str(), Some("image/png")); + assert_eq!(contents[1]["blob"].as_str(), Some("AQID")); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn read_dynamic_resource_string(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .read_resource("golem://DynamicResource/get_weather_report/test-city") + .await?; + + let contents = result["contents"].as_array().unwrap(); + assert_eq!(contents.len(), 1); + + assert_eq!(contents[0]["mimeType"].as_str(), Some("application/json")); + let text = contents[0]["text"].as_str().unwrap(); + let json_value: Value = serde_json::from_str(text)?; + assert_eq!( + json_value, + json!("This is a dynamic weather report for test-city.") + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn read_dynamic_resource_binary(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .read_resource("golem://DynamicResource/get_snow_fall_image/test-city") + .await?; + + let contents = result["contents"].as_array().unwrap(); + assert_eq!(contents.len(), 1); + + assert_eq!(contents[0]["mimeType"].as_str(), Some("image/png")); + assert_eq!(contents[0]["blob"].as_str(), Some("AQID")); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn list_prompts(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + let prompts = client.list_prompts().await?; + + let prompt_names: Vec = prompts + .iter() + .filter_map(|p| p["name"].as_str().map(String::from)) + .collect(); + + assert!( + prompt_names.contains(&"WeatherAgent".to_string()), + "Expected WeatherAgent prompt in {:?}", + prompt_names + ); + + assert!( + prompt_names.contains(&"WeatherAgent-get_weather_report_for_city".to_string()), + "Expected WeatherAgent-get_weather_report_for_city prompt in {:?}", + prompt_names + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn get_prompt_agent_level(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client.get_prompt("WeatherAgent").await?; + + let messages = result["messages"].as_array().unwrap(); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0]["role"].as_str(), Some("user")); + + let text = messages[0]["content"]["text"].as_str().unwrap(); + assert_eq!( + text, + "You are a weather agent. Help the user get weather information for cities." + ); + + Ok(()) +} + +#[test] +#[tracing::instrument] +async fn get_prompt_method(ctx: &McpTestContext) -> anyhow::Result<()> { + let client = ctx.connect_mcp_client().await?; + + let result = client + .get_prompt("WeatherAgent-get_weather_report_for_city") + .await?; + + let messages = result["messages"].as_array().unwrap(); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0]["role"].as_str(), Some("user")); + + let text = messages[0]["content"]["text"].as_str().unwrap(); + let expected = "Get a weather report for a specific city\n\n\ + Expected JSON input properties: name, city\n\n\ + output hint: JSON"; + assert_eq!( + text, expected, + "Method prompt text mismatch.\nGot: {}\nExpected: {}", + text, expected + ); + + Ok(()) +} diff --git a/integration-tests/tests/custom_api/mod.rs b/integration-tests/tests/custom_api/mod.rs index fbfebd7c41..d1deff6a45 100644 --- a/integration-tests/tests/custom_api/mod.rs +++ b/integration-tests/tests/custom_api/mod.rs @@ -16,6 +16,7 @@ mod agent_http_principal_ts; mod agent_http_routes_rust; mod agent_http_routes_ts; mod http_test_context; +mod mcp; mod openapi_generation; use golem_test_framework::config::EnvBasedTestDependencies; diff --git a/test-components/agent-mcp/.gitignore b/test-components/agent-mcp/.gitignore new file mode 100644 index 0000000000..ed09b53992 --- /dev/null +++ b/test-components/agent-mcp/.gitignore @@ -0,0 +1,2 @@ +/golem-temp +/target diff --git a/test-components/agent-mcp/AGENTS.md b/test-components/agent-mcp/AGENTS.md new file mode 100644 index 0000000000..ecc4d68729 --- /dev/null +++ b/test-components/agent-mcp/AGENTS.md @@ -0,0 +1,428 @@ +# Golem Application Development Guide (Rust) + +## Overview + +This is a **Golem Application** — a distributed computing project targeting WebAssembly (WASM). Components are compiled to `wasm32-wasip1` and executed on the Golem platform, which provides durable execution, persistent state, and agent-to-agent communication. + +Key concepts: +- **Component**: A WASM module compiled from Rust, defining one or more agent types +- **Agent type**: A trait annotated with `#[agent_definition]`, defining the agent's API +- **Agent (worker)**: A running instance of an agent type, identified by constructor parameters, with persistent state + +## Agent Fundamentals + +- Every agent is uniquely identified by its **constructor parameter values** — two agents with the same parameters are the same agent +- Agents are **durable by default** — their state persists across invocations, failures, and restarts +- Invocations are processed **sequentially in a single thread** — no concurrency within a single agent, no need for locks +- Agents can **spawn other agents** and communicate with them via **RPC** (see Agent-to-Agent Communication) +- An agent is created implicitly on first invocation — no separate creation step needed + +## Project Structure + +``` +golem.yaml # Root application manifest +Cargo.toml # Workspace Cargo.toml +components-rust/ # Component crates (each becomes a WASM component) + / + src/lib.rs # Agent definitions and implementations + Cargo.toml # Must use crate-type = ["cdylib"] + golem.yaml # Component-level manifest (templates, env, dependencies) +common-rust/ # Shared Golem Rust templates + golem.yaml # Build templates for all Rust components +golem-temp/ # Build artifacts (gitignored) +``` + +## Prerequisites + +- Rust with `wasm32-wasip1` target: `rustup target add wasm32-wasip1` +- Golem CLI (`golem`): download from https://github.com/golemcloud/golem/releases + +## Building + +```shell +golem build # Build all components +golem component build my:comp # Build a specific component +golem build --build-profile release # Build with release profile +``` + +The build compiles Rust to WASM, generates an agent wrapper, composes them, and links dependencies. Output goes to `golem-temp/`. + +Do NOT run `cargo build` directly — always use `golem build` which orchestrates the full pipeline including WIT generation and WASM component linking. + +## Deploying and Running + +```shell +golem server run # Start local Golem server +golem deploy # Deploy all components to the configured server +golem deploy --try-update-agents # Deploy and update running agents +golem deploy --reset # Deploy and delete all previously created agents +``` + +**WARNING**: `golem server run --clean` deletes all existing state (agents, data, deployed components). Never run it without explicitly asking the user for confirmation first. + +After starting the server, components must be deployed with `golem deploy` before agents can be invoked. When iterating on code changes, use `golem deploy --reset` to delete all previously created agents — without this, existing agent instances continue running with the old component version. This is by design: Golem updates do not break existing running instances. + +To try out agents after deploying, use `golem agent invoke` for individual method calls, or write a Rib script and run it with `golem repl` for interactive testing. The Golem server must be running in a separate process before invoking or testing agents. + +## Name Mapping (Kebab-Case Convention) + +All Rust identifiers are converted to **kebab-case** when used externally (in CLI commands, Rib scripts, REPL, agent IDs, and WAVE values). This applies to: + +- **Agent type names**: `CounterAgent` → `counter-agent` +- **Method names**: `get_count` or `getCount` → `get-count` +- **Record field names**: `field_name` → `field-name` +- **Enum/variant case names**: `MyCase` → `my-case` + +This conversion is automatic and consistent across all external interfaces. + +## Testing Agents + +### Using the REPL + +```shell +golem repl # Interactive Rib scripting REPL +``` + +In the REPL, use kebab-case names and WAVE-encoded values: +```rib +let agent = counter-agent("my-counter") +agent.increment() +agent.increment() +``` + +### Using `golem agent invoke` + +Invoke agent methods directly from the CLI. The method name must be fully qualified: + +```shell +# Method name format: /.{method-name} +# All names in kebab-case + +golem agent invoke 'counter-agent("my-counter")' \ + 'my:comp/counter-agent.{increment}' + +# With arguments (WAVE-encoded) +golem agent invoke 'my-agent("id")' \ + 'my:comp/my-agent.{set-value}' '"hello world"' + +# With a record argument +golem agent invoke 'my-agent("id")' \ + 'my:comp/my-agent.{update}' '{field-name: "value", count: 42}' + +# Fire-and-forget (enqueue without waiting for result) +golem agent invoke --enqueue 'counter-agent("c1")' \ + 'my:comp/counter-agent.{increment}' + +# With idempotency key +golem agent invoke --idempotency-key 'unique-key-123' \ + 'counter-agent("c1")' 'my:comp/counter-agent.{increment}' +``` + +## WAVE Value Encoding + +All argument values passed to `golem agent invoke` and used in Rib scripts follow the [WAVE (WebAssembly Value Encoding)](https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasm-wave) format. See the full [type mapping reference](https://learn.golem.cloud/type-mapping). + +### Rust Type to WAVE Mapping + +| Rust Type | WIT Type | WAVE Example | +|-----------|----------|--------------| +| `String` | `string` | `"hello world"` | +| `bool` | `bool` | `true`, `false` | +| `u8`, `u16`, `u32`, `u64` | `u8`, `u16`, `u32`, `u64` | `42` | +| `i8`, `i16`, `i32`, `i64` | `s8`, `s16`, `s32`, `s64` | `-7` | +| `f32`, `f64` | `f32`, `f64` | `3.14`, `nan`, `inf`, `-inf` | +| `char` | `char` | `'x'`, `'\u{1F44B}'` | +| `Vec` | `list` | `[1, 2, 3]` | +| `Option` | `option` | `some("value")`, `none` | +| `Result` | `result` | `ok("value")`, `err("msg")` | +| `(T1, T2)` | `tuple` | `("hello", 42)` | +| `HashMap` | `list>` | `[("key1", 100), ("key2", 200)]` | +| Struct (with `Schema`) | `record { ... }` | `{field-name: "value", count: 42}` | +| Enum (unit variants) | `enum { ... }` | `my-variant` | +| Enum (with data) | `variant { ... }` | `my-case("data")` | + +### WAVE Encoding Rules + +**Strings**: double-quoted with escape sequences (`\"`, `\\`, `\n`, `\t`, `\r`, `\u{...}`) +``` +"hello \"world\"" +``` + +**Records**: field names in kebab-case, optional fields (`Option`) can be omitted (defaults to `none`) +``` +{required-field: "value", optional-field: some(42)} +{required-field: "value"} +``` + +**Variants/Enums**: case name in kebab-case, with optional payload in parentheses +``` +my-case +my-case("payload") +``` + +**Options**: can use shorthand (bare value = `some`) +``` +some(42) // explicit +42 // shorthand for some(42), only for non-option/non-result inner types +none +``` + +**Results**: can use shorthand (bare value = `ok`) +``` +ok("value") // explicit ok +err("oops") // explicit err +"value" // shorthand for ok("value") +``` + +**Flags**: set of labels in curly braces +``` +{read, write} +{} +``` + +**Keywords as identifiers**: prefix with `%` if a name conflicts with `true`, `false`, `some`, `none`, `ok`, `err`, `inf`, `nan` +``` +%true +%none +``` + +## Defining Agents + +Agents are defined using the `#[agent_definition]` and `#[agent_implementation]` macros from `golem-rust`: + +```rust +use golem_rust::{agent_definition, agent_implementation}; + +#[agent_definition] +pub trait MyAgent { + // Constructor parameters form the agent's identity + fn new(name: String) -> Self; + + // Agent methods — can be sync or async + fn get_count(&self) -> u32; + fn increment(&mut self) -> u32; + async fn fetch_data(&self, url: String) -> String; +} + +struct MyAgentImpl { + name: String, + count: u32, +} + +#[agent_implementation] +impl MyAgent for MyAgentImpl { + fn new(name: String) -> Self { + Self { name, count: 0 } + } + + fn get_count(&self) -> u32 { + self.count + } + + fn increment(&mut self) -> u32 { + self.count += 1; + self.count + } + + async fn fetch_data(&self, url: String) -> String { + // Use wstd::http for HTTP requests + todo!() + } +} +``` + +### Ephemeral agents + +By default agents are durable (state persists indefinitely). For stateless per-invocation agents: + +```rust +#[agent_definition(ephemeral)] +pub trait StatelessAgent { + fn new() -> Self; + fn handle(&self, input: String) -> String; +} +``` + +### Custom types + +All parameter and return types must implement the `Schema` trait. For custom types, derive it along with `IntoValue` and `FromValueAndType`: + +```rust +use golem_rust::Schema; +use serde::{Serialize, Deserialize}; + +#[derive(Clone, Schema, Serialize, Deserialize)] +pub struct MyData { + pub field1: String, + pub field2: u32, +} +``` + +Shared types can be placed in `common-rust/common-lib/` and used across components. + +### Method annotations + +```rust +use golem_rust::{agent_definition, prompt, description}; + +#[agent_definition] +pub trait MyAgent { + fn new(name: String) -> Self; + + #[prompt("Increment the counter")] + #[description("Increments the counter by 1 and returns the new value")] + fn increment(&mut self) -> u32; +} +``` + +## Agent-to-Agent Communication (RPC) + +The `#[agent_definition]` macro auto-generates a `Client` type for calling agents remotely: + +```rust +// Awaited call (blocks until result) +let other = OtherAgentClient::get("param".to_string()); +let result = other.some_method(arg).await; + +// Fire-and-forget (returns immediately) +other.trigger_some_method(arg); + +// Scheduled invocation +use golem_rust::wasm_rpc::golem_rpc_0_2_x::types::Datetime; +other.schedule_some_method(Datetime { seconds: ts, nanoseconds: 0 }, arg); + +// Phantom agents (multiple instances with same constructor params) +let phantom = OtherAgentClient::new_phantom("param".to_string()); +let id = phantom.phantom_id().unwrap(); +let same = OtherAgentClient::get_phantom(id, "param".to_string()); +``` + +Avoid RPC cycles (A calls B calls A) — use `trigger_` to break deadlocks. + +## Durability Features + +Golem provides **automatic durable execution** — all agents are durable by default without any special code. State is persisted via an oplog (operation log) and agents survive failures, restarts, and updates transparently. + +The APIs below are **advanced controls** that most agents will never need. Only use them when you have specific requirements around persistence granularity, idempotency, or transactional compensation: + +```rust +use golem_rust::{ + with_persistence_level, PersistenceLevel, + with_idempotence_mode, + atomically, + oplog_commit, + generate_idempotency_key, + with_retry_policy, RetryPolicy, +}; + +// Atomic operations — retried together on failure +let result = atomically(|| { +let a = side_effect_1(); +let b = side_effect_2(a); +(a, b) +}); + +// Control persistence level +with_persistence_level(PersistenceLevel::PersistNothing, || { +// No oplog entries — side effects replayed on recovery +}); + +// Control idempotence mode +with_idempotence_mode(false, || { +// HTTP requests won't be retried if result is uncertain +}); + +// Ensure oplog is replicated +oplog_commit(3); // Wait for 3 replicas + +// Generate a durable idempotency key (persisted, safe for payment APIs etc.) +let key = generate_idempotency_key(); +``` + +### Transactions + +For saga-pattern compensation: + +```rust +use golem_rust::{fallible_transaction, infallible_transaction, operation}; + +let op1 = operation( +|input: String| { /* execute */ Ok(result) }, +|input: String, result| { /* compensate/rollback */ Ok(()) }, +); + +// Fallible: compensates on failure, returns error +let result = fallible_transaction(|tx| { +let r = tx.execute(op1, "input".to_string())?; +Ok(r) +}); + +// Infallible: compensates and retries on failure +let result = infallible_transaction(|tx| { +tx.execute(op1, "input".to_string()); +42 +}); +``` + +## Adding New Components + +```shell +golem component new rust my:new-component +``` + +This creates a new directory under `components-rust/` with the standard structure. + +## Application Manifest (golem.yaml) + +- Root `golem.yaml`: app name, includes, witDeps, environments +- `common-rust/golem.yaml`: build templates (debug/release profiles) shared by all Rust components +- `components-rust//golem.yaml`: component-specific config (templates reference, env vars, dependencies) + +Key fields in component manifest: +- `templates`: references a template from common golem.yaml (e.g., `rust`) +- `env`: environment variables passed to agents at runtime +- `dependencies`: WASM dependencies (e.g., LLM providers from golem-ai) + +## Available Libraries + +From workspace `Cargo.toml`: +- `golem-rust` (with `export_golem_agentic` feature) — agent framework, durability, transactions +- `wstd` — WASI standard library (HTTP client via `wstd::http`, async I/O, etc.) +- `log` — logging (uses `wasi-logger` backend, logs visible via `golem agent stream`) +- `serde` / `serde_json` — serialization +- Optional: `golem-wasi-http` — advanced HTTP client alternative + +To enable AI features, add the relevant golem-ai provider crate as a dependency (e.g., `golem-ai-llm-openai`). + +## Debugging + +```shell +golem agent get '' # Check agent state +golem agent stream '' # Stream live logs +golem agent oplog '' # View operation log +golem agent revert '' --number-of-invocations 1 # Revert last invocation +golem agent invoke '' 'method' args # Invoke method directly +``` + +## Key Constraints + +- Target is `wasm32-wasip1` — no native system calls, threads, or platform-specific code +- Crate type must be `cdylib` for component crates +- All agent method parameters passed by value (no references) +- All custom types need `Schema` derive (plus `IntoValue` and `FromValueAndType`, which `Schema` implies) +- `proc-macro-enable` must be true in rust-analyzer settings (already configured in `.vscode/settings.json`) +- Do not manually edit files in `.wit/` directories — they are auto-managed by the build tooling +- `golem-temp/` and `target/` are gitignored build artifacts + +## Formatting and Linting + +```shell +cargo fmt # Format code +cargo clippy --target wasm32-wasip1 # Lint (must target wasm32-wasip1) +``` + +## Documentation + +- App manifest reference: https://learn.golem.cloud/app-manifest +- Full docs: https://learn.golem.cloud +- golem-rust SDK: https://docs.rs/golem-rust diff --git a/test-components/agent-mcp/Cargo.lock b/test-components/agent-mcp/Cargo.lock new file mode 100644 index 0000000000..9468832886 --- /dev/null +++ b/test-components/agent-mcp/Cargo.lock @@ -0,0 +1,1283 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "ctor" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[package]] +name = "dtor" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "golem-rust" +version = "0.0.0" +dependencies = [ + "async-trait", + "ctor", + "golem-rust-macro", + "golem-wasm", + "http", + "log", + "serde", + "serde_json", + "uuid", + "wasi-logger", + "wasip2", + "wit-bindgen 0.53.1", + "wstd 0.6.5", +] + +[[package]] +name = "golem-rust-macro" +version = "0.0.0" +dependencies = [ + "heck 0.5.0", + "humantime", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "golem-wasm" +version = "0.0.0" +dependencies = [ + "chrono", + "itertools", + "uuid", + "wasip2", + "wit-bindgen-rt 0.44.0", + "wstd 0.6.5", +] + +[[package]] +name = "golem_it_mcp" +version = "0.0.1" +dependencies = [ + "golem-rust", + "log", + "serde", + "serde_json", + "wstd 0.5.4", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasi" +version = "0.14.1+wasi-0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7df608df60a1c33e247881838b0f809512086e3e3bb1c18323b77eeb1f844e" +dependencies = [ + "wit-bindgen-rt 0.39.0", +] + +[[package]] +name = "wasi-logger" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aa5201b7f5d96ef2e747a1f60a6dbc38bdd1287ce5e046d1498bd7a793f74b" +dependencies = [ + "log", + "wit-bindgen 0.24.0", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd106365a7f5f7aa3c1916a98cbb3ad477f5ff96ddb130285a91c6e7429e67a" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-metadata" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094aea3cb90e09f16ee25a4c0e324b3e8c934e7fd838bfa039aef5352f44a917" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.202.0", + "wasmparser 0.202.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da55e60097e8b37b475a0fa35c3420dd71d9eb7bd66109978ab55faf56a57efb" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasmparser" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6998515d3cf3f8b980ef7c11b29a9b1017d4cf86b99ae93b546992df9931413" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indexmap", + "semver", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb4e7653763780be47e38f479e9aa83c768aa6a3b2ed086dc2826fdbbb7e7f5" +dependencies = [ + "wit-bindgen-rt 0.24.0", + "wit-bindgen-rust-macro 0.24.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro 0.51.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e915216dde3e818093168df8380a64fba25df468d626c80dd5d6a184c87e7c7" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro 0.53.1", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b67e11c950041849a10828c7600ea62a4077c01e8af72e8593253575428f91b" +dependencies = [ + "anyhow", + "wit-parser 0.202.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deda4b7e9f522d994906f6e6e0fc67965ea8660306940a776b76732be8f3933" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser 0.245.1", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0780cf7046630ed70f689a098cd8d56c5c3b22f2a7379bbdb088879963ff96" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653c85dd7aee6fe6f4bded0d242406deadae9819029ce6f7d258c920c384358a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30acbe8fb708c3a830a33c4cb705df82659bf831b492ec6ca1a17a369cfeeafb" +dependencies = [ + "anyhow", + "heck 0.4.1", + "indexmap", + "wasm-metadata 0.202.0", + "wit-bindgen-core 0.24.0", + "wit-component 0.202.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata 0.244.0", + "wit-bindgen-core 0.51.0", + "wit-component 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863a7ab3c4dfee58db196811caeb0718b88412a0aef3d1c2b02fcbae1e37c688" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata 0.245.1", + "wit-bindgen-core 0.53.1", + "wit-component 0.245.1", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b1b06eae85feaecdf9f2854f7cac124e00d5a6e5014bfb02eb1ecdeb5f265b9" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core 0.24.0", + "wit-bindgen-rust 0.24.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core 0.51.0", + "wit-bindgen-rust 0.51.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d14f3a9bfa3804bb0e9ab7f66da047f210eded6a1297ae3ba5805b384d64797f" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core 0.53.1", + "wit-bindgen-rust 0.53.1", +] + +[[package]] +name = "wit-component" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c836b1fd9932de0431c1758d8be08212071b6bba0151f7bac826dbc4312a2a9" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.202.0", + "wasm-metadata 0.202.0", + "wasmparser 0.202.0", + "wit-parser 0.202.0", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata 0.244.0", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-component" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4894f10d2d5cbc17c77e91f86a1e48e191a788da4425293b55c98b44ba3fcac9" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.245.1", + "wasm-metadata 0.245.1", + "wasmparser 0.245.1", + "wit-parser 0.245.1", +] + +[[package]] +name = "wit-parser" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744237b488352f4f27bca05a10acb79474415951c450e52ebd0da784c1df2bcc" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.202.0", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.245.1", +] + +[[package]] +name = "wstd" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f51495e1ae93476d1629b5810bd6068fdf22545a8ada7ea5929e2faed7b793" +dependencies = [ + "futures-core", + "http", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasi", + "wstd-macro 0.5.4", +] + +[[package]] +name = "wstd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f743611ee524c2416bc1513157eb3235ca24f4270d1b3ab19f93676fcff21398" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro 0.6.5", +] + +[[package]] +name = "wstd-macro" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225ac858e4405bdf164d92d070422c0b3b9b81f9b0b68836841f4d1bafc446b3" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "wstd-macro" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5db5d13d6e3f2b180b04be8ff8d5c35b37d5621d3e2d0aa85ab99adf817a780" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/test-components/agent-mcp/Cargo.toml b/test-components/agent-mcp/Cargo.toml new file mode 100644 index 0000000000..f6dd398fd4 --- /dev/null +++ b/test-components/agent-mcp/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["components-rust/*"] + +[profile.release] +opt-level = "s" +lto = true + +[workspace.dependencies] + +golem-rust = { path = "../../sdks/rust/golem-rust", features = [ + "export_golem_agentic", +] } + +# Advanced HTTP client, alternative of wstd::http +# golem-wasi-http = { version = "0.1.0", features = ["json"] } + +log = { version = "0.4.29", features = ["kv"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1.15.1", features = ["v4"] } +wstd = {version = "=0.5.4", features = ["default", "json"] } diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/Cargo.lock b/test-components/agent-mcp/components-rust/golem-it-mcp/Cargo.lock new file mode 100644 index 0000000000..7a84090f38 --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/Cargo.lock @@ -0,0 +1,1376 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "golem-it-mcp" +version = "0.0.1" +dependencies = [ + "golem-rust", + "reqwest", + "serde", + "serde_json", + "wit-bindgen-rt", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "golem-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c967eb388fb81f9b9f4df5d5b6634de803f21cd410c1bf687202794a4fbc0267" +dependencies = [ + "golem-rust-macro", + "serde", + "serde_json", + "uuid", + "wit-bindgen-rt", +] + +[[package]] +name = "golem-rust-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb87f831cfe4371427c63f5f4cabcc3bae1b66974c8fbcf22be9274fee3a7d1" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "git+https://github.com/zivergetech/reqwest?branch=update-jun-2024#1cf59c67b93aa6292961f8948b93df5bca2753b6" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", + "wit-bindgen-rt", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c7526379ace8709ee9ab9f2bb50f112d95581063a59ef3097d9c10153886c9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/Cargo.toml b/test-components/agent-mcp/components-rust/golem-it-mcp/Cargo.toml new file mode 100644 index 0000000000..6f4e73ad40 --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "golem_it_mcp" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[dependencies] +log = { workspace = true } +golem-rust = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +wstd = { workspace = true } diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/golem.yaml b/test-components/agent-mcp/components-rust/golem-it-mcp/golem.yaml new file mode 100644 index 0000000000..1e525048fd --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/golem.yaml @@ -0,0 +1,197 @@ +# Schema for IDEA: +# $schema: https://schema.golem.cloud/app/golem/1.5.0-dev.1/golem.schema.json +# Schema for vscode-yaml: +# yaml-language-server: $schema=https://schema.golem.cloud/app/golem/1.5.0-dev.1/golem.schema.json + +# Field reference: https://learn.golem.cloud/app-manifest#field-reference +# Creating HTTP APIs: https://learn.golem.cloud/invoke/making-custom-apis + +mcp: + deployments: + local: + - domain: localhost:9007 + agents: + DynamicResource: {} + StaticResource: {} + WeatherAgent: {} + WeatherAgentSingleton: {} + +components: + golem-it:mcp: + templates: rust + + # Component environment variables can reference system environment variables with minijinja syntax: + # + # env: + # ENV_VAR_1: "{{ ENV_VAR_1 }}" + # RENAMED_VAR_2: "{{ ENV_VAR_2 }}" + # COMPOSED_VAR_3: "{{ ENV_VAR_3 }}-{{ ENV_VAR_4}}" + # + env: + # LLM providers + # ------------- + + ## Common + # GOLEM_LLM_LOG: "trace" # Optional, defaults to warn + + ## Anthropic + # ANTHROPIC_API_KEY: "" + + ## OpenAI + # OPENAI_API_KEY: "" + + ## OpenRouter + # OPENROUTER_API_KEY: "" + + ## Amazon Bedrock + # AWS_ACCESS_KEY_ID: "" + # AWS_REGION: "" + # AWS_SECRET_ACCESS_KEY: "" + # AWS_SESSION_TOKEN: "" # Optional + + ## Grok + # XAI_API_KEY: "" + + ## Ollama + # GOLEM_OLLAMA_BASE_URL: "" # Optional, defaults to http://localhost:11434 + + + # Embedding providers + # ------------------- + + ## OpenAI + # OPENAI_API_KEY: "" + + ## Cohere + # COHERE_API_KEY: "" + + ## HuggingFace + # HUGGING_FACE_API_KEY: "" + + ## VoyageAI + # VOYAGEAI_API_KEY: "" + + + # Graph database providers + # ------------------------ + + ## ArangoDB + # ARANGODB_HOST: "" + # ARANGODB_PORT: "" # Optional, defaults to 8529 + # ARANGODB_USER: "" + # ARANGODB_PASSWORD: "" + # ARANGO_DATABASE: "" + + ## JanusGraph + # JANUSGRAPH_HOST: "" + # JANUSGRAPH_PORT: "" # Optional, defaults to 8182 + # JANUSGRAPH_USER: "" + # JANUSGRAPH_PASSWORD: "" + + ## Neo4j + # NEO4J_HOST: "" + # NEO4J_PORT: "" # Optional, defaults to 7687 + # NEO4J_USER: "" + # NEO4J_PASSWORD: "" + + + # Search providers + # ---------------- + + ## Common + # GOLEM_SEARCH_LOG: "trace" # Optional, defaults to warn + + ## Algolia + # ALGOLIA_APPLICATION_ID: "" + # ALGOLIA_API_KEY: "" + + ## ElasticSearch + # ELASTICSEARCH_URL: "" + # ELASTICSEARCH_USERNAME: "" + # ELASTICSEARCH_PASSWORD: "" + # ELASTICSEARCH_API_KEY: "" + + ## Meilisearch + # MEILISEARCH_BASE_URL: "" + # MEILISEARCH_API_KEY: "" + + ## OpenSearch + # OPENSEARCH_BASE_URL: "" + # OPENSEARCH_USERNAME: "" + # OPENSEARCH_PASSWORD: "" + # OPENSEARCH_API_KEY: "" + + ## Typesense + # TYPESENSE_BASE_URL: "" + # TYPESENSE_API_KEY: "" + + + # Speech-to-text providers + # ------------------------ + + ## Common + # STT_PROVIDER_LOG_LEVEL: "trace" # Optional, defaults to warn + # STT_PROVIDER_MAX_RETRIES: "10" # Optional, defaults to 10 + + ## AWS + # AWS_REGION: "" + # AWS_ACCESS_KEY: "" + # AWS_SECRET_KEY: "" + # AWS_BUCKET_NAME: "" + + ## Azure + # AZURE_REGION: "" + # AZURE_SUBSCRIPTION_KEY: "" + + ## Deepgram + # DEEPGRAM_API_TOKEN: "" + # DEEPGRAM_ENDPOINT: "" # Optional + + ## Google + # GOOGLE_LOCATION: "" + # GOOGLE_BUCKET_NAME: "" + # GOOGLE_APPLICATION_CREDENTIALS: "" # or use the vars below + # GOOGLE_PROJECT_ID: "" + # GOOGLE_CLIENT_EMAIL: "" + # GOOGLE_PRIVATE_KEY: "" + + ## Whisper + # OPENAI_API_KEY: "" + + + # Video generation providers + # -------------------------- + + ## Kling + # KLING_ACCESS_KEY: "" + # KLING_SECRET_KEY: "" + + ## Runway + # RUNWAY_API_KEY: "" + + ## Stability + # STABILITY_API_KEY: "" + + ## Veo + # VEO_PROJECT_ID: "" + # VEO_CLIENT_EMAIL: "" + # VEO_PRIVATE_KEY: "" + + + # WebSearch providers + # ------------------- + + ## Brave + # BRAVE_API_KEY: "" + + ## Google + # GOOGLE_API_KEY: "" + # GOOGLE_SEARCH_ENGINE_ID: "" + + ## Serper + # SERPER_API_KEY: "" + + ## Tavily + # TAVILY_API_KEY: "" + + diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/src/dynamic_resource.rs b/test-components/agent-mcp/components-rust/golem-it-mcp/src/dynamic_resource.rs new file mode 100644 index 0000000000..c32b45531a --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/src/dynamic_resource.rs @@ -0,0 +1,50 @@ +use golem_rust::{agent_definition, agent_implementation}; +use golem_rust::agentic::{BasicModality, Multimodal, UnstructuredBinary, UnstructuredText}; + +// Agent methods that don't take any arguments becomes a resource +// However they are dynamic because it depends on agent identity (constructor) +// and therefore becomes "resource templates" according to MCP + +#[agent_definition] +pub trait DynamicResource { + // The resource depends on the agent identity + fn new(name: String) -> Self; + + fn get_weather_report(&self) -> String; + fn get_weather_report_with_images(&self) -> Multimodal; + fn get_weather_report_text(&self) -> UnstructuredText; + fn get_snow_fall_image(&self) -> UnstructuredBinary; +} + +pub struct MyDynamicResourceImpl { + name: String +} + +#[agent_implementation] +impl DynamicResource for MyDynamicResourceImpl { + fn new(name: String) -> Self { + Self { name } + } + + fn get_weather_report(&self) -> String { + format!("This is a dynamic weather report for {}.", self.name) + } + + fn get_weather_report_with_images(&self) -> Multimodal { + Multimodal::new([ + BasicModality::text(format!("This is an image of the snow fall in {}.", self.name)), + BasicModality::binary(vec![1, 2, 3], "image/png") + ]) + } + + fn get_weather_report_text(&self) -> UnstructuredText { + UnstructuredText::from_inline_any(format!("This is an unstructured weather report for {}.", self.name)) + } + + fn get_snow_fall_image(&self) -> UnstructuredBinary { + UnstructuredBinary::from_inline( + vec![1, 2, 3], + "image/png".to_string(), + ) + } +} \ No newline at end of file diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/src/lib.rs b/test-components/agent-mcp/components-rust/golem-it-mcp/src/lib.rs new file mode 100644 index 0000000000..ea9ba738f0 --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/src/lib.rs @@ -0,0 +1,5 @@ +mod static_resource; +mod dynamic_resource; +mod weather_agent_singleton; +mod weather_agent; +mod location_details; \ No newline at end of file diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/src/location_details.rs b/test-components/agent-mcp/components-rust/golem-it-mcp/src/location_details.rs new file mode 100644 index 0000000000..aa988cb382 --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/src/location_details.rs @@ -0,0 +1,9 @@ +use golem_rust::Schema; + +#[derive(Schema)] +pub struct LocationDetails { + pub lat: f64, + pub long: f64, + pub country: String, + pub population: u64 +} \ No newline at end of file diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/src/static_resource.rs b/test-components/agent-mcp/components-rust/golem-it-mcp/src/static_resource.rs new file mode 100644 index 0000000000..f944da80b6 --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/src/static_resource.rs @@ -0,0 +1,57 @@ +use golem_rust::{agent_definition, agent_implementation}; +use golem_rust::agentic::{BasicModality, Multimodal, UnstructuredBinary, UnstructuredText}; + +// Agent methods that don't take any arguments becomes a resource +// However they are static because they don't depend on the agent identity, +// and therefore becomes "static resources" according to MCP +#[agent_definition] +pub trait StaticResource { + // The resource is static, it doesn't depend on the agent identity + fn new() -> Self; + fn get_static_weather_report(&self) -> String; + fn get_static_weather_report_with_images(&self) -> Multimodal; + fn get_static_weather_report_text(&self) -> UnstructuredText; + fn get_static_now_fall_image(&self) -> UnstructuredBinary; +} + +struct MyStaticResourceImpl; + +#[agent_implementation] +impl StaticResource for MyStaticResourceImpl { + fn new() -> Self { + Self + } + + fn get_static_weather_report(&self) -> String { + let weather_reports = vec![ + ("Sydney", "Sunny"), + ("Darwin", "Rainy"), + ("Hobart", "Cloudy"), + ]; + + weather_reports + .iter() + .map(|(country, weather)| format!("{}: {}", country, weather)) + .collect::>() + .join(", ") + + } + + fn get_static_weather_report_with_images(&self) -> Multimodal { + Multimodal::new([ + BasicModality::text("This is an image of the snow fall in Sydney.".to_string()), + BasicModality::binary(vec![1, 2, 3], "image/png") + ]) + } + + fn get_static_weather_report_text(&self) -> UnstructuredText { + UnstructuredText::from_inline_any("This is an unstructured weather report.".to_string()) + } + + fn get_static_now_fall_image(&self) -> UnstructuredBinary { + UnstructuredBinary::from_inline( + vec![1, 2, 3], + "image/png".to_string(), + ) + } +} \ No newline at end of file diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/src/weather_agent.rs b/test-components/agent-mcp/components-rust/golem-it-mcp/src/weather_agent.rs new file mode 100644 index 0000000000..358c49786d --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/src/weather_agent.rs @@ -0,0 +1,61 @@ +use golem_rust::{agent_definition, agent_implementation, prompt}; +use golem_rust::agentic::{BasicModality, Multimodal, UnstructuredBinary, UnstructuredText}; +use crate::location_details::LocationDetails; + +// These agent methods are tools, since they take arguments, but with constructor params +#[agent_definition] +trait WeatherAgent { + #[prompt("You are a weather agent. Help the user get weather information for cities.")] + fn new(name: String) -> Self; + + #[prompt("Get a weather report for a specific city")] + fn get_weather_report_for_city(&self, city: String) -> String; + fn get_weather_report_for_city_with_images(&self, city: String) -> Multimodal; + fn get_weather_report_for_city_text(&self, city: String) -> UnstructuredText; + fn get_snow_fall_image_for_city(&self, city: String) -> UnstructuredBinary; + fn get_lat_long_for_city(&self, city: String) -> LocationDetails; +} + + +struct MyDynamicWeatherToolImpl { + name: String +} + +#[agent_implementation] +impl WeatherAgent for MyDynamicWeatherToolImpl { + fn new(name: String) -> Self { + Self { name } + } + + fn get_weather_report_for_city(&self, city: String) -> String { + format!("Agent {}: This is a weather report for {}", self.name, city) + } + + fn get_weather_report_for_city_with_images(&self, city: String) -> Multimodal { + Multimodal::new([ + BasicModality::text(format!("Agent: {}, This is an image of the snow fall in {}.", self.name, city)), + BasicModality::binary(vec![1, 2, 3], "image/png") + ]) + } + + fn get_weather_report_for_city_text(&self, city: String) -> UnstructuredText { + UnstructuredText::from_inline_any(format!("Agent: {}, This is an unstructured weather report for {}.", self.name, city)) + } + + fn get_snow_fall_image_for_city(&self, _city: String) -> UnstructuredBinary { + UnstructuredBinary::from_inline( + vec![1, 2, 3], + "image/png".to_string(), + ) + } + + fn get_lat_long_for_city(&self, _city: String) -> LocationDetails { + // For simplicity, we return dummy lat/long values + LocationDetails { + lat: 0.0, + long: 0.0, + country: "Unknown".to_string(), + population: 0 + } + } +} \ No newline at end of file diff --git a/test-components/agent-mcp/components-rust/golem-it-mcp/src/weather_agent_singleton.rs b/test-components/agent-mcp/components-rust/golem-it-mcp/src/weather_agent_singleton.rs new file mode 100644 index 0000000000..4543676552 --- /dev/null +++ b/test-components/agent-mcp/components-rust/golem-it-mcp/src/weather_agent_singleton.rs @@ -0,0 +1,57 @@ +use golem_rust::{agent_definition, agent_implementation}; +use golem_rust::agentic::{BasicModality, Multimodal, UnstructuredBinary, UnstructuredText}; +use crate::location_details::LocationDetails; + +// These agent methods are tools, since they take arguments, however +// no constructor params +#[agent_definition] +trait WeatherAgentSingleton { + fn new() -> Self; + + fn get_weather_report_for_city(&self, city: String) -> String; + fn get_weather_report_for_city_with_images(&self, city: String) -> Multimodal; + fn get_weather_report_for_city_text(&self, city: String) -> UnstructuredText; + fn get_snow_fall_image_for_city(&self, city: String) -> UnstructuredBinary; + fn get_lat_long_for_city(&self, city: String) -> LocationDetails; +} + +struct MyStaticWeatherToolImpl; + +#[agent_implementation] +impl WeatherAgentSingleton for MyStaticWeatherToolImpl { + fn new() -> Self { + Self + } + + fn get_weather_report_for_city(&self, city: String) -> String { + format!("This is a weather report for {}.", city) + } + + fn get_weather_report_for_city_with_images(&self, city: String) -> Multimodal { + Multimodal::new([ + BasicModality::text(format!("This is an image of the snow fall in {}.", city)), + BasicModality::binary(vec![1, 2, 3], "image/png") + ]) + } + + fn get_weather_report_for_city_text(&self, city: String) -> UnstructuredText { + UnstructuredText::from_inline_any(format!("This is an unstructured weather report for {}.", city)) + } + + fn get_snow_fall_image_for_city(&self, _city: String) -> UnstructuredBinary { + UnstructuredBinary::from_inline( + vec![1, 2, 3], + "image/png".to_string(), + ) + } + + fn get_lat_long_for_city(&self, _city: String) -> LocationDetails { + // For simplicity, we return dummy lat/long values + LocationDetails { + lat: 0.0, + long: 0.0, + country: "Unknown".to_string(), + population: 0 + } + } +} \ No newline at end of file diff --git a/test-components/agent-mcp/golem.yaml b/test-components/agent-mcp/golem.yaml new file mode 100644 index 0000000000..25608fff37 --- /dev/null +++ b/test-components/agent-mcp/golem.yaml @@ -0,0 +1,19 @@ +# Schema for IDEA: +# $schema: https://schema.golem.cloud/app/golem/1.5.0-dev.1/golem.schema.json +# Schema for vscode-yaml: +# yaml-language-server: $schema=https://schema.golem.cloud/app/golem/1.5.0-dev.1/golem.schema.json + +# Field reference: https://learn.golem.cloud/app-manifest#field-reference +# Creating HTTP APIs: https://learn.golem.cloud/invoke/making-custom-apis + +app: agent-mcp + +includes: +- components-*/*/golem.yaml +environments: + local: + server: local + componentPresets: debug + cloud: + server: cloud + componentPresets: release \ No newline at end of file diff --git a/test-components/build-components.sh b/test-components/build-components.sh index bb4dfe5b50..3c1a775a1c 100755 --- a/test-components/build-components.sh +++ b/test-components/build-components.sh @@ -3,7 +3,7 @@ set -euo pipefail IFS=$'\n\t' rust_test_components=("oplog-processor") -rust_test_apps=("host-api-tests" "http-tests" "initial-file-system" "agent-counters" "agent-updates-v1" "agent-updates-v2" "agent-updates-v3" "agent-updates-v4" "scalability" "agent-sdk-rust" "agent-invocation-context" "agent-rpc") +rust_test_apps=("host-api-tests" "http-tests" "initial-file-system" "agent-counters" "agent-updates-v1" "agent-updates-v2" "agent-updates-v3" "agent-updates-v4" "scalability" "agent-sdk-rust" "agent-invocation-context" "agent-rpc" "agent-mcp") ts_test_apps=("agent-constructor-parameter-echo" "agent-promise" "agent-sdk-ts") benchmark_apps=("benchmarks") diff --git a/test-components/golem_it_mcp_release.wasm b/test-components/golem_it_mcp_release.wasm new file mode 100644 index 0000000000..8958beefc1 Binary files /dev/null and b/test-components/golem_it_mcp_release.wasm differ