extension MCP Curriculum
translate KO / EN
code Module 03

Getting Started

Getting Started

_(Click the image above to view video of this lesson)_

This section consists of several lessons:

  • 1 Your first server, in this first lesson, you will learn how to create your first server and inspect it with the inspector tool, a valuable way to test and debug your server, to the lesson

    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:

  • Set up development environments for MCP in C#, Java, Python, TypeScript, and Rust
  • Build and deploy basic MCP servers with custom features (resources, prompts, and tools)
  • Create host applications that connect to MCP servers
  • Test and debug MCP implementations
  • 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:

  • Development Environment: For your chosen language (C#, Java, Python, TypeScript, or Rust)
  • IDE/Editor: Visual Studio, Visual Studio Code, IntelliJ, Eclipse, PyCharm, or any modern code editor
  • Package Managers: NuGet, Maven/Gradle, pip, npm/yarn, or Cargo
  • API Keys: For any AI services you plan to use in your host applications
  • Basic MCP Server Structure

    An MCP server typically includes:

  • Server Configuration: Setup port, authentication, and other settings
  • Resources: Data and context made available to LLMs
  • Tools: Functionality that models can invoke
  • Prompts: Templates for generating or structuring text
  • 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:

  • Import the necessary classes from the MCP TypeScript SDK.
  • Create and configure a new MCP server instance.
  • Register a custom tool (calculator) with a handler function.
  • Start the server to listen for incoming MCP requests.
  • 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:

  • Inspector tool, this graphical interface allows you to connect to your server and test your tools, prompts and resources.
  • curl, you can also connect to your server using a command line tool like curl or other clients than can create and run HTTP commands.
  • 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

    Issue Possible Solution ------- ------------------- Connection refused Check if server is running and port is correct Tool execution errors Review parameter validation and error handling Authentication failures Verify API keys and permissions Schema validation errors Ensure parameters match the defined schema Server not starting Check for port conflicts or missing dependencies CORS errors Configure proper CORS headers for cross-origin requests Authentication issues Verify token validity and permissions

    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:

  • Access local files and databases
  • Connect to remote APIs
  • Perform computations
  • Integrate with other tools and services
  • Provide a user interface for interaction
  • 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:

  • Install the MCP SDK.
  • Create a a project and set up the project structure.
  • Write the server code.
  • Test the server.
  • -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:

  • Defined a tool add that takes parameters a and b, both integers.
  • Created a resource called 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:

    !Connected

    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:

    !Result of running add

    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:

  • C# SDK - Maintained in collaboration with Microsoft
  • Java SDK - Maintained in collaboration with Spring AI
  • TypeScript SDK - The official TypeScript implementation
  • Python SDK - The official Python implementation
  • Kotlin SDK - The official Kotlin implementation
  • Swift SDK - Maintained in collaboration with Loopwork AI
  • Rust SDK - The official Rust implementation
  • Key Takeaways

  • Setting up an MCP development environment is straightforward with language-specific SDKs
  • Building MCP servers involves creating and registering tools with clear schemas
  • Testing and debugging are essential for reliable MCP implementations
  • Samples

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Rust Calculator
  • 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

  • Build Agents using Model Context Protocol on Azure
  • Remote MCP with Azure Container Apps (Node.js/TypeScript/JavaScript)
  • .NET OpenAI MCP Agent
  • What's next

    Next: Getting Started with MCP Clients

  • 2 Client, in this lesson, you will learn how to write a client that can connect to your server, to the lesson

    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:

  • Understand what a client can do.
  • Write your own client.
  • Connect and test the client with an MCP server to ensure the latter works as expected.
  • What goes into writing a client?

    To write a client, you'll need to do the following:

  • Import the correct libraries. You'll be using the same library as before, just different constructs.
  • Instantiate a client. This will involve creating a client instance and connect it to the chosen transport method.
  • Decide on what resources to list. Your MCP server comes with resources, tools and prompts, you need to decide which one to list.
  • Integrate the client to a host application. Once you know the capabilities of the server you need to integrate this your host application so that if a user types a prompt or other command the corresponding server feature is invoked.
  • Now that we understand at high level what we're about to do, let's look at an example next.

    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:

  • Import the libraries
  • Create an instance of a client and connect it using stdio for transport.
  • List prompts, resources and tools and invoke them all.
  • There you have it, a client that can talk to an MCP Server.

    Let's take our time in the next exercise section and break down each code snippet and explain what's going on.

    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:

  • Created an stdio transport instance. Note how it specifies command and args for how to find and start up the server as that's something we will need to do as we create the client.
  • ```typescript

    const transport = new StdioClientTransport({

    command: "node",

    args: ["server.js"]

    });

    ```

  • Instantiated a client by giving it a name and version.
  • ```typescript

    const client = new Client(

    {

    name: "example-client",

    version: "1.0.0"

    });

    ```

  • Connected the client to the chosen transport.
  • ```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:

  • Imported the needed libraries
  • Instantiated a server parameters object as we will use this to run the server so we can connect to it with our client.
  • Defined a method run that in turn calls stdio_client which starts a client session.
  • Created an entry point where we provide the 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:

  • Imported the needed libraries.
  • Create an stdio transport and created a client mcpClient. The latter is something we will use to list and invoke features on the MCP Server.
  • Note, in "Arguments", you can either point to the *.csproj* or to the executable.

    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:

  • Created a main method that sets up an SSE transport pointing to http://localhost:8080 where our MCP server will be running.
  • Created a client class that takes the transport as a constructor parameter.
  • In the run method, we create a synchronous MCP client using the transport and initialize the connection.
  • Used SSE (Server-Sent Events) transport which is suitable for HTTP-based communication with Java Spring Boot MCP servers.
  • 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:

  • Called listTools() to get all available tools from the MCP server.
  • Used ping() to verify that the connection to the server is working.
  • The 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:

  • Read a resource, we call the resource by calling 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.

  • Call a tool, we call it by specifying its name and its arguments like so:
  • ```typescript

    const result = await client.callTool({

    name: "example-tool",

    arguments: {

    arg1: "value"

    }

    });

    ```

  • Get prompt, to get a prompt, you call 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:

  • Called a resource called greeting using read_resource.
  • Invoked a tool called add using call_tool.
  • .NET

    1. Let's add some code to call a tool:

    ```csharp

    var result = await mcpClient.CallToolAsync(

    "Add",

    new Dictionary() { ["a"] = 1, ["b"] = 3 },

    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:

  • Called multiple calculator tools using callTool() method with CallToolRequest objects.
  • Each tool call specifies the tool name and a Map of arguments required by that tool.
  • The server tools expect specific parameter names (like "a", "b" for mathematical operations).
  • Results are returned as 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:

  • Complete client implementation with all features from the tutorial
  • Working project structure with proper dependencies and configuration
  • Build and run scripts for easy setup and execution
  • Detailed README with language-specific instructions
  • Error handling and result processing examples
  • 📖 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:

  • TypeScript

    Running this sample

    You're recommended to install 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
    
    

    You should see a result similar to:

    
    Prompt:  {
    
      type: 'text',
    
      text: 'Please review this code:\n\nconsole.log("hello");'
    
    }
    
    Resource template:  file
    
    Tool result:  { content: [ { type: 'text', text: '9' } ] }
    
    
  • Python

    Running this sample

    You're recommended to install 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
    
    

    You should see an output similar to:

    
    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)]
    
    
  • .NET
  • Java

    MCP Java Client - Calculator Demo

    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.

    Prerequisites

    Before running this client, you need to:

    1. Start the Calculator Server from Chapter 01:

    - Navigate to the calculator server directory: 03-GettingStarted/01-first-server/solution/java/

    - 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 http://localhost:8080

    2. Java 21 or higher installed on your system

    3. Maven (included via Maven Wrapper)

    What is the SDKClient?

    The SDKClient is a Java application that demonstrates how to:

  • Connect to an MCP server using Server-Sent Events (SSE) transport
  • List available tools from the server
  • Call various calculator functions remotely
  • Handle responses and display results
  • How It Works

    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

    Project Structure

    
    src/
    
    └── main/
    
        └── java/
    
            └── com/
    
                └── microsoft/
    
                    └── mcp/
    
                        └── sample/
    
                            └── client/
    
                                └── SDKClient.java    # Main client implementation
    
    

    Key Dependencies

    The project uses the following key dependencies:

    
    <dependency>
    
        <groupId>org.springframework.ai</groupId>
    
        <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
    
    </dependency>
    
    

    This dependency provides:

  • McpClient - The main client interface
  • WebFluxSseClientTransport - SSE transport for web-based communication
  • MCP protocol schemas and request/response types
  • Building the Project

    Build the project using the Maven wrapper:

    
    .\mvnw clean install
    
    

    Running the Client

    
    java -jar .\target\calculator-client-0.0.1-SNAPSHOT.jar
    
    

    Note: Make sure the calculator server is running on http://localhost:8080 before executing any of these commands.

    What the Client Does

    When you run the client, it will:

    1. Connect to the calculator server at http://localhost:8080

    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

    Expected 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]
    
    

    Note: You may see Maven warnings about lingering threads at the end - this is normal for reactive applications and doesn't indicate an error.

    Understanding the Code

    1. Transport Setup

    
    var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
    
    

    This creates an SSE (Server-Sent Events) transport that connects to the calculator server.

    2. Client Creation

    
    var client = McpClient.sync(this.transport).build();
    
    client.initialize();
    
    

    Creates a synchronous MCP client and initializes the connection.

    3. Calling Tools

    
    CallToolResult resultAdd = client.callTool(new CallToolRequest("add", Map.of("a", 5.0, "b", 3.0)));
    
    

    Calls the "add" tool with parameters a=5.0 and b=3.0.

    Troubleshooting

    Server Not Running

    If you get connection errors, make sure the calculator server from Chapter 01 is running:

    
    Error: Connection refused
    
    

    Solution: Start the calculator server first.

    Port Already in Use

    If port 8080 is busy:

    
    Error: Address already in use
    
    

    Solution: Stop other applications using port 8080 or modify the server to use a different port.

    Build Errors

    If you encounter build errors:

    
    .\mvnw clean install -DskipTests
    
    

    Learn More

  • Spring AI MCP Documentation
  • Model Context Protocol Specification
  • Spring WebFlux Documentation
  • Rust
  • 🎯 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

    Language File Description ---------- ------ ------------- Java client_example_java.java Complete Java client using SSE transport with comprehensive error handling C# client_example_csharp.cs Complete C# client using stdio transport with automatic server startup TypeScript client_example_typescript.ts Complete TypeScript client with full MCP protocol support Python client_example_python.py Complete Python client using async/await patterns Rust client_example_rust.rs Complete Rust client using Tokio for async operations

    Each complete example includes:

  • Connection establishment and error handling
  • Server discovery (tools, resources, prompts where applicable)
  • Calculator operations (add, subtract, multiply, divide, help)
  • Result processing and formatted output
  • Comprehensive error handling
  • Clean, documented code with step-by-step comments
  • 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

    Solution Folder Complete Examples -------------------- --------------------- Full project structure with build files Single-file implementations Ready-to-run with dependencies Focused code examples Production-like setup Educational reference Language-specific tooling Cross-language comparison

    Both approaches are valuable - use the solution folder for complete projects and the complete examples for learning and reference.

    Key Takeaways

    The key takeaways for this chapter is the following about clients:

  • Can be used to both discover and invoke features on the server.
  • Can start a server while it starts itself (like in this chapter) but clients can connect to running servers as well.
  • Is a great way to test out server capabilities next to alternatives like the Inspector as was described in the previous chapter.
  • Additional Resources

  • Building clients in MCP
  • Samples

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Rust Calculator
  • What's Next

  • Next: Creating a client with an LLM
  • 3 Client with LLM, an even better way of writing a client is by adding an LLM to it so it can "negotiate" with your server on what to do, to the lesson

    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:

  • Create a client with an LLM.
  • Seamlessly interact with an MCP server using an LLM.
  • Provide a better end user experience on the client side.
  • 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:

  • Go to GitHub Settings – Click on your profile picture in the top right corner and select Settings.
  • Navigate to Developer Settings – Scroll down and click on Developer Settings.
  • Select Personal Access Tokens – Click on Fine-grained tokens and then Generate new token.
  • Configure Your Token – Add a note for reference, set an expiration date, and select the necessary scopes (permissions). In this case be sure to add the Models permission.
  • Generate and Copy the Token – Click Generate token, and make sure to copy it immediately, as you won’t be able to see it again.
  • -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:

  • Imported the needed libraries
  • Create a class with two members, client and openai that will help us manage a client and interact with an LLM respectively.
  • Configured our LLM instance to use GitHub Models by setting 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:

  • Imported the needed libraries for MCP
  • Created a client
  • .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:

  • Added LangChain4j dependencies: Required for MCP integration, OpenAI official client, and GitHub Models support
  • Imported the LangChain4j libraries: For MCP integration and OpenAI chat model functionality
  • Created a ChatLanguageModel: Configured to use GitHub Models with your GitHub token
  • Set up HTTP transport: Using Server-Sent Events (SSE) to connect to the MCP server
  • Created an MCP client: That will handle communication with the server
  • Used LangChain4j's built-in MCP support: Which simplifies integration between LLMs and MCP servers
  • Rust

    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:

  • Added code for connecting to the server, connectToServer.
  • Created a 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:

  • Listing resources and tools and printed them. For tools we also list 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:

  • Listed the tools available on the MCP Server
  • For each tool, listed name, description and its schema. The latter is something we will use to call the tools shortly.
  • 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:

  • Created a McpToolProvider that automatically discovers and registers all tools from the MCP server
  • The tool provider handles the conversion between MCP tool schemas and LangChain4j's tool format internally
  • This approach abstracts away the manual tool listing and conversion process
  • Rust

    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:

  • Created a function ConvertFrom that takes, name, description and input schema.
  • Defined functionality that creates a FunctionDefinition that gets passed to a ChatCompletionsDefinition. The latter is something the LLM can understand.
  • 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 toolDefinitions = new 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:

  • Defined a simple Bot interface for natural language interactions
  • Used LangChain4j's AiServices to automatically bind the LLM with the MCP tool provider
  • The framework automatically handles tool schema conversion and function calling behind the scenes
  • This approach eliminates manual tool conversion - LangChain4j handles all the complexity of converting MCP tools to LLM-compatible format
  • Rust

    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>(call.Arguments);

    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:

  • Used simple natural language prompts to interact with the MCP server tools
  • The LangChain4j framework automatically handles:
  • - 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

  • The bot.chat() method returns natural language responses that may include results from MCP tool executions
  • This approach provides a seamless user experience where users don't need to know about the underlying MCP implementation
  • Complete 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

  • Adding an LLM to your client provides a better way for users to interact with MCP Servers.
  • You need to convert the MCP Server response to something the LLM can understand.
  • Samples

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Rust Calculator
  • Additional Resources

    What's Next

  • Next: Consuming a server using Visual Studio Code
  • 4 Consuming a server GitHub Copilot Agent mode in Visual Studio Code. Here, we're looking at running our MCP Server from within Visual Studio Code, to the lesson

    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:

  • Consume an MCP Server via Visual Studio Code.
  • Run capabilities like tools via GitHub Copilot.
  • Configure Visual Studio Code to find and manage your MCP Server.
  • Usage

    You can control your MCP server in two different ways:

  • User interface, you will see how this is done later in this chapter.
  • Terminal, it's possible to control things from the terminal using the 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:

  • Configure a file to find our MCP Server.
  • Start up/Connect to said server to have it list its capabilities.
  • Use said capabilities through GitHub Copilot Chat interface.
  • 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:

  • Visual Studio Code is a great client that lets you consume several MCP Servers and their tools.
  • GitHub Copilot Chat interface is how you interact with the servers.
  • You can prompt the user for inputs like API keys that can be passed to the MCP Server when configuring the server entry in *mcp.json* file.
  • Samples

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Additional Resources

  • Visual Studio docs
  • What's Next

  • Next: Creating a stdio Server
  • 5 stdio Transport Server stdio transport is the recommended standard for local MCP server-to-client communication, providing secure subprocess-based communication with built-in process isolation to the lesson

    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:

  • Build an MCP Server using the stdio transport.
  • Debug an MCP Server using the Inspector.
  • Consume an MCP Server using Visual Studio Code.
  • Understand the current MCP transport mechanisms and why stdio is recommended.
  • 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:

  • Simple Communication: The server reads JSON-RPC messages from standard input (stdin) and sends messages to standard output (stdout).
  • Process-based: The client launches the MCP server as a subprocess.
  • Message Format: Messages are individual JSON-RPC requests, notifications, or responses, delimited by newlines.
  • Logging: The server MAY write UTF-8 strings to standard error (stderr) for logging purposes.
  • Key Requirements:

  • Messages MUST be delimited by newlines and MUST NOT contain embedded newlines
  • The server MUST NOT write anything to stdout that is not a valid MCP message
  • The client MUST NOT write anything to the server's stdin that is not a valid MCP message
  • 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);
    
    

    In the preceding code:

  • We import the Server class and StdioServerTransport from the MCP SDK
  • We create a server instance with basic configuration and capabilities
  • We create a StdioServerTransport instance and connect the server to it, enabling communication over stdin/stdout
  • Python

    
    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:

  • Create a server instance using the MCP SDK
  • Define tools using decorators
  • Use the stdio_server context manager to handle the transport
  • .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:

  • Don't require web server setup or HTTP endpoints
  • Are launched as subprocesses by the client
  • Communicate through stdin/stdout streams
  • Are simpler to implement and debug
  • Exercise: Creating a stdio Server

    To create our server, we need to keep two things in mind:

  • We need to use a web server to expose endpoints for connection and messages.
  • 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

  • Python 3.8 or later
  • MCP Python SDK: pip install mcp
  • Basic understanding of async programming
  • Let'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):

  • Simple subprocess model - client launches server as child process
  • Communication via stdin/stdout using JSON-RPC messages
  • No HTTP server setup required
  • Better performance and security
  • Easier debugging and development
  • SSE Transport (Deprecated as of MCP 2025-06-18):

  • Required HTTP server with SSE endpoints
  • More complex setup with web server infrastructure
  • Additional security considerations for HTTP endpoints
  • Now replaced by Streamable HTTP for web-based scenarios
  • 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
    
    
    
    
  • 6 HTTP Streaming with MCP (Streamable HTTP). Learn about modern HTTP streaming transport (the recommended approach for remote MCP servers per MCP Specification 2025-11-25), progress notifications, and how to implement scalable, real-time MCP servers and clients using Streamable HTTP. to the lesson

    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:

  • stdio: Standard input/output, suitable for local and CLI-based tools. Simple but not suitable for web or cloud.
  • SSE (Server-Sent Events): Allows servers to push real-time updates to clients over HTTP. Good for web UIs, but limited in scalability and flexibility. As of MCP Specification 2025-06-18, the standalone SSE (Server-Sent Events) transport has been deprecated and replaced by "Streamable HTTP" transport.
  • Streamable HTTP: Modern HTTP-based streaming transport, supporting notifications and better scalability. Recommended for most production and cloud scenarios.
  • Comparison Table

    Have a look at the comparison table below to understand the differences between these transport mechanisms:

    Transport Real-time Updates Streaming Scalability Use Case ------------------- ------------------ ----------- ------------- ------------------------- stdio No No Low Local CLI tools SSE Yes Yes Medium Web, real-time updates Streamable HTTP Yes Yes High Cloud, multi-client

    > 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:

  • Large files or datasets.
  • Real-time updates (e.g., chat, progress bars).
  • Long-running computations where you want to keep the user informed.
  • Here's what you need to know about streaming at high level:

  • Data is delivered progressively, not all at once.
  • The client can process data as it arrives.
  • Reduces perceived latency and improves user experience.
  • Why use streaming?

    The reasons for using streaming are the following:

  • Users get feedback immediately, not just at the end
  • Enables real-time applications and responsive UIs
  • More efficient use of network and compute resources
  • 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:

  • The server yields each message as it is ready.
  • The client receives and prints each chunk as it arrives.
  • Requirements:

  • The server must use a streaming response (e.g., StreamingResponse in FastAPI).
  • The client must process the response as a stream (stream=True in requests).
  • Content-Type is usually 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:

  • Uses Spring Boot's reactive stack with Flux for streaming
  • ServerSentEvent provides structured event streaming with event types
  • WebClient with bodyToFlux() enables reactive streaming consumption
  • delayElements() simulates processing time between events
  • Events can have types (info, result) for better client handling
  • Comparison: 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:

    Feature Classic HTTP Streaming MCP Streaming (Notifications) ------------------------ ------------------------------- ------------------------------------- Main response Chunked Single, at end Progress updates Sent as data chunks Sent as notifications Client requirements Must process stream Must implement message handler Use case Large files, AI token streams Progress, logs, real-time feedback

    Key Differences Observed

    Additionally, here are some key differences:

  • Communication Pattern:
  • - Classic HTTP streaming: Uses simple chunked transfer encoding to send data in chunks

    - MCP streaming: Uses a structured notification system with JSON-RPC protocol

  • Message Format:
  • - Classic HTTP: Plain text chunks with newlines

    - MCP: Structured LoggingMessageNotification objects with metadata

  • Client Implementation:
  • - Classic HTTP: Simple client that processes streaming responses

    - MCP: More sophisticated client with a message handler to process different types of messages

  • Progress Updates:
  • - 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.

  • For simple streaming needs: Classic HTTP streaming is simpler to implement and sufficient for basic streaming needs.
  • For complex, interactive applications: MCP streaming provides a more structured approach with richer metadata and separation between notifications and final results.
  • For AI applications: MCP's notification system is particularly useful for long-running AI tasks where you want to keep users informed of progress.
  • 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:

    Level Description Example Use Case ----------- ------------------------------- --------------------------------- debug Detailed debugging information Function entry/exit points info General informational messages Operation progress updates notice Normal but significant events Configuration changes warning Warning conditions Deprecated feature usage error Error conditions Operation failures critical Critical conditions System component failures alert Action must be taken immediately Data corruption detected emergency System is unusable Complete system failure

    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:

  • Better user experience: Users see updates as work progresses, not just at the end.
  • Real-time feedback: Clients can display progress bars or logs, making the app feel responsive.
  • Easier debugging and monitoring: Developers and users can see where a process might be slow or stuck.
  • How to Implement Progress Notifications

    Here's how you can implement progress notifications in MCP:

  • On the server: Use 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.
  • On the client: Implement a message handler that listens for and displays notifications as they arrive. This handler distinguishes between notifications and the final result.
  • 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 Validation: Always validate the Origin header to prevent DNS rebinding attacks.
  • Localhost Binding: For local development, bind servers to localhost to avoid exposing them to the public internet.
  • Authentication: Implement authentication (e.g., API keys, OAuth) for production deployments.
  • CORS: Configure Cross-Origin Resource Sharing (CORS) policies to restrict access.
  • HTTPS: Use HTTPS in production to encrypt traffic.
  • Best Practices

  • Never trust incoming requests without validation.
  • Log and monitor all access and errors.
  • Regularly update dependencies to patch security vulnerabilities.
  • Challenges

  • Balancing security with ease of development
  • Ensuring compatibility with various client environments
  • 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:

  • Streamable HTTP offers better scalability, compatibility, and richer notification support than SSE.
  • It is the recommended transport for new MCP applications.
  • Migration Steps

    Here's how you can migrate from SSE to Streamable HTTP in your MCP applications:

  • Update server code to use transport="streamable-http" in mcp.run().
  • Update client code to use streamablehttp_client instead of SSE client.
  • Implement a message handler in the client to process notifications.
  • Test for compatibility with existing tools and workflows.
  • Maintaining Compatibility

    It's recommended to maintain compatibility with existing SSE clients during the migration process. Here are some strategies:

  • You can support both SSE and Streamable HTTP by running both transports on different endpoints.
  • Gradually migrate clients to the new transport.
  • Challenges

    Ensure you address the following challenges during migration:

  • Ensuring all clients are updated
  • Handling differences in notification delivery
  • 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 Validation: Always validate the Origin header to prevent DNS rebinding attacks.
  • Localhost Binding: For local development, bind servers to localhost to avoid exposing them to the public internet.
  • Authentication: Implement authentication (e.g., API keys, OAuth) for production deployments.
  • CORS: Configure Cross-Origin Resource Sharing (CORS) policies to restrict access.
  • HTTPS: Use HTTPS in production to encrypt traffic.
  • Best Practices

    Additionally, here are some best practices to follow when implementing security in your MCP streaming server:

  • Never trust incoming requests without validation.
  • Log and monitor all access and errors.
  • Regularly update dependencies to patch security vulnerabilities.
  • Challenges

    You will face some challenges when implementing security in MCP streaming servers:

  • Balancing security with ease of development
  • Ensuring compatibility with various client environments
  • 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

  • Microsoft: Introduction to HTTP Streaming
  • Microsoft: Server-Sent Events (SSE)
  • Microsoft: CORS in ASP.NET Core
  • Python requests: Streaming Requests
  • What Next?

  • Try building more advanced MCP tools that use streaming for real-time analytics, chat, or collaborative editing.
  • Explore integrating MCP streaming with frontend frameworks (React, Vue, etc.) for live UI updates.
  • Next: Utilising AI Toolkit for VSCode
  • 7 Utilising AI Toolkit for VSCode to consume and test your MCP Clients and Servers to the lesson

    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:

  • Consume an MCP server via the AI Toolkit.
  • Configure an agent configuration to enable it to discover and utilize tools provided by the MCP server.
  • Utilize MCP tools via natural language.
  • Approach

    Here's how we need to approach this at a high level:

  • Create an agent and define its system prompt.
  • Create a MCP server with calculator tools.
  • Connect the Agent Builder to the MCP server.
  • Test the agent's tool invocation via natural language.
  • Great, now that we understand the flow, let's configure an AI agent to leverage external tools through MCP, enhancing its capabilities!

    Prerequisites

  • Visual Studio Code
  • AI Toolkit for Visual Studio Code
  • 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:

  • The AI Toolkit extension is a great client that lets you consume MCP Servers and their tools.
  • You can add new tools to MCP servers, expanding the agent's capabilities to meet evolving requirements.
  • The AI Toolkit includes templates (e.g., Python MCP server templates) to simplify the creation of custom tools.
  • Additional Resources

  • AI Toolkit docs
  • What's Next

  • Next: Testing & Debugging
  • 8 Testing. Here we will focus especially how we can test out our server and client in different ways, to the lesson

    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:

  • Describe various approaches for testing.
  • Use different tools to effectively test your code.
  • Testing MCP Servers

    MCP provides tools to help you test and debug your servers:

  • MCP Inspector: A command line tool that can be run both as a CLI tool and as a visual tool.
  • Manual testing: You can use a tool like curl to run web requests, but any tool capabable of running HTTP will do.
  • Unit testing: It's possible to use your preferred testing framework to test the features of both server and client.
  • 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:

  • Discover Server Capabilities: Automatically detect available resources, tools, and prompts
  • Test Tool Execution: Try different parameters and see responses in real-time
  • View Server Metadata: Examine server info, schemas, and configurations
  • 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:

  • Leverages pytest framework which lets you create tests as functions and use assert statements.
  • Creates an MCP Server with two different tools.
  • Uses 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

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Additional Resources

  • Python SDK
  • What's Next

  • Next: Deployment
  • 9 Deployment. This chapter will look at different ways of deploying your MCP solutions, to the lesson

    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:

  • Evaluate different deployment approaches.
  • Deploy your app.
  • 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:

  • Serverless Functions: Deploy lightweight MCP servers as serverless functions
  • Container Services: Use services like Azure Container Apps, AWS ECS, or Google Cloud Run
  • Kubernetes: Deploy and manage MCP servers in Kubernetes clusters for high availability
  • 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 -n weather-mcp --environment mcp -l westus --env-vars API_KEYS= --source .

    ```

    There you have it, deploy it locally, deploy it to Azure through these steps.

    Additional Resources

  • Azure Functions + MCP
  • Azure Container Apps article
  • Azure Container Apps MCP repo
  • What's Next

  • Next: Advanced Server Topics
  • 10 Advanced server usage. This chapter covers advanced server usage, to the lesson

    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:

  • Better architecture. It's possible to create a clean architecture with both the regular server and a low-level server but it can be argued that it's slightly easier with a low-level server.
  • Feature availability. Some advanced features can only be used with a low-level server. You will see this in later chapters as we add sampling and elicitation.
  • 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:

  • Listing all tools. One function would be responsible for all attempts to list tools.
  • handle calling all tools. Here also, there's only one function handling calls to a tool
  • 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:

  • Move the logic for creating a feature (tool, resource or prompt) to its dedicated folder.
  • Add a way to validate an incoming request asking to for example call a tool.
  • 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:

  • Create a schema using Pydantic AddInputModel with fields a and b in file *schema.py*.
  • Attempt to parse the incoming request to be of type 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;
    
    
  • In the handler dealing with all tool calls, we now try to parse the incoming request into the tool's defined schema:
  • ```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:

  • name, this is the name of the tool.
  • rawSchema, this is the Zod schema, it will be used to validate incoming requests to call this tool.
  • inputSchema, this schema will be used by the handler.
  • callback, this is used to invoke the tool.
  • 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:

  • Our tool name is already present as the input parameter name which is true for our arguments in the form of the arguments dictionary.
  • The tool is called with 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

  • Python

    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
    
    
  • TypeScript

    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]"
    
      }
    
    }
    
    
  • here

    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

  • Next: Simple Authentication
  • 11 Auth. This chapter covers how to add simple auth, from Basic Auth to using JWT and RBAC. You're encouraged to start here and then look at Advanced Topics in Chapter 5 and perform additional security hardening via recommendations in Chapter 2, to the lesson

    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:

  • Authentication, which is the process of figuring out whether we let a person enter our house, that they have the right to be "here" that is have access to our resource server where our MCP Server features live.
  • Authorization, is the process of finding out if a user should have access to these specific resources they're asking for, for example these orders or these products or whether they're allowed to read the content but not delete as another example.
  • 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:

  • Created a middleware called AuthMiddleware where its dispatch method is being invoked by the web server.
  • Added the middleware to the web server:
  • ```python

    starlette_app.add_middleware(AuthMiddleware)

    ```

  • Written validation logic that checks if Authorization header is present and if the secret being sent is valid:
  • ```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

  • Create a web server and MCP instance.
  • Implement a middleware for the server.
  • Client

  • Send web request, with credential, via header.
  • -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:

  • Create the MCP Server.
  • Construct the the starlette web app from the MCP Server, app.streamable_http_app().
  • Host and server the web app using uvicorn 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:

  • The request 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:

  • Interact with the request variable to check the passed credential in the Authorization property.
  • Validate the credential, and if so let the request continue and have the client's MCP request do what it should (e.g list tools, read resource or anything other MCP related).
  • 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?

  • Security improvements. In basic auth, you send the username and password as a base64 encoded token (or you send an API key) over and over which increases the risk. With JWT, you send your username and password and get a token in return and it's also time bound meaning it will expire. JWT lets you easily use fine-grained access control using roles, scopes and permissions.
  • Statelessness and scalability. JWTs are self-contained, they carry all user info and eliminate the need to store server-side session storage. Token can also be validated locally.
  • Interoperability and federation. JWTs is central of Open ID Connect and is used with known identity providers like Entra ID, Google Identity and Auth0. They also make it possible to use single sign on and much more making it enterprise-grade.
  • Modularity and flexibility. JWTs can also be used with API Gateways like Azure API Management, NGINX and more. It also supports use authentication scenarios and server-to-service communication including impersonation and delegation scenarios.
  • Performance and caching. JWTs can be cached after decoding which reduces the need for parsing. This helps specifically with high-traffic apps as it improves throughput and reduces load on your chosen infrastructure.
  • Advanced features. It also supports introspection (checking validity on server) and revocation (making a token invalid).
  • 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:

  • Learn to construct a JWT token and make it ready for being sent from client to server.
  • Validate a JWT token, and if so, let the client have our resources.
  • Secure token storage. How we store this token.
  • Protect the routes. We need to protect the routes, in our case, we need to protect routes and specific MCP features.
  • Add refresh tokens. Ensure we create tokens that are short-lived but refresh tokens that are long-lived that can be used to acquire new tokens if they expire. Also ensure there's a refresh endpoint and a rotation strategy.
  • -1- Construct a JWT token

    First off, a JWT token has the following parts:

  • header, algorithm used and token type.
  • payload, claims, like sub (the user or entity the token represents. In an auth scenario this is typically the userid), exp (when it expires) role (the role)
  • signature, signed with a secret or private key.
  • 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:

  • Defined a header using HS256 as algorithm and type to be JWT.
  • Constructed a payload that contains a subject or user id, a username, a role, when it was issued and when it's set to expire thereby implementing the time bound aspect we mentioned earlier.
  • 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:

  • Admin.Write
  • User.Read
  • Guest.Read
  • 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:

  • Add a check for each tool, resource, prompt where you need to check permission level.
  • 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} }]

    };

    }

    );

    ```

  • Use advanced server approach and the request handlers so you minimize how many places you need to make the check.
  • 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

  • Next: Setting Up MCP Hosts
  • 12 MCP Hosts. Configure and use popular MCP host clients including Claude Desktop, Cursor, Cline, and Windsurf. Learn transport types and troubleshooting, to the lesson

    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

  • An MCP server to connect to (see Module 3.1 - First Server)
  • The host application installed on your system
  • Basic familiarity with JSON configuration files
  • ---

    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:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json
  • Example 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

    Field Description Example ------- ------------- --------- command The executable to run "python", "node", "npx" args Command line arguments ["-m", "my_server"] env Environment variables {"API_KEY": "xxx"} cwd Working directory "/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:

  • Check the configuration file syntax with a JSON validator
  • Ensure the command path is correct
  • Check Claude Desktop logs: Help → Show Logs
  • Server crashes on startup:

  • Test your server manually in terminal first
  • Check environment variables are set correctly
  • Ensure all dependencies are installed
  • ---

    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:

  • Check Output panel → "MCP" for error logs
  • Reload window: Ctrl+Shift+P → "Developer: Reload Window"
  • Verify the server runs standalone first
  • ---

    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:

  • macOS: ~/.cursor/mcp.json
  • Windows: %USERPROFILE%\.cursor\mcp.json
  • Linux: ~/.cursor/mcp.json
  • Example 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:

    Host stdio SSE/HTTP WebSocket ------ ------- ---------- ----------- Claude Desktop ✅ ❌ ❌ VS Code ✅ ✅ ❌ Cursor ✅ ✅ ❌ Cline ✅ ✅ ❌ Windsurf ✅ ✅ ❌

    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

  • Some hosts sanitize environment variables
  • Use the env configuration field explicitly
  • Avoid sensitive data in config files (use secrets management)
  • ---

    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

  • 3.13 - Debugging with MCP Inspector
  • 3.1 - Create your first MCP server
  • Module 5 - Advanced Topics
  • ---

    Additional Resources

  • Claude Desktop MCP Documentation
  • VS Code MCP Extension
  • MCP Specification - Transports
  • Official MCP Servers Registry
  • 13 MCP Inspector. Debug and test your MCP servers interactively using the MCP Inspector tool. Learn to troubleshoot tools, resources, and protocol messages, to the lesson

    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:

  • "Is my server even running?" - Inspector shows connection status
  • "Are my tools registered correctly?" - Inspector lists all available tools
  • "What's the response format?" - Inspector displays full JSON responses
  • "Why isn't this tool working?" - Inspector shows detailed error messages
  • Prerequisites

  • Node.js 18+ installed
  • npm (comes with Node.js)
  • An MCP server to test (see Module 3.1 - First Server)
  • 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:

    Code Meaning ------ --------- -32700 Parse error (invalid JSON) -32600 Invalid request -32601 Method not found -32602 Invalid params -32603 Internal error

    ---

    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

  • Request/Response pairs: Each should have a matching
  • Error messages: Look for "error" in responses
  • Timing: Large gaps might indicate performance issues
  • Protocol version: Ensure server and client agree on version
  • ---

    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:

  • Module 4: Practical Implementation
  • ---

    Additional Resources

  • MCP Inspector GitHub Repository
  • MCP Specification - Protocol Messages
  • JSON-RPC 2.0 Specification
  • 14 Sampling. Create MCP Servers that collaborate with MCP clients on LLM related tasks. to the lesson

    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:

  • Explain what Sampling is and when to use it.
  • Show how to configure Sampling in MCP.
  • Provide examples of Sampling in action.
  • 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:

  • Prompt, under content -> text, is our prompt that is an instruction for the LLM to summarize blog post content.
  • modelPreferences. This section is just that, a preference, a recommendation of what configuration to use with the LLM. The user can choose whether to go with these recommendations or change them. In this case there are recommendations on model to use and speed and intelligence priority.
  • systemPrompt, this is your normal system prompt that gives your LLM a personality and contains guidance instructions.
  • maxTokens, this is another property that's used to say how many tokens are recommended to use for this task.
  • 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

  • Chapter 4 - Practical implementation
  • 15 MCP Apps. Build MCP Servers that also reply with UI instructions, to the lesson

    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:

  • Explain what MCP Apps are.
  • When to use MCP Apps.
  • Build and integrate your own MCP Apps.
  • 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 tools we want to interact with.
  • Define the component.
  • 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:

  • A frontend written in pure HTML.
  • Code that handles events and what to do, e.g calling tools or messaging the parent window.
  • 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:

  • There should be a button and an input element where the user types a keyword to search for for example "Shipping". This should call a tool on the backend that does a search in the FAQ data.
  • A tool that supports the mentioned FAQ search.
  • 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:

  • Create references to the interactive UI elements.
  • Handle a button click to parse out the input element value and we also call 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

  • TypeScript

    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://.app.github.dev/mcp

    Choice -1 Test the app in Visual Studio Code

    To test the solution in Visual Studio Code, do the following:

  • Add a server entry to 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:

    !Visual Studio Code MCP Apps

    Choice -2- Test the app with a host

    The repo contains several different hosts that you can use to test your MVP Apps.

    We will present you with two different options here:

    Local machine

  • Navigate to *ext-apps* after you've cloned the repo.
  • Install dependencies
  • ```sh

    npm install

    ```

  • In a separate terminal window, navigate to *ext-apps/examples/basic-host*
  • > 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

  • Run the host:
  • ```sh

    npm start

    ```

    This should connect the host with backend and you should see the app running like so:

    !App running

    Codespace

    It takes a bit of extra work to get a Codespace environment to work. To use a host through Codespace:

  • See the *ext-apps* directory and navigate to *examples/basic-host*.
  • Run npm install to install dependencies
  • Run npm start to start the host.
  • Test out the app

    Try the app in the following way:

  • Select "Call Tool" button and you should see the results like so:
  • !Tool result

    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:

  • a drop down list with options
  • a button to submit a choice
  • a label showing who picked what and who won
  • Server:

  • should have a tool rock paper scissor tool that takes "choice" as input. It should also render a computer choice and determine winner
  • 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:

  • MCP Apps is a new standard that can be useful when you want to ship both data and UI features.
  • These types of apps run in an IFrame for security reasons.
  • What's Next

  • Chapter 4
  • 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:

  • Set up development environments for MCP in C#, Java, Python, TypeScript, and JavaScript
  • Build and deploy basic MCP servers with custom features (resources, prompts, and tools)
  • Create host applications that connect to MCP servers
  • Test and debug MCP implementations
  • Understand common setup challenges and their solutions
  • Connect your MCP implementations to popular LLM services
  • 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:

  • Development Environment: For your chosen language (C#, Java, Python, TypeScript, or JavaScript)
  • IDE/Editor: Visual Studio, Visual Studio Code, IntelliJ, Eclipse, PyCharm, or any modern code editor
  • Package Managers: NuGet, Maven/Gradle, pip, or npm/yarn
  • API Keys: For any AI services you plan to use in your host applications
  • 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):

  • C# SDK - Maintained in collaboration with Microsoft
  • Java SDK - Maintained in collaboration with Spring AI
  • TypeScript SDK - The official TypeScript implementation
  • Python SDK - The official Python implementation (FastMCP)
  • Kotlin SDK - The official Kotlin implementation
  • Swift SDK - Maintained in collaboration with Loopwork AI
  • Rust SDK - The official Rust implementation
  • Go SDK - The official Go implementation
  • Key Takeaways

  • Setting up an MCP development environment is straightforward with language-specific SDKs
  • Building MCP servers involves creating and registering tools with clear schemas
  • MCP clients connect to servers and models to leverage extended capabilities
  • Testing and debugging are essential for reliable MCP implementations
  • Deployment options range from local development to cloud-based solutions
  • 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

  • Java Calculator

    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:

  • Support for SSE (Server-Sent Events)
  • Automatic tool registration using Spring AI's @Tool annotation
  • Basic calculator functions:
  • - 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 together
  • subtract(a, b): Subtract the second number from the first
  • multiply(a, b): Multiply two numbers
  • divide(a, b): Divide the first number by the second (with zero check)
  • power(base, exponent): Calculate the power of a number
  • squareRoot(number): Calculate the square root (with negative number check)
  • modulus(a, b): Calculate the remainder when dividing
  • absolute(number): Calculate the absolute value
  • help(): Get information about available operations
  • Test 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

    dev.langchain4j

    langchain4j-github

    ${langchain4j.version}

    ```

    3. Ensure the calculator server is running on localhost:8080

    Running the LangChain4j Client

    This example demonstrates:

  • Connecting to the calculator MCP server via SSE transport
  • Using LangChain4j to create a chat bot that leverages calculator operations
  • Integrating with GitHub AI models (now using phi-4 model)
  • 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:

  • Build a multi-stage Docker image with Maven 3.9.9 and Eclipse Temurin 24 JDK
  • Create an optimized container image
  • Expose the service on port 8080
  • Start the MCP calculator service inside the container
  • 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.

  • .Net Calculator
  • JavaScript Calculator

    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
    
    
  • TypeScript Calculator

    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
    
    
  • Python Calculator
  • Additional Resources

  • Build Agents using Model Context Protocol on Azure
  • Remote MCP with Azure Container Apps (Node.js/TypeScript/JavaScript)
  • .NET OpenAI MCP Agent
  • 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:

  • Set up development environments for MCP in C#, Java, Python, TypeScript, and Rust
  • Build and deploy basic MCP servers with custom features (resources, prompts, and tools)
  • Create host applications that connect to MCP servers
  • Test and debug MCP implementations
  • 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:

  • Development Environment: For your chosen language (C#, Java, Python, TypeScript, or Rust)
  • IDE/Editor: Visual Studio, Visual Studio Code, IntelliJ, Eclipse, PyCharm, or any modern code editor
  • Package Managers: NuGet, Maven/Gradle, pip, npm/yarn, or Cargo
  • API Keys: For any AI services you plan to use in your host applications
  • Basic MCP Server Structure

    An MCP server typically includes:

  • Server Configuration: Setup port, authentication, and other settings
  • Resources: Data and context made available to LLMs
  • Tools: Functionality that models can invoke
  • Prompts: Templates for generating or structuring text
  • 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:

  • Import the necessary classes from the MCP TypeScript SDK.
  • Create and configure a new MCP server instance.
  • Register a custom tool (calculator) with a handler function.
  • Start the server to listen for incoming MCP requests.
  • 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:

  • Inspector tool, this graphical interface allows you to connect to your server and test your tools, prompts and resources.
  • curl, you can also connect to your server using a command line tool like curl or other clients than can create and run HTTP commands.
  • 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

    Issue Possible Solution ------- ------------------- Connection refused Check if server is running and port is correct Tool execution errors Review parameter validation and error handling Authentication failures Verify API keys and permissions Schema validation errors Ensure parameters match the defined schema Server not starting Check for port conflicts or missing dependencies CORS errors Configure proper CORS headers for cross-origin requests Authentication issues Verify token validity and permissions

    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:

  • Access local files and databases
  • Connect to remote APIs
  • Perform computations
  • Integrate with other tools and services
  • Provide a user interface for interaction
  • 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:

  • Install the MCP SDK.
  • Create a a project and set up the project structure.
  • Write the server code.
  • Test the server.
  • -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:

  • Defined a tool add that takes parameters a and b, both integers.
  • Created a resource called 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:

    !Connected

    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:

    !Result of running add

    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:

  • C# SDK - Maintained in collaboration with Microsoft
  • Java SDK - Maintained in collaboration with Spring AI
  • TypeScript SDK - The official TypeScript implementation
  • Python SDK - The official Python implementation
  • Kotlin SDK - The official Kotlin implementation
  • Swift SDK - Maintained in collaboration with Loopwork AI
  • Rust SDK - The official Rust implementation
  • Key Takeaways

  • Setting up an MCP development environment is straightforward with language-specific SDKs
  • Building MCP servers involves creating and registering tools with clear schemas
  • Testing and debugging are essential for reliable MCP implementations
  • Samples

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Rust Calculator
  • 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

  • Build Agents using Model Context Protocol on Azure
  • Remote MCP with Azure Container Apps (Node.js/TypeScript/JavaScript)
  • .NET OpenAI MCP Agent
  • What's next

    Next: Getting Started with MCP Clients

    Once you've completed this module, continue to: Module 4: Practical Implementation

    code Module 03

    Module 03 — 시작하기

    시작하기

    _(위 이미지를 클릭하면 이 강의의 영상을 볼 수 있습니다)_

    이 섹션은 여러 강의로 구성되어 있습니다:

  • 1 Your first server, 첫 번째 강의에서는 첫 서버를 만드는 방법과 검사 도구를 사용해 서버를 확인하는 방법을 배웁니다. 검사 도구는 서버 테스트와 디버깅에 유용합니다, 강의로 가기

    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 모델을 다양한 데이터 소스 및 도구와 연결하는 표준화된 방법을 제공합니다.

    학습 목표

    이 수업을 마치면 다음을 수행할 수 있습니다:

  • C#, Java, Python, TypeScript, Rust에서 MCP 개발 환경 설정
  • 맞춤 기능(리소스, 프롬프트, 도구)을 갖춘 기본 MCP 서버 구축 및 배포
  • MCP 서버에 연결하는 호스트 애플리케이션 생성
  • MCP 구현 테스트 및 디버깅
  • MCP 환경 설정하기

    MCP 작업을 시작하기 전에 개발 환경을 준비하고 기본 작업 흐름을 이해하는 것이 중요합니다. 이 섹션은 MCP 시작을 원활하게 하기 위한 초기 설정 단계를 안내합니다.

    사전 준비 사항

    MCP 개발에 착수하기 전에 다음이 준비되었는지 확인하세요:

  • 개발 환경: 선택한 언어(C#, Java, Python, TypeScript, Rust)용
  • IDE/편집기: Visual Studio, Visual Studio Code, IntelliJ, Eclipse, PyCharm, 또는 최신 코드 편집기
  • 패키지 관리자: NuGet, Maven/Gradle, pip, npm/yarn, Cargo
  • API 키: 호스트 애플리케이션에서 사용할 AI 서비스용
  • 기본 MCP 서버 구조

    MCP 서버는 일반적으로 다음을 포함합니다:

  • 서버 구성: 포트, 인증 및 기타 설정
  • 리소스: LLM에 제공할 데이터 및 컨텍스트
  • 도구: 모델이 호출할 수 있는 기능
  • 프롬프트: 텍스트 생성 또는 구조화 템플릿
  • 아래는 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);
    
    

    위 코드에서 우리는:

  • MCP TypeScript SDK에서 필요한 클래스를 가져왔습니다.
  • 새 MCP 서버 인스턴스를 생성 및 구성했습니다.
  • 핸들러 함수가 있는 사용자 지정 도구(calculator)를 등록했습니다.
  • MCP 요청을 수신하도록 서버를 시작했습니다.
  • 테스트 및 디버깅

    MCP 서버를 테스트하기 전에 이용 가능한 도구 및 디버깅 모범 사례를 이해하는 것이 중요합니다. 효과적인 테스트는 서버가 예상대로 작동하는지 확인하고 문제를 신속히 파악 및 해결하는 데 도움이 됩니다. 다음 섹션에서 MCP 구현을 검증하기 위한 권장 방법을 설명합니다.

    MCP는 서버 테스트 및 디버깅을 도와주는 도구를 제공합니다:

  • Inspector 도구: 이 그래픽 인터페이스는 서버에 연결하여 도구, 프롬프트, 리소스를 테스트할 수 있습니다.
  • curl: 커맨드 라인 도구인 curl이나 다른 HTTP 명령을 생성 및 실행할 수 있는 클라이언트로도 서버에 연결할 수 있습니다.
  • MCP Inspector 사용하기

    1. 서버 기능 탐색: 사용 가능한 리소스, 도구, 프롬프트 자동 감지

    2. 도구 실행 테스트: 다양한 매개변수로 실시간 응답 확인

    3. 서버 메타데이터 조회: 서버 정보, 스키마, 구성 검토

    
    # 예제 TypeScript, MCP Inspector 설치 및 실행
    
    npx @modelcontextprotocol/inspector node build/index.js
    
    

    위 명령어를 실행하면 MCP Inspector가 브라우저에서 로컬 웹 인터페이스를 실행합니다. 등록된 MCP 서버, 사용 가능한 도구, 리소스 및 프롬프트 대시보드를 볼 수 있습니다. 이 인터페이스로 도구 실행 테스트, 서버 메타데이터 조사, 실시간 응답 확인 등이 가능해 MCP 서버 구현 검증 및 디버깅이 수월해집니다.

    다음은 화면 예시입니다:

    일반적인 설정 문제 및 해결책

    문제 가능한 해결책 ------------------------- -------------------------------------------- 연결 거부됨 서버 실행 여부 및 포트 확인 도구 실행 오류 매개변수 검증 및 오류 처리 검토 인증 실패 API 키 및 권한 확인 스키마 검증 오류 매개변수가 정의된 스키마와 일치하는지 확인 서버가 시작되지 않음 포트 충돌 또는 누락된 종속성 점검 CORS 오류 교차 출처 요청에 적절한 CORS 헤더 구성 인증 문제 토큰 유효성 및 권한 확인

    로컬 개발

    로컬 개발 및 테스트용으로, MCP 서버를 자신의 머신에서 직접 실행할 수 있습니다:

    1. 서버 프로세스 시작: MCP 서버 애플리케이션 실행

    2. 네트워킹 구성: 서버가 예상 포트에서 접근 가능하게 설정

    3. 클라이언트 연결: http://localhost:3000 같은 로컬 연결 URL 사용

    
    # 예시: TypeScript MCP 서버를 로컬에서 실행하기
    
    npm run start
    
    # 서버가 http://localhost:3000 에서 실행 중입니다
    
    

    첫 번째 MCP 서버 구축하기

    이전 수업에서 핵심 개념을 다뤘으니 이제 그 지식을 실습해 보겠습니다.

    서버가 할 수 있는 일

    코딩을 시작하기 전에 서버의 역할을 상기해 봅시다:

    MCP 서버는 예를 들어:

  • 로컬 파일 및 데이터베이스 접근
  • 원격 API 연결
  • 계산 수행
  • 다른 도구 및 서비스 통합
  • 상호작용을 위한 사용자 인터페이스 제공
  • 좋습니다, 무엇을 할 수 있는지 알았으니 코딩을 시작해 봅시다.

    연습: 서버 만들기

    서버를 만들려면 다음 단계를 따르세요:

  • MCP SDK 설치
  • 프로젝트 생성 및 구조 설정
  • 서버 코드 작성
  • 서버 테스트
  • -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}!`
    
        }]
    
      })
    
    );
    
    

    도구는 ab 매개변수를 받고, 다음 형식의 응답을 생성합니다:

    
    {
    
      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}!"
    
    

    위 코드에서 우리는:

  • ab라는 정수 매개변수를 받는 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을 설정하고 Argumentsserver.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" 버튼을 선택하여 서버에 연결하세요

    서버에 연결되면 다음 화면이 보입니다:

    !Connected

    1. "Tools"에서 "listTools"를 선택하세요. "Add"가 표시되면 "Add"를 선택하고 매개변수 값을 입력하세요.

    다음과 같은 응답, 즉 "add" 도구의 결과가 표시됩니다:

    !Result of running 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를 제공합니다:

  • C# SDK - Microsoft와 협력하여 유지 관리
  • Java SDK - Spring AI와 협력하여 유지 관리
  • TypeScript SDK - 공식 TypeScript 구현
  • Python SDK - 공식 Python 구현
  • Kotlin SDK - 공식 Kotlin 구현
  • Swift SDK - Loopwork AI와 협력하여 유지 관리
  • Rust SDK - 공식 Rust 구현
  • 주요 요점

  • 언어별 SDK를 통해 MCP 개발 환경을 간단히 구축할 수 있습니다
  • MCP 서버 구축은 명확한 스키마를 가진 도구를 생성하고 등록하는 과정입니다
  • 테스트 및 디버깅은 신뢰할 수 있는 MCP 구현에 필수적입니다
  • 샘플

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Rust Calculator
  • 과제

    선택한 도구를 사용하여 간단한 MCP 서버를 만드세요:

    1. 선호하는 언어(.NET, Java, Python, TypeScript, Rust)로 도구를 구현하세요.

    2. 입력 매개변수와 반환 값을 정의하세요.

    3. 인스펙터 도구를 실행하여 서버가 제대로 작동하는지 확인하세요.

    4. 다양한 입력으로 구현을 테스트하세요.

    솔루션

    추가 리소스

  • Azure에서 Model Context Protocol을 사용하여 에이전트 빌드하기
  • 원격 MCP with Azure Container Apps (Node.js/TypeScript/JavaScript)
  • .NET OpenAI MCP 에이전트
  • 다음 단계

    다음: MCP 클라이언트 시작하기

    ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나 자동 번역에는 오류나 부정확성이 포함될 수 있음을 유의해 주시기 바랍니다.

    원본 문서가 원어로 된 공식 자료임을 참고하시기 바랍니다.

    중요한 정보에 대해서는 전문 인간 번역가의 번역을 권장합니다.

    본 번역 사용으로 인해 발생하는 모든 오해나 오역에 대해 당사는 책임을 지지 않습니다.

  • 2 Client, 이 강의에서는 서버에 연결할 수 있는 클라이언트 작성법을 배웁니다, 강의로 가기

    클라이언트 생성하기

    클라이언트는 MCP 서버와 직접 통신하여 리소스, 도구 및 프롬프트를 요청하는 맞춤형 애플리케이션 또는 스크립트입니다. 서버와 상호작용하기 위한 그래픽 인터페이스를 제공하는 검사 도구와 달리, 자신만의 클라이언트를 작성하면 프로그래밍 방식으로 자동화된 상호작용이 가능합니다. 이를 통해 개발자는 MCP 기능을 자신의 워크플로우에 통합하고 작업을 자동화하며 특정 요구 사항에 맞춘 맞춤 솔루션을 구축할 수 있습니다.

    개요

    이 수업에서는 Model Context Protocol(MCP) 생태계 내 클라이언트의 개념을 소개합니다. 직접 클라이언트를 작성하고 MCP 서버에 연결하는 방법을 배우게 됩니다.

    학습 목표

    이 수업이 끝나면 다음을 할 수 있습니다:

  • 클라이언트가 무엇을 할 수 있는지 이해합니다.
  • 자신만의 클라이언트를 작성합니다.
  • 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"
    
      }
    
    });
    
    

    위 코드에서:

  • 라이브러리를 가져왔습니다.
  • 클라이언트 인스턴스를 생성하고 stdio 전송 방식을 사용해 연결했습니다.
  • 프롬프트, 리소스, 도구를 나열하고 모두 호출했습니다.
  • 이렇게 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);
    
    

    위 코드에서는:

  • stdio 전송 인스턴스를 생성했습니다. 명령과 인자(argument)를 지정해 서버를 찾고 시작하는 방법을 정의했는데, 클라이언트를 만들 때 필요한 부분입니다.
  • ```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.runrun 메서드를 전달하여 진입점을 만들었습니다.
  • .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);
    
    

    위 코드에서는:

  • 라이브러리를 임포트했습니다.
  • stdio 전송을 생성하고 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();
    
            
    
            // 클라이언트 로직은 여기에 작성하세요
    
        }
    
    }
    
    

    위 코드에서는:

  • MCP 서버가 실행 중일 http://localhost:8080를 가리키는 SSE 전송을 설정하는 main 메서드를 만들었습니다.
  • 생성자 매개변수로 전송을 받는 클라이언트 클래스를 만들었습니다.
  • run 메서드에서 전송을 사용해 동기 MCP 클라이언트를 만들고 연결을 초기화했습니다.
  • Java Spring Boot MCP 서버와의 HTTP 기반 통신에 적합한 SSE(Server-Sent Events) 전송을 사용했습니다.
  • 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}!

    }]

    })

    );

    ```

    urifile://example.txt는 서버의 file://{name}에 매핑되며, example.txtname으로 처리됩니다.

  • 도구 호출: 도구 이름(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() { ["a"] = 1, ["b"] = 3 },

    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으로 전달했습니다.
  • 서버 도구는 수학 연산 등을 위해 “a”, “b” 같은 특정 매개변수 이름을 기대합니다.
  • 결과는 서버 응답을 포함하는 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
    
    

    🚀 각 솔루션에 포함된 내용

    각 언어별 솔루션에는 다음이 포함되어 있습니다:

  • 튜토리얼의 모든 기능을 포함한 완전한 클라이언트 구현
  • 적절한 의존성과 구성 파일이 포함된 작동 가능한 프로젝트 구조
  • 손쉬운 설정과 실행을 위한 빌드 및 실행 스크립트
  • 언어별 안내가 있는 상세 README
  • 에러 처리 및 결과 처리 예제
  • 📖 솔루션 사용법

    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' } ] }

    ```

    자세한 문서와 단계별 안내는 다음을 참조하세요: 📖 솔루션 문서

    각 런타임에 대한 솔루션은 다음과 같습니다:

  • TypeScript

    이 샘플 실행하기

    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' } ] }
    
    

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • Python

    이 샘플 실행하기

    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)]
    
    

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • .NET
  • Java

    MCP Java Client - 계산기 데모

    이 프로젝트는 MCP(Model Context Protocol) 서버에 연결하고 상호작용하는 Java 클라이언트를 만드는 방법을 보여줍니다. 이 예제에서는 1장에 나온 계산기 서버에 연결하여 다양한 수학 연산을 수행합니다.

    사전 준비 사항

    이 클라이언트를 실행하기 전에 다음을 준비해야 합니다:

    1. 1장의 계산기 서버를 시작합니다:

    - 계산기 서버 디렉터리로 이동: 03-GettingStarted/01-first-server/solution/java/

    - 계산기 서버를 빌드하고 실행합니다:

    ```cmd

    cd ..\01-first-server\solution\java

    .\mvnw clean install -DskipTests

    java -jar target\calculator-server-0.0.1-SNAPSHOT.jar

    ```

    - 서버는 http://localhost:8080에서 실행 중이어야 합니다

    2. 시스템에 Java 21 이상이 설치되어 있어야 합니다

    3. Maven (Maven Wrapper를 통해 포함됨)

    SDKClient란?

    SDKClient는 다음을 보여주는 Java 애플리케이션입니다:

  • Server-Sent Events(SSE) 전송을 사용해 MCP 서버에 연결하는 방법
  • 서버에서 사용 가능한 도구 목록을 가져오는 방법
  • 원격으로 다양한 계산기 함수를 호출하는 방법
  • 응답을 처리하고 결과를 표시하는 방법
  • 작동 방식

    클라이언트는 Spring AI MCP 프레임워크를 사용하여:

    1. 연결 설정: WebFlux SSE 클라이언트 전송을 생성해 계산기 서버에 연결

    2. 클라이언트 초기화: MCP 클라이언트를 설정하고 연결을 확립

    3. 도구 검색: 사용 가능한 모든 계산기 연산 목록 조회

    4. 연산 실행: 샘플 데이터를 사용해 다양한 수학 함수 호출

    5. 결과 표시: 각 계산 결과를 화면에 출력

    프로젝트 구조

    
    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 전송
  • MCP 프로토콜 스키마 및 요청/응답 타입
  • 프로젝트 빌드

    Maven Wrapper를 사용해 프로젝트를 빌드합니다:

    
    .\mvnw clean install
    
    

    클라이언트 실행

    
    java -jar .\target\calculator-client-0.0.1-SNAPSHOT.jar
    
    

    참고: 명령어를 실행하기 전에 계산기 서버가 http://localhost:8080에서 실행 중인지 확인하세요.

    클라이언트 동작 내용

    클라이언트를 실행하면 다음 작업을 수행합니다:

    1. http://localhost:8080의 계산기 서버에 연결

    2. 도구 목록 조회 - 사용 가능한 모든 계산기 연산 표시

    3. 계산 수행:

    - 덧셈: 5 + 3 = 8

    - 뺄셈: 10 - 4 = 6

    - 곱셈: 6 × 7 = 42

    - 나눗셈: 20 ÷ 4 = 5

    - 거듭제곱: 2^8 = 256

    - 제곱근: √16 = 4

    - 절댓값: |-5.5| = 5.5

    - 도움말: 사용 가능한 연산 표시

    예상 출력

    
    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]
    
    

    참고: 실행 종료 시 Maven에서 남아있는 스레드에 대한 경고가 나타날 수 있는데, 이는 리액티브 애플리케이션에서 정상적인 현상이며 오류가 아닙니다.

    코드 이해하기

    1. 전송 설정

    
    var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
    
    

    계산기 서버에 연결하는 SSE(Server-Sent Events) 전송을 생성합니다.

    2. 클라이언트 생성

    
    var client = McpClient.sync(this.transport).build();
    
    client.initialize();
    
    

    동기식 MCP 클라이언트를 생성하고 연결을 초기화합니다.

    3. 도구 호출

    
    CallToolResult resultAdd = client.callTool(new CallToolRequest("add", Map.of("a", 5.0, "b", 3.0)));
    
    

    매개변수 a=5.0, b=3.0으로 "add" 도구를 호출합니다.

    문제 해결

    서버가 실행 중이지 않을 때

    연결 오류가 발생하면 1장의 계산기 서버가 실행 중인지 확인하세요:

    
    Error: Connection refused
    
    

    해결 방법: 먼저 계산기 서버를 시작하세요.

    포트가 이미 사용 중일 때

    포트 8080이 사용 중이라면:

    
    Error: Address already in use
    
    

    해결 방법: 포트 8080을 사용하는 다른 애플리케이션을 종료하거나 서버가 다른 포트를 사용하도록 변경하세요.

    빌드 오류 발생 시

    빌드 오류가 발생하면:

    
    .\mvnw clean install -DskipTests
    
    

    더 알아보기

  • Spring AI MCP Documentation
  • Model Context Protocol Specification
  • Spring WebFlux Documentation
  • 면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 자료로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    본 번역의 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • Rust
  • 면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있지만, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다.

    원본 문서의 원어 버전을 권위 있는 출처로 간주해야 합니다.

    중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 책임을 지지 않습니다.

    🎯 완성된 예제

    튜토리얼에서 다룬 모든 프로그래밍 언어용 완성된 클라이언트 구현 예제를 제공했습니다. 이 예제들은 위에서 설명한 전체 기능을 구현하며 참고 또는 자신만의 프로젝트 시작점으로 사용할 수 있습니다.

    사용 가능 완성 예제

    언어 파일 설명 -------- ----------------------------- ----------------------------------------------------------- Java client_example_java.java SSE 전송을 사용하는 완전한 Java 클라이언트로 포괄적 에러 처리 포함 C# client_example_csharp.cs stdio 전송 및 자동 서버 시작 기능을 갖춘 완전한 C# 클라이언트 TypeScript client_example_typescript.ts 완벽한 MCP 프로토콜 지원을 제공하는 완전한 TypeScript 클라이언트 Python client_example_python.py async/await 패턴을 사용하는 완전한 Python 클라이언트 Rust client_example_rust.rs Tokio 기반 비동기 작업을 지원하는 완전한 Rust 클라이언트

    각 완성 예제에는:

  • 연결 설정 및 오류 처리
  • 서버 검색 (도구, 리소스, 프롬프트 포함 시)
  • 계산기 작업 (더하기, 빼기, 곱하기, 나누기, 도움말)
  • 결과 처리 및 포맷된 출력
  • 포괄적인 오류 처리
  • 명확하고 문서화된 코드와 단계별 주석
  • 완전한 예제 시작하기

    1. 위 표에서 선호하는 언어 선택

    2. 전체 구현을 이해하기 위해 완전한 예제 파일 검토

    3. complete_examples.md 에서 지침에 따라 예제 실행

    4. 특정 사용 사례에 맞게 예제 수정 및 확장

    자세한 실행 및 사용자 정의 문서는 다음을 참조하세요: 📖 완전한 예제 문서

    💡 솔루션 vs. 완전한 예제

    솔루션 폴더 완전한 예제 -------------------------- -------------------------- 빌드 파일을 포함한 전체 프로젝트 구조 단일 파일 구현 종속성 포함 즉시 실행 가능 집중된 코드 예제 프로덕션 환경과 유사한 설정 교육용 참고 자료 언어별 툴링 언어 간 비교

    두 접근법 모두 유용합니다 - 전체 프로젝트는 솔루션 폴더를, 학습과 참조는 완전한 예제를 사용하세요.

    주요 내용 요약

    이번 장에서 클라이언트에 대한 주요 내용은 다음과 같습니다:

  • 서버에서 기능을 발견하고 호출하는 데 모두 사용할 수 있습니다.
  • 클라이언트가 자신을 시작하는 동안 서버를 시작할 수 있으며(이번 장처럼) 실행 중인 서버에 연결할 수도 있습니다.
  • 이전 장에서 설명한 Inspector와 같은 대안과 함께 서버 기능을 테스트하는 좋은 방법입니다.
  • 추가 자료

  • MCP에서 클라이언트 구축하기
  • 샘플

  • Java 계산기
  • .Net 계산기
  • JavaScript 계산기
  • TypeScript 계산기
  • Python 계산기
  • Rust 계산기
  • 다음 내용

  • 다음: LLM을 사용한 클라이언트 만들기
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확한 내용이 포함될 수 있음을 유의하시기 바랍니다.

    원문 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.

    본 번역 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • 3 Client with LLM, 더 나은 방법으로서 클라이언트에 LLM을 추가해 서버와 "협상"하며 동작하도록 작성하는 방법을 배웁니다, 강의로 가기

    LLM을 사용하여 클라이언트 만들기

    지금까지 서버와 클라이언트를 만드는 방법을 보았습니다.

    클라이언트는 서버를 명시적으로 호출하여 도구, 리소스, 프롬프트를 나열할 수 있었습니다.

    하지만 이는 그리 실용적인 방법이 아닙니다.

    사용자는 에이전트 시대에 살고 있으며 프롬프트를 사용하고 LLM과 소통하기를 기대합니다.

    사용자가 MCP를 사용하여 기능을 저장하는지 여부는 신경 쓰지 않고 단순히 자연어를 사용하여 상호작용하기를 기대합니다.

    그렇다면 이것을 어떻게 해결할 수 있을까요?

    해결책은 클라이언트에 LLM을 추가하는 것입니다.

    개요

    이번 수업에서는 클라이언트에 LLM을 추가하는 데 중점을 두며, 이를 통해 사용자에게 훨씬 더 나은 경험을 제공하는 방법을 보여줍니다.

    학습 목표

    이 수업이 끝나면 다음을 할 수 있습니다:

  • LLM이 포함된 클라이언트를 만듭니다.
  • LLM을 사용하여 MCP 서버와 원활하게 상호작용합니다.
  • 클라이언트 측에서 더 나은 최종 사용자 경험을 제공합니다.
  • 접근 방법

    우리가 취해야 할 접근 방식을 이해해 봅시다. LLM을 추가하는 것은 간단해 보이지만 실제로 이렇게 할 수 있을까요?

    클라이언트가 서버와 상호작용하는 방식은 다음과 같습니다:

    1. 서버와 연결을 설정합니다.

    1. 기능, 프롬프트, 리소스 및 도구를 나열하고 해당 스키마를 저장합니다.

    1. LLM을 추가하고, 저장된 기능과 그들의 스키마를 LLM이 이해하는 형식으로 전달합니다.

    1. 사용자 프롬프트를 처리하여 클라이언트가 나열한 도구와 함께 LLM에 전달합니다.

    좋습니다. 이제 어떻게 고수준에서 이 작업을 수행할 수 있는지 이해했으니, 아래 연습 문제에서 직접 시도해 봅시다.

    연습 문제: LLM이 포함된 클라이언트 만들기

    이 연습 문제에서는 클라이언트에 LLM을 추가하는 방법을 배웁니다.

    GitHub 개인 액세스 토큰을 사용한 인증

    GitHub 토큰 만드는 과정은 간단합니다. 방법은 다음과 같습니다:

  • GitHub 설정으로 이동 – 오른쪽 상단의 프로필 사진을 클릭하고 설정을 선택합니다.
  • 개발자 설정으로 이동 – 아래로 스크롤하여 개발자 설정(Developer Settings)을 클릭합니다.
  • 개인 액세스 토큰 선택 – 정밀 범위 토큰(Fine-grained tokens)을 클릭한 다음 새 토큰 생성(Generate new token)을 클릭합니다.
  • 토큰 구성 – 참고용 메모를 추가하고 만료 날짜를 설정하며 필요한 범위(권한)를 선택합니다. 이번 경우에는 Models 권한을 반드시 추가하세요.
  • 토큰 생성 및 복사 – 토큰 생성을 클릭하고, 다시 볼 수 없으니 바로 복사하세요.
  • -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: {}
    
                    }
    
                }
    
                );    
    
        }
    
    }
    
    

    위 코드에서는:

  • 필요한 라이브러리를 임포트했습니다.
  • 클라이언트와 LLM 상호작용을 돕는 두 멤버 clientopenai가 있는 클래스를 생성했습니다.
  • 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())
    
    
    
    

    위 코드에서는:

  • MCP에 필요한 라이브러리를 임포트했습니다.
  • 클라이언트를 생성했습니다.
  • .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();
    
        }
    
    }
    
    

    위 코드에서는:

  • LangChain4j 의존성 추가: MCP 통합, OpenAI 공식 클라이언트, GitHub 모델 지원을 위해 필요합니다.
  • LangChain4j 라이브러리 임포트: MCP 통합과 OpenAI 채팅 모델 기능을 위해
  • ChatLanguageModel 생성: GitHub 토큰으로 GitHub 모델을 사용하도록 구성함
  • HTTP 전송 설정: 서버 발신 이벤트(SSE)를 사용하여 MCP 서버에 연결함
  • MCP 클라이언트 생성: 서버와의 통신을 처리함
  • LangChain4j 내장 MCP 지원 사용: LLM과 MCP 서버 간 통합을 간소화함
  • 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;
    
    }
    
    

    위 코드에서:

  • MCP 서버에서 사용 가능한 도구를 나열했습니다.
  • 각 도구의 이름, 설명 및 스키마를 나열했습니다. 후자는 곧 도구 호출에 사용할 예정입니다.
  • Java
    
    // MCP 도구를 자동으로 검색하는 도구 제공자 생성
    
    ToolProvider toolProvider = McpToolProvider.builder()
    
            .mcpClients(List.of(mcpClient))
    
            .build();
    
    
    
    // MCP 도구 제공자는 자동으로 다음을 처리합니다:
    
    // - MCP 서버에서 사용 가능한 도구 목록 작성
    
    // - MCP 도구 스키마를 LangChain4j 형식으로 변환
    
    // - 도구 실행 및 응답 관리
    
    

    위 코드에서:

  • MCP 서버의 모든 도구를 자동으로 검색하고 등록하는 McpToolProvider를 만들었습니다.
  • 도구 공급자는 MCP 도구 스키마를 LangChain4j 도구 포맷으로 내부 변환합니다.
  • 수동으로 도구를 나열하고 변환하는 과정을 추상화합니다.
  • 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 toolDefinitions = new 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 인터페이스를 정의했습니다.
  • LangChain4j의 AiServices를 사용해 LLM과 MCP 도구 제공자를 자동 바인딩했습니다.
  • 프레임워크가 도구 스키마 변환과 함수 호출을 자동으로 처리합니다.
  • 이 접근법은 수동 도구 변환을 없애고 LangChain4j가 MCP 도구를 LLM 호환 포맷으로 전부 처리합니다.
  • 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>(call.Arguments);

    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();
    
    }
    
    

    위 코드에서는:

  • 간단한 자연어 프롬프트를 사용하여 MCP 서버 도구와 상호작용했습니다.
  • LangChain4j 프레임워크가 다음을 자동 처리합니다:
  • - 필요 시 사용자 프롬프트를 도구 호출로 변환

    - LLM 판단에 따라 적절한 MCP 도구 호출

    - LLM과 MCP 서버 간 대화 흐름 관리

  • bot.chat() 메서드는 MCP 도구 실행 결과를 포함할 수 있는 자연어 응답을 반환합니다.
  • 이 접근법은 사용자가 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 서버 호출이 있는지도 인지하지 못합니다.

    해결책

    주요 내용 정리

  • 클라이언트에 LLM을 추가하면 사용자가 MCP 서버와 더 나은 방식으로 상호작용할 수 있습니다.
  • MCP 서버 응답을 LLM이 이해할 수 있도록 변환해야 합니다.
  • 샘플

  • Java 계산기
  • .Net 계산기
  • JavaScript 계산기
  • TypeScript 계산기
  • Python 계산기
  • Rust 계산기
  • 추가 자료

    다음 단계

  • 다음: Visual Studio Code를 사용하여 서버 사용하기
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 양지하시기 바랍니다.

    원문 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 오해나 오해석에 대해 당사는 책임을 지지 않습니다.

  • 4 Consuming a server GitHub Copilot Agent mode in Visual Studio Code. 여기서는 Visual Studio Code 내에서 MCP 서버를 실행하는 방법을 살펴봅니다, 강의로 가기

    GitHub Copilot 에이전트 모드에서 서버 사용하기

    Visual Studio Code와 GitHub Copilot은 클라이언트 역할을 하며 MCP 서버를 사용할 수 있습니다.

    왜 이런 기능이 필요할까요?

    MCP 서버의 모든 기능을 IDE 내에서 사용할 수 있다는 뜻입니다.

    예를 들어 GitHub의 MCP 서버를 추가하면 터미널에서 특정 명령어를 입력하는 대신 프롬프트를 통해 GitHub을 제어할 수 있습니다.

    또는 개발자 경험을 개선할 수 있는 모든 것을 자연어로 제어할 수 있다고 상상해 보세요.

    이제 이 기능의 장점을 이해할 수 있겠죠?

    개요

    이 강의에서는 Visual Studio Code와 GitHub Copilot의 에이전트 모드를 사용하여 MCP 서버를 클라이언트로 활용하는 방법을 다룹니다.

    학습 목표

    이 강의를 마치면 다음을 할 수 있습니다:

  • Visual Studio Code를 통해 MCP 서버를 사용할 수 있습니다.
  • GitHub Copilot을 통해 도구와 같은 기능을 실행할 수 있습니다.
  • Visual Studio Code를 설정하여 MCP 서버를 찾고 관리할 수 있습니다.
  • 사용법

    MCP 서버를 제어하는 방법은 두 가지가 있습니다:

  • 사용자 인터페이스: 이 방법은 이후 챕터에서 다룹니다.
  • 터미널: code 실행 파일을 사용하여 터미널에서 제어할 수 있습니다.
  • 사용자 프로필에 MCP 서버를 추가하려면 --add-mcp 명령줄 옵션을 사용하고 JSON 서버 설정을 다음 형식으로 제공하세요: {\"name\":\"server-name\",\"command\":...}.

    ```

    code --add-mcp "{\"name\":\"my-server\",\"command\": \"uvx\",\"args\": [\"mcp-server-fetch\"]}"

    ```

    스크린샷

    다음 섹션에서 시각적 인터페이스를 사용하는 방법에 대해 더 알아보겠습니다.

    접근 방식

    다음은 고수준에서 접근해야 할 방법입니다:

  • MCP 서버를 찾기 위한 파일을 설정합니다.
  • 서버를 시작하거나 연결하여 서버의 기능을 나열합니다.
  • GitHub Copilot 채팅 인터페이스를 통해 해당 기능을 사용합니다.
  • 좋습니다, 이제 흐름을 이해했으니 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로 작성된 서버를 시작하는 간단한 예입니다.

    다른 런타임의 경우 commandargs를 사용하여 서버를 시작하는 적절한 명령을 지정하세요.

    -3- 서버 시작

    항목을 추가했으니 이제 서버를 시작해봅시다:

    1. *mcp.json*에서 항목을 찾아 "재생" 아이콘을 확인하세요:

    !Visual Studio Code에서 서버 시작

    1. "재생" 아이콘을 클릭하면 GitHub Copilot 채팅의 도구 아이콘에 사용 가능한 도구 수가 증가하는 것을 볼 수 있습니다. 해당 도구 아이콘을 클릭하면 등록된 도구 목록이 표시됩니다. 각 도구를 체크하거나 체크 해제하여 GitHub Copilot이 이를 컨텍스트로 사용할지 여부를 결정할 수 있습니다:

    !Visual Studio Code에서 도구 시작

    1. 도구를 실행하려면 도구 설명과 일치하는 프롬프트를 입력하세요. 예를 들어 "22에 1을 더해줘"와 같은 프롬프트를 입력합니다:

    !GitHub Copilot에서 도구 실행

    응답으로 23이 표시될 것입니다.

    과제

    *mcp.json* 파일에 서버 항목을 추가하고 서버를 시작/중지할 수 있는지 확인하세요. GitHub Copilot 채팅 인터페이스를 통해 서버의 도구와 통신할 수 있는지도 확인하세요.

    솔루션

    주요 내용

    이 챕터의 주요 내용은 다음과 같습니다:

  • Visual Studio Code는 여러 MCP 서버와 그 도구를 사용할 수 있는 훌륭한 클라이언트입니다.
  • GitHub Copilot 채팅 인터페이스는 서버와 상호작용하는 방법입니다.
  • API 키와 같은 입력을 사용자에게 요청하여 *mcp.json* 파일에서 서버 항목을 설정할 때 MCP 서버에 전달할 수 있습니다.
  • 샘플

  • Java 계산기
  • .Net 계산기
  • JavaScript 계산기
  • TypeScript 계산기
  • Python 계산기
  • 추가 자료

  • Visual Studio 문서
  • 다음 단계

  • 다음: Stdio 서버 생성하기
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있지만, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다.

    원본 문서의 원어 버전을 권위 있는 출처로 간주해야 합니다.

    중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 책임을 지지 않습니다.

  • 5 stdio Transport Server stdio 전송은 로컬 MCP 서버-클라이언트 통신에 권장되는 표준으로, 프로세스 격리가 내장된 보안 서브프로세스 기반 통신을 제공합니다, 강의로 가기

    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 전송을 사용해 MCP 서버를 구축하기
  • Inspector를 사용해 MCP 서버 디버깅하기
  • Visual Studio Code에서 MCP 서버를 사용하기
  • 현재 MCP 전송 메커니즘과 stdio가 권장되는 이유 이해하기
  • stdio 전송 - 작동 방식

    stdio 전송은 현재 MCP 사양(2025-06-18)에서 지원하는 두 가지 전송 유형 중 하나입니다. 작동 방식은 다음과 같습니다:

  • 간단한 통신: 서버는 표준 입력(stdin)으로부터 JSON-RPC 메시지를 읽고 표준 출력(stdout)으로 메시지를 보냄
  • 프로세스 기반: 클라이언트가 MCP 서버를 서브프로세스로 실행
  • 메시지 형식: 메시지는 개별 JSON-RPC 요청, 알림 또는 응답이며 줄바꿈 문자로 구분됨
  • 로깅: 서버는 표준 에러(stderr)에 UTF-8 문자열을 기록할 수 있음
  • 주요 요구사항:

  • 메시지는 줄바꿈 문자로 구분되어야 하며 줄내림 문자 포함 불가
  • 서버는 유효한 MCP 메시지가 아닌 어떤 것도 stdout에 출력해선 안 됨
  • 클라이언트는 유효하지 않은 MCP 메시지를 서버의 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);
    
    

    앞 코드에서는:

  • MCP SDK에서 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())
    
    

    앞 코드에서는:

  • MCP SDK를 사용해 서버 인스턴스 생성
  • 데코레이터로 도구 정의
  • stdio_server 컨텍스트 매니저로 전송 처리
  • .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 서버는:

  • 웹 서버 설정이나 HTTP 엔드포인트가 필요 없음
  • 클라이언트가 서브프로세스로 실행
  • stdin/stdout 스트림을 통해 통신
  • 구현과 디버깅이 더 간단함
  • 연습: stdio 서버 생성

    서버를 만들 때 두 가지를 염두에 둬야 합니다:

  • 연결과 메시지를 위한 엔드포인트를 노출하려면 웹 서버를 사용해야 합니다.
  • 실습: 간단한 MCP stdio 서버 만들기

    이 실습에서는 권장되는 stdio 전송을 사용해 간단한 MCP 서버를 만듭니다. 이 서버는 표준 Model Context Protocol을 사용해 클라이언트가 호출할 수 있는 도구를 노출합니다.

    사전 준비 사항

  • Python 3.8 이상
  • MCP Python SDK: 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 전송 (현재 표준):

  • 간단한 서브프로세스 모델 - 클라이언트가 서버를 자식 프로세스로 실행
  • stdin/stdout을 이용한 JSON-RPC 메시지 통신
  • HTTP 서버 설정 불필요
  • 성능과 보안 향상
  • 개발 및 디버깅 용이
  • SSE 전송 (MCP 2025-06-18부터 더 이상 사용 안 함):

  • SSE 엔드포인트가 있는 HTTP 서버 필요
  • 웹 서버 인프라 설정 복잡
  • HTTP 엔드포인트 관련 추가 보안 고려 사항 존재
  • 웹 기반 시나리오에 대해 이제 스트리밍 HTTP로 대체됨
  • 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 -->
    
    
  • 6 HTTP Streaming with MCP (Streamable HTTP). 현대적인 HTTP 스트리밍 전송 방식(원격 MCP 서버에 권장되는 방법, MCP Specification 2025-11-25참조), 진행 알림, Streamable HTTP를 사용해 확장 가능하고 실시간으로 동작하는 MCP 서버와 클라이언트 구현 방법을 배웁니다, 강의로 가기

    HTTPS 스트리밍과 모델 컨텍스트 프로토콜(MCP)

    이 장에서는 HTTPS를 사용하여 모델 컨텍스트 프로토콜(MCP)로 보안성, 확장성 및 실시간 스트리밍을 구현하는 포괄적인 가이드를 제공합니다. 스트리밍의 동기, 사용 가능한 전송 메커니즘, MCP에서 스트림 가능한 HTTP를 구현하는 방법, 보안 모범 사례, SSE에서의 마이그레이션, 그리고 스트리밍 MCP 애플리케이션을 구축하는 실용적인 지침을 다룹니다.

    MCP의 전송 메커니즘과 스트리밍

    이 섹션에서는 MCP에서 사용할 수 있는 다양한 전송 메커니즘과 이들이 클라이언트와 서버 간 실시간 통신을 가능하게 하는 스트리밍 기능에서 어떤 역할을 하는지 살펴봅니다.

    전송 메커니즘이란 무엇인가?

    전송 메커니즘은 클라이언트와 서버 간 데이터가 교환되는 방식을 정의합니다. MCP는 다양한 환경과 요구 사항에 맞춰 여러 전송 유형을 지원합니다:

  • stdio: 표준 입력/출력으로, 로컬 및 CLI 기반 도구에 적합합니다. 단순하지만 웹이나 클라우드에는 적합하지 않습니다.
  • SSE (Server-Sent Events): 서버가 HTTP를 통해 클라이언트에 실시간 업데이트를 푸시할 수 있게 합니다. 웹 UI에 적합하지만 확장성과 유연성에 제한이 있습니다. MCP 규격 2025-06-18 기준으로 독립적인 SSE 전송은 더 이상 사용되지 않으며 "Streamable HTTP" 전송으로 대체되었습니다.
  • Streamable HTTP: 알림과 더 나은 확장성을 지원하는 최신 HTTP 기반 스트리밍 전송 방식입니다. 대부분의 프로덕션 및 클라우드 환경에서 권장됩니다.
  • 비교 표

    아래 비교 표를 보면 이러한 전송 메커니즘 간의 차이점을 이해할 수 있습니다:

    전송 타입 실시간 업데이트 스트리밍 확장성 사용 사례 ------------------- ---------------- --------- ------------ --------------------------- stdio 아니오 아니오 낮음 로컬 CLI 도구 SSE 예 예 중간 웹, 실시간 업데이트 Streamable HTTP 예 예 높음 클라우드, 다중 클라이언트

    > 팁: 적절한 전송 방식을 선택하는 것은 성능, 확장성, 사용자 경험에 중요한 영향을 미칩니다. Streamable HTTP는 최신의 확장 가능하고 클라우드 대응 애플리케이션에 권장됩니다.

    이전 장에서 소개한 stdio 및 SSE 전송과 달리, 이 장에서는 Streamable HTTP 전송에 대해 다룹니다.

    스트리밍: 개념과 동기

    스트리밍의 기본 개념과 동기를 이해하는 것은 효과적인 실시간 통신 시스템을 구현하는 데 필수적입니다.

    스트리밍은 네트워크 프로그래밍에서 전체 응답이 준비될 때까지 기다리지 않고 데이터가 작고 관리 가능한 청크로 또는 이벤트 시퀀스로 전송되고 수신될 수 있게 하는 기술입니다. 특히 다음과 같은 경우에 유용합니다:

  • 대용량 파일 또는 데이터셋.
  • 실시간 업데이트(예: 채팅, 진행 표시줄).
  • 사용자에게 진행 상황을 알려야 하는 장시간 실행되는 계산.
  • 스트리밍에 대해 알아야 할 주요 사항은 다음과 같습니다:

  • 데이터가 한꺼번에가 아니라 점진적으로 전달됩니다.
  • 클라이언트는 도착하는 데이터를 즉시 처리할 수 있습니다.
  • 지각 대기 시간이 줄어들어 사용자 경험이 향상됩니다.
  • 왜 스트리밍을 사용하는가?

    스트리밍 사용 이유는 다음과 같습니다:

  • 사용자가 작업이 끝날 때까지 기다리지 않고 즉시 피드백을 받음
  • 실시간 애플리케이션과 반응형 UI를 가능하게 함
  • 네트워크와 컴퓨팅 자원의 효율적 사용
  • 간단한 예: 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())
    
    

    이 예는 모든 메시지가 준비될 때까지 기다리지 않고, 서버가 준비되는 대로 일련의 메시지를 클라이언트에 전송하는 방식을 보여줍니다.

    작동 방식:

  • 서버는 각 메시지가 준비될 때마다 이를 yield합니다.
  • 클라이언트는 도착하는 각 청크를 수신하고 출력합니다.
  • 요구 사항:

  • 서버는 스트리밍 응답(e.g., FastAPI의 StreamingResponse)을 사용해야 합니다.
  • 클라이언트는 스트림으로 응답을 처리해야 합니다(requestsstream=True).
  • Content-Type은 보통 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 는 이벤트 타입을 갖는 구조화된 이벤트 스트리밍 제공
  • WebClientbodyToFlux()를 통해 반응형 스트리밍 소비 가능
  • delayElements()로 이벤트 간 처리 시간 시뮬레이션
  • 이벤트별 inforesult 타입 부여하여 클라이언트 처리 개선
  • 비교: 클래식 스트리밍 vs MCP 스트리밍

    클래식 방식의 스트리밍과 MCP 스트리밍이 어떻게 다른지 다음 표에서 확인할 수 있습니다:

    특징 클래식 HTTP 스트리밍 MCP 스트리밍 (알림) ------------------------ ------------------------------- ------------------------------------- 주요 응답 청크 단위로 전송 단일 응답, 끝에 전달 진행 상황 업데이트 데이터 청크로 전송 알림으로 전송 클라이언트 요구 사항 스트림 처리 필요 메시지 핸들러 구현 필요 사용 사례 대용량 파일, AI 토큰 스트림 진행 상황, 로그, 실시간 피드백

    관찰된 주요 차이점

    추가로 주요 차이점은 다음과 같습니다:

  • 통신 패턴:
  • - 클래식 HTTP 스트리밍: 간단한 청크 전송 인코딩 사용

    - MCP 스트리밍: JSON-RPC 프로토콜 기반 구조화된 알림 시스템 사용

  • 메시지 형식:
  • - 클래식 HTTP: 줄바꿈 포함한 텍스트 청크

    - MCP: 메타데이터가 포함된 구조화된 LoggingMessageNotification 객체

  • 클라이언트 구현:
  • - 클래식 HTTP: 스트리밍 응답을 처리하는 단순 클라이언트

    - MCP: 메시지 핸들러를 구현하여 다양한 메시지 유형 처리

  • 진행 상황 업데이트:
  • - 클래식 HTTP: 진행 상황이 주요 응답 스트림의 일부

    - MCP: 주요 응답은 끝에 전송되고 진행 상황은 별도 알림 메시지로 전송

    권장 사항

    클래식 스트리밍(/stream 엔드포인트) 구현과 MCP 스트리밍 구현 간 선택 시 다음 사항들을 권장합니다.

  • 간단한 스트리밍 요구: 클래식 HTTP 스트리밍이 구현이 간단하며 기본적인 스트리밍에 충분합니다.
  • 복잡하고 상호작용이 필요한 애플리케이션: MCP 스트리밍은 더 풍부한 메타데이터와 알림과 최종 결과의 분리를 제공하는 구조적 접근법입니다.
  • AI 애플리케이션용: 장시간 실행되는 AI 작업에서 진행 상황을 사용자에게 알리기 위해 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에 따라 로깅이 기본적으로 활성화되어 있거나 서버 설정에서 명시적으로 활성화해야 할 수 있습니다.

    알림에는 여러 유형이 있습니다:

    레벨 설명 예제 사용 사례 ----------- ------------------------------- -------------------------------- debug 상세 디버깅 정보 함수 진입/종료 지점 info 일반 정보 메시지 작업 진행 상황 업데이트 notice 일반적이지만 중요한 이벤트 설정 변경 warning 경고 상황 사용 중단된 기능 사용 error 오류 상황 작업 실패 critical 치명적인 상황 시스템 구성 요소 실패 alert 즉각적 조치 필요 데이터 손상 탐지 emergency 시스템 사용 불가 전체 시스템 장애

    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 함수는 들어오는 메시지가 알림인지 체크합니다.

    알림이면 출력하고, 아니면 일반 서버 메시지로 처리합니다. ClientSessionClientSessionOptions를 통해 메시지 핸들러로 초기화됩니다.

    알림 활성화를 위해 서버가 스트리밍 전송(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 헤더 검증: DNS 리바인딩 공격을 방지하기 위해 항상 Origin 헤더를 검증하세요.
  • 로컬호스트 바인딩: 로컬 개발 시 서버를 localhost에 바인딩하여 공개 인터넷 노출을 방지하세요.
  • 인증: 프로덕션 배포에선 인증(API 키, OAuth 등)을 구현하세요.
  • CORS: 교차 출처 리소스 공유 정책을 설정해 접근을 제한하세요.
  • HTTPS: 프로덕션에서는 HTTPS를 사용해 트래픽 암호화를 보장하세요.
  • 모범 사례

  • 검증 없이 들어오는 요청을 신뢰하지 마세요.
  • 모든 접근 및 오류를 로깅하고 모니터링하세요.
  • 보안 취약점 패치를 위해 의존성을 정기적으로 업데이트하세요.
  • 도전과제

  • 보안과 개발의 용이성의 균형 맞추기
  • 다양한 클라이언트 환경과의 호환성 보장
  • SSE에서 Streamable HTTP로 업그레이드하기

    현재 Server-Sent Events(SSE)를 사용하는 애플리케이션의 경우, Streamable HTTP로 마이그레이션하면 향상된 기능과 MCP 구현의 장기적 유지 가능성을 제공합니다.

    업그레이드가 필요한 이유

    SSE에서 Streamable HTTP로 업그레이드해야 하는 두 가지 설득력 있는 이유가 있습니다:

  • Streamable HTTP는 SSE보다 뛰어난 확장성, 호환성, 그리고 풍부한 알림 지원을 제공합니다.
  • 새로운 MCP 애플리케이션에 권장되는 전송 방식입니다.
  • 마이그레이션 단계

    MCP 애플리케이션에서 SSE에서 Streamable HTTP로 마이그레이션하는 방법은 다음과 같습니다:

  • 서버 코드를 mcp.run()에서 transport="streamable-http"로 업데이트합니다.
  • 클라이언트 코드를 SSE 클라이언트 대신 streamablehttp_client를 사용하도록 업데이트합니다.
  • 클라이언트에서 알림을 처리할 메시지 핸들러를 구현합니다.
  • 기존 도구 및 워크플로와의 호환성을 테스트합니다.
  • 호환성 유지

    마이그레이션 과정에서 기존 SSE 클라이언트와의 호환성을 유지하는 것이 권장됩니다. 다음과 같은 전략이 있습니다:

  • 서로 다른 엔드포인트에서 두 전송 방식을 동시에 실행하여 SSE와 Streamable HTTP를 모두 지원할 수 있습니다.
  • 클라이언트를 점진적으로 새 전송 방식으로 마이그레이션합니다.
  • 과제

    마이그레이션 시 다음 과제를 반드시 해결해야 합니다:

  • 모든 클라이언트가 업데이트되었는지 확인
  • 알림 전달 방식의 차이 처리
  • 보안 고려사항

    어떤 서버를 구현하든, 특히 MCP의 Streamable HTTP 같은 HTTP 기반 전송을 사용할 때 보안은 최우선 과제여야 합니다.

    HTTP 기반 전송을 사용하는 MCP 서버를 구현할 때는 여러 공격 벡터와 보호 메커니즘에 세심한 주의를 기울여야 합니다.

    개요

    MCP 서버를 HTTP를 통해 노출할 때 보안은 매우 중요합니다. Streamable HTTP는 새로운 공격 지점을 제공하며 신중한 구성과 관리가 필요합니다.

    주요 보안 고려사항은 다음과 같습니다:

  • Origin 헤더 검증: DNS 리바인딩 공격을 방지하기 위해 항상 Origin 헤더를 검증합니다.
  • 로컬호스트 바인딩: 로컬 개발 시 서버를 localhost에 바인딩하여 공개 인터넷에 노출되지 않도록 합니다.
  • 인증: 운영 환경에서는 API 키, OAuth 같은 인증을 구현합니다.
  • CORS: 접근을 제한하기 위해 교차 출처 리소스 공유(CORS) 정책을 구성합니다.
  • HTTPS: 운영 환경에서는 트래픽 암호화를 위해 HTTPS를 사용합니다.
  • 모범 사례

    MCP 스트리밍 서버에서 보안을 구현할 때 따를 모범 사례는 다음과 같습니다:

  • 검증 없이 들어오는 요청을 신뢰하지 마세요.
  • 모든 접근과 오류를 로깅하고 모니터링하세요.
  • 보안 취약점 패치를 위해 의존성을 정기적으로 업데이트하세요.
  • 과제

    MCP 스트리밍 서버 보안 구현 중에는 다음과 같은 과제를 직면합니다:

  • 보안과 개발 편의성 간의 균형 맞추기
  • 다양한 클라이언트 환경과의 호환성 보장
  • 과제: 직접 나만의 스트리밍 MCP 애플리케이션 만들기

    시나리오:

    서버가 아이템 목록(예: 파일 또는 문서)을 처리하고 처리된 각 아이템에 대해 알림을 전송하는 MCP 서버와 클라이언트를 만드세요. 클라이언트는 도착하는 각 알림을 실시간으로 표시해야 합니다.

    단계:

    1. 목록을 처리하고 각 항목에 대해 알림을 보내는 서버 도구를 구현하세요.

    2. 알림을 실시간으로 표시할 메시지 핸들러가 있는 클라이언트를 구현하세요.

    3. 서버와 클라이언트를 함께 실행하여 구현을 테스트하고 알림이 잘 표시되는지 확인하세요.

    추가 학습 및 다음 단계

    MCP 스트리밍을 계속 사용하고 지식을 확장하기 위해 이 섹션에서는 추가 리소스와 더 고급 애플리케이션을 만들기 위한 권장 다음 단계를 제공합니다.

    추가 학습

  • Microsoft: HTTP 스트리밍 소개
  • Microsoft: Server-Sent Events (SSE)
  • Microsoft: ASP.NET Core에서 CORS
  • Python requests: 스트리밍 요청
  • 다음 단계

  • 실시간 분석, 채팅, 협업 편집에 스트리밍을 사용하는 더 고급 MCP 도구를 만들어보세요.
  • MCP 스트리밍을 프론트엔드 프레임워크(React, Vue 등)와 통합하여 실시간 UI 업데이트를 구현해보세요.
  • 다음: VSCode용 AI 툴킷 활용하기
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 기하기 위해 노력하고 있으나, 자동 번역은 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.

    원본 문서의 원어본이 권위 있는 자료로 간주되어야 합니다.

    중요한 정보의 경우, 전문가의 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • 7 Utilising AI Toolkit for VSCode MCP 클라이언트와 서버를 사용해 보고 테스트하는 방법, 강의로 가기

    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를 지원합니다.

    학습 목표

    이 레슨을 마치면 다음을 수행할 수 있습니다:

  • AI Toolkit을 통해 MCP 서버를 활용하기.
  • MCP 서버가 제공하는 도구를 발견하고 사용할 수 있도록 에이전트 설정을 구성하기.
  • 자연어를 통해 MCP 도구를 활용하기.
  • 접근 방식

    다음은 전체적인 접근 방식입니다:

  • 에이전트를 생성하고 시스템 프롬프트를 정의합니다.
  • 계산기 도구를 포함한 MCP 서버를 생성합니다.
  • Agent Builder를 MCP 서버에 연결합니다.
  • 자연어를 통해 에이전트의 도구 호출을 테스트합니다.
  • 좋습니다. 이제 흐름을 이해했으니, MCP를 통해 외부 도구를 활용하여 AI 에이전트의 기능을 확장해봅시다!

    사전 준비

  • Visual Studio Code
  • Visual Studio Code용 AI Toolkit
  • 실습: 서버 활용하기

    > [!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) BuilderSystem prompt 필드에 프롬프트가 나타납니다.

    1. System prompt를 검토하고 필요하면 수정합니다.

    -3- MCP 서버 생성하기

    에이전트의 시스템 프롬프트를 정의하여 동작과 응답을 안내한 후, 이제 에이전트에 실질적인 기능을 추가할 차례입니다. 이 섹션에서는 덧셈, 뺄셈, 곱셈, 나눗셈 계산을 실행할 도구를 포함한 계산기 MCP 서버를 생성합니다. 이 서버는 에이전트가 자연어 프롬프트에 따라 실시간으로 수학 연산을 수행할 수 있도록 합니다.

    AI Toolkit은 사용자 정의 MCP 서버를 쉽게 생성할 수 있는 템플릿을 제공합니다. 우리는 계산기 MCP 서버를 생성하기 위해 Python 템플릿을 사용할 것입니다.

    *참고*: AI Toolkit은 현재 Python과 TypeScript를 지원합니다.

    1. Agent (Prompt) BuilderTools 섹션에서 + 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 BarExplorer 보기에서 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) BuilderUser prompt 필드에 다음 프롬프트를 입력합니다: 나는 각각 $25인 물건 3개를 샀고, $20 할인권을 사용했어. 총 얼마를 지불했지?

    1. Run 버튼을 클릭하여 에이전트의 응답을 생성합니다.

    1. 에이전트 출력을 검토합니다. 모델은 $55를 지불했다고 결론을 내려야 합니다.

    1. 다음과 같은 과정이 발생해야 합니다:

    - 에이전트가 계산을 돕기 위해 multiplysubtract 도구를 선택합니다.

    - multiply 도구에 각각 ab 값이 할당됩니다.

    - subtract 도구에 각각 ab 값이 할당됩니다.

    - 각 도구의 응답이 Tool Response에 제공됩니다.

    - 모델의 최종 출력이 Model Response에 제공됩니다.

    1. 추가 프롬프트를 제출하여 에이전트를 더 테스트합니다. User prompt 필드에서 기존 프롬프트를 클릭하여 수정할 수 있습니다.

    1. 테스트가 끝나면 터미널에서 CTRL/CMD+C를 입력하여 서버를 종료할 수 있습니다.

    과제

    server.py 파일에 추가 도구 항목(예: 숫자의 제곱근 반환)을 추가해보세요. 에이전트가 새 도구(또는 기존 도구)를 활용해야 하는 추가 프롬프트를 제출하세요. 새로 추가된 도구를 로드하려면 서버를 재시작해야 합니다.

    솔루션

    주요 내용

    이 장의 주요 내용은 다음과 같습니다:

  • AI Toolkit 확장은 MCP 서버와 그 도구를 활용할 수 있는 훌륭한 클라이언트입니다.
  • MCP 서버에 새 도구를 추가하여 에이전트의 기능을 확장하고 변화하는 요구 사항을 충족할 수 있습니다.
  • AI Toolkit은 사용자 정의 도구 생성을 간소화하는 템플릿(예: Python MCP 서버 템플릿)을 포함하고 있습니다.
  • 추가 자료

  • AI Toolkit 문서
  • 다음 단계

  • 다음: 테스트 및 디버깅
  • 면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있지만, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다.

    원본 문서의 원어를 신뢰할 수 있는 권위 있는 출처로 간주해야 합니다.

    중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • 8 Testing 다양한 방법으로 서버와 클라이언트를 테스트하는 방법에 중점을 둡니다, 강의로 가기

    테스트 및 디버깅

    MCP 서버 테스트를 시작하기 전에, 사용 가능한 도구와 디버깅을 위한 모범 사례를 이해하는 것이 중요합니다. 효과적인 테스트는 서버가 예상대로 작동하는지 확인하고 문제를 신속하게 식별하고 해결하는 데 도움을 줍니다. 다음 섹션에서는 MCP 구현을 검증하기 위한 권장 접근 방식을 설명합니다.

    개요

    이 수업에서는 올바른 테스트 접근 방식을 선택하는 방법과 가장 효과적인 테스트 도구를 다룹니다.

    학습 목표

    이 수업을 마치면 다음을 수행할 수 있습니다:

  • 다양한 테스트 접근 방식을 설명할 수 있습니다.
  • 다양한 도구를 사용하여 코드를 효과적으로 테스트할 수 있습니다.
  • MCP 서버 테스트

    MCP는 서버 테스트 및 디버깅을 돕는 도구를 제공합니다:

  • MCP Inspector: CLI 도구와 시각적 도구 모두로 실행할 수 있는 명령행 도구입니다.
  • 수동 테스트: curl과 같은 도구를 사용하여 웹 요청을 실행할 수 있지만 HTTP를 실행할 수 있는 모든 도구를 사용할 수 있습니다.
  • 단위 테스트: 선호하는 테스트 프레임워크를 사용하여 서버와 클라이언트의 기능을 테스트할 수 있습니다.
  • 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 문을 사용할 수 있는 pytest 프레임워크를 활용합니다.
  • 두 가지 다른 도구를 포함하는 MCP 서버를 만듭니다.
  • 특정 조건이 충족되었는지 확인하기 위해 assert 문을 사용합니다.
  • 위 파일을 참고하여 자신의 서버가 예상대로 기능을 생성하는지 테스트할 수 있습니다.

    모든 주요 SDK는 유사한 테스트 섹션을 가지고 있으므로 선택한 런타임에 맞게 조정할 수 있습니다.

    샘플

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • 추가 자료

  • Python SDK
  • 다음 단계

  • 다음: 배포
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하였으나, 자동 번역은 오류나 부정확성이 있을 수 있음을 알려드립니다.

    원본 문서의 원어가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 모든 오해나 오역에 대해 당사는 책임지지 않습니다.

  • 9 Deployment MCP 솔루션을 배포하는 여러 방식을 알아봅니다, 강의로 가기

    MCP 서버 배포

    MCP 서버를 배포하면 로컬 환경을 넘어서 다른 사람들이 해당 도구와 리소스에 접근할 수 있습니다. 확장성, 신뢰성, 관리 용이성에 대한 요구 사항에 따라 고려할 수 있는 여러 배포 전략이 있습니다. 아래에는 MCP 서버를 로컬, 컨테이너, 클라우드에 배포하는 방법에 대한 안내가 나와 있습니다.

    개요

    이 레슨에서는 MCP 서버 앱을 배포하는 방법을 다룹니다.

    학습 목표

    이 레슨을 마치면 다음을 할 수 있습니다:

  • 다양한 배포 접근 방식을 평가할 수 있습니다.
  • 앱을 배포할 수 있습니다.
  • 로컬 개발 및 배포

    서버가 사용자 머신에서 실행되어 소비되는 경우 다음 단계를 따르십시오:

    1. 서버 다운로드. 서버를 직접 작성하지 않았다면 먼저 서버를 머신에 다운로드하십시오.

    1. 서버 프로세스 시작: MCP 서버 애플리케이션을 실행합니다.

    SSE의 경우 (stdio 유형 서버에는 필요하지 않음)

    1. 네트워킹 구성: 서버가 예상된 포트에서 접근 가능하도록 설정합니다.

    1. 클라이언트 연결: http://localhost:3000 같은 로컬 연결 URL을 사용합니다.

    클라우드 배포

    MCP 서버는 여러 클라우드 플랫폼에 배포할 수 있습니다:

  • 서버리스 함수: 경량 MCP 서버를 서버리스 함수로 배포
  • 컨테이너 서비스: Azure Container Apps, AWS ECS, Google Cloud Run 등의 서비스를 사용
  • 쿠버네티스: 고가용성을 위해 쿠버네티스 클러스터에서 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 -n weather-mcp --environment mcp -l westus --env-vars API_KEYS= --source .

    ```

    이렇게 하면 로컬에 배포하고, 이 단계들을 통해 Azure에 배포할 수 있습니다.

    추가 자료

  • Azure Functions + MCP
  • Azure Container Apps 기사
  • Azure Container Apps MCP 리포지토리
  • 다음 단계

  • 다음: 고급 서버 주제
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다했지만, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 알려드립니다.

    원본 문서는 해당 언어로 작성된 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 모든 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • 10 Advanced server usage 고급 서버 사용법을 다룹니다, 강의로 가기

    고급 서버 사용법

    MCP SDK에는 두 가지 유형의 서버가 있습니다. 일반 서버와 저수준 서버(low-level server)입니다. 보통은 일반 서버를 사용하여 기능을 추가합니다. 하지만 때때로 저수준 서버를 사용해야 할 때가 있는데, 예를 들면 다음과 같은 경우입니다:

  • 더 나은 아키텍처. 일반 서버와 저수준 서버를 함께 사용해서 깨끗한 아키텍처를 만들 수 있지만, 저수준 서버를 사용할 때 조금 더 쉽다고 할 수 있습니다.
  • 기능 가용성. 일부 고급 기능은 저수준 서버에서만 사용할 수 있습니다. 이후 장에서 샘플링과 유도(elicitation)를 추가하며 이를 확인할 수 있습니다.
  • 일반 서버 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 
    
    }
    
    

    여기서는 다음을 수행합니다.

  • Pydantic을 사용해 AddInputModel 스키마를 만들고, *schema.py* 파일에 필드 ab를 정의합니다.
  • 들어오는 요청을 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;
    
    

    여기서는 다음 속성으로 이루어진 사전을 생성합니다:

  • name: 도구 이름.
  • rawSchema: Zod 스키마로, 도구 호출 요청 검증용.
  • inputSchema: 핸들러에서 사용하는 스키마.
  • callback: 도구를 호출하는 함수.
  • 또한 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를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있지만, 자동 번역에는 오류나 부정확성이 있을 수 있음을 유의하시기 바랍니다.

    원본 문서는 해당 언어의 공식 문서로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 오해나 오해에 대해 당사는 책임을 지지 않습니다.

  • 11 Auth Basic Auth에서 JWT 및 RBAC 사용까지 간단한 인증 추가 방법을 배웁니다. 여기에서 시작한 후 고급 주제(5장)를 참고하고 2장 추천 사항을 통해 추가 보안 강화 작업을 수행하시기 바랍니다, 강의로 가기

    Simple auth

    MCP SDKs는 OAuth 2.1 사용을 지원합니다.

    솔직히 말하면 인증 서버, 리소스 서버, 자격 증명 게시, 코드 수신, 코드를 베어러 토큰으로 교환하여 최종적으로 리소스 데이터를 얻는 등의 개념이 포함된 꽤 복잡한 프로세스입니다.

    OAuth는 구현하기에 훌륭한 방식이지만 익숙하지 않은 경우, 기본적인 인증 수준부터 시작하여 더 나은 보안으로 점차 구축하는 것이 좋습니다.

    이 장이 존재하는 이유가 바로 더 발전된 인증으로 넘어갈 수 있도록 도와주기 위함입니다.

    인증, 우리가 의미하는 바는?

    인증은 authentication과 authorization의 줄임말입니다. 즉, 우리는 두 가지 작업을 해야 합니다:

  • 인증(Authentication): 사용자가 우리 집에 들어올 수 있는지, 즉 MCP 서버 기능이 있는 리소스 서버에 접근할 권한이 있는지 확인하는 과정입니다.
  • 인가(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)

    ```

  • Authorization 헤더 존재 여부와 전송된 비밀이 유효한지 확인하는 검증 로직을 작성했습니다:
  • ```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. 마지막으로 요청 파이프라인에 요청을 전달하여 요청한 리소스를 반환합니다.

    연습: 인증 구현하기

    지식을 적용해 구현해 봅시다. 계획은 다음과 같습니다:

    서버

  • 웹 서버와 MCP 인스턴스 생성
  • 서버용 미들웨어 구현
  • 클라이언트

  • 헤더를 통해 자격 증명과 함께 웹 요청 전송
  • -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)
    
    

    이 코드는:

  • MCP 서버를 생성하고,
  • MCP 서버에서 starlette 웹 앱 app.streamable_http_app()을 생성하며,
  • uvicorn으로 웹 앱을 호스팅(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 속성에 담긴 자격 증명을 확인하고,
  • 자격 증명 검증 후 요청을 계속 진행시키고 클라이언트 MCP 요청이 정상 동작하도록 허용합니다(도구 목록 요청, 리소스 읽기 또는 기타 MCP 관련 작업).
  • 헤더가 없으면 요청 진행을 차단합니다:

    
    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를 채택하면 어떤 즉각적인 이점이 있을까요?

  • 보안 개선: 기본 인증에서는 사용자 이름과 비밀번호를 base64 인코딩한 토큰(또는 API 키)을 계속 반복해서 전송하는데, 이로 인해 위험이 커집니다. JWT는 사용자 이름과 비밀번호를 보내면 토큰이 발급되고, 이 토큰은 만료 시간이 정해져 있어 기간 제한이 있습니다. 또한 역할, 범위, 권한을 사용해 세밀한 접근 제어가 가능합니다.
  • 무상태성(stateless)과 확장성: JWT는 자체 포함(self-contained)되어 사용자 정보를 모두 담으며 서버 측 세션 저장소가 필요 없어집니다. 토큰은 로컬에서 검증할 수도 있습니다.
  • 상호운용성과 연합: JWT는 Open ID Connect의 핵심이며 Entra ID, Google Identity, Auth0 같은 잘 알려진 아이덴티티 제공자와 함께 사용됩니다. 싱글 사인온 및 그 이상의 기업급 기능을 가능하게 합니다.
  • 모듈성 및 유연성: JWT는 Azure API Management, NGINX 등 API 게이트웨이와도 사용할 수 있으며 인증 시나리오, 서버 대 서비스 통신, 대리 및 위임 시나리오도 지원합니다.
  • 성능 및 캐싱: JWT는 디코딩 후 캐시할 수 있어 파싱 필요성을 줄이며, 특히 트래픽이 많은 애플리케이션에서 처리량을 높이고 인프라 부하를 낮춥니다.
  • 고급 기능: 인트로스펙션(서버에서 유효성 검사)과 토큰 무효화(리보크) 기능도 지원합니다.
  • 이 모든 이점을 바탕으로 구현을 한 단계 업그레이드해봅니다.

    기본 인증을 JWT로 전환하기

    고수준에서 바꿔야 할 사항은 다음과 같습니다:

  • JWT 토큰 구성 방법을 배우고 클라이언트에서 서버로 전송 준비를 합니다.
  • JWT 토큰 검증을 하여 유효하면 클라이언트에게 자원 접근 권한을 제공합니다.
  • 토큰을 안전하게 저장하는 방법을 익힙니다.
  • 경로 보호: MCP 기능 및 경로를 보호합니다.
  • 리프레시 토큰 추가: 짧은 수명의 액세스 토큰과 만료 시 새 토큰을 받을 수 있는 긴 수명의 리프레시 토큰을 만듭니다. 리프레시 엔드포인트와 토큰 회전 전략도 포함합니다.
  • -1- JWT 토큰 구성

    우선 JWT 토큰은 다음 세 부분으로 구성됩니다:

  • 헤더(header): 사용된 알고리즘과 토큰 유형
  • 페이로드(payload): 클레임, 예를 들어 sub(토큰이 나타내는 사용자 또는 엔터티, 보통 userid), exp(만료 시간), role(역할)
  • 서명(signature): 비밀키 또는 개인키로 서명
  • 헤더, 페이로드, 인코딩된 토큰을 구성해야 합니다.

    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)
    
    

    이 코드에서는:

  • HS256 알고리즘과 JWT 타입을 가진 헤더를 정의하였고,
  • sub(주체 또는 사용자 ID), 사용자명, 역할, 발행 시간과 만료 시간을 포함하는 페이로드를 만들어 시간 제한 기능을 구현했습니다.
  • 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)를 살펴봅시다.

    역할 기반 접근 제어 추가하기

    다양한 역할이 각각 다른 권한을 가진다는 것을 표현하고자 합니다. 예를 들어, 관리자는 모든 작업을 할 수 있고, 일반 사용자는 읽기/쓰기를 할 수 있으며, 손님은 읽기만 할 수 있다고 가정합니다. 따라서 가능한 권한 수준은 다음과 같습니다:

  • Admin.Write
  • User.Read
  • Guest.Read
  • 이제 미들웨어로 이러한 제어를 어떻게 구현할 수 있는지 살펴보겠습니다. 미들웨어는 개별 라우트별로 그리고 전체 라우트에 대해 추가할 수 있습니다.

    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 챕터가 준비되어 있습니다.

    다음 단계

  • 다음: MCP 호스트 설정
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나, 자동 번역은 오류나 부정확함을 포함할 수 있음을 유의하시기 바랍니다.

    원문이 해당 언어로 된 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우, 전문 인간 번역을 권장합니다.

    본 번역 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • 12 MCP Hosts Claude Desktop, Cursor, Cline, Windsurf 등 인기 있는 MCP 호스트 클라이언트를 설정하고 사용하는 방법, 전송 유형 및 문제 해결 방법 학습, 강의로 가기

    인기 있는 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
    
    

    사전 준비 사항

  • 연결할 MCP 서버 (자세한 내용은 Module 3.1 - First Server 참조)
  • 시스템에 설치된 호스트 애플리케이션
  • JSON 구성 파일에 대한 기본 이해
  • ---

    1. Claude Desktop

    Claude Desktop은 Anthropic의 공식 데스크톱 애플리케이션으로 MCP를 네이티브로 지원합니다.

    설치

    1. claude.ai/download에서 Claude Desktop 다운로드

    2. 설치 후 Anthropic 계정으로 로그인

    구성

    Claude Desktop은 MCP 서버를 정의하기 위해 JSON 구성 파일을 사용합니다.

    구성 파일 위치:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.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 문제 해결

    서버가 나타나지 않는 경우:

  • JSON 유효성 검사 도구로 구성 파일 문법 확인
  • 명령 경로가 올바른지 확인
  • 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 서버 로드 실패:

  • 출력 패널 → "MCP"에서 오류 로그 확인
  • 창 새로 고침: Ctrl+Shift+P → "Developer: Reload Window"
  • 서버가 독립 실행형으로 먼저 작동하는지 확인
  • ---

    3. Cursor

    Cursor는 MCP를 내장한 AI 중심 코드 에디터입니다.

    설치

    1. cursor.sh에서 Cursor 다운로드

    2. 설치 후 로그인

    구성

    Cursor는 Claude Desktop과 비슷한 구성 형식을 사용합니다.

    구성 파일 위치:

  • macOS: ~/.cursor/mcp.json
  • Windows: %USERPROFILE%\.cursor\mcp.json
  • Linux: ~/.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 WebSocket ------ ------- ---------- ----------- Claude Desktop ✅ ❌ ❌ VS Code ✅ ✅ ❌ Cursor ✅ ✅ ❌ Cline ✅ ✅ ❌ Windsurf ✅ ✅ ❌

    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. 파일 시스템 및 네트워크 접근에 허용 목록 사용

    ---

    다음 단계

  • 3.13 - MCP Inspector로 디버깅하기
  • 3.1 - 첫 번째 MCP 서버 만들기
  • 모듈 5 - 고급 주제
  • ---

    추가 자료

  • Claude Desktop MCP 문서
  • VS Code MCP 확장
  • MCP 명세 - 전송 방식
  • 공식 MCP 서버 레지스트리
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나, 자동 번역은 오류나 부정확성이 포함될 수 있음을 유의해 주시기 바랍니다.

    원본 문서의 원어 버전이 권위 있는 자료로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 사람에 의한 번역을 권장합니다.

    본 번역 사용으로 인한 어떠한 오해나 잘못된 해석에 대해서도 당사는 책임을 지지 않습니다.

  • 13 MCP Inspector MCP Inspector 도구를 사용해 MCP 서버를 인터랙티브하게 디버그하고 테스트하는 방법, 도구와 리소스, 프로토콜 메시지 문제 해결법 학습, 강의로 가기

    MCP Inspector로 디버깅하기

    MCP Inspector는 전체 AI 호스트 애플리케이션 없이도 MCP 서버를 대화형으로 테스트하고 문제를 해결할 수 있는 필수 디버깅 도구입니다. "MCP를 위한 Postman"으로 생각할 수 있으며, 요청을 보내고, 응답을 보고, 서버 동작 방식을 이해할 수 있는 시각적 인터페이스를 제공합니다.

    MCP Inspector를 사용하는 이유

    MCP 서버를 구축할 때 자주 겪는 문제들:

  • "내 서버가 실행 중인가?" - Inspector가 연결 상태를 보여줌
  • "내 도구들이 올바르게 등록되었나?" - Inspector가 모든 도구 목록을 표시함
  • "응답 형식이 어떻게 되지?" - Inspector가 전체 JSON 응답을 표시함
  • "이 도구가 왜 작동하지 않지?" - Inspector가 상세한 오류 메시지를 보여줌
  • 전제 조건

  • Node.js 18 이상 설치
  • npm (Node.js에 포함)
  • 테스트할 MCP 서버 (Module 3.1 - First Server 참조)
  • 설치

    옵션 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"
    
      }
    
    }
    
    

    일반 오류 코드:

    코드 의미 ------ --------- -32700 파싱 오류 (잘못된 JSON) -32600 잘못된 요청 -32601 메서드 없음 -32602 매개변수 오류 -32603 내부 오류

    ---

    리소스 테스트

    리소스 목록 확인

    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를 완료했습니다! 학습을 계속하세요:

  • Module 4: Practical Implementation
  • ---

    추가 자료

  • MCP Inspector GitHub 저장소
  • MCP 명세 - 프로토콜 메시지
  • JSON-RPC 2.0 명세
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 양지해 주시기 바랍니다.

    원본 문서의 원어 버전이 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우, 전문 인력에 의한 번역을 권장합니다.

    본 번역 사용으로 인한 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • 14 Sampling MCP 서버가 MCP 클라이언트와 협력해 LLM 관련 작업 수행하는 서버 만들기, 강의로 가기

    샘플링 - 클라이언트에 기능 위임하기

    때때로 MCP 클라이언트와 MCP 서버가 공동의 목표를 달성하기 위해 협력해야 할 때가 있습니다. 서버가 클라이언트에 있는 LLM의 도움이 필요한 경우가 있을 수 있습니다. 이런 상황에서는 샘플링을 사용해야 합니다.

    샘플링을 포함하는 몇 가지 사용 사례와 솔루션 구축 방법을 살펴보겠습니다.

    개요

    이번 강의에서는 샘플링을 언제, 어디서 사용해야 하는지와 샘플링 구성 방법에 대해 중점적으로 설명합니다.

    학습 목표

    이번 장에서는 다음을 다룹니다:

  • 샘플링이 무엇이며 언제 사용하는지 설명합니다.
  • MCP에서 샘플링을 구성하는 방법을 안내합니다.
  • 샘플링의 실제 예제를 제공합니다.
  • 샘플링이란 무엇이며 왜 사용하는가?

    샘플링은 다음과 같은 방식으로 작동하는 고급 기능입니다:

    
    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
    
      }
    
    }
    
    

    여기서 주목할 만한 점은 다음과 같습니다:

  • content -> text 아래의 Prompt는 LLM에게 블로그 게시물 내용을 요약하라는 지시문입니다.
  • modelPreferences. 이 섹션은 이름 그대로 선호 사항이며, LLM과 함께 사용할 구성을 추천합니다. 사용자는 이 추천대로 하거나 변경할 수 있습니다. 여기서는 사용할 모델, 속도, 지능 우선순위에 대한 추천이 포함되어 있습니다.
  • systemPrompt는 LLM에 성격을 부여하고 안내 지침이 포함된 일반적인 시스템 프롬프트입니다.
  • maxTokens는 이 작업에 권장되는 토큰 수를 나타내는 속성입니다.
  • 샘플링 응답

    이 응답은 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의 도움이 필요할 때 작업을 클라이언트에 위임할 수 있게 하는 강력한 기능입니다.

    다음에 배울 내용

  • 4장 - 실용적 구현
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나 자동 번역에는 오류나 부정확함이 있을 수 있음을 유의해 주시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문 인간 번역을 권장합니다.

    본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임지지 않습니다.

  • 15 MCP Apps UI 지침도 함께 응답하는 MCP 서버 구축, 강의로 가기

    MCP 앱

    MCP 앱은 MCP의 새로운 패러다임입니다.

    아이디어는 도구 호출에서 데이터를 반환하는 것뿐만 아니라 이 정보와 상호작용하는 방법에 대한 정보를 제공하는 것입니다.

    즉, 도구 결과에 이제 UI 정보가 포함될 수 있다는 뜻입니다.

    하지만 왜 그런 것이 필요할까요?

    오늘날 여러분이 하는 방식을 생각해 보세요.

    MCP 서버의 결과를 소비하기 위해 프런트엔드를 만들어야 하며, 이는 여러분이 작성하고 유지해야 하는 코드입니다.

    때로는 그것이 필요한 경우도 있지만, 때로는 데이터부터 사용자 인터페이스까지 모두 포함하는 독립적인 정보 조각을 가져올 수 있다면 좋을 것입니다.

    개요

    이 수업에서는 MCP 앱에 대한 실용적인 가이드, 시작 방법 및 기존 웹 앱에 통합하는 방법을 제공합니다. MCP 앱은 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 파일을 로드하고 구성요소를 반환하는 콜백도 흥미롭습니다.

    구성요소 프런트엔드

    백엔드와 마찬가지로 두 부분이 있습니다:

  • 순수 HTML로 작성된 프런트엔드.
  • 이벤트 처리 및 도구 호출 또는 부모 창에 메시지 보내기 등을 담당하는 코드.
  • 사용자 인터페이스

    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 기능을 구현해 보겠습니다. 작동 방식은 다음과 같습니다:

  • 버튼과 사용자가 검색 키워드(예: "Shipping")를 입력할 수 있는 입력 요소가 있어야 합니다. 이것은 백엔드에서 FAQ 데이터를 검색하는 도구를 호출합니다.
  • 앞서 설명한 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]";
    
    });
    
    

    위 코드에서 우리는:

  • 상호작용 UI 요소에 대한 참조를 만듭니다.
  • 버튼 클릭 시 입력 요소 값을 구문 분석하고 app.callServerTool()을 호출하는데, 여기서 namearguments를 넘기며, 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://.app.github.dev/mcp 에서 엔드포인트에 접속할 수 있는지 확인하세요.

    선택 -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를 입력하세요. 다음과 비슷한 결과를 볼 수 있습니다:

    !Visual Studio Code MCP Apps

    선택 -2- 호스트와 앱 테스트

    리포지토리 에는 MVP 앱을 테스트할 수 있는 여러 호스트가 들어 있습니다.

    여기 두 가지 옵션을 안내해 드립니다:

    로컬 머신

  • 리포를 클론한 뒤 *ext-apps* 폴더로 이동합니다.
  • 종속성을 설치합니다
  • ```sh

    npm install

    ```

  • 별도 터미널 창에서 *ext-apps/examples/basic-host* 폴더로 이동합니다.
  • > Codespace를 사용하는 경우 *serve.ts* 파일 27번째 줄로 이동하여 http://localhost:3001/mcp 를 Codespace 백엔드 URL로 교체해야 합니다. 예를 들어 https://psychic-xylophone-657rpjgvxpc5g64-3001.app.github.dev/mcp 와 같이 변경하세요.

  • 호스트를 실행합니다:
  • ```sh

    npm start

    ```

    이 작업으로 호스트와 백엔드가 연결되고 다음과 같이 앱이 실행되는 것을 볼 수 있습니다:

    !App running

    Codespace

    Codespace 환경을 작동시키려면 약간 추가 작업이 필요합니다. Codespace에서 호스트를 사용하려면:

  • *ext-apps* 디렉터리에서 *examples/basic-host* 로 이동합니다.
  • 종속성을 설치하기 위해 npm install을 실행하세요.
  • 호스트를 시작하기 위해 npm start를 실행하세요.
  • 앱 테스트하기

    다음 방법으로 앱을 테스트해 보세요:

  • "Call Tool" 버튼을 선택하면 다음 그림과 같이 결과를 볼 수 있습니다:
  • !Tool result

    잘 작동하고 있습니다.

    ---

    면책 조항:

    이 문서는 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:

  • 옵션이 있는 드롭다운 리스트
  • 선택을 제출하는 버튼
  • 누가 무엇을 선택했고 누가 이겼는지를 보여 주는 라벨
  • 서버:

  • 입력으로 "choice"를 받는 가위바위보 도구를 가져야 합니다. 컴퓨터 선택을 렌더링하고 승자를 결정합니다.
  • 해답

    요약

    우리는 MCP 앱이라는 새로운 패러다임에 대해 배웠습니다. 이는 MCP 서버가 데이터뿐 아니라 이 데이터를 어떻게 표현할지도 의견을 가질 수 있는 새로운 패러다임입니다.

    또한, MCP 앱은 보안을 위해 IFrame에 호스팅되며 MCP 서버와 소통하려면 부모 웹 앱에 메시지를 보내야 한다는 점도 배웠습니다. 순수 자바스크립트, React 등 다양한 라이브러리가 이 통신을 쉽게 만듭니다.

    주요 내용 정리

    배운 내용은 다음과 같습니다:

  • MCP 앱은 데이터와 UI 기능을 함께 제공하려 할 때 유용한 새로운 표준입니다.
  • 이런 유형의 앱은 보안상 이유로 IFrame에서 실행됩니다.
  • 다음 단계

  • 4장
  • ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 양지하시기 바랍니다.

    원문 문서는 해당 언어의 공식 자료로 간주되어야 합니다.

    중요한 정보에 대해서는 전문적인 인간 번역을 권장합니다.

    이 번역의 사용으로 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

    Model Context Protocol (MCP)은 애플리케이션이 LLM에 컨텍스트를 제공하는 방식을 표준화한 오픈 프로토콜입니다. MCP는 AI 애플리케이션을 위한 USB-C 포트와 같아서 다양한 데이터 소스와 도구를 AI 모델에 연결하는 표준화된 방식을 제공합니다.

    학습 목표

    이 강의가 끝나면 다음을 할 수 있습니다:

  • C#, Java, Python, TypeScript, JavaScript를 위한 MCP 개발 환경 설정
  • 맞춤 기능(리소스, 프롬프트, 도구)을 갖춘 기본 MCP 서버 구축 및 배포
  • MCP 서버에 연결하는 호스트 애플리케이션 작성
  • MCP 구현 테스트 및 디버깅
  • 일반적인 설정 문제점과 해결 방법 이해
  • MCP 구현을 인기 LLM 서비스에 연결하기
  • MCP 환경 설정

    MCP 작업을 시작하기 전 개발 환경을 준비하고 기본 워크플로우를 이해하는 것이 중요합니다. 이 섹션에서 원활하게 MCP를 시작할 수 있도록 초기 설정 단계를 안내합니다.

    사전 준비 사항

    MCP 개발에 들어가기 전에 다음을 준비하세요:

  • 개발 환경: 선택한 언어(C#, Java, Python, TypeScript, JavaScript)용
  • IDE/편집기: Visual Studio, Visual Studio Code, IntelliJ, Eclipse, PyCharm 또는 최신 코드 편집기
  • 패키지 관리자: NuGet, Maven/Gradle, pip, npm/yarn
  • API 키: 호스트 애플리케이션에서 사용할 AI 서비스용
  • 공식 SDK

    앞선 장들에서 Python, TypeScript, Java, .NET을 사용한 솔루션 예시를 볼 수 있습니다. 아래는 공식 지원하는 SDK 목록입니다.

    MCP는 여러 언어용 공식 SDK를 제공합니다 (MCP Specification 2025-11-25 에 맞춤):

  • C# SDK - Microsoft와 협력해 유지 관리
  • Java SDK - Spring AI와 협력해 유지 관리
  • TypeScript SDK - 공식 TypeScript 구현체
  • Python SDK - 공식 Python 구현체 (FastMCP)
  • Kotlin SDK - 공식 Kotlin 구현체
  • Swift SDK - Loopwork AI와 협력해 유지 관리
  • Rust SDK - 공식 Rust 구현체
  • Go SDK - 공식 Go 구현체
  • 주요 내용 정리

  • 언어별 SDK로 MCP 개발 환경 설정은 간단합니다
  • MCP 서버 구축 시 명확한 스키마를 갖춘 도구를 생성하고 등록합니다
  • MCP 클라이언트는 서버 및 모델에 연결해 확장 기능을 활용합니다
  • 테스트와 디버깅은 신뢰할 수 있는 MCP 구현에 필수적입니다
  • 배포 방법은 로컬 개발부터 클라우드 기반 솔루션까지 다양합니다
  • 연습하기

    이 섹션의 모든 장에서 볼 수 있는 연습 문제를 보완하는 샘플 세트를 제공합니다. 각 장마다 고유의 연습문제와 과제도 포함되어 있습니다.

  • Java Calculator

    Basic Calculator MCP Service

    이 서비스는 Spring Boot와 WebFlux 전송을 사용하여 Model Context Protocol(MCP)을 통해 기본 계산기 연산을 제공합니다. MCP 구현을 배우는 초보자를 위한 간단한 예제로 설계되었습니다.

    자세한 내용은 MCP Server Boot Starter 참조 문서를 확인하세요.

    개요

    이 서비스는 다음을 보여줍니다:

  • SSE(Server-Sent Events) 지원
  • Spring AI의 @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

    dev.langchain4j

    langchain4j-github

    ${langchain4j.version}

    ```

    3. 계산기 서버가 localhost:8080에서 실행 중인지 확인

    LangChain4j 클라이언트 실행

    이 예제는 다음을 보여줍니다:

  • SSE 전송을 통해 계산기 MCP 서버에 연결
  • LangChain4j를 사용해 계산기 연산을 활용하는 챗봇 생성
  • GitHub AI 모델(phi-4 모델 사용)과 통합
  • 클라이언트는 다음 샘플 쿼리를 보내 기능을 시연합니다:

    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

    ```

    이 작업은:

  • Maven 3.9.9와 Eclipse Temurin 24 JDK를 사용하는 다단계 Docker 이미지 빌드
  • 최적화된 컨테이너 이미지 생성
  • 포트 8080 노출
  • 컨테이너 내에서 MCP 계산기 서비스 시작
  • 컨테이너가 실행되면 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를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의해 주시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • .Net Calculator
  • JavaScript Calculator

    샘플

    이것은 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를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의해 주시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    본 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • TypeScript Calculator

    샘플

    이것은 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를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 자료로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    본 번역의 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

  • Python Calculator
  • 추가 자료

  • Build Agents using Model Context Protocol on Azure
  • Remote MCP with Azure Container Apps (Node.js/TypeScript/JavaScript)
  • .NET OpenAI MCP Agent
  • 다음 단계

    첫 번째 강의부터 시작하세요: 처음 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 모델을 다양한 데이터 소스 및 도구와 연결하는 표준화된 방법을 제공합니다.

    학습 목표

    이 수업을 마치면 다음을 수행할 수 있습니다:

  • C#, Java, Python, TypeScript, Rust에서 MCP 개발 환경 설정
  • 맞춤 기능(리소스, 프롬프트, 도구)을 갖춘 기본 MCP 서버 구축 및 배포
  • MCP 서버에 연결하는 호스트 애플리케이션 생성
  • MCP 구현 테스트 및 디버깅
  • MCP 환경 설정하기

    MCP 작업을 시작하기 전에 개발 환경을 준비하고 기본 작업 흐름을 이해하는 것이 중요합니다. 이 섹션은 MCP 시작을 원활하게 하기 위한 초기 설정 단계를 안내합니다.

    사전 준비 사항

    MCP 개발에 착수하기 전에 다음이 준비되었는지 확인하세요:

  • 개발 환경: 선택한 언어(C#, Java, Python, TypeScript, Rust)용
  • IDE/편집기: Visual Studio, Visual Studio Code, IntelliJ, Eclipse, PyCharm, 또는 최신 코드 편집기
  • 패키지 관리자: NuGet, Maven/Gradle, pip, npm/yarn, Cargo
  • API 키: 호스트 애플리케이션에서 사용할 AI 서비스용
  • 기본 MCP 서버 구조

    MCP 서버는 일반적으로 다음을 포함합니다:

  • 서버 구성: 포트, 인증 및 기타 설정
  • 리소스: LLM에 제공할 데이터 및 컨텍스트
  • 도구: 모델이 호출할 수 있는 기능
  • 프롬프트: 텍스트 생성 또는 구조화 템플릿
  • 아래는 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);
    
    

    위 코드에서 우리는:

  • MCP TypeScript SDK에서 필요한 클래스를 가져왔습니다.
  • 새 MCP 서버 인스턴스를 생성 및 구성했습니다.
  • 핸들러 함수가 있는 사용자 지정 도구(calculator)를 등록했습니다.
  • MCP 요청을 수신하도록 서버를 시작했습니다.
  • 테스트 및 디버깅

    MCP 서버를 테스트하기 전에 이용 가능한 도구 및 디버깅 모범 사례를 이해하는 것이 중요합니다. 효과적인 테스트는 서버가 예상대로 작동하는지 확인하고 문제를 신속히 파악 및 해결하는 데 도움이 됩니다. 다음 섹션에서 MCP 구현을 검증하기 위한 권장 방법을 설명합니다.

    MCP는 서버 테스트 및 디버깅을 도와주는 도구를 제공합니다:

  • Inspector 도구: 이 그래픽 인터페이스는 서버에 연결하여 도구, 프롬프트, 리소스를 테스트할 수 있습니다.
  • curl: 커맨드 라인 도구인 curl이나 다른 HTTP 명령을 생성 및 실행할 수 있는 클라이언트로도 서버에 연결할 수 있습니다.
  • MCP Inspector 사용하기

    1. 서버 기능 탐색: 사용 가능한 리소스, 도구, 프롬프트 자동 감지

    2. 도구 실행 테스트: 다양한 매개변수로 실시간 응답 확인

    3. 서버 메타데이터 조회: 서버 정보, 스키마, 구성 검토

    
    # 예제 TypeScript, MCP Inspector 설치 및 실행
    
    npx @modelcontextprotocol/inspector node build/index.js
    
    

    위 명령어를 실행하면 MCP Inspector가 브라우저에서 로컬 웹 인터페이스를 실행합니다. 등록된 MCP 서버, 사용 가능한 도구, 리소스 및 프롬프트 대시보드를 볼 수 있습니다. 이 인터페이스로 도구 실행 테스트, 서버 메타데이터 조사, 실시간 응답 확인 등이 가능해 MCP 서버 구현 검증 및 디버깅이 수월해집니다.

    다음은 화면 예시입니다:

    일반적인 설정 문제 및 해결책

    문제 가능한 해결책 ------------------------- -------------------------------------------- 연결 거부됨 서버 실행 여부 및 포트 확인 도구 실행 오류 매개변수 검증 및 오류 처리 검토 인증 실패 API 키 및 권한 확인 스키마 검증 오류 매개변수가 정의된 스키마와 일치하는지 확인 서버가 시작되지 않음 포트 충돌 또는 누락된 종속성 점검 CORS 오류 교차 출처 요청에 적절한 CORS 헤더 구성 인증 문제 토큰 유효성 및 권한 확인

    로컬 개발

    로컬 개발 및 테스트용으로, MCP 서버를 자신의 머신에서 직접 실행할 수 있습니다:

    1. 서버 프로세스 시작: MCP 서버 애플리케이션 실행

    2. 네트워킹 구성: 서버가 예상 포트에서 접근 가능하게 설정

    3. 클라이언트 연결: http://localhost:3000 같은 로컬 연결 URL 사용

    
    # 예시: TypeScript MCP 서버를 로컬에서 실행하기
    
    npm run start
    
    # 서버가 http://localhost:3000 에서 실행 중입니다
    
    

    첫 번째 MCP 서버 구축하기

    이전 수업에서 핵심 개념을 다뤘으니 이제 그 지식을 실습해 보겠습니다.

    서버가 할 수 있는 일

    코딩을 시작하기 전에 서버의 역할을 상기해 봅시다:

    MCP 서버는 예를 들어:

  • 로컬 파일 및 데이터베이스 접근
  • 원격 API 연결
  • 계산 수행
  • 다른 도구 및 서비스 통합
  • 상호작용을 위한 사용자 인터페이스 제공
  • 좋습니다, 무엇을 할 수 있는지 알았으니 코딩을 시작해 봅시다.

    연습: 서버 만들기

    서버를 만들려면 다음 단계를 따르세요:

  • MCP SDK 설치
  • 프로젝트 생성 및 구조 설정
  • 서버 코드 작성
  • 서버 테스트
  • -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}!`
    
        }]
    
      })
    
    );
    
    

    도구는 ab 매개변수를 받고, 다음 형식의 응답을 생성합니다:

    
    {
    
      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}!"
    
    

    위 코드에서 우리는:

  • ab라는 정수 매개변수를 받는 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을 설정하고 Argumentsserver.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" 버튼을 선택하여 서버에 연결하세요

    서버에 연결되면 다음 화면이 보입니다:

    !Connected

    1. "Tools"에서 "listTools"를 선택하세요. "Add"가 표시되면 "Add"를 선택하고 매개변수 값을 입력하세요.

    다음과 같은 응답, 즉 "add" 도구의 결과가 표시됩니다:

    !Result of running 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를 제공합니다:

  • C# SDK - Microsoft와 협력하여 유지 관리
  • Java SDK - Spring AI와 협력하여 유지 관리
  • TypeScript SDK - 공식 TypeScript 구현
  • Python SDK - 공식 Python 구현
  • Kotlin SDK - 공식 Kotlin 구현
  • Swift SDK - Loopwork AI와 협력하여 유지 관리
  • Rust SDK - 공식 Rust 구현
  • 주요 요점

  • 언어별 SDK를 통해 MCP 개발 환경을 간단히 구축할 수 있습니다
  • MCP 서버 구축은 명확한 스키마를 가진 도구를 생성하고 등록하는 과정입니다
  • 테스트 및 디버깅은 신뢰할 수 있는 MCP 구현에 필수적입니다
  • 샘플

  • Java Calculator
  • .Net Calculator
  • JavaScript Calculator
  • TypeScript Calculator
  • Python Calculator
  • Rust Calculator
  • 과제

    선택한 도구를 사용하여 간단한 MCP 서버를 만드세요:

    1. 선호하는 언어(.NET, Java, Python, TypeScript, Rust)로 도구를 구현하세요.

    2. 입력 매개변수와 반환 값을 정의하세요.

    3. 인스펙터 도구를 실행하여 서버가 제대로 작동하는지 확인하세요.

    4. 다양한 입력으로 구현을 테스트하세요.

    솔루션

    추가 리소스

  • Azure에서 Model Context Protocol을 사용하여 에이전트 빌드하기
  • 원격 MCP with Azure Container Apps (Node.js/TypeScript/JavaScript)
  • .NET OpenAI MCP 에이전트
  • 다음 단계

    다음: MCP 클라이언트 시작하기

    ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 노력하고 있으나 자동 번역에는 오류나 부정확성이 포함될 수 있음을 유의해 주시기 바랍니다.

    원본 문서가 원어로 된 공식 자료임을 참고하시기 바랍니다.

    중요한 정보에 대해서는 전문 인간 번역가의 번역을 권장합니다.

    본 번역 사용으로 인해 발생하는 모든 오해나 오역에 대해 당사는 책임을 지지 않습니다.

    이 모듈을 완료한 후에는 계속해서: 모듈 4: 실전 구현

    ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다했지만, 자동 번역에는 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다.

    원문은 해당 언어의 원본 문서가 권위 있는 출처로 간주되어야 합니다.

    중요한 정보의 경우 전문적인 인간 번역을 권장합니다.

    본 번역의 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.