@@ -2,7 +2,17 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
22import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" ;
33import type { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
44import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js" ;
5- import { EmptyResultSchema } from "@modelcontextprotocol/sdk/types.js" ;
5+ import {
6+ CallToolResultSchema ,
7+ EmptyResultSchema ,
8+ ListPromptsResultSchema ,
9+ ListResourcesResultSchema ,
10+ ListResourceTemplatesResultSchema ,
11+ PromptListChangedNotificationSchema ,
12+ ReadResourceResultSchema ,
13+ ResourceListChangedNotificationSchema ,
14+ ToolListChangedNotificationSchema ,
15+ } from "@modelcontextprotocol/sdk/types.js" ;
616
717import { App } from "./app" ;
818import { AppBridge , type McpUiHostCapabilities } from "./app-bridge" ;
@@ -508,4 +518,189 @@ describe("App <-> AppBridge integration", () => {
508518 expect ( result ) . toEqual ( { } ) ;
509519 } ) ;
510520 } ) ;
521+
522+ describe ( "AppBridge without MCP client (manual handlers)" , ( ) => {
523+ let app : App ;
524+ let bridge : AppBridge ;
525+ let appTransport : InMemoryTransport ;
526+ let bridgeTransport : InMemoryTransport ;
527+
528+ beforeEach ( ( ) => {
529+ [ appTransport , bridgeTransport ] = InMemoryTransport . createLinkedPair ( ) ;
530+ app = new App ( testAppInfo , { } , { autoResize : false } ) ;
531+ // Pass null instead of a client - manual handler registration
532+ bridge = new AppBridge ( null , testHostInfo , testHostCapabilities ) ;
533+ } ) ;
534+
535+ afterEach ( async ( ) => {
536+ await appTransport . close ( ) ;
537+ await bridgeTransport . close ( ) ;
538+ } ) ;
539+
540+ it ( "connect() works without client" , async ( ) => {
541+ await bridge . connect ( bridgeTransport ) ;
542+ await app . connect ( appTransport ) ;
543+
544+ // Initialization should still work
545+ const hostInfo = app . getHostVersion ( ) ;
546+ expect ( hostInfo ) . toEqual ( testHostInfo ) ;
547+ } ) ;
548+
549+ it ( "oncalltool setter registers handler for tools/call requests" , async ( ) => {
550+ const receivedCalls : unknown [ ] = [ ] ;
551+ bridge . oncalltool = async ( params ) => {
552+ receivedCalls . push ( params ) ;
553+ return { content : [ { type : "text" , text : "result" } ] } ;
554+ } ;
555+
556+ await bridge . connect ( bridgeTransport ) ;
557+ await app . connect ( appTransport ) ;
558+
559+ // App calls a tool via callServerTool
560+ const result = await app . callServerTool ( {
561+ name : "test-tool" ,
562+ arguments : { arg : "value" } ,
563+ } ) ;
564+
565+ expect ( receivedCalls ) . toHaveLength ( 1 ) ;
566+ expect ( receivedCalls [ 0 ] ) . toMatchObject ( {
567+ name : "test-tool" ,
568+ arguments : { arg : "value" } ,
569+ } ) ;
570+ expect ( result . content ) . toEqual ( [ { type : "text" , text : "result" } ] ) ;
571+ } ) ;
572+
573+ it ( "onlistresources setter registers handler for resources/list requests" , async ( ) => {
574+ const receivedRequests : unknown [ ] = [ ] ;
575+ bridge . onlistresources = async ( params ) => {
576+ receivedRequests . push ( params ) ;
577+ return { resources : [ { uri : "test://resource" , name : "Test" } ] } ;
578+ } ;
579+
580+ await bridge . connect ( bridgeTransport ) ;
581+ await app . connect ( appTransport ) ;
582+
583+ // App sends resources/list request via the protocol's request method
584+ const result = await app . request (
585+ { method : "resources/list" , params : { } } ,
586+ ListResourcesResultSchema ,
587+ ) ;
588+
589+ expect ( receivedRequests ) . toHaveLength ( 1 ) ;
590+ expect ( result . resources ) . toEqual ( [
591+ { uri : "test://resource" , name : "Test" } ,
592+ ] ) ;
593+ } ) ;
594+
595+ it ( "onreadresource setter registers handler for resources/read requests" , async ( ) => {
596+ const receivedRequests : unknown [ ] = [ ] ;
597+ bridge . onreadresource = async ( params ) => {
598+ receivedRequests . push ( params ) ;
599+ return { contents : [ { uri : params . uri , text : "content" } ] } ;
600+ } ;
601+
602+ await bridge . connect ( bridgeTransport ) ;
603+ await app . connect ( appTransport ) ;
604+
605+ const result = await app . request (
606+ { method : "resources/read" , params : { uri : "test://resource" } } ,
607+ ReadResourceResultSchema ,
608+ ) ;
609+
610+ expect ( receivedRequests ) . toHaveLength ( 1 ) ;
611+ expect ( receivedRequests [ 0 ] ) . toMatchObject ( { uri : "test://resource" } ) ;
612+ expect ( result . contents ) . toEqual ( [
613+ { uri : "test://resource" , text : "content" } ,
614+ ] ) ;
615+ } ) ;
616+
617+ it ( "onlistresourcetemplates setter registers handler for resources/templates/list requests" , async ( ) => {
618+ const receivedRequests : unknown [ ] = [ ] ;
619+ bridge . onlistresourcetemplates = async ( params ) => {
620+ receivedRequests . push ( params ) ;
621+ return {
622+ resourceTemplates : [
623+ { uriTemplate : "test://{id}" , name : "Test Template" } ,
624+ ] ,
625+ } ;
626+ } ;
627+
628+ await bridge . connect ( bridgeTransport ) ;
629+ await app . connect ( appTransport ) ;
630+
631+ const result = await app . request (
632+ { method : "resources/templates/list" , params : { } } ,
633+ ListResourceTemplatesResultSchema ,
634+ ) ;
635+
636+ expect ( receivedRequests ) . toHaveLength ( 1 ) ;
637+ expect ( result . resourceTemplates ) . toEqual ( [
638+ { uriTemplate : "test://{id}" , name : "Test Template" } ,
639+ ] ) ;
640+ } ) ;
641+
642+ it ( "onlistprompts setter registers handler for prompts/list requests" , async ( ) => {
643+ const receivedRequests : unknown [ ] = [ ] ;
644+ bridge . onlistprompts = async ( params ) => {
645+ receivedRequests . push ( params ) ;
646+ return { prompts : [ { name : "test-prompt" } ] } ;
647+ } ;
648+
649+ await bridge . connect ( bridgeTransport ) ;
650+ await app . connect ( appTransport ) ;
651+
652+ const result = await app . request (
653+ { method : "prompts/list" , params : { } } ,
654+ ListPromptsResultSchema ,
655+ ) ;
656+
657+ expect ( receivedRequests ) . toHaveLength ( 1 ) ;
658+ expect ( result . prompts ) . toEqual ( [ { name : "test-prompt" } ] ) ;
659+ } ) ;
660+
661+ it ( "sendToolListChanged sends notification to app" , async ( ) => {
662+ const receivedNotifications : unknown [ ] = [ ] ;
663+ app . setNotificationHandler ( ToolListChangedNotificationSchema , ( n ) => {
664+ receivedNotifications . push ( n . params ) ;
665+ } ) ;
666+
667+ await bridge . connect ( bridgeTransport ) ;
668+ await app . connect ( appTransport ) ;
669+
670+ bridge . sendToolListChanged ( ) ;
671+ await flush ( ) ;
672+
673+ expect ( receivedNotifications ) . toHaveLength ( 1 ) ;
674+ } ) ;
675+
676+ it ( "sendResourceListChanged sends notification to app" , async ( ) => {
677+ const receivedNotifications : unknown [ ] = [ ] ;
678+ app . setNotificationHandler ( ResourceListChangedNotificationSchema , ( n ) => {
679+ receivedNotifications . push ( n . params ) ;
680+ } ) ;
681+
682+ await bridge . connect ( bridgeTransport ) ;
683+ await app . connect ( appTransport ) ;
684+
685+ bridge . sendResourceListChanged ( ) ;
686+ await flush ( ) ;
687+
688+ expect ( receivedNotifications ) . toHaveLength ( 1 ) ;
689+ } ) ;
690+
691+ it ( "sendPromptListChanged sends notification to app" , async ( ) => {
692+ const receivedNotifications : unknown [ ] = [ ] ;
693+ app . setNotificationHandler ( PromptListChangedNotificationSchema , ( n ) => {
694+ receivedNotifications . push ( n . params ) ;
695+ } ) ;
696+
697+ await bridge . connect ( bridgeTransport ) ;
698+ await app . connect ( appTransport ) ;
699+
700+ bridge . sendPromptListChanged ( ) ;
701+ await flush ( ) ;
702+
703+ expect ( receivedNotifications ) . toHaveLength ( 1 ) ;
704+ } ) ;
705+ } ) ;
511706} ) ;
0 commit comments