MCP Server
Recently, I've showed you how to easily create MCP Server for the Microsoft Graph API in C# and how to use Claude for Desktop to communicate with MCP Server.
Claude for Desktop is more or less MCP Host that instantiates a MCP Client and communicates with the MCP Server.
With your own MCP Host you have more flexibility to call any AI models like Claude Sonnet or GPT. Let's see how to call Anthropic Claude API and Azure OpenAI programmatically.
Prerequisites
To be able to communicate with Anthropic Claude API and Azure OpenAI, you need API keys.
Anthropic Claude API
Go to the Anthropic Console and enter your email. You will receive an email with the secure link to log in to Anthropic Console.
Go to API keys settings and create a new API key.
Azure OpenAI service
Go to the Azure portal
Create a new Azure OpenAI resource. Once the resource is created, you can find the API key in the Keys and Endpoint section of the resource.
Go to Azure AI Foundry portal and deploy new AI model.
MCP Host
Now, create a new console application in C# and add the following NuGet packages:
- Anthropic.SDK - interacts with the Anthropic Claude API
- Azure.AI.OpenAI - interact with the Azure OpenAI service
- Azure.Identity - for Azure authentication
- Microsoft.Extensions.AI - utilities for AI components
- Microsoft.Extensions.Hosting - contains host builders
- ModelContextProtocol - the official C# SDK for the Model Context Protocol
- Microsoft.Extensions.AI.OpenAI - provides an implementation of the
IChatClient
interface for the OpenAI package and OpenAI-compatible endpoints.
Use latest preview versions for Azure.AI.OpenAI, Microsoft.Extensions.AI, ModelContextProtocol and Microsoft.Extensions.AI.OpenAI
AI services
Let's create services for the Anthropic Claude API and Azure OpenAI service.
internal abstract class AIService : IDisposable
{
protected IChatClient _client;
protected ChatOptions _options;
private bool disposedValue;
public async Task<ChatResponse> GetResponseAsync(string chatMessage)
{
return await _client.GetResponseAsync(chatMessage, _options);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_client?.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
The base class AIService
implements IDisposable
interface and contains the GetResponseAsync
method that will be used to send messages to AI model.
The IChatClient
and ChatOptions
are part of the Microsoft.Extensions.AI
namespace. The IChatClient
interface is used to send requests to AI model and the ChatOptions
class contains options for the requests.
Now, create a service class for the Anthropic Claude API.
internal class AnthropicService : AIService
{
public AnthropicService(IList<McpClientTool> tools)
{
_client = new AnthropicClient(new APIAuthentication(GetAnthropicApiKey()))
.Messages
.AsBuilder()
.UseFunctionInvocation()
.Build();
_options = new ChatOptions
{
MaxOutputTokens = 1000,
ModelId = "claude-3-5-sonnet-20241022",
Tools = [.. tools],
ToolMode = ChatToolMode.RequireAny
};
}
private static string GetAnthropicApiKey()
{
return "<anthropic_api_key>";
}
}
The AnthropicService
class inherits from the AIService
class and implements the constructor that creates a new instance of the AnthropicClient
class which is converted to the IChatClient
.
The parameter tools
is a list of tools exposed by the MCP Server(s).
A service class for the Azure OpenAI service is similar to the AnthropicService
class.
internal class AzureOpenAIService : AIService
{
public AzureOpenAIService(IList<McpClientTool> tools)
{
var endpoint = GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? "https://<your_endpoint>.openai.azure.com/";
var key = GetEnvironmentVariable("AZURE_OPENAI_KEY") ?? GetOpenAIApiKey();
var credential = new AzureKeyCredential(key);
var azureClient = new AzureOpenAIClient(new Uri(endpoint), credential);
_client = azureClient.GetOpenAIResponseClient("<deployment_name>")
.AsIChatClient()
.AsBuilder()
.UseFunctionInvocation()
.Build();
_options = new ChatOptions
{
Temperature = (float)0.7,
MaxOutputTokens = 800,
TopP = (float)0.95,
FrequencyPenalty = (float)0,
PresencePenalty = (float)0,
Tools = [.. tools],
ToolMode = ChatToolMode.RequireAny
};
}
private static string GetOpenAIApiKey()
{
return "<open_api_key>";
}
}
MCP Client
The last part is to create a MCP Client that will communicate with the MCP Server.
var clientTransport = new StdioClientTransport(new()
{
Name = "my-tenant-local",
Command = "dotnet",
Arguments = ["run", "--project", "path\to\mcp\server\console\project\folder", "--no-build"],
EnvironmentVariables = new Dictionary<string, string>()
{
{ "TENANT_ID", "<tenant_id>" },
{ "CLIENT_ID", "<client_id>" },
{ "CLIENT_SECRET", "<client_secret>" },
{ "NATIONAL_CLOUD", "Global" }
}
});
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, new()
{
ClientInfo = new Implementation
{
Name = "MCP Console App Client",
Version = "1.0.0"
}
});
Console.WriteLine($"Connected to server: {mcpClient.ServerInfo.Name} ({mcpClient.ServerInfo.Version})");
Because the server is running locally, we can use the StdioClientTransport
class, so the MCP client will communicate with the MCP Server via stdio.
If you want to communicate with the MCP Server via SSE, you can use the SseClientTransport
class.
Now, we can list available tools exposed by the MCP Server and instantiates the AnthropicService
and AzureOpenAIService
services.
var tools = await mcpClient.ListToolsAsync();
foreach (var tool in tools)
{
Console.WriteLine($"Tool: {tool.Name}, {tool.Description}");
}
using var anthropicService = new AnthropicService(tools);
using var azureService = new AzureOpenAIService(tools);
And the main loop
Console.WriteLine("Enter 1 for anthropic, 2 for azure (or 'exit' to quit):");
while (Console.ReadLine() is string input && !"exit".Equals(input, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Enter a message (or 'service' to change service or 'exit' to quit):");
while (Console.ReadLine() is string message && !"service".Equals(message, StringComparison.OrdinalIgnoreCase))
{
if (message.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
input = "exit";
break;
}
if (!string.IsNullOrWhiteSpace(message))
{
try
{
if (input == "1")
{
var chatResponse = await anthropicService.GetResponseAsync(message);
Console.Write(chatResponse);
}
else if (input == "2")
{
var chatResponse = await azureService.GetResponseAsync(message);
Console.Write(chatResponse);
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
Console.WriteLine();
}
Console.WriteLine("Enter a message (or 'service' to change service):");
}
if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
Console.WriteLine("Enter 1 for anthropic, 2 for azure (or 'exit' to quit):");
}
You can find the full code in my GitHub repository.
Conclusion
In this article, I showed you how to create a simple MCP Client that communicates with the MCP Server for the Microsoft Graph API and how to use it with the Anthropic Claude API and Azure OpenAI service.