SMS.ir MCP server
by ali-toghiani
README.md
# SMS.ir MCP server
A local [Model Context Protocol](https://modelcontextprotocol.io) server that
exposes a **curated, safety-gated** set of tools for the
[SMS.ir](https://sms.ir) Panel V2 API. Built with Python + [FastMCP](https://github.com/jlowin/fastmcp).
- **stdio** transport for Codex / Claude Desktop / Claude Code
- **streamable HTTP** transport for local development and testing
- Read operations work out of the box; **every send is billable and blocked by
default** behind a confirmation flag *and* a server-side kill switch.
- Phone numbers, message text, API keys and OTP codes are masked in logs.
Built from the *SMS.ir Panel V2* Postman collection (not included in this repo —
it embeds a real API key). The normalized API description lives in
[`docs/API.md`](docs/API.md) and [`docs/openapi.yaml`](docs/openapi.yaml).
---
## 1. Setup
Requires Python 3.10+ (developed and tested on CPython 3.12).
```powershell
cd C:\Users\Kasra\Documents\sms.ir-mcp
# create the project-local virtual environment
py -3.12 -m venv .venv
# install runtime deps (pinned)
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
# ...or install with the package + dev/test extras
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
```
## 2. Configuration
All configuration comes from **environment variables**. For local use, copy the
example env file and fill it in — it is git-ignored and never committed:
```powershell
copy .env.example .env
notepad .env
```
| Variable | Required | Default | Purpose |
|---|---|---|---|
| `SMSIR_API_KEY` | **yes** | – | SMS.ir Panel API key, sent as the `X-API-KEY` header |
| `SMSIR_DEFAULT_LINE_NUMBER` | no | – | Fallback sender line for send tools |
| `SMSIR_ALLOW_SEND` | no | `false` | **Kill switch.** Must be `true` for any real send to leave the process |
| `SMSIR_BASE_URL` | no | `https://api.sms.ir` | API base URL (host-allowlisted) |
| `SMSIR_ALLOW_CUSTOM_BASE_URL` | no | `false` | Allow a non-`api.sms.ir` host (local mocks only) |
| `SMSIR_TIMEOUT_SECONDS` | no | `15` | Per-request timeout |
| `SMSIR_MAX_RETRIES` | no | `2` | Retries for transient failures (429 / 5xx / network) |
| `SMSIR_RATE_LIMIT_PER_MINUTE` | no | `60` | Client-side rate limit |
| `SMSIR_MAX_PAGE_SIZE` | no | `200` | Upper bound accepted for `page_size` |
| `SMSIR_LOG_LEVEL` | no | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |
| `SMSIR_ENV_FILE` | no | `./.env` | Path to the env file to auto-load |
Real environment variables always override values from the env file.
## 3. Running
```powershell
# stdio (what MCP clients launch)
.\.venv\Scripts\python.exe -m sms_ir_mcp --transport stdio
# streamable HTTP for local testing (http://127.0.0.1:8000/mcp)
.\.venv\Scripts\python.exe -m sms_ir_mcp --transport http --host 127.0.0.1 --port 8000
```
For a quick connectivity + auth check that never spends credit, call the
`health_check` tool (or `get_balance`) from any connected client — both are
`GET /v1/credit` under the hood.
## 4. Tools
Read-only tools are always available. **Write tools require `confirm=true`
_and_ `SMSIR_ALLOW_SEND=true`**; the destructive tool requires `confirm=true`.
| Tool | Kind | API | Description |
|---|---|---|---|
| `get_balance` | read | `GET /v1/credit` | Remaining SMS credit |
| `list_lines` | read | `GET /v1/line` | Sender line numbers / virtual numbers |
| `get_message_report` | read | `GET /v1/send/{id}` | Delivery report/status for one sent message |
| `get_pack_report` | read | `GET /v1/send/pack/{packId}` | Per-recipient results for a bulk pack (paginated) |
| `list_sent_messages` | read | `GET /v1/send/live` · `/archive` | Sent messages, `scope=today\|archive` |
| `list_sent_packs` | read | `GET /v1/send/pack` · `/archive/pack` | Bulk packs, `scope=today\|archive` |
| `list_inbound_messages` | read | `GET /v1/receive/latest` · `/live` · `/archive` | Inbound messages, `scope=latest\|today\|archive` |
| `extract_latest_otp` | read | `GET /v1/receive/latest` | Newest inbound message containing a parseable one-time code (heuristic) |
| `health_check` | read | `GET /v1/credit` | Reachability + auth check, never billable; also returns effective config |
| `reload_config` | admin | – | Re-read `.env` / env vars without restarting the server (e.g. after flipping `SMSIR_ALLOW_SEND`); sends nothing |
| `send_sms` | **billable** | `POST /v1/send/bulk` | One text to one or more recipients |
| `send_verification_code` | **billable** | `POST /v1/send/verify` | Templated OTP/verification message |
| `send_personalized_sms` | **billable** | `POST /v1/send/likeToLike` | A distinct text per recipient |
| `cancel_scheduled_send` | **destructive** | `DELETE /v1/send/scheduled/{packId}` | Cancel a not-yet-sent scheduled pack |
Every tool returns `{"ok": true, "data": …, …}` on success or
`{"ok": false, "error": {"code": …, "message": …}}` on failure. Error codes:
`config_error`, `validation_error`, `confirmation_required`, `send_disabled`,
`auth_error`, `rate_limited`, `transient_error`, `api_error`, `internal_error`.
### Examples
```jsonc
// check balance
get_balance() -> {"ok": true, "data": {"credit": 45210}}
// read the latest OTP received on a given number
extract_latest_otp({"mobile": "9821000"})
-> {"ok": true, "data": {"otp": "834122", "matched": true, "from": "*****1000", ...}}
// attempt a send without confirming -> refused, nothing sent
send_sms({"message_text": "Hi", "mobiles": ["09121234567"]})
-> {"ok": false, "error": {"code": "confirmation_required", ...}}
// confirmed send, but kill switch still off -> refused, nothing sent
send_sms({"message_text": "Hi", "mobiles": ["09121234567"], "confirm": true})
-> {"ok": false, "error": {"code": "send_disabled", ...}}
// edited .env to set SMSIR_ALLOW_SEND=true -> apply it without restarting
reload_config()
-> {"ok": true, "data": {"config": {"allow_send": true, ...}, "changed": ["allow_send"]}}
// with SMSIR_ALLOW_SEND=true AND confirm=true -> actually sends (billable)
send_sms({"message_text": "Hi", "mobiles": ["09121234567"],
"line_number": "30007732000000", "confirm": true})
-> {"ok": true, "data": {"packId": "…", "messageIds": [123], "cost": 1.0}, "recipients": 1}
```
## 5. Safety model
- **Billable operations** (`send_sms`, `send_verification_code`,
`send_personalized_sms`) need **both**:
1. `confirm=true` in the tool call, and
2. `SMSIR_ALLOW_SEND=true` in the server environment.
With the kill switch off, a confirmed call still sends nothing.
- **Destructive operation** (`cancel_scheduled_send`) needs `confirm=true`.
- **No arbitrary base URLs**: only `api.sms.ir` is accepted unless
`SMSIR_ALLOW_CUSTOM_BASE_URL=true`. HTTPS is enforced.
- **No header injection**: callers cannot set request headers; only typed,
validated fields are forwarded.
- **Timeouts + bounded retries + client-side rate limiting** on every request.
- **Masking**: API keys, phone numbers, message bodies and OTP values are masked
in log output.
- **No admin endpoints**: only the operations in the Postman collection are
exposed; nothing for account/config management.
## 6. Client registration
Your real API key goes in `.env` in this folder — **never** in a client config
file. Each config below only points the client at this server and its `.env`.
### Codex CLI (installed)
```powershell
codex mcp add sms-ir `
--env SMSIR_ENV_FILE=C:\Users\Kasra\Documents\sms.ir-mcp\.env `
-- C:\Users\Kasra\Documents\sms.ir-mcp\.venv\Scripts\python.exe -m sms_ir_mcp --transport stdio
codex mcp list # sms-ir should appear
codex mcp get sms-ir
```
Manual equivalent: [`docs/codex_config.example.toml`](docs/codex_config.example.toml).
### Claude Code (installed)
This repo ships a project-scoped [`.mcp.json`](.mcp.json). Open Claude Code in
this directory and approve the `sms-ir` server when prompted:
```powershell
cd C:\Users\Kasra\Documents\sms.ir-mcp
claude
/mcp # shows sms-ir and its tools
```
To register it at user scope instead:
```powershell
claude mcp add sms-ir --scope user `
--env SMSIR_ENV_FILE=C:\Users\Kasra\Documents\sms.ir-mcp\.env `
-- C:\Users\Kasra\Documents\sms.ir-mcp\.venv\Scripts\python.exe -m sms_ir_mcp --transport stdio
```
### Claude Desktop (not installed)
When installed, merge [`docs/claude_desktop_config.example.json`](docs/claude_desktop_config.example.json)
into `%APPDATA%\Claude\claude_desktop_config.json` (back it up first; keep other
servers).
## 7. Development
```powershell
.\.venv\Scripts\python.exe -m ruff check src tests
.\.venv\Scripts\python.exe -m ruff format --check src tests
.\.venv\Scripts\python.exe -m pytest
```
Tests cover request construction, auth header, envelope parsing, error
normalization, retries/rate-limiting, argument validation, masking, OTP
extraction, mocked integration for every tool (using the Postman example
payloads), OpenAPI-vs-collection consistency, and MCP tool discovery.
## 8. First live test (after you provide a credential)
Nothing in this repo has made a billable call. To run the **first real send**,
which you explicitly authorize:
1. Put your key in `.env`:
```
SMSIR_API_KEY=<your real key>
SMSIR_DEFAULT_LINE_NUMBER=<your approved line>
SMSIR_ALLOW_SEND=true
```
2. Verify connectivity without spending anything — from a connected client call
`health_check` (or `get_balance`).
3. Then, and only then, make the first billable call. Exact tool call:
```json
send_sms({
"message_text": "SMS.ir MCP test",
"mobiles": ["<your own mobile>"],
"line_number": "<your approved line>",
"confirm": true
})
```
Codex phrasing: *"Use the sms-ir server's send_sms tool to send 'SMS.ir MCP
test' to <your own mobile> from line <line>, with confirm true."*
## 9. Troubleshooting
| Symptom | Cause / fix |
|---|---|
| `config_error: SMSIR_API_KEY is not set` | No key in env or `.env`; check `SMSIR_ENV_FILE` path the client passes |
| `auth_error` on every call | Wrong/rotated key, or key lacks Panel API access |
| `send_disabled` | `SMSIR_ALLOW_SEND` is not `true` in the server environment |
| Edited `.env` but nothing changed | The server reads config once at startup. Call `reload_config`, or restart the MCP client so it respawns the server |
| `confirmation_required` | Re-call the tool with `"confirm": true` |
| `validation_error: Invalid mobile number` | Use 10–15 digits, optional leading `+` |
| `rate_limited` | Client-side limiter tripped; raise `SMSIR_RATE_LIMIT_PER_MINUTE` or slow down |
| `transient_error` | Network/5xx after retries; check connectivity and SMS.ir status |
| Client shows no tools | Wrong `command` path in the client config; point at `.venv\Scripts\python.exe` |
| `api_error` with `api_status` | SMS.ir rejected the request; `message` carries their reason |
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues