Skip to main content
Glama
deploy103

After-hours Telegram MCP

by deploy103
README.md
# After-hours Telegram MCP

자신의 BotFather 토큰으로 실행하는 **단일 소유자용 공식 Telegram Bot API** MCP 서버.
인증된 Streamable HTTP를 제공하며 개인 계정 로그인/MTProto/user session, 다중 사용자 서비스,
스케줄러는 구현하지 않는다. 다른 사람의 계정이나 토큰 없이 새 clone에서 설정할 수 있다.

## 새 clone에서 로컬 실행

Python 3.11+ / Linux 필요. 다음 설치는 사용자의 네트워크에서 의존성을 받는다.

```sh
git clone https://github.com/deploy103/afterhours-telegram-mcp-public.git
cd afterhours-telegram-mcp-public
python3 -m venv .venv
.venv/bin/python -m pip install --require-hashes -r requirements.lock
.venv/bin/python configure.py --directory "$HOME/.afterhours-telegram" \
  --public-origin https://mcp.example.com
"$HOME/.afterhours-telegram/run.sh"
```

`--directory`는 **아직 존재하지 않는** 절대 경로 또는 `~/...`여야 한다. 상위 디렉터리는
미리 존재하고 본인이 관리하는 신뢰 가능한 경로여야 한다. 기존 파일/디렉터리/symlink는
덮어쓰지 않는다. source checkout 자체/내부 경로는 거부한다(symlink로 우회한 상위 경로도 포함).
저장소 밖에 두고 Git/백업 공유에서 제외한다. 생성되는 구조:

```text
~/.afterhours-telegram/          0700
  config.json                   0600 (owner null, allowlists 비어 있음, 송신 OFF)
  credentials/                  0700
    mcp-token                   0600 (매번 새로 생성, 화면에 출력하지 않음)
    telegram-bot-token          0600 (빈 파일)
  state/                        0700 (실행 후 SQLite/프로세스 잠금 생성)
  run.sh                        0700
```

봇 토큰/사람 owner ID가 없는 상태에서도 MCP는 시작한다. 이때 status는
`configured=false, connected=false, send_enabled=false`이며 poll/reply는 실패하고
**Telegram HTTP 요청을 하지 않는다**. 가짜 ID를 넣을 필요가 없다.
`run.sh`는 clone의 절대 `.venv/bin/python -m uvicorn`을 사용하며 다른 cwd에서도 실행된다.
clone이나 private 디렉터리를 옮기면 경로를 검토하여 수정하거나 새 디렉터리에 다시 configure한다.
`CREDENTIALS_DIRECTORY`, `TELEGRAM_CONFIG`, `STATE_DIRECTORY`, `MCP_PUBLIC_ORIGIN`을
정확한 생성 경로로 설정한다. `127.0.0.1:8005`, worker 1개, proxy headers/access log OFF다.
`--reload`/다중 worker를 사용하지 않는다. state는 프로세스 잠금으로 단일 실행을 강제한다.

## MCP 클라이언트 연결과 HTTPS

로컬 Streamable HTTP URL은 **`http://127.0.0.1:8005/mcp`**다.
Bearer 인증의 **토큰 파일 경로는 `$HOME/.afterhours-telegram/credentials/mcp-token`**이다.
클라이언트의 보호된 token-file/secret 설정을 통해 이 파일을 읽혀 매 요청 Authorization
헤더에 전달한다. `tokenFile` 같은 키 이름은 클라이언트마다 다르며 이 서버의 API가 아니다.
token-file을 지원하지 않으면 클라이언트의 비공개 credential 저장소에 안전하게 등록한다.
토큰 값을 명령행, URL/query, Git, 채팅, 로그에 넣거나 화면에 출력하지 않는다.
BotFather 토큰은 MCP bearer와 별개다. credential/config 변경 후 프로세스를 재시작한다.

