Build Custom UI

Connect any frontend to your agents

Build a custom chat interface, dashboard, or app that talks to your MUXI formation. Works with React, Vue, vanilla JS, mobile apps - anything that can make HTTP requests.

Quick Start

The TypeScript SDK works in browsers, Node.js, and all major frameworks:

npm install @muxi-ai/muxi-typescript
import { FormationClient } from '@muxi-ai/muxi-typescript';

const formation = new FormationClient({
  serverUrl: 'http://localhost:7890',
  formationId: 'my-assistant',
  clientKey: 'fmc_...'
});

// Chat
const response = await formation.chat({ message: 'Hello!' }, 'user-123');

// Streaming
for await (const chunk of formation.chatStream({ message: 'Tell me a story' }, 'user-123')) {
  if (typeof chunk.token === 'string') console.log(chunk.token);
}

SDK Examples

<!DOCTYPE html>
<html>
<head>
  <title>MUXI Chat</title>
</head>
<body>
  <div id="messages"></div>
  <input id="input" placeholder="Type a message..." />
  <button id="send">Send</button>

  <script type="module">
    import { FormationClient } from 'https://esm.sh/@muxi-ai/muxi-typescript';

    const formation = new FormationClient({
      serverUrl: 'http://localhost:7890',
      formationId: 'my-assistant',
      clientKey: 'fmc_...'
    });

    const userId = 'user-' + Math.random().toString(36).slice(2);
    const messagesDiv = document.getElementById('messages');
    const input = document.getElementById('input');

    document.getElementById('send').onclick = async () => {
      const message = input.value;
      if (!message) return;

      const userP = document.createElement('p');
      userP.textContent = You: ${message};
      messagesDiv.appendChild(userP);
      input.value = '';

      // Streaming response
      let response = '';
      const responseP = document.createElement('p');
      responseP.textContent = 'Assistant: ';
      messagesDiv.appendChild(responseP);

      for await (const chunk of formation.chatStream({ message }, userId)) {
        if (typeof chunk.token === 'string') {
          response += chunk.token;
          responseP.textContent = Assistant: ${response};
        }
      }
    };
  </script>
</body>
</html>
import { useState } from 'react';
import { FormationClient } from '@muxi-ai/muxi-typescript';

const formation = new FormationClient({
  serverUrl: 'http://localhost:7890',
  formationId: 'my-assistant',
  clientKey: 'fmc_...'
});

export function Chat({ userId }: { userId: string }) {
  const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([]);
  const [input, setInput] = useState('');
  const [streaming, setStreaming] = useState('');

  const send = async () => {
    if (!input.trim()) return;

    setMessages(prev => [...prev, { role: 'user', content: input }]);
    setInput('');
    setStreaming('');

    let response = '';
    for await (const chunk of formation.chatStream({ message: input }, userId)) {
      if (typeof chunk.token === 'string') {
        response += chunk.token;
        setStreaming(response);
      }
    }

    setMessages(prev => [...prev, { role: 'assistant', content: response }]);
    setStreaming('');
  };

  return (
    <div>
      {messages.map((m, i) => (
        <div key={i} className={m.role}>{m.content}</div>
      ))}
      {streaming && <div className="assistant">{streaming}▌</div>}
      <input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} />
      <button onClick={send}>Send</button>
    </div>
  );
}
import { FormationClient } from '@muxi-ai/muxi-typescript';

const formation = new FormationClient({
  serverUrl: 'http://localhost:7890',
  formationId: 'my-assistant',
  clientKey: 'fmc_...'
});

// Simple chat
const response = await formation.chat(
  { message: 'Hello!' },
  'user-123'
);
console.log(response.response);

// Streaming
for await (const chunk of formation.chatStream(
  { message: 'Tell me a story' },
  'user-123'
)) {
  if (typeof chunk.token === 'string') {
    process.stdout.write(chunk.token);
  }
}
console.log();
import json
from muxi import FormationClient

formation = FormationClient(
    server_url="http://localhost:7890",
    formation_id="my-assistant",
    client_key="fmc_..."
)

# Simple chat
response = formation.chat({"message": "Hello!"}, user_id="user-123")
print(response["response"])

# Streaming
for event in formation.chat_stream({"message": "Tell me a story"}, user_id="user-123"):
    if event.get("event") == "message":
        payload = json.loads(event.get("data", "{}"))
        token = payload.get("token")
        if isinstance(token, str):
            print(token, end="", flush=True)
print()

CDN delivery: Use https://esm.sh/@muxi-ai/muxi-typescript to import directly in browsers without a build step.

Session Management

Sessions maintain conversation context:

// List the user's sessions
const sessions = await formation.getSessions('user-123');

// Chat with session
const response = await formation.chat(
  { message: 'Hello!', session_id: 'sess_abc123' },
  'user-123'
);

// Get session history
const history = await formation.getSessionMessages('sess_abc123', 'user-123');
# List the user's sessions
sessions = formation.get_sessions("user-123")

# Chat with session
response = formation.chat(
    {"message": "Hello!", "session_id": "sess_abc123"},
    user_id="user-123"
)

# Get session history
history = formation.get_session_messages("sess_abc123", "user-123")

Rendering UI Widgets

A response can carry optional UI widgets - a set of choices to pick from, a link to send the user somewhere, or an MCP UI resource. They arrive on streaming chat as a dedicated ui event just before the stream completes. Rendering them natively is optional: the response text always works on its own, so a client that ignores widgets still behaves correctly.

for await (const chunk of formation.chatStream({ message }, userId)) {
  if (chunk.type === 'ui') {
    for (const widget of chunk.ui) {
      if (widget.type === 'options') {
        renderChoiceButtons(widget.prompt, widget.options, (value) => {
          // Reply with the picked value; the text stands alone too.
          formation.chat({ message: value, ui_response: { id: widget.id, value } }, userId);
        });
      } else if (widget.type === 'action_link') {
        renderLink(widget.label, widget.url);
      }
      // Ignore unknown widget types (progressive enhancement).
    }
  } else if (typeof chunk.token === 'string') {
    appendText(chunk.token);
  }
}
import json
from muxi import parse_ui_widgets

for event in formation.chat_stream({"message": message}, user_id=user_id):
    widgets = parse_ui_widgets(event)
    if widgets:
        for widget in widgets:
            if widget["type"] == "options":
                choice = prompt_user(widget["prompt"], widget["options"])
                formation.chat(
                    {"message": choice, "ui_response": {"id": widget["id"], "value": choice}},
                    user_id=user_id,
                )
    elif event.get("event") == "message":
        payload = json.loads(event.get("data", "{}"))
        token = payload.get("token")
        if isinstance(token, str):
            print(token, end="", flush=True)

See Response UI Widgets for every widget type, its fields, and the reply path.

API Reference

For direct API access (without SDK), see the endpoint reference:

Endpoint Method Description
/v1/chat POST Send message
/v1/sessions GET List sessions
/v1/sessions/{id} GET Get session history
/v1/agents GET List agents

Required headers:

  • X-Muxi-Client-Key - Your client key (fmc_...)
  • X-Muxi-User-Id - User identifier
  • Content-Type: application/json
  • Accept: text/event-stream (for streaming)

Optional headers:

  • X-Muxi-Idempotency-Key - Unique per logical request so a successful non-streaming mutation can be retried without re-running it. Streams and failures are not replayed. The SDKs set this automatically; see Idempotency.

Full API Documentation →

Learn More


Home Docs SDKs
Star on GitHub