analytics
Aggregate post views, reactions, and comments from your connected CMS platforms (Devto, Ghost, Hashnode, WordPress). Get per-platform breakdowns and total summaries to monitor content performance.
Instructions
Fetch post views, reactions, and comments from every configured CMS platform (devto, ghost, hashnode, wordpress) and aggregate the totals. FREE. Requires platform credentials. Medium and Substack are listed but their APIs do not expose post-level analytics. Returns: { platforms: [{ platform, posts: [{ title, url, views?, reactions?, comments?, published_at? }], note? }], summary: { total_posts, total_views, total_reactions } }. Common errors: no platforms configured (VALIDATION_ERROR), upstream auth failure surfaces per-platform in 'note'.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| platform | No | Specific platform to fetch analytics for, or omit for all configured platforms | |
| limit | No | Max posts to fetch per platform (default: 10) |
Implementation Reference
- src/tools/analytics-tools.ts:254-328 (handler)Main handler for the analytics tool - fetches post analytics (views, reactions, comments) from all configured CMS platforms (devto, ghost, hashnode, wordpress) and aggregates the totals.
export async function handleAnalytics( input: z.infer<typeof analyticsSchema> ): Promise<ToolResult<AnalyticsResult>> { const config = readConfig(); const platforms = config.platforms; if (!platforms || Object.keys(platforms).length === 0) { return makeError( "VALIDATION_ERROR", "No platforms configured. Run the \"setup\" tool to add platform credentials first." ); } const targetPlatforms = input.platform ? [input.platform] : Object.keys(platforms); const limit = input.limit ?? 10; const results: PlatformAnalytics[] = []; for (const platform of targetPlatforms) { if (input.platform && !platforms[platform as keyof typeof platforms]) { return makeError( "VALIDATION_ERROR", `Platform "${platform}" is not configured. Run the "setup" tool first.` ); } if (platform === "devto" && platforms.devto?.api_key) { results.push(await fetchDevtoAnalytics(platforms.devto.api_key, limit)); } else if (platform === "ghost" && platforms.ghost?.url && platforms.ghost?.admin_key) { results.push(await fetchGhostAnalytics(platforms.ghost, limit)); } else if ( platform === "hashnode" && platforms.hashnode?.token && platforms.hashnode?.publication_id ) { results.push(await fetchHashnodeAnalytics(platforms.hashnode, limit)); } else if ( platform === "wordpress" && platforms.wordpress?.url && platforms.wordpress?.username && platforms.wordpress?.app_password ) { results.push(await fetchWordpressAnalytics(platforms.wordpress, limit)); } else if (platform === "medium") { results.push({ platform: "medium", posts: [], note: "Medium API does not support analytics", }); } else if (platform === "substack") { results.push({ platform: "substack", posts: [], note: "Substack post analytics are not yet wired into the unified analytics view. Use list_posts on the substack platform to see drafts + published posts.", }); } } // Compute summary let total_posts = 0; let total_views = 0; let total_reactions = 0; for (const r of results) { total_posts += r.posts.length; for (const p of r.posts) { total_views += p.views ?? 0; total_reactions += p.reactions ?? 0; } } return makeSuccess({ platforms: results, summary: { total_posts, total_views, total_reactions }, }); } - src/tools/analytics-tools.ts:8-18 (schema)Zod schema for the analytics tool input - accepts an optional platform filter and optional limit (defaults to 10).
export const analyticsSchema = z.object({ platform: z .string() .optional() .describe("Specific platform to fetch analytics for, or omit for all configured platforms"), limit: z .number() .optional() .default(10) .describe("Max posts to fetch per platform (default: 10)"), }); - src/index.ts:178-182 (registration)Registration of the 'analytics' tool on the MCP server - wires up the schema, handler, and formatter.
server.tool("analytics", "Fetch post views, reactions, and comments from every configured CMS platform (devto, ghost, hashnode, wordpress) and aggregate the totals. FREE. Requires platform credentials. Medium and Substack are listed but their APIs do not expose post-level analytics. Returns: { platforms: [{ platform, posts: [{ title, url, views?, reactions?, comments?, published_at? }], note? }], summary: { total_posts, total_views, total_reactions } }. Common errors: no platforms configured (VALIDATION_ERROR), upstream auth failure surfaces per-platform in 'note'.", analyticsSchema.shape, async (input) => { const parsed = analyticsSchema.parse(input); const result = await handleAnalytics(parsed); return { content: [{ type: "text", text: formatToolResponse("analytics", result, formatAnalytics) }] }; }); - src/format-response.ts:325-395 (helper)Markdown formatter for the analytics tool response - displays a summary section and per-platform post tables.
export function formatAnalytics(data: unknown): string { const d = data as { platforms: Array<{ platform: string; posts: Array<{ title: string; url: string; views?: number; reactions?: number; comments?: number; published_at?: string; status?: string; }>; note?: string; }>; summary: { total_posts: number; total_views: number; total_reactions: number }; }; const lines = [ "# Analytics Overview", "", section( "Summary", [ field("Total posts", `${d.summary.total_posts} across ${d.platforms.length} platform${d.platforms.length !== 1 ? "s" : ""}`), field("Total views", d.summary.total_views.toLocaleString()), field("Total reactions", d.summary.total_reactions.toLocaleString()), ].join("\n") ), ]; for (const plat of d.platforms) { const label = platformLabel(plat.platform); if (plat.note) { lines.push("", section(label, note(plat.note))); continue; } if (plat.posts.length === 0) { lines.push("", section(label, "No posts found.")); continue; } const hasViews = plat.posts.some((p) => p.views !== undefined); const hasReactions = plat.posts.some((p) => p.reactions !== undefined); const hasComments = plat.posts.some((p) => p.comments !== undefined); const headers = ["Title"]; if (hasViews) headers.push("Views"); if (hasReactions) headers.push("Reactions"); if (hasComments) headers.push("Comments"); if (!hasViews && !hasReactions && !hasComments) { headers.push("Status", "Published"); } const rows = plat.posts.map((p) => { const row: (string | number)[] = [p.title]; if (hasViews) row.push(p.views?.toLocaleString() ?? "\u2014"); if (hasReactions) row.push(p.reactions?.toLocaleString() ?? "\u2014"); if (hasComments) row.push(p.comments?.toLocaleString() ?? "\u2014"); if (!hasViews && !hasReactions && !hasComments) { row.push(p.status ?? "\u2014"); row.push(p.published_at ? formatDate(p.published_at) : "\u2014"); } return row; }); lines.push("", section(label, table(headers, rows))); } return lines.join("\n"); } - src/tools/analytics-tools.ts:47-77 (helper)Fetches analytics from Dev.to API - returns posts with page_views_count, positive_reactions_count, and comments_count.
async function fetchDevtoAnalytics(apiKey: string, limit: number): Promise<PlatformAnalytics> { const result = await httpRequest(`https://dev.to/api/articles/me?per_page=${limit}`, { method: "GET", headers: { "api-key": apiKey }, }); if (!result.success) { return { platform: "devto", posts: [], note: `Error: ${result.error.message}` }; } const articles = result.data as Array<{ title: string; url: string; page_views_count: number; positive_reactions_count: number; comments_count: number; published_at: string | null; }>; return { platform: "devto", posts: articles.map((a) => ({ title: a.title, url: a.url, views: a.page_views_count, reactions: a.positive_reactions_count, comments: a.comments_count, published_at: a.published_at || undefined, status: "published", })), };