Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ef9f0a6
support mcp resources
afsalthaj Mar 3, 2026
cc5cac4
support mcp resources
afsalthaj Mar 3, 2026
5e2a05e
rename invoke functions in mcp
afsalthaj Mar 3, 2026
abb8cb6
rename invoke functions in mcp
afsalthaj Mar 3, 2026
ba179c2
Merge branch 'main' into resource_support
afsalthaj Mar 5, 2026
4ccd40e
clean up
afsalthaj Mar 5, 2026
494a58a
reformat
afsalthaj Mar 5, 2026
6a3a3c2
make tools consistent with resources
afsalthaj Mar 5, 2026
a669faa
handle unstructured binary
afsalthaj Mar 5, 2026
271422a
handle unstructured text
afsalthaj Mar 5, 2026
39b9fc3
remove the need of generic
afsalthaj Mar 5, 2026
c9316f0
rename
afsalthaj Mar 5, 2026
77ef3a6
update multimodal
afsalthaj Mar 5, 2026
c67ed8e
fix multimodal
afsalthaj Mar 5, 2026
d9beea7
reformat
afsalthaj Mar 9, 2026
7957bc2
fix multimodal invokes
afsalthaj Mar 10, 2026
3fdf139
fix bugs
afsalthaj Mar 10, 2026
2fb66bb
fix more bugs
afsalthaj Mar 10, 2026
c0a863e
fix more bugs
afsalthaj Mar 10, 2026
87fa89c
add integration tests
afsalthaj Mar 11, 2026
753a971
license docs
afsalthaj Mar 11, 2026
9dbfb44
fix some more bugs related to tool results
afsalthaj Mar 11, 2026
d5489db
fix a lot of more bugs after more testing with different MCP clients
afsalthaj Mar 11, 2026
61a2d51
reformat code
afsalthaj Mar 11, 2026
26c7fe1
fix multimodal invokes
afsalthaj Mar 11, 2026
ab15503
fix integration tests
afsalthaj Mar 11, 2026
fe1c31a
Merge branch 'main' into resource_support
afsalthaj Mar 11, 2026
5519dfa
resolve conflicts
afsalthaj Mar 11, 2026
92e28f2
resolve conflicts
afsalthaj Mar 11, 2026
c16dedc
fix integration tests
afsalthaj Mar 11, 2026
f36e506
make sure all tests pass
afsalthaj Mar 11, 2026
d3b3a80
add tests for invoke modules
afsalthaj Mar 11, 2026
20bc4d6
fix test component and update build scripts
afsalthaj Mar 11, 2026
d4e7159
Merge branch 'main' into resource_support
afsalthaj Mar 12, 2026
6f6be23
Merge branch 'main' into resource_support
afsalthaj Mar 12, 2026
18ad958
Add prompts in MCP (#2961)
afsalthaj Mar 13, 2026
2dbc155
Merge branch 'main' into resource_support
afsalthaj Mar 14, 2026
df3ab6b
Make sure unstructured-binary and multimodal are rendered correctly w…
afsalthaj Mar 14, 2026
d27bde5
Fix unit tests (#2989)
afsalthaj Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 34 additions & 4 deletions golem-common/src/base_model/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this changed to manual implementation? Revert if possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automatic didn't work for functions that returned multimodal if I remember correctly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just re-tested and yes I need this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explain "did not work". We can fix the deriver.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, WIT defines multimodal as list<tuple<string, element-value>>, and the actual runtime value is Value::Tuple. Not Value::Record.

What's in here is UntypedNamedElementValue {name, value} and the derive macro for FromVlaue will produce something that expects Value::Record.

So, ended up having Expected Record value with 2 fields, got Tuple([String("Text"), Variant { ... }]).

I think, its not a bug in the deriver to fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing is untyped-named-element-value is used only in multimodal..No other wit types ends up being untyped-named-element-value.. so may be a rename will solve it, but i don't want to do that.

Also note that this is nothing related to MCP. Invoking a function that returns multimodal didn't work at all

pub struct UntypedNamedElementValue {
pub name: String,
pub value: UntypedElementValue,
}

#[cfg(feature = "full")]
impl golem_wasm::FromValue for UntypedNamedElementValue {
fn from_value(value: Value) -> Result<Self, String> {
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",
Expand Down
1 change: 1 addition & 0 deletions golem-worker-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
180 changes: 122 additions & 58 deletions golem-worker-service/src/mcp/agent_mcp_capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,93 +12,157 @@
// 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<AgentMcpTool>),
#[allow(unused)]
Resource(AgentMcpResource),
Resource(Box<AgentMcpResource>),
}

impl McpAgentCapability {
pub fn from(
pub fn from_agent_method(
account_id: &AccountId,
environment_id: &EnvironmentId,
agent_type_name: &AgentTypeName,
method: &AgentMethod,
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(),
}))
}
}
}

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<String> {
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,
}
}
Loading
Loading