Skip to content

Commit 398f5b4

Browse files
authored
Merge pull request modelcontextprotocol#294 from matteo8p/create-helper-functions-tool-visibility
Add `isToolVisibilityModelOnly` and `isToolVisibilityAppOnly` helper functions
2 parents ec0f217 + d27acc4 commit 398f5b4

File tree

2 files changed

+182
-1
lines changed

2 files changed

+182
-1
lines changed

src/app-bridge.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { App } from "./app";
1717
import {
1818
AppBridge,
1919
getToolUiResourceUri,
20+
isToolVisibilityModelOnly,
21+
isToolVisibilityAppOnly,
2022
type McpUiHostCapabilities,
2123
} from "./app-bridge";
2224

@@ -921,3 +923,153 @@ describe("getToolUiResourceUri", () => {
921923
});
922924
});
923925
});
926+
927+
describe("isToolVisibilityModelOnly", () => {
928+
describe("returns true", () => {
929+
it("when visibility is exactly ['model']", () => {
930+
const tool = {
931+
name: "test-tool",
932+
_meta: { ui: { visibility: ["model"] } },
933+
};
934+
expect(isToolVisibilityModelOnly(tool)).toBe(true);
935+
});
936+
});
937+
938+
describe("returns false", () => {
939+
it("when visibility is ['app']", () => {
940+
const tool = {
941+
name: "test-tool",
942+
_meta: { ui: { visibility: ["app"] } },
943+
};
944+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
945+
});
946+
947+
it("when visibility is ['model', 'app']", () => {
948+
const tool = {
949+
name: "test-tool",
950+
_meta: { ui: { visibility: ["model", "app"] } },
951+
};
952+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
953+
});
954+
955+
it("when visibility is ['app', 'model']", () => {
956+
const tool = {
957+
name: "test-tool",
958+
_meta: { ui: { visibility: ["app", "model"] } },
959+
};
960+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
961+
});
962+
963+
it("when visibility is empty array", () => {
964+
const tool = {
965+
name: "test-tool",
966+
_meta: { ui: { visibility: [] } },
967+
};
968+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
969+
});
970+
971+
it("when visibility is undefined", () => {
972+
const tool = {
973+
name: "test-tool",
974+
_meta: { ui: {} },
975+
};
976+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
977+
});
978+
979+
it("when _meta.ui is missing", () => {
980+
const tool = {
981+
name: "test-tool",
982+
_meta: {},
983+
};
984+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
985+
});
986+
987+
it("when _meta is missing", () => {
988+
const tool = { name: "test-tool" };
989+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
990+
});
991+
992+
it("when tool has resourceUri but no visibility", () => {
993+
const tool = {
994+
name: "test-tool",
995+
_meta: { ui: { resourceUri: "ui://server/app.html" } },
996+
};
997+
expect(isToolVisibilityModelOnly(tool)).toBe(false);
998+
});
999+
});
1000+
});
1001+
1002+
describe("isToolVisibilityAppOnly", () => {
1003+
describe("returns true", () => {
1004+
it("when visibility is exactly ['app']", () => {
1005+
const tool = {
1006+
name: "test-tool",
1007+
_meta: { ui: { visibility: ["app"] } },
1008+
};
1009+
expect(isToolVisibilityAppOnly(tool)).toBe(true);
1010+
});
1011+
});
1012+
1013+
describe("returns false", () => {
1014+
it("when visibility is ['model']", () => {
1015+
const tool = {
1016+
name: "test-tool",
1017+
_meta: { ui: { visibility: ["model"] } },
1018+
};
1019+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1020+
});
1021+
1022+
it("when visibility is ['model', 'app']", () => {
1023+
const tool = {
1024+
name: "test-tool",
1025+
_meta: { ui: { visibility: ["model", "app"] } },
1026+
};
1027+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1028+
});
1029+
1030+
it("when visibility is ['app', 'model']", () => {
1031+
const tool = {
1032+
name: "test-tool",
1033+
_meta: { ui: { visibility: ["app", "model"] } },
1034+
};
1035+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1036+
});
1037+
1038+
it("when visibility is empty array", () => {
1039+
const tool = {
1040+
name: "test-tool",
1041+
_meta: { ui: { visibility: [] } },
1042+
};
1043+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1044+
});
1045+
1046+
it("when visibility is undefined", () => {
1047+
const tool = {
1048+
name: "test-tool",
1049+
_meta: { ui: {} },
1050+
};
1051+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1052+
});
1053+
1054+
it("when _meta.ui is missing", () => {
1055+
const tool = {
1056+
name: "test-tool",
1057+
_meta: {},
1058+
};
1059+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1060+
});
1061+
1062+
it("when _meta is missing", () => {
1063+
const tool = { name: "test-tool" };
1064+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1065+
});
1066+
1067+
it("when tool has resourceUri but no visibility", () => {
1068+
const tool = {
1069+
name: "test-tool",
1070+
_meta: { ui: { resourceUri: "ui://server/app.html" } },
1071+
};
1072+
expect(isToolVisibilityAppOnly(tool)).toBe(false);
1073+
});
1074+
});
1075+
});

src/app-bridge.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
McpUiRequestDisplayModeRequestSchema,
7979
McpUiRequestDisplayModeResult,
8080
McpUiResourcePermissions,
81+
McpUiToolMeta,
8182
} from "./types";
8283
export * from "./types";
8384
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app";
@@ -110,7 +111,7 @@ export { PostMessageTransport } from "./message-transport";
110111
*/
111112
export function getToolUiResourceUri(tool: Partial<Tool>): string | undefined {
112113
// Try new nested format first: _meta.ui.resourceUri
113-
const uiMeta = tool._meta?.ui as { resourceUri?: unknown } | undefined;
114+
const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined;
114115
let uri: unknown = uiMeta?.resourceUri;
115116

116117
// Fall back to deprecated flat format: _meta["ui/resourceUri"]
@@ -126,6 +127,34 @@ export function getToolUiResourceUri(tool: Partial<Tool>): string | undefined {
126127
return undefined;
127128
}
128129

130+
/**
131+
* Check if a tool is visible to the model only.
132+
*
133+
* @param tool - Tool object with visibility metadata
134+
* @returns True if the tool is visible to the model only, false otherwise
135+
*/
136+
export function isToolVisibilityModelOnly(tool: Partial<Tool>): boolean {
137+
const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined;
138+
const visibility = uiMeta?.visibility as Array<"model" | "app"> | undefined;
139+
if (!visibility) return false;
140+
if (visibility.length === 1 && visibility[0] === "model") return true;
141+
return false;
142+
}
143+
144+
/**
145+
* Check if a tool is visible to the app only.
146+
*
147+
* @param tool - Tool object with visibility metadata
148+
* @returns True if the tool is visible to the app only, false otherwise
149+
*/
150+
export function isToolVisibilityAppOnly(tool: Partial<Tool>): boolean {
151+
const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined;
152+
const visibility = uiMeta?.visibility as Array<"model" | "app"> | undefined;
153+
if (!visibility) return false;
154+
if (visibility.length === 1 && visibility[0] === "app") return true;
155+
return false;
156+
}
157+
129158
/**
130159
* Build iframe `allow` attribute string from permissions.
131160
*

0 commit comments

Comments
 (0)