@cosmoops/connect-sdk 0.15.0 → 0.16.1

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +2 -1
  3. package/dist/cjs/client/components/ai-screens.js +73 -0
  4. package/dist/cjs/client/components/ai-widgets.js +37 -0
  5. package/dist/cjs/client/components/ask-ai.js +31 -5
  6. package/dist/cjs/client/index.js +5 -2
  7. package/dist/cjs/index.js +6 -1
  8. package/dist/cjs/mcp/index.js +90 -0
  9. package/dist/cjs/mcp/tools.js +369 -0
  10. package/dist/cjs/server/client.js +1 -1
  11. package/dist/cjs/server/handler.js +7 -0
  12. package/dist/cjs/server/resources/ai.js +8 -1
  13. package/dist/cjs/store-schema.js +66 -0
  14. package/dist/esm/client/components/ai-screens.js +69 -0
  15. package/dist/esm/client/components/ai-widgets.js +37 -1
  16. package/dist/esm/client/components/ask-ai.js +32 -6
  17. package/dist/esm/client/index.js +1 -0
  18. package/dist/esm/index.js +3 -0
  19. package/dist/esm/mcp/index.js +85 -0
  20. package/dist/esm/mcp/tools.js +366 -0
  21. package/dist/esm/server/client.js +1 -1
  22. package/dist/esm/server/handler.js +7 -0
  23. package/dist/esm/server/resources/ai.js +8 -1
  24. package/dist/esm/store-schema.js +62 -0
  25. package/dist/types/client/components/ai-screens.d.ts +25 -0
  26. package/dist/types/client/components/ai-widgets.d.ts +34 -1
  27. package/dist/types/client/components/ask-ai.d.ts +47 -1
  28. package/dist/types/client/index.d.ts +2 -0
  29. package/dist/types/index.d.ts +4 -0
  30. package/dist/types/mcp/index.d.ts +85 -0
  31. package/dist/types/mcp/tools.d.ts +107 -0
  32. package/dist/types/server/client.d.ts +1 -1
  33. package/dist/types/server/resources/ai.d.ts +11 -1
  34. package/dist/types/store-schema.d.ts +52 -0
  35. package/dist/types/types.d.ts +50 -1
  36. package/package.json +10 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,92 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.16.1
7
+
8
+ ### Fixed
9
+
10
+ - **`McpToolResult` is a type alias, not an interface** — so `callTool()` can be
11
+ returned straight from `server.setRequestHandler(CallToolRequestSchema, ...)`.
12
+
13
+ The MCP SDK types a request handler's return as a union whose members carry an
14
+ index signature. TypeScript does not grant an interface the implicit index
15
+ signature it grants a type alias, so as an interface the result was assignable
16
+ to none of them, and the four-line wiring in 0.16.0's own documentation failed
17
+ to compile with a misleading `Property 'task' is missing`. The only way past it
18
+ was a cast, at exactly the boundary where a cast hides shape errors.
19
+
20
+ Type-level only; no runtime change, and nothing that compiled against 0.16.0
21
+ stops compiling.
22
+
23
+ ## 0.16.0
24
+
25
+ ### Added
26
+
27
+ - **`@cosmoops/connect-sdk/mcp` - your store declaration, as MCP tools.**
28
+
29
+ ```ts
30
+ import { createStoreToolset } from "@cosmoops/connect-sdk/mcp";
31
+
32
+ const toolset = createStoreToolset({
33
+ collections: STORE_SCHEMA, // the array you already seed to Connect
34
+ data: connect.data,
35
+ workspaceId: session.workspaceId,
36
+ asUser: session.sessionToken, // every call runs as this user
37
+ });
38
+
39
+ server.setRequestHandler(ListToolsRequestSchema, () => toolset.listTools());
40
+ server.setRequestHandler(CallToolRequestSchema, (request) =>
41
+ toolset.callTool(request.params.name, request.params.arguments ?? {}),
42
+ );
43
+ ```
44
+
45
+ `Application.storeSchema` already says what each collection holds, which
46
+ fields may be filtered on, which query shapes the database can serve, and
47
+ whether it refuses everything but a create. Writing tools over that by hand
48
+ means keeping a second copy of all of it, and the copy is wrong the first time
49
+ a filter is added to a screen. So they are derived: adding a queryable field
50
+ and re-seeding widens the tools on the next boot, with no tool code to touch.
51
+
52
+ Three read tools per collection (`_get`, `_list`, `_aggregate`), and writes
53
+ only if you ask - `writes: "create" | "all"`, defaulting to `"none"`. An
54
+ `appendOnly` collection never derives an update or a delete, because Connect
55
+ would refuse them anyway and a tool that can only fail is worse than no tool.
56
+
57
+ Each tool's description carries the collection's queryable fields and its
58
+ declared composite indexes, and an unservable query is refused locally with
59
+ the index it would need - so a model adapts instead of spending a turn
60
+ discovering the shape.
61
+
62
+ **Every call carries an end user's session token.** Connect resolves it to a
63
+ member and checks the collection's declared `readPermission`/`writePermission`
64
+ against that member's live roles, so a toolset never reaches further than the
65
+ person it runs for, and a permission revoked in the console is gone from the
66
+ next call. Build one per request from `getConnectSession()`, or pass a
67
+ function for `asUser`.
68
+
69
+ There is deliberately **no scan tool**. `data.scan` reads across every
70
+ workspace and for that reason takes no `asUser` - deriving one would mean that
71
+ single call escalating to your API key while every tool beside it ran as the
72
+ user. Cross-tenant reads belong behind their own gate.
73
+
74
+ Server-only, like `/server` and `/next`: a browser import resolves to the
75
+ guard stub and fails the build. **No new dependency** - `listTools()` and
76
+ `callTool()` answer in the shapes the MCP protocol defines, so the official
77
+ MCP SDK stays yours to install and version.
78
+
79
+ **Pass `include`.** Measured on the reference applications, deriving over
80
+ every collection yields 45 read tools for club and 21 for CRM - whole-catalogue
81
+ numbers, and a model offered them chooses worse than one offered a handful.
82
+ Name the collections the surface at hand is about. Where a collection declares
83
+ more index shapes than are worth listing (club's ledger declares around
84
+ forty), the description states the rule and leaves the specifics to the
85
+ refusal, which names the exact index a rejected query needed.
86
+
87
+ - **`StoreCollectionDeclaration`, `describeQueryRejection` and
88
+ `storeIndexCovers`** on the root entry. The declaration type is the shape
89
+ Connect stores on `Application.storeSchema`, so an app deriving its schema
90
+ from its own query matrix can stop declaring a local copy of it.
91
+
6
92
  ## 0.15.0
7
93
 
8
94
  Your application's own database stops being a toy. Real queries, real
package/README.md CHANGED
@@ -12,13 +12,14 @@ npm install @cosmoops/connect-sdk
12
12
  npm install streamdown # optional — Ask AI's markdown renderer, see step 4
