@@ -11,13 +11,24 @@ import {
1111 type SpillBlobWriter ,
1212} from "../plugins/result-truncation-plugin.js" ;
1313import type { CompactionArchive } from "../session/compaction-archive.js" ;
14- import type { MCPClient , MCPContentBlock } from "./client.js" ;
14+ import type {
15+ MCPClient ,
16+ MCPContentBlock ,
17+ MCPToolResultEnvelope ,
18+ } from "./client.js" ;
1519import { mcpToolName } from "./tool-name.js" ;
1620import { unwrapToolContent } from "./client.js" ;
1721
1822export const MCP_RECONNECTING_TOOL_ERROR =
1923 "MCP server is reconnecting; retry the call once it reports connected." ;
2024
25+ /**
26+ * Stable marker prefixing JSON-serialized `structuredContent` when it is the
27+ * only payload (or supplements an empty flatten). Lets the model — and log
28+ * grep — distinguish server-structured data from free text.
29+ */
30+ export const MCP_STRUCTURED_CONTENT_MARKER = "mcp structured result:" ;
31+
2132/** True while the server keeps its tools mounted but cannot execute. */
2233export function isDegradedMcpState ( state : { state : string } ) : boolean {
2334 return state . state === "reconnecting" ;
@@ -90,22 +101,51 @@ export function mcpClientTools(
90101 signal : AbortSignal ,
91102 ) : Promise < ToolResult > => {
92103 try {
93- const rawBlocks =
94- typeof client . callBlocks === "function"
95- ? await client . callBlocks ( tool . name , call . arguments , signal )
96- : [
97- {
98- type : "text" ,
99- text : await client . call ( tool . name , call . arguments , signal ) ,
100- } satisfies MCPContentBlock ,
101- ] ;
102- const authorizedBlocks = applyPolicyToBlocks ( rawBlocks ) ;
104+ const envelope : MCPToolResultEnvelope =
105+ typeof client . callResult === "function"
106+ ? await client . callResult ( tool . name , call . arguments , signal )
107+ : typeof client . callBlocks === "function"
108+ ? {
109+ blocks : await client . callBlocks (
110+ tool . name ,
111+ call . arguments ,
112+ signal ,
113+ ) ,
114+ isError : false ,
115+ }
116+ : {
117+ blocks : [
118+ {
119+ type : "text" ,
120+ text : await client . call (
121+ tool . name ,
122+ call . arguments ,
123+ signal ,
124+ ) ,
125+ } satisfies MCPContentBlock ,
126+ ] ,
127+ isError : false ,
128+ } ;
129+ const authorizedBlocks = applyPolicyToBlocks ( envelope . blocks ) ;
130+ const scrubbedStructured =
131+ envelope . structuredContent === undefined
132+ ? undefined
133+ : ( scrubSecretShapedValue ( envelope . structuredContent ) as Record <
134+ string ,
135+ unknown
136+ > ) ;
103137 const archive = getEvidenceArchive ?.( ) ;
104138 if ( archive !== undefined ) {
105139 try {
106140 await archive . recordAuthorizedPayload ( {
107141 kind : "tool_result" ,
108- payload : { blocks : authorizedBlocks } ,
142+ payload :
143+ scrubbedStructured === undefined
144+ ? { blocks : authorizedBlocks }
145+ : {
146+ blocks : authorizedBlocks ,
147+ structuredContent : scrubbedStructured ,
148+ } ,
109149 callId : call . id ,
110150 provenance : "mcp:post-policy-pre-flatten" ,
111151 } ) ;
@@ -114,6 +154,15 @@ export function mcpClientTools(
114154 }
115155 }
116156 const flattened = unwrapToolContent ( authorizedBlocks ) ;
157+ const isError = envelope . isError === true ;
158+ const baseContent =
159+ flattened !== ""
160+ ? flattened
161+ : scrubbedStructured !== undefined
162+ ? `${ MCP_STRUCTURED_CONTENT_MARKER } \n${ JSON . stringify ( scrubbedStructured ) } `
163+ : isError
164+ ? `MCP tool ${ client . serverName } /${ tool . name } reported an error with empty content.`
165+ : flattened ;
117166 const writeBlob = getBlobWriter ?.( ) ;
118167 const contextDir = getContextDir ?.( ) ;
119168 const spill =
@@ -124,8 +173,15 @@ export function mcpClientTools(
124173 ...( contextDir !== undefined ? { contextDir } : { } ) ,
125174 }
126175 : undefined ;
127- const content = await sanitizeMcpResultContent ( flattened , spill ) ;
128- return { callId : call . id , content } ;
176+ const content = await sanitizeMcpResultContent ( baseContent , spill ) ;
177+ return {
178+ callId : call . id ,
179+ content,
180+ ...( isError ? { isError : true as const } : { } ) ,
181+ ...( scrubbedStructured !== undefined
182+ ? { detail : scrubbedStructured }
183+ : { } ) ,
184+ } ;
129185 } catch ( err ) {
130186 const message = err instanceof Error ? err . message : String ( err ) ;
131187 const scrubbed = scrubSecretShapedContent ( message ) ;
0 commit comments