{"_id":"@apideck/mcp-connect","_rev":"5-428872a22a9ffda4133ee6a8a9bd35cb","name":"@apideck/mcp-connect","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@apideck/mcp-connect","version":"0.1.0","keywords":["mcp","model-context-protocol","tools","oauth","ai","llm","apideck"],"author":{"name":"Apideck"},"license":"MIT","_id":"@apideck/mcp-connect@0.1.0","maintainers":[{"name":"gdewilde","email":"gertjan@apideck.com"},{"name":"nicklloyd","email":"nick@apideck.com"},{"name":"ritiksingh7","email":"ritik@apideck.com"},{"name":"samzani","email":"samir.amzani@gmail.com"},{"name":"lagoni","email":"jonas-lt@live.dk"},{"name":"jakeprins","email":"jake@apideck.com"},{"name":"gmenoiaa","email":"gmenoiaa@gmail.com"}],"homepage":"https://github.com/apideck-libraries/mcp-connect#readme","bugs":{"url":"https://github.com/apideck-libraries/mcp-connect/issues"},"dist":{"shasum":"97dd3ae4c10205bdb5baa0ad9219a331e941fa0e","tarball":"https://registry.npmjs.org/@apideck/mcp-connect/-/mcp-connect-0.1.0.tgz","fileCount":40,"integrity":"sha512-83qdRKJ+jBnl/XC0w7r1dsC1d7OwrfNEyHrfv+mU7HUs8465iRkF9l8Ds3+L6T+fVIhmuKW5iW5ZAzzZTivHcA==","signatures":[{"sig":"MEYCIQDTOFxsirEDLYF+PCzVNSmoYnjmKFrv/1yFn1fjptRgggIhAIXNICjh8xZnljNHGYtYMDW7nva+P20vSvdfcnMTEwFI","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":90475},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"017fe01b0a189eef3f33910d92cf2beb3e174f42","scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","example":"tsx examples/quickstart.ts","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build && npm run test"},"_npmUser":{"name":"gdewilde","email":"gertjan@apideck.com"},"repository":{"url":"git+https://github.com/apideck-libraries/mcp-connect.git","type":"git"},"_npmVersion":"12.0.1","description":"Framework-agnostic core for connecting, namespacing, and dispatching Model Context Protocol (MCP) tools — with pluggable persistence, OAuth providers, and token encryption.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","eslint":"^9.17.0","vitest":"^2.1.0","@eslint/js":"^9.17.0","typescript":"^5.6.0","@types/node":"^20.0.0","typescript-eslint":"^8.0.0","eslint-plugin-notice":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mcp-connect_0.1.0_1784586159237_0.8936769895073251","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@apideck/mcp-connect","version":"0.1.1","keywords":["mcp","model-context-protocol","tools","oauth","ai","llm","apideck"],"author":{"name":"Apideck"},"license":"MIT","_id":"@apideck/mcp-connect@0.1.1","maintainers":[{"name":"gdewilde","email":"gertjan@apideck.com"},{"name":"nicklloyd","email":"nick@apideck.com"},{"name":"ritiksingh7","email":"ritik@apideck.com"},{"name":"samzani","email":"samir.amzani@gmail.com"},{"name":"lagoni","email":"jonas-lt@live.dk"},{"name":"jakeprins","email":"jake@apideck.com"},{"name":"gmenoiaa","email":"gmenoiaa@gmail.com"}],"homepage":"https://github.com/apideck-libraries/mcp-connect#readme","bugs":{"url":"https://github.com/apideck-libraries/mcp-connect/issues"},"dist":{"shasum":"7007845afa95db74f9c80dd2c2a03a2a0bb28269","tarball":"https://registry.npmjs.org/@apideck/mcp-connect/-/mcp-connect-0.1.1.tgz","fileCount":40,"integrity":"sha512-hA/DhU4RoyHcfE1xX9Q+X01Y23p6qD+uqiHccNAEmPWvnjqbBCHSKnnCSKOA2gml5DRlFlbYDiLL23jpu7foMQ==","signatures":[{"sig":"MEYCIQD2cMQhmTODInXJUsy+8h+Cs+BxZBFI8P8FOcbEo8eGJAIhAPkZok4MBBCtSTsF9ujHvH1/St/00yY4CRj2LpSqmAUD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":95758},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"017fe01b0a189eef3f33910d92cf2beb3e174f42","scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","example":"tsx examples/quickstart.ts","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build && npm run test"},"_npmUser":{"name":"gdewilde","email":"gertjan@apideck.com"},"repository":{"url":"git+https://github.com/apideck-libraries/mcp-connect.git","type":"git"},"_npmVersion":"12.0.1","description":"Framework-agnostic core for connecting, namespacing, and dispatching Model Context Protocol (MCP) tools — with pluggable persistence, OAuth providers, and token encryption.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","eslint":"^9.17.0","vitest":"^2.1.0","@eslint/js":"^9.17.0","typescript":"^5.6.0","@types/node":"^20.0.0","typescript-eslint":"^8.0.0","eslint-plugin-notice":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mcp-connect_0.1.1_1784609891827_0.23904152321564687","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-07-20T22:22:39.081Z","modified":"2026-09-17T09:13:34.539Z","0.1.0":"2026-07-20T22:22:39.388Z","0.1.1":"2026-07-21T04:58:11.969Z"},"bugs":{"url":"https://github.com/apideck-libraries/mcp-connect/issues"},"author":{"name":"Apideck"},"license":"MIT","homepage":"https://github.com/apideck-libraries/mcp-connect#readme","keywords":["mcp","model-context-protocol","tools","oauth","ai","llm","apideck"],"repository":{"url":"git+https://github.com/apideck-libraries/mcp-connect.git","type":"git"},"description":"Framework-agnostic core for connecting, namespacing, and dispatching Model Context Protocol (MCP) tools — with pluggable persistence, OAuth providers, and token encryption.","maintainers":[{"email":"gertjan@apideck.com","name":"gdewilde"},{"email":"nick@apideck.com","name":"nicklloyd"},{"email":"ritik@apideck.com","name":"ritiksingh7"},{"email":"samir.amzani@gmail.com","name":"samzani"},{"email":"mithgroth@gmail.com","name":"mithgroth"},{"email":"gmenoiaa@gmail.com","name":"gmenoiaa"}],"readme":"# @apideck/mcp-connect\n\nFramework-agnostic core for **connecting, namespacing, and dispatching\n[Model Context Protocol](https://modelcontextprotocol.io) (MCP) tools** — with\npluggable persistence, OAuth providers, and token encryption.\n\nIt is the reusable heart of a \"connect your own MCP servers\" feature: given a\nset of connected MCP servers, it loads their tools, namespaces them so their\nnames never collide, exposes them as neutral tool definitions you can hand to\nany LLM, and routes inbound tool calls back to the right server. It has **no\ndatabase, no web framework, and no LLM-vendor dependency** — everything\napp-specific is injected through small seams.\n\n```\nconnected servers ──► load + namespace tools ──► neutral ToolDef[] ──► your LLM\n        ▲                                                                  │\n        │                                                          tool call by name\n   ConnectionStore                                                        │\n   (you implement)  ◄──── dispatch routes it back to the right server ◄───┘\n```\n\n## Install\n\n```bash\nnpm install @apideck/mcp-connect\n# peer runtime dep is bundled: @modelcontextprotocol/sdk\n```\n\nRequires Node.js >= 20. ESM only (`\"type\": \"module\"`).\n\n## Quickstart\n\nA complete, runnable version of this (with a throwaway localhost MCP server so\nthe dispatch is real) lives in [`examples/quickstart.ts`](./examples/quickstart.ts)\n— run it with `npm run example`.\n\n```ts\nimport {\n  InMemoryConnectionStore,\n  loadMcpToolset,\n  dispatchToolCall,\n  renderMcpSystemPromptHint,\n  encryptToken,\n  decryptToken,\n  generateKey,\n  type Identity,\n} from \"@apideck/mcp-connect\";\n\n// 1. Resolve a 32-byte key-encryption key (KEK). In production this comes from\n//    your secret manager; never hard-code it.\nconst kek = generateKey();\n\n// 2. Persist a connection. The store only ever sees the ENCRYPTED token.\nconst store = new InMemoryConnectionStore();\nstore.addConnection({\n  tenantId: \"acme-inc\",\n  serverSlug: \"front\",\n  serverName: \"Front\",\n  mcpEndpoint: \"https://mcp.frontapp.com/mcp\",\n  accessTokenObfuscated: encryptToken(\"the-oauth-access-token\", kek),\n  scope: \"workspace\",\n  toolCatalog: { tools: [/* fetched at connect time via mcpListTools */] },\n});\n\n// 3. Load + namespace the tools for a given identity. The core decrypts via the\n//    injected function — the store never does crypto.\nconst identity: Identity = { tenantId: \"acme-inc\", ownerId: \"user-1\" };\nconst toolset = await loadMcpToolset(store, identity, (enc) => decryptToken(enc, kek));\n\n// 4. Hand toolset.toolDefs to your LLM, and optionally add a prompt hint.\nconst systemHint = renderMcpSystemPromptHint(toolset);\n\n// 5. When the model calls a tool, route it back:\nconst result = await dispatchToolCall(\n  toolset,\n  \"front__search\",           // namespaced name the model called\n  { query: \"acme.com\" },      // tool input\n  { resolveHeaders: () => ({}) },\n);\n// result.text is ready to drop into a tool_result block.\n```\n\n## Concepts\n\n### Tool namespacing\n\nMCP servers don't namespace their tool names, so two servers could both expose a\n`search` tool. `mcp-connect` prefixes every tool with its server slug and a\ndouble underscore: `front__search`. Names are sanitized to `[A-Za-z0-9_-]{1,64}`\n(the Anthropic constraint, a safe lowest common denominator) and, when a name\nwould exceed 64 chars, truncated with a stable hash suffix so distinct tools\nnever collide. That same `slug__tool` convention is what lets `dispatchToolCall`\nroute an inbound call back to the right server with no extra round trip.\n\n### The `ConnectionStore` seam\n\nPersistence is injected. The core only ever calls:\n\n```ts\ninterface ConnectionStore {\n  listConnections(identity: Identity): Promise<StoredConnection[]>;\n}\n\ninterface Identity {\n  tenantId: string;   // opaque routing key\n  ownerId: string;    // the signed-in user (scopes per-user connections)\n}\n```\n\nYour implementation must honour the **visibility contract**:\n\n- a connection is only visible within its own `tenantId`;\n- `workspace`-scope connections are visible to every user in the tenant;\n- `per_user`-scope connections are visible only to their `userId` owner.\n\n`InMemoryConnectionStore` ships in the box (great for tests, prototypes, and the\nexample) and demonstrates exactly that contract. For production, implement the\ninterface against your own datastore (SQL, KV, an ORM, …). `StoredConnection`\ncarries no `tenantId` field because tenant scoping is the store's job, not the\ncore's — your store filters by tenant before returning rows.\n\n`StoredConnection` fields:\n\n| field | meaning |\n| --- | --- |\n| `connectionId` | your primary key |\n| `accessTokenObfuscated` | the token **as persisted** (obfuscated or encrypted — the core decrypts via the injected function) |\n| `toolCatalog` | cached `{ tools: [...] }` fetched at connect time |\n| `scope` | `\"workspace\"` or `\"per_user\"` |\n| `userId` | owner for per-user connections, else `null` |\n| `serverId` / `serverSlug` / `serverName` / `mcpEndpoint` | the joined server metadata |\n\n## Provider-plugin authoring guide (OAuth)\n\nOAuth-backed servers connect through a small **provider plugin**. Instead of\nbranching on vendor slugs in your connect routes, you register an `OAuthProvider`\nand resolve it by slug. Adding a new provider is a registration in your code —\nnever an edit to this package.\n\n```ts\nimport { registerOAuthProvider, getOAuthProvider, type OAuthProvider } from \"@apideck/mcp-connect\";\n\nconst frontProvider: OAuthProvider = {\n  slug: \"front\",\n\n  // Is the host config present? (Read your own env/secrets here — the core never does.)\n  isConfigured: () => Boolean(process.env.FRONT_CLIENT_ID && process.env.FRONT_CLIENT_SECRET),\n  notConfiguredMessage: \"Set FRONT_CLIENT_ID and FRONT_CLIENT_SECRET.\",\n\n  // Where the vendor should redirect back to. Derive from the request origin\n  // (optionally honouring an env override).\n  deriveRedirectUri: (origin) => `${origin}/api/integrations/front/oauth/callback`,\n\n  // The vendor authorize URL you redirect the user to.\n  buildAuthorizeUrl: ({ redirectUri, state, scope }) => {\n    const u = new URL(\"https://app.frontapp.com/oauth/authorize\");\n    u.searchParams.set(\"client_id\", process.env.FRONT_CLIENT_ID!);\n    u.searchParams.set(\"redirect_uri\", redirectUri);\n    u.searchParams.set(\"response_type\", \"code\");\n    u.searchParams.set(\"state\", state);\n    return u.toString();\n  },\n\n  // Exchange the authorization code for tokens, returned in the neutral shape.\n  exchangeCode: async ({ code, redirectUri }) => {\n    const res = await fetch(\"https://app.frontapp.com/oauth/token\", {\n      method: \"POST\",\n      headers: { \"content-type\": \"application/json\" },\n      body: JSON.stringify({\n        grant_type: \"authorization_code\",\n        code,\n        redirect_uri: redirectUri,\n        client_id: process.env.FRONT_CLIENT_ID,\n        client_secret: process.env.FRONT_CLIENT_SECRET,\n      }),\n    });\n    const json = await res.json();\n    return {\n      accessToken: json.access_token,\n      refreshToken: json.refresh_token ?? null,\n      tokenType: json.token_type ?? \"Bearer\",\n      expiresInSeconds: json.expires_in ?? null,\n    };\n  },\n\n  // Optional: refresh support.\n  refresh: async ({ refreshToken }) => { /* ... */ },\n};\n\nregisterOAuthProvider(frontProvider);\n```\n\nYour connect route then does the vendor-agnostic thing:\n\n```ts\nconst provider = getOAuthProvider(slug);\nif (!provider) return respond(501, \"Unknown provider\");\nif (!provider.isConfigured()) return respond(400, provider.notConfiguredMessage);\nconst redirectUri = provider.deriveRedirectUri(requestOrigin);\nredirect(provider.buildAuthorizeUrl({ redirectUri, state, scope }));\n// ...callback: const tokens = await provider.exchangeCode({ code, redirectUri });\n//    encrypt tokens.accessToken, fetch the tool catalog, store.addConnection(...)\n```\n\nRegistration is idempotent (last registration for a slug wins), so hot-reload\nand repeated imports are safe.\n\n## Security: token storage\n\nAccess tokens are secrets. This package gives you **two** token-at-rest options.\nChoose deliberately:\n\n### `crypto.ts` — AES-256-GCM (recommended)\n\nReal authenticated encryption. `encryptToken` / `decryptToken` provide\nconfidentiality **and** tamper-detection: any modification to the ciphertext\n(or the wrong key) makes `decryptToken` throw rather than return garbage.\n\n```ts\nimport { encryptToken, decryptToken, generateKey, deriveKeyFromPassphrase } from \"@apideck/mcp-connect\";\n\nconst kek = generateKey();                         // 32-byte key → your secret manager\nconst envelope = encryptToken(\"secret-token\", kek); // \"v1.<iv>.<tag>.<ciphertext>\"\nconst plain = decryptToken(envelope, kek);\n```\n\n- The key (a **KEK**, key-encryption key) is injected as a 32-byte `Buffer`. The\n  package never reads env — your host resolves the key (KMS, secret manager, env\n  var) and passes it in, so **key rotation is entirely under your control**.\n- `deriveKeyFromPassphrase(passphrase, salt)` derives a KEK via scrypt for simple\n  single-operator setups. Store and reuse the salt. Prefer `generateKey()` + a\n  secret manager for production.\n- **Use this as your default.**\n\n### `obfuscate.ts` — HMAC-XOR obfuscation (NOT encryption)\n\n`obfuscateToken` / `deobfuscateToken` implement an HMAC-SHA256 keystream XOR.\n\n> **This is obfuscation, not encryption. It is NOT secure and must never be\n> described as encryption-at-rest.** It only stops tokens from appearing in\n> plaintext to a casual `cat`, log dump, or generic secrets scanner. It provides\n> **no integrity guarantee** and should not be relied on to protect against an\n> attacker who reads your database.\n\nIt is kept **only** for back-compat with data written by earlier versions and\nfor environments that explicitly accept its (non-)guarantees. New integrations\nshould use AES-256-GCM.\n\nBoth modules share the same shape (`(value, key) → string` and back), so the\ncore's injected `deobfuscate` function works with either — you decide which by\nchoosing which decrypt function you pass to `loadMcpToolset` / `buildToolset`.\n\n## API surface\n\nSee [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the module boundary and the full\nexport list. In short:\n\n- **Persistence:** `ConnectionStore`, `Identity`, `StoredConnection`, `InMemoryConnectionStore`\n- **Toolset:** `buildToolset`, `loadMcpToolset`, `dispatchToolCall`, `safeToolName`, `normalizeSchema`, `renderMcpSystemPromptHint`, `McpConfigError`\n- **Transport:** `mcpListTools`, `mcpCallTool`, `McpRpcError`\n- **OAuth plugins:** `registerOAuthProvider`, `getOAuthProvider`, `registeredOAuthProviderSlugs`, `OAuthProvider`\n- **Token protection:** `encryptToken`, `decryptToken`, `generateKey`, `deriveKeyFromPassphrase`, `safeEqual` (AES-GCM); `obfuscateToken`, `deobfuscateToken` (legacy)\n\n## Development\n\n```bash\nnpm install\nnpm run build       # tsc → dist/ with .d.ts declarations\nnpm test            # vitest\nnpm run typecheck   # tsc --noEmit (includes examples/)\nnpm run lint        # eslint\nnpm run example     # runs examples/quickstart.ts end-to-end\n```\n\n## License\n\nMIT © Apideck. See [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}