Skip to content

Commit 489e651

Browse files
authored
Support MCP resources, multimodal, prompts, unstructured-* with integration tests and other fixes
1 parent 6c8ec11 commit 489e651

36 files changed

+7335
-419
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

golem-common/src/base_model/agent.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -547,15 +547,45 @@ pub enum UntypedDataValue {
547547
}
548548

549549
#[derive(Debug, Clone, PartialEq)]
550-
#[cfg_attr(
551-
feature = "full",
552-
derive(IntoValue, FromValue, desert_rust::BinaryCodec)
553-
)]
550+
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
554551
pub struct UntypedNamedElementValue {
555552
pub name: String,
556553
pub value: UntypedElementValue,
557554
}
558555

556+
#[cfg(feature = "full")]
557+
impl golem_wasm::FromValue for UntypedNamedElementValue {
558+
fn from_value(value: Value) -> Result<Self, String> {
559+
match value {
560+
Value::Tuple(fields) if fields.len() == 2 => {
561+
let mut iter = fields.into_iter();
562+
let name = String::from_value(iter.next().unwrap())?;
563+
let value = UntypedElementValue::from_value(iter.next().unwrap())?;
564+
Ok(UntypedNamedElementValue { name, value })
565+
}
566+
_ => Err(format!(
567+
"Expected Tuple with 2 fields for UntypedNamedElementValue, got {:?}",
568+
value
569+
)),
570+
}
571+
}
572+
}
573+
574+
#[cfg(feature = "full")]
575+
impl golem_wasm::IntoValue for UntypedNamedElementValue {
576+
fn into_value(self) -> Value {
577+
Value::Tuple(vec![self.name.into_value(), self.value.into_value()])
578+
}
579+
580+
fn get_type() -> AnalysedType {
581+
AnalysedType::Tuple(golem_wasm::analysis::TypeTuple {
582+
name: None,
583+
owner: None,
584+
items: vec![String::get_type(), UntypedElementValue::get_type()],
585+
})
586+
}
587+
}
588+
559589
#[derive(Debug, Clone, PartialEq)]
560590
#[cfg_attr(
561591
feature = "full",

golem-worker-service/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ golem-wasm = { workspace = true, default-features = true }
3838
golem-wasm-derive = { workspace = true }
3939

4040
anyhow = { workspace = true }
41+
base64 = { workspace = true }
4142
async-trait = { workspace = true }
4243
bigdecimal = { workspace = true }
4344
bytes = { workspace = true }

golem-worker-service/src/mcp/agent_mcp_capability.rs

Lines changed: 122 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,93 +12,157 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use crate::mcp::agent_mcp_resource::AgentMcpResource;
15+
use crate::mcp::agent_mcp_resource::{AgentMcpResource, AgentMcpResourceKind};
1616
use crate::mcp::agent_mcp_tool::AgentMcpTool;
17-
use crate::mcp::schema::{McpToolSchema, get_mcp_schema, get_mcp_tool_schema};
17+
use crate::mcp::schema::{McpToolSchema, get_input_mcp_schema, get_mcp_tool_schema};
1818
use golem_common::base_model::account::AccountId;
19-
use golem_common::base_model::agent::{AgentMethod, AgentTypeName, DataSchema};
19+
use golem_common::base_model::agent::{
20+
AgentMethod, AgentTypeName, DataSchema, ElementSchema, NamedElementSchemas,
21+
};
2022
use golem_common::base_model::component::ComponentId;
2123
use golem_common::base_model::environment::EnvironmentId;
2224
use golem_common::model::agent::AgentConstructor;
23-
use rmcp::model::Tool;
25+
use rmcp::model::{Annotated, RawResource, RawResourceTemplate, Tool};
2426
use std::borrow::Cow;
2527
use std::sync::Arc;
2628

2729
#[derive(Clone)]
2830
pub enum McpAgentCapability {
2931
Tool(Box<AgentMcpTool>),
30-
#[allow(unused)]
31-
Resource(AgentMcpResource),
32+
Resource(Box<AgentMcpResource>),
3233
}
3334

