A beginner-friendly MCP starter repository for the Agentic AI Open-Source Workshop.
- What is MCP?
- Why MCP?
- MCP Architecture
- Tool vs Resource
- Tool Registration vs Tool Invocation
- JSON-RPC Basics
- Repository Structure
- Setup
- Run the Examples
- Run the Agent
- Run Tests
- Run Ruff
- Mock Mode
- First Contribution
- Contributor Issues
- Troubleshooting
Model Context Protocol (MCP) is a standardised interface that lets AI agents communicate with external tools and data sources.
Imagine you are building an AI assistant that can search the web, query a database, and call a calculator. Without a standard, you have to write custom integration code for each tool. Every agent has a different way of calling tools, and every tool has to support every agent separately.
MCP defines one common protocol.
Any Agent ──(MCP)──▶ Any Tool
An agent that speaks MCP can talk to any MCP-compatible tool. A tool that speaks MCP can be used by any MCP-compatible agent.
Without MCP:
# Agent has to know how to call this specific calculator
import calculator
result = calculator.add(5, 7)With MCP:
# Agent calls any tool via the same interface
result = await mcp_client.call_tool("calculate", {"expression": "5 + 7"})The agent does not need to know how the calculator is implemented. It just needs to know the tool's name and its input schema.
| Without MCP | With MCP |
|---|---|
| Custom integration per tool | One protocol for all tools |
| Agent and tool are tightly coupled | Agent and tool are decoupled |
| Hard to swap tools | Swap tools without changing the agent |
| Hard to discover what a tool does | Tools describe themselves |
| Hard to test in isolation | Server and client can be tested separately |
MCP is to AI agents what HTTP is to the web — a shared language that makes everything composable.
┌─────────────┐
│ Agent │
└──────┬──────┘
│
▼
┌─────────────┐
│ MCP Client │
└──────┬──────┘
│
│ MCP / JSON-RPC
▼
┌─────────────┐
│ MCP Server │
└──────┬──────┘
│
┌───┴────────┐
▼ ▼
┌───────┐ ┌──────────┐
│ Tool │ │ Resource │
└───────┘ └──────────┘
| Component | Role |
|---|---|
| Agent | The AI loop that decides what to do |
| MCP Client | Speaks MCP protocol on behalf of the agent |
| MCP Server | Hosts tools and resources; handles requests |
| Tool | A callable capability (e.g. calculate) |
| Resource | A readable piece of information (e.g. documentation) |
The client and server communicate using JSON-RPC 2.0 over standard I/O (in local mode) or another transport.
MCP distinguishes between two types of things a server can expose:
Something the agent can call to perform an operation.
A tool takes input, does something, and returns a result.
calculate("5 + 7") → "12"
Information the agent can read.
A resource is identified by a URI and returns static or semi-static content.
workshop://introduction → "Welcome to the workshop …"
| Tool | Resource | |
|---|---|---|
| Purpose | Perform an action | Provide information |
| How agent uses it | call_tool() |
read_resource() |
| Example | calculate("2+3") |
workshop://introduction |
| Analogy | A function call | A file or web page |
When the server starts, it registers its tools. This is the server saying:
"I provide a tool called
calculate. It takes one parameter:expression(a string). Here is its description."
@mcp.tool(name="calculate", description="…")
def calculate(expression: str) -> str:
...A client can ask the server: "What tools do you have?"
Client → tools/list → Server
Server → [{"name": "calculate", …}] → Client
Once the client knows the tool exists, it can call it:
Client → tools/call (name="calculate", args={"expression": "5+7"}) → Server
Server → "12" → Client
The registration is separate from the invocation. You register once; you can invoke many times.
MCP uses JSON-RPC 2.0 as its message format. You do not need to write JSON-RPC by hand — the MCP SDK handles it. But understanding it helps you read logs and debug problems.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "calculate",
"arguments": { "expression": "5 + 7" }
}
}| Field | Purpose |
|---|---|
jsonrpc |
Always "2.0" |
id |
A unique request ID — matched with the response |
method |
The operation to perform |
params |
Arguments for the operation |
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{ "type": "text", "text": "12" }]
}
}The id in the response matches the request — this is how the client
knows which response belongs to which request.
agentic-mcp-starter/
│
├── README.md ← You are here
├── CONTRIBUTING.md ← Contribution guide
├── CODE_OF_CONDUCT.md
├── SECURITY.md
├── LICENSE
├── .gitignore
├── .env.example ← Copy to .env (optional)
├── pyproject.toml
├── requirements.txt
│
├── src/
│ └── starter/
│ ├── __init__.py
│ ├── config.py ← Environment / configuration
│ ├── tools.py ← Calculator tool (safe ast-based eval)
│ ├── resources.py ← Static MCP resources
│ ├── prompts.py ← Prompt templates
│ ├── server.py ← MCP server entry point
│ ├── client.py ← MCP client wrapper
│ └── agent.py ← Basic agent loop
│
├── mocks/
│ ├── __init__.py
│ └── llm.py ← Deterministic mock LLM (no API key needed)
│
├── examples/
│ ├── basic_server.py ← Start the MCP server
│ ├── basic_client.py ← Connect and discover tools/resources
│ ├── tool_call_demo.py ← Step-by-step tool call demo
│ └── resource_demo.py ← Resource discovery and retrieval demo
│
├── tests/
│ ├── test_config.py
│ ├── test_tools.py
│ ├── test_resources.py
│ ├── test_prompts.py
│ ├── test_mock_llm.py
│ └── test_agent.py
│
├── schemas/
│ └── tool.schema.json ← JSON Schema for MCP tool definitions
│
└── docs/
└── ISSUES.md ← 5 contributor issues
git clone https://github.com/<org>/agentic-mcp-starter.git
cd agentic-mcp-starterLinux / macOS:
python3.12 -m venv .venv
source .venv/bin/activateWindows PowerShell:
py -3.12 -m venv .venv
.venv\Scripts\Activate.ps1Windows tip: If you get a script execution error, run:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserThen try activating again.
pip install -r requirements.txt
pip install -e .python --version # Should show Python 3.12.x
pytest --version
ruff --versioncp .env.example .envThe defaults in .env.example work without modification.
No API keys are required.
All examples should be run from the repository root with the virtual environment active.
python examples/basic_server.pyThe server starts and waits for a client. Press Ctrl+C to stop it.
In normal use you do not need to run the server manually — the client and agent start it automatically as a subprocess.
python examples/basic_client.pyExpected output:
=== MCP Basic Client Demo ===
[Tools]
- calculate: Evaluate a simple arithmetic expression …
[Resources]
- workshop://introduction : Workshop Introduction
- workshop://architecture : MCP Architecture Overview
[Tool call] calculate('2 + 3') → 5
python examples/tool_call_demo.pyShows the complete tool lifecycle: definition → discovery → invocation.
python examples/resource_demo.pyLists all resources and prints their content.
The agent loop ties everything together.
python -c "
import asyncio, sys
sys.path.insert(0, 'src')
sys.path.insert(0, '.')
from starter.agent import Agent
async def main():
agent = Agent()
reply = await agent.run('calculate 5 + 7')
print('Agent reply:', reply)
asyncio.run(main())
"Expected output:
Agent reply: The result of 5 + 7 is 12.
User input: "calculate 5 + 7"
↓
Agent receives the message
↓
Mock LLM decides: use_tool=True, tool=calculate, args={"expression": "5 + 7"}
↓
MCP Client calls tools/call on the MCP Server
↓
MCP Server evaluates "5 + 7" → "12"
↓
Agent formats final reply: "The result of 5 + 7 is 12."
pytest -qAll tests should pass. You should see output similar to:
.................................
33 passed in 0.42s
To see verbose output:
pytest -vruff check .No issues should be reported.
To auto-fix safe issues:
ruff check . --fixThis repository is designed to work entirely offline without any API keys.
The default configuration (from .env.example) is:
LLM_PROVIDER=mock
MCP_MODE=local
- The Mock LLM (
mocks/llm.py) uses keyword matching to decide which tool to call. - It makes no network requests.
- It requires no API keys.
- It is fully deterministic: the same input always produces the same output.
This makes the workshop:
- Free to attend
- Reproducible
- Testable
When you are ready to use a real LLM, you can extend src/starter/agent.py
to call a real provider. That is a separate workshop repository.
Find Issue → docs/ISSUES.md lists 5 available issues
↓
Claim Issue → Comment "I'd like to work on this"
↓
Create Branch → git checkout -b feat/B01-description
↓
Implement → Write code
↓
Write Tests → Add tests in tests/
↓
Run CI Locally → pytest -q && ruff check .
↓
Open PR → Fill in the PR template
↓
Review → Respond to maintainer comments
↓
Merge → 🎉
See CONTRIBUTING.md for the full guide.
See docs/ISSUES.md for full details.
| ID | Title | Level |
|---|---|---|
| B01 | Improve Tool Schema Validation | Beginner |
| B02 | Add Calculator Edge-Case Tests | Beginner |
| B03 | Add a New Static MCP Resource | Beginner |
| I01 | Add a New MCP Resource Type | Intermediate |
| I02 | Improve Mock LLM to MCP Tool Routing | Intermediate |
python --version # Must be 3.12.xIf you have an older Python, install Python 3.12 from python.org.
On Linux/macOS you can use pyenv to manage versions:
pyenv install 3.12.0
pyenv local 3.12.0Run this once in PowerShell:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserThen activate:
.venv\Scripts\Activate.ps1Make sure your virtual environment is active and you installed dependencies:
pip install -r requirements.txt
pip install -e .Make sure you installed the package in editable mode:
pip install -e .Run commands from the repository root so that the mocks/ directory is on the Python path.
If you see RuntimeError: no running event loop, you need to use asyncio.run():
import asyncio
asyncio.run(my_async_function())Make sure .env exists (not just .env.example):
cp .env.example .envInstall ruff:
pip install ruffCheck that mcp is installed:
pip install mcpRun:
python -c "import mcp; print(mcp.__version__)"The agent tests use a fake MCP client by default so they do not start a subprocess.
If you have modified tests/test_agent.py to use the real client, revert that change.
1. Read this README
↓
2. Run setup
↓
3. Run examples
↓
4. Read source code: server.py → client.py → agent.py
↓
5. Run tests
↓
6. Pick a contributor issue
↓
7. Implement, test, submit PR
After completing this repository you will understand:
"I know what MCP is, I understand the client/server architecture, and I know how an agent can discover and invoke an MCP tool."
Then you are ready for Repo 2 — mcp-tools-lab where you will build real MCP tools.
MIT — see LICENSE.