From ee4b232a1f1c42d94d91d12da39e5c23d385e542 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 1 Jul 2026 19:14:16 -0400 Subject: [PATCH] refactor(core): narrow readUiMeta from unknown; document ui-meta precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address round-1 review nits on apps.ts: - readUiMeta now accepts `unknown` and narrows internally, removing the three `as WithUiMeta` casts at its call sites (readability win). - Add a comment documenting that the content-block-then-result precedence for `_meta.ui` is intentional (posture lives on the content block; no shallow merge). - Add a TODO to switch to the named McpUiToolMeta/McpUiResourceMeta types once the upstream extensionless re-export is fixed for NodeNext. Coverage: apps.ts 100% lines / 97.05% branch / 100% funcs — clears the gate. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX --- core/mcp/apps.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/core/mcp/apps.ts b/core/mcp/apps.ts index 37198e52..937acc73 100644 --- a/core/mcp/apps.ts +++ b/core/mcp/apps.ts @@ -68,6 +68,9 @@ type WithUiMeta = { _meta?: { ui?: unknown } }; * package's `.d.ts`, so we read the fields structurally instead. The values * pass through verbatim into {@link AppInfo}; callers can narrow them against * the published Zod schemas if they need strict typing. + * + * TODO: switch to the named `McpUiToolMeta` / `McpUiResourceMeta` types once + * upstream fixes the extensionless re-export so they resolve under NodeNext. */ interface UiMetaShape { visibility?: readonly string[]; @@ -77,8 +80,16 @@ interface UiMetaShape { prefersBorder?: boolean; } -function readUiMeta(carrier: WithUiMeta | undefined): UiMetaShape | undefined { - const ui = carrier?._meta?.ui; +/** + * Reads a carrier's `_meta.ui` structurally, narrowing from `unknown` so call + * sites don't need their own casts. Returns `undefined` unless both the carrier + * and its `_meta.ui` are non-null objects. + */ +function readUiMeta(carrier: unknown): UiMetaShape | undefined { + const ui = + carrier !== null && typeof carrier === "object" + ? (carrier as WithUiMeta)._meta?.ui + : undefined; return ui !== null && typeof ui === "object" ? (ui as UiMetaShape) : undefined; @@ -135,11 +146,14 @@ export function extractAppInfo( if (resourceUri === undefined) { return { hasApp: false, toolName: tool.name }; } - const toolUi = readUiMeta(tool as WithUiMeta); + const toolUi = readUiMeta(tool); const content = resource && findResourceContent(resource, resourceUri); - const resourceUi = - readUiMeta(content as WithUiMeta | undefined) ?? - readUiMeta(resource as WithUiMeta | undefined); + // Precedence is intentional: prefer the matched content block's `_meta.ui`, + // falling back to the result-level `_meta.ui`. Per the current spec the + // security posture (csp/permissions/domain) lives on the content block, so we + // don't shallow-merge the two carriers — a content block that declares any + // `ui` is treated as authoritative for all of its fields. + const resourceUi = readUiMeta(content) ?? readUiMeta(resource); return { hasApp: true, toolName: tool.name,