Skip to main content
Glama

EuiccBridge

A CLI (and MCP server) for managing eSIM profiles, SIM SMS, and cellular modem diagnostics on Linux — local-first, scriptable, and safe to point an AI agent at.

  • eSIM lifecycle: list, download, enable, disable, delete, switch Profiles

  • SMS: send/receive over AT/PDU directly, no ModemManager dependency

  • Diagnostics: registration, APN/PDP, interface, and connectivity checks, plus a doctor command for environment pre-flight checks

  • Safety by default: confirmations, redaction, allowlists, audit logging

  • One core library, three fronts: CLI today, MCP server for AI tooling, Web API planned — all calling the same code, not shelling out to each other

Built on top of proven lower-level tools (lpac, libqmi/qmicli, standard Linux networking and AT commands) rather than reimplementing GSMA RSP, QMI, or AT protocol stacks from scratch.

Goals

  • Keep eSIM activation codes, Profile operations, SMS, and SIM identifiers under local, auditable control.

  • Use a Linux VM or small Linux host as the hardware controller.

  • Expose one shared core library through CLI first, then MCP, then optional Web API/WebUI.

  • Make every dangerous operation explicit, confirmed, logged safely, and reversible where possible.

Related MCP server: ESP RainMaker MCP Server

Non-Goals

  • Do not implement GSMA RSP, APDU, QMI, MBIM, IMS, ePDG, IPsec, or SIP stacks.

  • Do not promise VoWiFi calling or SMS over WiFi Calling.

  • Do not build a multi-tenant/commercial traffic pool, IP-rotation-for-hire service, or Bot platform. (A single-SIM, self-use SOCKS5/HTTP proxy bound to whichever cellular interface is currently active is a different, smaller thing — see Roadmap. The line being drawn is multi-tenant/pool vs. self-use, not "proxy vs. no proxy".)

Architecture

euiccbridge core library
  |-- CLI          first deliverable, machine-readable JSON
  |-- MCP server   AI/tooling entrypoint, same core library
  `-- Web API      later layer, same core library

The CLI is an entrypoint, not the business logic layer. Web API and MCP call the same Python core library directly rather than shelling out to the CLI.

Core modules:

  • core.device / core.device_paths: AT port / QMI device / cellular interface discovery, AT command execution, modem status parsing.

  • core.qmi: QMI device discovery and status via qmicli.

  • core.esim: lpac wrapper for eUICC/Profile operations (info, list, download, enable, disable, delete, switch).

  • core.gsm_pdu: self-contained GSM 03.40/03.38 SMS PDU codec (no ModemManager dependency — see "SMS backend" below).

  • core.sms / core.sms_store: SMS sync/list/send, backed by a local SQLite store that's the decoupling point for downstream consumers.

  • core.data: APN, registration, PDP, interface, and public-IP diagnostics.

  • core.doctor: read-only environment pre-flight checks.

  • core.config: config loading + SMS allowlist management.

  • core.security: confirmation, masking, allowlist, rate limits, audit.

SMS backend: AT/PDU-direct, not ModemManager

SMS is implemented from scratch against AT+CMGF/AT+CPMS/AT+CMGL/AT+CMGS in PDU mode, not via ModemManager/mmcli. This was a deliberate choice: a ModemManager instance and this project's own direct QMI/AT usage can end up contending for the same control channel, and comparable closed-source modem managers in this space document the same conflict and instruct disabling ModemManager rather than depending on it. Going AT/PDU-direct avoids adding a persistent background service and keeps this project's "auditable, local-first, no unnecessary daemons" posture consistent across eSIM, SMS, and diagnostics.

Relationship with EasyLPAC

EasyLPAC is a mature open-source lpac GUI frontend. EuiccBridge uses it as a reference and optional manual fallback, not as a runtime dependency.

Useful parts borrowed from EasyLPAC's design: lpac discovery behavior (same directory, system PATH, then /usr/bin/lpac), eSIM operation flow and user-facing error handling, eUICC manufacturer/certificate/SAS display ideas, and profile-notification handling (process after operations by default, with a setting to disable it).

EuiccBridge still calls lpac directly because it needs machine-readable CLI/MCP/Web API output, SMS, modem status, APN/PDP diagnosis, allowlists, audit, and redaction — those are service/controller concerns, not GUI concerns.

v1 Scope

  • Device discovery: AT port, QMI device, cellular interface, IMEI, ICCID, IMSI, subscriber number (when the SIM has one written to it — many data-only/IoT SIMs don't).

  • Radio state: operator, PLMN, registration state, signal, LTE band, APN, PDP, interface IP.

  • eSIM management: EID, Profile list, download, enable, disable, delete, switch (one-step disable-current + enable-target).

  • SMS: sync/list/send, allowlist, daily limit, safe audit, incremental (--since-id) reads for downstream consumers.

  • Data diagnosis: distinguish no registration, empty APN, inactive PDP, no interface IP, and public connectivity failure.

  • doctor: read-only pre-flight checks (AT/QMI reachability, lpac presence and backend health, device-node integrity, QMI raw-ip mode).

  • CLI and MCP: both use the same core library and default to redacted output.

v1 does not attempt: WiFi Calling SMS, VoWiFi outbound calls, IMS/ePDG/ IPsec/SIP implementation, or compatibility with any specific closed-source modem manager's API.

v1 CLI

The CLI supports JSON output so MCP, Web API, and scripts can consume the same result shape.

python3 -m euiccbridge doctor --json
python3 -m euiccbridge device list --json
python3 -m euiccbridge device status --json
python3 -m euiccbridge esim info --json
python3 -m euiccbridge esim list --json
python3 -m euiccbridge esim download --activation-code-stdin --confirm --json
python3 -m euiccbridge esim enable --iccid <iccid> --confirm --json
python3 -m euiccbridge esim disable --iccid <iccid> --confirm --json
python3 -m euiccbridge esim switch --iccid <iccid> --confirm --json
python3 -m euiccbridge esim delete --iccid <iccid> --confirm --force --json
python3 -m euiccbridge sms sync --json
python3 -m euiccbridge sms list --json
python3 -m euiccbridge sms send --to <number> --text <text> --confirm --json
python3 -m euiccbridge sms allowlist add --number <number> --json
python3 -m euiccbridge data diagnose --apn <apn> --json
python3 -m euiccbridge data test --interface <iface> --json

esim download prompts for the activation code on stdin (paste + Enter, no Ctrl+D needed) rather than taking it as an argument; add -v/--verbose to stream lpac's progress live instead of waiting for one final JSON blob.

Multiple devices

If more than one physical modem is attached, every command above needs a top-level --device <label> (before the subcommand) to say which one to target:

python3 -m euiccbridge device list --json
python3 -m euiccbridge --device 5-3 device status --json
python3 -m euiccbridge --device 5-3 esim list --json
python3 -m euiccbridge --device 5-3 sms send --to <number> --text <text> --confirm --json

device list shows each attached device's label (its USB serial number if the hardware reports one, otherwise its USB bus-port path e.g. 5-3 -- some rebranded/OEM modules don't report a serial). Devices are identified by correlating AT port + QMI device + network interface via Linux USB topology (sysfs), not by enumeration order, since /dev/ttyUSB* numbering isn't stable across reconnects.

Omitting --device works exactly as before when exactly one device is attached (nothing changes for single-device setups). With more than one attached and no --device given, every command fails explicitly with "N devices found (...) but none selected" rather than silently guessing -- auto-discovering AT port and QMI device independently, with no guarantee they picked the same physical modem, previously produced a device status response silently mixing fields from two different modems together (see docs/TROUBLESHOOTING.md). Set default_device in config to avoid passing --device on every call.

SMS messages are tagged with the device they were sent/received through, and the daily send limit (sms_daily_limit) is tracked per device, not globally -- two modems each get their own budget.

Safety Rules

  • eSIM activation codes are read from stdin/interactive input, never shell arguments, and are never written to logs, audit files, tracebacks, or JSON responses.

  • eSIM write operations refuse to run while a conflicting modem-manager service is active.

  • Profile deletion requires both --confirm and --force.

  • SMS sending requires explicit confirmation and an allowlisted recipient, plus a configurable daily send limit.

  • EID, ICCID, IMSI, IMEI, phone numbers, activation codes, and SMS bodies are redacted by default in every command's output; an explicit opt-out flag (audited when used) reveals it per-call — --reveal on esim/sms commands, --no-redact on device status.

  • MCP dangerous tools require confirm: true.

  • Services do not listen on public interfaces.

Compatibility

Validated against a Quectel EC25/EG25-class USB modem (AT + QMI) paired with an eUICC exposing a GSMA RSP2 Root CI1 certificate, on a Debian Linux controller. The design is not hardcoded to that one device.

Compatibility tiers:

  • Tier 1: Quectel EC25/EG25/EC20-like AT + QMI modems.

  • Tier 2: Other cellular modems exposing standard AT registration/SMS commands.

  • Tier 3: lpac-supported eUICC readers, eSIM management only.

Tiers 1 and 2 need only a SIM (physical or eSIM) to work — device status, signal, registration, data diagnosis, and SMS all operate at the AT/QMI layer and don't care what kind of SIM is inserted. Tier 3 (the esim command family) specifically needs an actual eUICC with a GSMA ISD-R applet; a plain (non-eUICC) SIM makes every esim command fail, and doctor's lpac_backend check reports this distinctly (warn, "no eUICC detected") rather than treating it as a broken environment.

Not supported: phone built-in eSIM interfaces, native macOS/Windows USB modem management, generic VoWiFi calling/SMS.

Roadmap

  • v1: local CLI + MCP, device status, eSIM management, SMS, data diagnosis, doctor, multi-device selection (--device, per-device SMS attribution and daily limits) — complete, every item unit-tested and real-hardware validated (including with two physical modems attached simultaneously) except eSIM download's success path, which is validated but depends on network conditions outside this project's control (see docs/TROUBLESHOOTING.md).

  • v1.1: scheduled keepalive SMS with allowlist, limits, retries, and reports.

  • v1.2: local Web API backed by the same core library.

  • v1.3: lightweight WebUI for status, eSIM, SMS, and diagnostics.

  • v1.x: self-use single-card SOCKS5/HTTP proxy bound to whichever cellular interface is currently active (explicitly not the multi-tenant/pool feature described in Non-Goals).

  • v2: human-friendly device labels/nicknames (beyond the raw serial/USB path), per-device queues, and cross-device aggregation views.

  • v3: VoWiFi/IMS status diagnostics and log collection only.

  • v4: advanced data routing and per-interface outbound testing.

Near-term, more granular candidates: +CMTI real-time SMS notification (deferred — needs a persistent AT-port listener, a different execution model than the rest of this project; a periodic sms sync + notification hook covers most of the same need at much less complexity), optional GSM 7-bit encode path for outgoing SMS (currently UCS2-only by design), install/ dependency guidance per distro, an uqmi backend for OpenWrt.

Status

All v1 Scope items are implemented, unit-tested, and validated against real hardware (eSIM info/list/download/enable/disable/delete/switch; SMS sync/ list/send including multi-part concatenation and non-ASCII content; device status; data diagnosis; doctor).

Two real bugs were found and fixed during hardware validation that are worth knowing about if you're bringing up your own hardware — see docs/TROUBLESHOOTING.md for the full symptom/cause/fix writeups:

  • USB re-enumeration can corrupt AT-port device nodes into plain regular files; doctor's device_node_types check catches this directly.

  • QMI WWAN interfaces in Ethernet/802.3 mode can fail to route reliably on some hardware; doctor's qmi_raw_ip check flags this, and scripts/80-euiccbridge-qmi-rawip.rules is a working fix you can adapt to your modem's VID:PID.

Current Runtime Notes

  • A conflicting modem-manager service (if one is installed) should remain stopped while running EuiccBridge — it will contend for the same QMI control channel and AT port.

  • lpac is not vendored in this repository; install it separately and point lpac_path at it.

  • sms_allowed_numbers can be managed without hand-editing the config file: euiccbridge sms allowlist add/remove/list --number <number> --json.

  • Scheduled SMS sync, single device: scripts/euiccbridge-sms-sync.service + .timer (copy both to /etc/systemd/system/, systemctl daemon-reload, systemctl enable --now euiccbridge-sms-sync.timer). Runs euiccbridge sms sync every 30s by default; adjust OnUnitActiveSec in the .timer file to match sms_poll_interval_seconds if you change that config value.

  • Scheduled SMS sync, multiple devices: the plain service above has no --device, so with more than one physical device attached it fails every run ("N devices found but none selected") instead of silently syncing the wrong one. Use the templated units instead -- scripts/euiccbridge-sms-sync@.service + @.timer -- one instance per device, named after its device list label:

    systemctl enable --now euiccbridge-sms-sync@5-3.timer
    systemctl enable --now euiccbridge-sms-sync@5-2.timer

    Since device labels can be a USB bus-port path (when the hardware reports no serial number), replugging a device into a different port changes its label -- re-check device list and re-enable the timer under the new label if that happens.

  • scripts/sms_notify.py is a separate, optional consumer script (deliberately not part of the euiccbridge package — it's policy/orchestration, reading the stable CLI JSON contract, not a capability) — see scripts/sms_notify.config.example.json for its config shape. It runs with --reveal, so point hook_command/webhook_url at a target you trust with real numbers/text. Set device in its config if more than one physical device is attached (same label as --device) -- run one sms_notify.py instance, each with its own cursor_path and device, per device you want notifications for.

  • scripts/sms_email_notify.py is a ready-made hook_command target that emails each new message via SMTP (see scripts/sms_email_notify.config.example.json -- use an app-specific SMTP password, not your real account password, and keep that config file root-readable only). Wire it in from sms_notify.config.json: "hook_command": "python3 scripts/sms_email_notify.py /path/to/email_notify.config.json". Run sms_notify.py on its own schedule, separate from euiccbridge-sms-sync[@.]timer (which only pulls messages off the modem, it doesn't dispatch notifications) -- there's no packaged systemd unit for it yet, add one following the euiccbridge-sms-sync.timer pattern.

  • The cellular data interface commonly needs QMI raw-ip mode to route real traffic reliably — see scripts/80-euiccbridge-qmi-rawip.rules and docs/TROUBLESHOOTING.md.

Example Config

Create euiccbridge.config.json in this directory, or /opt/euiccbridge/config.json.

Minimal single-modem config:

{
  "lpac_path": "/opt/euiccbridge/bin/lpac",
  "lpac_apdu": "qmi",
  "lpac_http": "curl",
  "auto_process_notifications": false
}

Pinned config for multi-device or debugging:

{
  "at_port": "/dev/ttyUSB2",
  "qmi_device": "/dev/cdc-wdm0",
  "interface": "wwp0s3u3i4",
  "lpac_path": "/opt/euiccbridge/bin/lpac",
  "lpac_apdu": "qmi",
  "lpac_http": "curl",
  "lpac_qmi_device": "/dev/cdc-wdm0",
  "lpac_at_device": "/dev/ttyUSB2",
  "sms_allowed_numbers": ["10086"],
  "sms_daily_limit": 5,
  "sms_storage": "ME",
  "sms_db_path": "/var/lib/euiccbridge/sms.db",
  "sms_poll_interval_seconds": 30,
  "auto_process_notifications": true,
  "lpac_commands": {
    "info": ["chip", "info"],
    "list": ["profile", "list"],
    "download": ["profile", "download", "-a", "{activation_code}"],
    "enable": ["profile", "enable", "{iccid}"],
    "disable": ["profile", "disable", "{iccid}"],
    "delete": ["profile", "delete", "{iccid}"],
    "notifications": ["notification", "process"]
  }
}

The lpac_commands section exists because lpac command flags can vary by build/distribution. Calibrate it on your target host with lpac --help and lpac profile --help before using write operations.

Troubleshooting

See docs/TROUBLESHOOTING.md for real-hardware failure modes and fixes (corrupted AT-port device nodes, QMI raw-ip mode, DNS injection affecting specific domains on some networks).

License

MIT — see LICENSE.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Cat0513/euiccbridge'

If you have feedback or need assistance with the MCP directory API, please join our Discord server