Como Construir e Implementar um Servidor MCP Personalizado em 10 Minutos

💡Quer criar seu próprio Fluxo de Trabalho de IA Agentic sem código? Você pode facilmente criar fluxos de trabalho de IA com o Anakin AI sem nenhum conhecimento de programação. Conecte-se a APIs de LLM como: GPT-4, Claude 3.5 Sonnet, Uncensored Dolphin-Mixtral, Stable Diffusion, DALLE, Web Scraping.... em um

Build APIs Faster & Together in Apidog

Como Construir e Implementar um Servidor MCP Personalizado em 10 Minutos

Start for free
Inhalte
💡
Quer criar seu próprio Fluxo de Trabalho de IA Agentic sem código?

Você pode facilmente criar fluxos de trabalho de IA com o Anakin AI sem nenhum conhecimento de programação. Conecte-se a APIs de LLM como: GPT-4, Claude 3.5 Sonnet, Uncensored Dolphin-Mixtral, Stable Diffusion, DALLE, Web Scraping.... em um único Fluxo de Trabalho!

Esqueça a programação complicada, automatize seu trabalho cotidiano com o Anakin AI!

Por um tempo limitado, você também pode usar o Google Gemini 1.5 e o Stable Diffusion gratuitamente!
Construa facilmente Fluxos de Trabalho de IA Agentic com Anakin AI!
Construa facilmente Fluxos de Trabalho de IA Agentic com Anakin AI

Introdução

O Protocolo de Contexto do Modelo (MCP) representa um avanço significativo no ecossistema de IA, oferecendo uma maneira padronizada de se comunicar com grandes modelos de linguagem. Em vez de cada plataforma de IA implementar sua própria formatação única para mensagens, o MCP visa fornecer uma interface consistente para prompts, respostas e chamadas de função em vários modelos e plataformas.

Enquanto o protocolo em si está evoluindo, construir um servidor básico compatível com MCP pode ser simples. Neste guia, vou te conduzir na criação de um servidor simples, mas funcional, que adere aos princípios fundamentais de manipulação de mensagens em sistemas de IA modernos. Nossa implementação se concentrará em criar uma base que você poderá expandir posteriormente com recursos mais avançados.

10 minutos são tempo suficiente? Para um sistema pronto para produção, certamente não. Mas para um protótipo funcional que demonstra os conceitos-chave? Absolutamente. Vamos começar!

Requisitos

Antes de começarmos, você precisará de:

  • Node.js (v16+) instalado em seu sistema
  • Conhecimento básico de JavaScript/TypeScript
  • Familiaridade com Express.js ou frameworks web similares
  • Um editor de código (VS Code recomendado)
  • Acesso a terminal/línea de comando
  • Gerenciador de pacotes npm ou yarn

Etapa 1: Configurando Seu Projeto (2 minutos)

Primeiro, vamos criar um novo diretório e inicializar nosso projeto:

mkdir mcp-server
cd mcp-server
npm init -y

Agora, instale as dependências necessárias:

npm install express cors typescript ts-node @types/node @types/express @types/cors
npm install --save-dev nodemon

Crie um arquivo de configuração do TypeScript:

npx tsc --init

Edite o tsconfig.json gerado para incluir essas configurações essenciais:

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

Atualize a seção de scripts do seu package.json:

"scripts": {
  "start": "node dist/index.js",
  "dev": "nodemon --exec ts-node src/index.ts",
  "build": "tsc"
}

Etapa 2: Criando o Servidor Central (3 minutos)

Crie seu diretório de origem e arquivo principal do servidor:

mkdir -p src/handlers
touch src/index.ts
touch src/handlers/messageHandler.ts
touch src/types.ts

Vamos definir nossos tipos primeiro em src/types.ts:

// Estrutura básica da mensagem
export interface Content {
  type: string;
  text?: string;
}

export interface Message {
  role: "user" | "assistant" | "system";
  content: Content[] | string;
}

