DEV Community

Cover image for Customizing AI Agent Behavior with MCP: Callbacks, Hooks, Tools, Plugins, and Steering Explained
Anuj Tyagi
Anuj Tyagi

Posted on

Customizing AI Agent Behavior with MCP: Callbacks, Hooks, Tools, Plugins, and Steering Explained

Connecting an AI agent to an MCP server is relatively easy.

Making that agent behave reliably is the harder part.

A production agent must decide:

  • Which tools should be available?
  • When should a tool be called?
  • Which operations require approval?
  • What context should be sent to the model?
  • How should failures be handled?
  • What should be logged?
  • Can the workflow be redirected while it is running?
  • How do we prevent the agent from taking actions outside the user’s permissions?

This is where concepts such as callbacks, hooks, tools, plugins, and steering become important.

These terms are sometimes used interchangeably, but they solve different problems.

The most important idea to understand is this:

MCP connects an agent to capabilities. The agent runtime controls how those capabilities are used.

MCP is an open standard for connecting AI applications with external data, tools, and workflows. MCP servers can expose capabilities to many compatible hosts instead of requiring a custom integration for every model and application.

However, MCP by itself is not the complete agent.

The complete system normally looks like this:

Let us break down each layer.


MCP Is the Capability Layer, Not the Entire Agent

The Model Context Protocol defines how an AI application communicates with external systems.

Its core server-side capabilities include:

  • Resources
  • Prompts
  • Tools

MCP clients may also support features such as:

  • Sampling
  • Elicitation
  • Roots

The protocol uses JSON-RPC messages and capability negotiation so clients and servers can determine which features each side supports.

A useful mental model is:

MCP does not standardize every internal behavior of your agent.

For example, MCP does not require your application to use a particular:

  • Memory implementation
  • Planning algorithm
  • Retry strategy
  • Callback system
  • Agent framework
  • Observability platform
  • Policy engine
  • Human-approval interface

Those belong to the host application or agent framework.


A Quick Comparison

Concept Main purpose Usually belongs to Can change execution?
Tool Perform an operation MCP server or agent runtime Yes
Resource Provide contextual data MCP server Indirectly
Prompt Provide reusable instructions MCP server or agent runtime Yes
Callback Observe an event Agent framework Usually no
Hook Intercept a lifecycle stage Agent framework Yes
Middleware Wrap one or more execution stages Agent framework Yes
Plugin Package related capabilities Framework or application Yes
Steering Direct runtime behavior Entire agent system Yes
Guardrail Validate or block unsafe behavior Agent runtime or policy layer Yes

The exact names vary between frameworks.

One framework may call something a callback, while another calls the same mechanism a hook or middleware function. Focus less on the label and more on what the mechanism is allowed to do.


1. Tools: What the Agent Can Do

A tool is an operation the model can request.

Examples include:

  • Searching a database
  • Reading a file
  • Creating a support ticket
  • Sending a message
  • Updating a CRM record
  • Running a calculation
  • Triggering a deployment
  • Requesting another specialized agent

In MCP, tools are exposed by servers and discovered by clients. Each tool has a name, description, input schema, and optionally an output schema. MCP tools are intended to be model-controlled, although the application may require user approval before execution.

For example:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("billing-service")


@mcp.tool()
def lookup_order(order_id: str) -> dict:
    """Retrieve an order and its current payment status."""
    return {
        "order_id": order_id,
        "status": "delivered",
        "amount": 79.99,
        "refundable": True,
    }


@mcp.tool()
def issue_refund(order_id: str, amount: float) -> dict:
    """Submit a refund for an eligible order."""
    return {
        "order_id": order_id,
        "amount": amount,
        "refund_status": "submitted",
    }


def main() -> None:
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

The official Python MCP SDK uses Python type hints and docstrings to generate tool definitions and schemas automatically. MCP servers can run through local stdio connections or network transports such as Streamable HTTP.

A tool should be more than a Python function

An agent depends heavily on tool metadata.

Consider this description:

@mcp.tool()
def update(order_id: str, value: float):
    """Update an order."""
Enter fullscreen mode Exit fullscreen mode

The model cannot easily determine:

  • What is being updated?
  • Is this operation reversible?
  • Does value represent a price, refund, discount, or quantity?
  • Does the tool have side effects?
  • Does the user need to approve it?

A stronger definition would be:

@mcp.tool()
def issue_refund(order_id: str, amount: float) -> dict:
    """
    Submit a monetary refund for an eligible order.

    Use this only after:
    1. The order has been retrieved.
    2. Refund eligibility has been verified.
    3. The user has confirmed the refund amount.

    Args:
        order_id: Unique identifier of the order.
        amount: Refund amount in US dollars.
    """
Enter fullscreen mode Exit fullscreen mode

Tool descriptions are part of agent behavior design.

Poorly described tools create poorly behaved agents.


2. Resources: What the Agent Can Know

Resources provide contextual information.

Examples include:

  • Policy documents
  • Database schemas
  • Customer profiles
  • Configuration files
  • Product catalogs
  • Repository files
  • Application state
  • API documentation

MCP resources are identified using URIs and are designed to provide context that the host application can select, retrieve, or insert into the model’s context. The MCP specification describes resources as application-driven: the host decides how and when they are included.

Here is a simple resource:

@mcp.resource("policy://refunds")
def refund_policy() -> str:
    """Return the current refund policy."""
    return """
    Orders may be refunded within 30 days of delivery.

    Refunds below $100 can be processed automatically after customer
    confirmation. Refunds of $100 or more require supervisor approval.
    """
Enter fullscreen mode Exit fullscreen mode

The agent can now retrieve authoritative policy information rather than relying entirely on model memory.

This creates an important separation:

Resource = information about the world
Tool     = operation that changes the world
Enter fullscreen mode Exit fullscreen mode

Reading a refund policy is a resource operation.

Submitting a refund is a tool operation.

Treating both as tools may work technically, but separating contextual data from actions often produces a clearer and safer architecture.


3. Prompts: Reusable Behavior Templates

MCP prompts allow servers to expose reusable prompt templates.

Prompts are generally user-controlled: a user or application explicitly selects a prompt and supplies its arguments. Clients can list available prompts and retrieve a completed prompt from the server.

For example:

@mcp.prompt()
def investigate_refund(order_id: str) -> str:
    """Create instructions for investigating a refund request."""
    return f"""
    Investigate refund eligibility for order {order_id}.

    Follow this process:
    1. Retrieve the order.
    2. Read the refund policy.
    3. Determine the maximum refundable amount.
    4. Explain the result to the user.
    5. Do not issue a refund until the user confirms.
    """
Enter fullscreen mode Exit fullscreen mode

Prompts are useful when a domain expert owns a workflow.

Instead of embedding every domain instruction inside the agent application, an MCP server can expose prompts such as:

/investigate-refund
/review-pull-request
/analyze-incident
/prepare-customer-summary
/check-policy-compliance
Enter fullscreen mode Exit fullscreen mode

This lets the capability provider package not only data and actions, but also recommended ways of using them.

However, prompts are not hard security controls.

A prompt can tell an agent not to issue an unauthorized refund. A policy hook must actually prevent the unauthorized tool call.

That difference matters:

Prompt instruction:
"Do not issue refunds over $100."

Policy enforcement:
Reject issue_refund when amount >= 100 unless approval exists.
Enter fullscreen mode Exit fullscreen mode

Use prompts to guide behavior.

Use executable controls to enforce behavior.


4. Callbacks: Observing What Happened

A callback is a function invoked when an event occurs.

Typical callback events include:

  • Agent started
  • Model request started
  • Model response received
  • Tool selected
  • Tool completed
  • Agent handed work to another agent
  • Run completed
  • Run failed

Callbacks are commonly used for:

  • Logging
  • Metrics
  • Tracing
  • Cost tracking
  • Notifications
  • Audit records
  • Debugging
  • Evaluation data collection

For example:

async def on_tool_completed(tool_name: str, result: object) -> None:
    audit_log.write({
        "event": "tool_completed",
        "tool": tool_name,
        "result_type": type(result).__name__,
    })
Enter fullscreen mode Exit fullscreen mode

The callback observes the result.

It does not necessarily decide whether the tool should have run.

The OpenAI Agents SDK, for example, provides lifecycle handlers around agent, model, tool, and handoff events. It supports run-level hooks for observing the complete workflow and agent-level hooks attached to a particular agent.

A simplified example looks like this:

from agents import RunHooks


class AuditHooks(RunHooks):
    async def on_agent_start(self, context, agent):
        print(f"Agent started: {agent.name}")

    async def on_tool_start(self, context, agent, tool):
        print(f"Calling tool: {tool.name}")

    async def on_tool_end(self, context, agent, tool, result):
        print(f"Tool completed: {tool.name}")

    async def on_agent_end(self, context, agent, output):
        print(f"Agent completed: {agent.name}")
Enter fullscreen mode Exit fullscreen mode

Do not put all business logic in callbacks

Callbacks are excellent for side effects such as telemetry.

They become dangerous when critical workflow behavior depends on them.

For example, avoid making a logging callback responsible for refund authorization:

async def on_tool_start(...):
    # Bad architectural boundary
    if tool.name == "issue_refund":
        perform_authorization()
Enter fullscreen mode Exit fullscreen mode

A dedicated policy hook, tool wrapper, or approval system is usually easier to test and reason about.


5. Hooks: Intercepting the Agent Lifecycle

Hooks are interception points.

A hook can run:

  • Before the agent starts
  • Before a model request
  • After a model response
  • Before a tool call
  • After a tool call
  • Around the entire tool call
  • Before a handoff
  • Before the final response
  • After the run finishes

Unlike a purely observational callback, a hook may modify, block, retry, redirect, or replace an operation.

For example:

async def before_tool_call(tool_name: str, arguments: dict, context):
    if tool_name == "issue_refund":
        amount = arguments["amount"]

        if amount >= 100 and not context.supervisor_approved:
            raise PermissionError(
                "Supervisor approval is required for refunds of $100 or more."
            )

    return arguments
Enter fullscreen mode Exit fullscreen mode

This hook changes the behavior of the system by preventing an unauthorized action.

LangChain’s current middleware model illustrates two common hook styles:

  • Node-style hooks, which run at specific lifecycle points.
  • Wrap-style hooks, which execute around model or tool calls and can control the operation itself.

Common hook patterns

Input-validation hook

async def before_agent(user_input: str) -> str:
    if not user_input.strip():
        raise ValueError("Input cannot be empty.")

    return user_input
Enter fullscreen mode Exit fullscreen mode

Context-injection hook

async def before_model(request, context):
    request.system_prompt += (
        f"\nThe current user role is: {context.user_role}."
    )
    return request
Enter fullscreen mode Exit fullscreen mode

Tool-authorization hook

async def before_tool(tool_name, arguments, context):
    if tool_name not in context.allowed_tools:
        raise PermissionError(f"Tool not permitted: {tool_name}")

    return arguments
Enter fullscreen mode Exit fullscreen mode

Retry hook

async def around_tool(call_tool, tool_name, arguments):
    for attempt in range(3):
        try:
            return await call_tool(tool_name, arguments)
        except TimeoutError:
            if attempt == 2:
                raise
Enter fullscreen mode Exit fullscreen mode

Output-normalization hook

async def after_tool(tool_name, result):
    return {
        "tool": tool_name,
        "success": True,
        "data": result,
    }
Enter fullscreen mode Exit fullscreen mode

Hooks are one of the best places to implement deterministic runtime controls.


6. Middleware: Composing Multiple Hooks

Middleware is a reusable layer that wraps part of the agent execution pipeline.

A middleware component might contain several hooks:

SecurityMiddleware
  ├── before_model
  ├── before_tool
  └── after_tool

ObservabilityMiddleware
  ├── before_agent
  ├── after_model
  └── after_agent

CostControlMiddleware
  ├── before_model
  └── after_model
Enter fullscreen mode Exit fullscreen mode

Middleware is useful for cross-cutting concerns such as:

  • Authentication
  • Authorization
  • PII filtering
  • Rate limiting
  • Prompt transformation
  • Tool filtering
  • Retry policies
  • Context compression
  • Model routing
  • Caching
  • Human approval
  • Observability

Modern agent frameworks use middleware to dynamically transform prompts, select tools, manage context, add guardrails, and apply fallback behavior.

A conceptual pipeline could look like this:

User Request
     │
     ▼
Authentication Middleware
     │
     ▼
Input Safety Middleware
     │
     ▼
Context Construction Middleware
     │
     ▼
Model Call
     │
     ▼
