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

@casys/mcp-platform

v0.28.1

Published

Production-ready MCP server framework with concurrency control, auth, and observability

Readme

@casys/mcp-platform

npm JSR License: MIT

The "Hono for MCP" — a production-grade framework for building Model Context Protocol servers in TypeScript.

Composable middleware, OAuth2 auth, dual transport, observability, and everything you need to ship reliable MCP servers. Built on the official @modelcontextprotocol/server and @modelcontextprotocol/sdk packages.

rate-limit → auth → custom middleware → scope-check → validation → backpressure → handler

Why @casys/mcp-platform?

The official SDK gives you the protocol. This framework gives you the production stack.

| | Official SDK | @casys/mcp-platform | | --------------------------- | :----------: | :----------------------------: | | MCP protocol compliance | Yes | Yes | | Concurrency control | -- | 3 backpressure strategies | | Middleware pipeline | -- | Composable onion model | | OAuth2 / JWT auth | -- | Built-in + 4 OIDC presets | | Rate limiting | -- | Sliding window, per-client | | Schema validation | -- | JSON Schema (ajv) | | Streamable HTTP (stateless) | Manual | startHttp() / handler | | OpenTelemetry tracing | -- | Automatic spans per tool call | | Prometheus metrics | -- | /metrics endpoint | | MCP Apps (UI resources) | Manual | registerResource() + ui:// |


Install

# Deno (primary target — JSR)
deno add jsr:@casys/mcp-platform

# Node (secondary — npm, native ESM build)
npm install @casys/mcp-platform

Renamed in 0.28.0: this package was previously published as @casys/mcp-server. That name continues as a deprecated alias re-exporting this package, so existing imports keep working unchanged.

Migrate when convenient by following the package migration guide.

Which companion package should I use?

Start with @casys/mcp-platform to build and operate an MCP server. Add a companion only for the boundary it owns:

| Package | Use it when you need to... | | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | @casys/mcp-compose | Compose and synchronize multiple MCP Apps in one dashboard. | | @casys/mcp-bridge | Bridge MCP Apps to external hosts or route selected calls to a private network. | | @casys/mcp-view-contracts | Share dependency-free App, resource, composition, and recorded-session contracts. | | @casys/mcp-view | Build the browser-side lifecycle, routing, results, and events of an MCP App. | | @casys/mcp-view-components | Add the optional presentation runtime, theme, and component kit to an MCP App. |

@casys/mcp-server is the compatibility alias, not a separate framework or an additional layer to install for new projects.

Runtime targets

@casys/mcp-platform is Deno-first. The canonical deployment path is Deno 2.x running on Deno Deploy or self-hosted Deno, with a Node 20+ distribution as a secondary target. The npm package contains compiled ESM JavaScript and TypeScript declarations; its runtime selector loads the Node HTTP adapter without evaluating the Deno adapter.

| Runtime | Status | | --------------------------------------------- | :--------------: | | Deno 2.x (Deno Deploy, self-hosted) | ✅ Primary | | Node.js 20+ (Express, Hono-on-Node, bare) | ✅ Secondary | | Cloudflare Workers / workerd | ❌ Not supported | | Browser / WebContainer | ❌ Not supported |

If you need to target Cloudflare Workers or the browser, use @modelcontextprotocol/server directly with its workerd / browser shims — that package focuses on the protocol and runtime portability, while @casys/mcp-platform focuses on the production stack (auth, middleware, observability, multi-tenant, MCP Apps helpers) for Deno deployments.


Quick Start

STDIO Server (5 lines)

import { McpApp } from "@casys/mcp-platform";

const server = new McpApp({ name: "my-server", version: "1.0.0" });

server.registerTool(
  {
    name: "greet",
    description: "Greet a user",
    inputSchema: {
      type: "object",
      properties: { name: { type: "string" } },
      required: ["name"],
    },
  },
  ({ name }) => `Hello, ${name}!`,
);

await server.start();

