@swisper.ai/cards-sdk-client
v0.0.3
Published
Readme
@swisper.ai/cards-sdk-client
Define <swi-*> cards, and their lifecycle.
The host app supplies the message renderer and a CardProvider that connects cards to the conversation.
Boundary
Code belongs here if it defines a <swi-*> card or connects cards to a host: the card() call, the
hooks a card uses, the action wire format, the canvas, and the providers a host mounts. Cards
themselves, their styles, and the host's message renderer belong in the app or bundle that ships
them.
Install
npm install @swisper.ai/cards-sdk-client| Peer | Range | Why |
| ----------- | -------- | -------------------- |
| react | 18.3.1 | Components and hooks |
| react-dom | 18.3.1 | Rendering |
| zod | ^4.2.0 | Card schemas |
@module-federation/runtime and the @modelcontextprotocol/* packages are regular dependencies and
install with the package. Only the /host, /mcp-apps-host and /mcp-apps-sandbox entry points
import them.
Setup
- Define a card once, at module scope:
import { card, type SwiCardComponent } from '@swisper.ai/cards-sdk-client';
import { z } from 'zod/v4';
const weatherSchema = z.object({ city: z.string(), degrees: z.number() });
const Weather: SwiCardComponent<z.infer<typeof weatherSchema>> = ({ data }) => (
<p>
{data.city}: {data.degrees}°
</p>
);
export const SwiWeatherCard = card({
name: 'swi-weather',
schema: weatherSchema,
Component: Weather,
});- Hand the host's cards to
CardsProvider. Pass a module-level array, souseCards()stays stable:
import { CardsProvider } from '@swisper.ai/cards-sdk-client';
const cards = [SwiWeatherCard];
<CardsProvider cards={cards}>
<Chat />
</CardsProvider>;- In the message renderer, mount the cards from
useCards()and wrap each one inCardProvider, which supplies whatuseCard()returns for that card's message.
API
Defining cards
| Export | Why it exists |
| ------------------ | ------------------------------------------------------------------------------------------------- |
| card | Turns a name, schema and component into a card the renderer can match, parse, validate and mount. |
| DefaultCardError | The fallback shown when a card fails, without leaking the payload onto the screen. |
A card also exposes Renderer (your component, for tests and stories) and parse(raw) (the same
parse-and-validate path, for showing a card's data outside the message list; it never throws).
Inside a card
| Export | Why it exists |
| ---------------------- | --------------------------------------------------------------------------------- |
| useCard | What the host lets a card do: send actions, build tags, open the canvas. |
| EditableCardProvider | Shares one card's edit mode with every part of that card. |
| useEditableCard | Reads that shared edit mode. |
| useEditableCardState | Edit mode for a single component; every transition is a no-op while not editable. |
Actions
| Export | Why it exists |
| --------------------------- | --------------------------------------------------------------------------- |
| cardAction | Writes an action as a swisper:// markdown link, the whole wire format. |
| encodeCards | Joins several tags so the renderer doesn't read them as one card. |
| extractCardActionFromLink | Reads the first action link back out of message text. |
| isCardActionURL | Tells the host's markdown renderer which links to render as buttons. |
| useCardActions | Turns a list of links and plain replies into actions a card can render. |
| CARD_ACTION_PROTOCOL | The swisper scheme, for allow-listing it in the host's markdown renderer. |
Host
| Export | Why it exists |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| CardProvider | Wraps each card with what useCard() returns for that card's message. |
| CardsProvider | Registers the host's own cards. |
| useCards | Every available card, remote first then local; throws if two claim the same tag. |
| RemoteCardsProvider | Loads cards from a separately deployed module-federation bundle and exposes its load state. |
| useRemoteCards | Reads the remote bundle's load state; throws outside RemoteCardsProvider. |
| usePossibleRemoteCards | The same, but null outside the provider, for code that works with or without a remote. |
Canvas
| Export | Why it exists |
| -------------------- | ---------------------------------------------------------------------------------------------- |
| CanvasProvider | Owns the panel outside the chat that cards open: its stack of contents, width and full screen. |
| CanvasContent | Renders a card's content into the canvas while its id is stacked, keeping its state. |
| useCanvasContext | Opens, closes and resizes the canvas. |
| useCanvasLifecycle | Tells a component when the canvas starts and stops showing its id. |
| useCanvasResize | Turns an element into an accessible resize handle (drag, arrow keys, double-click to reset). |
Types for all of the above (SwiCard, SwiCardComponent, SwiCardName, SwiCardAction,
SwiCardErrorProps and the rest) are exported alongside them.
Troubleshooting
| Symptom | Cause | Fix |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| useCard() called outside CardProvider | The card isn't wrapped in CardProvider, or the host and a remote bundle each loaded their own copy | Wrap each card the message renderer mounts in CardProvider. With module federation, list this package as a shared singleton in both build configs. |
| useCanvasContext() called outside CanvasProvider | Same as above, for the canvas | Mount CanvasProvider in the host, and share this package as a singleton. |
| The card shows "Something went wrong." | The tag body didn't parse or match the schema, or the component threw | Check the console for could not parse its body or threw while rendering; the element's data-swi-card-error says which. |
| JSON tag body fails to parse | The tag has no data-format, so the body is read as CSV | Add data-format="json" to the tag. |
| Type error: "weather" is not assignable to swi-${string} | Card names must start with swi- | Rename the card, e.g. swi-weather. |
| "swi-…" is provided more than once. | The remote bundle and CardsProvider both define that tag | Remove one of them, or rename it. |
| did not default-export an array | The remote's exposed module isn't the card list | Default-export an array of card() results from the module named by exposedModule (default ./registry). |
| bundleUrl is not set | RemoteCardsProvider got an empty bundleUrl | Pass the URL of the remote's remoteEntry.js. |
| Action links render as plain text, or are stripped | The host's markdown renderer drops unknown URL schemes | Allow swisper: in its URL transform and render links where isCardActionURL(href) is true as buttons. |
| Two cards sent in one action render as one broken card | Back-to-back tags were concatenated | Join them with encodeCards([first, second]). |
| Console warns Card is not editable | startEdit, discard or toggleEdit was called while isEditable is false | Pass isEditable={useCard().isActionEnabled}; it is false on older messages by design. |
| CanvasContent never appears | CanvasContent doesn't open the canvas, or the host never attached containerRef | Call openInCanvas(id) (or useCanvasContext().onOpen(id)), and attach containerRef to the canvas element. |
| "Invalid hook call", or a context is missing | The app resolves two copies of React | Add resolve: { dedupe: ['react', 'react-dom'] } to the Vite config |
