Lessons from the Trenches of Building My Own MCP Server

A practical guide to implementing a Model Context Protocol server with Vercel Functions and Supabase.

7 min read

๐Ÿ‘€

TL;DR Building an MCP server sounds straightforward until you hit serverless environment limitations. I faced the dreaded addEventListener error when deploying to Vercel Functions and solved it by creating a direct API fallback. This post shares my battle-tested approach to building reliable MCP servers that actually work in production.


From Concept to Code

In my last post, I explained WTF MCP is and why it matters. Now let's talk about actually building one. My goal was simple. Create an MCP server for Renalto's quote management system that could:

  • Update VAT rates on quotes
  • Split compound line items
  • Calculate surface estimates

๐Ÿงฐ Tech Stack Choice

  • Vercel Functions - Serverless, scalable, and easy to deploy
  • Supabase - PostgreSQL database with a clean API
  • @vercel/mcp-adapter - Official MCP implementation
  • Zod - Type validation for our tools

The plan: define tools in TypeScript, expose them via MCP, and let our AI assistant (Rita) call them to manipulate renovation quotes.

The dream: AI that could update quotes without regenerating them from scratch.


When Theory Meets Reality

Everything worked beautifully in dev. Then I hit deploy... and everything broke. ๐Ÿ’€

TypeError: Cannot read properties of undefined (reading 'addEventListener')

The MCP server was returning 500 errors. The culprit? Server-Sent Events (SSE).

The error occurs because the MCP adapter tries to use browser-specific APIs in a Node.js serverless environment. This is a known issue with @vercel/mcp-adapter@0.3.1 when deployed to Vercel Functions.


โš ๏ธ The SSE Problem

MCP servers traditionally use Server-Sent Events (SSE) for real-time communication. But this architecture assumes:

  1. Persistent connections - Something serverless functions hate
  2. Browser APIs - Like addEventListener that don't exist in Node.js
  3. Redis for state management - Another dependency to configure

The Vercel logs revealed:

TypeError: Cannot read properties of undefined (reading 'addEventListener')
at /var/task/node_modules/.pnpm/@vercel+mcp-adapter@0.3.1_next@15.3.2/node_modules/@vercel/mcp-adapter/dist/chunk-Z3U2JHVP.js:443:12

Solutions I Tried (That Failed)

First, I tried the obvious fixes:

  1. Forcing Node.js runtime instead of Edge:
export const config = { runtime: "nodejs" };
  1. Configuring Redis for SSE state management:
redisUrl: process.env.REDIS_URL,
  1. Disabling SSE (which wasn't supported in the adapter):
disableSSE: true; // Property doesn't exist in ServerOptions

None of these approaches solved our problem. The MCP adapter was fundamentally incompatible with serverless environments.


๐Ÿ’ก The Direct API Approach

After much debugging, I realized: we don't actually need the full MCP adapter for simple tool calls.

So I created a direct API endpoint that bypasses the MCP adapter entirely:

// /api/apply-surface.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { supabase } from "../lib/supabase.js";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }

  try {
    const { quoteId } = req.body;

    // Fetch quote data from Supabase
    const { data, error } = await supabase
      .from("quotes")
      .select("surface, height")
      .eq("id", quoteId)
      .single();

    // Calculate surface estimates
    const S = data?.surface ?? 75;
    const H = data?.height ?? 2.6;
    const M2 = S * H;
    const P2 = S;
    const M1 = M2 * 0.2;
    const P1 = P2 * 0.2;

    // Update the quote
    await supabase.from("quotes").update({ M1, M2, P1, P2 }).eq("id", quoteId);

    return res.status(200).json({
      success: true,
      data: { S, H, M1, M2, P1, P2 },
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({ error: String(error) });
  }
}

๐Ÿงช Testing

I created a simple test script to verify our direct API approach:

// scripts/call-apply-surface-direct.mjs
const [, , origin, quoteId] = process.argv;

async function main() {
  const url = `${origin}/api/apply-surface`;
  console.log("๐Ÿ”ง Calling direct apply-surface โ†’", url);

  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ quoteId }),
  });

  if (!res.ok) {
    console.error(`โŒ Server returned ${res.status} ${res.statusText}`);
    process.exit(1);
  }

  const payload = await res.json();
  console.log("โœ… Surface estimates applied successfully");
  console.log("๐Ÿ“ฆ Response:", payload);
}

main();

And it worked! The direct API approach successfully updated the quote in Supabase:

๐Ÿ”ง Calling direct apply-surface โ†’
โœ… Surface estimates applied successfully
๐Ÿ“ฆ Response:
{
  success: true,
  message: 'Surface estimates applied to 046035d0-28d8-4fd2-a91b-4479123d4700',
  data: { S: 75, H: 2.6, M1: 39, M2: 195, P1: 15, P2: 75 }
}

This confirms our direct API approach successfully updates quotes in Supabase with the calculated surface estimates.


๐Ÿง  Lessons Learned

Building this MCP server taught me several valuable lessons:

  1. SSE and serverless don't mix well - The addEventListener error is a symptom of this fundamental mismatch

  2. Direct API endpoints are more reliable - Sometimes simpler is better:

    • No SSE/Redis complexity
    • Standard HTTP requests
    • Better error handling
  3. Environment validation is crucial - I added connection checks:

// Add a helper function to check connection status
export async function checkSupabaseConnection() {
  try {
    const { data, error } = await supabase.from("quotes").select("id").limit(1);

    if (error) {
      console.error("Supabase connection error:", error);
      return false;
    }

    return true;
  } catch (err) {
    console.error("Failed to connect to Supabase:", err);
    return false;
  }
}
  1. Health endpoints save debugging time - I added a /api/health endpoint to quickly verify our setup

๐Ÿ† The Hybrid Approach

The final architecture uses a hybrid approach:

  1. MCP Server (/api/server.ts) - For standard tool registration and discovery
  2. Direct API Endpoints (/api/apply-surface.ts) - For reliable tool execution
  3. Health Check (/api/health.ts) - For monitoring and debugging

This gives us the best of both worlds: MCP compatibility with serverless reliability.

          +---------------------+
          |    ๐Ÿค– LLM / Agent   |
          +---------+-----------+
                    |
                    v
          +---------+-----------+
          |   /api/server.ts    |
          |   MCP Handler       |
          | (with mcp-adapter)  |
          +---------+-----------+
                    |
        +-----------+-----------+
        |   MCP Tool Registry   |
        | (updateVAT, split...) |
        +-----------+-----------+
                    |
        (fallback if SSE fails)
                    |
                    v
          +---------+-----------+
          | /api/apply-surface  |
          |  Direct API Handler |
          | (no adapter / SSE)  |
          +---------+-----------+
                    |
                    v
          +---------+-----------+
          |   ๐Ÿ“ฆ Supabase DB    |
          |   (quotes table)    |
          +---------------------+

๐Ÿ”ฎ What's Next for Our MCP Server

With our architecture stabilized, I'm now focusing on:

  1. More tools - fillMissingInfo for automatically populating quote fields
  2. Quote validation - A quoteLinter to ensure consistency
  3. Better logging - Enhanced error tracking and performance monitoring
  4. Authentication - Securing our API endpoints

The goal: a complete suite of tools that make Rita a renovation quote wizard.


โœ๏ธ Tips

If you're building your own MCP server, here are my battle-tested recommendations:

  1. Start with direct API endpoints - They're simpler and more reliable
  2. Add MCP compatibility later - Once your core functionality works
  3. Implement proper error handling - Especially for database operations
  4. Create health check endpoints - They're invaluable for debugging
  5. Use TypeScript and Zod - Type safety saves countless headaches

The Future of MCP

The MCP ecosystem is evolving rapidly:

As these tools mature, building MCP servers will become easier. But understanding the underlying architecture and limitations will always be valuable.


๐Ÿ”— Helpful Resources


Made with โ˜• and significantly fewer tabs open