Provider repositories that expose a native stdio CLI should test their actual documented command, transport flags, protocol eras, stdout discipline, and EOF cleanup. Use the native stdio provider testing guide as a test-only adoption kit.

HTTP Server with Auth

import { createGoogleAuthProvider, McpApp } from "@casys/mcp-platform";

const server = new McpApp({
  name: "my-api",
  version: "1.0.0",
  maxConcurrent: 10,
  backpressureStrategy: "queue",
  validateSchema: true,
  rateLimit: { maxRequests: 100, windowMs: 60_000 },
  auth: {
    provider: createGoogleAuthProvider({
      audience: "https://my-mcp.example.com",
      resource: "https://my-mcp.example.com",
    }),
  },
});

server.registerTool(
  {
    name: "query",
    description: "Query the database",
    inputSchema: {
      type: "object",
      properties: { sql: { type: "string" } },
    },
    requiredScopes: ["db:read"],
  },
  async ({ sql }) => ({ rows: [] }),
);

await server.startHttp({ port: 3000 });
// GET  /health   → { status: "ok" }
// GET  /metrics  → Prometheus text format
// POST /mcp      → JSON-RPC (tools/call, tools/list, ...)
// GET  /mcp      → 405 Method Not Allowed (stateless transport)

startHttp() serves stateless Streamable HTTP. Every POST /mcp request is self-contained: the server does not issue Mcp-Session-Id, keep an MCP session, or expose the legacy SSE stream on GET /mcp.

See the HTTP security guide to choose between a shared static allowlist, identity-aware static credentials, and OIDC/JWT.

Secure-by-default HTTP options:

await server.startHttp({
  port: 3000,
  requireAuth: true, // fail fast if auth isn't configured
  corsOrigins: ["https://app.example.com"],
  maxBodyBytes: 1_000_000, // 1 MB
  ipRateLimit: { maxRequests: 120, windowMs: 60_000 },
});

Notes:

  • requireAuth: true throws if no auth provider is configured
  • corsOrigins defaults to "*" — use an allowlist in production
  • maxBodyBytes defaults to 1 MB (set null to disable)
  • ipRateLimit keys on client IP by default

Features

Middleware Pipeline

Composable onion model — same mental model as Hono, Koa, or Express.

import type { Middleware } from "@casys/mcp-platform";

const timing: Middleware = async (ctx, next) => {
  const start = performance.now();
  const result = await next();
  console.log(
    `${ctx.toolName} took ${(performance.now() - start).toFixed(0)}ms`,
  );
  return result;
};

server.use(timing);

Built-in pipeline: rate-limit → auth → custom → scope-check → validation → backpressure → handler

OAuth2 / JWT Auth

Four OIDC presets out of the box:

import {
  createAuth0AuthProvider, // Auth0
  createGitHubAuthProvider, // GitHub Actions OIDC
  createGoogleAuthProvider, // Google OIDC
  createOIDCAuthProvider, // Generic OIDC (Keycloak, Okta, etc.)
} from "@casys/mcp-platform";

const auth0 = createAuth0AuthProvider({
  domain: "my-tenant.auth0.com",
  audience: "https://my-mcp.example.com",
  resource: "https://my-mcp.example.com",
  scopesSupported: ["read", "write"],
});

Or use JwtAuthProvider directly for custom setups:

import { JwtAuthProvider } from "@casys/mcp-platform";

const provider = new JwtAuthProvider({
  issuer: "https://my-idp.example.com",
  audience: "https://my-mcp.example.com",
  resource: "https://my-mcp.example.com",
  authorizationServers: ["https://my-idp.example.com"],
});

Token verification is cached (SHA-256 hash → AuthInfo, TTL = min(token expiry, 5min)) to avoid redundant JWKS round-trips.

YAML + Env Config

For binary distribution — users configure auth without code:

# mcp-server.yaml
auth:
  provider: auth0
  audience: https://my-mcp.example.com
  resource: https://my-mcp.example.com
  domain: my-tenant.auth0.com
  scopesSupported: [read, write, admin]

Env vars override YAML at deploy time:

