@apideck/mcp-connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/dist/client.d.ts +35 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +142 -0
- package/dist/client.js.map +1 -0
- package/dist/connection-store-memory.d.ts +34 -0
- package/dist/connection-store-memory.d.ts.map +1 -0
- package/dist/connection-store-memory.js +72 -0
- package/dist/connection-store-memory.js.map +1 -0
- package/dist/connection-store.d.ts +44 -0
- package/dist/connection-store.d.ts.map +1 -0
- package/dist/connection-store.js +4 -0
- package/dist/connection-store.js.map +1 -0
- package/dist/crypto.d.ts +27 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +108 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth-provider.d.ts +56 -0
- package/dist/oauth-provider.d.ts.map +1 -0
- package/dist/oauth-provider.js +17 -0
- package/dist/oauth-provider.js.map +1 -0
- package/dist/obfuscate.d.ts +3 -0
- package/dist/obfuscate.d.ts.map +1 -0
- package/dist/obfuscate.js +62 -0
- package/dist/obfuscate.js.map +1 -0
- package/dist/toolset.d.ts +95 -0
- package/dist/toolset.d.ts.map +1 -0
- package/dist/toolset.js +0 -0
- package/dist/toolset.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@apideck/mcp-connect` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-07-20
|
|
9
|
+
|
|
10
|
+
Initial release. Extracted as a standalone, framework-agnostic package from the
|
|
11
|
+
in-repo MCP-Connect core.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Toolset engine** (`toolset.ts`): `buildToolset`, `loadMcpToolset`,
|
|
16
|
+
`dispatchToolCall`, `safeToolName`, `normalizeSchema`,
|
|
17
|
+
`renderMcpSystemPromptHint`, and the recoverable `McpConfigError`. Loads
|
|
18
|
+
connected MCP servers, namespaces tools as `slug__tool` (charset-safe, ≤64
|
|
19
|
+
chars with a stable hash suffix to avoid truncation collisions), and routes
|
|
20
|
+
inbound tool calls back to the right server.
|
|
21
|
+
- **Persistence seam** (`connection-store.ts`): the `ConnectionStore` interface
|
|
22
|
+
plus `Identity` and `StoredConnection` types. Tenancy and the
|
|
23
|
+
workspace-vs-per-user visibility rule are the host's responsibility.
|
|
24
|
+
- **In-memory reference store** (`connection-store-memory.ts`):
|
|
25
|
+
`InMemoryConnectionStore` — a zero-dependency implementation that enforces the
|
|
26
|
+
full visibility contract, so the package runs standalone.
|
|
27
|
+
- **MCP transport client** (`client.ts`): `mcpListTools`, `mcpCallTool`, and
|
|
28
|
+
`McpRpcError`. Thin shim over `@modelcontextprotocol/sdk` that tries Streamable
|
|
29
|
+
HTTP first and falls back to legacy HTTP+SSE, with an end-to-end request
|
|
30
|
+
timeout.
|
|
31
|
+
- **OAuth provider plugin registry** (`oauth-provider.ts`):
|
|
32
|
+
`registerOAuthProvider`, `getOAuthProvider`, `registeredOAuthProviderSlugs`,
|
|
33
|
+
and the `OAuthProvider` / `OAuthTokenResult` types. Connect flows resolve
|
|
34
|
+
providers by slug instead of branching on vendor literals.
|
|
35
|
+
- **AES-256-GCM token encryption** (`crypto.ts`, new in this package):
|
|
36
|
+
`encryptToken`, `decryptToken`, `generateKey`, `deriveKeyFromPassphrase`, and
|
|
37
|
+
`safeEqual`. Authenticated encryption-at-rest under an injected KEK — the
|
|
38
|
+
recommended default. Detects tampering and wrong keys on decrypt.
|
|
39
|
+
- **Legacy obfuscation** (`obfuscate.ts`): `obfuscateToken` /
|
|
40
|
+
`deobfuscateToken` (HMAC-XOR). Preserved for back-compat only and explicitly
|
|
41
|
+
documented as **not** encryption.
|
|
42
|
+
- **Neutral types** (`types.ts`): `ToolDef`, `ToolInputSchema`,
|
|
43
|
+
`ServerCatalogEntry`.
|
|
44
|
+
- **Runnable example** (`examples/quickstart.ts`): stands up a throwaway
|
|
45
|
+
localhost MCP server and exercises encrypt → store → load → namespace →
|
|
46
|
+
dispatch with zero external dependencies.
|
|
47
|
+
- Docs: `README.md` (quickstart, `ConnectionStore` contract, provider-plugin
|
|
48
|
+
authoring guide, honest token-storage security section), `ARCHITECTURE.md`,
|
|
49
|
+
`CONTRIBUTING.md`, MIT `LICENSE`.
|
|
50
|
+
|
|
51
|
+
[0.1.0]: https://github.com/apideck-libraries/mcp-connect/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Apideck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# @apideck/mcp-connect
|
|
2
|
+
|
|
3
|
+
Framework-agnostic core for **connecting, namespacing, and dispatching
|
|
4
|
+
[Model Context Protocol](https://modelcontextprotocol.io) (MCP) tools** — with
|
|
5
|
+
pluggable persistence, OAuth providers, and token encryption.
|
|
6
|
+
|
|
7
|
+
It is the reusable heart of a "connect your own MCP servers" feature: given a
|
|
8
|
+
set of connected MCP servers, it loads their tools, namespaces them so their
|
|
9
|
+
names never collide, exposes them as neutral tool definitions you can hand to
|
|
10
|
+
any LLM, and routes inbound tool calls back to the right server. It has **no
|
|
11
|
+
database, no web framework, and no LLM-vendor dependency** — everything
|
|
12
|
+
app-specific is injected through small seams.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
connected servers ──► load + namespace tools ──► neutral ToolDef[] ──► your LLM
|
|
16
|
+
▲ │
|
|
17
|
+
│ tool call by name
|
|
18
|
+
ConnectionStore │
|
|
19
|
+
(you implement) ◄──── dispatch routes it back to the right server ◄───┘
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @apideck/mcp-connect
|
|
26
|
+
# peer runtime dep is bundled: @modelcontextprotocol/sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Node.js >= 20. ESM only (`"type": "module"`).
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
A complete, runnable version of this (with a throwaway localhost MCP server so
|
|
34
|
+
the dispatch is real) lives in [`examples/quickstart.ts`](./examples/quickstart.ts)
|
|
35
|
+
— run it with `npm run example`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import {
|
|
39
|
+
InMemoryConnectionStore,
|
|
40
|
+
loadMcpToolset,
|
|
41
|
+
dispatchToolCall,
|
|
42
|
+
renderMcpSystemPromptHint,
|
|
43
|
+
encryptToken,
|
|
44
|
+
decryptToken,
|
|
45
|
+
generateKey,
|
|
46
|
+
type Identity,
|
|
47
|
+
} from "@apideck/mcp-connect";
|
|
48
|
+
|
|
49
|
+
// 1. Resolve a 32-byte key-encryption key (KEK). In production this comes from
|
|
50
|
+
// your secret manager; never hard-code it.
|
|
51
|
+
const kek = generateKey();
|
|
52
|
+
|
|
53
|
+
// 2. Persist a connection. The store only ever sees the ENCRYPTED token.
|
|
54
|
+
const store = new InMemoryConnectionStore();
|
|
55
|
+
store.addConnection({
|
|
56
|
+
tenantId: "acme-inc",
|
|
57
|
+
serverSlug: "front",
|
|
58
|
+
serverName: "Front",
|
|
59
|
+
mcpEndpoint: "https://mcp.frontapp.com/mcp",
|
|
60
|
+
accessTokenObfuscated: encryptToken("the-oauth-access-token", kek),
|
|
61
|
+
scope: "workspace",
|
|
62
|
+
toolCatalog: { tools: [/* fetched at connect time via mcpListTools */] },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 3. Load + namespace the tools for a given identity. The core decrypts via the
|
|
66
|
+
// injected function — the store never does crypto.
|
|
67
|
+
const identity: Identity = { tenantId: "acme-inc", ownerId: "user-1" };
|
|
68
|
+
const toolset = await loadMcpToolset(store, identity, (enc) => decryptToken(enc, kek));
|
|
69
|
+
|
|
70
|
+
// 4. Hand toolset.toolDefs to your LLM, and optionally add a prompt hint.
|
|
71
|
+
const systemHint = renderMcpSystemPromptHint(toolset);
|
|
72
|
+
|
|
73
|
+
// 5. When the model calls a tool, route it back:
|
|
74
|
+
const result = await dispatchToolCall(
|
|
75
|
+
toolset,
|
|
76
|
+
"front__search", // namespaced name the model called
|
|
77
|
+
{ query: "acme.com" }, // tool input
|
|
78
|
+
{ resolveHeaders: () => ({}) },
|
|
79
|
+
);
|
|
80
|
+
// result.text is ready to drop into a tool_result block.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Concepts
|
|
84
|
+
|
|
85
|
+
### Tool namespacing
|
|
86
|
+
|
|
87
|
+
MCP servers don't namespace their tool names, so two servers could both expose a
|
|
88
|
+
`search` tool. `mcp-connect` prefixes every tool with its server slug and a
|
|
89
|
+
double underscore: `front__search`. Names are sanitized to `[A-Za-z0-9_-]{1,64}`
|
|
90
|
+
(the Anthropic constraint, a safe lowest common denominator) and, when a name
|
|
91
|
+
would exceed 64 chars, truncated with a stable hash suffix so distinct tools
|
|
92
|
+
never collide. That same `slug__tool` convention is what lets `dispatchToolCall`
|
|
93
|
+
route an inbound call back to the right server with no extra round trip.
|
|
94
|
+
|
|
95
|
+
### The `ConnectionStore` seam
|
|
96
|
+
|
|
97
|
+
Persistence is injected. The core only ever calls:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
interface ConnectionStore {
|
|
101
|
+
listConnections(identity: Identity): Promise<StoredConnection[]>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Identity {
|
|
105
|
+
tenantId: string; // opaque routing key
|
|
106
|
+
ownerId: string; // the signed-in user (scopes per-user connections)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Your implementation must honour the **visibility contract**:
|
|
111
|
+
|
|
112
|
+
- a connection is only visible within its own `tenantId`;
|
|
113
|
+
- `workspace`-scope connections are visible to every user in the tenant;
|
|
114
|
+
- `per_user`-scope connections are visible only to their `userId` owner.
|
|
115
|
+
|
|
116
|
+
`InMemoryConnectionStore` ships in the box (great for tests, prototypes, and the
|
|
117
|
+
example) and demonstrates exactly that contract. For production, implement the
|
|
118
|
+
interface against your own datastore (SQL, KV, an ORM, …). `StoredConnection`
|
|
119
|
+
carries no `tenantId` field because tenant scoping is the store's job, not the
|
|
120
|
+
core's — your store filters by tenant before returning rows.
|
|
121
|
+
|
|
122
|
+
`StoredConnection` fields:
|
|
123
|
+
|
|
124
|
+
| field | meaning |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `connectionId` | your primary key |
|
|
127
|
+
| `accessTokenObfuscated` | the token **as persisted** (obfuscated or encrypted — the core decrypts via the injected function) |
|
|
128
|
+
| `toolCatalog` | cached `{ tools: [...] }` fetched at connect time |
|
|
129
|
+
| `scope` | `"workspace"` or `"per_user"` |
|
|
130
|
+
| `userId` | owner for per-user connections, else `null` |
|
|
131
|
+
| `serverId` / `serverSlug` / `serverName` / `mcpEndpoint` | the joined server metadata |
|
|
132
|
+
|
|
133
|
+
## Provider-plugin authoring guide (OAuth)
|
|
134
|
+
|
|
135
|
+
OAuth-backed servers connect through a small **provider plugin**. Instead of
|
|
136
|
+
branching on vendor slugs in your connect routes, you register an `OAuthProvider`
|
|
137
|
+
and resolve it by slug. Adding a new provider is a registration in your code —
|
|
138
|
+
never an edit to this package.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { registerOAuthProvider, getOAuthProvider, type OAuthProvider } from "@apideck/mcp-connect";
|
|
142
|
+
|
|
143
|
+
const frontProvider: OAuthProvider = {
|
|
144
|
+
slug: "front",
|
|
145
|
+
|
|
146
|
+
// Is the host config present? (Read your own env/secrets here — the core never does.)
|
|
147
|
+
isConfigured: () => Boolean(process.env.FRONT_CLIENT_ID && process.env.FRONT_CLIENT_SECRET),
|
|
148
|
+
notConfiguredMessage: "Set FRONT_CLIENT_ID and FRONT_CLIENT_SECRET.",
|
|
149
|
+
|
|
150
|
+
// Where the vendor should redirect back to. Derive from the request origin
|
|
151
|
+
// (optionally honouring an env override).
|
|
152
|
+
deriveRedirectUri: (origin) => `${origin}/api/integrations/front/oauth/callback`,
|
|
153
|
+
|
|
154
|
+
// The vendor authorize URL you redirect the user to.
|
|
155
|
+
buildAuthorizeUrl: ({ redirectUri, state, scope }) => {
|
|
156
|
+
const u = new URL("https://app.frontapp.com/oauth/authorize");
|
|
157
|
+
u.searchParams.set("client_id", process.env.FRONT_CLIENT_ID!);
|
|
158
|
+
u.searchParams.set("redirect_uri", redirectUri);
|
|
159
|
+
u.searchParams.set("response_type", "code");
|
|
160
|
+
u.searchParams.set("state", state);
|
|
161
|
+
return u.toString();
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Exchange the authorization code for tokens, returned in the neutral shape.
|
|
165
|
+
exchangeCode: async ({ code, redirectUri }) => {
|
|
166
|
+
const res = await fetch("https://app.frontapp.com/oauth/token", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "content-type": "application/json" },
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
grant_type: "authorization_code",
|
|
171
|
+
code,
|
|
172
|
+
redirect_uri: redirectUri,
|
|
173
|
+
client_id: process.env.FRONT_CLIENT_ID,
|
|
174
|
+
client_secret: process.env.FRONT_CLIENT_SECRET,
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
const json = await res.json();
|
|
178
|
+
return {
|
|
179
|
+
accessToken: json.access_token,
|
|
180
|
+
refreshToken: json.refresh_token ?? null,
|
|
181
|
+
tokenType: json.token_type ?? "Bearer",
|
|
182
|
+
expiresInSeconds: json.expires_in ?? null,
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Optional: refresh support.
|
|
187
|
+
refresh: async ({ refreshToken }) => { /* ... */ },
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
registerOAuthProvider(frontProvider);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Your connect route then does the vendor-agnostic thing:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const provider = getOAuthProvider(slug);
|
|
197
|
+
if (!provider) return respond(501, "Unknown provider");
|
|
198
|
+
if (!provider.isConfigured()) return respond(400, provider.notConfiguredMessage);
|
|
199
|
+
const redirectUri = provider.deriveRedirectUri(requestOrigin);
|
|
200
|
+
redirect(provider.buildAuthorizeUrl({ redirectUri, state, scope }));
|
|
201
|
+
// ...callback: const tokens = await provider.exchangeCode({ code, redirectUri });
|
|
202
|
+
// encrypt tokens.accessToken, fetch the tool catalog, store.addConnection(...)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Registration is idempotent (last registration for a slug wins), so hot-reload
|
|
206
|
+
and repeated imports are safe.
|
|
207
|
+
|
|
208
|
+
## Security: token storage
|
|
209
|
+
|
|
210
|
+
Access tokens are secrets. This package gives you **two** token-at-rest options.
|
|
211
|
+
Choose deliberately:
|
|
212
|
+
|
|
213
|
+
### `crypto.ts` — AES-256-GCM (recommended)
|
|
214
|
+
|
|
215
|
+
Real authenticated encryption. `encryptToken` / `decryptToken` provide
|
|
216
|
+
confidentiality **and** tamper-detection: any modification to the ciphertext
|
|
217
|
+
(or the wrong key) makes `decryptToken` throw rather than return garbage.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { encryptToken, decryptToken, generateKey, deriveKeyFromPassphrase } from "@apideck/mcp-connect";
|
|
221
|
+
|
|
222
|
+
const kek = generateKey(); // 32-byte key → your secret manager
|
|
223
|
+
const envelope = encryptToken("secret-token", kek); // "v1.<iv>.<tag>.<ciphertext>"
|
|
224
|
+
const plain = decryptToken(envelope, kek);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- The key (a **KEK**, key-encryption key) is injected as a 32-byte `Buffer`. The
|
|
228
|
+
package never reads env — your host resolves the key (KMS, secret manager, env
|
|
229
|
+
var) and passes it in, so **key rotation is entirely under your control**.
|
|
230
|
+
- `deriveKeyFromPassphrase(passphrase, salt)` derives a KEK via scrypt for simple
|
|
231
|
+
single-operator setups. Store and reuse the salt. Prefer `generateKey()` + a
|
|
232
|
+
secret manager for production.
|
|
233
|
+
- **Use this as your default.**
|
|
234
|
+
|
|
235
|
+
### `obfuscate.ts` — HMAC-XOR obfuscation (NOT encryption)
|
|
236
|
+
|
|
237
|
+
`obfuscateToken` / `deobfuscateToken` implement an HMAC-SHA256 keystream XOR.
|
|
238
|
+
|
|
239
|
+
> **This is obfuscation, not encryption. It is NOT secure and must never be
|
|
240
|
+
> described as encryption-at-rest.** It only stops tokens from appearing in
|
|
241
|
+
> plaintext to a casual `cat`, log dump, or generic secrets scanner. It provides
|
|
242
|
+
> **no integrity guarantee** and should not be relied on to protect against an
|
|
243
|
+
> attacker who reads your database.
|
|
244
|
+
|
|
245
|
+
It is kept **only** for back-compat with data written by earlier versions and
|
|
246
|
+
for environments that explicitly accept its (non-)guarantees. New integrations
|
|
247
|
+
should use AES-256-GCM.
|
|
248
|
+
|
|
249
|
+
Both modules share the same shape (`(value, key) → string` and back), so the
|
|
250
|
+
core's injected `deobfuscate` function works with either — you decide which by
|
|
251
|
+
choosing which decrypt function you pass to `loadMcpToolset` / `buildToolset`.
|
|
252
|
+
|
|
253
|
+
## API surface
|
|
254
|
+
|
|
255
|
+
See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the module boundary and the full
|
|
256
|
+
export list. In short:
|
|
257
|
+
|
|
258
|
+
- **Persistence:** `ConnectionStore`, `Identity`, `StoredConnection`, `InMemoryConnectionStore`
|
|
259
|
+
- **Toolset:** `buildToolset`, `loadMcpToolset`, `dispatchToolCall`, `safeToolName`, `normalizeSchema`, `renderMcpSystemPromptHint`, `McpConfigError`
|
|
260
|
+
- **Transport:** `mcpListTools`, `mcpCallTool`, `McpRpcError`
|
|
261
|
+
- **OAuth plugins:** `registerOAuthProvider`, `getOAuthProvider`, `registeredOAuthProviderSlugs`, `OAuthProvider`
|
|
262
|
+
- **Token protection:** `encryptToken`, `decryptToken`, `generateKey`, `deriveKeyFromPassphrase`, `safeEqual` (AES-GCM); `obfuscateToken`, `deobfuscateToken` (legacy)
|
|
263
|
+
|
|
264
|
+
## Development
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npm install
|
|
268
|
+
npm run build # tsc → dist/ with .d.ts declarations
|
|
269
|
+
npm test # vitest
|
|
270
|
+
npm run typecheck # tsc --noEmit (includes examples/)
|
|
271
|
+
npm run lint # eslint
|
|
272
|
+
npm run example # runs examples/quickstart.ts end-to-end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT © Apideck. See [LICENSE](./LICENSE).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface McpToolDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
/** JSON Schema for the tool's input. */
|
|
5
|
+
inputSchema?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface McpListToolsResponse {
|
|
8
|
+
tools: McpToolDefinition[];
|
|
9
|
+
}
|
|
10
|
+
export interface McpCallToolResult {
|
|
11
|
+
content: Array<{
|
|
12
|
+
type: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
data?: string;
|
|
15
|
+
mimeType?: string;
|
|
16
|
+
}>;
|
|
17
|
+
isError?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface McpClientOptions {
|
|
20
|
+
endpoint: string;
|
|
21
|
+
accessToken: string;
|
|
22
|
+
/** Optional additional headers (e.g. for API-key style auth that
|
|
23
|
+
* uses a non-standard header). */
|
|
24
|
+
extraHeaders?: Record<string, string>;
|
|
25
|
+
/** Soft timeout per RPC. Defaults to 30s. */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
declare class McpRpcError extends Error {
|
|
29
|
+
readonly code: number;
|
|
30
|
+
constructor(message: string, code: number);
|
|
31
|
+
}
|
|
32
|
+
export declare function mcpListTools(opts: McpClientOptions): Promise<McpListToolsResponse>;
|
|
33
|
+
export declare function mcpCallTool(opts: McpClientOptions, name: string, args?: Record<string, unknown>): Promise<McpCallToolResult>;
|
|
34
|
+
export { McpRpcError };
|
|
35
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB;uCACmC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,cAAM,WAAY,SAAQ,KAAK;aACgB,IAAI,EAAE,MAAM;gBAA7C,OAAO,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAI1D;AAiGD,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAWxF;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAQ5B;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Apideck
|
|
3
|
+
/**
|
|
4
|
+
* MCP client wrapper.
|
|
5
|
+
*
|
|
6
|
+
* Thin shim over @modelcontextprotocol/sdk so the rest of the app
|
|
7
|
+
* keeps speaking our small surface (mcpListTools / mcpCallTool) and
|
|
8
|
+
* doesn't see the SDK details. The SDK handles both transports
|
|
9
|
+
* transparently:
|
|
10
|
+
* - Legacy HTTP+SSE: GET <endpoint>/sse, server pushes a session-
|
|
11
|
+
* scoped POST URL via an `endpoint` event, client POSTs JSON-RPC
|
|
12
|
+
* there. (Front uses this.)
|
|
13
|
+
* - Streamable HTTP: single URL, POST gets the JSON-RPC response
|
|
14
|
+
* immediately or via a short-lived SSE stream.
|
|
15
|
+
* We try Streamable HTTP first; if the server speaks the legacy
|
|
16
|
+
* transport it'll reject and we fall back.
|
|
17
|
+
*/
|
|
18
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
19
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
20
|
+
// SSEClientTransport is the legacy HTTP+SSE transport. The SDK marks
|
|
21
|
+
// it deprecated but still ships it because servers like Front haven't
|
|
22
|
+
// migrated to Streamable HTTP yet. Keep until that's no longer true.
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
24
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
25
|
+
class McpRpcError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
constructor(message, code) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.name = "McpRpcError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the auth + Accept headers every MCP request needs. The SDK
|
|
35
|
+
* accepts a `requestInit` that's spread into each fetch.
|
|
36
|
+
*/
|
|
37
|
+
function buildHeaders(opts) {
|
|
38
|
+
return {
|
|
39
|
+
Authorization: `Bearer ${opts.accessToken}`,
|
|
40
|
+
...(opts.extraHeaders ?? {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Connect using Streamable HTTP first, fall back to legacy SSE if the
|
|
45
|
+
* server doesn't speak the new transport. Returns a connected Client
|
|
46
|
+
* instance the caller is responsible for closing.
|
|
47
|
+
*/
|
|
48
|
+
async function connect(opts) {
|
|
49
|
+
const client = new Client({ name: "@apideck/mcp-connect", version: "0.1.0" }, { capabilities: {} });
|
|
50
|
+
const url = new URL(opts.endpoint);
|
|
51
|
+
const headers = buildHeaders(opts);
|
|
52
|
+
// Try the newer transport first. If the server rejects with 4xx
|
|
53
|
+
// (typically 404 or 405), fall back to legacy SSE on the same URL.
|
|
54
|
+
try {
|
|
55
|
+
const transport = new StreamableHTTPClientTransport(url, {
|
|
56
|
+
requestInit: { headers },
|
|
57
|
+
});
|
|
58
|
+
await client.connect(transport);
|
|
59
|
+
return client;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Any non-success on Streamable HTTP → try legacy SSE. The SDK
|
|
63
|
+
// will throw various error shapes here (typed Error, status codes)
|
|
64
|
+
// so we just bail to the fallback rather than inspecting too hard.
|
|
65
|
+
// The SDK attaches `requestInit.headers` to BOTH the initial SSE
|
|
66
|
+
// GET and the follow-up POST requests (see SSEClientTransport
|
|
67
|
+
// _commonHeaders). We don't need to override eventSourceInit.fetch.
|
|
68
|
+
const sseTransport = new SSEClientTransport(url, {
|
|
69
|
+
requestInit: { headers },
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
await client.connect(sseTransport);
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
catch (sseErr) {
|
|
76
|
+
const msg = sseErr instanceof Error ? sseErr.message : String(sseErr);
|
|
77
|
+
// Surface BOTH errors so callers see what we tried. The route
|
|
78
|
+
// pattern-matches McpRpcError to choose the user-facing message.
|
|
79
|
+
const status = /401|unauthor/i.test(msg) ? 401 :
|
|
80
|
+
/403|forbidden/i.test(msg) ? 403 :
|
|
81
|
+
/5\d\d/.test(msg) ? 502 :
|
|
82
|
+
-32603;
|
|
83
|
+
// Re-throw the original error so its prototype/name is preserved
|
|
84
|
+
// (e.g. AbortError / TimeoutError) wrapped as McpRpcError.
|
|
85
|
+
throw new McpRpcError(`${err instanceof Error ? err.message : String(err)} | sse fallback: ${msg}`, status);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function withClient(opts, fn) {
|
|
90
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
91
|
+
// The race covers BOTH connect() and the inner call. Earlier
|
|
92
|
+
// versions only raced fn(client), which left a window where a
|
|
93
|
+
// hung SDK handshake (Streamable HTTP probe + SSE fallback) could
|
|
94
|
+
// block the request indefinitely before the timeout was armed.
|
|
95
|
+
// We share one timer between the two phases so the total budget
|
|
96
|
+
// is enforced end-to-end, not per phase.
|
|
97
|
+
let timeoutId = null;
|
|
98
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
99
|
+
timeoutId = setTimeout(() => reject(Object.assign(new Error("MCP request timed out"), { name: "TimeoutError" })), timeoutMs);
|
|
100
|
+
});
|
|
101
|
+
let client;
|
|
102
|
+
try {
|
|
103
|
+
client = await Promise.race([connect(opts), timeoutPromise]);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (timeoutId)
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return await Promise.race([fn(client), timeoutPromise]);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (timeoutId)
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
// Best-effort close; we don't care about errors during teardown.
|
|
117
|
+
void client.close().catch(() => undefined);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export async function mcpListTools(opts) {
|
|
121
|
+
return withClient(opts, async (client) => {
|
|
122
|
+
const res = await client.listTools();
|
|
123
|
+
return {
|
|
124
|
+
tools: res.tools.map((t) => ({
|
|
125
|
+
name: t.name,
|
|
126
|
+
description: t.description,
|
|
127
|
+
inputSchema: t.inputSchema,
|
|
128
|
+
})),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export async function mcpCallTool(opts, name, args = {}) {
|
|
133
|
+
return withClient(opts, async (client) => {
|
|
134
|
+
const res = await client.callTool({ name, arguments: args });
|
|
135
|
+
return {
|
|
136
|
+
content: (res.content ?? []),
|
|
137
|
+
isError: res.isError === true,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export { McpRpcError };
|
|
142
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ConnectionStore, Identity, StoredConnection } from "./connection-store.js";
|
|
2
|
+
/** Input for {@link InMemoryConnectionStore.addConnection}. Only the essentials
|
|
3
|
+
* are required; ids default to random UUIDs and scope defaults to per_user. */
|
|
4
|
+
export interface AddConnectionInput {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
serverSlug: string;
|
|
7
|
+
serverName: string;
|
|
8
|
+
mcpEndpoint: string;
|
|
9
|
+
/** Token as persisted — obfuscate (obfuscate.ts) or encrypt (crypto.ts)
|
|
10
|
+
* before passing it in. The store never sees plaintext by convention. */
|
|
11
|
+
accessTokenObfuscated: string;
|
|
12
|
+
/** Cached tool catalog, shaped `{ tools: [...] }`. Defaults to no tools. */
|
|
13
|
+
toolCatalog?: unknown;
|
|
14
|
+
scope?: "workspace" | "per_user";
|
|
15
|
+
/** Required when scope is per_user; ignored (forced null) for workspace. */
|
|
16
|
+
userId?: string | null;
|
|
17
|
+
connectionId?: string;
|
|
18
|
+
serverId?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class InMemoryConnectionStore implements ConnectionStore {
|
|
21
|
+
private records;
|
|
22
|
+
/** Register/save a connection (the "connect" step in a real flow, after the
|
|
23
|
+
* OAuth exchange + catalog fetch). Returns the stored row. */
|
|
24
|
+
addConnection(input: AddConnectionInput): StoredConnection;
|
|
25
|
+
/** Remove a connection by id. Returns true if a row was removed. */
|
|
26
|
+
removeConnection(connectionId: string): boolean;
|
|
27
|
+
/** Drop every connection (test/reset helper). */
|
|
28
|
+
clear(): void;
|
|
29
|
+
/** ConnectionStore contract: rows visible to `identity`, enforcing the tenant
|
|
30
|
+
* + scope visibility rule. */
|
|
31
|
+
listConnections(identity: Identity): Promise<StoredConnection[]>;
|
|
32
|
+
private strip;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=connection-store-memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-store-memory.d.ts","sourceRoot":"","sources":["../src/connection-store-memory.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAOzF;gFACgF;AAChF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;8EAC0E;IAC1E,qBAAqB,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IACjC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,uBAAwB,YAAW,eAAe;IAC7D,OAAO,CAAC,OAAO,CAAsB;IAErC;mEAC+D;IAC/D,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,gBAAgB;IAqB1D,oEAAoE;IACpE,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAM/C,iDAAiD;IACjD,KAAK,IAAI,IAAI;IAIb;mCAC+B;IACzB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAOtE,OAAO,CAAC,KAAK;CAMd"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Apideck
|
|
3
|
+
/**
|
|
4
|
+
* InMemoryConnectionStore — a zero-dependency reference implementation of the
|
|
5
|
+
* `ConnectionStore` seam.
|
|
6
|
+
*
|
|
7
|
+
* It lets the package run standalone (examples, tests, prototypes) with no
|
|
8
|
+
* database. It is NOT persistent — everything lives in a process-local array
|
|
9
|
+
* and is lost on restart. Production hosts implement `ConnectionStore` against
|
|
10
|
+
* their own datastore (SQL, KV, an ORM, …); this class shows the exact
|
|
11
|
+
* visibility contract they must honour.
|
|
12
|
+
*
|
|
13
|
+
* Visibility rule (the same one a real store must enforce):
|
|
14
|
+
* - a connection is only visible within its own `tenantId`;
|
|
15
|
+
* - `workspace`-scope connections are visible to every user in the tenant;
|
|
16
|
+
* - `per_user`-scope connections are visible only to their `userId` owner.
|
|
17
|
+
*
|
|
18
|
+
* The store is pure persistence: it stores the token exactly as given
|
|
19
|
+
* (`accessTokenObfuscated`, already obfuscated or encrypted by the caller) and
|
|
20
|
+
* never performs crypto itself.
|
|
21
|
+
*/
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
export class InMemoryConnectionStore {
|
|
24
|
+
records = [];
|
|
25
|
+
/** Register/save a connection (the "connect" step in a real flow, after the
|
|
26
|
+
* OAuth exchange + catalog fetch). Returns the stored row. */
|
|
27
|
+
addConnection(input) {
|
|
28
|
+
const scope = input.scope ?? "per_user";
|
|
29
|
+
if (scope === "per_user" && !input.userId) {
|
|
30
|
+
throw new Error("per_user connections require a userId");
|
|
31
|
+
}
|
|
32
|
+
const record = {
|
|
33
|
+
tenantId: input.tenantId,
|
|
34
|
+
connectionId: input.connectionId ?? randomUUID(),
|
|
35
|
+
accessTokenObfuscated: input.accessTokenObfuscated,
|
|
36
|
+
toolCatalog: input.toolCatalog ?? { tools: [] },
|
|
37
|
+
scope,
|
|
38
|
+
userId: scope === "workspace" ? null : (input.userId ?? null),
|
|
39
|
+
serverId: input.serverId ?? randomUUID(),
|
|
40
|
+
serverSlug: input.serverSlug,
|
|
41
|
+
serverName: input.serverName,
|
|
42
|
+
mcpEndpoint: input.mcpEndpoint,
|
|
43
|
+
};
|
|
44
|
+
this.records.push(record);
|
|
45
|
+
return this.strip(record);
|
|
46
|
+
}
|
|
47
|
+
/** Remove a connection by id. Returns true if a row was removed. */
|
|
48
|
+
removeConnection(connectionId) {
|
|
49
|
+
const before = this.records.length;
|
|
50
|
+
this.records = this.records.filter((r) => r.connectionId !== connectionId);
|
|
51
|
+
return this.records.length < before;
|
|
52
|
+
}
|
|
53
|
+
/** Drop every connection (test/reset helper). */
|
|
54
|
+
clear() {
|
|
55
|
+
this.records = [];
|
|
56
|
+
}
|
|
57
|
+
/** ConnectionStore contract: rows visible to `identity`, enforcing the tenant
|
|
58
|
+
* + scope visibility rule. */
|
|
59
|
+
async listConnections(identity) {
|
|
60
|
+
return this.records
|
|
61
|
+
.filter((r) => r.tenantId === identity.tenantId)
|
|
62
|
+
.filter((r) => r.scope === "workspace" || r.userId === identity.ownerId)
|
|
63
|
+
.map((r) => this.strip(r));
|
|
64
|
+
}
|
|
65
|
+
strip(record) {
|
|
66
|
+
// Hand back the neutral StoredConnection shape (drop the internal tenantId).
|
|
67
|
+
const { tenantId: _tenantId, ...rest } = record;
|
|
68
|
+
void _tenantId;
|
|
69
|
+
return rest;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=connection-store-memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-store-memory.js","sourceRoot":"","sources":["../src/connection-store-memory.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,6BAA6B;AAE7B;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2BzC,MAAM,OAAO,uBAAuB;IAC1B,OAAO,GAAmB,EAAE,CAAC;IAErC;mEAC+D;IAC/D,aAAa,CAAC,KAAyB;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,UAAU,CAAC;QACxC,IAAI,KAAK,KAAK,UAAU,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC3D,CAAC;QACD,MAAM,MAAM,GAAiB;YAC3B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,UAAU,EAAE;YAChD,qBAAqB,EAAE,KAAK,CAAC,qBAAqB;YAClD,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE;YAC/C,KAAK;YACL,MAAM,EAAE,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC;YAC7D,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,UAAU,EAAE;YACxC,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAED,oEAAoE;IACpE,gBAAgB,CAAC,YAAoB;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,YAAY,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;IACtC,CAAC;IAED,iDAAiD;IACjD,KAAK;QACH,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACpB,CAAC;IAED;mCAC+B;IAC/B,KAAK,CAAC,eAAe,CAAC,QAAkB;QACtC,OAAO,IAAI,CAAC,OAAO;aAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ,CAAC;aAC/C,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,WAAW,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,OAAO,CAAC;aACvE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;IAEO,KAAK,CAAC,MAAoB;QAChC,6EAA6E;QAC7E,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;QAChD,KAAK,SAAS,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|