개발 블로그
회고 4 분 소요

텔레그램 봇 개발 일지 #2 — 음성·이미지·MCP 통합

telegram claude-code mcp whisper

결론부터

음성 메시지는 Whisper로 텍스트 변환, 이미지는 봇이 inbox 디렉토리에 저장한 뒤 절대 경로를 프롬프트에 끼워 Claude가 Read 도구로 열게 했다. MCP는 환경변수 토글로 옵트인 — Playwright(브라우저)와 blog(Supabase 글쓰기) 두 개를 붙였다.

음성 입력

OPENAI_API_KEY가 있으면 음성 메시지를 받아서 Whisper로 transcribe하고, 그 텍스트를 일반 메시지처럼 처리한다.

audio = await context.bot.get_file(message.voice.file_id)
ogg = await audio.download_as_bytearray()
transcript = openai_client.audio.transcriptions.create(
    model="whisper-1",
    file=("voice.ogg", bytes(ogg)),
).text
await handle_text(update, context, transcript)

키가 비어 있으면 음성 메시지를 거부한다 — 모르는 사이 비용이 누적되지 않도록 하는 안전장치.

이미지 첨부

텔레그램이 보내는 사진은 download_to_driveinbox/<chat_id>/<unix_ts>.jpg에 저장하고, 프롬프트 앞쪽에 절대 경로를 끼워 Claude에 넘긴다. Claude는 Read 도구로 그 경로를 그대로 열어 멀티모달 인식한다.

프롬프트에 들어가는 형태는 대략 이렇다:

사용자가 첨부한 이미지: D:/playground/telegram-claude-bot/inbox/123456789/1715166432.jpg
 
[원본 메시지]
이 스크린샷에서 빨간 버튼 위치 좌표 알려줘

inbox는 시작 시 + 매시간 cleanup_inbox()INBOX_RETENTION_DAYS(기본 7일) 지난 파일을 정리한다.

멀티모달 비용 Claude는 이미지 한 장당 토큰을 꽤 먹는다. 가벼운 시각 확인이라면 좋지만, "스샷 50장 한꺼번에 분석" 같은 건 그대로 청구서로 돌아오니 주의.

MCP 서버 통합

MCP는 환경변수 토글로 켠다. 안 쓰는 사람은 startup 비용 0이고, 런타임 의존성이 미설치돼도 봇이 죽지 않도록 opt-in 구조로 갔다.

def build_mcp_servers() -> dict[str, dict]:
    servers = {}
    if env("MCP_PLAYWRIGHT_ENABLED"):
        servers["playwright"] = {
            "type": "stdio", "command": "npx",
            "args": ["-y", "@playwright/mcp@latest"],
        }
    if env("MCP_BLOG_ENABLED") and env("MCP_BLOG_ENTRY"):
        servers["blog"] = {
            "type": "stdio", "command": "node",
            "args": [env("MCP_BLOG_ENTRY")],
        }
    return servers

Playwright는 브라우저 자동화·스크린샷, blog는 my-blog의 Supabase 콘텐츠를 Claude가 직접 CRUD하게 해준다 — 실제로 이 글도 같은 경로로 발행했다.

자식에 환경변수 전달 stdio MCP 서버를 spawn할 때 entry에

env를 명시하지 않으면 SDK가 부모 환경의 일부만 상속해 BLOG_API_URL 같은 커스텀 변수가 누락될 수 있다. 자식에서 못 받아본다면 entry에 env={"BLOG_API_URL": os.environ.get("BLOG_API_URL", "")}처럼 명시적으로 박아주는 게 안전하다.

다음 편 예고

3편은 멀티 프로젝트 — 텔레그램 supergroup의 토픽 단위로 cwd를 분리하고, 토픽 이름을 폴더명에 매핑해 자동 라우팅한 구조를 다룬다.