MCP_AUTH_AUDIENCE=https://prod.example.com ./my-server --http --port 3000

Priority: programmatic > env vars > YAML > no auth

MRTR requestState replay protection

When a tool returns resultType: "input_required", configure a signing key so the framework can bind the continuation to the principal, method, arguments, expiry, and a random nonce:

const server = new McpApp({
  name: "my-api",
  version: "1.0.0",
  mrtr: {
    signingKey: Deno.env.get("MCP_MRTR_SIGNING_KEY"),
  },
});

Each verified nonce is consumed before the handler runs. With no explicit replayStore, the built-in MemoryMrtrReplayStore rejects a second use within one continuously running process.

Multi-instance or restart-safe deployments must inject one durable atomic store shared by every instance:

import type { MrtrReplayStore } from "@casys/mcp-platform";

const replayStore: MrtrReplayStore = {
  async consume(nonce, expiresAt) {
    // Atomically reserve the nonce until its signed expiry.
    // Redis equivalent: SET mrtr:<nonce> 1 NX EXAT <expiresAt>
    return await reserveNonce(nonce, expiresAt);
  },
};

const server = new McpApp({
  name: "my-api",
  version: "1.0.0",
  mrtr: {
    signingKey: Deno.env.get("MCP_MRTR_SIGNING_KEY"),
    replayStore,
  },
});

consume() must return true only for the caller that wins the atomic reservation, false for a nonce already consumed, and throw when the store is unavailable. Store failures are fail-closed; the handler is not executed.

This is at-most-once admission, not exactly-once completion. If business logic commits and the response is lost, replaying the same token is rejected. Returning the prior result safely requires a separate idempotency/result ledger, ideally paired with idempotency support in the downstream system.

RFC 9728

When auth is configured, the framework automatically exposes GET /.well-known/oauth-protected-resource per RFC 9728.

DCR Discovery Proxy (RFC 8414 + RFC 7591)

IdPs without native Dynamic Client Registration (Zitadel, unconfigured Keycloak, Okta free tier) don't publish registration_endpoint in their AS metadata, so MCP clients like Claude.ai or Cursor can't auto-register.

createAsMetadataHandler is a framework-agnostic Web Standard handler that proxies the upstream RFC 8414 metadata and injects a registration_endpoint pointing to your own DCR proxy:

// routes/.well-known/oauth-authorization-server.ts (Fresh example)
import { createAsMetadataHandler } from "@casys/mcp-platform";

const handle = createAsMetadataHandler({
  upstreamIssuer: "https://my-tenant.zitadel.cloud",
  registrationEndpoint: "https://my-app.example.com/oauth/register",
  // cacheTtlMs?: 24h default, stale-while-revalidate
  // extraFields?: override scopes_supported, etc.
});

export const handler = { GET: (ctx) => handle(ctx.req) };

Then point the PRM at your own host so clients hit the enriched metadata:

authorizationServers: ["https://my-app.example.com"],

The DCR endpoint itself (RFC 7591 /oauth/register) is out of scope — mount it in your framework and forward to the IdP's admin API.

Path caveat: if the MCP server lives at /mcp, clients may build the discovery URL as <host>/.well-known/oauth-authorization-server/mcp. Mount the handler at the exact path your PRM advertises.

Observability

Every tool call emits an OpenTelemetry span with rich attributes:

mcp.tool.call query
  mcp.tool.name       = "query"
  mcp.server.name     = "my-api"
  mcp.transport       = "http"
  mcp.tool.duration_ms = 42
  mcp.tool.success     = true

The built-in HTTP transport does not emit mcp.session.id, because it does not create MCP sessions.

Enable with Deno's native OTEL support:

OTEL_DENO=true deno run --unstable-otel server.ts

The HTTP server exposes a Prometheus-compatible /metrics endpoint:

mcp_server_tool_calls_total 1024
mcp_server_tool_calls_success_total 1018
mcp_server_tool_calls_failed_total 6
mcp_server_tool_call_duration_ms_bucket{le="50"} 892
mcp_server_tool_call_duration_ms_bucket{le="100"} 987
mcp_server_tool_calls_by_name{tool="query",status="success"} 512
mcp_server_active_requests 3
mcp_server_uptime_seconds 86400

Programmatic access:

server.getServerMetrics(); // Full snapshot (counters, histograms, gauges)
server.getPrometheusMetrics(); // Prometheus text format string

Concurrency Control

Three backpressure strategies when the server is at capacity:

| Strategy | Behavior | | ----------------- | ------------------------------------------ | | sleep (default) | Busy-wait with configurable sleep interval | | queue | FIFO queue with ordered release | | reject | Fail fast with immediate error |

new McpApp({
  maxConcurrent: 10,
  backpressureStrategy: "queue",
});

Rate Limiting

Sliding window rate limiter with per-client tracking:

new McpApp({
  rateLimit: {
    maxRequests: 100,
    windowMs: 60_000,
    keyExtractor: (ctx) => ctx.args.clientId as string,
    onLimitExceeded: "wait", // or "reject"
  },
});

For HTTP endpoints, use startHttp({ ipRateLimit: ... }) to rate limit by client IP (or custom key).

Security Best Practices (Tool Handlers)

Tool handlers receive untrusted JSON input. Treat args as hostile:

  • Define strict schemas: additionalProperties: false, minLength, pattern, enum.
  • Never pass raw args to a shell (Deno.Command, child_process.exec). If you must, use an allowlist + argv array (no shell).
  • Validate paths & resources: allowlisted roots, deny .., restrict env access.
  • Prefer safe APIs: parameterized DB queries, SDK methods, typed clients.
  • Log sensitive actions: file writes, network calls, admin ops.

MCP Apps (UI Resources)

Register interactive UIs as MCP resources:

import { MCP_APP_MIME_TYPE, McpApp } from "@casys/mcp-platform";

server.registerResource(
  { uri: "ui://my-server/viewer", name: "Data Viewer" },
  async (uri) => ({
    uri: uri.toString(),
    mimeType: MCP_APP_MIME_TYPE,
    text: "<html><body>...</body></html>",
  }),
);

Handlers return one payload form: text (including HTML) or blob for binary content encoded as standard padded base64. Existing text handlers remain valid. The framework also checks at runtime that the response URI exactly matches the requested URI and that the MIME type is non-empty, which protects JavaScript and unchecked TypeScript handlers as well as typed callers.

server.registerResource(
  { uri: "file://reports/latest.pdf", name: "Latest report", size: 184_320 },
  async (uri) => ({
    uri: uri.toString(),
    mimeType: "application/pdf",
    blob: await loadReportAsCanonicalBase64(),
  }),
);

size is optional resource metadata shown in resources/list; when present it must be a non-negative safe integer and is verified on every read against the exact UTF-8 byte length of text or decoded byte length of blob. Supplying mimeType in the resource metadata likewise binds every response to that exact MIME type. If it is absent, it is absent from resources/list; the handler still declares the MIME type when it serves the bytes. Resource content may include _meta. Put annotations, icons, title, and resource _meta on the MCPResource registration, where MCP defines those fields.

With resourceCsp, CSP injection applies only to the text branch of an HTML resource. Blobs are never decoded, transformed, or re-encoded.

Register resources before start() / startHttp() to install the resource handlers and advertise resources: { listChanged: true }. They can then be added or removed at any time through unregisterResource(uri), which returns true only once. For a registry that starts empty and discovers resources asynchronously, construct with expectResources: true; that mode installs the same handlers at construction time:

const app = new McpApp({
  name: "relay",
  version: "1.0.0",
  expectResources: true,
});

// After start(): list/read/templates handlers are already installed.
app.registerResource(resource, handler);
app.unregisterResource(resource.uri); // true, then false if called again

