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.
Do not generate your own session id per request and pass it in. There is no option to do so, deliberately — the shared stream is what keeps the session-scoped metrics honest.
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.