MCP stands for Model Context Protocol and provides a standardized way to integrate AI applications using LLMs such as OpenAI GPT, Anthropic Claude, Google Gemini, and other custom/self-hosted models into larger systems. MCP servers adhere to a client-server architecture and allow us to precisely define which parts of the system AI can interact with and how, through a client such as Claude Code. Here, we refer to Claude Code as an application that serves as a client. Another example would be Microsoft Copilot. This improves the control over the LLM’s actions towards our system, thus improving security. By utilising an MCP server, run either locally using STDIO or remotely via HTTP, LLMs can access the following in a standardized way:

  1. data sources such as API responses and files
  2. development tools, such as functions from an API library
  3. workflows such as Slack workflows

For example, assume functions for fetching data from a database and reporting were written in an API library, along with some other functions. MCP client, in this case Claude, initiates a connection to the MCP server and starts it locally, as instructed in Claude’s configuration file. Functions for fetching data from the database and functions for reporting have special annotation on them, allowing these functions from the API library to be registered on the server as MCP tools. Claude then receives a list of all available MCP tools. Let’s say we want to query the database to retrieve some company data at this point. The MCP server allows us to prompt Claude to query the database for us. LLM autonomously chooses an appropriate MCP tool and forwards a request to it. So the function from the API library is then executed. Once the MCP server receives a response from the API library, LLM processes it and presents it to the user via Claude interface. The steps above describe a local MCP server using STDIO as the means of data transport. If it were a remote MCP server, the only difference would be the fact that the MCP client would connect to an already up-and-running, remote MCP server and use HTTP to transport data.

The bidirectional flow of data, as described, is illustrated in Figure 1 below, emphasising that AI applications powered by an LLM, such as Claude Code, never directly communicate with the inner workings and code of the tools. LLM only has access to the prompts, signatures of the registered development tools, requests, and responses, as opposed to having access to potentially sensitive code of the API library. MCP communication between client and server is built on top of JSON RPC, a long-established protocol for structured, remote method calls. This means that everything exchanged in this client-server architecture, ranging from tool discovery and execution to responses, travels as JSON RPC requests or responses. This protocol is precisely what allows the server and the client to be created in their own independent programming languages. A JSON RPC request contains the ID of the request that relates the requests and responses, the method that should be called, such as tools/list, and the parameters for this method as extracted from the prompt. A JSON RPC response similarly contains the ID and the result of the method execution. Detailed steps in Figure 1 explain this data flow in a simplified manner.

Figure 1: MCP Data Flow

Figure 1: MCP Data Flow


Connection & Setup

  1. MCP client initiates a connection to the MCP server and starts it either locally or remotely

Tool Registration

  1. MCP server requests a list of annotated MCP tools from the API library
  2. MCP server retrieves a list of annotated MCP tools from the API library
  3. MCP client requests a list of registered MCP tools from the MCP server
  4. MCP client receives a list of annotated MCP tools from the API library

 LLM Processing 1

  1. MCP client sends the prompt and context (such as method signatures, parameter types, etc.) of all MCP tools. Example prompt: “Generate today’s sales report based on the current sales summary, save it as a report, and then confirm that the file was created successfully.”
  2. LLM autonomously picks the right tool(s) and firstly calls get_sales_summary().

Tool Execution

  1. MCP client forwards a request to get_sales_summary() MCP tool.
  2. MCP server forwards the request to get_sales_summary() function from the API library, and the code is executed.
  3. The API library returns the response to the MCP server, such as {“date”:”2025-11-19″,”total_sales”:19954,”region”:”Global”,”top_product”:”AI Chat Subscription”,”growth_pct”:5}

 LLM Processing 2

  1. The response is forwarded to the MCP client.
  2. MCP client allows the LLM to process the response and formulate what the end user will receive
  3. The end user receives the result: “Now I’ll create and submit the sales report.”
  4. The steps 7 – 13 starting from the first LLM processing are repeated until the tasks from the original prompt are exhausted, with the final result being:

Report created successfully!

The daily sales report for November 19, 2025, has been generated and saved to: /Users/hanapotogija/Desktop/MCP Server/reports/20251119_143613_Daily_Sales_Report_-_November_19,_2025.json

Report Summary:

  • Total Sales: $19,954
  • Growth Rate: 5.5%
  • Top Product: AI Chat Subscription
  • Status: Successfully saved

The comprehensive report includes an executive summary, key metrics, performance analysis, market insights, strategic recommendations, and conclusions based on today’s sales performance.

