Skip to main content
A streamable-HTTP server builds a new McpServer for every request, so there is no long-lived instance to wrap. Wrap it inside the factory instead, beside the tool registration:
watch returns the same server, so return watch(…) is the whole change.

Calling watch per request is expected and cheap

This is the part that looks wrong and is not. The session and its buffer are held per process, shared by every watch() in it that reports to the same place — so calling watch() a thousand times does not open a thousand sessions. Without that sharing, each request would open a session of its own, no two calls would ever land in the same session, and no retry could ever be detected. First-call success would read 100% forever, which is worse than reading nothing.
Session state must be process-scoped. This is the gotcha everyone hits: assuming the watch() wrapper instance persists between requests. It does not — a streamable-HTTP server builds a fresh McpServer per request, so the wrapper is new every time and only the stream behind it is shared.If session state is not process-scoped, every request becomes its own session. Retries can never be detected, because a retry is the same tool twice inside one session and there is only ever one call in each. First-call success then reads 100% forever, however badly the server is doing — the one number this product exists to tell the truth about.Do not generate your own session id per request and pass it in. There is no option to do so, deliberately.

Wrapping the outer HTTP app does nothing

MCPulse instruments the MCP server, not your HTTP framework. It hooks initialize and tools/call on the low-level protocol server, which is where the tool name, the outcome and the result size actually are. There is nothing useful to read at the Express or Hono layer.