13
13
  ```
14
14
 
15
- Two entry points, and the split is a security boundary, not a preference:
15
+ Five entry points, and the split is a security boundary, not a preference:
16
16
 
17
17
  | Import | Runs in | Holds the API key |
18
18
  | --- | --- | --- |
19
19
  | `@cosmoops/connect-sdk/server` | your backend | **yes** |
20
20
  | `@cosmoops/connect-sdk/client` | the browser | never |
21
21
  | `@cosmoops/connect-sdk/next` | your backend | **yes** (Next.js plug-and-play) |
22
+ | `@cosmoops/connect-sdk/mcp` | your backend | **yes** (store, as MCP tools) |
22
23
  | `@cosmoops/connect-sdk` | anywhere | types + `ConnectError` only |
23
24
 
24
25
  The browser half never sees a credential. It calls your own origin, and
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchConnectScreen = matchConnectScreen;
4
+ exports.isLinkableConnectScreen = isLinkableConnectScreen;
5
+ /**
6
+ * Matching a live path against the screens your application declared.
7
+ *
8
+ * The same rule Connect uses for its own routes, and it has to be here too
9
+ * because only this side knows what your app's routes are: a dynamic segment
10
+ * matches exactly one path segment, and where several patterns fit, the most
11
+ * *literal* wins - so `/deals/new` resolves to itself rather than to
12
+ * `/deals/[dealId]` merely because that also fits.
13
+ *
14
+ * Plain functions with no React and no imports beyond the type, so the panel,
15
+ * the action links and any host that wants to reuse the matching can share one
16
+ * implementation.
17
+ */
18
+ /** Trims the query string, the hash and any trailing slash. */
19
+ function normalise(path) {
20
+ const withoutQuery = path.split(/[?#]/)[0] ?? "";
21
+ return withoutQuery.length > 1 ? withoutQuery.replace(/\/+$/, "") : withoutQuery;
22
+ }
23
+ /**
24
+ * The declared screen a live path is on, or `null` if none matches.
25
+ *
26
+ * `null` is a normal outcome, not an error: an application need only describe
27
+ * the screens it wants the assistant to know about, and an undescribed one
28
+ * simply costs the assistant its orientation rather than costing the user an
29
+ * answer.
30
+ */
31
+ function matchConnectScreen(screens, path) {
32
+ const segments = normalise(path).split("/").filter(Boolean);
33
+ let best = null;
34
+ for (const screen of screens) {
35
+ const pattern = normalise(screen.path).split("/").filter(Boolean);
36
+ if (pattern.length !== segments.length)
37
+ continue;
38
+ let literals = 0;
39
+ let matches = true;
40
+ for (let i = 0; i < pattern.length; i++) {
41
+ const part = pattern[i];
42
+ if (part.startsWith("[") && part.endsWith("]"))
43
+ continue;
44
+ if (part !== segments[i]) {
45
+ matches = false;
46
+ break;
47
+ }
48
+ literals++;
49
+ }
50
+ if (matches && (!best || literals > best.literals))
51
+ best = { screen, literals };
52
+ }
53
+ return best?.screen ?? null;
54
+ }
55
+ /**
56
+ * Whether an href the model produced may be rendered as a link.
57
+ *
58
+ * Same reasoning as Connect's own `showActions` check, and the same conclusion:
59
+ * an assistant that can emit an arbitrary URL is a redirect the model can be
60
+ * talked into, and retrieved content shares a context window with its
61
+ * tool-calling authority. So a link is drawn only when it resolves to a screen
62
+ * *your application declared* - which makes an external destination structurally
63
+ * impossible, since no route pattern has a scheme or a host.
64
+ *
65
+ * The protocol-relative form is refused separately: `//evil.example.com` has no
66
+ * scheme for a path matcher to trip over, and would otherwise reach an anchor
67
+ * as an absolute destination.
68
+ */
69
+ function isLinkableConnectScreen(screens, href) {
70
+ if (!href.startsWith("/") || href.startsWith("//"))
71
+ return false;
72
+ return matchConnectScreen(screens, href) !== null;
73
+ }
@@ -8,10 +8,12 @@ exports.AiTimeline = AiTimeline;
8
8
  exports.AiDetails = AiDetails;
9
9
  exports.AiSteps = AiSteps;
10
10
  exports.AiSources = AiSources;
11
+ exports.AiActions = AiActions;
11
12
  const jsx_runtime_1 = require("react/jsx-runtime");
12
13
  const react_1 = require("react");
13
14
  const react_2 = require("@heroui/react");
14
15
  const lucide_react_1 = require("lucide-react");
16
+ const ai_screens_js_1 = require("./ai-screens.js");
15
17
  const ai_format_js_1 = require("./ai-format.js");
