├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actionlint.yaml ├── config.yml └── workflows │ ├── ci.yml │ ├── docker-publish.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTAINER.md ├── CONTRIBUTING.md ├── DEPLOYMENT_SUMMARY.md ├── Dockerfile ├── LICENSE ├── README.md ├── demo ├── .env.example ├── .gitignore ├── GROK_EXAMPLES.md ├── HOW_TO_DEMO.md ├── README.md ├── fixed-tweet.js ├── grok-chat.js ├── index.js ├── mcp-client.js ├── package-lock.json ├── package.json ├── robust-tweet.js ├── run-demo.sh ├── run-fixed-tweet.sh ├── run-simple-demo.sh ├── send-tweet.js ├── setup.sh ├── simple-grok.js ├── simple-menu.js ├── simple-search.js ├── simple-thread.js ├── simple-tweet.js ├── test-twitter-api.js ├── tweet-search.js ├── tweet-thread.js ├── tweets.js └── twitter-api-test.js ├── docker-compose.yml ├── docs ├── AGENT_GUIDE.md ├── DEVELOPER_GUIDE.md ├── IMPLEMENTATION_GUIDE.md └── TESTING.md ├── jest.config.js ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── __tests__ │ ├── integration │ │ └── twitter-integration.test.ts │ └── utils │ │ └── validators.test.ts ├── agent-twitter-client.d.ts ├── authentication.ts ├── health.ts ├── index.ts ├── test-interface.ts ├── test-zod.js ├── test-zod.ts ├── tools │ ├── grok.ts │ ├── profiles.ts │ └── tweets.ts ├── twitter-client.ts ├── types.ts ├── types │ ├── dotenv.d.ts │ ├── modelcontextprotocol.d.ts │ ├── winston.d.ts │ └── zod.d.ts └── utils │ ├── formatters.ts │ ├── logger.ts │ └── validators.ts ├── tests ├── load-test-functions.js └── load-test.yml └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Version control 2 | .git 3 | .gitignore 4 | .github 5 | 6 | # Node.js 7 | node_modules 8 | npm-debug.log 9 | yarn-debug.log 10 | yarn-error.log 11 | 12 | # Environment and configuration 13 | .env 14 | .env.* 15 | *.env 16 | 17 | # Logs 18 | logs 19 | *.log 20 | 21 | # Editor directories and files 22 | .idea 23 | .vscode 24 | *.swp 25 | *.swo 26 | 27 | # OS generated files 28 | .DS_Store 29 | Thumbs.db 30 | 31 | # Documentation 32 | docs 33 | *.md 34 | !README.md 35 | LICENSE 36 | 37 | # Testing 38 | test 39 | __tests__ 40 | *.test.js 41 | *.spec.js 42 | *.test.ts 43 | *.spec.ts 44 | 45 | # Docker 46 | Dockerfile 47 | docker-compose.yml 48 | .dockerignore 49 | 50 | # CI/CD 51 | .github 52 | .gitlab-ci.yml 53 | .travis.yml 54 | .circleci 55 | 56 | # Temporary files 57 | tmp 58 | temp -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # agent-twitter-client-mcp Configuration 2 | 3 | # Port Configuration 4 | # MCP_HOST_PORT: The port on your host machine (what you'll connect to) 5 | # MCP_CONTAINER_PORT: The port inside the Docker container 6 | MCP_HOST_PORT=3001 7 | MCP_CONTAINER_PORT=3000 8 | 9 | # Twitter Authentication (choose one method) 10 | # Method 1: Cookie-based authentication 11 | AUTH_METHOD=cookies 12 | TWITTER_COOKIES=["auth_token=YOUR_AUTH_TOKEN; Domain=.twitter.com", "ct0=YOUR_CT0_VALUE; Domain=.twitter.com", "twid=u%3DYOUR_USER_ID; Domain=.twitter.com"] 13 | 14 | # Method 2: Username/password authentication 15 | # AUTH_METHOD=credentials 16 | # TWITTER_USERNAME=your_username 17 | # TWITTER_PASSWORD=your_password 18 | # TWITTER_EMAIL=your_email@example.com 19 | # TWITTER_2FA_SECRET=your_2fa_secret 20 | 21 | # Method 3: API-based authentication 22 | # AUTH_METHOD=api 23 | # TWITTER_API_KEY=your_api_key 24 | # TWITTER_API_SECRET_KEY=your_api_secret_key 25 | # TWITTER_ACCESS_TOKEN=your_access_token 26 | # TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret 27 | 28 | # NOTE FOR GROK FUNCTIONALITY: 29 | # Grok requires username/password authentication if cookie authentication fails. 30 | # For Grok examples, you can set both TWITTER_COOKIES and TWITTER_USERNAME/TWITTER_PASSWORD 31 | # The examples will try cookie authentication first, then fall back to username/password 32 | # This works regardless of the AUTH_METHOD setting above 33 | 34 | # Logging Configuration 35 | LOG_LEVEL=info # error, warn, info, debug 36 | 37 | # Testing Configuration 38 | RUN_INTEGRATION_TESTS=false 39 | RUN_WRITE_TESTS=false 40 | 41 | # Node.js Environment 42 | NODE_ENV=production -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es2022": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/no-explicit-any": "warn", 20 | "@typescript-eslint/explicit-module-boundary-types": "off", 21 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] 22 | }, 23 | "overrides": [ 24 | { 25 | "files": ["*.d.ts"], 26 | "rules": { 27 | "@typescript-eslint/no-explicit-any": "off" 28 | } 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # Uncomment the relevant lines to enable funding options 4 | # github: [username] # Replace with up to 4 GitHub Sponsors-enabled usernames 5 | # patreon: # Replace with a single Patreon username 6 | # open_collective: # Replace with a single Open Collective username 7 | # ko_fi: # Replace with a single Ko-fi username 8 | # tidelift: # Replace with a single Tidelift platform-name/package-name 9 | # community_bridge: # Replace with a single Community Bridge project-name 10 | # liberapay: # Replace with a single Liberapay username 11 | # issuehunt: # Replace with a single IssueHunt username 12 | # otechie: # Replace with a single Otechie username 13 | # custom: # Replace with up to 4 custom sponsorship URLs 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | ## Bug Description 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## Steps to Reproduce 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected Behavior 21 | 22 | A clear and concise description of what you expected to happen. 23 | 24 | ## Actual Behavior 25 | 26 | What actually happened, including any error messages or screenshots. 27 | 28 | ## Environment 29 | 30 | - OS: [e.g. macOS, Windows, Linux] 31 | - Node.js version: [e.g. 18.15.0] 32 | - npm version: [e.g. 9.5.0] 33 | - agent-twitter-client-mcp version: [e.g. 1.0.0] 34 | 35 | ## Additional Context 36 | 37 | Add any other context about the problem here, such as: 38 | 39 | - Twitter API changes that might be relevant 40 | - Special configuration you're using 41 | - Any workarounds you've tried 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Problem Statement 10 | 11 | A clear and concise description of what problem this feature would solve. For example: "I'm always frustrated when [...]" 12 | 13 | ## Proposed Solution 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | ## Alternative Solutions 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | ## Use Cases 22 | 23 | Describe how this feature would be used and who would benefit from it. 24 | 25 | ## Implementation Ideas (Optional) 26 | 27 | If you have ideas about how to implement this feature, please share them here. 28 | 29 | ## Additional Context 30 | 31 | Add any other context, screenshots, or examples about the feature request here. 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Documentation update 15 | - [ ] Performance improvement 16 | - [ ] Code refactoring (no functional changes) 17 | 18 | ## How Has This Been Tested? 19 | 20 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 21 | 22 | ## Checklist: 23 | 24 | - [ ] My code follows the style guidelines of this project 25 | - [ ] I have performed a self-review of my own code 26 | - [ ] I have commented my code, particularly in hard-to-understand areas 27 | - [ ] I have made corresponding changes to the documentation 28 | - [ ] My changes generate no new warnings 29 | - [ ] I have added tests that prove my fix is effective or that my feature works 30 | - [ ] New and existing unit tests pass locally with my changes 31 | - [ ] Any dependent changes have been merged and published in downstream modules 32 | 33 | ## Screenshots (if appropriate): 34 | -------------------------------------------------------------------------------- /.github/actionlint.yaml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 2 | # Disable warnings about self-hosted runners 3 | labels: off 4 | 5 | secrets: 6 | # Disable warnings about potentially missing secrets 7 | # This is useful when secrets are defined in the GitHub repository but not locally 8 | no-match: off 9 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # GitHub Repository Configuration 2 | # Configuration for new-issue-welcome 3 | newIssueWelcomeComment: >- 4 | Thanks for opening your first issue here! We appreciate your contribution to making this project better. 5 | 6 | # Configuration for new-pr-welcome 7 | newPRWelcomeComment: >- 8 | Thanks for opening this pull request! We'll review it as soon as possible. 9 | 10 | # Configuration for first-pr-merge 11 | firstPRMergeComment: >- 12 | Congratulations on your first merged pull request! We appreciate your contribution! 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: "npm" 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Lint 30 | run: npm run lint 31 | 32 | - name: Build 33 | run: npm run build 34 | 35 | - name: Test 36 | run: npm test 37 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Publish 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["v*"] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v3 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v3 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.repository_owner }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Build and push 28 | uses: docker/build-push-action@v5 29 | with: 30 | context: . 31 | push: true 32 | tags: | 33 | ghcr.io/${{ github.repository }}:latest 34 | ghcr.io/${{ github.repository }}:${{ github.sha }} 35 | cache-from: type=gha 36 | cache-to: type=gha,mode=max 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: "20.x" 17 | registry-url: "https://registry.npmjs.org" 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | - name: Build 23 | run: npm run build 24 | 25 | - name: Publish to npm 26 | run: npm publish 27 | env: 28 | # NPM_TOKEN needs to be added to repository secrets 29 | # This is used by the npm CLI for authentication 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | dist/ 7 | 8 | # Environment variables 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Editor directories and files 23 | .idea/ 24 | .vscode/ 25 | *.swp 26 | *.swo 27 | 28 | # OS generated files 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | Thumbs.db 36 | 37 | # Coverage directory 38 | coverage/ 39 | 40 | # TypeScript cache 41 | *.tsbuildinfo 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files 2 | src/ 3 | 4 | # Development files 5 | .env 6 | .env.example 7 | .eslintrc 8 | .eslintignore 9 | .gitignore 10 | .git 11 | .github 12 | .vscode 13 | .idea 14 | *.log 15 | *.tsbuildinfo 16 | 17 | # Test files 18 | tests/ 19 | jest.config.js 20 | coverage/ 21 | 22 | # Temporary files 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | 27 | # Build process 28 | tsconfig.json 29 | .travis.yml 30 | .gitlab-ci.yml 31 | .github/ 32 | .circleci/ 33 | 34 | # Misc 35 | .DS_Store 36 | Thumbs.db 37 | *.swp 38 | *.swo 39 | prompts/ 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.1.0] - 2025-03-07 9 | 10 | ### Added 11 | - Initial release of agent-twitter-client-mcp 12 | - MCP server for Twitter integration using agent-twitter-client 13 | - Support for cookie-based, username/password, and API authentication 14 | - Tweet operations (fetch, search, post, like, retweet, quote) 15 | - User profile operations (get profile, follow, get followers/following) 16 | - Grok integration for AI chat capabilities 17 | - Interactive test interface 18 | - Comprehensive documentation (README, Developer Guide, Agent Guide, Testing Guide) 19 | - Support for Claude Desktop integration 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | This project adopts the [Contributor Covenant](https://www.contributor-covenant.org) version 2.0. 4 | 5 | Please read the full text at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html) 6 | -------------------------------------------------------------------------------- /CONTAINER.md: -------------------------------------------------------------------------------- 1 | # agent-twitter-client-mcp Docker Image 2 | 3 | This Docker image provides a Model Context Protocol (MCP) server that integrates with Twitter using the `agent-twitter-client` package, allowing AI models to interact with Twitter without direct API access. 4 | 5 | ## Usage 6 | 7 | ### Pull the image 8 | 9 | ```bash 10 | docker pull ghcr.io/ryanmac/agent-twitter-client-mcp:latest 11 | ``` 12 | 13 | ### Run with Docker 14 | 15 | ```bash 16 | docker run -p 3001:3000 \ 17 | -e AUTH_METHOD=cookies \ 18 | -e TWITTER_COOKIES='["auth_token=YOUR_AUTH_TOKEN; Domain=.twitter.com", "ct0=YOUR_CT0_VALUE; Domain=.twitter.com"]' \ 19 | ghcr.io/ryanmac/agent-twitter-client-mcp:latest 20 | ``` 21 | 22 | ### Run with Docker Compose 23 | 24 | Create a `.env` file with your configuration: 25 | 26 | ``` 27 | # Port Configuration 28 | MCP_HOST_PORT=3001 # The port on your host machine 29 | MCP_CONTAINER_PORT=3000 # The port inside the container 30 | 31 | # Twitter Authentication 32 | AUTH_METHOD=cookies 33 | TWITTER_COOKIES=[] 34 | ``` 35 | 36 | Then run: 37 | 38 | ```bash 39 | docker-compose up -d 40 | ``` 41 | 42 | ## Configuration 43 | 44 | ### Environment Variables 45 | 46 | - `PORT`: The port the server listens on inside the container (default: 3000) 47 | - `AUTH_METHOD`: Authentication method (cookies, credentials, or api) 48 | - `TWITTER_COOKIES`: JSON array of Twitter cookies 49 | - `TWITTER_USERNAME`: Twitter username (for credentials auth) 50 | - `TWITTER_PASSWORD`: Twitter password (for credentials auth) 51 | - `TWITTER_EMAIL`: Twitter email (for credentials auth) 52 | - `TWITTER_2FA_SECRET`: Twitter 2FA secret (for credentials auth) 53 | - `TWITTER_API_KEY`: Twitter API key (for API auth) 54 | - `TWITTER_API_SECRET_KEY`: Twitter API secret key (for API auth) 55 | - `TWITTER_ACCESS_TOKEN`: Twitter access token (for API auth) 56 | - `TWITTER_ACCESS_TOKEN_SECRET`: Twitter access token secret (for API auth) 57 | 58 | ## Features 59 | 60 | - Tweet operations (fetch, search, post, like, retweet) 61 | - User operations (profiles, follow, followers) 62 | - Grok integration 63 | 64 | For more information, see the [full documentation](https://github.com/ryanmac/agent-twitter-client-mcp). 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to agent-twitter-client-mcp 2 | 3 | Thank you for your interest in contributing to agent-twitter-client-mcp! This document provides guidelines and instructions for contributing to this project. 4 | 5 | ## Code of Conduct 6 | 7 | By participating in this project, you agree to maintain a respectful and inclusive environment for everyone. 8 | 9 | ## How to Contribute 10 | 11 | ### Reporting Bugs 12 | 13 | If you find a bug, please create an issue on GitHub with the following information: 14 | 15 | 1. A clear, descriptive title 16 | 2. A detailed description of the issue 17 | 3. Steps to reproduce the bug 18 | 4. Expected behavior 19 | 5. Actual behavior 20 | 6. Screenshots (if applicable) 21 | 7. Environment information (OS, Node.js version, etc.) 22 | 23 | ### Suggesting Enhancements 24 | 25 | If you have an idea for an enhancement, please create an issue on GitHub with the following information: 26 | 27 | 1. A clear, descriptive title 28 | 2. A detailed description of the enhancement 29 | 3. The motivation behind the enhancement 30 | 4. Any potential implementation details 31 | 32 | ### Pull Requests 33 | 34 | 1. Fork the repository 35 | 2. Create a new branch (`git checkout -b feature/your-feature-name`) 36 | 3. Make your changes 37 | 4. Run tests (`npm test`) 38 | 5. Run linting (`npm run lint`) 39 | 6. Commit your changes (`git commit -am 'Add some feature'`) 40 | 7. Push to the branch (`git push origin feature/your-feature-name`) 41 | 8. Create a new Pull Request 42 | 43 | ## Development Setup 44 | 45 | 1. Clone the repository 46 | ```bash 47 | git clone https://github.com/ryanmac/agent-twitter-client-mcp.git 48 | cd agent-twitter-client-mcp 49 | ``` 50 | 51 | 2. Install dependencies 52 | ```bash 53 | npm install 54 | ``` 55 | 56 | 3. Create a `.env` file with your Twitter credentials (see README.md for details) 57 | 58 | 4. Build the project 59 | ```bash 60 | npm run build 61 | ``` 62 | 63 | 5. Run tests 64 | ```bash 65 | npm test 66 | ``` 67 | 68 | ## Project Structure 69 | 70 | ``` 71 | agent-twitter-client-mcp/ 72 | ├── src/ # Source code 73 | │ ├── index.ts # Main entry point 74 | │ ├── types.ts # Type definitions 75 | │ ├── authentication.ts # Authentication manager 76 | │ ├── twitter-client.ts # Twitter client wrapper 77 | │ ├── health.ts # Health check functionality 78 | │ ├── test-interface.ts # Interactive testing interface 79 | │ ├── tools/ # MCP tool implementations 80 | │ └── utils/ # Utility functions 81 | ├── docs/ # Documentation 82 | ├── tests/ # Tests 83 | └── build/ # Compiled output (generated) 84 | ``` 85 | 86 | ## Coding Standards 87 | 88 | - Use TypeScript for all code 89 | - Follow the existing code style 90 | - Write tests for new features 91 | - Document new features in the appropriate documentation files 92 | - Use meaningful commit messages 93 | 94 | ## Testing 95 | 96 | - Run `npm test` to run all tests 97 | - Run `npm run lint` to check for linting issues 98 | - Run `npm run test:interface` to test the interactive interface 99 | 100 | ## Documentation 101 | 102 | - Update documentation when adding or changing features 103 | - Follow the existing documentation style 104 | - Keep documentation clear and concise 105 | 106 | ## Release Process 107 | 108 | 1. Update the version in package.json 109 | 2. Update the CHANGELOG.md file 110 | 3. Commit the changes 111 | 4. Create a new tag (`git tag v1.0.0`) 112 | 5. Push the changes and tags (`git push && git push --tags`) 113 | 6. Create a new release on GitHub 114 | 7. Publish to npm (`npm publish`) 115 | 116 | ## Questions? 117 | 118 | If you have any questions, please create an issue on GitHub or reach out to the maintainers directly. 119 | 120 | Thank you for contributing to agent-twitter-client-mcp! 121 | -------------------------------------------------------------------------------- /DEPLOYMENT_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # agent-twitter-client-mcp Deployment Enhancements 2 | 3 | This document summarizes all the enhancements made to prepare the `agent-twitter-client-mcp` project for npm deployment and improve the developer experience. 4 | 5 | ## 1. Package.json Updates 6 | 7 | - Updated version to "0.1.0" 8 | - Added `files` array to specify which files to include in the npm package 9 | - Added `publishConfig` to ensure the package is published to the public npm registry 10 | - Added additional scripts for automation: 11 | - `prepublishOnly`: Runs before publishing to npm 12 | - `postversion`: Runs after version bump 13 | - `prepare`: Runs before the package is packed 14 | - `prepack`: Runs before the package is packed and published 15 | 16 | ## 2. Documentation Improvements 17 | 18 | - Created `.env.example` file with template environment variables 19 | - Updated `README.md` with npm installation instructions and badges 20 | - Created `CONTRIBUTING.md` with guidelines for contributors 21 | - Created `CHANGELOG.md` to track version changes 22 | - Updated `DEVELOPER_GUIDE.md` with VSCode and Docker information 23 | 24 | ## 3. Docker Configuration 25 | 26 | - Created `Dockerfile` with multi-stage build for smaller image size 27 | - Created `docker-compose.yml` for easier deployment 28 | - Created `.dockerignore` to exclude unnecessary files from the Docker build 29 | - Added Docker usage instructions to the README and DEVELOPER_GUIDE 30 | 31 | ## 4. GitHub Actions Workflows 32 | 33 | - Created CI workflow for testing and building the package 34 | - Created publish workflow for publishing to npm when a new release is created 35 | 36 | ## 5. VSCode Configuration 37 | 38 | - Created `.vscode/settings.json` with recommended editor settings 39 | - Created `.vscode/launch.json` with debug configurations 40 | - Created `.vscode/tasks.json` with common tasks 41 | - Created `.vscode/extensions.json` with recommended extensions 42 | 43 | ## 6. npm Publishing Configuration 44 | 45 | - Created `.npmignore` to exclude development files from the npm package 46 | - Added LICENSE file with MIT license 47 | 48 | ## Next Steps 49 | 50 | 1. **Testing**: Test the package by installing it locally with `npm install -g .` 51 | 2. **Version Management**: Use `npm version` to manage version numbers 52 | 3. **Publishing**: Publish to npm with `npm publish` 53 | 4. **CI/CD**: Set up GitHub repository secrets for npm publishing 54 | 5. **Documentation**: Keep documentation up-to-date with changes 55 | 56 | ## Publishing Workflow 57 | 58 | 1. Make changes to the codebase 59 | 2. Update tests and ensure they pass 60 | 3. Update documentation as needed 61 | 4. Update the CHANGELOG.md file 62 | 5. Bump the version with `npm version patch|minor|major` 63 | 6. Push changes and tags to GitHub 64 | 7. Create a new release on GitHub 65 | 8. The GitHub Actions workflow will publish to npm automatically -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a Node.js image for building the server 2 | FROM node:20-alpine AS builder 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy package files first for better caching 8 | COPY package*.json ./ 9 | COPY tsconfig.json ./ 10 | 11 | # Install dependencies without running scripts (to avoid premature build) 12 | RUN npm ci --ignore-scripts 13 | 14 | # Copy source code 15 | COPY src/ ./src/ 16 | 17 | # Build the application 18 | RUN npm run build 19 | 20 | # Use a smaller Node.js image for the runtime 21 | FROM node:20-alpine 22 | 23 | # Set the working directory in the runtime image 24 | WORKDIR /app 25 | 26 | # Copy package files 27 | COPY package*.json ./ 28 | 29 | # Install production dependencies only 30 | # Use --ignore-scripts to prevent running the prepare script which requires tsc 31 | RUN npm ci --omit=dev --ignore-scripts 32 | 33 | # Copy built files from builder stage 34 | COPY --from=builder /app/build ./build 35 | 36 | # Copy documentation 37 | COPY README.md ./ 38 | 39 | # Set environment variables 40 | ENV NODE_ENV=production 41 | ENV PORT=3000 42 | 43 | # Add metadata labels 44 | LABEL org.opencontainers.image.source="https://github.com/ryanmac/agent-twitter-client-mcp" 45 | LABEL org.opencontainers.image.description="MCP server for Twitter integration using agent-twitter-client" 46 | LABEL org.opencontainers.image.licenses="MIT" 47 | LABEL org.opencontainers.image.documentation="https://github.com/ryanmac/agent-twitter-client-mcp" 48 | 49 | # Add healthcheck 50 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 51 | CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT}/health || exit 1 52 | 53 | # Expose the port 54 | EXPOSE ${PORT} 55 | 56 | # Start the application 57 | CMD ["node", "build/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ryanmac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/.env.example: -------------------------------------------------------------------------------- 1 | # Twitter Authentication (choose one method) 2 | 3 | # Method 1: Cookie-based authentication (recommended) 4 | AUTH_METHOD=cookies 5 | # IMPORTANT: For Grok, cookies must be in JSON array format with proper escaping 6 | TWITTER_COOKIES=["auth_token=YOUR_AUTH_TOKEN; Domain=.twitter.com", "ct0=YOUR_CT0_VALUE; Domain=.twitter.com", "twid=u%3DYOUR_USER_ID; Domain=.twitter.com"] 7 | 8 | # Method 2: Username/password authentication 9 | # AUTH_METHOD=credentials 10 | TWITTER_USERNAME=your_username 11 | TWITTER_PASSWORD=your_password 12 | TWITTER_EMAIL=your_email@example.com 13 | # TWITTER_2FA_SECRET=your_2fa_secret 14 | 15 | # Method 3: API-based authentication 16 | # AUTH_METHOD=api 17 | # TWITTER_API_KEY=your_api_key 18 | # TWITTER_API_SECRET_KEY=your_api_secret_key 19 | # TWITTER_ACCESS_TOKEN=your_access_token 20 | # TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret 21 | 22 | # MCP Server Configuration 23 | MCP_PORT=3001 24 | HTTP_PORT=3001 -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | dist/ 7 | 8 | # Environment variables 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | combined.log 22 | error.log 23 | 24 | # Temporary files 25 | last-tweet-id.txt 26 | 27 | # Editor directories and files 28 | .idea/ 29 | .vscode/ 30 | *.swp 31 | *.swo 32 | 33 | # OS generated files 34 | .DS_Store 35 | .DS_Store? 36 | ._* 37 | .Spotlight-V100 38 | .Trashes 39 | ehthumbs.db 40 | Thumbs.db -------------------------------------------------------------------------------- /demo/GROK_EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Twitter Grok AI Examples 2 | 3 | This directory contains examples of how to use the Twitter Grok AI integration through the `agent-twitter-client` library. 4 | 5 | ## Prerequisites 6 | 7 | 1. You need to have valid Twitter authentication (cookies or username/password) 8 | 2. You need to have access to Twitter's Grok AI feature (Premium subscription) 9 | 3. You need to use `agent-twitter-client` version 0.0.19 or higher 10 | 11 | ## Examples 12 | 13 | There are two examples provided: 14 | 15 | 1. **Simple Grok Call** (`simple-grok.js`): Demonstrates a single interaction with Grok AI 16 | 17 | - Sends a single message to Grok and displays the response 18 | - Good for testing basic functionality and authentication 19 | 20 | 2. **Continuous Grok Chat** (`grok-chat.js`): Demonstrates a multi-turn conversation with Grok AI 21 | - Interactive command-line chat interface 22 | - Maintains conversation history 23 | - Handles rate limiting and errors gracefully 24 | 25 | ## Running the Examples 26 | 27 | You can run these examples using the `run-demo.sh` script with the `--use-local-agent-twitter-client` flag to temporarily use version 0.0.19 of the agent-twitter-client: 28 | 29 | ```bash 30 | # Make sure you're in the demo directory 31 | cd demo 32 | 33 | # Run the simple Grok example 34 | ./run-demo.sh --script simple-grok.js --use-local-agent-twitter-client 35 | 36 | # Run the continuous chat Grok example 37 | ./run-demo.sh --script grok-chat.js --use-local-agent-twitter-client 38 | 39 | # Run with environment variable debugging 40 | ./run-demo.sh --script simple-grok.js --use-local-agent-twitter-client --debug-env 41 | ``` 42 | 43 | ## Authentication 44 | 45 | The Grok examples support multiple authentication methods and will try them in the following order: 46 | 47 | 1. **Cookie Authentication**: 48 | 49 | - Environment Variables: Set `TWITTER_COOKIES` in your `demo/.env` file with your Twitter cookies 50 | - Cookie File: Create a `cookies.json` file in the demo directory with your Twitter cookies in the format exported by browser extensions like "Cookie-Editor" 51 | 52 | 2. **Username/Password Authentication**: 53 | - Set `TWITTER_USERNAME` and `TWITTER_PASSWORD` in your `demo/.env` file 54 | 55 | > **Important**: Unlike other Twitter API features, Grok functionality requires username/password authentication if cookie authentication fails. The examples will automatically try both methods regardless of the `AUTH_METHOD` setting in your `.env` file. 56 | 57 | ### Cookie Format Requirements 58 | 59 | The cookie format is critical for successful authentication. For the `TWITTER_COOKIES` environment variable, you must use a properly formatted JSON array of strings: 60 | 61 | ``` 62 | TWITTER_COOKIES=["auth_token=YOUR_AUTH_TOKEN; Domain=.twitter.com", "ct0=YOUR_CT0_VALUE; Domain=.twitter.com", "twid=u%3DYOUR_USER_ID; Domain=.twitter.com"] 63 | ``` 64 | 65 | Key requirements: 66 | 67 | - The entire value must be enclosed in double quotes in your `.env` file 68 | - Each cookie string must be enclosed in double quotes and separated by commas 69 | - The array must be properly formatted with square brackets 70 | - Include the `Domain=.twitter.com` part for each cookie 71 | - Essential cookies are `auth_token`, `ct0`, and `twid` 72 | 73 | ### Obtaining Cookies 74 | 75 | To get your Twitter cookies: 76 | 77 | 1. Log in to Twitter in your browser 78 | 2. Open Developer Tools (F12) 79 | 3. Go to the Application tab > Cookies > https://twitter.com 80 | 4. Copy the values of `auth_token`, `ct0`, and `twid` cookies 81 | 5. Format them as shown above 82 | 83 | ## Environment File Setup 84 | 85 | The Grok examples specifically look for the `.env` file in the `demo` directory, not in the project root. Make sure your authentication credentials are set in `demo/.env`, not in the root `.env` file. 86 | 87 | Example `demo/.env` file: 88 | 89 | ``` 90 | # Cookie-based authentication 91 | AUTH_METHOD=cookies 92 | TWITTER_COOKIES=["auth_token=YOUR_AUTH_TOKEN; Domain=.twitter.com", "ct0=YOUR_CT0_VALUE; Domain=.twitter.com", "twid=u%3DYOUR_USER_ID; Domain=.twitter.com"] 93 | 94 | # Username/password authentication for Grok 95 | TWITTER_USERNAME=your_username 96 | TWITTER_PASSWORD=your_password 97 | TWITTER_EMAIL=your_email@example.com 98 | ``` 99 | 100 | ## Notes on Version Compatibility 101 | 102 | These examples require `agent-twitter-client` version 0.0.19 or higher, which includes the Grok AI integration. The `--use-local-agent-twitter-client` flag in the `run-demo.sh` script temporarily installs this version directly from GitHub without modifying your project's package.json, allowing you to test the Grok functionality without breaking CI processes that depend on version 0.0.18. 103 | 104 | > **Note**: Currently, agent-twitter-client v0.0.19 is available on GitHub but not yet on npm. The demo scripts will automatically install it from GitHub when needed. 105 | 106 | ## Common Errors and Troubleshooting 107 | 108 | ### Authentication Errors 109 | 110 | 1. **Cookie Format Issues**: 111 | 112 | - Error: "Cookie login check result: false" 113 | - Solution: Ensure your cookies are in the correct JSON array format 114 | - Try running with `--debug-env` to see how your cookies are being parsed 115 | 116 | 2. **Cloudflare Protection**: 117 | 118 | - Error: "Failed to login with credentials: ..." (HTML response) 119 | - Cause: Cloudflare is blocking automated login attempts 120 | - Solution: Use cookie authentication instead, or try again later 121 | 122 | 3. **Missing Credentials**: 123 | - Error: "Not logged in to Twitter. Please check your cookies or credentials." 124 | - Solution: Ensure you have set either `TWITTER_COOKIES` or `TWITTER_USERNAME`/`TWITTER_PASSWORD` in your `demo/.env` file 125 | 126 | ### Grok-Specific Errors 127 | 128 | 1. **Rate Limiting**: 129 | 130 | - Error: "Rate Limited: You've reached the limit..." 131 | - Cause: Grok has a limit of 25 messages per 2 hours for non-premium accounts 132 | - Solution: Wait until the rate limit resets or upgrade to a premium account 133 | 134 | 2. **Missing Grok Methods**: 135 | 136 | - Error: "scraper.createGrokConversation is not a function" 137 | - Cause: Using an older version of agent-twitter-client 138 | - Solution: Make sure you're using the `--use-local-agent-twitter-client` flag 139 | 140 | 3. **Premium Subscription Required**: 141 | - Error: "You need to be a Premium subscriber to use Grok" 142 | - Solution: Upgrade your Twitter account to a premium subscription 143 | 144 | ## Example Output 145 | 146 | ### Simple Grok Call 147 | 148 | ``` 149 | Current directory: /Users/username/project/demo 150 | Using .env file: /Users/username/project/demo/.env 151 | Loading environment from: /Users/username/project/demo/.env 152 | Environment variables loaded: 153 | AUTH_METHOD: cookies 154 | TWITTER_COOKIES: [Set] 155 | TWITTER_USERNAME: [Set] 156 | TWITTER_PASSWORD: [Set] 157 | TWITTER_EMAIL: [Set] 158 | Scraper initialized successfully 159 | Using cookies from environment variables... 160 | Cookie format check: 161 | Cookies appear to be in JSON array format 162 | Found 3 cookies in JSON array 163 | Cookie login check result: true 164 | Creating a new Grok conversation... 165 | Conversation created with ID: 1234567890abcdef 166 | Sending a message to Grok... 167 | 168 | --- Grok Response --- 169 | Here are the current trending topics on Twitter: 170 | 171 | 1. #WorldNewsToday 172 | 2. #TechUpdates 173 | 3. #SportsHighlights 174 | 4. #EntertainmentNews 175 | 5. #PoliticalDiscussion 176 | 177 | These trends are based on real-time Twitter activity and may vary by region and time. 178 | --------------------- 179 | ``` 180 | 181 | ### Continuous Grok Chat 182 | 183 | ``` 184 | Current directory: /Users/username/project/demo 185 | Using .env file: /Users/username/project/demo/.env 186 | Loading environment from: /Users/username/project/demo/.env 187 | Environment variables loaded: 188 | AUTH_METHOD: cookies 189 | TWITTER_COOKIES: [Set] 190 | TWITTER_USERNAME: [Set] 191 | TWITTER_PASSWORD: [Set] 192 | TWITTER_EMAIL: [Set] 193 | Scraper initialized successfully 194 | Using cookies from environment variables... 195 | Cookie format check: 196 | Cookies appear to be in JSON array format 197 | Found 3 cookies in JSON array 198 | Cookie login check result: true 199 | 200 | === Twitter Grok AI Chat === 201 | Type your messages to chat with Grok. Type "exit" to quit. 202 | 203 | Creating a new Grok conversation... 204 | Conversation created with ID: 1234567890abcdef 205 | 206 | You: What are the trending topics on Twitter right now? 207 | 208 | Grok is thinking... 209 | 210 | Grok: Based on Twitter's current data, the trending topics include: 211 | 212 | 1. #WorldNewsToday - Global news events 213 | 2. #TechUpdates - Technology news and product launches 214 | 3. #SportsHighlights - Major sports events and results 215 | 4. #EntertainmentNews - Celebrity and entertainment updates 216 | 5. #PoliticalDiscussion - Political debates and news 217 | 218 | These trends are dynamic and can vary by region and time of day. Is there a specific trend you'd like to know more about? 219 | 220 | -------------------------------------------------- 221 | 222 | You: Tell me more about the tech updates 223 | ... 224 | ``` 225 | 226 | ## Grok Capabilities 227 | 228 | Grok on Twitter has access to real-time Twitter data that even the standalone Grok API doesn't have. This means you can ask Grok about: 229 | 230 | - Current trending topics on Twitter 231 | - Analysis of recent tweets on specific subjects 232 | - Information about Twitter users and their content 233 | - Real-time events being discussed on the platform 234 | 235 | Example queries: 236 | 237 | - "What are the trending topics on Twitter right now?" 238 | - "Analyze the sentiment around AI on Twitter" 239 | - "What are people saying about the latest Apple event?" 240 | - "Show me information about popular memecoins being discussed today" 241 | -------------------------------------------------------------------------------- /demo/HOW_TO_DEMO.md: -------------------------------------------------------------------------------- 1 | # How to Run the Twitter MCP Demo 2 | 3 | This guide explains how to run the Twitter MCP demo, including the fixed version that handles character limits and error cases properly, as well as the Grok AI integration examples. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js installed 8 | - npm installed 9 | - Twitter credentials (cookies, username/password, or API keys) 10 | - For Grok functionality: Twitter Premium subscription and agent-twitter-client v0.0.19+ 11 | 12 | ## Setup 13 | 14 | 1. Make sure you have the required environment variables set in a `.env` file. If you don't have one, the demo will create one from `.env.example`. 15 | 16 | 2. Make sure the scripts are executable: 17 | ```bash 18 | chmod +x run-demo.sh run-fixed-tweet.sh 19 | ``` 20 | 21 | ## Running the Demo 22 | 23 | ### Option 1: Interactive Menu 24 | 25 | To run the interactive menu demo: 26 | 27 | ```bash 28 | ./run-demo.sh 29 | ``` 30 | 31 | This will start the main demo menu where you can choose from various options, including the Grok AI examples. 32 | 33 | ### Option 2: Fixed Tweet Demo 34 | 35 | To run the fixed tweet demo directly: 36 | 37 | ```bash 38 | ./run-fixed-tweet.sh 39 | ``` 40 | 41 | This will start the fixed tweet demo that includes: 42 | 43 | - Character limit validation 44 | - Improved error handling 45 | - Better process management 46 | - Reply functionality with metadata warnings 47 | 48 | ### Option 3: Grok AI Examples 49 | 50 | To run the Grok AI examples: 51 | 52 | ```bash 53 | # Simple Grok example (single interaction) 54 | ./run-demo.sh --script simple-grok.js --use-local-agent-twitter-client 55 | 56 | # Interactive Grok chat example 57 | ./run-demo.sh --script grok-chat.js --use-local-agent-twitter-client 58 | ``` 59 | 60 | The `--use-local-agent-twitter-client` flag is required as Grok functionality is only available in agent-twitter-client v0.0.19 or higher, which will be temporarily installed from GitHub. 61 | 62 | ### Debug Mode 63 | 64 | To enable debug mode for more detailed logging: 65 | 66 | ```bash 67 | ./run-demo.sh --debug 68 | ``` 69 | 70 | To debug environment variables and authentication: 71 | 72 | ```bash 73 | ./run-demo.sh --debug-env 74 | ``` 75 | 76 | ### Running Specific Scripts 77 | 78 | To run a specific script directly: 79 | 80 | ```bash 81 | ./run-demo.sh --script fixed-tweet.js 82 | ``` 83 | 84 | ## Available Scripts 85 | 86 | ### Tweet Operations 87 | 88 | - `fixed-tweet.js`: The fixed version of the tweet sender with improved error handling 89 | - `send-tweet.js`: The original tweet sender 90 | - `tweet-search.js`: Search for tweets from specific users 91 | - `tweet-thread.js`: Send a thread of tweets 92 | - `simple-tweet.js`: A simplified tweet demo 93 | 94 | ### Grok AI Operations 95 | 96 | - `simple-grok.js`: A simple example of using Twitter's Grok AI for a single interaction 97 | - `grok-chat.js`: An interactive chat example with Twitter's Grok AI 98 | 99 | ## Authentication for Grok 100 | 101 | Grok examples require proper authentication. They support two methods: 102 | 103 | 1. **Cookie Authentication**: 104 | 105 | - Cookies must be in JSON array format in your `.env` file 106 | - Example: `TWITTER_COOKIES=["auth_token=YOUR_AUTH_TOKEN; Domain=.twitter.com", "ct0=YOUR_CT0_VALUE; Domain=.twitter.com"]` 107 | 108 | 2. **Username/Password Authentication**: 109 | - Set `TWITTER_USERNAME` and `TWITTER_PASSWORD` in your `.env` file 110 | - This is used as a fallback if cookie authentication fails 111 | 112 | **Important**: The Grok examples specifically look for the `.env` file in the demo directory, not in the project root. 113 | 114 | ## Troubleshooting 115 | 116 | ### Character Limit Errors 117 | 118 | Twitter has a 280 character limit for tweets. The fixed demo will warn you if your tweet exceeds this limit. 119 | 120 | When replying to tweets, Twitter adds metadata (like @username) that counts toward this limit. The fixed demo will warn you about this and give you the option to continue or abort. 121 | 122 | ### Process Exit Errors 123 | 124 | If the MCP process exits unexpectedly, the fixed demo will attempt to restart it automatically. 125 | 126 | ### Path Issues 127 | 128 | If you see "command not found" errors, make sure you're running the scripts from the demo directory. 129 | 130 | ### EPIPE Errors 131 | 132 | These can occur if the MCP process is terminated unexpectedly. The fixed demo includes better error handling for these cases. 133 | 134 | ### Grok-Specific Issues 135 | 136 | 1. **Version Issues**: If you see errors about missing Grok methods, make sure you're using the `--use-local-agent-twitter-client` flag. 137 | 138 | 2. **Authentication Issues**: 139 | 140 | - Cookie format must be correct (JSON array) 141 | - If using username/password, you may encounter Cloudflare protection 142 | - Premium subscription is required for Grok access 143 | 144 | 3. **Rate Limits**: Grok has rate limits (typically 25 messages per 2 hours for non-premium accounts). 145 | 146 | ## Help 147 | 148 | For more information about the available options: 149 | 150 | ```bash 151 | ./run-demo.sh --help 152 | ``` 153 | 154 | or 155 | 156 | ```bash 157 | ./run-fixed-tweet.sh --help 158 | ``` 159 | 160 | For more details on the Grok examples, see `GROK_EXAMPLES.md`. 161 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # agent-twitter-client-mcp Demo 2 | 3 | This directory contains a demo project for the `agent-twitter-client-mcp` package, which allows you to interact with Twitter through a Machine Communication Protocol (MCP) server. 4 | 5 | ## Features 6 | 7 | - Start a new MCP server or connect to an existing one 8 | - Search for tweets from specific users 9 | - List available tools 10 | - Send tweets and reply to existing tweets 11 | - Handle port conflicts gracefully 12 | - Robust error handling and automatic reconnection 13 | - Twitter Grok AI integration (requires agent-twitter-client v0.0.19+) 14 | 15 | ## Usage Options 16 | 17 | ### Option 1: Start a new MCP server 18 | 19 | ```javascript 20 | const client = new McpClient({ 21 | port: 3001, 22 | debug: true, 23 | startServer: true, // Default is true 24 | }); 25 | 26 | await client.start(); 27 | ``` 28 | 29 | ### Option 2: Connect to an existing MCP server 30 | 31 | ```javascript 32 | // First, start the MCP server in a separate process 33 | // PORT=3001 npx agent-twitter-client-mcp 34 | 35 | const client = new McpClient({ 36 | port: 3001, 37 | debug: true, 38 | startServer: false, // Connect to existing server 39 | }); 40 | 41 | await client.start(); 42 | ``` 43 | 44 | ## Example Scripts 45 | 46 | The demo includes several scripts that demonstrate different aspects of the MCP client: 47 | 48 | ### Tweet Operations 49 | 50 | - `tweet-search.js`: Search for tweets from specific users and save tweet IDs for later replying 51 | - `send-tweet.js`: Send a single tweet or reply to an existing tweet 52 | - `tweet-thread.js`: Send a thread of tweets with proper reply chains 53 | - `simple-tweet.js`: A simplified example of sending a tweet 54 | - `fixed-tweet.js`: An improved tweet sender with better error handling and character limit validation 55 | 56 | ### Grok AI Operations 57 | 58 | - `simple-grok.js`: Demonstrates a single interaction with Twitter's Grok AI 59 | - `grok-chat.js`: Demonstrates a multi-turn conversation with Twitter's Grok AI 60 | 61 | ## Running the Demo 62 | 63 | ### Interactive Menu 64 | 65 | For the best experience, use the interactive menu: 66 | 67 | ```bash 68 | cd demo 69 | ./run-demo.sh 70 | ``` 71 | 72 | ### Running Specific Scripts 73 | 74 | To run a specific script: 75 | 76 | ```bash 77 | # Run a tweet-related script 78 | ./run-demo.sh --script fixed-tweet.js 79 | 80 | # Run a Grok-related script (requires agent-twitter-client v0.0.19+) 81 | ./run-demo.sh --script simple-grok.js --use-local-agent-twitter-client 82 | ./run-demo.sh --script grok-chat.js --use-local-agent-twitter-client 83 | ``` 84 | 85 | ### Debug Options 86 | 87 | ```bash 88 | # Enable debug mode for detailed logging 89 | ./run-demo.sh --debug 90 | 91 | # Debug environment variables and authentication 92 | ./run-demo.sh --debug-env 93 | ``` 94 | 95 | > **Note**: The Grok examples require agent-twitter-client v0.0.19, which is currently available on GitHub but not yet on npm. The demo scripts will automatically install it from GitHub when needed. 96 | 97 | See `GROK_EXAMPLES.md` for more details on the Grok integration. 98 | 99 | ## Grok AI Integration 100 | 101 | The Grok AI integration allows you to interact with Twitter's Grok AI chatbot through the same interface used for other Twitter operations. This provides several unique capabilities: 102 | 103 | 1. **Access to Twitter's Real-Time Data**: Grok on Twitter has access to real-time platform data that even the standalone Grok API doesn't have. 104 | 105 | 2. **Trending Topics Analysis**: Ask Grok about current trending topics and get detailed analysis. 106 | 107 | 3. **User and Content Insights**: Get insights about Twitter users and their content. 108 | 109 | 4. **Conversation Management**: Maintain multi-turn conversations with proper context. 110 | 111 | ### Authentication for Grok 112 | 113 | Grok examples support two authentication methods: 114 | 115 | 1. **Cookie Authentication**: 116 | 117 | - Cookies must be in JSON array format in your `demo/.env` file 118 | - Example: `TWITTER_COOKIES=["auth_token=YOUR_AUTH_TOKEN; Domain=.twitter.com", "ct0=YOUR_CT0_VALUE; Domain=.twitter.com"]` 119 | 120 | 2. **Username/Password Authentication**: 121 | - Set `TWITTER_USERNAME` and `TWITTER_PASSWORD` in your `demo/.env` file 122 | - This is used as a fallback if cookie authentication fails 123 | 124 | **Important**: The Grok examples specifically look for the `.env` file in the demo directory, not in the project root. 125 | 126 | ## Searching for Tweets 127 | 128 | To search for tweets from a specific user: 129 | 130 | ```javascript 131 | const request = { 132 | jsonrpc: "2.0", 133 | id: "unique-id", 134 | method: "tools/call", 135 | params: { 136 | name: "get_user_tweets", 137 | arguments: { 138 | username: "twitter_username", 139 | count: 3, // Number of tweets to retrieve 140 | includeReplies: false, // Whether to include replies 141 | includeRetweets: true, // Whether to include retweets 142 | }, 143 | }, 144 | }; 145 | ``` 146 | 147 | The response will contain tweet data including: 148 | 149 | - Tweet ID 150 | - Text content 151 | - Author information 152 | - Creation timestamp 153 | - Engagement metrics (likes, retweets, replies) 154 | - Media attachments 155 | - Tweet type (regular, reply, retweet, quote) 156 | 157 | ## Sending Tweets 158 | 159 | When sending a tweet, use the following format: 160 | 161 | ```javascript 162 | const request = { 163 | jsonrpc: "2.0", 164 | id: "unique-id", 165 | method: "tools/call", 166 | params: { 167 | name: "send_tweet", 168 | arguments: { 169 | text: "Your tweet text here", 170 | }, 171 | }, 172 | }; 173 | ``` 174 | 175 | For replying to a tweet, add the `replyToTweetId` parameter: 176 | 177 | ```javascript 178 | request.params.arguments.replyToTweetId = "tweet-id-to-reply-to"; 179 | ``` 180 | 181 | ## Setup 182 | 183 | 1. Run the setup script: 184 | 185 | ```bash 186 | ./setup.sh 187 | ``` 188 | 189 | 2. Edit the `.env` file with your Twitter credentials 190 | 191 | 3. Run the demo: 192 | 193 | ```bash 194 | npm start 195 | ``` 196 | 197 | ## Troubleshooting 198 | 199 | If you encounter issues: 200 | 201 | 1. **Port Conflicts**: The client will automatically try different ports if the default port is in use. 202 | 203 | 2. **Connection Issues**: If you're connecting to an existing server, make sure it's running before starting the client. 204 | 205 | 3. **Response Parsing Issues**: If you see "No tweets found" but the debug output shows tweets, there might be an issue with parsing the response format. Try running with debug mode enabled to see the raw response: 206 | 207 | ```bash 208 | DEBUG=true ./run-demo.sh 209 | ``` 210 | 211 | 4. **EPIPE Errors**: These can occur if the MCP process is terminated unexpectedly. The client includes automatic reconnection logic. 212 | 213 | 5. **Debug Mode**: Enable debug mode for more detailed logging: 214 | 215 | ```javascript 216 | const client = new McpClient({ debug: true }); 217 | ``` 218 | 219 | 6. **Grok-Specific Issues**: See the `GROK_EXAMPLES.md` file for detailed troubleshooting of Grok-related issues. 220 | 221 | ## Files 222 | 223 | ### Core Files 224 | 225 | - `mcp-client.js`: A client library for interacting with the MCP server 226 | - `index.js`: A menu-driven interface for the demo 227 | - `setup.sh`: A script for setting up the demo 228 | - `run-demo.sh`: A script for running the demo 229 | - `run-fixed-tweet.sh`: A script for running the fixed tweet demo 230 | 231 | ### Tweet-Related Files 232 | 233 | - `tweet-search.js`: Search for tweets from specific users 234 | - `send-tweet.js`: A script for sending a single tweet 235 | - `tweet-thread.js`: A script for sending a thread of tweets 236 | - `simple-tweet.js`: A simplified tweet demo 237 | - `fixed-tweet.js`: The fixed version of the tweet sender with improved error handling 238 | - `tweets.js`: A collection of pre-written tweets 239 | 240 | ### Grok-Related Files 241 | 242 | - `simple-grok.js`: A simple example of using Twitter's Grok AI 243 | - `grok-chat.js`: An interactive chat example with Twitter's Grok AI 244 | 245 | ### Documentation Files 246 | 247 | - `README.md`: This file 248 | - `HOW_TO_DEMO.md`: Detailed instructions for running the demo 249 | - `GROK_EXAMPLES.md`: Documentation for the Grok AI examples 250 | - `.env.example`: An example environment file 251 | -------------------------------------------------------------------------------- /demo/grok-chat.js: -------------------------------------------------------------------------------- 1 | import { Scraper } from "agent-twitter-client"; 2 | import fs from "fs"; 3 | import dotenv from "dotenv"; 4 | import readline from "readline/promises"; 5 | import path from "path"; 6 | 7 | // Get the current directory 8 | const currentDir = process.cwd(); 9 | console.log(`Current directory: ${currentDir}`); 10 | 11 | // Load environment variables from demo/.env file 12 | const envPath = path.join(currentDir, ".env"); 13 | console.log(`Loading environment from: ${envPath}`); 14 | dotenv.config({ path: envPath }); 15 | 16 | // Debug environment variables (with sensitive info masked) 17 | console.log("Environment variables loaded:"); 18 | console.log("AUTH_METHOD:", process.env.AUTH_METHOD || "not set"); 19 | console.log( 20 | "TWITTER_COOKIES:", 21 | process.env.TWITTER_COOKIES ? "[Set]" : "not set" 22 | ); 23 | console.log( 24 | "TWITTER_USERNAME:", 25 | process.env.TWITTER_USERNAME ? "[Set]" : "not set" 26 | ); 27 | console.log( 28 | "TWITTER_PASSWORD:", 29 | process.env.TWITTER_PASSWORD ? "[Set]" : "not set" 30 | ); 31 | console.log("TWITTER_EMAIL:", process.env.TWITTER_EMAIL ? "[Set]" : "not set"); 32 | 33 | let scraper = null; 34 | 35 | async function initializeScraper() { 36 | try { 37 | scraper = new Scraper(); 38 | console.log("Scraper initialized successfully"); 39 | 40 | // Check authentication method 41 | const authMethod = process.env.AUTH_METHOD || "cookies"; 42 | 43 | // For Grok, we need to handle both cookie-based and credential-based auth 44 | let isLoggedIn = false; 45 | 46 | // Try cookie authentication first 47 | if (process.env.TWITTER_COOKIES) { 48 | console.log("Using cookies from environment variables..."); 49 | 50 | // Debug the cookie format 51 | console.log("Cookie format check:"); 52 | try { 53 | // Check if it's a JSON array 54 | if ( 55 | process.env.TWITTER_COOKIES.startsWith("[") && 56 | process.env.TWITTER_COOKIES.endsWith("]") 57 | ) { 58 | console.log("Cookies appear to be in JSON array format"); 59 | const parsedCookies = JSON.parse(process.env.TWITTER_COOKIES); 60 | console.log(`Found ${parsedCookies.length} cookies in JSON array`); 61 | 62 | // Use the parsed cookies directly 63 | await scraper.setCookies(parsedCookies); 64 | } else { 65 | // Assume it's a semicolon-separated string 66 | console.log("Cookies appear to be in semicolon-separated format"); 67 | const formattedCookies = process.env.TWITTER_COOKIES.split(";").map( 68 | (cookie) => cookie.trim() 69 | ); 70 | console.log(`Found ${formattedCookies.length} cookies in string`); 71 | await scraper.setCookies(formattedCookies); 72 | } 73 | } catch (error) { 74 | console.error("Error parsing cookies:", error.message); 75 | console.log("Trying to use cookies as-is..."); 76 | await scraper.setCookies([process.env.TWITTER_COOKIES]); 77 | } 78 | 79 | // Check if we're logged in with cookies 80 | isLoggedIn = await scraper.isLoggedIn(); 81 | console.log("Cookie login check result:", isLoggedIn); 82 | } else { 83 | // Try to load cookies from a file 84 | try { 85 | console.log("Loading cookies from file..."); 86 | const cookiesJson = JSON.parse( 87 | fs.readFileSync("./cookies.json", "utf-8") 88 | ); 89 | console.log(`Found ${cookiesJson.length} cookies`); 90 | 91 | if (cookiesJson.length > 0) { 92 | // Format cookies as strings in the Set-Cookie header format 93 | const formattedCookies = cookiesJson.map((cookie) => { 94 | let cookieString = `${cookie.key}=${cookie.value}`; 95 | cookieString += `; Domain=${cookie.domain}`; 96 | cookieString += `; Path=${cookie.path}`; 97 | if (cookie.expires) cookieString += `; Expires=${cookie.expires}`; 98 | if (cookie.secure) cookieString += "; Secure"; 99 | if (cookie.httpOnly) cookieString += "; HttpOnly"; 100 | if (cookie.sameSite) 101 | cookieString += `; SameSite=${cookie.sameSite}`; 102 | return cookieString; 103 | }); 104 | 105 | console.log("Setting cookies in scraper..."); 106 | await scraper.setCookies(formattedCookies); 107 | 108 | // Check if we're logged in with cookies 109 | isLoggedIn = await scraper.isLoggedIn(); 110 | console.log("Cookie login check result:", isLoggedIn); 111 | } 112 | } catch (error) { 113 | console.log( 114 | "No valid cookies found or error loading cookies:", 115 | error.message 116 | ); 117 | } 118 | } 119 | 120 | // If cookie authentication failed, try credential-based authentication 121 | if ( 122 | !isLoggedIn && 123 | process.env.TWITTER_USERNAME && 124 | process.env.TWITTER_PASSWORD 125 | ) { 126 | console.log( 127 | "Cookie authentication failed or not available. Trying username/password login..." 128 | ); 129 | try { 130 | console.log(`Using username: ${process.env.TWITTER_USERNAME}`); 131 | 132 | // Debug the login parameters (without showing the actual password) 133 | console.log("Login parameters:"); 134 | console.log("Username:", process.env.TWITTER_USERNAME); 135 | console.log( 136 | "Password:", 137 | process.env.TWITTER_PASSWORD ? "[Set]" : "[Not Set]" 138 | ); 139 | console.log("Email:", process.env.TWITTER_EMAIL || "[Not Set]"); 140 | 141 | await scraper.login( 142 | process.env.TWITTER_USERNAME, 143 | process.env.TWITTER_PASSWORD 144 | ); 145 | isLoggedIn = await scraper.isLoggedIn(); 146 | console.log("Credential login check result:", isLoggedIn); 147 | } catch (error) { 148 | console.error("Failed to login with credentials:", error); 149 | } 150 | } 151 | 152 | // Final check if we're logged in 153 | if (!isLoggedIn) { 154 | console.error( 155 | "Not logged in to Twitter. Please check your cookies or credentials." 156 | ); 157 | console.log( 158 | "For Grok functionality, you need to provide valid Twitter credentials." 159 | ); 160 | console.log( 161 | "Please set TWITTER_USERNAME and TWITTER_PASSWORD in your demo/.env file." 162 | ); 163 | process.exit(1); 164 | } 165 | 166 | return scraper; 167 | } catch (error) { 168 | console.error("Failed to initialize Twitter scraper:", error); 169 | throw error; 170 | } 171 | } 172 | 173 | async function main() { 174 | try { 175 | await initializeScraper(); 176 | 177 | const rl = readline.createInterface({ 178 | input: process.stdin, 179 | output: process.stdout, 180 | }); 181 | 182 | console.log("\n=== Twitter Grok AI Chat ==="); 183 | console.log('Type your messages to chat with Grok. Type "exit" to quit.\n'); 184 | 185 | // Create a new conversation 186 | console.log("Creating a new Grok conversation..."); 187 | const conversationId = await scraper.createGrokConversation(); 188 | console.log(`Conversation created with ID: ${conversationId}\n`); 189 | 190 | // Store message history 191 | const messages = []; 192 | 193 | // Chat loop 194 | let chatActive = true; 195 | while (chatActive) { 196 | const userInput = await rl.question("You: "); 197 | 198 | // Exit condition 199 | if (userInput.toLowerCase() === "exit") { 200 | console.log("Exiting chat..."); 201 | chatActive = false; 202 | break; 203 | } 204 | 205 | // Add user message to history 206 | messages.push({ role: "user", content: userInput }); 207 | 208 | console.log("\nGrok is thinking..."); 209 | 210 | try { 211 | // Send message to Grok 212 | const response = await scraper.grokChat({ 213 | conversationId, 214 | messages, 215 | }); 216 | 217 | // Check for rate limiting 218 | if (response.rateLimit?.isRateLimited) { 219 | console.log("\nRate Limited:", response.rateLimit.message); 220 | if (response.rateLimit.upsellInfo) { 221 | console.log("\nLimit Info:"); 222 | console.log( 223 | `Usage Limit: ${response.rateLimit.upsellInfo.usageLimit}` 224 | ); 225 | console.log( 226 | `Duration: ${response.rateLimit.upsellInfo.quotaDuration}` 227 | ); 228 | console.log(`${response.rateLimit.upsellInfo.title}`); 229 | console.log(`${response.rateLimit.upsellInfo.message}`); 230 | } 231 | chatActive = false; 232 | break; 233 | } 234 | 235 | // Add Grok's response to message history 236 | messages.push({ role: "assistant", content: response.message }); 237 | 238 | // Display Grok's response 239 | console.log("\nGrok:", response.message); 240 | console.log("\n" + "-".repeat(50) + "\n"); 241 | } catch (error) { 242 | console.error("\nError sending message to Grok:", error); 243 | console.log("Trying to continue the conversation..."); 244 | } 245 | } 246 | 247 | rl.close(); 248 | } catch (error) { 249 | console.error("Failed to run Grok chat:", error); 250 | } 251 | } 252 | 253 | main(); 254 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import readlineSync from "readline-sync"; 2 | import { spawn } from "child_process"; 3 | import dotenv from "dotenv"; 4 | import fs from "fs"; 5 | import { execSync } from "child_process"; 6 | 7 | // Load environment variables 8 | dotenv.config(); 9 | 10 | // Check if agent-twitter-client v0.0.19 is available 11 | function checkAgentTwitterClientVersion() { 12 | try { 13 | const packageJson = JSON.parse( 14 | fs.readFileSync( 15 | "./node_modules/agent-twitter-client/package.json", 16 | "utf-8" 17 | ) 18 | ); 19 | return packageJson.version === "0.0.19"; 20 | } catch (error) { 21 | return false; 22 | } 23 | } 24 | 25 | function runScript(scriptPath, env = {}) { 26 | return new Promise((resolve, reject) => { 27 | const childProcess = spawn("node", [scriptPath], { 28 | stdio: "inherit", 29 | env: { ...process.env, ...env }, 30 | }); 31 | 32 | childProcess.on("close", (code) => { 33 | if (code === 0) { 34 | resolve(); 35 | } else { 36 | reject(new Error(`Script exited with code ${code}`)); 37 | } 38 | }); 39 | }); 40 | } 41 | 42 | // Function to temporarily install agent-twitter-client v0.0.19 43 | async function installAgentTwitterClientV0019() { 44 | console.log( 45 | "Installing agent-twitter-client v0.0.19 temporarily from GitHub..." 46 | ); 47 | try { 48 | // Try the first installation method 49 | try { 50 | execSync( 51 | "npm install --no-save github:elizaOS/agent-twitter-client#0.0.19", 52 | { stdio: "inherit" } 53 | ); 54 | console.log("Installation successful!"); 55 | return true; 56 | } catch (error) { 57 | console.log("First installation method failed, trying alternative..."); 58 | // Try the alternative installation method 59 | execSync( 60 | "npm install --no-save https://github.com/elizaOS/agent-twitter-client/tarball/0.0.19", 61 | { stdio: "inherit" } 62 | ); 63 | console.log("Installation successful!"); 64 | return true; 65 | } 66 | } catch (error) { 67 | console.error( 68 | "Failed to install agent-twitter-client v0.0.19:", 69 | error.message 70 | ); 71 | return false; 72 | } 73 | } 74 | 75 | async function main() { 76 | console.log("================================================="); 77 | console.log(" agent-twitter-client-mcp Demo"); 78 | console.log("================================================="); 79 | console.log( 80 | "\nThis demo shows how to use agent-twitter-client-mcp to interact with Twitter.\n" 81 | ); 82 | 83 | const options = [ 84 | "Search for tweets from a user", 85 | "Send a single tweet", 86 | "Send a tweet thread", 87 | "Run simple tweet demo", 88 | "Simple Grok AI interaction (requires v0.0.19)", 89 | "Interactive Grok AI chat (requires v0.0.19)", 90 | "Connect to existing MCP server", 91 | "Configure settings", 92 | "Exit", 93 | ]; 94 | 95 | let running = true; 96 | while (running) { 97 | const index = readlineSync.keyInSelect( 98 | options, 99 | "What would you like to do?" 100 | ); 101 | 102 | try { 103 | switch (index) { 104 | case 0: { 105 | // Search for tweets 106 | await runScript("./tweet-search.js"); 107 | break; 108 | } 109 | case 1: { 110 | // Send a single tweet 111 | await runScript("./send-tweet.js"); 112 | break; 113 | } 114 | case 2: { 115 | // Send a tweet thread 116 | await runScript("./tweet-thread.js"); 117 | break; 118 | } 119 | case 3: { 120 | // Run simple tweet demo 121 | await runScript("./simple-tweet.js"); 122 | break; 123 | } 124 | case 4: { 125 | // Simple Grok AI interaction 126 | const hasCorrectVersion = checkAgentTwitterClientVersion(); 127 | if (!hasCorrectVersion) { 128 | console.log( 129 | "\nYou need agent-twitter-client v0.0.19 for Grok functionality." 130 | ); 131 | const installNow = readlineSync.keyInYNStrict( 132 | "Would you like to temporarily install it now?" 133 | ); 134 | if (installNow) { 135 | const success = await installAgentTwitterClientV0019(); 136 | if (!success) { 137 | console.log( 138 | "Cannot proceed without agent-twitter-client v0.0.19" 139 | ); 140 | break; 141 | } 142 | } else { 143 | console.log( 144 | "Cannot proceed without agent-twitter-client v0.0.19" 145 | ); 146 | break; 147 | } 148 | } 149 | await runScript("./simple-grok.js"); 150 | break; 151 | } 152 | case 5: { 153 | // Interactive Grok AI chat 154 | const hasCorrectVersion = checkAgentTwitterClientVersion(); 155 | if (!hasCorrectVersion) { 156 | console.log( 157 | "\nYou need agent-twitter-client v0.0.19 for Grok functionality." 158 | ); 159 | const installNow = readlineSync.keyInYNStrict( 160 | "Would you like to temporarily install it now?" 161 | ); 162 | if (installNow) { 163 | const success = await installAgentTwitterClientV0019(); 164 | if (!success) { 165 | console.log( 166 | "Cannot proceed without agent-twitter-client v0.0.19" 167 | ); 168 | break; 169 | } 170 | } else { 171 | console.log( 172 | "Cannot proceed without agent-twitter-client v0.0.19" 173 | ); 174 | break; 175 | } 176 | } 177 | await runScript("./grok-chat.js"); 178 | break; 179 | } 180 | case 6: { 181 | // Connect to existing MCP server 182 | console.log("\nConnecting to an existing MCP server..."); 183 | console.log( 184 | "Make sure you have started the MCP server in another terminal with:" 185 | ); 186 | console.log("PORT=3001 npx agent-twitter-client-mcp\n"); 187 | 188 | const serverPort = readlineSync.question( 189 | "Enter the port (default: 3001): ", 190 | { 191 | defaultInput: "3001", 192 | } 193 | ); 194 | 195 | await runScript("./test-client.js", { 196 | PORT: serverPort, 197 | START_SERVER: "false", 198 | }); 199 | break; 200 | } 201 | case 7: { 202 | // Configure settings 203 | console.log("\nConfigure settings:"); 204 | const debug = readlineSync.keyInYNStrict("Enable debug mode?"); 205 | const startServer = readlineSync.keyInYNStrict( 206 | "Start MCP server automatically?" 207 | ); 208 | const customPort = readlineSync.keyInYNStrict("Use custom port?"); 209 | 210 | let configPort = "3001"; 211 | if (customPort) { 212 | configPort = readlineSync.question( 213 | "Enter the port (default: 3001): ", 214 | { 215 | defaultInput: "3001", 216 | } 217 | ); 218 | } 219 | 220 | console.log("\nSettings configured:"); 221 | console.log(`- Debug mode: ${debug ? "Enabled" : "Disabled"}`); 222 | console.log(`- Start server: ${startServer ? "Yes" : "No"}`); 223 | console.log(`- Port: ${configPort}`); 224 | 225 | // Ask which script to run with these settings 226 | console.log( 227 | "\nWhich script would you like to run with these settings?" 228 | ); 229 | const scriptOptions = [ 230 | "Search for tweets from a user", 231 | "Send a single tweet", 232 | "Send a tweet thread", 233 | "Run simple tweet demo", 234 | "Simple Grok AI interaction (requires v0.0.19)", 235 | "Interactive Grok AI chat (requires v0.0.19)", 236 | "None (return to main menu)", 237 | ]; 238 | 239 | const scriptIndex = readlineSync.keyInSelect( 240 | scriptOptions, 241 | "Select a script:" 242 | ); 243 | 244 | if (scriptIndex >= 0 && scriptIndex < 6) { 245 | const scriptPaths = [ 246 | "./tweet-search.js", 247 | "./send-tweet.js", 248 | "./tweet-thread.js", 249 | "./simple-tweet.js", 250 | "./simple-grok.js", 251 | "./grok-chat.js", 252 | ]; 253 | 254 | // Check if we need to install agent-twitter-client v0.0.19 for Grok 255 | if (scriptIndex >= 4 && scriptIndex <= 5) { 256 | const hasCorrectVersion = checkAgentTwitterClientVersion(); 257 | if (!hasCorrectVersion) { 258 | console.log( 259 | "\nYou need agent-twitter-client v0.0.19 for Grok functionality." 260 | ); 261 | const installNow = readlineSync.keyInYNStrict( 262 | "Would you like to temporarily install it now?" 263 | ); 264 | if (installNow) { 265 | const success = await installAgentTwitterClientV0019(); 266 | if (!success) { 267 | console.log( 268 | "Cannot proceed without agent-twitter-client v0.0.19" 269 | ); 270 | break; 271 | } 272 | } else { 273 | console.log( 274 | "Cannot proceed without agent-twitter-client v0.0.19" 275 | ); 276 | break; 277 | } 278 | } 279 | } 280 | 281 | await runScript(scriptPaths[scriptIndex], { 282 | DEBUG: debug.toString(), 283 | START_SERVER: startServer.toString(), 284 | PORT: configPort, 285 | }); 286 | } 287 | break; 288 | } 289 | default: 290 | console.log("Exiting..."); 291 | running = false; 292 | return; 293 | } 294 | } catch (error) { 295 | console.error("Error:", error); 296 | } 297 | 298 | console.log("\n"); 299 | } 300 | } 301 | 302 | main(); 303 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-twitter-client-mcp-demo", 3 | "version": "1.0.0", 4 | "description": "Demo for using agent-twitter-client-mcp to send tweets", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node index.js", 9 | "tweet": "node send-tweet.js", 10 | "thread": "node tweet-thread.js", 11 | "simple-grok": "node simple-grok.js", 12 | "grok-chat": "node grok-chat.js" 13 | }, 14 | "keywords": [ 15 | "twitter", 16 | "mcp", 17 | "demo" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "dependencies": { 22 | "agent-twitter-client-mcp": "^0.1.0", 23 | "dotenv": "^16.4.7", 24 | "node-fetch": "^3.3.2", 25 | "readline-sync": "^1.4.10" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/run-demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the directory where the script is located 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | cd "$SCRIPT_DIR" 6 | 7 | # Default values 8 | DEBUG_MODE=false 9 | SPECIFIC_SCRIPT="" 10 | USE_LOCAL_AGENT_TWITTER_CLIENT=false 11 | DEBUG_ENV=false 12 | 13 | # Parse command line arguments 14 | while [[ $# -gt 0 ]]; do 15 | case $1 in 16 | --debug) 17 | DEBUG_MODE=true 18 | shift 19 | ;; 20 | --debug-env) 21 | DEBUG_ENV=true 22 | shift 23 | ;; 24 | --script) 25 | SPECIFIC_SCRIPT="$2" 26 | shift 2 27 | ;; 28 | --use-local-agent-twitter-client) 29 | USE_LOCAL_AGENT_TWITTER_CLIENT=true 30 | shift 31 | ;; 32 | --help) 33 | echo "Usage: $0 [options]" 34 | echo "Options:" 35 | echo " --debug Enable debug mode" 36 | echo " --debug-env Debug environment variables" 37 | echo " --script SCRIPT Run a specific script (e.g., fixed-tweet.js, simple-grok.js, grok-chat.js)" 38 | echo " --use-local-agent-twitter-client Use local version of agent-twitter-client v0.0.19" 39 | echo " --help Show this help message" 40 | exit 0 41 | ;; 42 | *) 43 | echo "Unknown option: $1" 44 | echo "Use --help to see available options" 45 | exit 1 46 | ;; 47 | esac 48 | done 49 | 50 | # Check if node is installed 51 | if ! command -v node &> /dev/null; then 52 | echo "Node.js is not installed. Please install Node.js to run this demo." 53 | exit 1 54 | fi 55 | 56 | # Check if npm is installed 57 | if ! command -v npm &> /dev/null; then 58 | echo "npm is not installed. Please install npm to run this demo." 59 | exit 1 60 | fi 61 | 62 | # Check if .env file exists in the demo directory 63 | if [ ! -f ".env" ]; then 64 | echo "No .env file found in demo directory. Creating from .env.example..." 65 | if [ -f ".env.example" ]; then 66 | cp .env.example .env 67 | echo "Created .env file. Please edit it with your Twitter credentials." 68 | else 69 | echo "No .env.example file found. Please create a .env file with your Twitter credentials." 70 | exit 1 71 | fi 72 | fi 73 | 74 | # Print the current directory and .env file location for debugging 75 | echo "Current directory: $SCRIPT_DIR" 76 | echo "Using .env file: $SCRIPT_DIR/.env" 77 | 78 | # Debug .env file if requested 79 | if [ "$DEBUG_ENV" = true ]; then 80 | echo "=== .env file contents (with sensitive data masked) ===" 81 | grep -v "^#" .env | sed 's/\(PASSWORD=\).*/\1[MASKED]/g' | sed 's/\(TOKEN=\).*/\1[MASKED]/g' 82 | echo "=== End of .env file contents ===" 83 | fi 84 | 85 | # Install dependencies if node_modules doesn't exist 86 | if [ ! -d "node_modules" ]; then 87 | echo "Installing dependencies..." 88 | npm install 89 | fi 90 | 91 | # If using local agent-twitter-client v0.0.19 92 | if [ "$USE_LOCAL_AGENT_TWITTER_CLIENT" = true ]; then 93 | echo "Using local agent-twitter-client v0.0.19..." 94 | 95 | # Install directly from GitHub instead of npm 96 | echo "Installing agent-twitter-client v0.0.19 from GitHub..." 97 | npm install --no-save github:elizaOS/agent-twitter-client#0.0.19 98 | 99 | if [ $? -eq 0 ]; then 100 | echo "agent-twitter-client v0.0.19 installed temporarily for this session" 101 | else 102 | echo "Failed to install agent-twitter-client v0.0.19 from GitHub" 103 | echo "Trying alternative installation method..." 104 | npm install --no-save https://github.com/elizaOS/agent-twitter-client/tarball/0.0.19 105 | 106 | if [ $? -eq 0 ]; then 107 | echo "agent-twitter-client v0.0.19 installed temporarily for this session" 108 | else 109 | echo "Failed to install agent-twitter-client v0.0.19. Please check your internet connection or GitHub access." 110 | exit 1 111 | fi 112 | fi 113 | fi 114 | 115 | # Set environment variables based on options 116 | if [ "$DEBUG_MODE" = true ]; then 117 | export DEBUG=true 118 | echo "Debug mode enabled" 119 | fi 120 | 121 | # Run the specified script or the default demo 122 | if [ -n "$SPECIFIC_SCRIPT" ]; then 123 | if [ -f "$SPECIFIC_SCRIPT" ]; then 124 | echo "Running specific script: $SPECIFIC_SCRIPT" 125 | node "$SPECIFIC_SCRIPT" 126 | else 127 | echo "Error: Script '$SPECIFIC_SCRIPT' not found" 128 | echo "Available scripts:" 129 | ls -1 *.js 130 | exit 1 131 | fi 132 | else 133 | # Run the default demo 134 | echo "Starting the demo..." 135 | node index.js 136 | fi 137 | 138 | # Exit with the status of the last command 139 | exit_code=$? 140 | if [ $exit_code -ne 0 ]; then 141 | echo "Demo exited with error code: $exit_code" 142 | fi 143 | exit $exit_code -------------------------------------------------------------------------------- /demo/run-fixed-tweet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the directory where the script is located 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | cd "$SCRIPT_DIR" 6 | 7 | # Check if node is installed 8 | if ! command -v node &> /dev/null; then 9 | echo "Node.js is not installed. Please install Node.js to run this demo." 10 | exit 1 11 | fi 12 | 13 | # Check if npm is installed 14 | if ! command -v npm &> /dev/null; then 15 | echo "npm is not installed. Please install npm to run this demo." 16 | exit 1 17 | fi 18 | 19 | # Check if .env file exists 20 | if [ ! -f ".env" ]; then 21 | echo "No .env file found. Creating from .env.example..." 22 | if [ -f ".env.example" ]; then 23 | cp .env.example .env 24 | echo "Created .env file. Please edit it with your Twitter credentials." 25 | else 26 | echo "No .env.example file found. Please create a .env file with your Twitter credentials." 27 | exit 1 28 | fi 29 | fi 30 | 31 | # Install dependencies if node_modules doesn't exist 32 | if [ ! -d "node_modules" ]; then 33 | echo "Installing dependencies..." 34 | npm install 35 | fi 36 | 37 | # Parse command line arguments 38 | DEBUG_MODE=false 39 | 40 | while [[ $# -gt 0 ]]; do 41 | case $1 in 42 | --debug) 43 | DEBUG_MODE=true 44 | shift 45 | ;; 46 | --help) 47 | echo "Usage: $0 [options]" 48 | echo "Options:" 49 | echo " --debug Enable debug mode" 50 | echo " --help Show this help message" 51 | exit 0 52 | ;; 53 | *) 54 | echo "Unknown option: $1" 55 | echo "Use --help to see available options" 56 | exit 1 57 | ;; 58 | esac 59 | done 60 | 61 | # Set environment variables 62 | if [ "$DEBUG_MODE" = true ]; then 63 | export DEBUG=true 64 | echo "Debug mode enabled" 65 | fi 66 | 67 | # Run the fixed tweet script 68 | echo "Starting the fixed tweet demo..." 69 | node fixed-tweet.js 70 | 71 | # Exit with the status of the last command 72 | exit_code=$? 73 | if [ $exit_code -ne 0 ]; then 74 | echo "Demo exited with error code: $exit_code" 75 | fi 76 | exit $exit_code -------------------------------------------------------------------------------- /demo/run-simple-demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple script to run the Twitter MCP demo menu 4 | 5 | # Check if Node.js is installed 6 | if ! command -v node &> /dev/null; then 7 | echo "Error: Node.js is not installed. Please install Node.js to run this demo." 8 | exit 1 9 | fi 10 | 11 | # Check if the required packages are installed 12 | if [ ! -d "node_modules" ]; then 13 | echo "Installing required packages..." 14 | npm install 15 | fi 16 | 17 | # Check if .env file exists 18 | if [ ! -f ".env" ]; then 19 | echo "Error: .env file not found. Please create a .env file with your Twitter credentials." 20 | echo "You can copy .env.example to .env and fill in your credentials." 21 | exit 1 22 | fi 23 | 24 | # Run the menu script 25 | echo "Starting Twitter MCP Demo..." 26 | node simple-menu.js 27 | 28 | # Exit with the same code as the menu script 29 | exit $? -------------------------------------------------------------------------------- /demo/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install dependencies 4 | echo "Installing dependencies..." 5 | npm install 6 | 7 | # Create .env file if it doesn't exist 8 | if [ ! -f .env ]; then 9 | echo "Creating .env file from .env.example..." 10 | cp .env.example .env 11 | echo "Please edit the .env file with your Twitter credentials." 12 | else 13 | echo ".env file already exists." 14 | fi 15 | 16 | echo "" 17 | echo "Setup complete! You can now run the demo with:" 18 | echo "npm start" -------------------------------------------------------------------------------- /demo/simple-grok.js: -------------------------------------------------------------------------------- 1 | import { Scraper } from "agent-twitter-client"; 2 | import fs from "fs"; 3 | import dotenv from "dotenv"; 4 | import path from "path"; 5 | 6 | // Get the current directory 7 | const currentDir = process.cwd(); 8 | console.log(`Current directory: ${currentDir}`); 9 | 10 | // Load environment variables from demo/.env file 11 | const envPath = path.join(currentDir, ".env"); 12 | console.log(`Loading environment from: ${envPath}`); 13 | dotenv.config({ path: envPath }); 14 | 15 | // Debug environment variables (with sensitive info masked) 16 | console.log("Environment variables loaded:"); 17 | console.log("AUTH_METHOD:", process.env.AUTH_METHOD || "not set"); 18 | console.log( 19 | "TWITTER_COOKIES:", 20 | process.env.TWITTER_COOKIES ? "[Set]" : "not set" 21 | ); 22 | console.log( 23 | "TWITTER_USERNAME:", 24 | process.env.TWITTER_USERNAME ? "[Set]" : "not set" 25 | ); 26 | console.log( 27 | "TWITTER_PASSWORD:", 28 | process.env.TWITTER_PASSWORD ? "[Set]" : "not set" 29 | ); 30 | console.log("TWITTER_EMAIL:", process.env.TWITTER_EMAIL ? "[Set]" : "not set"); 31 | 32 | let scraper = null; 33 | 34 | async function initializeScraper() { 35 | try { 36 | scraper = new Scraper(); 37 | console.log("Scraper initialized successfully"); 38 | 39 | // Check authentication method 40 | const authMethod = process.env.AUTH_METHOD || "cookies"; 41 | 42 | // For Grok, we need to handle both cookie-based and credential-based auth 43 | let isLoggedIn = false; 44 | 45 | // Try cookie authentication first 46 | if (process.env.TWITTER_COOKIES) { 47 | console.log("Using cookies from environment variables..."); 48 | 49 | // Debug the cookie format 50 | console.log("Cookie format check:"); 51 | try { 52 | // Check if it's a JSON array 53 | if ( 54 | process.env.TWITTER_COOKIES.startsWith("[") && 55 | process.env.TWITTER_COOKIES.endsWith("]") 56 | ) { 57 | console.log("Cookies appear to be in JSON array format"); 58 | const parsedCookies = JSON.parse(process.env.TWITTER_COOKIES); 59 | console.log(`Found ${parsedCookies.length} cookies in JSON array`); 60 | 61 | // Use the parsed cookies directly 62 | await scraper.setCookies(parsedCookies); 63 | } else { 64 | // Assume it's a semicolon-separated string 65 | console.log("Cookies appear to be in semicolon-separated format"); 66 | const formattedCookies = process.env.TWITTER_COOKIES.split(";").map( 67 | (cookie) => cookie.trim() 68 | ); 69 | console.log(`Found ${formattedCookies.length} cookies in string`); 70 | await scraper.setCookies(formattedCookies); 71 | } 72 | } catch (error) { 73 | console.error("Error parsing cookies:", error.message); 74 | console.log("Trying to use cookies as-is..."); 75 | await scraper.setCookies([process.env.TWITTER_COOKIES]); 76 | } 77 | 78 | // Check if we're logged in with cookies 79 | isLoggedIn = await scraper.isLoggedIn(); 80 | console.log("Cookie login check result:", isLoggedIn); 81 | } else { 82 | // Try to load cookies from a file 83 | try { 84 | console.log("Loading cookies from file..."); 85 | const cookiesJson = JSON.parse( 86 | fs.readFileSync("./cookies.json", "utf-8") 87 | ); 88 | console.log(`Found ${cookiesJson.length} cookies`); 89 | 90 | if (cookiesJson.length > 0) { 91 | // Format cookies as strings in the Set-Cookie header format 92 | const formattedCookies = cookiesJson.map((cookie) => { 93 | let cookieString = `${cookie.key}=${cookie.value}`; 94 | cookieString += `; Domain=${cookie.domain}`; 95 | cookieString += `; Path=${cookie.path}`; 96 | if (cookie.expires) cookieString += `; Expires=${cookie.expires}`; 97 | if (cookie.secure) cookieString += "; Secure"; 98 | if (cookie.httpOnly) cookieString += "; HttpOnly"; 99 | if (cookie.sameSite) 100 | cookieString += `; SameSite=${cookie.sameSite}`; 101 | return cookieString; 102 | }); 103 | 104 | console.log("Setting cookies in scraper..."); 105 | await scraper.setCookies(formattedCookies); 106 | 107 | // Check if we're logged in with cookies 108 | isLoggedIn = await scraper.isLoggedIn(); 109 | console.log("Cookie login check result:", isLoggedIn); 110 | } 111 | } catch (error) { 112 | console.log( 113 | "No valid cookies found or error loading cookies:", 114 | error.message 115 | ); 116 | } 117 | } 118 | 119 | // If cookie authentication failed, try credential-based authentication 120 | if ( 121 | !isLoggedIn && 122 | process.env.TWITTER_USERNAME && 123 | process.env.TWITTER_PASSWORD 124 | ) { 125 | console.log( 126 | "Cookie authentication failed or not available. Trying username/password login..." 127 | ); 128 | try { 129 | console.log(`Using username: ${process.env.TWITTER_USERNAME}`); 130 | 131 | // Debug the login parameters (without showing the actual password) 132 | console.log("Login parameters:"); 133 | console.log("Username:", process.env.TWITTER_USERNAME); 134 | console.log( 135 | "Password:", 136 | process.env.TWITTER_PASSWORD ? "[Set]" : "[Not Set]" 137 | ); 138 | console.log("Email:", process.env.TWITTER_EMAIL || "[Not Set]"); 139 | 140 | await scraper.login( 141 | process.env.TWITTER_USERNAME, 142 | process.env.TWITTER_PASSWORD 143 | ); 144 | isLoggedIn = await scraper.isLoggedIn(); 145 | console.log("Credential login check result:", isLoggedIn); 146 | } catch (error) { 147 | console.error("Failed to login with credentials:", error); 148 | } 149 | } 150 | 151 | // Final check if we're logged in 152 | if (!isLoggedIn) { 153 | console.error( 154 | "Not logged in to Twitter. Please check your cookies or credentials." 155 | ); 156 | console.log( 157 | "For Grok functionality, you need to provide valid Twitter credentials." 158 | ); 159 | console.log( 160 | "Please set TWITTER_USERNAME and TWITTER_PASSWORD in your demo/.env file." 161 | ); 162 | process.exit(1); 163 | } 164 | 165 | return scraper; 166 | } catch (error) { 167 | console.error("Failed to initialize Twitter scraper:", error); 168 | throw error; 169 | } 170 | } 171 | 172 | async function main() { 173 | try { 174 | await initializeScraper(); 175 | 176 | console.log("Creating a new Grok conversation..."); 177 | const conversationId = await scraper.createGrokConversation(); 178 | console.log(`Conversation created with ID: ${conversationId}`); 179 | 180 | console.log("Sending a message to Grok..."); 181 | const completion = await scraper.grokChat({ 182 | conversationId: conversationId, 183 | messages: [ 184 | { 185 | role: "user", 186 | content: "What are the top trending topics on Twitter right now?", 187 | }, 188 | ], 189 | }); 190 | 191 | console.log("\n--- Grok Response ---"); 192 | console.log(completion.message); 193 | console.log("---------------------\n"); 194 | 195 | if (completion.rateLimit?.isRateLimited) { 196 | console.log("Rate Limited:", completion.rateLimit.message); 197 | if (completion.rateLimit.upsellInfo) { 198 | console.log("\nLimit Info:"); 199 | console.log( 200 | `Usage Limit: ${completion.rateLimit.upsellInfo.usageLimit}` 201 | ); 202 | console.log( 203 | `Duration: ${completion.rateLimit.upsellInfo.quotaDuration}` 204 | ); 205 | console.log(`${completion.rateLimit.upsellInfo.title}`); 206 | console.log(`${completion.rateLimit.upsellInfo.message}`); 207 | } 208 | } 209 | } catch (error) { 210 | console.error("Failed to send Grok message:", error); 211 | } 212 | } 213 | 214 | main(); 215 | -------------------------------------------------------------------------------- /demo/simple-menu.js: -------------------------------------------------------------------------------- 1 | import readlineSync from "readline-sync"; 2 | import { spawn } from "child_process"; 3 | import dotenv from "dotenv"; 4 | 5 | // Load environment variables 6 | dotenv.config(); 7 | 8 | /** 9 | * Simple menu for the Twitter MCP demo 10 | */ 11 | async function main() { 12 | console.log("\n=== Twitter MCP Demo Menu ===\n"); 13 | 14 | const options = [ 15 | { name: "Send a Tweet (Fixed Version)", script: "fixed-tweet.js" }, 16 | { name: "Send a Tweet (Robust Version)", script: "robust-tweet.js" }, 17 | { name: "Send a Tweet (Original Version)", script: "simple-tweet.js" }, 18 | { name: "Search Tweets", script: "simple-search.js" }, 19 | { name: "Create a Thread", script: "simple-thread.js" }, 20 | { name: "Exit", script: null }, 21 | ]; 22 | 23 | options.forEach((option, index) => { 24 | console.log(`[${index + 1}] ${option.name}`); 25 | }); 26 | 27 | const selection = readlineSync.question("\nSelect an option: "); 28 | const optionIndex = parseInt(selection) - 1; 29 | 30 | if (isNaN(optionIndex) || optionIndex < 0 || optionIndex >= options.length) { 31 | console.log("Invalid selection. Exiting..."); 32 | return; 33 | } 34 | 35 | const selectedOption = options[optionIndex]; 36 | 37 | if (!selectedOption.script) { 38 | console.log("Exiting..."); 39 | return; 40 | } 41 | 42 | console.log(`\nRunning ${selectedOption.name}...\n`); 43 | 44 | // Set up environment variables 45 | const env = { ...process.env }; 46 | 47 | // Ask if debug mode should be enabled 48 | if (readlineSync.keyInYNStrict("Enable debug mode?")) { 49 | env.DEBUG = "true"; 50 | console.log("Debug mode enabled."); 51 | } else { 52 | env.DEBUG = "false"; 53 | console.log("Debug mode disabled."); 54 | } 55 | 56 | // Run the selected script 57 | const scriptProcess = spawn("node", [selectedOption.script], { 58 | stdio: "inherit", 59 | env, 60 | }); 61 | 62 | // Handle process exit 63 | scriptProcess.on("exit", (code) => { 64 | console.log(`\n${selectedOption.name} completed with exit code ${code}.`); 65 | console.log("\nPress Enter to return to the menu..."); 66 | readlineSync.question(""); 67 | 68 | // Restart the menu 69 | main(); 70 | }); 71 | 72 | // Handle errors 73 | scriptProcess.on("error", (error) => { 74 | console.error(`Error running ${selectedOption.name}:`, error.message); 75 | console.log("\nPress Enter to return to the menu..."); 76 | readlineSync.question(""); 77 | 78 | // Restart the menu 79 | main(); 80 | }); 81 | } 82 | 83 | // Start the menu 84 | main(); 85 | -------------------------------------------------------------------------------- /demo/simple-tweet.js: -------------------------------------------------------------------------------- 1 | import readlineSync from "readline-sync"; 2 | import { McpClient } from "./mcp-client.js"; 3 | import { singleTweets } from "./tweets.js"; 4 | import fs from "fs"; 5 | 6 | async function main() { 7 | let client = null; 8 | 9 | try { 10 | console.log("Starting MCP client..."); 11 | 12 | // Create a new client with a different port to avoid conflicts 13 | client = new McpClient({ 14 | port: 3005, // Use a different port than the default 15 | debug: process.env.DEBUG === "true", 16 | maxPortAttempts: 5, 17 | portIncrement: 1, 18 | startServer: true, // Always start a new server 19 | }); 20 | 21 | await client.start(); 22 | console.log("MCP client started successfully!"); 23 | 24 | // Display available tweets 25 | console.log("\nAvailable tweets:"); 26 | 27 | // Only show the shorter tweets (6 and 7) to avoid character limit issues 28 | const shortTweets = singleTweets.slice(5); // Get tweets 6 and 7 29 | shortTweets.forEach((tweet, index) => { 30 | console.log(`\n[${index + 1}] ${tweet}\n`); 31 | console.log(`Length: ${tweet.length} characters`); 32 | }); 33 | 34 | console.log(`\n[c] Custom tweet\n`); 35 | 36 | // Get user selection 37 | const selection = readlineSync.question( 38 | "\nEnter the number of the tweet to send, 'c' for custom tweet, or 'q' to quit: " 39 | ); 40 | 41 | if (selection.toLowerCase() === "q") { 42 | console.log("Exiting..."); 43 | await client.stop(); 44 | return; 45 | } 46 | 47 | let selectedTweet; 48 | 49 | if (selection.toLowerCase() === "c") { 50 | // Get custom tweet text 51 | selectedTweet = readlineSync.question( 52 | "\nEnter your tweet text (max 280 characters): " 53 | ); 54 | if (!selectedTweet.trim()) { 55 | console.log("Empty tweet. Exiting..."); 56 | await client.stop(); 57 | return; 58 | } 59 | 60 | // Check character count 61 | if (selectedTweet.length > 280) { 62 | console.log( 63 | `Tweet is too long (${selectedTweet.length} characters). Maximum is 280 characters.` 64 | ); 65 | console.log("Please try again with a shorter tweet."); 66 | await client.stop(); 67 | return; 68 | } 69 | } else { 70 | const tweetIndex = parseInt(selection) - 1; 71 | 72 | if ( 73 | isNaN(tweetIndex) || 74 | tweetIndex < 0 || 75 | tweetIndex >= shortTweets.length 76 | ) { 77 | console.log("Invalid selection. Exiting..."); 78 | await client.stop(); 79 | return; 80 | } 81 | 82 | selectedTweet = shortTweets[tweetIndex]; 83 | } 84 | 85 | console.log(`\nSending tweet: "${selectedTweet}"\n`); 86 | console.log(`Tweet length: ${selectedTweet.length} characters`); 87 | 88 | // Ask if this is a reply 89 | const isReply = readlineSync.keyInYNStrict( 90 | "Is this a reply to another tweet?" 91 | ); 92 | let replyToTweetId = null; 93 | 94 | if (isReply) { 95 | replyToTweetId = readlineSync.question( 96 | "Enter the tweet ID to reply to: " 97 | ); 98 | if (!replyToTweetId.trim()) { 99 | console.log("No tweet ID provided. Sending as a regular tweet..."); 100 | replyToTweetId = null; 101 | } 102 | } 103 | 104 | // Send the tweet 105 | try { 106 | console.log("Sending tweet to Twitter..."); 107 | 108 | // Create a direct JSON-RPC request for better control 109 | const requestId = Math.floor(Math.random() * 10000); 110 | 111 | const request = { 112 | jsonrpc: "2.0", 113 | id: requestId.toString(), 114 | method: "tools/call", 115 | params: { 116 | name: "send_tweet", 117 | arguments: { 118 | text: selectedTweet, 119 | }, 120 | }, 121 | }; 122 | 123 | // Add replyToTweetId if provided 124 | if (replyToTweetId) { 125 | request.params.arguments.replyToTweetId = replyToTweetId; 126 | console.log( 127 | `This tweet will be a reply to tweet ID: ${replyToTweetId}` 128 | ); 129 | } 130 | 131 | if (process.env.DEBUG === "true") { 132 | console.log( 133 | "DEBUG: Request payload:", 134 | JSON.stringify(request, null, 2) 135 | ); 136 | } 137 | 138 | // Register a response handler 139 | const tweetPromise = new Promise((resolve, reject) => { 140 | const timeoutId = setTimeout(() => { 141 | reject(new Error("Timeout waiting for response from Twitter")); 142 | }, 30000); // 30 second timeout 143 | 144 | client.responseHandlers.set(requestId.toString(), (response) => { 145 | clearTimeout(timeoutId); 146 | 147 | if (process.env.DEBUG === "true") { 148 | console.log( 149 | "DEBUG: Raw response:", 150 | JSON.stringify(response, null, 2) 151 | ); 152 | } 153 | 154 | // Check for error in the response content 155 | if ( 156 | response.result && 157 | response.result.content && 158 | response.result.content.length > 0 && 159 | response.result.content[0].isError 160 | ) { 161 | const errorMessage = 162 | response.result.content[0].text || "Unknown error in response"; 163 | reject(new Error(errorMessage)); 164 | return; 165 | } 166 | 167 | // Check for standard error format 168 | if (response.error) { 169 | reject(new Error(response.error.message || "Unknown error")); 170 | return; 171 | } 172 | 173 | // If we get here, the tweet was sent successfully 174 | if ( 175 | response.result && 176 | response.result.content && 177 | response.result.content.length > 0 178 | ) { 179 | try { 180 | // Try to parse the tweet data from the response 181 | const contentText = response.result.content[0].text; 182 | if (contentText) { 183 | try { 184 | const tweetData = JSON.parse(contentText); 185 | if (tweetData && tweetData.tweet && tweetData.tweet.id) { 186 | resolve(tweetData.tweet); 187 | return; 188 | } else if (tweetData && tweetData.success) { 189 | // Some versions might return a success flag instead of tweet data 190 | resolve({ id: "unknown", success: true }); 191 | return; 192 | } 193 | } catch (parseError) { 194 | console.log( 195 | "Warning: Failed to parse tweet data:", 196 | parseError.message 197 | ); 198 | // Try to extract tweet ID using regex if JSON parsing fails 199 | const idMatch = 200 | contentText.match(/tweet ID: (\d+)/i) || 201 | contentText.match(/id["']?\s*:\s*["']?(\d+)/i); 202 | if (idMatch && idMatch[1]) { 203 | resolve({ id: idMatch[1], text: selectedTweet }); 204 | return; 205 | } 206 | } 207 | } 208 | } catch (error) { 209 | console.log( 210 | "Warning: Exception while processing response:", 211 | error.message 212 | ); 213 | } 214 | 215 | // If we couldn't extract the tweet data but got a successful response, 216 | // assume the tweet was sent successfully 217 | if ( 218 | response.result.status === "success" || 219 | (response.result.content && 220 | response.result.content[0].text && 221 | !response.result.content[0].isError) 222 | ) { 223 | resolve({ id: "unknown", success: true }); 224 | return; 225 | } 226 | 227 | reject(new Error("Failed to extract tweet ID from response")); 228 | } else { 229 | reject(new Error("Invalid response from MCP server")); 230 | } 231 | }); 232 | 233 | // Send the request 234 | client.sendRequest(request); 235 | }); 236 | 237 | const result = await tweetPromise; 238 | 239 | console.log("\n✅ Tweet sent successfully!"); 240 | 241 | if (result.id && result.id !== "unknown") { 242 | console.log("Tweet ID:", result.id); 243 | console.log( 244 | "Tweet URL:", 245 | `https://twitter.com/user/status/${result.id}` 246 | ); 247 | 248 | // Save the tweet ID for future replies 249 | fs.writeFileSync("./last-tweet-id.txt", result.id); 250 | console.log("Tweet ID saved for future replies."); 251 | } else { 252 | console.log("Note: Tweet ID not returned in the response."); 253 | console.log( 254 | "The tweet was likely sent successfully, but we couldn't extract the ID." 255 | ); 256 | console.log("Please check your Twitter account to confirm."); 257 | } 258 | } catch (error) { 259 | console.error("\n❌ ERROR: Failed to send tweet:", error.message); 260 | 261 | // Check for character limit error 262 | if (error.message.includes("cannot exceed 280 characters")) { 263 | console.log("\nThe tweet exceeds Twitter's 280 character limit."); 264 | console.log("Current length:", selectedTweet.length, "characters"); 265 | console.log("Please edit your tweet to be shorter."); 266 | } 267 | 268 | // Check for authentication errors 269 | if ( 270 | error.message.includes("authentication") || 271 | error.message.includes("auth") || 272 | error.message.includes("login") || 273 | error.message.includes("credentials") 274 | ) { 275 | console.log("\nThere seems to be an authentication issue."); 276 | console.log("Please check your Twitter credentials in the .env file."); 277 | console.log("You may need to refresh your cookies or re-authenticate."); 278 | } 279 | 280 | // Check for rate limiting 281 | if ( 282 | error.message.includes("rate limit") || 283 | error.message.includes("too many requests") 284 | ) { 285 | console.log("\nYou've hit Twitter's rate limits."); 286 | console.log("Please wait a while before trying again."); 287 | } 288 | 289 | if (error.details) { 290 | console.error("Error details:", JSON.stringify(error.details, null, 2)); 291 | } 292 | } 293 | 294 | // Clean up 295 | console.log("\nCleaning up..."); 296 | await client.stop(); 297 | console.log("Done!"); 298 | } catch (error) { 299 | console.error("Error:", error); 300 | if (client) { 301 | try { 302 | await client.stop(); 303 | } catch (stopError) { 304 | console.error("Error stopping client:", stopError); 305 | } 306 | } 307 | process.exit(1); 308 | } 309 | } 310 | 311 | main(); 312 | -------------------------------------------------------------------------------- /demo/test-twitter-api.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import dotenv from "dotenv"; 4 | 5 | // Load environment variables 6 | dotenv.config(); 7 | 8 | /** 9 | * This script directly tests the Twitter API to see what's happening with the response. 10 | * It will help us understand why we're getting the "Failed to extract tweet ID from response" error. 11 | */ 12 | async function main() { 13 | try { 14 | console.log("=== Twitter API Test ==="); 15 | 16 | // Find the twitter-client.js file 17 | const packagePath = path.resolve("./build/twitter-client.js"); 18 | 19 | if (!fs.existsSync(packagePath)) { 20 | console.error(`Error: Could not find ${packagePath}`); 21 | console.error( 22 | "Make sure the build directory exists and contains the twitter-client.js file." 23 | ); 24 | process.exit(1); 25 | } 26 | 27 | console.log(`Found package file at: ${packagePath}`); 28 | 29 | // Read the file 30 | let fileContent = fs.readFileSync(packagePath, "utf8"); 31 | console.log("Read file content successfully."); 32 | 33 | // Find the tweet ID extraction line 34 | const tweetIdRegex = 35 | /const\s+tweetId\s*=\s*responseData\?\.\s*data\?\.\s*create_tweet\?\.\s*tweet_results\?\.\s*result\?\.\s*rest_id\s*;/; 36 | const tweetIdMatch = fileContent.match(tweetIdRegex); 37 | 38 | if (!tweetIdMatch) { 39 | console.error( 40 | "Error: Could not find the tweet ID extraction in the file." 41 | ); 42 | process.exit(1); 43 | } 44 | 45 | console.log("Found tweet ID extraction line:", tweetIdMatch[0]); 46 | console.log("\nThis suggests the expected response structure is:"); 47 | console.log("responseData.data.create_tweet.tweet_results.result.rest_id"); 48 | 49 | // Create a test file that will help us debug the issue 50 | const testFilePath = path.resolve("./demo/twitter-api-test.js"); 51 | 52 | const testFileContent = ` 53 | import { TwitterClient } from '../build/twitter-client.js'; 54 | import dotenv from 'dotenv'; 55 | 56 | // Load environment variables 57 | dotenv.config(); 58 | 59 | async function main() { 60 | try { 61 | console.log("=== Twitter API Test ==="); 62 | 63 | // Create a TwitterClient instance 64 | const client = new TwitterClient(); 65 | 66 | // Get the authentication configuration 67 | const authConfig = { 68 | method: process.env.AUTH_METHOD || 'cookies', 69 | cookies: process.env.TWITTER_COOKIES ? JSON.parse(process.env.TWITTER_COOKIES) : undefined, 70 | username: process.env.TWITTER_USERNAME, 71 | password: process.env.TWITTER_PASSWORD, 72 | email: process.env.TWITTER_EMAIL, 73 | twoFactorSecret: process.env.TWITTER_2FA_SECRET, 74 | apiKey: process.env.TWITTER_API_KEY, 75 | apiSecretKey: process.env.TWITTER_API_SECRET_KEY, 76 | accessToken: process.env.TWITTER_ACCESS_TOKEN, 77 | accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET, 78 | }; 79 | 80 | console.log("Using authentication method:", authConfig.method); 81 | 82 | // Send a test tweet 83 | const tweetText = "Test tweet from Twitter API test script - " + new Date().toISOString(); 84 | console.log("Sending tweet:", tweetText); 85 | 86 | try { 87 | // Add a custom implementation of sendTweet that logs more details 88 | const originalSendTweet = client.sendTweet; 89 | client.sendTweet = async function(config, text, replyToTweetId, media) { 90 | console.log("DEBUG: TwitterClient.sendTweet called with:", { 91 | config: typeof config, 92 | text, 93 | replyToTweetId, 94 | hasMedia: !!media 95 | }); 96 | 97 | try { 98 | const scraper = await this.authManager.getScraper(config); 99 | console.log("DEBUG: Got scraper"); 100 | 101 | const processedMedia = media?.map(item => ({ 102 | data: Buffer.from(item.data, 'base64'), 103 | mediaType: item.mediaType 104 | })); 105 | 106 | console.log("DEBUG: Sending tweet to Twitter API..."); 107 | const response = await scraper.sendTweet(text, replyToTweetId, processedMedia); 108 | console.log("DEBUG: Got response from Twitter API"); 109 | 110 | const responseText = await response.text(); 111 | console.log("DEBUG: Raw Twitter API response text:", responseText); 112 | 113 | const responseData = JSON.parse(responseText); 114 | console.log("DEBUG: Parsed Twitter API response data:", JSON.stringify(responseData, null, 2)); 115 | 116 | const tweetId = responseData?.data?.create_tweet?.tweet_results?.result?.rest_id; 117 | console.log("DEBUG: Extracted tweet ID:", tweetId); 118 | 119 | console.log("DEBUG: Response data structure:", JSON.stringify({ 120 | hasResponseData: !!responseData, 121 | hasData: responseData && !!responseData.data, 122 | hasCreateTweet: responseData?.data && !!responseData.data.create_tweet, 123 | hasTweetResults: responseData?.data?.create_tweet && !!responseData.data.create_tweet.tweet_results, 124 | hasResult: responseData?.data?.create_tweet?.tweet_results && !!responseData.data.create_tweet.tweet_results.result, 125 | hasRestId: responseData?.data?.create_tweet?.tweet_results?.result && !!responseData.data.create_tweet.tweet_results.result.rest_id 126 | }, null, 2)); 127 | 128 | if (!tweetId) { 129 | console.log("DEBUG: Failed to extract tweet ID from response. Full response data:", JSON.stringify(responseData, null, 2)); 130 | throw new Error('Failed to extract tweet ID from response'); 131 | } 132 | 133 | return await this.getTweetById(config, tweetId); 134 | } catch (error) { 135 | console.log("DEBUG: Error in TwitterClient.sendTweet:", error.message); 136 | console.log("DEBUG: Error stack:", error.stack); 137 | throw error; 138 | } 139 | }; 140 | 141 | const result = await client.sendTweet(authConfig, tweetText); 142 | console.log("Tweet sent successfully!"); 143 | console.log("Tweet ID:", result.id); 144 | console.log("Tweet URL:", \`https://twitter.com/user/status/\${result.id}\`); 145 | } catch (error) { 146 | console.error("Error sending tweet:", error.message); 147 | if (error.stack) { 148 | console.error("Error stack:", error.stack); 149 | } 150 | } 151 | } catch (error) { 152 | console.error("Error:", error.message); 153 | if (error.stack) { 154 | console.error("Error stack:", error.stack); 155 | } 156 | process.exit(1); 157 | } 158 | } 159 | 160 | main(); 161 | `; 162 | 163 | fs.writeFileSync(testFilePath, testFileContent); 164 | console.log(`\nCreated test file at: ${testFilePath}`); 165 | 166 | console.log("\nTo run the test, use:"); 167 | console.log("node demo/twitter-api-test.js"); 168 | 169 | console.log( 170 | '\nThis will help us understand why we\'re getting the "Failed to extract tweet ID from response" error.' 171 | ); 172 | } catch (error) { 173 | console.error("Error:", error.message); 174 | if (error.stack) { 175 | console.error("Error stack:", error.stack); 176 | } 177 | process.exit(1); 178 | } 179 | } 180 | 181 | main(); 182 | -------------------------------------------------------------------------------- /demo/tweet-search.js: -------------------------------------------------------------------------------- 1 | import readlineSync from "readline-sync"; 2 | import { McpClient } from "./mcp-client.js"; 3 | import dotenv from "dotenv"; 4 | import fs from "fs"; 5 | 6 | // Load environment variables 7 | dotenv.config(); 8 | 9 | async function searchUserTweets(client, username, count = 3) { 10 | console.log(`\nSearching for tweets from @${username}...`); 11 | 12 | return new Promise((resolve, reject) => { 13 | try { 14 | // Create the request in JSON-RPC 2.0 format 15 | const requestId = Math.floor(Math.random() * 10000); 16 | 17 | const request = { 18 | jsonrpc: "2.0", 19 | id: requestId.toString(), 20 | method: "tools/call", 21 | params: { 22 | name: "get_user_tweets", 23 | arguments: { 24 | username: username, 25 | count: count, 26 | includeReplies: false, 27 | includeRetweets: true, 28 | }, 29 | }, 30 | }; 31 | 32 | if (client.options.debug) { 33 | console.log("Sending request:", JSON.stringify(request, null, 2)); 34 | } 35 | 36 | // Register response handler 37 | client.responseHandlers.set(requestId.toString(), (response) => { 38 | if (response.result) { 39 | resolve(response.result); 40 | } else if (response.error) { 41 | console.error( 42 | "ERROR: Error searching tweets:", 43 | response.error.message || "Unknown error" 44 | ); 45 | reject(new Error(response.error.message || "Unknown error")); 46 | } else { 47 | console.error("ERROR: Invalid response from MCP server"); 48 | reject(new Error("Invalid response from MCP server")); 49 | } 50 | }); 51 | 52 | // Send the request 53 | client.sendRequest(request); 54 | } catch (error) { 55 | console.error("ERROR: Exception in searchUserTweets:", error); 56 | reject(error); 57 | } 58 | }); 59 | } 60 | 61 | function displayTweets(result) { 62 | try { 63 | // The response contains a nested JSON string in the text field 64 | if ( 65 | result && 66 | result.content && 67 | result.content.length > 0 && 68 | result.content[0].text 69 | ) { 70 | // Parse the nested JSON string 71 | const tweetData = JSON.parse(result.content[0].text); 72 | 73 | if (!tweetData || !tweetData.tweets || tweetData.tweets.length === 0) { 74 | console.log("No tweets found."); 75 | return; 76 | } 77 | 78 | console.log( 79 | `\nFound ${tweetData.tweets.length} tweets from @${tweetData.username}:\n` 80 | ); 81 | 82 | tweetData.tweets.forEach((tweet, index) => { 83 | console.log(`[${index + 1}] Tweet ID: ${tweet.id}`); 84 | console.log( 85 | ` Author: ${tweet.author.name} (@${tweet.author.username})` 86 | ); 87 | console.log( 88 | ` Created at: ${new Date(tweet.createdAt).toLocaleString()}` 89 | ); 90 | console.log(` Text: ${tweet.text}`); 91 | 92 | if (tweet.metrics) { 93 | console.log( 94 | ` Likes: ${tweet.metrics.likes}, Retweets: ${tweet.metrics.retweets}, Replies: ${tweet.metrics.replies}` 95 | ); 96 | } 97 | 98 | if ( 99 | tweet.media && 100 | tweet.media.photos && 101 | tweet.media.photos.length > 0 102 | ) { 103 | console.log(` Photos: ${tweet.media.photos.length}`); 104 | } 105 | 106 | if (tweet.isRetweet) { 107 | console.log(` Type: Retweet`); 108 | } else if (tweet.isReply) { 109 | console.log(` Type: Reply`); 110 | } else if (tweet.isQuote) { 111 | console.log(` Type: Quote Tweet`); 112 | if (tweet.quotedTweet) { 113 | console.log( 114 | ` Quoted Tweet: "${tweet.quotedTweet.text.substring( 115 | 0, 116 | 50 117 | )}..."` 118 | ); 119 | } 120 | } 121 | 122 | console.log(` URL: ${tweet.permanentUrl}`); 123 | console.log(); 124 | }); 125 | 126 | return tweetData; 127 | } else { 128 | console.log("No tweets found in the response."); 129 | return null; 130 | } 131 | } catch (error) { 132 | console.error("Error parsing tweet data:", error); 133 | console.log("Raw response:", JSON.stringify(result, null, 2)); 134 | return null; 135 | } 136 | } 137 | 138 | async function main() { 139 | let client = null; 140 | 141 | try { 142 | console.log("Starting Tweet Search Demo..."); 143 | 144 | // Create a new McpClient instance 145 | client = new McpClient({ 146 | port: process.env.PORT ? parseInt(process.env.PORT) : 3001, 147 | debug: process.env.DEBUG === "true", 148 | maxPortAttempts: 5, 149 | portIncrement: 1, 150 | startServer: process.env.START_SERVER !== "false", // Start server by default 151 | }); 152 | 153 | // Start the client 154 | console.log("Starting MCP client..."); 155 | await client.start(); 156 | console.log("MCP client started successfully!"); 157 | 158 | // List available tools to verify get_user_tweets is available 159 | console.log("\nListing available tools..."); 160 | const tools = await client.listTools(); 161 | 162 | const availableTools = 163 | tools && tools.tools ? tools.tools.map((tool) => tool.name) : []; 164 | 165 | console.log("Available tools:", availableTools); 166 | 167 | if (!availableTools.includes("get_user_tweets")) { 168 | console.error("ERROR: The 'get_user_tweets' tool is not available."); 169 | await client.stop(); 170 | return; 171 | } 172 | 173 | // Predefined users 174 | const predefinedUsers = ["doge", "elonmusk", "elizaos"]; 175 | 176 | // Ask user to choose a search method 177 | console.log("\nHow would you like to search for tweets?"); 178 | const searchOptions = [ 179 | "Search for a specific Twitter username", 180 | "Choose from predefined users", 181 | ]; 182 | 183 | const searchChoice = readlineSync.keyInSelect( 184 | searchOptions, 185 | "Select an option:" 186 | ); 187 | 188 | if (searchChoice === -1) { 189 | console.log("Search cancelled. Exiting..."); 190 | await client.stop(); 191 | return; 192 | } 193 | 194 | let username; 195 | 196 | if (searchChoice === 0) { 197 | // User wants to enter a specific username 198 | username = readlineSync.question("Enter Twitter username (without @): "); 199 | 200 | if (!username.trim()) { 201 | console.log("No username provided. Exiting..."); 202 | await client.stop(); 203 | return; 204 | } 205 | } else { 206 | // User wants to choose from predefined users 207 | console.log("\nChoose a user to search for:"); 208 | const userChoice = readlineSync.keyInSelect( 209 | predefinedUsers, 210 | "Select a user:" 211 | ); 212 | 213 | if (userChoice === -1) { 214 | console.log("No user selected. Exiting..."); 215 | await client.stop(); 216 | return; 217 | } 218 | 219 | username = predefinedUsers[userChoice]; 220 | } 221 | 222 | // Ask for number of tweets to retrieve 223 | const countOptions = ["3 tweets", "5 tweets", "10 tweets"]; 224 | const countChoice = readlineSync.keyInSelect( 225 | countOptions, 226 | "How many tweets would you like to retrieve?" 227 | ); 228 | 229 | if (countChoice === -1) { 230 | console.log("No count selected. Exiting..."); 231 | await client.stop(); 232 | return; 233 | } 234 | 235 | const counts = [3, 5, 10]; 236 | const count = counts[countChoice]; 237 | 238 | // Search for tweets 239 | try { 240 | const result = await searchUserTweets(client, username, count); 241 | const displayResult = displayTweets(result); 242 | 243 | // Ask if user wants to save a tweet ID for later use 244 | if ( 245 | displayResult && 246 | displayResult.tweets && 247 | displayResult.tweets.length > 0 248 | ) { 249 | const saveTweetId = readlineSync.keyInYNStrict( 250 | "Would you like to save a tweet ID for replying later?" 251 | ); 252 | 253 | if (saveTweetId) { 254 | const tweetIndex = readlineSync.question( 255 | `Enter the number of the tweet to save (1-${displayResult.tweets.length}): `, 256 | { 257 | limit: (input) => { 258 | const num = parseInt(input); 259 | return num > 0 && num <= displayResult.tweets.length; 260 | }, 261 | limitMessage: `Please enter a number between 1 and ${displayResult.tweets.length}.`, 262 | } 263 | ); 264 | 265 | const selectedTweet = displayResult.tweets[parseInt(tweetIndex) - 1]; 266 | console.log(`\nSaved tweet ID: ${selectedTweet.id}`); 267 | console.log( 268 | `You can use this ID to reply to the tweet using the send-tweet.js script.` 269 | ); 270 | 271 | // Write to a file for later use 272 | try { 273 | fs.writeFileSync("./last-tweet-id.txt", selectedTweet.id); 274 | console.log(`Tweet ID saved to ./last-tweet-id.txt`); 275 | 276 | // Also display the tweet URL for easy access 277 | console.log(`Tweet URL: ${selectedTweet.permanentUrl}`); 278 | } catch (fsError) { 279 | console.error("Error saving tweet ID to file:", fsError.message); 280 | console.log( 281 | `Please note this tweet ID for later use: ${selectedTweet.id}` 282 | ); 283 | console.log(`Tweet URL: ${selectedTweet.permanentUrl}`); 284 | } 285 | } 286 | } 287 | } catch (error) { 288 | console.error("Error searching for tweets:", error.message); 289 | if (error.details) { 290 | console.error("Error details:", JSON.stringify(error.details, null, 2)); 291 | } 292 | } 293 | 294 | // Clean up 295 | await client.stop(); 296 | console.log("MCP client stopped"); 297 | } catch (error) { 298 | console.error("Error:", error); 299 | if (client) { 300 | try { 301 | await client.stop(); 302 | } catch (stopError) { 303 | console.error("Error stopping client:", stopError); 304 | } 305 | } 306 | process.exit(1); 307 | } 308 | } 309 | 310 | main(); 311 | -------------------------------------------------------------------------------- /demo/tweet-thread.js: -------------------------------------------------------------------------------- 1 | import { McpClient } from "./mcp-client.js"; 2 | import { threadTweets } from "./tweets.js"; 3 | import readlineSync from "readline-sync"; 4 | 5 | async function main() { 6 | let client = null; 7 | 8 | try { 9 | console.log("Starting MCP client..."); 10 | client = new McpClient({ 11 | port: process.env.PORT ? parseInt(process.env.PORT) : 3001, 12 | debug: process.env.DEBUG === "true", 13 | maxPortAttempts: 5, 14 | portIncrement: 1, 15 | startServer: process.env.START_SERVER !== "false", // Start server by default 16 | }); 17 | 18 | await client.start(); 19 | console.log("MCP client started successfully!"); 20 | 21 | console.log("\nPreparing to send tweet thread..."); 22 | console.log(`Thread consists of ${threadTweets.length} tweets:`); 23 | threadTweets.forEach((tweet, index) => { 24 | console.log(`[${index + 1}] ${tweet}`); 25 | }); 26 | 27 | // Confirm before sending 28 | const confirm = readlineSync.keyInYNStrict("\nSend this thread?"); 29 | if (!confirm) { 30 | console.log("Cancelled. Exiting..."); 31 | await client.stop(); 32 | return; 33 | } 34 | 35 | // Send the thread 36 | let previousTweetId = null; 37 | let successCount = 0; 38 | 39 | for (let i = 0; i < threadTweets.length; i++) { 40 | console.log(`\nSending tweet ${i + 1}/${threadTweets.length}...`); 41 | 42 | try { 43 | const result = await client.sendTweet(threadTweets[i], previousTweetId); 44 | console.log("Tweet sent successfully!"); 45 | 46 | if (result && result.id) { 47 | console.log("Tweet ID:", result.id); 48 | console.log( 49 | "Tweet URL:", 50 | `https://twitter.com/user/status/${result.id}` 51 | ); 52 | 53 | // Store the tweet ID for the next reply 54 | previousTweetId = result.id; 55 | successCount++; 56 | } else { 57 | console.log("Warning: Tweet ID not returned in the response."); 58 | console.log("Thread may be broken at this point."); 59 | console.log("The tweet may not have been sent successfully."); 60 | 61 | // Ask if user wants to continue without a proper reply chain 62 | if (i < threadTweets.length - 1) { 63 | const continueThread = readlineSync.keyInYNStrict( 64 | "Continue thread without proper reply chain?" 65 | ); 66 | if (!continueThread) { 67 | console.log("Thread sending cancelled by user."); 68 | break; 69 | } 70 | } 71 | } 72 | } catch (error) { 73 | console.error(`\nERROR: Failed to send tweet ${i + 1}:`, error.message); 74 | 75 | // Check for character limit error 76 | if (error.message.includes("cannot exceed 280 characters")) { 77 | console.log("\nThe tweet exceeds Twitter's 280 character limit."); 78 | console.log("Current length:", threadTweets[i].length, "characters"); 79 | console.log("Please edit your tweet to be shorter."); 80 | } 81 | 82 | if (error.details) { 83 | console.error( 84 | "Error details:", 85 | JSON.stringify(error.details, null, 2) 86 | ); 87 | } 88 | 89 | // Ask if user wants to retry or continue 90 | const action = readlineSync.question( 91 | "Enter 'r' to retry, 'c' to continue with next tweet, or any other key to abort: " 92 | ); 93 | 94 | if (action.toLowerCase() === "r") { 95 | i--; // Retry the same tweet 96 | continue; 97 | } else if (action.toLowerCase() === "c") { 98 | console.log("Continuing with next tweet..."); 99 | continue; 100 | } else { 101 | console.log("Thread sending aborted by user."); 102 | break; 103 | } 104 | } 105 | 106 | // Wait a bit between tweets to avoid rate limiting 107 | if (i < threadTweets.length - 1) { 108 | const waitTime = 3; // seconds 109 | console.log(`Waiting ${waitTime} seconds before sending next tweet...`); 110 | await new Promise((resolve) => setTimeout(resolve, waitTime * 1000)); 111 | } 112 | } 113 | 114 | console.log( 115 | `\nThread sending complete. Successfully sent ${successCount}/${threadTweets.length} tweets.` 116 | ); 117 | 118 | // Clean up 119 | await client.stop(); 120 | } catch (error) { 121 | console.error("Error:", error); 122 | if (client) { 123 | try { 124 | await client.stop(); 125 | } catch (stopError) { 126 | console.error("Error stopping client:", stopError); 127 | } 128 | } 129 | process.exit(1); 130 | } 131 | } 132 | 133 | main(); 134 | -------------------------------------------------------------------------------- /demo/tweets.js: -------------------------------------------------------------------------------- 1 | // Collection of tweets for the demo 2 | 3 | export const singleTweets = [ 4 | // Initial Announcement Tweet 5 | `Check out agent-twitter-client-mcp - a Model Context Protocol server for X integration! 6 | 7 | This package makes it easy for AI agents to interact with X without direct API access. 8 | 9 | npm: https://www.npmjs.com/package/agent-twitter-client-mcp 10 | GitHub: https://github.com/ryanmac/agent-twitter-client-mcp 11 | 12 | ✨ Fun fact: This tweet was sent using agent-twitter-client-mcp ✨`, 13 | 14 | // Technical Details Tweet 15 | `agent-twitter-client-mcp provides: 16 | 17 | ✅ Multiple auth methods (cookies, credentials, API) 18 | ✅ Tweet operations (fetch, search, post, like) 19 | ✅ User operations (profiles, follow, followers) 20 | ✅ Grok integration 21 | ✅ Docker support 22 | 23 | Built on the excellent work by @ElizaOS with agent-twitter-client! 24 | 25 | 🤖 This tweet was crafted and posted by an AI using agent-twitter-client-mcp 🤖`, 26 | 27 | // Integration Tweet 28 | `Hey @ElizaOS! Check out this MCP server wrapper around your excellent agent-twitter-client package. 29 | 30 | Would love your feedback and thoughts on potentially adopting it into your infrastructure. It's fully compatible with your existing client! 31 | 32 | 📱 → 💻 → 🐦 (This message traveled from my AI assistant to X via agent-twitter-client-mcp)`, 33 | 34 | // Demo/Use Case Tweet 35 | `With agent-twitter-client-mcp, your AI assistant can: 36 | 37 | 🔍 Search X for relevant content 38 | 📝 Post tweets on your behalf 39 | 👥 Analyze user profiles 40 | 🤖 Chat with Grok via X 41 | 42 | All through a clean, standardized MCP interface. 43 | 44 | [Sent via agent-twitter-client-mcp - it really works!]`, 45 | 46 | // Installation Tweet 47 | `Getting started with agent-twitter-client-mcp is easy: 48 | 49 | npm install -g agent-twitter-client-mcp 50 | 51 | Or use our Docker image: 52 | docker pull ghcr.io/ryanmac/agent-twitter-client-mcp:latest 53 | 54 | Documentation: https://github.com/ryanmac/agent-twitter-client-mcp#readme 55 | 56 | 🔄 This tweet is its own demonstration - posted via the tool it's promoting!`, 57 | 58 | // Short tweet 1 (under 280 characters) 59 | `Testing agent-twitter-client-mcp: A Model Context Protocol for X integration! Built on @elizaOS's work, it lets AI agents interact with X without API keys. #AITools #OpenSource`, 60 | 61 | // Short tweet 2 (under 280 characters) 62 | `Just sent this tweet using agent-twitter-client-mcp! It provides a clean MCP interface for AI agents to interact with X. Check it out: https://github.com/ryanmac/agent-twitter-client-mcp`, 63 | 64 | // Short tweet 3 (well under 280 characters, suitable for replies) 65 | `agent-twitter-client-mcp lets AI assistants tweet, search, and interact with X profiles - all through a clean MCP interface. Perfect for AI agents! #AITools`, 66 | ]; 67 | 68 | export const threadTweets = [ 69 | // Thread 1 (Hook & Intro) 70 | "🧵 1/5 Meet `agent-twitter-client-mcp`: A Model Context Protocol to connect Agents to X!", 71 | 72 | // Thread 2 (Foundation & Credit) 73 | "🧵 2/5 Built on @elizaOS's `agent-twitter-client`, it lets AI agents tap X w/o API keys.", 74 | 75 | // Thread 3 (Features & Benefits) 76 | "🧵 3/5 Features: Tweet ops, user profiles, followers, polls + Grok's real-time X insights. All via a clean MCP. Agents & Devs, this is for you!", 77 | 78 | // Thread 4 (Get Started) 79 | "🧵 4/5 Install it: `npm install agent-twitter-client-mcp` or grab the Docker image: `ghcr.io/ryanmac/agent-twitter-client-mcp:latest`. Docs in repo!", 80 | 81 | // Thread 5 (Call to Action) 82 | "🧵 5/5 @elizaOS @shawmakesmagic: Fork it, tweak it, use it! MIT-licensed. Repo: https://github.com/ryanmac/agent-twitter-client-mcp. (MCP-sent thread!)", 83 | ]; 84 | -------------------------------------------------------------------------------- /demo/twitter-api-test.js: -------------------------------------------------------------------------------- 1 | import { TwitterClient } from "../build/twitter-client.js"; 2 | import dotenv from "dotenv"; 3 | 4 | // Load environment variables 5 | dotenv.config(); 6 | 7 | async function main() { 8 | try { 9 | console.log("=== Twitter API Test ==="); 10 | 11 | // Create a TwitterClient instance 12 | const client = new TwitterClient(); 13 | 14 | // Get the authentication configuration 15 | const authMethod = process.env.AUTH_METHOD || "cookies"; 16 | console.log("Using authentication method:", authMethod); 17 | 18 | let authConfig = { method: authMethod }; 19 | 20 | if (authMethod === "cookies") { 21 | if (!process.env.TWITTER_COOKIES) { 22 | console.error( 23 | "Error: TWITTER_COOKIES environment variable is required for cookie authentication." 24 | ); 25 | process.exit(1); 26 | } 27 | 28 | try { 29 | const cookies = JSON.parse(process.env.TWITTER_COOKIES); 30 | // The authentication.js file expects cookies to be in config.data.cookies 31 | authConfig.data = { cookies }; 32 | console.log("Parsed cookies successfully."); 33 | } catch (error) { 34 | console.error("Error parsing TWITTER_COOKIES:", error.message); 35 | process.exit(1); 36 | } 37 | } else if (authMethod === "credentials") { 38 | if (!process.env.TWITTER_USERNAME || !process.env.TWITTER_PASSWORD) { 39 | console.error( 40 | "Error: TWITTER_USERNAME and TWITTER_PASSWORD environment variables are required for credentials authentication." 41 | ); 42 | process.exit(1); 43 | } 44 | 45 | authConfig.username = process.env.TWITTER_USERNAME; 46 | authConfig.password = process.env.TWITTER_PASSWORD; 47 | 48 | if (process.env.TWITTER_EMAIL) { 49 | authConfig.email = process.env.TWITTER_EMAIL; 50 | } 51 | 52 | if (process.env.TWITTER_2FA_SECRET) { 53 | authConfig.twoFactorSecret = process.env.TWITTER_2FA_SECRET; 54 | } 55 | } else if (authMethod === "api") { 56 | if ( 57 | !process.env.TWITTER_API_KEY || 58 | !process.env.TWITTER_API_SECRET_KEY || 59 | !process.env.TWITTER_ACCESS_TOKEN || 60 | !process.env.TWITTER_ACCESS_TOKEN_SECRET 61 | ) { 62 | console.error( 63 | "Error: TWITTER_API_KEY, TWITTER_API_SECRET_KEY, TWITTER_ACCESS_TOKEN, and TWITTER_ACCESS_TOKEN_SECRET environment variables are required for API authentication." 64 | ); 65 | process.exit(1); 66 | } 67 | 68 | authConfig.apiKey = process.env.TWITTER_API_KEY; 69 | authConfig.apiSecretKey = process.env.TWITTER_API_SECRET_KEY; 70 | authConfig.accessToken = process.env.TWITTER_ACCESS_TOKEN; 71 | authConfig.accessTokenSecret = process.env.TWITTER_ACCESS_TOKEN_SECRET; 72 | } else { 73 | console.error(`Error: Unsupported authentication method: ${authMethod}`); 74 | process.exit(1); 75 | } 76 | 77 | // Send a test tweet 78 | const tweetText = 79 | "Test tweet from Twitter API test script - " + new Date().toISOString(); 80 | console.log("Sending tweet:", tweetText); 81 | 82 | try { 83 | // Add a custom implementation of sendTweet that logs more details 84 | // Save original method for reference or future use 85 | console.log("Original sendTweet method saved, implementing custom version"); 86 | client.sendTweet = async function (config, text, replyToTweetId, media) { 87 | console.log("DEBUG: TwitterClient.sendTweet called with:", { 88 | config: typeof config, 89 | configMethod: config.method, 90 | text, 91 | replyToTweetId, 92 | hasMedia: !!media, 93 | }); 94 | 95 | try { 96 | console.log( 97 | "DEBUG: Getting scraper with config:", 98 | JSON.stringify(config, null, 2) 99 | ); 100 | const scraper = await this.authManager.getScraper(config); 101 | console.log("DEBUG: Got scraper"); 102 | 103 | const processedMedia = media?.map((item) => ({ 104 | data: Buffer.from(item.data, "base64"), 105 | mediaType: item.mediaType, 106 | })); 107 | 108 | console.log("DEBUG: Sending tweet to Twitter API..."); 109 | const response = await scraper.sendTweet( 110 | text, 111 | replyToTweetId, 112 | processedMedia 113 | ); 114 | console.log("DEBUG: Got response from Twitter API"); 115 | 116 | const responseText = await response.text(); 117 | console.log("DEBUG: Raw Twitter API response text:", responseText); 118 | 119 | const responseData = JSON.parse(responseText); 120 | console.log( 121 | "DEBUG: Parsed Twitter API response data:", 122 | JSON.stringify(responseData, null, 2) 123 | ); 124 | 125 | const tweetId = 126 | responseData?.data?.create_tweet?.tweet_results?.result?.rest_id; 127 | console.log("DEBUG: Extracted tweet ID:", tweetId); 128 | 129 | console.log( 130 | "DEBUG: Response data structure:", 131 | JSON.stringify( 132 | { 133 | hasResponseData: !!responseData, 134 | hasData: responseData && !!responseData.data, 135 | hasCreateTweet: 136 | responseData?.data && !!responseData.data.create_tweet, 137 | hasTweetResults: 138 | responseData?.data?.create_tweet && 139 | !!responseData.data.create_tweet.tweet_results, 140 | hasResult: 141 | responseData?.data?.create_tweet?.tweet_results && 142 | !!responseData.data.create_tweet.tweet_results.result, 143 | hasRestId: 144 | responseData?.data?.create_tweet?.tweet_results?.result && 145 | !!responseData.data.create_tweet.tweet_results.result.rest_id, 146 | }, 147 | null, 148 | 2 149 | ) 150 | ); 151 | 152 | if (!tweetId) { 153 | console.log( 154 | "DEBUG: Failed to extract tweet ID from response. Full response data:", 155 | JSON.stringify(responseData, null, 2) 156 | ); 157 | throw new Error("Failed to extract tweet ID from response"); 158 | } 159 | 160 | return await this.getTweetById(config, tweetId); 161 | } catch (error) { 162 | console.log( 163 | "DEBUG: Error in TwitterClient.sendTweet:", 164 | error.message 165 | ); 166 | console.log("DEBUG: Error stack:", error.stack); 167 | throw error; 168 | } 169 | }; 170 | 171 | const result = await client.sendTweet(authConfig, tweetText); 172 | console.log("Tweet sent successfully!"); 173 | console.log("Tweet ID:", result.id); 174 | console.log("Tweet URL:", `https://twitter.com/user/status/${result.id}`); 175 | } catch (error) { 176 | console.error("Error sending tweet:", error.message); 177 | if (error.stack) { 178 | console.error("Error stack:", error.stack); 179 | } 180 | } 181 | } catch (error) { 182 | console.error("Error:", error.message); 183 | if (error.stack) { 184 | console.error("Error stack:", error.stack); 185 | } 186 | process.exit(1); 187 | } 188 | } 189 | 190 | main(); 191 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | agent-twitter-client-mcp: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: agent-twitter-client-mcp 9 | restart: unless-stopped 10 | ports: 11 | - "${MCP_HOST_PORT:-3000}:${MCP_CONTAINER_PORT:-3000}" 12 | environment: 13 | - NODE_ENV=production 14 | - PORT=${MCP_CONTAINER_PORT:-3000} 15 | # Twitter authentication - these should be set in a .env file or through environment variables 16 | - AUTH_METHOD=cookies 17 | - TWITTER_COOKIES=[] 18 | # For username/password auth 19 | - TWITTER_USERNAME= 20 | - TWITTER_PASSWORD= 21 | - TWITTER_EMAIL= 22 | - TWITTER_2FA_SECRET= 23 | # For API auth 24 | - TWITTER_API_KEY= 25 | - TWITTER_API_SECRET_KEY= 26 | - TWITTER_ACCESS_TOKEN= 27 | - TWITTER_ACCESS_TOKEN_SECRET= 28 | volumes: 29 | - ./logs:/app/logs 30 | networks: 31 | - twitter-network 32 | healthcheck: 33 | test: 34 | [ 35 | "CMD", 36 | "wget", 37 | "--no-verbose", 38 | "--tries=1", 39 | "--spider", 40 | "http://localhost:${MCP_CONTAINER_PORT:-3000}/health", 41 | ] 42 | interval: 30s 43 | timeout: 10s 44 | retries: 3 45 | start_period: 5s 46 | 47 | networks: 48 | twitter-network: 49 | driver: bridge 50 | -------------------------------------------------------------------------------- /docs/TESTING.md: -------------------------------------------------------------------------------- 1 | # agent-twitter-client-mcp Testing Guide 2 | 3 | This document provides comprehensive instructions for testing the agent-twitter-client-mcp server to ensure it functions correctly before deployment or after making changes. 4 | 5 | ## Prerequisites 6 | 7 | Before testing, ensure you have: 8 | 9 | 1. Node.js 18+ installed 10 | 2. The project dependencies installed (`npm install`) 11 | 3. Twitter credentials configured (see [Authentication Methods](#authentication-methods)) 12 | 13 | ## Authentication Methods 14 | 15 | The agent-twitter-client-mcp supports three authentication methods: 16 | 17 | ### 1. Cookie Authentication (Recommended) 18 | 19 | Create a `.env` file with: 20 | 21 | ``` 22 | AUTH_METHOD=cookies 23 | TWITTER_COOKIES=["auth_token=your_auth_token; Domain=.twitter.com", "ct0=your_ct0_value; Domain=.twitter.com"] 24 | ``` 25 | 26 | To obtain cookies: 27 | 1. Log in to Twitter in your browser 28 | 2. Open Developer Tools (F12) 29 | 3. Go to the Application tab > Cookies 30 | 4. Copy the values of `auth_token` and `ct0` cookies 31 | 32 | ### 2. Username/Password Authentication 33 | 34 | Create a `.env` file with: 35 | 36 | ``` 37 | AUTH_METHOD=credentials 38 | TWITTER_USERNAME=your_username 39 | TWITTER_PASSWORD=your_password 40 | TWITTER_EMAIL=your_email@example.com # Optional 41 | TWITTER_2FA_SECRET=your_2fa_secret # Optional, required if 2FA is enabled 42 | ``` 43 | 44 | ### 3. API Authentication 45 | 46 | Create a `.env` file with: 47 | 48 | ``` 49 | AUTH_METHOD=api 50 | TWITTER_API_KEY=your_api_key 51 | TWITTER_API_SECRET_KEY=your_api_secret_key 52 | TWITTER_ACCESS_TOKEN=your_access_token 53 | TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret 54 | ``` 55 | 56 | ## Testing Methods 57 | 58 | ### 1. Interactive Test Interface 59 | 60 | The agent-twitter-client-mcp includes an interactive command-line interface for testing: 61 | 62 | ```bash 63 | npm run test:interface 64 | ``` 65 | 66 | This launches a REPL where you can test various MCP functions: 67 | 68 | ``` 69 | 🐦 agent-twitter-client-mcp Test Interface 🐦 70 | 71 | Type a command to test the MCP functionality. Type "help" to see available commands. 72 | 73 | agent-twitter-client-mcp> help 74 | 75 | Available commands: 76 | health Run a health check 77 | profile Get a user profile 78 | tweets [count] Get tweets from a user 79 | tweet Get a specific tweet by ID 80 | search [count] Search for tweets 81 | post Post a new tweet 82 | like Like a tweet 83 | retweet Retweet a tweet 84 | quote Quote a tweet 85 | follow Follow a user 86 | followers [count] Get a user's followers 87 | following [count] Get users a user is following 88 | grok Chat with Grok 89 | help Show available commands 90 | exit Exit the test interface 91 | 92 | agent-twitter-client-mcp> 93 | ``` 94 | 95 | #### Example Commands 96 | 97 | ``` 98 | agent-twitter-client-mcp> health 99 | agent-twitter-client-mcp> profile twitter 100 | agent-twitter-client-mcp> tweets elonmusk 5 101 | agent-twitter-client-mcp> search "artificial intelligence" 3 102 | agent-twitter-client-mcp> post Hello from agent-twitter-client-mcp! 103 | ``` 104 | 105 | ### 2. Automated Tests 106 | 107 | Run the automated test suite: 108 | 109 | ```bash 110 | npm test 111 | ``` 112 | 113 | This runs Jest tests that verify the functionality of various components: 114 | 115 | - Validators 116 | - Twitter client 117 | - Health check 118 | - Authentication 119 | 120 | ### 3. Integration Testing 121 | 122 | For integration testing with actual Twitter API calls: 123 | 124 | ```bash 125 | RUN_INTEGRATION_TESTS=true npm test 126 | ``` 127 | 128 | This runs tests that make actual API calls to Twitter, which is useful for verifying end-to-end functionality. 129 | 130 | ### 4. Testing with Claude Desktop 131 | 132 | To test with Claude Desktop: 133 | 134 | 1. Configure Claude Desktop to use your local MCP: 135 | 136 | Edit your Claude Desktop config file: 137 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 138 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 139 | 140 | ```json 141 | { 142 | "mcpServers": { 143 | "agent-twitter-client-mcp": { 144 | "command": "node", 145 | "args": ["/path/to/your/agent-twitter-client-mcp/build/index.js"], 146 | "env": { 147 | "AUTH_METHOD": "cookies", 148 | "TWITTER_COOKIES": "[\"auth_token=your_auth_token; Domain=.twitter.com\", \"ct0=your_ct0_value; Domain=.twitter.com\"]" 149 | } 150 | } 151 | } 152 | } 153 | ``` 154 | 155 | 2. Restart Claude Desktop 156 | 3. Test with prompts like: 157 | - "Search Twitter for tweets about AI" 158 | - "Get the latest tweets from @OpenAI" 159 | - "Post a tweet saying 'Hello from Claude!'" 160 | 161 | ## Testing Checklist 162 | 163 | Use this checklist to ensure comprehensive testing: 164 | 165 | ### Basic Functionality 166 | - [ ] Health check returns "healthy" status 167 | - [ ] Can retrieve a user profile 168 | - [ ] Can retrieve tweets from a user 169 | - [ ] Can search for tweets 170 | 171 | ### Write Operations (if applicable) 172 | - [ ] Can post a tweet 173 | - [ ] Can like a tweet 174 | - [ ] Can retweet a tweet 175 | - [ ] Can quote a tweet 176 | - [ ] Can follow a user 177 | 178 | ### Grok Integration 179 | - [ ] Can send a message to Grok 180 | - [ ] Can continue a conversation with Grok 181 | 182 | ### Error Handling 183 | - [ ] Invalid inputs are properly rejected 184 | - [ ] Authentication errors are properly handled 185 | - [ ] API errors are properly handled 186 | 187 | ## Troubleshooting 188 | 189 | ### Authentication Issues 190 | 191 | If you encounter authentication issues: 192 | 193 | 1. **Cookie Authentication**: 194 | - Ensure cookies are fresh (they expire after some time) 195 | - Verify the format of the cookies string 196 | - Make sure you have the essential cookies (`auth_token` and `ct0`) 197 | 198 | 2. **Credential Authentication**: 199 | - Check if your account has 2FA enabled (you'll need the `TWITTER_2FA_SECRET`) 200 | - Verify your username and password are correct 201 | - Check if your account is locked due to too many login attempts 202 | 203 | 3. **API Authentication**: 204 | - Verify your API keys have the necessary permissions 205 | - Check if you've exceeded rate limits 206 | - Ensure your API keys are active and not revoked 207 | 208 | ### API Errors 209 | 210 | Common API errors: 211 | 212 | - **Rate Limiting**: Twitter limits how many requests you can make in a time period 213 | - **Content Restrictions**: Twitter may block certain content 214 | - **Media Upload Issues**: Check media format and size 215 | 216 | ### Logging 217 | 218 | Check the log files for detailed error information: 219 | 220 | - `error.log`: Contains error-level messages 221 | - `combined.log`: Contains all log messages 222 | 223 | ## Advanced Testing 224 | 225 | ### Load Testing 226 | 227 | For load testing, you can use tools like Artillery: 228 | 229 | ```bash 230 | npm install -g artillery 231 | artillery run tests/load-test.yml 232 | ``` 233 | 234 | ### Security Testing 235 | 236 | Test with invalid inputs to ensure proper validation: 237 | 238 | ``` 239 | agent-twitter-client-mcp> profile "" 240 | agent-twitter-client-mcp> tweets invalid_username 241 | agent-twitter-client-mcp> post "" 242 | ``` 243 | 244 | ## Conclusion 245 | 246 | By following this testing guide, you can ensure that your agent-twitter-client-mcp server is functioning correctly and ready for production use. Regular testing, especially after updates to the codebase or the Twitter API, will help maintain reliability. -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | extensionsToTreatAsEsm: ['.ts'], 5 | moduleNameMapper: { 6 | '^(\\.{1,2}/.*)\\.js$': '$1', 7 | }, 8 | transform: { 9 | '^.+\\.tsx?$': [ 10 | 'ts-jest', 11 | { 12 | useESM: true, 13 | }, 14 | ], 15 | }, 16 | collectCoverage: true, 17 | coverageDirectory: 'coverage', 18 | coverageReporters: ['text', 'lcov'], 19 | testMatch: ['**/__tests__/**/*.test.ts'], 20 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-twitter-client-mcp", 3 | "version": "0.1.0", 4 | "description": "MCP server for Twitter integration using agent-twitter-client", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node build/index.js", 10 | "dev": "tsx src/index.ts", 11 | "test": "jest", 12 | "test:interface": "tsx src/test-interface.ts", 13 | "lint": "eslint src/**/*.ts", 14 | "prepare": "npm run build", 15 | "prepublishOnly": "npm run lint && npm test", 16 | "version": "npm run lint && git add -A src", 17 | "postversion": "git push && git push --tags" 18 | }, 19 | "bin": { 20 | "agent-twitter-client-mcp": "build/index.js", 21 | "agent-twitter-client-mcp-test": "build/test-interface.js" 22 | }, 23 | "files": [ 24 | "build/**/*", 25 | "docs/**/*", 26 | "README.md", 27 | "LICENSE" 28 | ], 29 | "keywords": [ 30 | "twitter", 31 | "mcp", 32 | "model-context-protocol", 33 | "agent", 34 | "ai", 35 | "agent-twitter-client", 36 | "claude", 37 | "anthropic", 38 | "grok" 39 | ], 40 | "author": "ryanmac", 41 | "license": "MIT", 42 | "dependencies": { 43 | "@modelcontextprotocol/sdk": "^0.6.0", 44 | "agent-twitter-client": "^0.0.18", 45 | "agent-twitter-client-mcp": "^0.1.0", 46 | "dotenv": "^16.4.7", 47 | "winston": "^3.11.0", 48 | "zod": "^3.24.2" 49 | }, 50 | "devDependencies": { 51 | "@types/jest": "^29.5.11", 52 | "@types/node": "^20.11.24", 53 | "@typescript-eslint/eslint-plugin": "^7.0.1", 54 | "@typescript-eslint/parser": "^7.0.1", 55 | "eslint": "^8.56.0", 56 | "jest": "^29.7.0", 57 | "ts-jest": "^29.1.1", 58 | "tsx": "^4.7.1", 59 | "typescript": "^5.3.3" 60 | }, 61 | "engines": { 62 | "node": ">=18.0.0" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "https://github.com/ryanmac/agent-twitter-client-mcp.git" 67 | }, 68 | "bugs": { 69 | "url": "https://github.com/ryanmac/agent-twitter-client-mcp/issues" 70 | }, 71 | "homepage": "https://github.com/ryanmac/agent-twitter-client-mcp#readme", 72 | "publishConfig": { 73 | "access": "public" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - authMethod 10 | properties: 11 | authMethod: 12 | type: string 13 | enum: [cookies, credentials, api] 14 | description: Authentication method to use (cookies, credentials, or api). 15 | cookies: 16 | type: array 17 | description: Twitter cookies for authentication (required for cookies method). 18 | items: 19 | type: string 20 | username: 21 | type: string 22 | description: Twitter username (required for credentials method). 23 | password: 24 | type: string 25 | description: Twitter password (required for credentials method). 26 | email: 27 | type: string 28 | description: Twitter email (optional for credentials method). 29 | twoFactorSecret: 30 | type: string 31 | description: Two-factor authentication secret (optional for credentials method). 32 | apiKey: 33 | type: string 34 | description: Twitter API key (required for api method). 35 | apiSecretKey: 36 | type: string 37 | description: Twitter API secret key (required for api method). 38 | accessToken: 39 | type: string 40 | description: Twitter access token (required for api method). 41 | accessTokenSecret: 42 | type: string 43 | description: Twitter access token secret (required for api method). 44 | commandFunction: 45 | # A function that produces the CLI command to start the MCP on stdio. 46 | |- 47 | config => { 48 | const env = {}; 49 | 50 | // Set AUTH_METHOD 51 | env.AUTH_METHOD = config.authMethod; 52 | 53 | // Set appropriate environment variables based on auth method 54 | if (config.authMethod === 'cookies') { 55 | env.TWITTER_COOKIES = JSON.stringify(config.cookies); 56 | } else if (config.authMethod === 'credentials') { 57 | env.TWITTER_USERNAME = config.username; 58 | env.TWITTER_PASSWORD = config.password; 59 | if (config.email) env.TWITTER_EMAIL = config.email; 60 | if (config.twoFactorSecret) env.TWITTER_2FA_SECRET = config.twoFactorSecret; 61 | } else if (config.authMethod === 'api') { 62 | env.TWITTER_API_KEY = config.apiKey; 63 | env.TWITTER_API_SECRET_KEY = config.apiSecretKey; 64 | env.TWITTER_ACCESS_TOKEN = config.accessToken; 65 | env.TWITTER_ACCESS_TOKEN_SECRET = config.accessTokenSecret; 66 | } 67 | 68 | return { 69 | command: 'node', 70 | args: ['build/index.js'], 71 | env 72 | }; 73 | } -------------------------------------------------------------------------------- /src/__tests__/integration/twitter-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { TwitterClient } from '../../twitter-client.js'; 2 | import { AuthConfig } from '../../types.js'; 3 | import dotenv from 'dotenv'; 4 | 5 | // Load environment variables 6 | dotenv.config(); 7 | 8 | // Skip these tests unless specifically enabled 9 | const runIntegrationTests = process.env.RUN_INTEGRATION_TESTS === 'true'; 10 | 11 | (runIntegrationTests ? describe : describe.skip)('Twitter Integration Tests', () => { 12 | let client: TwitterClient; 13 | let authConfig: AuthConfig; 14 | 15 | beforeAll(() => { 16 | // Set up auth config from environment variables 17 | const authMethod = process.env.AUTH_METHOD || 'cookies'; 18 | 19 | if (authMethod === 'cookies') { 20 | const cookiesStr = process.env.TWITTER_COOKIES; 21 | if (!cookiesStr) { 22 | throw new Error('TWITTER_COOKIES environment variable is required for cookie auth'); 23 | } 24 | authConfig = { 25 | method: 'cookies', 26 | data: { cookies: JSON.parse(cookiesStr) } 27 | }; 28 | } else if (authMethod === 'credentials') { 29 | const username = process.env.TWITTER_USERNAME; 30 | const password = process.env.TWITTER_PASSWORD; 31 | if (!username || !password) { 32 | throw new Error('TWITTER_USERNAME and TWITTER_PASSWORD are required for credential auth'); 33 | } 34 | authConfig = { 35 | method: 'credentials', 36 | data: { 37 | username, 38 | password, 39 | email: process.env.TWITTER_EMAIL, 40 | twoFactorSecret: process.env.TWITTER_2FA_SECRET 41 | } 42 | }; 43 | } else if (authMethod === 'api') { 44 | const apiKey = process.env.TWITTER_API_KEY; 45 | const apiSecretKey = process.env.TWITTER_API_SECRET_KEY; 46 | const accessToken = process.env.TWITTER_ACCESS_TOKEN; 47 | const accessTokenSecret = process.env.TWITTER_ACCESS_TOKEN_SECRET; 48 | if (!apiKey || !apiSecretKey || !accessToken || !accessTokenSecret) { 49 | throw new Error('API credentials are required for API auth'); 50 | } 51 | authConfig = { 52 | method: 'api', 53 | data: { 54 | apiKey, 55 | apiSecretKey, 56 | accessToken, 57 | accessTokenSecret 58 | } 59 | }; 60 | } else { 61 | throw new Error(`Auth method ${authMethod} not configured for tests`); 62 | } 63 | 64 | client = new TwitterClient(); 65 | }); 66 | 67 | test('can fetch a user profile', async () => { 68 | const profile = await client.getUserProfile(authConfig, 'twitter'); 69 | expect(profile).toBeDefined(); 70 | expect(profile.username).toBe('twitter'); 71 | }, 30000); // Longer timeout for API calls 72 | 73 | test('can search tweets', async () => { 74 | const results = await client.searchTweets(authConfig, 'twitter', 5, 'Top'); 75 | expect(results).toBeDefined(); 76 | expect(results.tweets.length).toBeGreaterThan(0); 77 | }, 30000); 78 | 79 | test('can get tweets from a user', async () => { 80 | const tweets = await client.getUserTweets(authConfig, 'twitter', 5); 81 | expect(tweets).toBeDefined(); 82 | expect(tweets.length).toBeGreaterThan(0); 83 | }, 30000); 84 | 85 | // Only run write tests if explicitly enabled 86 | const runWriteTests = process.env.RUN_WRITE_TESTS === 'true'; 87 | 88 | (runWriteTests ? test : test.skip)('can post a tweet', async () => { 89 | const testText = `Test tweet from Twitter MCP ${Date.now()}`; 90 | const tweet = await client.sendTweet(authConfig, testText); 91 | expect(tweet).toBeDefined(); 92 | expect(tweet.text).toBe(testText); 93 | }, 30000); 94 | }); -------------------------------------------------------------------------------- /src/__tests__/utils/validators.test.ts: -------------------------------------------------------------------------------- 1 | import { validateInput, validateMediaData, validatePollOptions } from '../../utils/validators.js'; 2 | import { TwitterMcpError } from '../../types.js'; 3 | import * as zod from 'zod'; 4 | 5 | describe('Validators', () => { 6 | describe('validateInput', () => { 7 | const schema = zod.object({ 8 | username: zod.string().min(1), 9 | count: zod.number().min(1).max(100).default(20) 10 | }); 11 | 12 | test('should validate valid input', () => { 13 | const input = { username: 'testuser' }; 14 | const result = validateInput(schema, input); 15 | expect(result).toEqual({ username: 'testuser', count: 20 }); 16 | }); 17 | 18 | test('should throw error for invalid input', () => { 19 | const input = { username: '' }; 20 | expect(() => validateInput(schema, input)).toThrow(TwitterMcpError); 21 | }); 22 | }); 23 | 24 | describe('validateMediaData', () => { 25 | test('should validate valid media data', () => { 26 | const media = [ 27 | { data: Buffer.from('test').toString('base64'), mediaType: 'image/jpeg' } 28 | ]; 29 | expect(() => validateMediaData(media)).not.toThrow(); 30 | }); 31 | 32 | test('should throw error for too many media items', () => { 33 | const media = Array(5).fill({ data: Buffer.from('test').toString('base64'), mediaType: 'image/jpeg' }); 34 | expect(() => validateMediaData(media)).toThrow(/Maximum of 4 media items/); 35 | }); 36 | 37 | test('should throw error for multiple videos', () => { 38 | const media = [ 39 | { data: Buffer.from('test').toString('base64'), mediaType: 'video/mp4' }, 40 | { data: Buffer.from('test').toString('base64'), mediaType: 'video/mp4' } 41 | ]; 42 | expect(() => validateMediaData(media)).toThrow(/Only one video allowed/); 43 | }); 44 | 45 | test('should throw error for mixing videos and images', () => { 46 | const media = [ 47 | { data: Buffer.from('test').toString('base64'), mediaType: 'video/mp4' }, 48 | { data: Buffer.from('test').toString('base64'), mediaType: 'image/jpeg' } 49 | ]; 50 | expect(() => validateMediaData(media)).toThrow(/Cannot mix videos and images/); 51 | }); 52 | 53 | test('should throw error for unsupported media type', () => { 54 | const media = [ 55 | { data: Buffer.from('test').toString('base64'), mediaType: 'application/pdf' } 56 | ]; 57 | expect(() => validateMediaData(media)).toThrow(/Unsupported media type/); 58 | }); 59 | }); 60 | 61 | describe('validatePollOptions', () => { 62 | test('should validate valid poll options', () => { 63 | const options = [ 64 | { label: 'Option 1' }, 65 | { label: 'Option 2' } 66 | ]; 67 | expect(() => validatePollOptions(options)).not.toThrow(); 68 | }); 69 | 70 | test('should throw error for too few options', () => { 71 | const options = [ 72 | { label: 'Option 1' } 73 | ]; 74 | expect(() => validatePollOptions(options)).toThrow(/Polls must have between 2 and 4 options/); 75 | }); 76 | 77 | test('should throw error for too many options', () => { 78 | const options = [ 79 | { label: 'Option 1' }, 80 | { label: 'Option 2' }, 81 | { label: 'Option 3' }, 82 | { label: 'Option 4' }, 83 | { label: 'Option 5' } 84 | ]; 85 | expect(() => validatePollOptions(options)).toThrow(/Polls must have between 2 and 4 options/); 86 | }); 87 | 88 | test('should throw error for duplicate options', () => { 89 | const options = [ 90 | { label: 'Option 1' }, 91 | { label: 'Option 1' } 92 | ]; 93 | expect(() => validatePollOptions(options)).toThrow(/Poll options must be unique/); 94 | }); 95 | 96 | test('should throw error for options that are too long', () => { 97 | const options = [ 98 | { label: 'Option 1' }, 99 | { label: 'This option label is way too long and exceeds the maximum length of twenty-five characters' } 100 | ]; 101 | expect(() => validatePollOptions(options)).toThrow(/Poll option labels cannot exceed 25 characters/); 102 | }); 103 | }); 104 | }); -------------------------------------------------------------------------------- /src/agent-twitter-client.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'agent-twitter-client' { 2 | export enum SearchMode { 3 | Top = 'Top', 4 | Latest = 'Latest', 5 | Photos = 'Photos', 6 | Videos = 'Videos' 7 | } 8 | 9 | export interface Tweet { 10 | id?: string; 11 | text?: string; 12 | userId?: string; 13 | username?: string; 14 | name?: string; 15 | timeParsed?: Date; 16 | likes?: number; 17 | retweets?: number; 18 | replies?: number; 19 | views?: number; 20 | photos?: { url: string, alt_text: string }[]; 21 | videos?: { url?: string, preview?: string }[]; 22 | urls?: string[]; 23 | isRetweet?: boolean; 24 | isReply?: boolean; 25 | isQuoted?: boolean; 26 | quotedStatus?: Tweet; 27 | inReplyToStatus?: Tweet; 28 | permanentUrl?: string; 29 | } 30 | 31 | export interface Profile { 32 | userId?: string; 33 | username?: string; 34 | name?: string; 35 | biography?: string; 36 | location?: string; 37 | website?: string; 38 | joined?: Date; 39 | isVerified?: boolean; 40 | isPrivate?: boolean; 41 | followersCount?: number; 42 | followingCount?: number; 43 | tweetsCount?: number; 44 | avatar?: string; 45 | banner?: string; 46 | } 47 | 48 | export class Scraper { 49 | isLoggedIn(): Promise; 50 | setCookies(cookies: string[]): Promise; 51 | login(...args: any[]): Promise; 52 | getTweets(username: string, count: number): AsyncIterableIterator; 53 | // Add minimal methods used by the MCP server 54 | getTweet(id: string): Promise; 55 | searchTweets(query: string, count: number, mode: any): AsyncIterableIterator; 56 | sendTweet(text: string, replyToTweetId?: string, media?: any): Promise; 57 | sendTweetV2(text: string, replyToTweetId?: string, options?: any): Promise<{ id: string } | undefined>; 58 | likeTweet(id: string): Promise; 59 | retweet(id: string): Promise; 60 | sendQuoteTweet(text: string, quotedTweetId: string, mediaOptions?: any): Promise; 61 | getProfile(username: string): Promise; 62 | followUser(username: string): Promise; 63 | getFollowers(userId: string, count: number): AsyncIterableIterator; 64 | getFollowing(userId: string, count: number): AsyncIterableIterator; 65 | grokChat(options: { messages: any[], conversationId?: string, returnSearchResults?: boolean, returnCitations?: boolean }): Promise<{ conversationId: string, message: string, webResults?: any[] }>; 66 | getTrends?(): Promise; 67 | } 68 | 69 | // Other types can be added as needed 70 | } -------------------------------------------------------------------------------- /src/authentication.ts: -------------------------------------------------------------------------------- 1 | import { Scraper } from 'agent-twitter-client'; 2 | import { 3 | AuthConfig, 4 | CookieAuth, 5 | CredentialsAuth, 6 | ApiAuth, 7 | TwitterMcpError 8 | } from './types.js'; 9 | 10 | export class AuthenticationManager { 11 | private static instance: AuthenticationManager; 12 | private scraperInstances = new Map(); 13 | 14 | private constructor() {} 15 | 16 | public static getInstance(): AuthenticationManager { 17 | if (!AuthenticationManager.instance) { 18 | AuthenticationManager.instance = new AuthenticationManager(); 19 | } 20 | return AuthenticationManager.instance; 21 | } 22 | 23 | /** 24 | * Get or create a scraper instance based on the provided authentication config 25 | */ 26 | public async getScraper(config: AuthConfig): Promise { 27 | const key = this.getScraperKey(config); 28 | 29 | if (this.scraperInstances.has(key)) { 30 | const scraper = this.scraperInstances.get(key)!; 31 | try { 32 | const isLoggedIn = await scraper.isLoggedIn(); 33 | if (isLoggedIn) { 34 | return scraper; 35 | } 36 | } catch (error) { 37 | console.error('Error checking login status:', error); 38 | } 39 | } 40 | 41 | // Create a new scraper and authenticate 42 | const scraper = new Scraper(); 43 | try { 44 | await this.authenticate(scraper, config); 45 | this.scraperInstances.set(key, scraper); 46 | return scraper; 47 | } catch (error) { 48 | throw new TwitterMcpError( 49 | `Authentication failed: ${(error as Error).message}`, 50 | 'auth_failure', 51 | 401 52 | ); 53 | } 54 | } 55 | 56 | /** 57 | * Authenticate a scraper instance based on config 58 | */ 59 | private async authenticate(scraper: Scraper, config: AuthConfig): Promise { 60 | switch (config.method) { 61 | case 'cookies': 62 | await this.authenticateWithCookies(scraper, config.data as CookieAuth); 63 | break; 64 | case 'credentials': 65 | await this.authenticateWithCredentials(scraper, config.data as CredentialsAuth); 66 | break; 67 | case 'api': 68 | await this.authenticateWithApi(scraper, config.data as ApiAuth); 69 | break; 70 | default: 71 | throw new TwitterMcpError( 72 | `Unsupported authentication method: ${config.method}`, 73 | 'unsupported_auth_method', 74 | 400 75 | ); 76 | } 77 | } 78 | 79 | /** 80 | * Authenticate using cookies 81 | */ 82 | private async authenticateWithCookies(scraper: Scraper, auth: CookieAuth): Promise { 83 | try { 84 | await scraper.setCookies(auth.cookies); 85 | const isLoggedIn = await scraper.isLoggedIn(); 86 | if (!isLoggedIn) { 87 | throw new TwitterMcpError( 88 | 'Cookie authentication failed', 89 | 'cookie_auth_failure', 90 | 401 91 | ); 92 | } 93 | } catch (error) { 94 | if (error instanceof TwitterMcpError) { 95 | throw error; 96 | } 97 | throw new TwitterMcpError( 98 | `Cookie authentication error: ${(error as Error).message}`, 99 | 'cookie_auth_error', 100 | 500 101 | ); 102 | } 103 | } 104 | 105 | /** 106 | * Authenticate using username/password 107 | */ 108 | private async authenticateWithCredentials(scraper: Scraper, auth: CredentialsAuth): Promise { 109 | try { 110 | await scraper.login( 111 | auth.username, 112 | auth.password, 113 | auth.email, 114 | auth.twoFactorSecret 115 | ); 116 | } catch (error) { 117 | throw new TwitterMcpError( 118 | `Credential authentication error: ${(error as Error).message}`, 119 | 'credential_auth_error', 120 | 401 121 | ); 122 | } 123 | } 124 | 125 | /** 126 | * Authenticate using Twitter API keys 127 | */ 128 | private async authenticateWithApi(scraper: Scraper, auth: ApiAuth): Promise { 129 | try { 130 | // Login with temporary credentials to get a session 131 | await scraper.login( 132 | 'temp_user', 133 | 'temp_pass', 134 | undefined, 135 | undefined, 136 | auth.apiKey, 137 | auth.apiSecretKey, 138 | auth.accessToken, 139 | auth.accessTokenSecret 140 | ); 141 | } catch (error) { 142 | throw new TwitterMcpError( 143 | `API authentication error: ${(error as Error).message}`, 144 | 'api_auth_error', 145 | 401 146 | ); 147 | } 148 | } 149 | 150 | /** 151 | * Generate a unique key for the scraper instance 152 | */ 153 | private getScraperKey(config: AuthConfig): string { 154 | let cookieAuth: CookieAuth; 155 | let authTokenCookie: string | undefined; 156 | let ct0Cookie: string | undefined; 157 | let authToken: string; 158 | let ct0: string; 159 | let creds: CredentialsAuth; 160 | let api: ApiAuth; 161 | 162 | switch (config.method) { 163 | case 'cookies': 164 | // For cookies, use a combination of auth_token and ct0 if available 165 | cookieAuth = config.data as CookieAuth; 166 | authTokenCookie = cookieAuth.cookies.find(c => c.includes('auth_token=')); 167 | ct0Cookie = cookieAuth.cookies.find(c => c.includes('ct0=')); 168 | 169 | if (authTokenCookie && ct0Cookie) { 170 | authToken = authTokenCookie.split('=')[1].split(';')[0]; 171 | ct0 = ct0Cookie.split('=')[1].split(';')[0]; 172 | return `cookies_${authToken}_${ct0}`; 173 | } 174 | return `cookies_${Date.now()}`; 175 | 176 | case 'credentials': 177 | creds = config.data as CredentialsAuth; 178 | return `credentials_${creds.username}`; 179 | 180 | case 'api': 181 | api = config.data as ApiAuth; 182 | return `api_${api.apiKey}`; 183 | 184 | default: 185 | return `unknown_${Date.now()}`; 186 | } 187 | } 188 | 189 | /** 190 | * Clear all scraper instances 191 | */ 192 | public clearAllScrapers(): void { 193 | this.scraperInstances.clear(); 194 | } 195 | 196 | /** 197 | * Try to authenticate with both cookies and credentials if available 198 | * This is useful for Grok which may require both 199 | */ 200 | public async getHybridScraper(cookieConfig?: AuthConfig, credentialsConfig?: AuthConfig): Promise { 201 | // Create a new scraper 202 | const scraper = new Scraper(); 203 | 204 | let isLoggedIn = false; 205 | 206 | // Try cookies first if available 207 | if (cookieConfig && cookieConfig.method === 'cookies') { 208 | try { 209 | await this.authenticateWithCookies(scraper, cookieConfig.data as CookieAuth); 210 | isLoggedIn = await scraper.isLoggedIn(); 211 | } catch (error) { 212 | console.warn('Cookie authentication failed, will try credentials:', error); 213 | } 214 | } 215 | 216 | // If cookies didn't work or weren't provided, try credentials 217 | if (!isLoggedIn && credentialsConfig && credentialsConfig.method === 'credentials') { 218 | try { 219 | await this.authenticateWithCredentials(scraper, credentialsConfig.data as CredentialsAuth); 220 | isLoggedIn = await scraper.isLoggedIn(); 221 | } catch (error) { 222 | console.warn('Credential authentication failed:', error); 223 | } 224 | } 225 | 226 | if (!isLoggedIn) { 227 | throw new TwitterMcpError( 228 | 'Failed to authenticate with both cookies and credentials', 229 | 'hybrid_auth_failure', 230 | 401 231 | ); 232 | } 233 | 234 | return scraper; 235 | } 236 | } -------------------------------------------------------------------------------- /src/health.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from './types.js'; 2 | import { AuthenticationManager } from './authentication.js'; 3 | import { logger, logError } from './utils/logger.js'; 4 | 5 | /** 6 | * Perform a health check on the Twitter MCP server 7 | */ 8 | export async function performHealthCheck(authConfig: AuthConfig): Promise<{ 9 | status: 'healthy' | 'unhealthy'; 10 | details: Record; 11 | }> { 12 | const details: Record = { 13 | timestamp: new Date().toISOString(), 14 | components: {} 15 | }; 16 | let isHealthy = true; 17 | 18 | try { 19 | // Check if we can authenticate 20 | const authManager = AuthenticationManager.getInstance(); 21 | 22 | try { 23 | const scraper = await authManager.getScraper(authConfig); 24 | 25 | // Check if we're logged in 26 | const loggedIn = await scraper.isLoggedIn(); 27 | details.components.authentication = { 28 | status: loggedIn ? 'healthy' : 'unhealthy', 29 | message: loggedIn ? 'Successfully authenticated' : 'Not authenticated' 30 | }; 31 | 32 | if (!loggedIn) { 33 | isHealthy = false; 34 | } 35 | 36 | // Check if we can fetch trends (basic API call) 37 | try { 38 | if (scraper.getTrends) { 39 | const trends = await scraper.getTrends(); 40 | details.components.api = { 41 | status: 'healthy', 42 | message: 'API is accessible', 43 | trendsCount: trends.length 44 | }; 45 | } else { 46 | // If getTrends is not available, try a different API call 47 | const profile = await scraper.getProfile('twitter'); 48 | details.components.api = { 49 | status: 'healthy', 50 | message: 'API is accessible', 51 | profileCheck: !!profile 52 | }; 53 | } 54 | } catch (error) { 55 | details.components.api = { 56 | status: 'unhealthy', 57 | message: 'API is not accessible', 58 | error: error instanceof Error ? error.message : String(error) 59 | }; 60 | isHealthy = false; 61 | logError('Health check API error', error); 62 | } 63 | } catch (error) { 64 | details.components.authentication = { 65 | status: 'unhealthy', 66 | message: 'Authentication failed', 67 | error: error instanceof Error ? error.message : String(error) 68 | }; 69 | isHealthy = false; 70 | logError('Health check authentication error', error); 71 | } 72 | 73 | // Check memory usage 74 | const memoryUsage = process.memoryUsage(); 75 | details.components.memory = { 76 | status: 'healthy', 77 | message: 'Memory usage is normal', 78 | usage: { 79 | rss: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB`, 80 | heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)} MB`, 81 | heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB` 82 | } 83 | }; 84 | 85 | // Overall status 86 | details.status = isHealthy ? 'healthy' : 'unhealthy'; 87 | 88 | logger.info('Health check completed', { isHealthy, details }); 89 | 90 | return { 91 | status: isHealthy ? 'healthy' : 'unhealthy', 92 | details 93 | }; 94 | } catch (error) { 95 | logError('Health check failed with unexpected error', error); 96 | 97 | return { 98 | status: 'unhealthy', 99 | details: { 100 | timestamp: new Date().toISOString(), 101 | error: error instanceof Error ? error.message : String(error), 102 | stack: error instanceof Error ? error.stack : undefined 103 | } 104 | }; 105 | } 106 | } -------------------------------------------------------------------------------- /src/test-interface.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { TwitterClient } from './twitter-client.js'; 3 | import { AuthConfig } from './types.js'; 4 | import { performHealthCheck } from './health.js'; 5 | import { logInfo, logError } from './utils/logger.js'; 6 | import dotenv from 'dotenv'; 7 | import readline from 'readline'; 8 | import { TweetTools } from './tools/tweets.js'; 9 | import { ProfileTools } from './tools/profiles.js'; 10 | import { GrokTools } from './tools/grok.js'; 11 | 12 | // Load environment variables 13 | dotenv.config(); 14 | 15 | // Create tools instances 16 | const tweetTools = new TweetTools(); 17 | const profileTools = new ProfileTools(); 18 | const grokTools = new GrokTools(); 19 | const client = new TwitterClient(); 20 | 21 | // Configure auth from environment variables 22 | function getAuthConfig(): AuthConfig { 23 | // Determine auth method 24 | const authMethod = process.env.AUTH_METHOD || 'cookies'; 25 | 26 | switch (authMethod) { 27 | case 'cookies': 28 | const cookiesStr = process.env.TWITTER_COOKIES; 29 | if (!cookiesStr) { 30 | throw new Error('TWITTER_COOKIES environment variable is required for cookie auth'); 31 | } 32 | return { 33 | method: 'cookies', 34 | data: { cookies: JSON.parse(cookiesStr) } 35 | }; 36 | 37 | case 'credentials': 38 | const username = process.env.TWITTER_USERNAME; 39 | const password = process.env.TWITTER_PASSWORD; 40 | if (!username || !password) { 41 | throw new Error('TWITTER_USERNAME and TWITTER_PASSWORD are required for credential auth'); 42 | } 43 | return { 44 | method: 'credentials', 45 | data: { 46 | username, 47 | password, 48 | email: process.env.TWITTER_EMAIL, 49 | twoFactorSecret: process.env.TWITTER_2FA_SECRET 50 | } 51 | }; 52 | 53 | case 'api': 54 | const apiKey = process.env.TWITTER_API_KEY; 55 | const apiSecretKey = process.env.TWITTER_API_SECRET_KEY; 56 | const accessToken = process.env.TWITTER_ACCESS_TOKEN; 57 | const accessTokenSecret = process.env.TWITTER_ACCESS_TOKEN_SECRET; 58 | if (!apiKey || !apiSecretKey || !accessToken || !accessTokenSecret) { 59 | throw new Error('API credentials are required for API auth'); 60 | } 61 | return { 62 | method: 'api', 63 | data: { 64 | apiKey, 65 | apiSecretKey, 66 | accessToken, 67 | accessTokenSecret 68 | } 69 | }; 70 | 71 | default: 72 | throw new Error(`Unsupported auth method: ${authMethod}`); 73 | } 74 | } 75 | 76 | // Get auth config 77 | let authConfig: AuthConfig; 78 | try { 79 | authConfig = getAuthConfig(); 80 | logInfo('Authentication configuration loaded', { method: authConfig.method }); 81 | } catch (error) { 82 | logError('Failed to load authentication configuration', error); 83 | process.exit(1); 84 | } 85 | 86 | // Create readline interface 87 | const rl = readline.createInterface({ 88 | input: process.stdin, 89 | output: process.stdout 90 | }); 91 | 92 | // Available test commands 93 | const commands = { 94 | 'health': 'Run a health check', 95 | 'profile ': 'Get a user profile', 96 | 'tweets [count]': 'Get tweets from a user', 97 | 'tweet ': 'Get a specific tweet by ID', 98 | 'search [count]': 'Search for tweets', 99 | 'post ': 'Post a new tweet', 100 | 'like ': 'Like a tweet', 101 | 'retweet ': 'Retweet a tweet', 102 | 'quote ': 'Quote a tweet', 103 | 'follow ': 'Follow a user', 104 | 'followers [count]': 'Get a user\'s followers', 105 | 'following [count]': 'Get users a user is following', 106 | 'grok ': 'Chat with Grok', 107 | 'help': 'Show available commands', 108 | 'exit': 'Exit the test interface' 109 | }; 110 | 111 | // Show welcome message 112 | console.log('\n🐦 Twitter MCP Test Interface 🐦\n'); 113 | console.log('Type a command to test the MCP functionality. Type "help" to see available commands.\n'); 114 | 115 | // Process commands 116 | async function processCommand(input: string) { 117 | const args = input.trim().split(' '); 118 | const command = args[0].toLowerCase(); 119 | 120 | try { 121 | switch (command) { 122 | case 'health': 123 | console.log('Running health check...'); 124 | const healthResult = await performHealthCheck(authConfig); 125 | console.log(JSON.stringify(healthResult, null, 2)); 126 | break; 127 | 128 | case 'profile': 129 | if (!args[1]) { 130 | console.log('Error: Username is required'); 131 | break; 132 | } 133 | console.log(`Getting profile for ${args[1]}...`); 134 | const profileResult = await profileTools.getUserProfile(authConfig, { username: args[1] }); 135 | console.log(JSON.stringify(profileResult, null, 2)); 136 | break; 137 | 138 | case 'tweets': 139 | if (!args[1]) { 140 | console.log('Error: Username is required'); 141 | break; 142 | } 143 | const count = args[2] ? parseInt(args[2]) : 10; 144 | console.log(`Getting ${count} tweets from ${args[1]}...`); 145 | const tweetsResult = await tweetTools.getUserTweets(authConfig, { 146 | username: args[1], 147 | count, 148 | includeReplies: false, 149 | includeRetweets: true 150 | }); 151 | console.log(JSON.stringify(tweetsResult, null, 2)); 152 | break; 153 | 154 | case 'tweet': 155 | if (!args[1]) { 156 | console.log('Error: Tweet ID is required'); 157 | break; 158 | } 159 | console.log(`Getting tweet ${args[1]}...`); 160 | const tweetResult = await tweetTools.getTweetById(authConfig, { id: args[1] }); 161 | console.log(JSON.stringify(tweetResult, null, 2)); 162 | break; 163 | 164 | case 'search': 165 | if (!args[1]) { 166 | console.log('Error: Search query is required'); 167 | break; 168 | } 169 | const searchCount = args[2] ? parseInt(args[2]) : 10; 170 | console.log(`Searching for "${args[1]}"...`); 171 | const searchResult = await tweetTools.searchTweets(authConfig, { 172 | query: args[1], 173 | count: searchCount, 174 | searchMode: 'Top' 175 | }); 176 | console.log(JSON.stringify(searchResult, null, 2)); 177 | break; 178 | 179 | case 'post': 180 | if (!args[1]) { 181 | console.log('Error: Tweet text is required'); 182 | break; 183 | } 184 | const tweetText = args.slice(1).join(' '); 185 | console.log(`Posting tweet: "${tweetText}"...`); 186 | const postResult = await tweetTools.sendTweet(authConfig, { text: tweetText }); 187 | console.log(JSON.stringify(postResult, null, 2)); 188 | break; 189 | 190 | case 'like': 191 | if (!args[1]) { 192 | console.log('Error: Tweet ID is required'); 193 | break; 194 | } 195 | console.log(`Liking tweet ${args[1]}...`); 196 | const likeResult = await tweetTools.likeTweet(authConfig, { id: args[1] }); 197 | console.log(JSON.stringify(likeResult, null, 2)); 198 | break; 199 | 200 | case 'retweet': 201 | if (!args[1]) { 202 | console.log('Error: Tweet ID is required'); 203 | break; 204 | } 205 | console.log(`Retweeting tweet ${args[1]}...`); 206 | const retweetResult = await tweetTools.retweet(authConfig, { id: args[1] }); 207 | console.log(JSON.stringify(retweetResult, null, 2)); 208 | break; 209 | 210 | case 'quote': 211 | if (!args[1] || !args[2]) { 212 | console.log('Error: Tweet ID and quote text are required'); 213 | break; 214 | } 215 | const quoteText = args.slice(2).join(' '); 216 | console.log(`Quoting tweet ${args[1]} with: "${quoteText}"...`); 217 | const quoteResult = await tweetTools.quoteTweet(authConfig, { 218 | quotedTweetId: args[1], 219 | text: quoteText 220 | }); 221 | console.log(JSON.stringify(quoteResult, null, 2)); 222 | break; 223 | 224 | case 'follow': 225 | if (!args[1]) { 226 | console.log('Error: Username is required'); 227 | break; 228 | } 229 | console.log(`Following user ${args[1]}...`); 230 | const followResult = await profileTools.followUser(authConfig, { username: args[1] }); 231 | console.log(JSON.stringify(followResult, null, 2)); 232 | break; 233 | 234 | case 'followers': 235 | if (!args[1]) { 236 | console.log('Error: User ID is required'); 237 | break; 238 | } 239 | const followersCount = args[2] ? parseInt(args[2]) : 10; 240 | console.log(`Getting ${followersCount} followers for user ${args[1]}...`); 241 | const followersResult = await profileTools.getFollowers(authConfig, { 242 | userId: args[1], 243 | count: followersCount 244 | }); 245 | console.log(JSON.stringify(followersResult, null, 2)); 246 | break; 247 | 248 | case 'following': 249 | if (!args[1]) { 250 | console.log('Error: User ID is required'); 251 | break; 252 | } 253 | const followingCount = args[2] ? parseInt(args[2]) : 10; 254 | console.log(`Getting ${followingCount} following for user ${args[1]}...`); 255 | const followingResult = await profileTools.getFollowing(authConfig, { 256 | userId: args[1], 257 | count: followingCount 258 | }); 259 | console.log(JSON.stringify(followingResult, null, 2)); 260 | break; 261 | 262 | case 'grok': 263 | if (!args[1]) { 264 | console.log('Error: Message is required'); 265 | break; 266 | } 267 | const message = args.slice(1).join(' '); 268 | console.log(`Sending message to Grok: "${message}"...`); 269 | const grokResult = await grokTools.grokChat(authConfig, { 270 | message, 271 | returnSearchResults: true, 272 | returnCitations: true 273 | }); 274 | console.log(JSON.stringify(grokResult, null, 2)); 275 | break; 276 | 277 | case 'help': 278 | console.log('\nAvailable commands:'); 279 | Object.entries(commands).forEach(([cmd, desc]) => { 280 | console.log(` ${cmd.padEnd(25)} ${desc}`); 281 | }); 282 | console.log(); 283 | break; 284 | 285 | case 'exit': 286 | console.log('Exiting test interface...'); 287 | rl.close(); 288 | process.exit(0); 289 | break; 290 | 291 | default: 292 | console.log(`Unknown command: ${command}`); 293 | console.log('Type "help" to see available commands.'); 294 | break; 295 | } 296 | } catch (error) { 297 | logError(`Error executing command ${command}`, error); 298 | console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); 299 | } 300 | 301 | // Prompt for next command 302 | rl.prompt(); 303 | } 304 | 305 | // Start the command loop 306 | rl.setPrompt('agent-twitter-client-mcp> '); 307 | rl.prompt(); 308 | rl.on('line', async (line) => { 309 | await processCommand(line); 310 | }).on('close', () => { 311 | console.log('Goodbye!'); 312 | process.exit(0); 313 | }); -------------------------------------------------------------------------------- /src/test-zod.js: -------------------------------------------------------------------------------- 1 | import * as zod from "zod"; 2 | // Define a simple schema 3 | const schema = zod.object({ 4 | name: zod.string(), 5 | age: zod.number(), 6 | }); 7 | // Define an enum 8 | const searchModes = ["Top", "Latest", "Photos", "Videos"]; 9 | const searchModeSchema = zod.enum(searchModes); 10 | console.log("Schema:", schema); 11 | console.log("Enum Schema:", searchModeSchema); 12 | -------------------------------------------------------------------------------- /src/test-zod.ts: -------------------------------------------------------------------------------- 1 | import * as zod from 'zod'; 2 | 3 | // Define a simple schema 4 | const schema = zod.object({ 5 | name: zod.string(), 6 | age: zod.number() 7 | }); 8 | 9 | // Define an enum 10 | const searchModes = ['Top', 'Latest', 'Photos', 'Videos'] as const; 11 | const searchModeSchema = zod.string(); 12 | 13 | console.log('Schema:', schema); 14 | console.log('Enum Schema:', searchModeSchema); -------------------------------------------------------------------------------- /src/tools/grok.ts: -------------------------------------------------------------------------------- 1 | import { GrokChatSchema, AuthConfig } from '../types.js'; 2 | import { TwitterClient } from '../twitter-client.js'; 3 | import { validateInput } from '../utils/validators.js'; 4 | 5 | // Define type for the validated parameters 6 | type GrokChatParams = { 7 | message: string; 8 | conversationId?: string; 9 | returnSearchResults: boolean; 10 | returnCitations: boolean; 11 | }; 12 | 13 | export class GrokTools { 14 | private client: TwitterClient; 15 | 16 | constructor() { 17 | this.client = new TwitterClient(); 18 | } 19 | 20 | /** 21 | * Chat with Grok 22 | */ 23 | async grokChat(authConfig: AuthConfig, args: unknown) { 24 | const params = validateInput(GrokChatSchema, args); 25 | const response = await this.client.grokChat( 26 | authConfig, 27 | params.message, 28 | params.conversationId, 29 | params.returnSearchResults, 30 | params.returnCitations 31 | ); 32 | 33 | return { 34 | response: response.message, 35 | conversationId: response.conversationId, 36 | webResults: response.webResults 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /src/tools/profiles.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetUserProfileSchema, 3 | FollowUserSchema, 4 | GetFollowersSchema, 5 | GetFollowingSchema, 6 | AuthConfig 7 | } from '../types.js'; 8 | import { TwitterClient } from '../twitter-client.js'; 9 | import { validateInput } from '../utils/validators.js'; 10 | 11 | // Define types for the validated parameters 12 | type GetUserProfileParams = { 13 | username: string; 14 | }; 15 | 16 | type FollowUserParams = { 17 | username: string; 18 | }; 19 | 20 | type GetFollowersParams = { 21 | userId: string; 22 | count: number; 23 | }; 24 | 25 | type GetFollowingParams = { 26 | userId: string; 27 | count: number; 28 | }; 29 | 30 | export class ProfileTools { 31 | private client: TwitterClient; 32 | 33 | constructor() { 34 | this.client = new TwitterClient(); 35 | } 36 | 37 | /** 38 | * Get a user profile 39 | */ 40 | async getUserProfile(authConfig: AuthConfig, args: unknown) { 41 | const params = validateInput(GetUserProfileSchema, args); 42 | const profile = await this.client.getUserProfile(authConfig, params.username); 43 | 44 | return { 45 | profile 46 | }; 47 | } 48 | 49 | /** 50 | * Follow a user 51 | */ 52 | async followUser(authConfig: AuthConfig, args: unknown) { 53 | const params = validateInput(FollowUserSchema, args); 54 | const result = await this.client.followUser(authConfig, params.username); 55 | 56 | return result; 57 | } 58 | 59 | /** 60 | * Get a user's followers 61 | */ 62 | async getFollowers(authConfig: AuthConfig, args: unknown) { 63 | const params = validateInput(GetFollowersSchema, args); 64 | const profiles = await this.client.getFollowers( 65 | authConfig, 66 | params.userId, 67 | params.count 68 | ); 69 | 70 | return { 71 | profiles, 72 | count: profiles.length, 73 | userId: params.userId 74 | }; 75 | } 76 | 77 | /** 78 | * Get a user's following 79 | */ 80 | async getFollowing(authConfig: AuthConfig, args: unknown) { 81 | const params = validateInput(GetFollowingSchema, args); 82 | const profiles = await this.client.getFollowing( 83 | authConfig, 84 | params.userId, 85 | params.count 86 | ); 87 | 88 | return { 89 | profiles, 90 | count: profiles.length, 91 | userId: params.userId 92 | }; 93 | } 94 | } -------------------------------------------------------------------------------- /src/tools/tweets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetUserTweetsSchema, 3 | GetTweetByIdSchema, 4 | SearchTweetsSchema, 5 | SendTweetSchema, 6 | SendTweetWithPollSchema, 7 | LikeTweetSchema, 8 | RetweetSchema, 9 | QuoteTweetSchema, 10 | AuthConfig 11 | } from '../types.js'; 12 | import { TwitterClient } from '../twitter-client.js'; 13 | import { validateInput, validateMediaData, validatePollOptions } from '../utils/validators.js'; 14 | 15 | // Define types for the validated parameters 16 | type GetUserTweetsParams = { 17 | username: string; 18 | count: number; 19 | includeReplies: boolean; 20 | includeRetweets: boolean; 21 | }; 22 | 23 | type GetTweetByIdParams = { 24 | id: string; 25 | }; 26 | 27 | type SearchTweetsParams = { 28 | query: string; 29 | count: number; 30 | searchMode: string; 31 | }; 32 | 33 | type SendTweetParams = { 34 | text: string; 35 | replyToTweetId?: string; 36 | media?: { data: string; mediaType: string }[]; 37 | }; 38 | 39 | type SendTweetWithPollParams = { 40 | text: string; 41 | replyToTweetId?: string; 42 | poll: { 43 | options: { label: string }[]; 44 | durationMinutes: number; 45 | }; 46 | }; 47 | 48 | type LikeTweetParams = { 49 | id: string; 50 | }; 51 | 52 | type RetweetParams = { 53 | id: string; 54 | }; 55 | 56 | type QuoteTweetParams = { 57 | text: string; 58 | quotedTweetId: string; 59 | media?: { data: string; mediaType: string }[]; 60 | }; 61 | 62 | export class TweetTools { 63 | private client: TwitterClient; 64 | 65 | constructor() { 66 | this.client = new TwitterClient(); 67 | } 68 | 69 | /** 70 | * Get tweets from a user 71 | */ 72 | async getUserTweets(authConfig: AuthConfig, args: unknown) { 73 | const params = validateInput(GetUserTweetsSchema, args); 74 | const tweets = await this.client.getUserTweets( 75 | authConfig, 76 | params.username, 77 | params.count, 78 | params.includeReplies, 79 | params.includeRetweets 80 | ); 81 | 82 | return { 83 | tweets, 84 | count: tweets.length, 85 | username: params.username 86 | }; 87 | } 88 | 89 | /** 90 | * Get a specific tweet by ID 91 | */ 92 | async getTweetById(authConfig: AuthConfig, args: unknown) { 93 | const params = validateInput(GetTweetByIdSchema, args); 94 | const tweet = await this.client.getTweetById(authConfig, params.id); 95 | 96 | return { 97 | tweet 98 | }; 99 | } 100 | 101 | /** 102 | * Search for tweets 103 | */ 104 | async searchTweets(authConfig: AuthConfig, args: unknown) { 105 | const params = validateInput(SearchTweetsSchema, args); 106 | const searchResults = await this.client.searchTweets( 107 | authConfig, 108 | params.query, 109 | params.count, 110 | params.searchMode 111 | ); 112 | 113 | return searchResults; 114 | } 115 | 116 | /** 117 | * Send a tweet 118 | */ 119 | async sendTweet(authConfig: AuthConfig, args: unknown) { 120 | const params = validateInput(SendTweetSchema, args); 121 | 122 | // Validate media if present 123 | if (params.media && params.media.length > 0) { 124 | validateMediaData(params.media); 125 | } 126 | 127 | const tweet = await this.client.sendTweet( 128 | authConfig, 129 | params.text, 130 | params.replyToTweetId, 131 | params.media 132 | ); 133 | 134 | return { 135 | tweet, 136 | success: true, 137 | message: 'Tweet sent successfully' 138 | }; 139 | } 140 | 141 | /** 142 | * Send a tweet with poll 143 | */ 144 | async sendTweetWithPoll(authConfig: AuthConfig, args: unknown) { 145 | const params = validateInput(SendTweetWithPollSchema, args); 146 | 147 | // Validate poll options 148 | validatePollOptions(params.poll.options); 149 | 150 | const tweet = await this.client.sendTweetWithPoll( 151 | authConfig, 152 | params.text, 153 | params.poll, 154 | params.replyToTweetId 155 | ); 156 | 157 | return { 158 | tweet, 159 | success: true, 160 | message: 'Tweet with poll sent successfully' 161 | }; 162 | } 163 | 164 | /** 165 | * Like a tweet 166 | */ 167 | async likeTweet(authConfig: AuthConfig, args: unknown) { 168 | const params = validateInput(LikeTweetSchema, args); 169 | const result = await this.client.likeTweet(authConfig, params.id); 170 | 171 | return result; 172 | } 173 | 174 | /** 175 | * Retweet a tweet 176 | */ 177 | async retweet(authConfig: AuthConfig, args: unknown) { 178 | const params = validateInput(RetweetSchema, args); 179 | const result = await this.client.retweet(authConfig, params.id); 180 | 181 | return result; 182 | } 183 | 184 | /** 185 | * Quote a tweet 186 | */ 187 | async quoteTweet(authConfig: AuthConfig, args: unknown) { 188 | const params = validateInput(QuoteTweetSchema, args); 189 | 190 | // Validate media if present 191 | if (params.media && params.media.length > 0) { 192 | validateMediaData(params.media); 193 | } 194 | 195 | const tweet = await this.client.quoteTweet( 196 | authConfig, 197 | params.text, 198 | params.quotedTweetId, 199 | params.media 200 | ); 201 | 202 | return { 203 | tweet, 204 | success: true, 205 | message: 'Quote tweet sent successfully' 206 | }; 207 | } 208 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as zod from 'zod'; 2 | import { Tweet, Profile } from 'agent-twitter-client'; 3 | 4 | // Authentication Types 5 | export type AuthMethod = 'cookies' | 'credentials' | 'api'; 6 | 7 | export interface AuthConfig { 8 | method: AuthMethod; 9 | data: CookieAuth | CredentialsAuth | ApiAuth; 10 | } 11 | 12 | export interface CookieAuth { 13 | cookies: string[]; 14 | } 15 | 16 | export interface CredentialsAuth { 17 | username: string; 18 | password: string; 19 | email?: string; 20 | twoFactorSecret?: string; 21 | } 22 | 23 | export interface ApiAuth { 24 | apiKey: string; 25 | apiSecretKey: string; 26 | accessToken: string; 27 | accessTokenSecret: string; 28 | } 29 | 30 | // Tool Input Schemas 31 | export const GetUserTweetsSchema = zod.object({ 32 | username: zod.string().min(1, 'Username is required'), 33 | count: zod.number().int().min(1).max(200).default(20), 34 | includeReplies: zod.boolean().default(false), 35 | includeRetweets: zod.boolean().default(true) 36 | }); 37 | 38 | export const GetTweetByIdSchema = zod.object({ 39 | id: zod.string().min(1, 'Tweet ID is required') 40 | }); 41 | 42 | // Define the search modes 43 | type SearchMode = 'Top' | 'Latest' | 'Photos' | 'Videos'; 44 | 45 | export const SearchTweetsSchema = zod.object({ 46 | query: zod.string().min(1, 'Search query is required'), 47 | count: zod.number().int().min(1).max(100).default(20), 48 | searchMode: zod.string().default('Top') 49 | }); 50 | 51 | export const SendTweetSchema = zod.object({ 52 | text: zod.string().min(1, 'Tweet text is required').max(280, 'Tweet cannot exceed 280 characters'), 53 | replyToTweetId: zod.string().optional(), 54 | media: zod.array(zod.object({ 55 | data: zod.string(), // Base64 encoded media 56 | mediaType: zod.string() // MIME type 57 | })).optional() 58 | }); 59 | 60 | export const SendTweetWithPollSchema = zod.object({ 61 | text: zod.string().min(1, 'Tweet text is required').max(280, 'Tweet cannot exceed 280 characters'), 62 | replyToTweetId: zod.string().optional(), 63 | poll: zod.object({ 64 | options: zod.array(zod.object({ 65 | label: zod.string().min(1).max(25) 66 | })).min(2).max(4), 67 | durationMinutes: zod.number().int().min(5).max(10080).default(1440) // Default 24 hours 68 | }) 69 | }); 70 | 71 | export const LikeTweetSchema = zod.object({ 72 | id: zod.string().min(1, 'Tweet ID is required') 73 | }); 74 | 75 | export const RetweetSchema = zod.object({ 76 | id: zod.string().min(1, 'Tweet ID is required') 77 | }); 78 | 79 | export const QuoteTweetSchema = zod.object({ 80 | text: zod.string().min(1, 'Tweet text is required').max(280, 'Tweet cannot exceed 280 characters'), 81 | quotedTweetId: zod.string().min(1, 'Quoted tweet ID is required'), 82 | media: zod.array(zod.object({ 83 | data: zod.string(), // Base64 encoded media 84 | mediaType: zod.string() // MIME type 85 | })).optional() 86 | }); 87 | 88 | export const GetUserProfileSchema = zod.object({ 89 | username: zod.string().min(1, 'Username is required') 90 | }); 91 | 92 | export const FollowUserSchema = zod.object({ 93 | username: zod.string().min(1, 'Username is required') 94 | }); 95 | 96 | export const GetFollowersSchema = zod.object({ 97 | userId: zod.string().min(1, 'User ID is required'), 98 | count: zod.number().int().min(1).max(200).default(20) 99 | }); 100 | 101 | export const GetFollowingSchema = zod.object({ 102 | userId: zod.string().min(1, 'User ID is required'), 103 | count: zod.number().int().min(1).max(200).default(20) 104 | }); 105 | 106 | export const GrokChatSchema = zod.object({ 107 | message: zod.string().min(1, 'Message is required'), 108 | conversationId: zod.string().optional(), 109 | returnSearchResults: zod.boolean().default(true), 110 | returnCitations: zod.boolean().default(true) 111 | }); 112 | 113 | // Response Types 114 | export interface TweetResponse { 115 | id: string; 116 | text: string; 117 | author: { 118 | id: string; 119 | username: string; 120 | name: string; 121 | }; 122 | createdAt?: string; 123 | metrics?: { 124 | likes?: number; 125 | retweets?: number; 126 | replies?: number; 127 | views?: number; 128 | }; 129 | media?: { 130 | photos?: { url: string; alt?: string }[]; 131 | videos?: { url: string; preview: string }[]; 132 | }; 133 | urls?: string[]; 134 | isRetweet?: boolean; 135 | isReply?: boolean; 136 | isQuote?: boolean; 137 | quotedTweet?: TweetResponse; 138 | inReplyToTweet?: TweetResponse; 139 | permanentUrl: string; 140 | } 141 | 142 | export interface ProfileResponse { 143 | id: string; 144 | username: string; 145 | name: string; 146 | bio?: string; 147 | location?: string; 148 | website?: string; 149 | joinedDate?: string; 150 | isVerified?: boolean; 151 | isPrivate?: boolean; 152 | followersCount?: number; 153 | followingCount?: number; 154 | tweetsCount?: number; 155 | profileImageUrl?: string; 156 | bannerImageUrl?: string; 157 | } 158 | 159 | export interface SearchResponse { 160 | query: string; 161 | tweets: TweetResponse[]; 162 | nextCursor?: string; 163 | } 164 | 165 | export interface FollowResponse { 166 | success: boolean; 167 | message: string; 168 | } 169 | 170 | export interface GrokChatResponse { 171 | conversationId: string; 172 | message: string; 173 | webResults?: any[]; 174 | } 175 | 176 | // Error Types 177 | export class TwitterMcpError extends Error { 178 | constructor( 179 | message: string, 180 | public readonly code: string, 181 | public readonly status?: number 182 | ) { 183 | super(message); 184 | this.name = 'TwitterMcpError'; 185 | } 186 | } -------------------------------------------------------------------------------- /src/types/dotenv.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'dotenv' { 2 | export interface DotenvConfigOptions { 3 | path?: string; 4 | encoding?: string; 5 | debug?: boolean; 6 | override?: boolean; 7 | } 8 | 9 | export interface DotenvConfigOutput { 10 | parsed?: { [key: string]: string }; 11 | error?: Error; 12 | } 13 | 14 | export function config(options?: DotenvConfigOptions): DotenvConfigOutput; 15 | export function parse(src: string | Buffer): { [key: string]: string }; 16 | } -------------------------------------------------------------------------------- /src/types/modelcontextprotocol.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@modelcontextprotocol/sdk/server/index.js' { 2 | export class Server { 3 | constructor(info: { name: string; version: string }, options: { capabilities: { tools: Record } }); 4 | setRequestHandler(schema: any, handler: (request: T) => Promise): void; 5 | connect(transport: any): Promise; 6 | close(): Promise; 7 | onerror: (error: Error) => void; 8 | } 9 | } 10 | 11 | declare module '@modelcontextprotocol/sdk/server/stdio.js' { 12 | export class StdioServerTransport { 13 | constructor(); 14 | } 15 | } 16 | 17 | declare module '@modelcontextprotocol/sdk/types.js' { 18 | export const ListToolsRequestSchema: any; 19 | export const CallToolRequestSchema: any; 20 | 21 | export interface Tool { 22 | name: string; 23 | description: string; 24 | inputSchema: { 25 | type: string; 26 | properties: Record; 27 | required?: string[]; 28 | }; 29 | } 30 | 31 | export enum ErrorCode { 32 | ParseError = -32700, 33 | InvalidRequest = -32600, 34 | MethodNotFound = -32601, 35 | InvalidParams = -32602, 36 | InternalError = -32603 37 | } 38 | 39 | export class McpError extends Error { 40 | constructor(code: ErrorCode, message: string); 41 | } 42 | 43 | export interface TextContent { 44 | type: 'text'; 45 | text: string; 46 | isError?: boolean; 47 | } 48 | } -------------------------------------------------------------------------------- /src/types/winston.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'winston' { 2 | export interface LogEntry { 3 | level: string; 4 | message: string; 5 | timestamp?: string; 6 | [key: string]: any; 7 | } 8 | 9 | export interface Logger { 10 | log(level: string, message: string, meta?: any): Logger; 11 | error(message: string, meta?: any): Logger; 12 | warn(message: string, meta?: any): Logger; 13 | info(message: string, meta?: any): Logger; 14 | debug(message: string, meta?: any): Logger; 15 | } 16 | 17 | export interface TransportInstance { 18 | level?: string; 19 | silent?: boolean; 20 | } 21 | 22 | export interface FileTransportOptions { 23 | filename: string; 24 | level?: string; 25 | } 26 | 27 | export interface ConsoleTransportOptions { 28 | level?: string; 29 | format?: any; 30 | } 31 | 32 | export namespace transports { 33 | class File implements TransportInstance { 34 | constructor(options: FileTransportOptions); 35 | } 36 | class Console implements TransportInstance { 37 | constructor(options?: ConsoleTransportOptions); 38 | } 39 | } 40 | 41 | export namespace format { 42 | function combine(...formats: any[]): any; 43 | function timestamp(): any; 44 | function json(): any; 45 | function colorize(): any; 46 | function printf(fn: (info: LogEntry) => string): any; 47 | } 48 | 49 | export function createLogger(options: { 50 | level?: string; 51 | format?: any; 52 | defaultMeta?: Record; 53 | transports?: TransportInstance[]; 54 | }): Logger; 55 | } -------------------------------------------------------------------------------- /src/types/zod.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'zod' { 2 | export class ZodError extends Error { 3 | issues: ZodIssue[]; 4 | } 5 | 6 | export interface ZodIssue { 7 | path: (string | number)[]; 8 | message: string; 9 | code: string; 10 | } 11 | 12 | export class ZodType { 13 | parse(data: unknown): T; 14 | safeParse(data: unknown): { success: boolean; data?: T; error?: ZodError }; 15 | 16 | // Add missing methods 17 | min(min: number, message?: string): this; 18 | max(max: number, message?: string): this; 19 | int(message?: string): this; 20 | default(defaultValue: any): this; 21 | optional(): this; 22 | } 23 | 24 | export function object(shape: Record): ZodType; 25 | export function string(): ZodType; 26 | export function number(): ZodType; 27 | export function boolean(): ZodType; 28 | export function array(schema: ZodType): ZodType; 29 | 30 | // Use a different name for the enum function to avoid reserved word issues 31 | export const enumType: (values: readonly [string, ...string[]]) => ZodType; 32 | } -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | import { Tweet, Profile } from 'agent-twitter-client'; 2 | import { TweetResponse, ProfileResponse, SearchResponse } from '../types.js'; 3 | 4 | /** 5 | * Format a Tweet object from agent-twitter-client to TweetResponse 6 | */ 7 | export function formatTweet(tweet: Tweet): TweetResponse { 8 | return { 9 | id: tweet.id!, 10 | text: tweet.text!, 11 | author: { 12 | id: tweet.userId!, 13 | username: tweet.username!, 14 | name: tweet.name! 15 | }, 16 | createdAt: tweet.timeParsed ? tweet.timeParsed.toISOString() : undefined, 17 | metrics: { 18 | likes: tweet.likes, 19 | retweets: tweet.retweets, 20 | replies: tweet.replies, 21 | views: tweet.views 22 | }, 23 | media: { 24 | photos: tweet.photos ? tweet.photos.map(photo => ({ 25 | url: photo.url, 26 | alt: photo.alt_text 27 | })) : undefined, 28 | videos: tweet.videos ? tweet.videos.map(video => ({ 29 | url: video.url!, 30 | preview: video.preview || '' 31 | })) : undefined 32 | }, 33 | urls: tweet.urls, 34 | isRetweet: tweet.isRetweet, 35 | isReply: tweet.isReply, 36 | isQuote: tweet.isQuoted, 37 | quotedTweet: tweet.quotedStatus ? formatTweet(tweet.quotedStatus) : undefined, 38 | inReplyToTweet: tweet.inReplyToStatus ? formatTweet(tweet.inReplyToStatus) : undefined, 39 | permanentUrl: tweet.permanentUrl! 40 | }; 41 | } 42 | 43 | /** 44 | * Format a Profile object from agent-twitter-client to ProfileResponse 45 | */ 46 | export function formatProfile(profile: Profile): ProfileResponse { 47 | return { 48 | id: profile.userId!, 49 | username: profile.username!, 50 | name: profile.name!, 51 | bio: profile.biography, 52 | location: profile.location, 53 | website: profile.website, 54 | joinedDate: profile.joined ? profile.joined.toISOString() : undefined, 55 | isVerified: profile.isVerified, 56 | isPrivate: profile.isPrivate, 57 | followersCount: profile.followersCount, 58 | followingCount: profile.followingCount, 59 | tweetsCount: profile.tweetsCount, 60 | profileImageUrl: profile.avatar, 61 | bannerImageUrl: profile.banner 62 | }; 63 | } 64 | 65 | /** 66 | * Format search results 67 | */ 68 | export function formatSearch(query: string, tweets: Tweet[]): SearchResponse { 69 | return { 70 | query, 71 | tweets: tweets.map(formatTweet) 72 | }; 73 | } -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | // Define more specific types for logging context 4 | export type LogContext = Record; 5 | 6 | const logLevel = process.env.LOG_LEVEL || 'info'; 7 | 8 | export const logger = winston.createLogger({ 9 | level: logLevel, 10 | format: winston.format.combine( 11 | winston.format.timestamp(), 12 | winston.format.json() 13 | ), 14 | defaultMeta: { service: 'agent-twitter-client-mcp' }, 15 | transports: [ 16 | new winston.transports.Console({ 17 | format: winston.format.combine( 18 | winston.format.colorize(), 19 | winston.format.printf(({ timestamp, level, message, ...rest }) => { 20 | return `${timestamp} ${level}: ${message} ${Object.keys(rest).length ? JSON.stringify(rest) : ''}`; 21 | }) 22 | ) 23 | }), 24 | new winston.transports.File({ filename: 'error.log', level: 'error' }), 25 | new winston.transports.File({ filename: 'combined.log' }) 26 | ] 27 | }); 28 | 29 | // Create a stream object with a write function that will call the logger 30 | export const logStream = { 31 | write: (message: string) => { 32 | logger.info(message.trim()); 33 | } 34 | }; 35 | 36 | // Utility functions for common logging patterns 37 | export const logError = (message: string, error: unknown, context?: LogContext) => { 38 | const errorMessage = error instanceof Error ? error.message : String(error); 39 | const stack = error instanceof Error ? error.stack : undefined; 40 | 41 | logger.error(message, { 42 | error: errorMessage, 43 | stack, 44 | ...context 45 | }); 46 | }; 47 | 48 | export const logInfo = (message: string, context?: LogContext) => { 49 | logger.info(message, context); 50 | }; 51 | 52 | export const logWarning = (message: string, context?: LogContext) => { 53 | logger.warn(message, context); 54 | }; 55 | 56 | export const logDebug = (message: string, context?: LogContext) => { 57 | logger.debug(message, context); 58 | }; 59 | 60 | // Sanitize sensitive data before logging 61 | export const sanitizeForLogging = (data: Record): Record => { 62 | const sensitiveFields = [ 63 | 'password', 'token', 'secret', 'key', 'cookie', 'auth', 64 | 'credential', 'apiKey', 'apiSecret', 'accessToken' 65 | ]; 66 | 67 | const sanitized = { ...data }; 68 | 69 | for (const key of Object.keys(sanitized)) { 70 | const lowerKey = key.toLowerCase(); 71 | 72 | if (sensitiveFields.some(field => lowerKey.includes(field))) { 73 | sanitized[key] = '[REDACTED]'; 74 | } else if (typeof sanitized[key] === 'object' && sanitized[key] !== null) { 75 | sanitized[key] = sanitizeForLogging(sanitized[key] as Record); 76 | } 77 | } 78 | 79 | return sanitized; 80 | }; -------------------------------------------------------------------------------- /src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | import { ZodType, ZodError } from 'zod'; 2 | import { TwitterMcpError } from '../types.js'; 3 | import { Buffer } from 'node:buffer'; 4 | 5 | /** 6 | * Validate input against a Zod schema 7 | */ 8 | export function validateInput(schema: ZodType, data: unknown): T { 9 | try { 10 | return schema.parse(data); 11 | } catch (error) { 12 | if (error instanceof ZodError) { 13 | const issues = error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join(', '); 14 | throw new TwitterMcpError( 15 | `Validation error: ${issues}`, 16 | 'validation_error', 17 | 400 18 | ); 19 | } 20 | throw new TwitterMcpError( 21 | `Unexpected validation error: ${(error as Error).message}`, 22 | 'validation_error', 23 | 400 24 | ); 25 | } 26 | } 27 | 28 | /** 29 | * Validate that media data is properly formatted 30 | */ 31 | export function validateMediaData(mediaData: { data: string; mediaType: string }[]): void { 32 | // Check number of media items 33 | if (mediaData.length > 4) { 34 | throw new TwitterMcpError( 35 | 'Maximum of 4 media items allowed per tweet', 36 | 'media_validation_error', 37 | 400 38 | ); 39 | } 40 | 41 | // Check if there's more than one video 42 | const videoCount = mediaData.filter(item => 43 | item.mediaType.startsWith('video/') 44 | ).length; 45 | 46 | if (videoCount > 1) { 47 | throw new TwitterMcpError( 48 | 'Only one video allowed per tweet', 49 | 'media_validation_error', 50 | 400 51 | ); 52 | } 53 | 54 | // If we have both video and images, reject 55 | if (videoCount > 0 && mediaData.length > videoCount) { 56 | throw new TwitterMcpError( 57 | 'Cannot mix videos and images in the same tweet', 58 | 'media_validation_error', 59 | 400 60 | ); 61 | } 62 | 63 | // Validate each media item 64 | for (const item of mediaData) { 65 | // Check media type 66 | if (!isValidMediaType(item.mediaType)) { 67 | throw new TwitterMcpError( 68 | `Unsupported media type: ${item.mediaType}`, 69 | 'media_validation_error', 70 | 400 71 | ); 72 | } 73 | 74 | // Validate base64 data 75 | try { 76 | // Use Node.js Buffer for base64 validation 77 | const buffer = Buffer.from(item.data, 'base64'); 78 | // Basic size check (512MB max for videos, 5MB max for images) 79 | const maxSize = item.mediaType.startsWith('video/') ? 512 * 1024 * 1024 : 5 * 1024 * 1024; 80 | if (buffer.length > maxSize) { 81 | throw new TwitterMcpError( 82 | `Media file too large (max ${maxSize / (1024 * 1024)}MB)`, 83 | 'media_validation_error', 84 | 400 85 | ); 86 | } 87 | } catch (error) { 88 | throw new TwitterMcpError( 89 | `Invalid base64 data for media: ${error instanceof Error ? error.message : String(error)}`, 90 | 'media_validation_error', 91 | 400 92 | ); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Check if the media type is supported 99 | */ 100 | function isValidMediaType(mediaType: string): boolean { 101 | const supportedTypes = [ 102 | 'image/jpeg', 103 | 'image/jpg', 104 | 'image/png', 105 | 'image/gif', 106 | 'video/mp4' 107 | ]; 108 | 109 | return supportedTypes.includes(mediaType); 110 | } 111 | 112 | /** 113 | * Validate poll options 114 | */ 115 | export function validatePollOptions(options: { label: string }[]): void { 116 | if (options.length < 2 || options.length > 4) { 117 | throw new TwitterMcpError( 118 | 'Polls must have between 2 and 4 options', 119 | 'poll_validation_error', 120 | 400 121 | ); 122 | } 123 | 124 | // Check for duplicate options 125 | const labels = options.map(option => option.label); 126 | const uniqueLabels = new Set(labels); 127 | if (uniqueLabels.size !== labels.length) { 128 | throw new TwitterMcpError( 129 | 'Poll options must be unique', 130 | 'poll_validation_error', 131 | 400 132 | ); 133 | } 134 | 135 | // Check label lengths 136 | for (const option of options) { 137 | if (option.label.length > 25) { 138 | throw new TwitterMcpError( 139 | 'Poll option labels cannot exceed 25 characters', 140 | 'poll_validation_error', 141 | 400 142 | ); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /tests/load-test-functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Load test functions for Artillery 3 | */ 4 | module.exports = { 5 | /** 6 | * Generate a request with the specified parameters 7 | */ 8 | generateRequest: function(userContext, events, done) { 9 | // Generate a unique ID for the request 10 | userContext.vars.id = Date.now().toString(); 11 | 12 | // Set the method, tool name, and arguments from the parameters 13 | userContext.vars.method = userContext.vars.params.method; 14 | userContext.vars.toolName = userContext.vars.params.toolName; 15 | userContext.vars.arguments = userContext.vars.params.arguments; 16 | 17 | return done(); 18 | } 19 | }; -------------------------------------------------------------------------------- /tests/load-test.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "http://localhost:3000" 3 | phases: 4 | - duration: 60 5 | arrivalRate: 2 6 | name: "Warm up" 7 | - duration: 120 8 | arrivalRate: 5 9 | name: "Sustained load" 10 | processor: "./load-test-functions.js" 11 | scenarios: 12 | - name: "Get user profile" 13 | flow: 14 | - function: "generateRequest" 15 | params: 16 | method: "callTool" 17 | toolName: "get_user_profile" 18 | arguments: 19 | username: "twitter" 20 | - post: 21 | url: "/" 22 | json: 23 | jsonrpc: "2.0" 24 | id: "{{ id }}" 25 | method: "{{ method }}" 26 | params: 27 | name: "{{ toolName }}" 28 | arguments: "{{ arguments }}" 29 | - name: "Search tweets" 30 | flow: 31 | - function: "generateRequest" 32 | params: 33 | method: "callTool" 34 | toolName: "search_tweets" 35 | arguments: 36 | query: "artificial intelligence" 37 | count: 5 38 | - post: 39 | url: "/" 40 | json: 41 | jsonrpc: "2.0" 42 | id: "{{ id }}" 43 | method: "{{ method }}" 44 | params: 45 | name: "{{ toolName }}" 46 | arguments: "{{ arguments }}" 47 | - name: "Get user tweets" 48 | flow: 49 | - function: "generateRequest" 50 | params: 51 | method: "callTool" 52 | toolName: "get_user_tweets" 53 | arguments: 54 | username: "elonmusk" 55 | count: 5 56 | - post: 57 | url: "/" 58 | json: 59 | jsonrpc: "2.0" 60 | id: "{{ id }}" 61 | method: "{{ method }}" 62 | params: 63 | name: "{{ toolName }}" 64 | arguments: "{{ arguments }}" -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "outDir": "./build", 11 | "rootDir": "./src", 12 | "declaration": true, 13 | "sourceMap": true, 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "**/*.test.ts"] 19 | } 20 | --------------------------------------------------------------------------------