Skip to content
Lesson 2/11 · MCP cho người mới bắt đầu (Microsoft)

Xây MCP client đầu tiên

Viết client tự kết nối tới MCP server, liệt kê và gọi prompt, resource, tool bằng TypeScript, Python, .NET, Java hoặc Rust.

This content is currently available in Vietnamese only.

Beginner@shine190418 min read

Client là ứng dụng hoặc script tuỳ chỉnh giao tiếp trực tiếp với MCP server để yêu cầu resource, tool và prompt. Khác với dùng Inspector, vốn có giao diện đồ hoạ để tương tác với server, tự viết client cho phép tương tác bằng lập trình và tự động. Nhờ đó lập trình viên đưa được khả năng (capability) của MCP vào quy trình (workflow) của mình, tự động hoá tác vụ và xây giải pháp theo nhu cầu riêng.

Tổng quan

Bài này giới thiệu khái niệm client trong hệ sinh thái Model Context Protocol (MCP). Bạn sẽ học cách viết client của riêng mình và kết nối nó tới một MCP server.

Mục tiêu học tập

Sau bài này, bạn sẽ có thể:

  • Hiểu client làm được gì.
  • Viết client của riêng bạn.
  • Kết nối và kiểm thử client với một MCP server để đảm bảo server hoạt động như mong đợi.

Viết client gồm những gì?

Để viết client, bạn cần:

  • Import đúng thư viện. Bạn dùng cùng thư viện như trước, chỉ khác các thành phần.
  • Khởi tạo client. Tạo một instance client và kết nối nó với cơ chế truyền tải (transport) đã chọn.
  • Quyết định liệt kê resource nào. MCP server có resource, tool và prompt; bạn cần quyết định liệt kê những gì.
  • Tích hợp client vào ứng dụng host. Khi đã biết capability của server, bạn tích hợp vào ứng dụng host để khi người dùng gõ prompt hoặc lệnh khác thì tính năng tương ứng của server được gọi.

Đã hiểu tổng quan việc sắp làm, hãy xem một ví dụ.

Ví dụ một client

Đây là một client mẫu bằng TypeScript:

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

Trong code trên, chúng ta:

  • Import các thư viện.
  • Tạo một instance client và kết nối bằng transport stdio.
  • Liệt kê prompt, resource, tool và gọi tất cả chúng.

Vậy là xong, một client nói chuyện được với MCP server. Ở phần bài thực hành dưới đây, chúng ta sẽ phân tích từng đoạn code.

Bài thực hành: viết client

Hãy từ từ giải thích code, và bạn cứ viết theo nếu muốn.

Bước 1: import thư viện

Hãy import các thư viện cần thiết: tham chiếu tới client và tới giao thức truyền tải đã chọn là stdio. stdio là giao thức cho những thứ chạy trên máy local. SSE là một giao thức truyền tải khác sẽ được giới thiệu ở các bài sau, đó là lựa chọn còn lại. Còn bây giờ, hãy tiếp tục với stdio.

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

Tiếp theo là phần khởi tạo.

Bước 2: khởi tạo client và transport

Chúng ta cần tạo một instance transport và một instance 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);

Trong code trên, chúng ta:

  • Tạo một instance transport stdio. Để ý nó chỉ định command và args để tìm và khởi động server, vì đó là việc client cần làm.

    typescript
    const transport = new StdioClientTransport({
        command: "node",
        args: ["server.js"]
    });
    
  • Khởi tạo client với tên và phiên bản.

    typescript
    const client = new Client(
    {
        name: "example-client",
        version: "1.0.0"
    });
    
  • Kết nối client với transport đã chọn.

    typescript
    await client.connect(transport);
    

Bước 3: liệt kê tính năng của server

Giờ ta có một client kết nối được khi chạy chương trình. Nhưng nó chưa liệt kê tính năng của server, hãy làm việc đó:

typescript
// List prompts
const prompts = await client.listPrompts();

// List resources
const resources = await client.listResources();

// list tools
const tools = await client.listTools();

Tốt, giờ ta đã nắm được mọi tính năng. Câu hỏi là khi nào dùng chúng? Client này khá đơn giản, theo nghĩa ta phải gọi tường minh các tính năng khi cần. Ở bài sau, chúng ta sẽ tạo client nâng cao hơn, có LLM riêng. Còn bây giờ, hãy xem cách gọi các tính năng trên server.

Bước 4: gọi tính năng

Để gọi tính năng, cần chỉ định đúng đối số và, trong một số trường hợp, tên của thứ cần gọi.

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\")"
    }
})

