Skip to main content
Glama
d8rt8v

ChatGPT to Telegram MCP

by d8rt8v
README.md
<div align="center">

# ChatGPT to Telegram MCP

**Publish reliable, duplicate-free Telegram posts from ChatGPT — manually or as part of a scheduled workflow.**

[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare&logoColor=white)](https://workers.cloudflare.com/)
[![Model Context Protocol](https://img.shields.io/badge/protocol-MCP-7C3AED)](https://modelcontextprotocol.io/)
[![OAuth 2.1](https://img.shields.io/badge/auth-OAuth_2.1-2F80ED)](#authentication)

[Features](#features) · [Architecture](#architecture) · [Quick start](#quick-start) · [Connect ChatGPT](#connect-chatgpt) · [Security](#security)

</div>

ChatGPT to Telegram MCP is a Streamable HTTP MCP server for Cloudflare Workers. It gives ChatGPT two focused tools: read the posts this server sent during the previous 48 hours, and publish one post to a fixed Telegram channel. Exact-text and source-URL deduplication prevents accidental reposts.

Use it in an ordinary ChatGPT conversation or make publishing part of a scheduled workflow when scheduled tasks and custom MCP apps are available for your account or workspace. The Worker itself does not run a cron job.

> [!NOTE]
> This is an unofficial open-source project and is not affiliated with OpenAI or Telegram.

## Features

- **ChatGPT-ready MCP endpoint** using Streamable HTTP.
- **One fixed Telegram destination** controlled by server configuration, not model input.
- **48-hour history** backed by Cloudflare D1.
- **Duplicate protection** for normalized text and canonical source URLs.
- **OAuth 2.1 authentication** with GitHub sign-in, PKCE `S256`, DCR, and CIMD.
- **GitHub allowlist** for explicit access control.
- **Safe retries** through pending D1 reservations and idempotent tool behavior.
- **Cloudflare-native deployment** with Workers, KV, D1, logs, and traces.

## Architecture

```mermaid
flowchart LR
    C["ChatGPT<br/>chat or scheduled workflow"]
    W["Cloudflare Worker<br/>OAuth + MCP server"]
    G["GitHub OAuth<br/>identity check"]
    K["Cloudflare KV<br/>OAuth state and tokens"]
    D["Cloudflare D1<br/>48-hour send history"]
    T["Telegram Bot API<br/>fixed channel"]

    C -->|"OAuth 2.1 + MCP"| W
    W <-->|"sign in"| G
    W <-->|"OAuth records"| K
    W <-->|"history and dedupe"| D
    W -->|"sendMessage"| T
```

### Publish flow

```mermaid
sequenceDiagram
    participant C as ChatGPT
    participant M as MCP Worker
    participant D as D1
    participant T as Telegram

    C->>M: get_previous_messages
    M->>D: Read the previous 48 hours
    D-->>M: Posts and source URLs
    M-->>C: History
    C->>C: Prepare a non-duplicate digest
    C->>M: send_message(text, source_urls)
    M->>D: Reserve fingerprint and URLs
    alt Duplicate found
        D-->>M: Existing matches
        M-->>C: sent=false, duplicate=true
    else New post
        D-->>M: Reservation created
        M->>T: sendMessage
        T-->>M: Telegram message
        M->>D: Mark reservation as sent
        M-->>C: sent=true
    end
```

## MCP tools

| Tool | Purpose | Important behavior |
| --- | --- | --- |
| `get_previous_messages` | Returns up to 100 posts sent by this server during the previous 48 hours. | Reads the Worker's D1 history; it cannot see manual posts or posts from other bots. |
| `send_message` | Publishes one post to the configured Telegram channel. | Rejects matching text or source URLs found in the 48-hour window. Include every represented article URL in `source_urls`. |

The public tool names and response formats are stable across the project rebrand.

## Prerequisites

- Node.js 20 or newer.
- A Cloudflare account with Wrangler authenticated.
- A Telegram bot that can post to the target channel.
- A GitHub OAuth App.
- A ChatGPT account or workspace with custom MCP app support for the way you intend to use it.

## Quick start

### 1. Install

```bash
git clone https://github.com/d8rt8v/chatgpt-to-telegram-mcp.git
cd chatgpt-to-telegram-mcp
npm install
```

### 2. Create local configuration

macOS/Linux:

```bash
cp wrangler.example.jsonc wrangler.jsonc
cp .env.example .env
```

PowerShell:

```powershell
Copy-Item wrangler.example.jsonc wrangler.jsonc
Copy-Item .env.example .env
```

Both files are ignored by Git. Keep real account IDs and credentials there only.

### 3. Create Cloudflare resources

```bash
npx wrangler d1 create chatgpt-telegram-history
npx wrangler kv namespace create OAUTH_KV
```

Copy the returned D1 and KV IDs into `wrangler.jsonc` without changing the `DB` and `OAUTH_KV` binding names.

### 4. Configure the GitHub OAuth App

For a Worker deployed at `https://chatgpt-tg-mcp.<subdomain>.workers.dev`, set:

- **Homepage URL:** `https://chatgpt-tg-mcp.<subdomain>.workers.dev`
- **Authorization callback URL:** `https://chatgpt-tg-mcp.<subdomain>.workers.dev/callback`

Put the OAuth Client ID and Client Secret in `.env`.

### 5. Configure environment values

| Variable | Description |
| --- | --- |
| `TELEGRAM_BOT_TOKEN` | Token issued by BotFather. |
| `TELEGRAM_CHANNEL_ID` | Fixed destination, such as `@channel_name` or a numeric channel ID. |
| `GITHUB_CLIENT_ID` | GitHub OAuth App Client ID. |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth App Client Secret. |
| `ALLOWED_GITHUB_LOGINS` | One GitHub login or a comma-separated allowlist, for example `alex,editor2`. |

All five values are uploaded as encrypted Worker secrets. Wrangler also loads `.env` for local development when no `.dev.vars` file exists.

### 6. Migrate and deploy

```bash
npm run db:migrate:remote
npm run deploy
npm run secrets:push
```

`npm run secrets:push` runs `wrangler secret bulk .env`. A production deployment does **not** receive values from the local `.env` file automatically.

<details>
<summary><strong>Migrating an existing deployment</strong></summary>

Keep the existing D1 database ID, KV namespace ID, binding names, and migration state in your local `wrangler.jsonc`. The rebrand changes only the Worker name and public URL; it does not recreate or erase D1/KV data.

Before deploying:

1. Fill the two blank secret values in the local `.env`.
2. Change the GitHub OAuth App homepage and callback to the new `chatgpt-tg-mcp` URL.
3. Deploy the renamed Worker and push `.env` with `npm run secrets:push`.
4. Reconnect the MCP app in ChatGPT.

The old Worker is not deleted automatically. After the new endpoint is verified, retire the old Worker or remove its OAuth and Telegram secrets; otherwise it remains a second write-capable endpoint and existing MCP tokens may continue to publish through it. Deleting the old Worker does not delete the separately managed D1 database or KV namespace reused by the new deployment.

If the existing D1 migration is already applied, do not apply it again just for the rebrand.

</details>

## Connect ChatGPT

Add the public MCP endpoint as a custom app/server:

```text
https://chatgpt-tg-mcp.<subdomain>.workers.dev/mcp
```

Choose OAuth authentication. ChatGPT should discover the OAuth endpoints automatically; do not paste GitHub credentials into ChatGPT. Sign in with a GitHub account listed in `ALLOWED_GITHUB_LOGINS`.

Interactive use works anywhere your ChatGPT account or workspace supports the custom MCP app. Scheduled publishing is an additional use case where scheduled workflows can access the app. Availability depends on the ChatGPT plan and workspace configuration; see OpenAI's documentation for [Scheduled Tasks](https://help.openai.com/en/articles/10291617-scheduled-tasks-in-chatgpt) and [Workspace Agents](https://help.openai.com/en/articles/20001143-chatgpt-workspace-agents-for-enterprise-and-business).

## Authentication

1. ChatGPT discovers the protected-resource and authorization-server metadata.
2. It registers with DCR or uses CIMD and starts an authorization-code flow with PKCE `S256`.
3. The Worker displays a consent page and redirects to GitHub.
4. Only users in `ALLOWED_GITHUB_LOGINS` are accepted.
5. The Worker issues its own access and refresh tokens to ChatGPT.

The GitHub token is used only for the identity check and is not stored in MCP token properties.

## Verify a deployment

```bash
curl https://chatgpt-tg-mcp.<subdomain>.workers.dev/health
curl -i https://chatgpt-tg-mcp.<subdomain>.workers.dev/mcp
curl https://chatgpt-tg-mcp.<subdomain>.workers.dev/.well-known/oauth-protected-resource/mcp
curl https://chatgpt-tg-mcp.<subdomain>.workers.dev/.well-known/oauth-authorization-server
```

Expected results:

- `/health` returns `{"ok":true,"service":"chatgpt-to-telegram-mcp","authentication":"oauth"}`.
- `/mcp` returns `401` without a token and includes a `WWW-Authenticate` challenge.
- Both `/.well-known/` endpoints return OAuth metadata as JSON.

## Local development

Create a separate GitHub OAuth App for local work with:

- Homepage: `http://localhost:8787`
- Callback: `http://localhost:8787/callback`

Then run:

```bash
npm run db:migrate:local
npm run dev
```

OAuth cookies are `Secure`, so the production `workers.dev` URL remains the most reliable way to test the complete browser flow.

### Commands

| Command | Description |
| --- | --- |
| `npm run dev` | Start the local Worker. |
| `npm test` | Run the Node.js test suite. |
| `npm run check` | Check JavaScript syntax. |
| `npm run db:migrate:local` | Apply D1 migrations locally through the `DB` binding. |
| `npm run db:migrate:remote` | Apply D1 migrations to the configured remote database. |
| `npm run deploy` | Deploy the Worker. |
| `npm run secrets:push` | Upload all `.env` values as Worker secrets. |

## Security

- Telegram destination selection remains server-side; the model cannot choose a different channel.
- GitHub login matching is case-insensitive and denies every account outside the allowlist.
- Access tokens expire after one hour, refresh tokens after 30 days, and dynamic client registrations after 90 days.
- Plain PKCE and the OAuth implicit flow are disabled.
- OAuth state and CSRF values are one-time and expire after ten minutes.
- The Telegram bot token, GitHub secret, account IDs, and live Wrangler configuration are excluded from Git.
- Telegram is called only after D1 successfully reserves the content fingerprint and source URLs.

## Limitations

- Telegram bots cannot read arbitrary historical channel messages. `get_previous_messages` returns only posts recorded by this Worker.
- Deduplication covers the previous 48 hours, not the entire lifetime of the channel.
- Semantic duplicate detection is performed by ChatGPT using the returned history; the server enforces exact normalized-text and canonical-URL matches.
- Scheduled execution is provided by the ChatGPT workflow using this MCP server, not by a Cloudflare cron trigger.

## License

Released under the [MIT License](LICENSE).