Clients are custom applications or scripts that communicate directly with an MCP Server to request resources, tools, and prompts. Unlike using the inspector tool, which provides a graphical interface for interacting with the server, writing your own client allows for programmatic and automated interactions. This enables developers to integrate MCP capabilities into their own workflows, automate tasks, and build custom solutions tailored to specific needs.
This lesson introduces the concept of clients within the Model Context Protocol (MCP) ecosystem. You'll learn how to write your own client and have it connect to an MCP Server.
By the end of this lesson, you will be able to:
- Understand what a client can do.
- Write your own client.
- Connect and test the client with an MCP server to ensure the latter works as expected.
To write a client, you'll need to do the following:
- Import the correct libraries. You'll be using the same library as before, just different constructs.
- Instantiate a client. This will involve creating a client instance and connect it to the chosen transport method.
- Decide on what resources to list. Your MCP server comes with resources, tools and prompts, you need to decide which one to list.
- Integrate the client to a host application. Once you know the capabilities of the server you need to integrate this your host application so that if a user types a prompt or other command the corresponding server feature is invoked.
Now that we understand at high level what we're about to do, let's look at an example next.
Let's have a look at this example client:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"]
});
const client = new Client(
{
name: "example-client",
version: "1.0.0"
}
);
await client.connect(transport);
// List prompts
const prompts = await client.listPrompts();
// Get a prompt
const prompt = await client.getPrompt({
name: "example-prompt",
arguments: {
arg1: "value"
}
});
// List resources
const resources = await client.listResources();
// Read a resource
const resource = await client.readResource({
uri: "file:///example.txt"
});
// Call a tool
const result = await client.callTool({
name: "example-tool",
arguments: {
arg1: "value"
}
});In the preceding code we:
- Import the libraries
- Create an instance of a client and connect it using stdio for transport.
- List prompts, resources and tools and invoke them all.
There you have it, a client that can talk to an MCP Server.
Let's take our time in the next exercise section and break down each code snippet and explain what's going on.
As said above, let's take our time explaining the code, and by all means code along if you want.
Let's import the libraries we need, we will need references to a client and to our chosen transport protocol, stdio. stdio is a protocol for things meant to run on your local machine. SSE is another transport protocol we will show in future chapters but that's your other option. For now though, let's continue with stdio.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_clientusing Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;For Java, you'll create a client that connects to the MCP server from the previous exercise. Using the same Java Spring Boot project structure from Getting Started with MCP Server, create a new Java class called SDKClient in the src/main/java/com/microsoft/mcp/sample/client/ folder and add the following imports:
import java.util.Map;
import org.springframework.web.reactive.function.client.WebClient;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;Let's move on to instantiation.
We will need to create an instance of the transport and that of our client:
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"]
});
const client = new Client(
{
name: "example-client",
version: "1.0.0"
}
);
await client.connect(transport);In the preceding code we've:
-
Created an stdio transport instance. Note how it specifices command and args for how to find and start up the server as that's something we will need to do as we create the client.
const transport = new StdioClientTransport({ command: "node", args: ["server.js"] });
-
Instantiated a client by giving it a name and version.
const client = new Client( { name: "example-client", version: "1.0.0" });
-
Connected the client to the chosen transport.
await client.connect(transport);
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="mcp", # Executable
args=["run", "server.py"], # Optional command line arguments
env=None, # Optional environment variables
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write
) as session:
# Initialize the connection
await session.initialize()
if __name__ == "__main__":
import asyncio
asyncio.run(run())In the preceding code we've:
- Imported the needed libraries
- Instantiated a server parameters object as we will use this to run the server so we can connect to it with our client.
- Defined a method
runthat in turn callsstdio_clientwhich starts a client session. - Created an entry point where we provide the
runmethod toasyncio.run.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.AddEnvironmentVariables()
.AddUserSecrets<Program>();
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "dotnet",
Arguments = ["run", "--project", "path/to/file.csproj"],
});
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);
In the preceding code we've:
- Imported the needed libraries.
- Create an stdio transport and created a client
mcpClient. The latter is something we will use to list and invoke features on the MCP Server.
Note, in "Arguments", you can either point to the .csproj or to the executable.
public class SDKClient {
public static void main(String[] args) {
var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
new SDKClient(transport).run();
}
private final McpClientTransport transport;
public SDKClient(McpClientTransport transport) {
this.transport = transport;
}
public void run() {
var client = McpClient.sync(this.transport).build();
client.initialize();
// Your client logic goes here
}
}In the preceding code we've:
- Created a main method that sets up an SSE transport pointing to
http://localhost:8080where our MCP server will be running. - Created a client class that takes the transport as a constructor parameter.
- In the
runmethod, we create a synchronous MCP client using the transport and initialize the connection. - Used SSE (Server-Sent Events) transport which is suitable for HTTP-based communication with Java Spring Boot MCP servers.
Now, we have a client that can connect to should the program be run. However, it doesn't actually list its features so let's do that next:
// List prompts
const prompts = await client.listPrompts();
// List resources
const resources = await client.listResources();
// list tools
const tools = await client.listTools();# List available resources
resources = await session.list_resources()
print("LISTING RESOURCES")
for resource in resources:
print("Resource: ", resource)
# List available tools
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)Here we list the available resources, list_resources() and tools, list_tools and print them out.
foreach (var tool in await client.ListToolsAsync())
{
Console.WriteLine($"{tool.Name} ({tool.Description})");
}
Above is an example how we can list the tools on the server. For each tool, we then print out its name.
// List and demonstrate tools
ListToolsResult toolsList = client.listTools();
System.out.println("Available Tools = " + toolsList);
// You can also ping the server to verify connection
client.ping();In the preceding code we've:
- Called
listTools()to get all available tools from the MCP server. - Used
ping()to verify that the connection to the server is working. - The
ListToolsResultcontains information about all tools including their names, descriptions, and input schemas.
Great, now we've captures all the features. Now the question is when do we use them? Well, this client is pretty simple, simple in the sense that we will need to explicitly call the features when we want them. In the next chapter, we will create a more advanced client that has access to it's own large language model, LLM. For now though, let's see how we can invoke the features on the server:
To invoke the features we need to ensure we specify the correct arguments and in some cases the name of what we're trying to invoke.
// Read a resource
const resource = await client.readResource({
uri: "file:///example.txt"
});
// Call a tool
const result = await client.callTool({
name: "example-tool",
arguments: {
arg1: "value"
}
});
// call prompt
const promptResult = await client.getPrompt({
name: "review-code",
arguments: {
code: "console.log(\"Hello world\")"
}
})In the preceding code we:
-
Read a resource, we call the resource by calling
readResource()specifyinguri. Here's what it most likely look like on the server side:server.resource( "readFile", new ResourceTemplate("file://{name}", { list: undefined }), async (uri, { name }) => ({ contents: [{ uri: uri.href, text: `Hello, ${name}!` }] }) );
Our
urivaluefile://example.txtmatchesfile://{name}on the server.example.txtwill be mapped toname. -
Call a tool, we call it by specifying its
nameand itsargumentslike so:const result = await client.callTool({ name: "example-tool", arguments: { arg1: "value" } });
-
Get prompt, to get a prompt, you call
getPrompt()withnameandarguments. The server code looks like so:server.prompt( "review-code", { code: z.string() }, ({ code }) => ({ messages: [{ role: "user", content: { type: "text", text: `Please review this code:\n\n${code}` } }] }) );
and your resulting client code therefore looks like so to match what's declared on the server:
const promptResult = await client.getPrompt({ name: "review-code", arguments: { code: "console.log(\"Hello world\")" } })
# Read a resource
print("READING RESOURCE")
content, mime_type = await session.read_resource("greeting://hello")
# Call a tool
print("CALL TOOL")
result = await session.call_tool("add", arguments={"a": 1, "b": 7})
print(result.content)In the preceding code, we've:
- Called a resource called
greetingusingread_resource. - Invoked a tool called
addusingcall_tool.
- Let's add some code to call a tool:
var result = await mcpClient.CallToolAsync(
"Add",
new Dictionary<string, object?>() { ["a"] = 1, ["b"] = 3 },
cancellationToken:CancellationToken.None);- To print out the result, here's some code to handle that:
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
// Sum 4// Call various calculator tools
CallToolResult resultAdd = client.callTool(new CallToolRequest("add", Map.of("a", 5.0, "b", 3.0)));
System.out.println("Add Result = " + resultAdd);
CallToolResult resultSubtract = client.callTool(new CallToolRequest("subtract", Map.of("a", 10.0, "b", 4.0)));
System.out.println("Subtract Result = " + resultSubtract);
CallToolResult resultMultiply = client.callTool(new CallToolRequest("multiply", Map.of("a", 6.0, "b", 7.0)));
System.out.println("Multiply Result = " + resultMultiply);
CallToolResult resultDivide = client.callTool(new CallToolRequest("divide", Map.of("a", 20.0, "b", 4.0)));
System.out.println("Divide Result = " + resultDivide);
CallToolResult resultHelp = client.callTool(new CallToolRequest("help", Map.of()));
System.out.println("Help = " + resultHelp);In the preceding code we've:
- Called multiple calculator tools using
callTool()method withCallToolRequestobjects. - Each tool call specifies the tool name and a
Mapof arguments required by that tool. - The server tools expect specific parameter names (like "a", "b" for mathematical operations).
- Results are returned as
CallToolResultobjects containing the response from the server.
To run the client, type the following command in the terminal:
Add the following entry to your "scripts" section in package.json:
"client": "tsx && node build/client.js"npm run clientCall the client with the following command:
python client.pydotnet runFirst, ensure your MCP server is running on http://localhost:8080. Then run the client:
# Build you project
./mvnw clean compile
# Run the client
./mvnw exec:java -Dexec.mainClass="com.microsoft.mcp.sample.client.SDKClient"Alternatively, you can run the complete client project provided in the solution folder 03-GettingStarted\02-client\solution\java:
# Navigate to the solution directory
cd 03-GettingStarted/02-client/solution/java
# Build and run the JAR
./mvnw clean package
java -jar target/calculator-client-0.0.1-SNAPSHOT.jarIn this assignment, you'll use what you've learned in creating a client but create a client of your own.
Here's a server you can use that you need to call via your client code, see if you can add more features to the server to make it more interesting.
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create an MCP server
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// Add an addition tool
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// Add a dynamic greeting resource
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
// Start receiving messages on stdin and sending messages on stdout
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCPServer started on stdin/stdout");
}
main().catch((error) => {
console.error("Fatal error: ", error);
process.exit(1);
});# server.py
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Demo")
# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
[McpServerToolType]
public static class CalculatorTool
{
[McpServerTool, Description("Adds two numbers")]
public static string Add(int a, int b) => $"Sum {a + b}";
}See this project to see how you can add prompts and resources.
Also, check this link for how to invoke prompts and resources.
The solution folder contains complete, ready-to-run client implementations that demonstrate all the concepts covered in this tutorial. Each solution includes both client and server code organized in separate, self-contained projects.
The solution directory is organized by programming language:
solution/
├── typescript/ # TypeScript client with npm/Node.js setup
│ ├── package.json # Dependencies and scripts
│ ├── tsconfig.json # TypeScript configuration
│ └── src/ # Source code
├── java/ # Java Spring Boot client project
│ ├── pom.xml # Maven configuration
│ ├── src/ # Java source files
│ └── mvnw # Maven wrapper
├── python/ # Python client implementation
│ ├── client.py # Main client code
│ ├── server.py # Compatible server
│ └── README.md # Python-specific instructions
├── dotnet/ # .NET client project
│ ├── dotnet.csproj # Project configuration
│ ├── Program.cs # Main client code
│ └── dotnet.sln # Solution file
└── server/ # Additional .NET server implementation
├── Program.cs # Server code
└── server.csproj # Server project file
Each language-specific solution provides:
- Complete client implementation with all features from the tutorial
- Working project structure with proper dependencies and configuration
- Build and run scripts for easy setup and execution
- Detailed README with language-specific instructions
- Error handling and result processing examples
-
Navigate to your preferred language folder:
cd solution/typescript/ # For TypeScript cd solution/java/ # For Java cd solution/python/ # For Python cd solution/dotnet/ # For .NET
-
Follow the README instructions in each folder for:
- Installing dependencies
- Building the project
- Running the client
-
Example output you should see:
Prompt: Please review this code: console.log("hello"); Resource template: file Tool result: { content: [ { type: 'text', text: '9' } ] }
For complete documentation and step-by-step instructions, see: 📖 Solution Documentation
We've provided complete, working client implementations for all programming languages covered in this tutorial. These examples demonstrate the full functionality described above and can be used as reference implementations or starting points for your own projects.
| Language | File | Description |
|---|---|---|
| Java | client_example_java.java |
Complete Java client using SSE transport with comprehensive error handling |
| C# | client_example_csharp.cs |
Complete C# client using stdio transport with automatic server startup |
| TypeScript | client_example_typescript.ts |
Complete TypeScript client with full MCP protocol support |
| Python | client_example_python.py |
Complete Python client using async/await patterns |
Each complete example includes:
- ✅ Connection establishment and error handling
- ✅ Server discovery (tools, resources, prompts where applicable)
- ✅ Calculator operations (add, subtract, multiply, divide, help)
- ✅ Result processing and formatted output
- ✅ Comprehensive error handling
- ✅ Clean, documented code with step-by-step comments
- Choose your preferred language from the table above
- Review the complete example file to understand the full implementation
- Run the example following the instructions in
complete_examples.md - Modify and extend the example for your specific use case
For detailed documentation about running and customizing these examples, see: 📖 Complete Examples Documentation
| Solution Folder | Complete Examples |
|---|---|
| Full project structure with build files | Single-file implementations |
| Ready-to-run with dependencies | Focused code examples |
| Production-like setup | Educational reference |
| Language-specific tooling | Cross-language comparison |
Both approaches are valuable - use the solution folder for complete projects and the complete examples for learning and reference.
The key takeaways for this chapter is the following about clients:
- Can be used to both discover and invoke features on the server.
- Can start a server while it starts itself (like in this chapter) but clients can connect to running servers as well.
- Is a great way to test out server capabilities next to alternatives like the Inspector as was described in the previous chapter.