{segments.map((segment, i) => {
if (segment.type === 'text') {
return
;
}
if (segment.type === 'tool') {
if (segment.frontendAction) {
return
;
}
return
;
}
return null;
})}
);
}
function ToolCard({ segment }) {
const isRunning = segment.status === 'running';
const isError = segment.status === 'error';
return (
{isRunning && }
{segment.tool}
{!isRunning && (isError ? : )}
{segment.args && (
{JSON.stringify(segment.args, null, 2)}
)}
{segment.result && (
{JSON.stringify(segment.result, null, 2)}
)}
{segment.error &&
{segment.error}
}
);
}
```
The tool card should be compact by default (just tool name + status icon) with collapsible sections for args and result, so it doesn't dominate the reading flow. While a tool is running (`status: 'running'`), show a spinner. When complete, show a check or error icon.
### Handling `__frontendAction` in Tool Results
When the AI calls certain tools (e.g., QR code, data view, payment, secret reveal), the tool result contains a `__frontendAction` object. This signals the frontend to render a special UI component **inline in the bubble at the tool segment's position** instead of the default collapsible ToolCard. This is already handled in the segments code above — when `segment.frontendAction` is present, render an `ActionCard` instead of a `ToolCard`.
The `extractFrontendAction` helper unwraps the action from various MCP response formats:
```js
function extractFrontendAction(result) {
if (!result) return null;
if (result.__frontendAction) return result.__frontendAction;
// Unwrap MCP wrapper format: result → result.result → content[].text → JSON
let data = result;
if (result?.result?.content) data = result.result;
if (data?.content && Array.isArray(data.content)) {
const textContent = data.content.find(c => c.type === 'text');
if (textContent?.text) {
try {
const parsed = JSON.parse(textContent.text);
if (parsed?.__frontendAction) return parsed.__frontendAction;
} catch { /* not JSON */ }
}
}
return null;
}
```
### Frontend Action Types
| Action Type | Component | Description |
|-------------|-----------|-------------|
| `qrcode` | `QrCodeActionCard` | Renders any string value as a QR code card |
| `dataView` | `DataViewActionCard` | Fetches a Business API route and renders a grid or gallery |
| `payment` | `PaymentActionCard` | "Pay Now" button that opens Stripe checkout modal |
| `secret` | `SecretActionCard` | Renders a secret field as text, barcode, or QR code |
#### QR Code Action (`type: "qrcode"`)
Triggered by the `showQrCode` MCP tool. Renders a QR code card from any string value.
```json
{
"__frontendAction": {
"type": "qrcode",
"value": "https://example.com/invite/ABC123",
"title": "Invite Link",
"subtitle": "Scan to open"
}
}
```
#### Data View Action (`type: "dataView"`)
Triggered by `showBusinessApiListInFrontEnd` or `showBusinessApiGalleryInFrontEnd`.
Frontend calls the provided Business API route using the user's bearer token, then renders:
- `viewType: "grid"` as tabular rows/columns
- `viewType: "gallery"` as image-first cards
```json
{
"__frontendAction": {
"type": "dataView",
"viewType": "grid",
"title": "Recent Orders",
"serviceName": "commerce",
"apiName": "listOrders",
"routePath": "/v1/listorders",
"httpMethod": "GET",
"queryParams": { "pageNo": 1, "pageRowCount": 10 },
"columns": [
{ "field": "id", "label": "Order ID" },
{ "field": "orderAmount", "label": "Amount", "format": "currency" }
]
}
}
```
#### Payment Action (`type: "payment"`)
Triggered by the `initiatePayment` MCP tool. Renders a payment card with amount and a "Pay Now" button.
```json
{
"__frontendAction": {
"type": "payment",
"orderId": "uuid",
"orderType": "order",
"serviceName": "commerce",
"amount": 99.99,
"currency": "USD",
"description": "Order #abc123"
}
}
```
#### Secret Action (`type: "secret"`)
Triggered by the `showSecretFieldInFrontEnd` MCP tool. Renders the secret value securely.
```json
{
"__frontendAction": {
"type": "secret",
"fieldName": "apiKey",
"fieldLabel": "API Key",
"value": "actual-secret-value",
"renderType": "asText",
"serviceName": "commerce",
"objectName": "coupon",
"modelName": "Coupon",
"recordId": "uuid"
}
}
```
The `renderType` determines how the value is displayed:
- `"asText"` — Plain text with copy-to-clipboard button
- `"asBarcode"` — Barcode image (using `react-barcode`)
- `"asQrCode"` — QR code image (using `react-qr-code`)
The AI chooses the `renderType` based on what the user asks for (e.g., "show me the QR code" → `asQrCode`, "let me see the key" → `asText`). If the user doesn't specify, the AI should ask them how they'd like to see it.
The card auto-hides the value after 2 minutes for security.
### Conversation Management
```js
// List user's conversations
GET /api/chat/conversations
// Get conversation history
GET /api/chat/conversations/:conversationId
// Delete a conversation
DELETE /api/chat/conversations/:conversationId
```
---
## MCP Tool Discovery & Direct Invocation
The MCP BFF exposes endpoints for discovering and directly calling MCP tools (useful for debugging or building custom UIs).
### GET /api/tools — List All Tools
```js
const response = await fetch(`${mcpBffUrl}/api/tools`, { headers });
const { tools, count } = await response.json();
// tools: [{ name, description, inputSchema, service }, ...]
```
### GET /api/tools/service/:serviceName — List Service Tools
```js
const response = await fetch(`${mcpBffUrl}/api/tools/service/commerce`, { headers });
const { tools } = await response.json();
```
### POST /api/tools/call — Call a Tool Directly
```js
const response = await fetch(`${mcpBffUrl}/api/tools/call`, {
method: 'POST',
headers,
body: JSON.stringify({
toolName: "listProducts",
args: { page: 1, limit: 10 },
}),
});
const result = await response.json();
```
### GET /api/tools/status — Connection Status
```js
const status = await fetch(`${mcpBffUrl}/api/tools/status`, { headers });
// Returns health of each MCP service connection
```
### POST /api/tools/refresh — Reconnect Services
```js
await fetch(`${mcpBffUrl}/api/tools/refresh`, { method: 'POST', headers });
// Reconnects to all MCP services and refreshes the tool registry
```
---
## Elasticsearch API
The MCP BFF provides direct access to Elasticsearch for searching, filtering, and aggregating data across all project indices.
All Elasticsearch endpoints are under `/api/elastic`.
### GET /api/elastic/allIndices — List Project Indices
Returns all Elasticsearch indices belonging to this project (prefixed with `workforceos_`).
```js
const indices = await fetch(`${mcpBffUrl}/api/elastic/allIndices`, { headers });
// ["workforceos_products", "workforceos_orders", ...]
```
### POST /api/elastic/:indexName/rawsearch — Raw Elasticsearch Query
Execute a raw Elasticsearch query on a specific index.
```js
const response = await fetch(`${mcpBffUrl}/api/elastic/products/rawsearch`, {
method: 'POST',
headers,
body: JSON.stringify({
query: {
bool: {
must: [
{ match: { status: "active" } },
{ range: { price: { gte: 10, lte: 100 } } }
]
}
},
size: 20,
from: 0,
sort: [{ createdAt: "desc" }]
}),
});
const { total, hits, aggregations, took } = await response.json();
// hits: [{ _id, _index, _score, _source: { ...document... } }, ...]
```
Note: The index name is automatically prefixed with `workforceos_` if not already prefixed.
### POST /api/elastic/:indexName/search — Simplified Search
A higher-level search API with built-in support for filters, sorting, and pagination.
```js
const response = await fetch(`${mcpBffUrl}/api/elastic/products/search`, {
method: 'POST',
headers,
body: JSON.stringify({
search: "wireless headphones", // Full-text search
filters: { status: "active" }, // Field filters
sort: { field: "createdAt", order: "desc" },
page: 1,
limit: 25,
}),
});
```
### POST /api/elastic/:indexName/aggregate — Aggregations
Run aggregation queries for analytics and dashboards.
```js
const response = await fetch(`${mcpBffUrl}/api/elastic/orders/aggregate`, {
method: 'POST',
headers,
body: JSON.stringify({
aggs: {
status_counts: { terms: { field: "status.keyword" } },
total_revenue: { sum: { field: "amount" } },
monthly_orders: {
date_histogram: { field: "createdAt", calendar_interval: "month" }
}
},
query: { range: { createdAt: { gte: "now-1y" } } }
}),
});
```
### GET /api/elastic/:indexName/mapping — Index Mapping
Get the field mapping for an index (useful for building dynamic filter UIs).
```js
const mapping = await fetch(`${mcpBffUrl}/api/elastic/products/mapping`, { headers });
```
### POST /api/elastic/:indexName/ai-search — AI-Assisted Search
Uses the configured AI model to convert a natural-language query into an Elasticsearch query.
```js
const response = await fetch(`${mcpBffUrl}/api/elastic/orders/ai-search`, {
method: 'POST',
headers,
body: JSON.stringify({
query: "orders over $100 from last month that are still pending",
}),
});
// Returns: { total, hits, generatedQuery, ... }
```
---
## Log API
The MCP BFF provides log viewing endpoints for monitoring application behavior.
### GET /api/logs — Query Logs
```js
const response = await fetch(`${mcpBffUrl}/api/logs?page=1&limit=50&logType=2&service=commerce&search=payment`, {
headers,
});
```
**Query Parameters:**
- `page` — Page number (default: 1)
- `limit` — Items per page (default: 50)
- `logType` — 0=INFO, 1=WARNING, 2=ERROR
- `service` — Filter by service name
- `search` — Search in subject and message
- `from` / `to` — Date range (ISO strings)
- `requestId` — Filter by request ID
### GET /api/logs/stream — Real-time Console Stream (SSE)
Streams real-time console output from all services via Server-Sent Events.
```js
const eventSource = new EventSource(`${mcpBffUrl}/api/logs/stream?services=commerce,auth`, {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
eventSource.addEventListener('log', (event) => {
const logEntry = JSON.parse(event.data);
// { service, timestamp, level, message, ... }
});
```
---
## Secret Field Management via MCP
Some data objects contain **secret properties** — sensitive values like API keys, QR codes, or tokens that are protected from AI model exposure.
### How It Works
1. When the AI retrieves data via MCP tools (GET/LIST), secret fields are **masked with `***`** in the response. The AI never sees the actual values.
2. Each masked record includes a `__proxyCode` — a temporary token proving the user was authorized to access that record.
3. When the user asks to see a secret, the AI calls `showSecretFieldInFrontEnd` with the `proxyCode` and desired render type.
4. The tool returns a `__frontendAction` with `type: "secret"` containing the actual value.
5. The frontend renders the value in a `SecretActionCard` (text, barcode, or QR code) with auto-hide.
### Objects with Secret Fields
- **EmployeeProfile** (`employeeProfile`): `notes`
- **EmployeeDocument** (`employeeProfile`): `documentUrl`
### Example MCP Flows
**User specifies render type:**
```
User: "Show me the QR code for coupon #abc"
AI → calls getCoupon(id: "abc")
← receives { coupon: { id: "abc", code: "***", __proxyCode: "f7a3b2..." } }
AI → calls showSecretFieldInFrontEnd(proxyCode: "f7a3b2...", fieldName: "code", renderType: "asQrCode")
← receives { __frontendAction: { type: "secret", value: "COUPON-XYZ-123", renderType: "asQrCode", ... } }
Frontend → renders SecretActionCard with QR code image
```
**User does not specify — AI asks first:**
```
User: "Show me the secret code for coupon #abc"
AI → calls getCoupon(id: "abc")
← receives { coupon: { id: "abc", code: "***", __proxyCode: "f7a3b2..." } }
AI → "How would you like to see the code? I can show it as plain text, a barcode, or a QR code."
User: "As a barcode please"
AI → calls showSecretFieldInFrontEnd(proxyCode: "f7a3b2...", fieldName: "code", renderType: "asBarcode")
← receives { __frontendAction: { type: "secret", value: "COUPON-XYZ-123", renderType: "asBarcode", ... } }
Frontend → renders SecretActionCard with barcode image
```
### Frontend Implementation
In your chat UI, handle the `secret` action type in the ActionCard dispatcher:
```jsx
function ActionCard({ action }) {
switch (action.type) {
case 'secret':
return