From d723358009b72a8eef8bf26bf7c1e77cc64f2ce0 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 11 Apr 2025 11:32:24 +0200 Subject: [PATCH 1/7] add support for prompts and sample --- .../sk_mcp_server/mcp_server_with_prompts.py | 81 +++++++++++++++++++ python/semantic_kernel/connectors/mcp.py | 56 +++++++++++++ python/semantic_kernel/kernel.py | 6 +- 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py diff --git a/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py new file mode 100644 index 000000000000..9d215911b077 --- /dev/null +++ b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py @@ -0,0 +1,81 @@ +# /// script # noqa: CPY001 +# dependencies = [ +# "semantic-kernel[mcp]", +# ] +# /// +# Copyright (c) Microsoft. All rights reserved. +import logging +from typing import Any + +import anyio +from mcp.server.stdio import stdio_server + +from semantic_kernel import Kernel +from semantic_kernel.prompt_template.input_variable import InputVariable +from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate +from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig + +logger = logging.getLogger(__name__) + +""" +This sample demonstrates how to expose your Semantic Kernel instance as a MCP server. + +To run this sample, set up your MCP host (like Claude Desktop or VSCode Github Copilot Agents) +with the following configuration: +```json +{ + "mcpServers": { + "sk_release_notes": { + "command": "uv", + "args": [ + "--directory=/semantic-kernel/python/samples/demos/mcp_server", + "run", + "mcp_server_with_prompts.py" + ], + } + } +} +``` +""" + + +def run() -> None: + """Run the MCP server with the release notes prompt template.""" + kernel = Kernel() + prompt = KernelPromptTemplate( + prompt_template_config=PromptTemplateConfig( + name="release_notes", + description="This creates release notes from a list of PRs messages.", + template="""{{$messages}} +--- +Group the following PRs into one of these buckets for release notes, keeping the same order: + +-New Features +-Enhancements and Improvements +-Bug Fixes +-Python Package Updates + +Include the output in raw markdown. +""", + input_variables=[ + InputVariable( + name="messages", + description="These are the PR messages, they are a single string with new lines.", + is_required=True, + json_schema='{ "type": "string"}', + ) + ], + ) + ) + + server = kernel.as_mcp_server(server_name="sk_release_notes", prompts=[prompt]) + + async def handle_stdin(stdin: Any | None = None, stdout: Any | None = None) -> None: + async with stdio_server() as (read_stream, write_stream): + await server.run(read_stream, write_stream, server.create_initialization_options()) + + anyio.run(handle_stdin) + + +if __name__ == "__main__": + run() diff --git a/python/semantic_kernel/connectors/mcp.py b/python/semantic_kernel/connectors/mcp.py index 14d60eee285e..6c5f7e8fe5ff 100644 --- a/python/semantic_kernel/connectors/mcp.py +++ b/python/semantic_kernel/connectors/mcp.py @@ -29,8 +29,10 @@ from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.exceptions import FunctionExecutionException, KernelPluginInvalidConfigurationError from semantic_kernel.functions.function_result import FunctionResult +from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.kernel_types import OptionalOneOrMany +from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase from semantic_kernel.utils.feature_stage_decorator import experimental if sys.version_info >= (3, 11): @@ -565,6 +567,7 @@ def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: @experimental def create_mcp_server_from_kernel( kernel: Kernel, + prompts: list[PromptTemplateBase] | None = None, *, server_name: str = "SK", version: str | None = None, @@ -584,6 +587,7 @@ def create_mcp_server_from_kernel( Args: kernel: The kernel instance to use. + prompts: The list of prompts to expose as prompts. server_name: The name of the server. version: The version of the server. instructions: The instructions to use for the server. @@ -612,6 +616,58 @@ def create_mcp_server_from_kernel( server: Server["LifespanResultT"] = Server(**server_args) # type: ignore[call-arg] + if prompts: + + @server.list_prompts() + async def _list_prompts() -> list[types.Prompt]: + """List all prompts in the kernel.""" + mcp_prompts = [] + for prompt in prompts: + mcp_prompts.append( + types.Prompt( + name=prompt.prompt_template_config.name, + description=prompt.prompt_template_config.description, + arguments=[ + types.PromptArgument( + name=var.name, + description=var.description, + required=var.is_required, + ) + for var in prompt.prompt_template_config.input_variables + ], + ) + ) + await _log(level="debug", data=f"List of prompts: {mcp_prompts}") + return mcp_prompts + + @server.get_prompt() + async def _get_prompt(name: str, arguments: dict[str, Any] | None) -> types.GetPromptResult: + """Get a prompt by name.""" + prompt = next((p for p in prompts if p.prompt_template_config.name == name), None) + if prompt is None: + return types.GetPromptResult(description="Prompt not found", messages=[]) + + # Call the prompt + rendered_prompt = await prompt.render( + kernel, + KernelArguments(**arguments) if arguments is not None else KernelArguments(), + ) + # since the return type of a get_prompts is a list of messages, + # we need to convert the rendered prompt to a list of messages + # by using the ChatHistory class + chat_history = ChatHistory.from_rendered_prompt(rendered_prompt) + messages = [] + for message in chat_history.messages: + messages.append( + types.PromptMessage( + role=message.role.value + if message.role in (AuthorRole.ASSISTANT, AuthorRole.USER) + else "assistant", + content=_kernel_content_to_mcp_content_types(message)[0], + ) + ) + return types.GetPromptResult(messages=messages) + @server.list_tools() async def _list_tools() -> list[types.Tool]: """List all tools in the kernel.""" diff --git a/python/semantic_kernel/kernel.py b/python/semantic_kernel/kernel.py index 507889d514fa..ae8b3b34d594 100644 --- a/python/semantic_kernel/kernel.py +++ b/python/semantic_kernel/kernel.py @@ -37,6 +37,7 @@ from semantic_kernel.functions.kernel_plugin import KernelPlugin from semantic_kernel.kernel_types import AI_SERVICE_CLIENT_TYPE, OneOrMany, OptionalOneOrMany from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME +from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase from semantic_kernel.reliability.kernel_reliability_extension import KernelReliabilityExtension from semantic_kernel.services.ai_service_selector import AIServiceSelector from semantic_kernel.services.kernel_services_extension import KernelServicesExtension @@ -490,6 +491,7 @@ async def add_embedding_to_object( @experimental def as_mcp_server( self, + prompts: list[PromptTemplateBase] | None = None, server_name: str = "Semantic Kernel MCP Server", version: str | None = None, instructions: str | None = None, @@ -508,6 +510,7 @@ def as_mcp_server( Args: kernel: The kernel instance to use. + prompts: A list of prompt templates to expose as prompts. server_name: The name of the server. version: The version of the server. instructions: The instructions to use for the server. @@ -524,7 +527,8 @@ def as_mcp_server( from semantic_kernel.connectors.mcp import create_mcp_server_from_kernel return create_mcp_server_from_kernel( - self, + kernel=self, + prompts=prompts, server_name=server_name, version=version, instructions=instructions, From c18f9a34aeff17f7dbd7c057ad41906a794c23ee Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 11 Apr 2025 11:34:19 +0200 Subject: [PATCH 2/7] change ordering --- python/semantic_kernel/connectors/mcp.py | 104 +++++++++++------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/python/semantic_kernel/connectors/mcp.py b/python/semantic_kernel/connectors/mcp.py index 6c5f7e8fe5ff..a0b715526b75 100644 --- a/python/semantic_kernel/connectors/mcp.py +++ b/python/semantic_kernel/connectors/mcp.py @@ -616,58 +616,6 @@ def create_mcp_server_from_kernel( server: Server["LifespanResultT"] = Server(**server_args) # type: ignore[call-arg] - if prompts: - - @server.list_prompts() - async def _list_prompts() -> list[types.Prompt]: - """List all prompts in the kernel.""" - mcp_prompts = [] - for prompt in prompts: - mcp_prompts.append( - types.Prompt( - name=prompt.prompt_template_config.name, - description=prompt.prompt_template_config.description, - arguments=[ - types.PromptArgument( - name=var.name, - description=var.description, - required=var.is_required, - ) - for var in prompt.prompt_template_config.input_variables - ], - ) - ) - await _log(level="debug", data=f"List of prompts: {mcp_prompts}") - return mcp_prompts - - @server.get_prompt() - async def _get_prompt(name: str, arguments: dict[str, Any] | None) -> types.GetPromptResult: - """Get a prompt by name.""" - prompt = next((p for p in prompts if p.prompt_template_config.name == name), None) - if prompt is None: - return types.GetPromptResult(description="Prompt not found", messages=[]) - - # Call the prompt - rendered_prompt = await prompt.render( - kernel, - KernelArguments(**arguments) if arguments is not None else KernelArguments(), - ) - # since the return type of a get_prompts is a list of messages, - # we need to convert the rendered prompt to a list of messages - # by using the ChatHistory class - chat_history = ChatHistory.from_rendered_prompt(rendered_prompt) - messages = [] - for message in chat_history.messages: - messages.append( - types.PromptMessage( - role=message.role.value - if message.role in (AuthorRole.ASSISTANT, AuthorRole.USER) - else "assistant", - content=_kernel_content_to_mcp_content_types(message)[0], - ) - ) - return types.GetPromptResult(messages=messages) - @server.list_tools() async def _list_tools() -> list[types.Tool]: """List all tools in the kernel.""" @@ -726,6 +674,58 @@ async def _call_tool(*args: Any) -> Sequence[types.TextContent | types.ImageCont ), ) + if prompts: + + @server.list_prompts() + async def _list_prompts() -> list[types.Prompt]: + """List all prompts in the kernel.""" + mcp_prompts = [] + for prompt in prompts: + mcp_prompts.append( + types.Prompt( + name=prompt.prompt_template_config.name, + description=prompt.prompt_template_config.description, + arguments=[ + types.PromptArgument( + name=var.name, + description=var.description, + required=var.is_required, + ) + for var in prompt.prompt_template_config.input_variables + ], + ) + ) + await _log(level="debug", data=f"List of prompts: {mcp_prompts}") + return mcp_prompts + + @server.get_prompt() + async def _get_prompt(name: str, arguments: dict[str, Any] | None) -> types.GetPromptResult: + """Get a prompt by name.""" + prompt = next((p for p in prompts if p.prompt_template_config.name == name), None) + if prompt is None: + return types.GetPromptResult(description="Prompt not found", messages=[]) + + # Call the prompt + rendered_prompt = await prompt.render( + kernel, + KernelArguments(**arguments) if arguments is not None else KernelArguments(), + ) + # since the return type of a get_prompts is a list of messages, + # we need to convert the rendered prompt to a list of messages + # by using the ChatHistory class + chat_history = ChatHistory.from_rendered_prompt(rendered_prompt) + messages = [] + for message in chat_history.messages: + messages.append( + types.PromptMessage( + role=message.role.value + if message.role in (AuthorRole.ASSISTANT, AuthorRole.USER) + else "assistant", + content=_kernel_content_to_mcp_content_types(message)[0], + ) + ) + return types.GetPromptResult(messages=messages) + async def _log(level: types.LoggingLevel, data: Any) -> None: """Log a message to the server and logger.""" # Log to the local logger From 040ce9b6ccfece8db7b529e4701538172cc537ba Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 11 Apr 2025 16:47:09 +0200 Subject: [PATCH 3/7] add new sample with local agent --- .../concepts/mcp/agent_with_mcp_plugin.py | 6 +- .../mcp/agent_with_mcp_prompt_sampling.py | 108 ++++++++++++++++++ .../sk_mcp_server/mcp_server_with_prompts.py | 26 ++--- python/semantic_kernel/connectors/mcp.py | 14 ++- 4 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 python/samples/concepts/mcp/agent_with_mcp_prompt_sampling.py diff --git a/python/samples/concepts/mcp/agent_with_mcp_plugin.py b/python/samples/concepts/mcp/agent_with_mcp_plugin.py index ad972dd661d7..31fb80b7e14f 100644 --- a/python/samples/concepts/mcp/agent_with_mcp_plugin.py +++ b/python/samples/concepts/mcp/agent_with_mcp_plugin.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +import os from semantic_kernel.agents import ChatCompletionAgent, ChatHistoryAgentThread from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion @@ -27,8 +28,9 @@ async def main(): async with MCPStdioPlugin( name="Github", description="Github Plugin", - command="npx", - args=["-y", "@modelcontextprotocol/server-github"], + command="docker", + args=["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"], + env={"GITHUB_PERSONAL_ACCESS_TOKEN": os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")}, ) as github_plugin: agent = ChatCompletionAgent( service=AzureChatCompletion(), diff --git a/python/samples/concepts/mcp/agent_with_mcp_prompt_sampling.py b/python/samples/concepts/mcp/agent_with_mcp_prompt_sampling.py new file mode 100644 index 000000000000..0546ef78d0e7 --- /dev/null +++ b/python/samples/concepts/mcp/agent_with_mcp_prompt_sampling.py @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from pathlib import Path + +from semantic_kernel.agents import ChatCompletionAgent, ChatHistoryAgentThread +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior +from semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion import OllamaChatCompletion +from semantic_kernel.connectors.mcp import MCPStdioPlugin +from semantic_kernel.functions.kernel_arguments import KernelArguments + +""" +The following sample demonstrates how to create a chat completion agent that +answers questions about Github using a Local Agent with two local MCP Servers. +""" + +USER_INPUTS = [ + "list the latest 10 issues that have the label: triage and python and are open", + """generate release notes with this list: +* Python: Add ChatCompletionAgent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11430 +* Python: Update Doc Gen demo based on latest agent invocation api pattern by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11426 +* Python: Update Python min version in README by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11428 +* Python: Fix `TypeError` when required is missing in MCP tool’s inputSchema by @KanchiShimono in https://github.com/microsoft/semantic-kernel/pull/11458 +* Python: Update chromadb requirement from <0.7,>=0.5 to >=0.5,<1.1 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11420 +* Python: Bump google-cloud-aiplatform from 1.86.0 to 1.87.0 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11423 +* Python: Support Auto Function Invocation Filter for AzureAIAgent and OpenAIAssistantAgent by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11460 +* Python: Improve agent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11475 +* Python: Allow Kernel Functions from Prompt for image and audio content by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11403 +* Python: Introducing SK as a MCP Server by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11362 +* Python: sample using GitHub MCP Server and Azure AI Agent by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11465 +* Python: allow settings to be created directly by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11468 +* Python: Bug fix for azure ai agent truncate strategy. Add sample. by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11503 +* Python: small code improvements in code of call automation sample by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11477 +* Added missing import asyncio to agent with plugin python by @sphenry in https://github.com/microsoft/semantic-kernel/pull/11472 +* Python: version updated to 1.28.0 by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11504""", +] + + +async def main(): + # Load the MCP Servers as Plugins + async with ( + MCPStdioPlugin( + name="Github", + description="Github Plugin", + command="docker", + args=["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"], + env={"GITHUB_PERSONAL_ACCESS_TOKEN": os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")}, + ) as github_plugin, + MCPStdioPlugin( + name="ReleaseNotes", + description="SK Release Notes Plugin", + command="uv", + args=[ + f"--directory={str(Path(os.path.dirname(__file__)).parent.parent.joinpath('demos', 'sk_mcp_server'))}", + "run", + "mcp_server_with_prompts.py", + ], + ) as release_notes_plugin, + ): + # Create the agent + # Using the OllamaChatCompletion service + agent = ChatCompletionAgent( + service=OllamaChatCompletion(), + name="GithubAgent", + instructions="You interact with the user to help them with the Microsoft semantic-kernel github project. " + "You have dedicated tools for this, including one to write release notes, " + "make sure to use that when needed. The repo is always semantic-kernel (aka SK) with owner Microsoft. " + "and when doing lists, always return 5 items and sort descending by created or updated" + "You are specialized in Python, so always include label, python, in addition to the other labels.", + plugins=[github_plugin, release_notes_plugin], + function_choice_behavior=FunctionChoiceBehavior.Auto( + filters={ + # exclude a bunch of functions because the local models have trouble with too many functions + "included_functions": [ + "Github-list_issues", + "ReleaseNotes-release_notes_prompt", + ] + } + ), + ) + print(f"Agent uses Ollama with the {agent.service.ai_model_id} model") + + # Create a thread to hold the conversation + # If no thread is provided, a new thread will be + # created and returned with the initial response + thread: ChatHistoryAgentThread | None = None + for user_input in USER_INPUTS: + print(f"# User: {user_input}", end="\n\n") + first_chunk = True + async for response in agent.invoke_stream( + messages=user_input, + thread=thread, + arguments=KernelArguments(owner="microsoft", repo="semantic-kernel"), + ): + if first_chunk: + print(f"# {response.name}: ", end="", flush=True) + first_chunk = False + print(response.content, end="", flush=True) + thread = response.thread + print() + + # Cleanup: Clear the thread + await thread.delete() if thread else None + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py index 9d215911b077..74743dfa3317 100644 --- a/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py +++ b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py @@ -11,9 +11,7 @@ from mcp.server.stdio import stdio_server from semantic_kernel import Kernel -from semantic_kernel.prompt_template.input_variable import InputVariable -from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate -from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig +from semantic_kernel.prompt_template import InputVariable, KernelPromptTemplate, PromptTemplateConfig logger = logging.getLogger(__name__) @@ -38,15 +36,7 @@ ``` """ - -def run() -> None: - """Run the MCP server with the release notes prompt template.""" - kernel = Kernel() - prompt = KernelPromptTemplate( - prompt_template_config=PromptTemplateConfig( - name="release_notes", - description="This creates release notes from a list of PRs messages.", - template="""{{$messages}} +template = """{{$messages}} --- Group the following PRs into one of these buckets for release notes, keeping the same order: @@ -56,7 +46,17 @@ def run() -> None: -Python Package Updates Include the output in raw markdown. -""", +""" + + +def run() -> None: + """Run the MCP server with the release notes prompt template.""" + kernel = Kernel() + prompt = KernelPromptTemplate( + prompt_template_config=PromptTemplateConfig( + name="release_notes_prompt", + description="This creates the prompts for a full set of release notes based on the PR messages given.", + template=template, input_variables=[ InputVariable( name="messages", diff --git a/python/semantic_kernel/connectors/mcp.py b/python/semantic_kernel/connectors/mcp.py index a0b715526b75..a94eb1fed2be 100644 --- a/python/semantic_kernel/connectors/mcp.py +++ b/python/semantic_kernel/connectors/mcp.py @@ -228,12 +228,14 @@ async def connect(self) -> None: logger.debug("Resource templates: %s", await self.session.list_resource_templates()) await self.load_tools() await self.load_prompts() - try: - await self.session.set_logging_level( - next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level) - ) - except Exception: - logger.warning("Failed to set log level to %s", logger.level) + + if logger.level != logging.NOTSET: + try: + await self.session.set_logging_level( + next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level) + ) + except Exception: + logger.warning("Failed to set log level to %s", logger.level) async def sampling_callback( self, context: RequestContext[ClientSession, Any], params: types.CreateMessageRequestParams From e54ff9c95677a018f4d35c201d102c6e22192478 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 11 Apr 2025 16:57:49 +0200 Subject: [PATCH 4/7] cleanup --- python/samples/concepts/mcp/README.md | 23 ++++++++----------- .../mcp/azure_ai_agent_with_mcp_plugin.py | 6 +++-- ...ng.py => local_agent_with_local_server.py} | 2 +- python/samples/concepts/mcp/mcp_as_plugin.py | 6 +++-- 4 files changed, 18 insertions(+), 19 deletions(-) rename python/samples/concepts/mcp/{agent_with_mcp_prompt_sampling.py => local_agent_with_local_server.py} (99%) diff --git a/python/samples/concepts/mcp/README.md b/python/samples/concepts/mcp/README.md index d11a8dbbb644..18ebf8d26c5c 100644 --- a/python/samples/concepts/mcp/README.md +++ b/python/samples/concepts/mcp/README.md @@ -20,27 +20,22 @@ The code shown works the same for a Sse server, only then a MCPSsePlugin needs t The reverse, using Semantic Kernel as a server, can be found in the [demos/sk_mcp_server](../../demos/sk_mcp_server/) folder. -## Running the sample +## Running the samples -1. Make sure you have the [Node.js](https://nodejs.org/en/download/) installed. -2. Make sure you have the [npx](https://docs.npmjs.com/cli/v8/commands/npx) available in PATH. -3. The Github MCP Server uses a Github Personal Access Token (PAT) to authenticate, see [the documentation](https://github.com/modelcontextprotocol/servers/tree/main/src/github) on how to create one. -4. Install Semantic Kernel with the mcp extra: +1. Depending on the sample you want to run: + 1. Install [Node.js](https://nodejs.org/en/download/), make sure you have the [npx](https://docs.npmjs.com/cli/v8/commands/npx) available in PATH. + 1. [Docker](https://www.docker.com/products/docker-desktop/) installed. + 1. [uv](https://docs.astral.sh/uv/getting-started/installation/) installed. +2. The Github MCP Server uses a Github Personal Access Token (PAT) to authenticate, see [the documentation](https://github.com/modelcontextprotocol/servers/tree/main/src/github) on how to create one. +3. Install Semantic Kernel with the mcp extra: ```bash pip install semantic-kernel[mcp] ``` -5. Run the sample: +4. Run any of the samples: ```bash cd python/samples/concepts/mcp -python mcp_as_plugin.py -``` - -or: - -```bash -cd python/samples/concepts/mcp -python agent_with_mcp_plugin.py +python .py ``` diff --git a/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py b/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py index 2270efba8b8a..9cbebd0ac45c 100644 --- a/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py +++ b/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +import os from azure.identity.aio import DefaultAzureCredential @@ -22,8 +23,9 @@ async def main(): MCPStdioPlugin( name="github", description="Github Plugin", - command="npx", - args=["-y", "@modelcontextprotocol/server-github"], + command="docker", + args=["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"], + env={"GITHUB_PERSONAL_ACCESS_TOKEN": os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")}, ) as github_plugin, ): # 3. Create the agent, with the MCP plugin and the thread diff --git a/python/samples/concepts/mcp/agent_with_mcp_prompt_sampling.py b/python/samples/concepts/mcp/local_agent_with_local_server.py similarity index 99% rename from python/samples/concepts/mcp/agent_with_mcp_prompt_sampling.py rename to python/samples/concepts/mcp/local_agent_with_local_server.py index 0546ef78d0e7..d05a2c43e0e0 100644 --- a/python/samples/concepts/mcp/agent_with_mcp_prompt_sampling.py +++ b/python/samples/concepts/mcp/local_agent_with_local_server.py @@ -59,8 +59,8 @@ async def main(): ) as release_notes_plugin, ): # Create the agent - # Using the OllamaChatCompletion service agent = ChatCompletionAgent( + # Using the OllamaChatCompletion service service=OllamaChatCompletion(), name="GithubAgent", instructions="You interact with the user to help them with the Microsoft semantic-kernel github project. " diff --git a/python/samples/concepts/mcp/mcp_as_plugin.py b/python/samples/concepts/mcp/mcp_as_plugin.py index 566cfcec98ec..f144cafd5252 100644 --- a/python/samples/concepts/mcp/mcp_as_plugin.py +++ b/python/samples/concepts/mcp/mcp_as_plugin.py @@ -2,6 +2,7 @@ import asyncio import logging +import os from samples.concepts.setup.chat_completion_services import Services, get_chat_completion_service_and_request_settings from semantic_kernel import Kernel @@ -99,8 +100,9 @@ async def main() -> None: async with MCPStdioPlugin( name="Github", description="Github Plugin", - command="npx", - args=["-y", "@modelcontextprotocol/server-github"], + command="docker", + args=["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"], + env={"GITHUB_PERSONAL_ACCESS_TOKEN": os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")}, ) as github_plugin: # instead of using this async context manager, you can also use: # await github_plugin.connect() From 007db71ddcf538c0b2ad893f72b4faf3df4ad239 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 11 Apr 2025 19:41:51 +0200 Subject: [PATCH 5/7] cleanup and one more sample --- .../mcp/azure_ai_agent_with_local_server.py | 107 ++++++++++++++++++ .../mcp/local_agent_with_local_server.py | 6 +- 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 python/samples/concepts/mcp/azure_ai_agent_with_local_server.py diff --git a/python/samples/concepts/mcp/azure_ai_agent_with_local_server.py b/python/samples/concepts/mcp/azure_ai_agent_with_local_server.py new file mode 100644 index 000000000000..68ad89b21269 --- /dev/null +++ b/python/samples/concepts/mcp/azure_ai_agent_with_local_server.py @@ -0,0 +1,107 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from pathlib import Path + +from azure.identity.aio import DefaultAzureCredential + +from semantic_kernel.agents import ( + AzureAIAgent, + AzureAIAgentSettings, + AzureAIAgentThread, +) +from semantic_kernel.connectors.mcp import MCPStdioPlugin +from semantic_kernel.functions import KernelArguments + +""" +The following sample demonstrates how to create a chat completion agent that +answers questions about Github using a Local Agent with two local MCP Servers. +""" + +USER_INPUTS = [ + "list the latest 10 issues that have the label: triage and python and are open", + """generate release notes with this list: +* Python: Add ChatCompletionAgent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11430 +* Python: Update Doc Gen demo based on latest agent invocation api pattern by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11426 +* Python: Update Python min version in README by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11428 +* Python: Fix `TypeError` when required is missing in MCP tool’s inputSchema by @KanchiShimono in https://github.com/microsoft/semantic-kernel/pull/11458 +* Python: Update chromadb requirement from <0.7,>=0.5 to >=0.5,<1.1 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11420 +* Python: Bump google-cloud-aiplatform from 1.86.0 to 1.87.0 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11423 +* Python: Support Auto Function Invocation Filter for AzureAIAgent and OpenAIAssistantAgent by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11460 +* Python: Improve agent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11475 +* Python: Allow Kernel Functions from Prompt for image and audio content by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11403 +* Python: Introducing SK as a MCP Server by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11362 +* Python: sample using GitHub MCP Server and Azure AI Agent by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11465 +* Python: allow settings to be created directly by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11468 +* Python: Bug fix for azure ai agent truncate strategy. Add sample. by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11503 +* Python: small code improvements in code of call automation sample by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11477 +* Added missing import asyncio to agent with plugin python by @sphenry in https://github.com/microsoft/semantic-kernel/pull/11472 +* Python: version updated to 1.28.0 by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11504""", +] + + +async def main(): + # Load the MCP Servers as Plugins + async with ( + # 1. Login to Azure and create a Azure AI Project Client + DefaultAzureCredential() as creds, + AzureAIAgent.create_client(credential=creds) as client, + MCPStdioPlugin( + name="Github", + description="Github Plugin", + command="docker", + args=["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"], + env={"GITHUB_PERSONAL_ACCESS_TOKEN": os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")}, + ) as github_plugin, + MCPStdioPlugin( + name="ReleaseNotes", + description="SK Release Notes Plugin", + command="uv", + args=[ + f"--directory={str(Path(os.path.dirname(__file__)).parent.parent.joinpath('demos', 'sk_mcp_server'))}", + "run", + "mcp_server_with_prompts.py", + ], + ) as release_notes_plugin, + ): + # 3. Create the agent, with the MCP plugin and the thread + agent = AzureAIAgent( + client=client, + definition=await client.agents.create_agent( + model=AzureAIAgentSettings().model_deployment_name, + name="GithubAgent", + instructions="You interact with the user to help them with the Microsoft semantic-kernel github " + "project. You have dedicated tools for this, including one to write release notes, " + "make sure to use that when needed. The repo is always semantic-kernel (aka SK) with owner Microsoft. " + "and when doing lists, always return 5 items and sort descending by created or updated" + "You are specialized in Python, so always include label, python, in addition to the other labels.", + ), + plugins=[github_plugin, release_notes_plugin], # add the sample plugin to the agent + ) + + # Create a thread to hold the conversation + # If no thread is provided, a new thread will be + # created and returned with the initial response + thread: AzureAIAgentThread | None = None + for user_input in USER_INPUTS: + print(f"# User: {user_input}", end="\n\n") + first_chunk = True + async for response in agent.invoke_stream( + messages=user_input, + thread=thread, + arguments=KernelArguments(owner="microsoft", repo="semantic-kernel"), + ): + if first_chunk: + print(f"# {response.name}: ", end="", flush=True) + first_chunk = False + print(response.content, end="", flush=True) + thread = response.thread + print() + + # Cleanup: Clear the thread + await thread.delete() if thread else None + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/concepts/mcp/local_agent_with_local_server.py b/python/samples/concepts/mcp/local_agent_with_local_server.py index d05a2c43e0e0..95bb90624e56 100644 --- a/python/samples/concepts/mcp/local_agent_with_local_server.py +++ b/python/samples/concepts/mcp/local_agent_with_local_server.py @@ -5,10 +5,10 @@ from pathlib import Path from semantic_kernel.agents import ChatCompletionAgent, ChatHistoryAgentThread -from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior -from semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion import OllamaChatCompletion +from semantic_kernel.connectors.ai import FunctionChoiceBehavior +from semantic_kernel.connectors.ai.ollama import OllamaChatCompletion from semantic_kernel.connectors.mcp import MCPStdioPlugin -from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.functions import KernelArguments """ The following sample demonstrates how to create a chat completion agent that From 01da88be4a7e6da59f4cd4e519b7820616c27063 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 14 Apr 2025 16:25:06 +0200 Subject: [PATCH 6/7] sampling sample with bunch of other updates --- .../concepts/mcp/agent_with_mcp_sampling.py | 110 +++++++++ .../sk_mcp_server/mcp_server_with_prompts.py | 2 +- .../sk_mcp_server/mcp_server_with_sampling.py | 111 +++++++++ python/semantic_kernel/connectors/mcp.py | 220 ++++++++++-------- .../functions/kernel_function_decorator.py | 24 +- .../functions/kernel_function_extension.py | 19 +- .../functions/kernel_function_from_method.py | 1 + python/semantic_kernel/kernel.py | 8 +- .../services/kernel_services_extension.py | 2 +- 9 files changed, 396 insertions(+), 101 deletions(-) create mode 100644 python/samples/concepts/mcp/agent_with_mcp_sampling.py create mode 100644 python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py diff --git a/python/samples/concepts/mcp/agent_with_mcp_sampling.py b/python/samples/concepts/mcp/agent_with_mcp_sampling.py new file mode 100644 index 000000000000..32e50fd0f619 --- /dev/null +++ b/python/samples/concepts/mcp/agent_with_mcp_sampling.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import logging +import os +from pathlib import Path + +from semantic_kernel.agents.chat_completion.chat_completion_agent import ChatCompletionAgent +from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion +from semantic_kernel.connectors.mcp import MCPStdioPlugin + +# set this lower or higher depending on your needs +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +""" +The following sample demonstrates how to use a MCP Server that requires sampling +to generate release notes from a list of issues. +""" + +PR_MESSAGES = """* Python: Add ChatCompletionAgent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11430 +* Python: Update Doc Gen demo based on latest agent invocation api pattern by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11426 +* Python: Update Python min version in README by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11428 +* Python: Fix `TypeError` when required is missing in MCP tool’s inputSchema by @KanchiShimono in https://github.com/microsoft/semantic-kernel/pull/11458 +* Python: Update chromadb requirement from <0.7,>=0.5 to >=0.5,<1.1 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11420 +* Python: Bump google-cloud-aiplatform from 1.86.0 to 1.87.0 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11423 +* Python: Support Auto Function Invocation Filter for AzureAIAgent and OpenAIAssistantAgent by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11460 +* Python: Improve agent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11475 +* Python: Allow Kernel Functions from Prompt for image and audio content by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11403 +* Python: Introducing SK as a MCP Server by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11362 +* Python: sample using GitHub MCP Server and Azure AI Agent by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11465 +* Python: allow settings to be created directly by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11468 +* Python: Bug fix for azure ai agent truncate strategy. Add sample. by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11503 +* Python: small code improvements in code of call automation sample by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11477 +* Added missing import asyncio to agent with plugin python by @sphenry in https://github.com/microsoft/semantic-kernel/pull/11472 +* Python: version updated to 1.28.0 by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11504""" + + +async def main(): + # 1. Create the agent + async with MCPStdioPlugin( + name="ReleaseNotes", + description="SK Release Notes Plugin", + command="uv", + args=[ + f"--directory={str(Path(os.path.dirname(__file__)).parent.parent.joinpath('demos', 'sk_mcp_server'))}", + "run", + "mcp_server_with_sampling.py", + ], + ) as plugin: + agent = ChatCompletionAgent( + service=OpenAIChatCompletion(), + name="IssueAgent", + instructions="For the messages supplied, call the release_notes_prompt function to get the broader " + "prompt, then call the run_prompt function to get the final output, return that without any other text." + "Do not add any other text to the output, or rewrite the output from run_prompt.", + plugins=[plugin], + ) + + print(f"# Task: {PR_MESSAGES}") + # 3. Invoke the agent for a response + response = await agent.get_response(messages=PR_MESSAGES) + print(str(response)) + + # 4. Cleanup: Clear the thread + await response.thread.delete() + + """ +# Task: * Python: Add ChatCompletionAgent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11430 +* Python: Update Doc Gen demo based on latest agent invocation api pattern by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11426 +* Python: Update Python min version in README by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11428 +* Python: Fix `TypeError` when required is missing in MCP tool’s inputSchema by @KanchiShimono in https://github.com/microsoft/semantic-kernel/pull/11458 +* Python: Update chromadb requirement from <0.7,>=0.5 to >=0.5,<1.1 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11420 +* Python: Bump google-cloud-aiplatform from 1.86.0 to 1.87.0 in /python by @dependabot in https://github.com/microsoft/semantic-kernel/pull/11423 +* Python: Support Auto Function Invocation Filter for AzureAIAgent and OpenAIAssistantAgent by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11460 +* Python: Improve agent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11475 +* Python: Allow Kernel Functions from Prompt for image and audio content by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11403 +* Python: Introducing SK as a MCP Server by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11362 +* Python: sample using GitHub MCP Server and Azure AI Agent by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11465 +* Python: allow settings to be created directly by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11468 +* Python: Bug fix for azure ai agent truncate strategy. Add sample. by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11503 +* Python: small code improvements in code of call automation sample by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11477 +* Added missing import asyncio to agent with plugin python by @sphenry in https://github.com/microsoft/semantic-kernel/pull/11472 +* Python: version updated to 1.28.0 by @eavanvalkenburg in https://github.com/microsoft/semantic-kernel/pull/11504 + +Here’s a summary of the recent changes and contributions made to the Microsoft Semantic Kernel repository: + +1. **Integration Tests**: + - Added integration tests for `ChatCompletionAgent` by @moonbox3 ([PR #11430](https://github.com/microsoft/semantic-kernel/pull/11430)). + - Improved agent integration tests by @moonbox3 ([PR #11475](https://github.com/microsoft/semantic-kernel/pull/11475)). + +2. **Documentation and Demos**: + - Updated the Doc Gen demo to align with the latest agent invocation API pattern by @moonbox3 ([PR #11426](https://github.com/microsoft/semantic-kernel/pull/11426)). + - Small code improvements made in the code of the call automation sample by @eavanvalkenburg ([PR #11477](https://github.com/microsoft/semantic-kernel/pull/11477)). + +3. **Version Updates**: + - Updated the minimum Python version in the README by @moonbox3 ([PR #11428](https://github.com/microsoft/semantic-kernel/pull/11428)). + - Updated `chromadb` requirement to allow versions >=0.5 and <1.1 by @dependabot ([PR #11420](https://github.com/microsoft/semantic-kernel/pull/11420)). + - Bumped `google-cloud-aiplatform` from 1.86.0 to 1.87.0 by @dependabot ([PR #11423](https://github.com/microsoft/semantic-kernel/pull/11423)). + - Version updated to 1.28.0 by @eavanvalkenburg ([PR #11504](https://github.com/microsoft/semantic-kernel/pull/11504)). + +4. **Bug Fixes**: + - Fixed a `TypeError` in the MCP tool’s input schema when the required field is missing by @KanchiShimono ([PR #11458](https://github.com/microsoft/semantic-kernel/pull/11458)). + - Bug fix for Azure AI agent truncate strategy with an added sample by @moonbox3 ([PR #11503](https://github.com/microsoft/semantic-kernel/pull/11503)). + - Added a missing import for `asyncio` in the agent with plugin Python by @sphenry ([PR #11472](https://github.com/microsoft/semantic-kernel/pull/11472)). + """ + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py index 74743dfa3317..b3299f062064 100644 --- a/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py +++ b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) """ -This sample demonstrates how to expose your Semantic Kernel instance as a MCP server. +This sample demonstrates how to expose your a Semantic Kernel prompt through a MCP server. To run this sample, set up your MCP host (like Claude Desktop or VSCode Github Copilot Agents) with the following configuration: diff --git a/python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py b/python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py new file mode 100644 index 000000000000..2828cc3cdac4 --- /dev/null +++ b/python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py @@ -0,0 +1,111 @@ +# /// script # noqa: CPY001 +# dependencies = [ +# "semantic-kernel[mcp]", +# ] +# /// +# Copyright (c) Microsoft. All rights reserved. +import logging +from typing import Annotated, Any + +import anyio +from mcp import types +from mcp.server.lowlevel import Server +from mcp.server.stdio import stdio_server + +from semantic_kernel import Kernel +from semantic_kernel.functions import kernel_function +from semantic_kernel.prompt_template import InputVariable, KernelPromptTemplate, PromptTemplateConfig + +logger = logging.getLogger(__name__) + +""" +This sample demonstrates how to expose your Semantic Kernel instance as a MCP server. + +To run this sample, set up your MCP host (like Claude Desktop or VSCode Github Copilot Agents) +with the following configuration: +```json +{ + "mcpServers": { + "sk_release_notes": { + "command": "uv", + "args": [ + "--directory=/semantic-kernel/python/samples/demos/mcp_server", + "run", + "mcp_server_with_prompts.py" + ], + } + } +} +``` +""" + +template = """{{$messages}} +--- +Group the following PRs into one of these buckets for release notes, keeping the same order: + +-New Features +-Enhancements and Improvements +-Bug Fixes +-Python Package Updates + +Include the output in raw markdown. +""" + + +@kernel_function( + name="run_prompt", + description="This run the prompts for a full set of release notes based on the PR messages given.", +) +async def sampling_function( + messages: Annotated[str, "The list of PR messages, as a string with newlines"], + temperature: float = 0.0, + max_tokens: int = 1000, + server: Annotated[Server | None, "The server session", {"include_in_function_choices": False}] = None, +) -> str: + if not server: + raise ValueError("Request context is required for sampling function.") + sampling_response = await server.request_context.session.create_message( + messages=[ + types.SamplingMessage(role="user", content=types.TextContent(type="text", text=messages)), + ], + max_tokens=max_tokens, + temperature=temperature, + model_preferences=types.ModelPreferences( + hints=[types.ModelHint(name="gpt-4o-mini")], + ), + ) + logger.info(f"Sampling response: {sampling_response}") + return sampling_response.content.text + + +def run() -> None: + """Run the MCP server with the release notes prompt template.""" + kernel = Kernel() + kernel.add_function("release_notes", sampling_function) + prompt = KernelPromptTemplate( + prompt_template_config=PromptTemplateConfig( + name="release_notes_prompt", + description="This creates the prompts for a full set of release notes based on the PR messages given.", + template=template, + input_variables=[ + InputVariable( + name="messages", + description="These are the PR messages, they are a single string with new lines.", + is_required=True, + json_schema='{ "type": "string"}', + ) + ], + ) + ) + + server = kernel.as_mcp_server(server_name="sk_release_notes", prompts=[prompt]) + + async def handle_stdin(stdin: Any | None = None, stdout: Any | None = None) -> None: + async with stdio_server() as (read_stream, write_stream): + await server.run(read_stream, write_stream, server.create_initialization_options()) + + anyio.run(handle_stdin) + + +if __name__ == "__main__": + run() diff --git a/python/semantic_kernel/connectors/mcp.py b/python/semantic_kernel/connectors/mcp.py index a94eb1fed2be..318605c7b6c9 100644 --- a/python/semantic_kernel/connectors/mcp.py +++ b/python/semantic_kernel/connectors/mcp.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +import json import logging import sys from abc import abstractmethod @@ -158,16 +159,18 @@ def _get_parameter_dicts_from_mcp_tool(tool: types.Tool) -> list[dict[str, Any]] # Check if 'properties' is missing or not a dictionary if not properties: return [] - return [ - { + params = [] + for prop_name, prop_details in properties.items(): + prop_details = json.loads(prop_details) if isinstance(prop_details, str) else prop_details + + params.append({ "name": prop_name, "is_required": prop_name in required, "type": prop_details.get("type"), "default_value": prop_details.get("default", None), "schema_data": prop_details, - } - for prop_name, prop_details in properties.items() - ] + }) + return params # region: MCP Plugin @@ -182,14 +185,14 @@ def __init__( name: str, description: str | None = None, session: ClientSession | None = None, - sample_completion_service: ChatCompletionClientBase | None = None, + kernel: Kernel | None = None, ) -> None: """Initialize the MCP Plugin Base.""" self.name = name self.description = description self._exit_stack = AsyncExitStack() self.session = session - self.sample_completion_service = sample_completion_service + self.kernel = kernel or None async def connect(self) -> None: """Connect to the MCP server.""" @@ -202,15 +205,15 @@ async def connect(self) -> None: "Failed to connect to the MCP server. Please check your configuration." ) from ex try: - session_args = { - "read_stream": transport[0], - "write_stream": transport[1], - "message_handler": self.message_handler, - "logging_callback": self.logging_callback, - } - if self.sample_completion_service: - session_args["sampling_callback"] = self.sampling_callback - session = await self._exit_stack.enter_async_context(ClientSession(**session_args)) + session = await self._exit_stack.enter_async_context( + ClientSession( + read_stream=transport[0], + write_stream=transport[1], + message_handler=self.message_handler, + logging_callback=self.logging_callback, + sampling_callback=self.sampling_callback, + ) + ) except Exception as ex: await self._exit_stack.aclose() raise KernelPluginInvalidConfigurationError( @@ -247,12 +250,29 @@ async def sampling_callback( This is a simple version of this function, it can be overridden to allow more complex sampling. It get's added to the session at initialization time, so overriding it is the best way to do this. """ - if not self.sample_completion_service: + if not self.kernel or not self.kernel.services: return types.ErrorData( code=types.INTERNAL_ERROR, - message="Sampling callback not set. Please set the sample completion service.", + message="No services in Kernel. Please set a kernel with one or more services.", ) - completion_settings = self.sample_completion_service.get_prompt_execution_settings_class()() + logger.debug("Sampling callback called with params: %s", params) + if params.modelPreferences is not None and params.modelPreferences.hints: + # TODO (eavanvalkenburg): deal with other parts of the modelPreferences concept + names = [hint.name for hint in params.modelPreferences.hints] + else: + names = ["default"] + + for name in names: + service = self.kernel.get_service(name, ChatCompletionClientBase) + break + if not service: + service = self.kernel.get_service("default", ChatCompletionClientBase) + if not service: + return types.ErrorData( + code=types.INTERNAL_ERROR, + message="No Chat completion service found.", + ) + completion_settings = service.get_prompt_execution_settings_class()() if "temperature" in completion_settings.__class__.model_fields: completion_settings.temperature = params.temperature # type: ignore @@ -265,10 +285,16 @@ async def sampling_callback( chat_history = ChatHistory(system_message=params.systemPrompt) for msg in params.messages: chat_history.add_message(_mcp_prompt_message_to_kernel_content(msg)) - result = await self.sample_completion_service.get_chat_message_content( - chat_history, - completion_settings, - ) + try: + result = await service.get_chat_message_content( + chat_history, + completion_settings, + ) + except Exception as ex: + return types.ErrorData( + code=types.INTERNAL_ERROR, + message=f"Failed to get chat message content: {ex}", + ) if not result: return types.ErrorData( code=types.INTERNAL_ERROR, @@ -283,12 +309,12 @@ async def sampling_callback( if not mcp_content: return types.ErrorData( code=types.INTERNAL_ERROR, - message="Failed to get chat message content.", + message="Failed to get right content types from the response.", ) return types.CreateMessageResult( role="assistant", content=mcp_content, - model=self.sample_completion_service.ai_model_id, + model=service.ai_model_id, ) async def logging_callback(self, params: types.LoggingMessageNotificationParams) -> None: @@ -403,6 +429,10 @@ async def __aexit__( """Exit the context manager.""" await self.close() + def added_to_kernel(self, kernel: Kernel) -> None: + """Add the plugin to the kernel.""" + self.kernel = kernel + # region: MCP Plugin Implementations @@ -419,7 +449,7 @@ def __init__( args: list[str] | None = None, env: dict[str, str] | None = None, encoding: str | None = None, - sample_completion_service: ChatCompletionClientBase | None = None, + kernel: Kernel | None = None, **kwargs: Any, ) -> None: """Initialize the MCP stdio plugin. @@ -437,11 +467,11 @@ def __init__( args: The arguments to pass to the command. env: The environment variables to set for the command. encoding: The encoding to use for the command output. - sample_completion_service: The sample completion service to use to do sampling. + kernel: The kernel instance with one or more Chat Completion clients. kwargs: Any extra arguments to pass to the stdio client. """ - super().__init__(name, description, session, sample_completion_service) + super().__init__(name, description, session, kernel) self.command = command self.args = args or [] self.env = env @@ -474,7 +504,7 @@ def __init__( headers: dict[str, Any] | None = None, timeout: float | None = None, sse_read_timeout: float | None = None, - sample_completion_service: ChatCompletionClientBase | None = None, + kernel: Kernel | None = None, **kwargs: Any, ) -> None: """Initialize the MCP sse plugin. @@ -493,11 +523,11 @@ def __init__( headers: The headers to send with the request. timeout: The timeout for the request. sse_read_timeout: The timeout for reading from the SSE stream. - sample_completion_service: The sample completion service to use to do sampling. + kernel: The kernel instance with one or more Chat Completion clients. kwargs: Any extra arguments to pass to the sse client. """ - super().__init__(name, description, session, sample_completion_service) + super().__init__(name=name, description=description, session=session, kernel=kernel) self.url = url self.headers = headers or {} self.timeout = timeout @@ -529,7 +559,7 @@ def __init__( url: str, session: ClientSession | None = None, description: str | None = None, - sample_completion_service: ChatCompletionClientBase | None = None, + kernel: Kernel | None = None, **kwargs: Any, ) -> None: """Initialize the MCP websocket plugin. @@ -545,11 +575,11 @@ def __init__( url: The URL of the MCP server. session: The session to use for the MCP connection. description: The description of the plugin. - sample_completion_service: The sample completion service to use to do sampling. + kernel: The kernel instance with one or more Chat Completion clients. kwargs: Any extra arguments to pass to the websocket client. """ - super().__init__(name, description, session, sample_completion_service) + super().__init__(name=name, description=description, session=session, kernel=kernel) self.url = url self._client_kwargs = kwargs @@ -583,9 +613,8 @@ def create_mcp_server_from_kernel( This function automatically creates a MCP server from a kernel instance, it uses the provided arguments to configure the server and expose functions as tools and prompts, see the mcp documentation for more details. - By default, all functions are exposed as Tools, you can specify which functions, - to do this you can use the `excluded_functions` argument. - These need to be set to the fully qualified function name (i.e. `-`). + By default, all functions are exposed as Tools, you can control this by using use the `excluded_functions` argument. + These need to be set to the function name, without the plugin_name. Args: kernel: The kernel instance to use. @@ -594,7 +623,7 @@ def create_mcp_server_from_kernel( version: The version of the server. instructions: The instructions to use for the server. lifespan: The lifespan of the server. - excluded_functions: The list of fully qualified function names to exclude from the server. + excluded_functions: The list of function names to exclude from the server. if None, no functions will be excluded. kwargs: Any extra arguments to pass to the server creation. @@ -618,63 +647,70 @@ def create_mcp_server_from_kernel( server: Server["LifespanResultT"] = Server(**server_args) # type: ignore[call-arg] - @server.list_tools() - async def _list_tools() -> list[types.Tool]: - """List all tools in the kernel.""" - functions_to_expose = [ - func - for func in kernel.get_full_list_of_function_metadata() - if func.fully_qualified_name not in (excluded_functions or []) - ] - tools = [ - types.Tool( - name=func.fully_qualified_name, - description=func.description, - inputSchema={ - "type": "object", - "properties": { - param.name: param.schema_data for param in func.parameters if param.name and param.schema_data + functions_to_expose = [ + func for func in kernel.get_full_list_of_function_metadata() if func.name not in (excluded_functions or []) + ] + + if len(functions_to_expose) > 0: + + @server.list_tools() + async def _list_tools() -> list[types.Tool]: + """List all tools in the kernel.""" + tools = [ + types.Tool( + name=func.name, + description=func.description, + inputSchema={ + "type": "object", + "properties": { + param.name: param.schema_data + for param in func.parameters + if param.name and param.schema_data and param.include_in_function_choices + }, + "required": [ + param.name + for param in func.parameters + if param.name and param.is_required and param.include_in_function_choices + ], }, - "required": [param.name for param in func.parameters if param.name and param.is_required], - }, - ) - for func in functions_to_expose - ] - await _log(level="debug", data=f"List of tools: {tools}") - await asyncio.sleep(0.0) - return tools - - @server.call_tool() - async def _call_tool(*args: Any) -> Sequence[types.TextContent | types.ImageContent | types.EmbeddedResource]: - """Call a tool in the kernel.""" - await _log(level="debug", data=f"Calling tool with args: {args}") - function_name, arguments = args[0], args[1] - result = await _call_kernel_function(function_name, arguments) - if result: - value = result.value - messages: list[types.TextContent | types.ImageContent | types.EmbeddedResource] = [] - if isinstance(value, list): - for item in value: + ) + for func in functions_to_expose + ] + await _log(level="debug", data=f"List of tools: {tools}") + await asyncio.sleep(0.0) + return tools + + @server.call_tool() + async def _call_tool(*args: Any) -> Sequence[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """Call a tool in the kernel.""" + await _log(level="debug", data=f"Calling tool with args: {args}") + function_name, arguments = args[0], args[1] + result = await _call_kernel_function(function_name, arguments) + if result: + value = result.value + messages: list[types.TextContent | types.ImageContent | types.EmbeddedResource] = [] + if isinstance(value, list): + for item in value: + if isinstance(value, (TextContent, ImageContent, BinaryContent, ChatMessageContent)): + messages.extend(_kernel_content_to_mcp_content_types(item)) + else: + messages.append( + types.TextContent(type="text", text=str(item)), + ) + else: if isinstance(value, (TextContent, ImageContent, BinaryContent, ChatMessageContent)): - messages.extend(_kernel_content_to_mcp_content_types(item)) + messages.extend(_kernel_content_to_mcp_content_types(value)) else: messages.append( - types.TextContent(type="text", text=str(item)), + types.TextContent(type="text", text=str(value)), ) - else: - if isinstance(value, (TextContent, ImageContent, BinaryContent, ChatMessageContent)): - messages.extend(_kernel_content_to_mcp_content_types(value)) - else: - messages.append( - types.TextContent(type="text", text=str(value)), - ) - return messages - raise McpError( - error=types.ErrorData( - code=types.INTERNAL_ERROR, - message=f"Function {function_name} returned no result", - ), - ) + return messages + raise McpError( + error=types.ErrorData( + code=types.INTERNAL_ERROR, + message=f"Function {function_name} returned no result", + ), + ) if prompts: @@ -746,7 +782,9 @@ async def _set_logging_level(level: types.LoggingLevel) -> None: await _log(level=level, data=f"Log level set to {level}") async def _call_kernel_function(function_name: str, arguments: Any) -> FunctionResult | None: - function = kernel.get_function_from_fully_qualified_function_name(function_name) + function = kernel.get_function(plugin_name=None, function_name=function_name) + arguments["server"] = server + print("arguments", arguments) return await function.invoke(kernel=kernel, **arguments) return server diff --git a/python/semantic_kernel/functions/kernel_function_decorator.py b/python/semantic_kernel/functions/kernel_function_decorator.py index 4c57a9aeb9f0..1fc00c383a01 100644 --- a/python/semantic_kernel/functions/kernel_function_decorator.py +++ b/python/semantic_kernel/functions/kernel_function_decorator.py @@ -26,9 +26,19 @@ def kernel_function( The parameters are parsed from the function signature, use typing.Annotated to provide a description for the parameter. - To parse the type, first it checks if the parameter is annotated, and get's the description from there. - After that it checks recursively until it reaches the lowest level, and it combines + To parse the type, first it checks if the parameter is annotated. + + If there are annotations, the first annotation that is a string is used as the description. + Any other annotations are checked if they are a dict, if so, they will be added to the parameter info. + If the keys align with the KernelParameterMetadata, they will be added to the parameter info. + This is useful for things like parameters like `kernel`, `service` and `arguments`, for instance + if you set `{"include_in_function_choices": False}` in the annotation, that parameter will not be included in + the representation of the function towards LLM's or MCP Servers. If you do set this and the parameter is required + but you do not set it in a invoke level arguments, the function will raise an error. + + After the annotations, it checks recursively until it reaches the lowest level, and it combines the types into a single comma-separated string, a forwardRef is also supported. + All of this is are stored in __kernel_function_parameters__. The return type and description are parsed from the function signature, @@ -137,7 +147,15 @@ def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]: return ret if not isinstance(param, str): if hasattr(param, "__metadata__"): - ret["description"] = param.__metadata__[0] + for meta in param.__metadata__: + if isinstance(meta, str): + ret["description"] = meta + elif isinstance(meta, dict): + if "description" in meta and "description" not in ret: + ret["description"] = meta["description"] + ret.update(meta) + else: + logger.debug(f"Unknown metadata type: {meta}") if hasattr(param, "__origin__"): ret.update(_parse_parameter(name, param.__origin__, default)) if hasattr(param, "__args__"): diff --git a/python/semantic_kernel/functions/kernel_function_extension.py b/python/semantic_kernel/functions/kernel_function_extension.py index 34206b2c811a..55c4c388ae45 100644 --- a/python/semantic_kernel/functions/kernel_function_extension.py +++ b/python/semantic_kernel/functions/kernel_function_extension.py @@ -4,7 +4,7 @@ from abc import ABC from collections.abc import Mapping, Sequence from functools import singledispatchmethod -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, Protocol, runtime_checkable from pydantic import Field, field_validator @@ -23,11 +23,25 @@ ) from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.types import KERNEL_FUNCTION_TYPE + from semantic_kernel.kernel import Kernel logger: logging.Logger = logging.getLogger(__name__) +@runtime_checkable +class AddToKernelCallbackProtocol(Protocol): + """Protocol for the callback to be called when the plugin is added to the kernel.""" + + def added_to_kernel(self, kernel: "Kernel") -> None: + """Called when the plugin is added to the kernel. + + Args: + kernel (Kernel): The kernel instance + """ + pass + + class KernelFunctionExtension(KernelBaseModel, ABC): """Kernel function extension.""" @@ -66,6 +80,7 @@ def add_plugin( a custom class that contains methods with the kernel_function decorator or a dictionary of functions with the kernel_function decorator for one or several methods. + if the custom class has a `added_to_kernel` method, it will be called with the kernel instance. plugin_name: The name of the plugin, used if the plugin is not a KernelPlugin, if the plugin is None and the parent_directory is set, KernelPlugin.from_directory is called with those parameters, @@ -92,6 +107,8 @@ def add_plugin( self.plugins[plugin_name] = KernelPlugin.from_object( plugin_name=plugin_name, plugin_instance=plugin, description=description ) + if isinstance(plugin, AddToKernelCallbackProtocol): + plugin.added_to_kernel(self) # type: ignore return self.plugins[plugin_name] if plugin is None and parent_directory is not None: self.plugins[plugin_name] = KernelPlugin.from_directory( diff --git a/python/semantic_kernel/functions/kernel_function_from_method.py b/python/semantic_kernel/functions/kernel_function_from_method.py index 07f020d9d04b..3f7c254c8d1a 100644 --- a/python/semantic_kernel/functions/kernel_function_from_method.py +++ b/python/semantic_kernel/functions/kernel_function_from_method.py @@ -170,6 +170,7 @@ def gather_function_parameters(self, context: FunctionInvocationContext) -> dict and "," not in param.type_ and param.type_object and param.type_object is not inspect._empty + and param.type_object is not Any ): try: value = self._parse_parameter(value, param.type_object) diff --git a/python/semantic_kernel/kernel.py b/python/semantic_kernel/kernel.py index ae8b3b34d594..68f21439c8ab 100644 --- a/python/semantic_kernel/kernel.py +++ b/python/semantic_kernel/kernel.py @@ -504,9 +504,9 @@ def as_mcp_server( This function automatically creates a MCP server from a kernel instance, it uses the provided arguments to configure the server and expose functions as tools and prompts, see the mcp documentation for more details. - By default, all functions are exposed as Tools, you can specify which functions, - to do this you can use the `excluded_functions` argument. - These need to be set to the fully qualified function name (i.e. `-`). + By default, all functions are exposed as Tools, you can control this by + using use the `excluded_functions` argument. + These need to be set to the function name, without the plugin_name. Args: kernel: The kernel instance to use. @@ -515,7 +515,7 @@ def as_mcp_server( version: The version of the server. instructions: The instructions to use for the server. lifespan: The lifespan of the server. - excluded_functions: The list of fully qualified function names to exclude from the server. + excluded_functions: The list of function names to exclude from the server. if None, no functions will be excluded. kwargs: Any extra arguments to pass to the server creation. diff --git a/python/semantic_kernel/services/kernel_services_extension.py b/python/semantic_kernel/services/kernel_services_extension.py index 37d425ce16d8..5b6c2d06b79d 100644 --- a/python/semantic_kernel/services/kernel_services_extension.py +++ b/python/semantic_kernel/services/kernel_services_extension.py @@ -68,7 +68,7 @@ def get_service( self, service_id: str | None = None, type: type[AI_SERVICE_CLIENT_TYPE] | tuple[type[AI_SERVICE_CLIENT_TYPE], ...] | None = None, - ) -> AIServiceClientBase: + ) -> AI_SERVICE_CLIENT_TYPE: """Get a service by service_id and type. Type is optional and when not supplied, no checks are done. From 12f9af5611700db55e2d88b68583fb671eb9beaf Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Tue, 15 Apr 2025 09:26:48 +0200 Subject: [PATCH 7/7] updates from feedback --- python/samples/concepts/mcp/README.md | 8 ++++---- .../samples/concepts/mcp/agent_with_mcp_plugin.py | 9 +++++++-- .../samples/concepts/mcp/agent_with_mcp_sampling.py | 9 +++++++-- .../mcp/azure_ai_agent_with_local_server.py | 5 +++++ .../concepts/mcp/azure_ai_agent_with_mcp_plugin.py | 5 +++++ .../concepts/mcp/local_agent_with_local_server.py | 4 ++++ python/samples/concepts/mcp/mcp_as_plugin.py | 2 +- .../demos/sk_mcp_server/mcp_server_with_prompts.py | 6 ++++-- .../demos/sk_mcp_server/mcp_server_with_sampling.py | 9 +++++++-- python/samples/demos/sk_mcp_server/sk_mcp_server.py | 5 ++++- .../functions/kernel_function_decorator.py | 8 ++++++-- .../services/kernel_services_extension.py | 13 +++++++------ 12 files changed, 61 insertions(+), 22 deletions(-) diff --git a/python/samples/concepts/mcp/README.md b/python/samples/concepts/mcp/README.md index 18ebf8d26c5c..db37cab11c4f 100644 --- a/python/samples/concepts/mcp/README.md +++ b/python/samples/concepts/mcp/README.md @@ -23,11 +23,11 @@ The reverse, using Semantic Kernel as a server, can be found in the [demos/sk_mc ## Running the samples 1. Depending on the sample you want to run: - 1. Install [Node.js](https://nodejs.org/en/download/), make sure you have the [npx](https://docs.npmjs.com/cli/v8/commands/npx) available in PATH. - 1. [Docker](https://www.docker.com/products/docker-desktop/) installed. - 1. [uv](https://docs.astral.sh/uv/getting-started/installation/) installed. + 1. [Docker](https://www.docker.com/products/docker-desktop/) installed, for the samples that use the Github MCP server. + 1. [uv](https://docs.astral.sh/uv/getting-started/installation/) installed, for the samples that use the local MCP server. 2. The Github MCP Server uses a Github Personal Access Token (PAT) to authenticate, see [the documentation](https://github.com/modelcontextprotocol/servers/tree/main/src/github) on how to create one. -3. Install Semantic Kernel with the mcp extra: +1. Check the comment at the start of the sample you want to run, for the appropriate environment variables to set. +1. Install Semantic Kernel with the mcp extra: ```bash pip install semantic-kernel[mcp] diff --git a/python/samples/concepts/mcp/agent_with_mcp_plugin.py b/python/samples/concepts/mcp/agent_with_mcp_plugin.py index 31fb80b7e14f..5229b4c664ed 100644 --- a/python/samples/concepts/mcp/agent_with_mcp_plugin.py +++ b/python/samples/concepts/mcp/agent_with_mcp_plugin.py @@ -10,8 +10,13 @@ """ The following sample demonstrates how to create a chat completion agent that answers questions about Github using a Semantic Kernel Plugin from a MCP server. -The Chat Completion Service is passed directly via the ChatCompletionAgent constructor. -Additionally, the plugin is supplied via the constructor. + +It uses the Azure OpenAI service to create a agent, so make sure to +set the required environment variables for the Azure AI Foundry service: +- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Optionally: AZURE_OPENAI_API_KEY +If this is not set, it will try to use DefaultAzureCredential. + """ diff --git a/python/samples/concepts/mcp/agent_with_mcp_sampling.py b/python/samples/concepts/mcp/agent_with_mcp_sampling.py index 32e50fd0f619..1fafaf1b3de8 100644 --- a/python/samples/concepts/mcp/agent_with_mcp_sampling.py +++ b/python/samples/concepts/mcp/agent_with_mcp_sampling.py @@ -5,8 +5,8 @@ import os from pathlib import Path -from semantic_kernel.agents.chat_completion.chat_completion_agent import ChatCompletionAgent -from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion +from semantic_kernel.agents import ChatCompletionAgent +from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.connectors.mcp import MCPStdioPlugin # set this lower or higher depending on your needs @@ -16,6 +16,11 @@ """ The following sample demonstrates how to use a MCP Server that requires sampling to generate release notes from a list of issues. + +It uses the OpenAI service to create a agent, so make sure to +set the required environment variables for the Azure AI Foundry service: +- OPENAI_API_KEY +- OPENAI_CHAT_MODEL_ID """ PR_MESSAGES = """* Python: Add ChatCompletionAgent integration tests by @moonbox3 in https://github.com/microsoft/semantic-kernel/pull/11430 diff --git a/python/samples/concepts/mcp/azure_ai_agent_with_local_server.py b/python/samples/concepts/mcp/azure_ai_agent_with_local_server.py index 68ad89b21269..8ba4e14bb68c 100644 --- a/python/samples/concepts/mcp/azure_ai_agent_with_local_server.py +++ b/python/samples/concepts/mcp/azure_ai_agent_with_local_server.py @@ -17,6 +17,11 @@ """ The following sample demonstrates how to create a chat completion agent that answers questions about Github using a Local Agent with two local MCP Servers. + +It uses the Azure AI Foundry Agent service to create a agent, so make sure to +set the required environment variables for the Azure AI Foundry service: +- AZURE_AI_AGENT_PROJECT_CONNECTION_STRING +- AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME """ USER_INPUTS = [ diff --git a/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py b/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py index 9cbebd0ac45c..2a2382910e6f 100644 --- a/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py +++ b/python/samples/concepts/mcp/azure_ai_agent_with_mcp_plugin.py @@ -11,6 +11,11 @@ """ The following sample demonstrates how to create a AzureAIAgent that answers questions about Github using a Semantic Kernel Plugin from a MCP server. + +It uses the Azure AI Foundry Agent service to create a agent, so make sure to +set the required environment variables for the Azure AI Foundry service: +- AZURE_AI_AGENT_PROJECT_CONNECTION_STRING +- AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME """ diff --git a/python/samples/concepts/mcp/local_agent_with_local_server.py b/python/samples/concepts/mcp/local_agent_with_local_server.py index 95bb90624e56..0c0bb3e6ddce 100644 --- a/python/samples/concepts/mcp/local_agent_with_local_server.py +++ b/python/samples/concepts/mcp/local_agent_with_local_server.py @@ -13,6 +13,10 @@ """ The following sample demonstrates how to create a chat completion agent that answers questions about Github using a Local Agent with two local MCP Servers. + +It uses a Ollama Chat Completion to create a agent, so make sure to +set the required environment variables for the Azure AI Foundry service: +- OLLAMA_CHAT_MODEL_ID """ USER_INPUTS = [ diff --git a/python/samples/concepts/mcp/mcp_as_plugin.py b/python/samples/concepts/mcp/mcp_as_plugin.py index f144cafd5252..a7fd7f7e4426 100644 --- a/python/samples/concepts/mcp/mcp_as_plugin.py +++ b/python/samples/concepts/mcp/mcp_as_plugin.py @@ -6,7 +6,7 @@ from samples.concepts.setup.chat_completion_services import Services, get_chat_completion_service_and_request_settings from semantic_kernel import Kernel -from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior +from semantic_kernel.connectors.ai import FunctionChoiceBehavior from semantic_kernel.connectors.mcp import MCPStdioPlugin from semantic_kernel.contents import ChatHistory from semantic_kernel.utils.logging import setup_logging diff --git a/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py index b3299f062064..50e99e7b5795 100644 --- a/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py +++ b/python/samples/demos/sk_mcp_server/mcp_server_with_prompts.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) """ -This sample demonstrates how to expose your a Semantic Kernel prompt through a MCP server. +This sample demonstrates how to expose a Semantic Kernel prompt through a MCP server. To run this sample, set up your MCP host (like Claude Desktop or VSCode Github Copilot Agents) with the following configuration: @@ -34,6 +34,8 @@ } } ``` +Note: You might need to set the uv to it's full path. + """ template = """{{$messages}} @@ -62,7 +64,7 @@ def run() -> None: name="messages", description="These are the PR messages, they are a single string with new lines.", is_required=True, - json_schema='{ "type": "string"}', + json_schema='{"type": "string"}', ) ], ) diff --git a/python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py b/python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py index 2828cc3cdac4..169862a5c3c8 100644 --- a/python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py +++ b/python/samples/demos/sk_mcp_server/mcp_server_with_sampling.py @@ -19,7 +19,8 @@ logger = logging.getLogger(__name__) """ -This sample demonstrates how to expose your Semantic Kernel instance as a MCP server. +This sample demonstrates how to expose your Semantic Kernel `kernel` instance as a MCP server, with the a function +that uses sampling (see the docs: https://modelcontextprotocol.io/docs/concepts/sampling) to generate release notes. To run this sample, set up your MCP host (like Claude Desktop or VSCode Github Copilot Agents) with the following configuration: @@ -37,6 +38,8 @@ } } ``` + +Note: You might need to set the uv to it's full path. """ template = """{{$messages}} @@ -60,6 +63,8 @@ async def sampling_function( messages: Annotated[str, "The list of PR messages, as a string with newlines"], temperature: float = 0.0, max_tokens: int = 1000, + # The include_in_function_choices is set to False, so it won't be included in the function choices, + # but it will get the server instance from the MCPPlugin that consumes this server. server: Annotated[Server | None, "The server session", {"include_in_function_choices": False}] = None, ) -> str: if not server: @@ -92,7 +97,7 @@ def run() -> None: name="messages", description="These are the PR messages, they are a single string with new lines.", is_required=True, - json_schema='{ "type": "string"}', + json_schema='{"type": "string"}', ) ], ) diff --git a/python/samples/demos/sk_mcp_server/sk_mcp_server.py b/python/samples/demos/sk_mcp_server/sk_mcp_server.py index 90ce111003db..cf267d759d23 100644 --- a/python/samples/demos/sk_mcp_server/sk_mcp_server.py +++ b/python/samples/demos/sk_mcp_server/sk_mcp_server.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) """ -This sample demonstrates how to expose your Semantic Kernel instance as a MCP server. +This sample demonstrates how to expose your Semantic Kernel `kernel` instance as a MCP server. To run this sample, set up your MCP host (like Claude Desktop or VSCode Github Copilot Agents) with the following configuration: @@ -39,6 +39,9 @@ } } ``` + +Note: You might need to set the uv to its full path. + Alternatively, you can run this as a SSE server, by setting the same environment variables as above, and running the following command: ```bash diff --git a/python/semantic_kernel/functions/kernel_function_decorator.py b/python/semantic_kernel/functions/kernel_function_decorator.py index 1fc00c383a01..4d103a1a9093 100644 --- a/python/semantic_kernel/functions/kernel_function_decorator.py +++ b/python/semantic_kernel/functions/kernel_function_decorator.py @@ -151,8 +151,9 @@ def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]: if isinstance(meta, str): ret["description"] = meta elif isinstance(meta, dict): - if "description" in meta and "description" not in ret: - ret["description"] = meta["description"] + # only override from the metadata if it is not already set + if "description" not in ret and (description := meta.pop("description", None)): + ret["description"] = description ret.update(meta) else: logger.debug(f"Unknown metadata type: {meta}") @@ -186,4 +187,7 @@ def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]: param = param.replace(" |", ",") ret["type_"] = param ret["is_required"] = True + # if the include_in_function_choices is set to false, we set the is_required to false + if not ret.get("include_in_function_choices", True): + ret["is_required"] = False return ret diff --git a/python/semantic_kernel/services/kernel_services_extension.py b/python/semantic_kernel/services/kernel_services_extension.py index 5b6c2d06b79d..61d0b4f36f02 100644 --- a/python/semantic_kernel/services/kernel_services_extension.py +++ b/python/semantic_kernel/services/kernel_services_extension.py @@ -2,7 +2,8 @@ import logging from abc import ABC -from typing import TYPE_CHECKING +from collections.abc import Mapping, MutableMapping +from typing import TYPE_CHECKING, TypeVar from pydantic import Field, field_validator @@ -10,7 +11,6 @@ from semantic_kernel.const import DEFAULT_SERVICE_NAME from semantic_kernel.exceptions import KernelFunctionAlreadyExistsError, KernelServiceNotFoundError from semantic_kernel.kernel_pydantic import KernelBaseModel -from semantic_kernel.kernel_types import AI_SERVICE_CLIENT_TYPE from semantic_kernel.services.ai_service_client_base import AIServiceClientBase from semantic_kernel.services.ai_service_selector import AIServiceSelector @@ -18,6 +18,7 @@ from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function import KernelFunction +AI_SERVICE_CLIENT_TYPE = TypeVar("AI_SERVICE_CLIENT_TYPE", bound=AIServiceClientBase) logger: logging.Logger = logging.getLogger(__name__) @@ -28,7 +29,7 @@ class KernelServicesExtension(KernelBaseModel, ABC): Adds all services related entities to the Kernel. """ - services: dict[str, AIServiceClientBase] = Field(default_factory=dict) + services: MutableMapping[str, AIServiceClientBase] = Field(default_factory=dict) ai_service_selector: AIServiceSelector = Field(default_factory=AIServiceSelector) @field_validator("services", mode="before") @@ -109,11 +110,11 @@ def get_service( def get_services_by_type( self, type: type[AI_SERVICE_CLIENT_TYPE] | tuple[type[AI_SERVICE_CLIENT_TYPE], ...] | None - ) -> dict[str, AIServiceClientBase]: + ) -> Mapping[str, AI_SERVICE_CLIENT_TYPE]: """Get all services of a specific type.""" if type is None: - return self.services - return {service.service_id: service for service in self.services.values() if isinstance(service, type)} + return self.services # type: ignore + return {service.service_id: service for service in self.services.values() if isinstance(service, type)} # type: ignore def get_prompt_execution_settings_from_service_id( self, service_id: str, type: type[AI_SERVICE_CLIENT_TYPE] | None = None