mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
WIP still
This commit is contained in:
@@ -21,7 +21,7 @@ struct ProxyQuery {
|
||||
}
|
||||
|
||||
const MCP_APP_PROXY_HTML: &str = include_str!("templates/mcp_app_proxy.html");
|
||||
const MCP_APP_SDK_JS: &str = include_str!("templates/mcp_app_sdk.js");
|
||||
|
||||
|
||||
/// Build the outer sandbox CSP based on declared domains.
|
||||
///
|
||||
@@ -142,27 +142,8 @@ async fn mcp_app_proxy(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Serve the MCP Apps SDK (`@modelcontextprotocol/ext-apps`) as a JavaScript module.
|
||||
///
|
||||
/// Guest apps inside the sandbox can load this via:
|
||||
/// `<script type="module">import { App } from '/mcp-app-sdk.js';</script>`
|
||||
///
|
||||
/// This works because the sandbox iframe has `allow-same-origin` and the CSP
|
||||
/// includes `script-src 'self'`, allowing script loads from the goosed origin.
|
||||
async fn mcp_app_sdk() -> Response {
|
||||
(
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)],
|
||||
MCP_APP_SDK_JS,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub fn routes(secret_key: String) -> Router {
|
||||
Router::new()
|
||||
.route("/mcp-app-proxy", get(mcp_app_proxy))
|
||||
.route("/mcp-app-sdk.js", get(mcp_app_sdk))
|
||||
.with_state(secret_key)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -223,6 +223,122 @@ impl AppsManagerClient {
|
||||
.result_with_platform_notification(result, EXTENSION_NAME, event_type, params)
|
||||
}
|
||||
|
||||
/// Platform extensions that should not be exposed to Goose apps.
|
||||
const HIDDEN_EXTENSIONS: &'static [&'static str] = &[
|
||||
"apps",
|
||||
"extensionmanager",
|
||||
"extension_manager",
|
||||
"code_execution",
|
||||
"summon",
|
||||
"tom",
|
||||
"chatrecall",
|
||||
"todo",
|
||||
];
|
||||
|
||||
async fn get_available_tools_description(&self, session_id: &str) -> String {
|
||||
let extension_manager = match self
|
||||
.context
|
||||
.extension_manager
|
||||
.as_ref()
|
||||
.and_then(|weak| weak.upgrade())
|
||||
{
|
||||
Some(em) => em,
|
||||
None => return String::new(),
|
||||
};
|
||||
|
||||
let tools = match extension_manager
|
||||
.get_prefixed_tools_excluding(session_id, EXTENSION_NAME)
|
||||
.await
|
||||
{
|
||||
Ok(tools) => tools,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
|
||||
if tools.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Get extension descriptions via get_extensions_info
|
||||
let working_dir = self
|
||||
.context
|
||||
.session_manager
|
||||
.get_session(session_id, false)
|
||||
.await
|
||||
.ok()
|
||||
.map(|s| s.working_dir)
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
||||
let ext_infos = extension_manager.get_extensions_info(&working_dir).await;
|
||||
let ext_descriptions: HashMap<String, String> = ext_infos
|
||||
.into_iter()
|
||||
.map(|info| {
|
||||
let desc = info
|
||||
.instructions
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
(info.name, desc)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Group tools by extension, filtering out platform extensions
|
||||
let mut by_extension: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
|
||||
for tool in &tools {
|
||||
let name = tool.name.to_string();
|
||||
if let Some((ext, tool_name)) = name.split_once("__") {
|
||||
if Self::HIDDEN_EXTENSIONS.contains(&ext) {
|
||||
continue;
|
||||
}
|
||||
let desc = tool
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|d| d.to_string())
|
||||
.unwrap_or_default();
|
||||
let params = tool
|
||||
.input_schema
|
||||
.get("properties")
|
||||
.and_then(|p| p.as_object())
|
||||
.map(|props| {
|
||||
props
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
by_extension
|
||||
.entry(ext.to_string())
|
||||
.or_default()
|
||||
.push((tool_name.to_string(), desc, params));
|
||||
}
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let mut extensions: Vec<_> = by_extension.into_iter().collect();
|
||||
extensions.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for (ext, ext_tools) in extensions {
|
||||
let ext_desc = ext_descriptions
|
||||
.get(&ext)
|
||||
.filter(|d| !d.is_empty())
|
||||
.map(|d| format!(" — {d}"))
|
||||
.unwrap_or_default();
|
||||
lines.push(format!(" goose.{ext}:{ext_desc}"));
|
||||
for (tool_name, desc, params) in &ext_tools {
|
||||
let sig = if params.is_empty() {
|
||||
format!("goose.{ext}.{tool_name}()")
|
||||
} else {
|
||||
format!("goose.{ext}.{tool_name}({{{params}}})")
|
||||
};
|
||||
let short_desc = desc
|
||||
.split_once(". ")
|
||||
.map(|(first, _)| first.to_string())
|
||||
.unwrap_or_else(|| desc.clone());
|
||||
lines.push(format!(" await {sig} - {short_desc}"));
|
||||
}
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
async fn get_provider(&self) -> Result<Arc<dyn Provider>, String> {
|
||||
let extension_manager = self
|
||||
.context
|
||||
@@ -277,8 +393,8 @@ impl AppsManagerClient {
|
||||
let existing_apps = self.list_stored_apps().unwrap_or_default();
|
||||
let existing_names = existing_apps.join(", ");
|
||||
|
||||
let mut context: HashMap<&str, bool> = HashMap::new();
|
||||
context.insert("is_new", true);
|
||||
let available_tools = self.get_available_tools_description(session_id).await;
|
||||
let context = json!({"is_new": true, "available_tools": available_tools});
|
||||
let system_prompt = render_template("apps.md", &context)
|
||||
.map_err(|e| format!("Failed to render template: {}", e))?;
|
||||
|
||||
@@ -291,6 +407,12 @@ impl AppsManagerClient {
|
||||
let messages = vec![Message::user().with_text(&user_prompt)];
|
||||
let tools = vec![Self::create_app_content_tool()];
|
||||
|
||||
// Dump the full prompt for debugging
|
||||
let _ = std::fs::write(
|
||||
"/tmp/prompt.txt",
|
||||
format!("=== SYSTEM ===\n{}\n\n=== USER ===\n{}", system_prompt, user_prompt),
|
||||
);
|
||||
|
||||
let mut model_config = provider.get_model_config();
|
||||
model_config.max_tokens = Some(16384);
|
||||
|
||||
@@ -311,8 +433,8 @@ impl AppsManagerClient {
|
||||
) -> Result<UpdateAppContentResponse, String> {
|
||||
let provider = self.get_provider().await?;
|
||||
|
||||
let mut context: HashMap<&str, bool> = HashMap::new();
|
||||
context.insert("is_new", false);
|
||||
let available_tools = self.get_available_tools_description(session_id).await;
|
||||
let context = json!({"is_new": false, "available_tools": available_tools});
|
||||
let system_prompt = render_template("apps.md", &context)
|
||||
.map_err(|e| format!("Failed to render template: {}", e))?;
|
||||
|
||||
@@ -514,12 +636,12 @@ impl McpClientTrait for AppsManagerClient {
|
||||
),
|
||||
McpTool::new(
|
||||
"create_app".to_string(),
|
||||
"Create a new Goose app based on a description or PRD. The extension will use an LLM to generate the HTML/CSS/JavaScript. Apps are sandboxed and run in standalone windows.".to_string(),
|
||||
"Create a new Goose app based on a description or PRD. Apps can call MCP tools at runtime (e.g., goose.developer.shell to run commands, goose.developer.text_editor to read files), so describe what data the app needs and which tools to use rather than embedding static data. The PRD should focus on what the app does and how it gets its data.".to_string(),
|
||||
schema::<CreateAppParams>(),
|
||||
),
|
||||
McpTool::new(
|
||||
"iterate_app".to_string(),
|
||||
"Improve an existing app based on feedback. The extension will use an LLM to update the HTML while preserving the app's intent.".to_string(),
|
||||
"Improve an existing app based on feedback. Apps can call MCP tools at runtime for live data. The extension will use an LLM to update the HTML while preserving the app's intent.".to_string(),
|
||||
schema::<IterateAppParams>(),
|
||||
),
|
||||
McpTool::new(
|
||||
@@ -629,11 +751,8 @@ impl McpClientTrait for AppsManagerClient {
|
||||
.map_err(|_| Error::TransportClosed)?;
|
||||
|
||||
let html = app
|
||||
.resource
|
||||
.text
|
||||
.unwrap_or_else(|| String::from("No content"));
|
||||
|
||||
let html = inject_sdk_script(&html);
|
||||
.to_render_html()
|
||||
.map_err(|_| Error::TransportClosed)?;
|
||||
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![ResourceContents::text(html, uri)],
|
||||
@@ -685,26 +804,4 @@ fn extract_tool_response<T: serde::de::DeserializeOwned>(
|
||||
Err(format!("LLM did not call the required tool: {}", tool_name))
|
||||
}
|
||||
|
||||
/// Inject the MCP Apps SDK script tag into Goose app HTML.
|
||||
///
|
||||
/// This makes `@modelcontextprotocol/ext-apps` available to Goose apps via:
|
||||
/// `import { App, PostMessageTransport } from '/mcp-app-sdk.js';`
|
||||
///
|
||||
/// The SDK is served by goosed at `/mcp-app-sdk.js` and is loadable because
|
||||
/// the sandbox iframe has `allow-same-origin` and the CSP allows `script-src 'self'`.
|
||||
fn inject_sdk_script(html: &str) -> String {
|
||||
const SDK_TAG: &str = r#"<script type="module" src="/mcp-app-sdk.js"></script>"#;
|
||||
|
||||
// We search for ASCII-only patterns so all positions are valid split points.
|
||||
if let Some(pos) = html.find("<head>") {
|
||||
let mut result = String::with_capacity(html.len() + SDK_TAG.len());
|
||||
let (before, after) = html.split_at(pos + "<head>".len());
|
||||
result.push_str(before);
|
||||
result.push_str(SDK_TAG);
|
||||
result.push_str(after);
|
||||
result
|
||||
} else {
|
||||
// No <head> tag — prepend SDK in a new <head> block
|
||||
format!("<head>{SDK_TAG}</head>{html}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ pub struct GooseApp {
|
||||
pub prd: Option<String>,
|
||||
}
|
||||
|
||||
const BRIDGE_JS: &str = include_str!("goose_bridge.js");
|
||||
|
||||
impl GooseApp {
|
||||
const METADATA_SCRIPT_TYPE: &'static str = "application/ld+json";
|
||||
const PRD_SCRIPT_TYPE: &'static str = "application/x-goose-prd";
|
||||
@@ -123,7 +125,20 @@ impl GooseApp {
|
||||
})
|
||||
}
|
||||
|
||||
/// Produce HTML for rendering in the sandbox, with the Goose bridge injected.
|
||||
pub fn to_render_html(&self) -> Result<String, String> {
|
||||
let bridge = format!(
|
||||
"<script>\n{}\n</script>\n",
|
||||
BRIDGE_JS.replace("'{{APP_NAME}}'", &format!("'{}'", self.resource.name))
|
||||
);
|
||||
self.to_html_inner(Some(&bridge))
|
||||
}
|
||||
|
||||
pub fn to_html(&self) -> Result<String, String> {
|
||||
self.to_html_inner(None)
|
||||
}
|
||||
|
||||
fn to_html_inner(&self, extra_head: Option<&str>) -> Result<String, String> {
|
||||
let html = self
|
||||
.resource
|
||||
.text
|
||||
@@ -173,15 +188,20 @@ impl GooseApp {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let scripts = if prd_script.is_empty() {
|
||||
format!("{}\n", metadata_script)
|
||||
} else {
|
||||
format!("{}\n{}\n", metadata_script, prd_script)
|
||||
};
|
||||
let mut head_content = String::new();
|
||||
if let Some(extra) = extra_head {
|
||||
head_content.push_str(extra);
|
||||
}
|
||||
head_content.push_str(&metadata_script);
|
||||
head_content.push('\n');
|
||||
if !prd_script.is_empty() {
|
||||
head_content.push_str(&prd_script);
|
||||
head_content.push('\n');
|
||||
}
|
||||
|
||||
let result = if let Some(head_pos) = html.find("</head>") {
|
||||
let mut result = html.clone();
|
||||
result.insert_str(head_pos, &scripts);
|
||||
result.insert_str(head_pos, &head_content);
|
||||
result
|
||||
} else if let Some(html_pos) = html.find("<html") {
|
||||
let after_html = html
|
||||
@@ -190,15 +210,15 @@ impl GooseApp {
|
||||
.map(|p| html_pos + p + 1);
|
||||
if let Some(pos) = after_html {
|
||||
let mut result = html.clone();
|
||||
result.insert_str(pos, &format!("\n<head>\n{}</head>", scripts));
|
||||
result.insert_str(pos, &format!("\n<head>\n{}</head>", head_content));
|
||||
result
|
||||
} else {
|
||||
format!("<head>\n{}</head>\n{}", scripts, html)
|
||||
format!("<head>\n{}</head>\n{}", head_content, html)
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"<html>\n<head>\n{}</head>\n<body>\n{}\n</body>\n</html>",
|
||||
scripts, html
|
||||
head_content, html
|
||||
)
|
||||
};
|
||||
|
||||
@@ -307,3 +327,5 @@ pub async fn fetch_mcp_apps(
|
||||
|
||||
Ok(apps)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ impl McpAppCache {
|
||||
if self.get_app(APPS_EXTENSION_NAME, uri).is_none() {
|
||||
if let Ok(mut app) = GooseApp::from_html(html) {
|
||||
app.mcp_servers = vec![APPS_EXTENSION_NAME.to_string()];
|
||||
// Store with bridge-injected HTML so cached display works
|
||||
if let Ok(render_html) = app.to_render_html() {
|
||||
app.resource.text = Some(render_html);
|
||||
}
|
||||
let _ = self.store_app(&app);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
body { display: flex; flex-direction: column; background: #fff; }
|
||||
|
||||
|
||||
body { display: flex; flex-direction: column; }
|
||||
body.light { background: #fff; color: #000; }
|
||||
body.dark { background: #1e1e2e; color: #cdd6f4; }
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -27,7 +30,7 @@
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
.message {
|
||||
max-width: 80%;
|
||||
padding: 10px 14px;
|
||||
@@ -36,60 +39,50 @@
|
||||
font-size: 14px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
align-self: flex-start;
|
||||
background: #f0f0f0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.message.loading {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
body.light .message.user { align-self: flex-end; background: #000; color: #fff; }
|
||||
body.light .message.assistant { align-self: flex-start; background: #f0f0f0; color: #000; }
|
||||
body.dark .message.user { align-self: flex-end; background: #89b4fa; color: #1e1e2e; }
|
||||
body.dark .message.assistant { align-self: flex-start; background: #313244; color: #cdd6f4; }
|
||||
|
||||
.message.loading { font-style: italic; opacity: 0.6; }
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
|
||||
body.light .input-area { border-top: 1px solid #e0e0e0; background: #fafafa; }
|
||||
body.dark .input-area { border-top: 1px solid #45475a; background: #181825; }
|
||||
|
||||
#messageInput {
|
||||
flex: 1;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#messageInput:focus { border-color: #999; }
|
||||
|
||||
|
||||
body.light #messageInput { border: 1px solid #ddd; background: #fff; color: #000; }
|
||||
body.light #messageInput:focus { border-color: #999; }
|
||||
body.dark #messageInput { border: 1px solid #45475a; background: #313244; color: #cdd6f4; }
|
||||
body.dark #messageInput:focus { border-color: #89b4fa; }
|
||||
|
||||
#sendBtn {
|
||||
padding: 10px 20px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sendBtn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
body.light #sendBtn { background: #000; color: #fff; }
|
||||
body.dark #sendBtn { background: #89b4fa; color: #1e1e2e; }
|
||||
#sendBtn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="light">
|
||||
<div class="messages" id="messages"></div>
|
||||
<div class="input-area">
|
||||
<input type="text" id="messageInput" placeholder="Type a message..." />
|
||||
@@ -101,8 +94,6 @@
|
||||
const inputEl = document.getElementById('messageInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const conversationHistory = [];
|
||||
const pendingRequests = new Map();
|
||||
let requestId = 0;
|
||||
|
||||
function addMessage(role, text, isLoading = false) {
|
||||
const div = document.createElement('div');
|
||||
@@ -113,33 +104,6 @@
|
||||
return div;
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++requestId;
|
||||
pendingRequests.set(id, { resolve, reject });
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id, method, params }, '*');
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
|
||||
if ('id' in data && pendingRequests.has(data.id)) {
|
||||
const { resolve, reject } = pendingRequests.get(data.id);
|
||||
pendingRequests.delete(data.id);
|
||||
if (data.error) {
|
||||
reject(new Error(data.error.message || 'Unknown error'));
|
||||
} else {
|
||||
resolve(data.result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
request('ui/initialize', {}).then(() => {
|
||||
window.parent.postMessage({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} }, '*');
|
||||
});
|
||||
|
||||
async function sendMessage() {
|
||||
const text = inputEl.value.trim();
|
||||
if (!text) return;
|
||||
@@ -153,15 +117,14 @@
|
||||
const loadingEl = addMessage('assistant', 'Thinking...', true);
|
||||
|
||||
try {
|
||||
const response = await request('sampling/createMessage', {
|
||||
messages: conversationHistory,
|
||||
const response = await goose.chat(conversationHistory, {
|
||||
systemPrompt: 'You are a helpful assistant. Keep responses concise.',
|
||||
maxTokens: 1000
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
const responseText = response.content.text;
|
||||
conversationHistory.push({ role: 'assistant', content: { type: 'text', text: responseText } });
|
||||
|
||||
|
||||
loadingEl.textContent = responseText;
|
||||
loadingEl.classList.remove('loading');
|
||||
} catch (err) {
|
||||
@@ -178,7 +141,11 @@
|
||||
if (e.key === 'Enter') sendMessage();
|
||||
});
|
||||
|
||||
inputEl.focus();
|
||||
goose.onReady = () => {
|
||||
document.body.className = goose.theme;
|
||||
inputEl.focus();
|
||||
};
|
||||
goose.onThemeChange = (theme) => document.body.className = theme;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -132,12 +132,6 @@
|
||||
this.animate();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.drawClock();
|
||||
this.animationFrame = requestAnimationFrame(() => this.animate());
|
||||
@@ -269,35 +263,16 @@
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new ClockWidget();
|
||||
widget.onMount();
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
widget.onClose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import { App, PostMessageTransport } from '/mcp-app-sdk.js';
|
||||
<script>
|
||||
const widget = new ClockWidget();
|
||||
|
||||
try {
|
||||
const app = new App({ name: 'clock', version: '1.0.0' }, {});
|
||||
await app.connect(new PostMessageTransport(window.parent, window.parent));
|
||||
|
||||
const context = app.getHostContext();
|
||||
if (context?.theme) {
|
||||
widget.setTheme(context.theme);
|
||||
}
|
||||
|
||||
app.onhostcontextchanged = (params) => {
|
||||
if (params.theme) {
|
||||
widget.setTheme(params.theme);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn('MCP Apps SDK not available, using default theme:', e.message);
|
||||
}
|
||||
goose.onReady = () => {
|
||||
widget.onMount();
|
||||
widget.setTheme(goose.theme);
|
||||
};
|
||||
goose.onThemeChange = (theme) => widget.setTheme(theme);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
You are an expert HTML/CSS/JavaScript developer. Generate standalone, single-file HTML applications.
|
||||
You are an expert HTML/CSS/JavaScript developer. Generate standalone, single-file HTML applications that has
|
||||
access to a server side api through the goos object
|
||||
|
||||
REQUIREMENTS:
|
||||
- Create a complete, self-contained HTML file with embedded CSS and JavaScript
|
||||
@@ -8,8 +9,58 @@ REQUIREMENTS:
|
||||
- Add appropriate error handling
|
||||
- Make the app interactive and functional
|
||||
- Use vanilla JavaScript; do not load external JavaScript libraries (no JS dependencies from CDNs or packages)
|
||||
- If you need external resources (fonts, icons, or CSS only), use CDN links from well-known, trusted providers
|
||||
- The app will be sandboxed with strict CSP, so all JavaScript must be inline; only non-script assets (fonts, icons, CSS) may be loaded from trusted CDNs
|
||||
- The app will be sandboxed with strict CSP, so all JavaScript must be inline
|
||||
|
||||
GOOSE API:
|
||||
Every app has access to `goose`, which is automatically injected
|
||||
|
||||
Properties and lifecycle:
|
||||
goose.theme - "light" or "dark", kept in sync with host
|
||||
goose.onReady = () => {} - called once after goose is initialized; start your app here
|
||||
goose.onThemeChange = (theme) => {} - called when theme changes
|
||||
|
||||
Theme CSS variables (MCP standard, automatically set, update on theme change):
|
||||
--color-background-primary, --color-background-secondary, --color-background-tertiary
|
||||
--color-text-primary, --color-text-secondary, --color-text-tertiary
|
||||
--color-border-primary, --color-border-secondary
|
||||
--color-text-info, --color-text-danger, --color-text-success, --color-text-warning
|
||||
--color-background-info, --color-background-danger, --color-background-success, --color-background-warning
|
||||
--font-sans, --font-mono
|
||||
--border-radius-sm, --border-radius-md, --border-radius-lg
|
||||
Also: <html data-theme="light|dark"> is set automatically.
|
||||
Use these variables in CSS for automatic theme support — no JS needed for styling.
|
||||
|
||||
Chat / LLM access:
|
||||
await goose.chat(messages, {systemPrompt, maxTokens})
|
||||
- messages: [{role: "user", content: {type: "text", text: "..."}}]
|
||||
- returns: {role: "assistant", content: {type: "text", text: "..."}, model: "..."}
|
||||
|
||||
Example patterns:
|
||||
```css
|
||||
/* Theme-aware styling using MCP CSS variables — no JS needed */
|
||||
body { background: var(--color-background-primary); color: var(--color-text-primary); font-family: var(--font-sans); }
|
||||
a { color: var(--color-text-info); }
|
||||
.card { border: 1px solid var(--color-border-primary); background: var(--color-background-secondary); border-radius: var(--border-radius-md); }
|
||||
.error { color: var(--color-text-danger); }
|
||||
```
|
||||
```javascript
|
||||
// Start app after goose is ready
|
||||
goose.onReady = async () => {
|
||||
const output = await goose.developer.shell({command: "ls ~/Downloads"});
|
||||
document.getElementById("files").textContent = output;
|
||||
};
|
||||
|
||||
// Chat with the LLM
|
||||
const response = await goose.chat(
|
||||
[{role: "user", content: {type: "text", text: "Summarize this"}}],
|
||||
{systemPrompt: "You are helpful.", maxTokens: 500}
|
||||
);
|
||||
```
|
||||
|
||||
{% if available_tools %}
|
||||
The following tools can be called, they all take an object with their payload:
|
||||
{{ available_tools }}
|
||||
{% endif %}
|
||||
|
||||
WINDOW SIZING:
|
||||
{% if is_new %}
|
||||
|
||||
@@ -457,7 +457,7 @@ export default function McpAppRenderer({
|
||||
throw new Error('Session not initialized for MCP request');
|
||||
}
|
||||
|
||||
const fullToolName = `${extensionName}__${name}`;
|
||||
const fullToolName = name.includes('__') ? name : `${extensionName}__${name}`;
|
||||
const response = await callTool({
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
|
||||
Reference in New Issue
Block a user