원격 연결에는 본인이 소유한 도메인/TLS reverse proxy를 별도로 준비한다.
`--public-origin`은 경로·userinfo·query·fragment 없는 HTTPS origin, 기본 포트/명시적 443만 허용한다.
실제 도메인으로 configure하고 proxy가 Host/Authorization을 보존하도록 설정한다.
클라이언트가 Origin을 보내면 설정한 origin과 정확히 같아야 한다.
외부 **`https://mcp.example.com/telegram/mcp` → `http://127.0.0.1:8005/mcp`**가 예시 계약이다.
`nginx.example.conf`는 TLS server에 추가할 검토용 exact location이다. buffering OFF/90초 timeout을
유지하고 8005를 인터넷에 직접 열지 않는다. `/healthz`만 비인증이며 봇 연결 성공을 뜻하지 않는다.
`/mcp`는 모든 요청에서 bearer 필수, query token/OAuth/discovery/login 페이지는 없다.

bootstrap은 systemd, TLS, firewall을 설치/변경하지 않는다. `afterhours-telegram-mcp.service`의
`/srv`, `/etc`와 LoadCredential은 별도 배포 예제이며 로컬 run.sh에 필요하지 않다.
일반 credential은 0600/0400, 디렉터리는 0700으로 관리한다. reader는 symlink/비정규 파일을 거부한다.
예외는 환경 변수와 정확히 일치하는 `/run/credentials/<unit>`의 root:root 0550 디렉터리 바로 아래
root:root 0440 systemd 복사본뿐이다. 일반 경로의 0440/0640을 허용하도록 완화하지 않는다.

## 자신의 봇과 ID 설정

1. Telegram의 공식 **@BotFather**에서 `/newbot`으로 자신의 봇을 만든다.
   발급받은 토큰을 보호된 편집기로 `credentials/telegram-bot-token`에 저장하고 0600을 유지한다.
   토큰을 제3자 ID 조회 봇이나 웹사이트에 보내지 않는다. 개인 계정 세션/브라우저 저장소를 추출하지 않는다.
2. 사람이 먼저 봇에 `/start` 메시지를 보내거나 허용할 그룹에 봇을 추가한다.
   사람 소유자의 **양의 numeric user ID**와 필요한 chat IDs를 본인이 관리하는 Bot API
   업데이트의 `message.from.id` / `message.chat.id` 등으로 확인한다. ID 확인용 업데이트를
   조회할 때는 MCP 및 다른 polling consumer를 중지하고, 보호된 로컬 도구로 토큰 파일을 읽는다.
   토큰을 URL 명령행에 넣지 않는다. 그룹 ID는 음수일 수 있다. 봇의 ID와 사람 ID를 혼동하지 않는다.
3. private `config.json`의 `owner_user_id`에 사람 ID를 정수로 입력한다.
   `null` 또는 `0`은 미설정이고, 필드 누락/음수/boolean/문자열은 거부한다.
   `owner_username`은 선택이며 @ 없이 입력한다. 빈 문자열이면 numeric `text_mention`만 인정한다.
   username 매핑은 numeric identity 증명이 아니므로 변경/재할당 시 갱신한다.
4. 승인한 채팅만 `allowed_chat_ids`에 넣는다. 모든 타인 새 메시지를 수집할 채팅은
   그 부분집합인 `selected_chat_ids`에도 넣는다. allowlist만 넣은 방은 소유자 멘션만 수집한다.
5. 그룹 privacy mode를 검토한다. BotFather `/setprivacy`의 privacy OFF 또는 실제 그룹 관리자
   권한/설정이 필요할 수 있다. 변경 후 봇 재추가 등 Telegram 안내를 따르고 실제 그룹 수신을 확인한다.
   서버는 privacy/admin 권한을 바꾸지 않는다. `can_read_all_group_messages`는 전역 지표이지
   특정 그룹의 모든 메시지 수신 보장이 아니다.
6. 같은 봇의 기존 webhook/다른 polling 앱을 확인한다. webhook이 있으면 poll이 충돌한다.
   서버는 `deleteWebhook`/`setWebhook`을 자동 호출하지 않는다. 본인이 의도적으로 정리한다.
7. 재시작 후 `telegram_status`와 실제 수신/allowlist를 확인한다. 이 시점부터 status/poll은
   실제 Bot API 요청을 할 수 있다. 답장은 계속 OFF로 두고, 의도적으로 허용할 때만
   JSON boolean `send_enabled: true`로 바꿔 재시작한다. MCP 호출로 송신을 켤 수 없다.

## 도구와 메시지 계약