// Estruturas de solicitação e resposta
export interface ModelRequest {
  messages: Message[];
  max_tokens?: number;
  temperature?: number;
  stream?: boolean;
}

export interface ModelResponse {
  message: Message;
}

// Interfaces de chamada de ferramenta
export interface Tool {
  name: string;
  description: string;
  input_schema: {
    type: string;
    properties: Record;
    required?: string[];
  };
}

export interface ToolCall {
  type: "tool_call";
  id: string;
  name: string;
  input: Record;
}

export interface ToolResult {
  type: "tool_result";
  tool_call_id: string;
  content: string;
}

Agora, implemente o servidor básico em src/index.ts:

import express from 'express';
import cors from 'cors';
import { handleMessageRequest } from './handlers/messageHandler';

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json({ limit: '10mb' }));

// Endpoint principal para processamento de mensagens
app.post('/v1/messages', handleMessageRequest);

// Endpoint de verificação de saúde
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Iniciar servidor
app.listen(PORT, () => {
  console.log(`Servidor funcionando na porta ${PORT}`);
  console.log(`Verificação de saúde: http://localhost:${PORT}/health`);
  console.log(`Endpoint de mensagens: http://localhost:${PORT}/v1/messages`);
});

Em seguida, implemente o manipulador de mensagens em src/handlers/messageHandler.ts:

import { Request, Response } from 'express';
import { ModelRequest, ModelResponse, Message, Content } from '../types';

export async function handleMessageRequest(req: Request, res: Response) {
  try {
    const request = req.body as ModelRequest;
    
    // Validação básica
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: 'Formato de solicitação inválido. Array de mensagens é obrigatório.' });
    }
    
    // Log do pedido recebido (para depuração)
    console.log('Solicitação recebida com', request.messages.length, 'mensagens');
    
    // Processar as mensagens
    const response = processMessages(request.messages);
    
    // Retornar a resposta
    return res.status(200).json(response);
  } catch (error) {
    console.error('Erro ao processar a solicitação:', error);
    return res.status(500).json({ error: 'Erro interno do servidor' });
  }
}

function processMessages(messages: Message[]): ModelResponse {
  // Extrair a última mensagem do usuário
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("Nenhuma mensagem do usuário encontrada na conversa");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // Lógica simples de geração de resposta
  let responseText = "";
  
  if (userQuery.toLowerCase().includes('olá') || userQuery.toLowerCase().includes('oi')) {
    responseText = "Olá! Como posso te ajudar hoje?";
  } else if (userQuery.toLowerCase().includes('clima')) {
    responseText = "Não tenho acesso a dados climáticos em tempo real, mas posso te ajudar a entender padrões climáticos em geral.";
  } else if (userQuery.toLowerCase().includes('hora')) {
    responseText = `A hora atual do servidor é ${new Date().toLocaleTimeString()}.`;
  } else {
    responseText = "Recebi sua mensagem. Esta é uma resposta simples do servidor modelo.";
  }
  
  // Construir e retornar a resposta
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: responseText 
      }]
    }
  };
}

function findLastUserMessage(messages: Message[]): Message | undefined {
  // Encontrar a última mensagem com o papel 'user'
  for (let i = messages.length - 1; i >= 0; i--) {
    if (messages[i].role === 'user') {
      return messages[i];
    }
  }
  return undefined;
}

function extractTextContent(message: Message): string {
  if (typeof message.content === 'string') {
    return message.content;
  } else if (Array.isArray(message.content)) {
    return message.content
      .filter(item => item.type === 'text' && item.text)
      .map(item => item.text)
      .join(' ');
  }
  return '';
}

function createErrorResponse(errorMessage: string): ModelResponse {
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: `Erro: ${errorMessage}` 
      }]
    }
  };
}

Etapa 3: Adicionando Capacidade de Chamada de Ferramentas (3 minutos)

Crie um novo arquivo para definições e implementações de ferramentas:

touch src/tools.ts

Implemente algumas ferramentas básicas em src/tools.ts:

