Integrating Model Context Protocol Tools with Semantic Kernel: A Step-by-Step Guide

This article walks through connecting an MCP server to a .NET application built with Semantic Kernel, using the official ModelContextProtocol package. Model Context Protocol is an open protocol, originally released by Anthropic, that standardizes how applications feed context, tools and data into large language models. Instead of every framework inventing its own tool-calling wire format, an MCP client written for Semantic Kernel can talk to any MCP server, whether that server exposes GitHub operations, a database, or a filesystem.

The sample in this guide focuses on one thing specifically: connecting a Semantic Kernel application to an MCP server over stdio, pulling the list of tools that server exposes, converting each tool into a native Kernel Function, and letting the model call those tools through ordinary function calling. Once that conversion step is done, the MCP tools behave exactly like any other Semantic Kernel plugin. You do not need to write custom code paths to handle MCP tools differently from your own C# functions.

How the pieces fit together

MCP follows a client-server model. An MCP host, your Semantic Kernel application in this case, connects to one or more MCP servers through an MCP client. Each server is a small, focused program that exposes a specific set of capabilities: one server might expose GitHub operations, another might expose a local filesystem, and another a database.

Semantic Kernel connects to a local GitHub MCP server, which in turn talks to GitHub's own Web API.
Semantic Kernel connects to a local GitHub MCP server, which in turn talks to GitHub’s own Web API.
  • MCP Hosts: the application, an IDE, a chat client, or in this case a Semantic Kernel app, that wants to consume tools and data
  • MCP Clients: the protocol layer that maintains a one to one connection with a single MCP server
  • MCP Servers: lightweight, focused programs that expose specific capabilities through the standardized protocol

Setting up the Kernel

Before wiring up MCP, you need an ordinary Semantic Kernel instance with a chat completion service configured. This sample uses OpenAI, so you need a valid API key. Store it using .NET user secrets locally or an environment variable, never hard code it into the project.

// Prepare and build kernel
var builder = Kernel.CreateBuilder();
builder.Services.AddLogging(c => c.AddDebug().SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace));
builder.Services.AddOpenAIChatCompletion(
    modelId: config["OpenAI:ChatModelId"] ?? "gpt-4o",
    apiKey: config["OpenAI:ApiKey"]!);
Kernel kernel = builder.Build();

This is standard Semantic Kernel boilerplate, nothing MCP specific yet. Notice the logging line sets the minimum level to Trace, which is useful while debugging MCP tool calls because Semantic Kernel logs every function invocation, including the arguments the model chose to pass. Turn it down to Information or Warning once things are working, trace level logging on a production app generates a lot of noise for very little benefit.

Creating the MCP client

With the kernel ready, the next step is creating an MCP client that talks to an actual MCP server. This sample connects to the community GitHub MCP server over stdio transport, which means Semantic Kernel starts the server as a child process and talks to it over standard input and output rather than over a network socket. The server itself is started using npx, the command line tool bundled with Node.js since version 5.2.0, which runs an npm package without installing it globally first.

// Create an MCPClient for the GitHub server
await using IMcpClient mcpClient = await McpClientFactory.CreateAsync(new StdioClientTransport(new()
{
    Name = "GitHub",
    Command = "npx",
    Arguments = ["-y", "@modelcontextprotocol/server-github"],
}));

The await using here matters. IMcpClient owns a child process, and disposing it properly shuts that process down when your application exits. Skip the await using and just let the client fall out of scope, and you can end up with orphaned npx processes hanging around, especially if your app crashes mid session. Also keep in mind that the first time this runs, npx has to download the server-github package from the npm registry, so expect a delay on cold start, and make sure the machine running this has outbound network access and Node.js installed.

Retrieving the available tools

Once connected, you can ask the server what tools it exposes. This is what makes MCP useful across a team: your Semantic Kernel app does not need to know in advance what a given server can do, it discovers the tool list at runtime.

// Retrieve the list of tools available on the GitHub server
var tools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
foreach (var tool in tools.Tools)
{
    Console.WriteLine($"{tool.Name}: {tool.Description}");
}

Running this against the GitHub MCP server prints out a fairly long list of tools, each with a name and a plain English description that the model will later use to decide which tool to call for a given request. A few of the tools this particular server exposes:

  • create_or_update_file: create or update a single file in a GitHub repository
  • search_repositories: search for GitHub repositories
  • create_repository: create a new GitHub repository in your account
  • get_file_contents: get the contents of a file or directory from a GitHub repository
  • push_files: push multiple files to a GitHub repository in a single commit
  • create_issue: create a new issue in a GitHub repository
  • create_pull_request: create a new pull request in a GitHub repository
  • list_commits: get the list of commits of a branch in a GitHub repository

and several more, covering branches, forks, code search and user search. Read through this list once before wiring a model up to it. Some of these tools, create_repository, push_files and create_pull_request among them, are not read only. Give a model access to them and it can create real repositories and push real commits on your behalf, so treat the credentials backing this server the same way you would treat any other automation with write access to your GitHub account.

Converting MCP tools to Kernel functions

A Semantic Kernel Kernel works with KernelFunction instances, not with MCP tools directly, so the next step is converting each discovered tool into a KernelFunction and registering it as a plugin.

kernel.Plugins.AddFromFunctions("GitHub", tools.Select(aiFunction => aiFunction.AsKernelFunction()));

