Getting Started with MCP: From Core Concepts to Building Server and Client
This article explains why the Model Context Protocol (MCP) is needed for LLMs, describes its client‑server architecture, data and transport layers, and provides step‑by‑step Python examples for creating both an MCP server and a client using FastMCP and low‑level APIs.
The author, after completing Anthropic's MCP course on DeepLearning.AI and reviewing the official documentation (version 2025‑06‑18), writes a practical guide for developers who want to understand and build with the Model Context Protocol (MCP).
Note: This article was compiled on 2025‑07‑28; its content and viewpoints may become outdated.
Why MCP?
Providing higher‑quality context improves LLM performance, so connecting LLMs to external environments (e.g., GitHub, Notion) can supply richer context and enable the model to act on those environments. MCP standardizes this "LLM‑to‑external" interaction, similar to how RESTful APIs standardize web services, without introducing a new tool‑use mechanism. Any scenario that can use MCP can also operate without it.
Without a standard, each app would need a custom integration strategy for each LLM, and each LLM‑enabled app would need its own adapters, leading to duplicated effort. MCP defines a common protocol so that any MCP‑compliant app and server can interoperate without extra adaptation. Major LLM providers such as OpenAI and Google now officially support MCP, making it the de‑facto standard for LLM‑external integration.
MCP Architecture and Core Concepts
MCP follows a client‑server model with three key components:
MCP Server : Provides Tools, Resources, and Prompts services and responds to client requests.
MCP Client : A connector that maintains a one‑to‑one communication channel with the server.
Host : The top‑level AI application that manages multiple clients, aggregates data, and can issue requests to the server.
The protocol consists of two layers:
Data layer : Defines JSON‑RPC‑based message formats, primitives, and lifecycle logic.
Transport layer : Defines how connections are established and how messages are transmitted.
Data Layer Primitives
Three primitives are defined:
Tools : Executable functions that an AI application can call (e.g., file operations, API calls, database queries).
Resources : Data sources that provide context (e.g., file contents, database records, API responses).
Prompts : Reusable templates for interacting with language models (e.g., system prompts, few‑shot examples).
As of 2025‑07‑27, most MCP servers expose the Tools primitive; many developers are unaware that Resources and Prompts are also available, which can cause confusion between generic tool use and MCP‑specific services.
The article will not cover Resources and Prompts because they are less common; interested readers can explore them in the official docs.
On the client side, MCP exposes additional primitives for the server:
Sampling : Allows the server to request LLM completions via the client, avoiding direct LLM calls.
Roots : Lets the client expose a namespace of accessible file‑system paths.
Elicitation : Enables the server to ask the client to collect structured user input.
Lifecycle
Initialization : Handshake and capability negotiation; both sides exchange supported primitives and establish communication boundaries.
Operation : JSON‑RPC messages are exchanged according to the negotiated capabilities; different transport modes can be selected per scenario.
Shutdown : The connection is closed according to the chosen transport mode.
Transport Layer
The specification defines several transport options:
Local: stdio – the host launches the MCP server as a subprocess, sending JSON‑RPC requests via stdin and receiving responses via stdout. This mode is ideal for operations that need direct access to the user's local machine (e.g., local database or file access).
Remote:
Streamable HTTP – the current standard remote implementation that solves the drawbacks of HTTP + SSE. It supports both stateless (plain POST/GET) and stateful (SSE) interactions, allowing a simple request‑response flow that can be upgraded to streaming when real‑time push is required.
HTTP + SSE – a deprecated stateful mode that relies on a long‑lived connection, which is less scalable and unnecessary for many one‑off calls.
Streamable HTTP therefore replaces HTTP + SSE and is recommended for most cloud‑native and elastic deployment scenarios.
Developing an MCP Server
Using the Python SDK (FastMCP), a stateless Streamable HTTP server can be created with two simple tools that query GitHub: get_github_user_info: Retrieves a user's name, bio, and location. get_github_repo_issues: Retrieves titles, labels, and creation dates of issues for a repository.
from mcp.server.fastmcp import FastMCP
import httpx
mcp = FastMCP("StatelessServer", stateless_http=True)
@mcp.tool()
def get_github_user_info(username: str = "SuperTapir"):
"""Get information about a GitHub user."""
url = f"https://api.github.com/users/{username}"
response = httpx.get(url)
json = response.json()
return {
"name": json["name"],
"bio": json["bio"],
"location": json["location"],
}
@mcp.tool()
def get_github_repo_issues(owner: str = "SuperTapir", repo: str = "Blog"):
"""Get issues for a GitHub repository."""
url = f"https://api.github.com/repos/{owner}/{repo}/issues"
response = httpx.get(url)
issues = response.json()
return [
{
"title": issue["title"],
"labels": [lbl["name"] for lbl in issue.get("labels", [])],
"created_at": issue["created_at"],
}
for issue in issues
]
if __name__ == "__main__":
mcp.run(transport="streamable-http")Debugging can be performed with @modelcontextprotocol/inspector, which visualizes the server’s behavior and confirms that it matches expectations.
For more flexibility, the low‑level API can be used to define custom tools/list and tools/call endpoints, assign namespaced identifiers (e.g., tapir.github/get_user_info), and adjust lifecycle or mount‑point logic.
import json, logging, contextlib
from collections.abc import AsyncIterator
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.types import Receive, Scope, Send
import httpx
logger = logging.getLogger(__name__)
app = Server("My GitHub MCP")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="tapir.github/get_user_info",
description="Get information about a GitHub user.",
inputSchema={
"type": "object",
"required": ["username"],
"properties": {"username": {"type": "string", "default": "SuperTapir"}},
},
),
types.Tool(
name="tapir.github/get_repo_issues",
description="Get issues for a GitHub repository.",
inputSchema={
"type": "object",
"required": ["owner", "repo"],
"properties": {
"owner": {"type": "string", "default": "SuperTapir"},
"repo": {"type": "string", "default": "Blog"},
},
},
),
]
@app.call_tool()
async def call_tool(tool_name: str, tool_input: dict) -> list[types.ContentBlock]:
if tool_name == "tapir.github/get_user_info":
url = f"https://api.github.com/users/{tool_input['username']}"
user_info = httpx.get(url).json()
result = {"name": user_info["name"], "bio": user_info["bio"], "location": user_info["location"]}
return [types.TextContent(type="text", text=json.dumps(result))]
elif tool_name == "tapir.github/get_repo_issues":
url = f"https://api.github.com/repos/{tool_input['owner']}/{tool_input['repo']}/issues"
issues = httpx.get(url).json()
result = [
{
"title": issue["title"],
"labels": [lbl["name"] for lbl in issue.get("labels", [])],
"created_at": issue["created_at"],
}
for issue in issues
]
return [types.TextContent(type="text", text=json.dumps(result))]
return []
session_manager = StreamableHTTPSessionManager(app=app, event_store=None, stateless=True)
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
await session_manager.handle_request(scope, receive, send)
@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
logger.info("Application started with StreamableHTTP session manager!")
try:
yield
finally:
logger.info("Application shutting down...")
def main():
starlette_app = Starlette(
debug=True,
routes=[Mount("/mcp", app=handle_streamable_http)],
lifespan=lifespan,
)
import uvicorn
uvicorn.run(starlette_app, host="127.0.0.1", port=8001)
if __name__ == "__main__":
main()This low‑level approach lets developers dynamically generate tool lists and customize server lifecycle behavior beyond the high‑level decorator style.
Developing an MCP Client
Although most developers focus on the server side, building a client helps understand the full MCP workflow. The following example connects to the local Streamable HTTP server, lists available tools, and calls the tapir.github/get_user_info tool.
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
# Connect to the local Streamable HTTP MCP service
async with streamablehttp_client("http://localhost:8001/mcp") as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
tools = await session.list_tools()
print(f"Available tools: {[tool.name for tool in tools.tools]}")
result = await session.call_tool("tapir.github/get_user_info", {"username": "SuperTapir"})
print(f"Result: {result}")
if __name__ == "__main__":
asyncio.run(main())Conclusion
MCP has become the de‑facto standard for connecting LLMs to external environments. Understanding and building an MCP server is valuable; this article started with the motivation, explained core concepts, and walked through concrete server and client implementations. The focus was on the Tools primitive and a stateless Streamable HTTP server; other primitives (Resources, Prompts), stateful services, advanced client capabilities, authentication, and notification mechanisms are covered in the official documentation.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Cultivation Path
Focused on sharing practical tech content about TypeScript, Vue 3, front-end architecture, and source code analysis.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
