-
Notifications
You must be signed in to change notification settings - Fork 14
feat(code): rich formatting for posthog mcp exec tool calls #1992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
skoob13
wants to merge
2
commits into
main
Choose a base branch
from
feat/posthog-exec-rich-display
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+333
−2
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 201 additions & 0 deletions
201
apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <tool>` 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 <tool> {json}`", () => { | ||
| expect( | ||
| getPostHogExecDisplay({ | ||
| command: 'call --json experiment-update {"id":1}', | ||
| }), | ||
| ).toEqual({ | ||
| label: "experiment-update", | ||
| input: '{"id":1}', | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("info verb", () => { | ||
| it("formats `info <tool>` with no args", () => { | ||
| expect(getPostHogExecDisplay({ command: "info execute-sql" })).toEqual({ | ||
| label: "Read execute-sql", | ||
| input: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it("falls back to a generic label when no tool given", () => { | ||
| expect(getPostHogExecDisplay({ command: "info" })).toEqual({ | ||
| label: "Read tool", | ||
| input: undefined, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("schema verb", () => { | ||
| it("formats `schema <tool>` (no field path) as field summary", () => { | ||
| expect(getPostHogExecDisplay({ command: "schema query-trends" })).toEqual( | ||
| { | ||
| label: "Inspect query-trends fields", | ||
| input: undefined, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it("formats `schema <tool> <field_path>` as a dotted locator", () => { | ||
| expect( | ||
| getPostHogExecDisplay({ | ||
| command: "schema query-trends series", | ||
| }), | ||
| ).toEqual({ | ||
| label: "Inspect query-trends.series", | ||
| input: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it("supports dotted field paths", () => { | ||
| expect( | ||
| getPostHogExecDisplay({ | ||
| command: "schema query-trends breakdownFilter.breakdowns", | ||
| }), | ||
| ).toEqual({ | ||
| label: "Inspect query-trends.breakdownFilter.breakdowns", | ||
| input: undefined, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("search verb", () => { | ||
| it("uses the regex pattern as input", () => { | ||
| expect(getPostHogExecDisplay({ command: "search query-" })).toEqual({ | ||
| label: "Search tools", | ||
| input: "query-", | ||
| }); | ||
| }); | ||
|
|
||
| it("falls back to bare `Search tools` when no pattern given", () => { | ||
| expect(getPostHogExecDisplay({ command: "search" })).toEqual({ | ||
| label: "Search tools", | ||
| input: undefined, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("tools verb", () => { | ||
| it("formats bare `tools`", () => { | ||
| expect(getPostHogExecDisplay({ command: "tools" })).toEqual({ | ||
| label: "List 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("folds explicit `input` into the schema dotted locator", () => { | ||
| expect( | ||
| getPostHogExecDisplay({ | ||
| command: "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", () => { | ||
| 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: "List tools", | ||
| input: undefined, | ||
| }); | ||
| expect( | ||
| getPostHogExecDisplay({ command: " call execute-sql " }), | ||
| ).toEqual({ label: "execute-sql", input: undefined }); | ||
| }); | ||
| }); | ||
| }); | ||
110 changes: 110 additions & 0 deletions
110
apps/code/src/renderer/features/mcp-apps/utils/posthog-exec-display.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /** | ||
| * 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 - 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 | ||
| * search <regex> — search by name/title/description | ||
| * info <tool> — show description + input schema | ||
| * schema <tool> [field_path] — drill into a specific field | ||
| * call [--json] <tool> <json_input> — 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", "Read 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": | ||
| // `tools` returns names only, not full schemas — "List", not "Read". | ||
| return { label: "List tools", input: undefined }; | ||
|
|
||
| case "search": | ||
| return { | ||
| label: "Search tools", | ||
| input: explicitInput ?? (rest.length > 0 ? rest : undefined), | ||
| }; | ||
|
|
||
| case "info": | ||
| // `info <tool>` — fold the tool name into the label so the args slot stays clean. | ||
| return rest.length > 0 | ||
| ? { label: `Read ${rest}`, input: undefined } | ||
| : { label: "Read tool", input: undefined }; | ||
|
|
||
| case "schema": { | ||
| // `schema <tool> [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: "Inspect schema", input: undefined }; | ||
| const subTool = m[1]; | ||
| const fieldPath = (m[2] ?? "").trim(); | ||
| const path = | ||
| explicitInput ?? (fieldPath.length > 0 ? fieldPath : undefined); | ||
| return { | ||
| label: path | ||
| ? `Inspect ${subTool}.${path}` | ||
| : `Inspect ${subTool} fields`, | ||
| input: undefined, | ||
| }; | ||
| } | ||
|
|
||
| case "call": { | ||
| // `call [--json] <tool> [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; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.