Skip to content

Commit 0842037

Browse files
Apps: Simplify meta properties due to openai now supporting mcp apps standard (#644)
* Apps: Simplify meta properties due to openai now supporting mcp apps standard * Typo in variable name, use to_string() instead of into()
1 parent a65567d commit 0842037

File tree

3 files changed

+126
-326
lines changed

3 files changed

+126
-326
lines changed

crates/apollo-mcp-server/src/apps/resource.rs

Lines changed: 46 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,10 @@ use crate::apps::app::{AppResource, AppResourceSource, AppTarget};
77

88
use super::App;
99

10-
pub(crate) fn get_mime_type(app_target: &AppTarget) -> String {
11-
match app_target {
12-
AppTarget::AppsSDK => "text/html+skybridge".to_string(),
13-
AppTarget::MCPApps => "text/html;profile=mcp-app".to_string(),
14-
}
15-
}
10+
const MCP_MIME_TYPE: &str = "text/html;profile=mcp-app";
1611

17-
// Attach resource mime type when requested to allow swapping between app targets (Apps SDK, MCP Apps)
18-
pub(crate) fn attach_resource_mime_type(
19-
mut resource: Resource,
20-
app_target: &AppTarget,
21-
) -> Resource {
22-
resource.raw.mime_type = Some(get_mime_type(app_target));
12+
pub(crate) fn attach_resource_mime_type(mut resource: Resource) -> Resource {
13+
resource.raw.mime_type = Some(MCP_MIME_TYPE.to_string());
2314
resource
2415
}
2516

@@ -88,34 +79,32 @@ pub(crate) async fn get_app_resource(
8879
}
8980
};
9081

82+
// Most properties now are listed under _meta.ui.* but some openai specific properties are still at the root
83+
// So, we will populate both and then nest "ui" into "meta" later in this function
9184
let mut meta: Option<Meta> = None;
85+
let mut ui: Option<Meta> = None;
9286
if let Some(csp) = &app.csp_settings {
93-
match app_target {
94-
// Note that the difference in which keys are set here and the camelCase vs snake_key is on purpose. These are differences between the two specs.
95-
AppTarget::AppsSDK => {
96-
meta.get_or_insert_with(Meta::new).insert(
97-
"openai/widgetCSP".into(),
98-
json!({
99-
"connect_domains": csp.connect_domains,
100-
"resource_domains": csp.resource_domains,
101-
"frame_domains": csp.frame_domains,
102-
"redirect_domains": csp.redirect_domains
103-
}),
104-
);
105-
}
106-
AppTarget::MCPApps => {
107-
meta.get_or_insert_with(Meta::new).insert(
108-
"csp".into(),
109-
json!({
110-
"connectDomains": csp.connect_domains,
111-
"resourceDomains": csp.resource_domains,
112-
"frameDomains": csp.frame_domains,
113-
"baseUriDomains": csp.base_uri_domains
114-
}),
115-
);
116-
}
87+
ui.get_or_insert_with(Meta::new).insert(
88+
"csp".into(),
89+
json!({
90+
"connectDomains": csp.connect_domains,
91+
"resourceDomains": csp.resource_domains,
92+
"frameDomains": csp.frame_domains,
93+
"baseUriDomains": csp.base_uri_domains
94+
}),
95+
);
96+
97+
// Openai has a specific property so we'll set that separately
98+
if matches!(app_target, AppTarget::AppsSDK) {
99+
meta.get_or_insert_with(Meta::new).insert(
100+
"openai/widgetCSP".into(),
101+
json!({
102+
"redirect_domains": csp.redirect_domains
103+
}),
104+
);
117105
}
118106
}
107+
119108
if let Some(widget_settings) = &app.widget_settings {
120109
if let Some(description) = &widget_settings.description
121110
&& matches!(app_target, AppTarget::AppsSDK)
@@ -127,36 +116,26 @@ pub(crate) async fn get_app_resource(
127116
}
128117

129118
if let Some(domain) = &widget_settings.domain {
130-
meta.get_or_insert_with(Meta::new).insert(
131-
match app_target {
132-
AppTarget::AppsSDK => "openai/widgetDomain".into(),
133-
AppTarget::MCPApps => "domain".into(),
134-
},
119+
ui.get_or_insert_with(Meta::new).insert(
120+
"domain".into(),
135121
serde_json::to_value(domain).unwrap_or_default(),
136122
);
137123
}
138124

139125
if let Some(prefers_border) = &widget_settings.prefers_border {
140-
meta.get_or_insert_with(Meta::new).insert(
141-
match app_target {
142-
AppTarget::AppsSDK => "openai/widgetPrefersBorder".into(),
143-
AppTarget::MCPApps => "prefersBorder".into(),
144-
},
126+
ui.get_or_insert_with(Meta::new).insert(
127+
"prefersBorder".into(),
145128
serde_json::to_value(prefers_border).unwrap_or_default(),
146129
);
147130
}
148131
}
149132

150-
// In the case of MCP Apps, the meta data is nested under `_meta.ui`
151-
if matches!(app_target, AppTarget::MCPApps) {
152-
let mut nested = Meta::new();
153-
nested.insert("ui".into(), serde_json::to_value(meta).unwrap_or_default());
154-
meta = Some(nested);
155-
}
133+
meta.get_or_insert_with(Meta::new)
134+
.insert("ui".into(), serde_json::to_value(ui).unwrap_or_default());
156135

157136
Ok(ResourceContents::TextResourceContents {
158137
uri: request.uri,
159-
mime_type: Some(get_mime_type(app_target)),
138+
mime_type: Some(MCP_MIME_TYPE.to_string()),
160139
text,
161140
meta,
162141
})
@@ -172,7 +151,7 @@ mod tests {
172151
use super::*;
173152

174153
#[test]
175-
fn attach_correct_mime_type_when_open_ai() {
154+
fn attach_correct_mime_type() {
176155
let resource = Resource::new(
177156
RawResource {
178157
name: "TestResource".to_string(),
@@ -187,89 +166,14 @@ mod tests {
187166
None,
188167
);
189168

190-
let mut extensions = Extensions::new();
191-
let request = axum::http::Request::builder()
192-
.uri("http://localhost?appTarget=openai")
193-
.body(())
194-
.unwrap();
195-
let (parts, _) = request.into_parts();
196-
extensions.insert(parts);
197-
let app_target = AppTarget::try_from(extensions).unwrap();
198-
199-
let result = attach_resource_mime_type(resource, &app_target);
200-
201-
assert_eq!(
202-
result.raw.mime_type,
203-
Some("text/html+skybridge".to_string())
204-
);
205-
}
206-
207-
#[test]
208-
fn attach_correct_mime_type_when_mcp_apps() {
209-
let resource = Resource::new(
210-
RawResource {
211-
name: "TestResource".to_string(),
212-
uri: "ui://test".to_string(),
213-
mime_type: None,
214-
title: None,
215-
description: None,
216-
icons: None,
217-
size: None,
218-
meta: None,
219-
},
220-
None,
221-
);
222-
223-
let mut extensions = Extensions::new();
224-
let request = axum::http::Request::builder()
225-
.uri("http://localhost?appTarget=mcp")
226-
.body(())
227-
.unwrap();
228-
let (parts, _) = request.into_parts();
229-
extensions.insert(parts);
230-
let app_target = AppTarget::try_from(extensions).unwrap();
231-
232-
let result = attach_resource_mime_type(resource, &app_target);
169+
let result = attach_resource_mime_type(resource);
233170

234171
assert_eq!(
235172
result.raw.mime_type,
236173
Some("text/html;profile=mcp-app".to_string())
237174
);
238175
}
239176

240-
#[test]
241-
fn attach_correct_mime_type_when_not_provided() {
242-
let resource = Resource::new(
243-
RawResource {
244-
name: "TestResource".to_string(),
245-
uri: "ui://test".to_string(),
246-
mime_type: None,
247-
title: None,
248-
description: None,
249-
icons: None,
250-
size: None,
251-
meta: None,
252-
},
253-
None,
254-
);
255-
256-
let mut extensions = Extensions::new();
257-
let request = axum::http::Request::builder()
258-
.uri("http://localhost")
259-
.body(())
260-
.unwrap();
261-
let (parts, _) = request.into_parts();
262-
extensions.insert(parts);
263-
let app_target = AppTarget::try_from(extensions).unwrap();
264-
265-
let result = attach_resource_mime_type(resource, &app_target);
266-
267-
assert_eq!(
268-
result.raw.mime_type,
269-
Some("text/html+skybridge".to_string())
270-
);
271-
}
272-
273177
#[test]
274178
fn errors_when_invalid_target_provided() {
275179
let mut extensions = Extensions::new();
@@ -329,21 +233,23 @@ mod tests {
329233
else {
330234
unreachable!()
331235
};
332-
assert_eq!(mime_type, Some("text/html+skybridge".to_string()));
236+
assert_eq!(mime_type, Some("text/html;profile=mcp-app".to_string()));
333237

334238
let meta = meta.unwrap();
335-
// AppsSDK CSP uses snake_case keys and includes redirect_domains (not base_uri_domains)
239+
// OpenAI-specific CSP with redirect_domains should be at root
336240
let csp = meta.get("openai/widgetCSP").unwrap();
337-
assert!(csp.get("connect_domains").is_some());
338-
assert!(csp.get("resource_domains").is_some());
339-
assert!(csp.get("frame_domains").is_some());
340241
assert!(csp.get("redirect_domains").is_some());
341-
assert!(csp.get("base_uri_domains").is_none());
242+
// OpenAI-specific description should be at root
342243
assert!(meta.get("openai/widgetDescription").is_some());
343-
assert!(meta.get("openai/widgetDomain").is_some());
344-
assert!(meta.get("openai/widgetPrefersBorder").is_some());
345-
// AppsSDK should not have ui nesting
346-
assert!(meta.get("ui").is_none());
244+
// ui nesting should contain the common properties
245+
let ui_meta = meta.get("ui").unwrap();
246+
let ui_csp = ui_meta.get("csp").unwrap();
247+
assert!(ui_csp.get("connectDomains").is_some());
248+
assert!(ui_csp.get("resourceDomains").is_some());
249+
assert!(ui_csp.get("frameDomains").is_some());
250+
assert!(ui_csp.get("baseUriDomains").is_some());
251+
assert!(ui_meta.get("domain").is_some());
252+
assert!(ui_meta.get("prefersBorder").is_some());
347253
}
348254

349255
#[tokio::test]

0 commit comments

Comments
 (0)