{"slug": "how-to-expose-your-saas-data-to-claude-and-chatgpt-with-mcp-a-real-44-tool", "title": "How to expose your SaaS data to Claude and ChatGPT with MCP (a real 44-tool server)", "summary": "Howseen founder Raphaël Aubry detailed a production MCP server at app.howseen.ai/api/mcp (version 1.3.0) that exposes the company's AI-visibility data to Claude, ChatGPT and Cursor through 44 tools, 26 of them read-only. The server is stateless, running as a Next.js route handler on a serverless runtime with sessions disabled and JSON responses enabled, and it allows keyless tool discovery while requiring a bearer key for tool calls. Aubry said keys are prefixed hs_live_, shown once and stored only as a sha256 hash, and that keyless requests receive a database handle that throws on any access.", "body_md": "*Originally published on howseen.ai: [howseen.ai/mcp](https://howseen.ai/mcp?utm_source=devto&utm_medium=referral&utm_campaign=o2-syndication)*\n\n*By Raphaël Aubry, founder of Howseen*\n\nOur product measures how ChatGPT, Gemini, Perplexity and Google's AI answers talk about a brand. That data lived in a dashboard, while more of our users were already working inside Claude, ChatGPT or Cursor. So we brought the data to the assistant instead of asking people to open one more tab, and shipped an MCP server. It runs in production at `https://app.howseen.ai/api/mcp`, version 1.3.0, with 44 tools: 26 read tools and 18 write tools. This post walks through the design choices, with the real code, so you can do the same for your own SaaS.\n\nTool discovery works without a key, so you can list the tools right now:\n\n```\ncurl -s -X POST https://app.howseen.ai/api/mcp \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}' \\\n  | jq '.result.tools | length'\n# 44\n```\n\nCount the read-only ones:\n\n```\n... | jq '[.result.tools[] | select(.annotations.readOnlyHint)] | length'\n# 26\n```\n\nNow try to call a tool without a key:\n\n```\ncurl -s -X POST https://app.howseen.ai/api/mcp \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"list_brands\",\"arguments\":{}}}'\n```\n\nYou get an HTTP 401 with `WWW-Authenticate: Bearer realm=\"howseen\"` and a JSON-RPC error (code `-32001`) whose message tells you where to create a key. That split, open discovery and keyed calls, is the first design decision.\n\nThe app is a Next.js route handler on a serverless runtime. Keeping MCP sessions in memory between requests does not fit that model, so the server is stateless: one server and one transport per POST, JSON responses, no session id.\n\n``` js\nconst server = buildMcpServer(ctx);\nconst transport = new WebStandardStreamableHTTPServerTransport({\n  sessionIdGenerator: undefined,\n  enableJsonResponse: true,\n});\nawait server.connect(transport);\nreturn withCors(await transport.handleRequest(fwd));\n```\n\n`sessionIdGenerator: undefined` turns sessions off. `enableJsonResponse: true` returns a plain JSON body instead of opening an event stream. Since there is no stream to open, a GET asking for `text/event-stream` gets a 405, and a plain GET returns a small JSON description (name, version, tool names, auth rules) that doubles as a health check.\n\nOne detail to get right: we read the request body once to decide on auth, so we hand the transport a fresh `Request` with the same bytes. Otherwise the transport finds an already consumed body.\n\nMCP directories and clients probe a remote server before the user has pasted anything: `initialize`, then `tools/list`. If those calls return 401, your server looks broken in every listing. These methods only return static metadata, so we let them through:\n\n``` js\nexport const DISCOVERY_METHODS: ReadonlySet<string> = new Set([\n  \"initialize\",\n  \"notifications/initialized\",\n  \"ping\",\n  \"tools/list\",\n  \"prompts/list\",\n  \"resources/list\",\n  \"resources/templates/list\",\n]);\n\nexport function decideMcpAuth(keyPresent: boolean, body: unknown): McpAuthDecision {\n  if (keyPresent) return \"authenticate\";\n  return isDiscoveryOnly(body) ? \"open\" : \"needs_key\";\n}\n```\n\nTwo guards keep this safe. First, JSON-RPC allows batches, so `isDiscoveryOnly` requires every message in the batch to be a discovery method: one `tools/call` hidden in a batch makes the whole request need a key. Second, the keyless context passes a database handle that throws on any access, so even a coding mistake cannot leak data:\n\n```\ndb: new Proxy({}, { get() { throw new Error(\"no data access without an API key\"); } }) as SupabaseClient,\n```\n\nUsers create a personal key in Settings. It starts with `hs_live_`, is shown once, and only its sha256 hash is stored. The server reads it from `Authorization: Bearer hs_live_...`.\n\nSome AI apps cannot send custom headers, so the key can also go in the server URL as `?key=`. We document that this URL must be treated like a password and that a leaked key can be revoked in Settings.\n\nWhen a key is missing or invalid, the 401 carries a JSON-RPC error body, not just a status code. MCP clients display that message to the user, so it is where you explain what to do next.\n\nEvery tool declares MCP annotations. Here is what `tools/list` returns for the first read tool:\n\n```\n{\n  \"name\": \"list_brands\",\n  \"title\": \"List brands\",\n  \"annotations\": {\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"openWorldHint\": false\n  }\n}\n```\n\nClients use `readOnlyHint` to decide what they can run without asking. Our 26 read tools cover the questions people actually ask: which buyer questions name a competitor and not their brand (`get_prompt_gaps`), which sites AI cites in those answers (` get_easiest_wins`), what moved since the last scan (` get_changes`), real answer excerpts with their citations (` search_responses`).\n\nThe read tools call the same data layer as our REST API, so the numbers in Claude match the dashboard. That matters more than it sounds: a figure in the chat that differs from the app would make users doubt both.\n\nThe 18 write tools (track a question, add a competitor, draft an article, publish it, request indexing...) all go through one gate, in a fixed order:\n\n```\nexport function writeGate(o: { scopes: readonly string[] | null | undefined; confirm?: boolean | null; blocked?: string | null }): GateResult {\n  if (!hasScope(o.scopes, \"write\")) return { kind: \"forbidden\", message: READ_ONLY_KEY_MESSAGE };\n  if (o.blocked) return { kind: \"blocked\", message: o.blocked };\n  if (o.confirm !== true) return { kind: \"preview\" };\n  return { kind: \"execute\" };\n}\n```\n\n`confirm: true`, nothing changes. The tool returns a preview: what will change, the quota impact, and the exact arguments to send back with The server instructions tell the model to show the preview, wait for an explicit yes, and never chain several confirmed changes from one approval. A model can still ignore instructions, which is why the gate is in code and not only in the prompt.\n\nThe server also exposes three ready-made prompts (`weekly_review`, `client_report`, `fix_my_site`) that chain the tools for common jobs. Each key is limited to 120 requests per minute and 5,000 per day.\n\nThe server is listed in the official MCP registry as `ai.howseen/howseen-mcp`, with a remote entry like this:\n\n```\n\"remotes\": [{\n  \"type\": \"streamable-http\",\n  \"url\": \"https://app.howseen.ai/api/mcp\",\n  \"headers\": [{ \"name\": \"Authorization\", \"isRequired\": true, \"isSecret\": true }]\n}]\n```\n\nYou can check any entry with `curl \"https://registry.modelcontextprotocol.io/v0/servers?search=howseen\"`. Keyless discovery is what lets registries and directories read your tool list without credentials.\n\nThe full tool list, setup per app (Claude, ChatGPT, Cursor, Claude Code, Codex) and the REST endpoints are on the [Howseen MCP page](https://howseen.ai/mcp?utm_source=devto&utm_medium=referral&utm_campaign=o2-syndication), and ready-to-paste commands are in [MCP use cases](https://howseen.ai/mcp-use-cases?utm_source=devto&utm_medium=referral&utm_campaign=o2-syndication).\n\nSee what AI says about your brand: [howseen.ai](https://howseen.ai/?utm_source=devto&utm_medium=referral&utm_campaign=o2-syndication)", "url": "https://wpnews.pro/news/how-to-expose-your-saas-data-to-claude-and-chatgpt-with-mcp-a-real-44-tool", "canonical_source": "https://dev.to/raphaelaubry/how-to-expose-your-saas-data-to-claude-and-chatgpt-with-mcp-a-real-44-tool-server-2gn2", "published_at": "2026-11-06 04:40:56+00:00", "updated_at": "2026-11-06 04:54:40.334117+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "generative-engine-optimization", "ai-search"], "entities": ["Howseen", "Raphaël Aubry", "Claude", "ChatGPT", "Cursor", "Gemini", "Perplexity", "Next.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-expose-your-saas-data-to-claude-and-chatgpt-with-mcp-a-real-44-tool", "markdown": "https://wpnews.pro/news/how-to-expose-your-saas-data-to-claude-and-chatgpt-with-mcp-a-real-44-tool.md", "text": "https://wpnews.pro/news/how-to-expose-your-saas-data-to-claude-and-chatgpt-with-mcp-a-real-44-tool.txt", "jsonld": "https://wpnews.pro/news/how-to-expose-your-saas-data-to-claude-and-chatgpt-with-mcp-a-real-44-tool.jsonld"}}