MCP Apps (1/3): backend plumbing — tool _meta.ui, resources/read, app-resource endpoint - #911
MCP Apps (1/3): backend plumbing — tool _meta.ui, resources/read, app-resource endpoint#911nickmisasi wants to merge 10 commits into
_meta.ui, resources/read, app-resource endpoint#911Conversation
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Collapse origin resolution into ClientManager.ReadUserAppResource (ErrServerNotConfigured), connect only the requested enabled server, map live resources/read 401s through oauthNeededError, and harden ui:// URI/MIME/UTF-8/size validation (exact URI match, 4MB cap). Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Centralize tool_use/tool_result ContentBlock scanning in conversation, anchor to the given post_id span, and reject ambiguous duplicate tool_call_id matches so reused IDs cannot cross-authorize. Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Return MCP ReadResourceResult wire JSON on success; gate D6(b) on tool_use.Shared; map ErrServerNotConfigured/OAuth/invalid MIME via typed errors with fixed non-sensitive 502 messages; use FindToolCallBlocks for post-anchored lookup. Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Close any map entry overwritten in connectToServer so concurrent cold ConnectToRemoteServer calls cannot leak a session, and remove the unused getClientForUser helper (tests now hit getOrCreateUserClientsShell). Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
🤖 LLM Evaluation ResultsOpenAI
❌ Failed EvaluationsShow 7 failuresOPENAI1. TestReactEval/[openai]_react_cat_message
2. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json
3. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json
4. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json
5. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json
6. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json
7. TestDirectMessageConversations/[openai]_bot_dm_tool_introspection
Anthropic
❌ Failed EvaluationsShow 6 failuresANTHROPIC1. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json
2. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json
3. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json
4. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json
5. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json
6. TestDirectMessageConversations/[anthropic]_bot_dm_tool_introspection
This comment was automatically generated by the eval CI pipeline. |
📝 WalkthroughWalkthroughAdds MCP Apps UI metadata support across tools and conversations, validated user-scoped app-resource retrieval with reconnect and OAuth handling, and an authenticated ChangesMCP Apps UI support
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant AppClient
participant APIHandler
participant MCPClientManager
participant MCPServer
AppClient->>APIHandler: GET /mcp/app-resource
APIHandler->>MCPClientManager: ReadUserAppResource(userID, serverOrigin, uri)
MCPClientManager->>MCPServer: resources/read(uri)
MCPServer-->>MCPClientManager: App resource content
MCPClientManager-->>APIHandler: AppResource or mapped error
APIHandler-->>AppClient: JSON response
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8)go.modTraceback (most recent call last): Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
conversation/convert.go (1)
83-97: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winClear
UIMetaon the redaction path.ArgumentsandMCPBareNameare stripped whenredactToolUseis true, butblock.UIMetastill gets copied through, leaving app metadata on unshared tool calls. That matches neitherFilterForNonRequesternorredactToolCalls, both of which clearUIMeta.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@conversation/convert.go` around lines 83 - 97, The redactToolUse branch in the tool-call conversion must also clear toolCall.UIMeta, matching the existing redaction behavior for MCPBareName and Arguments and the conventions in FilterForNonRequester and redactToolCalls. Keep UIMeta intact on the non-redacted enrichment path.
🧹 Nitpick comments (2)
mcp/user_clients.go (1)
180-188: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winMove
existing.Close()out from underclientsMu.Client.Close()synchronously tears down the MCP session, so holding the write lock during that call blocks concurrentGetTools/hasClientreaders until shutdown completes. Capture the old client, swap it in the map, unlock, then close the replaced client.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mcp/user_clients.go` around lines 180 - 188, Update the client replacement logic in the Client connection flow to capture the existing client, assign serverClient under clientsMu, then release the lock before calling existing.Close(). Preserve replacement behavior and only close the captured client after unlocking, so GetTools and hasClient are not blocked during shutdown.conversation/find_tool_call_test.go (1)
15-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider a table-driven structure for the four scenarios.
As per path instructions,
**/*_test.go: "Go tests must be table-driven when they contain more than one case." This test has four distinct cases implemented as separatet.Runblocks rather than a[]struct{...}+ range loop.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@conversation/find_tool_call_test.go` around lines 15 - 98, Refactor TestFindToolCallBlocksPostAnchored into a table-driven test with a []struct of four cases and a range over t.Run. Keep the shared turns, mustBlocks helper, per-case inputs, expected blocks/error assertions, and scenario coverage unchanged while consolidating the duplicated subtests.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/api_mcp_app_resource.go`:
- Around line 140-162: Add a bounded per-call timeout around the
ReadUserAppResource invocation, deriving the context from c.Request.Context()
and passing the timed context instead of the uncancelled request context. Ensure
the timeout covers both remote connection and resources/read operations while
preserving the existing error handling and response behavior.
In `@go.mod`:
- Line 19: Enable CrossOriginProtection explicitly in every
NewStreamableHTTPHandler call within mcpserver/http_server.go,
mcpserver/plugin_handlers.go, and external/pluginmcp/server.go, configuring the
handler options so the /mcp endpoints reject cross-site requests.
---
Outside diff comments:
In `@conversation/convert.go`:
- Around line 83-97: The redactToolUse branch in the tool-call conversion must
also clear toolCall.UIMeta, matching the existing redaction behavior for
MCPBareName and Arguments and the conventions in FilterForNonRequester and
redactToolCalls. Keep UIMeta intact on the non-redacted enrichment path.
---
Nitpick comments:
In `@conversation/find_tool_call_test.go`:
- Around line 15-98: Refactor TestFindToolCallBlocksPostAnchored into a
table-driven test with a []struct of four cases and a range over t.Run. Keep the
shared turns, mustBlocks helper, per-case inputs, expected blocks/error
assertions, and scenario coverage unchanged while consolidating the duplicated
subtests.
In `@mcp/user_clients.go`:
- Around line 180-188: Update the client replacement logic in the Client
connection flow to capture the existing client, assign serverClient under
clientsMu, then release the lock before calling existing.Close(). Preserve
replacement behavior and only close the captured client after unlocking, so
GetTools and hasClient are not blocked during shutdown.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: f4324647-74ec-4ff1-aaf3-0a907219f7de
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (32)
api/api.goapi/api_conversation_test.goapi/api_mcp_app_resource.goapi/api_mcp_app_resource_test.goapi/api_test.goconversation/content_block.goconversation/content_block_test.goconversation/convert.goconversation/convert_test.goconversation/find_tool_call.goconversation/find_tool_call_test.goconversation/helpers.goconversation/helpers_test.gogo.modllm/tools.gollm/tools_test.gollm/tools_ui.gomcp/apps_meta.gomcp/apps_meta_test.gomcp/client.gomcp/client_manager.gomcp/client_manager_test.gomcp/client_test.gomcp/oauth_manager.gomcp/resources.gomcp/resources_test.gomcp/user_clients.gomcp/user_clients_test.gostreaming/streaming.gostreaming/turn_persistence_test.gotoolrunner/toolrunner.gotoolrunner/toolrunner_test.go
| res, err := a.mcpClientManager.ReadUserAppResource(c.Request.Context(), userID, toolUse.ServerOrigin, toolUse.UIMeta.ResourceURI) | ||
| if err != nil { | ||
| var oauthErr *mcp.OAuthNeededError | ||
| if errors.As(err, &oauthErr) { | ||
| writeAppResourceError(c, http.StatusUnauthorized, appResourceErrAuthRequired, "MCP authentication required", oauthErr.AuthURL()) | ||
| return | ||
| } | ||
| if errors.Is(err, mcp.ErrServerNotConfigured) { | ||
| writeAppResourceError(c, http.StatusNotFound, appResourceErrNotFound, "server no longer configured", "") | ||
| return | ||
| } | ||
| var invalidErr *mcp.InvalidAppResourceError | ||
| if errors.As(err, &invalidErr) { | ||
| a.pluginAPI.Log.Error("Invalid MCP app resource", | ||
| "userID", userID, "serverOrigin", toolUse.ServerOrigin, "uri", toolUse.UIMeta.ResourceURI, "error", invalidErr) | ||
| writeAppResourceError(c, http.StatusBadGateway, appResourceErrInvalidResourceMime, "invalid app resource", "") | ||
| return | ||
| } | ||
| a.pluginAPI.Log.Error("MCP app resource upstream failure", | ||
| "userID", userID, "serverOrigin", toolUse.ServerOrigin, "uri", toolUse.UIMeta.ResourceURI, "error", err) | ||
| writeAppResourceError(c, http.StatusBadGateway, appResourceErrUpstreamUnreachable, "MCP server unreachable", "") | ||
| return | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -A5 -B5 'context.WithTimeout|context.WithDeadline' mcp/resources.go mcp/client.goRepository: mattermost/mattermost-plugin-agents
Length of output: 173
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file list ==\n'
git ls-files | rg '(^|/)(mcp|api)/.*\.(go|ts|tsx)$|^mcp/|^api/api_mcp_app_resource\.go$'
printf '\n== locate ReadUserAppResource ==\n'
rg -n 'ReadUserAppResource|WithTimeout|WithDeadline|context\.Background|context\.TODO' api mcp
printf '\n== map candidate files ==\n'
for f in api/api_mcp_app_resource.go mcp/resources.go mcp/client.go; do
if [ -f "$f" ]; then
echo "--- $f ---"
ast-grep outline "$f" --view compact || true
fi
done
printf '\n== relevant slices ==\n'
for f in mcp/resources.go mcp/client.go api/api_mcp_app_resource.go; do
if [ -f "$f" ]; then
echo "--- $f ---"
nl -ba "$f" | sed -n '1,260p'
fi
doneRepository: mattermost/mattermost-plugin-agents
Length of output: 18718
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== ReadUserAppResource implementation =='
sed -n '230,320p' mcp/resources.go
echo
echo '== client manager timeout-related path =='
sed -n '130,190p' mcp/user_clients.go
echo
echo '== client manager wrapper =='
sed -n '1,120p' mcp/client_manager.go
echo
echo '== client read path =='
sed -n '1,260p' mcp/client.go | sed -n '1,260p'Repository: mattermost/mattermost-plugin-agents
Length of output: 17777
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== user_clients remote connect =='
sed -n '1,140p' mcp/user_clients.go
echo
sed -n '140,240p' mcp/user_clients.go
echo
echo '== client session setup =='
sed -n '260,520p' mcp/client.go
echo
echo '== search for connect/read timeouts in mcp package =='
rg -n 'WithTimeout|WithDeadline|Deadline|timeout' mcp/client.go mcp/user_clients.go mcp/http_client.go mcp/plugin_roundtripper.go mcp/oauth_transport.go mcp/dcrp.goRepository: mattermost/mattermost-plugin-agents
Length of output: 15630
Add a bounded timeout to the remote MCP app-resource path
ReadUserAppResource passes cacheableContext(ctx) into ConnectToRemoteServer(...), which drops request cancellation. If the MCP server hangs during connect or resources/read, this request can block indefinitely; use a per-call timeout here instead of relying on the client connection.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/api_mcp_app_resource.go` around lines 140 - 162, Add a bounded per-call
timeout around the ReadUserAppResource invocation, deriving the context from
c.Request.Context() and passing the timed context instead of the uncancelled
request context. Ensure the timeout covers both remote connection and
resources/read operations while preserving the existing error handling and
response behavior.
| github.com/mattermost/testcontainers-mattermost-go v0.1.0 | ||
| github.com/maximhq/bifrost/core v1.5.18 | ||
| github.com/modelcontextprotocol/go-sdk v1.4.1 | ||
| github.com/modelcontextprotocol/go-sdk v1.6.1 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate where the go-sdk StreamableHTTPHandler is constructed and check for explicit CrossOriginProtection config.
rg -n -A5 -B5 'NewStreamableHTTPHandler|StreamableHTTPOptions' --type=goRepository: mattermost/mattermost-plugin-agents
Length of output: 173
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== go.mod ==\n'
cat -n go.mod | sed -n '1,80p'
printf '\n== sdk and MCP references ==\n'
rg -n -A4 -B4 'modelcontextprotocol/go-sdk|CrossOriginProtection|enableoriginverification|StreamableHTTP|mcpServerGroup.Any\("/mcp"|mcpAuthMiddleware' --type=go .Repository: mattermost/mattermost-plugin-agents
Length of output: 34595
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file list ==\n'
git ls-files | rg '(^|/)(go\.mod|.*\.go)$'
printf '\n== candidate file outlines ==\n'
for f in $(git ls-files | rg '(^|/)(go\.mod|.*\.go)$'); do
case "$f" in
*go.mod|*go.sum) continue ;;
esac
doneRepository: mattermost/mattermost-plugin-agents
Length of output: 13100
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== go.mod line refs ==\n'
cat -n go.mod | sed -n '1,60p'
printf '\n== MCP-related Go files ==\n'
rg -l --type=go 'mcp|StreamableHTTP|CrossOriginProtection|modelcontextprotocol/go-sdk' .
printf '\n== likely handler construction snippets ==\n'
for f in $(rg -l --type=go 'mcp|StreamableHTTP|CrossOriginProtection|modelcontextprotocol/go-sdk' .); do
echo "--- $f ---"
rg -n -A8 -B8 'mcp|StreamableHTTP|CrossOriginProtection|modelcontextprotocol/go-sdk' "$f"
doneRepository: mattermost/mattermost-plugin-agents
Length of output: 50391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== direct imports / module references ==\n'
rg -n 'github.com/modelcontextprotocol/go-sdk|modelcontextprotocol/go-sdk' .Repository: mattermost/mattermost-plugin-agents
Length of output: 3495
🌐 Web query:
modelcontextprotocol go-sdk v1.6.1 StreamableHTTPHandler CrossOriginProtection release notes
💡 Result:
There are no public, dedicated release notes for version 1.6.1 of the Model Context Protocol (MCP) Go SDK. The version v1.6.1 was released on May 22, 2026 [1]. Regarding the technical components mentioned in your query: StreamableHTTPHandler The StreamableHTTPHandler is a core component within the MCP Go SDK (found in the mcp package) designed to serve streamable MCP sessions over HTTP, conforming to the MCP specification [2][3]. It manages HTTP requests for MCP sessions, including session lifecycle and transport handling [2][4]. CrossOriginProtection The CrossOriginProtection field is a configuration option available within StreamableHTTPOptions used by the StreamableHTTPHandler [2]. In the implementation, if CrossOriginProtection is not explicitly set and a specific internal flag (enableoriginverification) is enabled, the handler initializes it with a default http.CrossOriginProtection instance [2]. This functionality is part of the SDK's effort to provide secure transport primitives, including support for cross-origin verification [2]. For detailed changes in specific versions, you can review the official repository's commit history or issues tracker, where features like these are typically discussed as proposals or pull requests [5][6][7].
Citations:
- 1: https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk
- 2: https://github.com/modelcontextprotocol/go-sdk/blob/main/mcp/streamable.go
- 3: https://go.googlesource.com/tools/+/36360cdf844bb2553617bdc496c8d972c7229e7d/internal/mcp/streamable.go
- 4: https://github.com/modelcontextprotocol/go-sdk/blob/31e97ad69edad679c726449c4584353502965cf5/mcp/streamable.go
- 5: https://github.com/modelcontextprotocol/go-sdk
- 6: Proposal: add StreamableHTTPHandler.Close modelcontextprotocol/go-sdk#440
- 7: Proposal: Export callback to StreamableHTTPHandler for closed transports modelcontextprotocol/go-sdk#479
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== origin-protection references ==\n'
rg -n 'CrossOriginProtection|enableoriginverification' .
printf '\n== mcpserver/http_server.go ==\n'
cat -n mcpserver/http_server.go | sed -n '88,112p'
printf '\n== mcpserver/plugin_handlers.go ==\n'
cat -n mcpserver/plugin_handlers.go | sed -n '88,110p'
printf '\n== external/pluginmcp/server.go ==\n'
cat -n external/pluginmcp/server.go | sed -n '1,120p'
printf '\n== api/mcp_handlers.go ==\n'
cat -n api/mcp_handlers.go | sed -n '1,120p'Repository: mattermost/mattermost-plugin-agents
Length of output: 209
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== StreamableHTTPHandler call sites in production code ==\n'
rg -n -A3 -B3 'NewStreamableHTTPHandler\(' api mcp mcpserver external/pluginmcp server
printf '\n== files that mention CrossOriginProtection in go-sdk imports or docs ==\n'
rg -n 'CrossOriginProtection' api mcp mcpserver external/pluginmcp serverRepository: mattermost/mattermost-plugin-agents
Length of output: 4401
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== mcpserver/http_server.go (90-106) ==\n'
cat -n mcpserver/http_server.go | sed -n '90,106p'
printf '\n== mcpserver/plugin_handlers.go (90-110) ==\n'
cat -n mcpserver/plugin_handlers.go | sed -n '90,110p'
printf '\n== external/pluginmcp/server.go (1-120) ==\n'
cat -n external/pluginmcp/server.go | sed -n '1,120p'
printf '\n== api/api.go (286-296) ==\n'
cat -n api/api.go | sed -n '286,296p'Repository: mattermost/mattermost-plugin-agents
Length of output: 4056
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== mcpserver/http_server.go (1-140) ==\n'
cat -n mcpserver/http_server.go | sed -n '1,140p'
printf '\n== mcpserver/plugin_handlers.go (1-140) ==\n'
cat -n mcpserver/plugin_handlers.go | sed -n '1,140p'Repository: mattermost/mattermost-plugin-agents
Length of output: 11094
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== external/pluginmcp/server.go (1-120) ==\n'
cat -n external/pluginmcp/server.go | sed -n '1,120p'
printf '\n== mcpserver/http_server.go (88-120) ==\n'
cat -n mcpserver/http_server.go | sed -n '88,120p'
printf '\n== mcpserver/plugin_handlers.go (88-120) ==\n'
cat -n mcpserver/plugin_handlers.go | sed -n '88,120p'Repository: mattermost/mattermost-plugin-agents
Length of output: 4788
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== external/pluginmcp/server.go ServeHTTP and handler setup ==\n'
rg -n -A20 -B10 'func \(s \*Server\) ServeHTTP|NewStreamableHTTPHandler|StreamableHTTPOptions' external/pluginmcp/server.go
printf '\n== mcpserver/http_server.go streamable setup ==\n'
rg -n -A10 -B5 'NewStreamableHTTPHandler|StreamableHTTPOptions' mcpserver/http_server.go
printf '\n== mcpserver/plugin_handlers.go streamable setup ==\n'
rg -n -A10 -B5 'NewStreamableHTTPHandler|StreamableHTTPOptions' mcpserver/plugin_handlers.goRepository: mattermost/mattermost-plugin-agents
Length of output: 3222
Enable CrossOriginProtection on the Streamable HTTP handlers here. go-sdk v1.6.1 no longer enables it by default, and mcpserver/http_server.go, mcpserver/plugin_handlers.go, and external/pluginmcp/server.go all call NewStreamableHTTPHandler(...) without it, leaving /mcp open to cross-site requests.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go.mod` at line 19, Enable CrossOriginProtection explicitly in every
NewStreamableHTTPHandler call within mcpserver/http_server.go,
mcpserver/plugin_handlers.go, and external/pluginmcp/server.go, configuring the
handler options so the /mcp endpoints reject cross-site requests.
Summary
First PR in a 3-PR stack adding MCP Apps (
io.modelcontextprotocol/ui, spec 2026-01-26) host support to the Agents plugin. This PR is backend-only plumbing; nothing user-visible changes yet.modelcontextprotocol/go-sdkto v1.6.1 and declares theio.modelcontextprotocol/uiextension (with requiredmimeTypes: ["text/html;profile=mcp-app"]) in client capabilities during MCP handshake._meta.ui(canonical nested form; deprecated flatui/resourceUriaccepted as fallback) intollm.Tool, and plumbs it throughllm.ToolCall→conversation.ContentBlock(tool_use) → thepostupdatewebsocket payload andGET /conversations/:id. Tools withvisibility: ["app"]are hidden from the model. UI metadata is redacted for non-requesters until the result is shared.ReadAppResourceto the MCP client layer: per-user, lazily connects only the target server, locked session access with single-flight reconnect, 4 MiB size cap, exact-URI match, UTF-8 andtext/html;profile=mcp-appMIME validation.GET /mcp/app-resource?post_id=&tool_call_id=: derives the server origin andui://URI exclusively from the persisted tool_use block (unforgeable), enforces the authorization invariant — caller can read the conversation, result shared or caller is requester, caller has/establishes their own session to that server — and returns a spec-shaped MCPReadResourceResult(contents[].uri/mimeType/text/_meta.ui). Error taxonomy:401 mcp_auth_required(+ OAuth start URL),403 forbidden,404,502with fixed non-sensitive messages.conversation.FindToolCallBlocksresolves tool calls scoped to the response owned by the anchoring post, rejecting ambiguous duplicate tool-call IDs.Review process: implemented via a prescriptive plan, then two independent strict review passes with all blockers/majors remediated (session concurrency single-flight, lazy origin-targeted connect, response-owned turn spans, spec-shaped response contract, resource boundary hardening, error hygiene).
Testing: table-driven unit tests throughout (
go test -raceclean) — endpoint authorization matrix (20+ cases incl. cross-round duplicate-ID and needs-auth vs forbidden), meta parsing edge cases, concurrent reconnect races, live-401-during-read OAuth detection against real in-memory/HTTP go-sdk fixtures. End-to-end validation (browser demo with a real MCP App) is in the final PR of the stack.Release Note
Summary by CodeRabbit
New Features
Bug Fixes