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

davepi-plugin-mcp-oauth

v0.2.0

Published

OAuth 2.1 sign-in for dAvePi's MCP endpoint, so people connect Claude (or any MCP client) as themselves: protected-resource and authorization-server metadata, dynamic client registration, PKCE, consent on the app's own page, refresh-token rotation, grants

Readme

davepi-plugin-mcp-oauth

Lets people connect Claude (or any MCP client that follows the MCP authorization spec) to your dAvePi app as themselves, by signing in, instead of pasting an API key into a config file.

It turns the app into an OAuth 2.1 authorization server for its own POST /mcp endpoint. Once it's on:

  • someone pastes https://<your API>/mcp into Claude's Settings › Connectors › Add custom connector;
  • Claude opens your app's sign-in;
  • the person allows it on your consent page;
  • Claude then calls your MCP tools as that person, with their roles, claims and ACL, and no more.

How it works

  1. The challenge. Claude calls /mcp with no token. It gets a 401 with WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp" (RFC 9728).
  2. Registration. Claude reads /.well-known/oauth-authorization-server (RFC 8414) and registers itself at POST /oauth/register (RFC 7591). It registers as a public client: no secret, with PKCE as the proof.
  3. Sign-in. Claude sends the person's browser to /oauth/authorize with PKCE (S256) and resource=<issuer>/mcp (RFC 8707). The plugin checks the request, then redirects to your consent page (MCP_OAUTH_CONSENT_URL?request=<id>).
  4. Consent. Your page, signed in as usual, calls:
    • GET /oauth/requests/:id for the client's name, where it will send the person back, and the scopes;
    • POST /oauth/requests/:id/decision { allow, scopes?, context? }, which answers { redirectTo }. The page then sends the browser there.
  5. Tokens. Claude swaps the single-use code for an access token and a refresh token at POST /oauth/token, then calls /mcp with the access token.

Tokens

Access tokens are davepi JWTs (utils/tokens signGrantAccessToken) with four extra claims:

  • aud: the /mcp resource;
  • grant: the grant they were issued under;
  • scopes: read and/or write;
  • client_id: the client that holds them.

Rules that follow from that:

  • Only /mcp accepts them. REST and GraphQL refuse them, so a token handed to an assistant can't be replayed against the whole API.
  • Every request checks the grant. Revoking it, from your Connected apps screen or by the client, ends access on the next call.
  • Scopes are enforced as for API keys. A read-only grant isn't offered the tools that change data.
  • Your claims provider decides the claims. It's called with { grant: { id, clientId, scopes, context } }, so a token carries the claims for the context the person chose (a workspace, say). The grant claims, reserved claims and timing claims are re-applied after it, so it can't widen a token.
  • Roles are user.roles, unless you say otherwise. If a person's roles depend on the context they chose (an owner in one workspace and a member in another), register grantRoles (below) so a grant for the second workspace doesn't act as an owner there.

Refresh tokens rotate on every use. Presenting one that has already been rotated out revokes the whole grant, and so does reusing an authorization code. Codes and refresh tokens are stored only as SHA-256 hashes.

Configuration

| Variable | | |---|---| | MCP_OAUTH_ISSUER | Required. The API's public origin, e.g. https://api.example.com. The MCP resource is <issuer>/mcp. | | MCP_OAUTH_CONSENT_URL | Required. Your app's consent page, e.g. https://app.example.com/connect. | | MCP_OAUTH_PATH | Default /oauth. | | MCP_OAUTH_ACCESS_TTL | Access-token lifetime in seconds, 60–3600. Default 900. | | MCP_OAUTH_REFRESH_DAYS | Refresh-token lifetime in days, 1–90. Default 30. |

Without the two required variables the plugin logs a warning and stays dormant, and /mcp keeps answering as before. It needs a davepi with grant-token support (2.13.0 or later).

{ "davepi": { "plugins": ["davepi-plugin-mcp-oauth"] } }

Hooks

const mcpOAuth = require('davepi-plugin-mcp-oauth');

// Extra data for your consent page: returned as `app` from GET /oauth/requests/:id.
mcpOAuth.describeRequest(async ({ user, client, scopes, req }) => ({ workspaces: await workspacesOf(user) }));

// Before a grant is made: throw a typed error to refuse, return the context to store.
mcpOAuth.authorizeGrant(async ({ user, client, scopes, context, req }) => {
  const membership = await mustBeActiveMember(user, context);
  return { membershipId: String(membership._id) };
});

// Which grants a person may list and revoke at GET/DELETE /oauth/grants (default: their own).
mcpOAuth.grantFilter(async ({ user }) => (isOwner(user) ? { 'context.workspaceId': user.workspaceId } : { userId: String(user.user_id) }));

// The roles a grant's tokens carry, worked out on every token and refresh
// (default: user.roles). Anything but a non-empty array of strings gives
// ['user']; a throw fails the token request. Needs davepi 2.14.0 or later.
mcpOAuth.grantRoles(async ({ user, grant }) => rolesIn(grant.context.membershipId));

// After grant.created / grant.revoked, e.g. to email a security notice.
mcpOAuth.onEvent(async ({ type, grant, userId }) => {});

// For your own code: a workspace switching connections off.
await mcpOAuth.revokeGrants({ 'context.workspaceId': id }, 'workspace-off');

Only a person's own session can see a request, decide one, or manage grants. API keys, client-id callers and other OAuth tokens are refused.

Routes

| Route | Who | |---|---| | GET /.well-known/oauth-protected-resource[/mcp] | public | | GET /.well-known/oauth-authorization-server | public | | POST /oauth/register | public, rate-limited | | GET /oauth/authorize | public, rate-limited | | POST /oauth/token | public, rate-limited (form or JSON) | | POST /oauth/revoke | public, rate-limited (RFC 7009) | | GET /oauth/requests/:id | the person, signed in | | POST /oauth/requests/:id/decision | the person, signed in | | GET /oauth/grants | the person, signed in | | DELETE /oauth/grants/:id | the person, signed in |

Behind a browser-based MCP client (rather than Claude, whose calls come from its servers), make sure your CORS settings allow the discovery, registration and token endpoints.