How to expose your SaaS data to Claude and ChatGPT with MCP (a real 44-tool server) 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. Originally published on howseen.ai: howseen.ai/mcp https://howseen.ai/mcp?utm source=devto&utm medium=referral&utm campaign=o2-syndication By Raphaël Aubry, founder of Howseen Our 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. Tool discovery works without a key, so you can list the tools right now: curl -s -X POST https://app.howseen.ai/api/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \ | jq '.result.tools | length' 44 Count the read-only ones: ... | jq ' .result.tools | select .annotations.readOnlyHint | length' 26 Now try to call a tool without a key: curl -s -X POST https://app.howseen.ai/api/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list brands","arguments":{}}}' You 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. The 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. js const server = buildMcpServer ctx ; const transport = new WebStandardStreamableHTTPServerTransport { sessionIdGenerator: undefined, enableJsonResponse: true, } ; await server.connect transport ; return withCors await transport.handleRequest fwd ; 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. One 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. MCP 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: js export const DISCOVERY METHODS: ReadonlySet