Safaricom Daraja MCP Server
by mboya
README.md
# Safaricom Daraja MCP Server
A Model Context Protocol (MCP) server that integrates Safaricom's M-PESA Daraja API with Claude, enabling natural language payment processing and real-time transaction notifications.
## π Features
- **STK Push Payments**: Initiate M-PESA payment requests through natural language
- **Real-time Callbacks**: Automatic payment notification handling with Flask server
- **Payment Tracking**: Store and query payment history with read/unread status
- **Natural Language Interface**: Interact with M-PESA through Claude conversations
- **Sandbox Testing**: Full support for Daraja sandbox environment
- **Automated Testing**: Comprehensive test suite for validation
## π Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Configuration](#configuration)
- [Getting Daraja Credentials](#getting-daraja-credentials)
- [Usage](#usage)
- [Testing](#testing)
- [Integrating with Claude Desktop](#integrating-with-claude-desktop)
- [Claude Desktop and Railway](#claude-desktop-and-railway)
- [Available Tools](#available-tools)
- [Callback Setup](#callback-setup)
- [Troubleshooting](#troubleshooting)
- [Security Best Practices](#security-best-practices)
- [Production Deployment](#production-deployment)
- [API coverage vs Postman](#api-coverage-vs-postman)
- [API Reference](#api-reference)
- [Project Structure](#project-structure)
- [Contributing](#contributing)
- [License](#license)
## Prerequisites
- **Python 3.10+** installed on your system
- **Daraja API Account** - Register at [developer.safaricom.co.ke](https://developer.safaricom.co.ke/)
- **ngrok** (optional, for testing callbacks) - Download from [ngrok.com](https://ngrok.com/)
- **Claude Desktop** (optional, for MCP integration)
## Installation
### 1. Clone the Repository
```bash
# Clone the repository
git clone https://github.com/mboya/daraja-mcp.git
cd daraja-mcp
# Or if you already have the repository, navigate to it
cd daraja-mcp
```
### 2. Set Up Virtual Environment
```bash
# Create virtual environment
python3 -m venv venv
# Activate virtual environment
# macOS/Linux:
source venv/bin/activate
# Windows:
venv\Scripts\activate
```
You should see `(venv)` in your terminal prompt.
### 3. Install Dependencies
```bash
# Install all required packages from requirements.txt
pip install -r requirements.txt
```
This will install:
- `mcp` - Model Context Protocol server
- `requests` - HTTP library for API calls
- `flask` - Web framework for callback server
- `python-dotenv` - Environment variable management
- `gunicorn` - WSGI HTTP server (for production deployment)
### 4. Configure Environment Variables
Create a `.env` file in the project root directory. The repository includes a `.env.example` file as a template.
**Quick Setup:**
```bash
# Copy the example file
cp .env.example .env
# Edit the .env file with your actual credentials
# Use your preferred text editor (nano, vim, code, etc.)
nano .env
```
Then replace the placeholder values with your actual Daraja API credentials (see [Getting Daraja Credentials](#getting-daraja-credentials)).
## Configuration
### Environment Variables
Copy [`.env.example`](.env.example) to `.env` and fill in your credentials. Key variables:
| Variable | Purpose |
|----------|---------|
| `DARAJA_CONSUMER_KEY` / `DARAJA_CONSUMER_SECRET` | OAuth (all APIs) |
| `DARAJA_SHORTCODE` / `DARAJA_PASSKEY` | STK Push |
| `DARAJA_INITIATOR` / `DARAJA_SECURITY_CREDENTIAL` | B2C, B2B, balance, status, reversal |
| `DARAJA_ENV` | `sandbox` or `production` |
| `PUBLIC_URL` | HTTPS base URL Safaricom can reach (ngrok locally, Railway in prod) |
| `CALLBACK_HOST` | `127.0.0.1` for local dev (avoids macOS bind errors) |
| `CALLBACK_PORT` | Default `3000` |
| `IOT_*` | IoT SIM Portal (Phase 5) β see `.env.example` |
Never commit `.env`. For production, set variables in your hosting dashboard (Railway, etc.).
## Getting Daraja Credentials
### 1. Register on Daraja Portal
1. Visit [developer.safaricom.co.ke](https://developer.safaricom.co.ke/)
2. Create an account
3. Verify your email
### 2. Create an App
1. Navigate to "My Apps" β "Create New App"
2. Select APIs:
- Lipa Na M-PESA Online
- M-PESA Express (STK Push)
3. Submit your app
4. Get your credentials:
- Consumer Key
- Consumer Secret
- Passkey (in app details)
### 3. Sandbox Test Credentials
For testing, use these sandbox values:
- **Business Short Code:** 174379 (default sandbox)
- **Passkey:** Check your app details on Daraja portal
- **Test Phone Numbers:** 254708374149 (check Daraja docs for updated test numbers)
- **Test PIN:** Varies by sandbox version (usually simulated automatically)
### 4. Production Credentials
1. Test thoroughly in sandbox
2. Apply for production access through Daraja portal
3. Complete KYC and business verification
4. Receive production credentials
5. Update `.env` with production values and set `DARAJA_ENV=production`
## Usage
### Run the server
| Scenario | Command | Notes |
|----------|---------|--------|
| Claude Desktop (local) | `python server.py` | stdio MCP + Flask callbacks on `CALLBACK_PORT` |
| HTTP / cloud / Railway | `gunicorn server_http:app --bind 0.0.0.0:$PORT` | See `Procfile`; exposes `/mcp/tools`, `/mcp/call_tool`, all callback routes |
| Local HTTP smoke test | `python server_http.py` | Same routes as production, without gunicorn |
Verify the server:
```bash
curl http://127.0.0.1:3000/health
```
Both `server.py` and `server_http.py` share the same callback routes and MCP tools via `daraja_core.py`. `server.py` talks to Claude over stdio and runs Flask in a background thread; `server_http.py` is a single Flask app for deployment.
For **real Safaricom callbacks** in sandbox, set `PUBLIC_URL` to an HTTPS URL (ngrok locally). See [Callback setup](#callback-setup).
Then configure [Claude Desktop](#integrating-with-claude-desktop).
## Testing
All tests are in **`test_daraja.py`**.
| Command | What it runs |
|---------|----------------|
| `python test_daraja.py` | Full E2E (local): env, OAuth, MCP, webhooks, optional live API probes |
| `python test_daraja.py --platform` | CI-safe: no live Safaricom calls (GitHub Actions) |
| `python test_daraja.py --quick` | Auth + HTTP + callback smoke |
| `python test_daraja.py --callbacks` | All 17 webhook routes + MCP storage/retrieval |
| `python test_daraja.py --callbacks --public` | Above + POST webhooks through ngrok |
| `python test_daraja.py --live` | Live sandbox: ngrok, real STK, IMSI, IoT |
| `python test_daraja.py --b2c` | Live B2C payout + ngrok callback only |
| `python test_daraja.py --all` | Everything: platform tests + live STK/B2C/IoT via ngrok (no skips) |
Exit code **0** on pass, **1** on failure. Warnings (e.g. IMSI sandbox `404`) do not fail CI.
Optional: `DARAJA_TEST_PHONE` (default `254708374149`), `DARAJA_B2C_TEST_AMOUNT` (default `10`).
### GitHub Actions CI
Workflow: [`.github/workflows/ci.yml`](.github/workflows/ci.yml) (push/PR to `main`, `workflow_dispatch`).
| Job | Command / check | Secrets |
|-----|-----------------|--------|
| **compile** | `compileall` + MCP tool count | No |
| **platform** | `test_daraja.py --platform` (Python 3.10β3.12) | No |
| **webhooks** | `test_daraja.py --callbacks` | No |
| **gunicorn-smoke** | `gunicorn server_http:app` + curl health/MCP/callback | No |
| **live-sandbox** | `test_daraja.py --live` | Daraja + optional `NGROK_AUTHTOKEN` |
The **live-sandbox** job runs on `workflow_dispatch` or push to `main`; it runs live tests only when `DARAJA_CONSUMER_KEY` and `DARAJA_CONSUMER_SECRET` repository secrets are set (otherwise the job skips those steps).
## Integrating with Claude Desktop
### 1. Locate Configuration File
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
### 2. Add MCP Server Configuration
```json
{
"mcpServers": {
"daraja": {
"command": "/absolute/path/to/daraja-mcp/venv/bin/python",
"args": ["/absolute/path/to/daraja-mcp/server.py"],
"env": {
"DARAJA_CONSUMER_KEY": "your_consumer_key",
"DARAJA_CONSUMER_SECRET": "your_consumer_secret",
"DARAJA_SHORTCODE": "174379",
"DARAJA_PASSKEY": "your_passkey",
"DARAJA_ENV": "sandbox",
"CALLBACK_PORT": "3000",
"PUBLIC_URL": "https://your-ngrok-url.ngrok.io"
}
}
}
}
```
**Important:**
- Use absolute paths (not relative)
- Use virtual environment's Python: `venv/bin/python`
- Update PUBLIC_URL with your ngrok HTTPS URL
### 3. Restart Claude Desktop
Completely quit and reopen Claude Desktop to load the MCP server.
### 4. Verify Integration
In Claude Desktop, ask:
```
"Is the Daraja callback server working?"
```
Claude should respond with server status information.
## Claude Desktop and Railway
This section explains how **Claude Desktop** relates to a **Railway** deployment. Read this before pointing `PUBLIC_URL` at Railway while running Claude locally.
### Two servers, two transports
| File | Runs on | Protocol | Typical use |
|------|---------|----------|-------------|
| `server.py` | Your Mac/PC | **stdio** MCP | **Claude Desktop** (spawns local process) |
| `server_http.py` | Railway / VPS | **HTTP** (`/mcp/tools`, `/mcp/call_tool`) | Production callbacks, curl, your own backend |
Claude Desktop does **not** connect to Railway as a native MCP server today. It expects a **local command** (`server.py`), not `https://your-app.railway.app/mcp/call_tool`.
Railwayβs HTTP endpoints are a **REST-style** tool API, not the full MCP stream Claude Desktop uses over stdio (or remote MCP URL/SSE).
### Callbacks must hit the same process as Claude
STK payments and async callbacks are stored **in memory** in the running server process.
| Setup | Works for Claude chat + payment history? |
|-------|------------------------------------------|
| Claude β local `server.py`, `PUBLIC_URL` β **ngrok β same machine** | Yes |
| Claude β local `server.py`, `PUBLIC_URL` β **Railway** | **No** β Safaricom posts to Railway; Claudeβs local process never sees those callbacks |
| No Claude; only Railway `server_http.py` | Yes for HTTP/callbacks; use `/mcp/call_tool`, not Claude Desktop |
| Claude on Railway | Not supported without adding remote MCP transport to `server_http.py` |
**Rule:** The host in `PUBLIC_URL` must be the **same instance** that handles MCP tools and stores payments.
### Recommended setups
#### A β Claude Desktop + local server (recommended for chatting with Claude)
1. Configure Claude with **`server.py`** (see [Integrating with Claude Desktop](#integrating-with-claude-desktop)).
2. Run **ngrok** (or similar): `ngrok http 3000`.
3. Set `PUBLIC_URL` in Claudeβs `env` to the ngrok **HTTPS** URL (or in `.env` if you load it locally).
4. Skip Railway for Claude, or use Railway only for unrelated HTTP integrations.
#### B β Railway only (no Claude Desktop)
1. Deploy via `Procfile` (`gunicorn server_http:app`).
2. Set Railway variables (see [Railway Deployment](#railway-deployment-quick-start)), especially:
```bash
PUBLIC_URL=https://your-service.up.railway.app
```
3. Verify: `curl https://your-service.up.railway.app/health`
4. Call tools with HTTP:
```bash
curl -X POST https://your-service.up.railway.app/mcp/call_tool \
-H "Content-Type: application/json" \
-d '{"name":"get_notification_summary","arguments":{}}'
```
5. Register Safaricom callback URLs using your Railway domain (see [Callback routes](#api-coverage-vs-postman)).
#### C β Railway for callbacks + Claude (common mistake)
Setting Claudeβs local `PUBLIC_URL` to Railway **looks** convenient but breaks payment tracking in Claude: STK/B2C results land on Railway, while `get_recent_payments` reads the local store.
Use **setup A** instead, or **setup B** without Claude until shared storage or remote MCP is implemented.
### Railway env vars (Claude-free deployment)
| Variable | Value |
|----------|--------|
| `DARAJA_*` | Daraja credentials (same as local) |
| `PUBLIC_URL` | `https://<your-service>.up.railway.app` (no trailing slash) |
| `DARAJA_ENV` | `sandbox` or `production` |
| Do **not** set `CALLBACK_HOST` | Railway sets `$PORT`; gunicorn binds `0.0.0.0:$PORT` |
Initiator and IoT variables are optional; see [`.env.example`](.env.example).
## Available Tools
Once configured, Claude can use these tools:
### 1. `stk_push`
Initiate an STK Push payment request.
**Example:**
```
"Send a payment request for 500 KES to 0712345678 for order #INV-001"
```
**Parameters:**
- `phone_number` - Customer phone (254XXXXXXXXX or 07XXXXXXXX)
- `amount` - Amount in KES (minimum 1)
- `account_reference` - Reference like invoice/order number
- `transaction_desc` - Description of transaction
### 2. `stk_query`
Check the status of a payment request.
**Example:**
```
"Check the status of checkout request ws_CO_12345"
```
**Parameters:**
- `checkout_request_id` - ID returned from STK push
### 3. `get_recent_payments`
View recent payment notifications.
**Example:**
```
"Show me the last 10 payments"
```
**Parameters:**
- `limit` - Number of payments to retrieve (default: 10, max: 50)
### 4. `get_payment_details`
Get details of a specific payment.
**Example:**
```
"Show me details for receipt QAR7I8K3LM"
```
**Parameters:**
- `checkout_request_id` - Or -
- `mpesa_receipt` - M-PESA receipt number
### 5. `mark_payment_read`
Mark a notification as read.
**Example:**
```
"Mark payment ws_CO_12345 as read"
```
### 6. `get_notification_summary`
Get summary of all notifications.
**Example:**
```
"How many unread payments do I have?"
```
### 7. `get_callback_status`
Check if callback server is running.
**Example:**
```
"Is the callback server working?"
```
Additional tools (B2C, B2B, C2B, Ratiba, Pull, IMSI, IoT) are listed under [API coverage vs Postman](#api-coverage-vs-postman).
### Phase 1 β B2C, B2B, C2B, balance, status, reversal
| Tool | Description |
|------|-------------|
| `b2c_payment` | Pay customer (B2C) β needs initiator credentials |
| `b2pochi_payment` | B2Pochi wallet payout |
| `b2b_payment` | Business-to-business transfer |
| `c2b_register_url` | Register validation/confirmation URLs |
| `c2b_simulate` | Sandbox C2B payment simulation |
| `account_balance` | Query paybill balance (async callback) |
| `transaction_status` | Query by M-PESA transaction ID |
| `reversal` | Reverse a transaction |
| `get_recent_callbacks` | List B2C/B2B/C2B/balance/reversal callbacks |
| `get_access_token` | Debug OAuth token |
## Callback Setup
Safaricom requires **HTTPS** callback URLs reachable from the internet. Localhost works for simulated webhooks (`python test_daraja.py --callbacks`); use a tunnel or deployed host for real STK/B2C results.
Automated tests can start ngrok for you (`--live`, `--b2c`, `--all`).
### Why You Need ngrok (or Similar Tunneling Service)
**The Problem:**
- M-PESA Daraja API requires **HTTPS callbacks** (not HTTP)
- Safaricom's servers need to reach your callback endpoint from the internet
- Your local development server (`localhost:3000`) is not accessible from the internet
- Firewalls and NAT prevent external access to your local machine
**The Solution:**
ngrok creates a secure tunnel that:
- β
Exposes your local server to the internet via HTTPS
- β
Provides a public URL that Safaricom can reach
- β
Automatically handles SSL/TLS encryption
- β
Allows real-time testing without deploying to production
- β
Shows all incoming requests in a web interface for debugging
**How It Works:**
```
Safaricom Servers β ngrok HTTPS URL β ngrok Tunnel β Your Local Server (localhost:3000)
```
### Local Testing with ngrok
#### 1. Install ngrok
```bash
# macOS
brew install ngrok
# Linux (using snap)
sudo snap install ngrok
# Windows
# Download from https://ngrok.com/download
# Or use Chocolatey: choco install ngrok
# Or download directly from https://ngrok.com/download
```
**Sign up for free:** Visit [ngrok.com](https://ngrok.com/) and create an account to get your authtoken.
#### 2. Authenticate ngrok (First Time Only)
```bash
ngrok config add-authtoken YOUR_AUTHTOKEN_HERE
```
#### 3. Start ngrok Tunnel
```bash
# Forward HTTPS traffic to your local port 3000
ngrok http 3000
```
**Output:**
```
Session Status online
Account Your Name (Plan: Free)
Version 3.x.x
Region United States (us)
Latency 45ms
Web Interface http://127.0.0.1:4040
Forwarding https://abc123.ngrok.io -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
```
**Important:** Copy the `Forwarding` HTTPS URL (e.g., `https://abc123.ngrok.io`)
#### 4. Update Configuration
Update `PUBLIC_URL` in your `.env` file:
```bash
PUBLIC_URL=https://abc123.ngrok.io
```
Or update Claude Desktop config with the ngrok URL.
**Note:** Free ngrok URLs change each time you restart ngrok. For a static URL, upgrade to a paid plan or use ngrok's reserved domains feature.
#### 5. Restart Server
```bash
# Stop the server (Ctrl+C)
# Restart with new PUBLIC_URL
python server.py
```
#### 6. Verify ngrok is Working
**Check ngrok web interface:**
- Open http://localhost:4040 in your browser
- You'll see all requests being forwarded through ngrok
- Useful for debugging callback issues
**Test the tunnel:**
```bash
# Test health endpoint through ngrok
curl https://abc123.ngrok.io/health
# Should return:
# {"status":"healthy","callback_url":"https://abc123.ngrok.io/mpesa/callback",...}
```
#### 7. Keep ngrok Running
**Important:** Keep the ngrok terminal window open while testing. If you close it, the tunnel stops and Safaricom won't be able to reach your callback endpoint.
**Pro Tip:** Run ngrok in a separate terminal or use a process manager like `tmux` or `screen`:
```bash
# Using tmux
tmux new -s ngrok
ngrok http 3000
# Press Ctrl+B then D to detach (keeps running in background)
```
### ngrok Alternatives
If you prefer other tunneling services:
- **Cloudflare Tunnel (cloudflared)** - Free, no account needed for basic use
```bash
cloudflared tunnel --url http://localhost:3000
```
- **localtunnel** - Simple npm-based tunnel
```bash
npx localtunnel --port 3000
```
- **serveo** - SSH-based tunnel (no installation)
```bash
ssh -R 80:localhost:3000 serveo.net
```
However, ngrok is recommended because:
- Most reliable and stable
- Best documentation and community support
- Web interface for request inspection
- Easy to use and configure
### Production Callback Setup
For production, deploy to a server with:
1. **Public HTTPS endpoint** (SSL certificate required)
2. **Static IP or domain name**
3. **Firewall rules** allowing incoming HTTPS traffic
4. **Monitoring and logging**
**Popular options:**
- AWS EC2 with Elastic IP
- DigitalOcean Droplet
- Heroku with SSL
- Google Cloud Run
- **Railway** (recommended - see deployment guide below)
**Example nginx configuration:**
```nginx
server {
listen 443 ssl;
server_name api.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /mpesa/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### Railway Deployment (Quick Start)
Railway is an excellent choice for deploying this MCP server because it:
- β
Provides HTTPS endpoints automatically
- β
Handles SSL certificates
- β
Easy environment variable configuration
- β
Automatic deployments from Git
- β
Free tier available for testing
#### Railway Deployment Steps
1. **Create Railway Account**
- Visit [railway.app](https://railway.app)
- Sign up with GitHub/GitLab
2. **Create New Project**
- Click "New Project"
- Select "Deploy from GitHub repo" (or upload code)
3. **Configure Environment Variables**
In Railway dashboard, add these environment variables:
```
DARAJA_CONSUMER_KEY=your_consumer_key
DARAJA_CONSUMER_SECRET=your_consumer_secret
DARAJA_SHORTCODE=174379
DARAJA_PASSKEY=your_passkey
DARAJA_ENV=sandbox
CALLBACK_PORT=3000
PUBLIC_URL=https://your-app-name.railway.app
```
Note: On Railway, the app is served by gunicorn which binds to `0.0.0.0:$PORT` automatically; you don't need to set `CALLBACK_HOST`.
4. **Deploy**
- Railway will automatically detect `Procfile` and `railway.json`
- The `Procfile` uses `server_http.py` with gunicorn
- Railway will build and deploy automatically
5. **Get Your Public URL**
- Railway provides a public HTTPS URL (e.g., `https://your-app.railway.app`)
- Update `PUBLIC_URL` environment variable with this URL
- Railway will restart the service automatically
6. **Verify Deployment**
```bash
# Test health endpoint
curl https://your-app.railway.app/health
# Should return:
# {"status":"healthy","callback_url":"https://your-app.railway.app/mpesa/callback",...}
```
**Important Notes:**
- Railway automatically provides HTTPS, so no ngrok needed in production
- The `PUBLIC_URL` must match your Railway app URL exactly
- Use `server_http.py` (configured in `Procfile`) for Railway deployments
- Railway handles port binding automatically via `$PORT` environment variable
## Troubleshooting
### Startup
| Issue | Fix |
|-------|-----|
| `Can't assign requested address` (macOS) | Set `CALLBACK_HOST=127.0.0.1` in `.env` |
| Port 3000 in use | `lsof -i :3000` then kill process, or change `CALLBACK_PORT` |
| `.env` not loading | `cp .env.example .env`; verify with `python -c "from dotenv import load_dotenv; load_dotenv(); import os; print(os.getenv('DARAJA_CONSUMER_KEY'))"` |
| Health check fails | Ensure server is running; `curl http://127.0.0.1:3000/health` |
### Common Issues
#### 1. "Failed to get access token"
**Causes:**
- Invalid Consumer Key or Secret
- Wrong environment (sandbox vs production)
- Network connectivity issues
**Solutions:**
```bash
# Test authentication manually
python -c "
from dotenv import load_dotenv
import os, base64, requests
load_dotenv()
key = os.getenv('DARAJA_CONSUMER_KEY')
secret = os.getenv('DARAJA_CONSUMER_SECRET')
auth = base64.b64encode(f'{key}:{secret}'.encode()).decode()
r = requests.get('https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials',
headers={'Authorization': f'Basic {auth}'})
print(r.json())
"
```
#### 2. "Callback server not responding"
**Solutions:**
```bash
# Check if port 3000 is available
lsof -i :3000
# Kill any process using the port
kill -9 <PID>
# Restart server
python server.py
```
#### 3. "MCP server not found in Claude"
**Solutions:**
- Verify config file path is correct
- Use absolute paths in configuration
- Ensure virtual environment Python path is correct
- Check Claude Desktop logs: Help β View Logs
- Restart Claude Desktop completely
#### 4. "No callbacks received"
**Solutions:**
- Verify ngrok is running: `curl https://your-url.ngrok.io/health`
- Check PUBLIC_URL environment variable
- Ensure ngrok URL is HTTPS (required by Safaricom)
- View ngrok request logs: http://localhost:4040
- Check firewall settings
#### 5. "Invalid phone number"
**Solutions:**
- Use format: 254XXXXXXXXX (not +254 or 07XX)
- Sandbox: Use test numbers from Daraja portal
- Remove spaces, dashes, or special characters
### Debug Commands
```bash
# Check server process
ps aux | grep server.py
# Test callback health
curl http://localhost:3000/health
# Test ngrok forwarding
curl https://your-ngrok-url.ngrok.io/health
# View Python errors
tail -f server.log
# Check Claude logs
# macOS: ~/Library/Logs/Claude/
# Windows: %APPDATA%\Claude\logs\
```
### Getting Help
1. Check Daraja API documentation: [developer.safaricom.co.ke/Documentation](https://developer.safaricom.co.ke/Documentation)
2. Review ngrok request inspector: http://localhost:4040
3. Check Claude Desktop logs
4. Verify all environment variables are set correctly
5. Test each component independently
## Security Best Practices
### 1. Credential Management
- β
**Never commit credentials** to version control
- β
Use `.env` files with `.gitignore`
- β
Rotate credentials regularly
- β
Use different credentials for sandbox and production
- β
Store production secrets in secure vaults (AWS Secrets Manager, etc.)
### 2. Network Security
- β
Use HTTPS for all callbacks (required by Safaricom)
- β
Implement webhook signature verification
- β
Restrict callback endpoint to Safaricom IPs
- β
Use firewall rules to limit access
- β
Enable rate limiting
### 3. Application Security
- β
Validate all input data
- β
Sanitize phone numbers and amounts
- β
Implement request logging
- β
Add authentication for sensitive operations
- β
Use environment-specific configurations
### 4. Data Privacy
- β
Don't log sensitive data (PINs, full card numbers)
- β
Mask phone numbers in logs
- β
Implement data retention policies
- β
Comply with data protection regulations
- β
Encrypt data at rest and in transit
### 5. Monitoring
- β
Set up error alerting
- β
Monitor callback success rates
- β
Track failed transactions
- β
Log all API calls
- β
Implement health checks
## Production Deployment
### Pre-deployment Checklist
- [ ] Thoroughly tested in sandbox environment
- [ ] Obtained production credentials from Daraja
- [ ] Set up production server with SSL/TLS
- [ ] Configured firewall and security groups
- [ ] Implemented proper logging and monitoring
- [ ] Set up error alerting
- [ ] Documented deployment process
- [ ] Created backup and recovery plan
- [ ] Tested with small amounts first
- [ ] Configured auto-restart on failure
### Deployment Steps
#### 1. Prepare Server
```bash
# Update system
sudo apt update && sudo apt upgrade -y
# Install Python
sudo apt install python3.10 python3.10-venv -y
# Install nginx (for reverse proxy)
sudo apt install nginx -y
# Install supervisor (for process management)
sudo apt install supervisor -y
```
#### 2. Deploy Application
```bash
# Create application directory
sudo mkdir -p /opt/daraja-mcp
sudo chown $USER:$USER /opt/daraja-mcp
cd /opt/daraja-mcp
# Clone or copy application files
# Set up virtual environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Create production .env
nano .env
# Add production credentials
```
#### 3. Configure Supervisor
Create `/etc/supervisor/conf.d/daraja-mcp.conf`:
```ini
[program:daraja-mcp]
command=/opt/daraja-mcp/venv/bin/python /opt/daraja-mcp/server.py
directory=/opt/daraja-mcp
user=www-data
autostart=true
autorestart=true
stderr_logfile=/var/log/daraja-mcp/error.log
stdout_logfile=/var/log/daraja-mcp/access.log
environment=PRODUCTION="true"
```
#### 4. Configure nginx
Create `/etc/nginx/sites-available/daraja-mcp`:
```nginx
server {
listen 80;
server_name api.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
#### 5. Start Services
```bash
# Reload supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start daraja-mcp
# Enable and restart nginx
sudo ln -s /etc/nginx/sites-available/daraja-mcp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
# Check status
sudo supervisorctl status daraja-mcp
curl https://api.yourdomain.com/health
```
### Monitoring and Maintenance
```bash
# View logs
sudo tail -f /var/log/daraja-mcp/error.log
# Restart service
sudo supervisorctl restart daraja-mcp
# Check resource usage
htop
# Monitor nginx access
sudo tail -f /var/log/nginx/access.log
```
## Postman collection (API reference)
The repo includes Safaricomβs official Postman collection as the source-of-truth checklist for Daraja endpoints:
| | |
|---|---|
| **File** | [`Safaricom APIs.postman_collection.json`](./Safaricom%20APIs.postman_collection.json) |
| **Postman link** | [Safaricom APIs on Postman](https://go.postman.co/collection/13728471-e0965456-7e58-4a62-afff-92e4a84a1adf?source=collection_link) |
| **Sandbox base** | `https://sandbox.safaricom.co.ke` |
| **Production base** | `https://api.safaricom.co.ke` |
Use this collection when adding MCP tools: each request maps to a Daraja API product. Coverage vs this server is summarized in [API coverage](#api-coverage-vs-postman) below.
## API coverage vs Postman
| Status | Count | Notes |
|--------|------:|-------|
| Phase 1 (MCP tools) | 10 | STK, B2C, B2Pochi, B2B, C2B register/simulate, balance, status, reversal |
| Phase 2 (MCP tools) | 2 | M-PESA Ratiba standing orders (paybill, buy goods) |
| Phase 3 (MCP tools) | 3 | Pull API register/query, SFC Verify org lookup |
| Phase 4 (MCP tools) | 2 | IMSI / SWAP CheckATI |
| Phase 5 (MCP tools) | 14 | IoT SIM Portal messaging and SIM operations |
| Helpers | 6 | Payments, async callbacks, summary, access token, callback status |
| Postman coverage | **Complete** | All 35 collection requests have MCP tools |
**Initiator APIs** need `DARAJA_INITIATOR_NAME`, `DARAJA_INITIATOR`, and `DARAJA_SECURITY_CREDENTIAL` in `.env`.
**Callback routes** (prefix with your HTTPS `PUBLIC_URL`):
| Route | Purpose |
|-------|---------|
| `/mpesa/callback` | STK Push |
| `/mpesa/b2c/result`, `/mpesa/b2c/timeout` | B2C / B2Pochi |
| `/mpesa/b2b/result`, `/mpesa/b2b/timeout` | B2B |
| `/mpesa/c2b/validation`, `/mpesa/c2b/confirmation` | C2B |
| `/mpesa/accountbalance/result`, `.../timeout` | Account balance |
| `/mpesa/transactionstatus/result`, `.../timeout` | Transaction status |
| `/mpesa/reversal/result`, `.../timeout` | Reversal |
| `/mpesa/ratiba/callback` | M-PESA Ratiba standing order events |
| `/mpesa/pull/callback` | Pull API transaction notifications |
### Phase 2 β M-PESA Ratiba (standing orders)
| Tool | Description |
|------|-------------|
| `ratiba_standing_order_paybill` | Schedule recurring paybill payments from a customer |
| `ratiba_standing_order_buy_goods` | Schedule recurring till/buy-goods payments |
**Frequency codes:** `1` once, `2` daily, `3` weekly, `4` monthly, `5` bi-monthly, `6` quarterly, `7` half-year, `8` yearly (or aliases like `monthly`, `weekly`).
**Dates:** `start_date` and `end_date` as `yyyymmdd` (e.g. `20260101`).
### Phase 3 β Pull API & SFC Verify
| Tool | Description |
|------|-------------|
| `pull_register` | Register pull callback (`request_type`: `Pull` or `Cancellation`) |
| `pull_query` | Fetch transactions for a datetime range |
| `sfcverify_query_org_info` | Look up party/org by identifier type and value |
**Pull query dates:** `YYYY-MM-DD H:MM:SS` (e.g. `2024-08-04 8:36:00`).
### Phase 4 β IMSI / SWAP
| Tool | Endpoint |
|------|----------|
| `imsi_check_ati` | `POST /imsi/v1/checkATI` |
| `imsi_swap_check_ati` | `POST /imsi/v2/checkATI` |
**Sandbox note:** `404 Not found` usually means the IMSI product is not enabled on your Daraja app, or the test MSISDN is not provisioned for IMSI. Enable **IMSI CheckATI** in the developer portal, confirm sandbox test numbers, and set `DARAJA_TEST_PHONE` if needed.
### Phase 5 β IoT SIM Portal
| Tool | Purpose |
|------|---------|
| `iot_search_messages` | Search messages |
| `iot_filter_messages` | Filter by date/status |
| `iot_delete_message_thread` | Delete thread |
| `iot_get_all_messages` | List messages |
| `iot_send_single_message` | Send SMS to SIM |
| `iot_delete_message` | Delete message by ID |
| `iot_all_sims` | List SIMs in VPN group |
| `iot_query_lifecycle_status` | SIM lifecycle |
| `iot_query_customer_info` | Customer info |
| `iot_sim_activation` | Activate SIM |
| `iot_get_activation_trends` | Activation trends |
| `iot_rename_asset` | Rename asset |
| `iot_get_location_info` | Location info |
| `iot_suspend_unsuspend_sub` | Suspend/unsuspend |
## API Reference
### Daraja API Endpoints
#### Authentication
```
GET https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials
Authorization: Basic <base64(consumer_key:consumer_secret)>
```
#### STK Push
```
POST https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest
Authorization: Bearer <access_token>
Content-Type: application/json
{
"BusinessShortCode": "174379",
"Password": "<base64(shortcode+passkey+timestamp)>",
"Timestamp": "20240108143022",
"TransactionType": "CustomerPayBillOnline",
"Amount": 100,
"PartyA": "254712345678",
"PartyB": "174379",
"PhoneNumber": "254712345678",
"CallBackURL": "https://your-domain.com/callback",
"AccountReference": "Order123",
"TransactionDesc": "Payment for Order123"
}
```
#### STK Query
```
POST https://api.safaricom.co.ke/mpesa/stkpushquery/v1/query
Authorization: Bearer <access_token>
Content-Type: application/json
{
"BusinessShortCode": "174379",
"Password": "<base64(shortcode+passkey+timestamp)>",
"Timestamp": "20240108143022",
"CheckoutRequestID": "ws_CO_08012024123456789"
}
```
### MCP Server Endpoints
#### Health Check
```
GET http://localhost:3000/health
Response:
{
"status": "healthy",
"environment": "sandbox",
"callback_url": "http://localhost:3000/mpesa/callback",
"callback_urls": { "stk": "...", "b2c_result": "...", "ratiba": "...", "pull": "..." },
"unread_payments": 0,
"unread_callbacks": 0
}
```
#### M-PESA Callback
```
POST http://localhost:3000/mpesa/callback
Content-Type: application/json
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_08012024123456789",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{"Name": "Amount", "Value": 100},
{"Name": "MpesaReceiptNumber", "Value": "QAR7I8K3LM"},
{"Name": "TransactionDate", "Value": 20240108143022},
{"Name": "PhoneNumber", "Value": 254712345678}
]
}
}
}
}
```
## Project Structure
```
daraja-mcp/
βββ venv/ # Virtual environment (not in git)
βββ daraja_core.py # Shared Daraja client, callbacks, MCP tools
βββ daraja_iot.py # IMSI and IoT SIM Portal APIs (Phases 4β5)
βββ server.py # MCP server for local Claude Desktop (stdio)
βββ server_http.py # MCP server for cloud deployment (HTTP)
βββ test_daraja.py # Unified test suite (see Testing)
βββ .github/workflows/ci.yml # CI: platform, webhooks, gunicorn, optional live
βββ Procfile # Railway: gunicorn server_http:app
βββ railway.json # Railway platform configuration
βββ .env.example # Environment template (copy to .env)
βββ requirements.txt # Python dependencies
βββ README.md # This file
βββ Safaricom APIs.postman_collection.json # Postman reference (optional local copy)
```
| File | Use |
|------|-----|
| `server.py` | Claude Desktop (stdio) |
| `server_http.py` | Production / HTTP MCP + callbacks |
| `daraja_core.py` | Daraja client, callbacks, core MCP tools |
| `daraja_iot.py` | IMSI + IoT SIM Portal tools |
| `test_daraja.py` | All automated tests |
## Contributing
Contributions are welcome! Please follow these guidelines:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Development Setup
Follow [Installation](#installation), then run `python test_daraja.py --platform` before opening a PR.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- [Safaricom Daraja API](https://developer.safaricom.co.ke/) - M-PESA API platform
- [Anthropic MCP](https://modelcontextprotocol.io/) - Model Context Protocol
- [Flask](https://flask.palletsprojects.com/) - Web framework for callbacks
- [ngrok](https://ngrok.com/) - Secure tunneling for local development
## Support
- **Documentation:** [developer.safaricom.co.ke/Documentation](https://developer.safaricom.co.ke/Documentation)
- **Daraja Support:** support@safaricom.co.ke
- **MCP Documentation:** [modelcontextprotocol.io](https://modelcontextprotocol.io/)
- **Issues:** [GitHub Issues](https://github.com/mboya/daraja-mcp/issues)
## Changelog
### v1.0.0 (2024-01-08)
- Initial release
- STK Push implementation
- Real-time callback handling
- Payment notification storage
- Automated testing suite
- Claude Desktop integration
- Comprehensive documentation
---
**Made with β€οΈ for the M-PESA ecosystem**
For questions or support, please open an issue on GitHub or contact the maintainers.This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues