1. Bütöv mənzərə: alətin server üzərindən çağırışı
Koda başlamazdan əvvəl arxitekturanı fiks edək. Bu, detallarda itib-batmağa imkan verməyəcək.
Apps SDK + MCP terminologiyasında hər şey belə görünür: bizdə MCP‑server var (kursumuzda bu, Next.js-də Route Handler — app/mcp/route.ts), o, alətləri və resursları qeydiyyatdan keçirir və bu alətlər üçün handler-ləri reallaşdırır.
Yüksək səviyyəli sxem:
sequenceDiagram
participant User as İstifadəçi
participant Chat as ChatGPT (model)
participant App as ChatGPT App
participant MCP as MCP-server / backend
participant DB as Kataloq/xarici API-lər
User->>Chat: "Hədiyyə seç..."
Chat->>App: aləti çağırmağa qərar verir `suggest_gifts`
App->>MCP: JSON-RPC call_tool (ad + arqumentlər)
MCP->>MCP: Doğrulama, avtorizasiya
MCP->>DB: Kataloq sorğusu/filtrləmə
DB-->>MCP: Namizədlər siyahısı
MCP-->>App: structuredContent + content + _meta
App-->>Chat: Nəticəni modelə + vidcetə ötürür
Chat-->>User: Seçimi izah edir, vidceti göstərir
Əsas fikir: server modelin “sehrini” bilmir. O, adi bir sorğu görür: alətin adı + arqumentlər və strukturlu cavab qaytarmalıdır. Model isə ümumiyyətlə sizin kodu görmür, o yalnız aşağıdakıları görür:
- mövcud alətlər və onların sxemləri;
- özünün formalaşdırdığı arqumentlər;
- sizin qaytardığınız JSON cavabı.
Ona görə bu mühazirədəki vəzifəmiz — orta hissəni səliqə ilə reallaşdırmaqdır: MCP‑server və tools üçün handler‑lər.
Insight: mcp-tools limit
MCP-serverdə alətlərin sayı — yaddaş və ya kontekst tokenləri kimi məhdud metrikadır. Formal olaraq onlarca, hətta yüzlərlə aləti qeydiyyatdan keçirmək olar, amma platforma və model bununla xətti işləmirlər: hər yeni alət marşrutlaşdırmada “səs-küyü” artırır.
Təcrübə aşağıdakı sərhədləri göstərir:
- sərt tavan ChatGPT üçün ≈ server başına 128 MCP‑tools-a qədər;
- işlək diapazon — 50 alətədək. Daha sonra keyfiyyət nəzərəçarpacaq azalır: model təsviri yaxın alətləri qarışdırır, nadir alətləri daha az xatırlayır, daha tez səhv seçim edir.
Anthropic-də də oxşar mənzərə var: limit təxminən maksimum 100 alət, özləri isə 50‑yə qədər saxlamağı tövsiyə edirlər.
2. Next.js + Apps SDK şablonunda server məntiqi harada yaşayır
Modul 2‑də artıq ChatGPT App üçün rəsmi Next.js şablonunu qaldırmış və strukturuna qısa baxış keçirmişdik. İndi onda MCP‑serverin harada yaşadığını və vidcetlə necə bağlı olduğunu görək.
Bu şablondan istifadə edirsinizsə, MCP‑server adətən app/mcp/route.ts faylında (App Router) reallaşdırılır. ChatGPT‑dən JSON‑RPC çağırışları məhz ora gəlir: tools/call, resources/list, handshake və s.
Layihənin tipik strukturu:
my-chatgpt-app/
├─ app/
│ ├─ mcp/
│ │ └─ route.ts # MCP-server + alətlərin qeydiyyatı
│ ├─ page.tsx # React vidcet (UI)
│ ├─ layout.tsx # Root layout, Bootstrap SDK
│ └─ globals.css # Qlobal stillər
│
├─ proxy.ts # CORS və s.
├─ next.config.ts
├─ package.json
├─ tsconfig.json
└─ .env
route.ts faylında biz:
- MCP‑server instansını yaradırıq (@modelcontextprotocol/sdk vasitəsilə);
- alətləri qeydiyyatdan keçiririk (server.registerTool(...));
- ChatGPT‑dən gələn sorğuları qəbul edən və onları MCP‑serverə ötürən HTTP handler müəyyənləşdiririk.
Daha sonra bu struktura əsaslanaraq TypeScript‑də kod yazacağıq.
3. Minimal MCP‑server və alət handler‑i
Ən sadəsindən başlayaq: server quraq və tədris alətimiz suggest_gifts-i əlavə edək; o, müvəqqəti cavab qaytaracaq.
Tutaq ki, MCP‑SDK artıq quraşdırılıb:
pnpm add @modelcontextprotocol/sdk
Və sadə app/mcp/route.ts yaradaq:
// app/mcp/route.ts
import { NextRequest } from "next/server";
import { McpServer } from "@modelcontextprotocol/sdk/server";
const server = new McpServer({ name: "giftgenius-mcp" });
// Alətin minimal sxemlə qeydiyyatı
server.registerTool(
"suggest_gifts",
{
title: "Hədiyyələrin seçimi",
description: "Maraqlara və büdcəyə əsasən hədiyyələr seçir.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Alıcı haqqında qısa təsvir." },
},
required: ["query"],
},
},
async ({ input }) => {
// Burada biznes məntiqi olacaq
return {
content: [
{
type: "text",
text: `Müvəqqəti cavab: "${input.query}" üçün hədiyyələr.`,
},
],
structuredContent: {},
};
}
);
// Next.js üçün HTTP handler
export async function POST(req: NextRequest) {
const body = await req.text(); // JSON-RPC sətiri
const response = await server.handle(body);
return new Response(response, {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
Bu artıq işlək variantdır: ChatGPT suggest_gifts alətini çağıra biləcək və server mətn tipli müvəqqəti cavab qaytaracaq.
Vacib məqam: server.registerTool aşağıdakıları qəbul edir:
- alətin adını;
- metadataları və giriş üçün JSON Schema;
- handler — arqumentlərin input kimi gəldiyi asinxron funksiya.
Amma hələ doğrulama, normal structured output və avtorizasiya yoxdur. İndi məhz bunları əlavə edəcəyik.
4. Giriş məlumatlarının doğrulanması və qatların ayrılması
Niyə tək JSON‑sxem kifayət deyil
Bəli, platforma sxem üzrə əsas şeyləri özü valide edir: sahə tipləri, məcburi propertilər və s. Amma:
- model məntiqən yalnış məlumatlar verə bilər (məsələn, büdcə −100 və ya 1000 elementlik maraqlar siyahısı);
- sizdə biznes məhdudiyyətləri var (maksimal büdcə, dəstəklənən valyutalar və s.);
- bəzən ChatGPT və ya başqa müştəri qəribə davranıb tamam gözlənilməz bir şey göndərə bilər.
Bu səbəbdən handler daxilində əlavə doğrulama yenə də lazımdır.
Kodu ayıraq: handler ↔ biznes məntiqi
Server kodu “spagetti”yə çevrilməsin deyə, biznes məntiqini ayrılıqda saxlamaq rahatdır. Məsələn, app/mcp/gifts.ts yaradaq:
// app/mcp/gifts.ts
export type SuggestGiftsInput = {
age?: number | null;
relationship: "friend" | "partner" | "colleague";
maxBudget: number;
interests: string[];
};
export type GiftItem = {
id: string;
title: string;
price: number;
currency: "USD";
score: number;
tags: string[];
shortDescription: string;
};
// Sadə "hədiyyə bazası"
const CATALOG: GiftItem[] = [
{
id: "board-game-1",
title: "Stolüstü oyun “Kosmik strategiya”",
price: 39,
currency: "USD",
score: 0.93,
tags: ["board_games", "strategy", "2-4_players"],
shortDescription: "Stolüstü oyun sevənlər üçün əla hədiyyə.",
},
// ...
];
export function suggestGifts(input: SuggestGiftsInput): GiftItem[] {
if (input.maxBudget <= 0) {
throw new Error("Büdcə müsbət ədəd olmalıdır.");
}
const filtered = CATALOG.filter(
(item) => item.price <= input.maxBudget
);
// Sadələşdirilmiş: sadəcə score üzrə sıralayıb ilk 3-ü götürürük
return filtered.sort((a, b) => b.score - a.score).slice(0, 3);
}
İndi MCP‑alətinin handler‑ində biz bunlarla məşğul oluruq:
- input-un pars edilməsi;
- SuggestGiftsInput tipinə map edilməsi;
- suggestGifts-in təhlükəsiz çağırılması;
- nəticənin ChatGPT və UI üçün anlaşılan formata qablaşdırılması.
5. Handler reallaşdırılması: input‑dan structuredContent‑ə
registerTool-u route.ts-də biznes məntiqindən istifadə edərək yenidən yazaq:
// app/mcp/route.ts (fraqment)
import { suggestGifts, SuggestGiftsInput } from "./gifts";
server.registerTool(
"suggest_gifts",
{
title: "Hədiyyələrin seçimi",
description:
"Maraqlar, büdcə və münasibət növünə görə hədiyyə seçmək lazım olduqda istifadə et.",
inputSchema: {
type: "object",
properties: {
age: {
type: "integer",
minimum: 0,
maximum: 120,
description: "Alıcının yaşı (əgər məlumdursa).",
},
relationship: {
type: "string",
enum: ["friend", "partner", "colleague"],
description: "Alıcı ilə münasibət növü.",
},
maxBudget: {
type: "number",
minimum: 1,
description: "Maksimal büdcə (USD).",
},
interests: {
type: "array",
items: { type: "string" },
description: "Alıcının maraqları (məsələn, board games, hiking).",
},
},
required: ["relationship", "maxBudget", "interests"],
},
},
async ({ input }) => {
// Baza məntiqi doğrulama
if (!Array.isArray(input.interests) || input.interests.length === 0) {
return {
isError: true,
content: [
{
type: "text",
text: "Ən azı bir marağı göstərmək lazımdır.",
},
],
structuredContent: { errorCode: "NO_INTERESTS" },
};
}
const payload: SuggestGiftsInput = {
age: input.age ?? null,
relationship: input.relationship,
maxBudget: input.maxBudget,
interests: input.interests,
};
const items = suggestGifts(payload);
if (items.length === 0) {
return {
content: [
{
type: "text",
text:
"Verilən büdcə daxilində uyğun hədiyyələr tapmadım. Büdcəni artırmağı və ya maraqları dəyişməyi yoxlayın.",
},
],
structuredContent: {
items: [],
emptyReason: "NO_MATCHES",
},
};
}
return {
content: [
{
type: "text",
text: `Uyğun ${items.length} hədiyyə variantı tapdım.`,
},
],
structuredContent: {
items: items.map((item) => ({
id: item.id,
title: item.title,
price: item.price,
currency: item.currency,
shortDescription: item.shortDescription,
tags: item.tags,
})),
},
};
}
);
Burada bir neçə vacib nüans var.
Birincisi, interests-in boş siyahı olmadığını açıq şəkildə yoxlayırıq. JSON Schema formal olaraq boş massivə icazə versə belə, bizim üçün belə sorğu mənasızdır. Təsadüfi siyahı düzəltməyə çalışmaqdansa, aydın xəta qaytarmaq daha yaxşıdır.
İkincisi, iki məlumat dəsti qaytarırıq:
- content — model üçün. Bu, qısa mətn xülasəsidir: “N variant tapıldı”. Model bunu istifadəçiyə cavabında istifadə edəcək.
- structuredContent — həm model, həm də UI üçün. Bu, artıq hədiyyələr siyahısı olan strukturlu JSON-dur; vidcetimiz onu kartlarla çəkə bilər.
Tez-tez edilən səhv — bütün JSON “portyankasını” content-ə yükləməkdir. Belə etmək lazım deyil: model bunun üçün token sərf edir və çaşqınlaşa bilər. content-i qısa saxlayın, detalları isə structuredContent-ə qoyun.
6. UI şablonu və _meta/openai/outputTemplate əlavə edək
Apps SDK səviyyəsində server həm də ChatGPT‑yə alət nəticəsinin vizuallaşdırılması üçün hansı UI şablonundan istifadə etməyi deyir. Bu, resurslar və _meta["openai/outputTemplate"] vasitəsilə edilir: server mimeType: "text/html+skybridge" olan HTML resursu qeydiyyatdan keçirir, alət isə cavabında ona istinad edir.
Next.js şablonunda bu adətən rahat bir wrapper altında gizlədilir, amma sadələşdirilmiş formada belə görünür:
// MCP-serverin təşəbbüsü zamanı hardasa
server.registerResource("ui://widget/gifts.html", {
name: "Gift suggestions widget",
mimeType: "text/html+skybridge",
// sonra: HTML-i ötürmə üsulu (gömülü şablon və ya fayl)
});
Alət cavabında isə:
return {
content: [{ type: "text", text: ` ${items.length} hədiyyə tapdım.` }],
structuredContent: { items: /* ... */ },
_meta: {
"openai/outputTemplate": "ui://widget/gifts.html",
},
};
Bu halda ChatGPT yalnız nəticənin strukturunu anlamayacaq, həm də vidcet üçün lazım olan HTML/JS‑i yükləyəcək, iframe daxilindəki React komponentimiz isə window.openai.toolOutput-u oxuyub hədiyyələr siyahısını çəkəcək.
UI hissəsini daha detallı Moduldakı ToolOutput → UI işlənməsi haqqında mühazirələrdə danışacağıq, ona görə indi yalnız əlaqəyə diqqət edirik: alət handler‑i təkcə biznes məlumatlarına cavabdeh deyil, həm də nəticənin hansı UI şablonuna bağlanacağına cavabdehdir. Burada bu bağı məhz MCP‑serverin gözü ilə görürük: hansı şablonu göstərməli və structuredContent-ə nə qoymalı.
Insight
ChatGPT yaradıcıları vidceti JSON-un göstərilməsi üçün şablon kimi düşünüblər. Buna görə də onun üçün outputTemplate adı istifadə olunur. İdeya belədir: ChatGPT mcp-tool çağırır, mcp-tool JSON qaytarır və bəzən vidcet qaytarır. Əgər vidcet olmayıbsa, ChatGPT JSON-u necə göstərmək lazım olduğuna özü qərar verir.
Əgər vidcet göstərilibsə, ChatGPT vidceti göstərir, JSON-u toolOutput kimi vidcetə ötürür və vidcet JSON-u göstərməlidir. Vadcet — JSON-u göstərmək üçün şablondur. Məhz buna görə o, tətbiq Store-da qeydiyyat mərhələsində keşlənir.
Vidcetdən sizə uyğun şəkildə istifadə edə bilərsiniz: onun içində fetch() çağırmaq olar. Amma ChatGPT inkişaf etdiricilərinin ilkin niyyətini anlasanız, bəzi məhdudiyyətləri və ehtimal edilən gələcək dəyişiklikləri qəbul etmək daha asan olar.
7. Handler-də avtorizasiya və giriş
İndiyə qədər hər şeyin publik olduğunu farz edirdik. Praktikada isə bəzi alətlər avtorizasiya tələb edir: istifadəçi hesabına, sifarişlərinə, ödənişlərinə, sənədlərinə və s. giriş.
Apps SDK / MCP terminologiyasında alətə securitySchemes təyin etmək və sonra handler‑də tokenləri və konteksti yoxlamaq olar.
Ən sadə nümunə:
server.registerTool(
"list_user_orders",
{
title: "İstifadəçinin sifarişlərinin siyahısı",
description: "Avtorizə olunmuş istifadəçinin son sifarişlərini qaytarır.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
_meta: {
securitySchemes: [{ type: "oauth2", scopes: ["orders.read"] }],
}
},
async ({ auth }) => {
if (!auth?.accessToken) {
return {
isError: true,
content: [
{
type: "text",
text: "Sifarişləri görmək üçün hesabınıza daxil olmalısınız.",
},
],
_meta: {
// ChatGPT-dən OAuth UI-ni göstərməyi xahiş edirik
"mcp/www_authenticate": [
'Bearer resource_metadata="https://your-mcp.example.com/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="Davam etmək üçün avtorizə olun."',
],
},
};
}
// Burada token, issuer, audience, scope yoxlanılır...
const orders = await fetchUserOrders(auth.accessToken);
return {
content: [
{
type: "text",
text: `Son ${orders.length} sifarişi tapdım.`,
},
],
structuredContent: { orders },
};
}
);
Burada başa düşmək vacibdir ki:
- ChatGPT sizin yoxlamalarınızı “özbaşına tapmır”. O, yalnız tokenləri və konteksti ötürür, siz isə mütləq normal avtorizasiya etməlisiniz.
- Xüsusi _meta["mcp/www_authenticate"] sahəsi platformaya belə deyir: “istifadəçiyə giriş/davam üçün token yeniləmə UI‑si göstər”. Bunsuz ChatGPT sadəcə xətanı görəcək.
Avtorizasiyanın çətinliklərini ayrıca 10‑cu modulda danışacağıq. İndi əsas konsepsiya kifayətdir: tokeni handler‑də yoxlayın, modelə sözünüzə görə güvənməyin.
8. Xarici API və DB ilə iş: qatlar və praktikalar
“Hər şeyi handler‑də etmək” cazibəsi çox böyükdür: arqumentlərin parsı, bazaya sorğu, filtrləmə, structuredContent-ə mappinq, loqlaşdırma və bir az fəlsəfə — hamısı 150 sətrlik bir funksiyada. Bu, təxminən bütün tətbiqi pages/index.tsx-də yazmağa bənzəyir — olar, amma ağrılıdır.
Daha yaxşısı qatları ayırmaqdır:
// gifts-repository.ts
import type { GiftItem } from "./gifts";
export async function fetchGiftsFromApi(
maxBudget: number,
interests: string[]
): Promise<GiftItem[]> {
const resp = await fetch("https://example.com/api/gifts", {
method: "POST",
body: JSON.stringify({ maxBudget, interests }),
headers: { "Content-Type": "application/json" },
});
if (!resp.ok) {
throw new Error(`Gift API error: ${resp.status}`);
}
const data = (await resp.json()) as GiftItem[];
return data;
}
// gifts.ts (yenilənmiş)
import { fetchGiftsFromApi } from "./gifts-repository";
export async function suggestGifts(input: SuggestGiftsInput): Promise<GiftItem[]> {
if (input.maxBudget <= 0) {
throw new Error("Büdcə müsbət ədəd olmalıdır.");
}
const items = await fetchGiftsFromApi(input.maxBudget, input.interests);
return items.sort((a, b) => b.score - a.score).slice(0, 3);
}
// route.ts (handler fraqmenti)
async ({ input }) => {
try {
const payload: SuggestGiftsInput = {
age: input.age ?? null,
relationship: input.relationship,
maxBudget: input.maxBudget,
interests: input.interests,
};
const items = await suggestGifts(payload);
// ...
} catch (err) {
console.error("suggest_gifts failed", err);
return {
isError: true,
content: [
{
type: "text",
text: "Hədiyyə seçimi zamanı xəta baş verdi. Xahiş edirik daha sonra yenidən cəhd edin.",
},
],
structuredContent: {
errorCode: "INTERNAL_ERROR",
},
};
}
}
Bu yanaşmanın bir neçə üstünlüyü var.
- Test ediləbilənlik: MCP‑serveri qaldırmadan suggestGifts və fetchGiftsFromApi üçün unit testlər yaza bilərsiniz.
- Oxunaqlılıq: handler MCP protokolu ilə biznes məntiqi arasında nazik adapter olaraq qalır.
- Təkrar istifadə: sonradan eyni hədiyyə seçimi başqa yerdə (məsələn, ayrıca REST‑API) lazım olsa, məntiqi MCP‑dən “söküb çıxarmağa” ehtiyac olmayacaq.
9. Loqlaşdırma və baza observabillik
Alətlərin server reallaşdırılması — minimal observabilliyi dərhal təmin etmək üçün əla yerdir. Prodakşnda bilmək istəyəcəksiniz:
- hansı alətlər çağırılır;
- hansı arqumentlərlə (əlbəttə, PII olmadan);
- emal nə qədər vaxt aparır;
- nə qədər və hansı xətalar var.
Hazırda ChatGPT App quruluşunu öyrənirik, buna görə peşəkar loqgerlərlə işi sonraya saxlayırıq. Handler‑lərin ətrafında ən sadə wrapper‑loqger belə görünə bilər:
// simple-logger.ts
export function logToolInvocationStart(tool: string, args: unknown) {
console.log(
JSON.stringify({
level: "info",
event: "tool_invocation_started",
tool,
timestamp: new Date().toISOString(),
// Prod-da heç vaxt PII loqlamayın!
args,
})
);
}
export function logToolInvocationEnd(tool: string, ms: number, success: boolean) {
console.log(
JSON.stringify({
level: "info",
event: "tool_invocation_finished",
tool,
durationMs: ms,
success,
timestamp: new Date().toISOString(),
})
);
}
// route.ts (handler üçün wrapper)
import { logToolInvocationStart, logToolInvocationEnd } from "./simple-logger";
server.registerTool(
"suggest_gifts",
{ /* ...meta... */ },
async ({ input }) => {
const startedAt = Date.now();
logToolInvocationStart("suggest_gifts", {
relationship: input.relationship,
maxBudget: input.maxBudget,
interestsCount: Array.isArray(input.interests)
? input.interests.length
: 0,
});
try {
// ... əsas məntiq ...
const duration = Date.now() - startedAt;
logToolInvocationEnd("suggest_gifts", duration, true);
return result;
} catch (err) {
const duration = Date.now() - startedAt;
logToolInvocationEnd("suggest_gifts", duration, false);
throw err;
}
}
);
Gələcəkdə, metrikalar, SLO və monitorinq modullarında bu loqlara əsasən qrafiklər və alertrlər qura biləcəksiniz. Amma loqlaşdırma vərdişini elə indi formalaşdırmaq yaxşıdır.
10. Server nəticəsi vidcetə necə düşür (və əksinə)
6‑cı bölmədə alət nəticəsini _meta["openai/outputTemplate"] vasitəsilə UI şablonuna bağlamışdıq. İndi bu yolu digər tərəfdən — həmin structuredContent‑in React vidcetinin içinə necə düşdüyünü və UI‑də onunla nə etmək lazım olduğunu nəzərdən keçirək.
Bu mühazirə serverə fokuslansa da, təkcə “model üçün API” yox, həm də “UI üçün API” dizayn etdiyinizi anlamaq vacibdir. Server aşağıdakıları qaytarır:
- structuredContent — həm modelin, həm də vidcetin gördüyü ( toolOutput vasitəsilə) məlumatlar;
- content — nəticənin model üçün “sıxılmış” təsviri;
- _meta — vidcet üçün özəl sahələr: openai/outputTemplate, openai/widgetCSP, openai/widgetDomain və s.
React vidcetinin içində sonra təxminən belə edirsiniz:
// app/page.tsx (fraqment)
type ToolOutput = {
items?: {
id: string;
title: string;
price: number;
currency: string;
shortDescription: string;
tags: string[];
}[];
emptyReason?: string;
};
declare global {
interface Window {
openai?: {
toolOutput?: ToolOutput;
};
}
}
export default function GiftWidget() {
const output = typeof window !== "undefined"
? window.openai?.toolOutput
: undefined;
if (!output) {
return <div>Hədiyyə seçimi nəticələrini gözləyirəm…</div>;
}
if (!output.items || output.items.length === 0) {
return <div>Uyğun hədiyyə yoxdur. Şərtləri dəyişməyə cəhd edin.</div>;
}
return (
<ul>
{output.items.map((item) => (
<li key={item.id}>
<strong>{item.title}</strong> — {item.price} {item.currency}
</li>
))}
</ul>
);
}
Məhz buna görə structuredContent-in sabit kontraktı olması və UI üçün rahat olması çox önəmlidir: ayrı sahələr, 10 səviyyə nested‑cəhənnəm yox.
Bu yolu Modul 4‑ün ayrıca mühazirəsində ətraflı danışacağıq, burada isə fiks edirik: server və vidcet eyni structuredContent strukturuna söykənir.
11. Serverdə xəta emalı: format və strategiya
8–9‑cu bölmələrdə handler daxilində xətalar və loqlaşdırmaya azca toxunduq. İndi bunları vahid formata yığaq: alət xətalarını model və UI ilə işləyə biləcək şəkildə necə qaytarmaq lazımdır.
Handler‑lərdə xətalar qaçılmazdır: haradasa xarici API yıxılacaq, haradasa pis input gələcək, haradasa siz özünüz səhv yazacaqsınız. Əsas — model və istifadəçi üçün onları “izahsız 500 Internal Server Error”‑a çevirməməkdir.
Yaxşı server reallaşdırması:
- istifadəçi/model doğrulama xətaları ilə daxili xətaları ayırd edir;
- isError üçün aydın sahə və structuredContent-də anlaşılan errorCode qaytarır;
- content-də insana dost mesaj verir.
Nümunə (tutaq ki, alətin metadatası — title, description, inputSchema və s. — artıq təkrarı azaltmaq üçün meta dəyişəninə çıxarılıb):
function makeErrorResult(message: string, code: string) {
return {
isError: true,
content: [
{
type: "text",
text: message,
},
],
structuredContent: {
errorCode: code,
},
};
}
server.registerTool(
"suggest_gifts",
meta,
async ({ input }) => {
try {
if (input.maxBudget > 10000) {
return makeErrorResult(
"Büdcə çox böyükdür. Sorğunu dəqiqləşdirin (10000 USD-dək).",
"BUDGET_TOO_HIGH"
);
}
const items = await suggestGifts({
age: input.age ?? null,
relationship: input.relationship,
maxBudget: input.maxBudget,
interests: input.interests,
});
if (!items.length) {
return {
content: [
{
type: "text",
text:
"Bu büdcəyə hədiyyə tapılmadı. Maraqları dəyişməyə və ya büdcəni artırmağa cəhd edin.",
},
],
structuredContent: {
items: [],
emptyReason: "NO_MATCHES",
},
};
}
return {/* normal nəticə */};
} catch (err) {
console.error(err);
return makeErrorResult(
"Hədiyyə seçimi zamanı serverin daxili xətası.",
"INTERNAL_ERROR"
);
}
}
);
Belə format modelə də kömək edir (o, arqumentləri dəyişməyə cəhd edə bilər), UI‑yə də (vidcet müxtəlif errorCode üçün spesifik mesajlar göstərə bilər).
Dayanıqlılıq, idemponentlik və alətlərin təhlükəsiz dizaynı barədə bir neçə mühazirə sonra danışacağıq, amma artıq indi öyrəşmək faydalıdır: qəribə bir şey “səssizcə” etməkdənsə, xətanı açıq qaytarmaq daha yaxşıdır.
Mühazirənin sonunda serverdə alətlərin reallaşdırılmasında tipik xətaları check‑list kimi istifadə etmək rahat olsun deyə bir siyahıda toplayacağıq.
12. Qısa end‑to‑end nümunə: sorğudan cavaba
HədiyyəGenius tətbiqimizdə gördüklərimizi məntiqi zəncirə yığaq.
- İstifadəçi ChatGPT‑yə yazır:
“Dost üçün hədiyyə seç, o, stolüstü oyunları sevir, büdcə 50 dollaradək”. - Model suggest_gifts alətini və onun sxemini bildiyinə görə, onu çağırmağa qərar verir və tool_call formalaşdırır:
{ "tool": "suggest_gifts", "arguments": { "relationship": "friend", "maxBudget": 50, "interests": ["board games"], "age": null } } - Platforma bu JSON‑RPC-ni bizim MCP‑serverə göndərir (POST /app/mcp), Next.js isə gövdəni server.handle(...)‑a ötürür.
- Bizim suggest_gifts handler‑i:
- interests-in boş olmadığını doğrulayır;
- suggestGifts(payload) çağırır;
- GiftItem[] massivini alır (score üzrə top‑3);
- bunu structuredContent.items-ə qablaşdırır və _meta["openai/outputTemplate"] = "ui://widget/gifts.html" əlavə edir.
- ChatGPT cavabı alır, structuredContent-i kontekstə qoyur, vidcetin HTML resursunu — gifts.html — yükləyir və ora toolOutput ötürür.
- Bizim React vidcet window.openai.toolOutput.items-i oxuyur və hədiyyələr siyahısını çəkir; model isə content və structuredContent əsasında istifadəçiyə niyə bu hədiyyələrin uyğun olduğunu izah edir.
- İstifadəçi, məsələn, vidcetdə “Daha çox göstər” düyməsinə basır — vidcet SDK vasitəsilə callTool çağırır → yenə bizim handler‑ə düşür, amma artıq başqa arqumentlərlə (məsələn, artırılmış büdcə).
Bu zənciri tutan əsas şey odur ki, alətin server reallaşdırılması:
- razılaşdırılmış JSON Schema üzrə strukturlu input qəbul edir;
- məlumatları səliqə ilə doğrulayır;
- izolyasiya olunmuş biznes məntiqini çağırır;
- sabit structured output qaytarır;
- lazım gəldikdə UI şablonunu və metadataları göstərir.
13. Alətlərin server reallaşdırılmasında tipik xətalar
Xəta №1: “Hər şey bir yerdə” — nəhəng handler.
Bütün məntiq və xarici API ilə iş server.registerTool(..., async () => { ... }) daxilində yaşayanda kod sürətlə böyüyür və oxunmaz monolitə çevrilir. Kiçik dəyişiklikdə belə hər şey birdən qırılır. Biznes məntiqini ayrıca funksiyalara/modullara çıxarmaq və handler‑i nazik adapter kimi saxlamaq daha yaxşıdır.
Xəta №2: JSON‑sxemə kor‑koranə inam.
Tərtibatçılar tez-tez belə düşünür: “Sxem var — deməli giriş həmişə etibarlıdır”. Amma model qəribə dəyərlər göndərə bilər, xarici müştərilər isə lap çox. Təkcə tiplərə və JSON Schema‑ya güvənmək olmaz — məntiqi doğrulama lazımdır (büdcə sərhədləri, massiv uzunluqları, icazə verilən dəyərlər və s.).
Xəta №3: Hər şeyi content‑ə yığmaq və structuredContent‑i görməməzlikdən gəlmək.
Bəzən content-ə “birdən gərək olar” deyə böyük JSON sətirdə qoyulur. Bu, modelin istinadını səs-küylü və token baxımından baha edir, UI isə əziyyət çəkir, çünki sətiri dekodlamaqdansa, normal strukturu almaq istəyir. content-i qısa saxlayın, detalları structuredContent-ə qoyun.
Xəta №4: Structured output formatının qeyri‑sabitliyi.
Bu gün items — id, title, price sahələri olan obyektlər massividir, sabah qəfil price-ı amount ilə əvəz edirsiniz və vidcet yıxılır. Və ya yeni bir nested səviyyə əlavə edirsiniz. Belə dəyişikliklər ya kontraktı versiyalaşdırmağı, ya da sxemi kiçik addımlarla inkişaf etdirməyi tələb edir. Əks halda UI və testlər daima qırılacaq.
Xəta №5: Mənalı xəta emalının olmaması.
İstisna atıb platformanın bunu “nə iləsə” emal edəcəyinə ümid etmək yaxşı strategiya deyil. Model anlaşılmaz JSON‑RPC error görəcək, istifadəçi — qırmızı panel, siz isə problemin kontekstini itirəcəksiniz. isError, errorCode və insan oxunuşlu mesajı açıq qaytarmaq, detalları serverdə loqlamaq daha yaxşıdır.
Xəta №6: Avtorizasiyanı görməməzlikdən gəlmək və modelə güvənmək.
Bəzən tərtibatçılar düşünür: “Model ağıllıdır, istifadəçi avtorizə olmayıbsa aləti çağırmaz”. Əslində model sizin ACL və limitlərdən xəbərsizdir, o yalnız alət təsvirlərini görür. Bütün hüquq yoxlamaları alətin server handler‑ində olmalıdır, alətin necə təsvir olunmasından asılı olmayaraq.
Xəta №7: PII daxil olmaqla hər şeyi loqlamaq.
Çox asanlıqla bütün input-u tam halda loqlamaq vərdişi formalaşır. ChatGPT App kontekstində bu, PII (ad, e‑mail, ünvan və s.) daxil edə bilər ki, bu da həm OpenAI siyasətini, həm də sağlam düşüncəni pozur. Yalnız aqreqatlaşdırılmış/anonymizasiya olunmuş məlumatları loqlamaq daha yaxşıdır: münasibət növü, büdcə aralığı, maraqların sayı.
Xəta №8: Xarici API‑lərlə işləyərkən timeout və retry olmaması.
Əgər handler daxilində alət fetch ilə xarici API‑yə timeout və təkrar cəhdlər olmadan çıxırsa, həmin API‑nin istənilən gecikməsi “ChatGPT dondu” kimi görünəcək. İstifadəçi bütün tətbiqin sıradan çıxdığını düşünəcək. Server tərəfində zaman limiti qoymaq, timeoutları emal etmək və məntiqli xəta qaytarmaq lazımdır.
GO TO FULL VERSION