Skip to main content
Glama
mboya

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.