How to structure the calls from the Agent to the tools they need
RETOUR AU BLOG

11 avril 2026
7 min read

How to Make Your Agent Know Which Tools It Has

A practical guide to building agents that are reliably aware of their own capabilities.

Why This Matters

One of the most common mistakes when building agents is writing a static system prompt that lists tools by hand. This breaks the moment you add, remove, or conditionally enable a tool — the agent still thinks it has them, hallucinates calls, or fails to use new ones.

The right approach is to generate the system prompt dynamically, derived directly from the real pool of tools that exist at runtime.


Core Principles

1. Compute the Tool List at Runtime

Never hardcode tool names in your system prompt. Instead, derive the set of enabled tools from the actual tool objects passed to your agent at startup.

// BAD ❌ — static, fragile
const systemPrompt = "You have tools: Bash, ReadFile, WriteFile."

// GOOD ✅ — dynamic, always accurate
const enabledTools = new Set(tools.map(tool => tool.name))

Pass enabledTools to every function that generates tool-related prompt text. This single source of truth ensures the agent never sees stale or incorrect tool lists.


2. Export a Name Constant From Every Tool

Each tool should own its name as an exported constant. This makes it trivially safe to rename a tool — you change it in one place and all prompt text updates automatically.

// tools/BashTool/constants.ts
export const BASH_TOOL_NAME = "Bash"

// tools/FileReadTool/constants.ts
export const FILE_READ_TOOL_NAME = "ReadFile"

// tools/FileWriteTool/constants.ts
export const FILE_WRITE_TOOL_NAME = "WriteFile"

Then in your prompt builder, import and interpolate:

import { BASH_TOOL_NAME } from "./tools/BashTool/constants"
import { FILE_READ_TOOL_NAME } from "./tools/FileReadTool/constants"

const guidance = To read files, use ${FILE_READ_TOOL_NAME} instead of cat or head.

3. Gate Every Prompt Section on Tool Availability

Before mentioning a tool in the system prompt, check that it is actually enabled. This prevents the agent from being told about tools it cannot use.

function getSessionGuidance(enabledTools: Set<string>): string {
  const hasFileRead  = enabledTools.has(FILE_READ_TOOL_NAME)
  const hasFileWrite = enabledTools.has(FILE_WRITE_TOOL_NAME)
  const hasBash      = enabledTools.has(BASH_TOOL_NAME)

  const items: string[] = []

  if (hasFileRead)  items.push(Read files with ${FILE_READ_TOOL_NAME} — do not use cat or head.)
  if (hasFileWrite) items.push(Write files with ${FILE_WRITE_TOOL_NAME} — do not use echo redirection.)
  if (hasBash)      items.push(Run shell commands with ${BASH_TOOL_NAME} only when no dedicated tool applies.)

  return items.join("\n")
}

4. Teach the Agent When to Use Each Tool

Knowing a tool exists is not enough. The agent also needs to understand when to prefer it over alternatives (like a raw Bash command). Be explicit in the system prompt.

function getToolPreferences(enabledTools: Set<string>): string {
  const lines: string[] = [
    "Prefer dedicated tools over Bash whenever possible:",
  ]

  if (enabledTools.has(FILE_READ_TOOL_NAME))
    lines.push( - To read files: use ${FILE_READ_TOOL_NAME}, NOT cat/head/tail)

  if (enabledTools.has(FILE_WRITE_TOOL_NAME))
    lines.push( - To write files: use ${FILE_WRITE_TOOL_NAME}, NOT echo or heredoc)

  if (enabledTools.has(BASH_TOOL_NAME))
    lines.push( - Reserve ${BASH_TOOL_NAME} for system commands and shell operations only)

  return lines.join("\n")
}

5. Handle Dynamic Tools (e.g. MCP Servers) Separately

Tools that are added at runtime (such as MCP servers) often come with their own instructions. Inject these as a dedicated section in the system prompt, built from the live server list.

function getMcpInstructions(mcpClients: MCPServerConnection[]): string | null {
  const connected = mcpClients.filter(c => c.type === "connected" && c.instructions)

  if (connected.length === 0) return null

  const blocks = connected
    .map(client => ## ${client.name}\n${client.instructions})
    .join("\n\n")

  return # MCP Server Instructions\n\n${blocks}
}

Putting It All Together — System Prompt Assembly

Compose the system prompt by calling all your section builders with the live enabledTools set:

async function getSystemPrompt(tools: Tool[]): Promise<string> {
  const enabledTools = new Set(tools.map(t => t.name))

  return [
    "You are a helpful software engineering agent.",
    getToolPreferences(enabledTools),
    getSessionGuidance(enabledTools),
  ]
    .filter(Boolean)
    .join("\n\n")
}

Full Worked Example — Adding a Bash Tool

The following is a complete, minimal example showing how to define a Bash tool, register it with your agent, and have the agent correctly call it in response to a user prompt.

Step 1 — Define the Tool

// tools/BashTool/index.ts

export const BASH_TOOL_NAME = "Bash"

export const BashTool = {
  name: BASH_TOOL_NAME,
  description: "Executes a shell command and returns its stdout and stderr.",
  parameters: {
    type: "object",
    properties: {
      command: {
        type: "string",
        description: "The shell command to execute.",
      },
    },
    required: ["command"],
  },

  // The actual implementation called when the AI invokes this tool
  async execute({ command }: { command: string }): Promise<string> {
    const { execSync } = await import("child_process")
    try {
      return execSync(command, { encoding: "utf-8", timeout: 30_000 })
    } catch (err: any) {
      return Error: ${err.message}
    }
  },
}

Step 2 — Register and Build the Agent

// agent.ts

import Anthropic from "@anthropic-ai/sdk"
import { BashTool, BASH_TOOL_NAME } from "./tools/BashTool"

const tools = [BashTool]
const enabledTools = new Set(tools.map(t => t.name))

// Dynamically build system prompt from live tool list
function buildSystemPrompt(): string {
  const toolGuidance = enabledTools.has(BASH_TOOL_NAME)
    ? You have access to the ${BASH_TOOL_NAME} tool.\n +
      Use it to run shell commands when needed.\n +
      Always prefer safe, read-only commands unless the user explicitly asks for writes.
    : ""

  return [
    "You are a helpful agent that can run shell commands on the user's machine.",
    toolGuidance,
  ]
    .filter(Boolean)
    .join("\n\n")
}

// Convert our tool definitions to the format the API expects
const apiTools = tools.map(tool => ({
  name: tool.name,
  description: tool.description,
  input_schema: tool.parameters,
}))

const client = new Anthropic()

async function runAgent(userMessage: string) {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ]

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      system: buildSystemPrompt(),
      tools: apiTools,
      messages,
    })

    // If the model is done, print the final text and exit
    if (response.stop_reason === "end_turn") {
      const text = response.content
        .filter(b => b.type === "text")
        .map(b => b.text)
        .join("")
      console.log("Agent:", text)
      break
    }

    // If the model wants to call a tool, execute it and feed the result back
    if (response.stop_reason === "tool_use") {
      // Add the assistant's response (including the tool call) to history
      messages.push({ role: "assistant", content: response.content })

      // Process each tool call
      const toolResults: Anthropic.ToolResultBlockParam[] = []

      for (const block of response.content) {
        if (block.type !== "tool_use") continue

        const tool = tools.find(t => t.name === block.name)
        if (!tool) {
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: Error: unknown tool "${block.name}",
          })
          continue
        }

        console.log([Tool call] ${block.name}:, block.input)
        const result = await tool.execute(block.input as any)
        console.log([Tool result]:, result)

        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: result,
        })
      }

      // Feed tool results back into the conversation
      messages.push({ role: "user", content: toolResults })
    }
  }
}

Step 3 — Run It

// main.ts

import { runAgent } from "./agent"

// The user asks something that requires a shell command
await runAgent("What is the current date and time on my machine?")
What happens internally:

1. The user prompt is sent to Claude along with the dynamically built system prompt and the Bash tool definition.

2. Claude recognizes that answering requires running a command and responds with a tool_use block:

   {
     "type": "tool_use",
     "name": "Bash",
     "input": { "command": "date" }
   }
   

3. Your agent executes date on the machine and gets back e.g. Sat Apr 11 19:00:00 UTC 2026.

4. That result is sent back to Claude as a tool_result.

5. Claude produces a final text answer: "The current date and time on your machine is Saturday, April 11, 2026 at 19:00 UTC."


Summary

| Goal | Pattern |

|---|---|

| Know which tools exist | Compute enabledTools from the live tool pool at runtime |

| Safe tool name references | Export a name constant from each tool module |

| Only mention available tools | Gate every prompt section on enabledTools.has(...) |

| Teach usage priorities | Explicit preference rules in the system prompt |

| Handle runtime/MCP tools | Per-server instruction block injected into the prompt |

| Execute tool calls | Agentic loop: call → execute → feed result back → repeat |

The key insight is simple: your system prompt should be a function of your tools, not a hardcoded string. Build it fresh every session from whatever tools are actually present, and your agent will always know exactly what it can do.