Build a ChatGPT App with a self-hosted MCP server (TypeScript)
Build a ChatGPT App with a self-hosted MCP server (TypeScript)
|
|
A Python variant of this guide is also available. See Build a ChatGPT App with a self-hosted MCP server (Python). |
Conversational AI clients such as ChatGPT are emerging as new channels for product discovery. With the Model Context Protocol (MCP), you can connect your Coveo-powered commerce catalog data to these clients, letting shoppers find products through natural-language conversations.
The Coveo Commerce API provides the search, ranking, and merchandising capabilities that power this experience. A self-hosted MCP server acts as middleware, translating conversational queries from ChatGPT into structured Coveo Commerce API requests and formatting the results for display. This guide walks through building that server, pairing it with a rich product-card UI, and connecting it to ChatGPT as an App.
The result is a conversational shopping experience where users ask natural-language questions, receive relevant product results with images and pricing, and click through to product detail pages. You can adapt these patterns to your own stack and requirements.
|
|
Note
This guide concentrates on a commerce search integration. For other capabilities such as retrieval-augmented generation (RAG), passage retrieval, or content search, adapt the patterns shown here to the corresponding Coveo API endpoints. |
Architecture
When a user asks a product question in ChatGPT, the ChatGPT App sends an MCP tools/call request over Streamable HTTP (POST /mcp) to your server.
The server parses the natural-language query and any structured constraints (such as brand or price filters), translates them into a Coveo Commerce API search request with the appropriate facet payloads, and returns normalized product results.
ChatGPT uses those results to compose a conversational answer.
Along with the tool results, the server returns a tool UI: a self-contained HTML resource that ChatGPT renders in an iframe beside the conversation. This gives users a visual, interactive product card layout, complete with images, prices, and detail page links, while the conversational response provides the narrative summary. The MCP server operates in stateless mode, meaning each request creates an independent transport with no server-side session state, which simplifies horizontal scaling and is the recommended pattern for ChatGPT Apps.
Prerequisites
-
You have a Coveo organization with catalog data indexed in a Coveo source.
-
You’ve configured a Coveo API key with the Execute queries privilege on the target sources.
-
You have a ChatGPT Plus, Teams, Enterprise, or Education subscription.
-
You’ve installed Node.js 18+ and are comfortable with TypeScript basics.
Phase 1: Build the MCP proxy server
This phase creates a TypeScript MCP server that exposes a commerce_search_coveo tool.
When ChatGPT invokes this tool, the server forwards the query to the Coveo Commerce API and returns normalized product results.
The server also demonstrates how to translate natural-language constraints into Coveo facets.
For example, when a user asks for "kayaks under $500", the server extracts the price ceiling and passes it as a numericalRange facet, returning only products within budget.
You can implement other facet types.
The tool description instructs the LLM to pass structured constraints through a facetFilters parameter, and the server adds a regex-based budget extraction as a fallback.
The specific facet fields (such as ec_price) used in this example depend on the fields indexed in your Coveo source. Adapt them to your commerce fields.
Set up the project
Create a project directory and install the required dependencies:
mkdir coveo-mcp-server && cd coveo-mcp-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/sdk zod dotenv
npm install -D typescript tsx @types/node
Create a .env file with your Coveo credentials:
COVEO_API_KEY=<YOUR_API_KEY>
COVEO_ORGANIZATION_ID=<YOUR_ORG_ID>
COVEO_COMMERCE_TRACKING_ID=<YOUR_TRACKING_ID>
COVEO_COMMERCE_CLIENT_ID=<YOUR_CLIENT_ID>
COVEO_COMMERCE_VIEW_URL=https://www.example.com/
Create the MCP server
Create a server.ts file that imports the commerce module, registers it as an MCP tool, and starts the HTTP server.
This thin shell demonstrates the pattern for wiring any API module into an MCP server.
The server also optionally serves a tool UI resource (see Phase 2):
import 'dotenv/config';
import {existsSync, readFileSync} from 'node:fs';
import {
createServer,
type IncomingMessage,
type ServerResponse,
} from 'node:http';
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {makeCommerceSearchRequest} from './commerceSearch.js';
import {toolInputSchema} from './schema.js';
const API_KEY = process.env.COVEO_API_KEY!;
const ORG_ID = process.env.COVEO_ORGANIZATION_ID!;
const TRACKING_ID = process.env.COVEO_COMMERCE_TRACKING_ID!;
const CLIENT_ID = process.env.COVEO_COMMERCE_CLIENT_ID!;
const VIEW_URL =
process.env.COVEO_COMMERCE_VIEW_URL ?? 'https://www.example.com/';
const COMMERCE_ENDPOINT = `https://platform.cloud.coveo.com/rest/organizations/${ORG_ID}/commerce/v2/search`;
const COMMERCE_APP_URI = 'ui://coveo/commerce-search';
const COMMERCE_APP_HTML = existsSync('commerce-search.html')
? readFileSync('commerce-search.html', 'utf-8')
: null;
const server = new McpServer({
name: 'coveo-mcp-server',
version: '1.0.0',
});
const TOOL_DESCRIPTION =
'Search products in Coveo Commerce and return normalized product results. ' +
'When the shopper provides structured constraints such as budget, price ceiling, ' +
'or product attributes, keep the natural-language query concise and pass those ' +
'constraints through facetFilters. Use range filters for price or budget ' +
'constraints (for example, field ec_price with max 700) and value filters for ' +
'other attributes. The available facet fields depend on the commerce fields ' +
'indexed in the Coveo source.';
const credentials = {
apiKey: API_KEY,
endpoint: COMMERCE_ENDPOINT,
trackingId: TRACKING_ID,
clientId: CLIENT_ID,
viewUrl: VIEW_URL,
};
server.registerTool(
'commerce_search_coveo',
{
description: TOOL_DESCRIPTION,
inputSchema: toolInputSchema,
...(COMMERCE_APP_HTML
? {_meta: {ui: {resourceUri: COMMERCE_APP_URI}}}
: {}),
},
async ({query, numberOfResults, page, facetFilters}) => {
const data = await makeCommerceSearchRequest(
query,
numberOfResults,
page,
facetFilters,
credentials
);
return {
content: [{type: 'text' as const, text: JSON.stringify(data)}],
};
}
);
if (COMMERCE_APP_HTML) {
server.registerResource(
'coveo-commerce-search-app',
COMMERCE_APP_URI,
{mimeType: 'text/html;profile=mcp-app'},
async () => ({
contents: [
{
uri: COMMERCE_APP_URI,
mimeType: 'text/html;profile=mcp-app',
text: COMMERCE_APP_HTML,
},
],
})
);
}
const httpServer = createServer(
async (req: IncomingMessage, res: ServerResponse) => {
if (req.url === '/mcp') {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res);
await transport.close();
} catch (err) {
console.error('MCP request error:', err);
if (!res.headersSent) {
res.writeHead(500).end();
}
}
} else {
res.writeHead(404).end();
}
}
);
httpServer.listen(8000, () => {
console.log('MCP server listening on http://127.0.0.1:8000/mcp');
});
Details
Load credentials from a .env file in the project root. |
|
| Streamable HTTP transport is required for remote MCP connections from ChatGPT. | |
| Import the commerce module defined in commerceSearch.ts. This separation keeps API integration logic independent of the MCP framework. | |
| Import the tool input schema defined in schema.ts. Keeping schemas in a dedicated file makes them easier to review and reuse across tools. | |
| Coveo Commerce API v2 search endpoint, scoped to your organization. | |
| Load the built tool UI HTML at startup. The file is produced by the UI example project. If missing, the server still works but without rich product cards. | |
The tool description guides the LLM on when and how to call this tool. Mentioning facetFilters explicitly teaches the model to extract structured constraints from natural language rather than embedding them in the query string. |
|
| Bundle the environment credentials into a single object passed to the commerce module at call time. | |
Register the commerce_search_coveo tool. To add another tool, import a second module and register it with another registerTool call. |
|
| Link the tool to the tool UI resource so the host renders rich product cards alongside the text response. | |
| Delegate to the commerce module, passing through the environment credentials. | |
The mcp-app MIME type profile tells the host this resource is a tool UI that should be rendered in an iframe. |
|
Stateless mode: no session affinity. Each request creates a fresh transport. For session-based routing, provide a sessionIdGenerator function instead. |
|
Close the transport after each request to release the server’s internal connection. Without this call, the next server.connect() throws because the server is still bound to the previous transport. |
|
| The server is now ready for ChatGPT to connect via Settings > Apps & Connectors. |
Create the commerce module
Create a commerceSearch.ts file that encapsulates the Coveo Commerce API integration.
This module handles budget extraction, facet payload construction, the API request, and response normalization.
It has no dependency on the MCP framework, so you can test and reuse it independently:
import type {FacetFilter} from './schema.js';
import type {
CoveoProduct,
Credentials,
NormalizedProduct,
SearchResult,
} from './types.js';
const BUDGET_PATTERNS = [
/(?:budget(?:\s+(?:is|of|around|about|under|below|up\s+to|max(?:imum)?))?\s*\$?)(?<amount>\d[\d,]*(?:\.\d+)?)\b/i,
/(?:under|below|less\s+than|up\s+to|max(?:imum)?(?:\s+budget)?|price\s+ceiling)\s*\$?(?<amount>\d[\d,]*(?:\.\d+)?)\b/i,
/\$\s*(?<amount>\d[\d,]*(?:\.\d+)?)/i,
];
export const extractBudgetMax = (query: string): number | null => {
for (const pattern of BUDGET_PATTERNS) {
const m = pattern.exec(query);
if (m?.groups?.amount) {
const amount = Number.parseFloat(m.groups.amount.replace(/,/g, ''));
if (Number.isFinite(amount)) return amount;
}
}
return null;
};
export const buildFacetsPayload = (filters: FacetFilter[]) => {
const facets: Record<string, unknown>[] = [];
for (const f of filters) {
if (f.type === 'range') {
facets.push({
facetId: f.field,
field: f.field,
type: 'numericalRange',
values: [
{
start: f.min ?? 0,
end: f.max,
endInclusive: true,
state: 'selected',
},
],
});
} else if (f.type === 'value') {
facets.push({
facetId: f.field,
field: f.field,
type: 'regular',
values: (f.values ?? []).map((v: string) => ({
value: v,
state: 'selected',
})),
});
}
}
return facets;
};
export const makeCommerceSearchRequest = async (
query: string,
numResults = 5,
page = 0,
facetFilters: FacetFilter[] = [],
{apiKey, endpoint, trackingId, clientId, viewUrl}: Credentials
): Promise<SearchResult> => {
const effectiveFilters: FacetFilter[] = [...facetFilters];
const budgetMax = extractBudgetMax(query);
const hasExplicitPrice = effectiveFilters.some(
(f) => f.type === 'range' && f.field === 'ec_price'
);
if (budgetMax !== null && !hasExplicitPrice) {
effectiveFilters.push({
type: 'range',
field: 'ec_price',
min: 0,
max: budgetMax,
});
}
const payload: Record<string, unknown> = {
query: `<@- ${query.trim()} -@>`,
perPage: numResults,
page,
trackingId,
clientId,
country: 'US',
currency: 'USD',
language: 'en',
context: {view: {url: viewUrl}},
};
if (effectiveFilters.length > 0) {
payload.facets = buildFacetsPayload(effectiveFilters);
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Coveo Commerce API error: ${response.status} — ${body}`);
}
const data = (await response.json()) as {
products?: CoveoProduct[];
totalCount?: number;
};
let products: NormalizedProduct[] = (data.products ?? []).map(
(p: CoveoProduct) => ({
productId: p.ec_product_id ?? null,
name: p.ec_name ?? null,
brand: p.ec_brand ?? null,
description: p.ec_description ?? null,
imageUrl: p.ec_images ?? null,
clickUri: p.clickUri ?? null,
price: p.ec_price ?? null,
currency: 'USD',
})
);
if (budgetMax !== null && products.length > 0) {
products = products.filter((p) => p.price === null || p.price <= budgetMax);
}
const result: SearchResult = {
query,
page,
perPage: numResults,
totalCount: data.totalCount ?? 0,
products,
};
if (budgetMax !== null) {
result.appliedConstraints = {priceMax: budgetMax};
}
return result;
};
Details
Import the FacetFilter type defined in schema.ts. Using import type ensures this import is erased at runtime. |
|
| Import shared interfaces from types.ts. | |
Simple regex heuristics that extract a price ceiling from natural language. The LLM can also pass an explicit price filter through facetFilters, making these a server-side fallback. |
|
Convert the portable filter objects into the Coveo Commerce facets array format. The field names (e.g., ec_price) are examples. Adapt them to the fields indexed in your source and pipeline. |
|
| The main entry point for commerce queries. Call this from your MCP tool handler. | |
| If the LLM did not pass a price filter but the query mentions a budget, inject one automatically. This merges LLM-structured constraints with server-side inference. | |
Wrap the query in a no-syntax block so Coveo treats it as literal text, not a query expression making use of special symbols such as $. |
|
trackingId and clientId are required Commerce API parameters that associate requests with your storefront configuration. |
|
| Include the response body in the error message so API validation errors are visible during debugging. | |
Normalize Coveo field names (ec_name, ec_brand, etc.) to a portable product schema. These field names depend on your commerce fields configuration.
|
|
| Post-query safety net: remove any products that exceed the inferred budget. The facet filter handles most cases, but this catches edge cases. |
Define the tool input schema
Create a schema.ts file that declares the zod schema for the tool’s input parameters.
The MCP SDK converts this schema to JSON Schema at registration time, so ChatGPT knows the parameter names, types, and descriptions:
import {z} from 'zod';
export const facetFilterSchema = z.array(
z.union([
z.object({
type: z.literal('range'),
field: z.string().describe('Coveo field name, e.g. ec_price'),
min: z.number().optional().default(0),
max: z.number().describe('Upper bound (inclusive)'),
}),
z.object({
type: z.literal('value'),
field: z.string().describe('Coveo field name, e.g. ec_brand'),
values: z.array(z.string()).describe('Values to match'),
}),
])
);
export type FacetFilter = z.infer<typeof facetFilterSchema>[number];
export const toolInputSchema = {
query: z.string().describe('The product search query'),
numberOfResults: z
.number()
.optional()
.default(5)
.describe('Number of products to return'),
page: z.number().optional().default(0).describe('Zero-based result page'),
facetFilters: facetFilterSchema
.optional()
.default([])
.describe(
'Structured facet filters extracted from the query. ' +
'Range filters narrow numeric fields; value filters match categorical fields.'
),
};

| The MCP SDK uses Zod for schema declaration. Zod schemas double as runtime validators and JSON Schema generators, so tool input schemas stay type-safe and in sync with the MCP specification. | |
Structured schema for facet filters. The LLM populates this with constraint objects extracted from the user’s message. For example, \{"type": "range", "field": "ec_price", "max": 500} for a budget constraint. |
|
Derive the TypeScript type from the Zod schema. Using z.infer keeps the runtime schema and the static type in sync. |
|
Each key becomes a named parameter in the MCP tool. The .describe() strings are surfaced to ChatGPT as parameter documentation. |
Create the types module
Create a types.ts file with the shared TypeScript interfaces for the Coveo Commerce API response and the normalized product output:
export interface Credentials {
apiKey: string;
endpoint: string;
trackingId: string;
clientId: string;
viewUrl: string;
}
export interface CoveoProduct {
ec_product_id?: string;
ec_name?: string;
ec_brand?: string;
ec_description?: string;
ec_images?: string;
clickUri?: string;
ec_price?: number;
}
export interface NormalizedProduct {
productId: string | null;
name: string | null;
brand: string | null;
description: string | null;
imageUrl: string | null;
clickUri: string | null;
price: number | null;
currency: string;
}
export interface SearchResult {
query: string;
page: number;
perPage: number;
totalCount: number;
products: NormalizedProduct[];
appliedConstraints?: {priceMax: number};
}
| These are shared interfaces for the Coveo Commerce API response and the normalized output shape. These types are not required at runtime but help catch integration errors at compile time. |
Start the server
Run the server:
npx tsx server.ts
The server starts on http://127.0.0.1:8000/mcp.
|
|
For local development, you can expose your server to the public internet with a tunneling service (for example, ngrok.com or Cloudflare Tunnel). |
Phase 2: Build a tool UI
A tool UI is an HTML view that the host (ChatGPT) renders inside an iframe alongside tool results. Instead of showing raw JSON, ChatGPT can display rich product cards with images, names, brands, prices, and clickable links to product detail pages.
This phase creates a React application that renders Coveo Commerce product results, compiles it to a single HTML file, and serves it as an MCP resource.
|
|
Note
Phase 2 is optional. Without a tool UI, ChatGPT still receives the tool results as structured data and can summarize them in text. A tool UI adds a visual product card rendering alongside the AI-generated response. |
How tool UIs work
The tool UI architecture has three components:
-
Tool: Returns structured data (product results).
-
Resource: Serves an HTML file with the MIME type
text/html;profile=mcp-app. -
Metadata: Links the tool to the resource through a
resourceUriin the tool’s_meta.uifield.
When the host invokes a tool that has a linked resource, it loads the HTML into an iframe and passes the tool input and result to the app at runtime.
Create the React app
Initialize a Vite + React project:
npm create vite@latest coveo-commerce-app -- --template react-ts
cd coveo-commerce-app
npm install @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps vite-plugin-singlefile
Configure Vite to produce a single self-contained HTML file.
In vite.config.ts:
import react from '@vitejs/plugin-react';
import {defineConfig} from 'vite';
import {viteSingleFile} from 'vite-plugin-singlefile';
export default defineConfig({
plugins: [react(), viteSingleFile()],
});
| Inline all JS and CSS into a single HTML file. MCP resources must be self-contained. |
Create a product card component in src/App.tsx.
The component uses the useApp hook from the MCP Apps SDK to receive tool lifecycle events:
import {useApp} from '@modelcontextprotocol/ext-apps/react';
import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js';
import {useState} from 'react';
interface Product {
productId: string | null;
name: string | null;
brand: string | null;
description: string | null;
imageUrl: string | null;
clickUri: string | null;
price: number | null;
currency: string | null;
}
type Phase = 'waiting' | 'loading' | 'ready' | 'error';
function CommerceApp() {
const [phase, setPhase] = useState<Phase>('waiting');
const [products, setProducts] = useState<Product[]>([]);
const {error} = useApp({
appInfo: {name: 'coveo-commerce-app', version: '1.0.0'},
capabilities: {},
onAppCreated: (app) => {
app.ontoolinput = () => setPhase('loading');
app.ontoolresult = (result: CallToolResult) => {
try {
const text = result.content?.find((c) => c.type === 'text')?.text;
const data = text ? (JSON.parse(text) as {products?: Product[]}) : null;
setProducts(data?.products ?? []);
setPhase('ready');
} catch {
setPhase('error');
}
};
},
});
if (error) return <p>Something went wrong: {error.message}</p>;
if (phase === 'waiting') return <p>Waiting for results...</p>;
if (phase === 'loading') return <p>Searching products...</p>;
if (phase === 'error') return <p>Something went wrong.</p>;
const formatPrice = (price: number | null, currency: string | null) => {
if (price === null) return '';
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency ?? 'USD',
}).format(price);
} catch {
return `$${price.toFixed(2)}`;
}
};
return (
<div style={{display: 'flex', flexDirection: 'column', gap: '1rem'}}>
{products.map((product: Product) => (
<article
key={product.productId}
style={{
display: 'flex',
gap: '1rem',
padding: '1rem',
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
>
{product.imageUrl && (
<img
src={product.imageUrl}
alt={product.name ?? ''}
style={{width: 80, height: 80, objectFit: 'cover'}}
/>
)}
<div>
<h3 style={{margin: 0}}>
<a
href={product.clickUri ?? '#'}
target="_blank"
rel="noreferrer"
>
{product.name}
</a>
</h3>
{product.brand && (
<p style={{margin: '0.25rem 0', color: '#666'}}>
{product.brand}
</p>
)}
{product.price !== null && (
<p style={{margin: '0.25rem 0', fontWeight: 'bold'}}>
{formatPrice(product.price, product.currency)}
</p>
)}
{product.description && (
<p style={{margin: '0.25rem 0', fontSize: '0.9rem'}}>
{product.description}
</p>
)}
</div>
</article>
))}
</div>
);
}
export default CommerceApp;
Details
The useApp React hook from the MCP Apps SDK manages App instantiation, handler registration, and host connection automatically. |
|
| Fires when the host begins invoking the linked tool. Use this to show a loading state. | |
The MCP Apps SDK normalizes host-specific payload wrappers into a standard CallToolResult. Extract the JSON string from the first text content block and parse it to get the product array. |
|
useApp creates the App instance, registers the callbacks via onAppCreated, and calls connect() — replacing the manual new App() + app.connect() pattern. |
|
| Locale-aware price formatting. Adjust the locale and currency to match your storefront. |
Build the app:
npm run build
This produces a single dist/index.html file containing all JavaScript and CSS inline.
Register the resource on the server
Copy the built dist/index.html to your server directory as commerce-search.html.
The Phase 1 server files already include the resource registration and tool metadata linking.
When commerce-search.html is present alongside the server, the server loads it at startup and registers it as an MCP resource with the text/html;profile=mcp-app MIME type.
The metadata.ui.resourceUri on the tool tells the host to load the tool UI HTML alongside the tool result.
Restart the server to pick up the new HTML resource.
Phase 3: Connect the MCP server as a ChatGPT App
With the MCP server running and publicly accessible over HTTPS, add it to ChatGPT as an App. ChatGPT discovers the available tools directly from the MCP server, so no additional tool configuration is needed beyond the connection itself.
|
|
For local development, you can expose your server to the public internet with a tunneling service (for example, ngrok.com or Cloudflare Tunnel). |
Enable developer mode
Developer mode is required to add custom Apps.
-
Log in to ChatGPT.
-
Navigate to Settings > Apps & Connectors > Advanced settings (at the bottom of the page).
-
Toggle Developer mode on.
A Create button appears under Settings > Apps & Connectors.
Create the App
-
In Settings > Apps & Connectors, click Create.
-
Provide the following metadata:
-
Connector name: A user-facing title (for example,
Coveo Commerce Search). -
Description: Explain what the App does. The model uses this text during tool discovery.
-
Connector URL: The public
/mcpendpoint of your server (for example,https://your-server.example.com/mcp).
-
-
Click Create.
If the connection succeeds, ChatGPT displays a list of the tools your server advertises (for example,
commerce_search_coveo). If it fails, see the Apps SDK testing guide for debugging with the MCP Inspector.
Configure authentication
ChatGPT Apps support two authentication modes:
No authentication (development)
By default, your MCP server can operate without authentication. Use this mode during development and testing. No additional configuration is needed in the ChatGPT UI.
OAuth 2.1 (production)
For production deployments that access customer-specific data, implement OAuth 2.1 authentication conforming to the MCP authorization specification.
This requires:
-
An identity provider (such as Auth0 or Okta) that publishes OAuth discovery metadata and supports dynamic client registration with PKCE.
-
A
/.well-known/oauth-protected-resourceendpoint on your MCP server that advertises the authorization server URL. -
Token verification on each incoming request (issuer, audience, expiry, and scopes).
ChatGPT handles the OAuth authorization-code flow with PKCE on behalf of the user.
|
|
Note
ChatGPT Apps don’t support API key authentication. For server-to-server access control during development, you can restrict access at the network level (for example, IP allowlisting or a reverse proxy). |
Use the App in a conversation
-
Open a new chat in ChatGPT.
-
Click the + button near the message composer, then click More.
-
Select your App from the list of available tools. This adds the App to the conversation context.
|
|
To guide ChatGPT’s behavior when using your tools, you can include a system prompt at the start of a conversation or in a ChatGPT Project. For example:
This is optional—ChatGPT can infer how to use the tools from their descriptions in the MCP server. |
|
|
Note
To package the experience as a branded, shareable assistant, you can create a custom GPT instead. A custom GPT lets you set a name, icon, and persistent instructions, and share through the GPT Store. The MCP server connection works the same way in both approaches. |
Phase 4: Validate
-
Open a new chat in ChatGPT with the App enabled (click + > More > select your App).
-
Enter a product query that includes natural-language constraints such as a budget, skill level, or intended use. ChatGPT parses these constraints and passes them to the MCP server, which translates them into facet filters (for example, a price-range facet for budget mentions) before calling the Coveo Commerce API.
-
Verify that:
-
ChatGPT invokes the
commerce_search_coveotool. -
The tool returns products from your Coveo commerce catalog with names, prices, and images.
-
If you built the tool UI, the results render as rich product cards with clickable links to product detail pages.
-
-
Test a follow-up refinement query (for example,
Show me the 3 cheapest options) to verify that the conversational flow supports iterative product discovery.
|
|
If ChatGPT doesn’t invoke the tool, verify that the App is connected in Settings > Apps & Connectors and that you’ve added it to the conversation context. |
What’s next
-
See Get started with the Hosted MCP Server for an overview of other available Coveo MCP tools.
-
See the Model Context Protocol specification for the full protocol reference.