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

@behavioralstate/best-mcp

v2.5.6

Published

MCP server for any BEST-compliant endpoint — exposes commands and queries as tools for LLM clients

Readme

best-mcp

MCP server for any BEST-compliant endpoint. Exposes the BEST command and query surface as MCP tools so any LLM client (ChatGPT Desktop, Claude Desktop, GitHub Copilot, Cursor) can discover and interact with a BEST service.

Supports multiple named connections in a single server instance — useful for admins who need to operate across tenant-scoped and platform-level surfaces, or across entirely separate BEST applications.

Who this is for

best-mcp is an adapter for clients you don't control. If you use an off-the-shelf MCP-capable client, this server is the right integration: it is the only plug-in mechanism those clients offer.

If you are writing your own agent, backend, or tooling, you don't need it — call the BEST HTTP surface directly. BEST endpoints are self-describing (command/query catalogues, JSON Schemas, workflows), and every tool below is a thin wrapper over exactly one HTTP call. Putting best-mcp between your own code and the service adds a network hop and a deployment to operate, flattens structured BEST error responses into prose, and widens your supply chain — while providing nothing a small HTTP client in your codebase wouldn't. See Choosing a Transport in the spec docs.

Running it as a shared server in production? Pin a version (npx @behavioralstate/[email protected], or your package manager's equivalent) rather than resolving latest at start-up — callers' credentials flow through this process, so upgrades should be deliberate.


Start with AI

Paste either prompt into your LLM client to get configured in under a minute.

Configure best-mcp — generates the exact env vars and mcpServers JSON for your client:

Configure best-mcp so I can use my BEST service from [VS Code Copilot / Claude Desktop / Cursor].

Service base URL: [https://api.example.com/best]
API key: [my-api-key]
Tenant ID: [my-tenant-id]  ← remove this line if not multi-tenant

Output the exact env vars and mcpServers JSON block to add to my client config.

best-mcp docs: https://behavioralstate.io/docs/transports/mcp

Make your service BEST-compliant — scaffolds the four required endpoints in your framework:

Make my [ASP.NET Core / Express / FastAPI / Spring Boot] service BEST-compliant.

I need these four endpoints:
- GET /.well-known/best — discovery manifest
- GET /commands — catalogue listing accepted commands with JSON Schema
- POST /commands — CloudEvents 1.0 entry point
- GET /queries — query catalogue

Auth: X-Api-Key header. Set authentication.type = "apikey" in the manifest.

Spec reference: https://behavioralstate.io/docs

Tools

| Tool | What it does | |---|---| | list_connections | List the configured connections (names, endpoints, descriptions) plus every further service the apps' root manifests list, reachable as <app>/<serviceId> | | get_command_catalogue | List all commands this endpoint accepts (descriptions truncated; detail: "full" for verbatim) | | get_command_schema | Fetch the full JSON Schema for a command type — learn the exact fields required | | send_command | Send a command (CloudEvent 1.0 envelope built automatically — the type is the commandType the catalogue states, else the PascalCase of the schema name; sent as application/cloudevents+json to a server that states spec 0.9.11 or later, application/json otherwise); optional correlation_id joins an existing chain, and the server's echoed correlation ID (spec 0.9.2+) is returned for use with the event tools | | send_command_and_wait | Send a command then poll a query until a condition is met (accepts correlation_id like send_command) | | get_query_catalogue | List all read queries this endpoint exposes (descriptions truncated; detail: "full" for verbatim) | | get_query_schema | Fetch the JSON Schema for a query — learn parameters and response shape | | execute_query | Execute a query and return current state synchronously | | get_manifest | Fetch the /.well-known/best discovery manifest (tenant-scoped when the host publishes one) — declared capabilities, push channels, authentication. The sign-in guidance (an authentication block's note, spec 0.9.14) is dropped: it is written for a consumer that makes the device-flow requests by hand, and best-mcp makes them itself (register_agent, exchange_device_code) | | get_events | Query the historical event log (GET /events) — filter by correlationId/type/source/time, paginate with the response cursor | | get_event_schema | Fetch the JSON Schema for a typed event (GET /events/{schema}/{version}) | | sample_event_stream | Open the live SSE stream (GET /events/stream), collect events until max_events/max_seconds, then return them — bounded client-side, so it works against any conformant endpoint | | register_agent | First step of agent registration (spec 0.9.11, RFC 8628): POSTs to the root manifest's authentication.deviceAuthorizationUrl with client_id=best-mcp and an optional agent_label, and returns the link and the short code to show the person — on that page they sign in (or sign up, where the service offers it) and approve. The service-generated device code is kept by best-mcp and never shown to the model. Where a service declares no such endpoint, the result says what to do instead. With name + manifest it signs in under that name (see Signing in under a name below) | | exchange_device_code | Last step of agent registration (RFC 8628): redeems the device code register_agent is holding (pass device_code only for a registration started by hand on a service without a device authorization endpoint) at the root manifest's authentication.tokenUrl, never faster than the service's interval. authorization_pending / slow_down come back as a non-error status to poll on. On success the issued credential (and tenant) is applied to this session's connections of the same app at once AND stored in the credential store for later starts; the key is never returned to the model — every copy in the response is redacted, and the service's credential block (mcp, http, note) is dropped: best-mcp is already the client that keeps it. On the HTTP transport a per-request caller (override headers present) gets neither the key nor a shared-state change | | get_workflows | List the service's published workflow recipes (GET /workflows — always answered as a shallow index), or fetch one full recipe with its steps via workflow_id (GET /workflows/{id}). Handles both the 0.9.4 io.best.agents.workflows capability and pre-0.9.4 vendor-extension servers; returns a note if the service publishes none |

Intended LLM flow: get_command_catalogue → pick a command → get_command_schema → gather fields → send_command.

Events flow: get_manifest (does the service declare events, and over which channels?) → get_events for what already happened (poll with the response cursor for turn-based drains) → sample_event_stream for a bounded window of what happens next. A turn-based client cannot hold the stream open — for standing reactions, configure the service's own alerting/webhook commands instead.

Correlation (spec 0.9.2+): every accepted command has a correlation ID — the caller's correlation_id, or defaulting to the command's own ID — echoed as correlationId in the send_command response and stamped as correlationid on every event the command causes, across process chains. Filter get_events / sample_event_stream by it to observe a command's outcome. Pre-0.9.2 servers ignore the attribute and echo nothing; behaviour there is unchanged.

Both catalogue tools truncate each entry's description by default, because a catalogue exists to let a caller choose an operation and a thoroughly documented service makes the full listing too large for that — one endpoint returns 47 KB for ~65 commands, which clients spill to disk before a model can read it. Truncation is ~60% smaller and still enough to pick from; the schema tools return one operation's complete text, and detail: "full" returns every description verbatim when you really need to compare across entries. Before hand-assembling a multi-step process, call get_workflows — catalogue entries may point at recipes via their workflows array — then get_workflows with workflow_id for the chosen recipe's steps.

High-impact commands (spec 0.9.6): when a command's schema document or catalogue entry carries an impact annotation (financial, destructive, irreversible, compliance), get_command_schema appends a deterministic HIGH-IMPACT note — including the server's warning text — telling the model to surface the warning and obtain the user's explicit confirmation before send_command. The annotation is descriptive; the server's own controls still apply.

Every operation tool takes an optional connection parameter; with a single configured connection it defaults to that one. If the LLM is not certain which connection the user intends, it calls list_connections and asks the user to confirm before proceeding.

Root-manifest services as connections. A platform's root manifest (/.well-known/best) lists its services, each with an HTTP endpoint, and only the tenant surface gets a configured connection. Every other listed service is reachable anyway as <app>/<serviceId> (e.g. acme/com.acme.onboarding): the client fetches the app's root manifest once, builds an ad-hoc connection at that service's endpoint with the app's credential, and caches it. list_connections lists them after the configured ones. Services whose endpoint is merely the parent of a configured surface (a tenants collection root) are not listed.

Signing in under a name. "Sign me in acme-live, start from https://acme.example/.well-known/best" is register_agent with name: "acme-live" and manifest set to the address the person gave (a bare site name means https://<site>/.well-known/best). The person approves on the link it returns, exchange_device_code with connection: "acme-live" redeems it, and from then on acme-live is the connection — in this session and at every later start, with nothing configured. The credential is stored under that name, with the manifest it signed in at and the endpoints that manifest resolved to (the tenant manifest's, when the root is multi-tenant and the approval answered a tenant). Another name is another sign-in with its own credential, even on the same site. The name is the person's, never inferred from a host: a name already signed in at another manifest is refused (name_in_use) unless the person confirms the move (replace: true), and a name the configuration defines cannot be taken. best-mcp also starts with nothing configured at all, so a fresh install can begin from the sentence alone. This is a stdio client's feature: a server signs no one in (next paragraph). As SPEC Name Resolution requires, an IP literal, localhost, or a name (or redirect) resolving to a loopback, link-local or private address is refused unless BEST_MCP_ALLOW_LOCAL=true states development use, and the credential is only ever sent to the host the manifest resolved to — a service endpoint on another host is refused. If discovery fails after approval, the credential is already stored and the next use completes it.

A server holds no credential. Over HTTP (MCP_TRANSPORT=http) best-mcp is a server that every caller who reaches it shares, so it never obtains, stores or loads a credential: register_agent and exchange_device_code are not offered (and refused if called), the credential store below is never opened, a configured key (BEST_<APP>_API_KEY, BEST_CONNECTIONS, BEST_API_KEY) is never used, and every call must carry the caller's own credential per request (see per-request credential overrides). A key held by a shared process would answer every later caller that sends none — they would all act as the person who approved it. Sign-in is for one person's own client, over stdio.

Credential store. A key issued through exchange_device_code is written to ~/.best-mcp/credentials.json (user-only; override with BEST_MCP_CREDENTIALS_FILE), keyed by the name it was signed in under (above) or, for a configured app, by the app's base URL, and never handed to the model — a chat transcript is not a secret store. At startup the stored credential fills in for an ABSENT BEST_<APP>_API_KEY or for the exact key it superseded (one-key-per-account services replaced that key when the new one was issued); a DIFFERENT configured key means you reconfigured deliberately, so the configuration wins and the stale entry is dropped. Entries of one account are one credential, whatever they are filed under — the account being the service's origin plus the tenant id: a configured app and a named sign-in of the same account (made by another assistant, or by this one on another day) are kept in step, so a configured key that was replaced heals from the stored sign-in on its first 401, a key issued on either is written to both, and on a one-key-per-account service one approval anywhere on the machine repairs every best-mcp client on it. This also makes onboarding from zero possible: configure only BEST_<APP>_BASE_URL, let the model run the service's onboarding workflow, and the tenant connection appears with its key stored.

No credential, or a dead one. Many BEST services hold ONE key per account, so a key replaced since it was stored is dead — the tenant surface answers 401. The server instructions tell the model what to do, and it is the same when there was never a key: never fall back to a browser, never ask the person for an API key; call register_agent, show the person the link and the code, and call exchange_device_code once they have approved — the new key is live in the session immediately and stored for later starts. Signing in and signing up are the person's, on the service's own page; registering is the agent's. For a service that predates spec 0.9.11 and declares no deviceAuthorizationUrl, register_agent names the surfaces the root manifest lists, where such a service publishes its own registration recipe.


Setup

1. Install and build

cd mcp-server
npm install
npm run build

2. Configure

There are three configuration modes. Use whichever fits your setup — they are mutually exclusive and checked in the order listed.

Upgrading from bsp-mcp (pre-2.0)? The protocol short name changed BSP → BEST in spec 0.9.0. All env vars are now BEST_*, but the server accepts legacy BSP_* names as a deprecated fallback (with a startup warning), so existing configurations keep working — rename them at your convenience.


Mode 1 — Per-app env vars (recommended)

One set of BEST_<APP>_* variables per application. The app name is a single uppercase word (letters and digits, no underscores), e.g. TRADING, HR, ACCOUNTING.

Required

| Variable | Description | |---|---| | BEST_<APP>_BASE_URL | Root URL of the BEST HTTP surface | | BEST_<APP>_API_KEY | Credential — not required when AUTH_TYPE=none |

Optional

| Variable | Default | Description | |---|---|---| | BEST_<APP>_TENANT_ID | — | When set, auto-generates two connections: <app>/tenant (tenant-scoped) and <app>/platform (platform-level). When omitted, generates one connection: <app>. Either way every further service the app's root manifest lists is reachable as <app>/<serviceId> without configuration (see Tools). | | BEST_<APP>_AUTH_TYPE | apikey | How the credential is sent — see Auth types below. Defaults to apikey in Mode 1 (unlike Modes 2 and 3 which default to bearer). | | BEST_<APP>_AUTH_HEADER | X-Api-Key | Header name — only used when AUTH_TYPE=apikey and AUTH_IN=header | | BEST_<APP>_AUTH_IN | header | Where the key is sent when AUTH_TYPE=apikey: header or query | | BEST_<APP>_AUTH_PARAM | apikey | Query parameter name — only used when AUTH_IN=query | | BEST_<APP>_ALLOW_BEARER_PASSTHROUGH | false | Allow a per-request Authorization: Bearer <token> header to be forwarded to the BEST endpoint as the caller's own credential — see Per-request credential overrides. |

Auth types

Default differs by mode. Mode 1 defaults to apikey because BEST services typically use API key headers. Modes 2 and 3 default to bearer for backward compatibility.

| AUTH_TYPE | What it does | Extra vars needed | |---|---|---| | apikey (Mode 1 default) | Sends the key in a custom header or query param | AUTH_HEADER (header name, default X-Api-Key) or AUTH_IN=query + AUTH_PARAM | | bearer (Modes 2 & 3 default) | Sends Authorization: Bearer <key> | none | | none | No credentials sent (public endpoint) | API_KEY not required |

Examples

Single app, tenant + platform surfaces (most common admin setup):

BEST_TRADING_BASE_URL=https://api.example.com/best
BEST_TRADING_API_KEY=your-api-key
BEST_TRADING_TENANT_ID=your-tenant-id
BEST_TRADING_AUTH_TYPE=apikey

This generates two connections automatically:

  • trading/tenant → https://api.example.com/best/tenants/your-tenant-id
  • trading/platform → https://api.example.com/best

Two separate apps:

BEST_TRADING_BASE_URL=https://trading.example.com/best
BEST_TRADING_API_KEY=trading-key
BEST_TRADING_TENANT_ID=tenant-abc
BEST_TRADING_AUTH_TYPE=apikey

BEST_HR_BASE_URL=https://hr.example.com/best
BEST_HR_API_KEY=hr-key
BEST_HR_TENANT_ID=tenant-abc
BEST_HR_AUTH_TYPE=apikey

This generates four connections: trading/tenant, trading/platform, hr/tenant, hr/platform.

App with no tenant scope:

BEST_MYAPP_BASE_URL=https://api.example.com/best
BEST_MYAPP_API_KEY=your-api-key

Generates one connection: myapp.

MCP client config (stdio)

{
  "mcpServers": {
    "best": {
      "command": "npx",
      "args": ["-y", "@behavioralstate/best-mcp"],
      "env": {
        "BEST_TRADING_BASE_URL": "https://api.example.com/best",
        "BEST_TRADING_API_KEY": "your-api-key",
        "BEST_TRADING_TENANT_ID": "your-tenant-id",
        "BEST_TRADING_AUTH_TYPE": "apikey"
      }
    }
  }
}

Mode 2 — BEST_CONNECTIONS JSON array

For advanced scenarios where per-app vars are not flexible enough. Set BEST_CONNECTIONS to a JSON array of connection objects — each connection is fully explicit with no auto-generation.

Each object:

| Field | Required | Default | Description | |---|---|---|---| | name | yes | — | Connection identifier used in the connection tool parameter | | endpoint | yes | — | Fully-resolved base URL (no {tenantId} placeholder) | | apiKey | yes* | — | Credential (*not required when authType is none) | | authType | no | bearer | bearer · apikey · none | | authHeader | no | X-Api-Key | Header name when authType=apikey and authIn=header | | authIn | no | header | header or query | | authParam | no | apikey | Query param name when authIn=query | | allowBearerPassthrough | no | false | Allow a per-request Authorization: Bearer <token> header to be forwarded to the BEST endpoint — see Per-request credential overrides | | description | no | — | Human-readable description surfaced to the LLM for connection selection |


Mode 3 — Legacy single connection

For simple single-endpoint setups. Use the flat BEST_* variables:

| Variable | Required | Default | Description | |---|---|---|---| | BEST_ENDPOINT | yes | — | Base URL of the BEST HTTP surface | | BEST_API_KEY | yes* | — | Credential (*not required when BEST_AUTH_TYPE=none) | | BEST_AUTH_TYPE | no | bearer | bearer · apikey · none | | BEST_AUTH_HEADER | no | X-Api-Key | Header name when AUTH_TYPE=apikey | | BEST_AUTH_IN | no | header | header or query | | BEST_AUTH_PARAM | no | apikey | Query param name when AUTH_IN=query | | BEST_ALLOW_BEARER_PASSTHROUGH | no | false | Allow a per-request Authorization: Bearer <token> header to be forwarded to the BEST endpoint — see Per-request credential overrides |


Transport options

stdio — VS Code Copilot, Cursor, Claude Desktop

MCP_TRANSPORT defaults to stdio. Add to your client's MCP config (see Mode 1 example above).

HTTP — ChatGPT Desktop

Over HTTP best-mcp is a server: it holds no credential, so a configured key is never used and every call must carry the caller's own (X-Api-Key / X-Tenant-Id, or a Bearer with passthrough). A bare tunnel to it therefore answers 401 — and that is the point: a tunnel with a key in it would hand that key to anyone who finds the URL. Give ChatGPT the service's own hosted MCP address (a platform puts best-mcp behind a door that signs the caller in and forwards their credential per request), or use best-mcp over stdio in a client that supports it.

MCP_TRANSPORT=http MCP_HTTP_PORT=3001 \
  BEST_TRADING_BASE_URL=https://api.example.com/best \
  BEST_TRADING_AUTH_TYPE=apikey \
  node dist/index.js

The MCP endpoint is the server's origin itself — a deployment hands out https://mcp.example.com and nothing more. /mcp is kept as an alias for connectors and configs that were given it before 2.4.2; /health answers GET with { "status": "ok" }.

HTTP — per-request credential overrides (multi-user backends)

A backend that calls best-mcp on behalf of many different logged-in users (e.g. a chat assistant) can't bake one fixed API key into the server's environment — it needs to supply the current caller's credentials on every request. When MCP_TRANSPORT=http, three optional request headers override the resolved connection for that single call only:

| Header | Effect | |---|---| | X-Api-Key | Replaces the connection's configured apiKey for this request. | | X-Tenant-Id | Replaces the tenant segment of the endpoint for this request. Only applies to a Mode 1 <app>/tenant connection (the one generated from BEST_<APP>_TENANT_ID) — ignored on connections with no tenant template. Must match ^[A-Za-z0-9_.-]+$; an invalid value is ignored (and logged) rather than spliced into the URL. | | Authorization: Bearer <token> | Forwarded verbatim to the BEST endpoint as the caller's own credential (e.g. a session JWT for a BEST surface that accepts JWTs) — only when the connection is explicitly configured with allowBearerPassthrough (BEST_<APP>_ALLOW_BEARER_PASSTHROUGH=true / allowBearerPassthrough: true / BEST_ALLOW_BEARER_PASSTHROUGH=true). Bearer scheme only. When forwarded, the effective auth for that request becomes Authorization: Bearer <token> regardless of the configured authType, so the token can never land in a query string or custom header. |

No override header is required — omit them all and a request behaves exactly as configured via environment variables. This has no effect on stdio (there's no per-request boundary to attach headers to).

Precedence: an explicit per-request X-Api-Key always wins; the Authorization Bearer token is only used when no X-Api-Key is present. This mirrors BEST dual-auth gates, where a present API key is authoritative and never falls through to the JWT.

Security — why Bearer passthrough is opt-in (default off): on the MCP HTTP transport, the Authorization header may carry a credential intended for this server (e.g. MCP OAuth between the client and best-mcp). Forwarding it upstream by default would leak that credential across a trust boundary. Enable passthrough only when the MCP caller and the BEST endpoint share one trust domain — i.e. the token the caller sends is the credential the BEST service expects. The token is only ever sent to the connection's configured endpoint, over the transport that endpoint's URL specifies (use HTTPS), and is never logged. If a request carries a Bearer token while passthrough is disabled, best-mcp falls back to the configured credential and logs a one-time warning per connection (without the token) so the misconfiguration is diagnosable.

Fail-closed by design (2.4.3+): over HTTP a configured apiKey is never used, so a request that carries no per-caller credential sends none and the BEST service answers it 401. Placeholder keys are no longer needed; existing ones are ignored the same way.

curl -X POST http://localhost:3001/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "X-Api-Key: <the current user's api key>" \
  -H "X-Tenant-Id: <the current user's tenant>" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"execute_query","arguments":{"connection":"trading/tenant","schema":"list-brokers","params":{}}}}'

Or, with BEST_TRADING_ALLOW_BEARER_PASSTHROUGH=true, authenticating the caller with their session JWT instead of an API key:

curl -X POST http://localhost:3001/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer <the current user's session JWT>" \
  -H "X-Tenant-Id: <the current user's tenant>" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"execute_query","arguments":{"connection":"trading/tenant","schema":"list-brokers","params":{}}}}'

CloudEvent source field

source is optional on send_command and defaults to urn:best-mcp — the client's own identity. Per the commands spec, source identifies the command's origin and servers must not route by it alone, so the default is correct for any conformant service. Pass an explicit source only when the schema description returned by get_command_schema documents a specific required value (legacy source-routing dialects) — never invent one.


Publishing to npm

Never run npm publish directly — the release is fully automated via CI:

git tag -a mcp/v<x.y.z> -m "Release mcp/v<x.y.z>"
git push origin mcp/v<x.y.z>