Figure 2 shows how the previously described MCP interactions look in Claude.

Figure 2: Using MCP server through Claude

Figure 2: Using MCP server through Claude


MCP Benefits

To reiterate, MCP provides a standardized way for LLM models to be integrated with existing data, tools, APIs, services, workflows, and so forth, so that no team or individual has to figure out how to use these LLM tools on their own. At its core, MCP is about controlling how much access AI has to the system so that LLMs can operate only on what’s been explicitly exposed to them, protecting the system from unauthorized access. Furthermore, the MCP server poses as an intermediary between the system and the AI, fostering the separation of concerns and ease of integrating diverse tooling. The choice of programming language in which the MCP server is created is not relevant, as long as the protocol defined in the previous section is followed. Similarly, the choice of LLM model is also not relevant, as there are no restrictions on it by the MCP. By providing standardized inputs and outputs, the reliability of these AI workflows is enhanced, making them suitable for AI-driven development environments, automated data pipelines, and workflow orchestration, as MCP tools can be chained easily, independently, and reliably.

So, how is using an MCP server to fetch data from a database different from calling the function for querying the database ourselves?  For example, previously, the person needed to know the database schema and SQL to query data. Now, MCP servers open the door for non-technical people to accomplish similar tasks in a human-readable prompt format. Perhaps we need a series of queries, perhaps we want to execute some user flow, or something else entirely. Using MCP servers gives us the flexibility to call exposed development tools in any order we want, without worrying about the underlying code or different APIs. Thus, a solution like this cannot be easily replaced by writing predefined scripts that can be given to stakeholders, management, and the development team. Non-technical stakeholders aren’t the only ones benefiting from using MCP servers. As a quality assurance engineer, I see many benefits to using an MCP server. This includes fast data seeding for complex systems with multiple services, fast bug reporting, and aids testing by completing time-consuming steps without having to write elaborate automation scripts, and more.

Implementation

Now that you see value in creating your own MCP server, this section will walk you through implementing a simple example MCP server connected to Claude on your own. Figures 1 and 2 in the previous sections showcase this implementation.

Dependencies

The created MCP server is local, using STDIO transport of data. The chosen programming language is Python, using the FastMCP library. An important prerequisite before trying it out yourself is that the FastMCP library requires a minimum Python version 3.10.x. This guide assumes you are familiar with AI applications powered by LLMs such as ChatGPT, Claude Code, or others. You should have the latest version of the Claude desktop installed.

Paste the following line of code in the terminal to install FastMCP, making this the only library dependency outside the standard Python for the implementation. Furthermore, your Python interpreter might be named differently from python3.10.

python3.10 -m pip install fastmcp

The implementation of this particular MCP server assumes that we have an API library that can fetch data about a company, customers, and sales. We want to expose functions from that API library to the MCP server connected to Claude so that we don’t have to execute them from this API ourselves.

API Library

First, let us create a simple API library. Create a directory and call it api. Inside the api directory, create an empty __init__.py file, marking this directory as a python package. Here, we will create a pool of functions, from which we want to expose some of those functions to the MCP server. Paste the following code into a new file called data_api.py.

import random
from datetime import date

def get_sales_summary():
   """Simulate fetching aggregated sales data."""
   return {
       "date": str(date.today()),
       "total_sales": random.randint(5000, 25000),
       "region": "Global",
       "top_product": "AI Chat Subscription",
       "growth_pct": round(random.uniform(3.5, 12.7), 2),
   }

def get_customer_info(customer_id: int):
   """Simulate fetching a single customer's data."""
   customers = {
       1: {"name": "Alice", "region": "Europe", "purchases": 12},
       2: {"name": "Bob", "region": "North America", "purchases": 7},
       3: {"name": "Carla", "region": "Asia", "purchases": 15},
   }
   return customers.get(customer_id, {"error": "Customer not found"})

def list_customers():
   """Simulate fetching a list of all customers."""
   return [
       {"id": 1, "name": "Alice", "region": "Europe"},
       {"id": 2, "name": "Bob", "region": "North America"},
       {"id": 3, "name": "Carla", "region": "Asia"},
   ]

In addition to reading information about the company, we may want to report data from our company elsewhere. Assume this comes from a different API. Create a new file in the api directory called report_api.py. Paste the following code in this file.

from datetime import datetime
import json
import os

BASE_DIR = os.path.dirname(os.path.abspath(__file__))  
PROJECT_ROOT = os.path.dirname(BASE_DIR)              
REPORTS_DIR = os.path.join(PROJECT_ROOT, "reports")

os.makedirs(REPORTS_DIR, exist_ok=True)

def post_report(title: str, content: str):
   """Simulate saving a report to disk inside /reports directory."""
   timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
   safe_title = title.replace(" ", "_")
   filename = f"{timestamp}_{safe_title}.json"
   path = os.path.join(REPORTS_DIR, filename)

   report_data = {
       "title": title,
       "content": content,
       "timestamp": timestamp
   }

   with open(path, "w", encoding="utf-8") as f:
       json.dump(report_data, f, indent=2)

   return {"status": "success", "path": path}

The pasted code simulates submitting a report by writing the report title, content, and timestamp to an automatically created reports directory inside the root directory of your project. Right here with your code and inside reports directory, you can find your saved reports. Now that the API library is created, we are done with one component described in Figure 2.


MCP Server

Now it’s time to create the actual MCP server. Create a new file in the main directory and name it server.py. Let us first handle imports and logging. Imports handle dynamic file paths, the FastMCP library, which enables us to build this server easily, and our API libraries. Similarly to submitting reports, storing the server’s logs works by writing to a local file during MCP tool execution. The logs directory is automatically created in the root directory, and you can find the logs of your executions in the   mock_company_server.log file.

import logging
import sys
import os
from fastmcp import FastMCP
from api import data_api, report_api

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
os.makedirs(LOG_DIR, exist_ok=True)

LOG_FILE = os.path.join(LOG_DIR, "mock_company_server.log")

logging.basicConfig(
   level=logging.INFO,
   format="%(asctime)s [%(levelname)s] %(message)s",
   handlers=[
       logging.FileHandler(LOG_FILE),
       logging.StreamHandler(sys.stderr)
   ]
)

Now, we instantiate the MCP server via the FastMCP class and give it a name.

mcp = FastMCP("MockCompanyDataServer")

At this point, we have sufficient data to register MCP tools. We are going to achieve this by encapsulating functions from previously created API libraries as MCP tools, marked by the @mcp.tool() annotation. Below the MCP server instance, paste the code for the MCP tools:

@mcp.tool()
def get_sales_summary() -> dict:
   """Fetch the latest sales summary from the database."""
   logging.info("Tool: get_sales_summary called")
   return data_api.get_sales_summary()

@mcp.tool()
def get_customer_info(customer_id: int) -> dict:
   """Retrieve information for a specific customer."""
   logging.info(f"Tool: get_customer_info({customer_id})")
   return data_api.get_customer_info(customer_id)

@mcp.tool()
def list_customers() -> list:
   """List all customers in the database."""
   logging.info("Tool: list_customers called")
   return data_api.list_customers()

@mcp.tool()
def post_report(title: str, content: str) -> dict:
   """Submit a report based on fetched data."""
   logging.info(f"Tool: post_report({title})")
   return report_api.post_report(title, content)

Every time an MCP tool is called, the previously configured logging is used, where simple data about the tool call is written to the  mock_company_server.log file, and the MCP tool post_report works in a similar fashion. Finally, in order to start the server, paste the following line of code:

# ===== Start the MCP Server =====
if __name__ == "__main__":
   logging.info("Starting MockCompanyDataServer Locally")
   mcp.run(transport="stdio")


Configure with Claude

Finally, it’s time to configure the MCP server with Claude. Go to Claude settings > Developer tab. Click on Edit config, which will point to claude_desktop_config.json file. This file might be empty at first, but to configure the MCP server to be discoverable to Claude, paste the following:

{
  "preferences": {
    "menuBarEnabled": false
  },
  "mcpServers": {
    "mock_company_data": {
      "command": "<system python filepath>",
      "args": ["<project_root>/server.py"],
      "env": {
        "PYTHONPATH": "<system filepath to this repository>"
      }
    }
  }
}

Make sure to replace <system python filepath>, <system filepath to the server from this repository>, and <system filepath to this repository> with proper file paths on your machine. Command assumes you will use the system-level Python, args is the file path to the server.py file you created, and PYTHONPATH assumes the file path to the top-level directory that contains both the server and the API library.

If you want to try out this MCP server without building it yourself step by step, feel free to check out the GitHub implementation. You may clone the repository and check out README.md for a detailed guide on how to set up this ready-made MCP server on your machine from scratch!

Complete code available on GitHub: https://github.com/ATLANTBH/mcp-server-demo.git

If you found this useful, check out other Atlantbh blogs!

Leave a comment

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