Preludio
Si escribes mucho Rust y pasas la mayor parte del día dentro de Claude Code, esos dos mundos pueden haber permanecido separados durante un tiempo. Rust para construir servicios y bibliotecas. Claude Code para hablar con una IA que ayuda a escribirlos. Entonces el Model Context Protocol lo cambió todo.
MCP te permite dar a Claude Code nuevas capacidades escribiendo servidores que exponen herramientas, recursos y prompts sobre un protocolo estandarizado. Puedes escribir estos servidores en TypeScript, Python, Go o cualquier lenguaje que pueda manejar JSON-RPC sobre stdio. Pero si ya conoces Rust, hay una razón convincente para elegirlo aquí. Los servidores MCP son procesos de larga duración que se sitúan entre tu asistente de IA y tu sistema. Manejan E/S de archivos, llamadas de red y operaciones potencialmente sensibles. Rust te da seguridad de memoria, rendimiento predecible y un único binario estático sin dependencias de runtime. Sin carpeta node_modules. Sin entorno virtual de Python. Solo un binario que puedes colocar en cualquier máquina y ejecutar.
El crate rmcp es el SDK oficial de Rust para el Model Context Protocol. Ha superado los 4,7 millones de descargas en crates.io a principios de 2026 y proporciona una API basada en macros que hace que construir servidores MCP se sienta natural en Rust. Esta guía recorre la construcción de un servidor MCP completo desde cero, uno que realmente hace algo útil, y su conexión a Claude Code.
Por qué construir un servidor MCP personalizado
Cada tutorial de MCP por ahí construye un cliente de API del tiempo. Está bien para aprender la forma del protocolo, pero no refleja por qué la mayoría de los desarrolladores realmente quieren servidores MCP personalizados. El caso de uso real es dar a Claude Code acceso más profundo a tu flujo de trabajo específico. Cosas que no puede hacer de serie.
Aquí va un escenario común. Cuando trabajas en un código base grande, los desarrolladores a menudo necesitan estadísticas rápidas. ¿Cuántas líneas de Rust hay en este proyecto? ¿Qué archivos son los más grandes? ¿Cuál es el desglose por lenguajes? Los one-liners de shell pueden responder estas preguntas, pero que Claude Code acceda a estas capacidades nativamente significa que puede usarlas mientras razona sobre tu código.
Así que vamos a construir code-stats, un servidor MCP que proporciona tres herramientas. La primera cuenta líneas en archivos que coinciden con una extensión dada. La segunda encuentra los archivos más grandes en un directorio. La tercera da un desglose completo de lenguajes de un proyecto. Al final, Claude Code podrá llamar a estas herramientas siempre que necesite entender la forma de un código base.
El camino
Qué es realmente MCP
Antes de escribir código, ayuda entender sobre qué estamos construyendo. El Model Context Protocol es un protocolo basado en JSON-RPC 2.0 que define cómo las aplicaciones de IA (llamadas clientes) se comunican con proveedores de capacidades externos (llamados servidores). Si has trabajado con el Language Server Protocol que impulsa funciones del editor como autocompletado, MCP sigue una arquitectura similar.
Un servidor MCP puede exponer tres tipos de capacidades. Las herramientas son funciones que la IA puede llamar, como "contar líneas en estos archivos". Los recursos son datos que la IA puede leer, como un archivo de configuración o un registro de base de datos. Los prompts son plantillas reutilizables que la IA puede usar. Para esta guía, nos centraremos en herramientas porque son las más inmediatamente útiles.
La comunicación ocurre sobre una capa de transporte. Las dos opciones principales son stdio y Streamable HTTP. Stdio es la más simple. El cliente lanza tu servidor como proceso hijo y envía mensajes JSON-RPC por stdin. Tu servidor responde por stdout. Esto es exactamente cómo Claude Code se integra con servidores MCP locales, y es lo que usaremos.
Un detalle crítico sobre el transporte stdio. Nunca uses println!() en un servidor MCP que comunica por stdio. Tu stdout es el canal del protocolo. Si imprimes mensajes de depuración a stdout, corromperás el flujo JSON-RPC y el cliente se desconectará. Usa eprintln!() o, mejor aún, un framework de logging propio dirigido a stderr.
Configurando el proyecto
Empecemos con un proyecto Rust nuevo.
cargo new code-stats
cd code-stats
Abre Cargo.toml y añade las dependencias que necesitamos.
[package]
name = "code-stats"
version = "0.1.0"
edition = "2021"
[dependencies]
rmcp = { version = "0.16", features = ["server", "transport-io", "macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
schemars = "0.8"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Aquí explicamos por qué está cada dependencia. El crate rmcp es el SDK MCP en sí. La feature server habilita la funcionalidad del lado servidor. La feature transport-io nos da el transporte stdio. La feature macros habilita las macros derivadas que hacen que las definiciones de herramientas sean limpias. Usamos tokio porque rmcp es async. serde y serde_json manejan la serialización. schemars genera definiciones de JSON Schema a partir de tipos Rust, que es cómo los clientes MCP descubren qué parámetros aceptan tus herramientas. anyhow nos da manejo ergonómico de errores. Y tracing con tracing-subscriber proporciona logging estructurado que escribe a stderr, manteniendo stdout limpio para el protocolo.
Definiendo tu primera herramienta
Ahora construyamos el servidor. Abre src/main.rs y empieza con los imports y nuestra struct del servidor.
use anyhow::Result;
use rmcp::handler::server::tool::ToolCallContext;
use rmcp::handler::server::wrapper::Json;
use rmcp::model::{ServerCapabilities, ServerInfo, Tool};
use rmcp::schemars;
use rmcp::serde;
use rmcp::{tool, ServerHandler};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::info;
#[derive(Debug, Clone)]
pub struct CodeStatsServer;
Nuestra struct del servidor es deliberadamente simple. No contiene estado porque cada llamada a herramienta es autocontenida. Recibe una ruta de directorio, lo analiza y devuelve resultados. Si tu servidor MCP necesitara mantener conexiones o cachés, añadirías campos aquí.
Ahora definamos el tipo de entrada para nuestra primera herramienta. Aquí es donde entra schemars. Al derivar JsonSchema, le decimos al cliente MCP exactamente qué parámetros acepta esta herramienta, incluyendo descripciones y tipos.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CountLinesInput {
/// The directory path to search in
pub path: String,
/// File extension to filter by (e.g. "rs", "py", "js"). Do not include the dot.
pub extension: String,
}
Los comentarios de documentación en cada campo se convierten en las descripciones de parámetros que Claude Code ve. Las buenas descripciones importan porque ayudan a la IA a entender cuándo y cómo usar tu herramienta.
Ahora implementamos la herramienta usando el sistema de macros de rmcp. El atributo #[tool] en un método dentro de un bloque impl lo registra como herramienta MCP.
#[tool(tool_box)]
impl CodeStatsServer {
#[tool(description = "Count total lines in files matching a given extension within a directory")]
pub async fn count_lines(
&self,
#[tool(aggr)] input: Json<CountLinesInput>,
) -> Result<String, anyhow::Error> {
let path = PathBuf::from(&input.path);
if !path.exists() {
return Ok(format!("Error: path '{}' does not exist", input.path));
}
if !path.is_dir() {
return Ok(format!("Error: path '{}' is not a directory", input.path));
}
let mut total_lines: u64 = 0;
let mut file_count: u64 = 0;
let ext = &input.extension;
count_lines_recursive(&path, ext, &mut total_lines, &mut file_count)?;
Ok(format!(
"Found {} .{} files containing {} total lines in '{}'",
file_count, ext, total_lines, input.path
))
}
}
Hay que notar algunas cosas aquí. El atributo #[tool(tool_box)] en el bloque impl le dice a rmcp que este bloque contiene definiciones de herramientas. El atributo #[tool(description = "...")] en el método define lo que Claude Code ve cuando lista las herramientas disponibles. El atributo #[tool(aggr)] en el parámetro de entrada significa "agregar todos los parámetros en esta struct", así que los campos de JSON Schema de CountLinesInput se convierten en los parámetros de la herramienta. El tipo de retorno es Result<String, anyhow::Error>. El contenido de la cadena se convierte en la respuesta de la herramienta que Claude Code lee.
También necesitamos la función auxiliar recursiva que hace el recorrido real de archivos.
fn count_lines_recursive(
dir: &Path,
extension: &str,
total_lines: &mut u64,
file_count: &mut u64,
) -> Result<()> {
let entries = fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
// Skip hidden directories and common non-source directories
if path.is_dir() {
let dir_name = path.file_name().unwrap_or_default().to_string_lossy();
if dir_name.starts_with('.')
|| dir_name == "target"
|| dir_name == "node_modules"
|| dir_name == "vendor"
{
continue;
}
count_lines_recursive(&path, extension, total_lines, file_count)?;
} else if path.extension().map_or(false, |e| e == extension) {
match fs::read_to_string(&path) {
Ok(content) => {
*total_lines += content.lines().count() as u64;
*file_count += 1;
}
Err(_) => {
// Skip binary or unreadable files silently
}
}
}
}
Ok(())
}
Observa que omitimos directorios ocultos, target, node_modules y vendor. Esta es una decisión de diseño práctica. Sin ella, escanear un proyecto Rust descendería al directorio target y contaría miles de archivos generados. Tus herramientas MCP deberían codificar este tipo de conocimiento del dominio. La IA no necesita pensar en qué directorios omitir, tu herramienta se encarga de eso.
Implementando el handler del servidor
rmcp requiere que implementes el trait ServerHandler en tu struct del servidor. Este trait define la identidad y las capacidades del servidor. Aquí está la implementación.
#[tool(tool_box)]
impl ServerHandler for CodeStatsServer {
fn name(&self) -> String {
"code-stats".to_string()
}
fn instructions(&self) -> String {
"A server that provides code statistics tools. Use count_lines to count lines of code \
by file extension, find_largest_files to identify the biggest files, and \
language_breakdown to get a summary of languages used in a project."
.to_string()
}
}
El método name() devuelve un identificador legible por humanos. El método instructions() devuelve una descripción que ayuda a la IA a entender para qué sirve este servidor y cuándo usarlo. El atributo #[tool(tool_box)] en este bloque impl le dice a rmcp que conecte automáticamente las herramientas que definimos en el bloque impl anterior.
La función main y el transporte
La función main une todo. Configura el logging, crea el servidor y comienza a escuchar en stdio.
#[tokio::main]
async fn main() -> Result<()> {
// Configure logging to stderr (critical for stdio transport)
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("code_stats=info".parse()?)
)
.with_writer(std::io::stderr)
.init();
info!("Starting code-stats MCP server");
let server = CodeStatsServer;
let transport = rmcp::transport::io::stdio();
let server_handle = server.serve(transport).await?;
server_handle.waiting().await?;
Ok(())
}
Esto es lo que sucede paso a paso. Primero, configuramos tracing_subscriber para escribir a stderr. Esto no es opcional para servidores MCP stdio. Si cualquier línea de log llega a stdout, el protocolo se rompe. La llamada from_default_env() significa que puedes controlar la verbosidad del log con la variable de entorno RUST_LOG, lo cual es útil para depuración.
Luego creamos nuestra struct del servidor y el transporte stdio. La función rmcp::transport::io::stdio() crea un transporte que lee de stdin y escribe a stdout. Llamamos a server.serve(transport) que inicia el bucle de mensajes JSON-RPC. La llamada waiting() bloquea hasta que el cliente se desconecta.
Compilando y probando localmente
Asegurémonos de que todo compila.
cargo build --release
Tu binario estará en target/release/code-stats. Puedes hacer una comprobación rápida enviando un mensaje JSON-RPC de inicialización manualmente, pero la prueba real es conectarlo a Claude Code.
Antes de eso, verifiquemos que el binario se ejecuta sin fallar.
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}' | ./target/release/code-stats
Deberías ver una respuesta JSON con las capacidades del servidor y la lista de herramientas. Si ves un error o nada en absoluto, comprueba que tu código compila limpiamente y que no estás escribiendo accidentalmente a stdout en ningún sitio.
Conectando a Claude Code
Ahora la parte satisfactoria. Conectemos nuestro servidor a Claude Code. Hay dos formas de hacerlo.
La primera es el comando CLI, que es bueno para pruebas rápidas.
claude mcp add --transport stdio code-stats -- /absolute/path/to/code-stats/target/release/code-stats
La segunda es un archivo .mcp.json en la raíz de tu proyecto, que es mejor para compartir con tu equipo. Crea .mcp.json con el siguiente contenido.
{
"mcpServers": {
"code-stats": {
"command": "/absolute/path/to/code-stats/target/release/code-stats"
}
}
}
Si estás usando el enfoque de .mcp.json, el servidor estará disponible siempre que abras Claude Code en ese directorio de proyecto. El enfoque CLI lo registra en tu ámbito local por defecto.
Verifica que el servidor esté registrado.
claude mcp list
Deberías ver code-stats en la salida. Ahora abre Claude Code e intenta preguntarle algo como "How many lines of Rust are in this project?" Claude Code descubrirá la herramienta count_lines de tu servidor y la llamará automáticamente.
Si has trabajado antes con servidores MCP en otros lenguajes, apreciarás no tener que instalar ningún runtime. Para una visión más profunda de cómo Claude Code gestiona los servidores MCP y qué más puedes hacer con ellos, nuestra guía sobre servidores MCP y extensiones de Claude Code cubre el panorama más amplio.
Añadiendo una segunda herramienta
Una herramienta es útil. Múltiples herramientas que trabajan juntas son poderosas. Añadamos dos herramientas más para demostrar cómo escala el patrón. Primero, una herramienta para encontrar los archivos más grandes en un directorio.
Añade el tipo de entrada.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct FindLargestFilesInput {
/// The directory path to search in
pub path: String,
/// Maximum number of files to return (defaults to 10)
pub limit: Option<u32>,
}
El Option<u32> para limit significa que este parámetro es opcional en el esquema MCP. Claude Code puede llamar a la herramienta con o sin especificar un límite. Luego añade el método de la herramienta dentro del bloque #[tool(tool_box)] impl CodeStatsServer existente.
#[tool(description = "Find the largest files in a directory, sorted by size descending")]
pub async fn find_largest_files(
&self,
#[tool(aggr)] input: Json<FindLargestFilesInput>,
) -> Result<String, anyhow::Error> {
let path = PathBuf::from(&input.path);
if !path.exists() {
return Ok(format!("Error: path '{}' does not exist", input.path));
}
if !path.is_dir() {
return Ok(format!("Error: path '{}' is not a directory", input.path));
}
let limit = input.limit.unwrap_or(10) as usize;
let mut files: Vec<(PathBuf, u64)> = Vec::new();
collect_file_sizes(&path, &mut files)?;
files.sort_by(|a, b| b.1.cmp(&a.1));
files.truncate(limit);
let mut output = format!("Top {} largest files in '{}':\n\n", limit, input.path);
for (file_path, size) in &files {
let display_path = file_path
.strip_prefix(&path)
.unwrap_or(file_path)
.display();
output.push_str(&format_file_size(*size, &display_path.to_string()));
output.push('\n');
}
Ok(output)
}
Y las funciones auxiliares.
fn collect_file_sizes(dir: &Path, files: &mut Vec<(PathBuf, u64)>) -> Result<()> {
let entries = fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
let dir_name = path.file_name().unwrap_or_default().to_string_lossy();
if dir_name.starts_with('.') || dir_name == "target" || dir_name == "node_modules" {
continue;
}
if path.is_dir() {
collect_file_sizes(&path, files)?;
} else {
let metadata = fs::metadata(&path)?;
files.push((path, metadata.len()));
}
}
Ok(())
}
fn format_file_size(bytes: u64, path: &str) -> String {
if bytes >= 1_048_576 {
format!(" {:.1} MB {}", bytes as f64 / 1_048_576.0, path)
} else if bytes >= 1024 {
format!(" {:.1} KB {}", bytes as f64 / 1024.0, path)
} else {
format!(" {} B {}", bytes, path)
}
}
Ahora añadamos la herramienta de desglose de lenguajes. Esta es la más interesante porque combina el conteo de archivos con el mapeo de extensiones a lenguajes.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct LanguageBreakdownInput {
/// The directory path to analyse
pub path: String,
}
Añade este método de herramienta al mismo bloque #[tool(tool_box)] impl CodeStatsServer.
#[tool(description = "Get a breakdown of programming languages used in a project by file count and line count")]
pub async fn language_breakdown(
&self,
#[tool(aggr)] input: Json<LanguageBreakdownInput>,
) -> Result<String, anyhow::Error> {
let path = PathBuf::from(&input.path);
if !path.exists() {
return Ok(format!("Error: path '{}' does not exist", input.path));
}
if !path.is_dir() {
return Ok(format!("Error: path '{}' is not a directory", input.path));
}
let mut stats: HashMap<String, LanguageStats> = HashMap::new();
collect_language_stats(&path, &mut stats)?;
let mut sorted: Vec<(String, LanguageStats)> = stats.into_iter().collect();
sorted.sort_by(|a, b| b.1.lines.cmp(&a.1.lines));
let total_files: u64 = sorted.iter().map(|(_, s)| s.files).sum();
let total_lines: u64 = sorted.iter().map(|(_, s)| s.lines).sum();
let mut output = format!(
"Language breakdown for '{}':\n\n{:<20} {:>8} {:>12}\n{}\n",
input.path,
"Language",
"Files",
"Lines",
"-".repeat(44)
);
for (language, language_stats) in &sorted {
output.push_str(&format!(
"{:<20} {:>8} {:>12}\n",
language, language_stats.files, language_stats.lines
));
}
output.push_str(&format!(
"{}\n{:<20} {:>8} {:>12}\n",
"-".repeat(44),
"Total",
total_files,
total_lines
));
Ok(output)
}
Y los tipos y funciones de soporte.
#[derive(Debug, Default)]
struct LanguageStats {
files: u64,
lines: u64,
}
fn extension_to_language(ext: &str) -> Option<&str> {
match ext {
"rs" => Some("Rust"),
"py" => Some("Python"),
"js" => Some("JavaScript"),
"ts" => Some("TypeScript"),
"tsx" => Some("TSX"),
"jsx" => Some("JSX"),
"go" => Some("Go"),
"java" => Some("Java"),
"c" => Some("C"),
"cpp" | "cc" | "cxx" => Some("C++"),
"h" | "hpp" => Some("C/C++ Header"),
"rb" => Some("Ruby"),
"php" => Some("PHP"),
"swift" => Some("Swift"),
"kt" => Some("Kotlin"),
"scala" => Some("Scala"),
"zig" => Some("Zig"),
"html" | "htm" => Some("HTML"),
"css" => Some("CSS"),
"scss" | "sass" => Some("Sass"),
"json" => Some("JSON"),
"yaml" | "yml" => Some("YAML"),
"toml" => Some("TOML"),
"xml" => Some("XML"),
"sql" => Some("SQL"),
"sh" | "bash" | "zsh" => Some("Shell"),
"md" | "markdown" => Some("Markdown"),
"hbs" => Some("Handlebars"),
_ => None,
}
}
fn collect_language_stats(dir: &Path, stats: &mut HashMap<String, LanguageStats>) -> Result<()> {
let entries = fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with('.') || name == "target" || name == "node_modules" || name == "vendor"
{
continue;
}
if path.is_dir() {
collect_language_stats(&path, stats)?;
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if let Some(language) = extension_to_language(ext) {
let entry = stats
.entry(language.to_string())
.or_insert_with(LanguageStats::default);
entry.files += 1;
match fs::read_to_string(&path) {
Ok(content) => {
entry.lines += content.lines().count() as u64;
}
Err(_) => {}
}
}
}
}
Ok(())
}
Después de añadir ambas herramientas, recompila con cargo build --release. Claude Code detectará automáticamente las nuevas herramientas la próxima vez que inicialice el servidor. Ahora tienes tres herramientas que funcionan juntas. Claude puede pedir un desglose de lenguajes, luego profundizar en el lenguaje específico con el código base más grande, y luego encontrar qué archivos en ese lenguaje son los más grandes. Las herramientas se componen naturalmente porque todas operan sobre rutas de archivos.
Consideraciones de producción
Si vas a usar este servidor a diario, o compartirlo con tu equipo, hay algunas cosas que vale la pena hacer bien.
Logging y depuración. La configuración de tracing que hicimos escribe a stderr, lo que significa que puedes ver los logs sin interferir con el protocolo. Establece RUST_LOG=code_stats=debug al lanzar el servidor para obtener salida detallada. Si tu servidor no aparece en Claude Code o las herramientas fallan silenciosamente, comprueba primero la salida de stderr. También puedes ejecutar claude mcp list para verificar que el servidor esté registrado y comprobar su estado.
Manejo de errores. Observa que nuestras herramientas devuelven mensajes de error amigables como Ok(String) en lugar de propagar errores con ? a nivel superior. Esto es intencional. Si una herramienta devuelve un Err, el cliente MCP ve un error a nivel de protocolo. Si devuelve Ok con un mensaje de error en la cadena, la IA puede leer el error y reaccionar inteligentemente. Puede probar una ruta diferente, pedir clarificación al usuario o explicar qué salió mal. Reserva Err para situaciones verdaderamente irrecuperables.
Validación de entrada. Siempre valida las rutas. Nuestras herramientas comprueban que las rutas existen y son directorios antes de recorrerlos. En un servidor de producción, también querrías canonizar rutas y restringirlas a ciertos directorios para evitar que la IA escanee accidentalmente ubicaciones sensibles. Cuando estés listo para ir más allá del stdio local y ejecutar tu servidor como servicio compartido, nuestra guía sobre servidores MCP en producción cubre transporte HTTP, contenedorización, health checks y apagado graceful.
Testing. Puedes probar las herramientas MCP como funciones async Rust normales. Los métodos de herramienta en tu struct de servidor son simplemente métodos. Llámalos directamente en tests con input construido.
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_count_lines_nonexistent_path() {
let server = CodeStatsServer;
let input = Json(CountLinesInput {
path: "/nonexistent/path".to_string(),
extension: "rs".to_string(),
});
let result = server.count_lines(input).await.unwrap();
assert!(result.contains("does not exist"));
}
#[tokio::test]
async fn test_language_mapping() {
assert_eq!(extension_to_language("rs"), Some("Rust"));
assert_eq!(extension_to_language("py"), Some("Python"));
assert_eq!(extension_to_language("xyz"), None);
}
}
Ejecuta los tests con cargo test. Como nuestras herramientas son funciones puras sobre el sistema de archivos, son directas de probar. También podrías crear directorios temporales con archivos conocidos para tests de integración.
Rendimiento. Para repositorios grandes, el recorrido de archivos puede tomar un tiempo notable. El runtime async ayuda aquí porque rmcp puede continuar manejando mensajes del protocolo mientras tu función de herramienta está ejecutándose. Si necesitaras escanear directorios verdaderamente masivos, podrías añadir informes de progreso a través del sistema de notificaciones integrado de MCP, pero para la mayoría de los código bases el enfoque síncrono de fs::read_dir es suficientemente rápido.
Gobernanza y gestión. Construir un servidor MCP personalizado es la elección correcta cuando necesitas herramientas específicas del dominio que aún no existen. Pero conforme tu equipo acumula múltiples servidores MCP, la cuestión operativa pasa de "¿cómo construyo esto?" a "¿cómo gestiono todo esto?" ¿Qué servidores están ejecutándose, quién tiene acceso y se están comportando correctamente? Aquí es donde encaja la infraestructura de gobernanza como EntendIA. Proporciona un plano de control para gestionar servidores MCP, skills y acceso a herramientas de IA entre equipos, para que puedas centrarte en construir las herramientas en sí en lugar de la fontanería alrededor del despliegue, control de acceso y observabilidad. Nota: esta guía está publicada por EntendIA.
La lección
Construir este servidor revela algo importante sobre la integración de herramientas de IA. Las mejores herramientas MCP no son envoltorios alrededor de APIs que Claude Code podría llamar directamente. Son herramientas que codifican conocimiento del dominio. Nuestra herramienta count_lines sabe que debe omitir target y node_modules. Nuestra herramienta language_breakdown sabe que .tsx es TSX y .hbs es Handlebars. Este conocimiento está incrustado en la herramienta para que la IA no tenga que averiguarlo cada vez.
Este es un modelo mental diferente al de escribir una API REST. Con una API, quieres ser genérico y dejar que el cliente decida cómo usarla. Con una herramienta MCP, quieres ser opinado y hacer que lo correcto sea fácil. La IA es tu usuario, y funciona mejor cuando tus herramientas dan respuestas claras, estructuradas y ricas en contexto.
Rust es particularmente adecuado para esto porque el sistema de tipos te obliga a pensar en el contrato de tu herramienta desde el principio. El derive JsonSchema hace que ese contrato sea explícito y legible por máquinas. El compilador detecta errores antes de que lleguen a la IA. Y el binario resultante es lo suficientemente rápido como para que la IA nunca tenga que esperar la respuesta de tu herramienta.
La especificación MCP sigue evolucionando. El transporte Streamable HTTP permite servidores MCP remotos a los que múltiples clientes pueden conectarse. Las suscripciones a recursos permiten a los servidores enviar actualizaciones. El protocolo está creciendo, y tener un servidor Rust sólido como base significa que puedes adoptar nuevas funcionalidades conforme aterrizan en el crate rmcp.
Conclusión
Hemos construido un servidor MCP completo en Rust que proporciona tres herramientas prácticas para analizar código bases. Usamos el sistema de macros del crate rmcp para definir herramientas de forma declarativa, implementamos el trait server handler y conectamos todo a Claude Code sobre transporte stdio. El servidor completo compila a un único binario sin dependencias de runtime.
El código fuente completo de esta guía tiene unas 250 líneas de Rust. Eso es todo lo que se necesita para extender significativamente lo que Claude Code puede hacer. Si quieres ir más allá, aquí tienes algunas ideas. Añade una herramienta que busque comentarios TODO y los clasifique por archivo. Añade una herramienta que calcule la complejidad ciclomática de funciones Rust. Añade un recurso que exponga el Cargo.toml de tu proyecto como datos estructurados. La documentación de rmcp cubre recursos y prompts además de las herramientas que usamos aquí.
La guía oficial de construcción de servidores MCP es una buena referencia siguiente si quieres entender el protocolo a un nivel más profundo. Y si ya estás usando Claude Code con servidores MCP, la documentación MCP de Claude Code cubre configuración avanzada incluyendo variables de entorno, alcance de permisos y gestión del ciclo de vida del servidor.
Tu asistente de IA es tan capaz como las herramientas a las que tiene acceso. Ahora sabes cómo darle nuevas.