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

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

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

ClawOps 가 제공하는 원격 MCP 서버(https://api.claw-ops.com/mcp)를 바로 붙여도 되지만, 직접 만들면 ① 통화 단가 cap·발신 prefix 화이트리스트 등 custom 정책 박기, ② Slack 알림·DB 로깅 같은 후크 추가, ③ 회사 내부 admin 만 쓰게 자체 auth 적용이 쉬워진다. 이 글은 ClawOps SDK 를 얇게 wrapping 해서 나만의 MCP 음성 서버를 만드는 방법이다.

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"],   # sk_...
    account_id=os.environ["CLAWOPS_ACCOUNT_ID"],
)

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 create_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()[0].number
    # url/ai 를 모두 생략하면 ClawOps Agent SDK 모드로 연결됩니다.
    call = client.calls.create(
        to=to, from_=sender,
        ai={
            "provider": "openai",
            "model": "gpt-realtime",
            "api_key": os.environ["OPENAI_API_KEY"],
            "language": "ko",
            "messages": [{"role": "system", "content": f"전화를 받으면 다음 내용을 정확히 전달하고 종료: {message}"}],
        },
    )
    return {"call_id": call.call_id, "status": call.status}

@server.tool()
def create_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()[0].number
    call = client.calls.create(
        to=to, from_=sender,
        ai={
            "provider": "openai",
            "model": "gpt-realtime",
            "api_key": os.environ["OPENAI_API_KEY"],
            "language": "ko",
            "messages": [{"role": "system", "content": system_prompt}],
        },
    )
    return {"call_id": call.call_id, "status": call.status}

@server.tool()
def send_message(to: str, body: str) -> dict:
    """한국 SMS/LMS 발송. body 길이로 SMS/LMS 자동 분기."""
    to = normalize_korean(to)
    sender = client.numbers.list()[0].number
    msg = client.messages.create(to=to, from_=sender, body=body)
    return {"message_id": msg.message_id, "type": msg.type}

@server.tool()
def get_call_summary(call_id: str) -> dict:
    """통화 종료 후 요약·전사 조회."""
    call = client.calls.get(call_id)
    summary = client.calls.get_summary(call_id)
    transcript = client.calls.get_transcript(call_id)
    return {
        "status": call.status,
        "duration": call.duration,
        "summary": summary.result_json if summary.status == "completed" else None,
        "transcript": transcript.segments if transcript.status == "completed" else None,
    }

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

Claude Desktop 에 등록

{
  "mcpServers": {
    "clawops-voice": {
      "command": "python",
      "args": ["/path/to/mcp_server.py"],
      "env": {
        "CLAWOPS_API_KEY": "sk_...",
        "CLAWOPS_ACCOUNT_ID": "AC...",
        "OPENAI_API_KEY": "sk-..."
      }
    }
  }
}

> 이 서버는 ClawOps SDK 를 직접 호출하는 자체 호스팅 stdio 서버라 SDK 용 API 키(`sk_...`)를 env 로 주입합니다. ClawOps 가 제공하는 원격 MCP 서버(`https://api.claw-ops.com/mcp`)에 그냥 붙이는 경우라면 API 키가 아니라 OAuth(브라우저 동의)로 인증하므로 키를 넣지 않습니다.

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 create_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 create_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/월

다음 단계

관련 글 더 보기

가이드

MCP 전화 tool 설계 — create_call / control_call / send_message 패턴 정리

MCP 서버에서 전화 기능을 노출할 때 어떤 tool 을 어떻게 쪼개야 LLM 이 제대로 쓰는지. ClawOps MCP / SDK 기반 실전 patterns.

가이드

MCP 음성 서버 — Claude / Claude Code / Cursor agent에 한국 070 전화 붙이기

MCP(Model Context Protocol) 원격 서버 연결로 Claude Desktop, Claude Code, Cursor agent 에 한국 070 번호 전화 tool 을 붙이는 가이드. ClawOps 원격 MCP 연결 + 실제 통화 데모.

가이드

Claude Code 에 한국 070 번호 붙이기 — MCP 한 줄로 'thomi 한테 전화 걸어' 가능하게

Claude Code(터미널 CLI) 안에서 한국 070 번호로 직접 전화 걸고 받게 만드는 가이드. MCP 서버 설정 + ClawOps 070 번호 발급 + Claude Code agent 가 부르는 실제 프롬프트.

가이드

Claude Desktop 에 한국 070 번호 붙이기 — ClawOps MCP 커넥터로 전화 거는 Claude 만들기

Claude Desktop(데스크톱 앱)에 한국 070 번호 전화 기능을 붙이는 가이드. ClawOps 원격 MCP 서버를 OAuth 커스텀 커넥터로 연결 + 070 발급 + 실제 사용 프롬프트. (설치할 npm 패키지·API 키 환경변수 불필요)

가이드

OpenClaw 에 한국 070 번호 붙이기 — 오픈소스 컴퓨터 에이전트 + 전화

OpenClaw(오픈소스 computer-use 에이전트)에 한국 070 번호 전화 도구를 붙이는 가이드. MCP 서버 통합 + ClawOps 070 발급 + 실전 시나리오.

ClawOps AI 전화 API로 시작하기

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