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.

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:
UsersOrganizationsMembershipsAuthenticationTenantContextRBACCompaniesContactsOpportunitiesSales 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_companiesget_companylist_contactsget_contactlist_company_contactslist_opportunitiesget_opportunitylist_company_opportunitieslist_opportunities_by_stageget_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 discoveryTool descriptionsInput schemasOutput schemasInvocationErrorsCapabilitiesModel 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 understandingIntent interpretationReasoningTool selectionResponse generation
Quorentra performs:
AuthenticationTenant resolutionAuthorizationBusiness rulesCRM operationsPersistence
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 APIReactMCPChatGPTMobile AppIntegration
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_companyupdate_companydelete_companycreate_contactupdate_contactdelete_contactcreate_opportunityupdate_opportunitydelete_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 connectivityTool discoveryAuthenticationTenant propagationRBACInput schemasOutput schemasChatGPT tool selection
without risking destructive actions.
12. Our First MCP Tool Set
The initial tool catalog is:
Companies├── list_companies└── get_companyContacts├── list_contacts├── get_contact└── list_company_contactsOpportunities├── 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_tablequery_contact_tablequery_opportunity_table
These are poor tool names.
Instead use business capabilities:
list_companiesget_companylist_company_contactslist_company_opportunitiesget_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:
RoutesResourcesHTTP methodsStatus codes
When building model-facing tools, we also need to think about:
Tool nameTool descriptionInput schemaOutput schemaRead/write behaviorDiscoverabilityAmbiguityResult 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_companiesget_companylist_company_contactsget_pipeline_summary
Avoid names such as:
companiescompany_apicrm_queryexecute_companyfetch_stuffpipeline
Tool names should be:
SpecificAction-orientedPredictableUnambiguous
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 theauthenticated user's active Quorentra organizationand requested currency.
This tells the model:
What it doesWhat scope it usesWhat input mattersWhat result it returns
17. Tool Descriptions Should Not Be Marketing Copy
Avoid:
Use Quorentra's revolutionary AI-powered salestechnology to unlock incredible insights into yourbusiness pipeline.
That is useless to the model.
Prefer precise operational language:
Return the number, total value, and weighted valueof 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:
NameDescriptionInput SchemaOutputAnnotationsHandler
For example:
Toolget_pipeline_summaryInputcurrency: stringOutputcurrencyopen_opportunitiestotal_pipelineweighted_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_companiesget_companylist_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 openopportunities worth €385,000 in total, with aweighted 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 fieldInternal metadataAudit timestampsORM detailsInternal tenant identifiers
A useful response might contain:
idnamewebsiteindustry
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\mcpmkdir app\mcp\toolsNew-Item app\mcp\__init__.py -ItemType FileNew-Item app\mcp\server.py -ItemType FileNew-Item app\mcp\context.py -ItemType FileNew-Item app\mcp\schemas.py -ItemType FileNew-Item app\mcp\tools\__init__.py -ItemType FileNew-Item app\mcp\tools\companies.py -ItemType FileNew-Item app\mcp\tools\contacts.py -ItemType FileNew-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.ps1pip 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:
FastAPISQLAlchemyPostgreSQL
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 toolsExpose tool metadataAccept tool callsExecute handlersReturn 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 deploymentClear service boundariesSeparate scalingNetwork-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_idorganization_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.readget_company→ companies.readlist_contacts→ contacts.readget_contact→ contacts.readlist_company_contacts→ contacts.readlist_opportunities→ opportunities.readget_opportunity→ opportunities.readlist_company_opportunities→ opportunities.readlist_opportunities_by_stage→ opportunities.readget_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_companiesget_companylist_contactsget_contactlist_company_contactslist_opportunitiesget_opportunitylist_company_opportunitieslist_opportunities_by_stageget_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 dataMutates dataHas destructive effectsIs 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_idcreated_atupdated_atinternal 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 IDCompany IDNameStageAmount
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_companiesDescription:List companies belonging to the authenticateduser's active Quorentra organization.Input:limitoffsetOutput:Company summariesPermission:companies.readBehavior:Read-only
This becomes our first model-facing CRM capability.
50. list_companies Input
Use bounded pagination.
For example:
limitdefault = 25maximum = 100offsetdefault = 0minimum = 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 argumentsQuery SQL directlyImplement tenant filteringImplement role logicCalculate permissions itself
It delegates to existing Quorentra capabilities.
That keeps security centralized.
53. Second Tool: get_company
Contract:
Name:get_companyDescription:Return a company by ID from the authenticateduser's active Quorentra organization.Input:company_idOutput:CompanyPermission:companies.readBehavior: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_contactsget_contactlist_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_contactsDescription:List contacts associated with a company in theauthenticated user's active Quorentra organization.Input:company_idlimitoffsetOutput:Contact summariesPermission:contacts.readBehavior:Read-only
The existing Contact service validates the Company relationship.
57. Opportunity Tools
Our opportunity tool set is:
list_opportunitiesget_opportunitylist_company_opportunitieslist_opportunities_by_stageget_pipeline_summary
These give ChatGPT enough information to answer useful sales questions.
58. list_opportunities
Contract:
Name:list_opportunitiesDescription:List sales opportunities in the authenticateduser's active Quorentra organization.Input:limitoffsetOutput:Opportunity summariesPermission:opportunities.readBehavior:Read-only
Again, keep results bounded.
59. list_opportunities_by_stage
Input:
stage
must use the same controlled stage values:
qualificationdiscoveryproposalnegotiationwonlost
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_stageInput: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_summaryDescription:Return the number, total value, and weighted valueof open sales opportunities for a requestedcurrency in the authenticated user's activeQuorentra organization.Input:currencyOutput:currencyopen_opportunitiestotal_pipelineweighted_pipelinePermission:opportunities.readBehavior: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:
ContactNotFoundErrorOpportunityNotFoundErrorPermissionDeniedAuthenticationRequiredInvalidTenant
The model should receive enough information to react correctly without exposing sensitive internals.
66. Do Not Return Stack Traces
Never return:
SQLAlchemy tracebackPython stackDatabase connection detailsFile pathsSecretsInternal 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_REQUIREDPERMISSION_DENIEDRESOURCE_NOT_FOUNDINVALID_ARGUMENTCONFLICTINTERNAL_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:
LatencyMemoryModel contextCostUser experience
Pagination is therefore part of AI architecture.
70. Search Will Eventually Be Better Than Listing
Later we will introduce tools such as:
search_companiessearch_contactssearch_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_companiesget_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_contactsget_contactlist_company_contacts
74. Register Opportunity Tools
Then:
register_opportunity_tools(mcp)
registers:
list_opportunitiesget_opportunitylist_company_opportunitieslist_opportunities_by_stageget_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:
StartAdvertise toolsAccept tool callsReturn 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_companiesget_companylist_contactsget_contactlist_company_contactslist_opportunitiesget_opportunitylist_company_opportunitieslist_opportunities_by_stageget_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 JohnsonRobert Smith
No Contacts from other Companies or tenants should appear.
81. Test Opportunities
Call:
list_company_opportunities
for Contoso.
Expected:
Microsoft 365 MigrationAzure 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,000Tenant 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:
RESTMCPChatGPTFuture React UIMobile clientsIntegrationsAutomationAI 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 CRMNavigate to OpportunitiesSelect PipelineChoose CurrencyOpen DashboardRead 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:
NavigationFormsTablesFiltersDashboards
ChatGPT-native interaction adds:
IntentConversationContextTool selectionNatural-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:
CompanyOpportunityStageValueProbabilityExpected 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 CardOpportunity TableCompany CardContact ListSales 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:
ViewUpdate StageAdd NoteCreate 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_companiesget_companylist_contactsget_contactlist_opportunitiesget_pipeline_summary
Low operational risk.
Create
create_companycreate_contactcreate_opportunity
Moderate risk.
Update
update_companyupdate_contactupdate_opportunity
Moderate to high risk depending on field.
Delete
delete_companydelete_contactdelete_opportunity
High risk.
This classification will influence future confirmation and UX design.
112. Do Not Expose Generic Mutation Tools
Avoid tools such as:
execute_sqlupdate_recordmodify_entityrun_command
These are too broad.
Prefer narrow capabilities:
create_opportunityupdate_opportunity_stageupdate_contact
Specific tools are easier to:
SecureDescribeTestAuditReason 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:
DeleteAdminDatabaseSystem
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:
timestamptool_nameuser_idorganization_idsuccessduration
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 servicesDatabase
This becomes useful during remote connectivity testing.
118. Failure Boundaries
We now have several possible failure points:
ChatGPT │ XRemote Connection │ XMCP Server │ XAuthentication │ XTenant Resolution │ XApplication Service │ XDatabase
Good logging will help identify which boundary failed.
119. Test MCP Without ChatGPT
This deserves emphasis.
Before connecting ChatGPT, prove:
MCP server startsTools are discoverableTools executeAuthentication worksTenant isolation worksRBAC worksResults 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:
TableCardsPipeline 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 architectureMCP UI resourcesStructured tool resultsUI resource registrationWidget architectureChatGPT renderingSandboxed UI executionTool-to-UI bindingPipeline summary componentResponsive layoutTheme awarenessLoading statesEmpty statesError statesCurrency formattingPipeline metricsOpportunity countsWeighted pipelineTool output versus UI-only metadataChatGPT narration versus visual presentationUI security boundariesTenant-safe dataLocal UI developmentBuilding frontend assetsServing UI resourcesConnecting the UI resource to get_pipeline_summaryTesting the component through MCPTesting 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.