Skip to content

Commit 47258b8

Browse files
ochafikclaude
andcommitted
Add setter-based handlers for MCP request/notification forwarding
- Add oncalltool, onlistresources, onlistresourcetemplates, onreadresource, onlistprompts setters for Guest UI → Host requests - Add sendToolListChanged, sendResourceListChanged, sendPromptListChanged methods for Host → Guest UI notifications - Refactor connect() to use new setters with inlined callbacks - Update documentation to clarify optional client parameter behavior - Remove unused forwardRequest/forwardNotification helper methods - Add comprehensive tests for AppBridge without MCP client (manual handlers) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 3a30e94 commit 47258b8

File tree

2 files changed

+591
-69
lines changed

2 files changed

+591
-69
lines changed

src/app-bridge.test.ts

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
22
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
33
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
44
import 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

717
import { App } from "./app";
818
import { 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

Comments
 (0)