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

@vendoai/mcp

v0.52.1

Published

The door: serve your product's tools to outside agents over MCP — OAuth, tool serving, and MCP Apps rendering behind one flag.

Readme

@vendoai/mcp

@vendoai/mcp is Vendo's door: one fetch-style handler that exposes a host's guard-bound tools to MCP clients, owns the OAuth 2.1 + PKCE flow, and optionally carries saved Vendo apps as MCP Apps.

The umbrella package wires it behind the one-flag setup, createVendo({ mcp: true }), so the same tool registry, guard policy, approvals, and audit trail apply to MCP calls as to in-product calls.

Read MCP.

The MCP Apps tree renderer is a committed, prebuilt HTML artifact. This keeps @vendoai/mcp dependent on core rather than UI at runtime. Regenerate the artifact from its owner with:

pnpm --filter @vendoai/ui build:mcp-shim

The server-card response tracks the provisional SEP-2127 draft and may move with that specification before ratification.

Reverse proxies and the canonical base URL

Behind a reverse proxy (Railway, Fly, any TLS terminator) the request URL reaching the process carries the proxy-internal origin. Configure the door's canonical public base so discovery metadata (issuer, endpoint URLs, the protected-resource resource) and RFC 8707 token audience binding advertise and enforce the public origin instead:

createMcpDoor({
  // tools, guard, oauth, store, ...
  baseUrl: "https://product.example.com", // origin only; the mount stays path-derived
});

The umbrella defaults baseUrl from VENDO_BASE_URL; pass createVendo({ mcp: { baseUrl } }) to override the env default. Unset, origins derive from each request's own URL. Forwarded headers such as X-Forwarded-Host are never trusted.

External authorization servers

By default the door owns the OAuth authorization-server flow. A host that runs its authorization server separately can instead configure:

createMcpDoor({
  // tools, guard, oauth, store, ...
  remoteAs: {
    issuer: "https://auth.example.com",
    audience: "https://product.example.com/api/mcp",
    // Optional. Otherwise the door discovers jwks_uri from RFC 8414 metadata.
    jwksUri: "https://auth.example.com/.well-known/jwks.json",
  },
});

In this mode the door accepts only ES256 bearer JWTs whose signature, iss, aud, exp, and iat validate against the external server's JWKS. Keys are cached and a new kid triggers a refresh for key rotation. The host's oauth.principal(sub) still runs on every request, so returning null remains the immediate account-level kill switch. The door's local /authorize, /token, and /register endpoints and RFC 8414 metadata return 404; RFC 9728 protected-resource metadata advertises the configured external issuer.

Token revocation

Local authorization-server metadata advertises {mount}/revoke and the supported read / write scopes. The RFC 7009 endpoint accepts an application/x-www-form-urlencoded POST containing token, the public client_id, and an optional token_type_hint. Unknown tokens and unknown or incorrect hints receive the RFC-required empty 200 response.

Access-token revocation invalidates that opaque token. Refresh-token revocation atomically revokes its authorization-grant family, including access tokens and rotated successors, without disconnecting a separate authorization for the same client. Hosts can disconnect all existing authorizations for one subject/client pair and close their live MCP sessions through the returned door:

const door = createMcpDoor({ /* ... */ });
await door.revokeClient(subject, clientId);

Grant-family and token revocation use the store's guarded atomic claim rather than a read-then-write update. In remoteAs mode, the external authorization server owns revocation and the door's local /revoke path returns 404.

Login federation

federation: { secret } enables GET {mount}/federate?request=<compact JWS> as a generic login handshake for an external authorization server. Requests are HS256-signed with the shared secret and carry iss, the door resource as aud, an expiration no more than five minutes away, jti, redirect_uri, scopes, and client_name. The redirect URI must have the same origin as iss.

The door passes the client name and scopes to HostOAuthAdapter.authorize. A Response from the adapter is returned unchanged for the host's login bounce; after authentication the browser can retry the same signed request. A resolved host subject is returned to the external server as assertion=<compact JWS> on the redirect URI. That HS256 assertion is audience-bound to the request issuer, issuer-bound to the canonical door resource, echoes the request jti, and expires after 60 seconds. The endpoint emits redirects or JSON errors only; it does not render request values into HTML.

