Real world lessons from building MCP servers
Quinton

Quinton @quintonwall

About: I build stuff.

Location:
California, USA
Joined:
Jun 30, 2024

Real world lessons from building MCP servers

Publish Date: Jun 20
6 6

MCP servers are everywhere now. Whether you are using tools like Claude Desktop, ChatGPT, Cursor, Cline, Postman, you name it; If a developer can plug in an MCP to it, they will. Having built a number of MCP servers recently at Airbyte, and running my own side hustle at mycaminoguide.com as an AI agent for the past year, I've learned a few things about what it really takes to build and run MCP servers.

Know the main components of an MCP server.

This might sound obvious, but knowing the main components of an MCP server is incredibly helpful as you build your own. It gives you a roadmap on what and how you want to implement services, and will save you significant time trying to custom code your own solutions when you should have used native MCP decorators and support.

  • Tools: Tools are functions clients can call to perform an action. Tools are what show up in something like Cursor. When designing an MCP, I typically start by thinking about the domain I want to work in. eg: functions a developer needs when working with my product, or calendar functions etc. From there I have a scope and can decide what actaul functions are available. If I find I am exceeding the domain, I typically create a separate MCP server.

Seeing the green light is a happy day

  • Resources: Resources allow your client to return specific data based on parameters. Resources are very helpful if your MCP service is going to perform some sort of query on a backend system. eg: My MCP offers a calendar service and I want to pass in a particular date to get availability.
@mcp.resource("availability://{user_id}/date")
Enter fullscreen mode Exit fullscreen mode
  • Prompts: Prompts are messages templates that include parameterized values that you can pass to an LLM to perform a query. I use prompts extensively within the PyAirbyte MCP server to allow the user to specify source and destination connectors. The MCP server then uses a consistent prompt and the OpenAI chat completion API to query the vector store for highly relevant results.

Understand the Transports

Clients support different transports depending on your deployment model.

stdio

If you are running locally, the transport is going to be stdio. Effectively, you configuring your mcp to execute a shell command to run a local file. I use stdio MCP services that I have built to help me automate frequent daily tasks such as checking the health of pipelines, looking at usage analytics, slack summaries etc from within Claude Desktop. I wouldn't recommend stdio for broader developer community facing tools. There is too much local config that the user needs to manage.

{
  "mcpServers": {
    "airbyte-status-checker": {
      "command": "/Users/quintonwall/.local/bin/uv",
      "args": [
        "--directory",
        "/Users/quintonwall/code/airbyte-mcp-list-sources",
        "run",
        "airbyte_status_checker.py"
      ]
    },
    "movedata-demo": {
      "command": "/Users/quintonwall/.local/bin/uv",
      "args": [
        "--directory",
        "/Users/quintonwall/code/movedata-demo-project",
        "run",
        "mcp_server.py"
      ]
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

SSE

Server Sent Events, or SSE, is the original transport for remote MCP servers. MCP servers built using this model require you to run a server such as Express or FastMCP to serve endpoints, both a POST and GET. Remote servers in general are not supported by Claude Desktop, but are supported in Cursor and Cline, although there are limitations, which I'll cover shortly. If you are starting to write MCP servers today, I would not recommend using SSE transports as they have been deprecated in favor of Streamable transports.

Streamable HTTP

Streamable HTTP transports removes the need to create two endpoints - a POST and GET - like you see in SSE transports and are slightly more complex to set up. Once you do have them configured though, there is a lot of benefits through scalability and resumable connections. In addition, they can work stateless meaning you can deploy them quite easily on Vercel vs. SSE services which you need to deploy on something like Railway or Heroku. The downside is that the Streamable HTTP transport is very new with Client tool vendors only now implementing it. There are positive sign though that this transport will become the most dominant. I've already see Claude Code implement a --transport http parameter, for example.

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, StreamingResponse
import asyncio
import json

app = FastAPI()

@app.post("/mcp")
async def handle_mcp(request: Request):
    body = await request.json()

    method = body.get("method")
    if method == "tool/echo":
        params = body.get("params", {})
        stream = params.get("stream", False)

        if stream:
            async def stream_response():
                for i in range(3):
                    event = {
                        "jsonrpc": "2.0",
                        "id": body.get("id"),
                        "result": {"message": f"streaming chunk {i}"}
                    }
                    yield f"data: {json.dumps(event)}\n\n"
                    await asyncio.sleep(0.5)
            return StreamingResponse(stream_response(), media_type="text/event-stream")

        # Non-streaming response
        return JSONResponse({
            "jsonrpc": "2.0",
            "id": body.get("id"),
            "result": {"message": "echo response"}
        })

    return JSONResponse({
        "jsonrpc": "2.0",
        "id": body.get("id"),
        "error": {"code": -32601, "message": "Method not found"}
    })

# Run with: uvicorn app:app --reload
Enter fullscreen mode Exit fullscreen mode

Utilize a good library

Most of my MCP development is done in python. Thankfully, there is a rich ecosystem of libraries available to that make working with MCP much easier.

FastMCP

FastMCP is the defacto standard. It is fully spec-compliant, supports streaming transport, and is easily deployed.

from fastmcp import MCPApp
app = MCPApp()

@app.tool("math/add")
def add(a: int, b: int) -> int:
    return a + b
Enter fullscreen mode Exit fullscreen mode

OpenAI Responses API

It's been interesting to see OpenAI support a competitors 'standard' (Anthrophic were the original authors of the MCP spec). As a heavy user of the Responses API in mycaminoguide.com, I've been excited to see that models can now use MCP servers to perform tasks. Currently the implementation doesn't feel very natural and there it's overly complex but the idea of an agent or model using my MCP server has me watching this space closely. Google is also pushing the same approach with their Agent SDK.

Not all Clients are created equal

When it client tools such as Claude Desktop, Cline, and Cursor, etc, the level of support for the MCP spec, and how this is represented in the mcp.json a user needs to add to connect a server can often lead to wasted time trying to figure out why an error is being raised. I have not found a centralized place where these differences are listed. Here are the ones I have encountered

  • Local MCP server support: Claude Desktop, Cline, Cursor, Claude Code.
  • Remote MCP server support: Cline, Cursor, Claude Code
  • Remote MCP server passing env in mcp.json: Cursor, Claude Code

Custom MCP services in Claude

The remote MCP server with support for passing environment variables is a interesting case. For example, we just deployed an MCP server for PyAirbyte. This server uses openAI and a vector store to generate data pipelines. It is deployed on Heroku. As part of the client config, we require that you pass in your OpenAI API key. This works great within Cursor, but unfortunately it not supported in Cline. You can, of course, add values to a serverside .env file, but we did not want to do this due to the risk of someone spamming the MCP server and running up a bit OpenAI bill.

{
  "mcpServers": {
    "pyairbyte-mcp": {
      "url": "https://pyairbyte-mcp-7b7b8566f2ce.herokuapp.com/mcp",
      "env": {
        "OPENAI_API_KEY": "your-api-key"
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Summary

MCP protocols are still evolving. Change is constant and can be frustrating when building services. Sometimes logging errors are not very helpful, and LLMs like ChatGPT often send you down a rabbit hole, only to find out that the spec has changed and the LLM doesn't have the most recent information. Vibe coding MCP servers can be an exercise in frustration. I hope these tips help you get started in building your own MCP servers and avoid some of the pitfalls I made when starting out.

Comments 6 total

  • Nevo David
    Nevo DavidJun 20, 2025

    honestly love how you keep it real about the pain and constant changes - makes me think, you reckon things will ever really settle in this space or are we just in permanent beta forever?

    • Quinton
      QuintonJun 23, 2025

      It's an evolving space for sure. The great thing is there is so much innovation happening that I would gladly take some uncertainty as a trade off.

  • Dotallio
    DotallioJun 20, 2025

    This is super helpful, especially the breakdown of real gotchas with transports and env configs. Have you found any good way to track updates to client MCP support, or is it still just scattered docs and trial and error?

    • Quinton
      QuintonJun 23, 2025

      Nothing great yet, although we do try and add Segment telemetry to understand usage.

  • Aaron Steers
    Aaron SteersJun 25, 2025

    Thanks for the helpful write-up, @quintonwall! This is great. 👍

  • DeRucci69
    DeRucci69Jun 25, 2025

    What are your thoughts on using run-time API discovery instead? Check out this whitepaper:

    blog.invoke.network/you-dont-know-...

Add comment