Quorentra

Quorentra ChatGPT Pipeline UI: Building from Zero — Part 16

Series: Building Quorentra CRM from Zero — A Modular, ChatGPT-Native AI CRM

Building the first interactive Quorentra CRM interface inside ChatGPT using MCP tools, structured results, React, and the MCP Apps UI architecture.

Quorentra ChatGPT Pipeline UI: Building from Zero — Part 16
Quorentra ChatGPT Pipeline UI: Building from Zero — Part 16

1. Introduction

In Part 15, Quorentra crossed an important boundary.

We connected:

ChatGPT
MCP
Quorentra

and exposed our first read-only CRM capabilities:

list_companies
get_company
list_contacts
get_contact
list_company_contacts
list_opportunities
get_opportunity
list_company_opportunities
list_opportunities_by_stage
get_pipeline_summary

For the first time, a user could ask:

What is my current EUR pipeline?

ChatGPT could invoke:

get_pipeline_summary

and Quorentra could return:

{
"currency": "EUR",
"open_opportunities": 4,
"total_pipeline": "385000.00",
"weighted_pipeline": "161000.00"
}

ChatGPT could then explain the result conversationally.

That already gives us a useful ChatGPT-native CRM.

But CRM information is not always best represented as prose.

Sometimes the better interface is visual.

That is what we will build in Part 16.


2. What Are We Building?

We will create Quorentra’s first UI component designed to render inside ChatGPT.

The target experience is:

User:
"Show my EUR pipeline."

Quorentra retrieves the pipeline data and presents a compact visual interface:

┌────────────────────────────────────────────┐
│ QUORENTRA │
│ Sales Pipeline │
│ │
│ Open Opportunities 4 │
│ │
│ Total Pipeline €385,000 │
│ Weighted Pipeline €161,000 │
│ │
│ Weighted Coverage 41.8% │
└────────────────────────────────────────────┘

ChatGPT can still provide conversational context around it.

The result becomes:

Natural Language
+
CRM Data
+
Interactive UI

This is our first true ChatGPT application surface.


3. Why Start With the Pipeline?

We could begin with:

Company List
Contact List
Opportunity Table
Company Detail
Opportunity Detail

But the pipeline summary is a better first component.

Why?

Because it is:

Small
Read-only
High-value
Easy to verify
Easy to visualize
Already implemented
Already tested
Already tenant-safe
Already available through MCP

Most importantly, the underlying business capability already works.

We are changing presentation, not business logic.


4. The Modular Principle Continues

The development sequence remains:

Business Capability
REST
MCP
ChatGPT
UI

Not:

Build fancy UI
Invent data later

The pipeline calculation already exists inside Quorentra.

The MCP interface already exposes it.

Now we add another adapter:

Pipeline Data
UI Resource
React Component

5. The Architecture Before Part 16

At the end of Part 15:

┌────────────────────┐
│ User │
└─────────┬──────────┘
┌────────────────────┐
│ ChatGPT │
└─────────┬──────────┘
┌────────────────────┐
│ MCP │
└─────────┬──────────┘
┌────────────────────┐
│ Quorentra Tools │
└─────────┬──────────┘
┌────────────────────┐
│ Application Core │
└─────────┬──────────┘
┌────────────────────┐
│ PostgreSQL │
└────────────────────┘

Everything above the MCP tool result was primarily conversational.


6. Architecture After Part 16

Now we introduce a UI path:

┌───────────────────────────────────────────┐
│ User │
└──────────────────────┬────────────────────┘
┌───────────────────────────────────────────┐
│ ChatGPT │
└──────────────────────┬────────────────────┘
┌───────┴────────┐
│ │
▼ ▼
Conversation Quorentra UI
MCP Apps Bridge
MCP Tools
Quorentra Core
PostgreSQL

This is an important evolution.

ChatGPT becomes both:

Conversational interface

and:

Application host

for Quorentra.


7. Current MCP Apps UI Model

The current architecture for MCP-backed ChatGPT interfaces is based around UI components running inside an isolated iframe.

Conceptually:

ChatGPT
Sandboxed iframe
Quorentra React Component

Communication between the component and host occurs through the MCP Apps bridge.

The important concepts are:

MCP Tool
MCP Resource
UI Template
Structured Content
MCP Apps Bridge
iframe
React Component

ChatGPT-specific extensions can additionally be accessed through:

window.openai

where appropriate.


8. MCP Apps First, ChatGPT Extensions Second

For new Quorentra UI components, our principle will be:

Use the standard MCP Apps UI mechanism for the baseline application and use ChatGPT-specific extensions only when they materially improve the experience.

This improves portability.

Conceptually:

MCP Apps
├── Tool I/O
├── Tool calls
├── Messages
└── Model context

with optional:

window.openai

features for ChatGPT-specific capabilities.


9. What Is a UI Resource?

An MCP tool provides a capability.

For example:

get_pipeline_summary

A UI resource provides a presentation.

For example:

ui://quorentra/pipeline.html

Conceptually:

Tool
get_pipeline_summary

returns:

Structured CRM data

while:

Resource
ui://quorentra/pipeline.html

defines how that information can be visually rendered.

These are separate concerns.


10. Data Is Not UI

This distinction is important.

Our business data is:

{
"currency": "EUR",
"open_opportunities": 4,
"total_pipeline": "385000.00",
"weighted_pipeline": "161000.00"
}

Our UI is:

┌────────────────────────────────────┐
│ Sales Pipeline │
│ │
│ 4 Open Opportunities │
│ €385,000 Total │
│ €161,000 Weighted │
└────────────────────────────────────┘

The data should remain useful even if no UI is available.


11. Do Not Make the Widget the Business Layer

Avoid:

React Component
Calculate Pipeline

Instead:

OpportunityService
Calculate Pipeline
MCP Structured Result
React Component

The UI renders facts.

It does not determine CRM truth.


12. A Better Data/UI Pattern

Current MCP Apps guidance supports a particularly useful pattern:

Data Tool
Structured Result
Render Tool
UI Resource

This decouples:

Data acquisition

from:

Presentation

For Quorentra, this is an excellent long-term architecture.


13. Why Decouple Data and Rendering?

Imagine later asking:

Show only the pipeline opportunities worth more than €100,000.

A tightly coupled tool might always render the same pipeline widget immediately.

A decoupled design allows:

ChatGPT
Fetch CRM Data
Reason / Filter
Render Appropriate UI

That gives the model more flexibility.


14. Part 16 Scope

We will therefore introduce two concepts:

get_pipeline_summary

as our data capability,

and:

render_pipeline_summary

as our presentation capability.

The first returns data.

The second renders the pipeline UI.

This separation will become useful as Quorentra grows.


15. Starting Checkpoint

Before modifying anything, verify Part 15.

From:

quorentra/backend

run:

python -m pytest

Then verify the MCP server.

Confirm these tools remain available:

list_companies
get_company
list_contacts
get_contact
list_company_contacts
list_opportunities
get_opportunity
list_company_opportunities
list_opportunities_by_stage
get_pipeline_summary

Finally verify:

What is my current EUR pipeline?

still works through ChatGPT.

Do not add UI until the data path is green.


16. Our New Project Structure

We now need frontend code specifically for ChatGPT-hosted components.

A clean structure is:

quorentra/
├── backend/
│ └── app/
│ └── mcp/
│ ├── server.py
│ ├── tools/
│ └── resources/
└── chatgpt-ui/
├── package.json
├── tsconfig.json
├── src/
│ ├── pipeline/
│ │ ├── PipelineWidget.tsx
│ │ ├── pipeline.css
│ │ └── index.tsx
│ └── shared/
│ └── mcp.ts
└── dist/

This keeps:

Python backend

separate from:

React UI

while keeping both inside the Quorentra repository.


17. Why a Separate chatgpt-ui Directory?

The component has different concerns:

TypeScript
React
Bundling
CSS
Browser runtime
Host bridge

The backend has:

Python
FastAPI
MCP
SQLAlchemy
PostgreSQL

Mixing them unnecessarily would make the project harder to maintain.

Our modular philosophy should apply to source structure too.


18. Create the UI Project

From the Quorentra root:

mkdir chatgpt-ui
cd chatgpt-ui
npm init -y

Install React:

npm install react react-dom

Then development tooling:

npm install -D typescript esbuild

We deliberately keep the dependency set small.


19. Why esbuild?

We need to transform:

TypeScript
+
React JSX

into browser-executable JavaScript.

For this small component, esbuild provides a simple build pipeline.

We do not need:

Large application framework
Complex router
SSR
Full web application platform

for our first widget.

The component runs inside a host environment.


20. Create the Source Structure

From:

chatgpt-ui

create:

mkdir src
mkdir src\pipeline
mkdir src\shared

Then create:

src/
├── pipeline/
│ ├── PipelineWidget.tsx
│ ├── pipeline.css
│ └── index.tsx
└── shared/
└── mcp.ts

21. Define the Pipeline Data Contract

The UI should consume the same conceptual structure returned by our tool.

Create a TypeScript type:

export type PipelineSummary = {
currency: string;
open_opportunities: number;
total_pipeline: string;
weighted_pipeline: string;
};

This gives the component an explicit contract.


22. Why Use Strings for Money?

Our Python API uses decimal-safe values.

For example:

"385000.00"

rather than relying on binary floating-point arithmetic.

The UI can parse these values for formatting.

It should not perform authoritative financial calculations.

The backend remains responsible for CRM financial truth.


23. Weighted Coverage

The UI can derive a presentation metric:

weighted_pipeline
-----------------
total_pipeline

For example:

161,000
-------
385,000
=
41.8%

This is acceptable as a display-only derived metric.

It does not modify business state.


24. Build a Currency Formatter

We want:

385000.00

displayed as:

€385,000

or according to the user’s locale.

Use:

Intl.NumberFormat

rather than manually concatenating currency symbols.

Conceptually:

function formatCurrency(
value: string,
currency: string,
locale: string
) {
return new Intl.NumberFormat(locale, {
style: "currency",
currency,
maximumFractionDigits: 0,
}).format(Number(value));
}

25. Respect Host Locale

The host can provide locale context to the component.

A good browser-native fallback is:

document.documentElement.lang

For example:

const locale =
document.documentElement.lang || "en-US";

This lets formatting adapt naturally.

A Dutch user might see:

€ 385.000

while a U.S. locale may render equivalent formatting differently.


26. Build the Pipeline Component

Open:

src/pipeline/PipelineWidget.tsx

Conceptually:

type Props = {
data: PipelineSummary;
};
export function PipelineWidget({ data }: Props) {
const locale =
document.documentElement.lang || "en-US";
return (
<main>
<header>
<span>Quorentra</span>
<h1>Sales Pipeline</h1>
</header>
<section>
<Metric
label="Open Opportunities"
value={String(data.open_opportunities)}
/>
<Metric
label="Total Pipeline"
value={formatCurrency(
data.total_pipeline,
data.currency,
locale
)}
/>
<Metric
label="Weighted Pipeline"
value={formatCurrency(
data.weighted_pipeline,
data.currency,
locale
)}
/>
</section>
</main>
);
}

The exact visual design can evolve.

The data contract is more important.


27. Create a Reusable Metric Component

Rather than repeating markup:

type MetricProps = {
label: string;
value: string;
};
function Metric({
label,
value,
}: MetricProps) {
return (
<div className="metric">
<span className="metric-label">
{label}
</span>
<strong className="metric-value">
{value}
</strong>
</div>
);
}

Small reusable primitives will help when we build future Quorentra components.


28. Keep the First Design Simple

Avoid immediately building:

Charts
Animations
Gradients
Large icons
Complex navigation
Tabs
Drill-down
Modals
Filters

The first component needs to prove:

Tool Result
MCP Apps Bridge
React
ChatGPT

A clean metrics card is enough.


29. Suggested First Layout

Conceptually:

QUORENTRA
Sales Pipeline
EUR
┌────────────────────────────┐
│ Open Opportunities 4 │
├────────────────────────────┤
│ Total Pipeline €385,000│
├────────────────────────────┤
│ Weighted Pipeline €161,000│
├────────────────────────────┤
│ Weighted Coverage 41.8%│
└────────────────────────────┘

It is immediately understandable.


30. Empty State

What if:

open_opportunities = 0

?

Do not show a broken-looking card.

Instead:

QUORENTRA
Sales Pipeline
No open EUR opportunities.
Your current open pipeline is empty.

Empty states are part of product design.


31. Loading State

The UI should tolerate data not being immediately available.

For example:

Loading pipeline…

This matters because tool input or tool results may arrive after the component initially mounts.

The component must not assume everything exists synchronously.


32. Error State

If the UI receives malformed data, show:

Unable to display pipeline data.

Do not show:

TypeError: Cannot read properties of undefined

to the user.

Client-side errors should remain safe and comprehensible.


33. Treat Tool Results as Untrusted Input

Even though the result comes from Quorentra, the browser component should still validate assumptions.

Check:

currency exists
open_opportunities is numeric
pipeline values exist

Do not blindly render arbitrary HTML returned by a tool.

The UI boundary deserves its own defensive programming.


34. Receiving MCP Tool Results

The current MCP Apps architecture sends tool results to the component through host messages.

Conceptually:

ChatGPT
ui/notifications/tool-result
iframe
React

The useful business data is available through:

structuredContent

The component should render from that structured data.


35. Create a Tool Result Hook

In:

src/shared/mcp.ts

we can create a small React helper.

Conceptually:

export function useToolResult() {
const [result, setResult] =
useState<unknown>(null);
useEffect(() => {
const handleMessage = (
event: MessageEvent
) => {
if (event.source !== window.parent) {
return;
}
const message = event.data;
if (
!message ||
message.jsonrpc !== "2.0"
) {
return;
}
if (
message.method !==
"ui/notifications/tool-result"
) {
return;
}
setResult(
message.params?.structuredContent
?? null
);
};
window.addEventListener(
"message",
handleMessage
);
return () => {
window.removeEventListener(
"message",
handleMessage
);
};
}, []);
return result;
}

This isolates host communication from presentation logic.


36. Why Wrap the Bridge?

We do not want every Quorentra component to contain:

postMessage
JSON-RPC
event listeners
host message parsing

Instead:

shared/mcp.ts

becomes our UI integration layer.

Future components can simply use hooks.

This is the same modular pattern we used on the backend.


37. Render From Structured Content

Our pipeline entry component can conceptually do:

const result = useToolResult();
if (!result) {
return <LoadingState />;
}
return (
<PipelineWidget
data={result as PipelineSummary}
/>
);

A production implementation should add proper runtime validation.

But the flow remains:

Tool Result
Hook
Validated Data
Component

38. ChatGPT Compatibility Layer

ChatGPT also provides:

window.openai

for Apps SDK compatibility and additional ChatGPT-specific functionality.

For example, depending on the environment, this can provide access to:

toolInput
toolOutput
callTool
requestDisplayMode
requestModal
requestClose

We should feature-detect such capabilities rather than assuming they always exist.


39. Do Not Depend on window.openai for Everything

Because the baseline UI architecture is now aligned around MCP Apps compatibility, our first component should primarily rely on the standard bridge.

Use:

window.openai

only when we specifically need ChatGPT extensions.

This gives Quorentra a cleaner architecture.


40. Build the Entry Point

Create:

src/pipeline/index.tsx

Conceptually:

import React from "react";
import { createRoot } from "react-dom/client";
import { PipelineWidget } from "./PipelineWidget";
import { useToolResult } from "../shared/mcp";
import "./pipeline.css";
function App() {
const result = useToolResult();
if (!result) {
return <div>Loading pipeline…</div>;
}
return (
<PipelineWidget
data={result as PipelineSummary}
/>
);
}
const root = document.getElementById("root");
if (root) {
createRoot(root).render(<App />);
}

41. Add the Build Script

In:

package.json

add:

{
"scripts": {
"build": "esbuild src/pipeline/index.tsx --bundle --format=esm --outfile=dist/pipeline.js"
}
}

Then create:

dist/

and run:

npm run build

Expected:

dist/pipeline.js

42. Keep the Bundle Small

The component will run inside ChatGPT.

Avoid adding large dependencies unless necessary.

For the pipeline card we do not need:

Chart.js
D3
Material UI
Large icon libraries
State management frameworks

React plus browser APIs are enough.

Later components may justify additional libraries.


43. Add the Backend Resource Package

Back in:

backend/app/mcp

create:

resources/

with:

resources/
├── __init__.py
└── pipeline.py

The resource module will register the UI template.


44. Define the Resource URI

Use a stable URI such as:

ui://quorentra/pipeline.html

Conceptually:

PIPELINE_UI_URI = (
"ui://quorentra/pipeline.html"
)

This identifier connects the MCP server to the UI resource.


45. UI Resources Are Not Public URLs

Notice:

ui://

rather than:

https://

The resource URI identifies an MCP resource.

The MCP server resolves it and returns the component content.

It is not simply a conventional webpage URL.


46. Build the HTML Template

The server needs to provide an HTML document containing:

root element
styles
component bundle

Conceptually:

<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
</head>
<body>
<div id="root"></div>
<script type="module">
/* bundled pipeline.js */
</script>
</body>
</html>

The build output can be embedded into this resource.


47. Why Bundle the Component?

The UI executes in a sandboxed host environment.

Bundling gives us:

One predictable artifact
No runtime npm dependency resolution
Simpler resource delivery
Smaller deployment surface

For our first component, this is ideal.


48. Register the UI Resource

The MCP server conceptually registers:

Name:
quorentra-pipeline
URI:
ui://quorentra/pipeline.html

and returns:

HTML

with the MCP Apps-compatible HTML MIME profile expected by the current SDK.

The exact registration syntax should follow the current MCP SDK version installed in the project.


49. Why We Keep SDK Syntax Isolated

The underlying architecture is stable:

Tool
Resource
Structured Result
Component

But SDK registration APIs can evolve.

Therefore:

app/mcp/resources/pipeline.py

should isolate the SDK-specific resource registration code.

That makes future upgrades easier.


50. Register a Render Tool

Now add:

render_pipeline_summary

This is not a business-data tool.

Its purpose is presentation.

Conceptually:

Name:
render_pipeline_summary
Description:
Render a Quorentra sales pipeline summary
using pipeline data previously retrieved
for the active organization.
Behavior:
Read-only presentation

51. Render Tool Input

The render tool can accept:

currency
open_opportunities
total_pipeline
weighted_pipeline

For example:

{
"currency": "EUR",
"open_opportunities": 4,
"total_pipeline": "385000.00",
"weighted_pipeline": "161000.00"
}

It does not need to query PostgreSQL again.

Its job is rendering.


52. The Decoupled Flow

The complete request can now become:

User
│ "Show my EUR pipeline."
ChatGPT
get_pipeline_summary
Quorentra Core
Structured Pipeline Data
ChatGPT
render_pipeline_summary
Pipeline UI Resource
React Widget

This cleanly separates:

Business retrieval

from:

Presentation

53. Why This Pattern Scales

Later:

search_opportunities

can retrieve data.

Then:

render_opportunity_table

can display it.

Likewise:

get_company

can retrieve data.

Then:

render_company_card

can display it.

Our future architecture becomes:

Data Tools
+
Render Tools
+
UI Resources

This is highly modular.


54. Tool Metadata

The render tool needs metadata linking it to the UI resource.

Conceptually, the current ecosystem supports associating a tool with a resource URI.

For ChatGPT compatibility this has historically included metadata such as an output template.

For broader MCP Apps compatibility, the UI resource URI can also be represented through the standardized UI metadata.

The implementation should use the current SDK conventions.


55. Do Not Attach UI to Every Data Tool

It is tempting to make:

get_pipeline_summary

always render a widget.

But then a request such as:

Compare my weighted pipeline with the total pipeline and explain the difference.

may cause unnecessary UI rendering when ChatGPT only needed the numbers.

The decoupled architecture lets the model choose when presentation adds value.


56. Data Tools Remain Reusable

Our original:

get_pipeline_summary

should remain useful for:

Reasoning
Comparison
Conversation
Future agents
Other MCP clients
REST parity
Automation

without requiring a UI.

That is good interface design.


57. Rendering Is a Separate Capability

The render tool is specifically for:

User-visible presentation

This also makes testing easier.

We can test:

pipeline calculation

separately from:

pipeline rendering

Again:

one concern per layer.


58. Structured Content

The render tool should return its business payload as:

structuredContent

Conceptually:

{
"currency": "EUR",
"open_opportunities": 4,
"total_pipeline": "385000.00",
"weighted_pipeline": "161000.00"
}

The widget receives this structured data.

It should not parse prose to reconstruct the pipeline.


59. Never Make the UI Parse Natural Language

Avoid:

"Your pipeline contains four opportunities
worth €385,000 with weighted value €161,000."

then trying to extract values from that string.

That is fragile.

Prefer:

structuredContent

for machines,

and:

content

or ChatGPT narration for humans.


60. Structured Content Is the Contract

This becomes another Quorentra principle:

Components render structured application data, never inferred prose.

The model can narrate the data separately.

This prevents presentation logic from depending on language-model wording.


61. UI-Only Metadata

Some information may be useful to the component but unnecessary for model reasoning.

For example:

display preferences
UI hints
session identifiers

can belong in metadata rather than the primary structured business result.

Keep the model-facing result focused.


62. Do Not Put Secrets in Widget Metadata

The UI runs client-side.

Never send:

JWT signing secret
Database credentials
API secrets
Internal service credentials

to the component.

Likewise, avoid unnecessary sensitive information.

The browser should receive only what it needs to render the user’s authorized data.


63. Tenant Security Still Happens Server-Side

The UI does not enforce tenant isolation.

This is critical.

Never rely on:

if (
company.organization_id ===
currentOrganization
)

inside React as a security mechanism.

The backend must already have filtered the data.

The UI receives authorized results only.


64. The Widget Is Not Trusted

Treat the widget as a client.

The security architecture remains:

User
ChatGPT
MCP Request
Authentication
TenantContext
RBAC
Application Service
Authorized Result
Widget

Never reverse that order.


65. First Visual Test

Use known demo data:

Open Opportunities 4
Total Pipeline €385,000
Weighted Pipeline €161,000

Ask:

Show my EUR pipeline.

Verify the component displays exactly those values.


66. Verify Against REST

Request:

GET /api/v1/opportunities/pipeline/summary?currency=EUR

Then compare with the widget.

Expected:

REST
=
MCP
=
Widget

The presentation can differ.

The business values cannot.


67. Verify Against the MCP Data Tool

Call:

get_pipeline_summary

directly.

Compare:

currency
open_opportunities
total_pipeline
weighted_pipeline

with the UI.

Again:

Business truth

must be identical across interfaces.


68. Add UI Tests

Create a small frontend test strategy.

At minimum verify:

valid pipeline renders
zero pipeline renders
currency formatting works
missing data shows safe state
weighted coverage handles zero total

For the first widget, we do not need an enormous frontend testing framework.

The goal is confidence around the data contract.


69. Division by Zero

Weighted coverage must handle:

total_pipeline = 0

Avoid:

NaN%

or:

Infinity%

Use:

0%

or omit the metric.

This is a small example of why UI edge cases matter.


70. Currency Edge Cases

Test:

EUR
USD
GBP

and potentially currencies with different fraction conventions later.

Use:

Intl.NumberFormat

instead of maintaining a manual symbol table.


71. Large Values

Test:

€385,000
€5,000,000
€125,000,000

The layout should remain usable.

Do not assume pipeline values are always short.


72. Responsive Layout

The ChatGPT component may render at different widths.

Therefore avoid fixed assumptions such as:

width: 900px;

Prefer flexible layouts.

For example:

CSS Grid
Flexbox
min/max widths
responsive spacing

The component should work inline without requiring fullscreen.


73. Inline First

The pipeline summary is compact.

Therefore its default presentation should be:

inline

rather than:

fullscreen

Fullscreen should be reserved for interfaces that genuinely need additional space.

Later examples might include:

Opportunity table
Sales analytics dashboard
Complex editor

74. Theme Awareness

ChatGPT can be used with different appearance settings.

Avoid assumptions such as:

background always white
text always black

Prefer host-compatible styling and semantic design tokens where supported.

At minimum, ensure the component remains readable in both light and dark contexts.


75. Accessibility

Our first widget should establish good habits.

Use:

Semantic HTML
Readable labels
Sufficient contrast
Keyboard-safe controls
Meaningful headings

Do not encode information only through color.

For example, the weighted pipeline value should have a textual label.


76. The Widget Should Not Fight ChatGPT

A ChatGPT component is not a conventional full-page website.

Avoid:

Large navigation header
Huge logo
Cookie banner
Website footer
Marketing sections
Side navigation

The conversation already provides application context.

The component should focus on the task.


77. Quorentra Branding

A subtle identifier is enough:

QUORENTRA
Sales Pipeline

The UI should feel like part of Quorentra without consuming valuable space with branding.

The CRM data is the priority.


78. ChatGPT Narration Plus UI

The ideal result might be:

Your EUR pipeline currently contains four open
opportunities worth €385,000, with a weighted
pipeline of €161,000.

followed by the visual component.

The prose provides interpretation.

The widget provides scanning and visual structure.


79. Avoid Duplicating Everything

If the widget already displays:

Open Opportunities 4
Total €385,000
Weighted €161,000

ChatGPT does not need to repeat every number multiple times.

The model response should complement the UI.

For example:

Your weighted pipeline is currently about 42% of the total open pipeline.

Then the card provides the detailed values.


80. Future Interaction

The first pipeline widget is read-only.

Later we could add:

View Opportunities
Filter by Stage
Change Currency
Open Pipeline Details

These actions could call additional MCP tools.

But we deliberately avoid that in the first version.


81. Why Read-Only UI First?

Interactive buttons introduce another execution path:

Widget
Tool Call
Quorentra

That path deserves separate testing.

For Part 16 we only need:

Quorentra
Widget

This keeps the milestone focused.


82. UI-Initiated Tool Calls

Later, a widget can request tools through the MCP Apps bridge.

Conceptually:

User clicks
"View Opportunities"
Widget
tools/call
Quorentra MCP
list_opportunities
Structured Result
Widget updates

This turns the component from visualization into an interactive application.

That will be a future step.


83. Do Not Duplicate REST Calls From the Widget

Avoid:

Widget JavaScript
fetch("/api/v1/opportunities")

if the same capability is already available through MCP.

Prefer:

Widget
MCP Tool
Quorentra

This preserves:

Authentication
TenantContext
RBAC
Tool auditing
Interface consistency

84. One Governed Capability Layer

The long-term architecture should be:

             REST
               │
               ▼
        Application Services
               ▲
               │
              MCP
               ▲
               │
        ┌──────┴──────┐
        │             │
     ChatGPT        Widgets

Not several independent paths into the database.


85. Content Security Policy

Hosted components operate under security constraints.

If a future widget needs external resources, such as:

API domains
Image domains
Map services
External assets

those origins may need to be declared appropriately in UI resource metadata.

Our pipeline widget needs none of these.

That is another reason it makes a good first component.


86. No External Dependencies at Runtime

For Part 16, aim for:

No external fonts
No external APIs
No remote images
No third-party iframe
No analytics scripts

The widget needs only the structured pipeline result.

This gives us a very small security surface.


87. Build the Component

From:

quorentra/chatgpt-ui

run:

npm run build

Verify:

dist/pipeline.js

exists.

The backend resource loader can now incorporate this artifact.


88. Register the Resource

Start the Quorentra MCP server and verify the UI resource is discoverable.

Conceptually:

ui://quorentra/pipeline.html

should resolve successfully.

If the resource cannot be loaded, do not test ChatGPT yet.


89. Test the Render Tool

Call:

render_pipeline_summary

with:

{
"currency": "EUR",
"open_opportunities": 4,
"total_pipeline": "385000.00",
"weighted_pipeline": "161000.00"
}

Verify the tool result includes:

structuredContent

and references the correct UI resource.


90. Test the Component Independently

Before ChatGPT, test:

Resource loads
JavaScript executes
React mounts
Tool result is received
Values render
No console errors occur

Again, isolate boundaries.


91. Then Test Through ChatGPT

Now ask:

Show my EUR pipeline.

Expected sequence:

ChatGPT
get_pipeline_summary
{
EUR,
4,
385000,
161000
}
render_pipeline_summary
Quorentra Pipeline Widget

If successful, we have our first complete ChatGPT-hosted Quorentra interface.


92. Test Another Currency

Ask:

Show my USD pipeline.

Expected:

get_pipeline_summary
currency = USD

followed by the same reusable render component.

The UI must not be hard-coded for EUR.


93. Test an Empty Pipeline

For a tenant with no GBP Opportunities:

Show my GBP pipeline.

Expected:

No open GBP opportunities.

with a clean empty-state component.

This proves the UI handles legitimate zero-data scenarios.


94. Test Tenant Isolation Again

Create:

Tenant A
EUR pipeline €385,000
Tenant B
EUR pipeline €5,000,000

Connect as Tenant A.

Ask:

Show my EUR pipeline.

The widget must display:

€385,000

Never:

€5,385,000

The UI does not change our security requirements.


95. Test Viewer Role

Connect as:

Viewer

Because:

get_pipeline_summary

requires only:

opportunities.read

the widget should render.

This proves ChatGPT UI respects Quorentra RBAC.


96. Test Unauthenticated Access

Without authentication, the pipeline tool should fail before rendering authorized CRM data.

The widget should never receive another user’s cached pipeline.

Authentication remains a server-side prerequisite.


97. Do Not Cache Tenant Data Globally

Avoid server-side code such as:

last_pipeline_result = result

shared across requests.

That could create catastrophic cross-tenant leakage.

All request data must remain properly scoped.


98. Widget State Is Not Business State

Another important principle:

Expanded section
Selected tab
Display mode

may be widget state.

But:

Opportunity stage
Pipeline amount
Company ownership
Contact data

is business state.

Business state belongs in Quorentra.

Do not make the iframe authoritative for CRM data.


99. If the Widget Reloads

The component should be able to reconstruct itself from authoritative tool results.

It should not depend on an undocumented browser session for critical CRM information.

This makes the application more robust across host environments.


100. Model Context and UI State

Later, if a user selects something inside a component that ChatGPT should know about, the MCP Apps bridge provides mechanisms for updating model-visible context.

For example:

User selects:
Contoso

The component could inform the host that:

Selected company = Contoso

This enables conversational follow-ups.

But the pipeline summary does not need this yet.


101. Example Future Interaction

Imagine:

Pipeline Widget
[Proposal: €75,000]

The user clicks Proposal.

The widget could update model context:

User selected the Proposal stage.

Then the user says:

Show me those deals.

ChatGPT could understand what “those” refers to.

This is where UI and conversation begin sharing context.


102. Why This Matters for CRM

CRM applications contain many referential interactions:

this company
these opportunities
that contact
those deals
this quarter
that pipeline stage

A strong ChatGPT-native UI needs to preserve enough state for natural follow-up conversation.

We will introduce this gradually.


103. First Component Boundary

For Part 16, however, our component remains:

Input
Pipeline Summary
Output
Visual Pipeline Summary
Interactions
None
Mutations
None

This is intentionally small.


104. Add Backend Tests

Create:

tests/mcp/test_pipeline_ui.py

Test:

pipeline UI resource exists
render tool exists
render tool is read-only
render tool accepts valid data
structuredContent matches input
resource URI is correct

This ensures the MCP/UI contract remains stable.


105. Add Security Tests

Verify:

render tool does not accept tenant_id
render tool does not access database
render tool does not bypass authentication
data tool remains tenant-scoped

Remember:

render_pipeline_summary

is presentation.

The authoritative data still comes from the secure data tool.


106. Add Contract Tests

Given:

{
"currency": "EUR",
"open_opportunities": 4,
"total_pipeline": "385000.00",
"weighted_pipeline": "161000.00"
}

the structured render result should preserve those values exactly.

No rounding should occur before presentation.

Formatting belongs to the UI.


107. Add Frontend Build to Development Workflow

Our development sequence now becomes:

cd chatgpt-ui
npm run build
cd ..\backend
python -m pytest
python -m uvicorn app.main:app --reload

plus the MCP server startup process established in Part 15.

Later we can automate this.

For now, explicit commands make each build stage understandable.


108. Do We Need Vite Yet?

No.

For one small embedded component:

React
+
TypeScript
+
esbuild

is enough.

We can introduce Vite or a more sophisticated frontend development environment when the number of components justifies it.

Avoid tooling complexity before product complexity requires it.


109. The Quorentra UI Library Begins Here

Even this tiny component establishes patterns for:

Typography
Spacing
Metric cards
Loading states
Empty states
Error states
Currency formatting
Host integration
Tool-result hooks

Future widgets should reuse these patterns.

Eventually:

chatgpt-ui/src/shared/

may contain a small Quorentra component system.


110. Future UI Components

Likely future components include:

Company Card
Contact Card
Contact List
Opportunity Card
Opportunity Table
Pipeline Summary
Pipeline by Stage
Task List
Activity Timeline
Meeting Summary
AI Insight Card

We should build them incrementally as CRM capabilities appear.


111. Do Not Build a Full Dashboard Yet

It would be tempting to jump immediately to:

Executive Dashboard
Sales Dashboard
Pipeline Funnel
Forecast Charts
Activity Charts
AI Recommendations

That would violate our modular strategy.

We first prove:

one tool
+
one resource
+
one component

Then expand.


112. ChatGPT Changes the Dashboard Concept

A conventional CRM requires users to open a dashboard and inspect whatever widgets were predefined.

A ChatGPT-native CRM can generate the relevant interface in response to intent.

User:

Show my pipeline.

gets:

Pipeline Widget

User:

Show Contoso.

gets:

Company Card

User:

Show proposal-stage opportunities.

gets:

Opportunity Table

The interface becomes contextual.


113. UI on Demand

This suggests a different product model:

Traditional CRM
User
Navigate
Screen
Data

versus:

ChatGPT-Native CRM
User Intent
Tool Selection
Data
Appropriate UI

The application surface is assembled around the task.

That is one of the most interesting aspects of the Quorentra architecture.


114. Conversation Remains the Primary Orchestrator

We are not replacing ChatGPT with React.

React handles:

Structured visual presentation

ChatGPT handles:

Intent
Reasoning
Tool orchestration
Conversation
Explanation

Quorentra handles:

Business rules
Security
CRM state
Persistence

Each layer has a distinct responsibility.


115. The Three-Layer Model

We can now describe Quorentra as:

┌─────────────────────────────────────────┐
│ EXPERIENCE │
│ │
│ ChatGPT Conversation │
│ ChatGPT-hosted UI │
└────────────────────┬────────────────────┘
┌─────────────────────────────────────────┐
│ CAPABILITIES │
│ │
│ MCP Tools │
│ REST API │
└────────────────────┬────────────────────┘
┌─────────────────────────────────────────┐
│ CORE │
│ │
│ Authentication │
│ TenantContext │
│ RBAC │
│ CRM Services │
│ PostgreSQL │
└─────────────────────────────────────────┘

This is becoming a mature platform architecture.


116. Version Update

Part 16 adds our first ChatGPT-hosted visual interface.

Update:

app/core/constants.py

from:

APP_VERSION = "0.2.0"

to:

APP_VERSION = "0.3.0"

This is another minor milestone because Quorentra now has a new user-experience surface.


117. Quorentra 0.3.0

Our status becomes:

Platform
├── FastAPI ✓
├── PostgreSQL ✓
├── SQLAlchemy ✓
└── Alembic ✓
Identity
├── Organizations ✓
├── Users ✓
├── Memberships ✓
├── Authentication ✓
└── JWT ✓
Security
├── TenantContext ✓
├── Tenant Isolation ✓
├── RBAC ✓
└── Permissions ✓
CRM
├── Companies ✓
├── Contacts ✓
└── Opportunities ✓
Sales
├── Pipeline ✓
├── Probability ✓
├── Pipeline Value ✓
└── Weighted Pipeline ✓
Interfaces
├── REST ✓
├── MCP ✓
└── ChatGPT ✓
MCP
├── Read-Only CRM Tools ✓
├── Structured Results ✓
├── Tenant-Aware Tool Calls ✓
└── Render Tool Foundation ✓
ChatGPT UI
├── React Component Foundation ✓
├── MCP Apps Bridge ✓
├── Pipeline UI Resource ✓
├── Pipeline Summary Widget ✓
├── Loading State ✓
├── Empty State ✓
└── Error State ✓
Mutation Tools -
Interactive Tool Calls -
Company UI -
Contact UI -
Opportunity UI -
Activities -
Tasks -
AI Intelligence -

Quorentra is no longer only conversational.

It now has a native visual surface inside ChatGPT.


118. Acceptance Criteria

Part 16 is complete when:

✓ Part 15 regression suite remains green
✓ chatgpt-ui project exists
✓ React is configured
✓ TypeScript is configured
✓ component build works
✓ pipeline bundle is generated
✓ PipelineSummary TypeScript contract exists
✓ currency formatting works
✓ locale-aware formatting works
✓ weighted coverage works
✓ zero-total handling works
✓ pipeline component exists
✓ metric component exists
✓ loading state exists
✓ empty state exists
✓ error state exists
✓ component is responsive
✓ MCP Apps tool-result handling exists
✓ structuredContent drives rendering
✓ widget does not parse model prose
✓ host bridge code is isolated
✓ pipeline UI resource exists
✓ stable ui:// resource URI exists
✓ resource returns component HTML
✓ component bundle loads
✓ render_pipeline_summary exists
✓ data and rendering remain separated
✓ get_pipeline_summary remains reusable
✓ render tool performs no CRM calculations
✓ render tool does not query PostgreSQL
✓ tool result values match REST values
✓ widget values match MCP values
✓ currency is not hard-coded
✓ Tenant A sees only Tenant A pipeline
✓ Viewer can render authorized pipeline
✓ unauthenticated access does not expose CRM data
✓ no tenant ID is trusted from widget input
✓ no secrets reach the browser
✓ no global tenant-data cache exists
✓ pipeline resource tests pass
✓ render-tool tests pass
✓ frontend contract tests pass
✓ existing CRM regression tests pass
✓ "Show my EUR pipeline" renders the widget
✓ "Show my USD pipeline" uses the same widget
✓ empty pipeline renders correctly
✓ Quorentra reports version 0.3.0

Most importantly:

A Quorentra business capability can now move securely from PostgreSQL through the CRM core, through MCP, into an interactive visual surface inside ChatGPT.


119. What We Have Achieved

Part 15 gave ChatGPT access to Quorentra’s capabilities.

Part 16 gives those capabilities a visual application surface.

We now have:

User
Natural Language
ChatGPT
MCP Tool
Quorentra Core
Structured Data
MCP UI Resource
React Component

This is significantly different from simply adding a chatbot to a CRM.


120. The CRM Is Moving Into the Conversation

Traditional architecture:

CRM Application
└── AI Assistant

Quorentra is exploring:

ChatGPT
├── Conversation
├── CRM Tools
└── CRM UI
Quorentra Core

The conversational environment itself becomes a major CRM workspace.


121. But the Backend Remains Independent

This remains non-negotiable.

If ChatGPT is unavailable tomorrow:

Quorentra Core

still exists.

REST still exists.

PostgreSQL still contains the authoritative CRM state.

Business rules still work.

Tenant isolation still works.

RBAC still works.

The ChatGPT experience is an interface, not the database or domain model.


122. Why This Is a Strong Modular Architecture

We can now replace or extend any interface independently.

For example:

React Web App
Mobile App
Microsoft Teams
Slack
Another AI Client
Automation Agent

could eventually use the same Quorentra capabilities.

The domain core does not care whether the request began as:

HTTP

or:

natural language

That is exactly what modularity should achieve.


123. What Should Come Next?

We now have:

Pipeline Data Tool
+
Pipeline Render Tool
+
Pipeline Widget

The next useful step is not another static card.

We should prove that a ChatGPT-hosted Quorentra component can become interactive.

A natural next workflow is:

Pipeline Summary
View Opportunities
Opportunity List

This introduces:

Widget-initiated tool calls
Dynamic UI updates
Opportunity list rendering
Model/UI context coordination

without yet introducing mutations.

That keeps the next step safe and modular.


124. Next Article

In Part 17, we will build:

Interactive Opportunities in ChatGPT — Tool Calls, Opportunity Lists, and Drill-Down Navigation

The goal will be to evolve:

┌─────────────────────────────┐
│ Sales Pipeline │
│ │
│ Open Opportunities 4 │
│ Total €385,000 │
│ Weighted €161,000 │
└─────────────────────────────┘

into:

┌─────────────────────────────┐
│ Sales Pipeline │
│ │
│ Open Opportunities 4 │
│ Total €385,000 │
│ Weighted €161,000 │
│ │
│ [View Opportunities] │
└─────────────────────────────┘

When the user selects:

View Opportunities

the Quorentra UI will invoke a governed MCP capability:

list_opportunities

and update to something conceptually like:

┌──────────────────────────────────────────────┐
│ Open Opportunities │
├──────────────────────────────────────────────┤
│ Microsoft 365 Migration │
│ Contoso │
│ Proposal · €75,000 · 60% │
├──────────────────────────────────────────────┤
│ Azure Modernization │
│ Contoso │
│ Discovery · €150,000 · 40% │
├──────────────────────────────────────────────┤
│ Security Assessment │
│ Fabrikam │
│ Negotiation · €40,000 · 80% │
├──────────────────────────────────────────────┤
│ Data Platform Upgrade │
│ Northwind │
│ Qualification · €120,000 · 20% │
└──────────────────────────────────────────────┘

We will introduce:

Widget → MCP tool calls
Opportunity list UI
Opportunity cards
Loading transitions
Tool-call errors
Pagination
Company context
Stage presentation
Currency formatting
Navigation
Model-visible UI state
Host-backed routing
Responsive list layouts
Tool-call authorization
Tenant isolation
Read-only interaction

This will move Quorentra from:

ChatGPT
+
Static CRM Visualization

to:

ChatGPT
+
Interactive CRM Application

without yet giving the AI permission to change CRM data.

That is the next safe modular step in building Quorentra as a ChatGPT-native CRM.

Quorentra

Contact

Menu

Designed with WordPress

Discover more from Quorentra

Subscribe now to keep reading and get access to the full archive.

Continue reading