16
18
  /**
17
19
  * What Ask AI draws.
@@ -230,3 +232,38 @@ function AiSources({ sources }) {
230
232
  return ((0, jsx_runtime_1.jsx)("li", { children: source.uri ? ((0, jsx_runtime_1.jsxs)("a", { href: source.uri, target: "_blank", rel: "noopener noreferrer", className: "flex gap-1.5 rounded-sm hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", children: [label, (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "mt-0.5 size-3 shrink-0 text-muted", "aria-hidden": true })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "flex gap-1.5", children: label })) }, `${source.uri ?? source.title}-${index}`));
231
233
  }) })] }));
232
234
  }
235
+ /**
236
+ * Where to go next, as links into *your* application.
237
+ *
238
+ * ## Why links and not navigation
239
+ *
240
+ * The obvious version of "the assistant can act" moves the user. It is also the
241
+ * version that breaks the property the whole design rests on: nothing happens
242
+ * without a click. Retrieved content shares a context window with the model's
243
+ * tool-calling authority, so an auto-navigate is a redirect primitive that
244
+ * content can reach. A link cannot be, and loses nothing - the user was going
245
+ * to click something anyway.
246
+ *
247
+ * ## Why every href is checked against your declared screens
248
+ *
249
+ * The model can emit any string. Only your application knows which strings are
250
+ * real routes, which is why `screens` is a prop rather than something Connect
251
+ * could supply. A link that does not resolve is *dropped* rather than rendered
252
+ * broken - the prose around it still stands, so a missing link reads as terse
253
+ * rather than wrong.
254
+ *
255
+ * `renderLink` is how this stays router-agnostic. Given one, a link is your
256
+ * framework's - a Next `<Link>`, a TanStack `<Link>` - and navigation is a
257
+ * client transition. Without one it falls back to an anchor, which works
258
+ * everywhere and costs a document load.
259
+ */
260
+ function AiActions({ title, actions, screens, renderLink, }) {
261
+ const resolved = actions.filter((action) => (0, ai_screens_js_1.isLinkableConnectScreen)(screens, action.href));
262
+ if (resolved.length === 0)
263
+ return null;
264
+ const className = "group flex items-center gap-2 rounded-lg border border-separator bg-surface px-3 py-2 no-underline transition-colors hover:border-accent";
265
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "my-3", children: [title ? (0, jsx_runtime_1.jsx)("p", { className: "mb-1.5 text-xs font-medium text-muted", children: title }) : null, (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-1.5", children: resolved.map((action) => {
266
+ const body = ((0, jsx_runtime_1.jsxs)("span", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("span", { className: "block truncate text-xs font-medium text-foreground", children: action.label }), action.detail ? ((0, jsx_runtime_1.jsx)("span", { className: "block truncate text-[11px] text-muted", children: action.detail })) : null] }));
267
+ return ((0, jsx_runtime_1.jsx)(react_1.Fragment, { children: renderLink ? (renderLink({ href: action.href, className, children: body })) : ((0, jsx_runtime_1.jsx)("a", { href: action.href, className: className, children: body })) }, action.href));
268
+ }) })] }));
269
+ }
@@ -8,6 +8,8 @@ const react_2 = require("@heroui/react");
8
8
  const lucide_react_1 = require("lucide-react");
9
9
  const streamdown_1 = require("streamdown");
10
10
  const provider_js_1 = require("../provider.js");
11
+ const use_active_path_js_1 = require("../use-active-path.js");
12
+ const ai_screens_js_1 = require("./ai-screens.js");
11
13
  const ai_widgets_js_1 = require("./ai-widgets.js");
12
14
  /**
13
15
  * The panel's open width. The *inner* pane always carries it, so the content
@@ -66,7 +68,7 @@ function ThinkingDisclosure({ text, isStreaming }) {
66
68
  */
67
69
  const isArray = (value) => Array.isArray(value) && value.length > 0;
68
70
  const field = (input, key) => typeof input === "object" && input !== null ? input[key] : undefined;
69
- function renderTool(name, input, renderMarkdown) {
71
+ function renderTool(name, input, renderMarkdown, screens, renderLink) {
70
72
  switch (name) {
71
73
  case "showTable":
72
74
  return isArray(field(input, "columns")) && Array.isArray(field(input, "rows")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTable, { ...input })) : null;
@@ -82,6 +84,12 @@ function renderTool(name, input, renderMarkdown) {
82
84
  return isArray(field(input, "steps")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSteps, { ...input, renderDetail: renderMarkdown })) : null;
83
85
  case "showSources":
84
86
  return isArray(field(input, "sources")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSources, { ...input }) : null;
87
+ case "showActions":
88
+ // Every href is checked against the declared screens before anything is
89
+ // drawn - see `AiActions`. With none declared, nothing resolves and the
90
+ // tool renders nothing, which is the correct outcome rather than a
91
+ // collection of dead links.
92
+ return isArray(field(input, "actions")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiActions, { ...input, screens: screens, renderLink: renderLink })) : null;
85
93
  default:
86
94
  return null;
87
95
  }
