@@ -182,6 +182,13 @@ const structuredOf = (result: Awaited<ReturnType<Client["callTool"]>>): Record<s
182182const textOf = ( result : Awaited < ReturnType < Client [ "callTool" ] > > ) : string =>
183183 ( result . content as Array < { type : string ; text : string } > ) [ 0 ] . text ;
184184
185+ /** Assert source is available through both MCP result channels. */
186+ const expectArtifactSource = ( result : Awaited < ReturnType < Client [ "callTool" ] > > , code : string ) => {
187+ expect ( structuredOf ( result ) . code ) . toBe ( code ) ;
188+ expect ( textOf ( result ) ) . toContain ( "Source:" ) ;
189+ expect ( textOf ( result ) ) . toContain ( code ) ;
190+ } ;
191+
185192const toolNames = async ( client : Client ) : Promise < string [ ] > =>
186193 ( await client . listTools ( ) ) . tools . map ( ( tool ) => tool . name ) ;
187194
@@ -688,8 +695,11 @@ describe("MCP host — create-artifact", () => {
688695 url : "https://executor.test/artifacts/art_1" ,
689696 artifactId : "art_1" ,
690697 } ) ;
691- // The model needs to be told to hand the URL over.
698+ // The model needs to be told to hand the URL over. Source is a
699+ // show-artifact read, not part of the create confirmation.
692700 expect ( textOf ( result ) ) . toContain ( "https://executor.test/artifacts/art_1" ) ;
701+ expect ( textOf ( result ) ) . not . toContain ( "Source:" ) ;
702+ expect ( structuredOf ( result ) ) . not . toHaveProperty ( "code" ) ;
693703 // Persistence is what makes the fallback possible at all.
694704 expect ( store . calls ) . toHaveLength ( 1 ) ;
695705 expect ( store . rows . get ( "art_1" ) ?. code ) . toBe ( COUNTER_CODE ) ;
@@ -1285,6 +1295,10 @@ describe("MCP host — artifact retrieval", () => {
12851295 code : COUNTER_CODE ,
12861296 artifactId : "art_1" ,
12871297 } ) ;
1298+ // Apps-capable hosts still need the source on the text channel: a
1299+ // later restore or a client that starts advertising apps must not
1300+ // make `show-artifact` unusable for `edit-artifact`.
1301+ expectArtifactSource ( shown , COUNTER_CODE ) ;
12881302 } ,
12891303 { artifacts : store . port } ,
12901304 ) ;
@@ -1368,7 +1382,12 @@ describe("MCP host — artifact retrieval", () => {
13681382 status : "fallback_url" ,
13691383 url : "https://executor.test/artifacts/art_1" ,
13701384 artifactId : "art_1" ,
1385+ code : COUNTER_CODE ,
13711386 } ) ;
1387+ // The URL instruction stays; the source rides after it so a text-only
1388+ // host can copy `oldText` for `edit-artifact` from this result.
1389+ expect ( textOf ( shown ) ) . toContain ( "https://executor.test/artifacts/art_1" ) ;
1390+ expectArtifactSource ( shown , COUNTER_CODE ) ;
13721391 } ,
13731392 {
13741393 artifacts : store . port ,
@@ -1377,6 +1396,35 @@ describe("MCP host — artifact retrieval", () => {
13771396 ) ;
13781397 } ) ;
13791398
1399+ it ( "returns show-artifact source when the client has no apps support and no web UI" , async ( ) => {
1400+ const store = makeArtifactStore ( ) ;
1401+ await Effect . runPromise (
1402+ store . port . save ( {
1403+ title : "Saved earlier" ,
1404+ description : null ,
1405+ code : COUNTER_CODE ,
1406+ } ) ,
1407+ ) ;
1408+ await withClient (
1409+ makeStubEngine ( { } ) ,
1410+ NO_APPS_CAPS ,
1411+ async ( client ) => {
1412+ const shown = await client . callTool ( {
1413+ name : "show-artifact" ,
1414+ arguments : { id : "art_1" } ,
1415+ } ) ;
1416+ expect ( structuredOf ( shown ) ) . toEqual ( {
1417+ status : "fallback_unavailable" ,
1418+ reason : "mcp_apps_unsupported" ,
1419+ artifactId : "art_1" ,
1420+ code : COUNTER_CODE ,
1421+ } ) ;
1422+ expectArtifactSource ( shown , COUNTER_CODE ) ;
1423+ } ,
1424+ { artifacts : store . port } ,
1425+ ) ;
1426+ } ) ;
1427+
13801428 it ( "reports a miss as an error result rather than failing the tool call" , async ( ) => {
13811429 const store = makeArtifactStore ( ) ;
13821430 await withClient (
0 commit comments