@shuffleio/shuffle-mcps 0.0.4 → 0.0.5

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/README.md CHANGED
@@ -8,7 +8,6 @@ Turn 3,000+ SaaS tools into MCP servers your agents can use. One search, one cli
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/@shuffleio/shuffle-mcps.svg)](https://www.npmjs.com/package/@shuffleio/shuffle-mcps)
10
10
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://www.npmjs.com/package/@shuffleio/shuffle-mcps)
11
- [![upstream](https://img.shields.io/badge/upstream-Shuffle%2Fsingul.js-orange)](https://github.com/Shuffle/singul.js)
12
11
 
13
12
  </div>
14
13
 
@@ -29,7 +28,7 @@ Turn 3,000+ SaaS tools into MCP servers your agents can use. One search, one cli
29
28
 
30
29
  &nbsp;
31
30
 
32
- > **See it live:** [security.shuffler.io/apps](https://security.shuffler.io/apps) uses this package in production to power its 3,000+ integration picker.
31
+ > **See it live:** [security.shuffler.io/shuffle-mcp-demo](https://security.shuffler.io/shuffle-mcp-demo) — a fully working demo of every component in this package (inline search, search drawer, app detail drawer, auth, Try MCP, and Try individual actions). No login required.
33
32
 
34
33
  > **No SDK required.** Once a tool is authenticated in Shuffle, it is live as an MCP server immediately — call it from any agent over plain HTTP. This npm package is the optional UI layer for embedding the search-and-connect flow inside your own product.
35
34
 
@@ -74,7 +73,7 @@ import { ShuffleMCP } from 'shuffle-mcps';
74
73
  export default function App() {
75
74
  return (
76
75
  <ShuffleMCP
77
- authToken="any-token"
76
+ apiKey="YOUR_SHUFFLE_API_KEY"
78
77
  inline
79
78
  layout="grid"
80
79
  gridColumns={3}
@@ -84,28 +83,122 @@ export default function App() {
84
83
  }
85
84
  ```
86
85
 
87
- ## Recipes
86
+ ## The three components
88
87
 
89
- ### Search → detail drawer
88
+ The library exports three React components — use one, two, or all three. They share state, so you can mix-and-match.
90
89
 
91
- The most common pattern. Set `preventDefault` and handle `onAppSelected` to chain into your own auth or detail UI:
90
+ | Component | What it does | Live demo |
91
+ |---|---|---|
92
+ | [`<ShuffleMCP />`](#1-shufflemcp--inline-search) | Inline search box that merges Algolia's 3,000+ public app catalog with the current user's private apps | [/shuffle-mcp-demo §1](https://security.shuffler.io/shuffle-mcp-demo) |
93
+ | [`<AppSearchDrawer />`](#2-appsearchdrawer--full-search-drawer) | Right-side drawer wrapping `<ShuffleMCP />` plus a "Your apps" status row. This is the exact "Add Ingestion Source" drawer used in production | [/shuffle-mcp-demo §2](https://security.shuffler.io/shuffle-mcp-demo) |
94
+ | [`<AppDetailDrawer />`](#3-appdetaildrawer--single-app-config) | Right-side drawer for a single app: list/edit/test authentications and try the MCP `tools/call` endpoint inline | [/shuffle-mcp-demo §3](https://security.shuffler.io/shuffle-mcp-demo) |
95
+
96
+ ### 1. `<ShuffleMCP />` — inline search
97
+
98
+ Drop-in search component. By default, clicking a result opens `<AppDetailDrawer />` automatically.
99
+
100
+ ```tsx
101
+ import { ShuffleMCP } from 'shuffle-mcps';
102
+
103
+ <ShuffleMCP
104
+ apiKey={user.apiKey}
105
+ inline
106
+ layout="grid"
107
+ gridColumns={3}
108
+ />
109
+ ```
110
+
111
+ Take over selection yourself (open your own modal, route to a detail page, kick off a custom auth flow):
92
112
 
93
113
  ```tsx
94
114
  <ShuffleMCP
95
- authToken={token}
115
+ apiKey={user.apiKey}
96
116
  inline
97
117
  preventDefault
98
- onAppSelected={(d) => openDetailDrawer(d.app)}
118
+ onAppSelected={({ app, authUrl }) => {
119
+ navigate(`/integrations/${app.objectID}`);
120
+ }}
99
121
  />
100
122
  ```
101
123
 
102
- Full two-drawer reference implementation: [`src/components/shared/AppSearchDrawer.tsx`](../components/shared/AppSearchDrawer.tsx).
124
+ ### 2. `<AppSearchDrawer />` — full search drawer
125
+
126
+ The complete "Add Ingestion Source" experience: header, your authenticated apps strip, search, and the Shuffle Pipelines banner. Clicking an app opens `<AppDetailDrawer />` for auth + MCP testing.
127
+
128
+ ```tsx
129
+ import { useState } from 'react';
130
+ import { AppSearchDrawer } from 'shuffle-mcps';
131
+
132
+ const [open, setOpen] = useState(false);
133
+
134
+ <>
135
+ <button onClick={() => setOpen(true)}>Add integration</button>
136
+
137
+ <AppSearchDrawer
138
+ open={open}
139
+ onClose={() => setOpen(false)}
140
+ title="Add Ingestion Source"
141
+ subtitle="Search and authenticate a tool to ingest incidents from"
142
+ />
143
+ </>
144
+ ```
145
+
146
+ Useful props:
147
+
148
+ | Prop | Type | Description |
149
+ |---|---|---|
150
+ | `open` | `boolean` | Controlled open state |
151
+ | `onClose` | `() => void` | Called on close |
152
+ | `title` / `subtitle` | `string` | Header copy |
153
+ | `anchor` | `'left' \| 'right'` | Default `'right'` |
154
+ | `width` | `number` | Drawer width in px. Default `560` |
155
+ | `pinnedApps` | `Array<{ name, image_url, ... }>` | Apps pinned to the top of search results |
156
+ | `onQuickSelect` | `(app) => void` | Skip the detail drawer and call this directly |
157
+ | `onSelectOverride` | `(app) => boolean` | Intercept selection — return `true` to prevent default |
158
+ | `priorityCategory` | `string` | Sort matching apps to the top of the "Your apps" strip |
159
+ | `showPipelinesBanner` | `boolean` | Show the Shuffle Pipelines CTA |
160
+
161
+ ### 3. `<AppDetailDrawer />` — single-app config
162
+
163
+ The right-side drawer that shows everything about one app: existing auth entries (with Configured / Tested / Inactive chips), an inline form to add or edit credentials, a **Test connection** button, an **Activate / Deactivate** toggle, and a built-in MCP `tools/call` chat to try it without leaving the page.
164
+
165
+ ```tsx
166
+ import { useState } from 'react';
167
+ import { AppDetailDrawer } from 'shuffle-mcps';
168
+
169
+ const [appName, setAppName] = useState<string | null>(null);
170
+
171
+ <>
172
+ <button onClick={() => setAppName('Gmail')}>Configure Gmail</button>
173
+
174
+ <AppDetailDrawer
175
+ open={appName !== null}
176
+ onClose={() => setAppName(null)}
177
+ appName={appName}
178
+ />
179
+ </>
180
+ ```
181
+
182
+ Useful props:
183
+
184
+ | Prop | Type | Description |
185
+ |---|---|---|
186
+ | `open` | `boolean` | Controlled open state |
187
+ | `onClose` | `() => void` | Called on close |
188
+ | `appName` | `string \| null` | The app to load. Resolved against Algolia by name |
189
+ | `anchor` | `'left' \| 'right'` | Default `'right'` |
190
+ | `width` | `number` | Drawer width in px. Default `520` |
191
+ | `onRefresh` | `() => void` | Called after a successful save/test, so the parent can re-fetch |
192
+ | `onAddToCanvas` | `(info) => void` | Replaces the **Activate** CTA with **+ Add** — used by the workflow editor |
193
+ | `isAuthenticated` | `boolean` | Whether the current user is signed in. Default `true` |
194
+
195
+ Reference implementation in production: [`src/Shuffle-MCPs/AppSearchDrawer.tsx`](./AppSearchDrawer.tsx) and [`AppDetailDrawer.tsx`](./AppDetailDrawer.tsx).
103
196
 
104
197
  ### Predefined search
105
198
 
106
199
  ```tsx
107
200
  <ShuffleMCP
108
- authToken="..."
201
+ apiKey="..."
109
202
  inline
110
203
  initialFilterQuery="siem"
111
204
  placeholder="Search SIEM tools..."
@@ -118,7 +211,7 @@ Full two-drawer reference implementation: [`src/components/shared/AppSearchDrawe
118
211
  const [picked, setPicked] = useState<AlgoliaSearchApp[]>([]);
119
212
 
120
213
  <ShuffleMCP
121
- authToken="..."
214
+ apiKey="..."
122
215
  inline
123
216
  multiSelect
124
217
  showCheckbox
@@ -131,7 +224,6 @@ const [picked, setPicked] = useState<AlgoliaSearchApp[]>([]);
131
224
 
132
225
  ```tsx
133
226
  <ShuffleMCP
134
- authToken={user.token}
135
227
  apiKey={user.apiKey}
136
228
  apiBaseUrl="https://your-backend.example.com"
137
229
  algoliaAppId="YOUR_APP_ID"
@@ -139,13 +231,16 @@ const [picked, setPicked] = useState<AlgoliaSearchApp[]>([]);
139
231
  />
140
232
  ```
141
233
 
234
+ > **About `apiKey`:** this is the **current user's personal Shuffle API key**, not an org-level or service token. Your backend should look up the signed-in user's API key (wherever you store it — DB, session, etc.) and pass it through to this component. The component sends it as `Authorization: Bearer` on every API request and forwards it into the auth handoff URL as `&auth=`, so all reads and authentications happen as that user.
235
+
142
236
  When `apiKey` is set, the component also fetches the user's **private apps** from `/api/v1/apps` and merges them into the search results, with an **All / Public / Private** filter shown above the list. Status dots appear too: validated, configured, selected, inactive.
143
237
 
144
238
  ## Common props
145
239
 
146
240
  | Prop | Type | Description |
147
241
  |---|---|---|
148
- | `authToken` | `string` | Required. Forwarded into the auth URL. |
242
+ | `apiKey` | `string` | The **current user's** Shuffle API key. Your backend should fetch it from wherever you store the user's credentials and pass it in. Used as `Authorization: Bearer` on every request and forwarded into the auth handoff URL as `&auth=`. This is the canonical credential. |
243
+ | `~~authToken~~` | `string` | **Deprecated.** Use `apiKey`. Kept for back-compat — only used as the auth-URL token if `apiKey` is not set. |
149
244
  | `orgId` | `string` | Optional Shuffle organization ID. When set, every API call (`/api/v1/apps/authentication`, `/api/v1/apps`) is sent with an `Org-Id: <orgId>` header, and the auth URL gets `&org_id=<orgId>` appended. Default: not sent. |
150
245
  | `inline` | `boolean` | Inline results vs floating dropdown. |
151
246
  | `layout` | `'list' \| 'grid'` | Result layout. Default `'list'`. |
@@ -153,7 +248,6 @@ When `apiKey` is set, the component also fetches the user's **private apps** fro
153
248
  | `preventDefault` | `boolean` | Skip the default click behavior (built-in drawer or `window.open(authUrl)`) so you can handle selection yourself. |
154
249
  | `onAppSelected` | `(detail) => void` | Fires on single-select pick. If provided, the built-in drawer is **not** opened — you take over. |
155
250
  | `multiSelect` | `boolean` | Allow selecting multiple apps. |
156
- | `apiKey` | `string` | Bearer token. Enables status dots and merges your private apps from `/api/v1/apps`. |
157
251
  | `showSourceFilter` | `boolean` | Toggle the All / Public / Private filter. Default `true`. |
158
252
  | `customStyles` | `CustomStyles` | Per-slot style overrides. |
159
253
 
@@ -165,20 +259,63 @@ When you do **not** pass `onAppSelected`, clicking an app opens a built-in right
165
259
 
166
260
  ```tsx
167
261
  <ShuffleMCP
168
- authToken={token}
169
- apiKey={user.apiKey}
170
- orgId={currentOrg.id} // every request now scoped to this org
262
+ apiKey={user.apiKey} // current user's Shuffle API key, fetched by your backend
263
+ orgId={currentOrg.id} // every request now scoped to this org
171
264
  />
172
265
  ```
173
266
 
174
267
 
268
+ ## Theming (light + dark)
269
+
270
+ There is no `theme` prop — every component reads from your app's CSS variables and inherits whatever palette is active. Define these tokens once in your global stylesheet (e.g. `:root` for light, `.dark` for dark) and the components flip automatically:
271
+
272
+ ```css
273
+ :root {
274
+ --background: 0 0% 100%;
275
+ --foreground: 222 47% 11%;
276
+ --card: 0 0% 100%;
277
+ --border: 220 13% 91%;
278
+ --muted: 220 14% 96%;
279
+ --muted-foreground: 220 9% 46%;
280
+ --primary: 24 100% 50%; /* Shuffle orange */
281
+ --primary-foreground: 0 0% 100%;
282
+ }
283
+
284
+ .dark {
285
+ --background: 222 47% 6%;
286
+ --foreground: 0 0% 98%;
287
+ --card: 222 47% 9%;
288
+ --border: 217 19% 18%;
289
+ --muted: 217 19% 14%;
290
+ --muted-foreground: 220 9% 65%;
291
+ --primary: 24 100% 55%;
292
+ --primary-foreground: 0 0% 100%;
293
+ }
294
+ ```
295
+
296
+ Values are raw HSL channels (no `hsl(...)` wrapper) because internally we use `hsl(var(--card))`, `hsl(var(--border) / 0.5)`, etc. Toggle dark mode by adding/removing the `.dark` class on `<html>` or `<body>` — same pattern as Tailwind / shadcn.
297
+
298
+ For finer-grained overrides on `<ShuffleMCP />`, pass `customStyles` — every slot (container, input, dropdown, gridItem, appName, appDescription, appCategory, appTags, emptyState, errorState, …) accepts a `React.CSSProperties` object. See [LIBRARY.md](./LIBRARY.md#styling) for the full slot list.
299
+
300
+ ```tsx
301
+ <ShuffleMCP
302
+ apiKey="..."
303
+ inline
304
+ customStyles={{
305
+ container: { borderRadius: 16 },
306
+ input: { fontFamily: 'Inter, sans-serif' },
307
+ gridItem: { background: 'hsl(var(--card))', border: '1px solid hsl(var(--border))' },
308
+ }}
309
+ />
310
+ ```
311
+
175
312
  Full prop reference, framework setup (Next.js, Vue), styling slots, custom rendering, and publishing: [**LIBRARY.md**](./LIBRARY.md).
176
313
 
177
314
  ## Imperative handle
178
315
 
179
316
  ```tsx
180
317
  const ref = useRef<ShuffleMCPHandle>(null);
181
- <ShuffleMCP ref={ref} authToken="..." inline />
318
+ <ShuffleMCP ref={ref} apiKey="..." inline />
182
319
 
183
320
  ref.current?.search('slack');
184
321
  ref.current?.clear();