Tool Policy Middleware
     │
     ▼
MCP Tool Call
     │
     ▼
Output Validation Middleware
     │
     ▼
Final Response
Enter fullscreen mode Exit fullscreen mode

The order matters.

For example, authorization should normally happen before a sensitive tool executes, not after it completes.


7. Plugins: Packaging Related Capabilities

“Plugin” is not a primary MCP protocol primitive.

It is usually a framework or application-level concept.

A plugin generally packages a group of related capabilities, such as:

Billing Plugin
  ├── lookup_order
  ├── calculate_refund
  ├── issue_refund
  ├── refund policy resource
  └── investigate-refund prompt
Enter fullscreen mode Exit fullscreen mode

Semantic Kernel defines a plugin as a group of functions that can be exposed to AI applications. It can import capabilities from native code, OpenAPI specifications, or MCP servers.

Depending on the framework, a plugin might contain:

  • Tools
  • Prompt templates
  • Resources
  • Authentication logic
  • Configuration
  • Dependencies
  • Lifecycle hooks
  • UI metadata
  • Version information

An MCP server can therefore act like a portable plugin boundary.

But the terms are not identical:

MCP server = Protocol endpoint exposing standardized capabilities
Plugin     = Logical package used by a framework or application
Enter fullscreen mode Exit fullscreen mode

One plugin may connect to several MCP servers.

One MCP server may expose capabilities that are imported as a plugin.

Why plugins are useful

Without plugins, an agent configuration may become a long list of unrelated tools:

tools = [
    lookup_order,
    issue_refund,
    get_customer,
    search_documents,
    create_ticket,
    send_email,
    update_crm,
    run_query,
]
Enter fullscreen mode Exit fullscreen mode

With plugins or namespaces, the structure is clearer:

billing.lookup_order
billing.issue_refund

support.create_ticket
support.search_articles

communication.send_email

crm.get_customer
crm.update_customer
Enter fullscreen mode Exit fullscreen mode

Grouping related tools can also make capability discovery easier for the model.


8. Steering: Controlling the Agent’s Direction

Steering is the broadest concept in this article.

It is not one specific MCP message or API.

Steering means influencing what the agent does next.

Examples include:

  • Changing its instructions
  • Adding contextual information
  • Removing tools
  • Requiring a specific tool
  • Blocking a tool
  • Routing to another model
  • Routing to another agent
  • Asking the user for clarification
  • Requiring approval
  • Stopping the workflow
  • Changing output structure
  • Reducing the remaining budget
  • Switching from action mode to read-only mode

Steering can happen before or during execution.

Prompt steering

Change the instructions given to the model:

def build_instructions(context) -> str:
    if context.mode == "read_only":
        return """
        Investigate the request using read-only tools.
        Do not call tools that modify external systems.
        """

    return """
    Resolve the request using available tools.
    Confirm with the user before performing irreversible actions.
    """
Enter fullscreen mode Exit fullscreen mode

Dynamic instructions are supported by agent frameworks such as the OpenAI Agents SDK, where the instruction function can use runtime context to construct the system prompt.

Prompt steering is flexible but probabilistic.

The model may misunderstand an instruction.

Therefore, prompt steering should be paired with capability and policy controls.

Context steering

Change the information visible to the model:

User asks for refund
        │
        ├── Include order details
        ├── Include refund policy
        ├── Include previous support interactions
        └── Exclude unrelated customer records
Enter fullscreen mode Exit fullscreen mode

Context steering is often more effective than simply adding more instructions.

The model’s behavior depends on the information available at the time it makes a decision.

MCP resources are especially useful here because the application can retrieve relevant data from external systems and selectively place it into the model context.

Capability steering

Change which tools the agent can see:

def available_tools(context) -> set[str]:
    tools = {
        "lookup_order",
        "read_refund_policy",
    }

    if context.user_confirmed:
        tools.add("issue_refund")

    if context.is_supervisor:
        tools.add("override_refund_limit")

    return tools
Enter fullscreen mode Exit fullscreen mode

This is stronger than writing:

Do not use issue_refund before confirmation.
Enter fullscreen mode Exit fullscreen mode

When the tool is hidden, the model cannot select it.

Agent runtimes can support static or dynamic MCP tool filtering. For example, the OpenAI Agents SDK allows an MCP tool filter to decide which tools are exposed based on the active agent and run context.

Tool-choice steering

Sometimes you may want to control whether tools are used:

auto      = Model decides whether to call a tool
required  = Model must call at least one tool
none      = Model cannot call a tool
specific  = Model must call a named tool
Enter fullscreen mode Exit fullscreen mode

This can be useful for deterministic workflow stages.

For example:

Stage 1: Force lookup_order
Stage 2: Let the model analyze the order
Stage 3: Require user confirmation
Stage 4: Permit issue_refund
Enter fullscreen mode Exit fullscreen mode

Policy steering

Policy steering applies deterministic rules:

def evaluate_tool_call(tool_name: str, arguments: dict, context):
    if tool_name == "issue_refund":
        if not context.user_confirmed:
            return "DENY", "Customer confirmation is missing."

        if arguments["amount"] >= 100 and not context.supervisor_approved:
            return "DEFER", "Supervisor approval is required."

    return "ALLOW", None
Enter fullscreen mode Exit fullscreen mode

A useful policy engine may return more than True or False:

ALLOW   = Execute the tool
DENY    = Block the tool
MODIFY  = Change the arguments
DEFER   = Request approval
RETRY   = Try again
ROUTE   = Send to another agent
Enter fullscreen mode Exit fullscreen mode

Human steering

Sometimes the agent does not have enough information to continue safely.

MCP elicitation allows a server to request additional user information through the client. Current MCP elicitation supports structured form interactions and URL-based flows for sensitive interactions.

For example:

Agent: A refund can be issued, but I need confirmation.

Requested information:
- Confirm refund amount: $79.99
- Refund destination: Original payment method

Actions:
[Confirm] [Modify] [Cancel]
Enter fullscreen mode Exit fullscreen mode

Human steering is especially valuable for:

  • Financial transactions
  • External communication
  • Production changes
  • Record deletion
  • Account changes
  • Decisions with legal or compliance impact

9. How MCP Features Support Steering

MCP does not provide a single steer_agent() operation.

Instead, several MCP capabilities can contribute to steering.

Resources steer through context

policy://refunds
customer://123/profile
order://A1004
schema://analytics
Enter fullscreen mode Exit fullscreen mode

Resources determine what factual and operational context is available.

Prompts steer through instructions

/investigate-refund
/review-security-incident
/prepare-release-notes
Enter fullscreen mode Exit fullscreen mode

Prompts provide reusable behavior templates.

Tools steer through affordances

The available tools tell the model what actions are possible.

An agent with only these tools:

search_policy
lookup_order
Enter fullscreen mode Exit fullscreen mode

will behave differently from an agent with:

search_policy
lookup_order
issue_refund
delete_order
send_email
Enter fullscreen mode Exit fullscreen mode

Elicitation steers through user input

The server can pause an interactive workflow and request information or approval from the user.

Sampling steers through nested model calls

MCP sampling allows a server to request an LLM generation through the client. This lets the client retain control over model selection, access, and permissions while enabling more agentic server workflows. Current MCP sampling can also support tool-enabled sampling when the client declares the required capability.

For example, an MCP server might request a model to:

  • Summarize retrieved records
  • Classify a document
  • Select relevant information
  • Generate a draft
  • Analyze tool output

The client still controls whether the sampling request is allowed.


10. Putting Everything Together

Let us build a simplified refund agent.

MCP server

# billing_server.py

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("billing")


@mcp.resource("policy://refunds")
def refund_policy() -> str:
    """Return the organization's refund rules."""
    return """
    Refunds are available within 30 days of delivery.

    Refunds below $100 require customer confirmation.
    Refunds of $100 or more require customer confirmation
    and supervisor approval.
    """


@mcp.prompt()
def investigate_refund(order_id: str) -> str:
    """Generate a refund investigation workflow."""
    return f"""
    Investigate order {order_id}.

    Retrieve the order, consult the refund policy, explain
    eligibility, and ask for confirmation.

    Do not issue the refund without confirmation.
    """


@mcp.tool()
def lookup_order(order_id: str) -> dict:
    """Retrieve an order and determine its basic refund eligibility."""
    return {
        "order_id": order_id,
        "delivery_age_days": 12,
        "amount": 79.99,
        "status": "delivered",
    }


