Skip to content

Commit 4a98035

Browse files
committed
Render MCP apps inline in Goose2
Signed-off-by: Andrew Harvard <aharvard@squareup.com>
1 parent 7d69e14 commit 4a98035

45 files changed

Lines changed: 2591 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/goose-cli/src/cli.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,9 @@ async fn handle_serve_command(host: String, port: u16, builtins: Vec<String>) ->
10861086
config_dir: Paths::config_dir(),
10871087
goose_platform: GoosePlatform::GooseCli,
10881088
}));
1089-
let router = create_router(server);
1089+
let secret_key =
1090+
std::env::var("GOOSE_SERVER__SECRET_KEY").unwrap_or_else(|_| "goose-acp-local".into());
1091+
let router = create_router(server, secret_key);
10901092

10911093
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
10921094
info!("Starting ACP server on {}", addr);

crates/goose-sdk/src/custom_requests.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ pub struct ReadResourceResponse {
7474
pub result: serde_json::Value,
7575
}
7676

77+
/// Call a tool from an extension.
78+
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
79+
#[request(method = "_goose/tool/call", response = GooseToolCallResponse)]
80+
#[serde(rename_all = "camelCase")]
81+
pub struct GooseToolCallRequest {
82+
pub session_id: String,
83+
pub name: String,
84+
#[serde(default)]
85+
pub arguments: serde_json::Value,
86+
}
87+
88+
/// Tool call response.
89+
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
90+
#[serde(rename_all = "camelCase")]
91+
pub struct GooseToolCallResponse {
92+
#[serde(default)]
93+
pub content: Vec<serde_json::Value>,
94+
#[serde(skip_serializing_if = "Option::is_none")]
95+
pub structured_content: Option<serde_json::Value>,
96+
pub is_error: bool,
97+
#[serde(skip_serializing_if = "Option::is_none")]
98+
#[serde(rename = "_meta")]
99+
pub meta: Option<serde_json::Value>,
100+
}
101+
77102
/// Update the working directory for a session.
78103
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
79104
#[request(method = "_goose/working_dir/update", response = EmptyResponse)]

crates/goose-server/src/routes/templates/mcp_app_proxy.html

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,66 @@
3939
*/
4040
function getProxyParams() {
4141
var params = new URLSearchParams(window.location.search);
42+
var colorScheme = params.get('color_scheme');
4243
return {
4344
secret: params.get('secret') || '',
44-
baseUrl: window.location.origin
45+
baseUrl: window.location.origin,
46+
colorScheme: colorScheme === 'light' || colorScheme === 'dark' ? colorScheme : null
4547
};
4648
}
4749

50+
function applyProxyColorScheme(nextColorScheme) {
51+
var proxyParams = getProxyParams();
52+
var colorScheme = nextColorScheme || proxyParams.colorScheme || 'light dark';
53+
document.documentElement.style.colorScheme = colorScheme;
54+
document.body.style.colorScheme = colorScheme;
55+
if (guestIframe) {
56+
guestIframe.style.setProperty('color-scheme', colorScheme);
57+
}
58+
}
59+
60+
function createColorSchemePrelude(colorScheme) {
61+
var hostColorScheme = colorScheme === 'dark' ? 'dark' : 'light';
62+
var matchMediaScript = [
63+
'<scr' + 'ipt>',
64+
'(function(){',
65+
'var nativeMatchMedia=window.matchMedia&&window.matchMedia.bind(window);',
66+
'function normalizeColorScheme(value){return value==="dark"?"dark":"light";}',
67+
'function setHostColorScheme(value){window.__mcpHostColorScheme=normalizeColorScheme(value);document.documentElement.style.colorScheme=window.__mcpHostColorScheme;if(document.body){document.body.style.colorScheme=window.__mcpHostColorScheme;}var meta=document.querySelector("meta[name=\\"color-scheme\\"]");if(meta){meta.setAttribute("content",window.__mcpHostColorScheme);}}',
68+
'setHostColorScheme(' + JSON.stringify(hostColorScheme) + ');',
69+
'document.addEventListener("DOMContentLoaded",function(){setHostColorScheme(window.__mcpHostColorScheme);});',
70+
'if(nativeMatchMedia){window.matchMedia=function(query){var normalized=String(query).replace(/\\s+/g," ").trim().toLowerCase();var isDark=normalized==="(prefers-color-scheme: dark)";var isLight=normalized==="(prefers-color-scheme: light)";if(!isDark&&!isLight){return nativeMatchMedia(query);}return {matches:isDark?window.__mcpHostColorScheme==="dark":window.__mcpHostColorScheme==="light",media:String(query),onchange:null,addListener:function(){},removeListener:function(){},addEventListener:function(){},removeEventListener:function(){},dispatchEvent:function(){return false;}};};}',
71+
'window.addEventListener("message",function(event){var data=event.data;if(!data||data.method!=="ui/notifications/host-context-changed"){return;}var theme=data.params&&data.params.theme;if(theme==="light"||theme==="dark"){setHostColorScheme(theme);}});',
72+
'})();',
73+
'</scr' + 'ipt>'
74+
].join('');
75+
76+
return '<meta name="color-scheme" content="' + hostColorScheme + '"><style id="mcp-app-host-color-scheme">:root{color-scheme:' + hostColorScheme + ';}html,body{background-color:transparent;}</style>' + matchMediaScript;
77+
}
78+
79+
function injectGuestColorScheme(html, colorScheme) {
80+
if (!colorScheme) {
81+
return html;
82+
}
83+
84+
var prelude = createColorSchemePrelude(colorScheme);
85+
var cleanedHtml = html.replace(/<meta\s+[^>]*name\s*=\s*["']color-scheme["'][^>]*>/gi, '');
86+
87+
if (/<head\b[^>]*>/i.test(cleanedHtml)) {
88+
return cleanedHtml.replace(/<head\b[^>]*>/i, function (match) {
89+
return match + prelude;
90+
});
91+
}
92+
93+
if (/<html\b[^>]*>/i.test(cleanedHtml)) {
94+
return cleanedHtml.replace(/<html\b[^>]*>/i, function (match) {
95+
return match + '<head>' + prelude + '</head>';
96+
});
97+
}
98+
99+
return prelude + cleanedHtml;
100+
}
101+
48102
/**
49103
* Store guest HTML on the server and get a nonce for retrieval.
50104
* This allows the guest iframe to load from a real HTTPS URL
@@ -93,20 +147,22 @@
93147
guestIframe.setAttribute('allow', allowList.join('; '));
94148
}
95149

96-
guestIframe.style.cssText = 'width:100%; height:100%; border:none;';
150+
var proxyParams = getProxyParams();
151+
var colorScheme = proxyParams.colorScheme || 'light dark';
152+
guestIframe.style.cssText = 'width:100%; height:100%; border:none; background-color:transparent; color-scheme:' + colorScheme + ';';
153+
var guestHtml = injectGuestColorScheme(html, proxyParams.colorScheme);
97154

98155
// Store the HTML server-side and load via real URL.
99156
// This gives the guest iframe a real https:// URL instead of about:srcdoc,
100157
// which is required by SDKs (like Square Web Payments) that check
101158
// window.location.protocol for secure context verification.
102159
try {
103-
var nonce = await storeGuestHtml(html);
104-
var proxyParams = getProxyParams();
160+
var nonce = await storeGuestHtml(guestHtml);
105161
guestIframe.src = proxyParams.baseUrl + '/mcp-app-guest?secret=' + encodeURIComponent(proxyParams.secret) + '&nonce=' + encodeURIComponent(nonce);
106162
} catch (e) {
107163
// Fallback to srcdoc if the store endpoint is not available
108164
console.warn('Failed to use /mcp-app-guest endpoint, falling back to srcdoc:', e);
109-
guestIframe.srcdoc = html;
165+
guestIframe.srcdoc = guestHtml;
110166
}
111167

112168
document
@@ -129,6 +185,13 @@
129185

130186
var method = data.method;
131187

188+
if (method === 'ui/notifications/host-context-changed') {
189+
var theme = data.params && data.params.theme;
190+
if (theme === 'light' || theme === 'dark') {
191+
applyProxyColorScheme(theme);
192+
}
193+
}
194+
132195
// Handle sandbox-specific notifications from Host
133196
if (method === 'ui/notifications/sandbox-resource-ready') {
134197
var params = data.params || {};
@@ -181,6 +244,7 @@
181244

182245
// Set up message listener
183246
window.addEventListener('message', handleMessage);
247+
applyProxyColorScheme();
184248

185249
// Notify Host that Sandbox is ready
186250
window

crates/goose/acp-meta.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
"requestType": "ReadResourceRequest",
2121
"responseType": "ReadResourceResponse"
2222
},
23+
{
24+
"method": "_goose/tool/call",
25+
"requestType": "GooseToolCallRequest",
26+
"responseType": "GooseToolCallResponse"
27+
},
2328
{
2429
"method": "_goose/working_dir/update",
2530
"requestType": "UpdateWorkingDirRequest",

crates/goose/acp-schema.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,48 @@
107107
"x-side": "agent",
108108
"x-method": "_goose/resource/read"
109109
},
110+
"GooseToolCallRequest": {
111+
"type": "object",
112+
"properties": {
113+
"sessionId": {
114+
"type": "string"
115+
},
116+
"name": {
117+
"type": "string"
118+
},
119+
"arguments": {
120+
"default": null
121+
}
122+
},
123+
"required": [
124+
"sessionId",
125+
"name"
126+
],
127+
"description": "Call a tool from an extension.",
128+
"x-side": "agent",
129+
"x-method": "_goose/tool/call"
130+
},
131+
"GooseToolCallResponse": {
132+
"type": "object",
133+
"properties": {
134+
"content": {
135+
"type": "array",
136+
"items": {},
137+
"default": []
138+
},
139+
"structuredContent": {},
140+
"isError": {
141+
"type": "boolean"
142+
},
143+
"_meta": {}
144+
},
145+
"required": [
146+
"isError"
147+
],
148+
"description": "Tool call response.",
149+
"x-side": "agent",
150+
"x-method": "_goose/tool/call"
151+
},
110152
"UpdateWorkingDirRequest": {
111153
"type": "object",
112154
"properties": {
@@ -1610,6 +1652,15 @@
16101652
"description": "Params for _goose/resource/read",
16111653
"title": "ReadResourceRequest"
16121654
},
1655+
{
1656+
"allOf": [
1657+
{
1658+
"$ref": "#/$defs/GooseToolCallRequest"
1659+
}
1660+
],
1661+
"description": "Params for _goose/tool/call",
1662+
"title": "GooseToolCallRequest"
1663+
},
16131664
{
16141665
"allOf": [
16151666
{
@@ -2015,6 +2066,14 @@
20152066
],
20162067
"title": "ReadResourceResponse"
20172068
},
2069+
{
2070+
"allOf": [
2071+
{
2072+
"$ref": "#/$defs/GooseToolCallResponse"
2073+
}
2074+
],
2075+
"title": "GooseToolCallResponse"
2076+
},
20182077
{
20192078
"allOf": [
20202079
{

0 commit comments

Comments
 (0)