How AI Actually Calls an API? Tool Calling Explained from Scratch A developer demonstrated how tool calling works in practice, showing that a foundation model does not execute code itself but instead emits a structured request that application code reads and runs. Using Amazon Bedrock's Converse API with a Claude model, the engineer built a get_weather tool backed by the Open-Meteo API, then extended the example to two tools and a fact-injection technique. "The model is the decision-maker. Your code is the hands," the developer wrote, with sample code published in a GitHub repository. In the previous post https://dev.to/aws/why-rag-gives-wrong-answers-and-how-to-fix-retrieval-failures-1234 , we taught a model to read our documents. It could search a pile of files and answer from them, which was very useful. But I still couldn't ask it if it was going to rain, check a live price or even what today's date is. Because as we discussed this earlier, a foundation model on its own is frozen in time. Its knowledge stops at its training cutoff and it's locked in a box. No window to the outside world. This post is about that window, tool calling. We give the model one tool and watch it reach out for live data, add a second tool, then get into the two very different ways an app can hand a model a fact it doesn't have. One of those two is the reason one of the AI assistant you've used can tell you today's date. All the code is in my GitHub repo https://github.com/gaonkarr/learning-ai-out-loud-samples-for-aws , in the ep07-tool-calling folder. Three tiny scripts, one idea each: one tool, two tools, and the injection trick. When I first heard "the model calls a tool," I pictured the model reaching out and running code by itself. That is not what happens. The model does not run anything because it really can't. It is still just reading a prompt and producing text. What it produces is a structured request that says "I'd like to call this tool, with these inputs." It just hands you a note, that your code reads and then runs the actual tool. It then hands the result back to the model for further actions, either to tell you the answer or call another tool. The model is the decision-maker. Your code is the hands. This is run every single time: call get weather, city is Toronto . I'm using Amazon Bedrock https://aws.amazon.com/bedrock?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&sc channel=el again, same as the whole series, calling a Claude Model through the Converse API. Converse has a spot built in for tools, called toolConfig . response = bedrock.converse modelId=MODEL, messages=messages, toolConfig={"tools": WEATHER TOOL }, inferenceConfig={"maxTokens": 2048}, additionalModelRequestFields=THINKING, Let's take the simplest tool to start: get the weather. Describing a tool to the model is three parts: a name, a plain-English description, and an input schema for the arguments. WEATHER TOOL = { "toolSpec": { "name": "get weather", "description": "Get the current weather for a single city.", "inputSchema": { "json": { "type": "object", "properties": { "city": { "type": "string", "description": "A plain city name, e.g. Toronto or Paris.", } }, "required": "city" , } }, } } This description and schema are the only things the model reads to decide when and how to use this tool. Your tool description is a prompt, so treat it like one. And separately, the real function that does the work: python import requests Open-Meteo returns a numeric weather code; map the ones we need to plain words. WEATHER CODES = {0: "clear sky", 2: "partly cloudy", 3: "overcast", 61: "light rain", 63: "moderate rain"} def get weather city: str - dict: geo = requests.get "https://geocoding-api.open-meteo.com/v1/search", params={"name": city, "count": 1}, .json "results" 0 now = requests.get "https://api.open-meteo.com/v1/forecast", params={ "latitude": geo "latitude" , "longitude": geo "longitude" , "current": "temperature 2m,weather code,wind speed 10m", }, .json "current" return { "city": geo "name" , "country": geo "country" , "temperature c": now "temperature 2m" , "conditions": WEATHER CODES.get now "weather code" , "unknown" , "wind kph": now "wind speed 10m" , } This is normal code. No AI in it. It hits Open-Meteo https://open-meteo.com/ , a free weather API with no key required. Question: "Do I need an umbrella in Toronto today?" I send that to the model along with the get weather definition. The model stops with a stopReason of tool use and hands back a request: { "toolUse": { "toolUseId": "tooluse abc123", "name": "get weather", "input": { "city": "Toronto" } } } I never told it which tool to use, and I never told it the argument. It read one question and worked out both. But nothing has run yet. So my code runs get weather "Toronto" , hits the API, and gets back the real conditions. Then I package that up and send it back to the model as a toolResult : messages.append { "role": "user", "content": { "toolResult": { "toolUseId": "tooluse abc123", "content": {"json": { "city": "Toronto", "country": "Canada", "temperature c": 23.8, "conditions": "overcast", "wind kph": 3.9, }} , } } , } With a single tool, the whole thing is a straight line. Send, get the request, run it, send the result back, get the answer. Top to bottom, no loop: messages = {"role": "user", "content": {"text": QUESTION} } 1. Send the question + the tool. response = bedrock.converse modelId=MODEL, messages=messages, toolConfig={"tools": WEATHER TOOL }, messages.append response "output" "message" 2. The model asks for the tool. 3. Run it. 4. Send the result back. tool request = next b "toolUse" for b in response "output" "message" "content" if "toolUse" in b result = get weather tool request "input" "city" messages.append { "role": "user", "content": { "toolResult": { "toolUseId": tool request "toolUseId" , "content": {"json": result} , } } , } The model writes the final answer, grounded in the real data. final = bedrock.converse modelId=MODEL, messages=messages, toolConfig={"tools": WEATHER TOOL } One tool, one round trip. I know exactly what's going to happen, so I can just write it out. With real data in hand, the model writes the answer: "Based on the current weather in Toronto, you probably don't need an umbrella right now ." That answer did not exist anywhere in the model. It went from frozen to current in one tool call. Now something that feels like it should be trivial. Question: "What's today's date?" No tool call comes back. The model just says, plainly, that it doesn't have access to the current date. The only tool it has access to is weather, so nothing here can reach a date. It can't answer, and this is the part I love, it doesn't pretend to. It just tells me it doesn't know, which is a real shift from the hallucinations post https://dev.to/aws/why-does-ai-sometimes-lie-hallucinations-explained-abcd . If the problem is "there's no tool for the date," the fix is obvious, lets give it one. DATETIME TOOL = { "toolSpec": { "name": "get current datetime", "description": "Get the current date and time.", "inputSchema": {"json": {"type": "object", "properties": {}}}, } } def get current datetime - dict: from datetime import datetime now = datetime.now return { "date": now.strftime "%Y-%m-%d" , "day of week": now.strftime "%A" , "time": now.strftime "%H:%M" , } No arguments, no AI, it just returns today's date and time. I add it to the list of tools the model is allowed to use. Now the model has two tools - weather and date. Question: "Do I need an umbrella in Toronto? And what is today's date?" Two requests come back, for two tools. get weather with {"city": "Toronto"} , then get current datetime with {} . My code runs each one, hands both results back, and the model writes one answer using both. One sentence, two different needs, right tool for each. It just routed it. But notice the problem with my nice straight line from before. With one tool, I knew there'd be exactly one round trip. With two, I don't know which the model will pick, or how many, or whether it'll come back for more after seeing the first result. So the four steps go inside a loop. Keep going while the model keeps asking for tools, and stop when it writes the answer instead: name → the real function to run when the model asks for it. TOOLS = { "get weather": get weather, "get current datetime": get current datetime, } messages = {"role": "user", "content": {"text": QUESTION} } while True: response = bedrock.converse modelId=MODEL, messages=messages, toolConfig={"tools": WEATHER TOOL, DATETIME TOOL }, assistant message = response "output" "message" messages.append assistant message Done? The model stopped asking for tools and wrote its answer. if response "stopReason" = "tool use": answer = "".join b "text" for b in assistant message "content" if "text" in b break Otherwise: run every tool the model requested, send the results back. tool results = for block in assistant message "content" : if "toolUse" not in block: continue request = block "toolUse" result = TOOLS request "name" request "input" tool results.append { "toolResult": { "toolUseId": request "toolUseId" , "content": {"json": result} , } } messages.append {"role": "user", "content": tool results} That while loop is the whole difference. One tool was a straight line I could hardcode. More than one, and I hand the control to the model and let it drive until it's done. This is so so so important to understand, because this is a seed of an agent This is the part that bugged me while I was learning. If a raw model doesn't know today's date, how does ChatGPT or Claude or any AI assistant know it? You ask what day it is and they answer instantly. Are they calling a date tool every time? Short answer, no. Anthropic actually publishes the system prompt they use for Claude, in their release notes https://docs.anthropic.com/en/release-notes/system-prompts . They say Claude's web interface and mobile apps use a system prompt to provide up-to-date information, such as the current date, at the start of every conversation . That's it. No tool runs. It's just text thats slipped into the instructions before your message ever gets there. The model was handed the date as context. You can do the exact same thing in a script. Take away the date tool and paste today's date into the system prompt as plain text: system prompt = { "text": f"Today's date is {datetime.now :%A, %d %B %Y}." } Ask "what's today's date?" and it answers, correctly, with no tool call at all. Because you handed it the date. So there are two ways to give a model a fact it doesn't have. A tool it calls and you run, or context you inject straight into the prompt. When do you use which? And remember that schema, city and nothing else? That's why I can't ask this thing about next week. There's no date to pass in. If I wanted a forecast, that's a different tool. So we've got two tools working. Weather and date. Great. But real systems don't have just two tools. They have dozens - check the calendar, search the CRM, query the database, send the email and/or read the file. And with what we just built, every one of those is something I hand-wire myself - write the schema, write the function, register it, keep the description in sync when the tool changes. For two tools, that's fine. Fifty tools, across five apps, all changing over time? That's a maintenance nightmare. And everyone building AI apps was writing the same glue code, over and over, for the same tools. This is the problem MCP solves. MCP stands for Model Context Protocol . It's an open standard, started by Anthropic and now used across the industry, for how AI apps and tools talk to each other. The clean way to think about it: MCP is like USB-C for AI tools. Before USB-C, every device had its own cable and connector. It was a chaos of cables. USB-C is one standard plug. MCP is that, but for connecting models to tools and data. The tool lives behind an MCP server, and that server describes itself: here are the tools I offer, here's what each does, here are the inputs I need. Your app is the MCP client. It just asks "what have you got?" and the server tells it. The tools get discovered at runtime. So if someone builds an MCP server for GitHub, or your database, or Slack, you don't write the integration. You point your app at the server and the tools show up. We're not building one today, that's a whole topic on its own. The mental model is enough for now: tool calling is how one model uses a tool, and MCP is how any model discovers and uses tools. If you're just getting started: Tool calling is how AI stops being a closed box. Give it tools and it can pull live information and take action instead of just talking. The one thing to hold onto: the model is the brain, your code is the hands. If you're more on the builder side: The model picks the tool and fills in the arguments, and the only thing it reads to make that call is your description and schema. So write them like prompts, and be specific about what the tool does and doesn't do. Then: static facts get injected, live facts get a tool. And once you're past a couple of tools, stop hardcoding and look at MCP. Today the model called one tool, or two, once each, then answered. But what happens when a question needs several tools, in the right order? Check my calendar, then check the weather for that day, then draft the email. The model has to plan, act, look at the result, and decide the next step. Over and over in a loop, until it's done. Well, that loop is actually called an agent. And next post, we build one with Strands Agents SDK https://strandsagents.com/?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&sc channel=el . Ride along. This post is part of the "Learning AI Out Loud" series, a cloud architect learning AI from first principles.