Getting Started
Getting Started
_(Click the image above to view video of this lesson)_
This section consists of several lessons:
Getting Started with MCP
Welcome to your first steps with the Model Context Protocol (MCP)!
Whether you're new to MCP or looking to deepen your understanding, this guide will walk you through the essential setup and development process.
You'll discover how MCP enables seamless integration between AI models and applications, and learn how to quickly get your environment ready for building and testing MCP-powered solutions.
> TLDR; If you build AI apps, you know that you can add tools and other resources to your LLM (large language model), to make the LLM more knowledgeable.
However if you place those tools and resources on a server, the app and the server capabilities can be used by any client with/without an LLM.
Overview
This lesson provides practical guidance on setting up MCP environments and building your first MCP applications.
You'll learn how to set up the necessary tools and frameworks, build basic MCP servers, create host applications, and test your implementations.
The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to LLMs.
Think of MCP like a USB-C port for AI applications - it provides a standardized way to connect AI models to different data sources and tools.
Learning Objectives
By the end of this lesson, you will be able to:
Setting Up Your MCP Environment
Before you begin working with MCP, it's important to prepare your development environment and understand the basic workflow. This section will guide you through the initial setup steps to ensure a smooth start with MCP.
Prerequisites
Before diving into MCP development, ensure you have:
Basic MCP Server Structure
An MCP server typically includes:
Here's a simplified example in TypeScript:
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(
"file",
// The 'list' parameter controls how the resource lists available files. Setting it to undefined disables listing for this resource.
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => ({
contents: [{
uri: uri.href,
text: `File, ${path}!`
}]
})
);
// Add a file resource that reads the file contents
server.resource(
"file",
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => {
let text;
try {
text = await fs.readFile(path, "utf8");
} catch (err) {
text = `Error reading file: ${err.message}`;
}
return {
contents: [{
uri: uri.href,
text
}]
};
}
);
server.prompt(
"review-code",
{ code: z.string() },
({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please review this code:\n\n${code}`
}
}]
})
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
In the preceding code we:
calculator) with a handler function.Testing and Debugging
Before you begin testing your MCP server, it's important to understand the available tools and best practices for debugging.
Effective testing ensures your server behaves as expected and helps you quickly identify and resolve issues.
The following section outlines recommended approaches for validating your MCP implementation.
MCP provides tools to help you test and debug your servers:
Using MCP Inspector
The MCP Inspector is a visual testing tool that helps you:
1. Discover Server Capabilities: Automatically detect available resources, tools, and prompts
2. Test Tool Execution: Try different parameters and see responses in real-time
3. View Server Metadata: Examine server info, schemas, and configurations
# ex TypeScript, installing and running MCP Inspector
npx @modelcontextprotocol/inspector node build/index.js
When you run the above commands, the MCP Inspector will launch a local web interface in your browser.
You can expect to see a dashboard displaying your registered MCP servers, their available tools, resources, and prompts.
The interface allows you to interactively test tool execution, inspect server metadata, and view real-time responses, making it easier to validate and debug your MCP server implementations.
Here's a screenshot of what it can look like:
Common Setup Issues and Solutions
Local Development
For local development and testing, you can run MCP servers directly on your machine:
1. Start the server process: Run your MCP server application
2. Configure networking: Ensure the server is accessible on the expected port
3. Connect clients: Use local connection URLs like http://localhost:3000
# Example: Running a TypeScript MCP server locally
npm run start
# Server running at http://localhost:3000
Building your first MCP Server
We've covered Core concepts in a previous lesson, now it's time to put that knowledge to work.
What a server can do
Before we start writing code, let's just remind ourselves what a server can do:
An MCP server can for example:
Great, now that we know what we can do for it, let's start coding.
Exercise: Creating a server
To create a server, you need to follow these steps:
-1- Create project
TypeScript
# Create project directory and initialize npm project
mkdir calculator-server
cd calculator-server
npm init -y
Python
# Create project dir
mkdir calculator-server
cd calculator-server
# Open the folder in Visual Studio Code - Skip this if you are using a different IDE
code .
.NET
dotnet new console -n McpCalculatorServer
cd McpCalculatorServer
Java
For Java, create a Spring Boot project:
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d javaVersion=21 \
-d type=maven-project \
-d groupId=com.example \
-d artifactId=calculator-server \
-d name=McpServer \
-d packageName=com.microsoft.mcp.sample.server \
-o calculator-server.zip
Extract the zip file:
unzip calculator-server.zip -d calculator-server
cd calculator-server
# optional remove the unused test
rm -rf src/test/java
Add the following complete configuration to your *pom.xml* file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Spring Boot parent for dependency management -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath />
</parent>
<!-- Project coordinates -->
<groupId>com.example</groupId>
<artifactId>calculator-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Calculator Server</name>
<description>Basic calculator MCP service for beginners</description>
<!-- Properties -->
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!-- Spring AI BOM for version management -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Dependencies -->
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Build configuration -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
<!-- Repositories for Spring AI snapshots -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>
Rust
mkdir calculator-server
cd calculator-server
cargo init
-2- Add dependencies
Now that you have your project created, let's add dependencies next:
TypeScript
# If not already installed, install TypeScript globally
npm install typescript -g
# Install the MCP SDK and Zod for schema validation
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
Python
# Create a virtual env and install dependencies
python -m venv venv
venv\Scripts\activate
pip install "mcp[cli]"
Java
cd calculator-server
./mvnw clean install -DskipTests
Rust
cargo add rmcp --features server,transport-io
cargo add serde
cargo add tokio --features rt-multi-thread
-3- Create project files
TypeScript
Open the *package.json* file and replace the content with the following to ensure you can build and run the server:
{
"name": "calculator-server",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "npm run build && node ./build/index.js",
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "A simple calculator server using Model Context Protocol",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.14",
"typescript": "^5.8.3"
}
}
Create a *tsconfig.json* with the following content:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Create a directory for your source code:
mkdir src
touch src/index.ts
Python
Create a file *server.py*
touch server.py
.NET
Install the required NuGet packages:
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
Java
For Java Spring Boot projects, the project structure is created automatically.
Rust
For Rust, a *src/main.rs* file is created by default when you run cargo init. Open the file and delete the default code.
-4- Create server code
TypeScript
Create a file *index.ts* and add the following code:
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: "Calculator MCP Server",
version: "1.0.0"
});
Now you have a server, but it doesn't do much, let' fix that.
Python
# server.py
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Demo")
.NET
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();
// add features
Java
For Java, create the core server components. First, modify the main application class:
*src/main/java/com/microsoft/mcp/sample/server/McpServerApplication.java*:
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
Create the calculator service *src/main/java/com/microsoft/mcp/sample/server/service/CalculatorService.java*:
package com.microsoft.mcp.sample.server.service;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
/**
* Service for basic calculator operations.
* This service provides simple calculator functionality through MCP.
*/
@Service
public class CalculatorService {
/**
* Add two numbers
* @param a The first number
* @param b The second number
* @return The sum of the two numbers
*/
@Tool(description = "Add two numbers together")
public String add(double a, double b) {
double result = a + b;
return formatResult(a, "+", b, result);
}
/**
* Subtract one number from another
* @param a The number to subtract from
* @param b The number to subtract
* @return The result of the subtraction
*/
@Tool(description = "Subtract the second number from the first number")
public String subtract(double a, double b) {
double result = a - b;
return formatResult(a, "-", b, result);
}
/**
* Multiply two numbers
* @param a The first number
* @param b The second number
* @return The product of the two numbers
*/
@Tool(description = "Multiply two numbers together")
public String multiply(double a, double b) {
double result = a * b;
return formatResult(a, "*", b, result);
}
/**
* Divide one number by another
* @param a The numerator
* @param b The denominator
* @return The result of the division
*/
@Tool(description = "Divide the first number by the second number")
public String divide(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a / b;
return formatResult(a, "/", b, result);
}
/**
* Calculate the power of a number
* @param base The base number
* @param exponent The exponent
* @return The result of raising the base to the exponent
*/
@Tool(description = "Calculate the power of a number (base raised to an exponent)")
public String power(double base, double exponent) {
double result = Math.pow(base, exponent);
return formatResult(base, "^", exponent, result);
}
/**
* Calculate the square root of a number
* @param number The number to find the square root of
* @return The square root of the number
*/
@Tool(description = "Calculate the square root of a number")
public String squareRoot(double number) {
if (number < 0) {
return "Error: Cannot calculate square root of a negative number";
}
double result = Math.sqrt(number);
return String.format("√%.2f = %.2f", number, result);
}
/**
* Calculate the modulus (remainder) of division
* @param a The dividend
* @param b The divisor
* @return The remainder of the division
*/
@Tool(description = "Calculate the remainder when one number is divided by another")
public String modulus(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a % b;
return formatResult(a, "%", b, result);
}
/**
* Calculate the absolute value of a number
* @param number The number to find the absolute value of
* @return The absolute value of the number
*/
@Tool(description = "Calculate the absolute value of a number")
public String absolute(double number) {
double result = Math.abs(number);
return String.format("|%.2f| = %.2f", number, result);
}
/**
* Get help about available calculator operations
* @return Information about available operations
*/
@Tool(description = "Get help about available calculator operations")
public String help() {
return "Basic Calculator MCP Service\n\n" +
"Available operations:\n" +
"1. add(a, b) - Adds two numbers\n" +
"2. subtract(a, b) - Subtracts the second number from the first\n" +
"3. multiply(a, b) - Multiplies two numbers\n" +
"4. divide(a, b) - Divides the first number by the second\n" +
"5. power(base, exponent) - Raises a number to a power\n" +
"6. squareRoot(number) - Calculates the square root\n" +
"7. modulus(a, b) - Calculates the remainder of division\n" +
"8. absolute(number) - Calculates the absolute value\n\n" +
"Example usage: add(5, 3) will return 5 + 3 = 8";
}
/**
* Format the result of a calculation
*/
private String formatResult(double a, String operator, double b, double result) {
return String.format("%.2f %s %.2f = %.2f", a, operator, b, result);
}
}
Optional components for a production-ready service:
Create a startup configuration *src/main/java/com/microsoft/mcp/sample/server/config/StartupConfig.java*:
package com.microsoft.mcp.sample.server.config;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StartupConfig {
@Bean
public CommandLineRunner startupInfo() {
return args -> {
System.out.println("\n" + "=".repeat(60));
System.out.println("Calculator MCP Server is starting...");
System.out.println("SSE endpoint: http://localhost:8080/sse");
System.out.println("Health check: http://localhost:8080/actuator/health");
System.out.println("=".repeat(60) + "\n");
};
}
}
Create a health controller *src/main/java/com/microsoft/mcp/sample/server/controller/HealthController.java*:
package com.microsoft.mcp.sample.server.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> healthCheck() {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("timestamp", LocalDateTime.now().toString());
response.put("service", "Calculator MCP Server");
return ResponseEntity.ok(response);
}
}
Create an exception handler *src/main/java/com/microsoft/mcp/sample/server/exception/GlobalExceptionHandler.java*:
package com.microsoft.mcp.sample.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
"Invalid_Input",
"Invalid input parameter: " + ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
public static class ErrorResponse {
private String code;
private String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
// Getters
public String getCode() { return code; }
public String getMessage() { return message; }
}
}
Create a custom banner *src/main/resources/banner.txt*:
_____ _ _ _
/ ____| | | | | | |
| | __ _| | ___ _ _| | __ _| |_ ___ _ __
| | / _` | |/ __| | | | |/ _` | __/ _ \| '__|
| |___| (_| | | (__| |_| | | (_| | || (_) | |
\_____\__,_|_|\___|\__,_|_|\__,_|\__\___/|_|
Calculator MCP Server v1.0
Spring Boot MCP Application
Rust
Add the following code to the top of the *src/main.rs* file. This imports the necessary libraries and modules for your MCP server.
use rmcp::{
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
ServerHandler, ServiceExt,
};
use std::error::Error;
The calculator server will be a simple one that can add two numbers together. Let's create a struct to represent the calculator request.
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
Next, create a struct to represent the calculator server. This struct will hold the tool router, which is used to register tools.
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
Now, we can implement the Calculator struct to create a new instance of the server and implement the server handler to provide server information.
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
Finally, we need to implement the main function to start the server.
This function will create an instance of the Calculator struct and serve it over standard input/output.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
The server is now set up to provide basic information about itself. Next, we will add a tool to perform addition.
-5- Adding a tool and a resource
Add a tool and a resource by adding the following code:
TypeScript
server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
Your tool takes parameters a and b and runs a function that produces a response on the form:
{
contents: [{
type: "text", content: "some content"
}]
}
Your resource is accessed through a string "greeting" and takes a parameter name and produces a similar response to the tool:
{
uri: "<href>",
text: "a text"
}
Python
# 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}!"
In the preceding code we've:
add that takes parameters a and b, both integers.greeting that takes parameter name..NET
Add this to your Program.cs file:
[McpServerToolType]
public static class CalculatorTool
{
[McpServerTool, Description("Adds two numbers")]
public static string Add(int a, int b) => $"Sum {a + b}";
}
Java
The tools have already been created in the previous step.
Rust
Add a new tool inside the impl Calculator block:
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
-6- Final code
Let's add the last code we need so the server can start:
TypeScript
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
Here's the full code:
// index.ts
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: "Calculator MCP Server",
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
const transport = new StdioServerTransport();
server.connect(transport);
Python
# 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}!"
# Main execution block - this is required to run the server
if __name__ == "__main__":
mcp.run()
.NET
Create a Program.cs file with the following content:
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}";
}
Java
Your complete main application class should look like this:
// McpServerApplication.java
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
Rust
The final code for the Rust server should look like this:
use rmcp::{
ServerHandler, ServiceExt,
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
};
use std::error::Error;
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
-7- Test the server
Start the server with the following command:
TypeScript
npm run build
Python
mcp run server.py
> To use MCP Inspector, use mcp dev server.py which automatically launches the Inspector and provides the required proxy session token.
If using mcp run server.py, you’ll need to manually start the Inspector and configure the connection.
.NET
Make sure you're in your project directory:
cd McpCalculatorServer
dotnet run
Java
./mvnw clean install -DskipTests
java -jar target/calculator-server-0.0.1-SNAPSHOT.jar
Rust
Run the following commands to format and run the server:
cargo fmt
cargo run
-8- Run using the inspector
The inspector is a great tool that can start up your server and lets you interact with it so you can test that it works. Let's start it up:
> [!NOTE]
> it might look different in the "command" field as it contains the command for running a server with your specific runtime/
TypeScript
npx @modelcontextprotocol/inspector node build/index.js
or add it to your *package.json* like so: "inspector": "npx @modelcontextprotocol/inspector node build/index.js" and then run npm run inspector
Python
Python wraps a Node.js tool called inspector. It's possible to call said tool like so:
mcp dev server.py
However, it doesn't implement all the methods available on the tool so you're recommended to run the Node.js tool directly like below:
npx @modelcontextprotocol/inspector mcp run server.py
If you're using a tool or IDE that allows you to configure commands and arguments for running scripts,
make sure to set python in the Command field and server.py as Arguments.
This ensures the script runs correctly.
.NET
Make sure you're in your project directory:
cd McpCalculatorServer
npx @modelcontextprotocol/inspector dotnet run
Java
Ensure you calculator server is running
The run the inspector:
npx @modelcontextprotocol/inspector
In the inspector web interface:
1. Select "SSE" as the transport type
2. Set the URL to: http://localhost:8080/sse
3. Click "Connect"
You're now connected to the server
The Java server testing section is completed now
The next section it's about interacting with the server.
You should see the following user interface:
1. Connect to the server by selecting the Connect button
Once you connect to the server, you should now see the following:
1. Select "Tools" and "listTools", you should see "Add" show up, select "Add" and fill in the parameter values.
You should see the following response, i.e a result from "add" tool:
Congrats, you've managed to create and run your first server!
Rust
To run the Rust server with the MCP Inspector CLI, use the following command:
npx @modelcontextprotocol/inspector cargo run --cli --method tools/call --tool-name add --tool-arg a=1 b=2
Official SDKs
MCP provides official SDKs for multiple languages:
Key Takeaways
Samples
Assignment
Create a simple MCP server with a tool of your choice:
1. Implement the tool in your preferred language (.NET, Java, Python, TypeScript, or Rust).
2. Define input parameters and return values.
3. Run the inspector tool to ensure the server works as intended.
4. Test the implementation with various inputs.
Solution
Additional Resources
What's next
Creating a client
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.
Overview
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.
Learning Objectives
By the end of this lesson, you will be able to:
What goes into writing a client?
To write a client, you'll need to do the following:
Now that we understand at high level what we're about to do, let's look at an example next.
An example client
Let's have a look at this example client:
TypeScript
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:
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.
Exercise: Writing a client
As said above, let's take our time explaining the code, and by all means code along if you want.
-1- Import the libraries
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.
TypeScript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
Python
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
.NET
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
Java
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;
Rust
You will need to add the following dependencies to your Cargo.toml file.
[package]
name = "calculator-client"
version = "0.1.0"
edition = "2024"
[dependencies]
rmcp = { version = "0.5.0", features = ["client", "transport-child-process"] }
serde_json = "1.0.141"
tokio = { version = "1.46.1", features = ["rt-multi-thread"] }
From there, you can import the necessary libraries in your client code.
use rmcp::{
RmcpError,
model::CallToolRequestParam,
service::ServiceExt,
transport::{ConfigureCommandExt, TokioChildProcess},
};
use tokio::process::Command;
Let's move on to instantiation.
-2- Instantiating client and transport
We will need to create an instance of the transport and that of our client:
TypeScript
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:
```typescript
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"]
});
```
```typescript
const client = new Client(
{
name: "example-client",
version: "1.0.0"
});
```
```typescript
await client.connect(transport);
```
Python
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:
run that in turn calls stdio_client which starts a client session.run method to asyncio.run..NET
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
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 McpClient.CreateAsync(clientTransport);
In the preceding code we've:
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.
Java
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:
http://localhost:8080 where our MCP server will be running.run method, we create a synchronous MCP client using the transport and initialize the connection.Rust
Note this Rust client assumes the server is a sibling project named "calculator-server" in the same directory. The code below will start the server and connect to it.
async fn main() -> Result<(), RmcpError> {
// Assume the server is a sibling project named "calculator-server" in the same directory
let server_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("failed to locate workspace root")
.join("calculator-server");
let client = ()
.serve(
TokioChildProcess::new(Command::new("cargo").configure(|cmd| {
cmd.arg("run").current_dir(server_dir);
}))
.map_err(RmcpError::transport_creation::<TokioChildProcess>)?,
)
.await?;
// TODO: Initialize
// TODO: List tools
// TODO: Call add tool with arguments = {"a": 3, "b": 2}
client.cancel().await?;
Ok(())
}
-3- Listing the server features
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:
TypeScript
// List prompts
const prompts = await client.listPrompts();
// List resources
const resources = await client.listResources();
// list tools
const tools = await client.listTools();
Python
# 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.
.NET
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.
Java
// 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:
listTools() to get all available tools from the MCP server.ping() to verify that the connection to the server is working.ListToolsResult contains 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:
Rust
In the main function, after initializing the client, we can initialize the server and list some of its features.
// Initialize
let server_info = client.peer_info();
println!("Server info: {:?}", server_info);
// List tools
let tools = client.list_tools(Default::default()).await?;
println!("Available tools: {:?}", tools);
-4- Invoke features
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.
TypeScript
// 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:
readResource() specifying uri. Here's what it most likely look like on the server side:```typescript
server.resource(
"readFile",
new ResourceTemplate("file://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: Hello, ${name}!
}]
})
);
```
Our uri value file://example.txt matches file://{name} on the server. example.txt will be mapped to name.
name and its arguments like so:```typescript
const result = await client.callTool({
name: "example-tool",
arguments: {
arg1: "value"
}
});
```
getPrompt() with name and arguments. The server code looks like so:```typescript
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:
```typescript
const promptResult = await client.getPrompt({
name: "review-code",
arguments: {
code: "console.log(\"Hello world\")"
}
})
```
Python
# 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:
greeting using read_resource.add using call_tool..NET
1. Let's add some code to call a tool:
```csharp
var result = await mcpClient.CallToolAsync(
"Add",
new Dictionary
cancellationToken:CancellationToken.None);
```
1. To print out the result, here's some code to handle that:
```csharp
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
// Sum 4
```
Java
// 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:
callTool() method with CallToolRequest objects.Map of arguments required by that tool.CallToolResult objects containing the response from the server.Rust
// Call add tool with arguments = {"a": 3, "b": 2}
let a = 3;
let b = 2;
let tool_result = client
.call_tool(CallToolRequestParam {
name: "add".into(),
arguments: serde_json::json!({ "a": a, "b": b }).as_object().cloned(),
})
.await?;
println!("Result of {:?} + {:?}: {:?}", a, b, tool_result);
-5- Run the client
To run the client, type the following command in the terminal:
TypeScript
Add the following entry to your "scripts" section in *package.json*:
"client": "tsc && node build/client.js"
npm run client
Python
Call the client with the following command:
python client.py
.NET
dotnet run
Java
First, 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.jar
Rust
cargo fmt
cargo run
Assignment
In 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.
TypeScript
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);
});
Python
# 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}!"
.NET
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.
Rust
In the previous section, you learned how to create a simple MCP server with Rust.
You can continue to build on that or check this link for more Rust-based MCP server examples: MCP Server Examples
Solution
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.
📁 Solution Structure
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
├── rust/ # Rust client implementation
| ├── Cargo.lock # Cargo lock file
| ├── Cargo.toml # Project configuration and dependencies
| ├── src # Source code
| │ └── main.rs # Main client code
└── server/ # Additional .NET server implementation
├── Program.cs # Server code
└── server.csproj # Server project file
🚀 What Each Solution Includes
Each language-specific solution provides:
📖 Using the Solutions
1. Navigate to your preferred language folder:
```bash
cd solution/typescript/ # For TypeScript
cd solution/java/ # For Java
cd solution/python/ # For Python
cd solution/dotnet/ # For .NET
```
2. Follow the README instructions in each folder for:
- Installing dependencies
- Building the project
- Running the client
3. Example output you should see:
```text
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 Here's the solutions for each runtime: You're recommended to install You should see a result similar to: You're recommended to install You should see an output similar to: This project demonstrates how to create a Java client that connects to and interacts with an MCP (Model Context Protocol) server. In this example, we'll connect to the calculator server from Chapter 01 and perform various mathematical operations. Before running this client, you need to: 1. Start the Calculator Server from Chapter 01: - Navigate to the calculator server directory: - Build and run the calculator server: ```cmd cd ..\01-first-server\solution\java .\mvnw clean install -DskipTests java -jar target\calculator-server-0.0.1-SNAPSHOT.jar ``` - The server should be running on 2. Java 21 or higher installed on your system 3. Maven (included via Maven Wrapper) The The client uses the Spring AI MCP framework to: 1. Establish Connection: Creates a WebFlux SSE client transport to connect to the calculator server 2. Initialize Client: Sets up the MCP client and establishes the connection 3. Discover Tools: Lists all available calculator operations 4. Execute Operations: Calls various mathematical functions with sample data 5. Display Results: Shows the results of each calculation The project uses the following key dependencies: This dependency provides: Build the project using the Maven wrapper: Note: Make sure the calculator server is running on When you run the client, it will: 1. Connect to the calculator server at 2. List Tools - Shows all available calculator operations 3. Perform Calculations: - Addition: 5 + 3 = 8 - Subtraction: 10 - 4 = 6 - Multiplication: 6 × 7 = 42 - Division: 20 ÷ 4 = 5 - Power: 2^8 = 256 - Square Root: √16 = 4 - Absolute Value: |-5.5| = 5.5 - Help: Shows available operations Note: You may see Maven warnings about lingering threads at the end - this is normal for reactive applications and doesn't indicate an error. This creates an SSE (Server-Sent Events) transport that connects to the calculator server. Creates a synchronous MCP client and initializes the connection. Calls the "add" tool with parameters a=5.0 and b=3.0. If you get connection errors, make sure the calculator server from Chapter 01 is running: Solution: Start the calculator server first. If port 8080 is busy: Solution: Stop other applications using port 8080 or modify the server to use a different port. If you encounter build errors:Running this sample
uv but it's not a must, see instructions-1- Install the dependencies
npm install
-3- Run the server
npm run build
-4- Run the client
npm run client
Prompt: {
type: 'text',
text: 'Please review this code:\n\nconsole.log("hello");'
}
Resource template: file
Tool result: { content: [ { type: 'text', text: '9' } ] }
Running this sample
uv but it's not a must, see instructions-0- Create a virtual environment
python -m venv venv
-1- Activate the virtual environment
venv\Scrips\activate
-2- Install the dependencies
pip install "mcp[cli]"
-3- Run the sample
python client.py
LISTING RESOURCES
Resource: ('meta', None)
Resource: ('nextCursor', None)
Resource: ('resources', [])
INFO Processing request of type ListToolsRequest server.py:534
LISTING TOOLS
Tool: add
READING RESOURCE
INFO Processing request of type ReadResourceRequest server.py:534
CALL TOOL
INFO Processing request of type CallToolRequest server.py:534
[TextContent(type='text', text='8', annotations=None)]
MCP Java Client - Calculator Demo
Prerequisites
03-GettingStarted/01-first-server/solution/java/http://localhost:8080What is the SDKClient?
SDKClient is a Java application that demonstrates how to:How It Works
Project Structure
src/
└── main/
└── java/
└── com/
└── microsoft/
└── mcp/
└── sample/
└── client/
└── SDKClient.java # Main client implementation
Key Dependencies
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
McpClient - The main client interfaceWebFluxSseClientTransport - SSE transport for web-based communicationBuilding the Project
.\mvnw clean install
Running the Client
java -jar .\target\calculator-client-0.0.1-SNAPSHOT.jar
http://localhost:8080 before executing any of these commands.What the Client Does
http://localhost:8080Expected Output
Available Tools = ListToolsResult[tools=[Tool[name=add, description=Add two numbers together, ...], ...]]
Add Result = CallToolResult[content=[TextContent[text="5,00 + 3,00 = 8,00"]], isError=false]
Subtract Result = CallToolResult[content=[TextContent[text="10,00 - 4,00 = 6,00"]], isError=false]
Multiply Result = CallToolResult[content=[TextContent[text="6,00 * 7,00 = 42,00"]], isError=false]
Divide Result = CallToolResult[content=[TextContent[text="20,00 / 4,00 = 5,00"]], isError=false]
Power Result = CallToolResult[content=[TextContent[text="2,00 ^ 8,00 = 256,00"]], isError=false]
Square Root Result = CallToolResult[content=[TextContent[text="√16,00 = 4,00"]], isError=false]
Absolute Result = CallToolResult[content=[TextContent[text="|-5,50| = 5,50"]], isError=false]
Help = CallToolResult[content=[TextContent[text="Basic Calculator MCP Service\n\nAvailable operations:\n1. add(a, b) - Adds two numbers\n2. subtract(a, b) - Subtracts the second number from the first\n..."]], isError=false]
Understanding the Code
1. Transport Setup
var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
2. Client Creation
var client = McpClient.sync(this.transport).build();
client.initialize();
3. Calling Tools
CallToolResult resultAdd = client.callTool(new CallToolRequest("add", Map.of("a", 5.0, "b", 3.0)));
Troubleshooting
Server Not Running
Error: Connection refused
Port Already in Use
Error: Address already in use
Build Errors
.\mvnw clean install -DskipTests
Learn More
🎯 Complete Examples
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.
Available Complete Examples
client_example_java.javaclient_example_csharp.csclient_example_typescript.tsclient_example_python.pyclient_example_rust.rsEach complete example includes:
Getting Started with Complete Examples
1. Choose your preferred language from the table above
2. Review the complete example file to understand the full implementation
3. Run the example following the instructions in complete_examples.md
4. Modify and extend the example for your specific use case
For detailed documentation about running and customizing these examples, see: 📖 Complete Examples Documentation
💡 Solution vs. Complete Examples
Both approaches are valuable - use the solution folder for complete projects and the complete examples for learning and reference.
Key Takeaways
The key takeaways for this chapter is the following about clients:
Additional Resources
Samples
What's Next
Creating a client with LLM
So far, you've seen how to create a server and a client.
The client has been able to call the server explicitly to list its tools, resources, and prompts.
However, this is not a very practical approach.
Your users live in the agentic era and expect to use prompts and communicate with an LLM instead.
They do not care whether you use MCP to store your capabilities; they simply expect to interact using natural language.
So how do we solve this?
The solution is to add an LLM to the client.
Overview
In this lesson we focus on adding an LLM to do your client and show how this provides a much better experience for your user.
Learning Objectives
By the end of this lesson, you will be able to:
Approach
Let's try to understand the approach we need to take. Adding an LLM sounds simple, but will we actually do this?
Here's how the client will interact with the server:
1. Establish connection with server.
1. List capabilities, prompts, resources and tools, and save down their schema.
1. Add an LLM and pass the saved capabilities and their schema in a format the LLM understands.
1. Handle a user prompt by passing it to the LLM together with the tools listed by the client.
Great, now we understand how we can do this at high level, let's try this out in below exercise.
Exercise: Creating a client with an LLM
In this exercise, we will learn to add an LLM to our client.
Authentication using GitHub Personal Access Token
Creating a GitHub token is a straightforward process. Here’s how you can do it:
-1- Connect to server
Let's create our client first:
TypeScript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // Import zod for schema validation
class MCPClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com",
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
}
In the preceding code we've:
client and openai that will help us manage a client and interact with an LLM respectively.baseUrl to point to the inference API.Python
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:
.NET
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using System.Text.Json;
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
Java
First, you'll need to add the LangChain4j dependencies to your pom.xml file. Add these dependencies to enable MCP integration and GitHub Models support:
<properties>
<langchain4j.version>1.0.0-beta3</langchain4j.version>
</properties>
<dependencies>
<!-- LangChain4j MCP Integration -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI Official API Client -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-official</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- GitHub Models Support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- Spring Boot Starter (optional, for production apps) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Then create your Java client class:
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
import java.time.Duration;
import java.util.List;
public class LangChain4jClient {
public static void main(String[] args) throws Exception { // Configure the LLM to use GitHub Models
ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.build();
// Create MCP transport for connecting to server
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
// Create MCP client
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
}
}
In the preceding code we've:
ChatLanguageModel: Configured to use GitHub Models with your GitHub tokenRust
This example assumes you have a Rust based MCP server running. If you don't have one, refer back to the 01-first-server lesson to create the server.
Once you have your Rust MCP server, open a terminal and navigate to the same directory as the server. Then run the following command to create a new LLM client project:
mkdir calculator-llmclient
cd calculator-llmclient
cargo init
Add the following dependencies to your Cargo.toml file:
[dependencies]
async-openai = { version = "0.29.0", features = ["byot"] }
rmcp = { version = "0.5.0", features = ["client", "transport-child-process"] }
serde_json = "1.0.141"
tokio = { version = "1.46.1", features = ["rt-multi-thread"] }
> [!NOTE]
> There isn't an official Rust library for OpenAI, however, the async-openai crate is a community maintained library that is commonly used.
Open the src/main.rs file and replace its content with the following code:
use async_openai::{Client, config::OpenAIConfig};
use rmcp::{
RmcpError,
model::{CallToolRequestParam, ListToolsResult},
service::{RoleClient, RunningService, ServiceExt},
transport::{ConfigureCommandExt, TokioChildProcess},
};
use serde_json::{Value, json};
use std::error::Error;
use tokio::process::Command;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Initial message
let mut messages = vec![json!({"role": "user", "content": "What is the sum of 3 and 2?"})];
// Setup OpenAI client
let api_key = std::env::var("OPENAI_API_KEY")?;
let openai_client = Client::with_config(
OpenAIConfig::new()
.with_api_base("https://models.github.ai/inference/chat")
.with_api_key(api_key),
);
// Setup MCP client
let server_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("calculator-server");
let mcp_client = ()
.serve(
TokioChildProcess::new(Command::new("cargo").configure(|cmd| {
cmd.arg("run").current_dir(server_dir);
}))
.map_err(RmcpError::transport_creation::<TokioChildProcess>)?,
)
.await?;
// TODO: Get MCP tool listing
// TODO: LLM conversation with tool calls
Ok(())
}
This code sets up a basic Rust application that will connect to an MCP server and GitHub Models for LLM interactions.
> [!IMPORTANT]
> Make sure to set the OPENAI_API_KEY environment variable with your GitHub token before running the application.
Great, for our next step, let's list the capabilities on the server.
-2- List server capabilities
Now we will connect to the server and ask for its capabilities:
Typescript
In the same class, add the following methods:
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
async run() {
console.log("Asking server for available tools");
// listing tools
const toolsResult = await this.client.listTools();
}
In the preceding code we've:
connectToServer.run method responsible for handling our app flow. So far it only lists the tools but we will add more to it shortly.Python
# 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)
print("Tool", tool.inputSchema["properties"])
Here's what we added:
inputSchema which we use later..NET
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
// TODO: convert tool definition from MCP tool to LLm tool
}
return toolDefinitions;
}
In the preceding code we've:
Java
// Create a tool provider that automatically discovers MCP tools
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
// The MCP tool provider automatically handles:
// - Listing available tools from the MCP server
// - Converting MCP tool schemas to LangChain4j format
// - Managing tool execution and responses
In the preceding code we've:
McpToolProvider that automatically discovers and registers all tools from the MCP serverRust
Retrieving tools from the MCP server is done using the list_tools method.
In your main function, after setting up the MCP client, add the following code:
// Get MCP tool listing
let tools = mcp_client.list_tools(Default::default()).await?;
-3- Convert server capabilities to LLM tools
Next step after listing server capabilities is to convert them into a format that the LLM understands. Once we do that, we can provide these capabilities as tools to our LLM.
TypeScript
1. Add the following code to convert response from MCP Server to a tool format the LLM can use:
```typescript
openAiToolAdapter(tool: {
name: string;
description?: string;
input_schema: any;
}) {
// Create a zod schema based on the input_schema
const schema = z.object(tool.input_schema);
return {
type: "function" as const, // Explicitly set type to "function"
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.input_schema.properties,
required: tool.input_schema.required,
},
},
};
}
```
The above code takes a response from the MCP Server and converts that to a tool definition format the LLM can understand.
2. Let's update the run method next to list server capabilities:
```typescript
async run() {
console.log("Asking server for available tools");
const toolsResult = await this.client.listTools();
const tools = toolsResult.tools.map((tool) => {
return this.openAiToolAdapter({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
});
});
}
```
In the preceding code, we've update the run method to map through the result and for each entry call openAiToolAdapter.
Python
1. First, let's create the following converter function
```python
def convert_to_llm_tool(tool):
tool_schema = {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"type": "function",
"parameters": {
"type": "object",
"properties": tool.inputSchema["properties"]
}
}
}
return tool_schema
```
In the function above convert_to_llm_tools we take an MCP tool response and convert it to a format that the LLM can understand.
2. Next, let's update our client code to leverage this function like so:
```python
functions = []
for tool in tools.tools:
print("Tool: ", tool.name)
print("Tool", tool.inputSchema["properties"])
functions.append(convert_to_llm_tool(tool))
```
Here, we're adding a call to convert_to_llm_tool to convert the MCP tool response to something we can feed the LLM later.
.NET
1. Let's add code to convert the MCP tool response to something the LLM can understand
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}
In the preceding code we've:
ConvertFrom that takes, name, description and input schema.2. Let's see how we can update some existing code to take advantage of this function above:
```csharp
async Task> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
Console.WriteLine($"Properties: {propertiesElement}");
}
return toolDefinitions;
}
``` In the preceding code, we've:
- Update the function to convert the MCP tool response to an LLm tool. Let's highlight the code we added:
```csharp
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
```
The input schema is part of the tool response but on the "properties" attribute, so we need to extract.
Furthermore, we now call ConvertFrom with the tool details.
Now we've done the heavy lifting, let's see how it call comes together as we handle a user prompt next.
Java
// Create a Bot interface for natural language interaction
public interface Bot {
String chat(String prompt);
}
// Configure the AI service with LLM and MCP tools
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();
In the preceding code we've:
Bot interface for natural language interactionsAiServices to automatically bind the LLM with the MCP tool providerRust
To convert the MCP tool response to a format that the LLM can understand, we will add a helper function that formats the tools listing.
Add the following code to your main.rs file below the main function.
This will be called when making requests to the LLM:
async fn format_tools(tools: &ListToolsResult) -> Result<Vec<Value>, Box<dyn Error>> {
let tools_json = serde_json::to_value(tools)?;
let Some(tools_array) = tools_json.get("tools").and_then(|t| t.as_array()) else {
return Ok(vec![]);
};
let formatted_tools = tools_array
.iter()
.filter_map(|tool| {
let name = tool.get("name")?.as_str()?;
let description = tool.get("description")?.as_str()?;
let schema = tool.get("inputSchema")?;
Some(json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": {
"type": "object",
"properties": schema.get("properties").unwrap_or(&json!({})),
"required": schema.get("required").unwrap_or(&json!([]))
}
}
}))
})
.collect();
Ok(formatted_tools)
}
Great, we're not set up to handle any user requests, so let's tackle that next.
-4- Handle user prompt request
In this part of the code, we will handle user requests.
TypeScript
1. Add a method that will be used to call our LLM:
```typescript
async callTools(
tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
toolResults: any[]
) {
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(Calling tool ${toolName} with args ${JSON.stringify(args)});
// 2. Call the server's tool
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. Do something with the result
// TODO
}
}
```
In the preceding code we:
- Added a method callTools.
- The method takes an LLM response and checks to see what tools have been called, if any:
```typescript
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(Calling tool ${toolName} with args ${JSON.stringify(args)});
// call tool
}
```
- Calls a tool, if LLM indicates it should be called:
```typescript
// 2. Call the server's tool
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. Do something with the result
// TODO
```
2. Update the run method to include calls to the LLM and calling callTools:
```typescript
// 1. Create messages that's input for the LLM
const prompt = "What is the sum of 2 and 3?"
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "user",
content: prompt,
},
];
console.log("Querying LLM: ", messages[0].content);
// 2. Calling the LLM
let response = this.openai.chat.completions.create({
model: "gpt-4.1-mini",
max_tokens: 1000,
messages,
tools: tools,
});
let results: any[] = [];
// 3. Go through the LLM response,for each choice, check if it has tool calls
(await response).choices.map(async (choice: { message: any; }) => {
const message = choice.message;
if (message.tool_calls) {
console.log("Making tool call")
await this.callTools(message.tool_calls, results);
}
});
```
Great, let's list the code in full:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // Import zod for schema validation
class MyClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com", // might need to change to this url in the future: https://models.github.ai/inference
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
openAiToolAdapter(tool: {
name: string;
description?: string;
input_schema: any;
}) {
// Create a zod schema based on the input_schema
const schema = z.object(tool.input_schema);
return {
type: "function" as const, // Explicitly set type to "function"
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.input_schema.properties,
required: tool.input_schema.required,
},
},
};
}
async callTools(
tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
toolResults: any[]
) {
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`);
// 2. Call the server's tool
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. Do something with the result
// TODO
}
}
async run() {
console.log("Asking server for available tools");
const toolsResult = await this.client.listTools();
const tools = toolsResult.tools.map((tool) => {
return this.openAiToolAdapter({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
});
});
const prompt = "What is the sum of 2 and 3?";
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "user",
content: prompt,
},
];
console.log("Querying LLM: ", messages[0].content);
let response = this.openai.chat.completions.create({
model: "gpt-4.1-mini",
max_tokens: 1000,
messages,
tools: tools,
});
let results: any[] = [];
// 3. Go through the LLM response, for each choice, check if it has tool calls
(await response).choices.map(async (choice: { message: any; }) => {
const message = choice.message;
if (message.tool_calls) {
console.log("Making tool call")
await this.callTools(message.tool_calls, results);
}
});
}
}
let client = new MyClient();
const transport = new StdioClientTransport({
command: "node",
args: ["./build/index.js"]
});
client.connectToServer(transport);
Python
1. Let's add some imports needed to call an LLM
```python
# llm
import os
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import SystemMessage, UserMessage
from azure.core.credentials import AzureKeyCredential
import json
```
2. Next, let's add the function that will call the LLM:
```python
# llm
def call_llm(prompt, functions):
token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o"
client = ChatCompletionsClient(
endpoint=endpoint,
credential=AzureKeyCredential(token),
)
print("CALLING LLM")
response = client.complete(
messages=[
{
"role": "system",
"content": "You are a helpful assistant.",
},
{
"role": "user",
"content": prompt,
},
],
model=model_name,
tools = functions,
# Optional parameters
temperature=1.,
max_tokens=1000,
top_p=1.
)
response_message = response.choices[0].message
functions_to_call = []
if response_message.tool_calls:
for tool_call in response_message.tool_calls:
print("TOOL: ", tool_call)
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
functions_to_call.append({ "name": name, "args": args })
return functions_to_call
```
In the preceding code we've:
- Passed our functions, that we found on the MCP server and converted, to the LLM.
- Then we called the LLM with said functions.
- Then, we're inspecting the result to see what functions we should call, if any.
- Finally, we pass an array of functions to call.
3. Final step, let's update our main code:
```python
prompt = "Add 2 to 20"
# ask LLM what tools to all, if any
functions_to_call = call_llm(prompt, functions)
# call suggested functions
for f in functions_to_call:
result = await session.call_tool(f["name"], arguments=f["args"])
print("TOOLS result: ", result.content)
```
There, that was the final step, in the code above we're:
- Calling an MCP tool via call_tool using a function that the LLM thought we should call based on our prompt.
- Printing the result of the tool call to the MCP Server.
.NET
1. Let's show some code for doing an LLM prompt request:
```csharp
var tools = await GetMcpTools();
for (int i = 0; i < tools.Count; i++)
{
var tool = tools[i];
Console.WriteLine($"MCP Tools def: {i}: {tool}");
}
// 0. Define the chat history and the user message
var userMessage = "add 2 and 4";
chatHistory.Add(new ChatRequestUserMessage(userMessage));
// 1. Define tools
ChatCompletionsToolDefinition def = CreateToolDefinition();
// 2. Define options, including the tools
var options = new ChatCompletionsOptions(chatHistory)
{
Model = "gpt-4.1-mini",
Tools = { tools[0] }
};
// 3. Call the model
ChatCompletions? response = await client.CompleteAsync(options);
var content = response.Content;
```
In the preceding code we've:
- Fetched tools from the MCP server, var tools = await GetMcpTools().
- Defined a user prompt userMessage.
- Constructor an options object specifying model and tools.
- Made a request towards the LLM.
2. One last step, let's see if the LLM thinks we should call a function:
```csharp
// 4. Check if the response contains a function call
ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault();
for (int i = 0; i < response.ToolCalls.Count; i++)
{
var call = response.ToolCalls[i];
Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}");
//Tool call 0: add with arguments {"a":2,"b":4}
var dict = JsonSerializer.Deserialize
var result = await mcpClient.CallToolAsync(
call.Name,
dict!,
cancellationToken: CancellationToken.None
);
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
}
```
In the preceding code we've:
- Looped through a list of function calls.
- For each tool call, parse out name and arguments and call the tool on the MCP server using the MCP client. Finally we print the results.
Here's the code in full:
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
var endpoint = "https://models.inference.ai.azure.com";
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); // Your GitHub Access Token
var client = new ChatCompletionsClient(new Uri(endpoint), new AzureKeyCredential(token));
var chatHistory = new List<ChatRequestMessage>
{
new ChatRequestSystemMessage("You are a helpful assistant that knows about AI")
};
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
Console.WriteLine("Setting up stdio transport");
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
Console.WriteLine($"Properties: {propertiesElement}");
}
return toolDefinitions;
}
// 1. List tools on mcp server
var tools = await GetMcpTools();
for (int i = 0; i < tools.Count; i++)
{
var tool = tools[i];
Console.WriteLine($"MCP Tools def: {i}: {tool}");
}
// 2. Define the chat history and the user message
var userMessage = "add 2 and 4";
chatHistory.Add(new ChatRequestUserMessage(userMessage));
// 3. Define options, including the tools
var options = new ChatCompletionsOptions(chatHistory)
{
Model = "gpt-4.1-mini",
Tools = { tools[0] }
};
// 4. Call the model
ChatCompletions? response = await client.CompleteAsync(options);
var content = response.Content;
// 5. Check if the response contains a function call
ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault();
for (int i = 0; i < response.ToolCalls.Count; i++)
{
var call = response.ToolCalls[i];
Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}");
//Tool call 0: add with arguments {"a":2,"b":4}
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.Arguments);
var result = await mcpClient.CallToolAsync(
call.Name,
dict!,
cancellationToken: CancellationToken.None
);
Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text);
}
// 6. Print the generic response
Console.WriteLine($"Assistant response: {content}");
Java
try {
// Execute natural language requests that automatically use MCP tools
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}
In the preceding code we've:
- Converting user prompts to tool calls when needed
- Calling the appropriate MCP tools based on the LLM's decision
- Managing the conversation flow between the LLM and MCP server
bot.chat() method returns natural language responses that may include results from MCP tool executionsComplete code example:
public class LangChain4jClient {
public static void main(String[] args) throws Exception { ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.timeout(Duration.ofSeconds(60))
.build();
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();
try {
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}
}
}
Rust
Here is where the majority of the work happens.
We will call the LLM with the initial user prompt, then process the response to see if any tools need to be called.
If so, we will call those tools and continue the conversation with the LLM until no more tool calls are needed and we have a final response.
We will be making multiple calls to the LLM, so let's define a function that will handle the LLM call. Add the following function to your main.rs file:
async fn call_llm(
client: &Client<OpenAIConfig>,
messages: &[Value],
tools: &ListToolsResult,
) -> Result<Value, Box<dyn Error>> {
let response = client
.completions()
.create_byot(json!({
"messages": messages,
"model": "openai/gpt-4.1",
"tools": format_tools(tools).await?,
}))
.await?;
Ok(response)
}
This function takes the LLM client, a list of messages (including the user prompt), tools from the MCP server, and sends a request to the LLM, returning the response.
The response from the LLM will contain an array of choices.
We will need to process the result to see if any tool_calls are present.
This let's us know the LLM is requesting a specific tool should be called with arguments.
Add the following code to the bottom of your main.rs file to define a function to handle the LLM response:
async fn process_llm_response(
llm_response: &Value,
mcp_client: &RunningService<RoleClient, ()>,
openai_client: &Client<OpenAIConfig>,
mcp_tools: &ListToolsResult,
messages: &mut Vec<Value>,
) -> Result<(), Box<dyn Error>> {
let Some(message) = llm_response
.get("choices")
.and_then(|c| c.as_array())
.and_then(|choices| choices.first())
.and_then(|choice| choice.get("message"))
else {
return Ok(());
};
// Print content if available
if let Some(content) = message.get("content").and_then(|c| c.as_str()) {
println!("🤖 {}", content);
}
// Handle tool calls
if let Some(tool_calls) = message.get("tool_calls").and_then(|tc| tc.as_array()) {
messages.push(message.clone()); // Add assistant message
// Execute each tool call
for tool_call in tool_calls {
let (tool_id, name, args) = extract_tool_call_info(tool_call)?;
println!("⚡ Calling tool: {}", name);
let result = mcp_client
.call_tool(CallToolRequestParam {
name: name.into(),
arguments: serde_json::from_str::<Value>(&args)?.as_object().cloned(),
})
.await?;
// Add tool result to messages
messages.push(json!({
"role": "tool",
"tool_call_id": tool_id,
"content": serde_json::to_string_pretty(&result)?
}));
}
// Continue conversation with tool results
let response = call_llm(openai_client, messages, mcp_tools).await?;
Box::pin(process_llm_response(
&response,
mcp_client,
openai_client,
mcp_tools,
messages,
))
.await?;
}
Ok(())
}
If tool_calls are present, it extracts the tool information, calls the MCP server with the tool request, and adds the results to the conversation messages.
It then continues the conversation with the LLM and the messages are updated with the assistant's response and tool call results.
To extract tool call information that the LLM returns for MCP calls, we will add another helper function to extract everything needed to make the call.
Add the following code to the bottom of your main.rs file:
fn extract_tool_call_info(tool_call: &Value) -> Result<(String, String, String), Box<dyn Error>> {
let tool_id = tool_call
.get("id")
.and_then(|id| id.as_str())
.unwrap_or("")
.to_string();
let function = tool_call.get("function").ok_or("Missing function")?;
let name = function
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string();
let args = function
.get("arguments")
.and_then(|a| a.as_str())
.unwrap_or("{}")
.to_string();
Ok((tool_id, name, args))
}
With all the pieces in place, we can now handle the initial user prompt and call the LLM. Update your main function to include the following code:
// LLM conversation with tool calls
let response = call_llm(&openai_client, &messages, &tools).await?;
process_llm_response(
&response,
&mcp_client,
&openai_client,
&tools,
&mut messages,
)
.await?;
This will query the LLM with the initial user prompt asking for the sum of two numbers, and it will process the response to dynamically handle tool calls.
Great, you did it!
Assignment
Take the code from the exercise and build out the server with some more tools.
Then create a client with an LLM, like in the exercise, and test it out with different prompts to make sure all your server tools gets called dynamically.
This way of building a client means the end user will have a great user experience as they're able to use prompts, instead of exact client commands, and be oblivious to any MCP server being called.
Solution
Key Takeaways
Samples
Additional Resources
What's Next
Consuming a server from GitHub Copilot Agent mode
Visual Studio Code and GitHub Copilot can act as a client and consume an MCP Server.
Why would we want to do that you might ask?
Well, that means whatever features the MCP Server has can now be used from within your IDE.
Imagine you adding for example GitHub's MCP server, this would allow for controlling GitHub via prompts over typing specific commands in the terminal.
Or imagine anything in general that could improve your developer experience all controlled by natural language.
Now you start to see the win right?
Overview
This lesson covers how to use Visual Studio Code and GitHub Copilot's Agent mode as a client for your MCP Server.
Learning Objectives
By the end of this lesson, you will be able to:
Usage
You can control your MCP server in two different ways:
code exectuable:To add an MCP server to your user profile, use the --add-mcp command line option, and provide the JSON server configuration in the form {\"name\":\"server-name\",\"command\":...}.
```
code --add-mcp "{\"name\":\"my-server\",\"command\": \"uvx\",\"args\": [\"mcp-server-fetch\"]}"
```
Screenshots
Let's talk more about how we use the visual interface in the next sections.
Approach
Here's how we need to approach this at high level:
Great, now that we understand the flow, let's try use an MCP Server through Visual Studio Code through an exercise.
Exercise: Consuming a server
In this exercise, we will configure Visual Studio Code to find your MCP server so that it can be used from GitHub Copilot Chat interface.
-0- Prestep, enable MCP Server discovery
You may need to enable discovery of MCP Servers.
1. Go to File -> Preferences -> Settings in Visual Studio Code.
1. Search for "MCP" and enable chat.mcp.discovery.enabled in the settings.json file.
-1- Create config file
Start by creating a config file in your project root, you will need a file called MCP.json and to place it in a folder called .vscode. It should look like so:
.vscode
|-- mcp.json
Next, let's see how we can add a server entry.
-2- Configure a server
Add the following content to *mcp.json*:
{
"inputs": [],
"servers": {
"hello-mcp": {
"command": "node",
"args": [
"build/index.js"
]
}
}
}
Here's a simple example above how to start a server written in Node.js, for other runtimes point out the proper command for starting the server using command and args.
-3- Start the server
Now that you've added an entry, let's start the server:
1. Locate your entry in *mcp.json* and make sure you find the "play" icon:
!Starting server in Visual Studio Code
1.
Click the "play" icon, you should see the tools icon in the GitHub Copilot Chat increase the number of available tools.
If you click said tools icon, you will see a list of registered tools.
You can check/uncheck each tool depending if you want GitHub Copilot to use them as context:
!Starting server in Visual Studio Code
1. To run a tool, type a prompt that you know will match the description of one of your tools, for example a prompt like so "add 22 to 1":
!Running a tool from GitHub Copilot
You should see a response saying 23.
Assignment
Try adding a server entry to your *mcp.json* file and make sure you can start/stop the server. Make sure you can also communicate with the tools on your server via GitHub Copilot Chat interface.
Solution
Key Takeaways
The takeaways from this chapter is the following:
Samples
Additional Resources
What's Next
MCP Server with stdio Transport
> ⚠️ Important Update: As of MCP Specification 2025-06-18, the standalone SSE (Server-Sent Events) transport has been deprecated and replaced by "Streamable HTTP" transport.
The current MCP specification defines two primary transport mechanisms:
> 1. stdio - Standard input/output (recommended for local servers)
> 2. Streamable HTTP - For remote servers that may use SSE internally
>
> This lesson has been updated to focus on the stdio transport, which is the recommended approach for most MCP server implementations.
The stdio transport allows MCP servers to communicate with clients through standard input and output streams.
This is the most commonly used and recommended transport mechanism in the current MCP specification, providing a simple and efficient way to build MCP servers that can be easily integrated with various client applications.
Overview
This lesson covers how to build and consume MCP Servers using the stdio transport.
Learning Objectives
By the end of this lesson, you will be able to:
stdio Transport - How it Works
The stdio transport is one of two supported transport types in the current MCP specification (2025-06-18). Here's how it works:
stdin) and sends messages to standard output (stdout).stderr) for logging purposes.Key Requirements:
stdout that is not a valid MCP messagestdin that is not a valid MCP messageTypeScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{
name: "example-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);
In the preceding code:
Server class and StdioServerTransport from the MCP SDKStdioServerTransport instance and connect the server to it, enabling communication over stdin/stdoutPython
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server
# Create server instance
server = Server("example-server")
@server.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
async def main():
async with stdio_server(server) as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
In the preceding code we:
.NET
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<Tools>();
builder.Services.AddLogging(logging => logging.AddConsole());
var app = builder.Build();
await app.RunAsync();
The key difference from SSE is that stdio servers:
Exercise: Creating a stdio Server
To create our server, we need to keep two things in mind:
Lab: Creating a simple MCP stdio server
In this lab, we'll create a simple MCP server using the recommended stdio transport. This server will expose tools that clients can call using the standard Model Context Protocol.
Prerequisites
pip install mcpLet's start by creating our first MCP stdio server:
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Create the server
server = Server("example-stdio-server")
@server.tool()
def calculate_sum(a: int, b: int) -> int:
"""Calculate the sum of two numbers"""
return a + b
@server.tool()
def get_greeting(name: str) -> str:
"""Generate a personalized greeting"""
return f"Hello, {name}! Welcome to MCP stdio server."
async def main():
# Use stdio transport
async with stdio_server(server) as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
Key differences from the deprecated SSE approach
Stdio Transport (Current Standard):
SSE Transport (Deprecated as of MCP 2025-06-18):
Creating a server with stdio transport
To create our stdio server, we need to:
1. Import the required libraries - We need the MCP server components and stdio transport
2. Create a server instance - Define the server with its capabilities
3. Define tools - Add the functionality we want to expose
4. Set up the transport - Configure stdio communication
5. Run the server - Start the server and handle messages
Let's build this step by step:
Step 1: Create a basic stdio server
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Create the server
server = Server("example-stdio-server")
@server.tool()
def get_greeting(name: str) -> str:
"""Generate a personalized greeting"""
return f"Hello, {name}! Welcome to MCP stdio server."
async def main():
async with stdio_server(server) as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
Step 2: Add more tools
@server.tool()
def calculate_sum(a: int, b: int) -> int:
"""Calculate the sum of two numbers"""
return a + b
@server.tool()
def calculate_product(a: int, b: int) -> int:
"""Calculate the product of two numbers"""
return a * b
@server.tool()
def get_server_info() -> dict:
"""Get information about this MCP server"""
return {
"server_name": "example-stdio-server",
"version": "1.0.0",
"transport": "stdio",
"capabilities": ["tools"]
}
Step 3: Running the server
Save the code as server.py and run it from the command line:
python server.py
The server will start and wait for input from stdin. It communicates using JSON-RPC messages over the stdio transport.
Step 4: Testing with the Inspector
You can test your server using the MCP Inspector:
1. Install the Inspector: npx @modelcontextprotocol/inspector
2. Run the Inspector and point it to your server
3. Test the tools you've created
.NET
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer();
```
## Debugging your stdio server
### Using the MCP Inspector
The MCP Inspector is a valuable tool for debugging and testing MCP servers. Here's how to use it with your stdio server:
1. **Install the Inspector**:
```bash
npx @modelcontextprotocol/inspector
```
2. **Run the Inspector**:
```bash
npx @modelcontextprotocol/inspector python server.py
```
3. **Test your server**: The Inspector provides a web interface where you can:
- View server capabilities
- Test tools with different parameters
- Monitor JSON-RPC messages
- Debug connection issues
### Using VS Code
You can also debug your MCP server directly in VS Code:
1. Create a launch configuration in `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug MCP Server",
"type": "python",
"request": "launch",
"program": "server.py",
"console": "integratedTerminal"
}
]
}
```
2. Set breakpoints in your server code
3. Run the debugger and test with the Inspector
### Common debugging tips
- Use `stderr` for logging - never write to `stdout` as it's reserved for MCP messages
- Ensure all JSON-RPC messages are newline-delimited
- Test with simple tools first before adding complex functionality
- Use the Inspector to verify message formats
## Consuming your stdio server in VS Code
Once you've built your MCP stdio server, you can integrate it with VS Code to use it with Claude or other MCP-compatible clients.
### Configuration
1. **Create an MCP configuration file** at `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac):
```json
{
"mcpServers": {
"example-stdio-server": {
"command": "python",
"args": ["path/to/your/server.py"]
}
}
}
```
2. **Restart Claude**: Close and reopen Claude to load the new server configuration.
3. **Test the connection**: Start a conversation with Claude and try using your server's tools:
- "Can you greet me using the greeting tool?"
- "Calculate the sum of 15 and 27"
- "What's the server info?"
### TypeScript stdio server example
Here's a complete TypeScript example for reference:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{
name: "example-stdio-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Add tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_greeting",
description: "Get a personalized greeting",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the person to greet",
},
},
required: ["name"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_greeting") {
return {
content: [
{
type: "text",
text: Hello, ${request.params.arguments?.name}! Welcome to MCP stdio server.,
},
],
};
} else {
throw new Error(Unknown tool: ${request.params.name});
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);
### .NET stdio server example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools
var app = builder.Build();
await app.RunAsync();
public class Tools
{
[McpServerTool, Description("Get a personalized greeting")]
public string GetGreeting(string name)
{
return $"Hello, {name}! Welcome to MCP stdio server.";
}
[McpServerTool, Description("Calculate the sum of two numbers")]
public int CalculateSum(int a, int b)
{
return a + b;
}
}
## Summary
In this updated lesson, you learned how to:
- Build MCP servers using the current **stdio transport** (recommended approach)
- Understand why SSE transport was deprecated in favor of stdio and Streamable HTTP
- Create tools that can be called by MCP clients
- Debug your server using the MCP Inspector
- Integrate your stdio server with VS Code and Claude
The stdio transport provides a simpler, more secure, and more performant way to build MCP servers compared to the deprecated SSE approach. It's the recommended transport for most MCP server implementations as of the 2025-06-18 specification.
### .NET
1. Let's create some tools first, for this we will create a file *Tools.cs* with the following content:
```csharp
using System.ComponentModel;
using System.Text.Json;
using ModelContextProtocol.Server;
```
## Exercise: Testing your stdio server
Now that you've built your stdio server, let's test it to make sure it works correctly.
### Prerequisites
1. Ensure you have the MCP Inspector installed:
```bash
npm install -g @modelcontextprotocol/inspector
```
2. Your server code should be saved (e.g., as `server.py`)
### Testing with the Inspector
1. **Start the Inspector with your server**:
```bash
npx @modelcontextprotocol/inspector python server.py
```
2. **Open the web interface**: The Inspector will open a browser window showing your server's capabilities.
3. **Test the tools**:
- Try the `get_greeting` tool with different names
- Test the `calculate_sum` tool with various numbers
- Call the `get_server_info` tool to see server metadata
4. **Monitor the communication**: The Inspector shows the JSON-RPC messages being exchanged between client and server.
### What you should see
When your server starts correctly, you should see:
- Server capabilities listed in the Inspector
- Tools available for testing
- Successful JSON-RPC message exchanges
- Tool responses displayed in the interface
### Common issues and solutions
**Server won't start:**
- Check that all dependencies are installed: `pip install mcp`
- Verify Python syntax and indentation
- Look for error messages in the console
**Tools not appearing:**
- Ensure `@server.tool()` decorators are present
- Check that tool functions are defined before `main()`
- Verify the server is properly configured
**Connection issues:**
- Make sure the server is using stdio transport correctly
- Check that no other processes are interfering
- Verify the Inspector command syntax
## Assignment
Try building out your server with more capabilities. See [this page](https://api.chucknorris.io/) to, for example, add a tool that calls an API. You decide what the server should look like. Have fun :)
## Solution
[Solution](./solution/README.md) Here's a possible solution with working code.
## Key Takeaways
The key takeaways from this chapter are the following:
- The stdio transport is the recommended mechanism for local MCP servers.
- Stdio transport allows seamless communication between MCP servers and clients using standard input and output streams.
- You can use both Inspector and Visual Studio Code to consume stdio servers directly, making debugging and integration straightforward.
## Samples
- [Java Calculator](../samples/java/calculator/README.md)
- [.Net Calculator](../samples/csharp/)
- [JavaScript Calculator](../samples/javascript/README.md)
- [TypeScript Calculator](../samples/typescript/README.md)
- [Python Calculator](../samples/python/)
## Additional Resources
- [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
## What's Next
## Next Steps
Now that you've learned how to build MCP servers with the stdio transport, you can explore more advanced topics:
- **Next**: [HTTP Streaming with MCP (Streamable HTTP)](../06-http-streaming/README.md) - Learn about the other supported transport mechanism for remote servers
- **Advanced**: [MCP Security Best Practices](../../02-Security/README.md) - Implement security in your MCP servers
- **Production**: [Deployment Strategies](../09-deployment/README.md) - Deploy your servers for production use
## Additional Resources
- [MCP Specification 2025-06-18](https://spec.modelcontextprotocol.io/specification/) - Official specification
- [MCP SDK Documentation](https://github.com/modelcontextprotocol/sdk) - SDK references for all languages
- [Community Examples](../../06-CommunityContributions/README.md) - More server examples from the community
HTTPS Streaming with Model Context Protocol (MCP)
This chapter provides a comprehensive guide to implementing secure, scalable, and real-time streaming with the Model Context Protocol (MCP) using HTTPS.
It covers the motivation for streaming, the available transport mechanisms, how to implement streamable HTTP in MCP, security best practices, migration from SSE, and practical guidance for building your own streaming MCP applications.
Transport Mechanisms and Streaming in MCP
This section explores the different transport mechanisms available in MCP and their role in enabling streaming capabilities for real-time communication between clients and servers.
What is a Transport Mechanism?
A transport mechanism defines how data is exchanged between the client and server. MCP supports multiple transport types to suit different environments and requirements:
Comparison Table
Have a look at the comparison table below to understand the differences between these transport mechanisms:
> Tip: Choosing the right transport impacts performance, scalability, and user experience. Streamable HTTP is recommended for modern, scalable, and cloud-ready applications.
Note the transports stdio and SSE that you were shown in the previous chapters and how streamable HTTP is the transport covered in this chapter.
Streaming: Concepts and Motivation
Understanding the fundamental concepts and motivations behind streaming is essential for implementing effective real-time communication systems.
Streaming is a technique in network programming that allows data to be sent and received in small, manageable chunks or as a sequence of events, rather than waiting for an entire response to be ready. This is especially useful for:
Here's what you need to know about streaming at high level:
Why use streaming?
The reasons for using streaming are the following:
Simple Example: HTTP Streaming Server & Client
Here's a simple example of how streaming can be implemented:
Python
Server (Python, using FastAPI and StreamingResponse):
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import time
app = FastAPI()
async def event_stream():
for i in range(1, 6):
yield f"data: Message {i}\n\n"
time.sleep(1)
@app.get("/stream")
def stream():
return StreamingResponse(event_stream(), media_type="text/event-stream")
Client (Python, using requests):
import requests
with requests.get("http://localhost:8000/stream", stream=True) as r:
for line in r.iter_lines():
if line:
print(line.decode())
This example demonstrates a server sending a series of messages to the client as they become available, rather than waiting for all messages to be ready.
How it works:
Requirements:
StreamingResponse in FastAPI).stream=True in requests).text/event-stream or application/octet-stream.Java
Server (Java, using Spring Boot and Server-Sent Events):
@RestController
public class CalculatorController {
@GetMapping(value = "/calculate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> calculate(@RequestParam double a,
@RequestParam double b,
@RequestParam String op) {
double result;
switch (op) {
case "add": result = a + b; break;
case "sub": result = a - b; break;
case "mul": result = a * b; break;
case "div": result = b != 0 ? a / b : Double.NaN; break;
default: result = Double.NaN;
}
return Flux.<ServerSentEvent<String>>just(
ServerSentEvent.<String>builder()
.event("info")
.data("Calculating: " + a + " " + op + " " + b)
.build(),
ServerSentEvent.<String>builder()
.event("result")
.data(String.valueOf(result))
.build()
)
.delayElements(Duration.ofSeconds(1));
}
}
Client (Java, using Spring WebFlux WebClient):
@SpringBootApplication
public class CalculatorClientApplication implements CommandLineRunner {
private final WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.build();
@Override
public void run(String... args) {
client.get()
.uri(uriBuilder -> uriBuilder
.path("/calculate")
.queryParam("a", 7)
.queryParam("b", 5)
.queryParam("op", "mul")
.build())
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class)
.doOnNext(System.out::println)
.blockLast();
}
}
Java Implementation Notes:
Flux for streamingServerSentEvent provides structured event streaming with event typesWebClient with bodyToFlux() enables reactive streaming consumptiondelayElements() simulates processing time between eventsinfo, result) for better client handlingComparison: Classic Streaming vs MCP Streaming
The differences between how streaming works in a "classical" manner versus how it works in MCP can be depicted like so:
Key Differences Observed
Additionally, here are some key differences:
- Classic HTTP streaming: Uses simple chunked transfer encoding to send data in chunks
- MCP streaming: Uses a structured notification system with JSON-RPC protocol
- Classic HTTP: Plain text chunks with newlines
- MCP: Structured LoggingMessageNotification objects with metadata
- Classic HTTP: Simple client that processes streaming responses
- MCP: More sophisticated client with a message handler to process different types of messages
- Classic HTTP: The progress is part of the main response stream
- MCP: Progress is sent via separate notification messages while the main response comes at the end
Recommendations
There are some things we recommend when it comes to choosing between implementing classical streaming (as an endpoint we showed you above using /stream) versus choosing streaming via MCP.
Streaming in MCP
Ok, so you've seen some recommendations and comparisons so far on the difference between classical streaming and streaming in MCP. Let's get into detail exactly how you can leverage streaming in MCP.
Understanding how streaming works within the MCP framework is essential for building responsive applications that provide real-time feedback to users during long-running operations.
In MCP, streaming is not about sending the main response in chunks, but about sending notifications to the client while a tool is processing a request. These notifications can include progress updates, logs, or other events.
How it works
The main result is still sent as a single response. However, notifications can be sent as separate messages during processing and thereby update the client in real time. The client must be able to handle and display these notifications.
What is a Notification?
We said "Notification", what does that mean in the context of MCP?
A notification is a message sent from the server to the client to inform about progress, status, or other events during a long-running operation. Notifications improve transparency and user experience.
For example, a client is supposed to send a notification once the initial handshake with the server has been made.
A notification looks like so as a JSON message:
{
jsonrpc: "2.0";
method: string;
params?: {
[key: string]: unknown;
};
}
Notifications belongs to a topic in MCP referred to as "Logging".
To get logging to work, the server needs to enable it as feature/capability like so:
{
"capabilities": {
"logging": {}
}
}
> [!NOTE]
> Depending on the SDK used, logging might be enabled by default, or you might need to explicitly enable it in your server configuration.
There different types of notifications:
Implementing Notifications in MCP
To implement notifications in MCP, you need to set up both the server and client sides to handle real-time updates. This allows your application to provide immediate feedback to users during long-running operations.
Server-side: Sending Notifications
Let's start with the server side.
In MCP, you define tools that can send notifications while processing requests.
The server uses the context object (usually ctx) to send messages to the client.
Python
@mcp.tool(description="A tool that sends progress notifications")
async def process_files(message: str, ctx: Context) -> TextContent:
await ctx.info("Processing file 1/3...")
await ctx.info("Processing file 2/3...")
await ctx.info("Processing file 3/3...")
return TextContent(type="text", text=f"Done: {message}")
In the preceding example, the process_files tool sends three notifications to the client as it processes each file.
The ctx.info() method is used to send informational messages.
Additionally, to enable notifications, ensure your server uses a streaming transport (like streamable-http) and your client implements a message handler to process notifications.
Here's how you can set up the server to use the streamable-http transport:
mcp.run(transport="streamable-http")
.NET
[Tool("A tool that sends progress notifications")]
public async Task<TextContent> ProcessFiles(string message, ToolContext ctx)
{
await ctx.Info("Processing file 1/3...");
await ctx.Info("Processing file 2/3...");
await ctx.Info("Processing file 3/3...");
return new TextContent
{
Type = "text",
Text = $"Done: {message}"
};
}
In this .NET example, the ProcessFiles tool is decorated with the Tool attribute and sends three notifications to the client as it processes each file.
The ctx.Info() method is used to send informational messages.
To enable notifications in your .NET MCP server, ensure you're using a streaming transport:
var builder = McpBuilder.Create();
await builder
.UseStreamableHttp() // Enable streamable HTTP transport
.Build()
.RunAsync();
Client-side: Receiving Notifications
The client must implement a message handler to process and display notifications as they arrive.
Python
async def message_handler(message):
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
else:
print("SERVER MESSAGE:", message)
async with ClientSession(
read_stream,
write_stream,
logging_callback=logging_collector,
message_handler=message_handler,
) as session:
In the preceding code, the message_handler function checks if the incoming message is a notification.
If it is, it prints the notification; otherwise, it processes it as a regular server message.
Also note how the ClientSession is initialized with the message_handler to handle incoming notifications.
.NET
// Define a message handler
void MessageHandler(IJsonRpcMessage message)
{
if (message is ServerNotification notification)
{
Console.WriteLine($"NOTIFICATION: {notification}");
}
else
{
Console.WriteLine($"SERVER MESSAGE: {message}");
}
}
// Create and use a client session with the message handler
var clientOptions = new ClientSessionOptions
{
MessageHandler = MessageHandler,
LoggingCallback = (level, message) => Console.WriteLine($"[{level}] {message}")
};
using var client = new ClientSession(readStream, writeStream, clientOptions);
await client.InitializeAsync();
// Now the client will process notifications through the MessageHandler
In this .NET example, the MessageHandler function checks if the incoming message is a notification.
If it is, it prints the notification; otherwise, it processes it as a regular server message.
The ClientSession is initialized with the message handler via the ClientSessionOptions.
To enable notifications, ensure your server uses a streaming transport (like streamable-http) and your client implements a message handler to process notifications.
Progress Notifications & Scenarios
This section explains the concept of progress notifications in MCP, why they matter, and how to implement them using Streamable HTTP. You'll also find a practical assignment to reinforce your understanding.
Progress notifications are real-time messages sent from the server to the client during long-running operations.
Instead of waiting for the entire process to finish, the server keeps the client updated about the current status.
This improves transparency, user experience, and makes debugging easier.
Example:
"Processing document 1/10"
"Processing document 2/10"
...
"Processing complete!"
Why Use Progress Notifications?
Progress notifications are essential for several reasons:
How to Implement Progress Notifications
Here's how you can implement progress notifications in MCP:
ctx.info() or ctx.log() to send notifications as each item is processed. This sends a message to the client before the main result is ready.Server Example:
Python
@mcp.tool(description="A tool that sends progress notifications")
async def process_files(message: str, ctx: Context) -> TextContent:
for i in range(1, 11):
await ctx.info(f"Processing document {i}/10")
await ctx.info("Processing complete!")
return TextContent(type="text", text=f"Done: {message}")
Client Example:
Python
async def message_handler(message):
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
else:
print("SERVER MESSAGE:", message)
Security Considerations
When implementing MCP servers with HTTP-based transports, security becomes a paramount concern that requires careful attention to multiple attack vectors and protection mechanisms.
Overview
Security is critical when exposing MCP servers over HTTP. Streamable HTTP introduces new attack surfaces and requires careful configuration.
Key Points
Origin header to prevent DNS rebinding attacks.localhost to avoid exposing them to the public internet.Best Practices
Challenges
Upgrading from SSE to Streamable HTTP
For applications currently using Server-Sent Events (SSE), migrating to Streamable HTTP provides enhanced capabilities and better long-term sustainability for your MCP implementations.
Why Upgrade?
There are two compelling reasons to upgrade from SSE to Streamable HTTP:
Migration Steps
Here's how you can migrate from SSE to Streamable HTTP in your MCP applications:
transport="streamable-http" in mcp.run().streamablehttp_client instead of SSE client.Maintaining Compatibility
It's recommended to maintain compatibility with existing SSE clients during the migration process. Here are some strategies:
Challenges
Ensure you address the following challenges during migration:
Security Considerations
Security should be a top priority when implementing any server, especially when using HTTP-based transports like Streamable HTTP in MCP.
When implementing MCP servers with HTTP-based transports, security becomes a paramount concern that requires careful attention to multiple attack vectors and protection mechanisms.
Overview
Security is critical when exposing MCP servers over HTTP. Streamable HTTP introduces new attack surfaces and requires careful configuration.
Here are some key security considerations:
Origin header to prevent DNS rebinding attacks.localhost to avoid exposing them to the public internet.Best Practices
Additionally, here are some best practices to follow when implementing security in your MCP streaming server:
Challenges
You will face some challenges when implementing security in MCP streaming servers:
Assignment: Build Your Own Streaming MCP App
Scenario:
Build an MCP server and client where the server processes a list of items (e.g., files or documents) and sends a notification for each item processed. The client should display each notification as it arrives.
Steps:
1. Implement a server tool that processes a list and sends notifications for each item.
2. Implement a client with a message handler to display notifications in real time.
3. Test your implementation by running both server and client, and observe the notifications.
Further Reading & What Next?
To continue your journey with MCP streaming and expand your knowledge, this section provides additional resources and suggested next steps for building more advanced applications.
Further Reading
What Next?
Consuming a server from the AI Toolkit extension for Visual Studio Code
When you’re building an AI agent, it’s not just about generating smart responses; it’s also about giving your agent the ability to take action.
That’s where the Model Context Protocol (MCP) comes in.
MCP makes it easy for agents to access external tools and services in a consistent way.
Think of it like plugging your agent into a toolbox it can *actually* use.
Let’s say you connect an agent to your calculator MCP server. Suddenly, your agent can perform math operations just by receiving a prompt like “What’s 47 times 89?”—no need to hardcode logic or build custom APIs.
Overview
This lesson covers how to connect a calculator MCP server to an agent with the AI Toolkit extension in Visual Studio Code, enabling your agent to perform math operations such as addition, subtraction, multiplication, and division through natural language.
AI Toolkit is a powerful extension for Visual Studio Code that streamlines agent development.
AI Engineers can easily build AI applications by developing and testing generative AI models—locally or in the cloud.
The extension supports most major generative models available today.
*Note*: The AI Toolkit currently supports Python and TypeScript.
Learning Objectives
By the end of this lesson, you will be able to:
Approach
Here's how we need to approach this at a high level:
Great, now that we understand the flow, let's configure an AI agent to leverage external tools through MCP, enhancing its capabilities!
Prerequisites
Exercise: Consuming a server
> [!WARNING]
> Note for macOS Users.
We're currently investigating an issue affecting dependency installation on macOS.
As a result, macOS users won’t be able to complete this tutorial at this time.
We’ll update the instructions as soon as a fix is available.
Thank you for your patience and understanding!
In this exercise, you will build, run, and enhance an AI agent with tools from a MCP server inside Visual Studio Code using the AI Toolkit.
-0- Prestep, add the OpenAI GPT-4o model to My Models
The exercise leverages the GPT-4o model. The model should be added to My Models before creating the agent.
1. Open the AI Toolkit extension from the Activity Bar.
1. In the Catalog section, select Models to open the Model Catalog. Selecting Models opens the Model Catalog in a new editor tab.
1. In the Model Catalog search bar, enter OpenAI GPT-4o.
1. Click + Add to add the model to your My Models list. Ensure that you've selected the model that's Hosted by GitHub.
1. In the Activity Bar, confirm that the OpenAI GPT-4o model appears in the list.
-1- Create an agent
The Agent (Prompt) Builder enables you to create and customize your own AI-powered agents. In this section, you’ll create a new agent and assign a model to power the conversation.
1. Open the AI Toolkit extension from the Activity Bar.
1. In the Tools section, select Agent (Prompt) Builder. Selecting Agent (Prompt) Builder opens the Agent (Prompt) Builder in a new editor tab.
1. Click the + New Agent button. The extension will launch a setup wizard via the Command Palette.
1. Enter the name Calculator Agent and press Enter.
1. In the Agent (Prompt) Builder, for the Model field, select the OpenAI GPT-4o (via GitHub) model.
-2- Create a system prompt for the agent
With the agent scaffolded, it’s time to define its personality and purpose.
In this section, you’ll use the Generate system prompt feature to describe the agent’s intended behavior—in this case, a calculator agent—and have the model write the system prompt for you.
1. For the Prompts section, click the Generate system prompt button. This button opens in the prompt builder which leverages AI to generate a system prompt for the agent.
1.
In the Generate a prompt window, enter the following: You are a helpful and efficient math assistant.
When given a problem involving basic arithmetic, you respond with the correct result.
1.
Click the Generate button.
A notification will appear in the bottom-right corner confirming that the system prompt is being generated.
Once the prompt generation is complete, the prompt will appear in the System prompt field of the Agent (Prompt) Builder.
1. Review the System prompt and modify if necessary.
-3- Create a MCP server
Now that you've defined your agent's system prompt—guiding its behavior and responses—it's time to equip the agent with practical capabilities.
In this section, you’ll create a calculator MCP server with tools to execute addition, subtraction, multiplication, and division calculations.
This server will enable your agent to perform real-time math operations in response to natural language prompts.
AI Toolkit is equipped with templates for ease of creating your own MCP server. We'll use the Python template for creating the calculator MCP server.
*Note*: The AI Toolkit currently supports Python and TypeScript.
1. In the Tools section of the Agent (Prompt) Builder, click the + MCP Server button. The extension will launch a setup wizard via the Command Palette.
1. Select + Add Server.
1. Select Create a New MCP Server.
1. Select python-weather as the template.
1. Select Default folder to save the MCP server template.
1. Enter the following name for the server: Calculator
1. A new Visual Studio Code window will open. Select Yes, I trust the authors.
1. Using the terminal (Terminal > New Terminal), create a virtual environment: python -m venv .venv
1. Using the terminal, activate the virtual environment:
1. Windows - .venv\Scripts\activate
1. macOS/Linux - source .venv/bin/activate
1. Using the terminal, install the dependencies: pip install -e .[dev]
1. In the Explorer view of the Activity Bar, expand the src directory and select server.py to open the file in the editor.
1. Replace the code in the server.py file with the following and save:
```python
"""
Sample MCP Calculator Server implementation in Python.
This module demonstrates how to create a simple MCP server with calculator tools
that can perform basic arithmetic operations (add, subtract, multiply, divide).
"""
from mcp.server.fastmcp import FastMCP
server = FastMCP("calculator")
@server.tool()
def add(a: float, b: float) -> float:
"""Add two numbers together and return the result."""
return a + b
@server.tool()
def subtract(a: float, b: float) -> float:
"""Subtract b from a and return the result."""
return a - b
@server.tool()
def multiply(a: float, b: float) -> float:
"""Multiply two numbers together and return the result."""
return a * b
@server.tool()
def divide(a: float, b: float) -> float:
"""
Divide a by b and return the result.
Raises:
ValueError: If b is zero
"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
```
-4- Run the agent with the calculator MCP server
Now that your agent has tools, it's time to use them! In this section, you'll submit prompts to the agent to test and validate whether the agent leverages the appropriate tool from the calculator MCP server.
You will run the calculator MCP server on your local dev machine via the Agent Builder as the MCP client.
1.
Press F5 to start debugging the MCP server.
The Agent (Prompt) Builder will open in a new editor tab.
The status of the server is visible in the terminal.
1.
In the User prompt field of the Agent (Prompt) Builder, enter the following prompt: I bought 3 items priced at $25 each, and then used a $20 discount.
How much did I pay?
1. Click the Run button to generate the agent's response.
1. Review the agent output. The model should conclude that you paid $55.
1. Here's a breakdown of what should occur:
- The agent selects the multiply and substract tools to aid in the calculation.
- The respective a and b values are assigned for the multiply tool.
- The respective a and b values are assigned for the subtract tool.
- The response from each tool is provided in the respective Tool Response.
- The final output from the model is provided in the final Model Response.
1. Submit additional prompts to further test the agent. You can modify the existing prompt in the User prompt field by clicking into the field and replacing the existing prompt.
1. Once you're done testing the agent, you can stop the server via the terminal by entering CTRL/CMD+C to quit.
Assignment
Try adding an additional tool entry to your server.py file (ex: return the square root of a number).
Submit additional prompts that would require the agent to leverage your new tool (or existing tools).
Be sure to restart the server to load newly added tools.
Solution
Key Takeaways
The takeaways from this chapter is the following:
Additional Resources
What's Next
Testing and Debugging
Before you begin testing your MCP server, it's important to understand the available tools and best practices for debugging.
Effective testing ensures your server behaves as expected and helps you quickly identify and resolve issues.
The following section outlines recommended approaches for validating your MCP implementation.
Overview
This lesson covers how to select the right testing approach and the most effective testing tool.
Learning Objectives
By the end of this lesson, you will be able to:
Testing MCP Servers
MCP provides tools to help you test and debug your servers:
Using MCP Inspector
We've described the usage of this tool in previous lessons but let's talk about it a bit at high level.
It's a tool built in Node.js and you can use it by calling the npx executable which will download and install the tool itself temporarily and will clean itself up once it's done running your request.
The MCP Inspector helps you:
A typical run of the tool looks like so:
npx @modelcontextprotocol/inspector node build/index.js
The above command starts an MCP and its visual interface and launches a local web interface in your browser.
You can expect to see a dashboard displaying your registered MCP servers, their available tools, resources, and prompts.
The interface allows you to interactively test tool execution, inspect server metadata, and view real-time responses, making it easier to validate and debug your MCP server implementations.
Here's what it can look like: !Inspector
You can also run this tool in CLI mode in which case you add --cli attribute.
Here's an example of running the tool in "CLI" mode which lists all the tools on the server:
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
Manual Testing
Apart from running the inspector tool to test server capabilities, another similar approach is to run a client capable of using HTTP lik for example curl.
With curl, you can test MCP servers directly using HTTP requests:
# Example: Test server metadata
curl http://localhost:3000/v1/metadata
# Example: Execute a tool
curl -X POST http://localhost:3000/v1/tools/execute \
-H "Content-Type: application/json" \
-d '{"name": "calculator", "parameters": {"expression": "2+2"}}'
As you can see from above usage of curl, you use a POST request to invoke a tool using a payload consisting of the tools name and its parameters.
Use the approach that fits you best.
CLI tools in general tends to be faster to use and lends themselves to be scripted which can be useful in a CI/CD environment.
Unit Testing
Create unit tests for your tools and resources to ensure they work as expected. Here's some example testing code.
import pytest
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
# Mark the whole module for async tests
pytestmark = pytest.mark.anyio
async def test_list_tools_cursor_parameter():
"""Test that the cursor parameter is accepted for list_tools.
Note: FastMCP doesn't currently implement pagination, so this test
only verifies that the cursor parameter is accepted by the client.
"""
server = FastMCP("test")
# Create a couple of test tools
@server.tool(name="test_tool_1")
async def test_tool_1() -> str:
"""First test tool"""
return "Result 1"
@server.tool(name="test_tool_2")
async def test_tool_2() -> str:
"""Second test tool"""
return "Result 2"
async with create_session(server._mcp_server) as client_session:
# Test without cursor parameter (omitted)
result1 = await client_session.list_tools()
assert len(result1.tools) == 2
# Test with cursor=None
result2 = await client_session.list_tools(cursor=None)
assert len(result2.tools) == 2
# Test with cursor as string
result3 = await client_session.list_tools(cursor="some_cursor_value")
assert len(result3.tools) == 2
# Test with empty string cursor
result4 = await client_session.list_tools(cursor="")
assert len(result4.tools) == 2
The preceding code does the following:
assert statement to check that certain conditions are fulfilled.Have a look at the full file here
Given the above file, you can test your own server to ensure capabilities are created as they should.
All major SDKs have similar testing sections so you can adjust to your chosen runtime.
Samples
Additional Resources
What's Next
Deploying MCP Servers
Deploying your MCP server allows others to access its tools and resources beyond your local environment.
There are several deployment strategies to consider, depending on your requirements for scalability, reliability, and ease of management.
Below you'll find guidance for deploying MCP servers locally, in containers, and to the cloud.
Overview
This lesson covers how to deploy your MCP Server app.
Learning Objectives
By the end of this lesson, you will be able to:
Local development and deployment
If your server is meant to be consumed by running on users machine, you can follow the following steps:
1. Download the server. If you didn't write the server, then download it first to your machine.
1. Start the server process: Run your MCP server application
For SSE (not needed for stdio type server)
1. Configure networking: Ensure the server is accessible on the expected port
1. Connect clients: Use local connection URLs like http://localhost:3000
Cloud Deployment
MCP servers can be deployed to various cloud platforms:
Example: Azure Container Apps
Azure Container Apps support deployment of MCP Servers. It's still a work in progress and it currently supports SSE servers.
Here's how you can go about it:
1. Clone a repo:
```sh
git clone https://github.com/anthonychu/azure-container-apps-mcp-sample.git
```
1. Run it locally to test things out:
```sh
uv venv
uv sync
# linux/macOS
export API_KEYS=
# windows
set API_KEYS=
uv run fastapi dev main.py
```
1. To try it locally, create an *mcp.json* file in a *.vscode* directory and add the following content:
```json
{
"inputs": [
{
"type": "promptString",
"id": "weather-api-key",
"description": "Weather API Key",
"password": true
}
],
"servers": {
"weather-sse": {
"type": "sse",
"url": "http://localhost:8000/sse",
"headers": {
"x-api-key": "${input:weather-api-key}"
}
}
}
}
```
Once the SSE server is started, you can click the play icon in the JSON file, you should now see tools on the server being picked up by GitHub Copilot, see the Tool icon.
1. To deploy, run the following command:
```sh
az containerapp up -g
```
There you have it, deploy it locally, deploy it to Azure through these steps.
Additional Resources
What's Next
Advanced server usage
There are two different types of servers exposed in the MCP SDK, your normal server and the low-level server. Normally, you would use the regular server to add features to it. For some cases though, you want to rely on the low-level server such as:
Regular server vs low-level server
Here's what the creation of an MCP Server looks like with the regular server
Python
mcp = FastMCP("Demo")
# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
TypeScript
const server = new McpServer({
name: "demo-server",
version: "1.0.0"
});
// Add an addition tool
server.registerTool("add",
{
title: "Addition Tool",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() }
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
The point being is that you explicitly add each tool, resource or prompt that you want the server to have. Nothing wrong with that.
Low-level server approach
However, when you use the low-level server approach you need to think about it differently.
Instead of registering each tool, you instead create two handlers per feature type (tools, resources or prompts).
So for example tools then only have two functions like so:
That sounds like potentially less work right? So instead of registering a tool, I just need to make sure the tool is listed when I list all tools and that it's called when there's an incoming request to call a tool.
Let's have a look at how the code now looks:
Python
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""List available tools."""
return [
types.Tool(
name="add",
description="Add two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "number to add"},
"b": {"type": "number", "description": "number to add"}
},
"required": ["query"],
},
)
]
TypeScript
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// Return the list of registered tools
return {
tools: [{
name: "add",
description: "Add two numbers",
inputSchema: {
"type": "object",
"properties": {
"a": {"type": "number", "description": "number to add"},
"b": {"type": "number", "description": "number to add"}
},
"required": ["query"],
}
}]
};
});
Here we now have a function that returns a list of features.
Each entry in the tools list now has fields like name, description and inputSchema to adhere to the return type.
This enables us to put our tools and feature definition elsewhere.
We can now create all our tools in a tools folder and the same goes for all your features so your project can suddenly be organized like so:
app
--| tools
----| add
----| substract
--| resources
----| products
----| schemas
--| prompts
----| product-description
That's great, our architecture can be made to look quite clean.
What about calling tools, is it the same idea then, one handler to call a tool, whichever tool? Yes, exactly, here's the code for that:
Python
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
# tools is a dictionary with tool names as keys
if name not in tools.tools:
raise ValueError(f"Unknown tool: {name}")
tool = tools.tools[name]
result = "default"
try:
result = await tool["handler"](arguments)
except Exception as e:
raise ValueError(f"Error calling tool {name}: {str(e)}")
return [
types.TextContent(type="text", text=str(result))
]
TypeScript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let tool = tools.find(t => t.name === name);
if(!tool) {
return {
error: {
code: "tool_not_found",
message: `Tool ${name} not found.`
}
};
}
// args: request.params.arguments
// TODO call the tool,
return {
content: [{ type: "text", text: `Tool ${name} called with arguments: ${JSON.stringify(input)}, result: ${JSON.stringify(result)}` }]
};
});
As you can see from above code, we need to parse out the tool to call, and with what arguments, and then we need to proceed to calling the tool.
Improving the approach with validation
So far, you've seen how all your registrations to add tools, resources and prompts can be replaced with these two handlers per feature type.
What else do we need to do?
Well, we should add some form of validation to ensure that the tool is called with right arguments.
Each runtime has their own solution for this, for example Python uses Pydantic and TypeScript uses Zod.
The idea is that we do the following:
Create a feature
To create a feature, we will need to create a file for that feature and make sure it has the mandatory fields required of that feature. Which fields differ a bit between tools, resources and prompts.
Python
# schema.py
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
# add.py
from .schema import AddInputModel
async def add_handler(args) -> float:
try:
# Validate input using Pydantic model
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
# TODO: add Pydantic, so we can create an AddInputModel and validate args
"""Handler function for the add tool."""
return float(input_model.a) + float(input_model.b)
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
here you can see how we do the following:
AddInputModel with fields a and b in file *schema.py*.AddInputModel, if there's a mismatch in parameters this will crash:```python
# add.py
try:
# Validate input using Pydantic model
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
```
You can choose whether to put this parsing logic in the tool call itself or in the handler function.
TypeScript
// server.ts
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let tool = tools.find(t => t.name === name);
if (!tool) {
return {
error: {
code: "tool_not_found",
message: `Tool ${name} not found.`
}
};
}
const Schema = tool.rawSchema;
try {
const input = Schema.parse(request.params.arguments);
// @ts-ignore
const result = await tool.callback(input);
return {
content: [{ type: "text", text: `Tool ${name} called with arguments: ${JSON.stringify(input)}, result: ${JSON.stringify(result)}` }]
};
} catch (error) {
return {
error: {
code: "invalid_arguments",
message: `Invalid arguments for tool ${name}: ${error instanceof Error ? error.message : String(error)}`
}
};
}
});
// schema.ts
import { z } from 'zod';
export const MathInputSchema = z.object({ a: z.number(), b: z.number() });
// add.ts
import { Tool } from "./tool.js";
import { MathInputSchema } from "./schema.js";
import { zodToJsonSchema } from "zod-to-json-schema";
export default {
name: "add",
rawSchema: MathInputSchema,
inputSchema: zodToJsonSchema(MathInputSchema),
callback: async ({ a, b }) => {
return {
content: [{ type: "text", text: String(a + b) }]
};
}
} as Tool;
```typescript
const Schema = tool.rawSchema;
try {
const input = Schema.parse(request.params.arguments);
```
if that works then we proceed to call the actual tool:
```typescript
const result = await tool.callback(input);
```
As you can see, this approach creates a great architecture as everything has its place, the *server.ts* is a very small file that only wires up the request handlers and each feature is in their respective folder i.e tools/, resources/ or /prompts.
Great, let's try to build this next.
Exercise: Creating a low-level server
In this exercise, we will do the following:
1. Create a low-level server handling listing of tools and calling of tools.
1. Implement an architecture you can build upon.
1. Add validation to ensure your tool calls are properly validated.
-1- Create an architecture
The first thing we need to address is an architecture that helps us scale as we add more features, here's what it looks like:
Python
server.py
--| tools
----| __init__.py
----| add.py
----| schema.py
client.py
TypeScript
server.ts
--| tools
----| add.ts
----| schema.ts
client.ts
Now we have set up an architecture that ensures we can easily add new tools in a tools folder. Feel free to follow this to add subdirectories for resources and prompts.
-2- Creating a tool
Let's see what creating a tool looks like next. First, it needs to be created in its *tool* subdirectory like so:
Python
from .schema import AddInputModel
async def add_handler(args) -> float:
try:
# Validate input using Pydantic model
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
# TODO: add Pydantic, so we can create an AddInputModel and validate args
"""Handler function for the add tool."""
return float(input_model.a) + float(input_model.b)
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
What we see here is how we define name, description, and input schema using Pydantic and a handler that will be invoked once this tool is being called.
Lastly, we expose tool_add which is a dictionary holding all these properties.
There's also *schema.py* that's used to define the input schema used by our tool:
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
We also need to populate *__init__.py* to ensure the tools directory is treated as a module. Additionally, we need to expose the modules within it like so:
from .add import tool_add
tools = {
tool_add["name"] : tool_add
}
We can keep adding to this file as we add more tools.
TypeScript
import { Tool } from "./tool.js";
import { MathInputSchema } from "./schema.js";
import { zodToJsonSchema } from "zod-to-json-schema";
export default {
name: "add",
rawSchema: MathInputSchema,
inputSchema: zodToJsonSchema(MathInputSchema),
callback: async ({ a, b }) => {
return {
content: [{ type: "text", text: String(a + b) }]
};
}
} as Tool;
Here we create a dictionary consisting of properties:
There's also Tool that's used to convert this dictionary into a type the mcp server handler can accept and it looks like so:
import { z } from 'zod';
export interface Tool {
name: string;
inputSchema: any;
rawSchema: z.ZodTypeAny;
callback: (args: z.infer<z.ZodTypeAny>) => Promise<{ content: { type: string; text: string }[] }>;
}
And there's *schema.ts* where we store the input schemas for each tool that looks like so with only one schema at present but as we add tools we can add more entries:
import { z } from 'zod';
export const MathInputSchema = z.object({ a: z.number(), b: z.number() });
Great, let's proceed to handle the listing of our tools next.
-3- Handle tool listing
Next, to handle listing our tools, we need to set up a request handler for that. Here's what we need to add to our server file:
Python
# code omitted for brevity
from tools import tools
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
tool_list = []
print(tools)
for tool in tools.values():
tool_list.append(
types.Tool(
name=tool["name"],
description=tool["description"],
inputSchema=pydantic_to_json(tool["input_schema"]),
)
)
return tool_list
Here, we add the decorator @server.list_tools and the implementing function handle_list_tools.
In the latter, we need to produce a list of tools.
Note how each tool needs to have a name, description and inputSchema.
TypeScript
To set up the request handler for listing tools, we need to call setRequestHandler on the server with a schema fitting what we're trying to do, in this case ListToolsRequestSchema.
// index.ts
import addTool from "./add.js";
import subtractTool from "./subtract.js";
import {server} from "../server.js";
import { Tool } from "./tool.js";
export let tools: Array<Tool> = [];
tools.push(addTool);
tools.push(subtractTool);
// server.ts
// code omitted for brevity
import { tools } from './tools/index.js';
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// Return the list of registered tools
return {
tools: tools
};
});
Great, now we have solved the piece of listing tools, let's look at how we could be calling tools next.
-4- Handle calling a tool
To call a tool, we need set up another request handler, this time focused on dealing with a request specifying which feature to call and with what arguments.
Python
Let's use the decorator @server.call_tool and implement it with a function like handle_call_tool.
Within that function, we need to parse out the tool name, its argument and ensure the arguments are valid for the tool in question.
We can either validate the arguments in this function or downstream in the actual tool.
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
# tools is a dictionary with tool names as keys
if name not in tools.tools:
raise ValueError(f"Unknown tool: {name}")
tool = tools.tools[name]
result = "default"
try:
# invoke the tool
result = await tool["handler"](arguments)
except Exception as e:
raise ValueError(f"Error calling tool {name}: {str(e)}")
return [
types.TextContent(type="text", text=str(result))
]
Here's what goes on:
name which is true for our arguments in the form of the arguments dictionary.result = await tool"handler". The validation of the arguments happens in the handler property which points to a function, if that fails it will raise an exception. There, now we have a full understanding of listing and calling tools using a low-level server.
See the full example
Runtimes
Run sample
Set up virtual environment
python -m venv venv
source ./venv/bin/activate
Install dependencies
pip install "mcp[cli]"
Run code
python client.py
You should see the text:
Available tools: ['add']
Result of add tool: meta=None content=[TextContent(type='text', text='8.0', annotations=None, meta=None)] structuredContent=None isError=False
Run sample
Install dependencies
npm install
Build it
npm run build
Run it
npm start
You should see the text:
Registering tools...
Starting server...
Great, that means it' starting up correctly.
Test the server
Test out the capabilities with the following command:
npx @modelcontextprotocol/inspector node build/app.js
This should start up the web interface of the inspector tool.
Test in CLI mode
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/list
You should see the following output:
{
"tools": [
{
"name": "add",
"inputSchema": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"rawSchema": {
"_def": {
"unknownKeys": "strip",
"catchall": {
"_def": {
"typeName": "ZodNever"
},
"~standard": {
"version": 1,
"vendor": "zod"
}
},
"typeName": "ZodObject"
},
"~standard": {
"version": 1,
"vendor": "zod"
},
"_cached": null
}
},
{
"name": "subtract",
"inputSchema": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"rawSchema": {
"_def": {
"unknownKeys": "strip",
"catchall": {
"_def": {
"typeName": "ZodNever"
},
"~standard": {
"version": 1,
"vendor": "zod"
}
},
"typeName": "ZodObject"
},
"~standard": {
"version": 1,
"vendor": "zod"
},
"_cached": null
}
}
]
}
Run a tool:
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add --tool-arg a=1 --tool-arg b=2
You should see a response similar to:
{
"content": [
{
"type": "text",
"text": "Tool add called with arguments: {\"a\":1,\"b\":2}, result: {\"content\":[{\"type\":\"text\",\"text\":\"3\"}]}"
}
]
Try testing a tool that doesn't exist like "add2" with this command:
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add2 --tool-arg a=1 --tool-arg b=2
You should now see this message showing that your validatiob works:
{
"content": [],
"error": {
"code": "tool_not_found",
"message": "Tool add2 not found."
}
Also try sending in a parameter c that should be rejected by the schema like so:
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add --tool-arg a=1 --tool-arg c=2
You should now see "invalid arguments" error:
{
"content": [],
"error": {
"code": "invalid_arguments",
"message": "Invalid arguments for tool add: [\n {\n \"code\": \"invalid_type\",\n \"expected\": \"number\",\n \"received\": \"undefined\",\n \"path\": [\n \"b\"\n ],\n \"message\": \"Required\"\n }\n]"
}
}
Assignment
Extend the code you've been given with a number of tools, resources and prompt and reflect over how you notice that you only need to add files in tools directory and nowhere else.
*No solution given*
Summary
In this chapter, we saw how low-level server approach worked and how that can help us create a nice architecture we can keep building on.
We also discussed validation and you were shown how to work with validation libraries to create schemas for input validation.
What's Next
Simple auth
MCP SDKs support the use of OAuth 2.1 which to be fair is a pretty involved process involving concepts like auth server, resource server, posting credentials, getting a code, exchanging the code for a bearer token until you can finally get your resource data.
If you're unused to OAuth which is a great thing to implement, it's a good idea to start with some basic level of auth and build up to better and better security.
That's why this chapter exists, to build you up to more advanced auth.
Auth, what do we mean?
Auth is short for authentication and authorization. The idea is that we need to do two things:
Credentials: how we tell the system who we are
Well, most web developers out there start thinking in terms of providing a credential to the server, usually a secret that says if they're allowed to be here "Authentication".
This credential is usually a base64 encoded version of username and password or an API key that uniquely identifies a specific user.
This involves sending it via a header called "Authorization" like so:
{ "Authorization": "secret123" }
This is usually referred to as basic authentication. How the overall flow then works is in the following way:
sequenceDiagram
participant User
participant Client
participant Server
User->>Client: show me data
Client->>Server: show me data, here's my credential
Server-->>Client: 1a, I know you, here's your data
Server-->>Client: 1b, I don't know you, 401
Now that we understand how it works from a flow standpoint, how do we implement it?
Well, most web servers have a concept called middleware, a piece of code that runs as part of the request that can verify credentials, and if credentials are valid can let the request pass through.
If the request doesn't have valid credentials then you get an auth error.
Let's see how this can be implemented:
Python
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
has_header = request.headers.get("Authorization")
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
print("Valid token, proceeding...")
response = await call_next(request)
# add any customer headers or change in the response in some way
return response
starlette_app.add_middleware(CustomHeaderMiddleware)
Here we have:
AuthMiddleware where its dispatch method is being invoked by the web server. ```python
starlette_app.add_middleware(AuthMiddleware)
```
```python
has_header = request.headers.get("Authorization")
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
```
if the secret is present and valid then we let the request pass through by calling call_next and return the response.
```python
response = await call_next(request)
# add any customer headers or change in the response in some way
return response
```
How it works is that if a web request is made towards the server the middleware will be invoked and given its implementation it will either let the request pass through or end up returning an error that indicates the client isn't allowed to proceed.
TypeScript
Here we create a middleware with the popular framework Express and intercept the request before it reaches the MCP Server. Here's the code for that:
function isValid(secret) {
return secret === "secret123";
}
app.use((req, res, next) => {
// 1. Authorization header present?
if(!req.headers["Authorization"]) {
res.status(401).send('Unauthorized');
}
let token = req.headers["Authorization"];
// 2. Check validity.
if(!isValid(token)) {
res.status(403).send('Forbidden');
}
console.log('Middleware executed');
// 3. Passes request to the next step in the request pipeline.
next();
});
In this code we:
1. Check if the Authorization header is present in the first place, if not, we send a 401 error.
2. Ensure the credential/token is valid, if not, we send a 403 error.
3. Finally passes on the request in the request pipeline and returns the asked for resource.
Exercise: Implement authentication
Lets take our knowledge and try implementing it. Here's the plan:
Server
Client
-1- Create a web server and MCP instance
In our first step, we need to create the web server instance and the MCP Server.
Python
Here we create an MCP server instance, create a starlette web app and host it with uvicorn.
# creating MCP Server
app = FastMCP(
name="MCP Resource Server",
instructions="Resource Server that validates tokens via Authorization Server introspection",
host=settings["host"],
port=settings["port"],
debug=True
)
# creating starlette web app
starlette_app = app.streamable_http_app()
# serving app via uvicorn
async def run(starlette_app):
import uvicorn
config = uvicorn.Config(
starlette_app,
host=app.settings.host,
port=app.settings.port,
log_level=app.settings.log_level.lower(),
)
server = uvicorn.Server(config)
await server.serve()
run(starlette_app)
In this code we:
app.streamable_http_app().server.serve().TypeScript
Here we create an MCP Server instance.
const server = new McpServer({
name: "example-server",
version: "1.0.0"
});
// ... set up server resources, tools, and prompts ...
This MCP Server creation will need to happen within our POST /mcp route definition, so let's take the above code and move it like so:
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
const app = express();
app.use(express.json());
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
// Handle POST requests for client-to-server communication
app.post('/mcp', async (req, res) => {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
// Store the transport by session ID
transports[sessionId] = transport;
},
// DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
// locally, make sure to set:
// enableDnsRebindingProtection: true,
// allowedHosts: ['127.0.0.1'],
});
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
const server = new McpServer({
name: "example-server",
version: "1.0.0"
});
// ... set up server resources, tools, and prompts ...
// Connect to the MCP server
await server.connect(transport);
} else {
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// Handle the request
await transport.handleRequest(req, res, req.body);
});
// Reusable handler for GET and DELETE requests
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', handleSessionRequest);
// Handle DELETE requests for session termination
app.delete('/mcp', handleSessionRequest);
app.listen(3000);
Now you see how the MCP Server creation was moved within app.post("/mcp").
Let's move on to the next step of creating the middleware so we can validate the incoming credential.
-2- Implement a middleware for the server
Let's get to the middleware portion next.
Here we will create a middleware that looks for a credential in the Authorization header and validates it.
If it's acceptable then the request will move on to do what it needs (e.g list tools, read a resource or whatever MCP functionality the client was asking for).
Python
To create the middleware, we need to create a class that inherits from BaseHTTPMiddleware. There are two interesting pieces:
request , that we read the header info from.call_next the callback we need to invoke if the client has brought a credential we accept.First, we need to handle the case if the Authorization header is missing:
has_header = request.headers.get("Authorization")
# no header present, fail with 401, otherwise move on.
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
Here we send a 401 unauthorized message as the client is failing authentication.
Next, if a credential was submitted, we need to check its validity like so:
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
Note how we send a 403 forbidden message above. Let's see the full middleware below implementing everything we mentioned above:
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
has_header = request.headers.get("Authorization")
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
print("Valid token, proceeding...")
print(f"-> Received {request.method} {request.url}")
response = await call_next(request)
response.headers['Custom'] = 'Example'
return response
Great, but what about valid_token function? Here it is below:
# DON'T use for production - improve it !!
def valid_token(token: str) -> bool:
# remove the "Bearer " prefix
if token.startswith("Bearer "):
token = token[7:]
return token == "secret-token"
return False
This should obviously improve.
IMPORTANT: You should NEVER have secrets like this in code. You should ideally retrieve the value to compare with from a data source or from an IDP (identity service provider) or better yet, let the IDP do the validation.
TypeScript
To implement this with Express, we need to call the use method that takes middleware functions.
We need to:
Authorization property.Here, we're checking if the Authorization header is present and if not, we stop the request from going through:
if(!req.headers["authorization"]) {
res.status(401).send('Unauthorized');
return;
}
If the header isn't sent in the first place, you receive a 401.
Next, we check if the credential is valid, if not we again stop the request but with a slightly different message:
if(!isValid(token)) {
res.status(403).send('Forbidden');
return;
}
Note how you now get a 403 error.
Here's the full code:
app.use((req, res, next) => {
console.log('Request received:', req.method, req.url, req.headers);
console.log('Headers:', req.headers["authorization"]);
if(!req.headers["authorization"]) {
res.status(401).send('Unauthorized');
return;
}
let token = req.headers["authorization"];
if(!isValid(token)) {
res.status(403).send('Forbidden');
return;
}
console.log('Middleware executed');
next();
});
We have set up the web server to accept a middleware to check the credential the client is hopefully sending us. What about the client itself?
-3- Send web request with credential via header
We need to ensure the client is passing the credential through the header. As we're going to use an MCP client to do so, we need to figure out how that's done.
Python
For the client, we need to pass a header with our credential like so:
# DON'T hardcode the value, have it at minimum in an environment variable or a more secure storage
token = "secret-token"
async with streamablehttp_client(
url = f"http://localhost:{port}/mcp",
headers = {"Authorization": f"Bearer {token}"}
) as (
read_stream,
write_stream,
session_callback,
):
async with ClientSession(
read_stream,
write_stream
) as session:
await session.initialize()
# TODO, what you want done in the client, e.g list tools, call tools etc.
Note how we populate the headers property like so headers = {"Authorization": f"Bearer {token}"}.
TypeScript
We can solve this in two steps:
1. Populate a configuration object with our credential.
2. Pass the configuration object to the transport.
// DON'T hardcode the value like shown here. At minimum have it as a env variable and use something like dotenv (in dev mode).
let token = "secret123"
// define a client transport option object
let options: StreamableHTTPClientTransportOptions = {
sessionId: sessionId,
requestInit: {
headers: {
"Authorization": "secret123"
}
}
};
// pass the options object to the transport
async function main() {
const transport = new StreamableHTTPClientTransport(
new URL(serverUrl),
options
);
Here you see above how we had to create an options object and place our headers under the requestInit property.
IMPORTANT: How do we improve it from here though?
Well, the current implementation has some issues.
First off, passing a credential like this is pretty risky unless you at minimum have HTTPS.
Even then, the credential can be stolen so you need a system where you can easily revoke the token and add additional checks like where in the world is it coming from, does the request happen way too often (bot-like behavior), in short, there's a whole host of concerns.
It should be said though, for very simple APIs where you don't want anyone calling your API without being authenticated and what we have here is a good start.
With that said, let's try to harden the security a little bit by using a standardized format like JSON Web Token, also known as JWT or "JOT" tokens.
JSON Web Tokens, JWT
So, we're trying to improve things from sending very simple credentials. What's the immediate improvements we get adopting JWT?
With all of these benefits, let's see how we can take our implementation to the next level.
Turning basic auth into JWT
So, the changes we need to make at mile-high level are to:
-1- Construct a JWT token
First off, a JWT token has the following parts:
For this, we will need to construct the header, payload and the encoded token.
Python
import jwt
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
import datetime
# Secret key used to sign the JWT
secret_key = 'your-secret-key'
header = {
"alg": "HS256",
"typ": "JWT"
}
# the user info and its claims and expiry time
payload = {
"sub": "1234567890", # Subject (user ID)
"name": "User Userson", # Custom claim
"admin": True, # Custom claim
"iat": datetime.datetime.utcnow(),# Issued at
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1) # Expiry
}
# encode it
encoded_jwt = jwt.encode(payload, secret_key, algorithm="HS256", headers=header)
In the above code we've:
TypeScript
Here we will need some dependencies that will help us construct the JWT token.
Dependencies
npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken
Now that we have that in place, let's create the header, payload and through that create the encoded token.
import jwt from 'jsonwebtoken';
const secretKey = 'your-secret-key'; // Use env vars in production
// Define the payload
const payload = {
sub: '1234567890',
name: 'User usersson',
admin: true,
iat: Math.floor(Date.now() / 1000), // Issued at
exp: Math.floor(Date.now() / 1000) + 60 * 60 // Expires in 1 hour
};
// Define the header (optional, jsonwebtoken sets defaults)
const header = {
alg: 'HS256',
typ: 'JWT'
};
// Create the token
const token = jwt.sign(payload, secretKey, {
algorithm: 'HS256',
header: header
});
console.log('JWT:', token);
This token is:
Signed using HS256
Valid for 1 hour
Includes claims like sub, name, admin, iat, and exp.
-2- Validate a token
We will also need to validate a token, this is something we should do on the server to ensure what the client is sending us is in fact valid.
There are many checks we should do here from validating its structure to its validity.
You're also encouraged to add other checks to see if the user is in your system and more.
To validate a token, we need to decode it so we can read it and then start checking its validity:
Python
# Decode and verify the JWT
try:
decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
print("✅ Token is valid.")
print("Decoded claims:")
for key, value in decoded.items():
print(f" {key}: {value}")
except ExpiredSignatureError:
print("❌ Token has expired.")
except InvalidTokenError as e:
print(f"❌ Invalid token: {e}")
In this code, we call jwt.decode using the token, the secret key and the chosen algorithm as input.
Note how we use a try-catch construct as a failed validation leads to an error being raised.
TypeScript
Here we need to call jwt.verify to get a decoded version of the token that we can analyze further.
If this call fails, that means the structure of the token is incorrect or it's no longer valid.
try {
const decoded = jwt.verify(token, secretKey);
console.log('Decoded Payload:', decoded);
} catch (err) {
console.error('Token verification failed:', err);
}
NOTE: as mentioned previously, we should perform additional checks to ensure this token points out a user in our system and ensure the user has the rights it claims to have.
Next, let's look into role based access control, also known as RBAC.
Adding role based access control
The idea is that we want to express that different roles have different permissions.
For example, we assume an admin can do everything and that a normal user can do read/write and that a guest can only read.
Therefore, here are some possible permission levels:
Let's look at how we can implement such a control with middleware. Middlewares can be added per route as well as for all routes.
Python
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
import jwt
# DON'T have the secret in the code like, this is for demonstration purposes only. Read it from a safe place.
SECRET_KEY = "your-secret-key" # put this in env variable
REQUIRED_PERMISSION = "User.Read"
class JWTPermissionMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return JSONResponse({"error": "Missing or invalid Authorization header"}, status_code=401)
token = auth_header.split(" ")[1]
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return JSONResponse({"error": "Token expired"}, status_code=401)
except jwt.InvalidTokenError:
return JSONResponse({"error": "Invalid token"}, status_code=401)
permissions = decoded.get("permissions", [])
if REQUIRED_PERMISSION not in permissions:
return JSONResponse({"error": "Permission denied"}, status_code=403)
request.state.user = decoded
return await call_next(request)
There a few different ways to add the middleware like below:
# Alt 1: add middleware while constructing starlette app
middleware = [
Middleware(JWTPermissionMiddleware)
]
app = Starlette(routes=routes, middleware=middleware)
# Alt 2: add middleware after starlette app is already constructed
starlette_app.add_middleware(JWTPermissionMiddleware)
# Alt 3: add middleware per route
routes = [
Route(
"/mcp",
endpoint=..., # handler
middleware=[Middleware(JWTPermissionMiddleware)]
)
]
TypeScript
We can use app.use and a middleware that will run for all requests.
app.use((req, res, next) => {
console.log('Request received:', req.method, req.url, req.headers);
console.log('Headers:', req.headers["authorization"]);
// 1. Check if authorization header has been sent
if(!req.headers["authorization"]) {
res.status(401).send('Unauthorized');
return;
}
let token = req.headers["authorization"];
// 2. Check if token is valid
if(!isValid(token)) {
res.status(403).send('Forbidden');
return;
}
// 3. Check if token user exist in our system
if(!isExistingUser(token)) {
res.status(403).send('Forbidden');
console.log("User does not exist");
return;
}
console.log("User exists");
// 4. Verify the token has the right permissions
if(!hasScopes(token, ["User.Read"])){
res.status(403).send('Forbidden - insufficient scopes');
}
console.log("User has required scopes");
console.log('Middleware executed');
next();
});
There's quite a few things we can let our middleware and that our middleware SHOULD do, namely:
1. Check if authorization header is present
2. Check if token is valid, we call isValid which is a method we wrote that check integrity and validity of JWT token.
3. Verify the user exists in our system, we should check this.
```typescript
// users in DB
const users = [
"user1",
"User usersson",
]
function isExistingUser(token) {
let decodedToken = verifyToken(token);
// TODO, check if user exists in DB
return users.includes(decodedToken?.name || "");
}
```
Above, we've created a very simple users list, which should be in a database obviously.
4. Additionally, we should also check the token has the right permissions.
```typescript
if(!hasScopes(token, ["User.Read"])){
res.status(403).send('Forbidden - insufficient scopes');
}
```
In this code above from the middleware, we check that the token contains User.Read permission, if not we send a 403 error. Below is the hasScopes helper method.
```typescript
function hasScopes(scope: string, requiredScopes: string[]) {
let decodedToken = verifyToken(scope);
return requiredScopes.every(scope => decodedToken?.scopes.includes(scope));
}
```
Have a think which additional checks you should be doing, but these are the absolute minimum of checks you should be doing.
Using Express as a web framework is a common choice. There are helpers library when you use JWT so you can write less code.
express-jwt, helper library that provides a middleware that helps decode your token.express-jwt-permissions, this provides a middleware guard that helps check if a certain permission is on the token.Here's what these libraries can look like when used:
const express = require('express');
const jwt = require('express-jwt');
const guard = require('express-jwt-permissions')();
const app = express();
const secretKey = 'your-secret-key'; // put this in env variable
// Decode JWT and attach to req.user
app.use(jwt({ secret: secretKey, algorithms: ['HS256'] }));
// Check for User.Read permission
app.use(guard.check('User.Read'));
// multiple permissions
// app.use(guard.check(['User.Read', 'Admin.Access']));
app.get('/protected', (req, res) => {
res.json({ message: `Welcome ${req.user.name}` });
});
// Error handler
app.use((err, req, res, next) => {
if (err.code === 'permission_denied') {
return res.status(403).send('Forbidden');
}
next(err);
});
Now you have seen how middleware can be used for both authentication and authorization, what about MCP though, does it change how we do auth? Let's find out in the next section.
-3- Add RBAC to MCP
You've seen so far how you can add RBAC via middleware, however, for MCP there's no easy way to add a per MCP feature RBAC, so what do we do?
Well, we just have to add code like this that checks in this case whether the client has the rights to call a specific tool:
You have a few different choices on how to accomplish per feature RBAC, here are some:
python
```python
@tool()
def delete_product(id: int):
try:
check_permissions(role="Admin.Write", request)
catch:
pass # client failed authorization, raise authorization error
```
typescript
```typescript
server.registerTool(
"delete-product",
{
title: Delete a product",
description: "Deletes a product",
inputSchema: { id: z.number() }
},
async ({ id }) => {
try {
checkPermissions("Admin.Write", request);
// todo, send id to productService and remote entry
} catch(Exception e) {
console.log("Authorization error, you're not allowed");
}
return {
content: [{ type: "text", text: Deletected product with id ${id} }]
};
}
);
```
Python
```python
tool_permission = {
"create_product": ["User.Write", "Admin.Write"],
"delete_product": ["Admin.Write"]
}
def has_permission(user_permissions, required_permissions) -> bool:
# user_permissions: list of permissions the user has
# required_permissions: list of permissions required for the tool
return any(perm in user_permissions for perm in required_permissions)
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
# Assume request.user.permissions is a list of permissions for the user
user_permissions = request.user.permissions
required_permissions = tool_permission.get(name, [])
if not has_permission(user_permissions, required_permissions):
# Raise error "You don't have permission to call tool {name}"
raise Exception(f"You don't have permission to call tool {name}")
# carry on and call tool
# ...
```
TypeScript
```typescript
function hasPermission(userPermissions: string[], requiredPermissions: string[]): boolean {
if (!Array.isArray(userPermissions) || !Array.isArray(requiredPermissions)) return false;
// Return true if user has at least one required permission
return requiredPermissions.some(perm => userPermissions.includes(perm));
}
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let permissions = request.user.permissions;
if (!hasPermission(permissions, toolPermissions[name])) {
return new Error(You don't have permission to call ${name});
}
// carry on..
});
```
Note, you will need to ensure your middleware assigns a decoded token to the request's user property so the code above is made simple.
Summing up
Now that we discussed how to add support for RBAC in general and for MCP in particular, it's time to try to implement security on your own to ensure you understood the concepts presented to you.
Assignment 1: Build an mcp server and mcp client using basic authentication
Here you will take what you've learnt in terms of sending credentials through headers.
Solution 1
Assignment 2: Upgrade the solution from Assignment 1 to use JWT
Take the first solution but this time, let's improve upon it.
Instead of using Basic Auth, let's use JWT.
Solution 2
Challenge
Add the RBAC per tool that we describe in section "Add RBAC to MCP".
Summary
You've hopefully learned a lot in this chapter, from no security at all, to basic security, to JWT and how it can be added to MCP.
We’ve built a solid foundation with custom JWTs, but as we scale, we’re moving toward a standards-based identity model.
Adopting an IdP like Entra or Keycloak lets us offload token issuance, validation, and lifecycle management to a trusted platform — freeing us to focus on app logic and user experience.
For that, we have a more advanced chapter on Entra
What's Next
Setting Up Popular MCP Host Clients
This guide covers how to configure and use MCP servers with popular AI host applications. Each host has its own configuration approach, but once set up, they all communicate with MCP servers using the standardized protocol.
What is an MCP Host?
An MCP Host is an AI application that can connect to MCP servers to extend its capabilities. Think of it as the "front end" that users interact with, while MCP servers provide the "back end" tools and data.
flowchart LR
User[👤 User] --> Host[🖥️ MCP Host]
Host --> S1[MCP Server A]
Host --> S2[MCP Server B]
Host --> S3[MCP Server C]
subgraph "Popular Hosts"
H1[Claude Desktop]
H2[VS Code]
H3[Cursor]
H4[Cline]
H5[Windsurf]
end
Prerequisites
---
1. Claude Desktop
Claude Desktop is Anthropic's official desktop application that natively supports MCP.
Installation
1. Download Claude Desktop from claude.ai/download
2. Install and sign in with your Anthropic account
Configuration
Claude Desktop uses a JSON configuration file to define MCP servers.
Configuration file location:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.json~/.config/Claude/claude_desktop_config.jsonExample configuration:
{
"mcpServers": {
"calculator": {
"command": "python",
"args": ["-m", "mcp_calculator_server"],
"env": {
"PYTHONPATH": "/path/to/your/server"
}
},
"weather": {
"command": "node",
"args": ["/path/to/weather-server/build/index.js"]
},
"database": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost/mydb"
}
}
}
}
Configuration Options
command"python", "node", "npx"args["-m", "my_server"]env{"API_KEY": "xxx"}cwd"/path/to/server"Testing Your Setup
1. Save the configuration file
2. Restart Claude Desktop completely (quit and reopen)
3. Open a new conversation
4. Look for the 🔌 icon indicating connected servers
5. Try asking Claude to use one of your tools
Troubleshooting Claude Desktop
Server not appearing:
Server crashes on startup:
---
2. VS Code with GitHub Copilot
VS Code supports MCP through GitHub Copilot Chat extensions.
Prerequisites
1. VS Code 1.99+ installed
2. GitHub Copilot extension installed
3. GitHub Copilot Chat extension installed
Configuration
VS Code uses .vscode/mcp.json in your workspace or user settings.
Workspace configuration (.vscode/mcp.json):
{
"servers": {
"my-calculator": {
"type": "stdio",
"command": "python",
"args": ["-m", "mcp_calculator_server"]
},
"my-database": {
"type": "sse",
"url": "http://localhost:8080/sse"
}
}
}
User settings (settings.json):
{
"mcp.servers": {
"global-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-memory"]
}
},
"mcp.enableLogging": true
}
Using MCP in VS Code
1. Open the Copilot Chat panel (Ctrl+Shift+I / Cmd+Shift+I)
2. Type @ to see available MCP tools
3. Use natural language to invoke tools: "Calculate 25 * 48 using the calculator"
Troubleshooting VS Code
MCP servers not loading:
---
3. Cursor
Cursor is an AI-first code editor with built-in MCP support.
Installation
1. Download Cursor from cursor.sh
2. Install and sign in
Configuration
Cursor uses a similar configuration format to Claude Desktop.
Configuration file location:
~/.cursor/mcp.json%USERPROFILE%\.cursor\mcp.json~/.cursor/mcp.jsonExample configuration:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here"
}
}
}
}
Using MCP in Cursor
1. Open Cursor's AI chat (Ctrl+L / Cmd+L)
2. MCP tools appear automatically in suggestions
3. Ask the AI to perform tasks using connected servers
---
4. Cline (Terminal-Based)
Cline is a terminal-based MCP client, ideal for command-line workflows.
Installation
npm install -g @anthropic/cline
Configuration
Cline uses environment variables and command-line arguments.
Using environment variables:
export ANTHROPIC_API_KEY="your-api-key"
export MCP_SERVER_CALCULATOR="python -m mcp_calculator_server"
Using command-line arguments:
cline --mcp-server "calculator:python -m mcp_calculator_server" \
--mcp-server "weather:node /path/to/weather/index.js"
Configuration file (~/.clinerc):
{
"apiKey": "your-api-key",
"mcpServers": {
"calculator": {
"command": "python",
"args": ["-m", "mcp_calculator_server"]
}
}
}
Using Cline
# Start an interactive session
cline
# Single query with MCP
cline "Calculate the square root of 144 using the calculator"
# List available tools
cline --list-tools
---
5. Windsurf
Windsurf is another AI-powered code editor with MCP support.
Installation
1. Download Windsurf from codeium.com/windsurf
2. Install and create an account
Configuration
Windsurf configuration is managed through the settings UI:
1. Open Settings (Ctrl+, / Cmd+,)
2. Search for "MCP"
3. Click "Edit in settings.json"
Example configuration:
{
"windsurf.mcp.servers": {
"my-tools": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {}
}
},
"windsurf.mcp.enabled": true
}
---
Transport Types Comparison
Different hosts support different transport mechanisms:
stdio (standard input/output): Best for local servers started by the host
SSE/HTTP: Best for remote servers or servers shared between multiple clients
---
Common Troubleshooting
Server won't start
1. Test the server manually first:
```bash
# For Python
python -m your_server_module
# For Node.js
node /path/to/server/index.js
```
2. Check the command path:
- Use absolute paths when possible
- Ensure the executable is in your PATH
3. Verify dependencies:
```bash
# Python
pip list | grep mcp
# Node.js
npm list @modelcontextprotocol/sdk
```
Server connects but tools don't work
1. Check server logs - Most hosts have logging options
2. Verify tool registration - Use MCP Inspector to test
3. Check permissions - Some tools need file/network access
Environment variables not passed
env configuration field explicitly---
Security Best Practices
1. Never commit API keys to configuration files
2. Use environment variables for sensitive data
3. Limit server permissions to only what's needed
4. Review server code before granting access to your system
5. Use allowlists for file system and network access
---
What's Next
---
Additional Resources
Debugging with MCP Inspector
The MCP Inspector is an essential debugging tool that lets you interactively test and troubleshoot your MCP servers without needing a full AI host application.
Think of it as "Postman for MCP" - it provides a visual interface to send requests, view responses, and understand how your server behaves.
Why Use MCP Inspector?
When building MCP servers, you'll often encounter these challenges:
Prerequisites
Installation
Option 1: Run with npx (Recommended for Quick Testing)
npx @modelcontextprotocol/inspector
Option 2: Install Globally
npm install -g @modelcontextprotocol/inspector
mcp-inspector
Option 3: Add to Your Project
cd your-mcp-server-project
npm install --save-dev @modelcontextprotocol/inspector
Add to package.json:
{
"scripts": {
"inspector": "mcp-inspector"
}
}
---
Connecting to Your Server
stdio Servers (Local Process)
For servers that communicate via standard input/output:
# Python server
npx @modelcontextprotocol/inspector python -m your_server_module
# Node.js server
npx @modelcontextprotocol/inspector node ./build/index.js
# With environment variables
OPENAI_API_KEY=xxx npx @modelcontextprotocol/inspector python server.py
SSE/HTTP Servers (Network)
For servers running as HTTP services:
1. Start your server first:
```bash
python server.py # Server running on http://localhost:8080
```
2. Launch Inspector and connect:
```bash
npx @modelcontextprotocol/inspector --sse http://localhost:8080/sse
```
---
Inspector Interface Overview
When Inspector launches, you'll see a web interface (typically at http://localhost:5173):
┌─────────────────────────────────────────────────────────────┐
│ MCP Inspector [Connected ✅] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 🔧 Tools │ │ 📄 Resources│ │ 💬 Prompts │ │
│ │ (3) │ │ (2) │ │ (1) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 📋 Message Log │ │
│ │ ─────────────────────────────────────────────────── │ │
│ │ → initialize │ │
│ │ ← initialized (server info) │ │
│ │ → tools/list │ │
│ │ ← tools (3 tools) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
---
Testing Tools
Listing Available Tools
1. Click the Tools tab
2. Inspector automatically calls tools/list
3. You'll see all registered tools with:
- Tool name
- Description
- Input schema (parameters)
Invoking a Tool
1. Select a tool from the list
2. Fill in the required parameters in the form
3. Click Run Tool
4. View the response in the results panel
Example: Testing a calculator tool
Tool: add
Parameters:
a: 25
b: 17
Response:
{
"content": [
{
"type": "text",
"text": "42"
}
]
}
Debugging Tool Errors
When a tool fails, Inspector shows:
Error Response:
{
"error": {
"code": -32602,
"message": "Invalid params: 'b' is required"
}
}
Common error codes:
---
Testing Resources
Listing Resources
1. Click the Resources tab
2. Inspector calls resources/list
3. You'll see:
- Resource URIs
- Names and descriptions
- MIME types
Reading a Resource
1. Select a resource
2. Click Read Resource
3. View the content returned
Example output:
Resource: file:///config/settings.json
Content-Type: application/json
{
"config": {
"debug": true,
"maxConnections": 10
}
}
---
Testing Prompts
Listing Prompts
1. Click the Prompts tab
2. Inspector calls prompts/list
3. View available prompt templates
Getting a Prompt
1. Select a prompt
2. Fill in any required arguments
3. Click Get Prompt
4. See the rendered prompt messages
---
Message Log Analysis
The message log shows all MCP protocol messages:
14:32:01 → {"jsonrpc":"2.0","id":1,"method":"initialize",...}
14:32:01 ← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25",...}}
14:32:02 → {"jsonrpc":"2.0","id":2,"method":"tools/list"}
14:32:02 ← {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}
14:32:05 → {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add",...}}
14:32:05 ← {"jsonrpc":"2.0","id":3,"result":{"content":[...]}}
What to Look For
→ should have a matching ←"error" in responses---
VS Code Integration
You can run Inspector directly from VS Code:
Using launch.json
Add to .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug with MCP Inspector",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": [
"@modelcontextprotocol/inspector",
"python",
"${workspaceFolder}/server.py"
],
"console": "integratedTerminal"
},
{
"name": "Debug SSE Server with Inspector",
"type": "chrome",
"request": "launch",
"url": "http://localhost:5173",
"preLaunchTask": "Start MCP Inspector"
}
]
}
Using Tasks
Add to .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "Start MCP Inspector",
"type": "shell",
"command": "npx @modelcontextprotocol/inspector node ${workspaceFolder}/build/index.js",
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^$"
},
"background": {
"activeOnStart": true,
"beginsPattern": "Inspector",
"endsPattern": "listening"
}
}
}
]
}
---
Common Debugging Scenarios
Scenario 1: Server Won't Connect
Symptoms: Inspector shows "Disconnected" or hangs on "Connecting..."
Checklist:
1. ✅ Is the server command correct?
2. ✅ Are all dependencies installed?
3. ✅ Is the server path absolute or relative to current directory?
4. ✅ Are required environment variables set?
Debug steps:
# Test server manually first
python -c "import your_server_module; print('OK')"
# Check for import errors
python -m your_server_module 2>&1 | head -20
# Verify MCP SDK is installed
pip show mcp
Scenario 2: Tools Not Appearing
Symptoms: Tools tab shows empty list
Possible causes:
1. Tools not registered during server initialization
2. Server crashed after startup
3. tools/list handler returning empty array
Debug steps:
1. Check message log for tools/list response
2. Add logging to your tool registration code
3. Verify @mcp.tool() decorators are present (Python)
Scenario 3: Tool Returns Error
Symptoms: Tool call returns error response
Debug approach:
1. Read the error message carefully
2. Check parameter types match schema
3. Add try/catch with detailed error messages
4. Check server logs for stack traces
Example improved error handling:
@mcp.tool()
async def my_tool(param1: str, param2: int) -> str:
try:
# Tool logic here
result = process(param1, param2)
return str(result)
except ValueError as e:
raise McpError(f"Invalid parameter: {e}")
except Exception as e:
raise McpError(f"Tool failed: {type(e).__name__}: {e}")
Scenario 4: Resource Content Empty
Symptoms: Resource returns but content is empty or null
Checklist:
1. ✅ File path or URI is correct
2. ✅ Server has permission to read the resource
3. ✅ Resource content is being returned correctly
---
Advanced Inspector Features
Custom Headers (SSE)
npx @modelcontextprotocol/inspector \
--sse http://localhost:8080/sse \
--header "Authorization: Bearer your-token"
Verbose Logging
DEBUG=mcp* npx @modelcontextprotocol/inspector python server.py
Recording Sessions
Inspector can export message logs for later analysis:
1. Click Export Log in the message panel
2. Save the JSON file
3. Share with team members for debugging
---
Best Practices
1. Test early and often - Use Inspector during development, not just when things break
2. Start simple - Test basic connectivity before complex tool calls
3. Check the schema - Many errors come from parameter type mismatches
4. Read error messages - MCP errors are usually descriptive
5. Keep Inspector open - It helps catch issues as you develop
---
What's Next
You've completed Module 3: Getting Started! Continue your learning:
---
Additional Resources
Sampling - delegate features to the Client
Sometimes, you need the MCP Client and the MCP Server to collaborate to achieve a common goal. You might have a case where the Server requires the help of an LLM that sits on the client. For this situation, sampling is what you should use.
Let's explore some use cases and how to build a solution involving sampling.
Overview
In this lesson, we focus on explaining when and where to use Sampling and how to configure it.
Learning Objectives
In this chapter, we will:
What is Sampling and why use it?
Sampling is an advanced feature that works in the following way:
sequenceDiagram
participant User
participant MCP Client
participant LLM
participant MCP Server
User->>MCP Client: Author blog post
MCP Client->>MCP Server: Tool call (blog post draft)
MCP Server->>MCP Client: Sampling request (create summary)
MCP Client->>LLM: Generate blog post summary
LLM->>MCP Client: Summary result
MCP Client->>MCP Server: Sampling response (summary)
MCP Server->>MCP Client: Complete blog post (draft + summary)
MCP Client->>User: Blog post ready
Sampling request
Ok, now we have a mile high view of a credible scenario, let's talk about the sampling request the server sends back to the client. Here's what such a request can look like in JSON-RPC format:
{
"jsonrpc": "2.0",
"id": 1,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Create a blog post summary of the following blog post: <BLOG POST>"
}
}
],
"modelPreferences": {
"hints": [
{
"name": "claude-3-sonnet"
}
],
"intelligencePriority": 0.8,
"speedPriority": 0.5
},
"systemPrompt": "You are a helpful assistant.",
"maxTokens": 100
}
}
There are a few things here worth calling out:
Sampling response
This response is what the MCP Client ends up sending back to the the MCP Server and is the result of the client calling the LLM, wait for that response and then construct this message. Here's what it can look like in JSON-RPC:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"role": "assistant",
"content": {
"type": "text",
"text": "Here's your abstract <ABSTRACT>"
},
"model": "gpt-5",
"stopReason": "endTurn"
}
}
Note how the response is an abstract of the blog post just like we asked for.
Also note how the used model isn't what we asked for but "gpt-5" over "claude-3-sonnet".
This is to illustrate that the user can change their mind on what to use and that your sampling request is a recommendation.
Ok, now that we understand the main flow, and useful task to use it for "blog post creation + abstract", let's see what we need to do to get it to work.
Message types
Sampling messages aren't constrained to just text but you can also send, images and audio. Here's how the JSON-RPC looks different:
Text
{
"type": "text",
"text": "The message content"
}
Image content
{
"type": "image",
"data": "base64-encoded-image-data",
"mimeType": "image/jpeg"
}
Audio content
{
"type": "audio",
"data": "base64-encoded-audio-data",
"mimeType": "audio/wav"
}
> NOTE: for more detailed info on Sampling, check out the official docs
How to Configure Sampling in the Client
> Note: if you're only building a server, you don't need to do much here.
In a client, you need to specify the following feature like so:
{
"capabilities": {
"sampling": {}
}
}
This will then be picked up when your chosen client initializes with the server.
Example of Sampling in Action - Create a Blog Post
Let's code a sampling server together, we will need to do the following:
1. Create a tool on the Server.
1. Said tool should create a sampling request
1. Tool should wait for the client's sampling request to be answered.
1. Then the tool result should be produced.
Let's see the code step by step:
-1- Create the tool
python
@mcp.tool()
async def create_blog(title: str, content: str, ctx: Context[ServerSession, None]) -> str:
"""Create a blog post and generate a summary"""
-2- Create a sampling request
Extend your tool with the following code:
python
post = BlogPost(
id=len(posts) + 1,
title=title,
content=content,
abstract=""
)
prompt = f"Create an abstract of the following blog post: title: {title} and draft: {content} "
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=prompt),
)
],
max_tokens=100,
)
-3- Wait for the response and return response
python
post.abstract = result.content.text
posts.append(post)
# return the complete product
return json.dumps({
"id": post.title,
"abstract": post.abstract
})
-4- Full code
python
from starlette.applications import Starlette
from starlette.routing import Mount, Host
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.types import SamplingMessage, TextContent
import json
from uuid import uuid4
from typing import List
from pydantic import BaseModel
mcp = FastMCP("Blog post generator")
# app = FastAPI()
posts = []
class BlogPost(BaseModel):
id: int
title: str
content: str
abstract: str
posts: List[BlogPost] = []
@mcp.tool()
async def create_blog(title: str, content: str, ctx: Context[ServerSession, None]) -> str:
"""Create a blog post and generate a summary"""
post = BlogPost(
id=len(posts) + 1,
title=title,
content=content,
abstract=""
)
prompt = f"Create an abstract of the following blog post: title: {title} and draft: {content} "
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=prompt),
)
],
max_tokens=100,
)
post.abstract = result.content.text
posts.append(post)
# return the complete blog post
return json.dumps({
"id": post.title,
"abstract": post.abstract
})
if __name__ == "__main__":
print("Starting server...")
# mcp.run()
mcp.run(transport="streamable-http")
# run app with: python server.py
-5- Testing it in Visual Studio Code
To test this out in Visual Studio Code, do the following:
1. Start server in terminal
1. Add it to *mcp.json* (and ensure it's started) e.g something like so:
```json
"servers": {
"blog-server": {
"type": "http",
"url": "http://localhost:8000/mcp"
}
}
```
1. Type a prompt:
```text
create a blog post named "Where Python comes from", the content is "Python is actually named after Monty Python Flying Circus"
```
1. Allow sampling to happen. First time you test this you will be presented with an additional dialog you will need to accept, then you will see the normal dialog for asking you to run a tool
1. Inspect results. You will see the results both nicely rendered in GitHub Copilot Chat but you can also inspect the raw JSON response.
Bonus. Visual Studio Code tooling has great support for sampling. You can configure Sampling access on your installed server by navigating to it like so:
1. Navigate to extension section.
1. Select the cog icon for your installed server in the "MCP SERVERS - INSTALLED" section.
1 Select "Configure Model Access", here you can select which Models GitHub Copilot is allowed to use when performing sampling. You can also see all sampling requests that happened lately by selecting "Show Sampling requests".
Assignment
In this assignment, you will build a slightly different Sampling namely a sampling integration that supports generating a product description. Here's your scenario:
Scenario: The back office worker at an e-commerce needs help, it takes way too much time to generate product descriptions.
Therefore, you are to build a solution where you can call a tool "create_product" with "title" and "keywords" as arguments and it should produce a complete product including a "description" field that should be populated by a client's LLM.
TIP: use what you learned earlier to construct this server and its tool using a sampling request.
Solution
Key Takeaways
Sampling is a powerful feature that allows the server to delegate tasks to the client when it needs the help of an LLM.
What's Next
MCP Apps
MCP Apps is a new paradigm in MCP.
The idea is that not only do you respond with data back from a tool call, you also provide information on how this information should be interacted with.
That means tool results now can contain UI information.
Why would we want that though?
Well, consider how you do things today.
You're likely consuming the results of an MCP Server by putting some type of frontend in front of it, that's code you need to write and maintain.
Sometimes that's what you want, but sometimes it would be great if you could just bring in a snippet of information that is self-contained that has it all from data to user interface.
Overview
This lesson provides practical guidance on MCP Apps, how to get started with it and how to integrate it in your existing Web Apps. MCP Apps is a very new addition to the MCP Standard.
Learning Objectives
By the end of this lesson, you will be able to:
MCP Apps - how does it work
The idea with MCP Apps is to provide a response that essentially is a component to be rendered.
Such a component can have both visuals and interactivity, e.g., button clicks, user input and more.
Let's start with the server side and our MCP Server.
To create an MCP App component you need to create a tool but also the application resource.
These two halves are connected by a resourceUri.
Here's an example. Let's try to visualize what's involved and what parts does what:
server.ts -- responsible for registering tools and the component as a UI component
src/
mcp-app.ts -- wiring up event handlers
mcp-app.html -- the user interface
This visual describes the architecture for creating a component and its logic.
flowchart LR
subgraph Backend[Backend: MCP Server]
T["Register tools: registerAppTool()"]
C["Register component resource: registerAppResource()"]
U[resourceUri]
T --- U
C --- U
end
subgraph Parent[Parent Web Page]
H[Host application]
IFRAME[IFrame container]
H -->|Inject MCP App UI| IFRAME
end
subgraph Frontend[Frontend: MCP App inside IFrame]
UI["User interface: mcp-app.html"]
EH["Event handlers: src/mcp-app.ts"]
UI --> EH
end
IFRAME --> UI
EH -->|Click triggers server tool call| T
T -->|Tool result data| EH
EH -->|Send message to parent page| H
Let's try to describe the responsibilities next for backend and frontend respectively.
The backend
There's two things we need to accomplish here:
Registering the tool
registerAppTool(
server,
"get-time",
{
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async () => {
const time = new Date().toISOString();
return { content: [{ type: "text", text: time }] };
},
);
The preceding code describes the behavior, where it exposes a tool called get-time.
It takes no inputs but ends up producing the current time.
We do have the ability to define an inputSchema for tools where we need to be able to accept user input.
Registering the component
In the same file, we also need to register the component:
const resourceUri = "ui://get-time/mcp-app.html";
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
registerAppResource(
server,
resourceUri,
resourceUri,
{ mimeType: RESOURCE_MIME_TYPE },
async () => {
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
return {
contents: [
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);
Note how we mention resourceUri to connect the component with its tools. Of interest is also the callback where we load the UI file and return the component.
The component frontend
Just like the backend, there's two pieces here:
User interface
Let's have a look at the user interface.
<!-- mcp-app.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Get Time App</title>
</head>
<body>
<p>
<strong>Server Time:</strong> <code id="server-time">Loading...</code>
</p>
<button id="get-time-btn">Get Server Time</button>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
Event wireup
The last piece is the event wireup. That means we identify which part in our UI needs event handlers and what to do if events are raised:
// mcp-app.ts
import { App } from "@modelcontextprotocol/ext-apps";
// Get element references
const serverTimeEl = document.getElementById("server-time")!;
const getTimeBtn = document.getElementById("get-time-btn")!;
// Create app instance
const app = new App({ name: "Get Time App", version: "1.0.0" });
// Handle tool results from the server. Set before `app.connect()` to avoid
// missing the initial tool result.
app.ontoolresult = (result) => {
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
};
// Wire up button click
getTimeBtn.addEventListener("click", async () => {
// `app.callServerTool()` lets the UI request fresh data from the server
const result = await app.callServerTool({ name: "get-time", arguments: {} });
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
});
// Connect to host
app.connect();
As you can see from the above, this is normal code for hooking up DOM elements to events.
Worth calling out is the call to callServerTool that ends up calling a tool on the backend.
Dealing with user input
So far, we've seen a component that has a button that when clicked calls a tool. Let's see if we can add more UI elements like an input field and see if we can send arguments to a tool. Let's implement an FAQ functionality. Here's how it should work:
Let's add the needed support to the backend first:
const faq: { [key: string]: string } = {
"shipping": "Our standard shipping time is 3-5 business days.",
"return policy": "You can return any item within 30 days of purchase.",
"warranty": "All products come with a 1-year warranty covering manufacturing defects.",
}
registerAppTool(
server,
"get-faq",
{
title: "Search FAQ",
description: "Searches the FAQ for relevant answers.",
inputSchema: zod.object({
query: zod.string().default("shipping"),
}),
_meta: { ui: { resourceUri: faqResourceUri } }, // Links this tool to its UI resource
},
async ({ query }) => {
const answer: string = faq[query.toLowerCase()] || "Sorry, I don't have an answer for that.";
return { content: [{ type: "text", text: answer }] };
},
);
What we're seeing here is how we populate inputSchema and give it a zod schema like so:
inputSchema: zod.object({
query: zod.string().default("shipping"),
})
In the above schema we declare we have an input parameter called query and that it's optional with a default value of "shipping".
Ok, let's move on to *mcp-app.html* to see what UI we need to create for this:
<div class="faq">
<h1>FAQ response</h1>
<p>FAQ Response: <code id="faq-response">Loading...</code></p>
<input type="text" id="faq-query" placeholder="Enter FAQ query" />
<button id="get-faq-btn">Get FAQ Response</button>
</div>
Great, now we have an input element and button. Let's go to *mcp-app.ts* next to wire up these events:
const getFaqBtn = document.getElementById("get-faq-btn")!;
const faqQueryInput = document.getElementById("faq-query") as HTMLInputElement;
getFaqBtn.addEventListener("click", async () => {
const query = faqQueryInput.value;
const result = await app.callServerTool({ name: "get-faq", arguments: { query } });
const faq = result.content?.find((c) => c.type === "text")?.text;
faqResponseEl.textContent = faq ?? "[ERROR]";
});
In the code above we:
app.callServerTool() with name and arguments where the latter is passing query as value. What actually happens when you call callServerTool is that it sends a message to the parent window and that window ends up calling the MCP Server.
Try it out
Trying this out we should now see the following:
and here's where we try it with input like "warranty"
To run this code, head over to Code section
Here's a sample demonstrating MCP App
Install
1. Navigate to *mcp-app* folder
1. Run npm install, this should install frontend and backend dependencies
Verify the backend compiles by running:
npx tsc --noEmit
There should be no output if everything is fine.
Run backend
> This takes a bit of extra work if you're on a Windows machine as the MCP Apps solution uses concurrently library to run that you need to find a replacement for.
Here's the offending line *package.json* on the MCP App:
```json
"start": "concurrently \"cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch\" \"tsx watch main.ts\""
```
This app has two parts, a backend part and a host part.
Start the backend by calling:
npm start
This should shart the backend on http://localhost:3001/mcp.
> Note, if you're in a Codespace, you may need to set port visibility to public. Check you can reach endpoint in the browser through https://
Choice -1 Test the app in Visual Studio Code
To test the solution in Visual Studio Code, do the following:
mcp.json like so:```json
{
"servers": {
"my-mcp-server-7178eca7": {
"url": "http://localhost:3001/mcp",
"type": "http"
}
},
"inputs": []
}
```
1. Click the "start" button in *mcp.json*
1. Make sure a chat window is open and type get-faq, you should see a result like so:
Choice -2- Test the app with a host
The repo
We will present you with two different options here:
Local machine
```sh
npm install
```
> if you Codespace, you need to navigate to serve.ts and line 27 and replace http://localhost:3001/mcp with your Codespace URL for the backend, so for example https://psychic-xylophone-657rpjgvxpc5g64-3001.app.github.dev/mcp
```sh
npm start
```
This should connect the host with backend and you should see the app running like so:
Codespace
It takes a bit of extra work to get a Codespace environment to work. To use a host through Codespace:
npm install to install dependenciesnpm start to start the host.Test out the app
Try the app in the following way:
Great, it's all working.
Testing in Visual Studio Code
Visual Studio Code has great support for MCP Apps and is probably one of the easiest ways of testing your MCP Apps. To use Visual Studio Code, add a server entry to *mcp.json* like so:
"my-mcp-server-7178eca7": {
"url": "http://localhost:3001/mcp",
"type": "http"
}
Then start the server, you should be able to communicate with your MCP App through the Chat Window provided you have GitHub Copilot installed.
You can trigger it via a prompt, for example "#get-faq":
and just like when you ran it through a web browser, it renders the same way like so:
Assignment
Create a rock paper scissor game. It should consist of the following:
UI:
Server:
Solution
Summary
We learned about this new paradigm MCP Apps. It's a new paradigm that allows MCP Servers to have an opinion about not only the data but also how this data should be presented.
Additionally, we learned that these MCP Apps are hosted into an IFrame and to communicate with MCP Servers they would need to send messages to the parent web app.
There are several libraries out there for both plain JavaScript and React and more that makes this communication easier.
Key Takeaways
Here's what you learned:
What's Next
The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to LLMs.
Think of MCP like a USB-C port for AI applications - it provides a standardized way to connect AI models to different data sources and tools.
Learning Objectives
By the end of this lesson, you will be able to:
Setting Up Your MCP Environment
Before you begin working with MCP, it's important to prepare your development environment and understand the basic workflow. This section will guide you through the initial setup steps to ensure a smooth start with MCP.
Prerequisites
Before diving into MCP development, ensure you have:
Official SDKs
In the upcoming chapters you will see solutions built using Python, TypeScript, Java and .NET. Here are all the officially supported SDKs.
MCP provides official SDKs for multiple languages (aligned with MCP Specification 2025-11-25):
Key Takeaways
Practicing
We have a set of samples that complements the exercises you will see in all chapters in this section. Additionally each chapter also has their own exercises and assignments
Basic Calculator MCP Service
This service provides basic calculator operations through the Model Context Protocol (MCP) using Spring Boot with WebFlux transport. It's designed as a simple example for beginners learning about MCP implementations.
For more information, see the MCP Server Boot Starter reference documentation.
Overview
The service showcases:
@Tool annotation- Addition, subtraction, multiplication, division
- Power calculation and square root
- Modulus (remainder) and absolute value
- Help function for operation descriptions
Features
This calculator service offers the following capabilities:
1. Basic Arithmetic Operations:
- Addition of two numbers
- Subtraction of one number from another
- Multiplication of two numbers
- Division of one number by another (with zero division check)
2. Advanced Operations:
- Power calculation (raising a base to an exponent)
- Square root calculation (with negative number check)
- Modulus (remainder) calculation
- Absolute value calculation
3. Help System:
- Built-in help function explaining all available operations
Using the Service
The service exposes the following API endpoints through the MCP protocol:
add(a, b): Add two numbers togethersubtract(a, b): Subtract the second number from the firstmultiply(a, b): Multiply two numbersdivide(a, b): Divide the first number by the second (with zero check)power(base, exponent): Calculate the power of a numbersquareRoot(number): Calculate the square root (with negative number check)modulus(a, b): Calculate the remainder when dividingabsolute(number): Calculate the absolute valuehelp(): Get information about available operationsTest Client
A simple test client is included in the com.microsoft.mcp.sample.client package.
The SampleCalculatorClient class demonstrates the available operations of the calculator service.
Using the LangChain4j Client
The project includes a LangChain4j example client in com.microsoft.mcp.sample.client.LangChain4jClient that demonstrates how to integrate the calculator service with LangChain4j and GitHub models:
Prerequisites
1. GitHub Token Setup:
To use GitHub's AI models (like phi-4), you need a GitHub personal access token:
a. Go to your GitHub account settings: https://github.com/settings/tokens
b. Click "Generate new token" → "Generate new token (classic)"
c. Give your token a descriptive name
d. Select the following scopes:
- repo (Full control of private repositories)
- read:org (Read org and team membership, read org projects)
- gist (Create gists)
- user:email (Access user email addresses (read-only))
e. Click "Generate token" and copy your new token
f. Set it as an environment variable:
On Windows:
```
set GITHUB_TOKEN=your-github-token
```
On macOS/Linux:
```bash
export GITHUB_TOKEN=your-github-token
```
g. For persistent setup, add it to your environment variables through system settings
2. Add the LangChain4j GitHub dependency to your project (already included in pom.xml):
```xml
```
3. Ensure the calculator server is running on localhost:8080
Running the LangChain4j Client
This example demonstrates:
The client sends the following sample queries to demonstrate functionality:
1. Calculating the sum of two numbers
2. Finding the square root of a number
3. Getting help information about available calculator operations
Run the example and check the console output to see how the AI model uses the calculator tools to respond to queries.
GitHub Model Configuration
The LangChain4j client is configured to use GitHub's phi-4 model with the following settings:
ChatLanguageModel model = GitHubChatModel.builder()
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("phi-4")
.logRequests(true)
.logResponses(true)
.build();
To use different GitHub models, simply change the modelName parameter to another supported model (e.g., "claude-3-haiku-20240307", "llama-3-70b-8192", etc.).
Dependencies
The project requires the following key dependencies:
<!-- For MCP Server -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<!-- For LangChain4j integration -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- For GitHub models support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github</artifactId>
<version>${langchain4j.version}</version>
</dependency>
Building the Project
Build the project using Maven:
./mvnw clean install -DskipTests
Running the Server
Using Java
java -jar target/calculator-server-0.0.1-SNAPSHOT.jar
Using MCP Inspector
The MCP Inspector is a helpful tool for interacting with MCP services. To use it with this calculator service:
1. Install and run MCP Inspector in a new terminal window:
```bash
npx @modelcontextprotocol/inspector
```
2. Access the web UI by clicking the URL displayed by the app (typically http://localhost:6274)
3. Configure the connection:
- Set the transport type to "SSE"
- Set the URL to your running server's SSE endpoint: http://localhost:8080/sse
- Click "Connect"
4. Use the tools:
- Click "List Tools" to see available calculator operations
- Select a tool and click "Run Tool" to execute an operation
Using Docker
The project includes a Dockerfile for containerized deployment:
1. Build the Docker image:
```bash
docker build -t calculator-mcp-service .
```
2. Run the Docker container:
```bash
docker run -p 8080:8080 calculator-mcp-service
```
This will:
You can access the service at http://localhost:8080 once the container is running.
Troubleshooting
Common Issues with GitHub Token
1. Token Permission Issues: If you get a 403 Forbidden error, check that your token has the correct permissions as outlined in the prerequisites.
2. Token Not Found: If you get a "No API key found" error, ensure the GITHUB_TOKEN environment variable is properly set.
3. Rate Limiting: GitHub API has rate limits. If you encounter a rate limit error (status code 429), wait a few minutes before trying again.
4. Token Expiration: GitHub tokens can expire. If you receive authentication errors after some time, generate a new token and update your environment variable.
If you need further assistance, check the LangChain4j documentation or GitHub API documentation.
Sample
This is a JavaScript sample for an MCP Server
Here's what the calculator portion of it looks like:
// Define calculator tools for each operation
server.tool(
"add",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.tool(
"subtract",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a - b) }]
})
);
server.tool(
"multiply",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a * b) }]
})
);
server.tool(
"divide",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => {
if (b === 0) {
return {
content: [{ type: "text", text: "Error: Cannot divide by zero" }],
isError: true
};
}
return {
content: [{ type: "text", text: String(a / b) }]
};
}
);
Install
Run the following command:
npm install
Run
npm start
Sample
This is a Typescript sample for an MCP Server
Here's what the calculator portion looks like:
// Define calculator tools for each operation
server.tool(
"add",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.tool(
"subtract",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a - b) }]
})
);
server.tool(
"multiply",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a * b) }]
})
);
server.tool(
"divide",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => {
if (b === 0) {
return {
content: [{ type: "text", text: "Error: Cannot divide by zero" }],
isError: true
};
}
return {
content: [{ type: "text", text: String(a / b) }]
};
}
);
Install
Run the following command:
npm install
Run
npm start
Additional Resources
What's next
Start with the first lesson: Creating your first MCP Server
Getting Started with MCP
Welcome to your first steps with the Model Context Protocol (MCP)!
Whether you're new to MCP or looking to deepen your understanding, this guide will walk you through the essential setup and development process.
You'll discover how MCP enables seamless integration between AI models and applications, and learn how to quickly get your environment ready for building and testing MCP-powered solutions.
> TLDR; If you build AI apps, you know that you can add tools and other resources to your LLM (large language model), to make the LLM more knowledgeable.
However if you place those tools and resources on a server, the app and the server capabilities can be used by any client with/without an LLM.
Overview
This lesson provides practical guidance on setting up MCP environments and building your first MCP applications.
You'll learn how to set up the necessary tools and frameworks, build basic MCP servers, create host applications, and test your implementations.
The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to LLMs.
Think of MCP like a USB-C port for AI applications - it provides a standardized way to connect AI models to different data sources and tools.
Learning Objectives
By the end of this lesson, you will be able to:
Setting Up Your MCP Environment
Before you begin working with MCP, it's important to prepare your development environment and understand the basic workflow. This section will guide you through the initial setup steps to ensure a smooth start with MCP.
Prerequisites
Before diving into MCP development, ensure you have:
Basic MCP Server Structure
An MCP server typically includes:
Here's a simplified example in TypeScript:
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(
"file",
// The 'list' parameter controls how the resource lists available files. Setting it to undefined disables listing for this resource.
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => ({
contents: [{
uri: uri.href,
text: `File, ${path}!`
}]
})
);
// Add a file resource that reads the file contents
server.resource(
"file",
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => {
let text;
try {
text = await fs.readFile(path, "utf8");
} catch (err) {
text = `Error reading file: ${err.message}`;
}
return {
contents: [{
uri: uri.href,
text
}]
};
}
);
server.prompt(
"review-code",
{ code: z.string() },
({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please review this code:\n\n${code}`
}
}]
})
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
In the preceding code we:
calculator) with a handler function.Testing and Debugging
Before you begin testing your MCP server, it's important to understand the available tools and best practices for debugging.
Effective testing ensures your server behaves as expected and helps you quickly identify and resolve issues.
The following section outlines recommended approaches for validating your MCP implementation.
MCP provides tools to help you test and debug your servers:
Using MCP Inspector
The MCP Inspector is a visual testing tool that helps you:
1. Discover Server Capabilities: Automatically detect available resources, tools, and prompts
2. Test Tool Execution: Try different parameters and see responses in real-time
3. View Server Metadata: Examine server info, schemas, and configurations
# ex TypeScript, installing and running MCP Inspector
npx @modelcontextprotocol/inspector node build/index.js
When you run the above commands, the MCP Inspector will launch a local web interface in your browser.
You can expect to see a dashboard displaying your registered MCP servers, their available tools, resources, and prompts.
The interface allows you to interactively test tool execution, inspect server metadata, and view real-time responses, making it easier to validate and debug your MCP server implementations.
Here's a screenshot of what it can look like:
Common Setup Issues and Solutions
Local Development
For local development and testing, you can run MCP servers directly on your machine:
1. Start the server process: Run your MCP server application
2. Configure networking: Ensure the server is accessible on the expected port
3. Connect clients: Use local connection URLs like http://localhost:3000
# Example: Running a TypeScript MCP server locally
npm run start
# Server running at http://localhost:3000
Building your first MCP Server
We've covered Core concepts in a previous lesson, now it's time to put that knowledge to work.
What a server can do
Before we start writing code, let's just remind ourselves what a server can do:
An MCP server can for example:
Great, now that we know what we can do for it, let's start coding.
Exercise: Creating a server
To create a server, you need to follow these steps:
-1- Create project
TypeScript
# Create project directory and initialize npm project
mkdir calculator-server
cd calculator-server
npm init -y
Python
# Create project dir
mkdir calculator-server
cd calculator-server
# Open the folder in Visual Studio Code - Skip this if you are using a different IDE
code .
.NET
dotnet new console -n McpCalculatorServer
cd McpCalculatorServer
Java
For Java, create a Spring Boot project:
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d javaVersion=21 \
-d type=maven-project \
-d groupId=com.example \
-d artifactId=calculator-server \
-d name=McpServer \
-d packageName=com.microsoft.mcp.sample.server \
-o calculator-server.zip
Extract the zip file:
unzip calculator-server.zip -d calculator-server
cd calculator-server
# optional remove the unused test
rm -rf src/test/java
Add the following complete configuration to your *pom.xml* file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Spring Boot parent for dependency management -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath />
</parent>
<!-- Project coordinates -->
<groupId>com.example</groupId>
<artifactId>calculator-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Calculator Server</name>
<description>Basic calculator MCP service for beginners</description>
<!-- Properties -->
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!-- Spring AI BOM for version management -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Dependencies -->
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Build configuration -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
<!-- Repositories for Spring AI snapshots -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>
Rust
mkdir calculator-server
cd calculator-server
cargo init
-2- Add dependencies
Now that you have your project created, let's add dependencies next:
TypeScript
# If not already installed, install TypeScript globally
npm install typescript -g
# Install the MCP SDK and Zod for schema validation
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
Python
# Create a virtual env and install dependencies
python -m venv venv
venv\Scripts\activate
pip install "mcp[cli]"
Java
cd calculator-server
./mvnw clean install -DskipTests
Rust
cargo add rmcp --features server,transport-io
cargo add serde
cargo add tokio --features rt-multi-thread
-3- Create project files
TypeScript
Open the *package.json* file and replace the content with the following to ensure you can build and run the server:
{
"name": "calculator-server",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "npm run build && node ./build/index.js",
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "A simple calculator server using Model Context Protocol",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.14",
"typescript": "^5.8.3"
}
}
Create a *tsconfig.json* with the following content:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Create a directory for your source code:
mkdir src
touch src/index.ts
Python
Create a file *server.py*
touch server.py
.NET
Install the required NuGet packages:
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
Java
For Java Spring Boot projects, the project structure is created automatically.
Rust
For Rust, a *src/main.rs* file is created by default when you run cargo init. Open the file and delete the default code.
-4- Create server code
TypeScript
Create a file *index.ts* and add the following code:
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: "Calculator MCP Server",
version: "1.0.0"
});
Now you have a server, but it doesn't do much, let' fix that.
Python
# server.py
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Demo")
.NET
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();
// add features
Java
For Java, create the core server components. First, modify the main application class:
*src/main/java/com/microsoft/mcp/sample/server/McpServerApplication.java*:
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
Create the calculator service *src/main/java/com/microsoft/mcp/sample/server/service/CalculatorService.java*:
package com.microsoft.mcp.sample.server.service;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
/**
* Service for basic calculator operations.
* This service provides simple calculator functionality through MCP.
*/
@Service
public class CalculatorService {
/**
* Add two numbers
* @param a The first number
* @param b The second number
* @return The sum of the two numbers
*/
@Tool(description = "Add two numbers together")
public String add(double a, double b) {
double result = a + b;
return formatResult(a, "+", b, result);
}
/**
* Subtract one number from another
* @param a The number to subtract from
* @param b The number to subtract
* @return The result of the subtraction
*/
@Tool(description = "Subtract the second number from the first number")
public String subtract(double a, double b) {
double result = a - b;
return formatResult(a, "-", b, result);
}
/**
* Multiply two numbers
* @param a The first number
* @param b The second number
* @return The product of the two numbers
*/
@Tool(description = "Multiply two numbers together")
public String multiply(double a, double b) {
double result = a * b;
return formatResult(a, "*", b, result);
}
/**
* Divide one number by another
* @param a The numerator
* @param b The denominator
* @return The result of the division
*/
@Tool(description = "Divide the first number by the second number")
public String divide(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a / b;
return formatResult(a, "/", b, result);
}
/**
* Calculate the power of a number
* @param base The base number
* @param exponent The exponent
* @return The result of raising the base to the exponent
*/
@Tool(description = "Calculate the power of a number (base raised to an exponent)")
public String power(double base, double exponent) {
double result = Math.pow(base, exponent);
return formatResult(base, "^", exponent, result);
}
/**
* Calculate the square root of a number
* @param number The number to find the square root of
* @return The square root of the number
*/
@Tool(description = "Calculate the square root of a number")
public String squareRoot(double number) {
if (number < 0) {
return "Error: Cannot calculate square root of a negative number";
}
double result = Math.sqrt(number);
return String.format("√%.2f = %.2f", number, result);
}
/**
* Calculate the modulus (remainder) of division
* @param a The dividend
* @param b The divisor
* @return The remainder of the division
*/
@Tool(description = "Calculate the remainder when one number is divided by another")
public String modulus(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a % b;
return formatResult(a, "%", b, result);
}
/**
* Calculate the absolute value of a number
* @param number The number to find the absolute value of
* @return The absolute value of the number
*/
@Tool(description = "Calculate the absolute value of a number")
public String absolute(double number) {
double result = Math.abs(number);
return String.format("|%.2f| = %.2f", number, result);
}
/**
* Get help about available calculator operations
* @return Information about available operations
*/
@Tool(description = "Get help about available calculator operations")
public String help() {
return "Basic Calculator MCP Service\n\n" +
"Available operations:\n" +
"1. add(a, b) - Adds two numbers\n" +
"2. subtract(a, b) - Subtracts the second number from the first\n" +
"3. multiply(a, b) - Multiplies two numbers\n" +
"4. divide(a, b) - Divides the first number by the second\n" +
"5. power(base, exponent) - Raises a number to a power\n" +
"6. squareRoot(number) - Calculates the square root\n" +
"7. modulus(a, b) - Calculates the remainder of division\n" +
"8. absolute(number) - Calculates the absolute value\n\n" +
"Example usage: add(5, 3) will return 5 + 3 = 8";
}
/**
* Format the result of a calculation
*/
private String formatResult(double a, String operator, double b, double result) {
return String.format("%.2f %s %.2f = %.2f", a, operator, b, result);
}
}
Optional components for a production-ready service:
Create a startup configuration *src/main/java/com/microsoft/mcp/sample/server/config/StartupConfig.java*:
package com.microsoft.mcp.sample.server.config;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StartupConfig {
@Bean
public CommandLineRunner startupInfo() {
return args -> {
System.out.println("\n" + "=".repeat(60));
System.out.println("Calculator MCP Server is starting...");
System.out.println("SSE endpoint: http://localhost:8080/sse");
System.out.println("Health check: http://localhost:8080/actuator/health");
System.out.println("=".repeat(60) + "\n");
};
}
}
Create a health controller *src/main/java/com/microsoft/mcp/sample/server/controller/HealthController.java*:
package com.microsoft.mcp.sample.server.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> healthCheck() {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("timestamp", LocalDateTime.now().toString());
response.put("service", "Calculator MCP Server");
return ResponseEntity.ok(response);
}
}
Create an exception handler *src/main/java/com/microsoft/mcp/sample/server/exception/GlobalExceptionHandler.java*:
package com.microsoft.mcp.sample.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
"Invalid_Input",
"Invalid input parameter: " + ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
public static class ErrorResponse {
private String code;
private String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
// Getters
public String getCode() { return code; }
public String getMessage() { return message; }
}
}
Create a custom banner *src/main/resources/banner.txt*:
_____ _ _ _
/ ____| | | | | | |
| | __ _| | ___ _ _| | __ _| |_ ___ _ __
| | / _` | |/ __| | | | |/ _` | __/ _ \| '__|
| |___| (_| | | (__| |_| | | (_| | || (_) | |
\_____\__,_|_|\___|\__,_|_|\__,_|\__\___/|_|
Calculator MCP Server v1.0
Spring Boot MCP Application
Rust
Add the following code to the top of the *src/main.rs* file. This imports the necessary libraries and modules for your MCP server.
use rmcp::{
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
ServerHandler, ServiceExt,
};
use std::error::Error;
The calculator server will be a simple one that can add two numbers together. Let's create a struct to represent the calculator request.
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
Next, create a struct to represent the calculator server. This struct will hold the tool router, which is used to register tools.
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
Now, we can implement the Calculator struct to create a new instance of the server and implement the server handler to provide server information.
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
Finally, we need to implement the main function to start the server.
This function will create an instance of the Calculator struct and serve it over standard input/output.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
The server is now set up to provide basic information about itself. Next, we will add a tool to perform addition.
-5- Adding a tool and a resource
Add a tool and a resource by adding the following code:
TypeScript
server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
Your tool takes parameters a and b and runs a function that produces a response on the form:
{
contents: [{
type: "text", content: "some content"
}]
}
Your resource is accessed through a string "greeting" and takes a parameter name and produces a similar response to the tool:
{
uri: "<href>",
text: "a text"
}
Python
# 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}!"
In the preceding code we've:
add that takes parameters a and b, both integers.greeting that takes parameter name..NET
Add this to your Program.cs file:
[McpServerToolType]
public static class CalculatorTool
{
[McpServerTool, Description("Adds two numbers")]
public static string Add(int a, int b) => $"Sum {a + b}";
}
Java
The tools have already been created in the previous step.
Rust
Add a new tool inside the impl Calculator block:
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
-6- Final code
Let's add the last code we need so the server can start:
TypeScript
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
Here's the full code:
// index.ts
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: "Calculator MCP Server",
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
const transport = new StdioServerTransport();
server.connect(transport);
Python
# 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}!"
# Main execution block - this is required to run the server
if __name__ == "__main__":
mcp.run()
.NET
Create a Program.cs file with the following content:
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}";
}
Java
Your complete main application class should look like this:
// McpServerApplication.java
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
Rust
The final code for the Rust server should look like this:
use rmcp::{
ServerHandler, ServiceExt,
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
};
use std::error::Error;
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
-7- Test the server
Start the server with the following command:
TypeScript
npm run build
Python
mcp run server.py
> To use MCP Inspector, use mcp dev server.py which automatically launches the Inspector and provides the required proxy session token.
If using mcp run server.py, you’ll need to manually start the Inspector and configure the connection.
.NET
Make sure you're in your project directory:
cd McpCalculatorServer
dotnet run
Java
./mvnw clean install -DskipTests
java -jar target/calculator-server-0.0.1-SNAPSHOT.jar
Rust
Run the following commands to format and run the server:
cargo fmt
cargo run
-8- Run using the inspector
The inspector is a great tool that can start up your server and lets you interact with it so you can test that it works. Let's start it up:
> [!NOTE]
> it might look different in the "command" field as it contains the command for running a server with your specific runtime/
TypeScript
npx @modelcontextprotocol/inspector node build/index.js
or add it to your *package.json* like so: "inspector": "npx @modelcontextprotocol/inspector node build/index.js" and then run npm run inspector
Python
Python wraps a Node.js tool called inspector. It's possible to call said tool like so:
mcp dev server.py
However, it doesn't implement all the methods available on the tool so you're recommended to run the Node.js tool directly like below:
npx @modelcontextprotocol/inspector mcp run server.py
If you're using a tool or IDE that allows you to configure commands and arguments for running scripts,
make sure to set python in the Command field and server.py as Arguments.
This ensures the script runs correctly.
.NET
Make sure you're in your project directory:
cd McpCalculatorServer
npx @modelcontextprotocol/inspector dotnet run
Java
Ensure you calculator server is running
The run the inspector:
npx @modelcontextprotocol/inspector
In the inspector web interface:
1. Select "SSE" as the transport type
2. Set the URL to: http://localhost:8080/sse
3. Click "Connect"
You're now connected to the server
The Java server testing section is completed now
The next section it's about interacting with the server.
You should see the following user interface:
1. Connect to the server by selecting the Connect button
Once you connect to the server, you should now see the following:
1. Select "Tools" and "listTools", you should see "Add" show up, select "Add" and fill in the parameter values.
You should see the following response, i.e a result from "add" tool:
Congrats, you've managed to create and run your first server!
Rust
To run the Rust server with the MCP Inspector CLI, use the following command:
npx @modelcontextprotocol/inspector cargo run --cli --method tools/call --tool-name add --tool-arg a=1 b=2
Official SDKs
MCP provides official SDKs for multiple languages:
Key Takeaways
Samples
Assignment
Create a simple MCP server with a tool of your choice:
1. Implement the tool in your preferred language (.NET, Java, Python, TypeScript, or Rust).
2. Define input parameters and return values.
3. Run the inspector tool to ensure the server works as intended.
4. Test the implementation with various inputs.
Solution
Additional Resources
What's next
Once you've completed this module, continue to: Module 4: Practical Implementation
Module 03 — 시작하기
시작하기
_(위 이미지를 클릭하면 이 강의의 영상을 볼 수 있습니다)_
이 섹션은 여러 강의로 구성되어 있습니다:
MCP 시작하기
Model Context Protocol (MCP)와 함께하는 첫 걸음에 오신 것을 환영합니다! MCP가 처음이든 이해도를 높이고자 하든, 이 가이드는 필수 설정 및 개발 과정을 안내합니다. MCP가 AI 모델과 애플리케이션 간의 원활한 통합을 어떻게 가능하게 하는지 살펴보고, MCP 기반 솔루션 구축 및 테스트를 위한 환경을 빠르게 준비하는 방법을 배우게 됩니다.
> TLDR; AI 애플리케이션을 개발한다면 LLM(대형 언어 모델)에 도구와 기타 리소스를 추가하여 LLM을 더 똑똑하게 만들 수 있다는 것을 아실 겁니다. 하지만 도구와 리소스를 서버에 배치하면 앱과 서버 기능은 LLM이 있든 없든 모든 클라이언트가 사용할 수 있습니다.
개요
이 수업은 MCP 환경 설정과 첫 MCP 애플리케이션 구축에 관한 실용적인 안내를 제공합니다. 필요한 도구 및 프레임워크 설정, 기본 MCP 서버 구축, 호스트 애플리케이션 생성, 구현 테스트 방법을 배우게 됩니다.
Model Context Protocol (MCP)은 애플리케이션이 LLM에 컨텍스트를 제공하는 방식을 표준화하는 오픈 프로토콜입니다. MCP는 AI 애플리케이션을 위한 USB-C 포트와 같아서 AI 모델을 다양한 데이터 소스 및 도구와 연결하는 표준화된 방법을 제공합니다.
학습 목표
이 수업을 마치면 다음을 수행할 수 있습니다:
MCP 환경 설정하기
MCP 작업을 시작하기 전에 개발 환경을 준비하고 기본 작업 흐름을 이해하는 것이 중요합니다. 이 섹션은 MCP 시작을 원활하게 하기 위한 초기 설정 단계를 안내합니다.
사전 준비 사항
MCP 개발에 착수하기 전에 다음이 준비되었는지 확인하세요:
기본 MCP 서버 구조
MCP 서버는 일반적으로 다음을 포함합니다:
아래는 TypeScript 예제입니다:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// MCP 서버를 생성합니다
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// 추가 도구를 추가합니다
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// 동적 인사말 리소스를 추가합니다
server.resource(
"file",
// 'list' 매개변수는 리소스가 사용 가능한 파일을 나열하는 방식을 제어합니다. undefined로 설정하면 이 리소스의 목록 표시가 비활성화됩니다.
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => ({
contents: [{
uri: uri.href,
text: `File, ${path}!`
}]
})
);
// 파일 내용을 읽는 파일 리소스를 추가합니다
server.resource(
"file",
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => {
let text;
try {
text = await fs.readFile(path, "utf8");
} catch (err) {
text = `Error reading file: ${err.message}`;
}
return {
contents: [{
uri: uri.href,
text
}]
};
}
);
server.prompt(
"review-code",
{ code: z.string() },
({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please review this code:\n\n${code}`
}
}]
})
);
// stdin에서 메시지를 받고 stdout으로 메시지를 전송하기 시작합니다
const transport = new StdioServerTransport();
await server.connect(transport);
위 코드에서 우리는:
calculator)를 등록했습니다.테스트 및 디버깅
MCP 서버를 테스트하기 전에 이용 가능한 도구 및 디버깅 모범 사례를 이해하는 것이 중요합니다. 효과적인 테스트는 서버가 예상대로 작동하는지 확인하고 문제를 신속히 파악 및 해결하는 데 도움이 됩니다. 다음 섹션에서 MCP 구현을 검증하기 위한 권장 방법을 설명합니다.
MCP는 서버 테스트 및 디버깅을 도와주는 도구를 제공합니다:
MCP Inspector 사용하기
1. 서버 기능 탐색: 사용 가능한 리소스, 도구, 프롬프트 자동 감지
2. 도구 실행 테스트: 다양한 매개변수로 실시간 응답 확인
3. 서버 메타데이터 조회: 서버 정보, 스키마, 구성 검토
# 예제 TypeScript, MCP Inspector 설치 및 실행
npx @modelcontextprotocol/inspector node build/index.js
위 명령어를 실행하면 MCP Inspector가 브라우저에서 로컬 웹 인터페이스를 실행합니다. 등록된 MCP 서버, 사용 가능한 도구, 리소스 및 프롬프트 대시보드를 볼 수 있습니다. 이 인터페이스로 도구 실행 테스트, 서버 메타데이터 조사, 실시간 응답 확인 등이 가능해 MCP 서버 구현 검증 및 디버깅이 수월해집니다.
다음은 화면 예시입니다:
일반적인 설정 문제 및 해결책
로컬 개발
로컬 개발 및 테스트용으로, MCP 서버를 자신의 머신에서 직접 실행할 수 있습니다:
1. 서버 프로세스 시작: MCP 서버 애플리케이션 실행
2. 네트워킹 구성: 서버가 예상 포트에서 접근 가능하게 설정
3. 클라이언트 연결: http://localhost:3000 같은 로컬 연결 URL 사용
# 예시: TypeScript MCP 서버를 로컬에서 실행하기
npm run start
# 서버가 http://localhost:3000 에서 실행 중입니다
첫 번째 MCP 서버 구축하기
이전 수업에서 핵심 개념을 다뤘으니 이제 그 지식을 실습해 보겠습니다.
서버가 할 수 있는 일
코딩을 시작하기 전에 서버의 역할을 상기해 봅시다:
MCP 서버는 예를 들어:
좋습니다, 무엇을 할 수 있는지 알았으니 코딩을 시작해 봅시다.
연습: 서버 만들기
서버를 만들려면 다음 단계를 따르세요:
-1- 프로젝트 생성하기
TypeScript
# 프로젝트 디렉토리를 생성하고 npm 프로젝트를 초기화하십시오
mkdir calculator-server
cd calculator-server
npm init -y
Python
# 프로젝트 디렉토리 생성
mkdir calculator-server
cd calculator-server
# Visual Studio Code에서 폴더 열기 - 다른 IDE를 사용하는 경우 생략하세요
code .
.NET
dotnet new console -n McpCalculatorServer
cd McpCalculatorServer
Java
Java의 경우 Spring Boot 프로젝트를 만드세요:
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d javaVersion=21 \
-d type=maven-project \
-d groupId=com.example \
-d artifactId=calculator-server \
-d name=McpServer \
-d packageName=com.microsoft.mcp.sample.server \
-o calculator-server.zip
압축 파일 풀기:
unzip calculator-server.zip -d calculator-server
cd calculator-server
# 선택적으로 사용하지 않는 테스트 제거
rm -rf src/test/java
*pom.xml* 파일에 다음과 같은 전체 구성을 추가하세요:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Spring Boot parent for dependency management -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath />
</parent>
<!-- Project coordinates -->
<groupId>com.example</groupId>
<artifactId>calculator-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Calculator Server</name>
<description>Basic calculator MCP service for beginners</description>
<!-- Properties -->
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!-- Spring AI BOM for version management -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Dependencies -->
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Build configuration -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
<!-- Repositories for Spring AI snapshots -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>
Rust
mkdir calculator-server
cd calculator-server
cargo init
-2- 의존성 추가하기
프로젝트를 생성했으니 다음은 의존성 추가입니다:
TypeScript
# 아직 설치하지 않은 경우 TypeScript를 전역에 설치하세요
npm install typescript -g
# MCP SDK와 스키마 검증을 위해 Zod를 설치하세요
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
Python
# 가상 환경을 만들고 종속성을 설치합니다
python -m venv venv
venv\Scripts\activate
pip install "mcp[cli]"
Java
cd calculator-server
./mvnw clean install -DskipTests
Rust
cargo add rmcp --features server,transport-io
cargo add serde
cargo add tokio --features rt-multi-thread
-3- 프로젝트 파일 생성하기
TypeScript
*package.json* 파일을 열어 다음 내용으로 교체해 서버 빌드 및 실행이 가능하게 합니다:
{
"name": "calculator-server",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "npm run build && node ./build/index.js",
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "A simple calculator server using Model Context Protocol",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.14",
"typescript": "^5.8.3"
}
}
*tsconfig.json* 파일을 다음 내용으로 생성하세요:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
소스 코드용 디렉터리를 생성합니다:
mkdir src
touch src/index.ts
Python
*server.py* 파일을 생성하세요
touch server.py
.NET
필요한 NuGet 패키지를 설치하세요:
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
Java
Java Spring Boot 프로젝트는 프로젝트 구조가 자동으로 생성됩니다.
Rust
Rust는 cargo init 실행 시 기본적으로 *src/main.rs* 파일이 생성됩니다. 해당 파일을 열고 기본 코드를 삭제하세요.
-4- 서버 코드 작성하기
TypeScript
*index.ts* 파일을 생성하고 다음 코드를 추가하세요:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// MCP 서버 생성
const server = new McpServer({
name: "Calculator MCP Server",
version: "1.0.0"
});
서버가 생성되었으나 할 일이 많지 않습니다. 고쳐 봅시다.
Python
# server.py
from mcp.server.fastmcp import FastMCP
# MCP 서버 생성
mcp = FastMCP("Demo")
.NET
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();
// add features
Java
Java는 핵심 서버 구성 요소를 생성합니다. 먼저 메인 애플리케이션 클래스를 수정하세요:
*src/main/java/com/microsoft/mcp/sample/server/McpServerApplication.java*:
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
계산기 서비스 생성 *src/main/java/com/microsoft/mcp/sample/server/service/CalculatorService.java*:
package com.microsoft.mcp.sample.server.service;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
/**
* Service for basic calculator operations.
* This service provides simple calculator functionality through MCP.
*/
@Service
public class CalculatorService {
/**
* Add two numbers
* @param a The first number
* @param b The second number
* @return The sum of the two numbers
*/
@Tool(description = "Add two numbers together")
public String add(double a, double b) {
double result = a + b;
return formatResult(a, "+", b, result);
}
/**
* Subtract one number from another
* @param a The number to subtract from
* @param b The number to subtract
* @return The result of the subtraction
*/
@Tool(description = "Subtract the second number from the first number")
public String subtract(double a, double b) {
double result = a - b;
return formatResult(a, "-", b, result);
}
/**
* Multiply two numbers
* @param a The first number
* @param b The second number
* @return The product of the two numbers
*/
@Tool(description = "Multiply two numbers together")
public String multiply(double a, double b) {
double result = a * b;
return formatResult(a, "*", b, result);
}
/**
* Divide one number by another
* @param a The numerator
* @param b The denominator
* @return The result of the division
*/
@Tool(description = "Divide the first number by the second number")
public String divide(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a / b;
return formatResult(a, "/", b, result);
}
/**
* Calculate the power of a number
* @param base The base number
* @param exponent The exponent
* @return The result of raising the base to the exponent
*/
@Tool(description = "Calculate the power of a number (base raised to an exponent)")
public String power(double base, double exponent) {
double result = Math.pow(base, exponent);
return formatResult(base, "^", exponent, result);
}
/**
* Calculate the square root of a number
* @param number The number to find the square root of
* @return The square root of the number
*/
@Tool(description = "Calculate the square root of a number")
public String squareRoot(double number) {
if (number < 0) {
return "Error: Cannot calculate square root of a negative number";
}
double result = Math.sqrt(number);
return String.format("√%.2f = %.2f", number, result);
}
/**
* Calculate the modulus (remainder) of division
* @param a The dividend
* @param b The divisor
* @return The remainder of the division
*/
@Tool(description = "Calculate the remainder when one number is divided by another")
public String modulus(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a % b;
return formatResult(a, "%", b, result);
}
/**
* Calculate the absolute value of a number
* @param number The number to find the absolute value of
* @return The absolute value of the number
*/
@Tool(description = "Calculate the absolute value of a number")
public String absolute(double number) {
double result = Math.abs(number);
return String.format("|%.2f| = %.2f", number, result);
}
/**
* Get help about available calculator operations
* @return Information about available operations
*/
@Tool(description = "Get help about available calculator operations")
public String help() {
return "Basic Calculator MCP Service\n\n" +
"Available operations:\n" +
"1. add(a, b) - Adds two numbers\n" +
"2. subtract(a, b) - Subtracts the second number from the first\n" +
"3. multiply(a, b) - Multiplies two numbers\n" +
"4. divide(a, b) - Divides the first number by the second\n" +
"5. power(base, exponent) - Raises a number to a power\n" +
"6. squareRoot(number) - Calculates the square root\n" +
"7. modulus(a, b) - Calculates the remainder of division\n" +
"8. absolute(number) - Calculates the absolute value\n\n" +
"Example usage: add(5, 3) will return 5 + 3 = 8";
}
/**
* Format the result of a calculation
*/
private String formatResult(double a, String operator, double b, double result) {
return String.format("%.2f %s %.2f = %.2f", a, operator, b, result);
}
}
프로덕션 준비 서비스를 위한 선택적 구성 요소:
시작 구성 생성 *src/main/java/com/microsoft/mcp/sample/server/config/StartupConfig.java*:
package com.microsoft.mcp.sample.server.config;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StartupConfig {
@Bean
public CommandLineRunner startupInfo() {
return args -> {
System.out.println("\n" + "=".repeat(60));
System.out.println("Calculator MCP Server is starting...");
System.out.println("SSE endpoint: http://localhost:8080/sse");
System.out.println("Health check: http://localhost:8080/actuator/health");
System.out.println("=".repeat(60) + "\n");
};
}
}
헬스 컨트롤러 생성 *src/main/java/com/microsoft/mcp/sample/server/controller/HealthController.java*:
package com.microsoft.mcp.sample.server.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> healthCheck() {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("timestamp", LocalDateTime.now().toString());
response.put("service", "Calculator MCP Server");
return ResponseEntity.ok(response);
}
}
예외 핸들러 생성 *src/main/java/com/microsoft/mcp/sample/server/exception/GlobalExceptionHandler.java*:
package com.microsoft.mcp.sample.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
"Invalid_Input",
"Invalid input parameter: " + ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
public static class ErrorResponse {
private String code;
private String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
// 게터
public String getCode() { return code; }
public String getMessage() { return message; }
}
}
커스텀 배너 생성 *src/main/resources/banner.txt*:
_____ _ _ _
/ ____| | | | | | |
| | __ _| | ___ _ _| | __ _| |_ ___ _ __
| | / _` | |/ __| | | | |/ _` | __/ _ \| '__|
| |___| (_| | | (__| |_| | | (_| | || (_) | |
\_____\__,_|_|\___|\__,_|_|\__,_|\__\___/|_|
Calculator MCP Server v1.0
Spring Boot MCP Application
Rust
*src/main.rs* 파일 상단에 다음 코드를 추가하세요. 이는 MCP 서버에 필요한 라이브러리와 모듈을 가져옵니다.
use rmcp::{
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
ServerHandler, ServiceExt,
};
use std::error::Error;
계산기 서버는 두 숫자를 더하는 간단한 서버가 될 것입니다. 계산기 요청을 나타내는 struct를 만들어 봅시다.
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
다음으로 계산기 서버를 나타내는 struct를 만듭니다. 이 struct는 도구 라우터를 보유하며 도구 등록에 사용됩니다.
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
이제 Calculator struct를 구현하여 서버 새 인스턴스를 생성하고 서버 정보를 제공하는 핸들러를 구현합니다.
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
마지막으로 서버를 시작하는 main 함수를 구현해야 합니다. 이 함수는 Calculator struct 인스턴스를 만들고 표준 입출력으로 서버를 운영합니다.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
서버는 이제 자체에 관한 기본 정보를 제공합니다. 다음으로 덧셈을 수행하는 도구를 추가합니다.
-5- 도구 및 리소스 추가
다음 코드로 도구와 리소스를 추가하세요:
TypeScript
server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
도구는 a 및 b 매개변수를 받고, 다음 형식의 응답을 생성합니다:
{
contents: [{
type: "text", content: "some content"
}]
}
리소스는 문자열 "greeting"으로 접근하며, 이름(name) 매개변수를 받아 도구와 유사한 응답을 생성합니다:
{
uri: "<href>",
text: "a text"
}
Python
# 덧셈 도구 추가
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# 동적 인사말 리소스 추가
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
위 코드에서 우리는:
a와 b라는 정수 매개변수를 받는 add 도구를 정의했습니다.name 매개변수를 받는 greeting 리소스를 만들었습니다..NET
Program.cs 파일에 다음을 추가하세요:
[McpServerToolType]
public static class CalculatorTool
{
[McpServerTool, Description("Adds two numbers")]
public static string Add(int a, int b) => $"Sum {a + b}";
}
Java
도구는 이전 단계에서 이미 생성했습니다.
Rust
impl Calculator 블록 내에 새 도구를 추가하세요:
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
-6- 최종 코드
서버가 시작할 수 있도록 마지막 코드를 추가합시다:
TypeScript
// stdin에서 메시지 수신을 시작하고 stdout에서 메시지 전송을 시작합니다
const transport = new StdioServerTransport();
await server.connect(transport);
전체 코드는 다음과 같습니다:
// index.ts
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// MCP 서버 생성
const server = new McpServer({
name: "Calculator MCP Server",
version: "1.0.0"
});
// 추가 도구 추가
server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// 동적 인사말 리소스 추가
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
// stdin에서 메시지 수신 시작 및 stdout으로 메시지 전송 시작
const transport = new StdioServerTransport();
server.connect(transport);
Python
# server.py
from mcp.server.fastmcp import FastMCP
# MCP 서버 생성
mcp = FastMCP("Demo")
# 추가 도구 추가
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# 동적 인사말 리소스 추가
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
# 메인 실행 블록 - 서버를 실행하려면 필요합니다
if __name__ == "__main__":
mcp.run()
.NET
다음 내용을 가진 Program.cs 파일을 생성하세요:
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}";
}
Java
완성된 메인 애플리케이션 클래스는 다음과 같아야 합니다:
// McpServerApplication.java
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
Rust
Rust 서버의 최종 코드는 다음과 같습니다:
use rmcp::{
ServerHandler, ServiceExt,
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
};
use std::error::Error;
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
-7- 서버 테스트
다음 명령어로 서버를 시작하세요:
TypeScript
npm run build
Python
mcp run server.py
> MCP Inspector를 사용하려면 mcp dev server.py를 사용하세요.
이는 Inspector를 자동으로 실행하고 필요한 프록시 세션 토큰을 제공합니다. mcp run server.py를 사용할 경우 Inspector를 수동으로 시작하고 연결을 구성해야 합니다.
.NET
프로젝트 디렉터리 안에 있는지 확인하세요:
cd McpCalculatorServer
dotnet run
Java
./mvnw clean install -DskipTests
java -jar target/calculator-server-0.0.1-SNAPSHOT.jar
Rust
서버를 형식화하고 실행하려면 다음 명령어를 실행하세요:
cargo fmt
cargo run
-8- Inspector를 사용해 실행하기
Inspector는 서버를 시작하고 상호작용할 수 있도록 도와주는 훌륭한 도구입니다. 시작해 봅시다:
> [!NOTE]
> "command" 필드의 내용은 특정 런타임으로 서버를 실행하는 명령어를 포함하므로 다르게 보일 수 있습니다.
TypeScript
npx @modelcontextprotocol/inspector node build/index.js
또는 package.json에 "inspector": "npx @modelcontextprotocol/inspector node build/index.js"를 추가하고 npm run inspector를 실행하세요.
Python
Python은 Node.js 도구인 inspector를 래핑합니다. 다음과 같이 해당 도구를 호출할 수 있습니다:
mcp dev server.py
하지만 전체 명령어를 구현하지 않으므로 Node.js 도구를 직접 실행하는 것이 권장됩니다:
npx @modelcontextprotocol/inspector mcp run server.py
스크립트 실행을 위한 명령과 인자를 구성할 수 있는 도구나 IDE를 사용하는 경우,
Command 필드에 python을 설정하고 Arguments에 server.py를 설정해야 합니다.
이렇게 해야 스크립트가 올바르게 실행됩니다.
.NET
프로젝트 디렉터리에 있는지 확인하세요:
cd McpCalculatorServer
npx @modelcontextprotocol/inspector dotnet run
Java
계산기 서버가 실행 중인지 확인하세요
그런 다음 인스펙터를 실행합니다:
npx @modelcontextprotocol/inspector
인스펙터 웹 인터페이스에서:
1. 전송 유형으로 "SSE"를 선택하세요
2. URL을 http://localhost:8080/sse로 설정하세요
3. "Connect"를 클릭하세요
이제 서버에 연결되었습니다
Java 서버 테스트 섹션이 완료되었습니다
다음 섹션은 서버와 상호작용하는 방법에 관한 내용입니다.
다음과 같은 사용자 인터페이스가 보일 것입니다:
1. "Connect" 버튼을 선택하여 서버에 연결하세요
서버에 연결되면 다음 화면이 보입니다:
1. "Tools"에서 "listTools"를 선택하세요. "Add"가 표시되면 "Add"를 선택하고 매개변수 값을 입력하세요.
다음과 같은 응답, 즉 "add" 도구의 결과가 표시됩니다:
축하합니다, 첫 번째 서버를 성공적으로 만들고 실행했습니다!
Rust
MCP 인스펙터 CLI로 Rust 서버를 실행하려면 다음 명령어를 사용하세요:
npx @modelcontextprotocol/inspector cargo run --cli --method tools/call --tool-name add --tool-arg a=1 b=2
공식 SDK
MCP는 여러 언어에 대한 공식 SDK를 제공합니다:
주요 요점
샘플
과제
선택한 도구를 사용하여 간단한 MCP 서버를 만드세요:
1. 선호하는 언어(.NET, Java, Python, TypeScript, Rust)로 도구를 구현하세요.
2. 입력 매개변수와 반환 값을 정의하세요.
3. 인스펙터 도구를 실행하여 서버가 제대로 작동하는지 확인하세요.
4. 다양한 입력으로 구현을 테스트하세요.
솔루션
추가 리소스
다음 단계
다음: MCP 클라이언트 시작하기
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나 자동 번역에는 오류나 부정확성이 포함될 수 있음을 유의해 주시기 바랍니다.
원본 문서가 원어로 된 공식 자료임을 참고하시기 바랍니다.
중요한 정보에 대해서는 전문 인간 번역가의 번역을 권장합니다.
본 번역 사용으로 인해 발생하는 모든 오해나 오역에 대해 당사는 책임을 지지 않습니다.
클라이언트 생성하기
클라이언트는 MCP 서버와 직접 통신하여 리소스, 도구 및 프롬프트를 요청하는 맞춤형 애플리케이션 또는 스크립트입니다. 서버와 상호작용하기 위한 그래픽 인터페이스를 제공하는 검사 도구와 달리, 자신만의 클라이언트를 작성하면 프로그래밍 방식으로 자동화된 상호작용이 가능합니다. 이를 통해 개발자는 MCP 기능을 자신의 워크플로우에 통합하고 작업을 자동화하며 특정 요구 사항에 맞춘 맞춤 솔루션을 구축할 수 있습니다.
개요
이 수업에서는 Model Context Protocol(MCP) 생태계 내 클라이언트의 개념을 소개합니다. 직접 클라이언트를 작성하고 MCP 서버에 연결하는 방법을 배우게 됩니다.
학습 목표
이 수업이 끝나면 다음을 할 수 있습니다:
클라이언트를 작성하려면 무엇이 필요한가?
클라이언트를 작성하려면 다음 작업을 수행해야 합니다:
이제 하이레벨로 준비할 내용을 이해했으니, 다음으로 예제를 살펴봅시다.
클라이언트 예제
이 클라이언트 예제를 보겠습니다:
TypeScript
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);
// 프롬프트 목록
const prompts = await client.listPrompts();
// 프롬프트 가져오기
const prompt = await client.getPrompt({
name: "example-prompt",
arguments: {
arg1: "value"
}
});
// 리소스 목록
const resources = await client.listResources();
// 리소스 읽기
const resource = await client.readResource({
uri: "file:///example.txt"
});
// 도구 호출
const result = await client.callTool({
name: "example-tool",
arguments: {
arg1: "value"
}
});
위 코드에서:
이렇게 MCP 서버와 통신할 수 있는 클라이언트가 완성되었습니다.
다음 연습 섹션에서 각 코드 조각을 자세히 분석하고 내용을 설명하겠습니다.
연습: 클라이언트 작성하기
위에서 말했듯이, 코드를 천천히 살펴보고 원하면 직접 따라 코딩해보세요.
-1- 라이브러리 임포트
필요한 라이브러리를 임포트합니다. 클라이언트와 선택한 전송 프로토콜 stdio에 대한 참조가 필요합니다. stdio는 로컬 머신에서 실행되는 애플리케이션용 프로토콜입니다. SSE는 이후 장에서 소개할 또 다른 전송 방식입니다. 지금은 stdio를 계속 사용합시다.
TypeScript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
Python
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
.NET
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
Java
Java에서는 이전 실습의 MCP 서버에 연결하는 클라이언트를 생성합니다. Getting Started with MCP Server의 Java Spring Boot 프로젝트 구조를 그대로 사용하며, src/main/java/com/microsoft/mcp/sample/client/ 폴더에 SDKClient라는 새 Java 클래스를 생성하고 다음 임포트를 추가합니다:
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;
Rust
Cargo.toml 파일에 다음 의존성을 추가해야 합니다.
[package]
name = "calculator-client"
version = "0.1.0"
edition = "2024"
[dependencies]
rmcp = { version = "0.5.0", features = ["client", "transport-child-process"] }
serde_json = "1.0.141"
tokio = { version = "1.46.1", features = ["rt-multi-thread"] }
그 후 클라이언트 코드에서 필요한 라이브러리를 임포트할 수 있습니다.
use rmcp::{
RmcpError,
model::CallToolRequestParam,
service::ServiceExt,
transport::{ConfigureCommandExt, TokioChildProcess},
};
use tokio::process::Command;
다음으로 인스턴스화를 해봅시다.
-2- 클라이언트 및 전송 인스턴스화
전송과 클라이언트 인스턴스를 생성해야 합니다:
TypeScript
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"]
});
const client = new Client(
{
name: "example-client",
version: "1.0.0"
}
);
await client.connect(transport);
위 코드에서는:
```typescript
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"]
});
```
```typescript
const client = new Client(
{
name: "example-client",
version: "1.0.0"
});
```
```typescript
await client.connect(transport);
```
Python
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
# stdio 연결을 위한 서버 매개변수 생성
server_params = StdioServerParameters(
command="mcp", # 실행 파일
args=["run", "server.py"], # 선택적 명령줄 인수
env=None, # 선택적 환경 변수
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write
) as session:
# 연결 초기화
await session.initialize()
if __name__ == "__main__":
import asyncio
asyncio.run(run())
위 코드에서는:
stdio_client를 호출하는 run 비동기 메서드를 정의했습니다.asyncio.run에 run 메서드를 전달하여 진입점을 만들었습니다..NET
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
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 McpClient.CreateAsync(clientTransport);
위 코드에서는:
mcpClient라는 클라이언트를 만들었습니다. 이를 통해 MCP 서버의 기능을 나열 및 호출할 수 있습니다.참고로, Arguments에는 *.csproj* 파일이나 실행 파일 경로를 지정할 수 있습니다.
Java
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();
// 클라이언트 로직은 여기에 작성하세요
}
}
위 코드에서는:
http://localhost:8080를 가리키는 SSE 전송을 설정하는 main 메서드를 만들었습니다.run 메서드에서 전송을 사용해 동기 MCP 클라이언트를 만들고 연결을 초기화했습니다.Rust
이 Rust 클라이언트는 서버가 같은 디렉터리 내 "calculator-server"라는 형제 프로젝트라고 가정합니다. 아래 코드는 서버를 시작하고 연결합니다.
async fn main() -> Result<(), RmcpError> {
// 서버가 같은 디렉토리에 있는 형제 프로젝트인 "calculator-server"라고 가정합니다
let server_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("failed to locate workspace root")
.join("calculator-server");
let client = ()
.serve(
TokioChildProcess::new(Command::new("cargo").configure(|cmd| {
cmd.arg("run").current_dir(server_dir);
}))
.map_err(RmcpError::transport_creation::<TokioChildProcess>)?,
)
.await?;
// 할 일: 초기화
// 할 일: 도구 목록 작성
// 할 일: 인수 = {"a": 3, "b": 2}로 add 도구 호출
client.cancel().await?;
Ok(())
}
-3- 서버 기능 나열하기
프로그램이 실행되면 클라이언트가 서버에 연결할 수 있습니다. 하지만 서버의 기능을 나열하지는 않으므로, 이를 해봅시다:
TypeScript
// 프롬프트 목록
const prompts = await client.listPrompts();
// 리소스 목록
const resources = await client.listResources();
// 도구 목록
const tools = await client.listTools();
Python
# 사용 가능한 리소스 나열
resources = await session.list_resources()
print("LISTING RESOURCES")
for resource in resources:
print("Resource: ", resource)
# 사용 가능한 도구 나열
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)
여기서는 사용 가능한 리소스 list_resources()와 도구 list_tools를 나열하고 출력합니다.
.NET
foreach (var tool in await client.ListToolsAsync())
{
Console.WriteLine($"{tool.Name} ({tool.Description})");
}
위 코드는 서버의 도구를 나열하는 예제입니다. 도구 각각의 이름을 출력합니다.
Java
// 도구 나열 및 시연
ListToolsResult toolsList = client.listTools();
System.out.println("Available Tools = " + toolsList);
// 연결 확인을 위해 서버에 핑을 보낼 수도 있습니다
client.ping();
위 코드에서는:
listTools()를 호출해 MCP 서버의 모든 도구를 가져왔습니다.ping()으로 서버 연결 상태 확인을 했습니다.ListToolsResult에는 도구 이름, 설명, 입력 스키마 등의 정보가 포함되어 있습니다.좋습니다. 이제 모든 기능을 가져왔습니다. 그럼 언제 이 기능들을 사용하는 걸까요? 이 클라이언트는 간단해서, 기능을 사용하려면 명시적으로 호출해야 합니다. 다음 장에서는 자체 대형 언어 모델(LLM)에 접근할 수 있는 더 진보된 클라이언트를 만들 것입니다. 지금은 서버 기능을 호출하는 법을 살펴봅시다.
Rust
main 함수에서 클라이언트를 초기화한 후 서버를 초기화하고 몇 가지 기능을 나열할 수 있습니다.
// 초기화
let server_info = client.peer_info();
println!("Server info: {:?}", server_info);
// 도구 목록
let tools = client.list_tools(Default::default()).await?;
println!("Available tools: {:?}", tools);
-4- 기능 호출하기
기능을 호출하려면 올바른 인자와, 경우에 따라 호출하려는 이름을 지정해야 합니다.
TypeScript
// 리소스를 읽습니다
const resource = await client.readResource({
uri: "file:///example.txt"
});
// 도구를 호출합니다
const result = await client.callTool({
name: "example-tool",
arguments: {
arg1: "value"
}
});
// 프롬프트 호출
const promptResult = await client.getPrompt({
name: "review-code",
arguments: {
code: "console.log(\"Hello world\")"
}
})
위 코드에서는:
readResource()를 호출하고 uri를 지정합니다. 서버 측 코드는 다음과 유사합니다:```typescript
server.resource(
"readFile",
new ResourceTemplate("file://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: Hello, ${name}!
}]
})
);
```
uri값 file://example.txt는 서버의 file://{name}에 매핑되며, example.txt는 name으로 처리됩니다.
name)과 인자(arguments)를 지정하여 호출합니다:```typescript
const result = await client.callTool({
name: "example-tool",
arguments: {
arg1: "value"
}
});
```
getPrompt()를 이름과 인자와 함께 호출합니다. 서버 코드는 다음과 같습니다:```typescript
server.prompt(
"review-code",
{ code: z.string() },
({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: Please review this code:\n\n${code}
}
}]
})
);
```
따라서 클라이언트 코드는 서버 선언과 일치하도록 다음과 같습니다:
```typescript
const promptResult = await client.getPrompt({
name: "review-code",
arguments: {
code: "console.log(\"Hello world\")"
}
})
```
Python
# 리소스를 읽습니다
print("READING RESOURCE")
content, mime_type = await session.read_resource("greeting://hello")
# 도구를 호출합니다
print("CALL TOOL")
result = await session.call_tool("add", arguments={"a": 1, "b": 7})
print(result.content)
위 코드에서:
greeting 리소스를 read_resource로 호출했습니다.add 도구를 call_tool로 호출했습니다..NET
1. 도구를 호출하는 코드를 추가합시다:
```csharp
var result = await mcpClient.CallToolAsync(
"Add",
new Dictionary
cancellationToken:CancellationToken.None);
```
2. 결과를 출력하는 코드는 다음과 같습니다:
```csharp
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
// Sum 4
```
Java
// 다양한 계산기 도구 호출
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);
위 코드에서는:
callTool() 메서드와 CallToolRequest 객체로 호출했습니다.Map으로 전달했습니다.CallToolResult 객체로 반환됩니다.Rust
// 인수 = {"a": 3, "b": 2}로 add 도구를 호출하십시오
let a = 3;
let b = 2;
let tool_result = client
.call_tool(CallToolRequestParam {
name: "add".into(),
arguments: serde_json::json!({ "a": a, "b": b }).as_object().cloned(),
})
.await?;
println!("Result of {:?} + {:?}: {:?}", a, b, tool_result);
-5- 클라이언트 실행하기
클라이언트를 실행하려면 터미널에서 다음 명령어를 입력하세요:
TypeScript
*package.json*의 "scripts" 섹션에 다음 항목을 추가합니다:
"client": "tsc && node build/client.js"
npm run client
Python
다음 명령어로 클라이언트를 호출합니다:
python client.py
.NET
dotnet run
Java
먼저 MCP 서버가 http://localhost:8080에서 실행 중인지 확인하세요. 그러고 나서 클라이언트를 실행합니다:
# 프로젝트를 빌드하세요
./mvnw clean compile
# 클라이언트를 실행하세요
./mvnw exec:java -Dexec.mainClass="com.microsoft.mcp.sample.client.SDKClient"
또는 솔루션 폴더 03-GettingStarted\02-client\solution\java에 제공된 완성된 클라이언트 프로젝트를 실행할 수 있습니다:
# 솔루션 디렉토리로 이동
cd 03-GettingStarted/02-client/solution/java
# JAR을 빌드하고 실행
./mvnw clean package
java -jar target/calculator-client-0.0.1-SNAPSHOT.jar
Rust
cargo fmt
cargo run
과제
이번 과제에서는 배운 내용을 활용해 직접 클라이언트를 만들어 봅니다.
아래 서버를 클라이언트 코드로 호출할 수 있습니다. 서버에 더 흥미로운 기능을 추가해 보세요.
TypeScript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// MCP 서버를 생성합니다
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// 덧셈 도구를 추가합니다
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// 동적 인사말 리소스를 추가합니다
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
// stdin에서 메시지를 수신하고 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);
});
Python
# server.py
from mcp.server.fastmcp import FastMCP
# MCP 서버 생성
mcp = FastMCP("Demo")
# 추가 도구 추가
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# 동적 인사말 리소스 추가
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
.NET
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}";
}
이 프로젝트에서 프롬프트와 리소스 추가 방법을 참조하세요.
또한, 프롬프트와 리소스 호출 방법도 확인하세요.
Rust
이전 섹션(../01-first-server)에서 Rust로 간단한 MCP 서버를 만드는 방법을 배웠습니다.
그 코드를 확장하거나 다음 링크에서 더 많은 Rust 기반 MCP 서버 예제를 확인하세요: MCP Server Examples
솔루션
솔루션 폴더에는 본 튜토리얼에서 다룬 개념들을 모두 포함한, 실행 가능한 완전한 클라이언트 구현 예제가 들어 있습니다. 각 솔루션은 클라이언트와 서버 코드를 별도의 독립형 프로젝트로 정리했습니다.
📁 솔루션 구조
솔루션 디렉터리는 프로그래밍 언어별로 구성됩니다:
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
├── rust/ # Rust client implementation
| ├── Cargo.lock # Cargo lock file
| ├── Cargo.toml # Project configuration and dependencies
| ├── src # Source code
| │ └── main.rs # Main client code
└── server/ # Additional .NET server implementation
├── Program.cs # Server code
└── server.csproj # Server project file
🚀 각 솔루션에 포함된 내용
각 언어별 솔루션에는 다음이 포함되어 있습니다:
📖 솔루션 사용법
1. 선호하는 언어 폴더로 이동합니다:
```bash
cd solution/typescript/ # TypeScript 용
cd solution/java/ # Java 용
cd solution/python/ # Python 용
cd solution/dotnet/ # .NET 용
```
2. 각 폴더 내 README 지침에 따라:
- 의존성 설치
- 프로젝트 빌드
- 클라이언트 실행
3. 예상 출력 예시:
```text
Prompt: Please review this code: console.log("hello");
Resource template: file
Tool result: { content: [ { type: 'text', text: '9' } ] }
```
자세한 문서와 단계별 안내는 다음을 참조하세요: 📖 솔루션 문서 각 런타임에 대한 솔루션은 다음과 같습니다: 다음과 비슷한 결과가 보여야 합니다: 면책 조항: 이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다. 원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다. 중요한 정보의 경우 전문적인 인간 번역을 권장합니다. 본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다. 다음과 비슷한 출력이 나타날 것입니다: 면책 조항: 이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다. 원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다. 중요한 정보의 경우 전문적인 인간 번역을 권장합니다. 본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다. 이 프로젝트는 MCP(Model Context Protocol) 서버에 연결하고 상호작용하는 Java 클라이언트를 만드는 방법을 보여줍니다. 이 예제에서는 1장에 나온 계산기 서버에 연결하여 다양한 수학 연산을 수행합니다. 이 클라이언트를 실행하기 전에 다음을 준비해야 합니다: 1. 1장의 계산기 서버를 시작합니다: - 계산기 서버 디렉터리로 이동: - 계산기 서버를 빌드하고 실행합니다: ```cmd cd ..\01-first-server\solution\java .\mvnw clean install -DskipTests java -jar target\calculator-server-0.0.1-SNAPSHOT.jar ``` - 서버는 2. 시스템에 Java 21 이상이 설치되어 있어야 합니다 3. Maven (Maven Wrapper를 통해 포함됨) 클라이언트는 Spring AI MCP 프레임워크를 사용하여: 1. 연결 설정: WebFlux SSE 클라이언트 전송을 생성해 계산기 서버에 연결 2. 클라이언트 초기화: MCP 클라이언트를 설정하고 연결을 확립 3. 도구 검색: 사용 가능한 모든 계산기 연산 목록 조회 4. 연산 실행: 샘플 데이터를 사용해 다양한 수학 함수 호출 5. 결과 표시: 각 계산 결과를 화면에 출력 프로젝트는 다음 주요 의존성을 사용합니다: 이 의존성은 다음을 제공합니다: Maven Wrapper를 사용해 프로젝트를 빌드합니다: 참고: 명령어를 실행하기 전에 계산기 서버가 클라이언트를 실행하면 다음 작업을 수행합니다: 1. 2. 도구 목록 조회 - 사용 가능한 모든 계산기 연산 표시 3. 계산 수행: - 덧셈: 5 + 3 = 8 - 뺄셈: 10 - 4 = 6 - 곱셈: 6 × 7 = 42 - 나눗셈: 20 ÷ 4 = 5 - 거듭제곱: 2^8 = 256 - 제곱근: √16 = 4 - 절댓값: |-5.5| = 5.5 - 도움말: 사용 가능한 연산 표시 참고: 실행 종료 시 Maven에서 남아있는 스레드에 대한 경고가 나타날 수 있는데, 이는 리액티브 애플리케이션에서 정상적인 현상이며 오류가 아닙니다. 계산기 서버에 연결하는 SSE(Server-Sent Events) 전송을 생성합니다. 동기식 MCP 클라이언트를 생성하고 연결을 초기화합니다. 매개변수 a=5.0, b=3.0으로 "add" 도구를 호출합니다. 연결 오류가 발생하면 1장의 계산기 서버가 실행 중인지 확인하세요: 해결 방법: 먼저 계산기 서버를 시작하세요. 포트 8080이 사용 중이라면: 해결 방법: 포트 8080을 사용하는 다른 애플리케이션을 종료하거나 서버가 다른 포트를 사용하도록 변경하세요. 빌드 오류가 발생하면: 면책 조항: 이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다. 원문은 해당 언어의 원본 문서가 권위 있는 자료로 간주되어야 합니다. 중요한 정보의 경우 전문적인 인간 번역을 권장합니다. 본 번역의 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다. 면책 조항: 이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 위해 최선을 다하고 있지만, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다. 원본 문서의 원어 버전을 권위 있는 출처로 간주해야 합니다. 중요한 정보의 경우, 전문적인 인간 번역을 권장합니다. 이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 책임을 지지 않습니다.이 샘플 실행하기
uv 설치를 권장하지만 필수는 아닙니다. 자세한 내용은 instructions를 참고하세요.-1- 의존성 설치하기
npm install
-3- 서버 실행하기
npm run build
-4- 클라이언트 실행하기
npm run client
Prompt: {
type: 'text',
text: 'Please review this code:\n\nconsole.log("hello");'
}
Resource template: file
Tool result: { content: [ { type: 'text', text: '9' } ] }
이 샘플 실행하기
uv 설치를 권장하지만 필수는 아닙니다. 자세한 내용은 instructions를 참고하세요.-0- 가상 환경 만들기
python -m venv venv
-1- 가상 환경 활성화하기
venv\Scrips\activate
-2- 의존성 설치하기
pip install "mcp[cli]"
-3- 샘플 실행하기
python client.py
LISTING RESOURCES
Resource: ('meta', None)
Resource: ('nextCursor', None)
Resource: ('resources', [])
INFO Processing request of type ListToolsRequest server.py:534
LISTING TOOLS
Tool: add
READING RESOURCE
INFO Processing request of type ReadResourceRequest server.py:534
CALL TOOL
INFO Processing request of type CallToolRequest server.py:534
[TextContent(type='text', text='8', annotations=None)]
MCP Java Client - 계산기 데모
사전 준비 사항
03-GettingStarted/01-first-server/solution/java/http://localhost:8080에서 실행 중이어야 합니다SDKClient란?
SDKClient는 다음을 보여주는 Java 애플리케이션입니다:작동 방식
프로젝트 구조
src/
└── main/
└── java/
└── com/
└── microsoft/
└── mcp/
└── sample/
└── client/
└── SDKClient.java # Main client implementation
주요 의존성
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
McpClient - 주요 클라이언트 인터페이스WebFluxSseClientTransport - 웹 기반 통신을 위한 SSE 전송프로젝트 빌드
.\mvnw clean install
클라이언트 실행
java -jar .\target\calculator-client-0.0.1-SNAPSHOT.jar
http://localhost:8080에서 실행 중인지 확인하세요.클라이언트 동작 내용
http://localhost:8080의 계산기 서버에 연결예상 출력
Available Tools = ListToolsResult[tools=[Tool[name=add, description=Add two numbers together, ...], ...]]
Add Result = CallToolResult[content=[TextContent[text="5,00 + 3,00 = 8,00"]], isError=false]
Subtract Result = CallToolResult[content=[TextContent[text="10,00 - 4,00 = 6,00"]], isError=false]
Multiply Result = CallToolResult[content=[TextContent[text="6,00 * 7,00 = 42,00"]], isError=false]
Divide Result = CallToolResult[content=[TextContent[text="20,00 / 4,00 = 5,00"]], isError=false]
Power Result = CallToolResult[content=[TextContent[text="2,00 ^ 8,00 = 256,00"]], isError=false]
Square Root Result = CallToolResult[content=[TextContent[text="√16,00 = 4,00"]], isError=false]
Absolute Result = CallToolResult[content=[TextContent[text="|-5,50| = 5,50"]], isError=false]
Help = CallToolResult[content=[TextContent[text="Basic Calculator MCP Service\n\nAvailable operations:\n1. add(a, b) - Adds two numbers\n2. subtract(a, b) - Subtracts the second number from the first\n..."]], isError=false]
코드 이해하기
1. 전송 설정
var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
2. 클라이언트 생성
var client = McpClient.sync(this.transport).build();
client.initialize();
3. 도구 호출
CallToolResult resultAdd = client.callTool(new CallToolRequest("add", Map.of("a", 5.0, "b", 3.0)));
문제 해결
서버가 실행 중이지 않을 때
Error: Connection refused
포트가 이미 사용 중일 때
Error: Address already in use
빌드 오류 발생 시
.\mvnw clean install -DskipTests
더 알아보기
🎯 완성된 예제
튜토리얼에서 다룬 모든 프로그래밍 언어용 완성된 클라이언트 구현 예제를 제공했습니다. 이 예제들은 위에서 설명한 전체 기능을 구현하며 참고 또는 자신만의 프로젝트 시작점으로 사용할 수 있습니다.
사용 가능 완성 예제
client_example_java.javaclient_example_csharp.csclient_example_typescript.tsclient_example_python.pyclient_example_rust.rs각 완성 예제에는:
완전한 예제 시작하기
1. 위 표에서 선호하는 언어 선택
2. 전체 구현을 이해하기 위해 완전한 예제 파일 검토
3. complete_examples.md 에서 지침에 따라 예제 실행
4. 특정 사용 사례에 맞게 예제 수정 및 확장
자세한 실행 및 사용자 정의 문서는 다음을 참조하세요: 📖 완전한 예제 문서
💡 솔루션 vs. 완전한 예제
두 접근법 모두 유용합니다 - 전체 프로젝트는 솔루션 폴더를, 학습과 참조는 완전한 예제를 사용하세요.
주요 내용 요약
이번 장에서 클라이언트에 대한 주요 내용은 다음과 같습니다:
추가 자료
샘플
다음 내용
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확한 내용이 포함될 수 있음을 유의하시기 바랍니다.
원문 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.
본 번역 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
LLM을 사용하여 클라이언트 만들기
지금까지 서버와 클라이언트를 만드는 방법을 보았습니다.
클라이언트는 서버를 명시적으로 호출하여 도구, 리소스, 프롬프트를 나열할 수 있었습니다.
하지만 이는 그리 실용적인 방법이 아닙니다.
사용자는 에이전트 시대에 살고 있으며 프롬프트를 사용하고 LLM과 소통하기를 기대합니다.
사용자가 MCP를 사용하여 기능을 저장하는지 여부는 신경 쓰지 않고 단순히 자연어를 사용하여 상호작용하기를 기대합니다.
그렇다면 이것을 어떻게 해결할 수 있을까요?
해결책은 클라이언트에 LLM을 추가하는 것입니다.
개요
이번 수업에서는 클라이언트에 LLM을 추가하는 데 중점을 두며, 이를 통해 사용자에게 훨씬 더 나은 경험을 제공하는 방법을 보여줍니다.
학습 목표
이 수업이 끝나면 다음을 할 수 있습니다:
접근 방법
우리가 취해야 할 접근 방식을 이해해 봅시다. LLM을 추가하는 것은 간단해 보이지만 실제로 이렇게 할 수 있을까요?
클라이언트가 서버와 상호작용하는 방식은 다음과 같습니다:
1. 서버와 연결을 설정합니다.
1. 기능, 프롬프트, 리소스 및 도구를 나열하고 해당 스키마를 저장합니다.
1. LLM을 추가하고, 저장된 기능과 그들의 스키마를 LLM이 이해하는 형식으로 전달합니다.
1. 사용자 프롬프트를 처리하여 클라이언트가 나열한 도구와 함께 LLM에 전달합니다.
좋습니다. 이제 어떻게 고수준에서 이 작업을 수행할 수 있는지 이해했으니, 아래 연습 문제에서 직접 시도해 봅시다.
연습 문제: LLM이 포함된 클라이언트 만들기
이 연습 문제에서는 클라이언트에 LLM을 추가하는 방법을 배웁니다.
GitHub 개인 액세스 토큰을 사용한 인증
GitHub 토큰 만드는 과정은 간단합니다. 방법은 다음과 같습니다:
-1- 서버에 연결
먼저 클라이언트를 만들어 봅시다:
TypeScript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // 스키마 검증을 위해 zod를 가져옵니다
class MCPClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com",
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
}
위 코드에서는:
client와 openai가 있는 클래스를 생성했습니다.baseUrl을 추론 API로 설정하여 GitHub 모델을 사용하도록 LLM 인스턴스를 구성했습니다.Python
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
# stdio 연결을 위한 서버 매개변수 생성
server_params = StdioServerParameters(
command="mcp", # 실행 파일
args=["run", "server.py"], # 선택적 명령줄 인수
env=None, # 선택적 환경 변수
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write
) as session:
# 연결 초기화
await session.initialize()
if __name__ == "__main__":
import asyncio
asyncio.run(run())
위 코드에서는:
.NET
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using System.Text.Json;
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
Java
먼저 pom.xml 파일에 LangChain4j 의존성을 추가해야 합니다. MCP 통합 및 GitHub 모델 지원을 가능하게 하는 의존성을 추가하세요:
<properties>
<langchain4j.version>1.0.0-beta3</langchain4j.version>
</properties>
<dependencies>
<!-- LangChain4j MCP Integration -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI Official API Client -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-official</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- GitHub Models Support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- Spring Boot Starter (optional, for production apps) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
그런 다음 Java 클라이언트 클래스를 만듭니다:
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
import java.time.Duration;
import java.util.List;
public class LangChain4jClient {
public static void main(String[] args) throws Exception { // LLM이 GitHub 모델을 사용하도록 구성하기
ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.build();
// 서버에 연결하기 위한 MCP 전송 생성하기
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
// MCP 클라이언트 생성하기
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
}
}
위 코드에서는:
ChatLanguageModel 생성: GitHub 토큰으로 GitHub 모델을 사용하도록 구성함Rust
이 예제는 Rust 기반 MCP 서버가 실행 중임을 가정합니다. 서버가 없다면 01-first-server 수업으로 돌아가서 서버를 만드세요.
Rust MCP 서버가 준비되면 터미널을 열고 서버와 같은 디렉터리로 이동한 후 다음 명령어로 새 LLM 클라이언트 프로젝트를 생성합니다:
mkdir calculator-llmclient
cd calculator-llmclient
cargo init
Cargo.toml 파일에 다음 의존성을 추가하세요:
[dependencies]
async-openai = { version = "0.29.0", features = ["byot"] }
rmcp = { version = "0.5.0", features = ["client", "transport-child-process"] }
serde_json = "1.0.141"
tokio = { version = "1.46.1", features = ["rt-multi-thread"] }
> [!NOTE]
> OpenAI 공식 Rust 라이브러리는 없지만, async-openai 크레이트는 많이 사용되는 커뮤니티 유지 라이브러리입니다.
src/main.rs 파일을 열고 내용을 다음 코드로 교체하세요:
use async_openai::{Client, config::OpenAIConfig};
use rmcp::{
RmcpError,
model::{CallToolRequestParam, ListToolsResult},
service::{RoleClient, RunningService, ServiceExt},
transport::{ConfigureCommandExt, TokioChildProcess},
};
use serde_json::{Value, json};
use std::error::Error;
use tokio::process::Command;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// 초기 메시지
let mut messages = vec![json!({"role": "user", "content": "What is the sum of 3 and 2?"})];
// OpenAI 클라이언트 설정
let api_key = std::env::var("OPENAI_API_KEY")?;
let openai_client = Client::with_config(
OpenAIConfig::new()
.with_api_base("https://models.github.ai/inference/chat")
.with_api_key(api_key),
);
// MCP 클라이언트 설정
let server_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("calculator-server");
let mcp_client = ()
.serve(
TokioChildProcess::new(Command::new("cargo").configure(|cmd| {
cmd.arg("run").current_dir(server_dir);
}))
.map_err(RmcpError::transport_creation::<TokioChildProcess>)?,
)
.await?;
// TODO: MCP 도구 목록 가져오기
// TODO: 도구 호출과 함께 LLM 대화
Ok(())
}
이 코드는 MCP 서버와 GitHub 모델에 연결할 기본 Rust 애플리케이션을 설정합니다.
> [!IMPORTANT]
> 애플리케이션을 실행하기 전에 반드시 OPENAI_API_KEY 환경 변수에 GitHub 토큰을 설정하세요.
좋습니다, 다음 단계로 서버에서 기능 목록을 불러옵시다.
-2- 서버 기능 목록 가져오기
이제 서버에 연결하여 기능을 요청해 봅시다:
Typescript
같은 클래스에 다음 메서드를 추가하세요:
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
async run() {
console.log("Asking server for available tools");
// 도구 나열하기
const toolsResult = await this.client.listTools();
}
위 코드에서:
connectToServer 코드를 추가했습니다.run 메서드를 만들었습니다. 현재는 도구만 나열하지만 곧 더 추가할 예정입니다.Python
# 사용 가능한 리소스 목록
resources = await session.list_resources()
print("LISTING RESOURCES")
for resource in resources:
print("Resource: ", resource)
# 사용 가능한 도구 목록
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)
print("Tool", tool.inputSchema["properties"])
다음과 같이 추가했습니다:
inputSchema도 나열했습니다..NET
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
// TODO: convert tool definition from MCP tool to LLm tool
}
return toolDefinitions;
}
위 코드에서:
Java
// MCP 도구를 자동으로 검색하는 도구 제공자 생성
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
// MCP 도구 제공자는 자동으로 다음을 처리합니다:
// - MCP 서버에서 사용 가능한 도구 목록 작성
// - MCP 도구 스키마를 LangChain4j 형식으로 변환
// - 도구 실행 및 응답 관리
위 코드에서:
McpToolProvider를 만들었습니다.Rust
MCP 서버에서 도구를 가져오는 것은 list_tools 메서드를 사용합니다. main 함수에서 MCP 클라이언트를 설정한 후 다음 코드를 추가하세요:
// MCP 도구 목록 가져오기
let tools = mcp_client.list_tools(Default::default()).await?;
-3- 서버 기능을 LLM 도구로 변환
서버 기능 목록을 불러온 다음, LLM이 이해할 수 있는 형식으로 변환해야 합니다. 변환하면 이를 LLM의 도구로 제공할 수 있습니다.
TypeScript
1. 다음 코드를 추가하여 MCP 서버 응답을 LLM이 사용할 도구 형식으로 변환하세요:
```typescript
openAiToolAdapter(tool: {
name: string;
description?: string;
input_schema: any;
}) {
// input_schema를 기반으로 zod 스키마 생성
const schema = z.object(tool.input_schema);
return {
type: "function" as const, // 타입을 명시적으로 "function"으로 설정
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.input_schema.properties,
required: tool.input_schema.required,
},
},
};
}
```
위 코드는 MCP 서버 응답을 받아 LLM이 이해할 수 있는 도구 정의 형식으로 변환합니다.
2. 다음으로 run 메서드를 업데이트하여 서버 기능을 나열해 봅시다:
```typescript
async run() {
console.log("Asking server for available tools");
const toolsResult = await this.client.listTools();
const tools = toolsResult.tools.map((tool) => {
return this.openAiToolAdapter({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
});
});
}
```
앞 코드에서 run 메서드를 업데이트하여 결과에서 각 항목에 대해 openAiToolAdapter를 호출하도록 했습니다.
Python
1. 먼저 다음 변환 함수를 만듭니다:
```python
def convert_to_llm_tool(tool):
tool_schema = {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"type": "function",
"parameters": {
"type": "object",
"properties": tool.inputSchema["properties"]
}
}
}
return tool_schema
```
위 함수 convert_to_llm_tools는 MCP 도구 응답을 받아 LLM이 이해할 수 있는 형식으로 변환합니다.
2. 다음으로 이 함수를 활용하도록 클라이언트 코드를 업데이트합니다:
```python
functions = []
for tool in tools.tools:
print("Tool: ", tool.name)
print("Tool", tool.inputSchema["properties"])
functions.append(convert_to_llm_tool(tool))
```
여기서는 MCP 도구 응답을 LLM에 공급할 수 있게 변환하는 convert_to_llm_tool 호출을 추가했습니다.
.NET
1. MCP 도구 응답을 LLM이 이해할 수 있는 형태로 변환하는 코드를 추가합니다:
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}
위 코드에서는:
ConvertFrom 함수를 만들었습니다.FunctionDefinition을 생성하며, 이는 ChatCompletionsDefinition에 전달됩니다. 후자는 LLM이 이해하는 형식입니다.2. 위 함수를 활용하도록 기존 코드를 업데이트해 봅시다:
```csharp
async Task> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
Console.WriteLine($"Properties: {propertiesElement}");
}
return toolDefinitions;
}
``` In the preceding code, we've:
- Update the function to convert the MCP tool response to an LLm tool. Let's highlight the code we added:
```csharp
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
```
The input schema is part of the tool response but on the "properties" attribute, so we need to extract.
Furthermore, we now call ConvertFrom with the tool details.
Now we've done the heavy lifting, let's see how it call comes together as we handle a user prompt next.
Java
// 자연어 상호작용을 위한 Bot 인터페이스 생성
public interface Bot {
String chat(String prompt);
}
// LLM 및 MCP 도구로 AI 서비스 구성
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();
위 코드에서는:
Bot 인터페이스를 정의했습니다.AiServices를 사용해 LLM과 MCP 도구 제공자를 자동 바인딩했습니다.Rust
MCP 도구 응답을 LLM이 이해할 수 있는 형식으로 변환하려면 도구 목록을 형식화하는 헬퍼 함수를 추가해야 합니다. main 함수 아래에 다음 코드를 추가하세요. LLM에 요청할 때 호출됩니다:
async fn format_tools(tools: &ListToolsResult) -> Result<Vec<Value>, Box<dyn Error>> {
let tools_json = serde_json::to_value(tools)?;
let Some(tools_array) = tools_json.get("tools").and_then(|t| t.as_array()) else {
return Ok(vec![]);
};
let formatted_tools = tools_array
.iter()
.filter_map(|tool| {
let name = tool.get("name")?.as_str()?;
let description = tool.get("description")?.as_str()?;
let schema = tool.get("inputSchema")?;
Some(json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": {
"type": "object",
"properties": schema.get("properties").unwrap_or(&json!({})),
"required": schema.get("required").unwrap_or(&json!([]))
}
}
}))
})
.collect();
Ok(formatted_tools)
}
좋습니다, 이제 사용자 요청을 처리할 준비가 되었으니 다음 단계로 넘어갑시다.
-4- 사용자 프롬프트 요청 처리
이 코드 부분에서는 사용자 요청을 처리합니다.
TypeScript
1. LLM을 호출하는데 사용할 메서드를 추가하세요:
```typescript
async callTools(
tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
toolResults: any[]
) {
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(Calling tool ${toolName} with args ${JSON.stringify(args)});
// 2. 서버의 도구를 호출합니다
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. 결과를 가지고 무언가를 합니다
// 할 일
}
}
```
위 코드에서:
- callTools 메서드를 추가했습니다.
- 메서드는 LLM 응답을 받고 어떤 도구가 호출되었는지 검사합니다:
```typescript
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(Calling tool ${toolName} with args ${JSON.stringify(args)});
// 도구 호출
}
```
- LLM이 호출할 것을 지시하면 도구를 호출합니다:
```typescript
// 2. 서버의 도구를 호출합니다
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. 결과로 무언가를 합니다
// 할 일
```
2. run 메서드를 업데이트하여 LLM 호출과 callTools 호출을 포함시킵니다:
```typescript
// 1. LLM의 입력으로 사용할 메시지를 생성합니다
const prompt = "What is the sum of 2 and 3?"
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "user",
content: prompt,
},
];
console.log("Querying LLM: ", messages[0].content);
// 2. LLM 호출
let response = this.openai.chat.completions.create({
model: "gpt-4.1-mini",
max_tokens: 1000,
messages,
tools: tools,
});
let results: any[] = [];
// 3. LLM 응답을 확인하며 각 선택지에 도구 호출이 있는지 점검합니다
(await response).choices.map(async (choice: { message: any; }) => {
const message = choice.message;
if (message.tool_calls) {
console.log("Making tool call")
await this.callTools(message.tool_calls, results);
}
});
```
좋습니다, 전체 코드를 나열해 봅시다:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // 스키마 유효성 검사를 위해 zod를 가져옵니다
class MyClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com", // 미래에 이 URL로 변경해야 할 수도 있습니다: https://models.github.ai/inference
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
openAiToolAdapter(tool: {
name: string;
description?: string;
input_schema: any;
}) {
// input_schema를 기반으로 zod 스키마를 생성합니다
const schema = z.object(tool.input_schema);
return {
type: "function" as const, // 타입을 명시적으로 "function"으로 설정합니다
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.input_schema.properties,
required: tool.input_schema.required,
},
},
};
}
async callTools(
tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
toolResults: any[]
) {
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`);
// 2. 서버의 도구를 호출합니다
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. 결과로 무언가를 수행합니다
// 해야 할 일
}
}
async run() {
console.log("Asking server for available tools");
const toolsResult = await this.client.listTools();
const tools = toolsResult.tools.map((tool) => {
return this.openAiToolAdapter({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
});
});
const prompt = "What is the sum of 2 and 3?";
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "user",
content: prompt,
},
];
console.log("Querying LLM: ", messages[0].content);
let response = this.openai.chat.completions.create({
model: "gpt-4.1-mini",
max_tokens: 1000,
messages,
tools: tools,
});
let results: any[] = [];
// 3. LLM 응답을 확인하고, 각 선택지에 도구 호출이 있는지 검사합니다
(await response).choices.map(async (choice: { message: any; }) => {
const message = choice.message;
if (message.tool_calls) {
console.log("Making tool call")
await this.callTools(message.tool_calls, results);
}
});
}
}
let client = new MyClient();
const transport = new StdioClientTransport({
command: "node",
args: ["./build/index.js"]
});
client.connectToServer(transport);
Python
1. LLM 호출에 필요한 일부 import를 추가합시다:
```python
# 대형 언어 모델
import os
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import SystemMessage, UserMessage
from azure.core.credentials import AzureKeyCredential
import json
```
2. LLM을 호출하는 함수를 추가합시다:
```python
# llm
def call_llm(prompt, functions):
token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o"
client = ChatCompletionsClient(
endpoint=endpoint,
credential=AzureKeyCredential(token),
)
print("CALLING LLM")
response = client.complete(
messages=[
{
"role": "system",
"content": "You are a helpful assistant.",
},
{
"role": "user",
"content": prompt,
},
],
model=model_name,
tools = functions,
# 선택적 매개변수
temperature=1.,
max_tokens=1000,
top_p=1.
)
response_message = response.choices[0].message
functions_to_call = []
if response_message.tool_calls:
for tool_call in response_message.tool_calls:
print("TOOL: ", tool_call)
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
functions_to_call.append({ "name": name, "args": args })
return functions_to_call
```
위 코드에서:
- MCP 서버에서 찾고 변환한 함수들을 LLM에 전달했습니다.
- 해당 함수들과 함께 LLM을 호출했습니다.
- 결과를 검사하여 호출해야 할 함수가 있으면 호출합니다.
- 호출할 함수 목록 배열을 전달합니다.
3. 마지막으로 메인 코드를 업데이트합시다:
```python
prompt = "Add 2 to 20"
# 어떤 도구가 모두 필요한지 LLM에 물어보세요, 만약 있다면
functions_to_call = call_llm(prompt, functions)
# 제안된 함수를 호출하세요
for f in functions_to_call:
result = await session.call_tool(f["name"], arguments=f["args"])
print("TOOLS result: ", result.content)
```
위 코드가 마지막 단계입니다. 여기서 우리는:
- LLM이 프롬프트에 따라 호출해야 한다고 판단한 함수를 사용해 call_tool로 MCP 도구를 호출했습니다.
- 도구 호출 결과를 출력했습니다.
.NET
1. LLM 프롬프트 요청을 수행하는 코드를 봅시다:
```csharp
var tools = await GetMcpTools();
for (int i = 0; i < tools.Count; i++)
{
var tool = tools[i];
Console.WriteLine($"MCP Tools def: {i}: {tool}");
}
// 0. Define the chat history and the user message
var userMessage = "add 2 and 4";
chatHistory.Add(new ChatRequestUserMessage(userMessage));
// 1. Define tools
ChatCompletionsToolDefinition def = CreateToolDefinition();
// 2. Define options, including the tools
var options = new ChatCompletionsOptions(chatHistory)
{
Model = "gpt-4.1-mini",
Tools = { tools[0] }
};
// 3. Call the model
ChatCompletions? response = await client.CompleteAsync(options);
var content = response.Content;
```
위 코드에서는:
- MCP 서버에서 도구를 가져왔습니다: var tools = await GetMcpTools().
- 사용자 프롬프트를 정의했습니다: userMessage.
- 모델과 도구를 지정하는 옵션 객체를 만들었습니다.
- LLM에 요청을 보냈습니다.
2. 마지막 단계로 LLM이 함수 호출이 필요하다고 판단하는지 확인합니다:
```csharp
// 4. Check if the response contains a function call
ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault();
for (int i = 0; i < response.ToolCalls.Count; i++)
{
var call = response.ToolCalls[i];
Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}");
//Tool call 0: add with arguments {"a":2,"b":4}
var dict = JsonSerializer.Deserialize
var result = await mcpClient.CallToolAsync(
call.Name,
dict!,
cancellationToken: CancellationToken.None
);
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
}
```
위 코드에서는:
- 함수 호출 목록을 반복했습니다.
- 각 도구 호출에 대해 이름과 인수를 파싱하고, MCP 클라이언트를 통해 MCP 서버에서 도구를 호출합니다. 결과를 출력합니다.
전체 코드는 다음과 같습니다:
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
var endpoint = "https://models.inference.ai.azure.com";
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); // Your GitHub Access Token
var client = new ChatCompletionsClient(new Uri(endpoint), new AzureKeyCredential(token));
var chatHistory = new List<ChatRequestMessage>
{
new ChatRequestSystemMessage("You are a helpful assistant that knows about AI")
};
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
Console.WriteLine("Setting up stdio transport");
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
Console.WriteLine($"Properties: {propertiesElement}");
}
return toolDefinitions;
}
// 1. List tools on mcp server
var tools = await GetMcpTools();
for (int i = 0; i < tools.Count; i++)
{
var tool = tools[i];
Console.WriteLine($"MCP Tools def: {i}: {tool}");
}
// 2. Define the chat history and the user message
var userMessage = "add 2 and 4";
chatHistory.Add(new ChatRequestUserMessage(userMessage));
// 3. Define options, including the tools
var options = new ChatCompletionsOptions(chatHistory)
{
Model = "gpt-4.1-mini",
Tools = { tools[0] }
};
// 4. Call the model
ChatCompletions? response = await client.CompleteAsync(options);
var content = response.Content;
// 5. Check if the response contains a function call
ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault();
for (int i = 0; i < response.ToolCalls.Count; i++)
{
var call = response.ToolCalls[i];
Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}");
//Tool call 0: add with arguments {"a":2,"b":4}
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.Arguments);
var result = await mcpClient.CallToolAsync(
call.Name,
dict!,
cancellationToken: CancellationToken.None
);
Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text);
}
// 6. Print the generic response
Console.WriteLine($"Assistant response: {content}");
Java
try {
// MCP 도구를 자동으로 사용하는 자연어 요청 실행
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}
위 코드에서는:
- 필요 시 사용자 프롬프트를 도구 호출로 변환
- LLM 판단에 따라 적절한 MCP 도구 호출
- LLM과 MCP 서버 간 대화 흐름 관리
bot.chat() 메서드는 MCP 도구 실행 결과를 포함할 수 있는 자연어 응답을 반환합니다.전체 코드 예제:
public class LangChain4jClient {
public static void main(String[] args) throws Exception { ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.timeout(Duration.ofSeconds(60))
.build();
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();
try {
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}
}
}
Rust
여기서 대부분 작업이 일어납니다. 초기 사용자 프롬프트로 LLM을 호출한 후 응답을 처리하여 호출해야 할 도구가 있는지 확인합니다. 있다면 해당 도구들을 호출하고, 더 이상 도구 호출이 필요 없고 최종 응답이 나올 때까지 LLM과 대화를 계속합니다.
LLM을 여러 번 호출해야 하므로, LLM 호출을 처리하는 함수를 정의합시다. main.rs 파일에 다음 함수를 추가하세요:
async fn call_llm(
client: &Client<OpenAIConfig>,
messages: &[Value],
tools: &ListToolsResult,
) -> Result<Value, Box<dyn Error>> {
let response = client
.completions()
.create_byot(json!({
"messages": messages,
"model": "openai/gpt-4.1",
"tools": format_tools(tools).await?,
}))
.await?;
Ok(response)
}
이 함수는 LLM 클라이언트, 메시지 목록(사용자 프롬프트 포함), MCP 서버 도구를 받아 LLM에 요청을 보내고 응답을 반환합니다.
LLM 응답에는 choices 배열이 포함됩니다.
결과를 처리하여 tool_calls가 있는지 확인해야 합니다.
이를 통해 LLM이 인수와 함께 특정 도구를 호출하도록 요청하는지를 알 수 있습니다.
다음 코드를 main.rs 파일 하단에 추가하여 LLM 응답을 처리하는 함수를 정의하세요:
async fn process_llm_response(
llm_response: &Value,
mcp_client: &RunningService<RoleClient, ()>,
openai_client: &Client<OpenAIConfig>,
mcp_tools: &ListToolsResult,
messages: &mut Vec<Value>,
) -> Result<(), Box<dyn Error>> {
let Some(message) = llm_response
.get("choices")
.and_then(|c| c.as_array())
.and_then(|choices| choices.first())
.and_then(|choice| choice.get("message"))
else {
return Ok(());
};
// 사용 가능한 경우 내용을 출력합니다
if let Some(content) = message.get("content").and_then(|c| c.as_str()) {
println!("🤖 {}", content);
}
// 도구 호출 처리
if let Some(tool_calls) = message.get("tool_calls").and_then(|tc| tc.as_array()) {
messages.push(message.clone()); // 어시스턴트 메시지 추가
// 각 도구 호출 실행
for tool_call in tool_calls {
let (tool_id, name, args) = extract_tool_call_info(tool_call)?;
println!("⚡ Calling tool: {}", name);
let result = mcp_client
.call_tool(CallToolRequestParam {
name: name.into(),
arguments: serde_json::from_str::<Value>(&args)?.as_object().cloned(),
})
.await?;
// 도구 결과를 메시지에 추가
messages.push(json!({
"role": "tool",
"tool_call_id": tool_id,
"content": serde_json::to_string_pretty(&result)?
}));
}
// 도구 결과로 대화 계속하기
let response = call_llm(openai_client, messages, mcp_tools).await?;
Box::pin(process_llm_response(
&response,
mcp_client,
openai_client,
mcp_tools,
messages,
))
.await?;
}
Ok(())
}
tool_calls가 있으면 도구 정보를 추출하고, MCP 서버에 도구 요청을 호출한 다음 결과를 대화 메시지에 추가합니다. 그런 다음 LLM과 계속 대화를 나누며, 메시지는 어시스턴트 응답과 도구 호출 결과로 업데이트됩니다.
LLM이 MCP 호출을 위해 반환하는 도구 호출 정보를 추출하려면, 호출에 필요한 모든 정보를 추출하는 또 다른 도우미 함수를 추가합니다. 다음 코드를 main.rs 파일 하단에 추가하세요:
fn extract_tool_call_info(tool_call: &Value) -> Result<(String, String, String), Box<dyn Error>> {
let tool_id = tool_call
.get("id")
.and_then(|id| id.as_str())
.unwrap_or("")
.to_string();
let function = tool_call.get("function").ok_or("Missing function")?;
let name = function
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string();
let args = function
.get("arguments")
.and_then(|a| a.as_str())
.unwrap_or("{}")
.to_string();
Ok((tool_id, name, args))
}
모든 부분이 준비되었으니 초기 사용자 프롬프트를 처리하고 LLM을 호출할 수 있습니다. main 함수를 다음 코드로 업데이트하세요:
// 도구 호출이 포함된 LLM 대화
let response = call_llm(&openai_client, &messages, &tools).await?;
process_llm_response(
&response,
&mcp_client,
&openai_client,
&tools,
&mut messages,
)
.await?;
이 코드는 두 숫자의 합을 요청하는 초기 사용자 프롬프트로 LLM에 질의하며, 응답을 처리하여 도구 호출을 동적으로 처리합니다.
잘하셨습니다!
과제
연습에서 작성한 코드를 바탕으로 더 많은 도구를 갖춘 서버를 구축하세요. 그런 다음 연습과 같이 LLM 클라이언트를 만들고, 다양한 프롬프트로 테스트하여 모든 서버 도구가 동적으로 호출되는지 확인하세요. 이렇게 클라이언트를 구축하면 최종 사용자가 정확한 클라이언트 명령 대신 프롬프트를 사용할 수 있어 훌륭한 사용자 경험을 제공합니다. MCP 서버 호출이 있는지도 인지하지 못합니다.
해결책
주요 내용 정리
샘플
추가 자료
다음 단계
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 양지하시기 바랍니다.
원문 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
이 번역 사용으로 인해 발생하는 오해나 오해석에 대해 당사는 책임을 지지 않습니다.
GitHub Copilot 에이전트 모드에서 서버 사용하기
Visual Studio Code와 GitHub Copilot은 클라이언트 역할을 하며 MCP 서버를 사용할 수 있습니다.
왜 이런 기능이 필요할까요?
MCP 서버의 모든 기능을 IDE 내에서 사용할 수 있다는 뜻입니다.
예를 들어 GitHub의 MCP 서버를 추가하면 터미널에서 특정 명령어를 입력하는 대신 프롬프트를 통해 GitHub을 제어할 수 있습니다.
또는 개발자 경험을 개선할 수 있는 모든 것을 자연어로 제어할 수 있다고 상상해 보세요.
이제 이 기능의 장점을 이해할 수 있겠죠?
개요
이 강의에서는 Visual Studio Code와 GitHub Copilot의 에이전트 모드를 사용하여 MCP 서버를 클라이언트로 활용하는 방법을 다룹니다.
학습 목표
이 강의를 마치면 다음을 할 수 있습니다:
사용법
MCP 서버를 제어하는 방법은 두 가지가 있습니다:
code 실행 파일을 사용하여 터미널에서 제어할 수 있습니다. 사용자 프로필에 MCP 서버를 추가하려면 --add-mcp 명령줄 옵션을 사용하고 JSON 서버 설정을 다음 형식으로 제공하세요: {\"name\":\"server-name\",\"command\":...}.
```
code --add-mcp "{\"name\":\"my-server\",\"command\": \"uvx\",\"args\": [\"mcp-server-fetch\"]}"
```
스크린샷
다음 섹션에서 시각적 인터페이스를 사용하는 방법에 대해 더 알아보겠습니다.
접근 방식
다음은 고수준에서 접근해야 할 방법입니다:
좋습니다, 이제 흐름을 이해했으니 Visual Studio Code를 통해 MCP 서버를 사용하는 연습을 해봅시다.
연습: 서버 사용하기
이 연습에서는 Visual Studio Code를 설정하여 MCP 서버를 찾아 GitHub Copilot 채팅 인터페이스에서 사용할 수 있도록 합니다.
-0- 사전 단계: MCP 서버 검색 활성화
MCP 서버 검색을 활성화해야 할 수도 있습니다.
1. Visual Studio Code에서 파일 -> 기본 설정 -> 설정으로 이동합니다.
1. "MCP"를 검색하고 settings.json 파일에서 chat.mcp.discovery.enabled를 활성화합니다.
-1- 설정 파일 생성
프로젝트 루트에 설정 파일을 생성하세요. MCP.json이라는 파일을 생성하고 .vscode 폴더에 배치해야 합니다. 다음과 같이 보일 것입니다:
.vscode
|-- mcp.json
다음으로 서버 항목을 추가하는 방법을 알아봅시다.
-2- 서버 설정
*mcp.json*에 다음 내용을 추가하세요:
{
"inputs": [],
"servers": {
"hello-mcp": {
"command": "node",
"args": [
"build/index.js"
]
}
}
}
위의 예는 Node.js로 작성된 서버를 시작하는 간단한 예입니다.
다른 런타임의 경우 command와 args를 사용하여 서버를 시작하는 적절한 명령을 지정하세요.
-3- 서버 시작
항목을 추가했으니 이제 서버를 시작해봅시다:
1. *mcp.json*에서 항목을 찾아 "재생" 아이콘을 확인하세요:
1. "재생" 아이콘을 클릭하면 GitHub Copilot 채팅의 도구 아이콘에 사용 가능한 도구 수가 증가하는 것을 볼 수 있습니다. 해당 도구 아이콘을 클릭하면 등록된 도구 목록이 표시됩니다. 각 도구를 체크하거나 체크 해제하여 GitHub Copilot이 이를 컨텍스트로 사용할지 여부를 결정할 수 있습니다:
1. 도구를 실행하려면 도구 설명과 일치하는 프롬프트를 입력하세요. 예를 들어 "22에 1을 더해줘"와 같은 프롬프트를 입력합니다:
응답으로 23이 표시될 것입니다.
과제
*mcp.json* 파일에 서버 항목을 추가하고 서버를 시작/중지할 수 있는지 확인하세요. GitHub Copilot 채팅 인터페이스를 통해 서버의 도구와 통신할 수 있는지도 확인하세요.
솔루션
주요 내용
이 챕터의 주요 내용은 다음과 같습니다:
샘플
추가 자료
다음 단계
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다하고 있지만, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다.
원본 문서의 원어 버전을 권위 있는 출처로 간주해야 합니다.
중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.
이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 책임을 지지 않습니다.
stdio 전송을 사용하는 MCP 서버
> ⚠️ 중요한 업데이트: MCP 사양 2025-06-18부터 독립형 SSE(Server-Sent Events) 전송은 더 이상 사용되지 않으며 "스트리밍 HTTP" 전송으로 대체되었습니다. 현재 MCP 사양은 두 가지 주요 전송 메커니즘을 정의합니다:
> 1. stdio - 표준 입력/출력 (로컬 서버에 권장)
> 2. 스트리밍 HTTP - 내부적으로 SSE를 사용할 수 있는 원격 서버용
>
> 이 강의는 대부분 MCP 서버 구현에 권장되는 stdio 전송에 중점을 두도록 업데이트되었습니다.
stdio 전송은 MCP 서버가 표준 입력과 출력 스트림을 통해 클라이언트와 통신할 수 있게 합니다. 이는 현재 MCP 사양에서 가장 많이 사용되고 권장되는 전송 메커니즘으로, 다양한 클라이언트 애플리케이션과 쉽게 통합할 수 있는 간단하고 효율적인 MCP 서버 구축 방식을 제공합니다.
개요
이 강의에서는 stdio 전송을 사용하여 MCP 서버를 구축하고 사용하는 방법을 다룹니다.
학습 목표
이 강의를 마치면 다음을 할 수 있습니다:
stdio 전송 - 작동 방식
stdio 전송은 현재 MCP 사양(2025-06-18)에서 지원하는 두 가지 전송 유형 중 하나입니다. 작동 방식은 다음과 같습니다:
stdin)으로부터 JSON-RPC 메시지를 읽고 표준 출력(stdout)으로 메시지를 보냄stderr)에 UTF-8 문자열을 기록할 수 있음주요 요구사항:
stdout에 출력해선 안 됨stdin에 보내선 안 됨TypeScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{
name: "example-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);
앞 코드에서는:
Server 클래스와 StdioServerTransport를 가져옴StdioServerTransport 인스턴스를 생성하고 서버를 연결하여 stdin/stdout을 통한 통신 활성화Python
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server
# 서버 인스턴스 생성
server = Server("example-server")
@server.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
async def main():
async with stdio_server(server) as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
앞 코드에서는:
.NET
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<Tools>();
builder.Services.AddLogging(logging => logging.AddConsole());
var app = builder.Build();
await app.RunAsync();
SSE와 다른 핵심 차이점은 stdio 서버는:
연습: stdio 서버 생성
서버를 만들 때 두 가지를 염두에 둬야 합니다:
실습: 간단한 MCP stdio 서버 만들기
이 실습에서는 권장되는 stdio 전송을 사용해 간단한 MCP 서버를 만듭니다. 이 서버는 표준 Model Context Protocol을 사용해 클라이언트가 호출할 수 있는 도구를 노출합니다.
사전 준비 사항
pip install mcp처음 MCP stdio 서버를 만들어 봅시다:
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
# 로깅 구성
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 서버 생성
server = Server("example-stdio-server")
@server.tool()
def calculate_sum(a: int, b: int) -> int:
"""Calculate the sum of two numbers"""
return a + b
@server.tool()
def get_greeting(name: str) -> str:
"""Generate a personalized greeting"""
return f"Hello, {name}! Welcome to MCP stdio server."
async def main():
# stdio 전송 사용
async with stdio_server(server) as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
더 이상 지원하지 않는 SSE 방식과의 주요 차이점
Stdio 전송 (현재 표준):
SSE 전송 (MCP 2025-06-18부터 더 이상 사용 안 함):
stdio 전송으로 서버 생성하기
stdio 서버를 만들려면:
1. 필요한 라이브러리 가져오기 - MCP 서버 구성 요소와 stdio 전송 필요
2. 서버 인스턴스 생성 - 기능과 함께 서버 정의
3. 도구 정의 - 노출할 기능 추가
4. 전송 설정 - stdio 통신 구성
5. 서버 실행 - 서버 시작 및 메시지 처리
단계별로 구축해 봅시다:
1단계: 기본 stdio 서버 만들기
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server
# 로깅 구성
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 서버 생성
server = Server("example-stdio-server")
@server.tool()
def get_greeting(name: str) -> str:
"""Generate a personalized greeting"""
return f"Hello, {name}! Welcome to MCP stdio server."
async def main():
async with stdio_server(server) as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
2단계: 도구 추가하기
@server.tool()
def calculate_sum(a: int, b: int) -> int:
"""Calculate the sum of two numbers"""
return a + b
@server.tool()
def calculate_product(a: int, b: int) -> int:
"""Calculate the product of two numbers"""
return a * b
@server.tool()
def get_server_info() -> dict:
"""Get information about this MCP server"""
return {
"server_name": "example-stdio-server",
"version": "1.0.0",
"transport": "stdio",
"capabilities": ["tools"]
}
3단계: 서버 실행하기
코드를 server.py로 저장하고 명령줄에서 실행:
python server.py
서버가 시작되어 stdin으로부터 입력을 기다립니다. stdio 전송을 통해 JSON-RPC 메시지로 통신합니다.
4단계: Inspector로 테스트하기
MCP Inspector를 사용해 서버를 테스트할 수 있습니다:
1. Inspector 설치: npx @modelcontextprotocol/inspector
2. Inspector 실행 후 서버에 연결
3. 생성한 도구 테스트
.NET
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer();
```
## stdio 서버 디버깅하기
### MCP Inspector 사용하기
MCP Inspector는 MCP 서버를 디버깅하고 테스트하는 유용한 도구입니다. stdio 서버와 함께 사용하는 방법은 다음과 같습니다:
1. **Inspector 설치**:
```bash
npx @modelcontextprotocol/inspector
```
2. **Inspector 실행**:
```bash
npx @modelcontextprotocol/inspector python server.py
```
3. **서버 테스트**: Inspector는 다음 기능을 제공하는 웹 인터페이스를 지원합니다:
- 서버 기능 보기
- 다양한 매개변수로 도구 테스트
- JSON-RPC 메시지 모니터링
- 연결 문제 디버깅
### VS Code 사용하기
VS Code에서 직접 MCP 서버 디버깅도 가능합니다:
1. `.vscode/launch.json` 파일에 실행 구성 생성:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug MCP Server",
"type": "python",
"request": "launch",
"program": "server.py",
"console": "integratedTerminal"
}
]
}
```
2. 서버 코드에 중단점 설정
3. 디버거 실행 후 Inspector로 테스트
### 일반 디버깅 팁
- 로깅에는 `stderr` 사용 - `stdout`은 MCP 메시지 전용이므로 절대 사용 금지
- 모든 JSON-RPC 메시지는 줄바꿈 문자로 구분되어야 함
- 복잡한 기능 추가 전 간단한 도구부터 테스트
- 메시지 형식 검증에 Inspector 활용
## VS Code에서 stdio 서버 사용하기
stdio MCP 서버를 구축했다면 VS Code와 통합하여 Claude 또는 다른 MCP 호환 클라이언트와 함께 사용할 수 있습니다.
### 설정
1. Windows의 경우 `%APPDATA%\Claude\claude_desktop_config.json`, Mac의 경우 `~/Library/Application Support/Claude/claude_desktop_config.json` 위치에 MCP 구성 파일 생성:
```json
{
"mcpServers": {
"example-stdio-server": {
"command": "python",
"args": ["path/to/your/server.py"]
}
}
}
```
2. **Claude 재시작**: Claude를 닫았다가 다시 열어 새 서버 구성을 반영
3. **연결 테스트**: Claude와 대화 시작 후 서버 도구 사용 시도:
- "인사 도구를 사용해 인사해 줄래?"
- "15와 27의 합을 계산해 줘"
- "서버 정보가 뭐야?"
### TypeScript stdio 서버 예제
참고용 TypeScript 완성 예제:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{
name: "example-stdio-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// 도구 추가
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_greeting",
description: "Get a personalized greeting",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the person to greet",
},
},
required: ["name"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_greeting") {
return {
content: [
{
type: "text",
text: Hello, ${request.params.arguments?.name}! Welcome to MCP stdio server.,
},
],
};
} else {
throw new Error(Unknown tool: ${request.params.name});
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);
### .NET stdio 서버 예제
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools
var app = builder.Build();
await app.RunAsync();
public class Tools
{
[McpServerTool, Description("Get a personalized greeting")]
public string GetGreeting(string name)
{
return $"Hello, {name}! Welcome to MCP stdio server.";
}
[McpServerTool, Description("Calculate the sum of two numbers")]
public int CalculateSum(int a, int b)
{
return a + b;
}
}
## 요약
이번 업데이트된 강의에서 배운 점:
- 현재 **stdio 전송**(권장 방식)을 사용해 MCP 서버 구축하기
- SSE 전송이 stdio와 스트리밍 HTTP로 대체된 이유 이해하기
- MCP 클라이언트가 호출할 도구 만들기
- MCP Inspector를 사용해 서버 디버깅하기
- VS Code 및 Claude와 stdio 서버 통합하기
stdio 전송은 더 이상 사용되지 않는 SSE 방식보다 MCP 서버를 더 간단하고, 안전하며, 성능 좋게 구축할 수 있는 방법입니다. 2025-06-18 사양 기준으로 대부분 MCP 서버 구현에 권장되는 전송입니다.
### .NET
1. 먼저 몇 가지 도구를 만들어보겠습니다. 이를 위해 <em>Tools.cs</em>라는 파일을 생성하고 다음 내용을 작성합니다:
```csharp
using System.ComponentModel;
using System.Text.Json;
using ModelContextProtocol.Server;
```
## 연습: stdio 서버 테스트하기
stdio 서버를 구축했으니 제대로 동작하는지 테스트해 봅시다.
### 사전 준비 사항
1. MCP Inspector 설치 여부 확인:
```bash
npm install -g @modelcontextprotocol/inspector
```
2. 서버 코드는 저장되어 있어야 함 (예: `server.py`)
### Inspector를 이용한 테스트
1. **서버와 함께 Inspector 시작**:
```bash
npx @modelcontextprotocol/inspector python server.py
```
2. **웹 인터페이스 열기**: Inspector가 브라우저 창에서 서버의 기능을 표시합니다.
3. **도구 테스트**:
- `get_greeting` 도구를 여러 이름으로 시도
- `calculate_sum` 도구를 다양한 숫자로 테스트
- `get_server_info` 도구 호출하여 서버 메타데이터 확인
4. **통신 모니터링**: Inspector가 클라이언트와 서버 간 JSON-RPC 메시지를 보여줍니다.
### 기대 결과
서버가 올바르게 시작되면 다음을 볼 수 있습니다:
- Inspector에서 서버 기능 목록 표시
- 테스트 가능한 도구들
- JSON-RPC 메시지의 정상 교환
- 인터페이스에 표시되는 도구 응답
### 자주 발생하는 문제와 해결책
**서버가 시작되지 않을 때:**
- 모든 의존성 설치 확인: `pip install mcp`
- Python 문법 및 들여쓰기 점검
- 콘솔의 오류 메시지 확인
**도구가 나타나지 않을 때:**
- `@server.tool()` 데코레이터가 있는지 확인
- 도구 함수가 `main()` 이전에 정의되어 있는지 확인
- 서버가 올바르게 구성됐는지 점검
**연결 문제:**
- 서버가 stdio 전송을 적절히 사용하는지 확인
- 다른 프로세스가 방해하지 않는지 점검
- Inspector 명령 구문 확인
## 과제
더 많은 기능을 추가하며 서버를 확장해 보세요. 예를 들어 [이 페이지](https://api.chucknorris.io/)를 참고해 API를 호출하는 도구를 추가할 수 있습니다. 서버를 어떻게 만들지는 여러분의 선택입니다. 즐겁게 만드세요 :)
## 해결책
[해결책](./solution/README.md) 정상 작동하는 코드 예시입니다.
## 주요 정리
이 챕터에서 얻은 주요 내용은 다음과 같습니다:
- stdio 전송은 로컬 MCP 서버에 권장되는 메커니즘입니다.
- stdio 전송은 표준 입출력 스트림을 이용해 MCP 서버와 클라이언트 간 원활한 통신을 가능케 합니다.
- Inspector와 Visual Studio Code 둘 다 stdio 서버를 직접 사용해 디버깅과 통합이 용이합니다.
## 샘플
- [Java 계산기](../samples/java/calculator/README.md)
- [.Net 계산기](../../../../03-GettingStarted/samples/csharp)
- [JavaScript 계산기](../samples/javascript/README.md)
- [TypeScript 계산기](../samples/typescript/README.md)
- [Python 계산기](../../../../03-GettingStarted/samples/python)
## 추가 자료
- [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
## 다음은?
## 다음 단계
stdlib 전송으로 MCP 서버를 만드는 법을 배웠으니, 더 심화된 주제를 탐색해보세요:
- <strong>다음</strong>: [MCP HTTP 스트리밍(스트리밍 HTTP)](../06-http-streaming/README.md) - 원격 서버용 다른 지원 전송 메커니즘 학습
- <strong>고급</strong>: [MCP 보안 모범 사례](../../02-Security/README.md) - MCP 서버 보안 구현
- <strong>운영</strong>: [배포 전략](../09-deployment/README.md) - 운영용 서버 배포
## 추가 자료
- [MCP 사양 2025-06-18](https://spec.modelcontextprotocol.io/specification/) - 공식 사양서
- [MCP SDK 문서](https://github.com/modelcontextprotocol/sdk) - 모든 언어용 SDK 참고 자료
- [커뮤니티 예제](../../06-CommunityContributions/README.md) - 커뮤니티에서 만든 서버 예제 더 보기
---
<!-- CO-OP TRANSLATOR DISCLAIMER START -->
**면책 조항**:
이 문서는 AI 번역 서비스 [Co-op Translator](https://github.com/Azure/co-op-translator)를 사용하여 번역되었습니다. 정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 유의하시기 바랍니다. 원문은 해당 언어의 원본 문서가 권위 있는 자료로 간주되어야 합니다. 중요한 정보의 경우 전문가의 인간 번역을 권장합니다. 이 번역의 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
<!-- CO-OP TRANSLATOR DISCLAIMER END -->
HTTPS 스트리밍과 모델 컨텍스트 프로토콜(MCP)
이 장에서는 HTTPS를 사용하여 모델 컨텍스트 프로토콜(MCP)로 보안성, 확장성 및 실시간 스트리밍을 구현하는 포괄적인 가이드를 제공합니다. 스트리밍의 동기, 사용 가능한 전송 메커니즘, MCP에서 스트림 가능한 HTTP를 구현하는 방법, 보안 모범 사례, SSE에서의 마이그레이션, 그리고 스트리밍 MCP 애플리케이션을 구축하는 실용적인 지침을 다룹니다.
MCP의 전송 메커니즘과 스트리밍
이 섹션에서는 MCP에서 사용할 수 있는 다양한 전송 메커니즘과 이들이 클라이언트와 서버 간 실시간 통신을 가능하게 하는 스트리밍 기능에서 어떤 역할을 하는지 살펴봅니다.
전송 메커니즘이란 무엇인가?
전송 메커니즘은 클라이언트와 서버 간 데이터가 교환되는 방식을 정의합니다. MCP는 다양한 환경과 요구 사항에 맞춰 여러 전송 유형을 지원합니다:
비교 표
아래 비교 표를 보면 이러한 전송 메커니즘 간의 차이점을 이해할 수 있습니다:
> 팁: 적절한 전송 방식을 선택하는 것은 성능, 확장성, 사용자 경험에 중요한 영향을 미칩니다. Streamable HTTP는 최신의 확장 가능하고 클라우드 대응 애플리케이션에 권장됩니다.
이전 장에서 소개한 stdio 및 SSE 전송과 달리, 이 장에서는 Streamable HTTP 전송에 대해 다룹니다.
스트리밍: 개념과 동기
스트리밍의 기본 개념과 동기를 이해하는 것은 효과적인 실시간 통신 시스템을 구현하는 데 필수적입니다.
스트리밍은 네트워크 프로그래밍에서 전체 응답이 준비될 때까지 기다리지 않고 데이터가 작고 관리 가능한 청크로 또는 이벤트 시퀀스로 전송되고 수신될 수 있게 하는 기술입니다. 특히 다음과 같은 경우에 유용합니다:
스트리밍에 대해 알아야 할 주요 사항은 다음과 같습니다:
왜 스트리밍을 사용하는가?
스트리밍 사용 이유는 다음과 같습니다:
간단한 예: HTTP 스트리밍 서버 및 클라이언트
다음은 스트리밍이 어떻게 구현될 수 있는지에 대한 간단한 예입니다:
Python
서버 (Python, FastAPI 및 StreamingResponse 사용):
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import time
app = FastAPI()
async def event_stream():
for i in range(1, 6):
yield f"data: Message {i}\n\n"
time.sleep(1)
@app.get("/stream")
def stream():
return StreamingResponse(event_stream(), media_type="text/event-stream")
클라이언트 (Python, requests 사용):
import requests
with requests.get("http://localhost:8000/stream", stream=True) as r:
for line in r.iter_lines():
if line:
print(line.decode())
이 예는 모든 메시지가 준비될 때까지 기다리지 않고, 서버가 준비되는 대로 일련의 메시지를 클라이언트에 전송하는 방식을 보여줍니다.
작동 방식:
요구 사항:
StreamingResponse)을 사용해야 합니다.requests의 stream=True).text/event-stream 또는 application/octet-stream입니다.Java
서버 (Java, Spring Boot 및 Server-Sent Events 사용):
@RestController
public class CalculatorController {
@GetMapping(value = "/calculate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> calculate(@RequestParam double a,
@RequestParam double b,
@RequestParam String op) {
double result;
switch (op) {
case "add": result = a + b; break;
case "sub": result = a - b; break;
case "mul": result = a * b; break;
case "div": result = b != 0 ? a / b : Double.NaN; break;
default: result = Double.NaN;
}
return Flux.<ServerSentEvent<String>>just(
ServerSentEvent.<String>builder()
.event("info")
.data("Calculating: " + a + " " + op + " " + b)
.build(),
ServerSentEvent.<String>builder()
.event("result")
.data(String.valueOf(result))
.build()
)
.delayElements(Duration.ofSeconds(1));
}
}
클라이언트 (Java, Spring WebFlux WebClient 사용):
@SpringBootApplication
public class CalculatorClientApplication implements CommandLineRunner {
private final WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.build();
@Override
public void run(String... args) {
client.get()
.uri(uriBuilder -> uriBuilder
.path("/calculate")
.queryParam("a", 7)
.queryParam("b", 5)
.queryParam("op", "mul")
.build())
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class)
.doOnNext(System.out::println)
.blockLast();
}
}
Java 구현 주석:
Flux)을 활용하여 스트리밍ServerSentEvent 는 이벤트 타입을 갖는 구조화된 이벤트 스트리밍 제공WebClient의 bodyToFlux()를 통해 반응형 스트리밍 소비 가능delayElements()로 이벤트 간 처리 시간 시뮬레이션info 및 result 타입 부여하여 클라이언트 처리 개선비교: 클래식 스트리밍 vs MCP 스트리밍
클래식 방식의 스트리밍과 MCP 스트리밍이 어떻게 다른지 다음 표에서 확인할 수 있습니다:
관찰된 주요 차이점
추가로 주요 차이점은 다음과 같습니다:
- 클래식 HTTP 스트리밍: 간단한 청크 전송 인코딩 사용
- MCP 스트리밍: JSON-RPC 프로토콜 기반 구조화된 알림 시스템 사용
- 클래식 HTTP: 줄바꿈 포함한 텍스트 청크
- MCP: 메타데이터가 포함된 구조화된 LoggingMessageNotification 객체
- 클래식 HTTP: 스트리밍 응답을 처리하는 단순 클라이언트
- MCP: 메시지 핸들러를 구현하여 다양한 메시지 유형 처리
- 클래식 HTTP: 진행 상황이 주요 응답 스트림의 일부
- MCP: 주요 응답은 끝에 전송되고 진행 상황은 별도 알림 메시지로 전송
권장 사항
클래식 스트리밍(/stream 엔드포인트) 구현과 MCP 스트리밍 구현 간 선택 시 다음 사항들을 권장합니다.
MCP에서의 스트리밍
이제까지 클래식 스트리밍과 MCP 스트리밍 차이에 대해 권장 사항과 비교를 보셨습니다. 이제 MCP에서 스트리밍을 어떻게 활용할 수 있는지 자세히 살펴봅시다.
MCP 프레임워크 내 스트리밍이 어떻게 작동하는지 이해하는 것은 장시간 실행 작업 중 사용자에게 실시간 피드백을 제공하는 반응형 애플리케이션을 구축하는 데 중요합니다.
MCP에서 스트리밍은 주요 응답을 청크로 나누어 보내는 것이 아니라, 툴이 요청을 처리하는 동안 알림(notification) 을 클라이언트로 전송하는 것입니다. 이 알림에는 진행 상황 업데이트, 로그, 기타 이벤트가 포함될 수 있습니다.
작동 방식
주요 결과는 여전히 단일 응답으로 전송됩니다. 그러나 처리 중에 알림이 별도의 메시지로 전송되어 클라이언트에 실시간으로 업데이트할 수 있습니다. 클라이언트는 이 알림을 수신하고 표시할 수 있어야 합니다.
알림(Notification)이란?
"알림"이란 무엇인가라고 했는데, MCP 맥락에서 이를 정의하면?
알림은 서버에서 클라이언트로 보내는 메시지로서, 장시간 실행 작업 중 진행 상황, 상태 또는 기타 이벤트를 알리기 위해 전송됩니다. 알림은 투명성과 사용자 경험을 개선합니다.
예를 들어, 클라이언트는 서버와의 초기 핸드셰이크가 완료되면 알림을 보내야 합니다.
알림은 JSON 메시지 형태로 다음과 같습니다:
{
jsonrpc: "2.0";
method: string;
params?: {
[key: string]: unknown;
};
}
알림은 MCP 내 "Logging" 토픽에 속합니다.
로깅 기능을 작동시키려면 서버는 다음과 같이 기능/역량으로 활성화해야 합니다:
{
"capabilities": {
"logging": {}
}
}
> [!NOTE]
> 사용 중인 SDK에 따라 로깅이 기본적으로 활성화되어 있거나 서버 설정에서 명시적으로 활성화해야 할 수 있습니다.
알림에는 여러 유형이 있습니다:
MCP에서 알림 구현
MCP에서 알림을 구현하려면 서버와 클라이언트 양쪽에서 실시간 업데이트를 처리하도록 설정해야 합니다. 이를 통해 장시간 실행 작업 중 즉각적인 피드백을 사용자에게 제공할 수 있습니다.
서버 측: 알림 전송
먼저 서버 측부터 시작합시다. MCP에서는 요청 처리 중 알림을 보낼 수 있는 툴을 정의합니다. 서버는 컨텍스트 객체(보통 ctx)를 사용하여 클라이언트에 메시지를 전송합니다.
Python
@mcp.tool(description="A tool that sends progress notifications")
async def process_files(message: str, ctx: Context) -> TextContent:
await ctx.info("Processing file 1/3...")
await ctx.info("Processing file 2/3...")
await ctx.info("Processing file 3/3...")
return TextContent(type="text", text=f"Done: {message}")
위 예제에서 process_files 툴은 각 파일을 처리할 때 클라이언트에 세 번의 알림을 보냅니다. ctx.info() 메서드는 정보 메시지 전송에 사용됩니다.
또한 알림을 활성화하려면 서버가 스트리밍 전송(streamable-http 등)을 사용하고 클라이언트가 알림을 처리할 메시지 핸들러를 구현해야 합니다.
서버에서 streamable-http 전송을 사용하는 방법은 다음과 같습니다:
mcp.run(transport="streamable-http")
.NET
[Tool("A tool that sends progress notifications")]
public async Task<TextContent> ProcessFiles(string message, ToolContext ctx)
{
await ctx.Info("Processing file 1/3...");
await ctx.Info("Processing file 2/3...");
await ctx.Info("Processing file 3/3...");
return new TextContent
{
Type = "text",
Text = $"Done: {message}"
};
}
이 .NET 예제에서 ProcessFiles 툴은 Tool 어트리뷰트가 적용되어 있으며 각 파일 처리 시 클라이언트에 세 번 알림을 보냅니다. ctx.Info() 메서드는 정보 메시지 전송에 이용됩니다.
.NET MCP 서버에서 알림을 활성화하려면 스트리밍 전송이 사용되고 있는지 확인하세요:
var builder = McpBuilder.Create();
await builder
.UseStreamableHttp() // Enable streamable HTTP transport
.Build()
.RunAsync();
클라이언트 측: 알림 수신
클라이언트는 메시지 핸들러를 구현해야 하며, 도착하는 알림을 처리하고 표시할 수 있어야 합니다.
Python
async def message_handler(message):
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
else:
print("SERVER MESSAGE:", message)
async with ClientSession(
read_stream,
write_stream,
logging_callback=logging_collector,
message_handler=message_handler,
) as session:
위 코드에서 message_handler 함수는 들어오는 메시지가 알림인지 확인합니다.
알림이면 출력하며, 아니면 일반 서버 메시지로 처리합니다.
또한 ClientSession은 도착하는 알림을 처리하기 위해 message_handler로 초기화됩니다.
.NET
// Define a message handler
void MessageHandler(IJsonRpcMessage message)
{
if (message is ServerNotification notification)
{
Console.WriteLine($"NOTIFICATION: {notification}");
}
else
{
Console.WriteLine($"SERVER MESSAGE: {message}");
}
}
// Create and use a client session with the message handler
var clientOptions = new ClientSessionOptions
{
MessageHandler = MessageHandler,
LoggingCallback = (level, message) => Console.WriteLine($"[{level}] {message}")
};
using var client = new ClientSession(readStream, writeStream, clientOptions);
await client.InitializeAsync();
// Now the client will process notifications through the MessageHandler
이 .NET 예제에서 MessageHandler 함수는 들어오는 메시지가 알림인지 체크합니다.
알림이면 출력하고, 아니면 일반 서버 메시지로 처리합니다. ClientSession은 ClientSessionOptions를 통해 메시지 핸들러로 초기화됩니다.
알림 활성화를 위해 서버가 스트리밍 전송(streamable-http 등)을 사용하고 클라이언트가 알림 메시지 핸들러를 구현하는지 확인하세요.
진행 알림 및 시나리오
이 섹션에서는 MCP에서 진행 알림의 개념과 중요성, 그리고 Streamable HTTP를 통해 이를 구현하는 방법을 설명합니다. 또한 이해를 돕기 위한 실용적인 과제도 포함되어 있습니다.
진행 알림(progress notifications)은 장시간 작업 중 서버에서 클라이언트로 실시간으로 보내는 메시지입니다. 전체 프로세스가 끝날 때까지 기다리지 않고 서버가 현재 상태를 계속해서 알립니다. 이는 투명성, 사용자 경험을 높이고 디버깅도 용이하게 합니다.
예시:
"Processing document 1/10"
"Processing document 2/10"
...
"Processing complete!"
왜 진행 알림을 사용하는가?
진행 알림이 중요한 이유는 다음과 같습니다:
진행 알림 구현 방법
MCP에서 진행 알림을 구현하는 방법은 다음과 같습니다:
ctx.info() 또는 ctx.log()를 사용해 각 아이템 처리 시 알림을 전송. 이는 주요 결과가 준비되기 전에 클라이언트에 메시지를 보냅니다.서버 예제:
Python
@mcp.tool(description="A tool that sends progress notifications")
async def process_files(message: str, ctx: Context) -> TextContent:
for i in range(1, 11):
await ctx.info(f"Processing document {i}/10")
await ctx.info("Processing complete!")
return TextContent(type="text", text=f"Done: {message}")
클라이언트 예제:
Python
async def message_handler(message):
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
else:
print("SERVER MESSAGE:", message)
보안 고려 사항
HTTP 기반 전송을 사용하는 MCP 서버를 구현할 때 보안은 다양한 공격 지점과 보호 메커니즘에 세심한 주의가 필요한 매우 중요한 문제입니다.
개요
MCP 서버를 HTTP로 노출할 때 보안은 필수적입니다. Streamable HTTP는 새로운 공격 표면을 도입하여 신중한 구성이 요구됩니다.
핵심 사항
Origin 헤더를 검증하세요.localhost에 바인딩하여 공개 인터넷 노출을 방지하세요.모범 사례
도전과제
SSE에서 Streamable HTTP로 업그레이드하기
현재 Server-Sent Events(SSE)를 사용하는 애플리케이션의 경우, Streamable HTTP로 마이그레이션하면 향상된 기능과 MCP 구현의 장기적 유지 가능성을 제공합니다.
업그레이드가 필요한 이유
SSE에서 Streamable HTTP로 업그레이드해야 하는 두 가지 설득력 있는 이유가 있습니다:
마이그레이션 단계
MCP 애플리케이션에서 SSE에서 Streamable HTTP로 마이그레이션하는 방법은 다음과 같습니다:
mcp.run()에서 transport="streamable-http"로 업데이트합니다.streamablehttp_client를 사용하도록 업데이트합니다.호환성 유지
마이그레이션 과정에서 기존 SSE 클라이언트와의 호환성을 유지하는 것이 권장됩니다. 다음과 같은 전략이 있습니다:
과제
마이그레이션 시 다음 과제를 반드시 해결해야 합니다:
보안 고려사항
어떤 서버를 구현하든, 특히 MCP의 Streamable HTTP 같은 HTTP 기반 전송을 사용할 때 보안은 최우선 과제여야 합니다.
HTTP 기반 전송을 사용하는 MCP 서버를 구현할 때는 여러 공격 벡터와 보호 메커니즘에 세심한 주의를 기울여야 합니다.
개요
MCP 서버를 HTTP를 통해 노출할 때 보안은 매우 중요합니다. Streamable HTTP는 새로운 공격 지점을 제공하며 신중한 구성과 관리가 필요합니다.
주요 보안 고려사항은 다음과 같습니다:
Origin 헤더를 검증합니다.localhost에 바인딩하여 공개 인터넷에 노출되지 않도록 합니다.모범 사례
MCP 스트리밍 서버에서 보안을 구현할 때 따를 모범 사례는 다음과 같습니다:
과제
MCP 스트리밍 서버 보안 구현 중에는 다음과 같은 과제를 직면합니다:
과제: 직접 나만의 스트리밍 MCP 애플리케이션 만들기
시나리오:
서버가 아이템 목록(예: 파일 또는 문서)을 처리하고 처리된 각 아이템에 대해 알림을 전송하는 MCP 서버와 클라이언트를 만드세요. 클라이언트는 도착하는 각 알림을 실시간으로 표시해야 합니다.
단계:
1. 목록을 처리하고 각 항목에 대해 알림을 보내는 서버 도구를 구현하세요.
2. 알림을 실시간으로 표시할 메시지 핸들러가 있는 클라이언트를 구현하세요.
3. 서버와 클라이언트를 함께 실행하여 구현을 테스트하고 알림이 잘 표시되는지 확인하세요.
추가 학습 및 다음 단계
MCP 스트리밍을 계속 사용하고 지식을 확장하기 위해 이 섹션에서는 추가 리소스와 더 고급 애플리케이션을 만들기 위한 권장 다음 단계를 제공합니다.
추가 학습
다음 단계
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 기하기 위해 노력하고 있으나, 자동 번역은 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.
원본 문서의 원어본이 권위 있는 자료로 간주되어야 합니다.
중요한 정보의 경우, 전문가의 인간 번역을 권장합니다.
이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
Visual Studio Code의 AI Toolkit 확장을 사용하여 서버 활용하기
AI 에이전트를 구축할 때, 단순히 스마트한 응답을 생성하는 것만이 아니라 에이전트가 실제로 행동을 취할 수 있는 능력을 부여하는 것이 중요합니다. 바로 여기서 Model Context Protocol (MCP)이 등장합니다. MCP는 에이전트가 외부 도구와 서비스를 일관된 방식으로 쉽게 접근할 수 있도록 해줍니다. 마치 에이전트를 실제로 사용할 수 있는 도구 상자에 연결하는 것과 같습니다.
예를 들어, 에이전트를 계산기 MCP 서버에 연결했다고 가정해봅시다. 그러면 에이전트는 "47 곱하기 89는 얼마야?"와 같은 프롬프트를 받는 것만으로 수학 연산을 수행할 수 있게 됩니다. 로직을 하드코딩하거나 커스텀 API를 구축할 필요가 없습니다.
개요
이 레슨에서는 Visual Studio Code의 AI Toolkit 확장을 사용하여 계산기 MCP 서버를 에이전트에 연결하는 방법을 다룹니다. 이를 통해 에이전트가 덧셈, 뺄셈, 곱셈, 나눗셈과 같은 수학 연산을 자연어를 통해 수행할 수 있게 됩니다.
AI Toolkit은 Visual Studio Code에서 에이전트 개발을 간소화하는 강력한 확장 프로그램입니다. AI 엔지니어는 로컬 또는 클라우드에서 생성형 AI 모델을 개발하고 테스트하여 AI 애플리케이션을 쉽게 구축할 수 있습니다. 이 확장은 현재 사용 가능한 주요 생성형 모델 대부분을 지원합니다.
*참고*: AI Toolkit은 현재 Python과 TypeScript를 지원합니다.
학습 목표
이 레슨을 마치면 다음을 수행할 수 있습니다:
접근 방식
다음은 전체적인 접근 방식입니다:
좋습니다. 이제 흐름을 이해했으니, MCP를 통해 외부 도구를 활용하여 AI 에이전트의 기능을 확장해봅시다!
사전 준비
실습: 서버 활용하기
> [!WARNING]
> macOS 사용자 참고: 현재 macOS에서 종속성 설치에 영향을 미치는 문제를 조사 중입니다. 이로 인해 macOS 사용자는 이 튜토리얼을 현재 완료할 수 없습니다. 문제가 해결되는 대로 지침을 업데이트하겠습니다. 기다려주셔서 감사합니다!
이 실습에서는 Visual Studio Code의 AI Toolkit을 사용하여 MCP 서버의 도구를 활용하여 AI 에이전트를 구축, 실행 및 개선합니다.
-0- 사전 단계: OpenAI GPT-4o 모델을 My Models에 추가하기
이 실습에서는 GPT-4o 모델을 사용합니다. 에이전트를 생성하기 전에 이 모델을 My Models에 추가해야 합니다.
1. Activity Bar에서 AI Toolkit 확장을 엽니다.
1. Catalog 섹션에서 Models를 선택하여 Model Catalog를 엽니다. Models를 선택하면 Model Catalog가 새 편집기 탭에서 열립니다.
1. Model Catalog 검색창에 OpenAI GPT-4o를 입력합니다.
1. + Add를 클릭하여 모델을 My Models 목록에 추가합니다. GitHub에서 호스팅된 모델을 선택했는지 확인하세요.
1. Activity Bar에서 OpenAI GPT-4o 모델이 목록에 나타나는지 확인합니다.
-1- 에이전트 생성하기
Agent (Prompt) Builder를 사용하면 AI 기반 에이전트를 생성하고 사용자 정의할 수 있습니다. 이 섹션에서는 새 에이전트를 생성하고 대화를 구동할 모델을 지정합니다.
1. Activity Bar에서 AI Toolkit 확장을 엽니다.
1. Tools 섹션에서 Agent (Prompt) Builder를 선택합니다. Agent (Prompt) Builder를 선택하면 새 편집기 탭에서 Agent (Prompt) Builder가 열립니다.
1. + New Agent 버튼을 클릭합니다. 확장은 Command Palette를 통해 설정 마법사를 실행합니다.
1. 이름으로 Calculator Agent를 입력하고 Enter를 누릅니다.
1. Agent (Prompt) Builder에서 Model 필드에 OpenAI GPT-4o (via GitHub) 모델을 선택합니다.
-2- 에이전트의 시스템 프롬프트 생성하기
에이전트의 기본 구조를 생성한 후, 이제 에이전트의 성격과 목적을 정의할 차례입니다. 이 섹션에서는 Generate system prompt 기능을 사용하여 계산기 에이전트의 의도된 동작을 설명하고 모델이 시스템 프롬프트를 작성하도록 합니다.
1. Prompts 섹션에서 Generate system prompt 버튼을 클릭합니다. 이 버튼은 에이전트의 시스템 프롬프트를 생성하는 프롬프트 빌더를 엽니다.
1. Generate a prompt 창에서 다음을 입력합니다: 당신은 유용하고 효율적인 수학 도우미입니다. 기본 산술 문제를 받으면 올바른 결과를 제공합니다.
1. Generate 버튼을 클릭합니다. 오른쪽 하단에 시스템 프롬프트가 생성 중임을 알리는 알림이 나타납니다. 프롬프트 생성이 완료되면 Agent (Prompt) Builder의 System prompt 필드에 프롬프트가 나타납니다.
1. System prompt를 검토하고 필요하면 수정합니다.
-3- MCP 서버 생성하기
에이전트의 시스템 프롬프트를 정의하여 동작과 응답을 안내한 후, 이제 에이전트에 실질적인 기능을 추가할 차례입니다. 이 섹션에서는 덧셈, 뺄셈, 곱셈, 나눗셈 계산을 실행할 도구를 포함한 계산기 MCP 서버를 생성합니다. 이 서버는 에이전트가 자연어 프롬프트에 따라 실시간으로 수학 연산을 수행할 수 있도록 합니다.
AI Toolkit은 사용자 정의 MCP 서버를 쉽게 생성할 수 있는 템플릿을 제공합니다. 우리는 계산기 MCP 서버를 생성하기 위해 Python 템플릿을 사용할 것입니다.
*참고*: AI Toolkit은 현재 Python과 TypeScript를 지원합니다.
1. Agent (Prompt) Builder의 Tools 섹션에서 + MCP Server 버튼을 클릭합니다. 확장은 Command Palette를 통해 설정 마법사를 실행합니다.
1. + Add Server를 선택합니다.
1. Create a New MCP Server를 선택합니다.
1. 템플릿으로 python-weather를 선택합니다.
1. MCP 서버 템플릿을 저장할 폴더로 Default folder를 선택합니다.
1. 서버 이름으로 Calculator를 입력합니다.
1. 새 Visual Studio Code 창이 열립니다. Yes, I trust the authors를 선택합니다.
1. Terminal 메뉴에서 New Terminal을 열어 가상 환경을 생성합니다: python -m venv .venv
1. 터미널에서 가상 환경을 활성화합니다:
- Windows - .venv\Scripts\activate
- macOS/Linux - source .venv/bin/activate
1. 터미널에서 종속성을 설치합니다: pip install -e .[dev]
1. Activity Bar의 Explorer 보기에서 src 디렉토리를 확장하고 server.py를 선택하여 파일을 편집기에서 엽니다.
1. server.py 파일의 코드를 다음으로 교체하고 저장합니다:
```python
"""
Sample MCP Calculator Server implementation in Python.
This module demonstrates how to create a simple MCP server with calculator tools
that can perform basic arithmetic operations (add, subtract, multiply, divide).
"""
from mcp.server.fastmcp import FastMCP
server = FastMCP("calculator")
@server.tool()
def add(a: float, b: float) -> float:
"""Add two numbers together and return the result."""
return a + b
@server.tool()
def subtract(a: float, b: float) -> float:
"""Subtract b from a and return the result."""
return a - b
@server.tool()
def multiply(a: float, b: float) -> float:
"""Multiply two numbers together and return the result."""
return a * b
@server.tool()
def divide(a: float, b: float) -> float:
"""
Divide a by b and return the result.
Raises:
ValueError: If b is zero
"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
```
-4- 계산기 MCP 서버와 함께 에이전트 실행하기
이제 에이전트가 도구를 갖췄으니 이를 사용해볼 차례입니다! 이 섹션에서는 에이전트에 프롬프트를 제출하여 계산기 MCP 서버의 적절한 도구를 활용하는지 테스트하고 검증합니다.
1. MCP 서버를 디버깅하려면 F5를 누릅니다. Agent (Prompt) Builder가 새 편집기 탭에서 열립니다. 서버 상태는 터미널에서 확인할 수 있습니다.
1. Agent (Prompt) Builder의 User prompt 필드에 다음 프롬프트를 입력합니다: 나는 각각 $25인 물건 3개를 샀고, $20 할인권을 사용했어. 총 얼마를 지불했지?
1. Run 버튼을 클릭하여 에이전트의 응답을 생성합니다.
1. 에이전트 출력을 검토합니다. 모델은 $55를 지불했다고 결론을 내려야 합니다.
1. 다음과 같은 과정이 발생해야 합니다:
- 에이전트가 계산을 돕기 위해 multiply와 subtract 도구를 선택합니다.
- multiply 도구에 각각 a와 b 값이 할당됩니다.
- subtract 도구에 각각 a와 b 값이 할당됩니다.
- 각 도구의 응답이 Tool Response에 제공됩니다.
- 모델의 최종 출력이 Model Response에 제공됩니다.
1. 추가 프롬프트를 제출하여 에이전트를 더 테스트합니다. User prompt 필드에서 기존 프롬프트를 클릭하여 수정할 수 있습니다.
1. 테스트가 끝나면 터미널에서 CTRL/CMD+C를 입력하여 서버를 종료할 수 있습니다.
과제
server.py 파일에 추가 도구 항목(예: 숫자의 제곱근 반환)을 추가해보세요. 에이전트가 새 도구(또는 기존 도구)를 활용해야 하는 추가 프롬프트를 제출하세요. 새로 추가된 도구를 로드하려면 서버를 재시작해야 합니다.
솔루션
주요 내용
이 장의 주요 내용은 다음과 같습니다:
추가 자료
다음 단계
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다하고 있지만, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다.
원본 문서의 원어를 신뢰할 수 있는 권위 있는 출처로 간주해야 합니다.
중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.
이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
테스트 및 디버깅
MCP 서버 테스트를 시작하기 전에, 사용 가능한 도구와 디버깅을 위한 모범 사례를 이해하는 것이 중요합니다. 효과적인 테스트는 서버가 예상대로 작동하는지 확인하고 문제를 신속하게 식별하고 해결하는 데 도움을 줍니다. 다음 섹션에서는 MCP 구현을 검증하기 위한 권장 접근 방식을 설명합니다.
개요
이 수업에서는 올바른 테스트 접근 방식을 선택하는 방법과 가장 효과적인 테스트 도구를 다룹니다.
학습 목표
이 수업을 마치면 다음을 수행할 수 있습니다:
MCP 서버 테스트
MCP는 서버 테스트 및 디버깅을 돕는 도구를 제공합니다:
MCP Inspector 사용하기
이 도구의 사용법은 이전 수업에서 설명했지만, 여기서는 간략히 말씀드리겠습니다.
이 도구는 Node.js로 만들어졌으며 npx 실행 파일을 호출하여 사용할 수 있습니다. npx는 도구를 일시적으로 다운로드 및 설치하고, 요청 실행이 완료되면 자동으로 정리합니다.
도구를 실행하는 일반적인 방법은 다음과 같습니다:
npx @modelcontextprotocol/inspector node build/index.js
위 명령어는 MCP 및 시각적 인터페이스를 시작하고 브라우저에서 로컬 웹 인터페이스를 실행합니다. 등록된 MCP 서버, 사용 가능한 도구, 리소스 및 프롬프트가 표시되는 대시보드가 나타납니다. 이 인터페이스를 통해 도구 실행을 대화식으로 테스트하고, 서버 메타데이터를 검사하며, 실시간 응답을 볼 수 있으므로 MCP 서버 구현을 검증하고 디버깅하기가 쉽습니다.
다음은 화면 예시입니다: !Inspector
또한 --cli 속성을 추가하여 CLI 모드로도 이 도구를 실행할 수 있습니다. 다음은 서버의 모든 도구를 나열하는 "CLI" 모드에서 도구를 실행하는 예입니다:
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
수동 테스트
서버 기능 테스트를 위해 Inspector 도구를 실행하는 것 이외에도, curl과 같이 HTTP를 사용할 수 있는 클라이언트를 실행하는 유사한 방법이 있습니다.
curl을 사용하면 MCP 서버를 HTTP 요청으로 직접 테스트할 수 있습니다:
# 예: 테스트 서버 메타데이터
curl http://localhost:3000/v1/metadata
# 예: 도구 실행
curl -X POST http://localhost:3000/v1/tools/execute \
-H "Content-Type: application/json" \
-d '{"name": "calculator", "parameters": {"expression": "2+2"}}'
위 curl 사용 예에서 보듯이, 도구 이름과 매개변수로 구성된 페이로드를 사용하여 POST 요청으로 도구를 호출합니다. 자신에게 가장 적합한 방법을 사용하세요. 일반적으로 CLI 도구는 사용 속도가 빠르고 스크립트 작성에 적합하여 CI/CD 환경에서 유용할 수 있습니다.
단위 테스트
도구와 리소스가 기대한 대로 작동하는지 확인하기 위해 단위 테스트를 작성하세요. 다음은 일부 테스트 예제 코드입니다.
import pytest
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
# 비동기 테스트를 위해 전체 모듈 표시
pytestmark = pytest.mark.anyio
async def test_list_tools_cursor_parameter():
"""Test that the cursor parameter is accepted for list_tools.
Note: FastMCP doesn't currently implement pagination, so this test
only verifies that the cursor parameter is accepted by the client.
"""
server = FastMCP("test")
# 몇 가지 테스트 도구 생성
@server.tool(name="test_tool_1")
async def test_tool_1() -> str:
"""First test tool"""
return "Result 1"
@server.tool(name="test_tool_2")
async def test_tool_2() -> str:
"""Second test tool"""
return "Result 2"
async with create_session(server._mcp_server) as client_session:
# 커서 매개변수 없이 테스트 (생략됨)
result1 = await client_session.list_tools()
assert len(result1.tools) == 2
# cursor=None으로 테스트
result2 = await client_session.list_tools(cursor=None)
assert len(result2.tools) == 2
# 문자열로서의 커서로 테스트
result3 = await client_session.list_tools(cursor="some_cursor_value")
assert len(result3.tools) == 2
# 빈 문자열 커서로 테스트
result4 = await client_session.list_tools(cursor="")
assert len(result4.tools) == 2
위 코드는 다음과 같은 작업을 수행합니다:
assert 문을 사용합니다.위 파일을 참고하여 자신의 서버가 예상대로 기능을 생성하는지 테스트할 수 있습니다.
모든 주요 SDK는 유사한 테스트 섹션을 가지고 있으므로 선택한 런타임에 맞게 조정할 수 있습니다.
샘플
추가 자료
다음 단계
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하였으나, 자동 번역은 오류나 부정확성이 있을 수 있음을 알려드립니다.
원본 문서의 원어가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
이 번역 사용으로 인해 발생하는 모든 오해나 오역에 대해 당사는 책임지지 않습니다.
MCP 서버 배포
MCP 서버를 배포하면 로컬 환경을 넘어서 다른 사람들이 해당 도구와 리소스에 접근할 수 있습니다. 확장성, 신뢰성, 관리 용이성에 대한 요구 사항에 따라 고려할 수 있는 여러 배포 전략이 있습니다. 아래에는 MCP 서버를 로컬, 컨테이너, 클라우드에 배포하는 방법에 대한 안내가 나와 있습니다.
개요
이 레슨에서는 MCP 서버 앱을 배포하는 방법을 다룹니다.
학습 목표
이 레슨을 마치면 다음을 할 수 있습니다:
로컬 개발 및 배포
서버가 사용자 머신에서 실행되어 소비되는 경우 다음 단계를 따르십시오:
1. 서버 다운로드. 서버를 직접 작성하지 않았다면 먼저 서버를 머신에 다운로드하십시오.
1. 서버 프로세스 시작: MCP 서버 애플리케이션을 실행합니다.
SSE의 경우 (stdio 유형 서버에는 필요하지 않음)
1. 네트워킹 구성: 서버가 예상된 포트에서 접근 가능하도록 설정합니다.
1. 클라이언트 연결: http://localhost:3000 같은 로컬 연결 URL을 사용합니다.
클라우드 배포
MCP 서버는 여러 클라우드 플랫폼에 배포할 수 있습니다:
예시: Azure Container Apps
Azure Container Apps는 MCP 서버 배포를 지원합니다. 아직 개발 중이며 현재는 SSE 서버를 지원합니다.
방법은 다음과 같습니다:
1. 리포지토리를 클론합니다:
```sh
git clone https://github.com/anthonychu/azure-container-apps-mcp-sample.git
```
1. 로컬에서 실행하여 테스트합니다:
```sh
uv venv
uv sync
# 리눅스/macOS
export API_KEYS=
# 윈도우
set API_KEYS=
uv run fastapi dev main.py
```
1. 로컬에서 시도하려면 *.vscode* 디렉터리에 *mcp.json* 파일을 생성하고 다음 내용을 추가합니다:
```json
{
"inputs": [
{
"type": "promptString",
"id": "weather-api-key",
"description": "Weather API Key",
"password": true
}
],
"servers": {
"weather-sse": {
"type": "sse",
"url": "http://localhost:8000/sse",
"headers": {
"x-api-key": "${input:weather-api-key}"
}
}
}
}
```
SSE 서버가 시작되면 JSON 파일의 재생 아이콘을 클릭할 수 있습니다. 이제 GitHub Copilot에서 서버의 도구를 인식하는 것을 확인할 수 있으며, 도구 아이콘을 참조하세요.
1. 배포하려면 다음 명령을 실행합니다:
```sh
az containerapp up -g
```
이렇게 하면 로컬에 배포하고, 이 단계들을 통해 Azure에 배포할 수 있습니다.
추가 자료
다음 단계
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다했지만, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 알려드립니다.
원본 문서는 해당 언어로 작성된 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
이 번역 사용으로 인해 발생하는 모든 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
고급 서버 사용법
MCP SDK에는 두 가지 유형의 서버가 있습니다. 일반 서버와 저수준 서버(low-level server)입니다. 보통은 일반 서버를 사용하여 기능을 추가합니다. 하지만 때때로 저수준 서버를 사용해야 할 때가 있는데, 예를 들면 다음과 같은 경우입니다:
일반 서버 vs 저수준 서버
아래는 일반 서버로 MCP 서버를 생성하는 예시입니다.
Python
mcp = FastMCP("Demo")
# 추가 도구를 추가하세요
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
TypeScript
const server = new McpServer({
name: "demo-server",
version: "1.0.0"
});
// 추가 도구를 추가하세요
server.registerTool("add",
{
title: "Addition Tool",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() }
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
요점은 서버가 가지길 원하는 각 도구, 리소스 또는 프롬프트를 명시적으로 추가한다는 것입니다. 이는 잘못된 방법이 아닙니다.
저수준 서버 접근법
하지만 저수준 서버 접근법을 사용할 때는 생각을 다르게 해야 합니다. 각 도구를 등록하는 대신, 기능 유형별로(도구, 리소스 또는 프롬프트) 두 개의 핸들러를 만듭니다. 예를 들어 도구의 경우 두 개의 함수만 존재합니다:
훨씬 적은 작업처럼 들리죠? 도구 등록 대신 모두 도구 목록에 포함시키고 도구 호출 요청이 들어올 때 호출되도록 하면 됩니다.
이제 코드가 어떻게 보이는지 살펴봅시다.
Python
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""List available tools."""
return [
types.Tool(
name="add",
description="Add two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "number to add"},
"b": {"type": "number", "description": "number to add"}
},
"required": ["query"],
},
)
]
TypeScript
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// 등록된 도구 목록을 반환합니다
return {
tools: [{
name: "add",
description: "Add two numbers",
inputSchema: {
"type": "object",
"properties": {
"a": {"type": "number", "description": "number to add"},
"b": {"type": "number", "description": "number to add"}
},
"required": ["query"],
}
}]
};
});
여기서는 기능 목록을 반환하는 함수가 있습니다.
도구 목록의 각 항목은 name, description, inputSchema와 같은 필드를 포함하여 반환 타입에 맞춥니다.
이를 통해 도구와 기능 정의를 다른 곳에 둘 수 있습니다.
모든 도구를 tools 폴더에 만들고 다른 기능들도 각자의 폴더에 모아 프로젝트를 다음과 같이 구성할 수 있습니다:
app
--| tools
----| add
----| substract
--| resources
----| products
----| schemas
--| prompts
----| product-description
아주 좋군요. 깔끔한 아키텍처를 만들 수 있습니다.
도구 호출은 어떤가요? 역시 하나의 핸들러가 어떤 도구든 호출하는 방식인가요? 맞습니다. 호출 코드는 다음과 같습니다.
Python
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
# tools는 도구 이름을 키로 갖는 사전입니다
if name not in tools.tools:
raise ValueError(f"Unknown tool: {name}")
tool = tools.tools[name]
result = "default"
try:
result = await tool["handler"](../../../../03-GettingStarted/10-advanced/arguments)
except Exception as e:
raise ValueError(f"Error calling tool {name}: {str(e)}")
return [
types.TextContent(type="text", text=str(result))
]
TypeScript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let tool = tools.find(t => t.name === name);
if(!tool) {
return {
error: {
code: "tool_not_found",
message: `Tool ${name} not found.`
}
};
}
// args: request.params.arguments
// TODO 도구를 호출하세요,
return {
content: [{ type: "text", text: `Tool ${name} called with arguments: ${JSON.stringify(input)}, result: ${JSON.stringify(result)}` }]
};
});
위 코드를 보면 호출할 도구와 인자를 파싱한 다음 도구를 호출하는 것을 알 수 있습니다.
검증을 통한 접근법 개선
지금까지는 각 기능 유형별로 두 개의 핸들러로 도구, 리소스, 프롬프트 등록을 대체하는 방법을 보았습니다. 그 외 무엇을 해야 하나요? 도구가 올바른 인자로 호출되는지 검증하는 절차가 필요합니다. 각 런타임마다 해결책이 다릅니다. 예를 들어 Python은 Pydantic을, TypeScript는 Zod를 사용합니다. 아이디어는 다음과 같습니다:
기능 만들기
기능을 만들려면 해당 기능 파일을 생성하고 필수 필드를 포함했는지 확인해야 합니다. 이 필드는 도구, 리소스, 프롬프트마다 조금 다릅니다.
Python
# schema.py
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
# add.py
from .schema import AddInputModel
async def add_handler(args) -> float:
try:
# Pydantic 모델을 사용하여 입력을 검증합니다
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
# TODO: Pydantic을 추가하여 AddInputModel을 만들고 인수를 검증할 수 있게 합니다
"""Handler function for the add tool."""
return float(input_model.a) + float(input_model.b)
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
여기서는 다음을 수행합니다.
AddInputModel 스키마를 만들고, *schema.py* 파일에 필드 a와 b를 정의합니다.AddInputModel 타입으로 파싱 시도합니다. 파라미터가 맞지 않으면 오류가 발생합니다:```python
# add.py
try:
# Pydantic 모델을 사용하여 입력 유효성 검사
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
```
이 파싱 로직을 도구 호출 자체에 두거나 핸들러 함수에 둘지 선택할 수 있습니다.
TypeScript
// server.ts
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let tool = tools.find(t => t.name === name);
if (!tool) {
return {
error: {
code: "tool_not_found",
message: `Tool ${name} not found.`
}
};
}
const Schema = tool.rawSchema;
try {
const input = Schema.parse(request.params.arguments);
// @ts-ignore
const result = await tool.callback(input);
return {
content: [{ type: "text", text: `Tool ${name} called with arguments: ${JSON.stringify(input)}, result: ${JSON.stringify(result)}` }]
};
} catch (error) {
return {
error: {
code: "invalid_arguments",
message: `Invalid arguments for tool ${name}: ${error instanceof Error ? error.message : String(error)}`
}
};
}
});
// schema.ts
import { z } from 'zod';
export const MathInputSchema = z.object({ a: z.number(), b: z.number() });
// add.ts
import { Tool } from "./tool.js";
import { MathInputSchema } from "./schema.js";
import { zodToJsonSchema } from "zod-to-json-schema";
export default {
name: "add",
rawSchema: MathInputSchema,
inputSchema: zodToJsonSchema(MathInputSchema),
callback: async ({ a, b }) => {
return {
content: [{ type: "text", text: String(a + b) }]
};
}
} as Tool;
```typescript
const Schema = tool.rawSchema;
try {
const input = Schema.parse(request.params.arguments);
```
성공하면 실제 도구를 호출합니다:
```typescript
const result = await tool.callback(input);
```
이 방식은 훌륭한 아키텍처를 만듭니다. *server.ts* 파일은 요청 핸들러 연결만 하는 아주 작은 파일이고, 각 기능은 각각의 폴더(도구는 tools/, 리소스는 resources/, 프롬프트는 prompts/)에 있습니다.
좋습니다. 다음으로 이를 직접 만들어 봅시다.
연습: 저수준 서버 만들기
이번 연습에서는 다음을 할 것입니다:
1. 도구 나열과 호출을 처리하는 저수준 서버 생성.
2. 확장이 용이한 아키텍처 구현.
3. 도구 호출이 적절히 검증되도록 검증 추가.
-1- 아키텍처 생성
먼저, 기능이 늘어날 때 확장 가능한 아키텍처를 만듭니다. 다음과 같습니다:
Python
server.py
--| tools
----| __init__.py
----| add.py
----| schema.py
client.py
TypeScript
server.ts
--| tools
----| add.ts
----| schema.ts
client.ts
이제 tools 폴더에 도구를 쉽게 추가할 수 있는 아키텍처가 마련되었습니다. 리소스와 프롬프트도 마찬가지로 하위 디렉터리를 추가해도 좋습니다.
-2- 도구 만들기
이제 도구 생성 방법을 보겠습니다. 먼저 도구는 tools 하위 디렉터리에 생성해야 합니다.
Python
from .schema import AddInputModel
async def add_handler(args) -> float:
try:
# Pydantic 모델을 사용하여 입력값을 검증합니다
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
# TODO: Pydantic을 추가하여 AddInputModel을 만들고 args를 검증할 수 있도록 합니다
"""Handler function for the add tool."""
return float(input_model.a) + float(input_model.b)
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
여기서는 이름, 설명, 입력 스키마를 Pydantic으로 정의하고, 도구 호출 시 실행되는 핸들러를 정의합니다. 마지막으로 tool_add 사전에 모든 속성을 담아 노출합니다.
또한 입력 스키마를 정의하는 schema.py가 있습니다:
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
tools 디렉터리를 모듈로 취급하도록 __init__.py도 작성해야 합니다. 모듈 내 노출도 다음과 같이 합니다:
from .add import tool_add
tools = {
tool_add["name"] : tool_add
}
더 많은 도구를 추가하면서 이 파일에도 계속 추가할 수 있습니다.
TypeScript
import { Tool } from "./tool.js";
import { MathInputSchema } from "./schema.js";
import { zodToJsonSchema } from "zod-to-json-schema";
export default {
name: "add",
rawSchema: MathInputSchema,
inputSchema: zodToJsonSchema(MathInputSchema),
callback: async ({ a, b }) => {
return {
content: [{ type: "text", text: String(a + b) }]
};
}
} as Tool;
여기서는 다음 속성으로 이루어진 사전을 생성합니다:
또한 mcp 서버 핸들러가 받을 수 있는 타입으로 변환하는 Tool 타입이 있습니다:
import { z } from 'zod';
export interface Tool {
name: string;
inputSchema: any;
rawSchema: z.ZodTypeAny;
callback: (args: z.infer<z.ZodTypeAny>) => Promise<{ content: { type: string; text: string }[] }>;
}
그리고 각 도구의 입력 스키마를 저장하는 schema.ts가 있으며 현재는 하나의 스키마만 있지만 도구가 늘면 항목을 추가할 수 있습니다:
import { z } from 'zod';
export const MathInputSchema = z.object({ a: z.number(), b: z.number() });
좋습니다. 이제 도구 나열 처리를 이어서 구현해 봅시다.
-3- 도구 나열 처리
도구 나열을 처리하려면 요청 핸들러를 설정해야 합니다. 서버 파일에 추가할 내용은 다음과 같습니다:
Python
# 간결함을 위해 코드 생략
from tools import tools
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
tool_list = []
print(tools)
for tool in tools.values():
tool_list.append(
types.Tool(
name=tool["name"],
description=tool["description"],
inputSchema=pydantic_to_json(tool["input_schema"]),
)
)
return tool_list
데코레이터 @server.list_tools와 구현 함수 handle_list_tools를 추가했습니다.
이 함수는 도구 목록을 반환하며, 각 도구는 name, description, inputSchema가 있어야 합니다.
TypeScript
도구 나열 요청 핸들러를 설정하려면 setRequestHandler를 호출하고, 처리할 작업에 맞는 스키마(ListToolsRequestSchema)를 전달합니다.
// index.ts
import addTool from "./add.js";
import subtractTool from "./subtract.js";
import {server} from "../server.js";
import { Tool } from "./tool.js";
export let tools: Array<Tool> = [];
tools.push(addTool);
tools.push(subtractTool);
// server.ts
// 간결함을 위해 코드를 생략함
import { tools } from './tools/index.js';
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// 등록된 도구 목록을 반환합니다
return {
tools: tools
};
});
잘했습니다. 도구 나열 부분을 해결했으니 이제 도구 호출 방법을 살펴봅시다.
-4- 도구 호출 처리
도구를 호출하려면, 이번에는 호출할 기능과 인자를 명시하는 요청을 다룰 또 다른 요청 핸들러를 설정해야 합니다.
Python
데코레이터 @server.call_tool을 사용하고 handle_call_tool 함수로 구현해봅시다.
이 함수 내에서 도구 이름과 인자를 파싱하고 인자가 적합한지 검증해야 합니다.
인자 검증은 이 함수나 실제 도구 내에서 할 수 있습니다.
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
# tools는 도구 이름을 키로 가지는 사전입니다
if name not in tools.tools:
raise ValueError(f"Unknown tool: {name}")
tool = tools.tools[name]
result = "default"
try:
# 도구를 호출합니다
result = await tool["handler"](../../../../03-GettingStarted/10-advanced/arguments)
except Exception as e:
raise ValueError(f"Error calling tool {name}: {str(e)}")
return [
types.TextContent(type="text", text=str(result))
]
설명:
name으로 이미 제공됩니다. 인자는 arguments 사전 형태입니다.result = await tool"handler"로 이루어집니다. 인자 검증은 이 handler 함수 내에서 수행되고, 실패하면 예외가 발생합니다.이제 저수준 서버를 이용해 도구 나열과 호출을 완전히 이해했습니다.
과제
배포된 코드를 확장하여 여러 도구, 리소스, 프롬프트를 추가하고 tools 디렉터리에 파일만 추가하면 된다는 점을 체감해보세요.
*해답 제공하지 않음*
요약
이번 장에서는 저수준 서버 접근법이 어떻게 작동하는지, 좋은 아키텍처를 계속 구축하는데 어떻게 도움이 되는지 보았습니다. 또한 검증이 어떻게 이루어지는지, 검증 라이브러리를 사용해 입력 검증 스키마를 만드는 방법도 배웠습니다.
다음 내용
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있지만, 자동 번역에는 오류나 부정확성이 있을 수 있음을 유의하시기 바랍니다.
원본 문서는 해당 언어의 공식 문서로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
이 번역 사용으로 인해 발생하는 오해나 오해에 대해 당사는 책임을 지지 않습니다.
Simple auth
MCP SDKs는 OAuth 2.1 사용을 지원합니다.
솔직히 말하면 인증 서버, 리소스 서버, 자격 증명 게시, 코드 수신, 코드를 베어러 토큰으로 교환하여 최종적으로 리소스 데이터를 얻는 등의 개념이 포함된 꽤 복잡한 프로세스입니다.
OAuth는 구현하기에 훌륭한 방식이지만 익숙하지 않은 경우, 기본적인 인증 수준부터 시작하여 더 나은 보안으로 점차 구축하는 것이 좋습니다.
이 장이 존재하는 이유가 바로 더 발전된 인증으로 넘어갈 수 있도록 도와주기 위함입니다.
인증, 우리가 의미하는 바는?
인증은 authentication과 authorization의 줄임말입니다. 즉, 우리는 두 가지 작업을 해야 합니다:
자격 증명: 시스템에 우리가 누구인지 알리는 방법
대부분의 웹 개발자들은 서버에 자격 증명을 제공하는 방식을 먼저 생각합니다. 일반적으로는 "여기 들어올 권한이 있다"는 비밀이죠. 이 자격 증명은 보통 사용자 이름과 비밀번호를 base64로 인코딩한 버전이나, 특정 사용자를 고유하게 식별하는 API 키입니다.
이 자격 증명은 보통 다음과 같이 "Authorization"이라는 헤더를 통해 전송됩니다:
{ "Authorization": "secret123" }
이를 기본 인증(basic authentication)이라고 부릅니다. 전체 흐름은 다음과 같습니다:
sequenceDiagram
participant User
participant Client
participant Server
User->>Client: 데이터 보여줘
Client->>Server: 데이터 보여줘, 여기 내 자격 증명 있어
Server-->>Client: 1a, 널 알아, 여기 네 데이터 있어
Server-->>Client: 1b, 널 몰라, 401
흐름을 이해했으니, 어떻게 구현할까요? 대부분의 웹 서버는 미들웨어(middleware) 개념이 있습니다. 요청의 일부로 실행되는 코드로, 자격 증명을 검증하고 인증이 성공하면 요청을 통과시킵니다. 유효하지 않은 자격 증명이면 인증 오류가 발생합니다. 구현 방법은 다음과 같습니다:
Python
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
has_header = request.headers.get("Authorization")
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
print("Valid token, proceeding...")
response = await call_next(request)
# 고객 헤더를 추가하거나 응답을 어떤 식으로든 변경하십시오
return response
starlette_app.add_middleware(CustomHeaderMiddleware)
여기서는:
AuthMiddleware라는 미들웨어를 만들었으며, 웹 서버가 dispatch 메서드를 호출합니다.```python
starlette_app.add_middleware(AuthMiddleware)
```
```python
has_header = request.headers.get("Authorization")
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
```
비밀이 존재하고 유효하다면 call_next를 호출하여 요청을 통과시키고 응답을 반환합니다.
```python
response = await call_next(request)
# 고객 헤더를 추가하거나 응답을 어떤 방식으로든 변경하십시오
return response
```
동작 방식은 웹 요청이 서버로 오면 미들웨어가 호출되고, 구현에 따라 요청을 통과시키거나 클라이언트가 진행할 수 없음을 나타내는 오류를 반환합니다.
TypeScript
여기서는 인기 프레임워크 Express를 사용해 미들웨어를 만들고 MCP 서버에 도달하기 전에 요청을 가로챕니다. 코드는 다음과 같습니다:
function isValid(secret) {
return secret === "secret123";
}
app.use((req, res, next) => {
// 1. 권한 부여 헤더가 존재합니까?
if(!req.headers["Authorization"]) {
res.status(401).send('Unauthorized');
}
let token = req.headers["Authorization"];
// 2. 유효성 검사.
if(!isValid(token)) {
res.status(403).send('Forbidden');
}
console.log('Middleware executed');
// 3. 요청을 요청 파이프라인의 다음 단계로 전달합니다.
next();
});
이 코드에서는:
1. 처음에 Authorization 헤더가 있는지 확인합니다. 없으면 401 오류를 보냅니다.
2. 자격 증명/토큰이 유효한지 검증합니다. 유효하지 않으면 403 오류를 보냅니다.
3. 마지막으로 요청 파이프라인에 요청을 전달하여 요청한 리소스를 반환합니다.
연습: 인증 구현하기
지식을 적용해 구현해 봅시다. 계획은 다음과 같습니다:
서버
클라이언트
-1- 웹 서버와 MCP 인스턴스 생성
첫 단계에서 웹 서버 인스턴스와 MCP 서버를 생성해야 합니다.
Python
MCP 서버 인스턴스를 생성하고 starlette 웹 앱을 만들며 uvicorn으로 호스팅합니다.
# MCP 서버 생성
app = FastMCP(
name="MCP Resource Server",
instructions="Resource Server that validates tokens via Authorization Server introspection",
host=settings["host"],
port=settings["port"],
debug=True
)
# starlette 웹 앱 생성
starlette_app = app.streamable_http_app()
# uvicorn을 통해 앱 제공
async def run(starlette_app):
import uvicorn
config = uvicorn.Config(
starlette_app,
host=app.settings.host,
port=app.settings.port,
log_level=app.settings.log_level.lower(),
)
server = uvicorn.Server(config)
await server.serve()
run(starlette_app)
이 코드는:
app.streamable_http_app()을 생성하며,server.serve())합니다.TypeScript
여기서는 MCP 서버 인스턴스를 생성합니다.
const server = new McpServer({
name: "example-server",
version: "1.0.0"
});
// ... 서버 리소스, 도구 및 프롬프트 설정 ...
이 MCP 서버 생성은 POST /mcp 경로 정의 내에서 이루어져야 하므로 위 코드를 다음과 같이 이동합니다:
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
const app = express();
app.use(express.json());
// 세션 ID별 전송 수단을 저장하는 맵
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
// 클라이언트에서 서버로의 통신을 위한 POST 요청 처리
app.post('/mcp', async (req, res) => {
// 기존 세션 ID 확인
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// 기존 전송 수단 재사용
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// 새로운 초기화 요청
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
// 세션 ID별 전송 수단 저장
transports[sessionId] = transport;
},
// DNS 리바인딩 보호는 이전 버전과의 호환성을 위해 기본적으로 비활성화되어 있습니다. 서버를
// 로컬에서 실행하는 경우 다음을 설정해야 합니다:
// enableDnsRebindingProtection: true,
// allowedHosts: ['127.0.0.1'],
});
// 닫힐 때 전송 수단 정리
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
const server = new McpServer({
name: "example-server",
version: "1.0.0"
});
// ... 서버 자원, 도구 및 프롬프트 설정 ...
// MCP 서버에 연결
await server.connect(transport);
} else {
// 잘못된 요청
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// 요청 처리
await transport.handleRequest(req, res, req.body);
});
// GET 및 DELETE 요청을 위한 재사용 가능 핸들러
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
// SSE를 통한 서버에서 클라이언트로 알림을 위한 GET 요청 처리
app.get('/mcp', handleSessionRequest);
// 세션 종료를 위한 DELETE 요청 처리
app.delete('/mcp', handleSessionRequest);
app.listen(3000);
보시다시피 MCP 서버 생성이 app.post("/mcp") 내로 이동했습니다.
다음 단계인 미들웨어 작성으로 넘어가 자격 증명을 검증할 수 있도록 합니다.
-2- 서버용 미들웨어 구현
이제 미들웨어 부분입니다. Authorization 헤더에서 자격 증명을 확인하고 검증하는 미들웨어를 만듭니다. 자격 증명이 적절하면 요청은 필요한 작업(도구 목록 요청, 리소스 읽기 또는 MCP 기능 수행 등)을 진행합니다.
Python
미들웨어를 만들려면 BaseHTTPMiddleware에서 상속받은 클래스를 만들어야 합니다. 관심 있는 부분은:
request, 여기서 헤더 정보를 읽습니다.call_next, 클라이언트가 올바른 자격 증명을 가져왔을 때 호출하는 콜백입니다.먼저 Authorization 헤더가 없으면 처리하는 부분입니다:
has_header = request.headers.get("Authorization")
# 헤더가 없으면 401 오류로 실패하고, 그렇지 않으면 계속 진행합니다.
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
클라이언트 인증 실패로 401 Unauthorized 메시지를 보냅니다.
자격 증명이 제출됐다면 유효성 검사를 합니다:
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
위에서 403 Forbidden 메시지를 보내는 부분을 확인하세요. 모든 내용을 포함한 미들웨어 전체 코드는 다음과 같습니다:
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
has_header = request.headers.get("Authorization")
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
print("Valid token, proceeding...")
print(f"-> Received {request.method} {request.url}")
response = await call_next(request)
response.headers['Custom'] = 'Example'
return response
좋지만 valid_token 함수는 무엇일까요? 아래에 있습니다:
# 프로덕션에서는 사용하지 마세요 - 개선하세요 !!
def valid_token(token: str) -> bool:
# "Bearer " 접두사를 제거하세요
if token.startswith("Bearer "):
token = token[7:]
return token == "secret-token"
return False
물론 개선이 필요합니다.
중요: 이런 비밀 코드를 절대 하드코딩해서는 안 됩니다. 이상적으로는 비교할 값을 데이터 소스나 IDP(아이덴티티 서비스 제공자)에서 가져오거나, 심지어는 IDP가 검증을 직접 하도록 해야 합니다.
TypeScript
Express에서는 미들웨어 함수를 처리하는 use 메서드를 호출해야 합니다.
해야 할 일은:
Authorization 속성에 담긴 자격 증명을 확인하고,헤더가 없으면 요청 진행을 차단합니다:
if(!req.headers["authorization"]) {
res.status(401).send('Unauthorized');
return;
}
헤더가 없으면 401 오류가 발생합니다.
자격 증명이 유효하지 않으면 요청을 또 차단하며 다른 메시지를 보냅니다:
if(!isValid(token)) {
res.status(403).send('Forbidden');
return;
}
403 오류가 발생하는 것을 확인하세요.
전체 코드는 다음과 같습니다:
app.use((req, res, next) => {
console.log('Request received:', req.method, req.url, req.headers);
console.log('Headers:', req.headers["authorization"]);
if(!req.headers["authorization"]) {
res.status(401).send('Unauthorized');
return;
}
let token = req.headers["authorization"];
if(!isValid(token)) {
res.status(403).send('Forbidden');
return;
}
console.log('Middleware executed');
next();
});
클라이언트가 보내는 자격 증명을 확인하는 미들웨어를 웹 서버에 설정했습니다. 그렇다면 클라이언트는 어떻게 해야 할까요?
-3- 헤더를 통한 자격 증명과 함께 웹 요청 보내기
클라이언트가 자격 증명을 헤더에 포함해 보내도록 해야 합니다. MCP 클라이언트를 사용할 것이므로 이를 어떻게 하는지 살펴봅니다.
Python
클라이언트에서는 다음과 같이 자격 증명 헤더를 전달해야 합니다:
# 값을 하드코딩하지 말고 최소한 환경 변수나 더 안전한 저장소에 보관하세요
token = "secret-token"
async with streamablehttp_client(
url = f"http://localhost:{port}/mcp",
headers = {"Authorization": f"Bearer {token}"}
) as (
read_stream,
write_stream,
session_callback,
):
async with ClientSession(
read_stream,
write_stream
) as session:
await session.initialize()
# TODO, 클라이언트에서 수행할 작업 예: 도구 목록 표시, 도구 호출 등
headers 속성을 headers = {"Authorization": f"Bearer {token}"}처럼 채운 것을 확인하세요.
TypeScript
두 단계로 해결할 수 있습니다:
1. 구성 객체에 자격 증명을 채웁니다.
2. 구성 객체를 전송 계층에 넘깁니다.
// 여기와 같이 값을 하드코딩하지 마세요. 최소한 환경 변수로 두고 개발 모드에서는 dotenv와 같은 것을 사용하세요.
let token = "secret123"
// 클라이언트 전송 옵션 객체를 정의하세요
let options: StreamableHTTPClientTransportOptions = {
sessionId: sessionId,
requestInit: {
headers: {
"Authorization": "secret123"
}
}
};
// 옵션 객체를 전송에 전달하세요
async function main() {
const transport = new StreamableHTTPClientTransport(
new URL(serverUrl),
options
);
위 코드에서 options 객체를 만들고 헤더를 requestInit 속성 아래에 넣은 방법을 보실 수 있습니다.
중요: 여기서 어떻게 개선할 수 있을까요? 현재 구현에는 몇 가지 문제가 있습니다. 첫째, 최소한 HTTPS가 없다면 자격 증명을 이렇게 전송하는 것은 꽤 위험합니다. 그나마 HTTPS가 있어도 자격 증명이 도난당할 수 있으므로, 토큰 취소가 쉬운 시스템과 어디서 요청하는지(지리적 위치 등), 요청 빈도가 너무 잦은지(봇 행동 등) 추가 검사 기능이 필요합니다. 요약하면 다양한 보안 문제가 존재합니다.
그런데 아무나 인증 없이 API를 호출하지 못하도록 하는 아주 간단한 API에는 이 방법도 좋은 출발점입니다.
그래서 JSON Web Token(JWT, JOT 토큰이라고도 함) 같은 표준화된 형식을 사용해 보안을 강화해 봅시다.
JSON Web Tokens, JWT
매우 단순한 자격 증명에서 개선하려고 할 때 JWT를 채택하면 어떤 즉각적인 이점이 있을까요?
이 모든 이점을 바탕으로 구현을 한 단계 업그레이드해봅니다.
기본 인증을 JWT로 전환하기
고수준에서 바꿔야 할 사항은 다음과 같습니다:
-1- JWT 토큰 구성
우선 JWT 토큰은 다음 세 부분으로 구성됩니다:
헤더, 페이로드, 인코딩된 토큰을 구성해야 합니다.
Python
import jwt
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
import datetime
# JWT에 서명하는 데 사용되는 비밀 키
secret_key = 'your-secret-key'
header = {
"alg": "HS256",
"typ": "JWT"
}
# 사용자 정보와 해당 클레임 및 만료 시간
payload = {
"sub": "1234567890", # 주제 (사용자 ID)
"name": "User Userson", # 사용자 정의 클레임
"admin": True, # 사용자 정의 클레임
"iat": datetime.datetime.utcnow(),# 발행 시간
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1) # 만료 시간
}
# 인코딩하기
encoded_jwt = jwt.encode(payload, secret_key, algorithm="HS256", headers=header)
이 코드에서는:
TypeScript
JWT 토큰 구성을 도와줄 라이브러리들이 필요합니다.
필요한 라이브러리:
npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken
준비가 되었으니 헤더와 페이로드를 만들고 인코딩된 토큰을 생성합니다.
import jwt from 'jsonwebtoken';
const secretKey = 'your-secret-key'; // 프로덕션에서 환경 변수를 사용하세요
// 페이로드 정의
const payload = {
sub: '1234567890',
name: 'User usersson',
admin: true,
iat: Math.floor(Date.now() / 1000), // 발행 시간
exp: Math.floor(Date.now() / 1000) + 60 * 60 // 1시간 후 만료
};
// 헤더 정의 (선택 사항, jsonwebtoken이 기본값 설정)
const header = {
alg: 'HS256',
typ: 'JWT'
};
// 토큰 생성
const token = jwt.sign(payload, secretKey, {
algorithm: 'HS256',
header: header
});
console.log('JWT:', token);
이 토큰은:
HS256으로 서명됨
1시간 동안 유효
sub, name, admin, iat, exp 등의 클레임 포함
-2- 토큰 검증
토큰을 검증하는 기능도 필요합니다. 서버에서 클라이언트가 보내는 토큰이 실제로 유효한지 확인해야 합니다. 구조 검증부터 유효성 검사까지 다양한 체크를 해야 합니다. 사용자 존재 여부 및 권한 추가 확인도 권장됩니다.
토큰을 검증하려면 먼저 디코딩하여 읽을 수 있어야 합니다.
Python
# JWT를 디코딩하고 검증합니다
try:
decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
print("✅ Token is valid.")
print("Decoded claims:")
for key, value in decoded.items():
print(f" {key}: {value}")
except ExpiredSignatureError:
print("❌ Token has expired.")
except InvalidTokenError as e:
print(f"❌ Invalid token: {e}")
여기서는 jwt.decode를 호출하는데, 토큰, 비밀 키, 알고리즘을 인자로 사용합니다. 실패하면 예외가 발생하므로 try-catch 문으로 감쌉니다.
TypeScript
jwt.verify를 호출해 디코딩된 토큰을 얻고, 더 분석할 수 있습니다. 호출 실패 시 토큰 구조가 잘못되었거나 더 이상 유효하지 않음을 의미합니다.
try {
const decoded = jwt.verify(token, secretKey);
console.log('Decoded Payload:', decoded);
} catch (err) {
console.error('Token verification failed:', err);
}
참고: 앞서 말했듯, 이 토큰의 사용자가 시스템에 존재하는지, 그리고 권한이 적절한지도 추가 점검해야 합니다.
다음으로 역할 기반 접근 제어(RBAC)를 살펴봅시다.
역할 기반 접근 제어 추가하기
다양한 역할이 각각 다른 권한을 가진다는 것을 표현하고자 합니다. 예를 들어, 관리자는 모든 작업을 할 수 있고, 일반 사용자는 읽기/쓰기를 할 수 있으며, 손님은 읽기만 할 수 있다고 가정합니다. 따라서 가능한 권한 수준은 다음과 같습니다:
이제 미들웨어로 이러한 제어를 어떻게 구현할 수 있는지 살펴보겠습니다. 미들웨어는 개별 라우트별로 그리고 전체 라우트에 대해 추가할 수 있습니다.
Python
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
import jwt
# 비밀 정보를 코드에 두지 마세요, 이것은 시연 목적일 뿐입니다. 안전한 곳에서 읽으세요.
SECRET_KEY = "your-secret-key" # 이 값을 환경 변수에 넣으세요
REQUIRED_PERMISSION = "User.Read"
class JWTPermissionMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return JSONResponse({"error": "Missing or invalid Authorization header"}, status_code=401)
token = auth_header.split(" ")[1]
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return JSONResponse({"error": "Token expired"}, status_code=401)
except jwt.InvalidTokenError:
return JSONResponse({"error": "Invalid token"}, status_code=401)
permissions = decoded.get("permissions", [])
if REQUIRED_PERMISSION not in permissions:
return JSONResponse({"error": "Permission denied"}, status_code=403)
request.state.user = decoded
return await call_next(request)
아래와 같이 미들웨어를 추가하는 몇 가지 방법이 있습니다:
# 대안 1: 스타렛 앱을 구성하는 동안 미들웨어 추가
middleware = [
Middleware(JWTPermissionMiddleware)
]
app = Starlette(routes=routes, middleware=middleware)
# 대안 2: 스타렛 앱이 이미 구성된 후 미들웨어 추가
starlette_app.add_middleware(JWTPermissionMiddleware)
# 대안 3: 라우트별 미들웨어 추가
routes = [
Route(
"/mcp",
endpoint=..., # 핸들러
middleware=[Middleware(JWTPermissionMiddleware)]
)
]
TypeScript
app.use와 모든 요청에 대해 실행되는 미들웨어를 사용할 수 있습니다.
app.use((req, res, next) => {
console.log('Request received:', req.method, req.url, req.headers);
console.log('Headers:', req.headers["authorization"]);
// 1. 인증 헤더가 전송되었는지 확인하십시오
if(!req.headers["authorization"]) {
res.status(401).send('Unauthorized');
return;
}
let token = req.headers["authorization"];
// 2. 토큰이 유효한지 확인하십시오
if(!isValid(token)) {
res.status(403).send('Forbidden');
return;
}
// 3. 토큰 사용자가 우리 시스템에 존재하는지 확인하십시오
if(!isExistingUser(token)) {
res.status(403).send('Forbidden');
console.log("User does not exist");
return;
}
console.log("User exists");
// 4. 토큰이 올바른 권한을 가지고 있는지 검증하십시오
if(!hasScopes(token, ["User.Read"])){
res.status(403).send('Forbidden - insufficient scopes');
}
console.log("User has required scopes");
console.log('Middleware executed');
next();
});
미들웨어가 해야 하며 미들웨어가 할 수 있는 일들이 꽤 많습니다:
1. 권한 부여 헤더가 있는지 확인하기
2. 토큰이 유효한지 확인하기, 우리는 isValid라는 메서드를 호출하는데, 이 메서드는 JWT 토큰의 무결성과 유효성을 검사합니다.
3. 사용자가 시스템에 존재하는지 확인하기, 이것도 검사를 해야 합니다.
```typescript
// DB의 사용자
const users = [
"user1",
"User usersson",
]
function isExistingUser(token) {
let decodedToken = verifyToken(token);
// TODO, DB에 사용자가 있는지 확인하기
return users.includes(decodedToken?.name || "");
}
```
위에서 우리는 아주 단순한 users 리스트를 만들었는데, 실제로는 데이터베이스에 있어야 합니다.
4. 추가로, 토큰이 올바른 권한을 갖고 있는지도 확인해야 합니다.
```typescript
if(!hasScopes(token, ["User.Read"])){
res.status(403).send('Forbidden - insufficient scopes');
}
```
위 미들웨어 코드에서는 토큰에 User.Read 권한이 포함되어 있는지 검사하며, 없으면 403 오류를 전송합니다. 아래는 hasScopes 도우미 메서드입니다.
```typescript
function hasScopes(scope: string, requiredScopes: string[]) {
let decodedToken = verifyToken(scope);
return requiredScopes.every(scope => decodedToken?.scopes.includes(scope));
}
```
Have a think which additional checks you should be doing, but these are the absolute minimum of checks you should be doing.
Using Express as a web framework is a common choice. There are helpers library when you use JWT so you can write less code.
express-jwt, helper library that provides a middleware that helps decode your token.express-jwt-permissions, this provides a middleware guard that helps check if a certain permission is on the token.Here's what these libraries can look like when used:
const express = require('express');
const jwt = require('express-jwt');
const guard = require('express-jwt-permissions')();
const app = express();
const secretKey = 'your-secret-key'; // put this in env variable
// Decode JWT and attach to req.user
app.use(jwt({ secret: secretKey, algorithms: ['HS256'] }));
// Check for User.Read permission
app.use(guard.check('User.Read'));
// multiple permissions
// app.use(guard.check(['User.Read', 'Admin.Access']));
app.get('/protected', (req, res) => {
res.json({ message: `Welcome ${req.user.name}` });
});
// Error handler
app.use((err, req, res, next) => {
if (err.code === 'permission_denied') {
return res.status(403).send('Forbidden');
}
next(err);
});
이제 미들웨어가 인증과 권한 부여 모두에 어떻게 사용될 수 있는지 보았습니다. 그렇다면 MCP는 어떻게 할까요? MCP가 인증 방식을 바꾸나요? 다음 섹션에서 알아보겠습니다.
-3- MCP에 RBAC 추가하기
지금까지 미들웨어를 통해 RBAC를 추가하는 방법을 보았습니다. 하지만 MCP에 대해선 기능별 RBAC를 쉽게 추가할 방법이 없습니다. 그렇다면 어떻게 할까요? 이 경우에는 클라이언트가 특정 도구를 호출할 권한이 있는지 확인하는 코드를 추가해야 합니다.
기능별 RBAC를 달성하는 방법은 여러 가지가 있습니다:
python
```python
@tool()
def delete_product(id: int):
try:
check_permissions(role="Admin.Write", request)
catch:
pass # 클라이언트가 인증에 실패했습니다, 인증 오류를 발생시킵니다
```
typescript
```typescript
server.registerTool(
"delete-product",
{
title: Delete a product",
description: "Deletes a product",
inputSchema: { id: z.number() }
},
async ({ id }) => {
try {
checkPermissions("Admin.Write", request);
// 할 일, id를 productService 및 원격 엔트리로 전송
} catch(Exception e) {
console.log("Authorization error, you're not allowed");
}
return {
content: [{ type: "text", text: Deletected product with id ${id} }]
};
}
);
```
Python
```python
tool_permission = {
"create_product": ["User.Write", "Admin.Write"],
"delete_product": ["Admin.Write"]
}
def has_permission(user_permissions, required_permissions) -> bool:
# user_permissions: 사용자가 가진 권한 목록
# required_permissions: 도구에 필요한 권한 목록
return any(perm in user_permissions for perm in required_permissions)
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
# request.user.permissions는 사용자의 권한 목록이라고 가정합니다
user_permissions = request.user.permissions
required_permissions = tool_permission.get(name, [])
if not has_permission(user_permissions, required_permissions):
# "도구 {name}을(를 호출할 권한이 없습니다"라는 오류를 발생시킵니다
raise Exception(f"You don't have permission to call tool {name}")
# 계속 진행하고 도구를 호출합니다
# ...
```
TypeScript
```typescript
function hasPermission(userPermissions: string[], requiredPermissions: string[]): boolean {
if (!Array.isArray(userPermissions) || !Array.isArray(requiredPermissions)) return false;
// 사용자가 최소 하나의 필요한 권한을 가지고 있으면 true를 반환합니다
return requiredPermissions.some(perm => userPermissions.includes(perm));
}
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let permissions = request.user.permissions;
if (!hasPermission(permissions, toolPermissions[name])) {
return new Error(You don't have permission to call ${name});
}
// 계속 진행하세요..
});
```
참고로, 위 코드를 간단하게 하려면 미들웨어가 요청의 user 속성에 디코드된 토큰을 할당하도록 해야 합니다.
요약
이제 일반적으로 그리고 MCP에 대해 RBAC를 추가하는 방법을 논의했으니, 개념을 이해했는지 확인하기 위해 직접 보안을 구현해 볼 차례입니다.
과제 1: 기본 인증을 사용하여 mcp 서버와 mcp 클라이언트 구축하기
여기서는 헤더를 통해 자격 증명을 전송하는 방법을 배울 것입니다.
솔루션 1
과제 2: 과제 1의 솔루션을 JWT 사용으로 업그레이드하기
첫 번째 솔루션을 바탕으로 개선해 봅시다.
기본 인증 대신 JWT를 사용합니다.
솔루션 2
도전과제
"Add RBAC to MCP" 섹션에서 설명한 도구별 RBAC를 추가하세요.
요약
이번 장을 통해 보안이 전혀 없는 상태에서 기본 보안, JWT 그리고 MCP에 추가하는 방법까지 많은 것을 배우셨길 바랍니다.
우리는 맞춤형 JWT로 탄탄한 기초를 쌓았지만, 확장하면서 표준 기반의 아이덴티티 모델로 옮겨가고 있습니다. Entra나 Keycloak 같은 IdP를 도입하면 토큰 발급, 검증, 생명주기 관리를 신뢰받는 플랫폼에 위임할 수 있어, 앱 로직과 사용자 경험에 집중할 수 있게 됩니다.
이를 위해 더 고급 Entra 챕터가 준비되어 있습니다.
다음 단계
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나, 자동 번역은 오류나 부정확함을 포함할 수 있음을 유의하시기 바랍니다.
원문이 해당 언어로 된 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우, 전문 인간 번역을 권장합니다.
본 번역 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
인기 있는 MCP 호스트 클라이언트 설정
이 가이드는 인기 있는 AI 호스트 애플리케이션에서 MCP 서버를 구성하고 사용하는 방법을 다룹니다. 각 호스트는 자체 구성 방식을 가지고 있지만, 설정이 완료되면 모두 표준화된 프로토콜을 사용하여 MCP 서버와 통신합니다.
MCP 호스트란?
MCP 호스트는 MCP 서버에 연결하여 기능을 확장할 수 있는 AI 애플리케이션입니다. 사용자가 상호작용하는 "프론트 엔드"로 생각할 수 있으며, MCP 서버는 "백 엔드" 도구와 데이터를 제공합니다.
flowchart LR
User[👤 사용자] --> Host[🖥️ MCP 호스트]
Host --> S1[MCP 서버 A]
Host --> S2[MCP 서버 B]
Host --> S3[MCP 서버 C]
subgraph "인기 호스트"
H1[클로드 데스크톱]
H2[VS 코드]
H3[커서]
H4[클라인]
H5[윈드서프]
end
사전 준비 사항
---
1. Claude Desktop
Claude Desktop은 Anthropic의 공식 데스크톱 애플리케이션으로 MCP를 네이티브로 지원합니다.
설치
1. claude.ai/download에서 Claude Desktop 다운로드
2. 설치 후 Anthropic 계정으로 로그인
구성
Claude Desktop은 MCP 서버를 정의하기 위해 JSON 구성 파일을 사용합니다.
구성 파일 위치:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.json~/.config/Claude/claude_desktop_config.json구성 예시:
{
"mcpServers": {
"calculator": {
"command": "python",
"args": ["-m", "mcp_calculator_server"],
"env": {
"PYTHONPATH": "/path/to/your/server"
}
},
"weather": {
"command": "node",
"args": ["/path/to/weather-server/build/index.js"]
},
"database": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost/mydb"
}
}
}
}
구성 옵션
command"python", "node", "npx"args["-m", "my_server"]env{"API_KEY": "xxx"}cwd"/path/to/server"설정 테스트
1. 구성 파일 저장
2. Claude Desktop 완전히 재시작 (종료 후 다시 열기)
3. 새 대화 열기
4. 연결된 서버를 나타내는 🔌 아이콘 확인
5. Claude에게 도구 중 하나를 사용해 보라고 요청
Claude Desktop 문제 해결
서버가 나타나지 않는 경우:
서버가 시작 시 충돌하는 경우:
---
2. VS Code 및 GitHub Copilot
VS Code는 GitHub Copilot Chat 확장 기능을 통해 MCP를 지원합니다.
사전 준비
1. VS Code 1.99 이상 설치
2. GitHub Copilot 확장 설치
3. GitHub Copilot Chat 확장 설치
구성
VS Code는 작업 공간 또는 사용자 설정에 .vscode/mcp.json 파일을 사용합니다.
작업 공간 구성 (.vscode/mcp.json):
{
"servers": {
"my-calculator": {
"type": "stdio",
"command": "python",
"args": ["-m", "mcp_calculator_server"]
},
"my-database": {
"type": "sse",
"url": "http://localhost:8080/sse"
}
}
}
사용자 설정 (settings.json):
{
"mcp.servers": {
"global-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-memory"]
}
},
"mcp.enableLogging": true
}
VS Code에서 MCP 사용하기
1. Copilot Chat 패널 열기 (Ctrl+Shift+I / Cmd+Shift+I)
2. @ 입력하여 사용 가능한 MCP 도구 보기
3. 자연어로 도구 호출: "계산기 사용하여 25 * 48 계산하기"
VS Code 문제 해결
MCP 서버 로드 실패:
---
3. Cursor
Cursor는 MCP를 내장한 AI 중심 코드 에디터입니다.
설치
1. cursor.sh에서 Cursor 다운로드
2. 설치 후 로그인
구성
Cursor는 Claude Desktop과 비슷한 구성 형식을 사용합니다.
구성 파일 위치:
~/.cursor/mcp.json%USERPROFILE%\.cursor\mcp.json~/.cursor/mcp.json구성 예시:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here"
}
}
}
}
Cursor에서 MCP 사용하기
1. Cursor의 AI 채팅 열기 (Ctrl+L / Cmd+L)
2. MCP 도구가 제안에 자동으로 나타남
3. 연결된 서버를 사용해 AI에게 작업 요청
---
4. Cline (터미널 기반)
Cline은 터미널 기반 MCP 클라이언트로 명령줄 작업에 적합합니다.
설치
npm install -g @anthropic/cline
구성
Cline은 환경 변수와 명령행 인수를 사용합니다.
환경 변수 사용:
export ANTHROPIC_API_KEY="your-api-key"
export MCP_SERVER_CALCULATOR="python -m mcp_calculator_server"
명령행 인수 사용:
cline --mcp-server "calculator:python -m mcp_calculator_server" \
--mcp-server "weather:node /path/to/weather/index.js"
구성 파일 (~/.clinerc):
{
"apiKey": "your-api-key",
"mcpServers": {
"calculator": {
"command": "python",
"args": ["-m", "mcp_calculator_server"]
}
}
}
Cline 사용법
# 대화형 세션 시작
cline
# MCP로 단일 쿼리
cline "Calculate the square root of 144 using the calculator"
# 사용 가능한 도구 목록
cline --list-tools
---
5. Windsurf
Windsurf는 MCP를 지원하는 또 다른 AI 기반 코드 에디터입니다.
설치
1. codeium.com/windsurf에서 Windsurf 다운로드
2. 설치 후 계정 생성
구성
Windsurf 구성은 설정 UI를 통해 관리됩니다:
1. 설정 열기 (Ctrl+, / Cmd+,)
2. "MCP" 검색
3. "settings.json에서 편집" 클릭
구성 예시:
{
"windsurf.mcp.servers": {
"my-tools": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {}
}
},
"windsurf.mcp.enabled": true
}
---
전송 방식 비교
각 호스트는 다양한 전송 방식을 지원합니다:
stdio (표준 입력/출력): 호스트가 시작한 로컬 서버에 가장 적합
SSE/HTTP: 원격 서버나 여러 클라이언트에서 공유하는 서버에 가장 적합
---
공통 문제 해결
서버가 시작되지 않을 때
1. 서버를 수동으로 먼저 테스트:
```bash
# 파이썬용
python -m your_server_module
# Node.js용
node /path/to/server/index.js
```
2. 명령 경로 확인:
- 가능하면 절대 경로 사용
- 실행 파일이 PATH에 포함되어 있는지 확인
3. 종속 항목 확인:
```bash
# 파이썬
pip list | grep mcp
# Node.js
npm list @modelcontextprotocol/sdk
```
서버는 연결되지만 도구가 작동하지 않을 때
1. 서버 로그 확인 - 대부분의 호스트는 로깅 옵션 제공
2. 도구 등록 확인 - MCP Inspector로 테스트
3. 권한 확인 - 일부 도구는 파일/네트워크 접근 권한 필요
환경 변수가 전달되지 않을 때
env 구성 필드를 명시적으로 사용---
보안 모범 사례
1. API 키를 구성 파일에 절대 커밋하지 말 것
2. 민감한 데이터는 환경 변수로 관리
3. 서버 권한은 필요한 범위로 제한
4. 시스템 접근 권한 부여 전 서버 코드 검토
5. 파일 시스템 및 네트워크 접근에 허용 목록 사용
---
다음 단계
---
추가 자료
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나, 자동 번역은 오류나 부정확성이 포함될 수 있음을 유의해 주시기 바랍니다.
원본 문서의 원어 버전이 권위 있는 자료로 간주되어야 합니다.
중요한 정보의 경우 전문적인 사람에 의한 번역을 권장합니다.
본 번역 사용으로 인한 어떠한 오해나 잘못된 해석에 대해서도 당사는 책임을 지지 않습니다.
MCP Inspector로 디버깅하기
MCP Inspector는 전체 AI 호스트 애플리케이션 없이도 MCP 서버를 대화형으로 테스트하고 문제를 해결할 수 있는 필수 디버깅 도구입니다. "MCP를 위한 Postman"으로 생각할 수 있으며, 요청을 보내고, 응답을 보고, 서버 동작 방식을 이해할 수 있는 시각적 인터페이스를 제공합니다.
MCP Inspector를 사용하는 이유
MCP 서버를 구축할 때 자주 겪는 문제들:
전제 조건
설치
옵션 1: npx로 실행 (빠른 테스트 권장)
npx @modelcontextprotocol/inspector
옵션 2: 전역 설치
npm install -g @modelcontextprotocol/inspector
mcp-inspector
옵션 3: 프로젝트에 추가
cd your-mcp-server-project
npm install --save-dev @modelcontextprotocol/inspector
package.json에 추가:
{
"scripts": {
"inspector": "mcp-inspector"
}
}
---
서버에 연결하기
stdio 서버 (로컬 프로세스)
표준 입력/출력으로 통신하는 서버용:
# 파이썬 서버
npx @modelcontextprotocol/inspector python -m your_server_module
# Node.js 서버
npx @modelcontextprotocol/inspector node ./build/index.js
# 환경 변수 사용
OPENAI_API_KEY=xxx npx @modelcontextprotocol/inspector python server.py
SSE/HTTP 서버 (네트워크)
HTTP 서비스로 실행되는 서버용:
1. 먼저 서버를 시작하세요:
```bash
python server.py # 서버가 http://localhost:8080 에서 실행 중입니다
```
2. Inspector를 실행하고 연결하세요:
```bash
npx @modelcontextprotocol/inspector --sse http://localhost:8080/sse
```
---
Inspector 인터페이스 개요
Inspector 실행 시 보이는 웹 인터페이스(보통 http://localhost:5173):
┌─────────────────────────────────────────────────────────────┐
│ MCP Inspector [Connected ✅] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 🔧 Tools │ │ 📄 Resources│ │ 💬 Prompts │ │
│ │ (3) │ │ (2) │ │ (1) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 📋 Message Log │ │
│ │ ─────────────────────────────────────────────────── │ │
│ │ → initialize │ │
│ │ ← initialized (server info) │ │
│ │ → tools/list │ │
│ │ ← tools (3 tools) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
---
도구 테스트
사용 가능한 도구 목록 확인
1. Tools 탭 클릭
2. Inspector가 자동으로 tools/list 호출
3. 등록된 모든 도구가 다음과 함께 표시됨:
- 도구 이름
- 설명
- 입력 스키마(매개변수)
도구 호출하기
1. 목록에서 도구 선택
2. 폼에 필요한 매개변수 입력
3. Run Tool 클릭
4. 결과 패널에서 응답 확인
예: 계산기 도구 테스트
Tool: add
Parameters:
a: 25
b: 17
Response:
{
"content": [
{
"type": "text",
"text": "42"
}
]
}
도구 오류 디버깅
도구 실패 시 Inspector가 보여주는 내용:
Error Response:
{
"error": {
"code": -32602,
"message": "Invalid params: 'b' is required"
}
}
일반 오류 코드:
---
리소스 테스트
리소스 목록 확인
1. Resources 탭 클릭
2. Inspector가 resources/list 호출
3. 다음 항목을 볼 수 있음:
- 리소스 URI
- 이름 및 설명
- MIME 타입
리소스 읽기
1. 리소스 선택
2. Read Resource 클릭
3. 반환된 콘텐츠 확인
예시 출력:
Resource: file:///config/settings.json
Content-Type: application/json
{
"config": {
"debug": true,
"maxConnections": 10
}
}
---
프롬프트 테스트
프롬프트 목록 확인
1. Prompts 탭 클릭
2. Inspector가 prompts/list 호출
3. 사용 가능한 프롬프트 템플릿 확인
프롬프트 가져오기
1. 프롬프트 선택
2. 필요한 인수 입력
3. Get Prompt 클릭
4. 렌더링된 프롬프트 메시지 확인
---
메시지 로그 분석
메시지 로그는 모든 MCP 프로토콜 메시지를 보여줌:
14:32:01 → {"jsonrpc":"2.0","id":1,"method":"initialize",...}
14:32:01 ← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25",...}}
14:32:02 → {"jsonrpc":"2.0","id":2,"method":"tools/list"}
14:32:02 ← {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}
14:32:05 → {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add",...}}
14:32:05 ← {"jsonrpc":"2.0","id":3,"result":{"content":[...]}}
확인할 내용
→는 대응하는 ←가 있어야 함"error" 확인---
VS Code 통합
VS Code에서 Inspector를 직접 실행 가능:
launch.json 사용
.vscode/launch.json에 추가:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug with MCP Inspector",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": [
"@modelcontextprotocol/inspector",
"python",
"${workspaceFolder}/server.py"
],
"console": "integratedTerminal"
},
{
"name": "Debug SSE Server with Inspector",
"type": "chrome",
"request": "launch",
"url": "http://localhost:5173",
"preLaunchTask": "Start MCP Inspector"
}
]
}
Tasks 사용
.vscode/tasks.json에 추가:
{
"version": "2.0.0",
"tasks": [
{
"label": "Start MCP Inspector",
"type": "shell",
"command": "npx @modelcontextprotocol/inspector node ${workspaceFolder}/build/index.js",
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^$"
},
"background": {
"activeOnStart": true,
"beginsPattern": "Inspector",
"endsPattern": "listening"
}
}
}
]
}
---
일반적인 디버깅 시나리오
시나리오 1: 서버 연결 실패
증상: Inspector가 "Disconnected" 표시하거나 "Connecting..." 상태에서 멈춤
점검 목록:
1. ✅ 서버 명령어가 올바른가?
2. ✅ 모든 종속성이 설치되었나?
3. ✅ 서버 경로가 절대 경로인지 현재 디렉터리 기준 상대 경로인지 확인
4. ✅ 필수 환경 변수 설정 여부
디버그 단계:
# 서버를 먼저 수동으로 테스트하세요
python -c "import your_server_module; print('OK')"
# 가져오기 오류를 확인하세요
python -m your_server_module 2>&1 | head -20
# MCP SDK가 설치되었는지 확인하세요
pip show mcp
시나리오 2: 도구 목록이 나타나지 않음
증상: Tools 탭에 빈 목록 표시
가능 원인:
1. 서버 초기화 중 도구가 등록되지 않음
2. 서버 시작 후 크래시 발생
3. tools/list 핸들러가 빈 배열 반환
디버그 단계:
1. 메시지 로그에서 tools/list 응답 확인
2. 도구 등록 코드에 로깅 추가
3. Python의 경우 @mcp.tool() 데코레이터 존재 여부 확인
시나리오 3: 도구 오류 반환
증상: 도구 호출 시 오류 응답
디버그 방법:
1. 오류 메시지 꼼꼼히 읽기
2. 매개변수 타입이 스키마와 일치하는지 확인
3. 상세 오류 메시지를 위한 try/catch 추가
4. 서버 로그에서 스택 트레이스 확인
개선된 오류 처리 예:
@mcp.tool()
async def my_tool(param1: str, param2: int) -> str:
try:
# 여기 도구 로직
result = process(param1, param2)
return str(result)
except ValueError as e:
raise McpError(f"Invalid parameter: {e}")
except Exception as e:
raise McpError(f"Tool failed: {type(e).__name__}: {e}")
시나리오 4: 리소스 내용이 비어 있음
증상: 리소스는 반환되나 내용이 비었거나 null
점검 목록:
1. ✅ 파일 경로나 URI가 정확한가
2. ✅ 서버에 리소스 읽기 권한이 있는가
3. ✅ 리소스 내용이 올바르게 반환되는가
---
고급 Inspector 기능
사용자 정의 헤더 (SSE)
npx @modelcontextprotocol/inspector \
--sse http://localhost:8080/sse \
--header "Authorization: Bearer your-token"
자세한 로깅
DEBUG=mcp* npx @modelcontextprotocol/inspector python server.py
세션 기록
Inspector는 메시지 로그를 내보내서 나중에 분석할 수 있음:
1. 메시지 패널에서 Export Log 클릭
2. JSON 파일 저장
3. 팀과 공유하여 디버깅
---
모범 사례
1. 조기에 자주 테스트하세요 - 문제가 생겼을 때만이 아니라 개발 중에도 Inspector 사용
2. 단순하게 시작하세요 - 복잡한 도구 호출 전에 기본 연결부터 확인
3. 스키마 체크 - 많은 오류가 매개변수 타입 불일치에서 발생
4. 오류 메시지 읽기 - MCP 오류는 대체로 설명적임
5. Inspector를 열어두세요 - 개발 중 문제를 발견하는 데 도움됨
---
다음 단계
Module 3: Getting Started를 완료했습니다! 학습을 계속하세요:
---
추가 자료
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 양지해 주시기 바랍니다.
원본 문서의 원어 버전이 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우, 전문 인력에 의한 번역을 권장합니다.
본 번역 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
샘플링 - 클라이언트에 기능 위임하기
때때로 MCP 클라이언트와 MCP 서버가 공동의 목표를 달성하기 위해 협력해야 할 때가 있습니다. 서버가 클라이언트에 있는 LLM의 도움이 필요한 경우가 있을 수 있습니다. 이런 상황에서는 샘플링을 사용해야 합니다.
샘플링을 포함하는 몇 가지 사용 사례와 솔루션 구축 방법을 살펴보겠습니다.
개요
이번 강의에서는 샘플링을 언제, 어디서 사용해야 하는지와 샘플링 구성 방법에 대해 중점적으로 설명합니다.
학습 목표
이번 장에서는 다음을 다룹니다:
샘플링이란 무엇이며 왜 사용하는가?
샘플링은 다음과 같은 방식으로 작동하는 고급 기능입니다:
sequenceDiagram
participant User
participant MCP Client
participant LLM
participant MCP Server
User->>MCP Client: 블로그 게시글 작성
MCP Client->>MCP Server: 도구 호출 (블로그 게시글 초안)
MCP Server->>MCP Client: 샘플링 요청 (요약 생성)
MCP Client->>LLM: 블로그 게시글 요약 생성
LLM->>MCP Client: 요약 결과
MCP Client->>MCP Server: 샘플링 응답 (요약)
MCP Server->>MCP Client: 블로그 게시글 완료 (초안 + 요약)
MCP Client->>User: 블로그 게시글 준비 완료
샘플링 요청
좋습니다, 이제 믿을 만한 시나리오에 대한 큰 그림을 살펴봤으니 서버가 클라이언트로 보내는 샘플링 요청에 대해 이야기해봅시다. 다음은 JSON-RPC 형식의 요청 예시입니다:
{
"jsonrpc": "2.0",
"id": 1,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Create a blog post summary of the following blog post: <BLOG POST>"
}
}
],
"modelPreferences": {
"hints": [
{
"name": "claude-3-sonnet"
}
],
"intelligencePriority": 0.8,
"speedPriority": 0.5
},
"systemPrompt": "You are a helpful assistant.",
"maxTokens": 100
}
}
여기서 주목할 만한 점은 다음과 같습니다:
샘플링 응답
이 응답은 MCP 클라이언트가 LLM을 호출하여 응답을 기다린 후 서버에 다시 보내는 결과 메시지입니다. JSON-RPC 형식 예시는 다음과 같습니다:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"role": "assistant",
"content": {
"type": "text",
"text": "Here's your abstract <ABSTRACT>"
},
"model": "gpt-5",
"stopReason": "endTurn"
}
}
응답이 요청한 것처럼 블로그 게시물의 요약임을 확인하세요. 또한 사용된 model이 요청한 것과 다르게 "claude-3-sonnet" 대신 "gpt-5"인 점에 주의하세요. 이는 사용자가 어떤 모델을 사용할지 마음을 바꿀 수 있음을 보여주며, 샘플링 요청은 하나의 권고 사항임을 뜻합니다.
이제 주요 흐름과 "블로그 게시물 생성 + 요약"이라는 유용한 작업을 이해했으니, 작동시키기 위해 해야 할 일을 살펴봅시다.
메시지 유형
샘플링 메시지는 텍스트뿐 아니라 이미지와 오디오도 보낼 수 있습니다. JSON-RPC 형식이 어떻게 다른지 살펴봅시다:
텍스트
{
"type": "text",
"text": "The message content"
}
이미지 콘텐츠
{
"type": "image",
"data": "base64-encoded-image-data",
"mimeType": "image/jpeg"
}
오디오 콘텐츠
{
"type": "audio",
"data": "base64-encoded-audio-data",
"mimeType": "audio/wav"
}
> 참고: 샘플링에 대한 자세한 정보는 공식 문서를 확인하세요.
클라이언트에서 샘플링 구성 방법
> 참고: 서버만 구축하는 경우 여기서는 크게 할 일이 없습니다.
클라이언트에서 다음 기능을 다음과 같이 지정해야 합니다:
{
"capabilities": {
"sampling": {}
}
}
이렇게 하면 선택한 클라이언트가 서버와 초기화할 때 이 설정을 자동으로 인식합니다.
샘플링 실전 예제 – 블로그 게시물 생성하기
함께 샘플링 서버를 코딩해 봅시다. 다음 작업이 필요합니다:
1. 서버에서 툴 생성.
1. 툴이 샘플링 요청 생성.
1. 툴이 클라이언트의 샘플링 요청 응답을 대기.
1. 툴 결과 생성.
코드를 단계별로 살펴봅시다:
-1- 툴 생성
python
@mcp.tool()
async def create_blog(title: str, content: str, ctx: Context[ServerSession, None]) -> str:
"""Create a blog post and generate a summary"""
-2- 샘플링 요청 생성
툴에 다음 코드를 추가하세요:
python
post = BlogPost(
id=len(posts) + 1,
title=title,
content=content,
abstract=""
)
prompt = f"Create an abstract of the following blog post: title: {title} and draft: {content} "
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=prompt),
)
],
max_tokens=100,
)
-3- 응답 대기 후 반환
python
post.abstract = result.content.text
posts.append(post)
# 전체 제품을 반환합니다
return json.dumps({
"id": post.title,
"abstract": post.abstract
})
-4- 전체 코드
python
from starlette.applications import Starlette
from starlette.routing import Mount, Host
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.types import SamplingMessage, TextContent
import json
from uuid import uuid4
from typing import List
from pydantic import BaseModel
mcp = FastMCP("Blog post generator")
# app = FastAPI()
posts = []
class BlogPost(BaseModel):
id: int
title: str
content: str
abstract: str
posts: List[BlogPost] = []
@mcp.tool()
async def create_blog(title: str, content: str, ctx: Context[ServerSession, None]) -> str:
"""Create a blog post and generate a summary"""
post = BlogPost(
id=len(posts) + 1,
title=title,
content=content,
abstract=""
)
prompt = f"Create an abstract of the following blog post: title: {title} and draft: {content} "
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=prompt),
)
],
max_tokens=100,
)
post.abstract = result.content.text
posts.append(post)
# 전체 블로그 게시물을 반환합니다
return json.dumps({
"id": post.title,
"abstract": post.abstract
})
if __name__ == "__main__":
print("Starting server...")
# mcp.run()
mcp.run(transport="streamable-http")
# 다음 명령어로 앱 실행: python server.py
-5- Visual Studio Code에서 테스트하기
Visual Studio Code에서 테스트하려면 다음을 수행하세요:
1. 터미널에서 서버 시작
1. mcp.json에 추가하고 서버가 실행 중인지 확인, 예:
```json
"servers": {
"blog-server": {
"type": "http",
"url": "http://localhost:8000/mcp"
}
}
```
1. 프롬프트 입력:
```text
create a blog post named "Where Python comes from", the content is "Python is actually named after Monty Python Flying Circus"
```
1. 샘플링 허용. 처음 테스트 시 추가 대화상자가 표시되며 승인해야 합니다. 이후 도구 실행 여부를 묻는 일반 대화상자가 나옵니다.
1. 결과 확인. GitHub Copilot Chat에서 멋지게 렌더링된 결과를 볼 수 있고, 원본 JSON 응답도 검사할 수 있습니다.
보너스. Visual Studio Code 도구는 샘플링을 뛰어나게 지원합니다. 설치한 서버에 대해 샘플링 접근 권한을 설정하려면 다음을 따르세요:
1. 확장 기능 섹션으로 이동
1. "MCP SERVERS - INSTALLED" 섹션에서 설치한 서버의 톱니바퀴 아이콘 선택
1 "Configure Model Access" 선택, 여기서 GitHub Copilot이 샘플링 수행 시 사용할 수 있는 모델을 선택할 수 있습니다. 최근 샘플링 요청은 "Show Sampling requests"를 선택해 확인할 수 있습니다.
과제
이번 과제에서는 약간 다른 샘플링, 즉 제품 설명 생성을 지원하는 샘플링 통합을 개발합니다. 시나리오는 다음과 같습니다:
시나리오: 전자상거래 백오피스 직원이 제품 설명 생성에 너무 많은 시간을 소모합니다. 따라서 "title"과 "keywords"를 매개변수로 받고, 클라이언트의 LLM이 채운 "description" 필드가 포함된 완전한 제품 정보를 생성하는 "create_product"라는 도구를 호출할 수 있는 솔루션을 만들어야 합니다.
TIP: 앞서 배운 내용을 활용해 샘플링 요청을 사용하여 이 서버와 도구를 구성하세요.
솔루션
주요 내용 정리
샘플링은 서버가 LLM의 도움이 필요할 때 작업을 클라이언트에 위임할 수 있게 하는 강력한 기능입니다.
다음에 배울 내용
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나 자동 번역에는 오류나 부정확함이 있을 수 있음을 유의해 주시기 바랍니다.
원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문 인간 번역을 권장합니다.
본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임지지 않습니다.
MCP 앱
MCP 앱은 MCP의 새로운 패러다임입니다.
아이디어는 도구 호출에서 데이터를 반환하는 것뿐만 아니라 이 정보와 상호작용하는 방법에 대한 정보를 제공하는 것입니다.
즉, 도구 결과에 이제 UI 정보가 포함될 수 있다는 뜻입니다.
하지만 왜 그런 것이 필요할까요?
오늘날 여러분이 하는 방식을 생각해 보세요.
MCP 서버의 결과를 소비하기 위해 프런트엔드를 만들어야 하며, 이는 여러분이 작성하고 유지해야 하는 코드입니다.
때로는 그것이 필요한 경우도 있지만, 때로는 데이터부터 사용자 인터페이스까지 모두 포함하는 독립적인 정보 조각을 가져올 수 있다면 좋을 것입니다.
개요
이 수업에서는 MCP 앱에 대한 실용적인 가이드, 시작 방법 및 기존 웹 앱에 통합하는 방법을 제공합니다. MCP 앱은 MCP 표준에 아주 새롭게 추가된 기능입니다.
학습 목표
이 수업이 끝나면 다음을 할 수 있습니다:
MCP 앱 - 어떻게 작동하는가
MCP 앱의 아이디어는 렌더링할 구성요소를 본질적으로 응답으로 제공하는 것입니다. 이러한 구성요소는 시각적 요소와 상호작용성, 예를 들어 버튼 클릭, 사용자 입력 등을 가질 수 있습니다. 먼저 서버 측과 MCP 서버부터 시작해 봅시다. MCP 앱 구성요소를 만들려면 도구와 애플리케이션 리소스를 모두 만들어야 합니다. 이 두 부분은 resourceUri로 연결됩니다.
예를 들어 보겠습니다. 관련된 내용을 시각화하고 어떤 부분이 어떤 역할을 하는지 봅니다:
server.ts -- responsible for registering tools and the component as a UI component
src/
mcp-app.ts -- wiring up event handlers
mcp-app.html -- the user interface
이 시각은 구성요소와 그 로직을 만드는 아키텍처를 설명합니다.
flowchart LR
subgraph Backend[백엔드: MCP 서버]
T["도구 등록: registerAppTool()"]
C["컴포넌트 리소스 등록: registerAppResource()"]
U[resourceUri]
T --- U
C --- U
end
subgraph Parent[상위 웹 페이지]
H[호스트 애플리케이션]
IFRAME[IFrame 컨테이너]
H -->|MCP 앱 UI 삽입| IFRAME
end
subgraph Frontend[프론트엔드: IFrame 내 MCP 앱]
UI["사용자 인터페이스: mcp-app.html"]
EH["이벤트 핸들러: src/mcp-app.ts"]
UI --> EH
end
IFRAME --> UI
EH -->|클릭 시 서버 도구 호출 트리거| T
T -->|도구 결과 데이터| EH
EH -->|상위 페이지에 메시지 전송| H
이제 백엔드와 프런트엔드 각각의 책임을 설명해 봅시다.
백엔드
여기서 해야 할 두 가지가 있습니다:
도구 등록
registerAppTool(
server,
"get-time",
{
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
_meta: { ui: { resourceUri } }, // 이 도구를 UI 리소스에 연결합니다
},
async () => {
const time = new Date().toISOString();
return { content: [{ type: "text", text: time }] };
},
);
위 코드는 get-time이라는 도구를 노출하는 동작을 설명합니다.
입력은 없지만 현재 시간을 생성합니다.
사용자 입력을 받아야 하는 도구에 대해선 inputSchema를 정의할 수 있습니다.
구성요소 등록
같은 파일에서 구성요소도 등록해야 합니다:
const resourceUri = "ui://get-time/mcp-app.html";
// UI를 위한 번들된 HTML/JavaScript를 반환하는 리소스를 등록합니다.
registerAppResource(
server,
resourceUri,
resourceUri,
{ mimeType: RESOURCE_MIME_TYPE },
async () => {
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
return {
contents: [
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);
resourceUri를 언급하여 구성요소를 도구와 연결하는 부분에 주목하세요. 또한 UI 파일을 로드하고 구성요소를 반환하는 콜백도 흥미롭습니다.
구성요소 프런트엔드
백엔드와 마찬가지로 두 부분이 있습니다:
사용자 인터페이스
UI를 살펴봅시다.
<!-- mcp-app.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Get Time App</title>
</head>
<body>
<p>
<strong>Server Time:</strong> <code id="server-time">Loading...</code>
</p>
<button id="get-time-btn">Get Server Time</button>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
이벤트 연결
마지막 부분은 이벤트 연결입니다. 즉, UI 내에서 이벤트 핸들러가 필요한 부분을 식별하고 이벤트가 발생했을 때 무엇을 할지 결정합니다:
// mcp-app.ts
import { App } from "@modelcontextprotocol/ext-apps";
// 요소 참조 가져오기
const serverTimeEl = document.getElementById("server-time")!;
const getTimeBtn = document.getElementById("get-time-btn")!;
// 앱 인스턴스 생성
const app = new App({ name: "Get Time App", version: "1.0.0" });
// 서버로부터 도구 결과 처리. 초기 도구 결과 누락을 방지하기 위해 `app.connect()` 전에 설정
// 초기 도구 결과 누락 방지
app.ontoolresult = (result) => {
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
};
// 버튼 클릭 연결
getTimeBtn.addEventListener("click", async () => {
// `app.callServerTool()`은 UI가 서버로부터 새로운 데이터를 요청하도록 함
const result = await app.callServerTool({ name: "get-time", arguments: {} });
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
});
// 호스트에 연결
app.connect();
위에서 보듯이, DOM 요소를 이벤트에 연결하는 일반적인 코드입니다. callServerTool 호출은 백엔드의 도구를 호출하는 부분임을 주목할 만합니다.
사용자 입력 처리
지금까지 본 구성요소는 버튼을 클릭하면 도구를 호출하는 기능이 있었습니다. 이제 입력 필드 같은 UI 요소를 추가하고 도구에 인수를 보낼 수 있는지 봅시다. FAQ 기능을 구현해 보겠습니다. 작동 방식은 다음과 같습니다:
먼저 백엔드에 필요한 지원을 추가해 봅시다:
const faq: { [key: string]: string } = {
"shipping": "Our standard shipping time is 3-5 business days.",
"return policy": "You can return any item within 30 days of purchase.",
"warranty": "All products come with a 1-year warranty covering manufacturing defects.",
}
registerAppTool(
server,
"get-faq",
{
title: "Search FAQ",
description: "Searches the FAQ for relevant answers.",
inputSchema: zod.object({
query: zod.string().default("shipping"),
}),
_meta: { ui: { resourceUri: faqResourceUri } }, // 이 도구를 UI 리소스에 연결합니다
},
async ({ query }) => {
const answer: string = faq[query.toLowerCase()] || "Sorry, I don't have an answer for that.";
return { content: [{ type: "text", text: answer }] };
},
);
여기서는 inputSchema를 채우는 방법과 zod 스키마를 정의하는 방식을 봅니다:
inputSchema: zod.object({
query: zod.string().default("shipping"),
})
위 스키마에서 query라는 입력 매개변수가 있고, 선택적이며 기본값이 "shipping"임을 선언합니다.
좋습니다. 이제 mcp-app.html로 가서 어떤 UI를 만들어야 하는지 봅시다:
<div class="faq">
<h1>FAQ response</h1>
<p>FAQ Response: <code id="faq-response">Loading...</code></p>
<input type="text" id="faq-query" placeholder="Enter FAQ query" />
<button id="get-faq-btn">Get FAQ Response</button>
</div>
이제 입력 요소와 버튼이 생겼습니다. 다음은 mcp-app.ts로 가서 이벤트를 연결합시다:
const getFaqBtn = document.getElementById("get-faq-btn")!;
const faqQueryInput = document.getElementById("faq-query") as HTMLInputElement;
getFaqBtn.addEventListener("click", async () => {
const query = faqQueryInput.value;
const result = await app.callServerTool({ name: "get-faq", arguments: { query } });
const faq = result.content?.find((c) => c.type === "text")?.text;
faqResponseEl.textContent = faq ?? "[ERROR]";
});
위 코드에서 우리는:
app.callServerTool()을 호출하는데, 여기서 name과 arguments를 넘기며, arguments에는 query 값을 전달합니다.callServerTool 호출 시 실제로는 부모 창에 메시지를 보내고 부모 창이 MCP 서버를 호출하게 됩니다.
직접 해보기
이 기능을 시도하면 다음과 같은 결과를 볼 수 있습니다:
그리고 입력을 "warranty"로 바꾸어 시도한 화면입니다:
이 코드를 실행하려면 코드 섹션
Here's a sample demonstrating MCP App
설치
1. *mcp-app* 폴더로 이동합니다.
1. npm install을 실행하면 프런트엔드 및 백엔드 종속성이 설치됩니다.
다음 명령어로 백엔드가 컴파일되는지 확인하세요:
npx tsc --noEmit
모든 것이 정상이라면 출력이 없어야 합니다.
백엔드 실행
> MCP Apps 솔루션이 concurrently 라이브러리를 사용하기 때문에 Windows 머신에서는 추가 작업이 필요합니다. 대체할 수단을 찾아야 합니다. MCP App의 *package.json*에서 문제가 되는 부분은 다음과 같습니다:
```json
"start": "concurrently \"cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch\" \"tsx watch main.ts\""
```
이 앱은 백엔드 부분과 호스트 부분, 두 부분으로 구성되어 있습니다.
다음 명령어로 백엔드를 시작하세요:
npm start
이 명령어는 http://localhost:3001/mcp에서 백엔드를 시작합니다.
> 참고로, Codespace를 사용하는 경우 포트 공개 설정을 공개로 해야 할 수 있습니다. 브라우저를 통해 https://
선택 -1 Visual Studio Code에서 앱 테스트
Visual Studio Code에서 솔루션을 테스트하려면 다음과 같이 하세요:
mcp.json에 다음과 같이 서버 항목을 추가하세요:```json
{
"servers": {
"my-mcp-server-7178eca7": {
"url": "http://localhost:3001/mcp",
"type": "http"
}
},
"inputs": []
}
```
1. *mcp.json*에서 "start" 버튼을 클릭하세요.
1. 채팅 창이 열려 있는지 확인하고 get-faq를 입력하세요. 다음과 비슷한 결과를 볼 수 있습니다:
선택 -2- 호스트와 앱 테스트
리포지토리
여기 두 가지 옵션을 안내해 드립니다:
로컬 머신
```sh
npm install
```
> Codespace를 사용하는 경우 *serve.ts* 파일 27번째 줄로 이동하여 http://localhost:3001/mcp 를 Codespace 백엔드 URL로 교체해야 합니다. 예를 들어 https://psychic-xylophone-657rpjgvxpc5g64-3001.app.github.dev/mcp 와 같이 변경하세요.
```sh
npm start
```
이 작업으로 호스트와 백엔드가 연결되고 다음과 같이 앱이 실행되는 것을 볼 수 있습니다:
Codespace
Codespace 환경을 작동시키려면 약간 추가 작업이 필요합니다. Codespace에서 호스트를 사용하려면:
npm install을 실행하세요.npm start를 실행하세요.앱 테스트하기
다음 방법으로 앱을 테스트해 보세요:
잘 작동하고 있습니다.
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나, 자동 번역은 오류나 부정확성을 포함할 수 있음을 유의하시기 바랍니다.
원본 문서의 원어 버전이 권위 있는 자료로 간주되어야 합니다.
중요한 정보의 경우 전문 인간 번역을 권장합니다.
본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해서는 책임을 지지 않습니다.
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다하고 있으나, 자동 번역은 오류나 부정확한 부분이 있을 수 있음을 유의해 주시기 바랍니다.
원본 문서는 해당 언어로 작성된 원본 문서를 권위 있는 자료로 간주해야 합니다.
중요한 정보의 경우, 전문 인간 번역을 권장합니다.
이 번역 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
Visual Studio Code에서 테스트하기
Visual Studio Code는 MCP 앱을 훌륭히 지원하며 MCP 앱을 테스트하기에 아마도 가장 쉬운 방법 중 하나입니다. Visual Studio Code를 사용하려면 mcp.json에 다음과 같이 서버 항목을 추가하세요:
"my-mcp-server-7178eca7": {
"url": "http://localhost:3001/mcp",
"type": "http"
}
그런 다음 서버를 시작하면 GitHub Copilot이 설치되어 있을 경우 채팅 창을 통해 MCP 앱과 통신할 수 있습니다.
예를 들어 "#get-faq" 프롬프트로 호출할 수 있습니다:
웹 브라우저에서 실행했을 때와 마찬가지로 동일하게 렌더링됩니다:
과제
가위바위보 게임을 만들어 보세요. 다음과 같이 구성해야 합니다:
UI:
서버:
해답
요약
우리는 MCP 앱이라는 새로운 패러다임에 대해 배웠습니다. 이는 MCP 서버가 데이터뿐 아니라 이 데이터를 어떻게 표현할지도 의견을 가질 수 있는 새로운 패러다임입니다.
또한, MCP 앱은 보안을 위해 IFrame에 호스팅되며 MCP 서버와 소통하려면 부모 웹 앱에 메시지를 보내야 한다는 점도 배웠습니다. 순수 자바스크립트, React 등 다양한 라이브러리가 이 통신을 쉽게 만듭니다.
주요 내용 정리
배운 내용은 다음과 같습니다:
다음 단계
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 양지하시기 바랍니다.
원문 문서는 해당 언어의 공식 자료로 간주되어야 합니다.
중요한 정보에 대해서는 전문적인 인간 번역을 권장합니다.
이 번역의 사용으로 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
Model Context Protocol (MCP)은 애플리케이션이 LLM에 컨텍스트를 제공하는 방식을 표준화한 오픈 프로토콜입니다. MCP는 AI 애플리케이션을 위한 USB-C 포트와 같아서 다양한 데이터 소스와 도구를 AI 모델에 연결하는 표준화된 방식을 제공합니다.
학습 목표
이 강의가 끝나면 다음을 할 수 있습니다:
MCP 환경 설정
MCP 작업을 시작하기 전 개발 환경을 준비하고 기본 워크플로우를 이해하는 것이 중요합니다. 이 섹션에서 원활하게 MCP를 시작할 수 있도록 초기 설정 단계를 안내합니다.
사전 준비 사항
MCP 개발에 들어가기 전에 다음을 준비하세요:
공식 SDK
앞선 장들에서 Python, TypeScript, Java, .NET을 사용한 솔루션 예시를 볼 수 있습니다. 아래는 공식 지원하는 SDK 목록입니다.
MCP는 여러 언어용 공식 SDK를 제공합니다 (MCP Specification 2025-11-25 에 맞춤):
주요 내용 정리
연습하기
이 섹션의 모든 장에서 볼 수 있는 연습 문제를 보완하는 샘플 세트를 제공합니다. 각 장마다 고유의 연습문제와 과제도 포함되어 있습니다.
Basic Calculator MCP Service
이 서비스는 Spring Boot와 WebFlux 전송을 사용하여 Model Context Protocol(MCP)을 통해 기본 계산기 연산을 제공합니다. MCP 구현을 배우는 초보자를 위한 간단한 예제로 설계되었습니다.
자세한 내용은 MCP Server Boot Starter 참조 문서를 확인하세요.
개요
이 서비스는 다음을 보여줍니다:
@Tool 애노테이션을 통한 자동 도구 등록- 덧셈, 뺄셈, 곱셈, 나눗셈
- 거듭제곱 계산과 제곱근
- 나머지 연산과 절댓값
- 연산 설명을 위한 도움말 기능
기능
이 계산기 서비스는 다음과 같은 기능을 제공합니다:
1. 기본 산술 연산:
- 두 수의 덧셈
- 한 수에서 다른 수를 뺌
- 두 수의 곱셈
- 한 수를 다른 수로 나눔 (0으로 나누기 검사 포함)
2. 고급 연산:
- 거듭제곱 계산 (밑을 지수만큼 올림)
- 제곱근 계산 (음수 검사 포함)
- 나머지(모듈로) 계산
- 절댓값 계산
3. 도움말 시스템:
- 사용 가능한 모든 연산을 설명하는 내장 도움말 기능
서비스 사용법
이 서비스는 MCP 프로토콜을 통해 다음 API 엔드포인트를 제공합니다:
add(a, b): 두 수를 더함subtract(a, b): 두 번째 수를 첫 번째 수에서 뺌multiply(a, b): 두 수를 곱함divide(a, b): 첫 번째 수를 두 번째 수로 나눔 (0 검사 포함)power(base, exponent): 거듭제곱 계산squareRoot(number): 제곱근 계산 (음수 검사 포함)modulus(a, b): 나눗셈 후 나머지 계산absolute(number): 절댓값 계산help(): 사용 가능한 연산에 대한 정보 제공테스트 클라이언트
com.microsoft.mcp.sample.client 패키지에 간단한 테스트 클라이언트가 포함되어 있습니다. SampleCalculatorClient 클래스는 계산기 서비스의 사용 가능한 연산을 시연합니다.
LangChain4j 클라이언트 사용법
프로젝트에는 com.microsoft.mcp.sample.client.LangChain4jClient에 LangChain4j 예제 클라이언트가 포함되어 있어 계산기 서비스를 LangChain4j 및 GitHub 모델과 통합하는 방법을 보여줍니다.
사전 준비 사항
1. GitHub 토큰 설정:
GitHub의 AI 모델(phi-4 등)을 사용하려면 GitHub 개인 액세스 토큰이 필요합니다:
a. GitHub 계정 설정으로 이동: https://github.com/settings/tokens
b. "Generate new token" → "Generate new token (classic)" 클릭
c. 토큰에 설명 이름 지정
d. 다음 권한 선택:
- repo (비공개 저장소 전체 제어)
- read:org (조직 및 팀 멤버십, 조직 프로젝트 읽기)
- gist (Gist 생성)
- user:email (사용자 이메일 주소 접근(읽기 전용))
e. "Generate token" 클릭 후 새 토큰 복사
f. 환경 변수로 설정:
Windows에서:
```
set GITHUB_TOKEN=your-github-token
```
macOS/Linux에서:
```bash
export GITHUB_TOKEN=your-github-token
```
g. 지속적인 설정을 위해 시스템 환경 변수에 추가
2. LangChain4j GitHub 의존성을 프로젝트에 추가 (pom.xml에 이미 포함됨):
```xml
```
3. 계산기 서버가 localhost:8080에서 실행 중인지 확인
LangChain4j 클라이언트 실행
이 예제는 다음을 보여줍니다:
클라이언트는 다음 샘플 쿼리를 보내 기능을 시연합니다:
1. 두 수의 합 계산
2. 수의 제곱근 계산
3. 사용 가능한 계산기 연산에 대한 도움말 정보 요청
예제를 실행하고 콘솔 출력을 확인하여 AI 모델이 계산기 도구를 어떻게 활용하는지 확인하세요.
GitHub 모델 구성
LangChain4j 클라이언트는 다음 설정으로 GitHub의 phi-4 모델을 사용하도록 구성되어 있습니다:
ChatLanguageModel model = GitHubChatModel.builder()
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("phi-4")
.logRequests(true)
.logResponses(true)
.build();
다른 GitHub 모델을 사용하려면 modelName 매개변수를 지원되는 다른 모델 이름(예: "claude-3-haiku-20240307", "llama-3-70b-8192" 등)으로 변경하면 됩니다.
의존성
프로젝트에 필요한 주요 의존성은 다음과 같습니다:
<!-- For MCP Server -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<!-- For LangChain4j integration -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- For GitHub models support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github</artifactId>
<version>${langchain4j.version}</version>
</dependency>
프로젝트 빌드
Maven을 사용하여 프로젝트를 빌드하세요:
./mvnw clean install -DskipTests
서버 실행
Java 사용
java -jar target/calculator-server-0.0.1-SNAPSHOT.jar
MCP Inspector 사용
MCP Inspector는 MCP 서비스와 상호작용할 수 있는 유용한 도구입니다. 이 계산기 서비스와 함께 사용하려면:
1. MCP Inspector 설치 및 실행 (새 터미널 창에서):
```bash
npx @modelcontextprotocol/inspector
```
2. 웹 UI 접속: 앱에서 표시하는 URL(보통 http://localhost:6274)을 클릭
3. 연결 설정:
- 전송 유형을 "SSE"로 설정
- 실행 중인 서버의 SSE 엔드포인트 URL을 http://localhost:8080/sse로 설정
- "Connect" 클릭
4. 도구 사용:
- "List Tools" 클릭하여 사용 가능한 계산기 연산 확인
- 도구 선택 후 "Run Tool" 클릭하여 연산 실행
Docker 사용
프로젝트에는 컨테이너 배포를 위한 Dockerfile이 포함되어 있습니다:
1. Docker 이미지 빌드:
```bash
docker build -t calculator-mcp-service .
```
2. Docker 컨테이너 실행:
```bash
docker run -p 8080:8080 calculator-mcp-service
```
이 작업은:
컨테이너가 실행되면 http://localhost:8080에서 서비스에 접속할 수 있습니다.
문제 해결
GitHub 토큰 관련 일반 문제
1. 토큰 권한 문제: 403 Forbidden 오류가 발생하면, 사전 준비 사항에 명시된 권한이 올바르게 설정되었는지 확인하세요.
2. 토큰 미설정: "No API key found" 오류가 발생하면 GITHUB_TOKEN 환경 변수가 제대로 설정되었는지 확인하세요.
3. 요청 제한 초과: GitHub API는 요청 제한이 있습니다. 429 상태 코드 오류가 발생하면 잠시 기다렸다가 다시 시도하세요.
4. 토큰 만료: GitHub 토큰은 만료될 수 있습니다. 인증 오류가 발생하면 새 토큰을 생성하고 환경 변수를 업데이트하세요.
추가 도움이 필요하면 LangChain4j 문서 또는 GitHub API 문서를 참고하세요.
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의해 주시기 바랍니다.
원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
샘플
이것은 MCP 서버용 JavaScript 샘플입니다
계산기 부분은 다음과 같습니다:
// Define calculator tools for each operation
server.tool(
"add",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.tool(
"subtract",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a - b) }]
})
);
server.tool(
"multiply",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a * b) }]
})
);
server.tool(
"divide",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => {
if (b === 0) {
return {
content: [{ type: "text", text: "Error: Cannot divide by zero" }],
isError: true
};
}
return {
content: [{ type: "text", text: String(a / b) }]
};
}
);
설치
다음 명령어를 실행하세요:
npm install
실행
npm start
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의해 주시기 바랍니다.
원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
샘플
이것은 MCP 서버용 Typescript 샘플입니다
계산기 부분은 다음과 같습니다:
// Define calculator tools for each operation
server.tool(
"add",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.tool(
"subtract",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a - b) }]
})
);
server.tool(
"multiply",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a * b) }]
})
);
server.tool(
"divide",
{
a: z.number(),
b: z.number()
},
async ({ a, b }) => {
if (b === 0) {
return {
content: [{ type: "text", text: "Error: Cannot divide by zero" }],
isError: true
};
}
return {
content: [{ type: "text", text: String(a / b) }]
};
}
);
설치
다음 명령어를 실행하세요:
npm install
실행
npm start
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.
원문은 해당 언어의 원본 문서가 권위 있는 자료로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
본 번역의 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.
추가 자료
다음 단계
첫 번째 강의부터 시작하세요: 처음 MCP 서버 만들기
MCP 시작하기
Model Context Protocol (MCP)와 함께하는 첫 걸음에 오신 것을 환영합니다! MCP가 처음이든 이해도를 높이고자 하든, 이 가이드는 필수 설정 및 개발 과정을 안내합니다. MCP가 AI 모델과 애플리케이션 간의 원활한 통합을 어떻게 가능하게 하는지 살펴보고, MCP 기반 솔루션 구축 및 테스트를 위한 환경을 빠르게 준비하는 방법을 배우게 됩니다.
> TLDR; AI 애플리케이션을 개발한다면 LLM(대형 언어 모델)에 도구와 기타 리소스를 추가하여 LLM을 더 똑똑하게 만들 수 있다는 것을 아실 겁니다. 하지만 도구와 리소스를 서버에 배치하면 앱과 서버 기능은 LLM이 있든 없든 모든 클라이언트가 사용할 수 있습니다.
개요
이 수업은 MCP 환경 설정과 첫 MCP 애플리케이션 구축에 관한 실용적인 안내를 제공합니다. 필요한 도구 및 프레임워크 설정, 기본 MCP 서버 구축, 호스트 애플리케이션 생성, 구현 테스트 방법을 배우게 됩니다.
Model Context Protocol (MCP)은 애플리케이션이 LLM에 컨텍스트를 제공하는 방식을 표준화하는 오픈 프로토콜입니다. MCP는 AI 애플리케이션을 위한 USB-C 포트와 같아서 AI 모델을 다양한 데이터 소스 및 도구와 연결하는 표준화된 방법을 제공합니다.
학습 목표
이 수업을 마치면 다음을 수행할 수 있습니다:
MCP 환경 설정하기
MCP 작업을 시작하기 전에 개발 환경을 준비하고 기본 작업 흐름을 이해하는 것이 중요합니다. 이 섹션은 MCP 시작을 원활하게 하기 위한 초기 설정 단계를 안내합니다.
사전 준비 사항
MCP 개발에 착수하기 전에 다음이 준비되었는지 확인하세요:
기본 MCP 서버 구조
MCP 서버는 일반적으로 다음을 포함합니다:
아래는 TypeScript 예제입니다:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// MCP 서버를 생성합니다
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// 추가 도구를 추가합니다
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// 동적 인사말 리소스를 추가합니다
server.resource(
"file",
// 'list' 매개변수는 리소스가 사용 가능한 파일을 나열하는 방식을 제어합니다. undefined로 설정하면 이 리소스의 목록 표시가 비활성화됩니다.
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => ({
contents: [{
uri: uri.href,
text: `File, ${path}!`
}]
})
);
// 파일 내용을 읽는 파일 리소스를 추가합니다
server.resource(
"file",
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => {
let text;
try {
text = await fs.readFile(path, "utf8");
} catch (err) {
text = `Error reading file: ${err.message}`;
}
return {
contents: [{
uri: uri.href,
text
}]
};
}
);
server.prompt(
"review-code",
{ code: z.string() },
({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please review this code:\n\n${code}`
}
}]
})
);
// stdin에서 메시지를 받고 stdout으로 메시지를 전송하기 시작합니다
const transport = new StdioServerTransport();
await server.connect(transport);
위 코드에서 우리는:
calculator)를 등록했습니다.테스트 및 디버깅
MCP 서버를 테스트하기 전에 이용 가능한 도구 및 디버깅 모범 사례를 이해하는 것이 중요합니다. 효과적인 테스트는 서버가 예상대로 작동하는지 확인하고 문제를 신속히 파악 및 해결하는 데 도움이 됩니다. 다음 섹션에서 MCP 구현을 검증하기 위한 권장 방법을 설명합니다.
MCP는 서버 테스트 및 디버깅을 도와주는 도구를 제공합니다:
MCP Inspector 사용하기
1. 서버 기능 탐색: 사용 가능한 리소스, 도구, 프롬프트 자동 감지
2. 도구 실행 테스트: 다양한 매개변수로 실시간 응답 확인
3. 서버 메타데이터 조회: 서버 정보, 스키마, 구성 검토
# 예제 TypeScript, MCP Inspector 설치 및 실행
npx @modelcontextprotocol/inspector node build/index.js
위 명령어를 실행하면 MCP Inspector가 브라우저에서 로컬 웹 인터페이스를 실행합니다. 등록된 MCP 서버, 사용 가능한 도구, 리소스 및 프롬프트 대시보드를 볼 수 있습니다. 이 인터페이스로 도구 실행 테스트, 서버 메타데이터 조사, 실시간 응답 확인 등이 가능해 MCP 서버 구현 검증 및 디버깅이 수월해집니다.
다음은 화면 예시입니다:
일반적인 설정 문제 및 해결책
로컬 개발
로컬 개발 및 테스트용으로, MCP 서버를 자신의 머신에서 직접 실행할 수 있습니다:
1. 서버 프로세스 시작: MCP 서버 애플리케이션 실행
2. 네트워킹 구성: 서버가 예상 포트에서 접근 가능하게 설정
3. 클라이언트 연결: http://localhost:3000 같은 로컬 연결 URL 사용
# 예시: TypeScript MCP 서버를 로컬에서 실행하기
npm run start
# 서버가 http://localhost:3000 에서 실행 중입니다
첫 번째 MCP 서버 구축하기
이전 수업에서 핵심 개념을 다뤘으니 이제 그 지식을 실습해 보겠습니다.
서버가 할 수 있는 일
코딩을 시작하기 전에 서버의 역할을 상기해 봅시다:
MCP 서버는 예를 들어:
좋습니다, 무엇을 할 수 있는지 알았으니 코딩을 시작해 봅시다.
연습: 서버 만들기
서버를 만들려면 다음 단계를 따르세요:
-1- 프로젝트 생성하기
TypeScript
# 프로젝트 디렉토리를 생성하고 npm 프로젝트를 초기화하십시오
mkdir calculator-server
cd calculator-server
npm init -y
Python
# 프로젝트 디렉토리 생성
mkdir calculator-server
cd calculator-server
# Visual Studio Code에서 폴더 열기 - 다른 IDE를 사용하는 경우 생략하세요
code .
.NET
dotnet new console -n McpCalculatorServer
cd McpCalculatorServer
Java
Java의 경우 Spring Boot 프로젝트를 만드세요:
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d javaVersion=21 \
-d type=maven-project \
-d groupId=com.example \
-d artifactId=calculator-server \
-d name=McpServer \
-d packageName=com.microsoft.mcp.sample.server \
-o calculator-server.zip
압축 파일 풀기:
unzip calculator-server.zip -d calculator-server
cd calculator-server
# 선택적으로 사용하지 않는 테스트 제거
rm -rf src/test/java
*pom.xml* 파일에 다음과 같은 전체 구성을 추가하세요:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Spring Boot parent for dependency management -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath />
</parent>
<!-- Project coordinates -->
<groupId>com.example</groupId>
<artifactId>calculator-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Calculator Server</name>
<description>Basic calculator MCP service for beginners</description>
<!-- Properties -->
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!-- Spring AI BOM for version management -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Dependencies -->
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Build configuration -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
<!-- Repositories for Spring AI snapshots -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>
Rust
mkdir calculator-server
cd calculator-server
cargo init
-2- 의존성 추가하기
프로젝트를 생성했으니 다음은 의존성 추가입니다:
TypeScript
# 아직 설치하지 않은 경우 TypeScript를 전역에 설치하세요
npm install typescript -g
# MCP SDK와 스키마 검증을 위해 Zod를 설치하세요
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
Python
# 가상 환경을 만들고 종속성을 설치합니다
python -m venv venv
venv\Scripts\activate
pip install "mcp[cli]"
Java
cd calculator-server
./mvnw clean install -DskipTests
Rust
cargo add rmcp --features server,transport-io
cargo add serde
cargo add tokio --features rt-multi-thread
-3- 프로젝트 파일 생성하기
TypeScript
*package.json* 파일을 열어 다음 내용으로 교체해 서버 빌드 및 실행이 가능하게 합니다:
{
"name": "calculator-server",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "npm run build && node ./build/index.js",
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "A simple calculator server using Model Context Protocol",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.14",
"typescript": "^5.8.3"
}
}
*tsconfig.json* 파일을 다음 내용으로 생성하세요:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
소스 코드용 디렉터리를 생성합니다:
mkdir src
touch src/index.ts
Python
*server.py* 파일을 생성하세요
touch server.py
.NET
필요한 NuGet 패키지를 설치하세요:
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
Java
Java Spring Boot 프로젝트는 프로젝트 구조가 자동으로 생성됩니다.
Rust
Rust는 cargo init 실행 시 기본적으로 *src/main.rs* 파일이 생성됩니다. 해당 파일을 열고 기본 코드를 삭제하세요.
-4- 서버 코드 작성하기
TypeScript
*index.ts* 파일을 생성하고 다음 코드를 추가하세요:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// MCP 서버 생성
const server = new McpServer({
name: "Calculator MCP Server",
version: "1.0.0"
});
서버가 생성되었으나 할 일이 많지 않습니다. 고쳐 봅시다.
Python
# server.py
from mcp.server.fastmcp import FastMCP
# MCP 서버 생성
mcp = FastMCP("Demo")
.NET
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();
// add features
Java
Java는 핵심 서버 구성 요소를 생성합니다. 먼저 메인 애플리케이션 클래스를 수정하세요:
*src/main/java/com/microsoft/mcp/sample/server/McpServerApplication.java*:
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
계산기 서비스 생성 *src/main/java/com/microsoft/mcp/sample/server/service/CalculatorService.java*:
package com.microsoft.mcp.sample.server.service;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
/**
* Service for basic calculator operations.
* This service provides simple calculator functionality through MCP.
*/
@Service
public class CalculatorService {
/**
* Add two numbers
* @param a The first number
* @param b The second number
* @return The sum of the two numbers
*/
@Tool(description = "Add two numbers together")
public String add(double a, double b) {
double result = a + b;
return formatResult(a, "+", b, result);
}
/**
* Subtract one number from another
* @param a The number to subtract from
* @param b The number to subtract
* @return The result of the subtraction
*/
@Tool(description = "Subtract the second number from the first number")
public String subtract(double a, double b) {
double result = a - b;
return formatResult(a, "-", b, result);
}
/**
* Multiply two numbers
* @param a The first number
* @param b The second number
* @return The product of the two numbers
*/
@Tool(description = "Multiply two numbers together")
public String multiply(double a, double b) {
double result = a * b;
return formatResult(a, "*", b, result);
}
/**
* Divide one number by another
* @param a The numerator
* @param b The denominator
* @return The result of the division
*/
@Tool(description = "Divide the first number by the second number")
public String divide(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a / b;
return formatResult(a, "/", b, result);
}
/**
* Calculate the power of a number
* @param base The base number
* @param exponent The exponent
* @return The result of raising the base to the exponent
*/
@Tool(description = "Calculate the power of a number (base raised to an exponent)")
public String power(double base, double exponent) {
double result = Math.pow(base, exponent);
return formatResult(base, "^", exponent, result);
}
/**
* Calculate the square root of a number
* @param number The number to find the square root of
* @return The square root of the number
*/
@Tool(description = "Calculate the square root of a number")
public String squareRoot(double number) {
if (number < 0) {
return "Error: Cannot calculate square root of a negative number";
}
double result = Math.sqrt(number);
return String.format("√%.2f = %.2f", number, result);
}
/**
* Calculate the modulus (remainder) of division
* @param a The dividend
* @param b The divisor
* @return The remainder of the division
*/
@Tool(description = "Calculate the remainder when one number is divided by another")
public String modulus(double a, double b) {
if (b == 0) {
return "Error: Cannot divide by zero";
}
double result = a % b;
return formatResult(a, "%", b, result);
}
/**
* Calculate the absolute value of a number
* @param number The number to find the absolute value of
* @return The absolute value of the number
*/
@Tool(description = "Calculate the absolute value of a number")
public String absolute(double number) {
double result = Math.abs(number);
return String.format("|%.2f| = %.2f", number, result);
}
/**
* Get help about available calculator operations
* @return Information about available operations
*/
@Tool(description = "Get help about available calculator operations")
public String help() {
return "Basic Calculator MCP Service\n\n" +
"Available operations:\n" +
"1. add(a, b) - Adds two numbers\n" +
"2. subtract(a, b) - Subtracts the second number from the first\n" +
"3. multiply(a, b) - Multiplies two numbers\n" +
"4. divide(a, b) - Divides the first number by the second\n" +
"5. power(base, exponent) - Raises a number to a power\n" +
"6. squareRoot(number) - Calculates the square root\n" +
"7. modulus(a, b) - Calculates the remainder of division\n" +
"8. absolute(number) - Calculates the absolute value\n\n" +
"Example usage: add(5, 3) will return 5 + 3 = 8";
}
/**
* Format the result of a calculation
*/
private String formatResult(double a, String operator, double b, double result) {
return String.format("%.2f %s %.2f = %.2f", a, operator, b, result);
}
}
프로덕션 준비 서비스를 위한 선택적 구성 요소:
시작 구성 생성 *src/main/java/com/microsoft/mcp/sample/server/config/StartupConfig.java*:
package com.microsoft.mcp.sample.server.config;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StartupConfig {
@Bean
public CommandLineRunner startupInfo() {
return args -> {
System.out.println("\n" + "=".repeat(60));
System.out.println("Calculator MCP Server is starting...");
System.out.println("SSE endpoint: http://localhost:8080/sse");
System.out.println("Health check: http://localhost:8080/actuator/health");
System.out.println("=".repeat(60) + "\n");
};
}
}
헬스 컨트롤러 생성 *src/main/java/com/microsoft/mcp/sample/server/controller/HealthController.java*:
package com.microsoft.mcp.sample.server.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> healthCheck() {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("timestamp", LocalDateTime.now().toString());
response.put("service", "Calculator MCP Server");
return ResponseEntity.ok(response);
}
}
예외 핸들러 생성 *src/main/java/com/microsoft/mcp/sample/server/exception/GlobalExceptionHandler.java*:
package com.microsoft.mcp.sample.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
"Invalid_Input",
"Invalid input parameter: " + ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
public static class ErrorResponse {
private String code;
private String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
// 게터
public String getCode() { return code; }
public String getMessage() { return message; }
}
}
커스텀 배너 생성 *src/main/resources/banner.txt*:
_____ _ _ _
/ ____| | | | | | |
| | __ _| | ___ _ _| | __ _| |_ ___ _ __
| | / _` | |/ __| | | | |/ _` | __/ _ \| '__|
| |___| (_| | | (__| |_| | | (_| | || (_) | |
\_____\__,_|_|\___|\__,_|_|\__,_|\__\___/|_|
Calculator MCP Server v1.0
Spring Boot MCP Application
Rust
*src/main.rs* 파일 상단에 다음 코드를 추가하세요. 이는 MCP 서버에 필요한 라이브러리와 모듈을 가져옵니다.
use rmcp::{
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
ServerHandler, ServiceExt,
};
use std::error::Error;
계산기 서버는 두 숫자를 더하는 간단한 서버가 될 것입니다. 계산기 요청을 나타내는 struct를 만들어 봅시다.
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
다음으로 계산기 서버를 나타내는 struct를 만듭니다. 이 struct는 도구 라우터를 보유하며 도구 등록에 사용됩니다.
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
이제 Calculator struct를 구현하여 서버 새 인스턴스를 생성하고 서버 정보를 제공하는 핸들러를 구현합니다.
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
마지막으로 서버를 시작하는 main 함수를 구현해야 합니다. 이 함수는 Calculator struct 인스턴스를 만들고 표준 입출력으로 서버를 운영합니다.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
서버는 이제 자체에 관한 기본 정보를 제공합니다. 다음으로 덧셈을 수행하는 도구를 추가합니다.
-5- 도구 및 리소스 추가
다음 코드로 도구와 리소스를 추가하세요:
TypeScript
server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
도구는 a 및 b 매개변수를 받고, 다음 형식의 응답을 생성합니다:
{
contents: [{
type: "text", content: "some content"
}]
}
리소스는 문자열 "greeting"으로 접근하며, 이름(name) 매개변수를 받아 도구와 유사한 응답을 생성합니다:
{
uri: "<href>",
text: "a text"
}
Python
# 덧셈 도구 추가
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# 동적 인사말 리소스 추가
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
위 코드에서 우리는:
a와 b라는 정수 매개변수를 받는 add 도구를 정의했습니다.name 매개변수를 받는 greeting 리소스를 만들었습니다..NET
Program.cs 파일에 다음을 추가하세요:
[McpServerToolType]
public static class CalculatorTool
{
[McpServerTool, Description("Adds two numbers")]
public static string Add(int a, int b) => $"Sum {a + b}";
}
Java
도구는 이전 단계에서 이미 생성했습니다.
Rust
impl Calculator 블록 내에 새 도구를 추가하세요:
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
-6- 최종 코드
서버가 시작할 수 있도록 마지막 코드를 추가합시다:
TypeScript
// stdin에서 메시지 수신을 시작하고 stdout에서 메시지 전송을 시작합니다
const transport = new StdioServerTransport();
await server.connect(transport);
전체 코드는 다음과 같습니다:
// index.ts
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// MCP 서버 생성
const server = new McpServer({
name: "Calculator MCP Server",
version: "1.0.0"
});
// 추가 도구 추가
server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// 동적 인사말 리소스 추가
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
// stdin에서 메시지 수신 시작 및 stdout으로 메시지 전송 시작
const transport = new StdioServerTransport();
server.connect(transport);
Python
# server.py
from mcp.server.fastmcp import FastMCP
# MCP 서버 생성
mcp = FastMCP("Demo")
# 추가 도구 추가
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# 동적 인사말 리소스 추가
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
# 메인 실행 블록 - 서버를 실행하려면 필요합니다
if __name__ == "__main__":
mcp.run()
.NET
다음 내용을 가진 Program.cs 파일을 생성하세요:
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}";
}
Java
완성된 메인 애플리케이션 클래스는 다음과 같아야 합니다:
// McpServerApplication.java
package com.microsoft.mcp.sample.server;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.microsoft.mcp.sample.server.service.CalculatorService;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculator) {
return MethodToolCallbackProvider.builder().toolObjects(calculator).build();
}
}
Rust
Rust 서버의 최종 코드는 다음과 같습니다:
use rmcp::{
ServerHandler, ServiceExt,
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
};
use std::error::Error;
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CalculatorRequest {
pub a: f64,
pub b: f64,
}
#[derive(Debug, Clone)]
pub struct Calculator {
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(description = "Adds a and b")]
async fn add(
&self,
Parameters(CalculatorRequest { a, b }): Parameters<CalculatorRequest>,
) -> String {
(a + b).to_string()
}
}
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator tool".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let service = Calculator::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
-7- 서버 테스트
다음 명령어로 서버를 시작하세요:
TypeScript
npm run build
Python
mcp run server.py
> MCP Inspector를 사용하려면 mcp dev server.py를 사용하세요.
이는 Inspector를 자동으로 실행하고 필요한 프록시 세션 토큰을 제공합니다. mcp run server.py를 사용할 경우 Inspector를 수동으로 시작하고 연결을 구성해야 합니다.
.NET
프로젝트 디렉터리 안에 있는지 확인하세요:
cd McpCalculatorServer
dotnet run
Java
./mvnw clean install -DskipTests
java -jar target/calculator-server-0.0.1-SNAPSHOT.jar
Rust
서버를 형식화하고 실행하려면 다음 명령어를 실행하세요:
cargo fmt
cargo run
-8- Inspector를 사용해 실행하기
Inspector는 서버를 시작하고 상호작용할 수 있도록 도와주는 훌륭한 도구입니다. 시작해 봅시다:
> [!NOTE]
> "command" 필드의 내용은 특정 런타임으로 서버를 실행하는 명령어를 포함하므로 다르게 보일 수 있습니다.
TypeScript
npx @modelcontextprotocol/inspector node build/index.js
또는 package.json에 "inspector": "npx @modelcontextprotocol/inspector node build/index.js"를 추가하고 npm run inspector를 실행하세요.
Python
Python은 Node.js 도구인 inspector를 래핑합니다. 다음과 같이 해당 도구를 호출할 수 있습니다:
mcp dev server.py
하지만 전체 명령어를 구현하지 않으므로 Node.js 도구를 직접 실행하는 것이 권장됩니다:
npx @modelcontextprotocol/inspector mcp run server.py
스크립트 실행을 위한 명령과 인자를 구성할 수 있는 도구나 IDE를 사용하는 경우,
Command 필드에 python을 설정하고 Arguments에 server.py를 설정해야 합니다.
이렇게 해야 스크립트가 올바르게 실행됩니다.
.NET
프로젝트 디렉터리에 있는지 확인하세요:
cd McpCalculatorServer
npx @modelcontextprotocol/inspector dotnet run
Java
계산기 서버가 실행 중인지 확인하세요
그런 다음 인스펙터를 실행합니다:
npx @modelcontextprotocol/inspector
인스펙터 웹 인터페이스에서:
1. 전송 유형으로 "SSE"를 선택하세요
2. URL을 http://localhost:8080/sse로 설정하세요
3. "Connect"를 클릭하세요
이제 서버에 연결되었습니다
Java 서버 테스트 섹션이 완료되었습니다
다음 섹션은 서버와 상호작용하는 방법에 관한 내용입니다.
다음과 같은 사용자 인터페이스가 보일 것입니다:
1. "Connect" 버튼을 선택하여 서버에 연결하세요
서버에 연결되면 다음 화면이 보입니다:
1. "Tools"에서 "listTools"를 선택하세요. "Add"가 표시되면 "Add"를 선택하고 매개변수 값을 입력하세요.
다음과 같은 응답, 즉 "add" 도구의 결과가 표시됩니다:
축하합니다, 첫 번째 서버를 성공적으로 만들고 실행했습니다!
Rust
MCP 인스펙터 CLI로 Rust 서버를 실행하려면 다음 명령어를 사용하세요:
npx @modelcontextprotocol/inspector cargo run --cli --method tools/call --tool-name add --tool-arg a=1 b=2
공식 SDK
MCP는 여러 언어에 대한 공식 SDK를 제공합니다:
주요 요점
샘플
과제
선택한 도구를 사용하여 간단한 MCP 서버를 만드세요:
1. 선호하는 언어(.NET, Java, Python, TypeScript, Rust)로 도구를 구현하세요.
2. 입력 매개변수와 반환 값을 정의하세요.
3. 인스펙터 도구를 실행하여 서버가 제대로 작동하는지 확인하세요.
4. 다양한 입력으로 구현을 테스트하세요.
솔루션
추가 리소스
다음 단계
다음: MCP 클라이언트 시작하기
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 노력하고 있으나 자동 번역에는 오류나 부정확성이 포함될 수 있음을 유의해 주시기 바랍니다.
원본 문서가 원어로 된 공식 자료임을 참고하시기 바랍니다.
중요한 정보에 대해서는 전문 인간 번역가의 번역을 권장합니다.
본 번역 사용으로 인해 발생하는 모든 오해나 오역에 대해 당사는 책임을 지지 않습니다.
이 모듈을 완료한 후에는 계속해서: 모듈 4: 실전 구현
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다했지만, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.
원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문적인 인간 번역을 권장합니다.
본 번역의 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.