基于MCP TypeScript SDK 手搓一个 MCP Server!

概述

MCP即Model Context Protocol(模型上下文协议)允许应用程序以标准化的方式为大语言模型(LLM)提供上下文,将提供上下文的关注点与实际的LLM交互分离开来。使用MCP的TypeScript SDK 实现了完整的MCP规范,可以轻松实现以下功能:

  • 构建能够连接到任何MCP服务器的MCP客户端
  • 创建并暴露资源、提示和工具的MCP服务器
  • 使用标准传输方式,例如stdio 和 Streamable HTTP
  • 处理所有MCP协议消息和生命周期事件

安装

npm install @modelcontextprotocol/sdk

快速开始

我们来创建一个简单的MCP服务器(mcpServer.js),它暴露一个计算BMI的工具和一些数据。

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',
})

// Simple tool with parameters
server.tool(
  'calculate-bmi',
  {
    weightKg: z.number(),
    heightM: z.number(),
  },
  async ({ weightKg, heightM }) => ({
    content: [
      {
        type: 'text',
        text: String(weightKg / (heightM * heightM)),
      },
    ],
  })
)

// Add a dynamic greeting resource
server.resource(
  'greeting',
  new ResourceTemplate('greeting://{name}', { list: undefined }),
  async (uri, { name }) => ({
    contents: [
      {
        uri: uri.href,
        text: `Hello, ${name}!`,
      },
    ],
  })
)

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)

什么是 MCP?

Model Context Protocol (MCP) 让你可以构建服务器,以安全且标准化的方式向大语言模型(LLM)应用程序暴露数据和功能。你可以把它想象成一个 Web API,但它是专门为 LLM 交互而设计的。

MCP 服务器可以:

  • 通过 Resources 暴露数据 (可以把它们看作是 GET 接口;它们用于将信息加载到 LLM 的上下文中)

  • 通过 Tools 提供功能 (类似于 POST 接口;它们用于执行代码或产生其他副作用)

  • 通过 Prompts 定义交互模式 (提供可重复使用的模板,帮助 LLM 更高效地进行交互)

核心概念

Server

McpServer 是你与 MCP 协议交互的核心接口。它负责处理连接管理、协议合规性以及消息路由

const server = new McpServer({
  name: "My App",
  version: "1.0.0"
});

Resources

Resources 是你向大语言模型(LLM)暴露数据的方式。它们类似于 REST API 中的 GET 接口——用于提供数据,但不应执行复杂的计算或产生副作用。

// Static resource
server.resource(
  "config",
  "config://app",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: "App configuration here"
    }]
  })
);

// Dynamic resource with parameters
server.resource(
  "user-profile",
  new ResourceTemplate("users://{userId}/profile", { list: undefined }),
  async (uri, { userId }) => ({
    contents: [{
      uri: uri.href,
      text: `Profile data for user ${userId}`
    }]
  })
);

Tools

Tools允许大语言模型(LLM)通过你的服务器执行操作。与 Resources 不同,Tools 通常会进行计算并产生副作用:

// Simple tool with parameters
server.tool(
  "calculate-bmi",
  {
    weightKg: z.number(),
    heightM: z.number()
  },
  async ({ weightKg, heightM }) => ({
    content: [{
      type: "text",
      text: String(weightKg / (heightM * heightM))
    }]
  })
);

// Async tool with external API call
server.tool(
  "fetch-weather",
  { city: z.string() },
  async ({ city }) => {
    const response = await fetch(`https://api.weather.com/${city}`);
    const data = await response.text();
    return {
      content: [{ type: "text", text: data }]
    };
  }
);

Prompts

Prompts(是可重复使用的模板,有助于大语言模型(LLM)更高效地与你的服务器进行交互:

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

运行服务

TypeScript 中的 MCP 服务器需要通过(transport)与客户端进行通信。启动服务器的方式取决于你选择的传输协议:

stdio

适用于命令行工具和直接集成场景:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "example-server",
  version: "1.0.0"
});

// ... set up server resources, tools, and prompts ...

const transport = new StdioServerTransport();
await server.connect(transport);

Streamable HTTP

对于远程服务器,可以设置一个 Streamable HTTP 传输层,用于处理客户端请求以及服务器向客户端的推送通知。

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

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

实现 MCP Clients

该 SDK 提供了一个高级的客户端接口,基于Client实现一个客户端(mcpClient.ts),用来测试和调试 Server提供的相关功能:

iimport { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'

const transport = new StdioClientTransport({
  command: 'node',
  args: ['mcpServer.js'],
})

const client = new Client({
  name: 'example-client',
  version: '1.0.0',
})

await client.connect(transport)
// List prompts
const prompts = await client.listPrompts()
console.log(prompts)
const prompt = await client.getPrompt({
  name: 'review-code',
  arguments: {
    code: 'console.log("Hello world")',
  },
})
console.log('prompt:', prompt)

// List resources
const resources = await client.listResources()
console.log(resources)

// Read a resource
const resource = await client.readResource({
  uri: 'greeting://john',
})
console.log('resource:', resource)

// Call a tool
const result = await client.callTool({
  name: 'calculate-bmi',
  arguments: {
    weightKg: 70,
    heightM: 1.7,
  },
})

console.log('tool:', result)

运行客户端,调用Server提供的功能

npx tsx mcpClient.ts

运行结果如下

{ prompts: [ { name: 'review-code', arguments: [Array] } ] }
prompt: { messages: [ { role: 'user', content: [Object] } ] }
{ resources: [] }
resource: { contents: [ { uri: 'greeting://john', text: 'Hello, john!' } ] }
tool: { content: [ { type: 'text', text: '24.221453287197235' } ] }

参考文档

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