How the PostHog MCP Server Exposes Tools: Registration Flow and HTTP Endpoints
The PostHog MCP server exposes tools by discovering available definitions during the worker's init() phase, registering normalized tool metadata with the underlying McpServer via registerTool, and automatically exposing each tool as a callable REST endpoint at /api/environments/<env>/mcp_tools/<tool-name>/.
The Model Context Protocol (MCP) implementation in PostHog enables AI models to interact with product analytics through standardized tool definitions. This article examines how the MCP server exposes tools by tracing the registration flow from YAML manifest discovery to HTTP endpoint generation in the PostHog/posthog repository.
Tool Discovery and Catalog Building
During initialization, the MCP worker gathers available tools based on feature flags and access controls. The getToolsFromContext function (lines 447-456 in services/mcp/src/mcp.ts) collects tools that satisfy client-specific rules, filtering by read-only requirements and exclusion lists.
Static tool definitions—including titles, descriptions, input schemas, and UI metadata—are loaded via getToolDefinition from generated JSON/YAML files located in services/mcp/src/tools/toolDefinitions.ts (lines 65-71). Product-level manifests in products/*/mcp/tools.yaml drive this generation, ensuring tool catalogs remain synchronized with product capabilities.
The Registration Pipeline
The registerTool method in services/mcp/src/mcp.ts (lines 364-498) serves as the core mechanism for exposing tools to the MCP ecosystem.
Normalizing UI Metadata
Before registration, the system processes the _meta object to ensure backward compatibility. If a tool definition includes ui.resourceUri without the legacy RESOURCE_URI_META_KEY, the code enriches the metadata at lines 329-337:
let normalizedMeta = tool._meta
if (tool._meta?.ui?.resourceUri && !tool._meta[RESOURCE_URI_META_KEY]) {
normalizedMeta = { ...tool._meta, [RESOURCE_URI_META_KEY]: tool._meta.ui.resourceUri }
}
This normalization ensures that UI resource identifiers remain accessible to legacy clients while supporting modern metadata structures.
Handler Wrapping and Validation
The user-provided handler undergoes wrapping to enforce validation, analytics tracking, and error conversion. The wrapped handler (lines 364-426) performs context-switching for analytics, handles UI-resource specific logic, and converts errors into standardized ToolResult payloads before returning to the caller.
The final registration call passes the normalized metadata to the underlying SDK:
this.server.registerTool(
tool.name,
{
title: tool.title,
description: tool.description,
inputSchema: tool.schema.shape,
annotations: tool.annotations,
...(normalizedMeta ? { _meta: normalizedMeta } : {}),
},
wrappedHandler as unknown as ToolCallback<TSchema['shape']>
)
Server Initialization and Endpoint Exposure
The MCP server initializes with a default prompt template before recreating the instance with the final tool catalog. Lines 66-67 in services/mcp/src/mcp.ts construct the initial server:
server = new McpServer({ name: 'PostHog', version: '1.0.0' }, { instructions: INSTRUCTIONS_TEMPLATE_V1 })
After tool discovery and prompt generation complete, the server reinstantiates with the updated instructions (lines 93-95).
Each registered tool automatically becomes available at a dedicated REST endpoint. The URL construction logic in services/mcp/src/tools/posthogAiTools/invokeTool.ts (line 23) builds the path:
const url = `${context.api.baseUrl}/api/environments/${projectId}/mcp_tools/${toolName}/`
This endpoint routes requests to the correct durable object instance, validates incoming payloads against the tool's Zod schema, and executes the wrapped handler.
Practical Implementation Examples
Registering a Custom Tool
Developers can expose new functionality by creating tool definitions and registering them during worker initialization:
import { z } from 'zod'
import { Tool } from '@/tools/types'
const myTool: Tool<typeof z.object({ query: z.string() })> = {
name: 'my-custom-query',
title: 'Run a custom query',
description: 'Executes a bespoke SQL query against the data warehouse.',
schema: z.object({ query: z.string() }),
handler: async (context, { query }) => {
return { rows: await context.api.sql.run(query) }
},
}
mcp.registerTool(myTool, myTool.handler)
Once registered, the tool appears in the MCP system prompt and becomes callable at /api/environments/<env>/mcp_tools/my-custom-query/.
Invoking Tools via HTTP
Clients interact with exposed tools through standardized HTTP POST requests:
curl -X POST "https://mcp.posthog.com/api/environments/123/mcp_tools/execute-sql/" \
-H "Authorization: Bearer <api-token>" \
-H "Content-Type: application/json" \
-d '{"query":"SELECT * FROM events LIMIT 10"}'
The server validates the request body against the tool's input schema, executes the handler, and returns a JSON payload containing either structuredContent for UI applications or plain text responses.
Summary
- Tool discovery occurs during the worker's
init()phase viagetToolsFromContextinservices/mcp/src/mcp.ts, filtering tools by feature flags and access rules. - Registration happens through the
registerToolmethod (lines 364-498), which normalizes UI metadata and wraps handlers for validation and analytics. - HTTP endpoints generate automatically at
/api/environments/<env>/mcp_tools/<tool-name>/as implemented inservices/mcp/src/tools/posthogAiTools/invokeTool.ts. - Metadata normalization ensures backward compatibility by mapping
ui.resourceUrito legacy keys before SDK registration. - Source definitions reside in product-level YAML manifests (
products/*/mcp/tools.yaml) and are consumed byservices/mcp/src/tools/toolDefinitions.ts.
Frequently Asked Questions
How does the MCP server determine which tools to expose?
The server calls getToolsFromContext during initialization to gather tools matching the client's feature flags, read-only constraints, and exclusion rules. Static definitions are loaded from generated JSON files via getToolDefinition in services/mcp/src/tools/toolDefinitions.ts, ensuring only authorized tools appear in the catalog.
What is the URL pattern for exposed MCP tools?
Each tool exposes a REST endpoint at /api/environments/{projectId}/mcp_tools/{tool-name}/ as constructed in services/mcp/src/tools/posthogAiTools/invokeTool.ts (line 23). This pattern routes requests to the specific tool handler registered with the McpServer instance.
How does the server handle UI-specific metadata in tool definitions?
The registerTool method normalizes the _meta object by checking for ui.resourceUri and injecting the legacy RESOURCE_URI_META_KEY if absent (lines 329-337 in services/mcp/src/mcp.ts). This ensures backward compatibility while supporting modern UI resource annotations.
Can custom tools be registered dynamically?
Yes. Developers can define tools using Zod schemas and the Tool interface, then pass them to mcp.registerTool() along with their handler functions. The tool immediately becomes available in the system prompt and at its dedicated HTTP endpoint without requiring server restarts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →