├── .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 | [](https://opensource.org/licenses/MIT)
8 | [](https://nodejs.org/)
9 | [](https://twenty.com)
10 | [](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 | |
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 | |
36 |
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 | |
48 |
49 |
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);
--------------------------------------------------------------------------------