LLM Tool Schema Design: A Checklist for Web APIs
A good LLM tool schema exposes only the parameters a model should choose, names them for what they do, constrains values with enums and limits, and returns a trimmed result. Copying a web API's parameter list one-to-one gives you a schema that works but wastes tokens and invites bad calls. This checklist applies eight rules to the Google Maps simple-search endpoint, with a before and after schema.
What does a model need from a tool schema?
A model reads your tool's name, description and parameter schema as plain text and decides from that text alone whether to call it and with what arguments. Both Anthropic's tool use docs and the MCP specification's tools page (2025) define a tool by a name, a description and a JSON Schema for the input. Those are the fields that carry your intent to the model, so ambiguity there becomes a wrong call at runtime.
The Google Maps API is a good test case because its search endpoint has parameters of different kinds: one the model must fill, some it shouldn't touch, and a result with far more fields than a reasoning step needs.
What does the one-to-one schema look like?
The one-to-one schema mirrors the endpoint's parameter list exactly. For GET /v1/google-maps/simple-search that means query (required) plus hl, gl, google_domain, limit, page and fields, with no descriptions. It's valid, and it's what you get from generating a tool straight from an OpenAPI file. Here's that version as a tool definition:
{
"name": "simple_search",
"description": "Search Google Maps.",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"hl": { "type": "string" },
"gl": { "type": "string" },
"google_domain": { "type": "string" },
"limit": { "type": "integer" },
"page": { "type": "integer" },
"fields": { "type": "string" }
},
"required": ["query"]
}
}
A model has to guess what hl means, whether google_domain is worth setting, and what to put in fields. It will sometimes set all three to plausible but wrong values.
The checklist
Work through these in order. Each item is a decision you make once, in the tool wrapper, rather than a rule you repeat in every prompt.
- Name the tool for the task, not the endpoint.
search_placestells a model when to reach for it;simple_searchdoesn't. If you expose several Google Maps endpoints, name them as verbs on nouns:search_places,get_business_details,get_reviews. - Write the description as a decision rule. Say what the tool returns, when to use it and what to call next. Mention that it returns a
business_idthat other tools take as input. Add the cost in credits so a cost-aware agent can plan; the endpoint costs 1 credit per successful call. - Rename parameters that only make sense to the API. Expose
glascountryand drophlfrom the schema entirely. The reference types both only as strings, so whatcountryshould hold is your decision (see item 5). Your wrapper mapscountryback toglwhen it builds the request. - Remove parameters the model shouldn't choose.
google_domain,hlandfieldsare configuration, not decisions. Set them in code. Fewer parameters means fewer ways to produce an invalid call. - Turn free strings into enums. If your agent only serves three markets,
countryis an enum of those three values. The model can't invent a fourth. Test each value in the playground on the Google Maps page before you commit to it; the reference only typesglas a string and doesn't list allowed values. - Bound numbers and say what they do. Give
limitaminimum, amaximum, adefaultand a description. Pick the numbers yourself: the endpoint reference documentslimitonly as an integer, so any cap or default is your policy, not the API's. Put the default in the schema and use the same value in the wrapper, so the model sees what happens when it omitslimit. - Add one example per free-text field.
"ramen in Berlin"in thequerydescription steers the model toward place-plus-location phrasing better than a paragraph of instructions. - Trim the output before the model sees it. Return the fields the next step needs, plus a
has_moreflag, and nothing else.
Which fields does the model actually need?
A search result is only useful to an agent if it can pick a place and hand its ID to the next tool. The recorded simple-search response for query=restaurants in new york returns items with 28 top-level fields, including name, rating, review_count, business_id, subtypes and full_address. It also carries owner IDs and links, a photos_sample array of photo objects, an attributes object and several near-duplicate address fields.
{
"name": "Altair Restaurant NYC",
"review_count": 1225,
"rating": 4.8,
"website": "https://www.altairnyc.com/",
"business_id": "0x89c2591598891521:0x4ebcde18610e64",
"subtypes": ["New American restaurant", "Restaurant"],
"full_address": "351 W 38th St, New York, NY 10018, United States",
…
}
Seven fields cover search, ranking and follow-up: name, rating, review_count, business_id, full_address, subtypes and website. The rest is real data, but it belongs to a different question.
To put a number on it, we compared the first recorded item with a copy holding only those seven fields. Method: compact JSON (no whitespace) of each item, counting characters. The full item is 1,573 characters and the trimmed one 274, an 83% cut; across both recorded items it's 3,114 against 531. The recording already shortens two long URLs with …, so real full items are somewhat larger. Characters aren't tokens, so treat this as a size ratio, not a token bill. A ten-place result carries that overhead ten times. If a later step needs phone numbers, call Business Details for that one place instead of carrying every result's phone_numbers through the whole conversation.
What does the after schema look like?
The after schema applies all eight items: a task-named tool, a decision-rule description, three model-facing parameters plus an optional page, an enum and a bounded integer with a default. The country values below are an example; choose the markets you serve.
{
"name": "search_places",
"description": "Find places on Google Maps from a text query. Returns up to `limit` places with name, rating, review_count, full_address, subtypes, website and business_id, plus has_more. Pass business_id to other Google Maps tools. Costs 1 credit per successful call.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "What and where, for example 'ramen in Berlin'."
},
"country": {
"type": "string",
"enum": ["US", "GB", "DE"],
"description": "Market to search in."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 5,
"description": "Number of places to return."
},
"page": {
"type": "integer",
"description": "Only set this when the previous result had has_more true."
}
},
"required": ["query"]
}
}
The wrapper that runs when the model calls it does the other half of the work. Its limit=5 matches the schema default. It maps country to gl, sends the request and projects each item down to the seven fields.
import os
import requests
KEEP = ["name", "rating", "review_count", "business_id",
"full_address", "subtypes", "website"]
def search_places(query, country=None, limit=5, page=None):
params = {"query": query, "limit": limit}
if country:
params["gl"] = country
if page is not None:
params["page"] = page
r = requests.get(
"https://api.perceptlayer.com/v1/google-maps/simple-search",
params=params,
headers={"X-API-Key": os.environ["API_KEY"]},
timeout=30,
)
body = r.json()
if not r.ok:
# Hand the model the code and message so it can correct the call.
return {"error": body["error"]["code"], "message": body["error"]["message"]}
return {
"places": [{k: p.get(k) for k in KEEP} for p in body["items"][:limit]],
"has_more": body["pagination"]["has_more"],
}
An invalid_parameters (422) error, for instance, reaches the model as a short code and message, not a stack trace. See the error reference for the full list of codes.
Does a tighter schema change what you pay?
A tighter schema doesn't change the credit cost of a call. Every successful simple-search call costs 1 credit whatever you do with the response, and failed calls cost nothing (credits). What it changes is tokens and retries: fewer fields in the result means a smaller tool message in the model's context, and fewer free-form parameters means fewer malformed calls.
Retries are where the schema affects spend most. An agent that makes 20 searches per task spends 20 credits at 1 credit each, and a loop that repeats a search because the model misread page adds credits without adding answers. Our post on capping agent data spend covers hard limits, and MCP rate limiting covers request rates.
If you'd rather skip writing wrappers for each endpoint, the MCP server exposes search_endpoints, get_endpoint and call_endpoint, so a model discovers parameters from the catalogue at run time. The trade-off is that your own curated schema, as above, constrains the model more tightly. MCP vs function calling compares the two approaches.
Next step: copy the wrapper above, then run the same query in the playground on the Google Maps API page to check each enum value before you ship it. You can get a free API key to do it.
APIs in this post
Frequently asked questions
How many parameters should a tool schema have?
As few as the model needs to decide. For simple-search, the endpoint has 7 parameters, but a model only needs query, a market, a limit and a page. Hold the rest constant in code. Each parameter you expose is another value the model can set wrongly, and another line in every request's context.
Should I generate tool schemas from the OpenAPI file?
Use it as a starting draft, not the final schema. A generated schema mirrors the API, which is built for programs that read documentation, not models that read descriptions. Add task-focused names, descriptions with examples and enums, and drop configuration-only parameters. The generated file is still useful for checking types and required fields.
Where should output trimming happen?
In your wrapper, before the result goes back to the model. Trimming in code is guaranteed, whereas asking the API for a subset depends on the format of the fields parameter, which the reference documents only as a string. Test fields in the playground if you want to trim at the source as well.
Does the model need to know the cost of a call?
It helps when the agent plans multi-step work. Stating "costs 1 credit per successful call" in the description lets a cost-aware agent budget searches. Failed calls cost nothing and return X-Credits-Used: 0, so the model doesn't need to worry about retrying after an error. See credits for how billing works.