feat(apps): add support for MCP apps to sample#7039
Conversation
|
cc @liady |
There was a problem hiding this comment.
Pull request overview
Adds MCP “sampling” support so sandboxed MCP Apps can ask the running goosed session/provider to generate model responses, and includes a simple built-in chat MCP App as a demo.
Changes:
- Adds a new MCP method (
sampling/createMessage) to the desktop MCP Apps bridge and types. - Adds a new
goose-serverroute (POST /sessions/{id}/sampling/message) to run provider completions for sampling. - Adds a built-in
chatMCP App and wires it into the default apps cache.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/components/McpApps/types.ts | Introduces sampling request/response types for MCP Apps. |
| ui/desktop/src/components/McpApps/McpAppRenderer.tsx | Forwards sampling/createMessage from the iframe to goosed via authenticated HTTP. |
| crates/goose/src/goose_apps/chat.html | New built-in demo MCP App that uses sampling to implement a simple chat UI. |
| crates/goose/src/goose_apps/cache.rs | Adds the new chat app to the default apps prepopulation logic. |
| crates/goose-server/src/routes/sampling.rs | New backend endpoint that converts MCP sampling messages to goose messages and calls the provider. |
| crates/goose-server/src/routes/mod.rs | Registers the new sampling routes with the server. |
| 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); |
There was a problem hiding this comment.
The message event handler accepts responses from any source/origin; add a check (at least event.source === window.parent) to prevent other windows/iframes from spoofing JSON-RPC responses into the app.
| const responseText = response.content.text; | ||
| conversationHistory.push({ role: 'assistant', content: { type: 'text', text: responseText } }); |
There was a problem hiding this comment.
sampling/createMessage returns an MCP CreateMessageResult (with a nested message field), but this code reads response.content.text; update the sample to use response.message.content.text (and adjust how you append to conversationHistory) so it matches the server response.
| const responseText = response.content.text; | |
| conversationHistory.push({ role: 'assistant', content: { type: 'text', text: responseText } }); | |
| const responseText = response.message.content.text; | |
| conversationHistory.push(response.message); |
| Router::new() | ||
| .route( | ||
| "/sessions/{session_id}/sampling/message", | ||
| post(create_message), | ||
| ) |
There was a problem hiding this comment.
This endpoint is intended to support image sampling, but there’s no explicit body-size limit layer here (unlike /reply and dictation), so base64 image requests may be rejected by the default request body limit; consider adding a DefaultBodyLimit::max(...) appropriate for image payloads.
| let (response, usage) = provider | ||
| .complete(&session_id, system, &messages, &[]) | ||
| .await | ||
| .map_err(|e| { | ||
| tracing::error!("Sampling completion failed: {}", e); |
There was a problem hiding this comment.
The request accepts maxTokens but it’s currently ignored (the provider call uses the default model config), so clients won’t be able to constrain output length as requested; either plumb request.max_tokens into an overridden ModelConfig via complete_with_model(...) or remove the parameter from the client contract.
| stopReason: string; | ||
| role: 'assistant'; | ||
| content: { type: 'text'; text: string }; |
There was a problem hiding this comment.
The sampling/createMessage response type here doesn’t match the backend/MCP CreateMessageResult shape (it returns a message: { role, content } object and stopReason may be optional), so consumers like the sample app will read the wrong fields.
| stopReason: string; | |
| role: 'assistant'; | |
| content: { type: 'text'; text: string }; | |
| stopReason?: string; | |
| message: { | |
| role: 'assistant'; | |
| content: { type: 'text'; text: string }; | |
| }; |
| for (uri, html) in [("apps://clock", CLOCK_HTML), ("apps://chat", CHAT_HTML)] { | ||
| 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()]; |
There was a problem hiding this comment.
This default-app cache check uses apps://... URIs, but GooseApp::from_html() sets resource.uri to ui://apps/{name} and store_app() keys the cache by app.resource.uri, so this get_app() call will never hit and will rewrite the default apps on every startup.
| for (uri, html) in [("apps://clock", CLOCK_HTML), ("apps://chat", CHAT_HTML)] { | |
| 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()]; | |
| for html in [CLOCK_HTML, CHAT_HTML] { | |
| if let Ok(mut app) = GooseApp::from_html(html) { | |
| app.mcp_servers = vec![APPS_EXTENSION_NAME.to_string()]; | |
| if self | |
| .get_app(APPS_EXTENSION_NAME, &app.resource.uri) | |
| .is_none() | |
| { |
| case 'sampling/createMessage': { | ||
| if (!sessionId || !apiHost || !secretKey) { | ||
| throw new Error('Session not initialized for sampling request'); | ||
| } | ||
| const { messages, systemPrompt, maxTokens } = | ||
| params as McpMethodParams['sampling/createMessage']; | ||
| const response = await fetch(`${apiHost}/sessions/${sessionId}/sampling/message`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'X-Secret-Key': secretKey, | ||
| }, | ||
| body: JSON.stringify({ | ||
| messages: messages.map((m) => ({ | ||
| role: m.role, | ||
| content: m.content, | ||
| })), | ||
| systemPrompt, | ||
| maxTokens, | ||
| }), | ||
| }); | ||
| if (!response.ok) { | ||
| throw new Error(`Sampling request failed: ${response.statusText}`); | ||
| } | ||
| return (await response.json()) as McpMethodResponse['sampling/createMessage']; | ||
| } |
There was a problem hiding this comment.
this is fantastic!
FYI, @alexhancock I'm working to integrate <AppRenderer/> from @mcp-ui/client over here. It has dedicated props for callbacks that map to a subset of JSON RPC messages supported by the MCP Apps spec. Don't think sampling/createMessage is supported on that side of things yet. Maybe I'm wrong?
@liady, how can we support sampling/createMessage while using <AppRenderer />?
++ @idosal, @ochafik, @infoxicator for input as well
There was a problem hiding this comment.
I discussed with @idosal that we might be able to have a generic onRequest prop added <AppRender />, as a catch-all that can allow goose to listen for unsupported/non-standard JSON RPC methods. Not to say that sampling/createMessage is nonstandard, just not supported by the client SDK...
There was a problem hiding this comment.
Yeah, I can wait for #7013 to go in and then adjust however needed. Feels like it would only be minor changes to use a better set of types etc.
Will stay tuned to this thread
6148d1b to
6fe5277
Compare
|
Awesome stuff @alexhancock! looks like you did manage to spin something cool on the plane ride back! |
6fe5277 to
89732b1
Compare
| body: JSON.stringify({ | ||
| messages: messages.map((m) => ({ | ||
| role: m.role, | ||
| content: m.content, |
There was a problem hiding this comment.
This request forwards content: m.content as-is, but MCP sampling messages typically use content as an array of blocks; if m.content isn’t serialized as an array, CreateMessageRequestParams deserialization will fail on the server.
| content: m.content, | |
| content: Array.isArray(m.content) | |
| ? m.content | |
| : [ | |
| { | |
| type: 'text', | |
| text: String(m.content ?? ''), | |
| }, | |
| ], |
| if (!response.ok) { | ||
| throw new Error(`Sampling request failed: ${response.statusText}`); | ||
| } | ||
| return (await response.json()) as McpMethodResponse['sampling/createMessage']; |
There was a problem hiding this comment.
Casting await response.json() directly to McpMethodResponse['sampling/createMessage'] is unsafe here because the backend returns an MCP CreateMessageResult (nested message), so callers will read the wrong fields unless you validate/transform the response.
| return (await response.json()) as McpMethodResponse['sampling/createMessage']; | |
| const raw = (await response.json()) as unknown; | |
| if ( | |
| !raw || | |
| typeof raw !== 'object' || | |
| !('message' in raw) || | |
| raw.message == null | |
| ) { | |
| throw new Error('Invalid sampling response format: missing message'); | |
| } | |
| const { message } = raw as { message: McpMethodResponse['sampling/createMessage'] }; | |
| return message; |
| const id = ++requestId; | ||
| pendingRequests.set(id, { resolve, reject }); | ||
| window.parent.postMessage({ jsonrpc: '2.0', id, method, params }, '*'); | ||
| }); |
There was a problem hiding this comment.
Using '*' as the targetOrigin for postMessage allows the JSON-RPC request to be sent to any embedding origin; use the known proxy origin (or at least window.location.origin) as the targetOrigin.
| addMessage('user', text); | ||
| conversationHistory.push({ role: 'user', content: { type: 'text', text } }); | ||
|
|
There was a problem hiding this comment.
conversationHistory pushes sampling messages with content as an object, but MCP sampling expects content to be an array of blocks, so the request shape won’t match what the host/server expects.
| Role::User => Message::user(), | ||
| Role::Assistant => Message::assistant(), | ||
| }; | ||
| content_to_message(base, &msg.content) |
There was a problem hiding this comment.
In this repo, rmcp sampling message content is handled as a list of blocks (msg.content.first() in crates/goose/src/agents/mcp_client.rs), but this route treats msg.content as a single Content; align the conversion with mcp_client.rs (iterate/take first block).
| content_to_message(base, &msg.content) | |
| if let Some(first_content) = msg.content.first() { | |
| content_to_message(base, first_content) | |
| } else { | |
| base | |
| } |
| fn content_to_message(base: Message, content: &Content) -> Message { | ||
| match &content.raw { | ||
| RawContent::Text(text) => base.with_text(&text.text), | ||
| RawContent::Image(image) => base.with_image(&image.data, &image.mime_type), | ||
| _ => base, | ||
| } |
There was a problem hiding this comment.
content_to_message assumes a single Content (content.raw), but sampling content is block-based in rmcp; update this helper to accept the sampling content block list and convert supported blocks (text/image) consistently.
| fn content_to_message(base: Message, content: &Content) -> Message { | |
| match &content.raw { | |
| RawContent::Text(text) => base.with_text(&text.text), | |
| RawContent::Image(image) => base.with_image(&image.data, &image.mime_type), | |
| _ => base, | |
| } | |
| fn content_to_message(mut base: Message, contents: &[Content]) -> Message { | |
| for content in contents { | |
| match &content.raw { | |
| RawContent::Text(text) => { | |
| base = base.with_text(&text.text); | |
| } | |
| RawContent::Image(image) => { | |
| base = base.with_image(&image.data, &image.mime_type); | |
| } | |
| _ => { | |
| // Ignore unsupported content types, preserving existing behavior. | |
| } | |
| } | |
| } | |
| base |
| Ok(Json(CreateMessageResult { | ||
| model: usage.model, | ||
| stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()), | ||
| message: SamplingMessage { | ||
| role: Role::Assistant, |
There was a problem hiding this comment.
The response construction uses SamplingMessage { content: Content::text(...) }, but goose’s existing MCP sampling implementation builds responses with SamplingMessage::new(..., SamplingMessageContent::text/ Image ...); using the wrong content type/shape here will break clients expecting MCP CreateMessageResult semantics.
| Router::new() | ||
| .route( | ||
| "/sessions/{session_id}/sampling/message", | ||
| post(create_message), | ||
| ) |
There was a problem hiding this comment.
Add an integration test for /sessions/{session_id}/sampling/message (similar to other route tests) to lock in the MCP request/response shape and prevent regressions.
|
|
||
| export type SamplingMessage = { | ||
| role: 'user' | 'assistant'; | ||
| content: { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }; |
There was a problem hiding this comment.
SamplingMessage.content should be an array of content blocks (the rmcp sampling types are used as a list in crates/goose/src/agents/mcp_client.rs via msg.content.first()), so modeling it as a single block will break interop/deserialization.
| content: { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }; | |
| content: ContentBlock[]; |
89732b1 to
648ed51
Compare
|
interesting - I like this of course, but i think we can do this simpler by just adding a tool to the apps platform extension, call_llm (or sample but I always found that confusing). we already have the plumbing in mcp apps for an app to call the mcp server that created it, so that should then all work. all we need to do is then add this to the prompt we use to generate or update the apps. let me know if you think this makes sense or we can chat about this? |
It does makes sense in the context of goose and the presence of that server, but I was trying to think of a way to do it that could work when the app is run in hosts other than goose as well. Would still need plumbing added in other hosts of course, but an app sending a |
|
yeah, but it will only work in other providers if this becomes part of the standard. we should push for that! but until that happens, what do we do? /cc @aharvard ? |
|
@idosal do you think adding sampling to the protocol like this would make sense? |
Wire up the onFallbackRequest callback on AppRenderer to handle unrecognized MCP JSON-RPC requests. This is a stub that logs the request method and returns success, with a todo to implement sampling/createMessage per #7039. - Import RequestHandlerExtra from @mcp-ui/client - Import JSONRPCRequest from @modelcontextprotocol/sdk/types.js - Add handleFallbackRequest callback with correct types - Pass handler to AppRenderer's onFallbackRequest prop
Add a fallback request handler to gracefully handle unrecognized MCP protocol requests from guest apps (e.g. sampling/createMessage). Changes: - Import RequestHandlerExtra and JSONRPCRequest types - Add handleFallbackRequest callback that logs and returns success - Wire onFallbackRequest prop to AppRenderer Ref: #7039
Add a fallback request handler to McpAppRenderer that catches unhandled MCP requests from the app iframe. This is a stub that logs the request method and returns success, with a todo to implement sampling/createMessage per #7039. Changes: - Import RequestHandlerExtra from @mcp-ui/client - Import JSONRPCRequest from @modelcontextprotocol/sdk/types.js - Add handleFallbackRequest useCallback - Wire onFallbackRequest prop to AppRenderer
Upgrade @mcp-ui/client from ^6.0.0 to ^6.1.0 to get the new onFallbackRequest prop on AppRenderer. Add a fallback request handler that catches unhandled MCP requests from the app iframe. This is a stub that logs the request method and returns success, with a todo to implement sampling/createMessage per #7039. Changes: - Upgrade @mcp-ui/client to ^6.1.0 - Import RequestHandlerExtra from @mcp-ui/client - Import JSONRPCRequest from @modelcontextprotocol/sdk/types.js - Add handleFallbackRequest async callback - Wire onFallbackRequest prop to AppRenderer
As to what we do, we can now use the new @alexhancock I upgraded to Handling |
648ed51 to
e9f1981
Compare
6c95970 to
b857bf8
Compare
b857bf8 to
fdb6520
Compare
| systemPrompt, | ||
| maxTokens, |
There was a problem hiding this comment.
The request body sends fields in camelCase (systemPrompt, maxTokens), but CreateMessageRequestParams from rmcp likely expects snake_case (system_prompt, max_tokens) following Rust conventions. This mismatch may cause deserialization to fail or the fields to be silently ignored. Verify the expected format from the rmcp library and ensure the frontend matches it.
| systemPrompt, | |
| maxTokens, | |
| system_prompt: systemPrompt, | |
| max_tokens: maxTokens, |
| const responseText = response.content.text; | ||
| conversationHistory.push({ role: 'assistant', content: { type: 'text', text: responseText } }); | ||
|
|
||
| loadingEl.textContent = responseText; |
There was a problem hiding this comment.
The response structure doesn't match what the frontend expects. The CreateMessageResult from rmcp returns { model, stop_reason, message: { role, content } }, but the code tries to access response.content.text. This should be response.message.content.text or similar, depending on how SamplingMessage serializes its content field (it may be an array or object).
| const responseText = response.content.text; | |
| conversationHistory.push({ role: 'assistant', content: { type: 'text', text: responseText } }); | |
| loadingEl.textContent = responseText; | |
| const message = response && response.message ? response.message : response; | |
| let responseText = ''; | |
| if (message && Array.isArray(message.content)) { | |
| const textPart = message.content.find( | |
| (part) => part && typeof part === 'object' && typeof part.text === 'string' | |
| ); | |
| if (textPart) { | |
| responseText = textPart.text; | |
| } | |
| } else if (message && message.content && typeof message.content === 'object') { | |
| if (typeof message.content.text === 'string') { | |
| responseText = message.content.text; | |
| } | |
| } | |
| conversationHistory.push({ role: 'assistant', content: { type: 'text', text: responseText } }); | |
| loadingEl.textContent = responseText || '[no response text]'; |
| }; | ||
| } | ||
| msg | ||
| } |
There was a problem hiding this comment.
The sampling route lacks test coverage. Other similar routes in the codebase (e.g., action_required.rs at lines 63-100, reply.rs at lines 468-506) have integration tests. Consider adding tests to verify the sampling endpoint works correctly, especially given the data transformation between MCP protocol types and the response format expected by the frontend.
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| #[test] | |
| fn content_to_message_with_empty_multiple_does_not_panic() { | |
| let base = Message::user(); | |
| let content: SamplingContent<SamplingMessageContent> = SamplingContent::Multiple(vec![]); | |
| let _result = content_to_message(base, &content); | |
| } | |
| } |
Yes this all sounds right to me Putting together a proposal here https://gist.github.com/alexhancock/2bb8b62500f33ccb29a0edb4aa7c5342 @liady @idosal @DOsinga @aharvard @ochafik What's the best forum to actually submit something like this? |
aharvard
left a comment
There was a problem hiding this comment.
This is gonna be a fundamental unlock. Great work!
Co-authored-by: Andrew Harvard <aharvard@block.xyz>
fdb6520 to
b8334e0
Compare
Co-authored-by: Andrew Harvard <aharvard@block.xyz>
b8334e0 to
e2917a2
Compare
| if (!response.ok) { | ||
| throw new Error(`Sampling request failed: ${response.statusText}`); | ||
| } | ||
| return (await response.json()) as SamplingCreateMessageResponse; |
There was a problem hiding this comment.
The sampling fallback returns the raw JSON from /sampling/message, but the backend returns an MCP CreateMessageResult object (with a nested message field), so this will not satisfy the expected UI types/consumers. Either transform the response into the shape expected by the app bridge, or update the types and downstream code to handle the message wrapper.
| return (await response.json()) as SamplingCreateMessageResponse; | |
| const data = await response.json(); | |
| const result = | |
| data && typeof data === 'object' && 'message' in data | |
| ? (data.message as SamplingCreateMessageResponse) | |
| : (data as SamplingCreateMessageResponse); | |
| return result; |
| // On StrictMode remount, replay the cached result instead of re-fetching. | ||
| if (fetchedDataRef.current) { | ||
| const { html: cachedResult, meta: cachedMeta } = fetchedDataRef.current; | ||
| dispatch({ type: 'RESOURCE_LOADED', html: cachedResult, meta: cachedMeta }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
fetchedDataRef/sandboxUrlRef are reused across prop changes; if resourceUri/extensionName changes without unmounting this component, the effect will early-return and replay stale HTML/meta/sandbox URL for the previous app. Clear these refs (and reset state) when the identifying props change, or key the component by resourceUri at the call site to force a remount.
| try { | ||
| const response = await request('sampling/createMessage', { | ||
| messages: conversationHistory, | ||
| systemPrompt: 'You are a helpful assistant. Keep responses concise.', | ||
| maxTokens: 1000 | ||
| }); | ||
|
|
||
| const responseText = response.content.text; | ||
| conversationHistory.push({ role: 'assistant', content: { type: 'text', text: responseText } }); | ||
|
|
||
| loadingEl.textContent = responseText; | ||
| loadingEl.classList.remove('loading'); |
There was a problem hiding this comment.
The response from sampling/createMessage is an MCP CreateMessageResult with a nested message field, so response.content.text will be undefined and the demo UI won't render replies. Read response.message.content (and handle non-text content) or adjust the host to return the flattened shape.
| 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); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
The message event handler accepts postMessages from any source/origin, which lets unrelated frames/scripts spoof JSON-RPC responses and resolve/reject pending requests. Restrict handling to event.source === window.parent (and, if possible, validate event.origin against the expected sandbox/host origin).
| ) | ||
| .with_state(state) | ||
| } | ||
|
|
There was a problem hiding this comment.
This new HTTP endpoint isn't annotated with #[utoipa::path] and isn't added to crates/goose-server/src/openapi.rs's paths(...) list, so it won't appear in the generated OpenAPI schema unlike the other routes. If this is intended to be a supported API for the desktop app, add the utoipa path annotation (and register it in ApiDoc) so schema validation/clients stay in sync.
| #[utoipa::path( | |
| post, | |
| path = "/sessions/{session_id}/sampling/message", | |
| params( | |
| ("session_id" = String, Path, description = "Session identifier") | |
| ), | |
| request_body = CreateMessageRequestParams, | |
| responses( | |
| (status = 200, description = "Sampling message created", body = CreateMessageResult), | |
| (status = 500, description = "Internal server error") | |
| ) | |
| )] |
* 'main' of github.com:block/goose: (40 commits) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) docs: remove ALPHA_FEATURES flag from documentation (#7315) docs: escape variable syntax in recipes (#7314) docs: update OTel environment variable and config guides (#7221) docs: system proxy settings (#7311) docs: add Summon extension tutorial and update Skills references (#7310) docs: agent session id (#7289) ...
* origin/main: fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316)
* origin/main: (29 commits) fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) docs: remove ALPHA_FEATURES flag from documentation (#7315) docs: escape variable syntax in recipes (#7314) ... # Conflicts: # ui/desktop/src/components/McpApps/McpAppRenderer.tsx # ui/desktop/src/components/McpApps/types.ts
PR #6933 (local inference provider) was based on a stale branch that didn't include #7039's sampling changes to McpAppRenderer.tsx. When it merged, it silently reverted all client-side sampling plumbing: - RequestHandlerExtra and JSONRPCRequest imports - SamplingCreateMessageParams/Response type imports - apiHost/secretKey state + initialization useEffect - handleFallbackRequest callback (sampling/createMessage handler) - onFallbackRequest prop on AppRenderer The server-side route (sampling.rs), types.ts, and chat.html all survived — only the McpAppRenderer.tsx wiring was lost. This restores the exact code that #7039 added and #6933 removed.
* 'main' of github.com:block/goose: (24 commits) Docs: claude code uses stream-json (#7358) Improve link confirmation modal (#7333) fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) ...
* main: (46 commits) chore(deps): bump hono from 4.11.9 to 4.12.0 in /ui/desktop (#7369) Include 3rd-party license copy for JavaScript/CSS minified files (#7352) docs for reasoning env var (#7367) docs: update skills detail page to reference Goose Summon extension (#7350) fix(apps): restore MCP app sampling support reverted by #6933 (#7366) feat: TUI client of goose-acp (#7362) docs: agent variable (#7365) docs: pass env vars to shell (#7361) docs: update sandbox topic (#7336) feat: add local inference provider with llama.cpp backend and HuggingFace model management (#6933) Docs: claude code uses stream-json (#7358) Improve link confirmation modal (#7333) fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) ...
* origin/main: (62 commits) Docs: claude code uses stream-json (#7358) Improve link confirmation modal (#7333) fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) ... # Conflicts: # crates/goose-server/src/main.rs
Summary
sampling/createMessagereqsgoosedfor handlingDemo
Request Flow