Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gatsby/onPreBootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ posthog.init("${process.env.GATSBY_POSTHOG_API_KEY}", {

// Cache the data if successful
if (!mcpToolsData.error && mcpToolsData.categories) {
await cache.set(MCP_TOOLS_CACHE_KEY, mcpToolsData.categories)
await cache.set(MCP_TOOLS_CACHE_KEY, mcpToolsData)
}

if (process.env.POSTHOG_APP_API_KEY && !(await cache.get(PAGEVIEW_CACHE_KEY))) {
Expand Down
27 changes: 24 additions & 3 deletions gatsby/utils/fetchMCPTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const MCP_TOOLS_URL =
interface MCPTool {
category?: string
summary: string
description?: string
required_scopes?: string[]
}

interface ToolCategory {
Expand All @@ -18,10 +20,20 @@ interface ToolCategory {
}>
}

export async function fetchAndProcessMCPTools(): Promise<{
interface ToolByName {
summary: string
description?: string
category?: string
required_scopes?: string[]
}

export interface MCPToolsData {
categories: ToolCategory[] | null
byName: Record<string, ToolByName> | null
error: boolean
}> {
}

export async function fetchAndProcessMCPTools(): Promise<MCPToolsData> {
try {
const response = await fetch(MCP_TOOLS_URL)

Expand All @@ -33,6 +45,7 @@ export async function fetchAndProcessMCPTools(): Promise<{

// Process the tools into categories
const toolCategories: Record<string, Array<{ name: string; summary: string }>> = {}
const byName: Record<string, ToolByName> = {}

Object.entries(mcpTools).forEach(([toolName, toolDef]) => {
const category = toolDef.category || 'Uncategorized'
Expand All @@ -43,6 +56,12 @@ export async function fetchAndProcessMCPTools(): Promise<{
name: toolName,
summary: toolDef.summary,
})
byName[toolName] = {
summary: toolDef.summary,
description: toolDef.description,
category: toolDef.category,
required_scopes: toolDef.required_scopes,
}
})

// Sort tools within each category alphabetically
Expand All @@ -57,18 +76,20 @@ export async function fetchAndProcessMCPTools(): Promise<{

return {
categories: categoriesArray,
byName,
error: false,
}
} catch (error) {
console.error('Error fetching MCP tools:', error)
return {
categories: null,
byName: null,
error: true,
}
}
}

export function writeMCPToolsToFile(data: { categories: ToolCategory[] | null; error: boolean }): void {
export function writeMCPToolsToFile(data: MCPToolsData): void {
const mcpToolsPath = path.resolve(__dirname, '../../src/data/mcp-tools.json')
fs.mkdirSync(path.dirname(mcpToolsPath), { recursive: true })
fs.writeFileSync(mcpToolsPath, JSON.stringify(data, null, 2))
Expand Down
104 changes: 104 additions & 0 deletions scripts/generate-mcp-rest-mapping-candidates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env node
/*
* One-shot helper to suggest entries for src/data/mcp-rest-mapping.json.
*
* Fetches the live OpenAPI spec and the MCP tool definitions, finds operationIds
* whose direct `_` → `-` transformation matches an existing MCP tool name, and
* prints a JSON blob of NEW candidates (i.e. not already in the mapping file).
*
* Output is meant to be reviewed by a human before pasting into the mapping
* file. The script never writes to the mapping file directly.
*
* Usage:
* node scripts/generate-mcp-rest-mapping-candidates.js
* node scripts/generate-mcp-rest-mapping-candidates.js --merge # writes merged file
*/

const fs = require('fs')
const path = require('path')
const https = require('https')

const SPEC_URL = process.env.POSTHOG_OPEN_API_SPEC_URL || 'https://app.posthog.com/api/schema/'
const TOOLS_URL =
'https://raw.githubusercontent.com/PostHog/posthog/refs/heads/master/services/mcp/schema/tool-definitions-all.json'
const MAPPING_FILE = path.resolve(__dirname, '../src/data/mcp-rest-mapping.json')

function get(url, headers = {}) {
return new Promise((resolve, reject) => {
https
.get(url, { headers }, (res) => {
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
resolve(get(res.headers.location, headers))
return
}
let body = ''
res.on('data', (c) => (body += c))
res.on('end', () => resolve(body))
})
.on('error', reject)
})
}

async function main() {
const [specBody, toolsBody] = await Promise.all([
get(SPEC_URL, { Accept: 'application/json' }),
get(TOOLS_URL),
])

let spec
try {
spec = JSON.parse(specBody)
} catch {
console.error('Failed to parse OpenAPI spec as JSON. Try setting POSTHOG_OPEN_API_SPEC_URL to a JSON variant.')
process.exit(1)
}
const tools = JSON.parse(toolsBody)
const toolNames = new Set(Object.keys(tools))

const operationIds = []
for (const methods of Object.values(spec.paths || {})) {
for (const op of Object.values(methods)) {
if (op && typeof op === 'object' && op.operationId) {
operationIds.push(op.operationId)
}
}
}

const existing = fs.existsSync(MAPPING_FILE) ? JSON.parse(fs.readFileSync(MAPPING_FILE, 'utf8')) : {}

const candidates = {}
for (const opId of operationIds) {
const candidate = opId.replace(/_/g, '-')
if (toolNames.has(candidate) && !existing[opId]) {
candidates[opId] = [candidate]
}
}

console.log(`Spec ops: ${operationIds.length}`)
console.log(`MCP tools: ${toolNames.size}`)
console.log(`Existing mappings: ${Object.keys(existing).length}`)
console.log(`New direct-match candidates: ${Object.keys(candidates).length}`)
console.log('')

if (Object.keys(candidates).length === 0) {
console.log('No new candidates.')
return
}

if (process.argv.includes('--merge')) {
const merged = { ...existing, ...candidates }
const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)))
fs.writeFileSync(MAPPING_FILE, JSON.stringify(sorted, null, 2) + '\n')
console.log(`Wrote ${Object.keys(merged).length} entries to ${MAPPING_FILE}`)
} else {
console.log('Candidate JSON (paste into src/data/mcp-rest-mapping.json after review):')
console.log(JSON.stringify(candidates, null, 2))
console.log('')
console.log('Or run with --merge to write directly.')
}
}

main().catch((err) => {
console.error(err)
process.exit(1)
})
55 changes: 55 additions & 0 deletions src/components/Docs/MCPCallout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import mcpRestMapping from '../../data/mcp-rest-mapping.json'
import mcpToolsData from '../../data/mcp-tools.json'

interface ToolByName {
summary: string
description?: string
category?: string
required_scopes?: string[]
}

interface MCPToolsData {
byName?: Record<string, ToolByName> | null
error?: boolean
}

interface MCPCalloutProps {
operationId: string
}

const MCPCallout: React.FC<MCPCalloutProps> = ({ operationId }) => {
const mapping = mcpRestMapping as Record<string, string[]>
const toolNames = mapping[operationId]
if (!toolNames || toolNames.length === 0) {
return null
}

const { byName } = mcpToolsData as MCPToolsData
const matched = toolNames
.map((name) => ({ name, info: byName?.[name] }))
.filter((t): t is { name: string; info: ToolByName } => Boolean(t.info))

if (matched.length === 0) {
return null
}

return (
<blockquote className="p-4 mb-4 rounded bg-accent border-l-4 border-yellow not-prose">
<p className="text-sm font-semibold mb-2">
Also available via the{' '}
<a href="/docs/model-context-protocol">PostHog MCP server</a>:
</p>
<ul className="m-0 p-0 list-none space-y-1">
{matched.map(({ name, info }) => (
<li key={name} className="text-sm">
<code>{name}</code>
{info.summary ? <> — {info.summary}</> : null}
</li>
))}
</ul>
</blockquote>
)
}

export default MCPCallout
Loading
Loading