1. Why instructions are not enough without good tools and metadata
It’s important to state an inconvenient truth: the model cannot see your code. It doesn’t know which controllers you have in Next.js, which functions in TypeScript, or what delightful heuristics you’ve built into your recommendation service.
It sees your App through several interfaces:
- System‑prompt (role contract).
- Tool descriptions: name, description, inputSchema, outputSchema, annotations, etc.
- The app’s own metadata: name, icon, short and long description, categories, conversation starters, and so on.
When processing a request, the model looks at the conversation context and these metadata to decide:
- whether it should suggest any App at all;
- if yes — which one among the available Apps;
- and if the App is chosen — which specific tool of that App fits the current request.
In the previous part of Module 5, we focused on what you can “tell” the model in words — the system‑prompt and UX instructions. Now we move to what it sees beyond text: tools and metadata.
Therefore, Module 5’s task is actually twofold. First, in the system‑prompt you formulate “what this App should do and how it should behave,” and then in the design of tools and metadata you package it into a form the model can actually use — including for discovery and routing.
You can think of it this way: the system‑prompt is the constitution, and the tools and metadata are the laws and the bureaucracy around it: application forms, database schemas, etc. If you only stick to the constitution, you won’t get far.
2. Decomposition: “one task — one tool,” but be sensible
Let’s start with the most painful issue: how many tools to build and how to slice them.
An intuitive principle: one tool — one clear task. This greatly simplifies the model’s choice: instead of one monstrous function do_everything, it gets several tidy actions with good names.
For GiftGenius, we might have these basic tools:
- profile_to_segments — turn a freeform recipient description (age, interests, relationship, context) into normalized segments like "tech", "fitness", "gamer".
- recommend_gifts — select a list of gift IDs by segments, budget, locale, and occasion.
- get_gift — fetch a full card of the selected gift (description, media, SKUs/variants) by its ID.
- (optional) similar_gifts — given a chosen gift, suggest 3–5 similar options.
In theory, you could make one gift_tool with a mode: "profile_to_segments" | "recommend" | "details" | "similar", but then you complicate life for both yourself and the model: the description turns into a wall of text, the inputSchema grows large, and the model has fewer crisp anchors when choosing a tool.
Anti‑pattern: God Tool
Imagine a scheme like this:
server.registerTool(
"gift_tool",
{
description: "Various operations with gifts.",
inputSchema: { /* 50 fields and flags */ },
},
async ({ input }) => { /* huge switch by mode */ }
);
In the model’s head, this looks like “there’s some abstract tool about gifts; we’ll figure out the rest later.” This worsens selection accuracy, hurts discovery, and makes maintenance harder for you.
But going to the other extreme — 50 microscopic tools for every sneeze — is also bad. Each additional tool ends up in context, taxes the model’s attention, and increases the risk of routing errors. The documentation explicitly warns: too many tiny tools is a quality negative, especially when their descriptions overlap.
A practical rule of thumb:
- anything the user perceives as a single “step” in the flow (e.g., the initial gift selection by profile) is a good candidate for a separate tool;
- anything that always runs strictly within that step and has no standalone meaning (e.g., compute a score or log card views) is better left inside the tool implementation.
Suppose you’ve sliced the scenarios into 2–4 tools along this principle. Next comes an important question — how to describe those tools’ inputs so the model can use them without guessing. Let’s start there.
3. Project use cases onto the input schema
Now take one concrete use case and honestly assess which data the tool really needs.
Scenario: “Giver on a deadline: pick 5–7 ideas for a 25‑year‑old friend who loves soccer and board games, budget up to $50.”
From jobs‑to‑be‑done, it’s clear GiftGenius’s recommendation core should narrow the options to a short list and reduce the anxiety of “what if I pick something lame.” At chat level, the assistant needs:
- basic info about the recipient (age, gender, relationship to the giver);
- interests/hobbies;
- budget and currency;
- occasion (birthday, anniversary, New Year, etc.);
- optional — country/city to filter by delivery.
In GiftGenius’s architecture, this is split into two steps:
- profile_to_segments(input) takes “raw” data (age, interests, textual description) and converts them into normalized segments that are convenient to work with further.
- recommend_gifts(segments, budget, locale, occasion) then selects specific gift IDs from the catalog by segments and budget.
From the perspective of the ChatGPT ↔ MCP contract, it’s important to describe the second step — the recommend_gifts schema — because this tool will be used in most recommendation scenarios.
At the same time, you don’t have to demand everything from the user up front: the model can collect some pieces via a follow‑up (“roughly what budget?”). So some fields in the profile can be optional; but by the time we call recommend_gifts, it should have a normalized set of parameters.
Example: TypeScript + JSON Schema for recommend_gifts
In a TypeScript MCP server, it might look like this:
// apps/mcp/server.ts
import { McpServer } from "@openai/mcp-server";
const server = new McpServer();
server.registerTool(
"recommend_gifts",
{
title: "Gift recommendations",
description:
"Use this tool when you need to select gifts by recipient segments, budget, locale, and occasion.",
inputSchema: {
type: "object",
properties: {
segments: {
type: "array",
description:
"A list of recipient segments, for example ['tech', 'football_fan']. Usually comes from profile_to_segments.",
items: { type: "string" },
minItems: 1
},
budget: {
type: "object",
description:
"Gift budget range in the user’s currency (min/max).",
properties: {
min: {
type: "number",
minimum: 0,
description: "The minimum amount the user is willing to spend."
},
max: {
type: "number",
minimum: 0,
description: "The maximum amount the user is willing to spend."
},
currency: {
type: "string",
minLength: 3,
maxLength: 3,
description: "Three‑letter currency code (e.g., USD, EUR, RUB)."
}
},
required: ["min", "max", "currency"]
},
locale: {
type: "string",
description:
"User locale in BCP‑47 format (e.g., 'ru-RU' or 'en-US')."
},
occasion: {
type: "string",
description:
"Gift occasion, for example 'birthday', 'new_year', 'anniversary'."
}
},
required: ["segments", "budget", "locale", "occasion"]
}
},
async ({ input }) => {
// We won’t get fancy yet; return a stub
return {
content: [
{
type: "text",
text: `Selecting gifts for segments ${input.segments?.join(
", "
)} in the budget ${input.budget?.min}–${input.budget?.max} ${input.budget?.currency}...`
}
],
structuredContent: {}
};
}
);
Notice a couple of things.
First, we actively use enum-like constraints and clear descriptions. Even if it’s formally just strings, the description hints to the model what values are expected, which noticeably increases the chance it will fill the arguments correctly. Instead of a vague string like "occasion": "something like a birthday", we have a neat occasion: "birthday".
Second, field descriptions are written not “for the team,” but literally as hints for the model: what the field is, what typical values are, whether there’s an example. The Apps SDK docs explicitly recommend adding human‑friendly descriptions and examples for each parameter.
What should not be in the input schema
Common parasitic fields that people often try to cram in:
- internal identifiers (tenantId, internalSegment) that you can insert on the server anyway;
- things the model cannot possibly know (for example, deploymentRegion) — that’s your responsibility;
- fields duplicating chat history (e.g., userPrompt): the model already sees the original message, don’t make it copy‑paste it.
The input schema is precisely what the model should determine and fill in, not a general bag of everything.
4. Output schema: not just data but meaning
In the Apps SDK, a tool’s result is sent back into the dialogue as a role: tool message. The model then decides what to do with it: how to format the answer, which follow‑ups to ask, whether to open a widget, and so on. So output schema design is no less important than input.
There are two approaches.
The “raw data” variant looks like this:
{
"items": [
{ "id": "GIFT_1" },
{ "id": "GIFT_2" }
]
}
The model sees a plain list of IDs without understanding why these options are here at all, how many candidates there were, and which ones are best. It may make something up, but the odds of oddities are higher.
The semantically rich variant:
{
"items": [
{
"id": "GIFT_1",
"score": 0.92,
"reason": "Strongly matches the 'football_fan' segment and fits the budget."
},
{
"id": "GIFT_2",
"score": 0.81,
"reason": "Good for a board‑game enthusiast, slightly near the top of the budget."
}
],
"meta": {
"totalCandidates": 27,
"returned": 5,
"segmentsUsed": ["football_fan", "board_games"],
"budget": { "min": 20, "max": 50, "currency": "USD" },
"advice": "It’s best to start with items with the highest score and a clear explanation."
}
}
Now the model can honestly explain why exactly these gifts and build follow‑ups: “I found 27 options, showing the top 5, here’s why these ones.”
Example: define the output schema for recommend_gifts
Let’s add a result schema to the tool description (even if technically you can omit it — it’s better to include it as part of the contract with the model):
const recommendGiftsOutputSchema = {
type: "object",
properties: {
items: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string", description: "Gift ID in the catalog." },
score: {
type: "number",
description: "Match score for the profile (0..1)."
},
reason: {
type: "string",
description:
"A short explanation of why the gift fits (can be generated on the backend)."
}
},
required: ["id", "score"]
},
description: "List of recommended gifts with relevance scores."
},
meta: {
type: "object",
properties: {
totalCandidates: {
type: "integer",
description: "How many total candidates were found in the catalog."
},
returned: {
type: "integer",
description: "How many gifts this call returned."
},
advice: {
type: "string",
description:
"General advice: for example, which types of gifts to start with."
}
}
}
},
required: ["items"]
};
And use this schema within the implementation:
server.registerTool(
"recommend_gifts",
{
title: "Gift recommendations",
description:
"Use when you need to select 3–7 gifts by segments and budget. Returns gift IDs and match scores; fetch detailed cards via get_gift.",
inputSchema: /* as above */,
// outputSchema is not always specified formally, but it’s useful for documentation:
// outputSchema: recommendGiftsOutputSchema
},
async ({ input }) => {
const recommendations = await recommendFromCatalog(input); // our business logic
return {
content: [
{
type: "text",
text: `Found ${recommendations.items.length} suitable ideas. I’ll show the best ones now.`
}
],
structuredContent: {
items: recommendations.items,
meta: {
totalCandidates: recommendations.meta.totalCandidates,
returned: recommendations.items.length,
advice: recommendations.meta.advice
}
}
};
}
);
We do two things: give the model minimal text for the user and simultaneously provide semantic JSON it can use to drive the dialogue and follow‑ups.
Meanwhile, get_gift will fetch full cards (name, media, SKUs, etc.) by ID, and the GiftGenius widget will render them as gift cards.
5. Naming and tool descriptions as the foundation of discovery
Now the fun part: how tool names and descriptions affect whether the model will call them.
Documentation and best practices for metadata recommend:
- use action‑oriented names: profile_to_segments, recommend_gifts, get_gift, similar_gifts, not tool1, search, do_stuff;
- start the description in the style of “Use this when…,” describing trigger scenarios and constraints (“do not use for…”).
This directly ties to your golden prompt set. The description phrasing should overlap with real user requests. If the description says “Use when the user asks to pick a gift by budget and recipient interests” and your golden prompt includes “pick a gift for a gamer friend up to $50,” the model will find it much easier to match the request to the tool.
Example of a good tool description
Consider an additional GiftGenius tool — similar_gifts, which helps broaden the selection with similar ideas based on a specific gift:
server.registerTool(
"similar_gifts",
{
title: "Similar gifts",
description:
"Use this tool when the user has selected a specific gift and wants to see a few similar options. Do not use it for the first selection from scratch — that’s what recommend_gifts is for.",
inputSchema: {
type: "object",
properties: {
giftId: {
type: "string",
description:
"The gift identifier from the previous selection for which similar options should be found."
},
limit: {
type: "integer",
description:
"How many similar gifts to return (default 3–5).",
minimum: 1,
default: 5
}
},
required: ["giftId"]
}
},
async () => {
/* ... */
}
);
Important points:
- We explicitly say when to use the tool and when not to.
- The description contains the words “similar options,” “selected a specific gift” — the same ones that will often appear in real user requests.
- We avoid overlap with recommend_gifts — this reduces competition between tools at selection time.
Example of a bad description
description: "Gift operations."
The model learns almost nothing from such a description. This tool might only be used if GPT is already desperately trying to “grab something at random.”
6. Annotations and hints: how to indicate the seriousness of an action to the model
A tool is more than just a name and a schema; there are annotations that tell ChatGPT how dangerous/important the action is and whether it should ask for user confirmation. The Apps SDK spec has different hints for this, such as readOnlyHint, destructiveHint, openWorldHint, and others.
- readOnlyHint: true says the tool only reads data and does not change state. The assistant can then skip extra confirmations and call it more freely.
- destructiveHint: true signals the tool can delete or irreversibly change something, so the user should be shown an explicit “Are you sure?”.
- openWorldHint: true indicates the action touches the outside world (posting to social media, creating a record outside the account, etc.), which should also be called out.
Minimum level — no confirmations
If you have public read‑only tools, it makes sense to mark them as readOnlyHint: true. Example:
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"openWorldHint": false
}
Such tools can be called without extra dialogue confirmations from GPT.
Single confirmation
If you have tools that change something on the server, it’s logical to mark them as readOnlyHint: false:
"annotations": {
"readOnlyHint": false,
"destructiveHint": false,
"openWorldHint": false
}
Seeing such a tool, the model will most likely request user confirmation once (typically a modal dialog in the ChatGPT UI).
Dangerous action
If you have a tool that deletes something on the server, mark it as destructiveHint: true:
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": false
}
The model will call this tool very cautiously and will check twice:
- first it will ask for confirmation in text,
- then the platform will show a standard dialog window.
For our GiftGenius, within this module we’re not writing commerce tools yet, but we can outline what a future create_gift_order would look like:
server.registerTool(
"create_gift_order",
{
title: "Create a gift order",
description:
"Use only after the user has explicitly agreed to purchase the selected gift. Creates an order in the system and returns its status.",
inputSchema: {
type: "object",
properties: {
giftId: {
type: "string",
description: "ID of the gift the user selected."
},
deliveryEmail: {
type: "string",
description: "Email to which the digital gift should be sent."
}
},
required: ["giftId", "deliveryEmail"]
},
annotations: {
destructiveHint: true,
openWorldHint: true
}
},
async () => {
/* ... */
}
);
Annotations don’t replace your server‑side permission checks; they simply help ChatGPT shape the UX: ask for confirmation, show a warning, and not execute such tools “silently.”
7. App metadata and two levels of discovery
Tools are half the story. The other half is how the user finds and launches your App in the first place.
In the ChatGPT ecosystem, there are two key levels of discovery.
The first is in‑conversation discovery. When the user writes something in chat (even without explicitly mentioning an App), the model looks at:
- the message text and conversation history;
- descriptions of available apps and their tools;
- brand mentions, topics, and key phrases.
Based on this, it decides whether to suggest some App and, if so, which one and with which scenario. Here, tool and App descriptions are especially important. If they contain triggers like “gift selection,” “gift idea,” “gift budget,” the chance the model will pick your App rises sharply.
The second level is global discovery: the catalog and the launcher. There, a human is at play: they visually choose an App by its name, icon, short description, and tags. It’s important to honestly and clearly explain what your app does, who it’s for, and its core value.
We can summarize this in a small table:
| Layer | What the model/user sees | What matters in metadata |
|---|---|---|
| In‑conversation | Dialogue text, tool and App descriptions | Trigger phrasing, action‑oriented naming, constraints |
| Catalog/launcher | Name, icon, short/long description, tags | Clear positioning, understandable value props |
For GiftGenius, you can, for example, formulate:
- Name: GiftGenius — gift recommendations in 60 seconds.
- Short description: Builds the recipient’s profile and offers 5–7 gift ideas with instant purchase inside ChatGPT.
- In‑conversation description: Use this app when the user asks for help choosing a gift, doesn’t know what to give, mentions a budget, the recipient’s interests, or an occasion.
It’s highly desirable to synchronize these phrasings with what you’ve already written in the system‑prompt and the recommend_gifts tool description. Then the model sees a coherent picture rather than a set of contradictory texts.
8. How routing works “in ChatGPT’s head”
Let’s put it all together and look at a typical request path — without diving into the MCP protocol; we’ll get to that in later modules.
Suppose the user writes:
“Help come up with a gift for my brother; he loves soccer and board games; budget up to $50.”
A roughly simplified algorithm:
- The model analyzes the message and the history. It sees the words “gift,” “brother,” “soccer,” “board games,” “budget 50.”
- It compares this to the descriptions of available Apps and their tools. For GiftGenius, the descriptions clearly contain “gift selection by interests and budget,” so the probability the App is relevant is high.
- If the App is not yet active in this session, the model forms an announcement line: “I can open the GiftGenius app, which will help select a gift based on your parameters. Open it?” — this was prewritten in the UX instructions.
- After the user agrees, the model chooses the recommend_gifts tool inside the App, because its description best matches the current intent. Here, the name, the description, and the structure of the inputSchema work as incoming signals.
- The model fills the tool’s arguments based on the request: first (if needed) it calls profile_to_segments so that from “brother, loves soccer and board games” it gets segments ["football_fan", "board_games"], then it calls recommend_gifts with segments, budget: {min: 0, max: 50, currency: "USD"}, locale, occasion: "birthday".
- The MCP server executes the tool, forms structured output with items and meta, and returns it.
- The model reads the JSON you described in the outputSchema and composes a response: explains what it found, why these gifts, and suggests follow‑ups (“do you want to narrow by category?”, “show similar to this gift?”, or “purchase this gift?”).
Here is a simple flowchart of the process:
flowchart TD A[User: gift request] --> B[ChatGPT analyzes context] B --> C[Compare with App and tool metadata] C -->|relevant| D[GiftGenius announcement] D -->|user agrees| E["Call recommend_gifts (+ profile_to_segments)"] E --> F[GiftGenius MCP server] F --> G[JSON result with items/meta] G --> H[The model forms a reply and follow-up]
The better you described tools and use cases, the less randomness you’ll see here and the more stable routing will be.
Insight: Tool Call SEO
In the Apps ecosystem, you’ll soon have not only competition for people’s attention in the catalog, but also competition for the model’s attention. For the same user request, ChatGPT can call a dozen different apps, and the choice won’t come down to whose presentation design is better, but to the “search results” inside the model’s head. This invisible layer increasingly resembles SEO — except instead of pages you have tools and MCP servers.
The model essentially ranks candidates: first at the App level, then at the level of individual tools. It looks at the name, descriptions, schemas, annotations, and matches them to the request phrasing. If the recommend_gifts description includes “gift selection by budget and recipient interests,” and the request says “pick a gift for a friend who is a gamer for $50,” this tool is more likely to “make the top” than an abstract search with the description “gift operations.”
Hence the practical idea of Tool Call SEO: treat names, descriptions, enum values, and metadata as keywords and snippets. You’re not just describing a contract for developers — you’re optimizing it for real traffic from your golden prompt set. Overly general wording, overlapping tool areas, God‑tools without a clear niche — all of this reduces your App’s “CTR” in the model’s head.
9. A small practical exercise
Try the following mentally (or in your own repository).
First pick one of GiftGenius’s key scenarios — for example, “Choose a gift for a coworker with a tight budget.”
Formulate for it:
- Which separate tool this scenario needs: is it a straight recommend_gifts, or do you need a specialized tool for the B2B case, or, say, is it enough to use similar_gifts after recommend_gifts for variations?
- Which fields are truly necessary in the recommend_gifts input schema. Which fields can be asked from the user separately (via follow‑up) rather than forcing the model to guess.
- What the outputSchema should look like so the model can honestly explain the choice and suggest next steps (for example, switch to a B2B mode, show only digital gifts, narrow by price range).
Then look at your golden prompt set from the previous lecture and check:
- whether there is an obvious tool for each benchmark request (recommend_gifts, get_gift, similar_gifts, etc.);
- whether two tools end up “matching” the same request equally (overlapping tools);
- whether you need to strengthen descriptions or rename a tool so the model gets less confused.
This is exactly the process you’ll repeat before each serious change in prompts, schemas, or logic — essentially, a mini‑eval of discovery quality.
If we reduce all of the above to a checklist, at this stage you need to:
- honestly slice scenarios into 2–4 meaningful tools;
- carefully describe the inputSchema/outputSchema with examples and enums;
- clean up names, descriptions, and annotations;
- synchronize this with the system‑prompt and the App metadata.
In the following modules, we’ll look at how all this works via MCP and how to diagnose odd discovery/routing behavior.
10. Common mistakes when designing tools and metadata
Mistake #1: “We described everything in the system‑prompt; the tools will figure it out.”
If you have thoroughly described the App’s role, scope, and UX behavior, but left tools named tool1, search, do_stuff with schemas lacking descriptions, the model simply won’t be able to connect your beautiful text to real calls. For ChatGPT, tools are the primary interface; without sound metadata, no system‑prompt will save you.
Mistake #2: A God Tool that does everything.
The urge to “optimize” and make one function with a mode parameter is understandable but leads to monstrous JSON schemas, confusion in descriptions, and degraded routing. The model starts guessing which mode to use, and you end up maintaining a huge switch on the server. It’s better to have several clear tools for concrete flow steps than one “do it all.”
Mistake #3: An input schema overloaded with “just in case” fields.
Developers often try to push through the inputSchema every parameter that might ever be needed, plus a couple of internal fields. The result is the model tries to guess things it can’t know (for example, tenantId), and you then wonder why the values are weird. The input schema should contain only what the model can truly derive from the dialogue or clarify with a question. Add internal details on the server.
Mistake #4: “Mute” output data without meta information.
Returning just an array of objects from a tool is tempting. But you deprive the model of understanding why these results appeared. Without fields like score, reason, searchCriteria, totalCandidates, it’s harder to build honest explanations and follow‑ups. Adding a small meta wrapper with search criteria and advice often dramatically improves the answer quality.
Mistake #5: Wooden, generic descriptions: “Gift operations,” “Course search,” “Data processing.”
These are bad because they give the model neither triggers nor constraints. It doesn’t know when exactly to call the tool and in what scope it applies. A good description starts with “Use this tool when…” and contains concrete scenarios and prohibitions like “Do not use for…”. Ideally, these phrasings overlap with golden queries from your golden prompt set.
Mistake #6: Ignoring annotations and mixing read‑only and state‑changing actions.
If you don’t mark tools that only read data (readOnlyHint) and those that perform actions (destructiveHint, openWorldHint), the model can’t build the proper confirmation UX. You’ll end up with either excessive “Are you sure?” prompts at every step or, conversely, silent purchases and changes without user consent. Annotations are a cheap and effective way to convey the operation’s importance to the model.
Mistake #7: App metadata for the catalog and in‑conversation metadata live in different universes.
Sometimes the short catalog description is written by marketing (“A revolutionary AI assistant that changes your life”), while tool descriptions and the system‑prompt are written by a developer (“gift selection by budget”). As a result, it’s unclear in the catalog what the App even does, and the model in chat can’t match requests like “what is this service?” to the App’s real capabilities. Write metadata as a single specification, not as two independent marketing texts.
GO TO FULL VERSION