@@ -26,6 +26,7 @@ import {
2626 mcpPresentationCleanupKeys ,
2727 planForkMcpPresentations ,
2828} from '@/lib/mcp/presentation-lifecycle'
29+ import { loadMcpPresentation } from '@/lib/mcp/presentation-storage'
2930import { mcpService } from '@/lib/mcp/service'
3031import { changeChatResources } from '@/lib/mothership/chat/application/change-resources'
3132import {
@@ -70,6 +71,8 @@ let listingOnlyPolicy = false
7071let providerTitle = 'Quarterly report'
7172let linkedReportText = 'Remote resource bytes'
7273let linkedResourceMissing = false
74+ let resourceReads = 0
75+ let echoAppUri = false
7376let connectDomain = 'https://allowed.test'
7477let rejectResourceStatus = 0
7578let rejectResourcesPersistently = false
@@ -111,7 +114,14 @@ const provider = createServer(async (request, response) => {
111114 name : 'show_report' ,
112115 title : reflectCredential ? String ( request . headers [ 'x-fixture-token' ] ) : providerTitle ,
113116 inputSchema : { type : 'object' as const } ,
114- _meta : appAvailable ? { ui : { resourceUri : appUri } } : { } ,
117+ _meta : appAvailable
118+ ? {
119+ ui : {
120+ resourceUri : echoAppUri ? `${ appUri } /${ credentialCanary } ` : appUri ,
121+ visibility : [ 'model' ] ,
122+ } ,
123+ }
124+ : { } ,
115125 } ,
116126 {
117127 name : 'change_report' ,
@@ -128,6 +138,12 @@ const provider = createServer(async (request, response) => {
128138 protocol . setRequestHandler ( CallToolRequestSchema , async ( { params } ) => {
129139 if ( params . name === 'change_report' ) {
130140 appCalls ++
141+ if ( params . arguments ?. linked )
142+ return {
143+ content : [
144+ { type : 'resource_link' , name : 'Report' , uri : sourceUri , mimeType : 'text/plain' } ,
145+ ] ,
146+ }
131147 if ( encodedCredential )
132148 return {
133149 content : [
@@ -178,6 +194,12 @@ const provider = createServer(async (request, response) => {
178194 ) ,
179195 } ,
180196 } ,
197+ {
198+ type : 'resource_link' as const ,
199+ uri : 'file:///later.txt' ,
200+ name : 'Later report' ,
201+ mimeType : 'text/plain' ,
202+ } ,
181203 ]
182204 : [ ] ) ,
183205 ] ,
@@ -245,7 +267,9 @@ const provider = createServer(async (request, response) => {
245267 ] ,
246268 } ) )
247269 protocol . setRequestHandler ( ReadResourceRequestSchema , async ( { params } ) => {
248- if ( linkedResourceMissing ) throw new Error ( 'Synthetic missing linked resource' )
270+ resourceReads ++
271+ if ( linkedResourceMissing && params . uri === sourceUri )
272+ throw new Error ( 'Synthetic missing linked resource' )
249273 return {
250274 contents : [
251275 {
@@ -408,6 +432,7 @@ beforeAll(async () => {
408432afterEach ( ( ) => {
409433 linkedReportText = 'Remote resource bytes'
410434 linkedResourceMissing = false
435+ echoAppUri = false
411436} )
412437
413438afterAll ( async ( ) => {
@@ -602,17 +627,54 @@ describe('native MCP results over real transport, storage and Postgres', () => {
602627 }
603628 )
604629
605- it ( 'keeps private metadata and encoded files out of model output when a linked snapshot fails' , async ( ) => {
630+ it ( 'preserves valid attachments and the App when a linked snapshot fails' , async ( ) => {
606631 linkedResourceMissing = true
607- const { result } = await invokeReport ( { linked : true } )
608- expect ( compactMcpPresentation ( result . output ) ) . toBeUndefined ( )
632+ const { result, receipt } = await executeReport ( { linked : true } )
633+ expect ( receipt . hasApp ) . toBe ( true )
634+ expect ( receipt . items . map ( ( item ) => item . index ) ) . toEqual ( [ 1 , 2 ] )
635+ const asset = await readMcpResultAsset . execute ( {
636+ principal : session ,
637+ input : { chatId, id : receipt . id , index : 1 } ,
638+ } )
639+ expect ( asset . buffer . toString ( ) ) . toContain ( 'Encoded report: ' )
640+ expect ( asset . buffer . toString ( ) ) . not . toContain ( credentialCanary )
641+ const later = await readMcpResultAsset . execute ( {
642+ principal : session ,
643+ input : { chatId, id : receipt . id , index : 2 } ,
644+ } )
645+ expect ( later . buffer . toString ( ) ) . toBe ( 'Remote resource bytes' )
646+ await expect (
647+ readMcpResultAsset . execute ( {
648+ principal : session ,
649+ input : { chatId, id : receipt . id , index : 0 } ,
650+ } )
651+ ) . rejects . toThrow ( 'MCP file not found' )
609652 expect ( JSON . stringify ( result . output ) ) . not . toContain ( 'Only the app should receive this' )
610653 expect ( JSON . stringify ( result . output ) ) . not . toContain (
611654 Buffer . from ( `Encoded report: ${ credentialCanary } :end` ) . toString ( 'base64' )
612655 )
613656 expect ( JSON . stringify ( result . output ) ) . toContain ( 'could not be displayed' )
614657 } )
615658
659+ it ( 'does not download linked resources returned by a live App call' , async ( ) => {
660+ const { receipt } = await executeReport ( )
661+ const before = resourceReads
662+ const result = await callMcpAppTool . execute ( {
663+ principal : session ,
664+ input : { chatId, id : receipt . id , name : 'change_report' , arguments : { linked : true } } ,
665+ } )
666+ expect ( result . content [ 0 ] ) . toMatchObject ( { type : 'resource_link' , uri : sourceUri } )
667+ expect ( resourceReads ) . toBe ( before )
668+ } )
669+
670+ it ( 'redacts credentials reflected in the discovered App address before storage' , async ( ) => {
671+ echoAppUri = true
672+ const { receipt } = await executeReport ( )
673+ const manifest = await loadMcpPresentation ( chatId , receipt . id )
674+ expect ( manifest . appUri ) . toContain ( 'ui://fixture/view.html/' )
675+ expect ( JSON . stringify ( manifest ) ) . not . toContain ( credentialCanary )
676+ } )
677+
616678 it ( 'redacts encoded credentials in linked snapshot bytes before reopening' , async ( ) => {
617679 encodedCredential = true
618680 try {
@@ -632,18 +694,19 @@ describe('native MCP results over real transport, storage and Postgres', () => {
632694 }
633695 } )
634696
635- it . each ( [ 'audio/aiff' , 'image/tiff' ] ) (
636- 'downloads unsupported %s bytes without attempting playback' ,
637- async ( mimeType ) => {
638- const { receipt } = await executeReport ( { unsupportedMedia : mimeType } )
639- const asset = await readMcpResultAsset . execute ( {
640- principal : session ,
641- input : { chatId, id : receipt . id , index : 0 } ,
642- } )
643- expect ( asset . disposition ) . toBe ( 'attachment' )
644- expect ( asset . buffer . toString ( ) ) . toBe ( 'Unsupported fixture bytes' )
645- }
646- )
697+ it . each ( [
698+ [ 'audio/aiff' , 'attachment' ] ,
699+ [ 'image/tiff' , 'attachment' ] ,
700+ [ 'audio/ogg; codecs=opus' , 'inline' ] ,
701+ ] as const ) ( 'serves %s with %s disposition' , async ( mimeType , disposition ) => {
702+ const { receipt } = await executeReport ( { unsupportedMedia : mimeType } )
703+ const asset = await readMcpResultAsset . execute ( {
704+ principal : session ,
705+ input : { chatId, id : receipt . id , index : 0 } ,
706+ } )
707+ expect ( asset . disposition ) . toBe ( disposition )
708+ expect ( asset . buffer . toString ( ) ) . toBe ( 'Unsupported fixture bytes' )
709+ } )
647710
648711 it ( 'reports credentials once for both a cold and a pooled resource read' , async ( ) => {
649712 await evictMcpServerConnections ( serverId , 'cold resource fixture' )
0 commit comments