npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gonzih/mcp-substrate

v0.1.0

Published

Production infrastructure for stateful MCP servers — session stores, leader election, stdio safety, per-session auth

Downloads

24

Readme

@gonzih/mcp-substrate

Production infrastructure for stateful MCP servers. Fills the operational gaps that @modelcontextprotocol/sdk maintainers closed as "won't fix":

| Module | Problem solved | |--------|---------------| | createMcpApp | Shared-transport footgun (issue #961): one server instance per client session | | SessionStore / RedisSessionStore | Per-session state that survives pod restarts | | withSingleton | Background workers that run in exactly one replica | | defineStdioServer | Orphan process accumulation when parent exits | | withAuth | Per-session credential injection into tool call context |

Install

npm install @gonzih/mcp-substrate @modelcontextprotocol/sdk zod
# For Redis-backed features:
npm install ioredis

createMcpApp

Multi-client-safe HTTP handler. Creates a fresh McpServer per client session — no shared state.

import express from 'express';
import { createMcpApp } from '@gonzih/mcp-substrate';

const handler = createMcpApp(
  (server) => {
    server.registerTool(
      'echo',
      { description: 'Echoes the input', inputSchema: { message: z.string() } },
      async ({ message }) => ({ content: [{ type: 'text', text: message }] }),
    );
  },
  { name: 'my-server', version: '1.0.0' },
);

const app = express();
app.use(express.json());
app.all('/mcp', handler);
app.listen(3000);

Why: The naive pattern of one McpServer + one StreamableHTTPServerTransport for all requests causes session collisions. createMcpApp creates isolated server instances per session and routes by mcp-session-id header.

SessionStore / RedisSessionStore

Portable session state that survives Kubernetes pod restarts.

import Redis from 'ioredis';
import { RedisSessionStore } from '@gonzih/mcp-substrate';

const redis = new Redis(process.env.REDIS_URL!);
const sessions = new RedisSessionStore(redis, {
  keyPrefix: 'mcp:session:',
  defaultTtlSeconds: 3600,
});

// In your tool handler:
const ctx = await sessions.get(sessionId);
await sessions.set(sessionId, { ...ctx, lastSeen: Date.now() }, 3600);

Why: In-memory session maps are lost on pod restart. Redis gives you durable, cross-pod session state with automatic TTL eviction.

For local dev and tests, use MemorySessionStore — same interface, no Redis needed.

withSingleton

Run a background worker in exactly one process across N replicas.

import Redis from 'ioredis';
import { withSingleton } from '@gonzih/mcp-substrate';

const redis = new Redis(process.env.REDIS_URL!);

await withSingleton(redis, 'my-app:queue-poller', async () => {
  setInterval(() => pollQueue(), 3_000);
});

Why: With replicas: 3 in Kubernetes, a naive background poller runs three times. withSingleton uses Redis SET NX EX leader election so exactly one replica runs the worker. If the leader pod dies, the lock expires (default 30 s) and another replica takes over.

Options:

await withSingleton(redis, 'my-app:scheduler', worker, {
  ttlSeconds: 60,       // lock expiry (must be > refreshIntervalMs / 1000)
  refreshIntervalMs: 20_000,  // how often the leader renews the lock
});

defineStdioServer

Stdio MCP server with SIGTERM/EPIPE handling that prevents orphan process accumulation.

import { defineStdioServer } from '@gonzih/mcp-substrate';

await defineStdioServer(
  (server) => {
    server.registerTool('ping', { description: 'Returns pong' }, async () => ({
      content: [{ type: 'text', text: 'pong' }],
    }));
  },
  { name: 'my-stdio-server', version: '1.0.0' },
);

Why: When Claude Desktop or an IDE plugin exits, the spawned stdio server's stdout write end breaks. Without EPIPE handling, Node.js throws an unhandled exception; without SIGTERM handling, the process lingers indefinitely. defineStdioServer installs both handlers for a clean exit.

withAuth

Inject per-session credentials into every tool call without threading them through every function signature.

import { withAuth, getActiveCredentials } from '@gonzih/mcp-substrate';

// Call before registering tools
withAuth(server, async (extra) => ({
  tenantId: extra.authInfo?.extra?.tenantId as string ?? '',
  apiKey: await lookupApiKey(extra.sessionId),
}));

server.registerTool('fetch-data', { description: 'Fetches tenant data' }, async () => {
  const creds = getActiveCredentials();
  if (!creds?.apiKey) throw new Error('Missing API key');
  const data = await fetchTenantData(creds.tenantId, creds.apiKey);
  return { content: [{ type: 'text', text: JSON.stringify(data) }] };
});

Why: Tool handlers need caller-specific tokens (API keys, tenant IDs, OAuth tokens) when calling downstream services. Passing credentials through every function signature creates coupling. withAuth + getActiveCredentials() uses AsyncLocalStorage to make credentials available anywhere in the call stack without prop drilling.

Call withAuth before registering tools — it patches server.registerTool.

License

MIT