From 12eeb10778cd735c8c160f21f95df694e3bb3912 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Mon, 4 May 2026 13:19:11 +0200 Subject: [PATCH 1/2] wip --- .../mcp-apps/components/McpToolView.tsx | 24 ++- .../utils/posthog-exec-display.test.ts | 201 ++++++++++++++++++ .../mcp-apps/utils/posthog-exec-display.ts | 103 +++++++++ 3 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts create mode 100644 apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts diff --git a/apps/code/src/renderer/features/mcp-apps/components/McpToolView.tsx b/apps/code/src/renderer/features/mcp-apps/components/McpToolView.tsx index e5a9a4a9b..ef692dddf 100644 --- a/apps/code/src/renderer/features/mcp-apps/components/McpToolView.tsx +++ b/apps/code/src/renderer/features/mcp-apps/components/McpToolView.tsx @@ -8,12 +8,19 @@ import { stripCodeFences, ToolTitle, type ToolViewProps, + truncateText, useToolCallStatus, } from "@features/sessions/components/session-update/toolCallUtils"; import { Plugs } from "@phosphor-icons/react"; import { Box, Flex } from "@radix-ui/themes"; import { useState } from "react"; import { parseMcpToolKey } from "../utils/mcp-app-host-utils"; +import { + getPostHogExecDisplay, + isPostHogExecTool, +} from "../utils/posthog-exec-display"; + +const POSTHOG_EXEC_INPUT_PREVIEW_MAX_LENGTH = 60; interface McpToolViewProps extends ToolViewProps { mcpToolName: string; @@ -34,8 +41,21 @@ export function McpToolView({ turnComplete, ); - const { serverName, toolName } = parseMcpToolKey(mcpToolName); - const inputPreview = compactInput(rawInput); + const { serverName: defaultServerName, toolName: defaultToolName } = + parseMcpToolKey(mcpToolName); + const posthogDisplay = isPostHogExecTool(mcpToolName) + ? getPostHogExecDisplay(rawInput) + : null; + const serverName = posthogDisplay ? "posthog" : defaultServerName; + const toolName = posthogDisplay?.label ?? defaultToolName; + const inputPreview = posthogDisplay + ? posthogDisplay.input + ? truncateText( + posthogDisplay.input, + POSTHOG_EXEC_INPUT_PREVIEW_MAX_LENGTH, + ) + : undefined + : compactInput(rawInput); const fullInput = formatInput(rawInput); const output = stripCodeFences(getContentText(content) ?? ""); diff --git a/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts new file mode 100644 index 000000000..70d4a6e36 --- /dev/null +++ b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, it } from "vitest"; +import { + getPostHogExecDisplay, + isPostHogExecTool, +} from "./posthog-exec-display"; + +describe("isPostHogExecTool", () => { + it("matches the bare posthog exec tool", () => { + expect(isPostHogExecTool("mcp__posthog__exec")).toBe(true); + }); + + it("matches plugin-prefixed variants", () => { + expect(isPostHogExecTool("mcp__posthog_posthog__exec")).toBe(true); + expect(isPostHogExecTool("mcp__plugin_posthog_posthog__exec")).toBe(true); + expect(isPostHogExecTool("mcp__posthog_cloud__exec")).toBe(true); + }); + + it("rejects other MCP tools", () => { + expect(isPostHogExecTool("mcp__posthog__list")).toBe(false); + expect(isPostHogExecTool("mcp__other__exec")).toBe(false); + expect(isPostHogExecTool("Bash")).toBe(false); + }); +}); + +describe("getPostHogExecDisplay", () => { + describe("call verb", () => { + it("collapses `call ` to the bare sub-tool label", () => { + expect( + getPostHogExecDisplay({ command: "call experiment-list" }), + ).toEqual({ + label: "experiment-list", + input: undefined, + }); + }); + + it("uses the JSON args portion as input", () => { + expect( + getPostHogExecDisplay({ + command: 'call execute-sql {"query":"SELECT 1"}', + }), + ).toEqual({ + label: "execute-sql", + input: '{"query":"SELECT 1"}', + }); + }); + + it("handles `call --json {json}`", () => { + expect( + getPostHogExecDisplay({ + command: 'call --json experiment-update {"id":1}', + }), + ).toEqual({ + label: "experiment-update", + input: '{"id":1}', + }); + }); + }); + + describe("info verb", () => { + it("formats `info ` with no args", () => { + expect(getPostHogExecDisplay({ command: "info execute-sql" })).toEqual({ + label: "info execute-sql", + input: undefined, + }); + }); + + it("falls back to bare `info` when no tool given", () => { + expect(getPostHogExecDisplay({ command: "info" })).toEqual({ + label: "info", + input: undefined, + }); + }); + }); + + describe("schema verb", () => { + it("formats `schema ` (no field path)", () => { + expect(getPostHogExecDisplay({ command: "schema query-trends" })).toEqual( + { + label: "schema query-trends", + input: undefined, + }, + ); + }); + + it("formats `schema ` with the path as input", () => { + expect( + getPostHogExecDisplay({ + command: "schema query-trends series", + }), + ).toEqual({ + label: "schema query-trends", + input: "series", + }); + }); + + it("supports dotted field paths", () => { + expect( + getPostHogExecDisplay({ + command: "schema query-trends breakdownFilter.breakdowns", + }), + ).toEqual({ + label: "schema query-trends", + input: "breakdownFilter.breakdowns", + }); + }); + }); + + describe("search verb", () => { + it("uses the regex pattern as input", () => { + expect(getPostHogExecDisplay({ command: "search query-" })).toEqual({ + label: "search", + input: "query-", + }); + }); + + it("falls back to bare `search` when no pattern given", () => { + expect(getPostHogExecDisplay({ command: "search" })).toEqual({ + label: "search", + input: undefined, + }); + }); + }); + + describe("tools verb", () => { + it("formats bare `tools`", () => { + expect(getPostHogExecDisplay({ command: "tools" })).toEqual({ + label: "tools", + input: undefined, + }); + }); + }); + + describe("explicit input field", () => { + it("prefers an explicit string `input` over command-embedded args (call)", () => { + expect( + getPostHogExecDisplay({ + command: 'call execute-sql {"query":"SELECT 1"}', + input: "SELECT 2", + }), + ).toEqual({ label: "execute-sql", input: "SELECT 2" }); + }); + + it("prefers an explicit object `input` (serialised) over command-embedded args (call)", () => { + expect( + getPostHogExecDisplay({ + command: "call execute-sql", + input: { query: "SELECT 1" }, + }), + ).toEqual({ label: "execute-sql", input: '{"query":"SELECT 1"}' }); + }); + + it("prefers explicit `input` over field path for schema", () => { + expect( + getPostHogExecDisplay({ + command: "schema query-trends", + input: "series.0", + }), + ).toEqual({ label: "schema query-trends", input: "series.0" }); + }); + + it("ignores empty-string explicit input and falls back to command args", () => { + expect( + getPostHogExecDisplay({ + command: 'call execute-sql {"query":"x"}', + input: " ", + }), + ).toEqual({ label: "execute-sql", input: '{"query":"x"}' }); + }); + }); + + describe("malformed / unsupported", () => { + it("returns null for unknown verbs", () => { + expect(getPostHogExecDisplay({ command: "unknown-verb foo" })).toBeNull(); + expect(getPostHogExecDisplay({ command: "list" })).toBeNull(); + expect(getPostHogExecDisplay({ command: "run something" })).toBeNull(); + }); + + it("returns null for missing or malformed input", () => { + expect(getPostHogExecDisplay(undefined)).toBeNull(); + expect(getPostHogExecDisplay(null)).toBeNull(); + expect(getPostHogExecDisplay({})).toBeNull(); + expect(getPostHogExecDisplay({ command: 42 })).toBeNull(); + expect(getPostHogExecDisplay({ command: "" })).toBeNull(); + }); + + it("returns null for `call` with no sub-tool", () => { + expect(getPostHogExecDisplay({ command: "call" })).toBeNull(); + expect(getPostHogExecDisplay({ command: "call " })).toBeNull(); + }); + + it("tolerates leading/trailing whitespace around the verb", () => { + expect(getPostHogExecDisplay({ command: " tools " })).toEqual({ + label: "tools", + input: undefined, + }); + expect( + getPostHogExecDisplay({ command: " call execute-sql " }), + ).toEqual({ label: "execute-sql", input: undefined }); + }); + }); +}); diff --git a/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts new file mode 100644 index 000000000..4f0ebbf78 --- /dev/null +++ b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts @@ -0,0 +1,103 @@ +/** + * The PostHog MCP exposes a single `exec` dispatcher that runs CLI-style + * subcommands. Generic MCP rendering would show this as + * `posthog - exec (MCP) {"command":"call execute-sql {…}"}` — pure plumbing + * with the dispatched action buried inside a JSON wrapper. + * + * These helpers pull the action out of the `command` string so the row can + * read `posthog - execute-sql {…}` (call), `posthog - info execute-sql`, + * `posthog - schema query-trends series`, `posthog - search query-`, or + * `posthog - tools` instead. + * + * Supported verbs (per the `exec` tool description): + * tools — list every tool + * search — search by name/title/description + * info — show description + input schema + * schema [field_path] — drill into a specific field + * call [--json] — invoke a tool + */ + +const POSTHOG_EXEC_TOOL_RE = /^mcp__(?:plugin_)?posthog(?:_[^_]+)*__exec$/; + +const POSTHOG_VERB_RE = + /^\s*(tools|search|info|schema|call)(?:\s+([\s\S]*))?\s*$/; +const POSTHOG_CALL_BODY_RE = /^(?:--json\s+)?([a-zA-Z0-9_-]+)\s*([\s\S]*)$/; +const POSTHOG_TOOL_NAME_RE = /^([a-zA-Z0-9_-]+)\s*([\s\S]*)$/; + +export interface PostHogExecDisplay { + /** Replaces the tool name in the title — e.g. "execute-sql", "info execute-sql". */ + label: string; + /** Args to show as the input preview, undefined when there is none to display. */ + input?: string; +} + +export function isPostHogExecTool(toolName: string): boolean { + return POSTHOG_EXEC_TOOL_RE.test(toolName); +} + +export function getPostHogExecDisplay( + toolInput: unknown, +): PostHogExecDisplay | null { + if (!toolInput || typeof toolInput !== "object") return null; + const obj = toolInput as { command?: unknown; input?: unknown }; + + if (typeof obj.command !== "string") return null; + const verbMatch = obj.command.match(POSTHOG_VERB_RE); + if (!verbMatch) return null; + + const verb = verbMatch[1] as "tools" | "search" | "info" | "schema" | "call"; + const rest = (verbMatch[2] ?? "").trim(); + const explicitInput = readExplicitInput(obj.input); + + switch (verb) { + case "tools": + return { label: "tools", input: undefined }; + + case "search": + return { + label: "search", + input: explicitInput ?? (rest.length > 0 ? rest : undefined), + }; + + case "info": + // `info ` — name the tool, no args portion. + return rest.length > 0 + ? { label: `info ${rest}`, input: undefined } + : { label: "info", input: undefined }; + + case "schema": { + // `schema [field_path]` — surface the tool, treat the path as args. + const m = rest.match(POSTHOG_TOOL_NAME_RE); + if (!m) return { label: "schema", input: undefined }; + const subTool = m[1]; + const fieldPath = (m[2] ?? "").trim(); + return { + label: `schema ${subTool}`, + input: explicitInput ?? (fieldPath.length > 0 ? fieldPath : undefined), + }; + } + + case "call": { + // `call [--json] [json_input]` — collapse the verb, surface the + // sub-tool as the label and the JSON body as args. + const m = rest.match(POSTHOG_CALL_BODY_RE); + if (!m) return null; + const subTool = m[1]; + const args = (m[2] ?? "").trim(); + return { + label: subTool, + input: explicitInput ?? (args.length > 0 ? args : undefined), + }; + } + } +} + +function readExplicitInput(value: unknown): string | undefined { + if (value === undefined || value === null) return undefined; + if (typeof value === "string") return value.trim() || undefined; + try { + return JSON.stringify(value); + } catch { + return undefined; + } +} From bf3c072b0501e4e811df90d28e828bef8fda09d0 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Mon, 4 May 2026 14:16:04 +0200 Subject: [PATCH 2/2] refine display labels to be more accurate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools → "List tools" (was "Read all tools" — `tools` returns names, not full schemas) - info → "Read " (was "Read "); bare info → "Read tool" - schema [path] → "Inspect fields" / "Inspect ." (folds path into a dotted locator so it reads as one path) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../utils/posthog-exec-display.test.ts | 34 +++++++++---------- .../mcp-apps/utils/posthog-exec-display.ts | 33 +++++++++++------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts index 70d4a6e36..8b19a03cd 100644 --- a/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts +++ b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts @@ -59,37 +59,37 @@ describe("getPostHogExecDisplay", () => { describe("info verb", () => { it("formats `info ` with no args", () => { expect(getPostHogExecDisplay({ command: "info execute-sql" })).toEqual({ - label: "info execute-sql", + label: "Read execute-sql", input: undefined, }); }); - it("falls back to bare `info` when no tool given", () => { + it("falls back to a generic label when no tool given", () => { expect(getPostHogExecDisplay({ command: "info" })).toEqual({ - label: "info", + label: "Read tool", input: undefined, }); }); }); describe("schema verb", () => { - it("formats `schema ` (no field path)", () => { + it("formats `schema ` (no field path) as field summary", () => { expect(getPostHogExecDisplay({ command: "schema query-trends" })).toEqual( { - label: "schema query-trends", + label: "Inspect query-trends fields", input: undefined, }, ); }); - it("formats `schema ` with the path as input", () => { + it("formats `schema ` as a dotted locator", () => { expect( getPostHogExecDisplay({ command: "schema query-trends series", }), ).toEqual({ - label: "schema query-trends", - input: "series", + label: "Inspect query-trends.series", + input: undefined, }); }); @@ -99,8 +99,8 @@ describe("getPostHogExecDisplay", () => { command: "schema query-trends breakdownFilter.breakdowns", }), ).toEqual({ - label: "schema query-trends", - input: "breakdownFilter.breakdowns", + label: "Inspect query-trends.breakdownFilter.breakdowns", + input: undefined, }); }); }); @@ -108,14 +108,14 @@ describe("getPostHogExecDisplay", () => { describe("search verb", () => { it("uses the regex pattern as input", () => { expect(getPostHogExecDisplay({ command: "search query-" })).toEqual({ - label: "search", + label: "Search tools", input: "query-", }); }); - it("falls back to bare `search` when no pattern given", () => { + it("falls back to bare `Search tools` when no pattern given", () => { expect(getPostHogExecDisplay({ command: "search" })).toEqual({ - label: "search", + label: "Search tools", input: undefined, }); }); @@ -124,7 +124,7 @@ describe("getPostHogExecDisplay", () => { describe("tools verb", () => { it("formats bare `tools`", () => { expect(getPostHogExecDisplay({ command: "tools" })).toEqual({ - label: "tools", + label: "List tools", input: undefined, }); }); @@ -149,13 +149,13 @@ describe("getPostHogExecDisplay", () => { ).toEqual({ label: "execute-sql", input: '{"query":"SELECT 1"}' }); }); - it("prefers explicit `input` over field path for schema", () => { + it("folds explicit `input` into the schema dotted locator", () => { expect( getPostHogExecDisplay({ command: "schema query-trends", input: "series.0", }), - ).toEqual({ label: "schema query-trends", input: "series.0" }); + ).toEqual({ label: "Inspect query-trends.series.0", input: undefined }); }); it("ignores empty-string explicit input and falls back to command args", () => { @@ -190,7 +190,7 @@ describe("getPostHogExecDisplay", () => { it("tolerates leading/trailing whitespace around the verb", () => { expect(getPostHogExecDisplay({ command: " tools " })).toEqual({ - label: "tools", + label: "List tools", input: undefined, }); expect( diff --git a/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts index 4f0ebbf78..f77f1e3c2 100644 --- a/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts +++ b/apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts @@ -5,9 +5,10 @@ * with the dispatched action buried inside a JSON wrapper. * * These helpers pull the action out of the `command` string so the row can - * read `posthog - execute-sql {…}` (call), `posthog - info execute-sql`, - * `posthog - schema query-trends series`, `posthog - search query-`, or - * `posthog - tools` instead. + * read `posthog - execute-sql {…}` (call), `posthog - Read execute-sql` + * (info), `posthog - Inspect query-trends.series` (schema), + * `posthog - Search tools query-` (search), or `posthog - List tools` + * (tools) instead. * * Supported verbs (per the `exec` tool description): * tools — list every tool @@ -25,7 +26,7 @@ const POSTHOG_CALL_BODY_RE = /^(?:--json\s+)?([a-zA-Z0-9_-]+)\s*([\s\S]*)$/; const POSTHOG_TOOL_NAME_RE = /^([a-zA-Z0-9_-]+)\s*([\s\S]*)$/; export interface PostHogExecDisplay { - /** Replaces the tool name in the title — e.g. "execute-sql", "info execute-sql". */ + /** Replaces the tool name in the title — e.g. "execute-sql", "Read execute-sql". */ label: string; /** Args to show as the input preview, undefined when there is none to display. */ input?: string; @@ -51,29 +52,35 @@ export function getPostHogExecDisplay( switch (verb) { case "tools": - return { label: "tools", input: undefined }; + // `tools` returns names only, not full schemas — "List", not "Read". + return { label: "List tools", input: undefined }; case "search": return { - label: "search", + label: "Search tools", input: explicitInput ?? (rest.length > 0 ? rest : undefined), }; case "info": - // `info ` — name the tool, no args portion. + // `info ` — fold the tool name into the label so the args slot stays clean. return rest.length > 0 - ? { label: `info ${rest}`, input: undefined } - : { label: "info", input: undefined }; + ? { label: `Read ${rest}`, input: undefined } + : { label: "Read tool", input: undefined }; case "schema": { - // `schema [field_path]` — surface the tool, treat the path as args. + // `schema [field_path]` is the drill-down verb. Fold the + // tool + path into a dotted locator so it reads as one path. const m = rest.match(POSTHOG_TOOL_NAME_RE); - if (!m) return { label: "schema", input: undefined }; + if (!m) return { label: "Inspect schema", input: undefined }; const subTool = m[1]; const fieldPath = (m[2] ?? "").trim(); + const path = + explicitInput ?? (fieldPath.length > 0 ? fieldPath : undefined); return { - label: `schema ${subTool}`, - input: explicitInput ?? (fieldPath.length > 0 ? fieldPath : undefined), + label: path + ? `Inspect ${subTool}.${path}` + : `Inspect ${subTool} fields`, + input: undefined, }; }