,

Building an MCP Server in JavaScript

Building an MCP server in Javascript

In my last post, Connecting Open WebUI to MCP Servers, we connected our local AI to existing MCP servers and saw the power of the protocol. But the real competitive advantage lies in building your own MCP servers — custom tools that wire your AI directly to your systems, your data, your workflows.

This is the second in a four-part series. We’re going to build the same MCP server in four different languages: Go, JavaScript, C#, and Python. Each post covers its language in isolation — you only need to read the one matching your stack.

The tool we’re building? A Google Reviews scraper. Your AI agent asks “What are our latest Google reviews?” or “Show me reviews with ratings below 3 stars” — and the MCP server fetches and returns structured data. One server. Every AI client. Forever reusable.

In the Go version, we compiled to a single binary. In JavaScript, we’re running on Node.js (or Bun), leveraging the NPM ecosystem. This is the path of least resistance for web developers who already live in JavaScript.

What Is an MCP Server?

MCP (Model Context Protocol) is an open specification that defines how AI clients communicate with external tools and data sources. Think of it as a universal USB-C port for AI. One standard. Any tool. Any client.

The protocol exposes three primitives:

  • Tools — actions the AI can invoke (scrape reviews, query a database, create a file)
  • Resources — readable data sources (a live dashboard, an API response, a document)
  • Prompts — reusable message templates for common workflows

The official MCP SDK is available in six languages, all Tier 1 for feature completeness and protocol support: TypeScript, Python, C#, and Go (maintained in collaboration with Google), plus Java and Rust at Tier 2. The SDK handles the JSON-RPC plumbing. You focus on the business logic.

For the full SDK reference, see the official MCP SDK documentation.

Setting Up the JavaScript SDK

The official JavaScript SDK is published as two NPM packages: @modelcontextprotocol/server for building servers, and @modelcontextprotocol/client for building clients. It runs on Node.js, Bun, and Deno.

Create a new project and install the server SDK along with Zod (a required peer dependency for schema validation):

mkdir mcp-google-reviews-js && cd mcp-google-reviews-js
npm init -y
npm install @modelcontextprotocol/server @modelcontextprotocol/node zod @cfworker/json-schema

What this command is doing:

  • npm init -y – Creates a package.json with default settings. This tells NPM how to manage your project’s dependencies.
  • npm install @modelcontextprotocol/server @modelcontextprotocol/node – Installs the official MCP server SDK. This provides the McpServer class and transport layers.
  • npm install zod – Installs Zod for input schema validation. The MCP TypeScript SDK requires Zod for tool input schemas. It uses Zod v4 internally but maintains backwards compatibility with Zod v3.25+.
  • npm install @cfworker/json-schema – Installs a dependency of the McpServer SDK

The Complete Google Reviews MCP Server in JavaScript

Here is the full server. It exposes a single tool — fetch_reviews — that takes a Google Maps place ID and returns the latest reviews as structured JSON.

Save this as server.js:

import { McpServer } from '@modelcontextprotocol/server';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { randomUUID } from 'node:crypto';
import * as z from 'zod/v4';

// Transport session storage
const transports = {};

// Create the MCP server with identification metadata
const server = new McpServer({
    name: 'google-reviews-scraper',
    version: '1.0.0',
});

// Register the fetch_reviews tool
server.registerTool(
    'fetch_reviews',
    {
        description: 'Fetch Google reviews for a business using its Place ID. Returns structured review data including ratings, text, and author names.',
        inputSchema: z.object({
            place_id: z.string().describe('The Google Maps place ID of the business'),
            limit: z.number().default(10).describe('Maximum number of reviews to return'),
        }),
    },
    async ({ place_id, limit }) => {
        // In a real implementation, you would:
        // 1. Call the Google Places API with place_id
        // 2. Parse the response into review objects
        // 3. Return structured data
        const reviews = [
            { author_name: 'Alice Johnson', rating: 5, text: 'Excellent service! The team went above and beyond.', time_ago: '2 days ago' },
            { author_name: 'Bob Smith', rating: 4, text: 'Great experience overall, minor wait time.', time_ago: '1 week ago' },
            { author_name: 'Carol Davis', rating: 1, text: 'Disappointing. Did not meet expectations.', time_ago: '3 weeks ago' },
        ];

        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify({
                        place_id,
                        reviews: reviews.slice(0, limit),
                        count: reviews.length,
                    }, null, 2),
                },
            ],
        };
    },
);

// Set up the HTTP transport
const MCP_PORT = process.env.MCP_PORT || 3002;

