Skip to content

Commit 4e721cd

Browse files
authored
MCP test updates and feedback loop updates (#153)
Adds cleaner mcp server and mcp client tests. Adds feedback loop tests. Fixes a bug that feedback loop discovered #### PR Dependency Tree * **PR #153** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
1 parent 8b6f39f commit 4e721cd

File tree

6 files changed

+253
-22
lines changed

6 files changed

+253
-22
lines changed

packages/apps/src/microsoft/teams/apps/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,9 @@ def router(self) -> ActivityRouter:
181181
@property
182182
def id(self) -> Optional[str]:
183183
"""The app's ID from tokens."""
184-
return getattr(self._tokens.bot, "app_id", None) or getattr(self._tokens.graph, "app_id", None)
184+
return (
185+
self._tokens.bot.app_id if self._tokens.bot else self._tokens.graph.app_id if self._tokens.graph else None
186+
)
185187

186188
@property
187189
def name(self) -> Optional[str]:

tests/ai-test/src/handlers/feedback_management.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ async def handle_feedback_submission(ctx: ActivityContext[MessageSubmitActionInv
4949
return
5050

5151
# Type-safe access to activity value
52-
value_dict = activity.value.model_dump() if hasattr(activity.value, "model_dump") else {}
53-
action_value: Dict[str, Any] = value_dict.get("actionValue", {})
54-
reaction: str | None = action_value.get("reaction")
55-
feedback_str: str | None = action_value.get("feedback")
56-
assert feedback_str, "No feedback string found in action_value"
52+
invoke_value = activity.value
53+
assert invoke_value.action_name == "feedback"
54+
feedback_str = invoke_value.action_value.feedback
55+
reaction = invoke_value.action_value.reaction
5756
feedback_json: Dict[str, Any] = json.loads(feedback_str)
57+
# { 'feedbackText': 'the ai response was great!' }
5858

5959
if not activity.reply_to_id:
6060
logger.warning(f"No replyToId found for messageId {activity.id}")

tests/mcp-client/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Sample: MCP Client
2+
3+
4+
### Available Commands
5+
6+
| Command | Description | Example Usage |
7+
|---------|-------------|---------------|
8+
| `agent <query>` | Use stateful Agent with MCP tools | `agent What's the weather like?` |
9+
| `prompt <query>` | Use stateless ChatPrompt with MCP tools | `prompt Find information about Python` |
10+
| `mcp info` | Show connected MCP servers and usage | `mcp info` |
11+
| `<any message>` | Fallback to Agent with MCP tools | `Hello, can you help me?` |
12+

tests/mcp-client/src/main.py

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,162 @@
44
"""
55

66
import asyncio
7+
import re
8+
from os import getenv
79

8-
from microsoft.teams.ai import Agent, ListMemory
9-
from microsoft.teams.api import MessageActivity, TypingActivityInput
10+
from dotenv import find_dotenv, load_dotenv
11+
from microsoft.teams.ai import Agent, ChatPrompt, ListMemory
12+
from microsoft.teams.api import MessageActivity, MessageActivityInput, TypingActivityInput
1013
from microsoft.teams.apps import ActivityContext, App
1114
from microsoft.teams.devtools import DevToolsPlugin
12-
from microsoft.teams.mcpplugin import McpClientPlugin
13-
from microsoft.teams.openai import OpenAIResponsesAIModel
15+
from microsoft.teams.mcpplugin import McpClientPlugin, McpClientPluginParams
16+
from microsoft.teams.openai import OpenAICompletionsAIModel, OpenAIResponsesAIModel
17+
18+
load_dotenv(find_dotenv(usecwd=True))
1419

1520
app = App(plugins=[DevToolsPlugin()])
1621

17-
responses_openai_ai_model = OpenAIResponsesAIModel(stateful=True)
18-
chat_memory = ListMemory()
22+
23+
def get_required_env(key: str) -> str:
24+
value = getenv(key)
25+
if not value:
26+
raise ValueError(f"Required environment variable {key} is not set")
27+
return value
28+
29+
30+
AZURE_OPENAI_MODEL = get_required_env("AZURE_OPENAI_MODEL")
31+
32+
33+
# GitHub PAT for MCP server (optional)
34+
def get_optional_env(key: str) -> str | None:
35+
return getenv(key)
36+
37+
38+
# This example uses a PersonalAccessToken, but you may get
39+
# the user's oauth token as well by getting them to sign in
40+
# and then using app.sign_in to get their token.
41+
GITHUB_PAT = get_optional_env("GITHUB_PAT")
42+
43+
# Set up AI models
44+
completions_model = OpenAICompletionsAIModel(model=AZURE_OPENAI_MODEL)
45+
responses_model = OpenAIResponsesAIModel(model=AZURE_OPENAI_MODEL, stateful=True)
46+
47+
# Configure MCP Client Plugin with multiple remote servers (as shown in docs)
1948
mcp_plugin = McpClientPlugin()
49+
50+
# Add multiple MCP servers to demonstrate the concept from documentation
2051
mcp_plugin.use_mcp_server("https://learn.microsoft.com/api/mcp")
2152

22-
responses_agent = Agent(responses_openai_ai_model, memory=chat_memory, plugins=[mcp_plugin])
53+
# Add GitHub MCP server with authentication headers (demonstrates header functionality)
54+
if GITHUB_PAT:
55+
mcp_plugin.use_mcp_server(
56+
"https://api.githubcopilot.com/mcp/", McpClientPluginParams(headers={"Authorization": f"Bearer {GITHUB_PAT}"})
57+
)
58+
print("✅ GitHub MCP server configured with authentication")
59+
else:
60+
print("⚠️ GITHUB_PAT not found - GitHub MCP server not configured")
61+
print(" Set GITHUB_PAT environment variable to enable GitHub MCP integration")
62+
# Example of additional servers (commented out - would need actual working endpoints):
63+
# mcp_plugin.use_mcp_server("https://example.com/mcp/weather")
64+
# mcp_plugin.use_mcp_server("https://example.com/mcp/pokemon")
65+
66+
# Memory for stateful conversations
67+
chat_memory = ListMemory()
68+
69+
# Agent using Responses API with MCP tools
70+
responses_agent = Agent(responses_model, memory=chat_memory, plugins=[mcp_plugin])
71+
72+
# ChatPrompt with MCP tools (demonstrating docs example)
73+
chat_prompt = ChatPrompt(completions_model, plugins=[mcp_plugin])
74+
75+
76+
# Pattern-based handlers to demonstrate different MCP usage patterns
77+
78+
79+
@app.on_message_pattern(re.compile(r"^agent\s+(.+)", re.IGNORECASE))
80+
async def handle_agent_chat(ctx: ActivityContext[MessageActivity]):
81+
"""Handle 'agent <query>' command using Agent with MCP tools (stateful)"""
82+
match = re.match(r"^agent\s+(.+)", ctx.activity.text, re.IGNORECASE)
83+
if match:
84+
query = match.group(1).strip()
85+
86+
print(f"[AGENT] Processing: {query}")
87+
await ctx.send(TypingActivityInput())
88+
89+
# Use Agent with MCP tools (stateful conversation)
90+
result = await responses_agent.send(query)
91+
if result.response.content:
92+
message = MessageActivityInput(text=result.response.content).add_ai_generated()
93+
await ctx.send(message)
94+
95+
96+
@app.on_message_pattern(re.compile(r"^prompt\s+(.+)", re.IGNORECASE))
97+
async def handle_prompt_chat(ctx: ActivityContext[MessageActivity]):
98+
"""Handle 'prompt <query>' command using ChatPrompt with MCP tools (stateless)"""
99+
match = re.match(r"^prompt\s+(.+)", ctx.activity.text, re.IGNORECASE)
100+
if match:
101+
query = match.group(1).strip()
102+
103+
print(f"[PROMPT] Processing: {query}")
104+
await ctx.send(TypingActivityInput())
105+
106+
# Use ChatPrompt with MCP tools (demonstrates docs pattern)
107+
result = await chat_prompt.send(
108+
input=query,
109+
instructions=(
110+
"You are a helpful assistant with access to remote MCP tools.Use them to help answer questions."
111+
),
112+
)
113+
114+
if result.response.content:
115+
message = MessageActivityInput(text=result.response.content).add_ai_generated()
116+
await ctx.send(message)
117+
118+
119+
@app.on_message_pattern(re.compile(r"^mcp\s+info", re.IGNORECASE))
120+
async def handle_mcp_info(ctx: ActivityContext[MessageActivity]):
121+
"""Handle 'mcp info' command to show available MCP servers and tools"""
122+
# Build server list dynamically based on what's configured
123+
servers_info = "**Connected MCP Servers:**\n"
124+
servers_info += "• `https://learn.microsoft.com/api/mcp` - Microsoft Learn API\n"
125+
126+
if GITHUB_PAT:
127+
servers_info += "• `https://api.githubcopilot.com/mcp/` - GitHub Copilot API (authenticated)\n"
128+
else:
129+
servers_info += "• GitHub MCP server (not configured - set GITHUB_PAT env var)\n"
130+
131+
info_text = (
132+
"🔗 **MCP Client Information**\n\n"
133+
f"{servers_info}\n"
134+
"**Authentication Demo:**\n"
135+
"• GitHub server uses Bearer token authentication via headers\n"
136+
"• Example: `headers={'Authorization': f'Bearer {GITHUB_PAT}'}`\n\n"
137+
"**Usage Patterns:**\n"
138+
"• `agent <query>` - Use stateful Agent with MCP tools\n"
139+
"• `prompt <query>` - Use stateless ChatPrompt with MCP tools\n"
140+
"• `mcp info` - Show this information\n\n"
141+
"**How it works:**\n"
142+
"1. MCP Client connects to remote servers via SSE protocol\n"
143+
"2. Headers (like Authorization) are passed with each request\n"
144+
"3. Remote tools are loaded and integrated with ChatPrompt/Agent\n"
145+
"4. LLM can call remote tools as needed to answer your questions"
146+
)
147+
await ctx.reply(info_text)
23148

24149

150+
# Fallback handler for general chat (uses Agent by default)
25151
@app.on_message
26-
async def handle_message(ctx: ActivityContext[MessageActivity]):
27-
"""Handle message activities using the new generated handler system."""
28-
print(f"[GENERATED onMessage] Message received: {ctx.activity.text}")
29-
print(f"[GENERATED onMessage] From: {ctx.activity.from_}")
152+
async def handle_fallback_message(ctx: ActivityContext[MessageActivity]):
153+
"""Fallback handler using Agent with MCP tools"""
154+
print(f"[FALLBACK] Message received: {ctx.activity.text}")
155+
print(f"[FALLBACK] From: {ctx.activity.from_}")
30156
await ctx.send(TypingActivityInput())
31157

158+
# Use Agent with MCP tools for general conversation
32159
result = await responses_agent.send(ctx.activity.text)
33160
if result.response.content:
34-
await ctx.reply(result.response.content)
161+
message = MessageActivityInput(text=result.response.content).add_ai_generated()
162+
await ctx.send(message)
35163

36164

37165
if __name__ == "__main__":

tests/mcp-server/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Sample: MCP Server
2+
3+
### Available Tools
4+
5+
| Tool | Description | Parameters | Example Usage |
6+
|------|-------------|------------|---------------|
7+
| `echo` | Echo back input text | `input: str` | Echo functionality from docs |
8+
| `get_weather` | Get weather for a location | `location: str` | Always returns "sunny" |
9+
| `calculate` | Basic arithmetic operations | `operation: str, a: float, b: float` | add, subtract, multiply, divide |
10+
| `alert` | Send proactive message to Teams user | `user_id: str, message: str` | Human-in-the-loop notifications |
11+

tests/mcp-server/src/main.py

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import asyncio
7+
from typing import Dict
78

89
from microsoft.teams.ai import Function
910
from microsoft.teams.api.activities.message.message import MessageActivity
@@ -13,9 +14,25 @@
1314
from microsoft.teams.mcpplugin import McpServerPlugin
1415
from pydantic import BaseModel
1516

16-
mcp_server_plugin = McpServerPlugin()
17+
# Configure MCP server with custom name (as shown in docs)
18+
mcp_server_plugin = McpServerPlugin(
19+
name="test-mcp",
20+
)
21+
22+
# Storage for conversation IDs (for proactive messaging)
23+
conversation_storage: Dict[str, str] = {}
24+
25+
26+
# Echo tool from documentation example
27+
class EchoParams(BaseModel):
28+
input: str
29+
30+
31+
async def echo_handler(params: EchoParams) -> str:
32+
return f"You said {params.input}"
1733

1834

35+
# Weather tool (existing)
1936
class GetWeatherParams(BaseModel):
2037
location: str
2138

@@ -44,7 +61,42 @@ async def calculate_handler(params: CalculateParams) -> str:
4461
return "Unknown operation"
4562

4663

47-
# Direct function call usage
64+
# Alert tool for proactive messaging (as mentioned in docs)
65+
class AlertParams(BaseModel):
66+
user_id: str
67+
message: str
68+
69+
70+
async def alert_handler(params: AlertParams) -> str:
71+
"""
72+
Send proactive message to user via Teams.
73+
This demonstrates the "piping messages to user" feature from docs.
74+
"""
75+
# 1. Validate if the incoming request is allowed to send messages
76+
if not params.user_id or not params.message:
77+
return "Invalid parameters: user_id and message are required"
78+
79+
# 2. Fetch the correct conversation ID for the given user
80+
conversation_id = conversation_storage.get(params.user_id)
81+
if not conversation_id:
82+
return f"No conversation found for user {params.user_id}. User needs to message the bot first."
83+
84+
# 3. Send proactive message (simplified - in real implementation would use proper proactive messaging)
85+
await app.send(conversation_id=conversation_id, activity=params.message)
86+
return f"Alert sent to user {params.user_id}: {params.message} (conversation: {conversation_id})"
87+
88+
89+
# Register echo tool (from documentation)
90+
mcp_server_plugin.use_tool(
91+
Function(
92+
name="echo",
93+
description="echo back whatever you said",
94+
parameter_schema=EchoParams,
95+
handler=echo_handler,
96+
)
97+
)
98+
99+
# Register weather tool
48100
mcp_server_plugin.use_tool(
49101
Function(
50102
name="get_weather",
@@ -54,7 +106,7 @@ async def calculate_handler(params: CalculateParams) -> str:
54106
)
55107
)
56108

57-
# Second tool registration
109+
# Register calculator tool
58110
mcp_server_plugin.use_tool(
59111
Function(
60112
name="calculate",
@@ -64,12 +116,38 @@ async def calculate_handler(params: CalculateParams) -> str:
64116
)
65117
)
66118

119+
# Register alert tool for proactive messaging
120+
mcp_server_plugin.use_tool(
121+
Function(
122+
name="alert",
123+
description="Send proactive message to a Teams user",
124+
parameter_schema=AlertParams,
125+
handler=alert_handler,
126+
)
127+
)
128+
67129
app = App(plugins=[mcp_server_plugin, DevToolsPlugin()])
68130

69131

70132
@app.on_message
71133
async def handle_message(ctx: ActivityContext[MessageActivity]):
72-
await ctx.reply(f"You said {ctx.activity.text}")
134+
"""
135+
Handle incoming messages and store conversation IDs for proactive messaging.
136+
This demonstrates the conversation ID storage mentioned in the docs.
137+
"""
138+
# Store conversation ID for this user (for proactive messaging)
139+
user_id = ctx.activity.from_.id
140+
conversation_id = ctx.activity.conversation.id
141+
conversation_storage[user_id] = conversation_id
142+
143+
print(f"User {ctx.activity.from_} just sent a message!")
144+
145+
# Echo back the message with info about stored conversation
146+
await ctx.reply(
147+
f"You said: {ctx.activity.text}\n\n"
148+
f"📝 Stored conversation ID `{conversation_id}` for user `{user_id}` "
149+
f"(for proactive messaging via MCP alert tool)"
150+
)
73151

74152

75153
if __name__ == "__main__":

0 commit comments

Comments
 (0)