import { Tool } from './types';

// Definições de ferramentas
export const availableTools: Tool[] = [
  {
    name: "get_current_time",
    description: "Obter a hora atual do servidor",
    input_schema: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "Fuso horário opcional (padrão é o fuso horário do servidor)"
        }
      }
    }
  },
  {
    name: "calculate",
    description: "Realizar um cálculo matemático",
    input_schema: {
      type: "object",
      properties: {
        expression: {
          type: "string",
          description: "Expressão matemática a ser avaliada"
        }
      },
      required: ["expression"]
    }
  }
];

// Implementações de ferramentas
export function executeToolCall(name: string, params: Record): string {
  switch (name) {
    case "get_current_time":
      return getTime(params.timezone);
    case "calculate":
      return calculate(params.expression);
    default:
      throw new Error(`Ferramenta desconhecida: ${name}`);
  }
}

function getTime(timezone?: string): string {
  const options: Intl.DateTimeFormatOptions = { 
    hour: '2-digit', 
    minute: '2-digit', 
    second: '2-digit',
    timeZoneName: 'short' 
  };
  
  try {
    if (timezone) {
      options.timeZone = timezone;
    }
    return new Date().toLocaleTimeString('pt-BR', options);
  } catch (error) {
    return `${new Date().toLocaleTimeString()} (Hora do servidor)`;
  }
}

function calculate(expression: string): string {
  try {
    // CUIDADO: Em uma aplicação real, você deve usar um método de avaliação mais seguro
    // Este é um exemplo simplificado apenas para demonstração
    const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, '');
    const result = eval(sanitizedExpression);
    return `${expression} = ${result}`;
  } catch (error) {
    return `Erro ao calcular ${expression}: ${error instanceof Error ? error.message : String(error)}`;
  }
}

Agora, atualize o manipulador de mensagens para suportar chamadas de ferramentas modificando src/handlers/messageHandler.ts:

// Adicione essas importações no topo
import { availableTools, executeToolCall } from '../tools';
import { ToolCall, ToolResult } from '../types';

// Atualize a função handleMessageRequest
export async function handleMessageRequest(req: Request, res: Response) {
  try {
    const request = req.body as ModelRequest;
    
    // Validação básica
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: 'Formato de solicitação inválido. Array de mensagens é obrigatório.' });
    }
    
    // Verificar se esta é uma solicitação com resultados de ferramentas
    const lastMessage = request.messages[request.messages.length - 1];
    if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
      const toolCalls = lastMessage.content.filter(item => 
        item.type === 'tool_call') as ToolCall[];
      
      if (toolCalls.length > 0) {
        // Esta é um acompanhamento com resultados de ferramentas
        return handleToolResultsResponse(request, res);
      }
    }
    
    // Processar como uma mensagem normal
    const response = processMessages(request.messages);
    
    // Retornar a resposta
    return res.status(200).json(response);
  } catch (error) {
    console.error('Erro ao processar solicitação:', error);
    return res.status(500).json({ error: 'Erro interno do servidor' });
  }
}

// Adicione esta função para lidar com chamadas de ferramentas
function handleToolResultsResponse(request: ModelRequest, res: Response) {
  const messages = request.messages;
  const lastAssistantMessage = messages[messages.length - 1];
  
  if (lastAssistantMessage.role !== 'assistant' || !Array.isArray(lastAssistantMessage.content)) {
    return res.status(400).json({ error: 'Formato de resultados de ferramentas inválido' });
  }
  
  // Encontrar chamadas de ferramentas e resultados
  const toolCalls = lastAssistantMessage.content.filter(
    item => item.type === 'tool_call'
  ) as ToolCall[];
  
  const toolResults = lastAssistantMessage.content.filter(
    item => item.type === 'tool_result'
  ) as ToolResult[];
  
  // Processar os resultados
  let finalResponse = "Eu processei as seguintes informações:\n\n";
  
  toolResults.forEach(result => {
    const relatedCall = toolCalls.find(call => call.id === result.tool_call_id);
    if (relatedCall) {
      finalResponse += `- ${relatedCall.name}: ${result.content}\n`;
    }
  });
  
  return res.status(200).json({
    message: {
      role: "assistant",
      content: [{ type: "text", text: finalResponse }]
    }
  });
}

