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

@apideck/mcp-connect

v0.1.1

Published

Framework-agnostic core for connecting, namespacing, and dispatching Model Context Protocol (MCP) tools — with pluggable persistence, OAuth providers, and token encryption.

Readme

@apideck/mcp-connect

Framework-agnostic core for connecting, namespacing, and dispatching Model Context Protocol (MCP) tools — with pluggable persistence, OAuth providers, and token encryption.

It is the reusable heart of a "connect your own MCP servers" feature: given a set of connected MCP servers, it loads their tools, namespaces them so their names never collide, exposes them as neutral tool definitions you can hand to any LLM, and routes inbound tool calls back to the right server. It has no database, no web framework, and no LLM-vendor dependency — everything app-specific is injected through small seams.

connected servers ──► load + namespace tools ──► neutral ToolDef[] ──► your LLM
        ▲                                                                  │
        │                                                          tool call by name
   ConnectionStore                                                        │
   (you implement)  ◄──── dispatch routes it back to the right server ◄───┘

Install

npm install @apideck/mcp-connect
# peer runtime dep is bundled: @modelcontextprotocol/sdk

Requires Node.js >= 20. ESM only ("type": "module").

Quickstart

A complete, runnable version of this (with a throwaway localhost MCP server so the dispatch is real) lives in examples/quickstart.ts — run it with npm run example.

import {
  InMemoryConnectionStore,
  loadMcpToolset,
  dispatchToolCall,
  renderMcpSystemPromptHint,
  encryptToken,
  decryptToken,
  generateKey,
  type Identity,
} from "@apideck/mcp-connect";

// 1. Resolve a 32-byte key-encryption key (KEK). In production this comes from
//    your secret manager; never hard-code it.
const kek = generateKey();

// 2. Persist a connection. The store only ever sees the ENCRYPTED token.
const store = new InMemoryConnectionStore();
store.addConnection({
  tenantId: "acme-inc",
  serverSlug: "front",
  serverName: "Front",
  mcpEndpoint: "https://mcp.frontapp.com/mcp",
  accessTokenObfuscated: encryptToken("the-oauth-access-token", kek),
  scope: "workspace",
  toolCatalog: { tools: [/* fetched at connect time via mcpListTools */] },
});

// 3. Load + namespace the tools for a given identity. The core decrypts via the
//    injected function — the store never does crypto.
const identity: Identity = { tenantId: "acme-inc", ownerId: "user-1" };
const toolset = await loadMcpToolset(store, identity, (enc) => decryptToken(enc, kek));

// 4. Hand toolset.toolDefs to your LLM, and optionally add a prompt hint.
const systemHint = renderMcpSystemPromptHint(toolset);

// 5. When the model calls a tool, route it back:
const result = await dispatchToolCall(
  toolset,
  "front__search",           // namespaced name the model called
  { query: "acme.com" },      // tool input
  { resolveHeaders: () => ({}) },
);
// result.text is ready to drop into a tool_result block.

Concepts

Tool namespacing

MCP servers don't namespace their tool names, so two servers could both expose a search tool. mcp-connect prefixes every tool with its server slug and a double underscore: front__search. Names are sanitized to [A-Za-z0-9_-]{1,64} (the Anthropic constraint, a safe lowest common denominator) and, when a name would exceed 64 chars, truncated with a stable hash suffix so distinct tools never collide. That same slug__tool convention is what lets dispatchToolCall route an inbound call back to the right server with no extra round trip.

The ConnectionStore seam

Persistence is injected. The core only ever calls:

interface ConnectionStore {
  listConnections(identity: Identity): Promise<StoredConnection[]>;
}

interface Identity {
  tenantId: string;   // opaque routing key
  ownerId: string;    // the signed-in user (scopes per-user connections)
}

Your implementation must honour the visibility contract:

  • a connection is only visible within its own tenantId;
  • workspace-scope connections are visible to every user in the tenant;
  • per_user-scope connections are visible only to their userId owner.

InMemoryConnectionStore ships in the box (great for tests, prototypes, and the example) and demonstrates exactly that contract. For production, implement the interface against your own datastore (SQL, KV, an ORM, …). StoredConnection carries no tenantId field because tenant scoping is the store's job, not the core's — your store filters by tenant before returning rows.

StoredConnection fields:

| field | meaning | | --- | --- | | connectionId | your primary key | | accessTokenObfuscated | the token as persisted (obfuscated or encrypted — the core decrypts via the injected function) | | toolCatalog | cached { tools: [...] } fetched at connect time | | scope | "workspace" or "per_user" | | userId | owner for per-user connections, else null | | serverId / serverSlug / serverName / mcpEndpoint | the joined server metadata |

Provider-plugin authoring guide (OAuth)

OAuth-backed servers connect through a small provider plugin. Instead of branching on vendor slugs in your connect routes, you register an OAuthProvider and resolve it by slug. Adding a new provider is a registration in your code — never an edit to this package.

import { registerOAuthProvider, getOAuthProvider, type OAuthProvider } from "@apideck/mcp-connect";

const frontProvider: OAuthProvider = {
  slug: "front",

  // Is the host config present? (Read your own env/secrets here — the core never does.)
  isConfigured: () => Boolean(process.env.FRONT_CLIENT_ID && process.env.FRONT_CLIENT_SECRET),
  notConfiguredMessage: "Set FRONT_CLIENT_ID and FRONT_CLIENT_SECRET.",

  // Where the vendor should redirect back to. Derive from the request origin
  // (optionally honouring an env override).
  deriveRedirectUri: (origin) => `${origin}/api/integrations/front/oauth/callback`,

  // The vendor authorize URL you redirect the user to.
  buildAuthorizeUrl: ({ redirectUri, state, scope }) => {
    const u = new URL("https://app.frontapp.com/oauth/authorize");
    u.searchParams.set("client_id", process.env.FRONT_CLIENT_ID!);
    u.searchParams.set("redirect_uri", redirectUri);
    u.searchParams.set("response_type", "code");
    u.searchParams.set("state", state);
    return u.toString();
  },

  // Exchange the authorization code for tokens, returned in the neutral shape.
  exchangeCode: async ({ code, redirectUri }) => {
    const res = await fetch("https://app.frontapp.com/oauth/token", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        grant_type: "authorization_code",
        code,
        redirect_uri: redirectUri,
        client_id: process.env.FRONT_CLIENT_ID,
        client_secret: process.env.FRONT_CLIENT_SECRET,
      }),
    });
    const json = await res.json();
    return {
      accessToken: json.access_token,
      refreshToken: json.refresh_token ?? null,
      tokenType: json.token_type ?? "Bearer",
      expiresInSeconds: json.expires_in ?? null,
    };
  },

  // Optional: refresh support.
  refresh: async ({ refreshToken }) => { /* ... */ },
};

registerOAuthProvider(frontProvider);

Your connect route then does the vendor-agnostic thing:

const provider = getOAuthProvider(slug);
if (!provider) return respond(501, "Unknown provider");
if (!provider.isConfigured()) return respond(400, provider.notConfiguredMessage);
const redirectUri = provider.deriveRedirectUri(requestOrigin);
redirect(provider.buildAuthorizeUrl({ redirectUri, state, scope }));
// ...callback: const tokens = await provider.exchangeCode({ code, redirectUri });
//    encrypt tokens.accessToken, fetch the tool catalog, store.addConnection(...)

Registration is idempotent (last registration for a slug wins), so hot-reload and repeated imports are safe.

Security: token storage

Access tokens are secrets. This package gives you two token-at-rest options. Choose deliberately:

crypto.ts — AES-256-GCM (recommended)

Real authenticated encryption. encryptToken / decryptToken provide confidentiality and tamper-detection: any modification to the ciphertext (or the wrong key) makes decryptToken throw rather than return garbage.

import { encryptToken, decryptToken, generateKey, deriveKeyFromPassphrase } from "@apideck/mcp-connect";

const kek = generateKey();                         // 32-byte key → your secret manager
const envelope = encryptToken("secret-token", kek); // "v1.<iv>.<tag>.<ciphertext>"
const plain = decryptToken(envelope, kek);
  • The key (a KEK, key-encryption key) is injected as a 32-byte Buffer. The package never reads env — your host resolves the key (KMS, secret manager, env var) and passes it in, so key rotation is entirely under your control.
  • deriveKeyFromPassphrase(passphrase, salt) derives a KEK via scrypt for simple single-operator setups. Store and reuse the salt. Prefer generateKey() + a secret manager for production.
  • Use this as your default.

obfuscate.ts — HMAC-XOR obfuscation (NOT encryption)

obfuscateToken / deobfuscateToken implement an HMAC-SHA256 keystream XOR.

This is obfuscation, not encryption. It is NOT secure and must never be described as encryption-at-rest. It only stops tokens from appearing in plaintext to a casual cat, log dump, or generic secrets scanner. It provides no integrity guarantee and should not be relied on to protect against an attacker who reads your database.

It is kept only for back-compat with data written by earlier versions and for environments that explicitly accept its (non-)guarantees. New integrations should use AES-256-GCM.

Both modules share the same shape ((value, key) → string and back), so the core's injected deobfuscate function works with either — you decide which by choosing which decrypt function you pass to loadMcpToolset / buildToolset.

API surface

See ARCHITECTURE.md for the module boundary and the full export list. In short:

  • Persistence: ConnectionStore, Identity, StoredConnection, InMemoryConnectionStore
  • Toolset: buildToolset, loadMcpToolset, dispatchToolCall, safeToolName, normalizeSchema, renderMcpSystemPromptHint, McpConfigError
  • Transport: mcpListTools, mcpCallTool, McpRpcError
  • OAuth plugins: registerOAuthProvider, getOAuthProvider, registeredOAuthProviderSlugs, OAuthProvider
  • Token protection: encryptToken, decryptToken, generateKey, deriveKeyFromPassphrase, safeEqual (AES-GCM); obfuscateToken, deobfuscateToken (legacy)

Development

npm install
npm run build       # tsc → dist/ with .d.ts declarations
npm test            # vitest
npm run typecheck   # tsc --noEmit (includes examples/)
npm run lint        # eslint
npm run example     # runs examples/quickstart.ts end-to-end

License

MIT © Apideck. See LICENSE.