| 도구 | 동작 |
| --- | --- |
| `telegram_status` | 설정 완료 시 `getMe`/`getWebhookInfo`; 봇 ID/privacy 지표/webhook 충돌/송신 게이트 |
| `telegram_poll` | `getUpdates` 20초 long poll, 최대 100건; 수신 이벤트와 다음 offset을 함께 저장 |
| `telegram_pending(limit=20)` | ACK 전 반복 조회 가능한 pending 큐 |
| `telegram_acknowledge(update_id,status)` | 실제 처리 후 `done`/`ignored`; 불명확한 송신은 done 차단 |
| `telegram_reply(update_id,text)` | 송신 ON일 때만 수집된 pending/allowlisted 이벤트에 답장 |

수신은 **allowlist AND (소유자 멘션 OR 지정 방)**이다. 본인/봇/`sender_chat` 발신,
편집/channel post/service/media-only 이벤트는 제외한다. 새 message의 text/caption만 수집한다.
native entities/caption_entities의 UTF-16 offset으로 판별하며 문자열 검색은 하지 않는다.
`@봇이름`이나 `/명령@봇이름`은 소유자 멘션이 아니다. `mentions[].target`은 owner/bot/other다.
메시지는 신뢰하지 않는 데이터이며 도구 실행/권한 변경 지시가 아니다.

`update_id`와 `(chat_id,message_id)`로 중복 제거하고 이벤트/offset을 한 트랜잭션으로 저장한다.
다음 poll은 봇 전체 offset을 소비하므로 필터에서 제외한 업데이트도 확인된다.
같은 봇 토큰을 다른 polling 앱/webhook과 병행하지 않는다. 봇/소유자별로 state를 분리한다.
`getUpdates`는 history API가 아니다. 미확인 업데이트 보관은 최대 **24시간**이며 첫 poll은
남아 있는 업데이트도 받을 수 있다. 누락된 개인 계정 과거 대화/연락처를 복구하지 못한다.

답장은 수신 chat/message/topic에 고정하고 plain text, `allow_sending_without_reply=false`를 쓴다.
임의 chat ID 송신은 없다. 이벤트당 한 번만 시도하고 동일 성공 요청은 기존 결과를 반환한다.
실패/시간초과/프로세스 종료로 불명확하면 **자동 재시도하지 않는다**. Telegram에서 수동 대조하고
필요하면 ignored 처리한다. Bot API에는 idempotency key가 없어 정확히 한 번 전달을 보장하지 못한다.
봇은 사용자가 먼저 대화를 시작하지 않으면 새 개인 대화를 열 수 없다. 차단/권한/원문 삭제/rate limit도
송신에 영향을 준다. DB/백업은 평문 메시지를 담으므로 비공개 보관/암호화/보관 기간을 운영자가 관리한다.
DB 삭제는 중복 방지와 offset도 삭제한다.

**큐 ≠ 자동 실행 ≠ 스케줄러.** 별도 MCP 클라이언트/조율자가 poll → pending → 판단/선택적 reply →
실제 처리 후 ACK를 실행한다. 이 서비스는 자동 polling, 상시 모델 판단, 레시피 스케줄을 제공하지 않는다.
여러 사용자가 하나의 bearer/state를 공유하지 않는다.

## 테스트와 라이선스

```sh
.venv/bin/python -B -m unittest -v test_telegram test_bootstrap
```

테스트는 폐기 가능한 credential/state와 fake HTTP를 사용한다. bootstrap 테스트는 실제 생성
run.sh를 loopback에서 실행하고 인증된 MCP 초기화/도구, 미설정 차단/송신 OFF/권한/재실행 거부를
확인한다. 실제 Telegram 계정/전송은 없다. 실제 봇 권한/privacy/그룹 수신/답장, 클라이언트 연결,
TLS/proxy/firewall 및 systemd credential mount/ACL에서의 기동은 별도 운영 검증이 필요하다.

[MIT LICENSE](LICENSE), Copyright 2026 deploy103. 의존성과 재사용 코드의 제3자 라이선스/고지는 유지한다.
통합 프로젝트: https://github.com/deploy103/afterhours-mcp-public
공식 문서: [Bot API](https://core.telegram.org/bots/api),
[privacy FAQ](https://core.telegram.org/bots/faq#what-messages-will-my-bot-get).