Quorentra

Quorentra CRM MCP Foundation: Building from Zero — Part 15

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

Building the first Model Context Protocol server and exposing the Quorentra CRM core as safe, typed, read-only tools for ChatGPT.

Quorentra MCP Foundation: Building from Zero — Part 15
Quorentra MCP Foundation: Building from Zero — Part 15

1. Introduction

Quorentra has reached an important architectural boundary.

At the end of Part 14, we had:

Quorentra 0.1.0

with a working Minimal Viable CRM core:

Users
Organizations
Memberships
Authentication
TenantContext
RBAC
Companies
Contacts
Opportunities
Sales Pipeline

The application can already answer questions such as:

Which companies exist?
Who are the contacts at Contoso?
Which opportunities are open?
Which opportunities are in proposal?
What is the EUR pipeline?
What is the weighted pipeline?

But those capabilities currently live behind a conventional application interface.

Conceptually:

Client
REST API
Quorentra Core

Starting in Part 15, we add another interface:

ChatGPT
MCP
Quorentra Core

This is the beginning of the ChatGPT-native phase of the project.


2. What Are We Building?

We will create Quorentra’s first:

MCP Server

MCP stands for:

Model Context Protocol

The MCP server will expose carefully selected Quorentra capabilities as tools.

Our first tools will be read-only:

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

The important point is that we are not rebuilding the CRM inside MCP.

The architecture will be:

MCP Tool
Quorentra Application Capability
Tenant Security
Repository
PostgreSQL

MCP is an adapter around the CRM core.


3. A Note About OpenAI Terminology

The OpenAI ecosystem continues to evolve.

Earlier versions of this project referred heavily to:

Apps SDK

The current OpenAI developer documentation organizes this area around:

Plugins
├── Skills
├── MCP Servers
├── Authentication
├── Optional UI
└── Packaging

The underlying architectural idea remains exactly what Quorentra needs:

expose application capabilities through MCP so ChatGPT can discover and invoke them.

The OpenAI examples also demonstrate MCP servers returning structured results and optionally connecting those results to interactive UI components.

For Quorentra, we will start with MCP tools.

UI comes later.


4. Why MCP?

Without MCP, we could theoretically build something like:

User
ChatGPT
Custom Prompt Logic
HTTP Requests
Quorentra

That would work technically.

But it would force us to invent our own conventions for:

Tool discovery
Tool descriptions
Input schemas
Output schemas
Invocation
Errors
Capabilities
Model integration

MCP provides a standardized tool boundary.

Conceptually:

ChatGPT
│ discovers
MCP Tools
│ invokes
Quorentra

That is a much cleaner architecture.


5. MCP Is Not the AI

This distinction is important.

MCP does not replace ChatGPT.

MCP is the protocol connecting ChatGPT to Quorentra.

Think of the layers as:

User
ChatGPT
MCP Client
Quorentra MCP Server
Quorentra Core

ChatGPT performs:

Language understanding
Intent interpretation
Reasoning
Tool selection
Response generation

Quorentra performs:

Authentication
Tenant resolution
Authorization
Business rules
CRM operations
Persistence

MCP connects them.


6. The Most Important Architectural Rule

We established this in Part 14:

ChatGPT is a client of Quorentra.

It is not the business layer.

Therefore:

ChatGPT
MCP Tool
Quorentra Application Layer
PostgreSQL

not:

ChatGPT
Database

and not:

ChatGPT
Special AI Business Logic
Database

The existing Quorentra application remains authoritative.


7. Why This Architecture Matters

Suppose the user asks:

What is my current EUR pipeline?

ChatGPT should not calculate the pipeline from arbitrary database rows.

Instead:

User
ChatGPT
get_pipeline_summary
OpportunityService
Tenant-scoped Opportunities
Pipeline Calculation

The same business rules apply whether the caller is:

REST API
React
MCP
ChatGPT
Mobile App
Integration

That is the value of the modular architecture we have built.


8. Our Architecture Before Part 15

Quorentra currently looks like:

             ┌──────────────────┐
             │      Client      │
             └────────┬─────────┘
                      │
                      ▼
             ┌──────────────────┐
             │     FastAPI      │
             └────────┬─────────┘
                      │
                      ▼
             ┌──────────────────┐
             │ Authentication   │
             └────────┬─────────┘
                      │
                      ▼
             ┌──────────────────┐
             │  TenantContext   │
             └────────┬─────────┘
                      │
                      ▼
             ┌──────────────────┐
             │       RBAC       │
             └────────┬─────────┘
                      │
                      ▼
        ┌─────────────────────────────┐
        │    Application Services     │
        ├─────────────────────────────┤
        │ CompanyService              │
        │ ContactService              │
        │ OpportunityService          │
        └──────────────┬──────────────┘
                       │
                       ▼
                ┌──────────────┐
                │ PostgreSQL   │
                └──────────────┘

9. Architecture After Part 15

We will add:

                  ┌──────────────────┐
                  │     ChatGPT      │
                  └────────┬─────────┘
                           │
                           ▼
                  ┌──────────────────┐
                  │       MCP        │
                  └────────┬─────────┘
                           │
                           ▼
                  ┌──────────────────┐
                  │ Quorentra Tools  │
                  └────────┬─────────┘
                           │
                           ▼
             ┌──────────────────────────┐
             │   Application Services   │
             ├──────────────────────────┤
             │ CompanyService           │
             │ ContactService           │
             │ OpportunityService       │
             └─────────────┬────────────┘
                           │
                           ▼
                    ┌────────────┐
                    │ PostgreSQL │
                    └────────────┘

REST continues to exist.

We are adding an interface, not replacing one.


10. The Dual-Interface Architecture

The result becomes:

                 ┌──────────────┐
                 │ REST Clients │
                 └───────┬──────┘
                         │
                         ▼
                    FastAPI
                         │
                         ▼
                Application Core
                         ▲
                         │
                    MCP Server
                         ▲
                         │
                      ChatGPT

Both paths converge on the same application capabilities.

This is one of the most important design decisions in the series.


11. Why Start With Read-Only Tools?

We could immediately expose:

create_company
update_company
delete_company
create_contact
update_contact
delete_contact
create_opportunity
update_opportunity
delete_opportunity

We will not.

Our first MCP milestone will be read-only.

Why?

Because:

Reading CRM data

has significantly lower operational risk than:

Changing CRM data

This lets us prove:

MCP connectivity
Tool discovery
Authentication
Tenant propagation
RBAC
Input schemas
Output schemas
ChatGPT tool selection

without risking destructive actions.


12. Our First MCP Tool Set

The initial tool catalog is:

Companies
├── list_companies
└── get_company
Contacts
├── list_contacts
├── get_contact
└── list_company_contacts
Opportunities
├── list_opportunities
├── get_opportunity
├── list_company_opportunities
├── list_opportunities_by_stage
└── get_pipeline_summary

Ten tools are enough.

Do not expose every backend operation simply because it exists.


13. Tools Should Represent User Intent

A common mistake is designing tools around database tables.

For example:

query_company_table
query_contact_table
query_opportunity_table

These are poor tool names.

Instead use business capabilities:

list_companies
get_company
list_company_contacts
list_company_opportunities
get_pipeline_summary

These express intent.

That helps both:

Developers

and:

Models

understand what each tool is for.


14. Tool Design Is Part of AI Design

When building REST APIs, developers often think primarily about:

Routes
Resources
HTTP methods
Status codes

When building model-facing tools, we also need to think about:

Tool name
Tool description
Input schema
Output schema
Read/write behavior
Discoverability
Ambiguity
Result size

A technically correct tool can still be difficult for a model to use correctly.

Tool design therefore becomes part of the AI architecture.


15. Good Tool Names

Prefer:

list_companies
get_company
list_company_contacts
get_pipeline_summary

Avoid names such as:

companies
company_api
crm_query
execute_company
fetch_stuff
pipeline

Tool names should be:

Specific
Action-oriented
Predictable
Unambiguous

16. Good Tool Descriptions

The tool name alone is not enough.

For example:

get_pipeline_summary

should have a description similar to:

Return the open sales pipeline summary for the
authenticated user's active Quorentra organization
and requested currency.

This tells the model:

What it does
What scope it uses
What input matters
What result it returns

17. Tool Descriptions Should Not Be Marketing Copy

Avoid:

Use Quorentra's revolutionary AI-powered sales
technology to unlock incredible insights into your
business pipeline.

That is useless to the model.

Prefer precise operational language:

Return the number, total value, and weighted value
of open opportunities for a specified currency.

Tool metadata is part of the machine interface.

Write it accordingly.


18. MCP Tool Contracts

Conceptually, every tool has:

Name
Description
Input Schema
Output
Annotations
Handler

For example:

Tool
get_pipeline_summary
Input
currency: string
Output
currency
open_opportunities
total_pipeline
weighted_pipeline

The model does not need to know how PostgreSQL calculates the result.

It needs a stable capability contract.


19. MCP Tool Discovery

One important MCP concept is tool discovery.

The MCP server advertises the tools it supports.

Conceptually:

ChatGPT
List Tools
Quorentra MCP Server
list_companies
get_company
list_contacts
...
get_pipeline_summary

The model can then determine which capability best matches the user’s request.


20. Tool Invocation

Suppose the user asks:

What is my EUR pipeline?

The model may select:

get_pipeline_summary

with:

{
"currency": "EUR"
}

The MCP server executes the tool.

Quorentra returns structured data.

ChatGPT then turns that structured result into a useful response.


21. Structured Results Matter

Suppose Quorentra returns:

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

ChatGPT can explain:

Your current EUR pipeline contains four open
opportunities worth €385,000 in total, with a
weighted value of €161,000.

The application returns facts.

The model handles conversation.

That separation is exactly what we want.


22. Do Not Return Prewritten AI Responses

Avoid making the tool return:

{
"message": "Congratulations! Your amazing sales
pipeline currently contains four exciting deals..."
}

That mixes application logic and presentation.

Prefer:

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

Let ChatGPT decide how to explain it.


23. Tool Output Should Be Compact

Models have context limits.

Tool results should therefore avoid returning unnecessary fields.

For example, list_companies does not necessarily need:

Every database field
Internal metadata
Audit timestamps
ORM details
Internal tenant identifiers

A useful response might contain:

id
name
website
industry

Tool output is not necessarily identical to the REST response.


24. MCP Is an Adapter Layer

This gives us another architectural principle:

MCP schemas may adapt application schemas for model consumption.

For example:

Database Model
Application Schema
MCP Tool Schema

Each layer has a different purpose.

Do not expose ORM models directly.


25. Starting Checkpoint

Before touching MCP, verify Quorentra 0.1.0.

From:

quorentra/backend

run:

python -m pytest

Then:

python -m uvicorn app.main:app --reload

Check:

GET /api/v1/health

Expected:

{
"status": "healthy",
"service": "quorentra-api",
"version": "0.1.0",
"database": "connected"
}

The CRM core must remain green before adding another interface.


26. Where Should MCP Live?

There are several architectural possibilities.

We could build:

quorentra/
├── backend/
└── mcp-server/

as completely separate applications.

Or:

backend/
└── app/
└── mcp/

inside the existing Python project.

For the MVO, we will keep MCP close to the backend.

A practical structure is:

backend/
├── app/
│ ├── api/
│ ├── core/
│ ├── db/
│ ├── modules/
│ └── mcp/
│ ├── __init__.py
│ ├── server.py
│ ├── context.py
│ ├── schemas.py
│ └── tools/
└── tests/

This allows MCP tools to reuse the existing application layer cleanly.


27. Create the MCP Package

From:

quorentra/backend

create:

mkdir app\mcp
mkdir app\mcp\tools
New-Item app\mcp\__init__.py -ItemType File
New-Item app\mcp\server.py -ItemType File
New-Item app\mcp\context.py -ItemType File
New-Item app\mcp\schemas.py -ItemType File
New-Item app\mcp\tools\__init__.py -ItemType File
New-Item app\mcp\tools\companies.py -ItemType File
New-Item app\mcp\tools\contacts.py -ItemType File
New-Item app\mcp\tools\opportunities.py -ItemType File

Our structure becomes:

app/mcp/
├── __init__.py
├── context.py
├── schemas.py
├── server.py
└── tools/
├── __init__.py
├── companies.py
├── contacts.py
└── opportunities.py

28. Install the MCP SDK

Use the current Python MCP SDK recommended by the MCP/OpenAI ecosystem.

Because SDK package names and APIs can evolve, verify the current installation command against the documentation before executing it.

For the project environment, the workflow is conceptually:

cd quorentra\backend
.\.venv\Scripts\Activate.ps1
pip install <current-mcp-python-package>

Then update the project’s dependency file.

The important architectural requirement is:

MCP dependency

belongs to the backend environment that hosts our tool adapter.


29. Why We Avoid Hard-Coding a Stale SDK API

MCP and ChatGPT integration are moving faster than:

FastAPI
SQLAlchemy
PostgreSQL

A tutorial series should distinguish between:

Stable architecture

and:

Fast-moving SDK syntax

The architecture in this article should remain valid even if the exact MCP SDK import changes.

Whenever implementing the code, check the current OpenAI and MCP documentation.


30. Create the MCP Server

Open:

app/mcp/server.py

The exact current SDK syntax should follow the installed MCP package.

Conceptually, we need:

mcp = MCPServer(
name="Quorentra CRM"
)

The server’s responsibility is to:

Register tools
Expose tool metadata
Accept tool calls
Execute handlers
Return structured results

It should not contain CRM business rules.


31. Keep server.py Small

Avoid:

server.py
├── MCP setup
├── Company queries
├── Contact queries
├── Opportunity queries
├── Pipeline calculations
├── Authentication logic
└── Everything else

Instead:

server.py
├── register_company_tools()
├── register_contact_tools()
└── register_opportunity_tools()

The modular structure should continue into MCP.


32. MCP Tool Architecture

Our internal structure becomes:

MCP Server
├── Company Tools
│ ↓
│ CompanyService
├── Contact Tools
│ ↓
│ ContactService
└── Opportunity Tools
OpportunityService

The tools adapt MCP calls to application-service calls.


33. Do MCP Tools Call REST?

There are two possible designs:

MCP
HTTP
FastAPI REST
Service

or:

MCP
Service

Because our MCP server currently lives inside the Quorentra backend codebase, the second approach is cleaner.

We can call the application services directly.

This avoids unnecessary internal HTTP calls.


34. When Would MCP Call REST Instead?

If we later deploy:

Quorentra API

and:

Quorentra MCP Server

as separate services, the MCP server may call the Quorentra API over HTTPS.

That architecture can provide:

Independent deployment
Clear service boundaries
Separate scaling
Network-level security

But it is unnecessary complexity for the MVO.

Start modularly inside one application.

Separate later if operational needs justify it.


35. The Authentication Problem

Our REST API currently receives:

JWT
+
X-Organization-ID

and creates:

TenantContext

MCP tools need equivalent identity information.

We must never create tools that simply do:

tenant_id = arguments["tenant_id"]

and trust it.

That would destroy our security model.


36. Never Trust a Tool-Supplied Tenant ID

A model-facing tool should not allow:

{
"organization_id": "someone-elses-tenant"
}

and then query that organization.

Tenant identity must come from authenticated context.

The same rule from our REST API remains:

The caller may request an active organization, but Quorentra must verify membership before constructing TenantContext.


37. MCP Context

Create:

app/mcp/context.py

Its job is conceptually to resolve:

Authenticated User
+
Active Organization
+
Membership
+
Permissions
=
TenantContext

The exact mechanism will depend on how authentication is configured between ChatGPT and the Quorentra MCP server.

For now, we define the boundary.


38. Tool Execution Context

Conceptually:

class MCPRequestContext:
user_id: UUID
organization_id: UUID
tenant: TenantContext

But remember:

user_id
organization_id

must come from validated authentication context.

They are not arbitrary tool parameters.


39. Authentication Comes Before Tools

The execution sequence must be:

MCP Request
Authenticate
Resolve User
Resolve Active Organization
Validate Membership
Create TenantContext
Check Permission
Execute Tool

Not:

Execute Tool
Check security later

40. Tool-Level RBAC

Consider:

list_companies

It requires:

companies.read

Therefore the tool should eventually enforce:

Authenticated
+
Valid TenantContext
+
companies.read

Similarly:

list_contacts

requires:

contacts.read

and:

get_pipeline_summary

requires:

opportunities.read

MCP does not bypass Quorentra permissions.


41. Define Tool Permissions Explicitly

A useful internal mapping is:

list_companies
→ companies.read
get_company
→ companies.read
list_contacts
→ contacts.read
get_contact
→ contacts.read
list_company_contacts
→ contacts.read
list_opportunities
→ opportunities.read
get_opportunity
→ opportunities.read
list_company_opportunities
→ opportunities.read
list_opportunities_by_stage
→ opportunities.read
get_pipeline_summary
→ opportunities.read

Later mutation tools will map to:

*.create
*.update
*.delete

42. Read-Only Tool Annotation

MCP tooling can expose metadata indicating that a tool is read-only.

For our initial tools, that is valuable.

Conceptually:

readOnlyHint = true

for:

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

This communicates tool behavior to compatible clients.


43. Why Tool Annotations Matter

The model should know the difference between:

get_company

and:

delete_company

before executing them.

Metadata can help describe whether a tool:

Reads data
Mutates data
Has destructive effects
Is idempotent

We should use these semantics wherever supported by the current MCP SDK.


44. Create MCP Output Schemas

Open:

app/mcp/schemas.py

We do not necessarily want to return the complete REST schemas.

Create compact tool-oriented representations.

For example:

class MCPCompany(BaseModel):
id: UUID
name: str
website: str | None = None
industry: str | None = None

This is enough for many conversational workflows.


45. Contact Tool Schema

Conceptually:

class MCPContact(BaseModel):
id: UUID
company_id: UUID | None
first_name: str
last_name: str
email: str | None
job_title: str | None

Notice what is missing:

organization_id
created_at
updated_at
internal ORM state

unless those fields are specifically useful.


46. Opportunity Tool Schema

Conceptually:

class MCPOpportunity(BaseModel):
id: UUID
company_id: UUID
name: str
stage: str
amount: Decimal
currency: str
probability: int
expected_close_date: date | None

This gives ChatGPT the business information it needs.


47. Pipeline Tool Schema

Conceptually:

class MCPPipelineSummary(BaseModel):
currency: str
open_opportunities: int
total_pipeline: Decimal
weighted_pipeline: Decimal

This maps almost directly to the existing application schema.


48. Why Keep IDs?

ChatGPT may need IDs for follow-up operations.

Suppose the user asks:

Show my Contoso opportunities.

The model retrieves:

Opportunity ID
Company ID
Name
Stage
Amount

Later, when mutation tools exist, it may need:

opportunity_id

to update the correct record.

IDs are therefore useful machine-facing identifiers even if they are not displayed prominently to the user.


49. First Tool: list_companies

Open:

app/mcp/tools/companies.py

The conceptual tool contract is:

Name:
list_companies
Description:
List companies belonging to the authenticated
user's active Quorentra organization.
Input:
limit
offset
Output:
Company summaries
Permission:
companies.read
Behavior:
Read-only

This becomes our first model-facing CRM capability.


50. list_companies Input

Use bounded pagination.

For example:

limit
default = 25
maximum = 100
offset
default = 0
minimum = 0

Do not expose an unbounded:

return_every_company

operation.

AI-accessible APIs should be bounded by design.


51. list_companies Handler

Conceptually:

def list_companies(
context,
limit: int = 25,
offset: int = 0,
):
tenant = context.tenant
require_permission(
tenant,
"companies.read",
)
companies = company_service.list_companies(
tenant=tenant,
limit=limit,
offset=offset,
)
return [
MCPCompany.model_validate(company)
for company in companies
]

The exact code will depend on our service signatures and MCP SDK.

The architectural flow is what matters.


52. Notice What the Tool Does Not Do

It does not:

Read organization_id from arguments
Query SQL directly
Implement tenant filtering
Implement role logic
Calculate permissions itself

It delegates to existing Quorentra capabilities.

That keeps security centralized.


53. Second Tool: get_company

Contract:

Name:
get_company
Description:
Return a company by ID from the authenticated
user's active Quorentra organization.
Input:
company_id
Output:
Company
Permission:
companies.read
Behavior:
Read-only

The tenant-safe Company service already protects this operation.


54. Cross-Tenant Behavior

Suppose ChatGPT somehow receives a UUID belonging to another tenant.

It calls:

get_company

with that UUID.

The service performs:

Company.id
+
Current Tenant

lookup.

Expected result:

Company not found

The MCP layer does not weaken our tenant isolation.


55. Contact Tools

Next expose:

list_contacts
get_contact
list_company_contacts

The most interesting is:

list_company_contacts

because it represents a relationship-oriented capability.

User:

Who do we know at Contoso?

ChatGPT can:

Find Contoso
list_company_contacts
Return Contacts

This begins to feel like a real conversational CRM.


56. list_company_contacts

Contract:

Name:
list_company_contacts
Description:
List contacts associated with a company in the
authenticated user's active Quorentra organization.
Input:
company_id
limit
offset
Output:
Contact summaries
Permission:
contacts.read
Behavior:
Read-only

The existing Contact service validates the Company relationship.


57. Opportunity Tools

Our opportunity tool set is:

list_opportunities
get_opportunity
list_company_opportunities
list_opportunities_by_stage
get_pipeline_summary

These give ChatGPT enough information to answer useful sales questions.


58. list_opportunities

Contract:

Name:
list_opportunities
Description:
List sales opportunities in the authenticated
user's active Quorentra organization.
Input:
limit
offset
Output:
Opportunity summaries
Permission:
opportunities.read
Behavior:
Read-only

Again, keep results bounded.


59. list_opportunities_by_stage

Input:

stage

must use the same controlled stage values:

qualification
discovery
proposal
negotiation
won
lost

Do not create a second MCP-specific stage vocabulary.

The domain model remains authoritative.


60. Example Conversation

User:

Show me opportunities in negotiation.

ChatGPT determines:

Tool:
list_opportunities_by_stage
Input:
stage = negotiation

Quorentra might return:

[
{
"id": "...",
"company_id": "...",
"name": "Security Assessment",
"stage": "negotiation",
"amount": "40000.00",
"currency": "EUR",
"probability": 80
}
]

ChatGPT can then answer:

You currently have one opportunity in negotiation:
Security Assessment, worth €40,000 at an 80%
probability.

61. list_company_opportunities

This tool enables:

What deals do we have with Contoso?

The flow may become:

User
ChatGPT
list_companies
Resolve Contoso
list_company_opportunities
Return Opportunities
ChatGPT Response

This demonstrates multi-tool conversational reasoning.


62. get_pipeline_summary

This is one of our most valuable first tools.

Contract:

Name:
get_pipeline_summary
Description:
Return the number, total value, and weighted value
of open sales opportunities for a requested
currency in the authenticated user's active
Quorentra organization.
Input:
currency
Output:
currency
open_opportunities
total_pipeline
weighted_pipeline
Permission:
opportunities.read
Behavior:
Read-only

63. Example Pipeline Call

Input:

{
"currency": "EUR"
}

Output:

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

The tool should return facts.

ChatGPT creates the narrative.


64. Currency Validation

The MCP tool should reuse Quorentra’s existing currency rules.

For example:

eur

can normalize to:

EUR

But:

EUROPEAN

should fail validation.

Do not create inconsistent validation rules between REST and MCP.


65. Error Mapping

Application exceptions need clean MCP representations.

For example:

CompanyNotFoundError

should become a predictable tool failure.

Likewise:

ContactNotFoundError
OpportunityNotFoundError
PermissionDenied
AuthenticationRequired
InvalidTenant

The model should receive enough information to react correctly without exposing sensitive internals.


66. Do Not Return Stack Traces

Never return:

SQLAlchemy traceback
Python stack
Database connection details
File paths
Secrets
Internal configuration

to ChatGPT through a tool result.

Unexpected errors should be logged server-side.

The tool should return a safe failure.


67. Tool Error Categories

A useful conceptual taxonomy is:

AUTHENTICATION_REQUIRED
PERMISSION_DENIED
RESOURCE_NOT_FOUND
INVALID_ARGUMENT
CONFLICT
INTERNAL_ERROR

We may formalize these later.

For Part 15, consistency is more important than sophistication.


68. The Model Should Not See Another Tenant

Cross-tenant access should remain indistinguishable from nonexistent data.

For example:

get_company(
company_id="<tenant-b-company>"
)

from Tenant A should return:

RESOURCE_NOT_FOUND

not:

RESOURCE_BELONGS_TO_ANOTHER_TENANT

Our existing information-disclosure rule still applies.


69. Tool Result Size

Suppose the tenant has:

100,000 contacts

ChatGPT asks:

Show my contacts.

The MCP server must not return all 100,000.

Our bounded result strategy protects:

Latency
Memory
Model context
Cost
User experience

Pagination is therefore part of AI architecture.


70. Search Will Eventually Be Better Than Listing

Later we will introduce tools such as:

search_companies
search_contacts
search_opportunities

These will be more efficient for natural-language interaction.

For example:

Find Contoso.

should eventually use:

search_companies(query="Contoso")

rather than paging through all Companies.

But Part 15 keeps the first tool surface deliberately small.


71. Why Not Build Search Now?

Because we are proving:

MCP
+
Authentication
+
TenantContext
+
RBAC
+
Existing Services

Adding fuzzy search, ranking, semantic search, and embeddings would mix too many concerns.

The modular principle remains:

Build one new architectural capability at a time.


72. Register the Company Tools

Our MCP server should import:

app.mcp.tools.companies

and register:

list_companies
get_company

The exact registration API should follow the current MCP SDK.

Conceptually:

register_company_tools(mcp)

This keeps server startup declarative.


73. Register Contact Tools

Similarly:

register_contact_tools(mcp)

registers:

list_contacts
get_contact
list_company_contacts

74. Register Opportunity Tools

Then:

register_opportunity_tools(mcp)

registers:

list_opportunities
get_opportunity
list_company_opportunities
list_opportunities_by_stage
get_pipeline_summary

75. server.py Becomes Simple

Conceptually:

mcp = create_mcp_server()
register_company_tools(mcp)
register_contact_tools(mcp)
register_opportunity_tools(mcp)

That is what we want.

Infrastructure wiring belongs in the server.

Business behavior belongs elsewhere.


76. First MCP Startup

Once the server is configured, start it using the transport supported by the current MCP SDK.

During local development, verify that the server can:

Start
Advertise tools
Accept tool calls
Return structured results

before involving ChatGPT.

Debug one boundary at a time.


77. Test Tool Discovery First

The first MCP test should not ask:

What is my pipeline?

Instead verify:

Can an MCP client discover the Quorentra tools?

Expected catalog:

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

If discovery fails, business testing is premature.


78. Test list_companies

With authenticated tenant context, call:

list_companies

Expected:

[
{
"id": "...",
"name": "Contoso",
"website": "https://contoso.example"
},
{
"id": "...",
"name": "Fabrikam",
"website": "https://fabrikam.example"
}
]

Only the active tenant’s Companies should appear.


79. Test get_company

Call:

get_company

with Contoso’s ID.

Verify:

Contoso

is returned.

Then call it with a Company ID from another tenant.

Expected:

not found

80. Test Contacts

Call:

list_company_contacts

for Contoso.

Expected:

Alice Johnson
Robert Smith

No Contacts from other Companies or tenants should appear.


81. Test Opportunities

Call:

list_company_opportunities

for Contoso.

Expected:

Microsoft 365 Migration
Azure Modernization

Again, only the active tenant’s records.


82. Test Stage Filtering

Call:

list_opportunities_by_stage

with:

{
"stage": "proposal"
}

Verify only proposal-stage Opportunities appear.

Invalid stage:

almost_won

should fail schema validation.


83. Test Pipeline

Call:

get_pipeline_summary

with:

{
"currency": "EUR"
}

Verify the result matches:

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

The REST and MCP interfaces should agree because they use the same business layer.


84. REST/MCP Parity Test

This is a useful integration test.

For the same tenant:

REST Pipeline Summary

should equal:

MCP Pipeline Summary

Likewise:

REST Company

should represent the same domain entity as:

MCP Company

even if the MCP schema contains fewer fields.


85. Create MCP Tests

Add:

tests/mcp/

with:

tests/mcp/
├── __init__.py
├── test_company_tools.py
├── test_contact_tools.py
├── test_opportunity_tools.py
├── test_pipeline_tool.py
├── test_tool_permissions.py
└── test_tool_tenant_isolation.py

MCP becomes a first-class tested interface.


86. Test Tool Permissions

Viewer:

list_companies ✓
get_company ✓
list_contacts ✓
get_contact ✓
list_company_contacts ✓
list_opportunities ✓
get_opportunity ✓
list_company_opportunities ✓
get_pipeline_summary ✓

That is expected because these are read-only tools.

Later mutation tools will produce different permission matrices.


87. Test Unauthenticated Calls

Attempt tool execution without valid authentication.

Expected:

Authentication required

No CRM service should execute.

No tenant data should be returned.


88. Test Invalid Tenant Membership

Authenticate a user belonging to:

Organization A

then attempt to establish:

Organization B

without membership.

Expected:

Tenant context rejected

The tool should never execute.


89. Test Cross-Tenant Company Access

Tenant A calls:

get_company

with Tenant B’s Company ID.

Expected:

not found

This mirrors our REST security tests.


90. Test Cross-Tenant Contact Access

Tenant A calls:

get_contact

with Tenant B’s Contact ID.

Expected:

not found

91. Test Cross-Tenant Opportunity Access

Tenant A calls:

get_opportunity

with Tenant B’s Opportunity ID.

Expected:

not found

92. Test Cross-Tenant Pipeline Isolation

Suppose:

Tenant A Pipeline
€385,000
Tenant B Pipeline
€5,000,000

Tenant A calls:

get_pipeline_summary(EUR)

Expected:

€385,000

This test is essential.

AI interfaces must obey exactly the same tenant boundaries as conventional APIs.


93. The Security Principle

We can now state a stronger rule:

Every Quorentra interface is untrusted until authentication, tenant resolution, and authorization succeed.

That includes:

REST
MCP
ChatGPT
Future React UI
Mobile clients
Integrations
Automation
AI agents

No interface receives special privileges.


94. Connecting to ChatGPT

Once the MCP server works independently, the next step is making it reachable by ChatGPT.

Current ChatGPT development workflows support connecting remote MCP-backed applications.

A local development server therefore needs a secure path that ChatGPT can reach.

Historically, developers commonly used tunneling during development.

OpenAI also provides infrastructure for connecting private MCP servers without simply making them public.

The exact connection method should follow the current OpenAI developer documentation at implementation time.


95. Why ChatGPT Cannot Simply Call localhost

Your browser can access:

localhost

because it runs on your computer.

ChatGPT’s remote infrastructure cannot assume access to:

127.0.0.1

on your development machine.

Therefore:

Local MCP Server

needs an appropriate remote connection mechanism during ChatGPT testing.


96. Development Connectivity

Conceptually:

ChatGPT
Secure Remote Connection
Local Quorentra MCP Server
Local Quorentra Backend
Local PostgreSQL

This allows us to continue developing Quorentra locally while testing the ChatGPT experience.


97. Do Not Expose PostgreSQL

Only the MCP application endpoint should be reachable through the chosen development connection.

Do not expose:

PostgreSQL 5432

to the public internet.

Likewise, do not expose:

pgAdmin

unnecessarily.

The external boundary should remain the application layer.


98. First ChatGPT Test

Once connected, use a simple request:

List my companies.

Expected reasoning:

User asks for companies
ChatGPT identifies Quorentra capability
list_companies
MCP request
TenantContext
companies.read
CompanyService
PostgreSQL
Structured result
ChatGPT response

This will be a major project milestone.


99. Second ChatGPT Test

Ask:

Who do we know at Contoso?

The ideal future sequence is:

Find Contoso
Resolve Company ID
list_company_contacts
Return Contacts
Explain Result

At this point, if we do not yet have a dedicated search tool, ChatGPT may need to use list_companies to identify Contoso.

That is acceptable for the MVO.


100. Third ChatGPT Test

Ask:

What opportunities do we have with Contoso?

Expected:

Resolve Contoso
list_company_opportunities
Return Opportunity Data
ChatGPT Summary

Example response:

Contoso currently has two opportunities:
• Microsoft 365 Migration — €75,000, Proposal, 60%
• Azure Modernization — €150,000, Discovery, 40%

Now ChatGPT is acting as a conversational CRM interface.


101. Fourth ChatGPT Test

Ask:

What is my current EUR pipeline?

Expected tool:

get_pipeline_summary

Input:

{
"currency": "EUR"
}

Result:

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

ChatGPT can answer naturally.


102. This Is Our First ChatGPT-Native CRM Experience

Notice what the user did not do.

They did not:

Open CRM
Navigate to Opportunities
Select Pipeline
Choose Currency
Open Dashboard
Read Widget

They simply asked:

What is my current EUR pipeline?

That is the product shift we have been building toward.


103. Conversational Interaction Changes CRM UX

Traditional CRM interaction is:

Navigation
Forms
Tables
Filters
Dashboards

ChatGPT-native interaction adds:

Intent
Conversation
Context
Tool selection
Natural-language synthesis

The two approaches can coexist.

But conversation can dramatically reduce navigation overhead.


104. We Still Need Structured UI

Conversation is not always the best presentation format.

For example:

Show all opportunities.

A paragraph may be worse than:

Opportunity Table

with:

Company
Opportunity
Stage
Value
Probability
Expected Close

This is where the UI capabilities associated with ChatGPT apps become important.

But not yet.

First we prove tools.


105. MCP Before UI

Our sequence should be:

MCP Connectivity
Read-Only Tools
Authentication
Tenant Isolation
ChatGPT Tool Calls
Structured Results
UI Components

Do not debug:

MCP
+
Auth
+
Tools
+
React Widget
+
Styling

simultaneously.

One boundary at a time.


106. What About the Apps SDK?

The Apps SDK concept remains highly relevant because Quorentra will eventually need more than text responses.

For example, ChatGPT could display:

Pipeline Card
Opportunity Table
Company Card
Contact List
Sales Funnel

inside the conversation.

The current OpenAI architecture supports MCP-backed experiences with optional UI resources.

That is where Quorentra will go next.


107. MCP Is the Foundation for UI

Eventually:

Tool Call
├── Structured Data
└── UI Resource
ChatGPT UI

The model can use the structured result while the user sees an interactive interface.

This is why building clean MCP tools first is so important.


108. Example Future Pipeline Widget

The user asks:

Show my pipeline.

Instead of only:

Your EUR pipeline is €385,000.

Quorentra might render:

┌───────────────────────────────────────────┐
│ Sales Pipeline │
├───────────────────────────────────────────┤
│ Qualification €120,000 │
│ Discovery €150,000 │
│ Proposal €75,000 │
│ Negotiation €40,000 │
├───────────────────────────────────────────┤
│ Total €385,000 │
│ Weighted €161,000 │
└───────────────────────────────────────────┘

inside ChatGPT.

That is where the project becomes much more than a chatbot.


109. Example Future Opportunity UI

User:

Show my Contoso opportunities.

Quorentra could eventually render:

Contoso Opportunities
┌────────────────────────┬─────────────┬─────────┐
│ Opportunity │ Stage │ Value │
├────────────────────────┼─────────────┼─────────┤
│ Microsoft 365 Migration│ Proposal │ €75,000 │
│ Azure Modernization │ Discovery │€150,000 │
└────────────────────────┴─────────────┴─────────┘

with buttons such as:

View
Update Stage
Add Note
Create Task

Later articles will build toward this.


110. Why We Are Not Adding Mutation Tools Yet

Suppose the user says:

Move the Contoso migration deal to negotiation.

That seems straightforward.

But it introduces new questions:

Did ChatGPT identify the correct Opportunity?
Does the user have permission?
Should the action require confirmation?
Should the old value be shown?
Should the action be audited?
Can the action be reversed?
What if multiple Opportunities have similar names?

These deserve their own design phase.

Read-only first.

Mutations second.


111. Tool Safety Classification

We can begin classifying future tools.

Read

list_companies
get_company
list_contacts
get_contact
list_opportunities
get_pipeline_summary

Low operational risk.

Create

create_company
create_contact
create_opportunity

Moderate risk.

Update

update_company
update_contact
update_opportunity

Moderate to high risk depending on field.

Delete

delete_company
delete_contact
delete_opportunity

High risk.

This classification will influence future confirmation and UX design.


112. Do Not Expose Generic Mutation Tools

Avoid tools such as:

execute_sql
update_record
modify_entity
run_command

These are too broad.

Prefer narrow capabilities:

create_opportunity
update_opportunity_stage
update_contact

Specific tools are easier to:

Secure
Describe
Test
Audit
Reason about

113. MCP Should Reduce Authority, Not Expand It

If a user has:

Viewer

permissions in Quorentra, connecting ChatGPT should not suddenly give them:

Admin

powers.

Likewise:

Member

should remain Member.

The MCP layer must preserve the existing authorization envelope.


114. The Principle of Least Privilege

For the first ChatGPT integration, expose only:

Read Tools

and only the permissions required for them.

Do not grant:

Delete
Admin
Database
System

permissions merely because the integration is easier that way.

AI integrations should follow the same least-privilege principles as every other client.


115. Logging MCP Calls

We should begin logging basic tool activity.

For example:

timestamp
tool_name
user_id
organization_id
success
duration

Avoid logging unnecessary sensitive CRM payloads.

This gives us visibility into how ChatGPT uses Quorentra.


116. Future Tool Audit Events

Later we may introduce:

ToolInvocation
├── id
├── user_id
├── organization_id
├── tool_name
├── arguments_summary
├── result_status
├── created_at
└── duration_ms

This becomes especially valuable once tools can mutate CRM state.

For Part 15, ordinary structured logs are enough.


117. MCP Health

Consider exposing an internal health check for the MCP server.

Conceptually:

MCP Server
Healthy

and verify it can access:

Application services
Database

This becomes useful during remote connectivity testing.


118. Failure Boundaries

We now have several possible failure points:

ChatGPT
X
Remote Connection
X
MCP Server
X
Authentication
X
Tenant Resolution
X
Application Service
X
Database

Good logging will help identify which boundary failed.


119. Test MCP Without ChatGPT

This deserves emphasis.

Before connecting ChatGPT, prove:

MCP server starts
Tools are discoverable
Tools execute
Authentication works
Tenant isolation works
RBAC works
Results are correct

Then add ChatGPT.

Otherwise every error becomes:

Is this ChatGPT, MCP, authentication, FastAPI, SQLAlchemy, or PostgreSQL?

Modular debugging follows modular architecture.


120. Test ChatGPT Last

Our integration sequence should be:

Unit Tests
Application Service Tests
MCP Tool Tests
MCP Client Tests
Remote Connectivity
ChatGPT

Each layer proves the one beneath it.


121. Quorentra Version

Part 15 introduces an important new platform capability.

Update:

app/core/constants.py

from:

APP_VERSION = "0.1.0"

to:

APP_VERSION = "0.2.0"

Why a minor version increase?

Because Quorentra now has a new application interface:

MCP

This is more significant than an ordinary patch.


122. Quorentra 0.2.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 ✓
MCP Tools
├── 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 ✓
ChatGPT Connection ✓
MCP Mutation Tools -
ChatGPT UI -
Apps UI -
Activities -
Tasks -
AI Intelligence -

This is a major architectural milestone.


123. Acceptance Criteria

Part 15 is complete when:

✓ existing Quorentra 0.1.0 tests remain green
✓ MCP package exists
✓ MCP server starts
✓ MCP tool modules are separated by domain
✓ Company tools exist
✓ Contact tools exist
✓ Opportunity tools exist
✓ list_companies works
✓ get_company works
✓ list_contacts works
✓ get_contact works
✓ list_company_contacts works
✓ list_opportunities works
✓ get_opportunity works
✓ list_company_opportunities works
✓ list_opportunities_by_stage works
✓ get_pipeline_summary works
✓ tools use typed inputs
✓ tools return structured results
✓ list tools use bounded pagination
✓ tool descriptions are precise
✓ read-only tools are identified as read-only
✓ MCP tools reuse Quorentra application logic
✓ MCP tools do not query PostgreSQL directly
✓ MCP tools do not trust arbitrary tenant IDs
✓ authentication occurs before tool execution
✓ TenantContext is validated
✓ membership is validated
✓ RBAC is enforced
✓ cross-tenant Company access fails
✓ cross-tenant Contact access fails
✓ cross-tenant Opportunity access fails
✓ cross-tenant pipeline leakage is impossible
✓ tool errors do not expose stack traces
✓ internal secrets are not returned
✓ tool outputs are compact
✓ MCP tests exist
✓ permission tests exist
✓ tenant-isolation tests exist
✓ REST/MCP parity is verified
✓ MCP works independently before ChatGPT testing
✓ ChatGPT can reach the MCP server
✓ ChatGPT can discover Quorentra capabilities
✓ ChatGPT can invoke a Quorentra read tool
✓ "List my companies" works
✓ "Who do we know at Contoso?" works
✓ "What opportunities do we have with Contoso?" works
✓ "What is my current EUR pipeline?" works
✓ Quorentra reports version 0.2.0

Most importantly:

ChatGPT can now retrieve real, tenant-scoped CRM information through governed Quorentra capabilities.


124. What We Have Actually Achieved

At first glance, Part 15 may look like:

We added MCP.

But architecturally, something much larger has happened.

Before:

Human
API Client
CRM

Now:

Human
Natural Language
ChatGPT
Tool Selection
MCP
Quorentra Capability
CRM

We have added natural language as an application interface.


125. Quorentra Is Now ChatGPT-Native

This is the first point in the series where the phrase:

ChatGPT-native CRM

becomes operational rather than architectural.

The user can ask:

What is my pipeline?

instead of navigating through CRM screens.

They can ask:

Who do we know at Contoso?

instead of searching Contacts.

They can ask:

Which deals are in negotiation?

instead of configuring filters.

ChatGPT interprets intent.

Quorentra supplies governed business data.


126. But We Have Only Built the Foundation

The current experience is still mostly:

Conversation
+
Structured Tool Calls

The next major capability is:

Conversation
+
Tools
+
Interactive UI

That is where Quorentra begins to feel like an application running inside ChatGPT rather than merely a data source connected to it.


127. The Next Architecture

We are moving toward:

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

This is much closer to the product vision.


128. Why UI Inside ChatGPT Matters

Some CRM questions are naturally conversational:

What is my pipeline?

Others are inherently visual:

Show my opportunities.

A list of ten Opportunities is easier to understand as:

Table
Cards
Pipeline view

than as a long paragraph.

The strongest ChatGPT-native CRM therefore combines:

Language
+
Tools
+
UI

rather than forcing everything into text.


129. The Next Modular Step

We should not build the complete Quorentra UI at once.

The first UI should be extremely small.

A good candidate is:

Pipeline Summary

because we already have:

get_pipeline_summary

working.

We can take one proven MCP tool and give it a visual presentation.

That follows our modular development philosophy perfectly.


130. Next Article

In Part 16, we will build:

The First Quorentra ChatGPT UI — Rendering the Sales Pipeline Inside ChatGPT

We will cover:

Current OpenAI plugin/UI architecture
MCP UI resources
Structured tool results
UI resource registration
Widget architecture
ChatGPT rendering
Sandboxed UI execution
Tool-to-UI binding
Pipeline summary component
Responsive layout
Theme awareness
Loading states
Empty states
Error states
Currency formatting
Pipeline metrics
Opportunity counts
Weighted pipeline
Tool output versus UI-only metadata
ChatGPT narration versus visual presentation
UI security boundaries
Tenant-safe data
Local UI development
Building frontend assets
Serving UI resources
Connecting the UI resource to get_pipeline_summary
Testing the component through MCP
Testing inside ChatGPT

The first target experience will be:

User:
"Show my EUR pipeline."

ChatGPT calls:

get_pipeline_summary

Quorentra returns:

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

But instead of displaying only prose, ChatGPT will be able to present something conceptually like:

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

alongside a natural-language explanation.

That gives us:

Natural Language
+
Business Tools
+
Interactive UI
=
ChatGPT-Native CRM

Part 15 establishes the protocol boundary.

Part 16 will make that boundary visible to the user.

Quorentra

Contact

Menu

Designed with WordPress

Discover more from Quorentra

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

Continue reading