# Model Context Playground

A React component for interacting with Model Context Servers within a chat interface.

> Demo: [https://model-context-playground.com](https://model-context-playground.com)

![GIF of Model Context Playground Demo](https://gitlab.com/ben_goodman/libraries/model-context-playground/-/raw/main/docs/demo.gif?ref_type=heads)

## Features

- Chat interface for interacting with language models
- Call MCP tools directly
- OpenAI support
- SSE streaming support with live tool-call events
- Support for file attachments (images, PDFs, etc.)
- Slash command palette for cached MCP prompts
- Import/export browser settings as JSON backups

## Installation

```bash
npm install model-context-playground
```

## Usage

```tsx
import React from 'react'
import ReactDOM from 'react-dom/client'

import 'model-context-playground/styles.css'
import { ModelContextPlayground } from 'model-context-playground'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ModelContextPlayground />
  </React.StrictMode>,
)
```

## Options

`ModelContextPlayground` accepts the following props:

### `className?: string`

Optional utility class string appended to the root container. Useful for sizing or theming the playground within your app layout.

### `options?: ModelContextPlaygroundOptions`

Container for optional configuration objects. All values are merged with the data persisted in local storage, with the values you pass via props always taking priority.

- **`initialMcpServers`** – Array of MCP server definitions. Each entry is a `McpServerEntry` (`{ id, options }`). Entries are normalized and merged with any previously stored servers:
  - Servers are matched by trimmed `url` when available; matching entries are updated, new ones are appended.
  - Provide `serverName`, `url`, and any optional `toolPreferences`, `serverEnabled`, or other connection metadata supported by the playground.
- **`initialLanguageModels`** – Array of `StoredModel` objects that seed the language-model catalog. Models are merged by `id` (or a generated id based on provider/model if `id` is omitted). When a stored model already exists, the prop value overwrites persisted data.

Both collections are persisted after the initial merge, so subsequent renders do not duplicate entries.

```ts
type McpServerEntry = {
  id: string
  options: StoredMcpServer
}

type StoredMcpServer = {
  url: string
  autoFetch?: boolean
  headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>)
  fetchTimeoutMs?: number
  serverName?: string
  serverEnabled?: boolean
  toolPreferences?: Record<string, boolean>
}

type StoredModel = {
  id: string
  provider: 'openai' | 'anthropic'
  model: string
  label: string
  description?: string
  baseUrl?: string
  settings?: Record<string, unknown>
  connection?: Record<string, string | number | boolean>
  disabled?: boolean
}
```

```tsx
<ModelContextPlayground
  options={{
    initialMcpServers: [
      {
        id: 'lorem-mcp',
        options: {
          serverName: 'Lorem MCP',
          url: 'https://lorem-api.com/api/mcp',
        }
      },
      {
        id: 'gastro-genie-mcp',
        options: {
          serverName: 'Gastro Genie MCP',
          url: 'https://mcp.gastro-genie.com',
        }
      }
    ],
    initialLanguageModels: [
      {
        id: 'openai-gpt-5',
        provider: 'openai',
        model: 'gpt-5',
        label: 'GPT-5',
        settings: {
          maxTokens: 2048,
          reasoningEffort: 'low',
          textVerbosity: 'low'
        }
      }
    ]
  }}
/>
```

### `theme?: ThemeTokens`

Provide a complete theme object to replace the default styling tokens. Each token is a Tailwind-friendly class string that the playground applies to specific UI elements (chat header, cards, buttons, etc.). Passing `theme` lets you fully control the visual language without touching component code.

### `themeOverrides?: ThemeOverrides`

Pass a deep-partial theme object to merge with the default theme. This is the quickest way to tweak just a few sections (for example, swapping gradients or badge colors) while inheriting the remaining defaults.

## File Attachments

The playground supports attaching files to chat messages. Users can upload images, PDFs, and other file types. Attachments are displayed inline within the chat history.

When a file is attached, the playground invokes an `attachmentCallback` function (defined in chat settings) with a `ChatAttachment` object containing metadata about the file. You can customize this callback to upload files to your server or a third-party service and return a `sourceUrl` that will be used to retrieve the attachment.

The `ChatAttachment` object has the following structure:

```json
{
  "dataUrl": "data:application/pdf;base64,JVBERi0xLjcK...",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "mimeType": "application/pdf",
  "name": "example.pdf",
  "size": 123456,
}
```

You can use the `dataUrl` for previewing the file immediately. If you upload the file and obtain a `sourceUrl`, return it from the callback:

```ts
async (attachment) => {
  const resp = await fetch('https://cache.files.com/api', {
    method: 'POST',
    headers: {
      'x-api-key': 'foo-bar-baz'
    },
    body: JSON.stringify(attachment)
  })

  const data = await resp.json()

  return { sourceUrl: data.url }
}
```

## Slash Commands

Typing `/` at the beginning of the chat input opens a quick command palette populated with every cached MCP prompt across your configured servers. Use the arrow keys to browse or keep typing to filter the list by prompt name, description, or server label. Press `Enter` (or click) to select a prompt—Model Context Playground now represents the selection as a pill that sits above the message composer.

Each pill shows the prompt name, the MCP server it came from, and its description. If the prompt defines arguments, they render as inline inputs inside the pill (required fields are flagged). Fill in whatever values you need; missing required fields are highlighted the moment you try to submit.

Need to back out? Click the × button on the pill or hit backspace while the Composer is empty to remove the slash command entirely. You can continue typing free-form text beneath the pill—the final message automatically prepends a slash-command line (for example `/summarize topic="market" tone="casual"`) before any additional text you enter, so the prompt context is preserved when the request is sent.

If no prompts are cached yet, the palette gently reminds you to refresh or connect a server before you can use slash commands.

## Theming

The library exports a small theming toolkit:

- `defaultTheme` – the out-of-the-box look
- `darkTheme` – (WIP) a dark, high-contrast palette
- `systemTheme` – switches between light/dark based on OS preference
- `createTheme(overrides)` – deep merges your overrides with the default theme and returns a brand-new theme object
- `ThemeProvider` – low-level provider if you embed individual playground components
- `ThemeTokens` / `ThemeOverrides` – TypeScript types describing every available token

```tsx
import { ModelContextPlayground, createTheme, darkTheme } from 'model-context-playground'

export function DarkApp() {
  return <ModelContextPlayground theme={darkTheme} />
}

const midnightTheme = createTheme({
  layout: {
    appRoot: 'flex h-full flex-col bg-slate-950 font-sans text-slate-100'
  },
  chatHeader: {
    container: 'fixed top-0 left-0 right-0 z-50 flex items-center justify-between gap-4 bg-slate-900/80 px-4 py-2 backdrop-blur'
  },
  message: {
    cardContainer: 'space-y-3 rounded-2xl border border-slate-700 bg-slate-900/60 p-3 text-xs text-slate-100 shadow-lg ring-1 ring-slate-800/60'
  },
  toolCallCard: {
    error: {
      container: 'space-y-3 rounded-2xl border border-rose-700 bg-rose-900/40 text-sm text-rose-100 ring-1 ring-rose-800/60'
    }
  }
})

export function App() {
  return <ModelContextPlayground theme={midnightTheme} />
}
```

For lighter edits you can rely on `themeOverrides` without building a full theme:

```tsx
<ModelContextPlayground
  themeOverrides={{
    chatDialog: {
      inputCard: 'pointer-events-auto flex w-full max-w-3xl flex-col gap-3 rounded-2xl border border-emerald-500/30 bg-emerald-950/40 p-3 shadow-lg'
    },
    chatInput: {
      submitButton: 'inline-flex items-center rounded-full bg-emerald-500 px-4 py-2 text-sm font-semibold text-emerald-50 shadow transition enabled:hover:bg-emerald-400'
    }
  }}
/>
```

Refer to `ThemeTokens` for the complete list of customizable areas. Every token maps back to the exact UI element now used throughout the playground, making it straightforward to swap palettes or build brand-aligned skins.

## Development

To run the project locally:

```bash
# clone the project
git clone https://gitlab.com/ben_goodman/libraries/model-context-playground.git

# then, in the project root:
npm install
npm start
```

## Managing Saved Settings

Model Context Playground stores chat sessions, MCP server configurations, model catalogs, and theme preferences in `localStorage`. You can back up or restore these settings without inspecting the browser manually:

- **Export settings** – Within the playground UI open **Settings → System** and click **Export settings**. A timestamped JSON file downloads containing the keys:
  - `chat-attachment-callback-source`
  - `chat-session-main-session`
  - `chat-session-main-session-attachments`
  - `llm-models`
  - `mcp-server-options`
  - `dashboard:theme-preference`
- **Import settings** – Use the matching **Import settings** button and select a JSON export. The playground overwrites the stored values for the keys above, so reload the page afterward to ensure the UI reflects the latest configuration.


## License

MIT © [Benjamin Goodman](https://ben.website)
