Building an Agentic Web App: Connecting MCP Server and MCP Client over HTTP

Adeesh SharmaAdeesh Sharma
5 min read

In this guide, we'll walk through how to move from a local-only Model Context Protocol (MCP) setup (where MCP Client and Server talk over stdio pipes) to a networked version where they communicate over HTTP/HTTPS. This allows you to deploy your MCP Server and Client independently across the internet.

We'll cover:

  • Setting up an MCP Server that listens over HTTP

  • Updating the MCP Client to connect over HTTP

  • Structuring tools like MongoDB item fetch and insert

  • Making it production-ready

Why Shift to HTTP?

When MCP Client and MCP Server talk over StdioClientTransport, they can only work locally. To support cloud deployments and make your application scalable, you must:

  • Host MCP Server independently.

  • Connect to it over HTTP (or HTTPS in production).

This allows your Express app (MCP Client) to orchestrate conversations with Anthropic Claude while dynamically calling tools deployed anywhere!


Understanding the Setup

In our setup:

  • MCP Server: Hosts tools like get_items and add_item that interact with a MongoDB database. It listens over an HTTP server and can be deployed independently.

  • MCP Client: An Express app that serves endpoints for interacting with Anthropic Claude. It connects to the MCP Server over HTTP, dynamically listing and calling tools when needed.

  • Anthropic Claude: Acts as the reasoning engine that decides whether a tool should be called based on user prompts.

  • MongoDB: Serves as the backend database where our tools perform CRUD operations.

This architecture makes your agentic app flexible, modular, and cloud-ready.

Key Components:

  • Tools: Small, focused server-side functions like get_items, add_item.

  • HTTP Communication: MCP Client talks to MCP Server over a simple HTTP POST-based protocol.

  • Deployment Ready: MCP Server and Client can scale independently.


1. Setting up MCP Server

server.ts

// server.ts

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { MongoClient } from "mongodb";
import http from "http";
import dotenv from "dotenv";

dotenv.config();

// MongoDB Connection Setup
const uri = process.env.MONGODB_URI;
if (!uri) throw new Error('MONGODB_URI environment variable is not set');

const dbName = 'mcptool';
const collectionName = 'items';

const validateMongoDBUri = (uri: string) => {
  if (!uri.startsWith('mongodb://') && !uri.startsWith('mongodb+srv://')) {
    throw new Error('Invalid MongoDB URI format.');
  }
  return uri;
};

// Define Tools
const tools = [
  {
    name: 'get_items',
    description: 'Fetch all items from MongoDB',
    input_schema: {
      type: 'object',
      properties: {},
    },
    run: async () => {
      const client = new MongoClient(validateMongoDBUri(uri));
      try {
        await client.connect();
        const collection = client.db(dbName).collection(collectionName);
        const items = await collection.find().toArray();
        return { content: JSON.stringify(items, null, 2) };
      } catch (error) {
        console.error('[MongoDB Error - get_items]:', error);
        return { content: 'Failed to fetch items.' };
      } finally {
        await client.close();
      }
    },
  },
  {
    name: 'add_item',
    description: 'Add a new item to MongoDB',
    input_schema: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
        price: { type: 'number' },
      },
      required: ['id', 'name', 'price'],
    },
    run: async ({ id, name, price }: { id: string; name: string; price: number }) => {
      const client = new MongoClient(validateMongoDBUri(uri));
      try {
        await client.connect();
        const collection = client.db(dbName).collection(collectionName);

        const existingItem = await collection.findOne({ id });
        if (existingItem) {
          return { content: `Item with ID ${id} already exists.` };
        }

        const newItem = { id, name, price, createdAt: new Date() };
        await collection.insertOne(newItem);
        return { content: `Successfully added item: ${name}` };
      } catch (error) {
        console.error('[MongoDB Error - add_item]:', error);
        return { content: 'Failed to add item.' };
      } finally {
        await client.close();
      }
    },
  },
];

// Create MCP Server
const mcpServer = new Server({
  name: "mongo-tools-server",
  version: "1.0.0",
});

// Register tools
for (const tool of tools) {
  mcpServer.registerTool({
    name: tool.name,
    description: tool.description,
    inputSchema: tool.input_schema,
    run: tool.run,
  });
}

// Start HTTP server
const httpServer = http.createServer();
mcpServer.listenHttpServer(httpServer);

const MCP_SERVER_PORT = process.env.MCP_SERVER_PORT || 4000;
httpServer.listen(MCP_SERVER_PORT, () => {
  console.log(`๐Ÿš€ MCP Server listening on http://localhost:${MCP_SERVER_PORT}`);
});

