AI agents need real phone numbers — not VoIP — and the VirtualSMS MCP server gives Anthropic Claude direct access to carrier-grade SIM activations without leaving the tool-use layer. This post covers four production-ready patterns for Claude + VirtualSMS MCP workflows, from single-shot OTP retrieval to full account-creation pipelines with retry reasoning. Whether you’re building with Claude Desktop, Claude Code, or the Anthropic API directly, these patterns work out of the box.
MCP Setup: Claude Code, Claude Desktop, and Claude API
The VirtualSMS MCP server installs as a Node package and exposes 18 tools to any MCP-compatible client. Setup is identical across all three Claude surfaces — only the config file location differs.
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows):
{
"mcpServers": {
"virtualsms": {
"command": "npx",
"args": ["-y", "virtualsms-mcp@latest"],
"env": {
"VIRTUALSMS_API_KEY": "vsms_live_..."
}
}
}
}
Claude Code (.claude/settings.json in project root, or global user settings):
{
"mcpServers": {
"virtualsms": {
"command": "npx",
"args": ["-y", "virtualsms-mcp@latest"],
"env": {
"VIRTUALSMS_API_KEY": "vsms_live_..."
}
}
}
}
Anthropic API (Python SDK with MCP tool injection — see Pattern 1 below for the full code block).
Restart Claude Desktop or start a new Claude Code session after editing the config. Claude will immediately show the VirtualSMS tools in its available tool list — you can verify by asking: “What VirtualSMS tools do you have available?”
Which transport to use?
MCP supports three transports: stdio, StreamableHTTP, and SSE. The npx virtualsms-mcp command runs stdio by default, which is the right choice for local development and Claude Code sessions. For production agents deployed on a server, use StreamableHTTP. The Transport Comparison section below has the full breakdown.
Why Claude + MCP for Phone Verification
The Model Context Protocol gives Claude structured, typed access to external tools without you writing any glue code. When you add VirtualSMS MCP to Claude’s config, it can call request_number, get_sms, cancel_order, and 15 other tools directly — the same way it calls a calculator or a web search. Claude handles the sequencing, polling logic, and error interpretation autonomously.
The alternative — wrapping the VirtualSMS REST API in a custom tool definition and passing it to client.messages.create(tools=[...]) — works, but requires you to maintain the tool schema, format responses, and handle every edge case explicitly. MCP shifts that to the server. For Claude-native workflows, MCP is the right abstraction.
The second reason is line-type quality. Most agent builders hit the same wall when they first try phone verification: the VoIP pool their provider uses gets flagged by WhatsApp’s and Telegram’s inbound number checks before the OTP even arrives. Read the full breakdown of why VoIP numbers fail line-type checks. VirtualSMS routes to real carrier-issued SIM cards — numbers that return mobile in Twilio Lookup V2 and Telesign PhoneID, because they are mobile. Platform line-type detection doesn’t fire. The OTP arrives. The agent continues.
The platform records a consistently high success rate on real-SIM orders. That number matters at pipeline scale: a 5% failure rate across 1,000 verifications is 50 manual retries. A 40% failure rate — what VoIP numbers produce on strict platforms — is a broken pipeline.
VirtualSMS MCP vs Twilio MCP, Vapi, Telnyx, and AgentPhone
| MCP Server | Number Type | Works for WhatsApp? | Works for Telegram? | Price/activation |
|---|---|---|---|---|
| VirtualSMS MCP | Real carrier SIM | Yes | Yes | from $0.05 |
| Twilio MCP | VoIP pool | No (line-type rejected) | No | varies |
| Vapi | VoIP/voice | No (call-focused) | No | call-focused |
| AgentPhone | VoIP | No | No | varies |
| Telnyx MCP | Real numbers | Partial | Partial | higher volume pricing |
Twilio MCP is the most common alternative developers reach for. The issue isn’t Twilio’s API quality — it’s the number inventory. Twilio’s standard pool is virtual numbers classified as Non-Fixed VoIP. WhatsApp, Telegram verification, Binance, and most 2FA-heavy platforms explicitly reject these at the line-type check level. VirtualSMS routes to dedicated SIM cards running on carriers like Vodafone, O2, T-Mobile, EE, and Lebara — the same carrier infrastructure your personal mobile number uses.
Vapi and AgentPhone serve different use cases entirely — voice call automation, not single-use SMS OTP. If your pipeline needs inbound SMS for account verification across 2,500+ services, VirtualSMS MCP is the right tool.
Pattern 1: Single-Shot Verification (Claude API + MCP)
Single-shot is the baseline pattern: instruct Claude to get a number for a specific service, wait for the SMS, and return the OTP. This works for interactive automation, one-off verifications, and testing.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
# MCP tools are auto-injected by the SDK when the server is configured
# via environment or config — no manual tool schema needed
system="""You have access to VirtualSMS tools. When asked to verify an account:
1. Call request_number with the target service and preferred country.
2. Poll get_sms every 30 seconds until the SMS arrives or 10 minutes pass.
3. Extract and return the OTP code only. Do not return the full message.
4. If no SMS arrives within 10 minutes, call cancel_order and report failure.""",
messages=[{
"role": "user",
"content": "Request a UK number for Telegram verification, wait for the SMS, and return the OTP code."
}]
)
print(response.content[0].text)
For WhatsApp verification, add service="whatsapp" to your prompt or system message so Claude passes the correct service identifier to request_number. The MCP server normalizes service names, but being explicit prevents ambiguity in Claude’s tool call.
The claude-sonnet-4-6 model handles single-shot verification well at a fraction of Opus cost. It correctly sequences the tool calls, understands polling, and interprets the VirtualSMS response format without additional prompting.
Pattern 2: Parallel Batch Verifications
When you need to verify multiple accounts simultaneously, you want Claude to fan out the request_number calls in parallel rather than sequentially. Claude’s parallel tool-call support handles this natively.
import anthropic
import asyncio
client = anthropic.Anthropic()
SYSTEM_PROMPT = """You have access to VirtualSMS tools.
For batch verification requests:
- Call request_number for ALL accounts simultaneously using parallel tool calls.
- Do NOT wait for one number before requesting the next.
- Once all numbers are assigned, poll get_sms for each in parallel.
- Return results as a JSON list: [{account, number, otp, status}]
- Cancel any order that hasn't received an SMS within 8 minutes."""
def run_batch_verification(accounts: list[dict]) -> list[dict]:
"""
accounts: [{"service": "telegram", "country": "gb"}, ...]
Returns: [{"service", "number", "otp", "status"}, ...]
"""
account_list = "\n".join(
f"- Account {i+1}: service={a['service']}, country={a['country']}"
for i, a in enumerate(accounts)
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": f"Verify these accounts in parallel:\n{account_list}\n\nRequest all numbers simultaneously."
}]
)
# In production: parse the structured JSON from response.content
return response.content
# Example: 5 simultaneous Telegram verifications
accounts = [{"service": "telegram", "country": "gb"} for _ in range(5)]
results = run_batch_verification(accounts)
Keep the concurrency cap at 10–20 simultaneous activations per API key to stay within the 120 req/min rate limit. For larger batches, chunk the accounts list and run chunks sequentially with a brief delay between them. The $1 minimum deposit and per-activation pricing make it straightforward to pre-fund a batch.
A developer building a Claude-powered onboarding pipeline ran into the limits of this pattern with VoIP numbers: the pipeline failed 40–50% of the time, and every failure required a manual retry. After switching to VirtualSMS MCP with real carrier SIMs, the failure rate dropped below 5%, and Claude handled the entire retry logic autonomously via the reasoning loop in Pattern 3.
Pattern 3: Multi-Step Reasoning Over Failures
Real-world verification pipelines encounter three failure modes: no SMS (timeout), SMS arrives but OTP format is unexpected, and rate limit / temporary rejection from the target service. Pattern 3 instructs Claude to reason over each failure mode and take the appropriate recovery action.
RETRY_SYSTEM_PROMPT = """You have access to VirtualSMS tools.
You are running a verification pipeline with explicit failure handling:
TIMEOUT HANDLING:
- Poll get_sms every 60 seconds.
- If no SMS after 5 minutes, call cancel_order immediately (triggers auto-refund).
- Request a fresh number and retry. Maximum 3 retry attempts per account.
- After 3 failed attempts, mark account as FAILED and move on.
OTP EXTRACTION FAILURE:
- If SMS arrives but you cannot extract a valid OTP, log the raw message.
- Do not retry — the number worked, the OTP format may be non-standard.
- Return raw SMS content for manual review.
RATE LIMIT / SERVICE REJECTION:
- If request_number returns a 'service_unavailable' or rate limit error, wait 90 seconds.
- Retry the request_number call. Maximum 2 retries before escalating.
Always return a structured result for each account regardless of outcome."""
def verify_with_retry(service: str, country: str) -> dict:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=RETRY_SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": f"Verify one account. Service: {service}, preferred country: {country}. Handle all failures per protocol."
}]
)
return {"raw": response.content[0].text}
The key insight in Pattern 3 is that Claude reasons over the failure condition, not just the success path. You don’t need to write explicit if/else retry loops in your application code — the system prompt defines the decision tree, and Claude executes it correctly across tool calls. This is where claude-sonnet-4-6 earns its place over Haiku: the model needs to interpret ambiguous tool responses and make judgment calls about when to retry vs when to escalate.
The VirtualSMS 20-minute auto-refund is the backstop. Even if Claude’s retry loop doesn’t explicitly call cancel_order, you won’t be charged for a number that never delivered an SMS. Pattern 3 calls cancel_order proactively (at 5 minutes) to get the refund faster and free up the number for other requests.
Pattern 4: Account-Creation Pipeline (Verify → Store → Reuse)
Pattern 4 is the full production pipeline: request a number, trigger the verification flow on the target service, extract the OTP, store the verified account credentials, and reuse the number for future SMS if it’s a rental rather than a one-time activation.
VirtualSMS offers two product tiers for this use case. Full Access and Platform Rentals differ in key ways:
- Full Access Rental: exclusive SIM rental, any service, 1/3/7/14/30-day periods. The number is yours for the rental period — you can receive multiple SMS from any sender.
- Platform Rental: one service only, shared SIM, 1/3/7-day periods. Includes a 20-minute refund window if no SMS arrives. Better price point for single-service pipelines.
For account-creation pipelines where you only need one OTP per number, Platform Rental is the cost-efficient choice. For pipelines where the verified account receives ongoing SMS (2FA codes, login notifications), Full Access Rental gives you continuous inbound access.
import json
from datetime import datetime
PIPELINE_SYSTEM_PROMPT = """You are running a full account-creation verification pipeline.
STEP 1 — REQUEST NUMBER:
Call request_number with the target service and country. Store the returned number and order_id.
STEP 2 — WAIT FOR OTP:
Poll get_sms every 45 seconds. Extract the OTP when it arrives.
STEP 3 — RETURN STRUCTURED RESULT:
Return a JSON object with this exact structure:
{
"number": "+44...",
"order_id": "vsms_...",
"otp": "123456",
"received_at": "ISO timestamp",
"status": "success" | "timeout" | "failed"
}
STEP 4 — TIMEOUT:
If no SMS after 8 minutes, call cancel_order and return status: "timeout"."""
def create_verified_account(service: str, country: str, storage_backend) -> dict:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=PIPELINE_SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": f"Run the full verification pipeline. Service: {service}, country: {country}."
}]
)
# Parse Claude's structured JSON response
result_text = response.content[0].text
try:
result = json.loads(result_text)
except json.JSONDecodeError:
# Claude returned prose — extract JSON block
import re
json_match = re.search(r'\{.*\}', result_text, re.DOTALL)
result = json.loads(json_match.group()) if json_match else {"status": "parse_error"}
# Store to your backend (DB, spreadsheet, Notion, etc.)
if result.get("status") == "success":
storage_backend.save({
"service": service,
"country": country,
"number": result["number"],
"otp": result["otp"],
"verified_at": result["received_at"],
"order_id": result["order_id"]
})
return result
This pattern works at scale because Claude handles the entire state machine — polling intervals, timeout logic, response parsing — and returns a clean structured result your application code can act on directly. The storage layer is decoupled: swap storage_backend for Postgres, Redis, Notion, or a local JSON file depending on your stack.
Error Handling: Timeouts, Rate Limits, Cancelled Orders
Three error conditions come up in every production deployment:
Timeout (no SMS): VirtualSMS auto-refunds at 20 minutes. In Claude workflows, call cancel_order proactively at 5–8 minutes to get the refund faster and request a replacement number. Include explicit timeout thresholds in your system prompt — Claude will not invent a timeout on its own.
Rate limits (429): The VirtualSMS API allows 120 requests per minute. In batch workflows, Claude will encounter 429s if you instruct it to fire too many simultaneous requests. Include explicit concurrency instructions in your system prompt: “Never request more than 10 numbers simultaneously.” Claude respects concurrency constraints when they’re stated explicitly.
Cancelled orders: If Claude calls cancel_order on a valid order (e.g., due to a misread timeout), the number is released and the credit is returned. Cancelled orders are logged by the API — you can audit them via the REST API if you need to reconcile.
Malformed OTP extraction: Sometimes the SMS arrives with a non-standard format — the OTP embedded in a URL, surrounded by punctuation, or in a language Claude’s extraction heuristic misses. Instruct Claude to return the raw SMS body as a fallback field in every result object, so you can handle edge cases without re-requesting a number.
Transport Comparison: HTTP vs stdio
| Transport | Latency | Use case |
|---|---|---|
| stdio | ~2ms | Local dev, Claude Code, Claude Desktop |
| StreamableHTTP | ~20–50ms | Production agents, remote Claude API calls |
| SSE (legacy) | ~20–50ms | Older MCP clients; being deprecated |
For local development and Claude Code sessions, stdio is the right default — it’s what npx virtualsms-mcp uses out of the box and adds essentially zero latency. For production deployments where your Claude agent runs on a server and makes API calls, StreamableHTTP is the correct choice. Configure it by pointing the MCP server URL at the HTTP endpoint rather than using the npx command.
SSE (Server-Sent Events) is the legacy transport — some older MCP client implementations use it, but StreamableHTTP supersedes it for new deployments.
Claude Tool-Use vs MCP — Which to Use?
| Approach | When to use |
|---|---|
| VirtualSMS MCP | Claude is the primary orchestrator; interactive sessions; Claude Code; Claude Desktop |
| VirtualSMS REST API via tool_use | LangChain/CrewAI/LangGraph pipelines; high-volume batch outside Claude; non-Claude agents |
The distinction matters for architecture decisions. MCP is the right choice when Claude owns the workflow — it gives Claude typed tool access, handles authentication at the server level, and requires no tool schema maintenance on your side. The tool catalog updates automatically when the MCP server ships new methods.
The VirtualSMS REST API via manual tool_use definitions is the right choice when Claude is one component in a larger pipeline that also includes non-Claude agents, or when you need throughput that exceeds what a single Claude session can sustain. LangChain agents, CrewAI crews, and LangGraph flows all work well with the REST API directly — they don’t benefit from MCP’s Claude-native ergonomics.
Both approaches access the same underlying activation inventory. The only practical difference is where the tool schema lives (MCP server vs your codebase) and how much glue code you write. For pure Claude-native workflows, MCP wins on developer experience every time.
Real opinion: real SIM is not a premium add-on for AI agent pipelines — it is the baseline. A pipeline that fails 40% of the time because it’s hitting VoIP numbers is not a pipeline; it’s a manual process with extra steps. MCP makes it trivial to get this right from day one. Add the config block, add your API key, and Claude handles the rest.
For related reading on agent phone verification at scale, see AI agent phone verification patterns and virtual numbers for developer QA testing.
The Bottom Line
The VirtualSMS MCP server gives Anthropic Claude direct, typed access to real carrier-issued phone numbers — from operators like Vodafone, O2, T-Mobile, and EE — without any glue code or tool schema maintenance. Four patterns cover the 95% of agent verification use cases: single-shot for simple automation, parallel batch for onboarding pipelines, multi-step retry reasoning for robust error handling, and full account-creation pipelines that verify, store, and reuse. Activations from $0.05 across 145+ countries and 2,500+ services. The VirtualSMS MCP server is a single config block away. For direct API access in non-Claude pipelines, the REST API exposes the same inventory with full programmatic control.