Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
platformNoSpecific platform to fetch analytics for, or omit for all configured platforms
limitNoMax posts to fetch per platform (default: 10)

Implementation Reference

  • 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 },
      });
    }
  • 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) }] };
    });
  • 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");
    }
  • 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",
        })),
      };
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Given no annotations, description fully covers behavior: requires credentials, free, platform limitations, return format, and common errors.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Efficiently structured with two main sentences plus error notes. Front-loaded purpose, no wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema, but description provides comprehensive return structure and error scenarios. Complete for a 2-parameter fetch tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but description adds value by explaining 'omit for all configured platforms' for platform parameter and default/behavior for limit.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it fetches post views, reactions, and comments from multiple CMS platforms and aggregates totals. Distinguishes from siblings which are social media or publishing tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides context on required credentials, free usage, platform limitations, and common errors. Lacks explicit when-to-use vs alternatives but still helpful.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MendleM/pipepost'

If you have feedback or need assistance with the MCP directory API, please join our Discord server