← 블로그 목록
가이드2026-05-28

MCP 음성 서버 직접 만들기 — Stdio + HTTP 양쪽 지원, 코드 50줄

MCP 음성 서버 직접 만들기 — Stdio + HTTP 양쪽 지원, 코드 50줄

call-me-mcp (npm 패키지) 가 표준 구현이지만, 직접 만들면 ① 통화 단가 cap·발신 prefix 화이트리스트 등 custom 정책 박기, ② Slack 알림·DB 로깅 같은 후크 추가, ③ 회사 내부 admin 만 쓰게 자체 auth 적용이 쉬워진다.

0. 사전 준비

  • Python 3.10+ 또는 Node 20+
  • ClawOps API 키
  • MCP SDK (pip install mcp 또는 npm install @modelcontextprotocol/sdk)

1. Python — Stdio MCP 서버 (50줄)

mcp_server.py:

import os
from mcp.server.fastmcp import FastMCP
from clawops import ClawOps

server = FastMCP("clawops-voice")
client = ClawOps(api_key=os.environ["CLAWOPS_API_KEY"])

ALLOWED_PREFIXES = ("010", "02", "031", "032", "033", "041", "042", "043",
                    "044", "051", "052", "053", "054", "055", "061", "062",
                    "063", "064", "070")

def normalize_korean(num: str) -> str:
    cleaned = num.replace("-", "").replace(" ", "").replace("+82", "0")
    if not cleaned.startswith("0"):
        cleaned = "0" + cleaned
    return cleaned

@server.tool()
def make_call(to: str, message: str, from_number: str = "") -> dict:
    """한국 번호로 전화 걸고 message 를 AI 가 read out. 짧은 단방향 알림용."""
    to = normalize_korean(to)
    if not any(to.startswith(p) for p in ALLOWED_PREFIXES):
        return {"error": f"unsupported prefix: {to}"}
    sender = from_number or client.numbers.list().items[0].phone_number
    call = client.calls.create(
        to=to, from_=sender,
        agent_config={
            "provider": "openai-realtime",
            "language": "ko-KR",
            "system_prompt": f"다음 메시지를 자연스러운 한국어로 전달하고 끊으세요: '{message}'",
        },
    )
    return {"call_id": call.id, "status": call.status}

@server.tool()
def make_call_with_prompt(to: str, system_prompt: str, from_number: str = "") -> dict:
    """대화형 통화. system_prompt 대로 AI 가 응대. 결과는 get_call_summary 로 조회."""
    to = normalize_korean(to)
    if not any(to.startswith(p) for p in ALLOWED_PREFIXES):
        return {"error": f"unsupported prefix: {to}"}
    sender = from_number or client.numbers.list().items[0].phone_number
    call = client.calls.create(
        to=to, from_=sender,
        agent_config={
            "provider": "openai-realtime",
            "language": "ko-KR",
            "system_prompt": system_prompt,
        },
    )
    return {"call_id": call.id, "status": call.status}

@server.tool()
def send_sms(to: str, body: str) -> dict:
    """한국 SMS/LMS 발송. body 길이로 SMS/LMS 자동 분기."""
    to = normalize_korean(to)
    msg = client.messages.create(to=to, body=body)
    return {"message_id": msg.id, "type": msg.type}

@server.tool()
def get_call_summary(call_id: str) -> dict:
    """통화 종료 후 요약·전사 조회."""
    call = client.calls.get(call_id)
    return {
        "status": call.status,
        "duration_seconds": call.duration_seconds,
        "summary": getattr(call, "summary", None),
        "transcript_url": getattr(call, "transcript_url", None),
    }

if __name__ == "__main__":
    server.run()

Claude Desktop 에 등록

{
  "mcpServers": {
    "clawops-voice": {
      "command": "python",
      "args": ["/path/to/mcp_server.py"],
      "env": { "CLAWOPS_API_KEY": "ck_live_..." }
    }
  }
}

2. HTTP 변형 (Cursor background / ChatGPT / 원격 에이전트용)

같은 tool 정의를 HTTP transport 로:

from mcp.server.fastmcp import FastMCP
# ... tool 정의 동일 ...

if __name__ == "__main__":
    server.run(transport="http", host="0.0.0.0", port=8080,
               auth_token=os.environ["MCP_AUTH_TOKEN"])

Vercel/Fly/Render 에 배포 후 endpoint url + auth_token 으로 원격 에이전트가 호출.

3. 정책 후크 추가 — 일 cap, 야간 차단

import sqlite3, datetime
from zoneinfo import ZoneInfo

DB = sqlite3.connect("/data/mcp-policy.sqlite")
DB.execute("CREATE TABLE IF NOT EXISTS calls (ts TEXT)")

def policy_check(to: str) -> str | None:
    now = datetime.datetime.now(ZoneInfo("Asia/Seoul"))
    # 야간 발신 금지 (21시-08시)
    if now.hour >= 21 or now.hour < 8:
        return "야간 발신 금지 (KST 21:00-08:00)"
    # 일 cap
    today_count = DB.execute(
        "SELECT count(*) FROM calls WHERE ts >= ?",
        (now.replace(hour=0, minute=0, second=0).isoformat(),),
    ).fetchone()[0]
    if today_count >= 50:
        return "일 발신 cap (50회) 초과"
    return None

@server.tool()
def make_call(to: str, message: str, from_number: str = "") -> dict:
    err = policy_check(to)
    if err: return {"error": err}
    # ... 기존 로직 ...
    DB.execute("INSERT INTO calls (ts) VALUES (?)",
               (datetime.datetime.now(ZoneInfo("Asia/Seoul")).isoformat(),))
    DB.commit()
    return {"call_id": call.id}

4. Slack 알림 후크

import httpx

def slack_notify(text: str):
    httpx.post("https://slack.com/api/chat.postMessage",
               headers={"Authorization": f"Bearer {os.environ['SLACK_BOT_TOKEN']}"},
               json={"channel": "ai-clawops", "text": text})

@server.tool()
def make_call_with_prompt(to: str, system_prompt: str, from_number: str = "") -> dict:
    # ... 기존 ...
    slack_notify(f"[MCP] AI 통화 시작: {to}\n프롬프트: {system_prompt[:200]}")
    return {"call_id": call.id}

5. 작동 원리

Claude/Codex/Cursor agent
   ↓ MCP stdio/HTTP
당신의 mcp_server.py (policy 후크)
   ↓ ClawOps SDK (Python/Node)
api.claw-ops.com (GCP 서울)
   ↓ SIP/2.0 + WebSocket bridge to LLM realtime
한국 070 게이트웨이 → 대상 휴대폰

6. 배포 형태

환경추천
개인 macOS devPython stdio, ~/.claude/mcp.json 에 등록
회사 내부 사용HTTP + auth_token, Fly.io shared-cpu-1x
Public SaaSHTTP + auth_token per-user + 일 cap + 결제 연동

7. 비용

  • 자체 호스팅 무료 (Fly/Vercel 무료 plan)
  • ClawOps Individual ₩19,000/월

다음 단계

관련 글 더 보기

ClawOps AI 전화 API로 시작하기

070 번호 발급부터 AI 음성 통화까지, REST API 몇 줄이면 됩니다.