Skip to content

Commit 6a22f0d

Browse files
committed
feat: add backward compat normalization in registerAppTool
- If _meta.ui.resourceUri is set, also set legacy flat key - If legacy flat key is set, also set _meta.ui.resourceUri - Preserves existing visibility when merging - Does not overwrite if both formats already set
1 parent 0cc7515 commit 6a22f0d

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

src/server/index.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,131 @@ describe("registerAppTool", () => {
5252
).toBe("ui://test/widget.html");
5353
expect(capturedHandler).toBe(handler);
5454
});
55+
56+
describe("backward compatibility", () => {
57+
it("should set legacy key when _meta.ui.resourceUri is provided", () => {
58+
let capturedConfig: Record<string, unknown> | undefined;
59+
60+
const mockServer = {
61+
registerTool: mock(
62+
(_name: string, config: Record<string, unknown>, _handler: unknown) => {
63+
capturedConfig = config;
64+
},
65+
),
66+
};
67+
68+
registerAppTool(
69+
mockServer as unknown as Pick<McpServer, "registerTool">,
70+
"my-tool",
71+
{
72+
_meta: {
73+
ui: { resourceUri: "ui://test/widget.html" },
74+
},
75+
},
76+
async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
77+
);
78+
79+
const meta = capturedConfig?._meta as Record<string, unknown>;
80+
// New format should be preserved
81+
expect((meta.ui as { resourceUri: string }).resourceUri).toBe(
82+
"ui://test/widget.html",
83+
);
84+
// Legacy key should also be set
85+
expect(meta[RESOURCE_URI_META_KEY]).toBe("ui://test/widget.html");
86+
});
87+
88+
it("should set _meta.ui.resourceUri when legacy key is provided", () => {
89+
let capturedConfig: Record<string, unknown> | undefined;
90+
91+
const mockServer = {
92+
registerTool: mock(
93+
(_name: string, config: Record<string, unknown>, _handler: unknown) => {
94+
capturedConfig = config;
95+
},
96+
),
97+
};
98+
99+
registerAppTool(
100+
mockServer as unknown as Pick<McpServer, "registerTool">,
101+
"my-tool",
102+
{
103+
_meta: {
104+
[RESOURCE_URI_META_KEY]: "ui://test/widget.html",
105+
},
106+
},
107+
async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
108+
);
109+
110+
const meta = capturedConfig?._meta as Record<string, unknown>;
111+
// Legacy key should be preserved
112+
expect(meta[RESOURCE_URI_META_KEY]).toBe("ui://test/widget.html");
113+
// New format should also be set
114+
expect((meta.ui as { resourceUri: string }).resourceUri).toBe(
115+
"ui://test/widget.html",
116+
);
117+
});
118+
119+
it("should preserve visibility when converting from legacy format", () => {
120+
let capturedConfig: Record<string, unknown> | undefined;
121+
122+
const mockServer = {
123+
registerTool: mock(
124+
(_name: string, config: Record<string, unknown>, _handler: unknown) => {
125+
capturedConfig = config;
126+
},
127+
),
128+
};
129+
130+
registerAppTool(
131+
mockServer as unknown as Pick<McpServer, "registerTool">,
132+
"my-tool",
133+
{
134+
_meta: {
135+
ui: { visibility: ["app"] },
136+
[RESOURCE_URI_META_KEY]: "ui://test/widget.html",
137+
},
138+
} as any,
139+
async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
140+
);
141+
142+
const meta = capturedConfig?._meta as Record<string, unknown>;
143+
const ui = meta.ui as { resourceUri: string; visibility: string[] };
144+
// Should have merged resourceUri into existing ui object
145+
expect(ui.resourceUri).toBe("ui://test/widget.html");
146+
expect(ui.visibility).toEqual(["app"]);
147+
});
148+
149+
it("should not overwrite if both formats are already set", () => {
150+
let capturedConfig: Record<string, unknown> | undefined;
151+
152+
const mockServer = {
153+
registerTool: mock(
154+
(_name: string, config: Record<string, unknown>, _handler: unknown) => {
155+
capturedConfig = config;
156+
},
157+
),
158+
};
159+
160+
registerAppTool(
161+
mockServer as unknown as Pick<McpServer, "registerTool">,
162+
"my-tool",
163+
{
164+
_meta: {
165+
ui: { resourceUri: "ui://new/widget.html" },
166+
[RESOURCE_URI_META_KEY]: "ui://old/widget.html",
167+
},
168+
} as any,
169+
async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
170+
);
171+
172+
const meta = capturedConfig?._meta as Record<string, unknown>;
173+
// Both should remain unchanged
174+
expect((meta.ui as { resourceUri: string }).resourceUri).toBe(
175+
"ui://new/widget.html",
176+
);
177+
expect(meta[RESOURCE_URI_META_KEY]).toBe("ui://old/widget.html");
178+
});
179+
});
55180
});
56181

57182
describe("registerAppResource", () => {

src/server/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ export function registerAppTool(
102102
config: McpUiAppToolConfig,
103103
handler: ToolCallback<ZodRawShape>,
104104
): void {
105+
// Normalize metadata for backward compatibility:
106+
// - If _meta.ui.resourceUri is set, also set the legacy flat key
107+
// - If the legacy flat key is set, also set _meta.ui.resourceUri
108+
const meta = config._meta;
109+
const uiMeta = meta.ui as McpUiToolMeta | undefined;
110+
const legacyUri = meta[RESOURCE_URI_META_KEY] as string | undefined;
111+
112+
if (uiMeta?.resourceUri && !legacyUri) {
113+
// New format -> also set legacy key
114+
meta[RESOURCE_URI_META_KEY] = uiMeta.resourceUri;
115+
} else if (legacyUri && !uiMeta?.resourceUri) {
116+
// Legacy format -> also set new format
117+
meta.ui = { ...uiMeta, resourceUri: legacyUri };
118+
}
119+
105120
server.registerTool(name, config, handler);
106121
}
107122

0 commit comments

Comments
 (0)