3435
impl McpAgentCapability {
35-
pub fn from(
36+
pub fn from_agent_method(
3637
account_id: &AccountId,
3738
environment_id: &EnvironmentId,
3839
agent_type_name: &AgentTypeName,
3940
method: &AgentMethod,
4041
constructor: &AgentConstructor,
4142
component_id: ComponentId,
4243
) -> Self {
43-
match &method.input_schema {
44-
DataSchema::Tuple(schemas) => {
45-
if !schemas.elements.is_empty() {
46-
tracing::debug!(
47-
"Method {} of agent type {} has input parameters, exposing as tool",
48-
method.name,
49-
agent_type_name.0
50-
);
51-
52-
let constructor_schema = get_mcp_schema(&constructor.input_schema);
53-
54-
let McpToolSchema {
55-
mut input_schema,
56-
output_schema,
57-
} = get_mcp_tool_schema(method);
58-
59-
input_schema.prepend_schema(constructor_schema);
60-
61-
let tool = Tool {
62-
name: Cow::from(get_tool_name(agent_type_name, method)),
44+
let schemas = match &method.input_schema {
45+
DataSchema::Tuple(schemas) | DataSchema::Multimodal(schemas) => schemas,
46+
};
47+
48+
if !schemas.elements.is_empty() {
49+
tracing::debug!(
50+
"Method {} of agent type {} has input parameters, exposing as tool",
51+
method.name,
52+
agent_type_name.0
53+
);
54+
55+
let constructor_schema = get_input_mcp_schema(&constructor.input_schema);
56+
57+
let McpToolSchema {
58+
mut input_schema,
59+
output_schema,
60+
} = get_mcp_tool_schema(method);
61+
62+
input_schema.prepend_schema(constructor_schema);
63+
64+
let tool = Tool {
65+
name: Cow::from(get_tool_name(agent_type_name, method)),
66+
title: None,
67+
description: Some(method.description.clone().into()),
68+
input_schema: Arc::new(rmcp::model::JsonObject::from(input_schema)),
69+
output_schema: output_schema
70+
.map(|internal| Arc::new(rmcp::model::JsonObject::from(internal))),
71+
annotations: None,
72+
execution: None,
73+
icons: None,
74+
meta: None,
75+
};
76+
77+
Self::Tool(Box::new(AgentMcpTool {
78+
environment_id: *environment_id,
79+
account_id: *account_id,
80+
constructor: constructor.clone(),
81+
raw_method: method.clone(),
82+
tool,
83+
component_id,
84+
agent_type_name: agent_type_name.clone(),
85+
}))
86+
} else {
87+
tracing::debug!(
88+
"Method {} of agent type {} has no input parameters, exposing as resource",
89+
method.name,
90+
agent_type_name.0
91+
);
92+
93+
let constructor_param_names = AgentMcpResource::constructor_param_names(constructor);
94+
let name = AgentMcpResource::resource_name(agent_type_name, method);
95+
96+
let mime_type = output_resource_mime_type(&method.output_schema);
97+
98+
let kind = if constructor_param_names.is_empty() {
99+
let uri = AgentMcpResource::static_uri(agent_type_name, method);
100+
AgentMcpResourceKind::Static(Annotated::new(
101+
RawResource {
102+
uri,
103+
name,
63104
title: None,
64-
description: Some(method.description.clone().into()),
65-
input_schema: Arc::new(rmcp::model::JsonObject::from(input_schema)),
66-
output_schema: output_schema
67-
.map(|internal| Arc::new(rmcp::model::JsonObject::from(internal))),
68-
annotations: None,
69-
execution: None,
105+
description: Some(method.description.clone()),
106+
mime_type,
107+
size: None,
70108
icons: None,
71109
meta: None,
72-
};
73-
74-
Self::Tool(Box::new(AgentMcpTool {
75-
environment_id: *environment_id,
76-
account_id: *account_id,
77-
constructor: constructor.clone(),
78-
raw_method: method.clone(),
79-
tool,
80-
component_id,
81-
agent_type_name: agent_type_name.clone(),
82-
}))
83-
} else {
84-
tracing::debug!(
85-
"Method {} of agent type {} has no input parameters, exposing as resource",
86-
method.name,
87-
agent_type_name.0
88-
);
89-
90-
Self::Resource(AgentMcpResource {
91-
resource: method.clone(),
92-
})
110+
},
111+
None,
112+
))
113+
} else {
114+
let uri_template = AgentMcpResource::template_uri(
115+
agent_type_name,
116+
method,
117+
&constructor_param_names,
118+
);
119+
AgentMcpResourceKind::Template {
120+
template: Annotated::new(
121+
RawResourceTemplate {
122+
uri_template,
123+
name,
124+
title: None,
125+
description: Some(method.description.clone()),
126+
mime_type,
127+
icons: None,
128+
},
129+
None,
130+
),
131+
constructor_param_names,
93132
}
94-
}
95-
DataSchema::Multimodal(_) => {
96-
todo!("Multimodal schema handling not implemented yet")
97-
}
133+
};
134+
135+
Self::Resource(Box::new(AgentMcpResource {
136+
kind,
137+
environment_id: *environment_id,
138+
account_id: *account_id,
139+
constructor: constructor.clone(),
140+
raw_method: method.clone(),
141+
component_id,
142+
agent_type_name: agent_type_name.clone(),
143+
}))
98144
}
99145
}
100146
}
101147

102148
fn get_tool_name(agent_type_name: &AgentTypeName, method: &AgentMethod) -> String {
103149
format!("{}-{}", agent_type_name.0, method.name)
104150
}
151+
152+
fn output_resource_mime_type(output_schema: &DataSchema) -> Option<String> {
153+
match output_schema {
154+
DataSchema::Tuple(NamedElementSchemas { elements }) => match elements.as_slice() {
155+
[single] => match &single.schema {
156+
ElementSchema::ComponentModel(_) => Some("application/json".to_string()),
157+
ElementSchema::UnstructuredText(_) => Some("text/plain".to_string()),
158+
// The actual mime type
159+
ElementSchema::UnstructuredBinary(_) => None,
160+
},
161+
_ => None,
162+
},
163+
164+
// Each individual resource contents could have its own mime type, so we can't assign a single mime type to the whole output
165+
// when it comes to multimodal output schemas.
166+
DataSchema::Multimodal(_) => None,
167+
}
168+
}

0 commit comments

Comments
 (0)