Tengo la oportunidad de crear un servidor MCP para una aplicación de observabilidad para proporcionar al agente AI capacidades de análisis de código dinámico. Debido a su potencial para transformar aplicaciones, MCP es una tecnología por la que estoy aún más extasiado de lo que originalmente era sobre Genai en general. Escribí más sobre eso y alguna introducción a MCP en general en un anterior correo.
Mientras que un POC inicial demostró que había un inmenso Potencial para que esto sea un multiplicador de fuerza al valor de nuestro producto, tomó varias iteraciones y varios tropiezos para cumplir esa promesa. En esta publicación, intentaré capturar algunas de las lecciones aprendidas, ya que creo que esto puede beneficiar a otros desarrolladores de servidores MCP.
Mi pila
- Estaba usando Cursor y VCODE intermitentemente como el cliente principal de MCP
- Para desarrollar el servidor MCP en sí, utilicé el .NET MCP SDKya que decidí alojar el servidor en otro servicio escrito en .net
Lección 1: No descarte todos sus datos en el agente
En mi aplicación, una herramienta devuelve información agregada sobre errores y excepciones. La API es muy detallada, ya que sirve una vista de interfaz de usuario compleja y arroja grandes cantidades de datos profundamente vinculados:
- Marcos de error
- Puntos finales afectados
- Trazas de pila
- Prioridad y tendencias
- Histogramas
Mi primer presentimiento fue simplemente exponer la API como es como herramienta MCP. Después de todo, el agente debería poder darle más sentido que cualquier vista de UI, y captar detalles o conexiones interesantes entre eventos. Había varios escenarios que tenía en mente sobre cómo esperaría que estos datos fueran útiles. El agente podría ofrecer automáticamente soluciones para excepciones recientes registradas en la producción o en el entorno de prueba, avíseme sobre errores que se destacan o me ayuden a abordar algunos problemas sistemáticos que son la causa raíz subyacente de los problemas.
Por lo tanto, la premisa básica era permitir que el agente trabaje su ‘magia’, con más datos que potencialmente significan más ganchos para que el agente se adhiera en sus esfuerzos de investigación. Rápidamente codifiqué un envoltorio alrededor de nuestra API en el punto final de MCP y decidí comenzar con un mensaje básico para ver si todo está funcionando:
Podemos ver que el agente era lo suficientemente inteligente como para saber que necesitaba llamar a otra herramienta para obtener la identificación del entorno para eso ‘prueba‘Ambiente que mencioné. Con eso a mano, después de descubrir que en realidad no hubo una excepción reciente en las últimas 24 horas, luego tomó la libertad de escanear un período de tiempo más extendido, y es cuando las cosas se pusieron un poco extrañas:
Qué respuesta tan extraña. El agente consulta las excepciones de los últimos siete días, recupera algunos resultados tangibles esta vez y, sin embargo, procede a divagar como si ignorara los datos por completo. Continúa tratando de usar la herramienta de diferentes maneras y diferentes combinaciones de parámetros, obviamente balbuceando, hasta que nota que no llama el hecho de que los datos son completamente invisibles para él. Mientras se envían errores en la respuesta, el agente en realidad afirma que hay Sin errores. ¿Qué está pasando?
Después de alguna investigación, se reveló que el problema era el hecho de que simplemente hemos alcanzado un límite a la capacidad del agente para procesar grandes cantidades de datos en la respuesta.
Utilicé una API existente que era extremadamente detallada, lo que inicialmente incluso consideré una ventaja. El resultado final, sin embargo, fue que de alguna manera logré abrumar el modelo. En general, había alrededor de 360k caracteres y 16k palabras en la respuesta JSON. Esto incluye pilas de llamadas, marcos de error y referencias. Este debería han sido compatibles con solo mirar el límite de la ventana de contexto para el modelo que estaba usando (el soneto Claude 3.7 debería admitir hasta 200k tokens), pero sin embargo, el gran volcado de datos dejó al agente completamente perplejo.
Una estrategia sería cambiar el modelo a uno que admite una ventana de contexto aún mayor. Me cambié al Géminis 2.5 Pro Modele solo para probar esa teoría, ya que cuenta con un límite escandaloso de un millón de tokens. Efectivamente, la misma consulta ahora produjo una respuesta mucho más inteligente:
¡Esto es genial! El agente pudo analizar los errores y encontrar la causa sistemática de muchos de ellos con algún razonamiento básico. Sin embargo, no podemos confiar en que el usuario use un modelo específico, y para complicar las cosas, esto se realizó de un entorno de prueba de ancho de banda relativamente bajo. ¿Qué pasaría si el conjunto de datos fuera aún más grande?
Para resolver este problema, hice algunos cambios fundamentales en cómo se estructuró la API:
- Jerarquía de datos anidada: Mantenga la respuesta inicial centrada en detalles y agregaciones de alto nivel. Cree una API separada para recuperar las pilas de llamadas de marcos específicos según sea necesario.
- Mejorar la consulta: Todas las consultas realizadas hasta ahora por el agente utilizaron un tamaño de página muy pequeño para los datos (10), si queremos que el agente pueda acceder a subconjuntos más relevantes de los datos que se ajustan a las limitaciones de su contexto, necesitamos proporcionar más API para consultar errores basados en diferentes dimensiones, por ejemplo: métodos afectados, tipo de error, prioridad e impacto, etc.
Con los nuevos cambios, la herramienta ahora analiza constantemente nuevas excepciones importantes y presenta sugerencias fijas. Sin embargo, miré sobre otro detalle menor que necesitaba clasificar antes de poder usarlo de manera confiable.
Lección 2: ¿Cuál es el momento?
El lector de ojos agudos puede haber notado que en el ejemplo anterior, para recuperar los errores en un rango de tiempo específico, el agente usa el ISO 8601 Duración de tiempo formatear en lugar de las fechas y tiempos reales. Entonces, en lugar de incluir estándar ‘De‘ y ‘A‘Parámetros con valores de fecha y hora, la IA envió un valor de duración, por ejemplo, siete días o P7D, Para indicar que quiere verificar los errores la semana pasada.
La razón de esto es algo extraño – ¡Es posible que el agente no conozca la fecha y hora actuales! Puede verificarlo usted mismo preguntando al agente esa simple pregunta. Lo siguiente habría tenido sentido si no fuera por el hecho de que escribí ese aviso alrededor del mediodía del 4 de mayo …
Uso de tiempo duración Los valores resultaron ser una gran solución que el agente manejó bastante bien. Sin embargo, no olvide documentar el valor esperado y la sintaxis de ejemplo en la descripción del parámetro de la herramienta.
Lección 3: Cuando el agente comete un error, muéstrale cómo hacerlo mejor
En el primer ejemplo, en realidad me sorprendió la forma en que el agente pudo descifrar las dependencias entre las diferentes llamadas de herramientas para proporcionar el identificador de entorno adecuado. Al estudiar el contrato de MCP, descubrió que tenía que llamar primero a una herramienta dependiente de otra herramienta para obtener la lista de ID de entorno.
Sin embargo, respondiendo a otras solicitudes, el agente a veces tomaba los nombres del entorno mencionados en el avance rápido. Por ejemplo, noté que en respuesta a esta pregunta: Compare trazas lentas para este método entre los entornos de prueba y PROD, ¿hay diferencias significativas? Dependiendo del contexto, El agente a veces usaba los nombres de entorno mencionados en la solicitud y enviaría las cadenas “prueba” y “produjo” como la identificación del entorno.
En mi implementación original, mi servidor MCP fallaría silenciosamente en este escenario, devolviendo una respuesta vacía. El agente, al no recibir datos o un error genérico, simplemente renunciaría e intentaría resolver la solicitud utilizando otra estrategia. Para compensar ese comportamiento, cambié rápidamente mi implementación para que si se proporcionara un valor incorrecto, la respuesta JSON describiría exactamente lo que salió mal e incluso proporcionaría una lista válida de valores posibles para guardar el agente otra llamada de la herramienta.
Esto fue suficiente para el agente, aprender de su error, repitió la llamada con el valor correcto y de alguna manera también evitó cometer el mismo error en el futuro.
Lección 4: Centrarse en la intención del usuario y no la funcionalidad
Si bien es tentador simplemente describir lo que está haciendo la API, a veces los términos genéricos no permiten que el agente realice el tipo de requisitos para los cuales esta funcionalidad podría aplicarse mejor.
Tomemos un ejemplo simple: mi servidor MCP tiene una herramienta que, para cada método, punto final o ubicación de código, puede indicar cómo se está utilizando en tiempo de ejecución. Específicamente, utiliza los datos de rastreo para indicar qué flujos de aplicación alcanzan la función o método específico.
La documentación original simplemente describió esta funcionalidad:
[McpServerTool,
Description(
@"For this method, see which runtime flows in the application
(including other microservices and code not in this project)
use this function or method.
This data is based on analyzing distributed tracing.")]
public static async Task<string> GetUsagesForMethod(IMcpService client,
[Description("The environment id to check for usages")]
string environmentId,
[Description("The name of the class. Provide only the class name without the namespace prefix.")]
string codeClass,
[Description("The name of the method to check, must specify a specific method to check")]
string codeMethod)
Lo anterior representa una descripción funcionalmente precisa de lo que hace esta herramienta, pero no necesariamente deja en claro para qué tipos de actividades podría ser relevante. Después de ver que el agente no estaba recogiendo esta herramienta para varias indicaciones, pensé que sería bastante útil, decidí reescribir la descripción de la herramienta, esta vez enfatizando los casos de uso:
[McpServerTool,
Description(
@"Find out what is the how a specific code location is being used and by
which other services/code.
Useful in order to detect possible breaking changes, to check whether
the generated code will fit the current usages,
to generate tests based on the runtime usage of this method,
or to check for related issues on the endpoints triggering this code
after any change to ensure it didnt impact it"
Updating the text helped the agent realize why the information was useful. For example, before making this change, the agent would not even trigger the tool in response to a prompt similar to the one below. Now, it has become completely seamless, without the user having to directly mention that this tool should be used:
Lesson 5: Document your JSON responses
The JSON standard, at least officially, does not support comments. That means that if the JSON is all the agent has to go on, it might be missing some clues about the context of the data you’re returning. For example, in my aggregated error response, I returned the following score object:
"Score": {"Score":21,
"ScoreParams":{ "Occurrences":1,
"Trend":0,
"Recent":20,
"Unhandled":0,
"Unexpected":0}}
Without proper documentation, any non-clairvoyant agent would be hard pressed to make sense of what these numbers mean. Thankfully, it is easy to add a comment element at the beginning of the JSON file with additional information about the data provided:
"_comment": "Each error contains a link to the error trace,
which can be retrieved using the GetTrace tool,
information about the affected endpoints the code and the
relevant stacktrace.
Each error in the list represents numerous instances
of the same error and is given a score after its been
prioritized.
The score reflects the criticality of the error.
The number is between 0 and 100 and is comprised of several
parameters, each can contribute to the error criticality,
all are normalized in relation to the system
and the other methods.
The score parameters value represents its contributation to the
overall score, they include:
1. 'Occurrences', representing the number of instances of this error
compared to others.
2. 'Trend' whether this error is escalating in its
frequency.
3. 'Unhandled' represents whether this error is caught
internally or poropagates all the way
out of the endpoint scope
4. 'Unexpected' are errors that are in high probability
bugs, for example NullPointerExcetion or
KeyNotFound",
"EnvironmentErrors":[]
Esto permite al agente explicarle al usuario qué significa el puntaje si lo solicita, pero también alimenta esta explicación en sus propios razonamientos y recomendaciones.
Elegir la arquitectura correcta: SSE vs stdio,
Hay dos arquitecturas que puede usar para desarrollar un servidor MCP. La implementación más común y ampliamente compatible está haciendo que su servidor esté disponible como un dominio activado por el cliente MCP. Esto podría ser cualquier comando activado por CLI; NPX, Dockery pitón son algunos ejemplos comunes. En esta configuración, toda la comunicación se realiza a través del procesoStdioy el proceso en sí se ejecuta en la máquina del cliente. El cliente es responsable de instanciar y mantener el ciclo de vida del servidor MCP.
Esta arquitectura del lado del cliente tiene un inconveniente importante desde mi perspectiva: dado que el cliente ejecuta la implementación del servidor MCP en la máquina local, es mucho más difícil implementar actualizaciones o nuevas capacidades. Incluso si ese problema se resuelve de alguna manera, el acoplamiento estricto entre el servidor MCP y las API de backend de las que depende de nuestras aplicaciones complicaría aún más este modelo en términos de versiones y compatibilidad hacia adelante/hacia atrás.
Por estas razones, elegí el segundo tipo de servidor MCP, un SSE Servidor alojado como parte de nuestros servicios de aplicación. Esto elimina cualquier fricción de ejecutar comandos de CLI en la máquina del cliente, así como me permite actualizar y ver la versión del código del servidor MCP junto con el código de aplicación que consume. En este escenario, el cliente cuenta con una URL del punto final de SSE con el que interactúa. Si bien no todos los clientes actualmente admiten esta opción, se llama un brillante comandoMCP rótula que se puede usar como un proxy para la implementación del servidor SSE. Eso significa que los usuarios aún pueden agregar la variante STDIO más ampliamente compatible y aún consumen la funcionalidad alojada en su backend SSE.
Los MCP siguen siendo nuevos
Hay muchas más lecciones y matices para usar esta tecnología engañosamente simple. He descubierto que existe una gran brecha entre implementar un MCP viable a uno que realmente pueda integrarse con las necesidades de los usuarios y los escenarios de uso, incluso más allá de los que ha anticipado. Con suerte, a medida que la tecnología madura, veremos más publicaciones en Mejores prácticas y
¿Quieres conectarte? Puedes comunicarme conmigo en Twitter en @Doppleware o vía LinkedIn.
Seguir mi MCP Para el análisis de código dinámico utilizando la observabilidad en https://github.com/digma-ai/digma-mcp-server