Showing posts with label MCP. Show all posts
Showing posts with label MCP. Show all posts

May 7, 2026

Building Site-Aware Enterprise AI Agents on Microsoft 365 Using Claude Agent SDK

Introduction

An enterprise running seven department-specific SharePoint intranet sites needed AI that could actually operate within their systems - not just answer questions about them. Here is what a single task looked like before we got involved - and after.

Before - Sales proposal workflow After - same task, one sentence
Open Pipeline Tracker on SharePoint - find the deal Type: "Draft a proposal for XYZ Corp's cloud migration deal and send it to their CTO."
Switch to Client Contacts - find the CTO's email Agent queries Pipeline Tracker - pulls deal value, stage, scope notes
Open Word - hunt for the proposal template on the shared drive Agent looks up CTO in Client Contacts - name, email, title
Manually fill in deal value, scope, and terms Branded proposal generated from python-docx template and uploaded to Sales Collateral
Save, switch back to SharePoint, upload to Sales Collateral library Email compose panel opens - pre-filled with CTO's address, subject, and body
Open Outlook - type the CTO's email, write the body, attach, send Review, tweak one line, hit Send
40 minutes  ·  6 applications  ·  1 task One sentence  ·  Five systems  ·  Done

We assessed the organisation's requirements against Copilot Cowork and Claude Cowork. Both are genuinely capable products - but neither could query custom SharePoint list schemas, connect to a SQL employee database, generate documents from branded templates, or switch to a different specialist persona depending on which department site the user was on. They needed something those products are structurally not built to do.

Same panel. Same backend. Same LLM. Completely different specialist per site, loaded at runtime from a JSON manifest. This is what we built. Here is how:

The Architecture in One Sentence

A single SPFx floating panel on every SharePoint page sends messages to a FastAPI backend, which enters a Claude agentic loop with a filtered set of MCP tools - filtered by which site the user is on, what they are allowed to do, and which department's data model applies. Each site loads its own agent persona, slash commands, and skill files - deep knowledge modules that teach the agent how a specific department's data is structured, what business rules apply, and which workflows exist.

System Architecture diagram

System Architecture - SPFx panel → FastAPI backend → Claude agentic loop → MCP server → enterprise data sources

That sentence hides a lot of machinery. Three decisions do most of the work: how each site loads its own specialist, how authentication is split between reads and writes, and how the agentic loop orchestrates everything in between. Let me walk through each.

Decision 1: The Plugin Manifest

When a user opens the Sales site and the panel initialises, the backend resolves their session: who are they (from the OBO token), which site are they on (from the site_url passed by the frontend), and what can they do (from the plugin manifest). The manifest is a single JSON file:

{
  "name": "sales",
  "display_name": "Sales Assistant",
  "entry_agent": "agents/sales-assistant.md",
  "connectors": ["sharepoint", "msgraph", "email", "docgen", "ems",
    "knowledge", "analytics", "automation"],
  "commands": ["commands/pipeline-report.md", "commands/add-lead.md", ...],
  "skills": ["skills/pipeline-management/SKILL.md",
    "skills/client-engagement/SKILL.md", ...],
  "sharepoint_lists": {
    "lists": {
      "Pipeline Tracker": "columns: OpportunityName, DealValue, PipelineStage, ..."
    }
  }
}

This manifest does five things at session start:

It loads a persona.

The entry_agent points to a markdown file that defines the agent's personality, role, behaviour rules, and domain expertise. The Sales agent is "friendly, professional, and results-driven." The Finance agent is precise and compliance-aware. The People agent is warm and policy-oriented. Same LLM, different specialist.

It filters the tool palette.

The connectors array controls which of the 58 MCP tools appear in Claude's tool list for this session. Sales gets SharePoint, Graph, Email, DocGen, EMS, Knowledge, Analytics, and Automation - but not Azure DevOps. Delivery gets Azure DevOps too. Technology gets everything. The LLM only sees tools it is allowed to use.

It loads skill files.

This is where the deep domain knowledge lives. Each skill is a markdown file that describes how a specific aspect of the department works. The Sales plugin has skills for pipeline management, client engagement, and competitive intelligence. A skill might describe how the Pipeline Tracker list is structured, what each column means, which OData filters produce useful results, what "stalled deal" means in this organisation's context, and what steps the agent should follow for a pipeline review. Skills are loaded into the system prompt based on the site and the user's query - they are the agent's domain training, delivered at runtime through text, not fine-tuning.

It injects list schemas.

The sharepoint_lists block is injected into the system prompt. This is how the agent knows the column is called PipelineStage, not Stage. It knows DealValue is the field for revenue, not Value or Amount. It never guesses. If a column is not in the manifest, the agent cannot reference it.

It registers slash commands.

Each command is a markdown file with trigger phrases, required parameters, the agent to hand off to, and step-by-step instructions. When a user types /pipeline-report, the command's markdown gets injected into the system prompt for that turn.

Plugin-Per-Site Architecture diagram

Plugin-Per-Site Architecture - 7 sites, auto-discovered, manifest-driven, session-filtered

The consequence of this design is that adding a new department - an eighth site, a regional hub, a project workspace - requires creating one folder with one JSON manifest and a few markdown files. No code changes. No backend redeployment. The registry discovers it on next restart.

The key insight: The plugin folder is the architecture. Not the LLM, not the framework, not the cloud infrastructure. The entire system's behaviour - which specialist responds, which tools are available, which data is accessible, which commands exist, which domain knowledge the agent draws on - is determined by which plugin.json file gets loaded and which skill files sit alongside it. Everything else is shared plumbing. This is what makes the system maintainable at scale: changing a department's AI behaviour is editing a JSON file and a few markdown documents, not shipping code.

Decision 2: Dual-Auth and the Invisible Session

Authentication is where most custom AI implementations get it wrong. Either they use the user's token for everything (writes are unauditable and tied to individual permissions) or they use an app-only token for everything (losing the identity context of who asked for the action).

We split it:

Reads use the user's delegated OBO token.

When the agent queries a SharePoint list or fetches calendar events, it does so as the user. If the user cannot see a site, neither can the agent. The existing M365 permission model is preserved - no data leakage, no privilege escalation.

Writes use an app-only certificate token.

  • All write operations - list item creation, document upload, email send - execute through a controlled service identity, not the user's token.
  • Every write is routed through an audit logger: who requested it, what changed, tool name, payload, status, and duration.
  • site_url, OBO token, app-only token, and the user's permission set are never parameters the LLM sees - they are injected from the session object before the agentic loop begins.

In the initial version, SharePoint tools accepted site_url as a tool parameter. During early integration testing, Claude hallucinated a URL - it substituted the Sales site URL when a user on the Finance site asked about budget items. The query returned nothing. The agent confidently reported "no budget items found." It was a silent, plausible failure - the worst kind. We refactored site_url out of every tool interface within the day and moved it to session-level injection. The LLM never sees it, never chooses it, never hallucinates it.

If the LLM does not need a value to reason about the task, do not put it in the tool interface. Every parameter visible to the LLM is a surface for hallucination. Session-level injection eliminates that surface.

Decision 3: The Agentic Loop

When a user sends a message, the backend does not call Claude once and return the answer. It enters a loop:

Agentic Loop diagram

Agentic Loop - Request Lifecycle with tool execution feedback loop

  • Multi-step tool chains. A single message like "find stalled deals, draft follow-ups, and notify the sales lead" triggers five sequential tool calls - SharePoint OData query, EMS employee lookup, three email draft preparations - each feeding the next decision.
  • Real-time status streaming. Each tool call emits an SSE event to the frontend ("Fetching list data: Pipeline Tracker...", "Looking up employee...") so the user sees the agent working, not a loading spinner.
  • Model is a config variable. Claude Sonnet is the default - fast enough for real-time streaming, capable enough for multi-step orchestration. Swapping models is a single environment variable change; the architecture is not coupled to any provider.
  • We tested Azure OpenAI + Semantic Kernel. The loop runs and tools get called. But in head-to-head testing, Claude produced fewer hallucinated tool parameters, followed complex multi-step prompts more faithfully, and handled branching logic - where a tool result determines whether to call the next tool or skip to a different action - more consistently. That is why it is our default.

What the Agent Can Actually Do

58 tools across 9 connectors. Here are the ones that matter most to daily operations:

Connector What it does Available on
SharePoint Read list items with OData filters, create and update items, browse document libraries, upload files, search across the site. Primary data layer. All sites
EMS Direct SQL via pyodbc against the HR database: employee lookup, org chart traversal, leave balances, skills search, project assignments, team allocation, capacity planning, budget tracking. No REST wrapper. All sites
Document generation Branded offer letters, proposals, and reports via python-docx. Budget workbooks via openpyxl. Generated on the backend, uploaded to SharePoint, download card in chat. Sales, People, Finance, Operations
Email Two-step workflow: agent drafts, human reviews in inline compose panel, then confirms Send. Never auto-sends. All sites
Azure DevOps Query and create work items, get sprint status. Delivery, Technology only
Proactive alerts Five APScheduler handlers: stalled deals, budget thresholds, expiring contracts, onboarding gaps, morning brief digest. Push to Teams channels and notification bell before anyone opens a browser. All sites (handler-specific)
MS Graph Users, calendar, Teams channels, mail integration. All sites
Analytics Natural language reports, anomaly detection. All sites
Knowledge Federated search, expert finder. All sites

Lessons from the Build

Every implementation surfaces lessons that inform the next one.

SharePoint field name complexity runs deeper than expected.

Display names and internal names diverge in non-obvious ways. "Status" might be stored as BudgetStatus. "Details" might be AnnouncementDetails. The initial deployment surfaced field name mismatches that cost debugging cycles. Our fix - a PowerShell schema export script that generates verified field mappings per site - is now a standard first step in our deployment methodology.

Not every data source needs an API layer.

Our first design had a full REST API wrapper around the SQL employee database. We replaced it with direct pyodbc queries wrapped in the MCP tool contract. Simpler, faster, easier to maintain.

The in-process MCP server will need extraction.

Running the tool server in-process was the right call for initial development - no serialisation overhead, easy debugging. But for production scale, tool execution needs to run independently so long-running operations (document generation, cross-site searches) do not block the API layer. The architecture was designed for this extraction - MCP's HTTP transport makes it a configuration change, not a rewrite - which is planned for the next phase.

Retry logic is now day-one infrastructure.

Claude's API occasionally returns transient errors under load. Without exponential backoff (1s, 2s, 4s), these surface as user-facing failures. Retry logic is now part of our standard agentic infrastructure from the start, based on this experience.

Production hardening - monitoring, observability, error recovery, and scale testing - deserves its own post. We will publish that next.

A Note on Portability

This case study uses SharePoint as the enterprise platform and Claude as the LLM, but the pattern is platform-agnostic. The same plugin-per-site architecture has been applied with Confluence, custom intranets, and internal portals. The frontend can be any web surface that supports a JavaScript embed - the SPFx panel is one implementation, not a requirement. The connectors, the plugin manifest pattern, the dual-auth model, and the session-scoped tool filtering are all transferable. If your organisation runs on a different stack, the architecture adapts to it.

Conclusion

The plugin-per-site pattern, dual-auth model, session-scoped tool filtering, and in-process MCP server described here form a reusable enterprise architecture - not a one-off implementation. Adding a new department means creating a folder with a JSON manifest and a few markdown skill files. No code changes, no redeployment. The system's behaviour is entirely determined by configuration, not by the codebase.

This architecture was designed and deployed by Binary Republik. It is adaptable to any organisation's departmental structure, data model, and governance requirements - and transferable to any enterprise platform with programmatic APIs.

If you have any questions you can reach out to our AI Consulting team here.

July 10, 2025

Azure AI Agents and Model Context Protocol (MCP) with Dynamic Tool Discovery: Step-by-Step Guide

Introduction:

In the era of AI agents, it’s common to see agents interacting with a wide range of external tools, APIs, and services. Whether it’s fetching data, triggering workflows, or connecting with domain-specific systems tools are how agents get things done.

But as the number of tools grows, so does the complexity of managing them. Hardcoding every tool into the agent logic isn’t scalable. Each update means changing code, redeploying agents, and tightly coupling functionality that should stay flexible.

That’s where Dynamic tool discovery comes in and this is exactly what Model Context Protocol (MCP) is built for.
In this guide, we’ll learn how to integrate MCP with an Azure AI Agent, enabling it to discover tools at runtime. No redeployment. No manual updates. Just a clean, modular setup where tools live independently and are made available to the agent on demand.

We’ll walk through building a medical assistant agent that can:

1. Retrieve upcoming patient appointments
2. Fetch recent medical history
3. Estimate wait times for specialists

All powered by tools registered on an MCP server and dynamically discovered by the agent.

Before we start building, let’s take a moment to understand what MCP is and why dynamic tool discovery matters in the context of Azure AI Agents.

Understanding MCP and Tool Discovery:

What is MCP?

Model Context Protocol (MCP) is a protocol that enables AI agents to discover and interact with external tools at runtime. Instead of defining tools inside the agent, MCP exposes them through a centralized server. Each tool is defined with clear inputs, outputs, and documentation and can be registered or updated without touching the agent code.

You can think of MCP as a live tool catalog that your agent connects to whenever it needs functionality. This means tools can be managed independently of the agent itself.

Now that we know where the tools live, let’s see how agents actually discover and use them.

What is Dynamic Tool Discovery?

Dynamic tool discovery allows an agent to:

1. Query the MCP server
2. Fetch the available tools
3. Generate function wrappers for each
4. Use them as if they were built into the agent

All of this happens at runtime, so your agent always has access to the latest toolset without needing redeployment or hardcoded logic.

So how does this all fit into the Azure ecosystem? Let’s connect the dots.

Why it works well with Azure AI Agents:

Azure AI Agents are designed to be modular and extensible. By integrating MCP:

1. You keep your agent code clean and focused on behavior
2. Tools can be maintained and scaled independently
3. Your system becomes more adaptable to changes in external services or APIs

This combination lets your agents evolve without rewrites making your setup easier to scale and maintain.

Now that we understand how dynamic tool discovery works with MCP, let’s quickly get our Azure AI Agent environment ready starting with setting up Azure AI Foundry.

Set Up Azure AI Foundry and Agent Service:

Before we dive into code, we'll need a working Azure AI Foundry project with a deployed model. Follow below steps to have one.

1. Open a browser and go to https://ai.azure.com.


2. Sign in using your Azure credentials.
3. On the homepage, select Create an agent.
4. When prompted, create a new project. Use a valid name and expand Advanced options to configure:
    1. Azure AI Foundry resource: Choose or create one
    2. Subscription: Your Azure subscription
    3. Resource group: Select or create
    4. Region: Pick any supported region (note that model quotas may vary)
5. Select Create and wait for the project to be provisioned.
6. If needed, deploy a GPT-4o model using the Global Standard or Standard option (based on quota availability).
7. Once the project is ready, go to the Overview tab in the left-hand menu.
8. Copy the Project Endpoint URL we’ll need it later to connect the client script to this agent.


That’s all we need from Azure for now. 

With the Azure AI Foundry project and agent in place, let’s shift to the local environment setup where we’ll start wiring everything together with MCP.

Set Up the Local Environment:

With our Azure AI project ready, it’s time to prepare the local development environment where the MCP server and client will run. This setup allows our agent to discover tools dynamically at runtime.

Step 1: Create a Virtual Environment
Open your terminal and run the following commands to create and activate a Python virtual environment:

python -m venv mcpVenv
mcpVenv\Scripts\activate   # For Windows

# OR

source mcpVenv/bin/activate   # For macOS/Linux

Step 2: Create .env File with Required Variables
Before installing the requirements, create a .env file in the project root and add the following:

# Project Connection String from Azure AI Project in Foundry
PROJECT_ENDPOINT="<<Project Connection String>>"

# Model deployment name from Azure
MODEL_DEPLOYMENT_NAME=gpt-4o

Note: The PROJECT_ENDPOINT is the connection string you copied in the previous step when setting up the Azure AI Project in Foundry.

Step 3: Install Required Packages
Create a requirements.txt file and add the following dependencies:

fastapi
aiohttp
python-dotenv
azure-ai-agents
azure-identity
mcp

Then install them:

pip install -r requirements.txt

That’s it out environment is now ready to build the MCP server and client.

With the environment ready, let’s start building the core of our setup the MCP server that will host and expose tools to our Azure AI Agent.

Build the MCP Server:

The MCP server acts as a centralized registry where tools are defined and exposed. These tools will later be discovered and used by the agent dynamically at runtime.

We’ll use the FastMCP class from the mcp package to quickly spin up an HTTP-based server and register tools using decorators.

Here’s the full server script (server.py):

import json
from mcp.server.fastmcp import FastMCP

# Initialize the MCP server
mcp = FastMCP("HealthAssistant")

# -----------------------------------------
# Tool: Get upcoming appointments for a patient
# -----------------------------------------
@mcp.tool()
def get_appointment_schedule(patient_id: str) -> str:
    """
    Retrieves the list of upcoming medical appointments scheduled for the specified patient.
    
    This tool accepts a patient ID and returns a structured list of upcoming appointments,
    including the date and department for each appointment. It can be used to display the
    patient's future visit schedule to help them stay informed and manage their time.

    Parameters:
        patient_id (str): The unique identifier of the patient.

    Returns:
        str: A JSON-formatted string containing the patient ID and a list of upcoming appointments,
             each with a date and department name.
    """
    data = {
        "patient_id": patient_id,
        "appointments": [
            {"date": "2025-07-15", "department": "Cardiology"},
            {"date": "2025-08-03", "department": "Dermatology"},
            {"date": "2025-08-18", "department": "Lab Work"}
        ]
    }
    return json.dumps(data)

# -----------------------------------------
# Tool: Get recent medical history
# -----------------------------------------
@mcp.tool()
def get_medical_history(patient_id: str) -> str:
    """
    Retrieves the recent medical history for the currently active patient.

    This tool returns a chronological summary of recent medical events, including visits,
    diagnoses, treatments, and test results. It's useful for reviewing a patient’s recent
    healthcare activities and identifying patterns or follow-up needs.

    Returns:
        str: A JSON-formatted string representing a list of medical history entries,
             each including the date and a summary of the medical event or consultation.
    """
    history = [
      {"date": "2025-06-20", "summary": "Annual physical checkup - normal"},
      {"date": "2025-05-02", "summary": "Prescribed allergy medication"},
      {"date": "2025-03-10", "summary": "Blood test - slightly elevated cholesterol"}
    ]
    result = {
        "patient_id": patient_id,
        "history": history
    }
    return json.dumps(result)

# -----------------------------------------
# Tool: Estimate wait time for a specialist
# -----------------------------------------
@mcp.tool()
def estimate_wait_time(specialty: str) -> str:
    """
    Provides an estimated wait time for scheduling an appointment with a medical specialist.

    By specifying a medical specialty (e.g., Cardiology, Dermatology), this tool returns
    an estimated waiting period based on current scheduling trends. This helps patients
    plan ahead and manage expectations for their care timeline.

    Parameters:
        specialty (str): The name of the medical specialty.

    Returns:
        str: A JSON-formatted string with the specialty name and its corresponding estimated wait time.
              If the specialty is not recognized, the wait time will be marked as 'Unavailable'.
    """
    wait_times = {
        "Cardiology": "2 weeks",
        "Dermatology": "3 weeks",
        "Neurology": "1 month",
        "Orthopedics": "10 days",
        "General Medicine": "3 days"
    }
    data = {
        "specialty": specialty,
        "estimated_wait_time": wait_times.get(specialty, "Unavailable")
    }
    return json.dumps(data)

# Run the server
mcp.run()

Key Components:

1. FastMCP("HealthAssistant"): Initializes the server with a label. This acts as the tool catalog name.
2. @mcp.tool(): This decorator registers each function as a tool. The server automatically exposes these tools for discovery.
3. Return values: Each tool returns structured data in JSON format. This helps the client parse responses cleanly.
4. mcp.run(): Starts the server and makes the tools available over HTTP.

Now that our tools are live and exposed through the MCP server, let’s build the MCP client that discovers these tools and hands them off to the Azure AI Agent.

Build the MCP Client and Connect to Azure AI Agent:

The MCP client is responsible for discovering tools from the MCP server and registering them with the Azure AI Agent. Once connected, the agent can call these tools as if they were native functions all without hardcoding anything.

Create a client script and use the below snippets to build the full (client.py):

import os, time
import asyncio
import json
from dotenv import load_dotenv
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from azure.ai.agents import AgentsClient
from azure.ai.agents.models import FunctionTool, MessageRole, ListSortOrder
from azure.identity import DefaultAzureCredential

os.system('cls' if os.name=='nt' else 'clear')

load_dotenv()
project_endpoint = os.getenv("PROJECT_ENDPOINT")
model_deployment = os.getenv("MODEL_DEPLOYMENT_NAME")

Key Components (Part 1: Setup)

1. .env variables: These hold your Azure AI project endpoint and model deployment name.
2. AsyncExitStack: Manages async connections useful for clean startup and teardown.
3. stdio_client(): This starts the MCP server as a subprocess and connects the client to it.

Now we define the function that connects to the MCP server:

async def connect_to_server(exit_stack: AsyncExitStack):
    server_params = StdioServerParameters(
        command="python",
        args=["server.py"],
        env=None
    )

    stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params))
    stdio, write = stdio_transport

    session = await exit_stack.enter_async_context(ClientSession(stdio, write))
    await session.initialize()

    response = await session.list_tools()
    tools = response.tools
    print("\nConnected to server with tools:", [tool.name for tool in tools]) 
    return session

Key Components (Part 2: Tool Discovery)

1. list_tools(): Queries the MCP server and returns available tools.
2. ClientSession: Wraps communication with the server and is used later to invoke tools.

Next, we’ll create a chat loop and connect it to the Azure AI Agent:

async def chat_loop(session):
    agents_client = AgentsClient(
        endpoint=project_endpoint,
        credential=DefaultAzureCredential(
            exclude_environment_credential=True,
            exclude_managed_identity_credential=True
        )
    )

    response = await session.list_tools()
    tools = response.tools

    def make_tool_func(tool_name):
        async def tool_func(**kwargs):
            result = await session.call_tool(tool_name, kwargs)
            return result
        tool_func.__name__ = tool_name
        return tool_func

    functions_dict = {tool.name: make_tool_func(tool.name) for tool in tools}
    mcp_function_tool = FunctionTool(functions=list(functions_dict.values()))

Key Components (Part 3: Wrapping Tools)

1. make_tool_func(): Dynamically wraps each discovered tool as an async Python function.
2. FunctionTool: Binds those functions to the agent using Azure’s SDK.

With the tools discovered and wrapped, the final step is to bring your Azure AI Agent to life connecting it to the tools and handling real-time user interactions.

This part handles:

1. Agent creation
2. Chat loop
3. Tool call execution
4. Printing the agent’s response

Here’s the final section of (client.py):

    agent = agents_client.create_agent(
        model=model_deployment,
        name="medical-agent",
        instructions="""
            You are a virtual healthcare assistant. Follow these guidelines:
            - If a user provides a patient ID, retrieve their upcoming appointments or medical history using the tools.
            - Estimate wait time if a user asks about seeing a specialist using specific tools.
            - Keep responses friendly, informative, and medically accurate.
        """,
        tools=mcp_function_tool.definitions
    )

    agents_client.enable_auto_function_calls(tools=mcp_function_tool)
    thread = agents_client.threads.create()

    while True:
        user_input = input("Enter a prompt for the medical agent (type 'quit' to exit):\nUSER: ").strip()
        if user_input.lower() == "quit":
            print("Exiting chat.")
            break

        message = agents_client.messages.create(
            thread_id=thread.id,
            role=MessageRole.USER,
            content=user_input,
        )

        run = agents_client.runs.create(thread_id=thread.id, agent_id=agent.id)

        while run.status in ["queued", "in_progress", "requires_action"]:
            time.sleep(1)
            run = agents_client.runs.get(thread_id=thread.id, run_id=run.id)

            tool_outputs = []

            if run.status == "requires_action":
                tool_calls = run.required_action.submit_tool_outputs.tool_calls

                for tool_call in tool_calls:
                    function_name = tool_call.function.name
                    args_json = tool_call.function.arguments
                    kwargs = json.loads(args_json)
                    required_function = functions_dict.get(function_name)
                    output = await required_function(**kwargs)

                    tool_outputs.append({
                        "tool_call_id": tool_call.id,
                        "output": output.content[0].text,
                    })

                agents_client.runs.submit_tool_outputs(
                    thread_id=thread.id,
                    run_id=run.id,
                    tool_outputs=tool_outputs
                )

        if run.status == "failed":
            print(f"Run failed: {run.last_error}")

        messages = agents_client.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
        for message in messages:
            if message.text_messages:
                last_msg = message.text_messages[-1]
                print(f"{message.role}:\n{last_msg.text.value}\n")

    print("Cleaning up agents:")
    agents_client.delete_agent(agent.id)
    print("Deleted medical-agent agent.")

Key Highlights:

1. create_agent(): Instantiates your agent and links it to the discovered tools.
2. enable_auto_function_calls(): Lets the agent decide when to use a tool based on user input.
3. Tool call handling: When a tool is needed, the client maps the request to the matching async wrapper function, runs it, and submits the result back to the agent.
4. Thread and run monitoring: This keeps track of each user message and agent response loop.
5. Cleanup: The agent is deleted at the end to avoid clutter or quota issues.

Test the Agent and See It in Action:

With everything set up, it’s time to run the client script, which will also spin up the MCP server automatically and connect to your Azure AI Agent.

Step 1: Run the Client Script
In your terminal (with the virtual environment activated), run:

python client.py

The script will:

1. Launch the MCP server as a subprocess
2. Discover available tools
3. Register them with your agent
4. Start a real-time chat loop

Note: In this setup, the client starts the server as a subprocess. This is suitable for local testing or development.
In a production environment, the MCP server and client should run independently typically as separate services or containers.

Step 2: Interact with Your Agent

Get all the Medical History for the Patient "Amey Good" With ID "453T6”

Step 3: Output it Geneates:


Step 4: Cleanup (Optional)
The client script deletes the agent automatically at the end of the session, so you won’t need to manage it manually. Just exit the script by typing:

quit

It will close the session and delete the agent automatically.

Conclusion:

We’ve just built an Azure AI Agent that discovers and uses tools dynamically through Model Context Protocol (MCP).

This setup keeps our agent logic clean, our tools modular, and our updates smooth no hardcoding, no redeployments.

While we ran the server and client together for testing, in production they should run independently.

With this foundation, we’re ready to scale dynamic agents across healthcare, support, and beyond.

If you have any questions you can reach out our SharePoint Consulting team here.