Transport state seam

The door's protocol-lifetime state is isolated behind the package-internal McpDoorState interface. createMcpDoor still composes the 2025-11-25 Streamable HTTP transport with InMemoryMcpDoorState, so initialization, Mcp-Session-Id, response bytes, and error behavior are unchanged.

The current adapter owns three related lifetimes:

  • a TTL-indexed session record keyed by Mcp-Session-Id, containing the authenticated subject and opaque SDK runtime;
  • the authenticated subject/client/grant-family binding used to close every matching live runtime on account, per-client, or refresh-family revocation;
  • a bounded approval-replay map keyed by an opaque replay scope plus the canonical tool/arguments fingerprint. A pending approval retains its exact ToolCall.id; any resolved outcome deletes it. Session deletion, revocation, and idle expiry also delete the scope's replay records.

A 2026-07-28 adapter would leave OAuth and guard behavior alone and replace the transport/state composition as follows:

  1. Do not mint, accept, or look up Mcp-Session-Id, and do not persist an SDK transport runtime across requests. Construct the runtime and McpRunContext for the authenticated request only.
  2. Supply a stable opaque replay scope for the logical approval/retry context, derived from authenticated request context and never from caller-controlled protocol input. Use that value for both the RunContext.sessionId context key and McpStateSession.replayScope.
  3. Back getReplay, setReplay, and deleteReplay with the durable store, preserving absolute expiry, the per-scope capacity bound, and eviction. The stored value includes the authenticated subject and parked ToolCall.id, so an identical approved retry reuses it, a resolved retry removes it, and subject revocation purges it even when there is no live transport runtime.
  4. Make the session registry operations request-scoped/no-op for the stateless transport, while preserving subject revocation before any tool execution.

The seam is intentionally internal until that protocol adapter exists; the package-root API remains the frozen createMcpDoor(config) surface.

Operating notes for host integrators

  • The prebuilt page escapes client_name. DCR and Client ID Metadata Documents make that value attacker-controlled. If you replace the page with HostOAuthAdapter.authorize, your renderer must keep escaping it too.
  • Return from login through session's returnTo. It preserves the complete OAuth request and prevents the common login → authorize → login loop.
  • Single-secret redemption is claimed in the database. Authorization codes and refresh tokens use the store's atomic compare-and-claim capability, so one redeemer wins across multiple door processes sharing Postgres. A custom StoreAdapter that omits atomic claims fails closed at the token endpoint.
  • CIMD fetch egress. The door resolves a Client ID Metadata Document URL server-side and rejects private/loopback/link-local resolutions (best-effort, via node:dns when present). On runtimes without a resolver, or for defense in depth, enforce egress at your network layer — the door cannot portably prevent DNS rebinding.
  • vendo init must ask before opening the door. The shipped guard policy blocks venue: "mcp"; opening the door is a host decision, never a default.

Prebuilt consent flow

Use session instead of hand-building consent in authorize. Return the current host subject, or redirect an unauthenticated browser to login through the exact returnTo the door supplies:

const oauth: HostOAuthAdapter = {
  async session(req, { returnTo }) {
    const subject = await currentSubject(req);
    if (subject) return { subject };

    const login = new URL("/login", req.url);
    login.searchParams.set("returnTo", returnTo);
    return Response.redirect(login);
  },
  async principal(subject) {
    return resolveUser(subject); // null revokes existing door tokens
  },
};

The door renders the consent page, handles approve/deny, CSRF-protects the POST, rejects replay, and performs the standard authorization-code redirect. Its CSS uses the UI pipeline's --vendo-* tokens, including --vendo-color-*, --vendo-font-*, --vendo-radius-*, and --vendo-space-*. The umbrella passes the resolved .vendo/theme.json automatically; direct createMcpDoor users can pass the same core VendoTheme as theme.

To replace the whole page without taking ownership of the protocol, also provide authorize. In session mode ctx.consent contains the door-owned form state; post transaction, csrf_token, and decision=approve|deny to its action. The door still handles and validates the decision.