Quickstart
@gram-ai/elements
Elements is a library built for the agentic age. We provide customizable and elegant UI primitives for building chat-like experiences for MCP Servers. Works best with Gram MCP, but also supports connecting any MCP Server.
Setup
Frontend Setup
The easiest way to install Elements and all its peer dependencies:
npx @gram-ai/elements installThis will automatically detect your package manager and install everything you need.
Manual installation
If you prefer to install manually, first install the peer dependencies:
pnpm add react react-dom motion remark-gfm zustand vega shikiThen install Elements:
pnpm add @gram-ai/elementsBackend Setup
If you’re only using the server handlers (@gram-ai/elements/server), you can install without needing to install any of the React peer dependencies:
pnpm add @gram-ai/elements/server exports, as React dependencies are marked as optional.Setting up your backend
We provide framework-specific adapters to make it easy to set up a backend endpoint for Gram Elements. These adapters handle the session token creation securely on your server without exposing your GRAM_API_KEY to the browser.
Express
import { createExpressHandler } from '@gram-ai/elements/server/express'
import express from 'express'
const app = express()
app.use(express.json())
app.post('/chat/session', createExpressHandler({
embedOrigin: 'http://localhost:3000',
userIdentifier: 'user-123',
expiresAfter: 3600, // optional, max 3600 seconds
}))
app.listen(3000)For dynamic options based on the request:
app.post('/chat/session', createExpressHandler((req) => ({
embedOrigin: req.headers.origin || 'http://localhost:3000',
userIdentifier: req.user?.id || 'anonymous',
expiresAfter: 3600,
})))Next.js App Router
// app/chat/session/route.ts
import { createNextHandler } from '@gram-ai/elements/server/nextjs'
export const POST = createNextHandler({
embedOrigin: 'http://localhost:3000',
userIdentifier: 'user-123',
expiresAfter: 3600,
})
import { cookies } from 'next/headers'
export const POST = createNextHandler(async (request) => {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value || 'anonymous'
return {
embedOrigin: request.headers.get('origin') || 'http://localhost:3000',
userIdentifier: userId,
expiresAfter: 3600,
}
})Fastify
import { createFastifyHandler } from '@gram-ai/elements/server/fastify'
import Fastify from 'fastify'
const fastify = Fastify()
fastify.post('/chat/session', createFastifyHandler({
embedOrigin: 'http://localhost:3000',
userIdentifier: 'user-123',
expiresAfter: 3600,
}))
fastify.listen({ port: 3000 })Hono
import { createHonoHandler } from '@gram-ai/elements/server/hono'
import { Hono } from 'hono'
const app = new Hono()
app.post('/chat/session', createHonoHandler({
embedOrigin: 'http://localhost:3000',
userIdentifier: 'user-123',
expiresAfter: 3600,
}))
export default appEnvironment Variables
All adapters require the GRAM_API_KEY environment variable to be set:
GRAM_API_KEY=xxxThis key is kept on your server and never exposed to the browser, ensuring secure communication with Gram’s API.
Setting up your frontend
@gram-ai/elements requires that you wrap your React tree with our context provider:
import { GramElementsProvider, Chat, type ElementsConfig } from '@gram-ai/elements'
// Please fill out projectSlug and mcp
const config: ElementsConfig = {
projectSlug: 'xxx',
mcp: 'https://app.getgram.ai/mcp/{your_slug}',
variant: 'widget',
welcome: {
title: 'Hello!',
subtitle: 'How can I help you today?',
},
composer: {
placeholder: 'Ask me anything...',
},
modal: {
defaultOpen: true,
},
}
export const App = () => {
return (
<GramElementsProvider config={config}>
<Chat />
</GramElementsProvider>
)
}Custom Session Endpoint
By default, Elements expects the session endpoint on your backend to be located at /chat/session. If you’ve mounted your session endpoint at a different path, you can provide a custom getSession function in the ElementsProvider:
import { GramElementsProvider, Chat, type ElementsConfig, type GetSessionFn } from '@gram-ai/elements'
const config: ElementsConfig = {
projectSlug: 'xxx',
mcp: 'https://app.getgram.ai/mcp/{your_slug}',
// ... other config
}
const customGetSession: GetSessionFn = async () => {
const response = await fetch('/api/custom-session-endpoint', {
method: 'POST',
})
const data = await response.json()
return data.client_token
}
export const App = () => {
return (
<GramElementsProvider config={config} getSession={customGetSession}>
<Chat />
</GramElementsProvider>
)
}Configuration
For complete configuration options and TypeScript type definitions, see the API documentation.
Quick Configuration Example
import { GramElementsProvider, Chat, type ElementsConfig } from '@gram-ai/elements'
const config: ElementsConfig = {
projectSlug: 'your-project',
mcp: 'https://app.getgram.ai/mcp/your-mcp-slug',
variant: 'widget', // 'widget' | 'sidecar' | 'standalone'
welcome: {
title: 'Hello!',
subtitle: 'How can I help you today?',
},
}
export const App = () => {
return (
<GramElementsProvider config={config}>
<Chat />
</GramElementsProvider>
)
}Plugins
Plugins extend the Gram Elements library with custom rendering capabilities for specific content types. They allow you to transform markdown code blocks into rich, interactive visualizations and components.
How Plugins Work
When you add a plugin:
- The plugin extends the system prompt with instructions for the LLM
- The LLM returns code blocks marked with the plugin’s language identifier
- The plugin’s custom component renders the code block content
For example, the built-in chart plugin instructs the LLM to return Vega specifications for visualizations, which are then rendered as interactive charts.
Using Recommended Plugins
Gram Elements includes a set of recommended plugins that you can use out of the box:
import { GramElementsProvider, Chat, type ElementsConfig } from '@gram-ai/elements'
import { recommended } from '@gram-ai/elements/plugins'
const config: ElementsConfig = {
projectSlug: 'my-project',
mcp: 'https://app.getgram.ai/mcp/my-mcp-slug',
welcome: {
title: 'Hello!',
subtitle: 'How can I help you today?',
},
// Add all recommended plugins
plugins: recommended,
}
export const App = () => {
return (
<GramElementsProvider config={config}>
<Chat />
</GramElementsProvider>
)
}Available Recommended Plugins
chart- Renders Vega chart specifications as interactive visualizations
Using Individual Plugins
You can also import and use plugins individually:
import { chart } from '@gram-ai/elements/plugins'
const config: ElementsConfig = {
// ... other config
plugins: [chart],
}Using Custom Plugins
You can create your own custom plugins to add specialized rendering capabilities:
import { GramElementsProvider, Chat, type ElementsConfig } from '@gram-ai/elements'
import { chart } from '@gram-ai/elements/plugins'
import { myCustomPlugin } from './plugins/myCustomPlugin'
const config: ElementsConfig = {
projectSlug: 'my-project',
mcp: 'https://app.getgram.ai/mcp/my-mcp-slug',
welcome: {
title: 'Hello!',
subtitle: 'How can I help you today?',
},
// Combine built-in and custom plugins
plugins: [chart, myCustomPlugin],
}
export const App = () => {
return (
<GramElementsProvider config={config}>
<Chat />
</GramElementsProvider>
)
}Creating Custom Plugins
To create your own plugins, see the comprehensive Plugin Development Guide. The guide covers:
- Plugin architecture and interface
- Step-by-step tutorial for creating plugins
- Best practices and common patterns
- Real-world examples
- Troubleshooting tips
Replay
Replay lets you play back a pre-recorded conversation with streaming animations — no auth, MCP, or network calls required. This is useful for marketing demos, documentation, and testing.
Recording a cassette
A cassette is a JSON file that captures a conversation. To record one from a live chat session, enable the built-in recorder by setting this environment variable:
VITE_ELEMENTS_ENABLE_CASSETTE_RECORDING=trueThis adds a record button to the composer. Click it to open the recorder popover, then start recording. Chat normally, and when you’re done, click Stop & Download to save the cassette as a .cassette.json file.
You can also record programmatically using the useRecordCassette hook:
import {
GramElementsProvider,
Chat,
useRecordCassette,
} from '@gram-ai/elements'
function RecordableChat() {
const { isRecording, startRecording, download } = useRecordCassette()
return (
<>
<Chat />
<button onClick={startRecording}>Record</button>
<button onClick={() => download('demo')}>Save</button>
</>
)
}Playing back a cassette
Use the <Replay> component, which replaces GramElementsProvider entirely:
import { Replay, Chat } from '@gram-ai/elements'
import cassette from './demo.cassette.json'
function MarketingDemo() {
return (
<Replay cassette={cassette} config={{ variant: 'standalone' }}>
<Chat />
</Replay>
)
}<Replay> accepts optional timing props to control the playback speed:
| Prop | Default | Description |
|---|---|---|
typingSpeed | 15 | Milliseconds per character for streaming |
userMessageDelay | 800 | Milliseconds before each user message appears |
assistantStartDelay | 400 | Milliseconds before the assistant starts typing |
onComplete | — | Callback when replay finishes |
React Compatibility
@gram-ai/elements supports React 16.8+, React 17, React 18, and React 19.
React 18 and 19 work out of the box. For React 16 or 17, you need to configure your bundler to shim newer React APIs (useSyncExternalStore, useId, useInsertionEffect) that are used by transitive dependencies like zustand and @assistant-ui/react.
Vite Setup (React 16/17)
Add the compatibility plugin to your Vite config:
import react from '@vitejs/plugin-react'
import { reactCompat } from '@gram-ai/elements/compat'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react(), reactCompat()],
})This automatically shims React 18 APIs (useSyncExternalStore, useId, useInsertionEffect) so that Elements and its dependencies work correctly on older React versions.
Contributing
We welcome pull requests to Elements. Please read the contributing guide.
Last updated on