Trong code trên, chúng ta:

  • Đọc một resource bằng readResource() với uri. Phía server có thể trông như sau:

    typescript
    server.resource(
        "readFile",
        new ResourceTemplate("file://{name}", { list: undefined }),
        async (uri, { name }) => ({
          contents: [{
            uri: uri.href,
            text: `Hello, ${name}!`
          }]
        })
    );
    

    Giá trị uri là file://example.txt khớp với file://{name} trên server; example.txt được ánh xạ vào name.

  • Gọi một tool bằng cách chỉ định name và arguments:

    typescript
    const result = await client.callTool({
        name: "example-tool",
        arguments: {
            arg1: "value"
        }
    });
    
  • Lấy prompt bằng getPrompt() với name và arguments. Code server như sau:

    typescript
    server.prompt(
        "review-code",
        { code: z.string() },
        ({ code }) => ({
            messages: [{
            role: "user",
            content: {
                type: "text",
                text: `Please review this code:\n\n${code}`
            }
            }]
        })
    );
    

    nên code client tương ứng để khớp với khai báo trên server là:

    typescript
    const promptResult = await client.getPrompt({
        name: "review-code",
        arguments: {
            code: "console.log(\"Hello world\")"
        }
    })
    

Bước 5: chạy client

Để chạy client, gõ lệnh sau trong terminal:

Thêm mục sau vào phần "scripts" trong package.json:

json
"client": "tsc && node build/client.js"
sh
npm run client

Bài tập

Trong bài tập này, bạn áp dụng những gì đã học để tạo client của riêng mình.

Đây là một server bạn có thể gọi từ code client; hãy thử thêm tính năng cho server để nó thú vị hơn.

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

Lời giải

Thư mục lời giải chứa các bản cài đặt client hoàn chỉnh, chạy được ngay, minh hoạ mọi khái niệm trong bài. Mỗi lời giải có cả code client lẫn server, tổ chức thành các project độc lập.

Cấu trúc lời giải

Thư mục lời giải được tổ chức theo ngôn ngữ lập trình:

text
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

Mỗi lời giải gồm gì

Mỗi lời giải theo ngôn ngữ cung cấp:

  • Bản cài đặt client hoàn chỉnh với mọi tính năng trong bài
  • Cấu trúc project chạy được với dependency và cấu hình đúng
  • Script build và chạy để thiết lập, chạy dễ dàng
  • README chi tiết hướng dẫn theo từng ngôn ngữ
  • Ví dụ xử lý lỗi và xử lý kết quả

Dùng lời giải

  1. Vào thư mục ngôn ngữ bạn chọn:

    bash
    cd solution/typescript/    # For TypeScript
    cd solution/java/          # For Java
    cd solution/python/        # For Python
    cd solution/dotnet/        # For .NET
    
  2. Làm theo README trong từng thư mục để cài dependency, build project và chạy client.

  3. Đầu ra mẫu bạn sẽ thấy:

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

Tài liệu đầy đủ và hướng dẫn từng bước: tài liệu lời giải.

Ví dụ hoàn chỉnh

Khoá gốc cung cấp các bản cài đặt client hoàn chỉnh, chạy được cho mọi ngôn ngữ trong bài. Chúng minh hoạ đầy đủ chức năng mô tả ở trên, dùng làm tham chiếu hoặc điểm xuất phát cho project của bạn.

Ngôn ngữFileMô tả
Javaclient_example_java.javaClient Java hoàn chỉnh dùng transport SSE, xử lý lỗi đầy đủ
C#client_example_csharp.csClient C# hoàn chỉnh dùng transport stdio, tự khởi động server
TypeScriptclient_example_typescript.tsClient TypeScript hoàn chỉnh, hỗ trợ đầy đủ giao thức MCP
Pythonclient_example_python.pyClient Python hoàn chỉnh dùng async/await
Rustclient_example_rust.rsClient Rust hoàn chỉnh dùng Tokio cho xử lý bất đồng bộ

Mỗi ví dụ hoàn chỉnh gồm:

  • Thiết lập kết nối và xử lý lỗi
  • Khám phá server (tool, resource, prompt nếu có)
  • Phép toán máy tính (add, subtract, multiply, divide, help)
  • Xử lý kết quả và định dạng đầu ra
  • Xử lý lỗi toàn diện
  • Code sạch, có tài liệu với chú thích từng bước

Bắt đầu với ví dụ hoàn chỉnh

  1. Chọn ngôn ngữ từ bảng trên.
  2. Đọc file ví dụ để hiểu toàn bộ phần cài đặt.
  3. Chạy ví dụ theo hướng dẫn trong complete_examples.md.
  4. Sửa và mở rộng ví dụ cho tình huống của bạn.

Thư mục lời giải và ví dụ hoàn chỉnh

Thư mục lời giảiVí dụ hoàn chỉnh
Cấu trúc project đầy đủ với file buildCài đặt trong một file
Chạy được ngay với dependencyVí dụ code tập trung
Thiết lập gần với productionTham chiếu để học
Công cụ riêng của từng ngôn ngữSo sánh giữa các ngôn ngữ

Cả hai cách đều có giá trị: dùng thư mục lời giải cho project hoàn chỉnh và ví dụ hoàn chỉnh để học và tham khảo.

Điểm chính

Những điểm chính của bài về client:

  • Dùng được cả để khám phá lẫn gọi các tính năng trên server.
  • Có thể khởi động server khi chính nó khởi động (như trong bài này), nhưng client cũng có thể kết nối tới server đang chạy.
  • Là cách tuyệt vời để thử các capability của server, bên cạnh các lựa chọn khác như Inspector ở bài trước.

Tài liệu thêm

Mẫu code

Bài tiếp theo: Client có LLM.

Nguồn