Building an MCP server for app analytics, so Claude can tell me how my apps are doing A developer built an MCP server for Apparent, a self-built mobile app analytics tool that reads App Store Connect, Google Play, RevenueCat and its own SDK, so that Claude Code can answer questions like "how are my apps doing?" or "why did downloads drop on Tuesday?" without opening a dashboard. The server exposes four read-only tools that return plain-language answers rather than raw rows, with newer tools also returning a compact JSON copy and structuredContent, and it uses loose output schemas, stderr redirection for logs, and lazy imports to keep the stdio JSON-RPC stream intact. I build a few mobile apps on the side, and I built my own analytics for them, Apparent https://apparent.fyi/?ref=devto . It reads App Store Connect, Google Play, RevenueCat and its own SDK, and turns them into plain sentences. I spend most of the day in Claude Code anyway. So I wanted to ask it "how are my apps doing?" or "why did downloads drop on Tuesday?" without opening a dashboard. That's what MCP is for: a small server that gives the agent tools it can call. The first version was small: four read-only tools. Making it answer well took longer. These are the things that mattered. I already had a command line tool, apparent , that answers in sentences. The MCP tools call the same answer functions, so the agent and the terminal say the same thing. I kept sentences on purpose. App numbers come with a lot of small print: store data arrives one or two days late, so today never has downloads yet. Google's pre-launch test phones and Apple's review are not real people. Money is what you keep after the store's cut. If the agent gets raw rows, it has to know all of that. If it gets a sentence, the small print is already in it. So the server's instructions end with one rule: js export const INSTRUCTIONS = "Apparent is the analytics for your mobile apps ... " + "Every tool is read-only and answers in plain words; quote its wording. " + "Test phones Google's pre-launch phones, Apple's review are never counted. " + "Store numbers arrive 1–2 days late, so 'today' has no downloads yet. ..."; "Quote its wording" does a lot. Without it the agent rewrites the answer and the caveats fall off. Sentences are great to read, but sometimes the agent wants to do something with the numbers: make a chart, compare two apps, write a note in a PR. The newer tools return three things: the sentence, a compact JSON copy as a second text block, and the same JSON as structuredContent . The copy in text is there because some clients only read text content. js const withJson = a: Answer, json = a.json = { content: { type: "text", text: a.text }, { type: "text", text: JSON.stringify json } , structuredContent: json, } ; The older tools stay text only. Their JSON is the whole page and it's too big for one reply. This one is easy to miss. If a tool declares an outputSchema , clients like Claude Code and the official SDK check the reply against it, and a key the schema doesn't list makes the call fail. My today tool answers with more keys for all apps together than for one app. So every output schema is an open object, and only the keys I promise are listed: js const out =