docs: add tool visibility and restructure _meta.ui (#131)
* docs: add tool visibility and restructure _meta.ui - Restructure tool metadata: `_meta["ui/resourceUri"]` → `_meta.ui.resourceUri` - Add `visibility` array field: ["model"], ["apps"], or ["model", "apps"] - Default ["model"] preserves standard MCP behavior - ["apps"] enables widget-only tools hidden from agent - Add McpUiToolMeta interface for type safety - Add Design Decision #4 explaining approach vs OpenAI's two-field model 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * address PR review comments - Change default visibility to ["model", "apps"] - Add deprecation notice for flat ui/resourceUri format - Add McpUiToolMeta and McpUiToolVisibility to spec.types.ts - Improve tools/list and tools/call behavior wording 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: regenerate schemas for McpUiToolMeta types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: tool visibility and nested _meta.ui format - Rename visibility "apps" → "app" in McpUiToolVisibility type - Add _meta.ui.resourceUri nested format (deprecate flat format) - Add getToolUiResourceUri() utility with backward compatibility - Add visibility demo to system-monitor-server: - get-system-stats: visibility ["model"] with resourceUri - refresh-stats: visibility ["app"] (app-only polling) - Update all example servers to use new _meta.ui format - Add 11 unit tests for getToolUiResourceUri() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: regenerate schemas with updated dependencies * feat: export McpUiToolMeta type and add type annotations - Export McpUiToolMeta and McpUiToolVisibility types from types.ts - Export corresponding Zod schemas (McpUiToolMetaSchema, McpUiToolVisibilitySchema) - Add `as McpUiToolMeta` type annotations to all example servers - Update docs/quickstart.md with proper typing Ensures type safety for `_meta.ui` tool metadata across the codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add server helpers and optional connect() transport Add `src/server/` with convenience functions for registering MCP App tools and resources: - `registerAppTool(server, name, config, handler)` - `registerAppResource(server, name, uri, config, callback)` The `transport` parameter in `App.connect()` is now optional, defaulting to `PostMessageTransport(window.parent)`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update src/server/index.ts Co-authored-by: Jonathan Hefner <jonathan@hefner.pro> * feat: update McpUiAppToolConfig to support both _meta.ui and flat formats * refactor: simplify server helper imports * docs: add deprecation notice and improve resourceUri documentation * 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 * refactor: avoid mutating config arg in registerAppTool * style: format with prettier --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Olivier Chafik <ochafik@anthropic.com> Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
This commit is contained in:
parent
bea0ae27f5
commit
37aab03de0
1 changed files with 530 additions and 43 deletions
|
|
@ -14,7 +14,11 @@ import {
|
|||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
import { App } from "./app";
|
||||
import { AppBridge, type McpUiHostCapabilities } from "./app-bridge";
|
||||
import {
|
||||
AppBridge,
|
||||
getToolUiResourceUri,
|
||||
type McpUiHostCapabilities,
|
||||
} from "./app-bridge";
|
||||
|
||||
/** Wait for pending microtasks to complete */
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
|
@ -707,3 +711,107 @@ describe("App <-> AppBridge integration", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getToolUiResourceUri", () => {
|
||||
describe("new nested format (_meta.ui.resourceUri)", () => {
|
||||
it("extracts resourceUri from _meta.ui.resourceUri", () => {
|
||||
const tool = {
|
||||
name: "test-tool",
|
||||
_meta: {
|
||||
ui: { resourceUri: "ui://server/app.html" },
|
||||
},
|
||||
};
|
||||
expect(getToolUiResourceUri(tool)).toBe("ui://server/app.html");
|
||||
});
|
||||
|
||||
it("extracts resourceUri when visibility is also present", () => {
|
||||
const tool = {
|
||||
name: "test-tool",
|
||||
_meta: {
|
||||
ui: {
|
||||
resourceUri: "ui://server/app.html",
|
||||
visibility: ["model"],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getToolUiResourceUri(tool)).toBe("ui://server/app.html");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deprecated flat format (_meta['ui/resourceUri'])", () => {
|
||||
it("extracts resourceUri from deprecated format", () => {
|
||||
const tool = {
|
||||
name: "test-tool",
|
||||
_meta: { "ui/resourceUri": "ui://server/app.html" },
|
||||
};
|
||||
expect(getToolUiResourceUri(tool)).toBe("ui://server/app.html");
|
||||
});
|
||||
});
|
||||
|
||||
describe("format precedence", () => {
|
||||
it("prefers new nested format over deprecated format", () => {
|
||||
const tool = {
|
||||
name: "test-tool",
|
||||
_meta: {
|
||||
ui: { resourceUri: "ui://server/new.html" },
|
||||
"ui/resourceUri": "ui://server/old.html",
|
||||
},
|
||||
};
|
||||
expect(getToolUiResourceUri(tool)).toBe("ui://server/new.html");
|
||||
});
|
||||
});
|
||||
|
||||
describe("missing resourceUri", () => {
|
||||
it("returns undefined when no resourceUri in empty _meta", () => {
|
||||
const tool = { name: "test-tool", _meta: {} };
|
||||
expect(getToolUiResourceUri(tool)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when _meta is missing", () => {
|
||||
const tool = {} as { _meta?: Record<string, unknown> };
|
||||
expect(getToolUiResourceUri(tool)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for app-only tools with visibility but no resourceUri", () => {
|
||||
const tool = {
|
||||
name: "refresh-stats",
|
||||
_meta: {
|
||||
ui: { visibility: ["app"] },
|
||||
},
|
||||
};
|
||||
expect(getToolUiResourceUri(tool)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("validation", () => {
|
||||
it("throws for invalid URI (not starting with ui://)", () => {
|
||||
const tool = {
|
||||
name: "test-tool",
|
||||
_meta: { ui: { resourceUri: "https://example.com" } },
|
||||
};
|
||||
expect(() => getToolUiResourceUri(tool)).toThrow(
|
||||
"Invalid UI resource URI",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws for non-string resourceUri", () => {
|
||||
const tool = {
|
||||
name: "test-tool",
|
||||
_meta: { ui: { resourceUri: 123 } },
|
||||
};
|
||||
expect(() => getToolUiResourceUri(tool)).toThrow(
|
||||
"Invalid UI resource URI",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws for null resourceUri", () => {
|
||||
const tool = {
|
||||
name: "test-tool",
|
||||
_meta: { ui: { resourceUri: null } },
|
||||
};
|
||||
expect(() => getToolUiResourceUri(tool)).toThrow(
|
||||
"Invalid UI resource URI",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue