From f1139f78f5e9ed17d2fc45108e6a0c077b8f4e66 Mon Sep 17 00:00:00 2001 From: Vincent Chen Date: Sun, 28 Jun 2026 21:31:59 -0700 Subject: [PATCH 1/2] Add WorkIQ MCP integration to Python agent-framework weather sample --- samples/python/agent-framework/README.md | 81 ++++++++++++ samples/python/agent-framework/env.TEMPLATE | 14 ++ .../python/agent-framework/requirements.txt | 3 + samples/python/agent-framework/src/agent.py | 122 +++++++++++++++--- 4 files changed, 204 insertions(+), 16 deletions(-) diff --git a/samples/python/agent-framework/README.md b/samples/python/agent-framework/README.md index 84d0a424..05e51276 100644 --- a/samples/python/agent-framework/README.md +++ b/samples/python/agent-framework/README.md @@ -153,3 +153,84 @@ agent-framework/ ├── date_time.py # Current date/time tool └── weather_lookup.py # OpenWeatherMap weather tools ``` + +--- + +## WorkIQ MCP Integration + +This agent is extended with **WorkIQ** (Microsoft 365 / Agent 365) tools served over the **Model Context Protocol (MCP)**. The agent routes requests as follows: + +| Question type | Tools used | +|---|---| +| **Weather in the United States** | Local tools — `get_current_weather`, `get_weather_forecast`, `get_date` | +| **Everything else** (M365, Teams chat, mail, files, calendar, non-US weather, general Q&A) | **WorkIQ MCP tools** | + +The WorkIQ tools are discovered at runtime and merged with the local weather tools via +`McpToolRegistrationService.add_tool_servers_to_agent(...)`. If WorkIQ is not configured (or fails to +load) and `SKIP_TOOLING_ON_ERRORS=true`, the agent still answers using the local weather tools only. + +### Prerequisites + +| Tool | Purpose | +|---|---| +| [Agent 365 CLI (`a365`)](https://learn.microsoft.com/microsoft-365/agents-sdk/) | Manage MCP servers, tokens, and permissions | +| Microsoft 365 tenant with WorkIQ / Agent 365 enabled | Provides the MCP servers (Teams, Mail, etc.) | + +The integration packages are already listed in `requirements.txt`: + +```text +microsoft-agents-a365-tooling +microsoft-agents-a365-tooling-extensions-agentframework +microsoft-agents-a365-runtime +``` + +### Step A — Register the MCP servers + +List the MCP servers available to your tenant, then add the one(s) you want. This writes a +`ToolingManifest.json` file — **do not hand-edit it**, always use the CLI. + +```bash +a365 develop list-available +a365 develop add-mcp-servers "mcp_TeamsTools" +``` + +### Step B — Grant permissions + +```bash +a365 setup permissions mcp +``` + +Skipping this results in `403` errors at runtime when the agent calls a WorkIQ tool. + +### Step C — Configure authentication + +Open your `.env` and set the WorkIQ values: + +**Local development** — get a short-lived token and paste it in: + +```bash +a365 develop get-token +``` + +```bash +BEARER_TOKEN= +USE_AGENTIC_AUTH=false +SKIP_TOOLING_ON_ERRORS=true +PYTHON_ENVIRONMENT=Development +``` + +**Production / Teams** — use on-behalf-of (OBO) token exchange instead of a static token: + +```bash +BEARER_TOKEN= +USE_AGENTIC_AUTH=true +WORKIQ_AUTH_HANDLER= +SKIP_TOOLING_ON_ERRORS=true +``` + +### Step D — Run and test + +Start the agent as usual (`python -m src.main`) and try both routes: + +- *"What's the weather in Seattle?"* → answered by the local weather tools. +- *"Summarize my recent Teams chats"* → answered by the WorkIQ MCP tools. diff --git a/samples/python/agent-framework/env.TEMPLATE b/samples/python/agent-framework/env.TEMPLATE index 63a439d3..0ba6618e 100644 --- a/samples/python/agent-framework/env.TEMPLATE +++ b/samples/python/agent-framework/env.TEMPLATE @@ -7,3 +7,17 @@ AZURE_OPENAI_API_KEY= AZURE_OPENAI_MODEL=gpt-4o OPEN_WEATHER_API_KEY= + +# --- WorkIQ MCP integration --- +# Local dev: paste a token from `a365 develop get-token` (resource: mcp). +BEARER_TOKEN= +# Set to true for production / Teams. Uses OBO token exchange instead of BEARER_TOKEN. +USE_AGENTIC_AUTH=false +# Auth handler name registered in your AgentApplication (used when USE_AGENTIC_AUTH=true). +WORKIQ_AUTH_HANDLER= +# Keep answering (weather-only) if the WorkIQ tools fail to load. +SKIP_TOOLING_ON_ERRORS=true +# Tells the A365 SDK it is running locally. +PYTHON_ENVIRONMENT=Development +# Leave empty to use the production MCP platform endpoint. +MCP_PLATFORM_ENDPOINT= diff --git a/samples/python/agent-framework/requirements.txt b/samples/python/agent-framework/requirements.txt index eec8b050..6eb6c90e 100644 --- a/samples/python/agent-framework/requirements.txt +++ b/samples/python/agent-framework/requirements.txt @@ -7,3 +7,6 @@ microsoft-agents-activity microsoft-agents-hosting-core microsoft-agents-hosting-aiohttp microsoft-agents-authentication-msal +microsoft-agents-a365-tooling +microsoft-agents-a365-tooling-extensions-agentframework +microsoft-agents-a365-runtime diff --git a/samples/python/agent-framework/src/agent.py b/samples/python/agent-framework/src/agent.py index 55651903..22b6abfa 100644 --- a/samples/python/agent-framework/src/agent.py +++ b/samples/python/agent-framework/src/agent.py @@ -24,6 +24,16 @@ logger = logging.getLogger(__name__) +# WorkIQ MCP tooling (Agent 365). Imported defensively so the agent still runs +# (weather-only) if the package or its dependencies are not installed. +try: + from microsoft_agents_a365.tooling.extensions.agentframework.services.mcp_tool_registration_service import ( + McpToolRegistrationService, + ) +except Exception as mcp_import_error: # pragma: no cover - optional dependency + McpToolRegistrationService = None + logger.warning("WorkIQ MCP tooling unavailable: %s", mcp_import_error) + load_dotenv() agents_sdk_config = load_configuration_from_env(environ) @@ -37,29 +47,107 @@ ) AGENT_INSTRUCTIONS = """ -You are a friendly feline assistant that helps people find the current weather or a weather forecast for a given place. -You will always speak like a cat. -Location is a city name, 2 letter US state codes should be resolved to the full name of the United States State. -You may ask follow up questions until you have enough information to answer the customers question, but once you have the current weather or a forecast, make sure to format it nicely in text. +You are a friendly feline assistant. You always speak like a cat (use "meow", playful cat puns, and emojis when they fit). + +You can help with two kinds of requests, and you must always pick the right tool for each: + +1. Weather in the United States -- use your local weather tools: + - Use get_current_weather for current conditions. Include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather. + - Use get_weather_forecast for forecasts. Report the next 5 days, including the current day, with the date, high and low temperatures, and a short description. + - Use get_date to get the current date and time. + - Location is a city name; resolve 2-letter US state codes to the full name of the United States state. -For current weather, use the get_current_weather tool. You should include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather. -For forecasts, use the get_weather_forecast tool. You should report on the next 5 days, including the current day, and include the date, high and low temperatures, and a short description of the weather. -You should use the get_date tool to get the current date and time. +2. Anything that is NOT United States weather -- use the WorkIQ tools to answer. This includes Microsoft 365 and Microsoft Teams tasks such as reading or posting chat messages, listing chats, channels, and teams, and other workplace questions. -When responding, make sure to format the information in a way that is easy to read and understand, markdown is good, and always speak like a cat. Use emojis if it fits the response! +Routing rule: US weather questions go to the weather tools; every other question goes to the WorkIQ tools. You may ask brief follow-up questions when you need more detail. Always format answers nicely in markdown, keep them easy to read, and always speak like a cat. Use emojis if it fits the response! """ +# Shared chat client and the local (weather) tools. These are reused both for the +# base weather agent and when WorkIQ MCP tools are attached on top of them. +CHAT_CLIENT = OpenAIChatClient( + azure_endpoint=environ.get("AZURE_OPENAI_ENDPOINT", ""), + api_key=environ.get("AZURE_OPENAI_API_KEY", ""), + model=environ.get("AZURE_OPENAI_MODEL", "gpt-4o"), +) + +WEATHER_TOOLS = [get_date, get_current_weather, get_weather_forecast] + WEATHER_AGENT = Agent( - client=OpenAIChatClient( - azure_endpoint=environ.get("AZURE_OPENAI_ENDPOINT", ""), - api_key=environ.get("AZURE_OPENAI_API_KEY", ""), - model=environ.get("AZURE_OPENAI_MODEL", "gpt-4o"), - ), + client=CHAT_CLIENT, name="Purrfect Weather Agent", instructions=AGENT_INSTRUCTIONS, - tools=[get_date, get_current_weather, get_weather_forecast], + tools=WEATHER_TOOLS, ) +# WorkIQ MCP integration state. The augmented agent (weather tools + WorkIQ MCP +# tools) is built lazily on the first message and then reused for the lifetime of +# the process. If WorkIQ is not configured or fails to load, the agent falls back +# to weather-only mode. +TOOL_SERVICE = McpToolRegistrationService() if McpToolRegistrationService else None +WORKIQ_AUTH_HANDLER = environ.get("WORKIQ_AUTH_HANDLER", "") +_workiq_agent = None +_workiq_setup_done = False + + +async def get_agent(context: TurnContext): + """Return the agent to use for this turn. + + Attempts once to attach the WorkIQ MCP tools to the base weather agent. On + success the augmented agent is cached and reused. When WorkIQ is unavailable + or not configured, the weather-only agent is returned instead. + """ + global _workiq_agent, _workiq_setup_done + + if _workiq_agent is not None: + return _workiq_agent + if _workiq_setup_done or TOOL_SERVICE is None: + return WEATHER_AGENT + + _workiq_setup_done = True + + use_agentic_auth = environ.get("USE_AGENTIC_AUTH", "false").lower() == "true" + bearer_token = environ.get("BEARER_TOKEN", "") + + if not use_agentic_auth and not bearer_token and not WORKIQ_AUTH_HANDLER: + logger.info( + "WorkIQ not configured (no BEARER_TOKEN, USE_AGENTIC_AUTH, or " + "WORKIQ_AUTH_HANDLER) - running in weather-only mode." + ) + return WEATHER_AGENT + + try: + if use_agentic_auth: + # Production / Teams: the SDK exchanges an OBO token via the auth handler. + agent = await TOOL_SERVICE.add_tool_servers_to_agent( + chat_client=CHAT_CLIENT, + agent_instructions=AGENT_INSTRUCTIONS, + initial_tools=WEATHER_TOOLS, + auth=AUTHORIZATION, + auth_handler_name=WORKIQ_AUTH_HANDLER, + turn_context=context, + ) + else: + # Local dev: use the bearer token from `a365 develop get-token`. + agent = await TOOL_SERVICE.add_tool_servers_to_agent( + chat_client=CHAT_CLIENT, + agent_instructions=AGENT_INSTRUCTIONS, + initial_tools=WEATHER_TOOLS, + auth=AUTHORIZATION, + auth_handler_name=WORKIQ_AUTH_HANDLER, + turn_context=context, + auth_token=bearer_token, + ) + _workiq_agent = agent or WEATHER_AGENT + logger.info("WorkIQ MCP tools attached to the agent.") + except Exception as e: + if environ.get("SKIP_TOOLING_ON_ERRORS", "true").lower() == "true": + logger.error("WorkIQ MCP setup failed - running weather-only: %s", e) + _workiq_agent = WEATHER_AGENT + else: + raise + + return _workiq_agent + WELCOME_MESSAGE = ( "Hello! I'm your friendly weather cat assistant. 🐱 " "I can help you find the current weather or a weather forecast for any city. " @@ -85,12 +173,14 @@ async def on_message(context: TurnContext, state: TurnState): session_data = None try: + agent = await get_agent(context) + session_data = state.get_value("ConversationState.agentSession", lambda: None) if session_data is None: - session_data = WEATHER_AGENT.create_session() + session_data = agent.create_session() - async for chunk in WEATHER_AGENT.run(user_text, session=session_data, stream=True): + async for chunk in agent.run(user_text, session=session_data, stream=True): if chunk.text: context.streaming_response.queue_text_chunk(chunk.text) From 645734ff2010da15cb7c4e26d5b5410e5977a980 Mon Sep 17 00:00:00 2001 From: Vincent Chen Date: Sun, 5 Jul 2026 21:56:55 -0700 Subject: [PATCH 2/2] Clarify WorkIQ agent setup comments and update welcome message --- samples/python/agent-framework/src/agent.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/samples/python/agent-framework/src/agent.py b/samples/python/agent-framework/src/agent.py index 22b6abfa..41cdd7ac 100644 --- a/samples/python/agent-framework/src/agent.py +++ b/samples/python/agent-framework/src/agent.py @@ -98,16 +98,20 @@ async def get_agent(context: TurnContext): """ global _workiq_agent, _workiq_setup_done + # Already built the WorkIQ-enabled agent -> reuse it (built once, cached). if _workiq_agent is not None: return _workiq_agent + # Setup already attempted (and failed), or the package isn't installed -> weather-only. if _workiq_setup_done or TOOL_SERVICE is None: return WEATHER_AGENT + # Only try setup once; later messages take the branches above. _workiq_setup_done = True use_agentic_auth = environ.get("USE_AGENTIC_AUTH", "false").lower() == "true" bearer_token = environ.get("BEARER_TOKEN", "") + # No auth configured -> nothing to call WorkIQ with, stay weather-only. if not use_agentic_auth and not bearer_token and not WORKIQ_AUTH_HANDLER: logger.info( "WorkIQ not configured (no BEARER_TOKEN, USE_AGENTIC_AUTH, or " @@ -140,6 +144,7 @@ async def get_agent(context: TurnContext): _workiq_agent = agent or WEATHER_AGENT logger.info("WorkIQ MCP tools attached to the agent.") except Exception as e: + # If setup fails, keep serving weather (default) instead of crashing. if environ.get("SKIP_TOOLING_ON_ERRORS", "true").lower() == "true": logger.error("WorkIQ MCP setup failed - running weather-only: %s", e) _workiq_agent = WEATHER_AGENT @@ -149,9 +154,10 @@ async def get_agent(context: TurnContext): return _workiq_agent WELCOME_MESSAGE = ( - "Hello! I'm your friendly weather cat assistant. 🐱 " - "I can help you find the current weather or a weather forecast for any city. " - "Just tell me the city name and, if you're in the US, the 2-letter state code. Meow!" + "Hello! I'm your friendly purr-ductivity cat assistant. 🐱 " + "I can fetch the current weather or forecast for any US city, " + "and I can help with your Microsoft 365 work too — like Teams chats, mail, and files. " + "Ask me a weather question (city + 2-letter state) or anything about your workday. Meow!" )