@mcp.tool()
def issue_refund(order_id: str, amount: float) -> dict:
    """Issue a confirmed refund for an eligible order."""
    return {
        "order_id": order_id,
        "amount": amount,
        "status": "submitted",
    }


def main() -> None:
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Agent runtime

The following example uses the OpenAI Agents SDK as one possible host runtime. The same architecture can be implemented with other frameworks.

# agent.py

import asyncio
from dataclasses import dataclass

from agents import Agent, RunContextWrapper, RunHooks, Runner
from agents.mcp import (
    MCPServerStdio,
    ToolFilterContext,
)


@dataclass
class RefundContext:
    user_id: str
    user_confirmed: bool = False
    supervisor_approved: bool = False


def dynamic_instructions(
    wrapper: RunContextWrapper[RefundContext],
    agent: Agent[RefundContext],
) -> str:
    context = wrapper.context

    approval_status = (
        "Supervisor approval is available."
        if context.supervisor_approved
        else "Supervisor approval is not available."
    )

    return f"""
    You are a refund-support agent.

    Investigate requests before taking action.
    Explain refund eligibility clearly.
    Never issue a refund without customer confirmation.

    {approval_status}
    """


async def filter_tools(
    filter_context: ToolFilterContext,
    tool,
) -> bool:
    run_context = filter_context.run_context.context

    # Read-only investigation tools remain available.
    if tool.name != "issue_refund":
        return True

    # Hide the action tool until confirmation is recorded.
    return bool(run_context.user_confirmed)


class AuditHooks(RunHooks):
    async def on_agent_start(self, context, agent):
        print(f"[audit] agent_started={agent.name}")

    async def on_tool_start(self, context, agent, tool):
        print(f"[audit] tool_started={tool.name}")

    async def on_tool_end(self, context, agent, tool, result):
        print(f"[audit] tool_completed={tool.name}")

    async def on_agent_end(self, context, agent, output):
        print(f"[audit] agent_completed={agent.name}")


async def main() -> None:
    refund_context = RefundContext(
        user_id="customer-123",
        user_confirmed=False,
        supervisor_approved=False,
    )

    async with MCPServerStdio(
        name="Billing MCP",
        params={
            "command": "python",
            "args": ["billing_server.py"],
        },
        cache_tools_list=True,
        tool_filter=filter_tools,
        require_approval={
            "always": {
                "tool_names": ["issue_refund"],
            }
        },
    ) as billing_server:
        agent = Agent[RefundContext](
            name="Refund Assistant",
            instructions=dynamic_instructions,
            mcp_servers=[billing_server],
        )

        result = await Runner.run(
            agent,
            "Can I get a refund for order A1004?",
            context=refund_context,
            hooks=AuditHooks(),
        )

        print(result.final_output)


if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

The runtime provides several layers of behavioral control:

Dynamic instructions
    └── Tell the model how to behave

Tool filter
    └── Hides issue_refund before confirmation

Approval policy
    └── Requires approval when issue_refund is requested

Audit hooks
    └── Record what happened

MCP server
    └── Provides the policy, workflow prompt, and billing tools
Enter fullscreen mode Exit fullscreen mode

The OpenAI Agents SDK currently supports local MCP servers through stdio, Streamable HTTP, and legacy SSE integrations. It also provides tool filtering, approval policies, MCP prompt retrieval, caching, metadata injection, and tracing around MCP activity.


11. Callbacks vs. Hooks: A Practical Rule

The boundary can be summarized with one question:

Is this code only observing the event, or can it influence the event?

Use a callback when you need to:

  • Record a metric
  • Create a trace
  • Send an internal notification
  • Store evaluation data
  • Measure latency
  • Track token usage

Use a hook or middleware layer when you need to:

  • Change the prompt
  • Add context
  • Validate arguments
  • Remove a tool
  • Block an operation
  • Retry a request
  • Route to another model
  • Require human approval
  • Change the result
  • Stop the agent

A callback says:

The agent called issue_refund.
Enter fullscreen mode Exit fullscreen mode

A hook says:

The agent is trying to call issue_refund.
Should this call be allowed?
Enter fullscreen mode Exit fullscreen mode

12. Production Design Recommendations

Keep MCP servers focused

