diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..82065a8 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(bun add:*)", + "Bash(bun run tsc:*)", + "Bash(find:*)", + "Bash(bun run:*)", + "Bash(mkdir:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..49859e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Type check + run: bun run tsc --noEmit + + - name: Build + run: bun run build + + - name: Check build output + run: | + if [ ! -f telegram-mcp ]; then + echo "Build failed: telegram-mcp executable not found" + exit 1 + fi + echo "Build successful: telegram-mcp executable created" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ef445f9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + strategy: + matrix: + include: + - os: ubuntu-latest + target: linux-x64 + build_name: telegram-mcp-linux-x64 + - os: macos-latest + target: darwin-x64 + build_name: telegram-mcp-darwin-x64 + - os: macos-latest + target: darwin-arm64 + build_name: telegram-mcp-darwin-arm64 + - os: windows-latest + target: win-x64 + build_name: telegram-mcp-win-x64.exe + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build executable + run: | + if [ "${{ matrix.os }}" = "windows-latest" ]; then + bun build ./src/main.ts --compile --outfile ${{ matrix.build_name }} --target=bun-${{ matrix.target }} + else + bun build ./src/main.ts --compile --outfile ${{ matrix.build_name }} --target=bun-${{ matrix.target }} + fi + shell: bash + + - name: Create tarball (Unix) + if: matrix.os != 'windows-latest' + run: tar -czf ${{ matrix.build_name }}.tar.gz ${{ matrix.build_name }} README.md + + - name: Create zip (Windows) + if: matrix.os == 'windows-latest' + run: | + Compress-Archive -Path ${{ matrix.build_name }}, README.md -DestinationPath ${{ matrix.build_name }}.zip + shell: pwsh + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.build_name }} + path: | + ${{ matrix.build_name }}.tar.gz + ${{ matrix.build_name }}.zip + + release: + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + telegram-mcp-linux-x64/telegram-mcp-linux-x64.tar.gz + telegram-mcp-darwin-x64/telegram-mcp-darwin-x64.tar.gz + telegram-mcp-darwin-arm64/telegram-mcp-darwin-arm64.tar.gz + telegram-mcp-win-x64.exe/telegram-mcp-win-x64.exe.zip + draft: false + prerelease: false + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a85026c..9ea8178 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,49 @@ +# Dependencies node_modules/ +.pnp +.pnp.js + +# Build outputs +telegram-mcp +telegram-mcp-* +*.exe dist/ -.nyc_output/ -**/.DS_Store -.idea -.vscode +build/ + +# Session data +bot-data/ +session* +*.session + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs *.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# TypeScript *.tsbuildinfo -.env \ No newline at end of file + +# Testing +.nyc_output/ + +# Misc +.cache/ +*.tmp +*.temp +telegram-mcp diff --git a/README.md b/README.md index 1ea0c8d..c6e0e3f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,130 @@ # telegram-mcp -mtcute powered Telegram bot +A Model Context Protocol (MCP) server for interacting with Telegram using mtcute. + +## Features + +- Send text messages to chats +- Read messages from chats +- Search messages +- List and get information about dialogs (chats) +- Get recent messages across all chats + +## Setup + +### Installation + +#### Option 1: Download Pre-built Binary + +Download the latest release for your platform from the [releases page](https://github.com/yourusername/telegram-mcp/releases): + +- **macOS (Apple Silicon)**: `telegram-mcp-darwin-arm64.tar.gz` +- **macOS (Intel)**: `telegram-mcp-darwin-x64.tar.gz` +- **Linux**: `telegram-mcp-linux-x64.tar.gz` +- **Windows**: `telegram-mcp-win-x64.exe.zip` + +Extract the archive and make the binary executable (Unix systems): +```bash +tar -xzf telegram-mcp-*.tar.gz +chmod +x telegram-mcp +``` + +#### Option 2: Build from Source + +1. Clone the repository and install dependencies: + ```bash + git clone https://github.com/yourusername/telegram-mcp.git + cd telegram-mcp + bun install + ``` + +2. Build the executable: + ```bash + bun run build + ``` + +### Initial Setup (First Time Only) + +1. Get your Telegram API credentials from https://my.telegram.org + +2. Run the initial setup to authenticate with Telegram: + ```bash + export API_ID=your_api_id + export API_HASH=your_api_hash + ./telegram-mcp + ``` + + The server will: + - Prompt you to enter your phone number + - Send you a verification code via Telegram + - Ask for the verification code + - Display the absolute storage path (you'll need this for MCP configuration) + +3. Note the storage path displayed in the output. It will look something like: + ``` + Storage path: /Users/username/telegram-mcp/bot-data/session + ``` + +## Usage + +### As an MCP Server + +Add to your Claude Desktop config using the storage path from the initial setup: + +```json +{ + "mcpServers": { + "telegram": { + "command": "/path/to/telegram-mcp", + "env": { + "API_ID": "your_api_id", + "API_HASH": "your_api_hash", + "TELEGRAM_STORAGE_PATH": "/absolute/path/from/initial/setup" + } + } + } +} +``` + +**Important**: The `TELEGRAM_STORAGE_PATH` must be the absolute path shown during initial setup. This ensures the MCP server uses the authenticated session. + +### Available Tools + +#### Message Tools + +- `messages_sendText` - Send a text message to a chat + - `chatId` (required): Chat/User ID or username + - `text` (required): Message text to send + - `replyToMessageId`: Optional message ID to reply to + +- `messages_getHistory` - Get message history from a chat + - `chatId` (required): Chat/User ID or username + - `limit`: Number of messages (default: 100, max: 100) + - `offsetId`: Message ID for pagination + +- `messages_search` - Search for messages + - `query` (required): Search query + - `chatId`: Specific chat to search in (optional) + - `limit`: Number of results (default: 50) + +- `messages_getRecent` - Get recent messages from all chats + - `limit`: Number of chats (default: 10) + - `messagesPerChat`: Messages per chat (default: 10) + +#### Dialog Tools + +- `dialogs_list` - List all dialogs + - `limit`: Maximum dialogs (default: 50) + - `filter`: Filter options (onlyUsers, onlyGroups, onlyChannels) + +- `dialogs_getInfo` - Get detailed dialog information + - `chatId` (required): Chat/User ID or username ## Development +Run in development mode: ```bash -bun install --frozen-lockfile -cp .env.example .env -# edit .env -bun start +bun run dev ``` -*generated with @mtcute/create-bot* \ No newline at end of file +The server stores session data in `bot-data/` directory. \ No newline at end of file diff --git a/bun.lock b/bun.lock index eaaeb69..e9d67c1 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "telegram-mcp", "dependencies": { + "@modelcontextprotocol/sdk": "^1.13.2", "@mtcute/bun": "^0.24.3", }, "devDependencies": { @@ -22,6 +23,8 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.13.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w=="], + "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -44,8 +47,36 @@ "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -54,16 +85,160 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + + "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 5e23b50..4d28e0d 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,11 @@ "packageManager": "bun@1.2.16", "scripts": { "dev": "bun --watch ./src/main.ts", - "start": "bun ./src/main.ts" + "start": "bun ./src/main.ts", + "build": "bun build ./src/main.ts --compile --outfile telegram-mcp" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.13.2", "@mtcute/bun": "^0.24.3" }, "devDependencies": { diff --git a/src/env.ts b/src/env.ts index c19857f..d6437ec 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,10 +1,11 @@ -import process from 'node:process' +import process from 'node:process'; -const API_ID = Number.parseInt(process.env.API_ID!) -const API_HASH = process.env.API_HASH! +const API_ID = Number.parseInt(process.env.API_ID!); +const API_HASH = process.env.API_HASH!; +const STORAGE_PATH = process.env.TELEGRAM_STORAGE_PATH || 'bot-data/session'; if (Number.isNaN(API_ID) || !API_HASH) { - throw new Error('API_ID or API_HASH not set!') + throw new Error('API_ID or API_HASH not set!'); } -export { API_HASH, API_ID } +export { API_HASH, API_ID, STORAGE_PATH }; diff --git a/src/main.ts b/src/main.ts index 157652e..4d96e04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,14 @@ -import { TelegramClient } from '@mtcute/bun' +import { TelegramServer } from './server/telegram-server.js'; -import * as env from './env.ts' +async function main() { + const server = new TelegramServer(); + + try { + await server.start(); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} -const tg = new TelegramClient({ - apiId: env.API_ID, - apiHash: env.API_HASH, - storage: 'bot-data/session', -}) - - -const user = await tg.start() -console.log('Logged in as', user.username) +main().catch(console.error); diff --git a/src/server/telegram-server.ts b/src/server/telegram-server.ts new file mode 100644 index 0000000..c126fc8 --- /dev/null +++ b/src/server/telegram-server.ts @@ -0,0 +1,101 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + ListToolsRequestSchema, + CallToolRequestSchema, + ErrorCode, + McpError, +} from '@modelcontextprotocol/sdk/types.js'; +import { TelegramClient } from '@mtcute/bun'; +import * as path from 'node:path'; +import * as env from '../env.js'; +import { registerTools, handleToolCall } from '../tools/index.js'; + +export class TelegramServer { + private server: Server; + private telegramClient: TelegramClient | null = null; + + constructor() { + this.server = new Server( + { + name: 'telegram-mcp', + version: '0.0.1', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.setupHandlers(); + this.setupErrorHandling(); + } + + private setupHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: registerTools(), + })); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (!this.telegramClient) { + throw new McpError( + ErrorCode.InternalError, + 'Telegram client not initialized' + ); + } + + return handleToolCall(request.params.name, request.params.arguments || {}, this.telegramClient); + }); + } + + private setupErrorHandling() { + process.on('SIGINT', async () => { + await this.cleanup(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + await this.cleanup(); + process.exit(0); + }); + } + + private async cleanup() { + if (this.telegramClient) { + await this.telegramClient.disconnect(); + } + await this.server.close(); + } + + async start() { + // Initialize Telegram client + this.telegramClient = new TelegramClient({ + apiId: env.API_ID, + apiHash: env.API_HASH, + storage: env.STORAGE_PATH, + }); + + try { + // Print storage path for initial setup + const absoluteStoragePath = path.resolve(env.STORAGE_PATH); + console.error(`\n=== Telegram MCP Setup ===`); + console.error(`Storage path: ${absoluteStoragePath}`); + console.error(`\nIf this is your first run, you'll need to authenticate with your phone number.`); + console.error(`After authentication, use the storage path above in your MCP configuration.\n`); + + const user = await this.telegramClient.start(); + console.error(`\nConnected to Telegram as ${user.username || user.id}`); + console.error(`Storage path: ${absoluteStoragePath}`); + console.error(`\nReady to accept MCP requests.`); + } catch (error) { + console.error('Failed to start Telegram client:', error); + throw error; + } + + // Start MCP server + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('Telegram MCP server started'); + } +} \ No newline at end of file diff --git a/src/tools/dialog-tools.ts b/src/tools/dialog-tools.ts new file mode 100644 index 0000000..22a0d76 --- /dev/null +++ b/src/tools/dialog-tools.ts @@ -0,0 +1,196 @@ +import type { TelegramClient, Dialog } from '@mtcute/bun'; +import type { ToolInfo } from './index.js'; + +export const dialogTools: ToolInfo[] = [ + { + name: 'dialogs_list', + description: 'List all dialogs (chats)', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of dialogs to return (default: 50)', + default: 50, + }, + filter: { + type: 'object', + description: 'Filter options', + properties: { + onlyUsers: { + type: 'boolean', + description: 'Only show user chats', + }, + onlyGroups: { + type: 'boolean', + description: 'Only show group chats', + }, + onlyChannels: { + type: 'boolean', + description: 'Only show channels', + }, + }, + }, + }, + }, + }, + { + name: 'dialogs_getInfo', + description: 'Get detailed information about a specific dialog', + inputSchema: { + type: 'object', + properties: { + chatId: { + type: 'string', + description: 'Chat/User ID or username', + }, + }, + required: ['chatId'], + }, + }, +]; + +export async function handleDialogTools( + name: string, + args: any, + client: TelegramClient +) { + switch (name) { + case 'dialogs_list': + return await listDialogs(client, args); + case 'dialogs_getInfo': + return await getDialogInfo(client, args); + default: + throw new Error(`Unknown dialog tool: ${name}`); + } +} + +async function listDialogs(client: TelegramClient, args: any) { + const { limit = 50, filter = {} } = args; + + try { + const dialogs: Dialog[] = []; + let count = 0; + + for await (const dialog of client.iterDialogs()) { + if (count >= limit) break; + + // Apply filters + if (filter.onlyUsers && dialog.peer.type !== 'user') continue; + if (filter.onlyGroups && dialog.peer.type !== 'chat') continue; + if (filter.onlyChannels && dialog.peer.type !== 'chat') continue; + + dialogs.push(dialog); + count++; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + dialogs: dialogs.map(formatDialog), + count: dialogs.length, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error listing dialogs: ${error.message}`, + }, + ], + isError: true, + }; + } +} + +async function getDialogInfo(client: TelegramClient, args: any) { + const { chatId } = args; + + try { + // Find the dialog first + let dialog: Dialog | null = null; + const numericChatId = Number(chatId); + const searchId = Number.isNaN(numericChatId) ? chatId : numericChatId; + + for await (const d of client.iterDialogs()) { + if (d.peer.id === searchId || d.peer.username === chatId) { + dialog = d; + break; + } + } + + if (!dialog) { + throw new Error('Dialog not found'); + } + + // Get full info about the peer + let fullInfo: any = {}; + if (dialog.peer.type === 'user') { + const userFull = await client.getFullUser(dialog.peer); + fullInfo = { + bio: userFull.bio, + commonChatsCount: userFull.commonChatsCount, + isBlocked: userFull.isBlocked, + }; + } else if (dialog.peer.type === 'chat') { + const chatFull = await client.getFullChat(dialog.peer); + fullInfo = { + bio: chatFull.bio, + participantsCount: chatFull.onlineCount || 0, + adminsCount: chatFull.adminsCount, + }; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + peer: { + id: dialog.peer.id, + type: dialog.peer.type, + username: dialog.peer.username, + displayName: dialog.peer.displayName, + }, + dialog: formatDialog(dialog), + fullInfo, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error getting dialog info: ${error.message}`, + }, + ], + isError: true, + }; + } +} + +function formatDialog(dialog: Dialog) { + return { + id: dialog.peer.id, + name: dialog.peer.displayName || `Chat ${dialog.peer.id}`, + username: dialog.peer.username, + type: dialog.peer.type, + unreadCount: dialog.unreadCount, + unreadMentionsCount: dialog.unreadMentionsCount, + isPinned: dialog.isPinned, + isMuted: dialog.isMuted, + lastMessage: dialog.lastMessage ? { + id: dialog.lastMessage.id, + date: dialog.lastMessage.date, + text: dialog.lastMessage.text, + isOutgoing: dialog.lastMessage.isOutgoing, + } : null, + }; +} \ No newline at end of file diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..9804726 --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,35 @@ +import type { TelegramClient } from '@mtcute/bun'; +import { messageTools, handleMessageTools } from './message-tools.js'; +import { dialogTools, handleDialogTools } from './dialog-tools.js'; + +export type ToolInfo = { + name: string; + description: string; + inputSchema: { + type: 'object'; + properties: Record; + required?: string[]; + }; +}; + +export function registerTools(): ToolInfo[] { + return [ + ...messageTools, + ...dialogTools, + ]; +} + +export async function handleToolCall( + name: string, + args: any, + client: TelegramClient +) { + // Route to appropriate handler based on tool name prefix + if (name.startsWith('messages_')) { + return handleMessageTools(name, args, client); + } else if (name.startsWith('dialogs_')) { + return handleDialogTools(name, args, client); + } + + throw new Error(`Unknown tool: ${name}`); +} \ No newline at end of file diff --git a/src/tools/message-tools.ts b/src/tools/message-tools.ts new file mode 100644 index 0000000..ee216b9 --- /dev/null +++ b/src/tools/message-tools.ts @@ -0,0 +1,297 @@ +import type { TelegramClient, Dialog, Message } from '@mtcute/bun'; +import type { ToolInfo } from './index.js'; + +export const messageTools: ToolInfo[] = [ + { + name: 'messages_sendText', + description: 'Send a text message to a chat', + inputSchema: { + type: 'object', + properties: { + chatId: { + type: 'string', + description: 'Chat/User ID or username to send message to', + }, + text: { + type: 'string', + description: 'Message text to send', + }, + replyToMessageId: { + type: 'number', + description: 'Optional message ID to reply to', + }, + }, + required: ['chatId', 'text'], + }, + }, + { + name: 'messages_getHistory', + description: 'Get message history from a chat', + inputSchema: { + type: 'object', + properties: { + chatId: { + type: 'string', + description: 'Chat/User ID or username to get messages from', + }, + limit: { + type: 'number', + description: 'Number of messages to retrieve (default: 100, max: 100)', + default: 100, + }, + offsetId: { + type: 'number', + description: 'Message ID to start from (for pagination)', + }, + }, + required: ['chatId'], + }, + }, + { + name: 'messages_search', + description: 'Search for messages in a chat', + inputSchema: { + type: 'object', + properties: { + chatId: { + type: 'string', + description: 'Chat/User ID or username to search in (optional, searches all chats if not provided)', + }, + query: { + type: 'string', + description: 'Search query', + }, + limit: { + type: 'number', + description: 'Number of messages to retrieve (default: 50)', + default: 50, + }, + }, + required: ['query'], + }, + }, + { + name: 'messages_getRecent', + description: 'Get recent messages from all chats', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Number of recent chats to include (default: 10)', + default: 10, + }, + messagesPerChat: { + type: 'number', + description: 'Number of messages per chat (default: 10)', + default: 10, + }, + }, + }, + }, +]; + +export async function handleMessageTools( + name: string, + args: any, + client: TelegramClient +) { + switch (name) { + case 'messages_sendText': + return await sendTextMessage(client, args); + case 'messages_getHistory': + return await getMessageHistory(client, args); + case 'messages_search': + return await searchMessages(client, args); + case 'messages_getRecent': + return await getRecentMessages(client, args); + default: + throw new Error(`Unknown message tool: ${name}`); + } +} + +async function sendTextMessage(client: TelegramClient, args: any) { + const { chatId, text, replyToMessageId } = args; + + try { + const sentMessage = await client.sendText( + Number.isNaN(Number(chatId)) ? chatId : Number(chatId), + text, + { + replyTo: replyToMessageId ? replyToMessageId : undefined, + } + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: formatMessage(sentMessage), + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error sending message: ${error.message}`, + }, + ], + isError: true, + }; + } +} + +async function getMessageHistory(client: TelegramClient, args: any) { + const { chatId, limit = 100, offsetId } = args; + + try { + const messages = await client.getHistory(Number.isNaN(Number(chatId)) ? chatId : Number(chatId), { + limit: Math.min(limit, 100), + offset: offsetId ? { id: offsetId, date: 0 } : undefined, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + messages: messages.map(formatMessage), + count: messages.length, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error getting message history: ${error.message}`, + }, + ], + isError: true, + }; + } +} + +async function searchMessages(client: TelegramClient, args: any) { + const { chatId, query, limit = 50 } = args; + + try { + const results: Message[] = []; + + if (chatId) { + // Search in specific chat + const messages = await client.searchMessages({ + chatId: Number.isNaN(Number(chatId)) ? chatId : Number(chatId), + query, + limit, + }); + results.push(...messages); + } else { + // Search globally + const messages = await client.searchGlobal({ + query, + limit, + }); + results.push(...messages); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + messages: results.map(formatMessage), + count: results.length, + query, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error searching messages: ${error.message}`, + }, + ], + isError: true, + }; + } +} + +async function getRecentMessages(client: TelegramClient, args: any) { + const { limit = 10, messagesPerChat = 10 } = args; + + try { + const recentChats: Array<{ + dialog: any; + messages: any[]; + }> = []; + + let count = 0; + for await (const dialog of client.iterDialogs()) { + if (count >= limit) break; + + const messages = await client.getHistory(dialog.peer, { + limit: messagesPerChat, + }); + + recentChats.push({ + dialog: { + id: dialog.peer.id, + name: dialog.peer.displayName || `Chat ${dialog.peer.id}`, + username: dialog.peer.username, + type: dialog.peer.type, + lastMessageDate: dialog.lastMessage?.date, + }, + messages: messages.map(formatMessage), + }); + + count++; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + chats: recentChats, + count: recentChats.length, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error getting recent messages: ${error.message}`, + }, + ], + isError: true, + }; + } +} + +function formatMessage(msg: Message) { + return { + id: msg.id, + date: msg.date, + text: msg.text, + senderId: msg.sender.id, + senderName: msg.sender.displayName || msg.sender.username || `User ${msg.sender.id}`, + senderUsername: msg.sender.username, + isOutgoing: msg.isOutgoing, + chatId: msg.chat.id, + chatName: msg.chat.displayName || msg.chat.username || `Chat ${msg.chat.id}`, + }; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 6238e75..552f590 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "module": "ESNext", "moduleResolution": "Bundler", "noEmit": true, - "allowImportingTsExtensions": true, + "allowImportingTsExtensions": false, "target": "es2022", "allowJs": true, "sourceMap": true,