The AsKernelFunction() extension method does the actual adaptation, and it was added to Semantic Kernel Core from version 1.44.0 onward. Try this on an older package version and the extension method simply will not exist, so upgrade first if you hit a compile error here. It looks like one line, but it is doing real work behind the scenes, mapping the MCP tool’s JSON schema for its parameters into the argument metadata Semantic Kernel needs for function calling.

Invoking the tools through function calling

With the GitHub tools registered as a plugin, they are indistinguishable from any other Semantic Kernel function as far as the model is concerned. Enable automatic function calling and send a prompt.

// Enable automatic function calling
OpenAIPromptExecutionSettings executionSettings = new()
{
    Temperature = 0,
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
};
 
// Test using GitHub tools
var prompt = "Summarize the last four commits to the microsoft/semantic-kernel repository?";
var result = await kernel.InvokePromptAsync(prompt, new(executionSettings)).ConfigureAwait(false);
Console.WriteLine($"\n\n{prompt}\n{result}");

Two things worth calling out here. Temperature is set to 0, which is sensible for tool calling scenarios, you want the model to be consistent about which tool it picks and what arguments it passes, not creative. Second, RetainArgumentTypes is set to true inside the FunctionChoiceBehavior options. Leave it out and Semantic Kernel may serialize arguments in a way that loses their original .NET type information, which can cause subtle failures when a tool expects a genuine integer or boolean rather than a string representation of one.

Running the prompt above against the live GitHub MCP server produces output along these lines, the actual commits will differ depending on when you run it:

Summarize the last four commits to the microsoft/semantic-kernel repository?
Here are the summaries of the last four commits to the microsoft/semantic-kernel repository:
 
1. Commit f8ee3ac by Mark Wallace on 2025-03-04:
   Title: .Net: Demo showing how to integrate MCP tools with Semantic Kernel
   Description: This commit introduces a demo for integrating MCP tools with the
   Semantic Kernel, including changes to ensure the code builds cleanly.
 
2. Commit 7c8dccc by Roger Barreto on 2025-03-04:
   Title: .Net: Add missing Ollama Connector Aspire Friendly Extensions
   Description: Addresses an issue by adding missing extensions for the Ollama
   Connector to ensure compatibility and functionality.
 
3. Commit 7787725 by dependabot[bot] on 2025-03-04:
   Title: Python: Bump google-cloud-aiplatform from 1.80.0 to 1.82.0
   Description: Updates the dependency, incorporating new features and fixes.
 
4. Commit 022f05e by Ross Smith on 2025-03-04:
   Title: .Net: dotnet format issues with SDK 9.0
   Description: Resolves formatting issues related to BOM encoding, addressing
   problems in ActivityExtensions.cs and removing access modifiers on interface members.

The model decided on its own to call list_commits, read the response, and turn four raw commit objects into a readable summary. That decision, which tool to call and how to interpret the result, is entirely a function calling outcome, nothing in the prompt hardcodes list_commits by name.

Invoking the same tools through an agent

The same Kernel, with the same MCP tools already registered, can be reused by a ChatCompletionAgent instead of calling InvokePromptAsync directly. This is worth doing once you move beyond a single one-off prompt and want conversational state, a persona, or multiple turns.

// Define the agent
ChatCompletionAgent agent =
    new()
    {
        Instructions = "Answer questions about GitHub repositories.",
        Name = "GitHubAgent",
        Kernel = kernel,
        Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true }) }),
    };
 
// Respond to user input, invoking functions where appropriate.
ChatMessageContent response = await agent.InvokeAsync("Summarize the last four commits to the microsoft/semantic-kernel repository?").FirstAsync();
Console.WriteLine($"\n\nResponse from GitHubAgent:\n{response.Content}");

Notice that the FunctionChoiceBehavior settings have to be repeated on the agent’s own Arguments. The Kernel already having the MCP tools registered as plugins is not enough by itself, the agent needs to be told separately to use function calling. Apart from that, nothing about this call differs from the earlier InvokePromptAsync example, same Kernel, same tools, same GitHub credentials underneath.

Practical considerations before you ship this pattern

Stdio transport starts the MCP server as a local child process on the same machine as your application. That is fine for a developer’s laptop or a controlled server environment, but it does not fit every deployment model. A containerized or serverless app may not have Node.js available, or may not permit spawning arbitrary child processes at all. For those cases, look at MCP’s HTTP based transports instead, which let the server run remotely rather than alongside your process.

Running npx -y @modelcontextprotocol/server-github means executing a third party npm package on your machine, and by default it fetches whatever version is currently published. Pin a specific version in production, and review what a community maintained MCP server actually does before pointing it at credentials with write access to your GitHub account. This is no different from vetting any other third party dependency, but it is easy to forget when a single line of code spins up an entire external tool server for you.

This guide also predates the Model Context Protocol C# SDK’s official support for authenticated servers. If you are connecting to a remote MCP server that requires OAuth rather than a local stdio server you control, you will need additional setup beyond what is shown here. Check the current ModelContextProtocol package documentation for whichever authentication flow it supports at the time you build this, since that part of the ecosystem has moved quickly since this sample was first published.

What the team was planning next

Two follow ups were flagged when this sample was published: consuming MCP prompts and resources, not just tools, from a Semantic Kernel application, and building an MCP server that exposes Semantic Kernel’s own functions, prompts and memory to other MCP clients. Worth checking the Semantic Kernel repository for progress on either if this pattern is central to what you are building.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading