A lightweight, local-first chat client that connects to MCP servers and streams responses from OpenAI or Ollama models. Single-user, no cloud backend — conversations persist in localStorage, OAuth tokens live in-memory on the server.
MCP tools can render interactive UI widgets inside sandboxed iframes. The iframe protocol is compatible with the ChatGPT Apps SDK.
# Install dependencies (npm workspaces)
npm install
# Create your config file
cp config.example.yaml config.yamlEdit config.yaml to add at least one LLM provider:
llm:
openai:
api_key: "sk-..."
default_model: "gpt-4o"
ollama:
base_url: "http://localhost:11434" # defaultOptionally add MCP servers under mcp_servers: — see config.example.yaml for stdio, HTTP, and OAuth examples.
Start both the Express server (port 3000) and the Vite dev server (port 5173):
# In separate terminals:
npm run dev:server
npm run dev:clientThen open http://localhost:5173. The Vite dev server proxies /api/* requests to Express.
npm run buildThis compiles the server with tsc and builds the client with Vite. In production, Express serves the Vite build as static files.
npm testServer tests use Node's built-in test runner; client tests use Vitest.
npm run lint # TypeScript type-checking (both workspaces)
npm run typecheck # Same as lint| Layer | Tech |
|---|---|
| Server | Node.js + Express 4 + TypeScript |
| Client | React 19 + Vite 6 + Tailwind CSS 4 |
| LLM streaming | Vercel AI SDK (ai v4) |
| MCP client | @modelcontextprotocol/sdk v1 |
| Config | config.yaml (single file, gitignored) |
Directory structure:
server/ Express backend — chat streaming, MCP client management, OAuth
client/ React frontend — chat UI, conversation management, iframe widgets
shared/ Shared TypeScript types
specs/ Feature specs and architecture docs
config.yaml Runtime config (gitignored, copy from config.example.yaml)
- The user sends a message; the client POSTs to
/api/chatwith the conversation history, selected model, and active MCP server list. - The server calls
streamText(Vercel AI SDK,maxSteps: 20) with all tools from connected MCP servers injected. - When the LLM invokes a tool, the execute wrapper calls
client.callTool(...)on the relevantMCPClientManagerconnection and returns the result for the next LLM step. - The stream is returned to the client as a Vercel AI SDK data stream (
text/plain,X-Vercel-AI-Data-Stream: v1); debug events (LLM steps, tool calls, OAuth flows) are multiplexed into the same stream.
MCPClientManager owns all MCP client connections server-side:
- STDIO servers use
StdioClientTransport; HTTP servers tryStreamableHTTPClientTransportfirst, then fall back toSSEClientTransport - Tool names are namespaced as
{serverId}__{toolName}(hyphens in tool names become underscores) - OAuth2 servers use Authorization Code + PKCE with RFC 7591 dynamic client registration; token refresh and 401 queuing are automatic
When a tool result includes _meta["ui/resourceUri"], the chat UI renders a sandboxed iframe. The iframe src is proxied through /api/mcp/resource/{serverId}?uri=..., which fetches the MCP resource HTML server-side and returns it directly.
The widget communicates with the host via JSON-RPC 2.0 postMessage. The handshake is widget-initiated (ui/initialize); after the host responds the widget receives the tool arguments and result. From there the widget can:
- Call tools directly (
tools/call) without involving the LLM - Send a follow-up user message (
ui/message) to trigger a new LLM turn - Request fullscreen (
ui/request-display-mode)
GET /api/models returns the union of configured OpenAI models and live Ollama models (fetched from the Ollama /api/tags endpoint). Both providers are driven via @ai-sdk/openai — Ollama's native AI SDK provider is avoided because it silently drops tool-call tokens. System prompts are configured per provider in config.yaml.
For full architecture details, see specs/architecture.md.
All configuration lives in config.yaml at the project root. No environment variables are required.
| Section | Purpose |
|---|---|
llm.openai |
OpenAI API key, default model, system prompt |
llm.ollama |
Ollama base URL, system prompt |
mcp_servers |
MCP server definitions (stdio, HTTP, or HTTP+OAuth) |
See config.example.yaml for the full reference.