Capability negotiation (clients that don't support MCP Apps)

Not every MCP client renders UI resources. Clients that do advertise the MCP Apps extension in their capabilities (per the SDK 1.29 extensions field). Read it from a tool handler to decide between rich UI and a text-only fallback:

import { MCP_APP_MIME_TYPE, McpApp } from "@casys/mcp-platform";

const app = new McpApp({ name: "weather-server", version: "1.0.0" });

app.registerTool(
  {
    name: "get-weather",
    description: "Get the weather forecast for a city",
    inputSchema: {
      type: "object",
      properties: { city: { type: "string" } },
      required: ["city"],
    },
  },
  async ({ city }) => {
    const forecast = await fetchForecast(city);
    const cap = app.getClientMcpAppsCapability();

    if (cap?.mimeTypes?.includes(MCP_APP_MIME_TYPE)) {
      // Rich UI: small text summary + interactive resource
      return {
        content: [{ type: "text", text: `Forecast for ${city} loaded` }],
        _meta: { ui: { resourceUri: `ui://weather/${city}` } },
      };
    }

    // Text-only fallback for clients that can't render the UI
    return {
      content: [{ type: "text", text: formatForecastAsText(forecast) }],
    };
  },
);

getClientMcpAppsCapability() returns undefined before the client has completed its initialize handshake, when the client doesn't advertise MCP Apps support, or when the advertised capability is malformed. The standalone getMcpAppsCapability(clientCapabilities) function is also exported for use against arbitrary capability objects.

The constants MCP_APPS_EXTENSION_ID ("io.modelcontextprotocol/ui") and MCP_APPS_PROTOCOL_VERSION ("2026-01-26") are exported for agents that need to introspect the protocol target directly.


API Reference

McpApp

Note: ConcurrentMCPServer and ConcurrentServerOptions remain exported as @deprecated aliases for backwards compatibility and will be removed in v1.0. New code should use McpApp / McpAppOptions. The aliases point to the exact same class — instanceof checks pass on both.

const server = new McpApp(options: McpAppOptions);

// Registration (before start, unless expectResources: true)
server.registerTool(tool, handler);
server.registerTools(tools, handlers);
server.registerResource(resource, handler);
server.registerResources(resources, handlers);
server.unregisterResource(resourceUri); // safe before or after start
server.use(middleware);

// Transport
await server.start();                  // STDIO
await server.startHttp({ port: 3000 }); // Stateless Streamable HTTP
await server.stop();                    // Graceful shutdown

// Observability
server.getMetrics();              // { inFlight, queued }
server.getServerMetrics();        // Full snapshot
server.getPrometheusMetrics();    // Prometheus text format
server.getRateLimitMetrics();     // { keys, totalRequests }

// Introspection
server.getToolCount();
server.getToolNames();
server.getResourceCount();
server.getResourceUris();

Standalone Components

Each component works independently:

import {
  RateLimiter,
  RequestQueue,
  SchemaValidator,
} from "@casys/mcp-platform";

// Rate limiter
const limiter = new RateLimiter({ maxRequests: 10, windowMs: 1000 });
if (limiter.checkLimit("client-123")) {
  /* proceed */
}

// Request queue
const queue = new RequestQueue({
  maxConcurrent: 5,
  strategy: "queue",
  sleepMs: 10,
});
await queue.acquire();
try {
  /* work */
} finally {
  queue.release();
}

// Schema validator
const validator = new SchemaValidator();
validator.addSchema("tool", {
  type: "object",
  properties: { n: { type: "number" } },
});
validator.validate("tool", { n: 5 }); // { valid: true, errors: [] }

HTTP Endpoints

When running with startHttp(), MCP traffic is stateless and POST-only. A GET or DELETE to the MCP route returns 405 Method Not Allowed rather than opening an SSE stream or managing a session:

| Method | Path | Description | | ------ | --------------------------------------- | ----------------------------------------------------------- | | POST | /mcp or / | JSON-RPC endpoint (initialize, tools/call, tools/list, ...) | | GET | /health | Health check | | GET | /metrics | Prometheus metrics | | GET | /.well-known/oauth-protected-resource | RFC 9728 metadata (when auth enabled) |


License

MIT