Avoid creating one enormous MCP server containing every organizational capability.

Prefer domain boundaries such as:

billing-mcp
customer-support-mcp
github-mcp
analytics-mcp
policy-mcp
communications-mcp
Enter fullscreen mode Exit fullscreen mode

Focused servers are easier to:

  • Secure
  • Test
  • Version
  • Monitor
  • Assign ownership
  • Apply least-privilege access

Expose the smallest necessary toolset

Too many tools can create several problems:

  • Larger model context
  • Confusing tool selection
  • Increased latency
  • Name collisions
  • Greater security exposure
  • More opportunities for incorrect actions

Dynamically expose tools based on:

  • User permissions
  • Active workflow stage
  • Tenant
  • Environment
  • Agent role
  • Feature flags
  • Authentication status

Separate read tools from write tools

For example:

Read-only:
- lookup_order
- get_customer
- search_policy

State-changing:
- issue_refund
- update_customer
- send_email
- delete_record
Enter fullscreen mode Exit fullscreen mode

Write operations should receive stronger validation, authorization, and approval requirements.

Treat tool output as untrusted input

An MCP tool may return:

  • Malformed data
  • Unexpected instructions
  • Excessively large content
  • Sensitive information
  • Stale records
  • Errors disguised as success
  • Content that attempts to influence the model

Validate and normalize tool results before returning them to the model.

Put authorization outside the model

Do not ask the model to decide whether the current user has permission.

Instead, calculate permissions using trusted application code:

allowed = authorization_service.can_execute(
    user_id=context.user_id,
    action="issue_refund",
    resource_id=order_id,
)
Enter fullscreen mode Exit fullscreen mode

The model may help identify the requested action.

The policy system should decide whether the action is allowed.

Make side effects explicit

Tool descriptions should clearly indicate whether an operation:

  • Reads data
  • Writes data
  • Sends communication
  • Deletes information
  • Charges money
  • Changes permissions
  • Triggers another workflow

This improves both model selection and human review.

Add idempotency

An agent may retry a tool because of:

  • Network timeouts
  • Model retries
  • Application restarts
  • Lost responses
  • Workflow recovery

A payment or refund tool should support an idempotency key:

issue_refund(
    order_id="A1004",
    amount=79.99,
    idempotency_key="refund-A1004-session-987",
)
Enter fullscreen mode Exit fullscreen mode

This prevents accidental duplicate operations.

Trace decisions, not only tool calls

A useful trace should answer:

What did the user request?
What instructions were active?
What resources were included?
Which tools were visible?
Which tool did the model select?
What arguments were proposed?
Which policy was evaluated?
Was approval requested?
What result was returned?
What did the agent tell the user?
Enter fullscreen mode Exit fullscreen mode

The OpenAI Agents SDK’s tracing system, for example, records model generations, tool calls, handoffs, guardrails, and custom workflow events.


13. The Final Mental Model

The behavior of an MCP-powered agent is not controlled by one prompt.

It emerges from several layers working together:

Instructions
    Tell the agent what it should do.

Resources
    Give the agent relevant knowledge.

Tools
    Define what the agent can do.

Plugins
    Package related capabilities.

Callbacks
    Record what the agent did.

Hooks
    Intercept what the agent is about to do.

Middleware
    Applies reusable behavior across the lifecycle.

Steering
    Changes the agent's direction at runtime.

Guardrails and policies
    Define what the agent is allowed to do.

MCP
    Standardizes how external capabilities are connected.
Enter fullscreen mode Exit fullscreen mode

A reliable agent does not depend entirely on the model making the right decision.

It combines probabilistic reasoning with deterministic controls.

That is the difference between a demo agent and a production agent.

A demo agent has a prompt and a few tools.

A production agent has:

  • Carefully scoped MCP capabilities
  • Dynamic context
  • Runtime steering
  • Tool filtering
  • Authorization
  • Human approval
  • Lifecycle hooks
  • Error handling
  • Tracing
  • Evaluations
  • Auditable policies

MCP gives the agent access to the outside world.

Callbacks help us understand what happened.

Hooks let us intervene.

Plugins keep capabilities organized.

Tools let the agent act.

Steering ensures that the agent moves in the right direction.

And production engineering ensures that it does so safely, reliably, and within the user’s authority.

Top comments (0)