const mcpPostHandler = async (req, res) => {
    try {
        let transport;
        const sessionId = req.headers['mcp-session-id'];

        if (sessionId && transports[sessionId]) {
            transport = transports[sessionId];
        } else {
            transport = new NodeStreamableHTTPServerTransport({
                sessionIdGenerator: () => randomUUID(),
                onsessioninitialized: (id) => {
                    transports[id] = transport;
                },
            });

            await server.connect(transport);

            // Clean up on close
            transport.onclose = () => {
                delete transports[transport.sessionId];
            };

            await transport.handleRequest(req, res);
            return;
        }

        await transport.handleRequest(req, res);
    } catch (error) {
        res.status(500).json({
            jsonrpc: '2.0',
            error: { code: -32603, message: 'Internal server error' },
            id: null,
        });
    }
};

const mcpGetHandler = async (req, res) => {
    const sessionId = req.headers['mcp-session-id'];
    if (!sessionId || !transports[sessionId]) {
        res.status(400).send('Invalid or missing session ID');
        return;
    }
    await transports[sessionId].handleRequest(req, res);
};

// Simple HTTP server using Node.js built-in http module
import http from 'node:http';
import { parse } from 'node:url';

const serverHttp = http.createServer(async (req, res) => {
    const { pathname } = parse(req.url);

    if (pathname === '/mcp' && req.method === 'POST') {
        await mcpPostHandler(req, res);
    } else if (pathname === '/mcp' && req.method === 'GET') {
        await mcpGetHandler(req, res);
    } else {
        res.writeHead(404);
        res.end('Not found');
    }
});

serverHttp.listen(Number(MCP_PORT), () => {
    console.log(`Google Reviews MCP server running on port ${MCP_PORT}`);
    console.log(`Open WebUI connection URL: http://localhost:${MCP_PORT}/mcp`);
});

What this code is doing:

  • new McpServer() – Creates the MCP server instance with identification metadata (name and version). This is sent to every connecting client.
  • server.registerTool() – Registers the fetch_reviews tool. The first argument is the tool name, the second defines the input schema using Zod (which the SDK uses to generate the JSON Schema that AI clients need to invoke the tool), and the third is the handler function that runs when the AI calls the tool.
  • StreamableHTTPServerTransport – Creates an HTTP-based transport that speaks the MCP Streamable HTTP protocol. The sessionIdGenerator creates unique session IDs, and onsessioninitialized stores the transport for the session.
  • server.connect(transport) – Connects the MCP server to its transport. This is required before any requests can be processed.
  • serverHttp.listen() – Starts the HTTP server on the configured port. Requests to /mcp are routed to the MCP transport handler.

Running the Server

Run the server with Node.js:

node server.js

Or with Bun (for faster startup):

bun server.js

Expected output:

Google Reviews MCP server running on port 3002
Open WebUI connection URL: http://localhost:3002/mcp

Connecting to Open WebUI

Follow the same process from my Connecting Open WebUI to MCP Servers post, but point the Server URL to http://localhost:3002 (or whatever port you configured):

  1. Navigate to Admin Settings → External Tools in your Open WebUI admin panel
  2. Click + (Add Server)
  3. Set Type to MCP (Streamable HTTP)
  4. Set Server URL to http://localhost:3002
  5. Set Auth to None
  6. Click Save

From Example to Production

The code above returns sample data. In production, replace the hardcoded reviews with a real Google Places API call:

import fetch from 'node-fetch';

// Replace the sample data with:
const response = await fetch(
  `https://places.googleapis.com/v1/places/${place_id}:reviews`,
  {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      'X-Goog-Api-Key': process.env.GOOGLE_API_KEY,
    },
  }
);
const data = await response.json();

The MCP structure stays identical. You’re only replacing the business logic inside the tool handler. The SDK handles protocol compliance, JSON-RPC, session management, and transport — everything you’d normally build yourself for an integration.

What’s Next

We built this in JavaScript. Here is how the other three languages compare:

  • Go version – Compiled to a single binary with zero runtime dependencies. Best for performance-critical deployments.
  • C# version – Built with the official Microsoft-maintained C# SDK and ASP.NET Core. Ideal for enterprise .NET stacks.
  • Python version – Built with the FastMCP API using decorators. The quickest to write and prototype.

For the full JavaScript SDK reference, see the MCP TypeScript SDK documentation.

Key Takeaway

  • Developer experience: The JavaScript MCP SDK uses Zod schemas that automatically generate JSON Schema for tool parameters. You write one Zod schema and the AI client gets a complete, typed interface — no manual schema definition.
  • Runtime flexibility: Runs on Node.js, Bun, or Deno. Choose the runtime that matches your deployment strategy and performance requirements.
  • Strategic: Your AI agent now has direct access to your company’s Google reviews. No middleware, no API gateway, no vendor lock-in. Just a server that returns data your LLM can act on.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *