Skip to content

Commit c57c347

Browse files
authored
Add MCP Client Plugin (#123)
Adds support for MCP clients in an MCP plugin. Makes use of the AI plugin system introduced in #121 . - Added new MCP package (packages/mcp/) with AI plugin and transport functionality - Updated chat prompt handling for MCP integration - Changed token from being parsed into JWT tokens because that's not necessarily true for many types of tokens. Github for eg, doesn't use JWT. This is roughly how the system works: ```mermaid sequenceDiagram participant U as User participant CP as ChatPrompt participant P as MCP Plugin participant MCP as MCP Server participant M as AI Model U->>CP: send(input) CP->>P: on_build_functions(functions) P->>MCP: get_available_tools() MCP-->>P: tool_definitions P-->>CP: functions + mcp_tools CP->>M: generate_text(input, system, functions) alt Model calls MCP tool M-->>CP: function_call(mcp_tool, params) CP-->>M: function_result end M-->>CP: response CP-->>U: ChatSendResult ``` #### PR Dependency Tree * **PR #123** 👈 * **PR #126** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
1 parent 82570ea commit c57c347

File tree

28 files changed

+1458
-56
lines changed

28 files changed

+1458
-56
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ CLAUDE.md
3232

3333
.env.claude/
3434
.claude/
35+
36+
tests/**/.vscode/
37+
tests/**/appPackage/
38+
tests/**/infra/
39+
tests/**/teamsapp*

packages/ai/src/microsoft/teams/ai/agent.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from typing import Any, Awaitable, Callable
77

8+
from microsoft.teams.ai.plugin import AIPluginProtocol
9+
810
from .ai_model import AIModel
911
from .chat_prompt import ChatPrompt, ChatSendResult
1012
from .function import Function
@@ -18,8 +20,15 @@ class Agent(ChatPrompt):
1820
through the existence of the Agent.
1921
"""
2022

21-
def __init__(self, model: AIModel, *, memory: Memory | None = None, functions: list[Function[Any]] | None = None):
22-
super().__init__(model, functions=functions)
23+
def __init__(
24+
self,
25+
model: AIModel,
26+
*,
27+
memory: Memory | None = None,
28+
functions: list[Function[Any]] | None = None,
29+
plugins: list[AIPluginProtocol] | None = None,
30+
):
31+
super().__init__(model, functions=functions, plugins=plugins)
2332
self.memory = memory or ListMemory()
2433

2534
async def send(

packages/ai/src/microsoft/teams/ai/chat_prompt.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import inspect
77
from dataclasses import dataclass
88
from inspect import isawaitable
9-
from typing import Any, Awaitable, Callable, TypeVar
9+
from typing import Any, Awaitable, Callable, Self, TypeVar
1010

1111
from pydantic import BaseModel
1212

@@ -36,11 +36,11 @@ def __init__(
3636
self.functions: dict[str, Function[Any]] = {func.name: func for func in functions} if functions else {}
3737
self.plugins: list[AIPluginProtocol] = plugins or []
3838

39-
def with_function(self, function: Function[T]) -> "ChatPrompt":
39+
def with_function(self, function: Function[T]) -> Self:
4040
self.functions[function.name] = function
4141
return self
4242

43-
def with_plugin(self, plugin: AIPluginProtocol) -> "ChatPrompt":
43+
def with_plugin(self, plugin: AIPluginProtocol) -> Self:
4444
"""Add a plugin to the chat prompt."""
4545
self.plugins.append(plugin)
4646
return self
@@ -122,25 +122,21 @@ async def _run_build_system_message_hooks(self, system_message: SystemMessage |
122122
return current_system_message
123123

124124
async def _build_wrapped_functions(self) -> dict[str, Function[BaseModel]] | None:
125-
wrapped_functions: dict[str, Function[BaseModel]] | None = None
126-
if self.functions:
127-
wrapped_functions = {}
128-
for name, func in self.functions.items():
129-
wrapped_functions[name] = Function[BaseModel](
130-
name=func.name,
131-
description=func.description,
132-
parameter_schema=func.parameter_schema,
133-
handler=self._wrap_function_handler(func.handler, name),
134-
)
135-
136-
if wrapped_functions:
137-
functions_list = list(wrapped_functions.values())
138-
for plugin in self.plugins:
139-
plugin_result = await plugin.on_build_functions(functions_list)
140-
if plugin_result is not None:
141-
functions_list = plugin_result
125+
functions_list = list(self.functions.values()) if self.functions else []
126+
for plugin in self.plugins:
127+
plugin_result = await plugin.on_build_functions(functions_list)
128+
if plugin_result is not None:
129+
functions_list = plugin_result
142130

143-
wrapped_functions = {func.name: func for func in functions_list}
131+
wrapped_functions: dict[str, Function[BaseModel]] | None = None
132+
wrapped_functions = {}
133+
for func in functions_list:
134+
wrapped_functions[func.name] = Function[BaseModel](
135+
name=func.name,
136+
description=func.description,
137+
parameter_schema=func.parameter_schema,
138+
handler=self._wrap_function_handler(func.handler, func.name),
139+
)
144140

145141
return wrapped_functions
146142

packages/ai/src/microsoft/teams/ai/function.py

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

66
from dataclasses import dataclass
7-
from typing import Any, Awaitable, Generic, Protocol, TypeVar, Union
7+
from typing import Any, Awaitable, Dict, Generic, Protocol, TypeVar, Union
88

99
from pydantic import BaseModel
1010

@@ -19,7 +19,7 @@ def __call__(self, params: Params) -> Union[str, Awaitable[str]]: ...
1919
class Function(Generic[Params]):
2020
name: str
2121
description: str
22-
parameter_schema: type[Params]
22+
parameter_schema: Union[type[Params], Dict[str, Any]]
2323
handler: FunctionHandler[Params]
2424

2525

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
ConversationReference,
1414
GetUserTokenParams,
1515
InvokeResponse,
16-
JsonWebToken,
1716
SentActivity,
1817
TokenProtocol,
1918
is_invoke_response,
@@ -97,16 +96,16 @@ async def _build_context(
9796

9897
# Check if user is signed in
9998
is_signed_in = False
100-
user_token: Optional[TokenProtocol] = None
99+
user_token: Optional[str] = None
101100
try:
102101
user_token_res = await api_client.users.token.get(
103102
GetUserTokenParams(
104-
connection_name=self.default_connection_name or "default",
103+
connection_name=self.default_connection_name,
105104
user_id=activity.from_.id,
106105
channel_id=activity.channel_id,
107106
)
108107
)
109-
user_token = JsonWebToken(user_token_res.token)
108+
user_token = user_token_res.token
110109
is_signed_in = True
111110
except Exception:
112111
# User token not available

packages/apps/src/microsoft/teams/apps/routing/activity_context.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
CreateConversationParams,
2020
GetBotSignInResourceParams,
2121
GetUserTokenParams,
22+
JsonWebToken,
2223
MessageActivityInput,
2324
SentActivity,
2425
SignOutUserParams,
@@ -65,6 +66,7 @@ class SignInOptions:
6566

6667
oauth_card_text: str = "Please Sign In..."
6768
sign_in_button_text: str = "Sign In"
69+
connection_name: Optional[str] = None
6870
override_sign_in_activity: Optional[
6971
Callable[
7072
[
@@ -90,7 +92,7 @@ def __init__(
9092
logger: Logger,
9193
storage: Storage[str, Any],
9294
api: ApiClient,
93-
user_token: Optional[TokenProtocol],
95+
user_token: Optional[str],
9496
conversation_ref: ConversationReference,
9597
is_signed_in: bool,
9698
connection_name: str,
@@ -135,7 +137,8 @@ def user_graph(self) -> "GraphServiceClient":
135137

136138
if self._user_graph is None:
137139
try:
138-
self._user_graph = _get_graph_client(self.user_token)
140+
user_token = JsonWebToken(self.user_token)
141+
self._user_graph = _get_graph_client(user_token)
139142
except Exception as e:
140143
self.logger.error(f"Failed to create user graph client: {e}")
141144
raise RuntimeError(f"Failed to create user graph client: {e}") from e
@@ -247,13 +250,13 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str
247250
signin_opts = options or DEFAULT_SIGNIN_OPTIONS
248251
oauth_card_text = signin_opts.oauth_card_text
249252
sign_in_button_text = signin_opts.sign_in_button_text
250-
253+
connection_name = signin_opts.connection_name or self.connection_name
251254
try:
252255
# Try to get existing token
253256
token_params = GetUserTokenParams(
254257
channel_id=self.activity.channel_id,
255258
user_id=self.activity.from_.id,
256-
connection_name=self.connection_name,
259+
connection_name=connection_name,
257260
)
258261
res = await self.api.users.token.get(token_params)
259262
return res.token
@@ -263,7 +266,7 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str
263266

264267
# Create token exchange state
265268
token_exchange_state = TokenExchangeState(
266-
connection_name=self.connection_name,
269+
connection_name=connection_name,
267270
conversation=self.conversation_ref,
268271
relates_to=self.activity.relates_to,
269272
ms_app_id=self.app_id,
@@ -297,7 +300,7 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str
297300
attachment=OAuthCardAttachment(
298301
content=OAuthCard(
299302
text=oauth_card_text,
300-
connection_name=self.connection_name,
303+
connection_name=connection_name,
301304
token_exchange_resource=resource.token_exchange_resource,
302305
token_post_resource=resource.token_post_resource,
303306
buttons=[

packages/mcp/README.md

Whitespace-only changes.

packages/mcp/pyproject.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[project]
2+
name = "microsoft.teams.mcp"
3+
version = "0.0.1-alpha.4"
4+
description = "library for handling mcp with teams ai library"
5+
authors = [{ name = "Microsoft", email = "[email protected]" }]
6+
readme = "README.md"
7+
requires-python = ">=3.12"
8+
repository = "https://github.com/microsoft/teams.py"
9+
keywords = ["microsoft", "teams", "ai", "bot", "agents"]
10+
license = "MIT"
11+
dependencies = [
12+
"mcp>=1.13.1",
13+
"microsoft-teams-ai",
14+
"microsoft-teams-common",
15+
]
16+
17+
[tool.microsoft-teams.metadata]
18+
external = true
19+
20+
[build-system]
21+
requires = ["hatchling"]
22+
build-backend = "hatchling.build"
23+
24+
[tool.hatch.build.targets.wheel]
25+
packages = ["src/microsoft"]
26+
27+
[tool.hatch.build.targets.sdist]
28+
include = ["src"]
29+
30+
[tool.uv.sources]
31+
microsoft-teams-ai = { workspace = true }
32+
microsoft-teams-common = { workspace = true }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
from .ai_plugin import McpClientPlugin, McpClientPluginParams, McpToolDetails
7+
8+
__all__ = ["McpClientPlugin", "McpClientPluginParams", "McpToolDetails"]

0 commit comments

Comments
 (0)