@@ -223,7 +231,7 @@ function appendText(blocks, text) {
223
231
  * Neither is something the browser can discover, so both surface as a message
224
232
  * in the panel rather than as a hidden button.
225
233
  */
226
- function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, }) {
234
+ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, screens = [], activePath, renderLink, }) {
227
235
  const { session, isLoading, streamResource } = (0, provider_js_1.useConnect)();
228
236
  const [isOpen, setIsOpen] = (0, react_1.useState)(defaultOpen);
229
237
  const [turns, setTurns] = (0, react_1.useState)([]);
@@ -242,6 +250,16 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
242
250
  // A conversation left mid-stream should stop costing tokens the moment the
243
251
  // panel goes away.
244
252
  (0, react_1.useEffect)(() => () => abortRef.current?.abort(), []);
253
+ /**
254
+ * The declared screen the user is on, if any.
255
+ *
256
+ * Drives the opening chips here and travels with each message so the answer
257
+ * is written from where they are standing. `null` whenever nothing matches -
258
+ * an application need only describe the screens it wants the assistant to
259
+ * know about.
260
+ */
261
+ const resolvedPath = (0, use_active_path_js_1.useActivePath)(activePath);
262
+ const currentScreen = (0, react_1.useMemo)(() => (resolvedPath ? (0, ai_screens_js_1.matchConnectScreen)(screens, resolvedPath) : null), [screens, resolvedPath]);
245
263
  const renderMarkdown = (0, react_1.useCallback)((text, isStreaming = false) => ((0, jsx_runtime_1.jsx)(streamdown_1.Streamdown, { className: "connect-ai-body", mode: isStreaming ? "streaming" : "static", isAnimating: isStreaming, animated: ANIMATED, caret: isStreaming ? "block" : undefined, plugins: markdownPlugins,
246
264
  // Retrieved documents cite real URLs; an interstitial on every one of
247
265
  // them turns a citation into a chore. The renderer still sanitises the
@@ -286,7 +304,15 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
286
304
  try {
287
305
  // Relative to the provider's own `basePath` - `streamResource` prefixes
288
306
  // it, the same way every other bridge call is addressed.
289
- const response = await streamResource("/ai/chat", { messages: history }, { signal: controller.signal });
307
+ const response = await streamResource("/ai/chat", {
308
+ messages: history,
309
+ // What the assistant is told about where the user is, and what your
310
+ // application's screens are. Both are prose: Connect uses them to
311
+ // write a better answer and to validate action links, never to
312
+ // decide what anyone may do. See `ConnectScreen`.
313
+ ...(resolvedPath ? { screenContext: { pathname: resolvedPath } } : {}),
314
+ ...(screens.length > 0 ? { screens } : {}),
315
+ }, { signal: controller.signal });
290
316
  // The assistant's turn is appended only once the stream is live, so a
291
317
  // failed request leaves an error under the question rather than an
292
318
  // empty bubble that never fills.
@@ -333,7 +359,7 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
333
359
  // The user's own turn is shown as they typed it - running a
334
360
  // question that happens to start with `#` through the markdown
335
361
  // renderer would turn it into a heading.
336
- (0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
362
+ (0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown, screens, renderLink) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
337
363
  }), [turns, isAnswering, lastTurn?.id, assistantName, renderMarkdown]);
338
364
  // Signed out there is no workspace to ground an answer in, and the bridge
339
365
  // would answer 401 - so there is nothing to offer, not even a button.
@@ -347,7 +373,7 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
347
373
  setInput("");
348
374
  void send(text);
349
375
  };
350
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${panelShellWidth(isOpen)} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width]`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
376
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${panelShellWidth(isOpen)} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width]`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "space-y-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), currentScreen?.suggestions?.length ? ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-1.5", children: currentScreen.suggestions.map((suggestion) => ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "secondary", className: "h-auto rounded-full px-2.5 py-1 text-start text-xs whitespace-normal", isDisabled: isBusy, onPress: () => void send(suggestion), children: suggestion }, suggestion))) })) : null] })), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
351
377
  }
