├── .env.example ├── .gitignore ├── package.json ├── CONTRIBUTING.md ├── README.md └── index.js /.env.example: -------------------------------------------------------------------------------- 1 | # Twenty CRM API Configuration 2 | TWENTY_API_KEY=your_api_key_here 3 | TWENTY_BASE_URL=https://api.twenty.com 4 | 5 | # For self-hosted instances, change TWENTY_BASE_URL to your domain: 6 | # TWENTY_BASE_URL=https://your-twenty-instance.com -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Environment variables 8 | .env 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage/ 22 | *.lcov 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Logs 28 | logs 29 | *.log 30 | 31 | # OS generated files 32 | .DS_Store 33 | .DS_Store? 34 | ._* 35 | .Spotlight-V100 36 | .Trashes 37 | ehthumbs.db 38 | Thumbs.db 39 | 40 | # IDE files 41 | .vscode/ 42 | .idea/ 43 | *.swp 44 | *.swo 45 | 46 | # Temporary folders 47 | tmp/ 48 | temp/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twenty-crm-mcp-server", 3 | "version": "1.0.0", 4 | "description": "A Model Context Protocol (MCP) server for Twenty CRM integration. Enables natural language interactions with your CRM data through Claude and other AI assistants.", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node index.js", 9 | "dev": "node --inspect index.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "dependencies": { 13 | "@modelcontextprotocol/sdk": "^0.6.0" 14 | }, 15 | "engines": { 16 | "node": ">=18" 17 | }, 18 | "keywords": [ 19 | "mcp", 20 | "twenty", 21 | "crm", 22 | "server", 23 | "claude", 24 | "ai", 25 | "assistant", 26 | "natural-language", 27 | "api", 28 | "integration" 29 | ], 30 | "author": "mhenry3164", 31 | "license": "MIT", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/mhenry3164/twenty-crm-mcp-server.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/mhenry3164/twenty-crm-mcp-server/issues" 38 | }, 39 | "homepage": "https://github.com/mhenry3164/twenty-crm-mcp-server#readme" 40 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Twenty CRM MCP Server 2 | 3 | Thank you for your interest in contributing! This project aims to provide the best possible integration between Twenty CRM and MCP-compatible AI assistants. 4 | 5 | ## Development Setup 6 | 7 | 1. **Fork and clone the repository**: 8 | ```bash 9 | git clone https://github.com/your-username/twenty-crm-mcp-server.git 10 | cd twenty-crm-mcp-server 11 | ``` 12 | 13 | 2. **Install dependencies**: 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | 3. **Set up environment variables**: 19 | ```bash 20 | cp .env.example .env 21 | ``` 22 | Edit `.env` with your Twenty CRM API key and base URL. 23 | 24 | 4. **Test your setup**: 25 | ```bash 26 | npm test 27 | ``` 28 | 29 | ## How to Contribute 30 | 31 | ### Reporting Issues 32 | 33 | Before creating an issue, please: 34 | 35 | 1. **Search existing issues** to avoid duplicates 36 | 2. **Use the issue templates** provided 37 | 3. **Include relevant details**: 38 | - Twenty CRM version (cloud/self-hosted) 39 | - Node.js version 40 | - Error messages and stack traces 41 | - Steps to reproduce 42 | 43 | ### Suggesting Features 44 | 45 | We welcome feature suggestions! Please: 46 | 47 | 1. **Check the roadmap** to see if it's already planned 48 | 2. **Open a discussion** before submitting large features 49 | 3. **Explain the use case** and expected behavior 50 | 4. **Consider backward compatibility** 51 | 52 | ### Code Contributions 53 | 54 | #### Before You Start 55 | 56 | 1. **Open an issue** to discuss your proposed changes 57 | 2. **Check if someone is already working** on similar functionality 58 | 3. **Review the codebase** to understand the patterns used 59 | 60 | #### Development Guidelines 61 | 62 | **Code Style**: 63 | - Use ES modules (`import`/`export`) 64 | - Follow existing naming conventions 65 | - Add JSDoc comments for new functions 66 | - Keep functions focused and small 67 | 68 | **Error Handling**: 69 | - Always handle API errors gracefully 70 | - Provide helpful error messages to users 71 | - Log errors with appropriate context 72 | 73 | **Testing**: 74 | - Add tests for new functionality 75 | - Ensure existing tests pass 76 | - Test with both cloud and self-hosted Twenty instances 77 | 78 | #### Pull Request Process 79 | 80 | 1. **Create a feature branch**: 81 | ```bash 82 | git checkout -b feature/your-feature-name 83 | ``` 84 | 85 | 2. **Make your changes**: 86 | - Follow the coding guidelines above 87 | - Add tests for new functionality 88 | - Update documentation as needed 89 | 90 | 3. **Test thoroughly**: 91 | ```bash 92 | npm test 93 | npm run lint 94 | ``` 95 | 96 | 4. **Commit with clear messages**: 97 | ```bash 98 | git commit -m "feat: add support for custom field types" 99 | ``` 100 | 101 | Use conventional commit format: 102 | - `feat:` for new features 103 | - `fix:` for bug fixes 104 | - `docs:` for documentation changes 105 | - `refactor:` for code refactoring 106 | - `test:` for test additions/modifications 107 | 108 | 5. **Push and create PR**: 109 | ```bash 110 | git push origin feature/your-feature-name 111 | ``` 112 | 113 | Then create a pull request with: 114 | - **Clear title and description** 115 | - **Reference any related issues** 116 | - **Include testing instructions** 117 | - **Update CHANGELOG.md** if applicable 118 | 119 | #### Review Process 120 | 121 | - All PRs require at least one approval 122 | - Maintainers will review within 48 hours 123 | - Address feedback promptly 124 | - Keep PRs focused and reasonably sized 125 | 126 | ## Roadmap 127 | 128 | ### Planned Features 129 | 130 | - **Bulk Operations**: Import/export large datasets 131 | - **Advanced Filtering**: Complex query capabilities 132 | - **Webhook Support**: Real-time notifications 133 | - **Data Enrichment**: Integration with external data sources 134 | - **Workflow Triggers**: Automated actions based on events 135 | - **Performance Optimization**: Caching and rate limiting 136 | - **TypeScript Support**: Full type definitions 137 | - **Additional Object Types**: Support for opportunities, custom objects 138 | 139 | ### Areas for Contribution 140 | 141 | - **Documentation**: Improve examples and tutorials 142 | - **Testing**: Add integration tests and edge cases 143 | - **Performance**: Optimize API calls and response handling 144 | - **Features**: Implement items from the roadmap 145 | - **Bug Fixes**: Address issues and improve stability 146 | 147 | ## Code of Conduct 148 | 149 | ### Our Standards 150 | 151 | - **Be respectful** and inclusive 152 | - **Focus on constructive feedback** 153 | - **Help others learn and grow** 154 | - **Assume good intentions** 155 | 156 | ### Unacceptable Behavior 157 | 158 | - Harassment or discrimination 159 | - Trolling or inflammatory comments 160 | - Personal attacks 161 | - Publishing private information 162 | 163 | ## Getting Help 164 | 165 | - **GitHub Discussions**: For questions and general discussion 166 | - **Issues**: For bug reports and feature requests 167 | - **Discord**: Join the Twenty CRM community 168 | 169 | ## Recognition 170 | 171 | Contributors will be: 172 | - **Listed in README.md** 173 | - **Credited in release notes** 174 | - **Invited to the contributors team** (for regular contributors) 175 | 176 | Thank you for helping make this project better! 🚀 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🤖 Twenty CRM MCP Server 4 | 5 | **Transform your CRM into an AI-powered assistant** 6 | 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/) 9 | [![Twenty CRM](https://img.shields.io/badge/Twenty_CRM-Compatible-blue)](https://twenty.com) 10 | [![MCP](https://img.shields.io/badge/MCP-Compatible-purple)](https://modelcontextprotocol.io/) 11 | 12 | *A Model Context Protocol server that connects [Twenty CRM](https://twenty.com) with Claude and other AI assistants, enabling natural language interactions with your customer data.* 13 | 14 | [🚀 Quick Start](#-installation) • [📖 Usage Examples](#-usage) • [🛠️ API Reference](#-api-reference) • [🤝 Contributing](#-contributing) 15 | 16 |
17 | 18 | --- 19 | 20 | ## ✨ Features 21 | 22 | 23 | 24 | 36 | 48 | 49 |
25 | 26 | ### 🔄 **Complete CRUD Operations** 27 | Create, read, update, and delete people, companies, tasks, and notes with simple commands 28 | 29 | ### 🧠 **Dynamic Schema Discovery** 30 | Automatically adapts to your Twenty CRM configuration and custom fields 31 | 32 | ### 🔍 **Advanced Search** 33 | Search across multiple object types with intelligent filtering and natural language queries 34 | 35 | 37 | 38 | ### 📊 **Metadata Access** 39 | Retrieve schema information and field definitions dynamically 40 | 41 | ### 💬 **Natural Language Interface** 42 | Use conversational commands to manage your CRM data effortlessly 43 | 44 | ### ⚡ **Real-time Updates** 45 | All changes sync immediately with your Twenty CRM instance 46 | 47 |
50 | 51 | --- 52 | 53 | ## 🚀 Installation 54 | 55 | ### Prerequisites 56 | 57 | - Node.js 18 or higher 58 | - A Twenty CRM instance (cloud or self-hosted) 59 | - Claude Desktop or compatible MCP client 60 | 61 | ### Setup 62 | 63 | 1. **Clone the repository**: 64 | ```bash 65 | git clone https://github.com/mhenry3164/twenty-crm-mcp-server.git 66 | cd twenty-crm-mcp-server 67 | ``` 68 | 69 | 2. **Install dependencies**: 70 | ```bash 71 | npm install 72 | ``` 73 | 74 | 3. **Get your Twenty CRM API key**: 75 | - Log in to your Twenty CRM workspace 76 | - Navigate to Settings → API & Webhooks (under Developers) 77 | - Generate a new API key 78 | 79 | 4. **Configure Claude Desktop**: 80 | 81 | Add the server to your `claude_desktop_config.json`: 82 | 83 | ```json 84 | { 85 | "mcpServers": { 86 | "twenty-crm": { 87 | "command": "node", 88 | "args": ["/path/to/twenty-crm-mcp-server/index.js"], 89 | "env": { 90 | "TWENTY_API_KEY": "your_api_key_here", 91 | "TWENTY_BASE_URL": "https://api.twenty.com" 92 | } 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | For self-hosted Twenty instances, change `TWENTY_BASE_URL` to your domain. 99 | 100 | 5. **Restart Claude Desktop** to load the new server. 101 | 102 | --- 103 | 104 | ## 💬 Usage 105 | 106 | Once configured, you can use natural language to interact with your Twenty CRM: 107 | 108 | ### 👥 People Management 109 | ``` 110 | "List the first 10 people in my CRM" 111 | "Create a new person named John Doe with email john@example.com" 112 | "Update Sarah's job title to Senior Developer" 113 | "Find all people working at tech companies" 114 | ``` 115 | 116 | ### 🏢 Company Management 117 | ``` 118 | "Show me all companies with more than 100 employees" 119 | "Create a company called Tech Solutions with domain techsolutions.com" 120 | "Update Acme Corp's annual revenue to $5M" 121 | ``` 122 | 123 | ### ✅ Task Management 124 | ``` 125 | "Create a task to follow up with John next Friday" 126 | "Show me all overdue tasks" 127 | "Mark the task 'Call client' as completed" 128 | ``` 129 | 130 | ### 📝 Notes & Search 131 | ``` 132 | "Add a note about my meeting with the client today" 133 | "Search for any records mentioning 'blockchain'" 134 | "Find all contacts without LinkedIn profiles" 135 | ``` 136 | 137 | --- 138 | 139 | ## 🛠️ API Reference 140 | 141 | The server provides the following tools: 142 | 143 |
144 | 👥 People Operations 145 | 146 | - `create_person` - Create a new person 147 | - `get_person` - Get person details by ID 148 | - `update_person` - Update person information 149 | - `list_people` - List people with filtering 150 | - `delete_person` - Delete a person 151 | 152 |
153 | 154 |
155 | 🏢 Company Operations 156 | 157 | - `create_company` - Create a new company 158 | - `get_company` - Get company details by ID 159 | - `update_company` - Update company information 160 | - `list_companies` - List companies with filtering 161 | - `delete_company` - Delete a company 162 | 163 |
164 | 165 |
166 | ✅ Task Operations 167 | 168 | - `create_task` - Create a new task 169 | - `get_task` - Get task details by ID 170 | - `update_task` - Update task information 171 | - `list_tasks` - List tasks with filtering 172 | - `delete_task` - Delete a task 173 | 174 |
175 | 176 |
177 | 📝 Note Operations 178 | 179 | - `create_note` - Create a new note 180 | - `get_note` - Get note details by ID 181 | - `update_note` - Update note information 182 | - `list_notes` - List notes with filtering 183 | - `delete_note` - Delete a note 184 | 185 |
186 | 187 |
188 | 🔍 Metadata & Search 189 | 190 | - `get_metadata_objects` - Get all object types and schemas 191 | - `get_object_metadata` - Get metadata for specific object 192 | - `search_records` - Search across multiple object types 193 | 194 |
195 | 196 | --- 197 | 198 | ## ⚙️ Configuration 199 | 200 | ### Environment Variables 201 | 202 | - `TWENTY_API_KEY` (required): Your Twenty CRM API key 203 | - `TWENTY_BASE_URL` (optional): Twenty CRM base URL (defaults to `https://api.twenty.com`) 204 | 205 | ### Custom Fields 206 | 207 | The server automatically discovers and supports custom fields in your Twenty CRM instance. No configuration changes needed when you add new fields. 208 | 209 | --- 210 | 211 | ## 🤝 Contributing 212 | 213 | We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. 214 | 215 | ### Development 216 | 217 | 1. **Clone the repo**: 218 | ```bash 219 | git clone https://github.com/mhenry3164/twenty-crm-mcp-server.git 220 | cd twenty-crm-mcp-server 221 | ``` 222 | 223 | 2. **Install dependencies**: 224 | ```bash 225 | npm install 226 | ``` 227 | 228 | 3. **Set up environment variables**: 229 | ```bash 230 | cp .env.example .env 231 | # Edit .env with your API key 232 | ``` 233 | 234 | 4. **Test the server**: 235 | ```bash 236 | npm test 237 | ``` 238 | 239 | --- 240 | 241 | ## 🐛 Troubleshooting 242 | 243 | ### Common Issues 244 | 245 | **Authentication Error**: Verify your API key is correct and has appropriate permissions. 246 | 247 | **Connection Failed**: Check that your `TWENTY_BASE_URL` is correct (especially for self-hosted instances). 248 | 249 | **Field Not Found**: The server automatically discovers fields. If you're getting field errors, try getting the metadata first: *"Show me the available fields for people"* 250 | 251 | --- 252 | 253 | ## 📄 License 254 | 255 | MIT License - see [LICENSE](LICENSE) file for details. 256 | 257 | --- 258 | 259 | ## 🙏 Acknowledgments 260 | 261 | - [Twenty CRM](https://twenty.com) for providing an excellent open-source CRM 262 | - [Anthropic](https://anthropic.com) for the Model Context Protocol 263 | - The MCP community for inspiration and examples 264 | 265 | --- 266 | 267 | ## 🔗 Links 268 | 269 | - [Twenty CRM Documentation](https://twenty.com/developers) 270 | - [Model Context Protocol Specification](https://modelcontextprotocol.io/) 271 | - [Claude Desktop](https://claude.ai/desktop) 272 | 273 | --- 274 | 275 |
276 | 277 | **Made with ❤️ for the open-source community** 278 | 279 | *⭐ Star this repo if you find it helpful!* 280 | 281 |
-------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | 10 | class TwentyCRMServer { 11 | constructor() { 12 | this.server = new Server( 13 | { 14 | name: "twenty-crm", 15 | version: "0.1.0", 16 | }, 17 | { 18 | capabilities: { 19 | tools: {}, 20 | }, 21 | } 22 | ); 23 | 24 | this.apiKey = process.env.TWENTY_API_KEY; 25 | this.baseUrl = process.env.TWENTY_BASE_URL || "https://api.twenty.com"; 26 | 27 | if (!this.apiKey) { 28 | throw new Error("TWENTY_API_KEY environment variable is required"); 29 | } 30 | 31 | this.setupToolHandlers(); 32 | } 33 | 34 | async makeRequest(endpoint, method = "GET", data = null) { 35 | const url = `${this.baseUrl}${endpoint}`; 36 | const options = { 37 | method, 38 | headers: { 39 | "Authorization": `Bearer ${this.apiKey}`, 40 | "Content-Type": "application/json", 41 | }, 42 | }; 43 | 44 | if (data && (method === "POST" || method === "PUT" || method === "PATCH")) { 45 | options.body = JSON.stringify(data); 46 | } 47 | 48 | try { 49 | const response = await fetch(url, options); 50 | 51 | if (!response.ok) { 52 | const errorText = await response.text(); 53 | throw new Error(`HTTP ${response.status}: ${errorText}`); 54 | } 55 | 56 | const result = await response.json(); 57 | return result; 58 | } catch (error) { 59 | throw new Error(`API request failed: ${error.message}`); 60 | } 61 | } 62 | 63 | setupToolHandlers() { 64 | this.server.setRequestHandler(ListToolsRequestSchema, async () => { 65 | return { 66 | tools: [ 67 | // People Management 68 | { 69 | name: "create_person", 70 | description: "Create a new person in Twenty CRM", 71 | inputSchema: { 72 | type: "object", 73 | properties: { 74 | firstName: { type: "string", description: "First name" }, 75 | lastName: { type: "string", description: "Last name" }, 76 | email: { type: "string", description: "Email address" }, 77 | phone: { type: "string", description: "Phone number" }, 78 | jobTitle: { type: "string", description: "Job title" }, 79 | companyId: { type: "string", description: "Company ID to associate with" }, 80 | linkedinUrl: { type: "string", description: "LinkedIn profile URL" }, 81 | city: { type: "string", description: "City" }, 82 | avatarUrl: { type: "string", description: "Avatar image URL" } 83 | }, 84 | required: ["firstName", "lastName"] 85 | } 86 | }, 87 | { 88 | name: "get_person", 89 | description: "Get details of a specific person by ID", 90 | inputSchema: { 91 | type: "object", 92 | properties: { 93 | id: { type: "string", description: "Person ID" } 94 | }, 95 | required: ["id"] 96 | } 97 | }, 98 | { 99 | name: "update_person", 100 | description: "Update an existing person's information", 101 | inputSchema: { 102 | type: "object", 103 | properties: { 104 | id: { type: "string", description: "Person ID" }, 105 | firstName: { type: "string", description: "First name" }, 106 | lastName: { type: "string", description: "Last name" }, 107 | email: { type: "string", description: "Email address" }, 108 | phone: { type: "string", description: "Phone number" }, 109 | jobTitle: { type: "string", description: "Job title" }, 110 | companyId: { type: "string", description: "Company ID" }, 111 | linkedinUrl: { type: "string", description: "LinkedIn profile URL" }, 112 | city: { type: "string", description: "City" } 113 | }, 114 | required: ["id"] 115 | } 116 | }, 117 | { 118 | name: "list_people", 119 | description: "List people with optional filtering and pagination", 120 | inputSchema: { 121 | type: "object", 122 | properties: { 123 | limit: { type: "number", description: "Number of results to return (default: 20)" }, 124 | offset: { type: "number", description: "Number of results to skip (default: 0)" }, 125 | search: { type: "string", description: "Search term for name or email" }, 126 | companyId: { type: "string", description: "Filter by company ID" } 127 | } 128 | } 129 | }, 130 | { 131 | name: "delete_person", 132 | description: "Delete a person from Twenty CRM", 133 | inputSchema: { 134 | type: "object", 135 | properties: { 136 | id: { type: "string", description: "Person ID to delete" } 137 | }, 138 | required: ["id"] 139 | } 140 | }, 141 | 142 | // Company Management 143 | { 144 | name: "create_company", 145 | description: "Create a new company in Twenty CRM", 146 | inputSchema: { 147 | type: "object", 148 | properties: { 149 | name: { type: "string", description: "Company name" }, 150 | domainName: { type: "string", description: "Company domain" }, 151 | address: { type: "string", description: "Company address" }, 152 | employees: { type: "number", description: "Number of employees" }, 153 | linkedinUrl: { type: "string", description: "LinkedIn company URL" }, 154 | xUrl: { type: "string", description: "X (Twitter) URL" }, 155 | annualRecurringRevenue: { type: "number", description: "Annual recurring revenue" }, 156 | idealCustomerProfile: { type: "boolean", description: "Is this an ideal customer profile" } 157 | }, 158 | required: ["name"] 159 | } 160 | }, 161 | { 162 | name: "get_company", 163 | description: "Get details of a specific company by ID", 164 | inputSchema: { 165 | type: "object", 166 | properties: { 167 | id: { type: "string", description: "Company ID" } 168 | }, 169 | required: ["id"] 170 | } 171 | }, 172 | { 173 | name: "update_company", 174 | description: "Update an existing company's information", 175 | inputSchema: { 176 | type: "object", 177 | properties: { 178 | id: { type: "string", description: "Company ID" }, 179 | name: { type: "string", description: "Company name" }, 180 | domainName: { type: "string", description: "Company domain" }, 181 | address: { type: "string", description: "Company address" }, 182 | employees: { type: "number", description: "Number of employees" }, 183 | linkedinUrl: { type: "string", description: "LinkedIn company URL" }, 184 | annualRecurringRevenue: { type: "number", description: "Annual recurring revenue" } 185 | }, 186 | required: ["id"] 187 | } 188 | }, 189 | { 190 | name: "list_companies", 191 | description: "List companies with optional filtering and pagination", 192 | inputSchema: { 193 | type: "object", 194 | properties: { 195 | limit: { type: "number", description: "Number of results to return (default: 20)" }, 196 | offset: { type: "number", description: "Number of results to skip (default: 0)" }, 197 | search: { type: "string", description: "Search term for company name" } 198 | } 199 | } 200 | }, 201 | { 202 | name: "delete_company", 203 | description: "Delete a company from Twenty CRM", 204 | inputSchema: { 205 | type: "object", 206 | properties: { 207 | id: { type: "string", description: "Company ID to delete" } 208 | }, 209 | required: ["id"] 210 | } 211 | }, 212 | 213 | // Notes Management 214 | { 215 | name: "create_note", 216 | description: "Create a new note in Twenty CRM", 217 | inputSchema: { 218 | type: "object", 219 | properties: { 220 | title: { type: "string", description: "Note title" }, 221 | body: { type: "string", description: "Note content" }, 222 | position: { type: "number", description: "Position for ordering" } 223 | }, 224 | required: ["title", "body"] 225 | } 226 | }, 227 | { 228 | name: "get_note", 229 | description: "Get details of a specific note by ID", 230 | inputSchema: { 231 | type: "object", 232 | properties: { 233 | id: { type: "string", description: "Note ID" } 234 | }, 235 | required: ["id"] 236 | } 237 | }, 238 | { 239 | name: "list_notes", 240 | description: "List notes with optional filtering and pagination", 241 | inputSchema: { 242 | type: "object", 243 | properties: { 244 | limit: { type: "number", description: "Number of results to return (default: 20)" }, 245 | offset: { type: "number", description: "Number of results to skip (default: 0)" }, 246 | search: { type: "string", description: "Search term for note title or content" } 247 | } 248 | } 249 | }, 250 | { 251 | name: "update_note", 252 | description: "Update an existing note", 253 | inputSchema: { 254 | type: "object", 255 | properties: { 256 | id: { type: "string", description: "Note ID" }, 257 | title: { type: "string", description: "Note title" }, 258 | body: { type: "string", description: "Note content" }, 259 | position: { type: "number", description: "Position for ordering" } 260 | }, 261 | required: ["id"] 262 | } 263 | }, 264 | { 265 | name: "delete_note", 266 | description: "Delete a note from Twenty CRM", 267 | inputSchema: { 268 | type: "object", 269 | properties: { 270 | id: { type: "string", description: "Note ID to delete" } 271 | }, 272 | required: ["id"] 273 | } 274 | }, 275 | 276 | // Tasks Management 277 | { 278 | name: "create_task", 279 | description: "Create a new task in Twenty CRM", 280 | inputSchema: { 281 | type: "object", 282 | properties: { 283 | title: { type: "string", description: "Task title" }, 284 | body: { type: "string", description: "Task description" }, 285 | dueAt: { type: "string", description: "Due date (ISO 8601 format)" }, 286 | status: { type: "string", description: "Task status", enum: ["TODO", "IN_PROGRESS", "DONE"] }, 287 | assigneeId: { type: "string", description: "ID of person assigned to task" }, 288 | position: { type: "number", description: "Position for ordering" } 289 | }, 290 | required: ["title"] 291 | } 292 | }, 293 | { 294 | name: "get_task", 295 | description: "Get details of a specific task by ID", 296 | inputSchema: { 297 | type: "object", 298 | properties: { 299 | id: { type: "string", description: "Task ID" } 300 | }, 301 | required: ["id"] 302 | } 303 | }, 304 | { 305 | name: "list_tasks", 306 | description: "List tasks with optional filtering and pagination", 307 | inputSchema: { 308 | type: "object", 309 | properties: { 310 | limit: { type: "number", description: "Number of results to return (default: 20)" }, 311 | offset: { type: "number", description: "Number of results to skip (default: 0)" }, 312 | status: { type: "string", description: "Filter by status", enum: ["TODO", "IN_PROGRESS", "DONE"] }, 313 | assigneeId: { type: "string", description: "Filter by assignee ID" } 314 | } 315 | } 316 | }, 317 | { 318 | name: "update_task", 319 | description: "Update an existing task", 320 | inputSchema: { 321 | type: "object", 322 | properties: { 323 | id: { type: "string", description: "Task ID" }, 324 | title: { type: "string", description: "Task title" }, 325 | body: { type: "string", description: "Task description" }, 326 | dueAt: { type: "string", description: "Due date (ISO 8601 format)" }, 327 | status: { type: "string", description: "Task status", enum: ["TODO", "IN_PROGRESS", "DONE"] }, 328 | assigneeId: { type: "string", description: "ID of person assigned to task" } 329 | }, 330 | required: ["id"] 331 | } 332 | }, 333 | { 334 | name: "delete_task", 335 | description: "Delete a task from Twenty CRM", 336 | inputSchema: { 337 | type: "object", 338 | properties: { 339 | id: { type: "string", description: "Task ID to delete" } 340 | }, 341 | required: ["id"] 342 | } 343 | }, 344 | 345 | // Metadata Operations 346 | { 347 | name: "get_metadata_objects", 348 | description: "Get all object types and their metadata", 349 | inputSchema: { 350 | type: "object", 351 | properties: {} 352 | } 353 | }, 354 | { 355 | name: "get_object_metadata", 356 | description: "Get metadata for a specific object type", 357 | inputSchema: { 358 | type: "object", 359 | properties: { 360 | objectName: { type: "string", description: "Object name (e.g., 'people', 'companies')" } 361 | }, 362 | required: ["objectName"] 363 | } 364 | }, 365 | 366 | // Search and Enrichment 367 | { 368 | name: "search_records", 369 | description: "Search across multiple object types", 370 | inputSchema: { 371 | type: "object", 372 | properties: { 373 | query: { type: "string", description: "Search query" }, 374 | objectTypes: { 375 | type: "array", 376 | items: { type: "string" }, 377 | description: "Object types to search (e.g., ['people', 'companies'])" 378 | }, 379 | limit: { type: "number", description: "Number of results per object type" } 380 | }, 381 | required: ["query"] 382 | } 383 | } 384 | ] 385 | }; 386 | }); 387 | 388 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 389 | const { name, arguments: args } = request.params; 390 | 391 | try { 392 | switch (name) { 393 | // People operations 394 | case "create_person": 395 | return await this.createPerson(args); 396 | case "get_person": 397 | return await this.getPerson(args.id); 398 | case "update_person": 399 | return await this.updatePerson(args); 400 | case "list_people": 401 | return await this.listPeople(args); 402 | case "delete_person": 403 | return await this.deletePerson(args.id); 404 | 405 | // Company operations 406 | case "create_company": 407 | return await this.createCompany(args); 408 | case "get_company": 409 | return await this.getCompany(args.id); 410 | case "update_company": 411 | return await this.updateCompany(args); 412 | case "list_companies": 413 | return await this.listCompanies(args); 414 | case "delete_company": 415 | return await this.deleteCompany(args.id); 416 | 417 | // Note operations 418 | case "create_note": 419 | return await this.createNote(args); 420 | case "get_note": 421 | return await this.getNote(args.id); 422 | case "list_notes": 423 | return await this.listNotes(args); 424 | case "update_note": 425 | return await this.updateNote(args); 426 | case "delete_note": 427 | return await this.deleteNote(args.id); 428 | 429 | // Task operations 430 | case "create_task": 431 | return await this.createTask(args); 432 | case "get_task": 433 | return await this.getTask(args.id); 434 | case "list_tasks": 435 | return await this.listTasks(args); 436 | case "update_task": 437 | return await this.updateTask(args); 438 | case "delete_task": 439 | return await this.deleteTask(args.id); 440 | 441 | // Metadata operations 442 | case "get_metadata_objects": 443 | return await this.getMetadataObjects(); 444 | case "get_object_metadata": 445 | return await this.getObjectMetadata(args.objectName); 446 | 447 | // Search operations 448 | case "search_records": 449 | return await this.searchRecords(args); 450 | 451 | default: 452 | throw new Error(`Unknown tool: ${name}`); 453 | } 454 | } catch (error) { 455 | return { 456 | content: [ 457 | { 458 | type: "text", 459 | text: `Error: ${error.message}` 460 | } 461 | ] 462 | }; 463 | } 464 | }); 465 | } 466 | 467 | // People methods 468 | async createPerson(data) { 469 | const result = await this.makeRequest("/rest/people", "POST", data); 470 | return { 471 | content: [ 472 | { 473 | type: "text", 474 | text: `Created person: ${JSON.stringify(result, null, 2)}` 475 | } 476 | ] 477 | }; 478 | } 479 | 480 | async getPerson(id) { 481 | const result = await this.makeRequest(`/rest/people/${id}`); 482 | return { 483 | content: [ 484 | { 485 | type: "text", 486 | text: `Person details: ${JSON.stringify(result, null, 2)}` 487 | } 488 | ] 489 | }; 490 | } 491 | 492 | async updatePerson(data) { 493 | const { id, ...updateData } = data; 494 | const result = await this.makeRequest(`/rest/people/${id}`, "PUT", updateData); 495 | return { 496 | content: [ 497 | { 498 | type: "text", 499 | text: `Updated person: ${JSON.stringify(result, null, 2)}` 500 | } 501 | ] 502 | }; 503 | } 504 | 505 | async listPeople(params = {}) { 506 | const { limit = 20, offset = 0, search, companyId } = params; 507 | let endpoint = `/rest/people?limit=${limit}&offset=${offset}`; 508 | 509 | if (search) { 510 | endpoint += `&search=${encodeURIComponent(search)}`; 511 | } 512 | if (companyId) { 513 | endpoint += `&companyId=${companyId}`; 514 | } 515 | 516 | const result = await this.makeRequest(endpoint); 517 | return { 518 | content: [ 519 | { 520 | type: "text", 521 | text: `People list: ${JSON.stringify(result, null, 2)}` 522 | } 523 | ] 524 | }; 525 | } 526 | 527 | async deletePerson(id) { 528 | await this.makeRequest(`/rest/people/${id}`, "DELETE"); 529 | return { 530 | content: [ 531 | { 532 | type: "text", 533 | text: `Successfully deleted person with ID: ${id}` 534 | } 535 | ] 536 | }; 537 | } 538 | 539 | // Company methods 540 | async createCompany(data) { 541 | const result = await this.makeRequest("/rest/companies", "POST", data); 542 | return { 543 | content: [ 544 | { 545 | type: "text", 546 | text: `Created company: ${JSON.stringify(result, null, 2)}` 547 | } 548 | ] 549 | }; 550 | } 551 | 552 | async getCompany(id) { 553 | const result = await this.makeRequest(`/rest/companies/${id}`); 554 | return { 555 | content: [ 556 | { 557 | type: "text", 558 | text: `Company details: ${JSON.stringify(result, null, 2)}` 559 | } 560 | ] 561 | }; 562 | } 563 | 564 | async updateCompany(data) { 565 | const { id, ...updateData } = data; 566 | const result = await this.makeRequest(`/rest/companies/${id}`, "PUT", updateData); 567 | return { 568 | content: [ 569 | { 570 | type: "text", 571 | text: `Updated company: ${JSON.stringify(result, null, 2)}` 572 | } 573 | ] 574 | }; 575 | } 576 | 577 | async listCompanies(params = {}) { 578 | const { limit = 20, offset = 0, search } = params; 579 | let endpoint = `/rest/companies?limit=${limit}&offset=${offset}`; 580 | 581 | if (search) { 582 | endpoint += `&search=${encodeURIComponent(search)}`; 583 | } 584 | 585 | const result = await this.makeRequest(endpoint); 586 | return { 587 | content: [ 588 | { 589 | type: "text", 590 | text: `Companies list: ${JSON.stringify(result, null, 2)}` 591 | } 592 | ] 593 | }; 594 | } 595 | 596 | async deleteCompany(id) { 597 | await this.makeRequest(`/rest/companies/${id}`, "DELETE"); 598 | return { 599 | content: [ 600 | { 601 | type: "text", 602 | text: `Successfully deleted company with ID: ${id}` 603 | } 604 | ] 605 | }; 606 | } 607 | 608 | // Note methods 609 | async createNote(data) { 610 | const result = await this.makeRequest("/rest/notes", "POST", data); 611 | return { 612 | content: [ 613 | { 614 | type: "text", 615 | text: `Created note: ${JSON.stringify(result, null, 2)}` 616 | } 617 | ] 618 | }; 619 | } 620 | 621 | async getNote(id) { 622 | const result = await this.makeRequest(`/rest/notes/${id}`); 623 | return { 624 | content: [ 625 | { 626 | type: "text", 627 | text: `Note details: ${JSON.stringify(result, null, 2)}` 628 | } 629 | ] 630 | }; 631 | } 632 | 633 | async listNotes(params = {}) { 634 | const { limit = 20, offset = 0, search } = params; 635 | let endpoint = `/rest/notes?limit=${limit}&offset=${offset}`; 636 | 637 | if (search) { 638 | endpoint += `&search=${encodeURIComponent(search)}`; 639 | } 640 | 641 | const result = await this.makeRequest(endpoint); 642 | return { 643 | content: [ 644 | { 645 | type: "text", 646 | text: `Notes list: ${JSON.stringify(result, null, 2)}` 647 | } 648 | ] 649 | }; 650 | } 651 | 652 | async updateNote(data) { 653 | const { id, ...updateData } = data; 654 | const result = await this.makeRequest(`/rest/notes/${id}`, "PUT", updateData); 655 | return { 656 | content: [ 657 | { 658 | type: "text", 659 | text: `Updated note: ${JSON.stringify(result, null, 2)}` 660 | } 661 | ] 662 | }; 663 | } 664 | 665 | async deleteNote(id) { 666 | await this.makeRequest(`/rest/notes/${id}`, "DELETE"); 667 | return { 668 | content: [ 669 | { 670 | type: "text", 671 | text: `Successfully deleted note with ID: ${id}` 672 | } 673 | ] 674 | }; 675 | } 676 | 677 | // Task methods 678 | async createTask(data) { 679 | const result = await this.makeRequest("/rest/tasks", "POST", data); 680 | return { 681 | content: [ 682 | { 683 | type: "text", 684 | text: `Created task: ${JSON.stringify(result, null, 2)}` 685 | } 686 | ] 687 | }; 688 | } 689 | 690 | async getTask(id) { 691 | const result = await this.makeRequest(`/rest/tasks/${id}`); 692 | return { 693 | content: [ 694 | { 695 | type: "text", 696 | text: `Task details: ${JSON.stringify(result, null, 2)}` 697 | } 698 | ] 699 | }; 700 | } 701 | 702 | async listTasks(params = {}) { 703 | const { limit = 20, offset = 0, status, assigneeId } = params; 704 | let endpoint = `/rest/tasks?limit=${limit}&offset=${offset}`; 705 | 706 | if (status) { 707 | endpoint += `&status=${status}`; 708 | } 709 | if (assigneeId) { 710 | endpoint += `&assigneeId=${assigneeId}`; 711 | } 712 | 713 | const result = await this.makeRequest(endpoint); 714 | return { 715 | content: [ 716 | { 717 | type: "text", 718 | text: `Tasks list: ${JSON.stringify(result, null, 2)}` 719 | } 720 | ] 721 | }; 722 | } 723 | 724 | async updateTask(data) { 725 | const { id, ...updateData } = data; 726 | const result = await this.makeRequest(`/rest/tasks/${id}`, "PUT", updateData); 727 | return { 728 | content: [ 729 | { 730 | type: "text", 731 | text: `Updated task: ${JSON.stringify(result, null, 2)}` 732 | } 733 | ] 734 | }; 735 | } 736 | 737 | async deleteTask(id) { 738 | await this.makeRequest(`/rest/tasks/${id}`, "DELETE"); 739 | return { 740 | content: [ 741 | { 742 | type: "text", 743 | text: `Successfully deleted task with ID: ${id}` 744 | } 745 | ] 746 | }; 747 | } 748 | 749 | // Metadata methods 750 | async getMetadataObjects() { 751 | const result = await this.makeRequest("/rest/metadata/objects"); 752 | return { 753 | content: [ 754 | { 755 | type: "text", 756 | text: `Metadata objects: ${JSON.stringify(result, null, 2)}` 757 | } 758 | ] 759 | }; 760 | } 761 | 762 | async getObjectMetadata(objectName) { 763 | const result = await this.makeRequest(`/rest/metadata/objects/${objectName}`); 764 | return { 765 | content: [ 766 | { 767 | type: "text", 768 | text: `Metadata for ${objectName}: ${JSON.stringify(result, null, 2)}` 769 | } 770 | ] 771 | }; 772 | } 773 | 774 | // Search methods 775 | async searchRecords(params) { 776 | const { query, objectTypes = ['people', 'companies'], limit = 10 } = params; 777 | const results = {}; 778 | 779 | for (const objectType of objectTypes) { 780 | try { 781 | const endpoint = `/rest/${objectType}?search=${encodeURIComponent(query)}&limit=${limit}`; 782 | results[objectType] = await this.makeRequest(endpoint); 783 | } catch (error) { 784 | results[objectType] = { error: error.message }; 785 | } 786 | } 787 | 788 | return { 789 | content: [ 790 | { 791 | type: "text", 792 | text: `Search results for "${query}": ${JSON.stringify(results, null, 2)}` 793 | } 794 | ] 795 | }; 796 | } 797 | 798 | async run() { 799 | const transport = new StdioServerTransport(); 800 | await this.server.connect(transport); 801 | console.error("Twenty CRM MCP server running on stdio"); 802 | } 803 | } 804 | 805 | const server = new TwentyCRMServer(); 806 | server.run().catch(console.error); --------------------------------------------------------------------------------