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

@lovable.dev/mcp-js

v0.8.0

Published

Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and the framework adapter (TanStack today, Supabase Edge Functions next) emits the route(s) at build time.

Readme

@lovable.dev/mcp-js

Author MCP servers for Lovable apps. Declare tools with defineTool, register them with defineMcp, and a Vite plugin emits the framework-specific routes at build time (TanStack today, Supabase Edge Functions next).

// src/lib/mcp/tools/echo.ts
import { defineTool } from "@lovable.dev/mcp-js";
import { z } from "zod";

export default defineTool({
  name: "echo",
  title: "Echo",
  description: "Echo the input text back.",
  inputSchema: { text: z.string().min(1) },
  handler: ({ text }) => ({ content: [{ type: "text", text }] }),
});
// src/lib/mcp/index.ts
import { defineMcp } from "@lovable.dev/mcp-js";
import echoTool from "./tools/echo";

export default defineMcp({
  name: "my-app-mcp",
  title: "My App MCP",
  version: "0.1.0",
  instructions: "Tools for interacting with My App. Use `echo` to verify connectivity.",
  tools: [echoTool],
});
// vite.config.ts
import { defineConfig } from "vite";
import { mcpPlugin } from "@lovable.dev/mcp-js/stacks/tanstack/vite";

export default defineConfig({ plugins: [mcpPlugin()] });

That's the whole authoring surface. No imperative server construction, no route handlers, no JSON-RPC envelope, no transport instantiation — all wrapped.

What the plugin emits

| URL | File | Purpose | | -------------------------------- | ------------------------------------------ | ---------------------------------------- | | POST /mcp | src/routes/mcp.ts | Full MCP streamable-HTTP protocol | | GET /.well-known/oauth-protected-resource | src/routes/[.well-known]/oauth-protected-resource.ts | OAuth protected-resource metadata | | GET /.mcp/list-tools | src/routes/[.mcp]/list-tools.ts | Tool catalog with JSON Schemas | | POST /.mcp/invoke-tool/<tool> | src/routes/[.mcp]/invoke-tool/$tool.ts | REST dispatcher; one handler call per tool |

MCP (POST /mcp) is the public wire format clients speak directly. REST (/.mcp/list-tools, /.mcp/invoke-tool/<tool>) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same defineMcp result, so they stay in sync on which tools exist and which auth policy protects them. If defineMcp({ auth: ... }) is omitted, the handlers stay unauthenticated; if OAuth auth is configured, MCP and REST both require the proxy/client to pass Authorization: Bearer <token>. CORS and rate limiting still belong at the app host or edge.

The OAuth metadata route is emitted by default and returns 404 until OAuth auth is configured. Disable it with mcpPlugin({ protectedResourceMetadataRoute: false }) only if the app owns /.well-known/oauth-protected-resource itself.

.lovable/mcp/manifest.json

.lovable/mcp/manifest.json is a snapshot the Lovable platform reads to register the MCP server. Envelope fields: version (manifest schema version), sdk_version (the @lovable.dev/mcp-js release that wrote the snapshot), path, and auth. auth is the server's auth configuration, lifted into the envelope (not into mcp): { "type": "none" }, or { "type": "oauth", ... } mirroring the defineMcp({ auth }) config (snake_case — issuer, accepted_audiences, required_scopes, resource, …). The mcp field is exactly the GET /.mcp/list-tools bodyserver plus the tool catalog (name/title/description/annotations and JSON-Schema inputSchema/outputSchema) — so the committed snapshot can't drift from what the live route serves. It's produced by loading the entry and reading the catalog off the defineMcp result, so the manifest reflects exactly what the server exposes, including tools built programmatically (spreads, computed names, tools from npm). The lovable-mcp-extract-manifest CLI writes it (loading the entry through Vite's SSR module loader, so it works under Node/Bun), and removes it when the entry is deleted. The Lovable platform runs the CLI in its commit pipeline; the Vite plugin only generates routes. Commit it: the platform reads the committed file.

Three caveats:

  • The entry must import cleanly. Because extraction imports the entry, it must not throw at module load — read env vars and do I/O inside tool handlers, not at module top level. A top-level throw also breaks Worker cold-start, so this is correct authoring regardless. An entry that can't be imported (or doesn't export default defineMcp(...)) makes the extract CLI exit non-zero with a clear error rather than emitting a partial manifest.
  • Tool title/description and the auth config are serialized verbatim. They land in a committed file. The auth config is whatever the entry resolves at build time — an issuer read from env resolves to its build-time value (e.g. a fallback like https://supabase.invalid/auth/v1 when the env var is unset), so don't put secrets in it.
  • The version field is a wire-format version, not the package version. The platform reader must accept a manifest version before this package starts emitting it, so bumping it is a coordinated deploy with the Lovable backend — not something an app author changes.

OAuth resource-server auth

@lovable.dev/mcp-js can protect an app-hosted MCP server as an OAuth 2.1 resource server. The package does not implement /authorize, /token, client registration, refresh tokens, or consent UI; those stay with the authorization server (for today's Supabase-backed Lovable Cloud apps, Supabase Auth). The MCP runtime validates bearer JWTs, publishes RFC 9728 protected-resource metadata, returns WWW-Authenticate: Bearer ... resource_metadata="..." challenges, and passes verified claims to tools.

Point the issuer at your OAuth/OIDC authorization server and anchor the accepted audience with either resource or acceptedAudiences:

// src/lib/mcp/index.ts
import { auth, defineMcp } from "@lovable.dev/mcp-js";

export default defineMcp({
  name: "my-app-mcp",
  title: "My App MCP",
  version: "0.1.0",
  instructions: "Tools for interacting with My App.",
  auth: auth.oauth.issuer({
    issuer: "https://auth.example.com",
    resource: "https://my-app.example.com/mcp",
    requiredScopes: ["mcp:tools"],
  }),
  tools: [],
});

The auth namespace groups auth families; today it exposes auth.oauth.issuer(...). It is exported from the package root (shown above), the canonical surface for app authors. issuer must be HTTPS (except localhost development URLs) and match the token iss exactly; the SDK discovers jwks_uri from the issuer's OAuth/OIDC metadata. The SDK reads no environment variables — pass values explicitly.

Supabase recipe

For a Supabase-backed project, the issuer is the project's <url>/auth/v1 and the accepted audience is Supabase's project-wide authenticated audience:

const supabaseUrl = process.env.SUPABASE_URL!.replace(/\/+$/, "");

auth: auth.oauth.issuer({
  issuer: `${supabaseUrl}/auth/v1`,
  acceptedAudiences: "authenticated",
}),

Supabase auth is project-scoped: Supabase access tokens use aud: "authenticated", so the same delegated token verifies against every MCP server in that project. The binding is "an OAuth client of the project," not "this MCP server." Use ctx.getClientId(), ctx.getUserId(), and ctx.getClaims() for handler-level app/business checks — never authorize on the audience — and pass ctx.getToken() through to Supabase so RLS evaluates the token itself. Supabase tokens do not carry OAuth scopes today, so leave requiredScopes unset.

To skip metadata discovery entirely, pin jwksUri to the issuer's JWKS endpoint. Verification then makes zero discovery probes; key rotation still works because the JWKS is fetched (and refreshed) on demand:

auth: auth.oauth.issuer({
  issuer: `${supabaseUrl}/auth/v1`,
  acceptedAudiences: "authenticated",
  jwksUri: `${supabaseUrl}/auth/v1/.well-known/jwks.json`,
}),

For a direct Supabase project, the relevant URLs look like this:

| Supabase URL | Used by | | --- | --- | | https://<project-ref>.supabase.co/auth/v1/.well-known/openid-configuration | SDK: discovers issuer and jwks_uri | | https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json | SDK: verifies bearer JWT signatures | | https://<project-ref>.supabase.co/auth/v1/oauth/authorize | OAuth clients: start user authorization | | https://<project-ref>.supabase.co/auth/v1/oauth/token | OAuth clients: exchange/refresh tokens |

The SDK only uses discovery + JWKS. It never calls the authorization or token endpoints.

Tool handlers receive their input args plus a ToolContext as the second argument. The SDK constructs one per request from the verified token and passes it to the handler; helpers that need the auth take a ToolContext parameter, so the dependency is explicit in their signatures.

import type { ToolContext } from "@lovable.dev/mcp-js";

handler: async ({ title }, ctx) => {
  const userId = ctx.getUserId();  // token `sub`, or undefined when unauthenticated
  // Apply app/business permissions here using ctx.getClaims() / ctx.getClientId().
  // For Supabase RLS, pass ctx.getToken() through to Supabase.
  return { content: [{ type: "text", text: `user=${userId}` }] };
};

| ToolContext method | Returns | | --- | --- | | isAuthenticated() | true when the call carries a verified auth context. | | getUserId() | The token sub (subject) — the user id. | | getUserEmail() | Token email when present. | | getClientId() | OAuth client_id, falling back to the OIDC azp (authorized party) — the client binding handle, useful for authorization. | | getScopes() | The parsed OAuth scopes. | | getIssuer() | The verified token issuer. | | getClaims() | The full verified JWT claims — for app/business authorization on issuer-specific claims with no dedicated accessor. | | getToken() | The raw bearer — pass to downstream APIs (e.g. Supabase RLS); never return or log it. |

Each returns undefined when the server is unauthenticated. ToolContext is a plain object passed by value — there is no ambient store, no AsyncLocalStorage, and no node:async_hooks dependency, so it works on any runtime and is safe under concurrency by construction. A handler that hands ctx to deferred work (a detached timer, an un-awaited promise) keeps full access to it.

Security contract for the bearer token. getToken() is the only way to read the raw token. ToolContext holds the verified context in a private field, so the credential can't be read off the instance via JSON.stringify, object spread, or structuredContent — only the accessors above expose anything, and getToken() is the lone path to the bearer. Pass getToken() only into outbound fetch/client construction (e.g. calling Supabase on the user's behalf); never return it from a tool or write it to logs. A leaked token stays valid at the authorization server until it expires.

Use an OAuth access token from the configured authorization server for MCP calls. Plain app-session JWTs, such as Supabase tokens from signInWithPassword, carry neither client_id nor azp; the SDK rejects them by default so copied browser sessions do not pass as delegated OAuth client tokens. Set requireOAuthClientClaim: false only when intentionally accepting those session tokens and relying on app checks plus downstream RLS via the forwarded bearer token.

auth.oauth.issuer(...) fields:

| Field | Meaning | | --- | --- | | issuer | OAuth/OIDC issuer URL. Must be HTTPS except localhost development URLs and must match the token iss exactly. Required. | | resource | Canonical protected-resource URL published in metadata. It also anchors the accepted audience unless acceptedAudiences is set. When omitted, the published resource is derived from the request origin plus the stack adapter's resourcePath and you must set acceptedAudiences; the metadata handler throws at construction if neither resource nor resourcePath is set. | | acceptedAudiences | Accepted JWT audience(s). Defaults to resource when that is set. One of resource or acceptedAudiences is required. | | jwksUri | JWKS URL override. If omitted, the runtime discovers jwks_uri from OAuth/OIDC metadata; setting it skips discovery entirely. | | requiredScopes | Required scopes. Advertised in metadata/challenges and enforced against the token scope. | | requireOAuthClientClaim | Defaults to true; rejects tokens carrying neither client_id nor the OIDC azp (authorized party) so copied app-session JWTs do not pass as OAuth client tokens. Supabase OAuth 2.1 server tokens carry client_id and issuers like Google/Keycloak/Entra use azp, so keep the default; only legacy signInWithPassword session tokens lack both and need false. | | algorithms | Accepted JWT signing algorithms. Defaults to asymmetric algorithms (RS*, ES*, EdDSA); set explicitly only for a narrower policy. | | clockToleranceSeconds | Allowed JWT clock skew for exp/nbf checks, in seconds. Defaults to 30; max 300. | | resourceName, resourceDocumentation | Optional metadata fields for MCP clients and humans. resourceName defaults to the MCP server title. | | protectedResourceMetadataUrl | Absolute HTTPS URL of an externally hosted RFC 9728 protected-resource metadata document. When set, the SDK advertises this URL in the 401 WWW-Authenticate: resource_metadata challenge and stops generating its own metadata — the /.well-known/oauth-protected-resource handler returns 404. Use it when the resource server already publishes its own PRM; you'll typically also pass protectedResourceMetadataRoute: false to the Vite plugin so the now-unused route isn't emitted. |

Protected-resource metadata is derived from the same config. When resource is omitted, the published resource is the incoming request origin plus the MCP route path the stack adapter supplies as resourcePath (for example, https://my-app.lovable.app/mcp); authorization_servers is the configured issuer, and resource_name is defineMcp({ title }) unless resourceName overrides it. Because the metadata endpoint is served from /.well-known/..., it can't infer the resource from its own path — the handler throws at construction when neither resource nor resourcePath is set (the generated TanStack routes always pass resourcePath). For Supabase auth, this metadata still names the MCP resource even though Supabase JWTs use the project audience "authenticated".

Stack adapters must pass the public MCP route as resourcePath when they also expose REST companion routes. The REST companion handlers (createInvokeToolHandler, createListToolsHandler) throw at construction unless resource or resourcePath is set, so principal.resource binds to /mcp rather than leaking the internal /.mcp/* RPC path it was reached through. The generated TanStack routes already pass it.

The request-derived resource default trusts the incoming Host to name this server's origin. Because a config always sets acceptedAudiences (Supabase: "authenticated") or resource, this default only names the advertised protected-resource metadata, never the accepted JWT audience. A platform that terminates TLS and sets Host from the verified domain (Lovable Cloud) makes this safe. Deployments fronted by a proxy that forwards an attacker-controlled Host should pin resource to the canonical URL so a spoofed host cannot shift the advertised metadata URL.

Use auth.oauth.issuer(...), set resource or acceptedAudiences to anchor the accepted audience, and optionally set jwksUri and requiredScopes. For Supabase project auth, set acceptedAudiences: "authenticated", keep app/business checks in app code, and forward ctx.getToken() to Supabase for RLS-backed data access.

Debug logging

The runtime is silent by default. Raise the log level to trace OAuth discovery, JWKS resolution, and token verification when a deployed MCP server returns 401/500 and you need to see why. Logs go to console (so they land in your platform's function logs) and never include the bearer token, full claims, or PII — only non-secret fields such as the JWT header alg/kid, issuer, jwks_uri, the token sub/client_id/scopes, and jose error codes.

Enable it either way:

// Programmatic — works on every runtime, including Workers that pass env via bindings.
import { setLogLevel } from "@lovable.dev/mcp-js";
setLogLevel("debug"); // "silent" | "error" | "warn" | "info" | "debug"
# Or via env var, read once at startup. Node/Deno only: it reads `process.env`,
# which is absent on a deployed Cloudflare Worker (env arrives via bindings), so
# the var is silently ignored there — use `setLogLevel()` on Workers.
LOVABLE_MCP_LOG_LEVEL=debug

A 500 on an OAuth-protected route is always one of two causes, logged at error: an OAuthConfigurationError from issuer-metadata discovery or a JWKS fetch failure (oauth.discovery.config_error / oauth.jwks.fetch_failedauth.config_error), or a transport-level fault in the MCP handler (mcp.transport_error). Auth outcomes are logged at info: a granted request as oauth.verify.ok, and a 401/403 as auth.token_rejected (with the jose reason) or auth.no_bearer_token — so a request rejected for insufficient scope shows both oauth.verify.ok and the auth.token_rejected that follows it.

Subpath exports

| Subpath | Contents | | --------------------------------------- | ----------------------------------------------------------------------- | | @lovable.dev/mcp-js | defineTool, defineMcp, public types | | @lovable.dev/mcp-js/protocols/mcp | createMcpProtocolHandler — Web-Standard MCP-over-HTTP | | @lovable.dev/mcp-js/protocols/oauth-metadata | createOAuthProtectedResourceMetadataHandler (the auth namespace lives at the root) | | @lovable.dev/mcp-js/protocols/rest | createListToolsHandler, createInvokeToolHandler — Web-Standard | | @lovable.dev/mcp-js/stacks/tanstack | TanStack-route-ctx adapters (createTanStack*Handler) | | @lovable.dev/mcp-js/stacks/tanstack/vite | The Vite plugin |

End users only need the root import (@lovable.dev/mcp-js). Generated route files import from @lovable.dev/mcp-js/stacks/tanstack. The other subpaths are escape hatches for hand-wiring custom stacks against the protocol layer directly.