2. Setting up MCP Client (Express App)

index.ts

// index.ts

import { Anthropic } from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { HTTPClientTransport } from "@modelcontextprotocol/sdk/client/http.js"; // <-- Change here
import express from "express";
import type { RequestHandler } from "express";
import cors from "cors";
import dotenv from "dotenv";

dotenv.config();

// Validate keys
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
const MCP_SERVER_URL = process.env.MCP_SERVER_URL; // Example: http://localhost:4000/mcp
if (!ANTHROPIC_API_KEY) throw new Error('ANTHROPIC_API_KEY not set');
if (!MCP_SERVER_URL) throw new Error('MCP_SERVER_URL not set');

class MCPClient {
  private mcp: Client;
  private llm: Anthropic;
  private transport: HTTPClientTransport | null = null;
  public tools: any[] = [];

  constructor() {
    this.llm = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
    this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
  }

  async connectToServer() {
    try {
      this.transport = new HTTPClientTransport({
        url: MCP_SERVER_URL, // <-- Now URL
      });
      await this.mcp.connect(this.transport);

      const toolsResult = await this.mcp.listTools();
      this.tools = toolsResult.tools.map((tool) => ({
        name: tool.name,
        description: tool.description,
        input_schema: tool.inputSchema,
      }));

      console.log("โœ… Connected to MCP Server at", MCP_SERVER_URL);
      console.log("Available tools:", this.tools.map(t => t.name));
    } catch (e) {
      console.error('Failed to connect to MCP Server:', e);
      throw e;
    }
  }

  async processQuery(query: string) {
    const messages = [{ role: "user", content: query }];

    const response = await this.llm.messages.create({
      model: "claude-3-5-sonnet-20241022",
      max_tokens: 1000,
      messages,
      tools: this.tools,
    });

    const finalText = [];

    for (const content of response.content) {
      if (content.type === "text") {
        finalText.push(content.text);
      } else if (content.type === "tool_use") {
        const result = await this.mcp.callTool({
          name: content.name,
          arguments: content.input as any,
        });
        finalText.push(`[Tool Output] ${JSON.stringify(result.content)}`);
      }
    }

    return finalText.join("\n");
  }

  async cleanup() {
    await this.mcp.close();
  }
}

// App Start
async function main() {
  const app = express();
  const port = process.env.PORT || 3000;

  const mcpClient = new MCPClient();

  try {
    await mcpClient.connectToServer();

    app.use(cors());
    app.use(express.json());

    app.get("/health", (req, res) => {
      res.json({ status: "ok", tools: mcpClient.tools.map(t => t.name) });
    });

    app.post("/chat", async (req, res) => {
      try {
        const { query } = req.body;
        if (!query) {
          res.status(400).json({ error: 'Query is required' });
          return;
        }
        const response = await mcpClient.processQuery(query);
        res.json({ response });
      } catch (e) {
        console.error("Chat error:", e);
        res.status(500).json({ error: "Failed to process query" });
      }
    });

    app.listen(port, () => {
      console.log(`๐Ÿš€ Express app running at http://localhost:${port}`);
    });

  } catch (e) {
    console.error('Startup error:', e);
    process.exit(1);
  }
}

main();

3. Environment Variables (.env)

MONGODB_URI=mongodb://localhost:27017
MCP_SERVER_PORT=4000
MCP_SERVER_URL=http://localhost:4000/mcp
ANTHROPIC_API_KEY=your_real_anthropic_api_key_here

4. How to Run

  1. Start the MCP Server:
ts-node server.ts
  1. Start the MCP Client (Express App):
ts-node index.ts
  1. Send a Chat Query:
curl -X POST http://localhost:3000/chat \
  -H "Content-Type: application/json" \
  -d '{"query": "Fetch all items from the database"}'

Conclusion

By shifting to HTTPClientTransport and listenHttpServer(), you made your MCP Server scalable, cloud-ready, and production-grade. Now your LLM applications can dynamically invoke powerful tools deployed independently!

This unlocks real agentic architecture for the web.

0
Subscribe to my newsletter

Read articles from Adeesh Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Adeesh Sharma
Adeesh Sharma

Adeesh is an Associate Architect in Software Development and a post graduate from BITS PILANI, with a B.E. in Computer Science from Osmania University. Adeesh is passionate about web and software development and strive to contribute to technical product growth and decentralized communities. Adeesh is a strategic, diligent, and disciplined individual with a strong work ethic and focus on maintainable and scalable software.