How Not to Write an MCP Server ![rw-book-cover](https://towardsdatascience.com/wp-content/uploads/2025/05/doppleware_ai_robot_facepalming_-ar_169_-v_6.1_ffc36bad-c0b8-41d7-be9e-66484ca8c4f4_1-1.png) ## Metadata - Author: [[Roni Dover]] - Full Title: How Not to Write an MCP Server - Category: #articles - Document Note: --- tags: - #mcp - #developer-diary - #observability - #ai-agents - #lessons-learned - #roni-dover Author: Roni Dover Source: Towards Data Science Reference: https://towardsdatascience.com/how-not-to-write-an-mcp-server/ date created: 2025-05-13 18:45 --- # Title: 🧠 How Not to Write an MCP Server: Lessons in Real-World Implementation ## Table of Contents: - [📝 Summary](#-summary) - [📖 Main Content](#-main-content) - [🗒️ Notes](#-notes) - [📖 References](#-references) - [🗣️ Paraphrase](#-paraphrase) - [📰 Newsletter](#-newsletter) - [✅ Action Items](#-action-items) - [🧪 Hypotheses](#-hypotheses) - [📊 Methodology](#-methodology) - [❓ Questions](#-questions) - [🔮 Future Research](#-future-research) - [💡 Implications](#-implications) - [🧠 Learning Styles Assessment](#-learning-styles-assessment) - [📊 Accessibility Assessment](#-accessibility-assessment) - [🌍 DEIB Assessment](#-deib-assessment) - [✅ Logical Validity](#-logical-validity) - [🔍 DARVO Analysis](#-darvo-analysis) - [🔍 SEO Evaluation](#-seo-evaluation) - [🎯 Conclusion](#-conclusion) --- ## 📝 Summary: Roni Dover reflects on the challenges of building an MCP (Model Context Protocol) server for a dynamic code analysis tool in an observability platform. The piece is structured around five major lessons learned—from overwhelming the agent with too much data, to time format issues, agent training strategies, clarity of API descriptions, and documenting JSON for AI understanding. Dover advocates for developer empathy with AI agent limitations, and iteratively tuning tooling for reliability, relevance, and clarity. --- ## 📖 Main Content: Dover’s retrospective on building an MCP server candidly outlines hard-won insights developers need when working at the edge of AI integrations. His five core lessons highlight both technical design principles and the nuance of human-AI collaboration. ### 🗒️ Notes #### 🔹 Stack Overview: - **Client Tools**: Cursor, VS Code - **Server Stack**: .NET with the official MCP SDK #### 🧱 Lesson 1: Don’t Dump All Data - Initial assumption: more data = more power for the agent. - Issue: verbose API response (360k+ characters) overwhelmed the model. - Result: model ignored data or failed to process key insights. - Fix: - Added a **nested hierarchy** to responses. - Created dimension-based **queryable APIs** to allow agents to navigate context in chunks. - Switched to a higher context window model (Gemini 2.5 Pro) to confirm limits. #### 🕰 Lesson 2: Time Isn’t Obvious - Agents may not know current datetime. - They default to ISO 8601 durations (e.g., P7D). - Solution: document and handle duration formats. #### 💡 Lesson 3: Correct and Teach - Agents misuse environment identifiers from prompts. - Old system failed silently → no feedback loop. - New system returns valid suggestions and errors. - Agents learn from responses and adapt behavior. #### 📣 Lesson 4: Lead with Intent - Original tool descriptions were too functional. - Agent ignored tools unless prompted specifically. - Rewrite emphasized **use cases**, improving tool activation in relevant prompts. #### 🧾 Lesson 5: Document JSON - JSON lacks native comments. - Added `_comment` field with rich metadata on score calculations and context. - Improved agent reasoning and explanation capabilities. #### 🛠 Architecture: SSE vs STDIO - STDIO-based MCP = local CLI, harder to update. - SSE = hosted microservice endpoint, easier versioning, better alignment with app lifecycle. - Supergateway proxy used to bridge client support gaps. --- ## 📖 References: - Dover, Roni. [How Not to Write an MCP Server](https://towardsdatascience.com/how-not-to-write-an-mcp-server/) --- ## 🗣️ Paraphrase: 1. *Original*: “The basic premise was therefore to allow the agent to work its ‘magic’, with more data potentially meaning more hooks for the agent to latch on.” *Paraphrased*: I believed giving the AI more data would increase its ability to find useful patterns—but too much actually overwhelmed it. 2. *Original*: “While it is tempting to simply describe what the API is doing, sometimes the generic terms don’t quite allow the agent to realize the type of requirements for which this functionality might apply best.” *Paraphrased*: Just describing API functionality isn’t enough—clarity around its *intended use cases* makes it much more effective for AI to apply. --- ## 📰 Newsletter: **🧪 Developer Lessons from Building an MCP Server** When AI meets observability, it’s not always magic—sometimes it's 360,000 characters of JSON that leave your agent totally clueless. Roni Dover’s latest dev diary offers a goldmine of practical takeaways from building an MCP server to power dynamic code analysis. From smart data structuring to agent education and JSON annotation hacks, this is a must-read for anyone integrating AI into developer tools. Bonus: lessons on whether to go SSE or STDIO for serving your MCP. 👉 [Read the article](https://towardsdatascience.com/how-not-to-write-an-mcp-server/) --- ## ✅ Action Items: - **Contributor Docs**: Write community-facing tutorials on best practices for MCP server JSON structure and API responses. - **Tooling**: Improve WordPress and open-source observability tools with SSE compatibility and agent-friendly APIs. - **Education**: Build course modules around real-world AI tooling pain points, including agent query behavior and error feedback loops. - **OSPO Alignment**: Highlight MCP design tradeoffs in open-source code intelligence tools, advocating for transparency in AI system integration. --- ## 🧪 Hypotheses: - Smaller, context-aware data responses yield better AI behavior than full data dumps. - Explicit error messaging improves agent learning over time. - SSE architecture reduces versioning friction compared to STDIO in MCP scenarios. ### 📊 Methodology: - Compare agent success rates pre/post refactor using test suites with structured prompts. - Run user research on latency and success rates between SSE and STDIO deployments. - Analyze frequency of tool invocation pre/post doc rewriting. ### ❓ Questions: - Can AI agents reliably learn from structured tool error responses? - Should MCP specs evolve to support native metadata alongside JSON? - How do different LLMs respond to identical data structures in high-volume scenarios? ### 🔮 Future Research: - Effects of JSON `_comment` fields on multi-agent collaboration. - Longitudinal studies on prompt adaptation post-agent error correction. - Standardizing time formats and temporal logic in agent APIs. ### 💡 Implications: - Raises UX stakes for AI toolchains—developers must design for agents, not just humans. - Supports argument for hybrid server-client architectures in modern AI infrastructure. - Could inform future open protocols for AI observability integrations. --- ## 🧠 Learning Styles Assessment: **Readability Score**: ~Grade 12; suitable for technical readers. - **Visual Learners**: ✅ Illustrative images from Midjourney *Improve*: Use flowcharts for data handling architecture. - **Aural Learners**: ❌ No auditory supplement *Improve*: Embed a narrated walkthrough or podcast. - **Verbal Learners**: ✅ Story-based explanations *Improve*: Glossary of MCP terms for newer devs. - **Physical Learners**: ❌ *Improve*: Add “try this” coding scenarios with GitHub. - **Logical Learners**: ✅ Step-by-step diagnostics and structure *Improve*: Charts comparing SSE vs STDIO. - **Social Learners**: ❌ *Improve*: Invite dev feedback via forums or Twitter. - **Solitary Learners**: ✅ Introspective, diary-style voice. - **Naturalistic Learners**: ❌ *Improve*: Use natural metaphors for data flow patterns. --- ## 📊 Accessibility Assessment: - **Screen Reader Compatibility**: Not confirmed *Improve*: Ensure semantic HTML and alt text. - **Keyboard Navigation**: Likely okay in Medium *Improve*: Audit for tab indexing and control access. - **Contrast and Readability**: ✅ Clear text, no visual noise. - **Text Resizing**: ✅ Responsive Medium layout *Improve*: Test with custom zoom levels and dark mode. --- ## 🌍 DEIB Assessment: - **Cultural Relevance**: 🌐 Globally neutral, t - Document Tags: [[mcp]] - Summary: Five hard lessons learned from my first attempt at leveraging the new MCP technology, a short developer’s diary - URL: https://towardsdatascience.com/how-not-to-write-an-mcp-server/ Five hard lessons learned from my first attempt at leveraging the new MCP technology, a short developer’s diary ![](https://towardsdatascience.com/wp-content/uploads/2025/05/doppleware_ai_robot_facepalming_-ar_169_-v_6.1_ffc36bad-c0b8-41d7-be9e-66484ca8c4f4_1-1.png)Image generated by author with Midjourney I recently had the chance to create an MCP server for an observability application in order to provide the AI agent with dynamic code analysis capabilities. Because of its potential to transform applications, MCP is a technology I’m even more ecstatic about than I originally was about genAI in general. I wrote more about that and some intro to MCPs in general in a previous [post](https://towardsdatascience.com/a-farewell-to-apms-the-future-of-observability-is-mcp-tools/). While an initial POCs demonstrated that there was an **immense** potential for this to be a force multiplier to our product’s value, it took several iterations and several stumbles to deliver on that promise. In this post, I’ll try to capture some of the lessons learned, as I think that this can benefit other MCP server developers. ##### My Stack * I was using [Cursor](https://www.cursor.com/) and [vscode](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) intermittently as the main MCP client * To develop the MCP server itself, I used the [.NET MCP SDK](https://github.com/modelcontextprotocol/csharp-sdk), as I decided to host the server on another service written in .NET ##### Lesson 1: Don’t dump all of your data on the agent In my application, one tool returns aggregated information on errors and exceptions. The API is very detailed as it serves a complex UI view, and spews out large amounts of deeply linked data: * Error frames * Affected endpoints * Stack traces * Priority and trends * Histograms My first hunch was to simply expose the API **as is** as an MCP tool. After all, the agent should be able to make more sense of it than any UI view, and catch on to interesting details or connections between events. There were several scenarios I had in mind as to how I would expect this data to be useful. The agent could automatically offer fixes for recent exceptions recorded in production or in the testing environment, let me know about errors that stand out, or help me address some systematic problems that are the underlying root cause of the issues. The basic premise was therefore to allow the agent to work its ‘magic’, with more data potentially meaning more hooks for the agent to latch on in its investigation efforts. I quickly coded a wrapper around our API on the MCP endpoint and decided to start with a basic prompt to see whether everything is working: ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/session_1_1.png)Image by author We can see the agent was smart enough to know that it needed to call another tool to grab the environment ID for that ‘**test**’ environment I mentioned. With that at hand, after discovering that there was actually no recent exception in the last 24 hours, it then took the liberty to scan a more extended time period, and this is when things got a little weird: ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/session_1_2.png)Image by author What a strange response. The agent queries for exceptions from the last seven days, gets back some tangible results this time, and yet proceeds to ramble on as if ignoring the data altogether. It continues to try and use the tool in different ways and different parameter combinations, obviously fumbling, until I notice it flat out calls out the fact that the data is completely invisible to it. While errors are being sent back in the response, the agent actually claims there are **no errors.** What is going on? ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/session_1_3.png)Image by author After some investigation, the problem was revealed to be the fact that we’ve simply reached a cap in the agent’s capacity to process large amounts of data in the response. I used an existing API that was extremely verbose, which I initially even considered to be an advantage. The end result, however, was that I somehow managed to overwhelm the model. Overall, there were around 360k characters and 16k words in the response JSON. This includes call stacks, error frames, and references. This **should** have been supported just by looking at the context window limit for the model I was using (Claude 3.7 Sonnet should support up to 200k tokens), but nevertheless the large data dump left the agent thoroughly stumped. One strategy would be to change the model to one that supports an even bigger context window. I switched over to the **Gemini 2.5 pro** model just to test that theory out, as it boasts an outrageous limit of one million tokens. Sure enough, the same query now yielded a much more intelligent response: ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/session_1_4.png)Image by author This is great! The agent was able to parse the errors and find the systematic cause of many of them with some basic reasoning. However, we can’t rely on the user using a specific model, and to complicate things, this was output from a relatively low bandwidth testing environment. What if the dataset were even larger? To solve this issue, I made some fundamental changes to how the API was structured: * **Nested data hierarchy:** Keep the initial response focused on high-level details and aggregations. Create a separate API to retrieve the call stacks of specific frames as needed. * **Enhance queryability:** All of the queries made so far by the agent used a very small page size for the data (10), if we want the agent to be able to to access more relevant subsets of the data to fit with the limitations of its context, we need to provide more APIs to query errors based on different dimensions, for example: affected methods, error type, priority and impact etc. With the new changes, the tool now consistently analyzes important new exceptions and comes up with fix suggestions. However, I glanced over another minor detail I needed to sort before I could really use it reliably. ##### Lesson 2: What’s the time? ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/doppleware_ai_robot_checking_the_time_on_its_wrist_watch_-ar_f373bba7-8994-4517-9ce9-ddfbfc881ee7_2-1024x574.png)Image generated by the author with Midjourney The keen-eyed reader may have noticed that in the previous example, to retrieve the errors in a specific time range, the agent uses the **ISO 8601 time duration** format instead of the actual dates and times. So instead of including standard ‘**From**’ and ‘**To**’ parameters with datetime values, the AI sent a duration value, for example, seven days or **P7D,** to indicate it wants to check for errors in the past week. The reason for this is somewhat strange — **the agent might not know the current date and time!** You can verify that yourself by asking the agent that simple question. The below would have made sense were it not for the fact that I typed that prompt in at around noon on May 4th… ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/session_2_1.png)Image by author Using time **duration** values turned out to be a great solution that the agent handled quite well. Don’t forget to document the expected value and example syntax in the tool parameter description, though! ##### Lesson 3: When the agent makes a mistake, show it how to do better In the first example, I was actually taken aback by how the agent was able to decipher the dependencies between the different tool calls In order to provide the right environment identifier. In studying the MCP contract, it figured out that it had to call on a dependent another tool to get the list of environment IDs first. However, responding to other requests, the agent would sometimes take the environment names mentioned in the prompt verbatim. For example, I noticed that in response to this question: **compare slow traces for this method between the test and prod environments, are there any significant differences?** Depending on the context, the agent would sometimes use the environment names mentioned in the request and would send the strings “test” and “prod” as the environment ID. In my original implementation, my MCP server would silently fail in this scenario, returning an empty response. The agent, upon receiving no data or a generic error, would simply quit and try to solve the request using another strategy. To offset that behavior, I quickly changed my implementation so that if an incorrect value was provided, the JSON response would describe exactly what went wrong, and even provide a valid list of possible values to save the agent another tool call. ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/session_3_1.png)Image by author This was enough for the agent, learning from its mistake, it repeated the call with the correct value and somehow also avoided making that same error in the future. ##### Lesson 4: Focus on user intent and not functionality While it is tempting to simply describe what the API is doing, sometimes the generic terms don’t quite allow the agent to realize the type of requirements for which this functionality might apply best. Let’s take a simple example: My MCP server has a tool that, for each method, endpoint, or code location, can indicate how it’s being used at runtime. Specifically, it uses the tracing data to indicate which application flows reach the specific function or method. The original documentation simply described this functionality: ``` [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) ``` The above represents a functionally accurate description of what this tool does, but it doesn’t necessarily make it clear what types of activities it might be relevant for. After seeing that the agent wasn’t picking this tool up for various prompts I thought it would be fairly useful for, I decided to rewrite the tool description, this time emphasizing the use cases: ``` [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: ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/session_4_1.png)Image by author ##### 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":[] ``` This enables the agent to explain to the user what the score means if they ask, but also feed this explanation into its own reasoning and recommendations. ##### Choosing the right architecture: SSE vs STDIO, There are two architectures you can use in developing an MCP server. The more common and widely supported implementation is making your server available as a **command** triggered by the MCP client. This could be any CLI-triggered command; **npx, docker**, and **python** are some common examples. In this configuration, all communication is done via the process **STDIO**, and the process itself is running on the client machine. The client is responsible for instantiating and maintaining the lifecycle of the MCP server. ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/arch1-1024x353.png)Image by author This client-side architecture has one major drawback from my perspective: Since the MCP server implementation is run by the client on the local machine, it is much harder to roll out updates or new capabilities. Even if that problem is somehow solved, the tight coupling between the MCP server and the backend APIs it depends on in our applications would further complicate this model in terms of versioning and forward/backward compatibility. For these reasons, I chose the second type of MCP Server — an [SSE](https://en.wikipedia.org/wiki/Server-sent_events) Server hosted as a part of our application services. This removes any friction from running CLI commands on the client machine, as well as allows me to update and version the MCP server code along with the application code that it consumes. In this scenario, the client is provided with a URL of the SSE endpoint with which it interacts. While not all clients currently support this option, there is a brilliant commandMCP called [supergateway](https://github.com/supercorp-ai/supergateway) that can be used as a proxy to the SSE server implementation. That means users can still add the more widely supported STDIO variant and still consume the functionality hosted on your SSE backend. ![](https://contributor.insightmediagroup.io/wp-content/uploads/2025/05/arch_2-1024x353.png)Image by author ##### MCPs are still new There are many more lessons and nuances to using this deceptively simple technology. I have found that there is a big gap between implementing a workable MCP to one that can actually integrate with user needs and usage scenarios, even beyond those you have anticipated. Hopefully, as the technology matures, we’ll see more posts on [Best Practices](https://towardsdatascience.com/tag/best-practices/) and **Want to Connect?** You can reach me on Twitter at @doppleware or via [LinkedIn](https://www.linkedin.com/in/ronidover/). Follow my **[mcp](https://towardsdatascience.com/tag/mcp/)** for dynamic code analysis using observability at <https://github.com/digma-ai/digma-mcp-server>, How Not to Write an MCP Server, Roni Dover, 01jv2rqt1b9ggcps85mbpenxm0.