// Modifique a função processMessages para lidar com potenciais chamadas de ferramentas
function processMessages(messages: Message[]): ModelResponse {
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("Nenhuma mensagem do usuário encontrada na conversa");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // Procure palavras-chave que podem acionar o uso de ferramentas
  if (userQuery.toLowerCase().includes('hora')) {
    return {
      message: {
        role: "assistant",
        content: [
          { type: "tool_call", id: "call_001", name: "get_current_time", input: {} },
          { 
            type: "text", 
            text: "Vou verificar a hora atual para você." 
          }
        ]
      }
    };
  } else if (userQuery.toLowerCase().match(/calcular|computar|qual é \d+[\+\-\*\/]/)) {
    // Extrair um possível cálculo
    const expression = userQuery.match(/(\d+[\+\-\*\/\(\)\.]*\d+)/)?.[0] || "1+1";
    
    return {
      message: {
        role: "assistant",
        content: [
          { 
            type: "tool_call", 
            id: "call_002", 
            name: "calculate", 
            input: { expression } 
          },
          { 
            type: "text", 
            text: `Vou calcular ${expression} para você.` 
          }
        ]
      }
    };
  }
  
  // Resposta padrão para outras consultas
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: `Recebi sua mensagem: "${userQuery}". Como posso ajudar mais?` 
      }]
    }
  };
}

Etapa 4: Testes e Implantação (2 minutos)

Vamos criar um script de teste simples para verificar nosso servidor. Crie um arquivo test.js no diretório raiz:

const fetch = require('node-fetch');

async function testServer() {
  const url = 'http://localhost:3000/v1/messages';
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messages: [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: "Que horas são agora?"
            }
          ]
        }
      ]
    })
  });
  
  const data = await response.json();
  console.log(JSON.stringify(data, null, 2));
}

testServer().catch(console.error);

Para testar seu servidor:

# Inicie o servidor
npm run dev

# Em outro terminal, execute o teste
node test.js

Para uma implantação rápida, vamos criar um Dockerfile simples:

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

Construa e execute o contêiner:

docker build -t mcp-server .
docker run -p 3000:3000 mcp-server

Conclusão

Em apenas 10 minutos, construímos um servidor básico que implementa os conceitos-chave dos modernos protocolos de mensagens de IA. Nosso servidor pode:

  1. Processar solicitações de mensagens estruturadas
  2. Responder em um formato padrão
  3. Manipular funcionalidade básica de chamadas de ferramentas
  4. Processar e responder a resultados de ferramentas

Embora esta implementação seja simplificada, ela fornece uma base sólida para desenvolvimento futuro. Para expandir este servidor para uso em produção, considere:

  • Adicionar autenticação e limitação de taxa
  • Implementar tratamento de erros adequado e validação
  • Conectar a modelos de IA reais para processamento
  • Adicionar ferramentas mais sofisticadas
  • Implementar respostas em streaming
  • Adicionar logging e monitoramento abrangentes

Lembre-se de que, embora nossa implementação siga princípios gerais dos protocolos modernos de mensagens de IA, implementações específicas como a API da OpenAI ou a API do Claude da Anthropic podem ter requisitos adicionais ou variações em seus formatos esperados. Sempre consulte a documentação oficial para o serviço específico com o qual você está integrando.

O campo dos protocolos de mensagens de IA está evoluindo rapidamente, então fique atualizado com os últimos desenvolvimentos e esteja preparado para adaptar sua implementação à medida que os padrões evoluem. Ao entender os conceitos principais demonstrados neste guia, você estará bem preparado para trabalhar com quaisquer novas inovações que surgirem neste emocionante campo.