352
378
  /** Hoisted for the same reference-equality reason as `ANIMATED` - Streamdown compares it by identity. */
353
379
  const LINK_SAFETY = { enabled: false };
@@ -2,8 +2,8 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useActivePath = exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
5
- exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = void 0;
6
- exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = void 0;
5
+ exports.annualSaving = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = void 0;
6
+ exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.ACTION_LABEL = exports.discountLabel = void 0;
7
7
  /**
8
8
  * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
9
9
  *
@@ -114,6 +114,9 @@ Object.defineProperty(exports, "resolveActivePath", { enumerable: true, get: fun
114
114
  Object.defineProperty(exports, "isNavItemActive", { enumerable: true, get: function () { return nav_js_1.isNavItemActive; } });
115
115
  Object.defineProperty(exports, "gateExemptPaths", { enumerable: true, get: function () { return nav_js_1.gateExemptPaths; } });
116
116
  Object.defineProperty(exports, "filterNavForAudience", { enumerable: true, get: function () { return nav_js_1.filterNavForAudience; } });
117
+ var ai_screens_js_1 = require("./components/ai-screens.js");
118
+ Object.defineProperty(exports, "matchConnectScreen", { enumerable: true, get: function () { return ai_screens_js_1.matchConnectScreen; } });
119
+ Object.defineProperty(exports, "isLinkableConnectScreen", { enumerable: true, get: function () { return ai_screens_js_1.isLinkableConnectScreen; } });
117
120
  var apps_grid_js_1 = require("./components/apps-grid.js");
118
121
  Object.defineProperty(exports, "ConnectAppsGrid", { enumerable: true, get: function () { return apps_grid_js_1.ConnectAppsGrid; } });
119
122
  // `AppShell askAi` mounts this; exported for an app that wants the panel
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = exports.serverTimestamp = exports.increment = exports.documentExists = exports.deleteField = exports.arrayUnion = exports.arrayRemove = exports.normalizePlanFeatures = exports.blockingBlockers = exports.isConnectError = exports.ConnectError = void 0;
3
+ exports.storeIndexCovers = exports.describeQueryRejection = exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = exports.serverTimestamp = exports.increment = exports.documentExists = exports.deleteField = exports.arrayUnion = exports.arrayRemove = exports.normalizePlanFeatures = exports.blockingBlockers = exports.isConnectError = exports.ConnectError = void 0;
4
4
  /**
5
5
  * `@cosmoops/connect-sdk` - the official client for the CosmoOps Connect
6
6
  * **application-partner API** (`/api/v1/application/*`).
@@ -13,6 +13,8 @@ exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = e
13
13
  * Holds the API key. Never import this from browser code.
14
14
  * - `@cosmoops/connect-sdk/client` - React components and hooks. Talks to your
15
15
  * own origin only.
16
+ * - `@cosmoops/connect-sdk/mcp` - your store's declaration, derived into MCP
17
+ * tools. Holds the API key; never import this from browser code.
16
18
  */
17
19
  var errors_js_1 = require("./errors.js");
18
20
  Object.defineProperty(exports, "ConnectError", { enumerable: true, get: function () { return errors_js_1.ConnectError; } });
@@ -30,3 +32,6 @@ Object.defineProperty(exports, "serverTimestamp", { enumerable: true, get: funct
30
32
  Object.defineProperty(exports, "storeIndexFor", { enumerable: true, get: function () { return store_js_1.storeIndexFor; } });
31
33
  Object.defineProperty(exports, "storeIndexSignature", { enumerable: true, get: function () { return store_js_1.storeIndexSignature; } });
32
34
  Object.defineProperty(exports, "unchangedSince", { enumerable: true, get: function () { return store_js_1.unchangedSince; } });
35
+ var store_schema_js_1 = require("./store-schema.js");
36
+ Object.defineProperty(exports, "describeQueryRejection", { enumerable: true, get: function () { return store_schema_js_1.describeQueryRejection; } });
37
+ Object.defineProperty(exports, "storeIndexCovers", { enumerable: true, get: function () { return store_schema_js_1.storeIndexCovers; } });
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveStoreTools = void 0;
4
+ exports.createStoreToolset = createStoreToolset;
5
+ /**
6
+ * `@cosmoops/connect-sdk/mcp` - your application's store, as an MCP server.
7
+ *
8
+ * Server-only: every call goes out through `ConnectClient`, which holds the API
9
+ * key. The `browser` export condition resolves this entry to a stub, so an
10
+ * import from client code fails the build rather than shipping the key.
11
+ *
12
+ * ## Zero dependencies, on purpose
13
+ *
14
+ * This entry does not import `@modelcontextprotocol/sdk`, and the package takes
15
+ * no dependency on it. `listTools()` and `callTool()` answer in the shapes the
16
+ * MCP protocol defines, so wiring them to the official server is four lines and
17
+ * a transport you choose:
18
+ *
19
+ * ```ts
20
+ * const toolset = createStoreToolset({
21
+ * collections: STORE_SCHEMA,
22
+ * data: connect.data,
23
+ * workspaceId: session.workspaceId,
24
+ * asUser: session.sessionToken,
25
+ * })
26
+ *
27
+ * server.setRequestHandler(ListToolsRequestSchema, () => toolset.listTools())
28
+ * server.setRequestHandler(CallToolRequestSchema, (request) =>
29
+ * toolset.callTool(request.params.name, request.params.arguments ?? {}),
30
+ * )
31
+ * ```
32
+ *
33
+ * The alternative - depending on the MCP SDK so this file could return an
34
+ * `McpServer` - would put a version of it in every install of this package,
35
+ * including the ones that never touch MCP, and pin partners to whichever
36
+ * version shipped here. The protocol shapes are stable and small; the coupling
37
+ * would not be.
38
+ *
39
+ * ## One toolset per session
40
+ *
41
+ * `asUser` is an end user's Connect session token, and Connect checks each
42
+ * collection's declared permission against that member's live roles. A toolset
43
+ * is therefore scoped to one signed-in user - build it per request from
44
+ * `getConnectSession()`, or pass a function for `asUser` if the surrounding
45
+ * server outlives a session.
46
+ */
47
+ var tools_js_1 = require("./tools.js");
48
+ Object.defineProperty(exports, "deriveStoreTools", { enumerable: true, get: function () { return tools_js_1.deriveStoreTools; } });
49
+ const tools_js_2 = require("./tools.js");
50
+ /**
51
+ * A refusal is a result, not a thrown error.
52
+ *
53
+ * MCP draws that distinction deliberately: a protocol error is a fault the
54
+ * model cannot act on, while `isError: true` is a tool result the model reads
55
+ * and can respond to. An unservable query and a permission Connect refused are
56
+ * both things a model should see and adapt to - naming a field it may not
57
+ * filter on, or telling the user it cannot reach that data - so they come back
58
+ * as content rather than as a transport failure.
59
+ */
60
+ function asResult(text, isError = false) {
61
+ return { content: [{ type: "text", text }], isError };
62
+ }
63
+ function createStoreToolset(options) {
64
+ const tools = (0, tools_js_2.deriveStoreTools)(options);
65
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
66
+ return {
67
+ tools,
68
+ listTools: () => ({
69
+ tools: tools.map((tool) => ({
70
+ name: tool.name,
71
+ title: tool.title,
72
+ description: tool.description,
73
+ inputSchema: tool.inputSchema,
74
+ annotations: tool.annotations,
75
+ })),
76
+ }),
77
+ async callTool(name, args) {
78
+ const tool = byName.get(name);
79
+ if (!tool) {
80
+ return asResult(`No such tool: "${name}". Available: ${[...byName.keys()].join(", ")}.`, true);
81
+ }
82
+ try {
83
+ return asResult(JSON.stringify(await tool.execute(args), null, 2));
84
+ }
85
+ catch (error) {
86
+ return asResult(error instanceof Error ? error.message : String(error), true);
87
+ }
88
+ },
89
+ };
90
+ }