{"_id":"@drewling/twenty-mcp","_rev":"5-ccc140a0bc939221c9fbd2d7a8c3aeb3","name":"@drewling/twenty-mcp","dist-tags":{"latest":"0.7.1"},"versions":{"0.5.0":{"name":"@drewling/twenty-mcp","version":"0.5.0","keywords":["mcp","model-context-protocol","twenty","twenty-crm","crm","openapi"],"license":"MIT","_id":"@drewling/twenty-mcp@0.5.0","maintainers":[{"name":"tayoonabule","email":"tayo@drewl.com"},{"name":"veezex","email":"vlad@drewl.com"}],"homepage":"https://github.com/drewling/twenty-mcp#readme","bugs":{"url":"https://github.com/drewling/twenty-mcp/issues"},"bin":{"twenty-mcp":"build/index.js"},"dist":{"shasum":"bd7c08873488a81998c8e733ba526cf8790d2df0","tarball":"https://registry.npmjs.org/@drewling/twenty-mcp/-/twenty-mcp-0.5.0.tgz","fileCount":53,"integrity":"sha512-JrG1zx9URiKAUQmyEEiPqCJwJZ5zXcTzV16IDn+rqjI4sI63gsQwywz7+h/W1VqWDlGJIu8uHc1zqNLQpq4C1A==","signatures":[{"sig":"MEYCIQDjv3WyUp7SZlvs1KnptKdkJdjq9kNH/0YWCnUrgBvvxwIhAKABDghYEMGIx3JyVfOkmQ8E/1Fq+PJ6gOgCNM4diiHq","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2053343},"main":"build/index.js","type":"module","types":"./build/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"ab2d0fafa136ffb1cacd290ef98a823935cd1bb1","scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc && shx chmod 755 build/index.js","regen":"openapi-mcp-generator --input openapi/twenty-core.json --output . --server-name twenty-mcp --server-version 0.2.0 --transport stdio --force","start":"node build/index.js","prestart":"npm run build","typecheck":"tsc --noEmit","check:spec":"node scripts/check-spec-drift.js","fetch:spec":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/core',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-core.json',t);console.log('Saved openapi/twenty-core.json')})\"","fetch:specs":"npm run fetch:spec && npm run fetch:spec:metadata","regen:metadata":"node scripts/gen-metadata-tools.js","fetch:spec:metadata":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/metadata',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-metadata.json',t);console.log('Saved openapi/twenty-metadata.json')})\""},"_npmUser":{"name":"tayoonabule","email":"tayo@drewl.com"},"repository":{"url":"git+https://github.com/drewling/twenty-mcp.git","type":"git"},"_npmVersion":"10.8.2","description":"MCP server for Twenty CRM — full REST + GraphQL surface, self-healing schema, retries, and structured logging.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"zod":"^3.24.3","pino":"^10.3.1","axios":"^1.9.0","dotenv":"^16.4.5","json-schema-to-zod":"^2.6.1","@modelcontextprotocol/sdk":"^1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"shx":"^0.4.0","nock":"^14.0.13","vitest":"^4.1.5","typescript":"^5.8.3","@types/node":"^22.15.2","openapi-mcp-generator":"^3.3.0"},"_npmOperationalInternal":{"tmp":"tmp/twenty-mcp_0.5.0_1777585820378_0.6546618547222185","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@drewling/twenty-mcp","version":"0.6.0","keywords":["mcp","model-context-protocol","twenty","twenty-crm","crm","openapi"],"license":"MIT","_id":"@drewling/twenty-mcp@0.6.0","maintainers":[{"name":"tayoonabule","email":"tayo@drewl.com"},{"name":"veezex","email":"vlad@drewl.com"}],"homepage":"https://github.com/drewling/twenty-mcp#readme","bugs":{"url":"https://github.com/drewling/twenty-mcp/issues"},"bin":{"twenty-mcp":"build/index.js"},"dist":{"shasum":"cc90dd19cd8412574b9f8f23e3442b9feb0b4c70","tarball":"https://registry.npmjs.org/@drewling/twenty-mcp/-/twenty-mcp-0.6.0.tgz","fileCount":59,"integrity":"sha512-VDyXFyC2u/cAGfHg3pYRoetU5Dkd3nuVI85jMqeoz8WKg0y+ByaBQla2hWBNaY7utv5czbwbeGUcJBwdkuQCDw==","signatures":[{"sig":"MEQCIGMIHqE6Ef5PhjDeU9RA6cCJ/meoH9V+omWAMEgfXKezAiB+8/yz/4Ft9NH62ayWg+AiZQq4IH7w4+7Uo/ZTSqrWng==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2750006},"main":"build/index.js","type":"module","types":"./build/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"4cd0f5f25e07ac1292f5353f0c71e6b5f1d2c1a6","scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc && shx chmod 755 build/index.js","regen":"openapi-mcp-generator --input openapi/twenty-core.json --output . --server-name twenty-mcp --server-version 0.2.0 --transport stdio --force","start":"node build/index.js","prestart":"npm run build","typecheck":"tsc --noEmit","check:spec":"node scripts/check-spec-drift.js","fetch:spec":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/core',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-core.json',t);console.log('Saved openapi/twenty-core.json')})\"","fetch:specs":"npm run fetch:spec && npm run fetch:spec:metadata","regen:metadata":"node scripts/gen-metadata-tools.js","fetch:spec:metadata":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/metadata',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-metadata.json',t);console.log('Saved openapi/twenty-metadata.json')})\""},"_npmUser":{"name":"tayoonabule","email":"tayo@drewl.com"},"repository":{"url":"git+https://github.com/drewling/twenty-mcp.git","type":"git"},"_npmVersion":"10.9.2","description":"MCP server for Twenty CRM — full REST + GraphQL surface, self-healing schema, retries, and structured logging.","directories":{},"_nodeVersion":"22.16.0","dependencies":{"zod":"^3.24.3","pino":"^10.3.1","axios":"^1.9.0","dotenv":"^16.4.5","json-schema-to-zod":"^2.6.1","@modelcontextprotocol/sdk":"^1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"shx":"^0.4.0","nock":"^14.0.13","vitest":"^4.1.5","typescript":"^5.8.3","@types/node":"^22.15.2","openapi-mcp-generator":"^3.3.0"},"_npmOperationalInternal":{"tmp":"tmp/twenty-mcp_0.6.0_1779918640099_0.4809396648985853","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"name":"@drewling/twenty-mcp","version":"0.6.1","keywords":["mcp","model-context-protocol","twenty","twenty-crm","crm","openapi"],"license":"MIT","_id":"@drewling/twenty-mcp@0.6.1","maintainers":[{"name":"tayoonabule","email":"tayo@drewl.com"},{"name":"veezex","email":"vlad@drewl.com"}],"homepage":"https://github.com/drewling/twenty-mcp#readme","bugs":{"url":"https://github.com/drewling/twenty-mcp/issues"},"bin":{"twenty-mcp":"build/index.js"},"dist":{"shasum":"c199a85aa35f623e31749d49b8b0634338e5fed4","tarball":"https://registry.npmjs.org/@drewling/twenty-mcp/-/twenty-mcp-0.6.1.tgz","fileCount":59,"integrity":"sha512-qNxCBzyYafSiF2I5bFzQN7GgG1ejMlPjtoogIwyASmfilLR0bLsQ9xm7VXip2CQd9sFMPOhTQ0fo/Jr1BVmAwg==","signatures":[{"sig":"MEUCIQDIGeNDVSIYtTOaklYZX3Pxcv59xIF5yMUVmVPAtNaT9wIgGdMOfBGBMfI8C6sDuTjG7Ape/bsRnTATAstvpmgtZbQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2752854},"main":"build/index.js","type":"module","types":"./build/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"31b468fb8a621c08ad1f0cf0c76d01026ed9a4a3","scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc && shx chmod 755 build/index.js","regen":"openapi-mcp-generator --input openapi/twenty-core.json --output . --server-name twenty-mcp --server-version 0.2.0 --transport stdio --force","start":"node build/index.js","prestart":"npm run build","typecheck":"tsc --noEmit","check:spec":"node scripts/check-spec-drift.js","fetch:spec":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/core',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-core.json',t);console.log('Saved openapi/twenty-core.json')})\"","fetch:specs":"npm run fetch:spec && npm run fetch:spec:metadata","regen:metadata":"node scripts/gen-metadata-tools.js","fetch:spec:metadata":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/metadata',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-metadata.json',t);console.log('Saved openapi/twenty-metadata.json')})\""},"_npmUser":{"name":"tayoonabule","email":"tayo@drewl.com"},"repository":{"url":"git+https://github.com/drewling/twenty-mcp.git","type":"git"},"_npmVersion":"10.9.2","description":"MCP server for Twenty CRM — full REST + GraphQL surface, self-healing schema, retries, and structured logging.","directories":{},"_nodeVersion":"22.16.0","dependencies":{"zod":"^3.24.3","pino":"^10.3.1","axios":"^1.9.0","dotenv":"^16.4.5","json-schema-to-zod":"^2.6.1","@modelcontextprotocol/sdk":"^1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"shx":"^0.4.0","nock":"^14.0.13","vitest":"^4.1.5","typescript":"^5.8.3","@types/node":"^22.15.2","openapi-mcp-generator":"^3.3.0"},"_npmOperationalInternal":{"tmp":"tmp/twenty-mcp_0.6.1_1779920157159_0.8254701313940109","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@drewling/twenty-mcp","version":"0.7.0","keywords":["mcp","model-context-protocol","twenty","twenty-crm","crm","openapi"],"license":"MIT","_id":"@drewling/twenty-mcp@0.7.0","maintainers":[{"name":"tayoonabule","email":"tayo@drewl.com"},{"name":"veezex","email":"vlad@drewl.com"}],"homepage":"https://github.com/drewling/twenty-mcp#readme","bugs":{"url":"https://github.com/drewling/twenty-mcp/issues"},"bin":{"twenty-mcp":"build/index.js"},"dist":{"shasum":"c6d19273816acf5bf530523376a342efd4869088","tarball":"https://registry.npmjs.org/@drewling/twenty-mcp/-/twenty-mcp-0.7.0.tgz","fileCount":62,"integrity":"sha512-sTjvumvOnpLGdEAMDoYHIVfe6kNUV+stWr1raRQ/VVX4tVHh2GXd1joLQinMC4s44ZAQwVhklr2Gcl0w9TwACQ==","signatures":[{"sig":"MEUCID5vBA/awtmNXZQz3EWPtoihp07+GAJ0yF6iCjv3In1mAiEA34OJKmS86fVL7SJ0RX2z8ZZ5nJpWbRJDpTCtxJMSk64=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2786593},"main":"build/index.js","type":"module","types":"./build/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"2f8409f4930116c6685649db86a05b49c3f56aaa","scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc && shx chmod 755 build/index.js","regen":"openapi-mcp-generator --input openapi/twenty-core.json --output . --server-name twenty-mcp --server-version 0.2.0 --transport stdio --force","start":"node build/index.js","prestart":"npm run build","typecheck":"tsc --noEmit","check:spec":"node scripts/check-spec-drift.js","fetch:spec":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/core',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-core.json',t);console.log('Saved openapi/twenty-core.json')})\"","fetch:specs":"npm run fetch:spec && npm run fetch:spec:metadata","regen:metadata":"node scripts/gen-metadata-tools.js","fetch:spec:metadata":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/metadata',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-metadata.json',t);console.log('Saved openapi/twenty-metadata.json')})\""},"_npmUser":{"name":"tayoonabule","email":"tayo@drewl.com"},"repository":{"url":"git+https://github.com/drewling/twenty-mcp.git","type":"git"},"_npmVersion":"10.8.2","description":"MCP server for Twenty CRM — full REST + GraphQL surface, self-healing schema, retries, and structured logging.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"zod":"^3.24.3","pino":"^10.3.1","axios":"^1.9.0","dotenv":"^16.4.5","json-schema-to-zod":"^2.6.1","@modelcontextprotocol/sdk":"^1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"shx":"^0.4.0","nock":"^14.0.13","vitest":"^4.1.5","typescript":"^5.8.3","@types/node":"^22.15.2","openapi-mcp-generator":"^3.3.0"},"_npmOperationalInternal":{"tmp":"tmp/twenty-mcp_0.7.0_1779980864583_0.6754106709092567","host":"s3://npm-registry-packages-npm-production"}},"0.7.1":{"name":"@drewling/twenty-mcp","version":"0.7.1","description":"MCP server for Twenty CRM — full REST + GraphQL surface, self-healing schema, retries, and structured logging.","type":"module","main":"build/index.js","bin":{"twenty-mcp":"build/index.js"},"scripts":{"start":"node build/index.js","build":"tsc && shx chmod 755 build/index.js","dev":"tsc --watch","typecheck":"tsc --noEmit","prestart":"npm run build","fetch:spec":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/core',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-core.json',t);console.log('Saved openapi/twenty-core.json')})\"","fetch:spec:metadata":"node -e \"const u=process.env.TWENTY_API_URL,k=process.env.TWENTY_API_KEY;if(!u||!k){console.error('Set TWENTY_API_URL and TWENTY_API_KEY');process.exit(1)}fetch(u.replace(/\\/$/,'')+'/open-api/metadata',{headers:{Authorization:'Bearer '+k}}).then(r=>r.ok?r.text():Promise.reject(r.status)).then(t=>{require('fs').writeFileSync('openapi/twenty-metadata.json',t);console.log('Saved openapi/twenty-metadata.json')})\"","fetch:specs":"npm run fetch:spec && npm run fetch:spec:metadata","regen":"openapi-mcp-generator --input openapi/twenty-core.json --output . --server-name twenty-mcp --server-version 0.2.0 --transport stdio --force","regen:metadata":"node scripts/gen-metadata-tools.js","check:spec":"node scripts/check-spec-drift.js","test":"vitest run"},"engines":{"node":">=20.0.0"},"keywords":["mcp","model-context-protocol","twenty","twenty-crm","crm","openapi"],"repository":{"type":"git","url":"git+https://github.com/drewling/twenty-mcp.git"},"license":"MIT","dependencies":{"@modelcontextprotocol/sdk":"^1.10.0","axios":"^1.9.0","dotenv":"^16.4.5","json-schema-to-zod":"^2.6.1","pino":"^10.3.1","zod":"^3.24.3"},"devDependencies":{"@types/node":"^22.15.2","nock":"^14.0.13","openapi-mcp-generator":"^3.3.0","shx":"^0.4.0","typescript":"^5.8.3","vitest":"^4.1.5"},"_id":"@drewling/twenty-mcp@0.7.1","gitHead":"1737d629e5926c6ef0512669003e6c833191a86f","types":"./build/index.d.ts","bugs":{"url":"https://github.com/drewling/twenty-mcp/issues"},"homepage":"https://github.com/drewling/twenty-mcp#readme","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-1YwQJyDV/kcGUtvvM+NV4nZTS9DHgQP16IkFsUCYepKzAiAqyP3WpH9A2mvYJMKcF77U9avVu9IG1ifBoMlzYg==","shasum":"48bd7f7f4724af6c42e98e60309d8d4619f52135","tarball":"https://registry.npmjs.org/@drewling/twenty-mcp/-/twenty-mcp-0.7.1.tgz","fileCount":62,"unpackedSize":2795801,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFQQjmB2BlwI/AKWpQvhgZ8Pi7rU8luRdjCHjxOqy7BzAiBXI5DIT+Z2HdnNiIEYztxYM3SZUG9WbFoMVinsISZHag=="}]},"_npmUser":{"name":"tayoonabule","email":"tayo@drewl.com"},"directories":{},"maintainers":[{"name":"tayoonabule","email":"tayo@drewl.com"},{"name":"veezex","email":"vlad@drewl.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/twenty-mcp_0.7.1_1780956850552_0.6731849828569756"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-30T21:50:20.307Z","modified":"2026-06-08T22:14:10.875Z","0.5.0":"2026-04-30T21:50:20.592Z","0.6.0":"2026-05-27T21:50:40.408Z","0.6.1":"2026-05-27T22:15:57.425Z","0.7.0":"2026-05-28T15:07:44.750Z","0.7.1":"2026-06-08T22:14:10.759Z"},"bugs":{"url":"https://github.com/drewling/twenty-mcp/issues"},"license":"MIT","homepage":"https://github.com/drewling/twenty-mcp#readme","keywords":["mcp","model-context-protocol","twenty","twenty-crm","crm","openapi"],"repository":{"type":"git","url":"git+https://github.com/drewling/twenty-mcp.git"},"description":"MCP server for Twenty CRM — full REST + GraphQL surface, self-healing schema, retries, and structured logging.","maintainers":[{"name":"tayoonabule","email":"tayo@drewl.com"},{"name":"veezex","email":"vlad@drewl.com"}],"readme":"# @drewling/twenty-mcp\n\n[![npm](https://img.shields.io/npm/v/@drewling/twenty-mcp)](https://www.npmjs.com/package/@drewling/twenty-mcp)\n[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fdrewling%2Ftwenty--mcp-blue)](https://ghcr.io/drewling/twenty-mcp)\n[![CI](https://github.com/drewling/twenty-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/drewling/twenty-mcp/actions/workflows/ci.yml)\n\nA [Model Context Protocol](https://modelcontextprotocol.io) server for [Twenty CRM](https://twenty.com), generated from Twenty's REST and Metadata OpenAPI specs.\n\nExposes **~59 tools** across four APIs so any MCP client (Claude Desktop, Claude Code, Cursor, etc.) can read, write, and manage your CRM:\n\n| API | Tools exposed | Operations covered | What you can do |\n|---|---|---|---|\n| **Core REST** | ~35 entity tools | 392 | Companies, people, opportunities, notes, tasks, attachments, workflows … |\n| **Metadata REST** | 13 grouped `metadata_*` tools | 65 | Custom objects, fields, webhooks, API keys, views … |\n| **GraphQL** | 3 | — | Deep nesting, aggregations, full schema introspection |\n| **File uploads** | 1 | — | Upload files to Twenty storage, then attach them to any CRM record |\n| **Helper tools** | 6 | — | Filter builder, auto-pagination wrappers for major entities |\n| **Observability** | 1 (`twenty_health`) | — | Spec source, age, tool count, last drift event |\n\nCore REST operations are grouped by entity (e.g. one `companies` tool with an `action` parameter) rather than one tool per endpoint. This keeps the tool list at ~35 instead of 392, reducing context overhead by ~90% while covering the full API surface. Call `list_twenty_capabilities` to discover all entities and their available actions.\n\nv0.3 added agent-ergonomics tooling: filter DSL helper, auto-pagination, structured errors, MCP resources, and guided workflow prompts.\n\nv0.35 adds self-healing schema: the server fetches the live OpenAPI spec from Twenty at boot, polls every 5 minutes for changes, and automatically rebuilds the tool list when your data model changes — no restart or regen needed.\n\nv0.36 collapses the 65 flat `metadata_*` tools into 13 grouped tools using the same entity+action pattern as core REST, reducing the tool list by ~52 entries.\n\nv0.4 adds production reliability: retries with jitter, request timeouts, client-side idempotency, pino structured logging, optional OTel tracing, and a 260-test suite.\n\nv0.5 adds distribution: published to npm as `@drewling/twenty-mcp`, Docker image on GHCR, automated daily spec regen CI, and copy-paste examples for all major MCP clients.\n\nv0.6 adds metadata ergonomics: `get_object_by_name` tool (look up objectMetadataId by name), `find_all_custom_fields` and `find_all_webhooks` convenience tools, valid JSON truncation with exposed pagination cursors, and improved webhook/field tool descriptions.\n\n---\n\n## Install\n\n```bash\n# One-shot via npx (no install needed)\nnpx @drewling/twenty-mcp\n\n# Or install globally\nnpm install -g @drewling/twenty-mcp\ntwenty-mcp\n\n# Docker\ndocker run --rm -i \\\n  -e TWENTY_API_KEY=your_key \\\n  -e TWENTY_API_URL=https://api.twenty.com/rest \\\n  ghcr.io/drewling/twenty-mcp:latest\n```\n\n---\n\n## Quick start\n\nYou need:\n\n- Node.js ≥ 20 (or Docker)\n- A Twenty API key — Settings → Developers → **New API Key** in your workspace\n- The base URL of your Twenty instance (Twenty Cloud or self-hosted)\n\n### 1. Get an API key\n\nIn Twenty: **Settings → Developers → API Keys → Create**. Copy the token.\n\n### 2. Wire it into your MCP client\n\n| Variable | Example |\n|---|---|\n| `TWENTY_API_KEY` | `eyJhbGciOi…` |\n| `TWENTY_API_URL` | `https://api.twenty.com/rest` (must end in `/rest`) |\n\nCopy-paste configs for each client are in the [`examples/`](examples/) directory.\n\n#### Claude Desktop\n\nEdit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"twenty\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@drewling/twenty-mcp\"],\n      \"env\": {\n        \"TWENTY_API_KEY\": \"your_twenty_api_key\",\n        \"TWENTY_API_URL\": \"https://api.twenty.com/rest\"\n      }\n    }\n  }\n}\n```\n\nRestart Claude Desktop. The tools appear under the 🔌 menu.\n\n> **Security note.** MCP client config files store the token in plaintext on disk. `chmod 600` the file, and prefer the CLI install path below where available.\n\n#### Claude Code\n\n```bash\nclaude mcp add twenty \\\n  --env TWENTY_API_KEY=your_twenty_api_key \\\n  --env TWENTY_API_URL=https://api.twenty.com/rest \\\n  -- npx -y @drewling/twenty-mcp\n```\n\n#### Cursor\n\n`~/.cursor/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"twenty\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@drewling/twenty-mcp\"],\n      \"env\": {\n        \"TWENTY_API_KEY\": \"your_twenty_api_key\",\n        \"TWENTY_API_URL\": \"https://api.twenty.com/rest\"\n      }\n    }\n  }\n}\n```\n\n#### Docker\n\n```bash\ndocker run --rm -i \\\n  -e TWENTY_API_KEY=your_key \\\n  -e TWENTY_API_URL=https://api.twenty.com/rest \\\n  ghcr.io/drewling/twenty-mcp:latest\n```\n\nFor Claude Desktop with Docker, set `command` to `docker` and `args` to `[\"run\", \"--rm\", \"-i\", \"-e\", \"TWENTY_API_KEY=...\", \"-e\", \"TWENTY_API_URL=...\", \"ghcr.io/drewling/twenty-mcp:latest\"]`.\n\n---\n\n## Run locally (development)\n\n```bash\ngit clone https://github.com/drewling/twenty-mcp\ncd twenty-mcp\nnpm install\ncp .env.example .env   # add your key and URL\nnpm start              # builds and runs on stdio\n```\n\nWatch mode:\n\n```bash\nnpm run dev\n```\n\nPoint a local MCP client at the build:\n\n```json\n{\n  \"mcpServers\": {\n    \"twenty\": {\n      \"command\": \"node\",\n      \"args\": [\"/absolute/path/to/twenty-mcp/build/index.js\"],\n      \"env\": {\n        \"TWENTY_API_KEY\": \"...\",\n        \"TWENTY_API_URL\": \"https://api.twenty.com/rest\"\n      }\n    }\n  }\n}\n```\n\n---\n\n## What's exposed\n\n### Core REST tools\n\nOperations from `/rest/open-api/core` are grouped into one tool per entity. Examples:\n\n- `companies(action: \"findMany\", filter: \"name[ILIKE]:%acme%\")`\n- `people(action: \"createOne\", requestBody: { ... })`\n- `opportunities(action: \"findOne\", id: \"<uuid>\")`\n- `notes(action: \"deleteOne\", id: \"<uuid>\")`\n\nCall `list_twenty_capabilities` first to see all available entities and which actions each supports. Actions include `findMany`, `findOne`, `createOne`, `createMany`, `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, `restoreOne`, `groupBy`, `findDuplicates`, and entity-specific variants.\n\n**Filter syntax:** Use `build_twenty_filter` to construct filter strings without trial-and-error, or write them manually: `field[COMPARATOR]:value` — e.g. `name[ILIKE]:%acme%`, `createdAt[GTE]:\"2024-01-01\"`. Combine with commas (AND). Use `and(…)`, `or(…)`, `not(…)` for complex conditions. Operators: `EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`, `IN`, `LIKE`, `ILIKE`, `IS`, `STARTS_WITH`.\n\n**Pagination:** Pass `limit` (default 60, max 60) and `starting_after` = `pageInfo.endCursor` for the next page. Or use `find_all_*` to auto-paginate.\n\n**Depth:** `depth=1` (default) includes direct relations; `depth=0` returns the bare object.\n\n### Metadata tools (`metadata_` prefix)\n\nThe 65 metadata operations are grouped into 13 entity tools using the same `action` pattern as core REST tools:\n\n- `metadata_objects(action: \"findMany\")` — list custom object types; `action: \"createOne\"` / `\"updateOne\"` / `\"deleteOne\"` to manage them\n- `metadata_fields(action: \"findMany\")` — custom fields; `createOne` / `updateOne` / `deleteOne` to manage them\n- `metadata_webhooks(action: \"findMany\")` — webhook subscriptions\n- `metadata_apiKeys(action: \"findMany\")` — API key management\n- `metadata_views(action: \"findMany\")` — saved views; also `metadata_viewFields`, `metadata_viewFilters`, `metadata_viewSorts`, `metadata_viewGroups`, `metadata_viewFilterGroups`\n- `metadata_pageLayouts(action: \"findMany\")` — page layouts; also `metadata_pageLayoutTabs`, `metadata_pageLayoutWidgets`\n\nAll 13 grouped tools support: `findMany`, `findOne`, `createOne`, `updateOne`, `deleteOne`.\n\n### GraphQL escape-hatch tools\n\n| Tool | Purpose |\n|---|---|\n| `graphql_query` | Arbitrary query or mutation against `/graphql` |\n| `graphql_introspect` | Full schema introspection; pass `type_name` to filter |\n| `graphql_metadata` | Query/mutation against the metadata GraphQL endpoint |\n\nUse these when REST doesn't reach: deep nested fetches, aggregations, batched mutations.\n\n### File upload tool\n\nTwenty's file upload endpoint isn't part of the REST OpenAPI spec, so it's hand-written in `src/file-tools.ts`.\n\n| Tool | Purpose |\n|---|---|\n| `upload_file` | Upload a file to Twenty storage; returns the `path` string |\n\n**Typical workflow:**\n\n1. Call `upload_file` with a local `file_path` (or `file_content` + `file_name` for in-memory data).\n2. The tool returns a JSON object containing a `path` field.\n3. Pass that path as `fullPath` when calling `createOneAttachment`, linking the file to a company, person, note, etc.\n\n```\nupload_file(file_path=\"/tmp/report.pdf\")\n→ { \"path\": \"workspace-id/report-uuid.pdf\" }\n\nattachments(action=\"createOne\", requestBody={\n  name: \"Q4 Report\",\n  fullPath: \"workspace-id/report-uuid.pdf\",\n  fileCategory: \"PRESENTATION\",\n  targetCompanyId: \"<company-uuid>\"\n})\n```\n\n### Helper tools (v0.3+)\n\n#### `build_twenty_filter`\n\nConstruct a valid Twenty filter string from a structured input — no syntax trial-and-error:\n\n```\nbuild_twenty_filter({ field: \"name\", operator: \"ILIKE\", value: \"acme\" })\n→ { \"filter\": \"name[ILIKE]:\\\"%acme%\\\"\" }\n\nbuild_twenty_filter({ field: \"revenue\", operator: \"GT\", value: 1000000 })\n→ { \"filter\": \"revenue[GT]:1000000\" }\n\nbuild_twenty_filter({ and: [\n  { field: \"name\", operator: \"ILIKE\", value: \"acme\" },\n  { field: \"revenue\", operator: \"GT\", value: 0 }\n]})\n→ { \"filter\": \"and(name[ILIKE]:\\\"%acme%\\\",revenue[GT]:0)\" }\n```\n\nSupported operators: `EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`, `IN`, `IS`, `LIKE`, `ILIKE`, `STARTS_WITH`. Strings and dates are automatically quoted; numbers and booleans are not. `ILIKE`/`LIKE` wraps values in `%` wildcards if none are present.\n\n#### `find_all_*` (auto-pagination)\n\nFetch all records without a manual pagination loop:\n\n```\nfind_all_companies(filter: \"name[ILIKE]:\\\"%acme%\\\"\", max: 500)\n→ { records: [...], totalFetched: 42, wasCapped: false, endCursor: \"...\" }\n```\n\nAvailable wrappers: `find_all_companies`, `find_all_people`, `find_all_opportunities`, `find_all_notes`, `find_all_tasks`. All accept the same `filter`, `order_by`, `depth`, and `max` parameters. Default `max` is 10000.\n\n### MCP Resources (v0.3+)\n\nFetch a record by URI without calling a tool:\n\n| Resource URI | Description |\n|---|---|\n| `twenty://company/{id}` | Fetch company by ID |\n| `twenty://person/{id}` | Fetch person by ID |\n| `twenty://opportunity/{id}` | Fetch opportunity by ID |\n| `twenty://note/{id}` | Fetch note by ID |\n| `twenty://task/{id}` | Fetch task by ID |\n\n### MCP Prompts (v0.3+)\n\nGuided multi-step workflows accessible via `ListPrompts` / `GetPrompt`:\n\n| Prompt | Description |\n|---|---|\n| `log-a-call` | Log a call activity linked to a company and contact |\n| `create-lead-from-email` | Create a lead from an inbound email (company, contact, note, task) |\n| `weekly-pipeline-review` | Summarize pipeline by stage and list tasks due this week |\n\n### Error responses (v0.3+)\n\nAll errors return a structured JSON object instead of raw axios details:\n\n```json\n{ \"error\": { \"code\": \"DUPLICATE\", \"message\": \"Company 'Acme' already exists\", \"hint\": \"Use updateOne to modify the existing company.\" } }\n```\n\nError codes: `VALIDATION`, `AUTH_FAILED`, `NOT_FOUND`, `DUPLICATE`, `CONFLICT`, `RATE_LIMIT`, `NETWORK_ERROR`, `SERVER_ERROR`.\n\n### Self-healing schema (v0.35+)\n\nThe server fetches Twenty's live OpenAPI spec at boot and polls for changes every 5 minutes (configurable via `TWENTY_SPEC_REFRESH_MS`). When a spec change is detected, the tool list is rebuilt and connected MCP clients receive a `notifications/tools/list_changed` event so they re-discover without reconnecting.\n\n**What this means in practice:**\n\n- Add a custom field to `Person` in Twenty's UI → within 5 minutes, `people(action: \"updateOne\", ...)` accepts the new field.\n- Delete a custom object → tools for it disappear from the list automatically.\n- No restart, no `npm run regen`, no redeploy.\n\n**Drift recovery:** If a tool call returns a `NOT_FOUND` error that looks like schema drift (e.g. \"Route not found\", \"Field X does not exist\"), the server immediately forces a spec refresh and retries the call once. If the retry succeeds, you get the result. If not, you get a structured error with the drift signal.\n\n**Monitoring:** Call `twenty_health` to see the current spec state:\n\n```json\n{\n  \"specSource\": \"live\",\n  \"specEtag\": \"\\\"abc123\\\"\",\n  \"specAgeSeconds\": 42,\n  \"lastRefreshAt\": \"2026-01-15T10:30:00.000Z\",\n  \"toolCount\": 111,\n  \"lastDriftEventAt\": null\n}\n```\n\n**Offline / airgapped:** Set `TWENTY_OFFLINE=true` to skip all live fetching and serve tools from the bundled `openapi/twenty-core.json`.\n\n---\n\n## Reliability & Observability (v0.4+)\n\n### Retries and timeouts\n\nFailed requests are retried up to 3 times with exponential backoff + jitter. Retries trigger on 5xx responses, 429 rate limits, and network errors (`ECONNABORTED`, `ETIMEDOUT`). 4xx errors (except 429) are not retried. The `Retry-After` header is respected.\n\n| Env var | Default | Description |\n|---|---|---|\n| `TWENTY_TIMEOUT_MS` | `30000` | Per-request timeout in milliseconds |\n| `TWENTY_RETRY_BASE_MS` | `100` | Exponential backoff base delay |\n| `TWENTY_RETRY_MAX_MS` | `10000` | Maximum backoff delay cap |\n| `TWENTY_RETRY_JITTER_MS` | `100` | Random jitter added to each backoff interval |\n\n### Client-side idempotency\n\nCreate operations (`createOne`, `createMany`) support an `idempotency_key` parameter. When the same key is seen twice within 24 hours, the cached response is returned without hitting the API — preventing duplicate records on network retries.\n\n```\ncompanies(action: \"createOne\", idempotency_key: \"onboard-acme-2026\", requestBody: { name: \"Acme\" })\n# Second call with same key → returns cached result, no duplicate created\n```\n\n| Env var | Default | Description |\n|---|---|---|\n| `TWENTY_IDEMPOTENCY_MAX_ENTRIES` | `1000` | LRU cache capacity (entries evicted LRU when full) |\n| `TWENTY_IDEMPOTENCY_TTL_MS` | `86400000` | Cache TTL in milliseconds (default 24h) |\n\n### Structured logging\n\nAll tool calls emit structured JSON logs to **stderr** (never stdout — keeps the MCP stdio transport clean). Sensitive fields (`apiKey`, `token`, `password`, `secret`, `authorization`) are redacted automatically.\n\n```bash\nLOG_LEVEL=debug  # trace | debug | info | warn | error (default: info)\n```\n\nEach log line includes `tool`, `status`, `latency_ms`, and `retryCount` for observability tooling.\n\n### OpenTelemetry tracing (optional)\n\nNo-op by default. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to enable tracing — the server will dynamically import `@opentelemetry/sdk-node` and emit spans for every tool call. If the packages aren't installed, it falls back to no-op gracefully.\n\n```bash\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n```\n\nInstall the OTel packages separately if you want traces:\n\n```bash\nnpm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http\n```\n\n### OpenAPI spec drift CI\n\nThree GitHub Actions workflows handle ongoing maintenance:\n\n| Workflow | Trigger | What it does |\n|---|---|---|\n| `ci.yml` | Every push + PR | Build, typecheck, full test suite |\n| `check-spec-drift.yml` | Daily + manual | SHA-256 diff of bundled spec vs live |\n| `spec-regen.yml` | Daily 06:00 UTC | Fetches fresh specs, reruns regen, opens PR if changed |\n| `npm-publish.yml` | Push to master (when `package.json` changes) or manual | Publishes to npm |\n| `docker-publish.yml` | Push to master or `v*.*.*` tag | Builds + pushes multi-arch image to GHCR |\n\nRequired GitHub secrets: `NPM_TOKEN`, `TWENTY_SPEC_API_KEY` (a Twenty Cloud API key for spec regen).\n\nRun the drift check locally:\n\n```bash\nTWENTY_API_URL=https://api.twenty.com/rest TWENTY_API_KEY=your_key npm run check:spec\n```\n\n---\n\n## Regenerating against a newer Twenty version\n\n```bash\n# Fetch both specs (requires TWENTY_API_URL and TWENTY_API_KEY in env)\nnpm run fetch:specs\n\n# Regenerate core tools (regenerates src/index.ts — see \"Local patches\" below)\nnpm run regen\n\n# Regenerate metadata tools (always safe to re-run, no patches needed)\nnpm run regen:metadata\n\nnpm run build\n```\n\n### Local patches on `src/index.ts`\n\nAfter every `npm run regen`, re-apply these patches on top of the generated `src/index.ts`:\n\n1. **`dotenv` import** at the top.\n2. **Friendly env aliases + fail-fast + error handlers** mapping `TWENTY_API_KEY` → `BEARER_TOKEN_BEARERAUTH` and `TWENTY_API_URL` → `API_BASE_URL`.\n3. **TypeScript build fix** — coerce `response.headers['content-type']` with `String(...)`.\n4. **MCP `instructions`** string documenting filter DSL, pagination, and API groups.\n5. **Metadata + GraphQL + file-upload imports and registration** — the `import` statements for `metadata-tools.js` / `graphql-tools.js` / `file-tools.js`, snapshot of `restToolDefinitions`, the registration loops, `executeGraphqlTool`, and the multipart `FormData` branch in `executeApiTool`.\n6. **Entity-grouped tool layer** — `restToolDefinitions` snapshot, `inferAction`, `entityGroups`, `buildEntitySchema`, `list_twenty_capabilities`, and the grouped `ListTools`/`CallTool` handlers.\n\n`src/metadata-tools.ts`, `src/graphql-tools.ts`, `src/file-tools.ts`, `src/filter-tools.ts`, `src/pagination-tools.ts`, `src/error-handler.ts`, `src/resources.ts`, and `src/prompts.ts` survive regen untouched — only `src/index.ts` needs patching. Phase 4 of the roadmap automates this via a `patches/` directory.\n\n---\n\n## Configuration reference\n\n| Env var | Required | Default | Description |\n|---|---|---|---|\n| `TWENTY_API_KEY` | **yes** | — | Workspace API key from Twenty Settings → Developers. Server refuses to start if unset. |\n| `TWENTY_API_URL` | **yes** | — | Twenty REST base URL, must end in `/rest`. Metadata and GraphQL paths are derived from this. |\n| `BEARER_TOKEN_BEARERAUTH` | — | — | Raw internal form; set `TWENTY_API_KEY` instead. |\n| `API_BASE_URL` | — | — | Raw internal form; set `TWENTY_API_URL` instead. |\n| `TWENTY_TOOLS` | — | all entities | Comma-separated entity names to expose (e.g. `companies,people,notes`). Reduces registered tools further. |\n| `TWENTY_MAX_RESPONSE` | — | `102400` | Max response size in bytes before truncation. Increase if large payloads are being cut off. |\n| `TWENTY_OBJECTS_ALLOWLIST` | — | all objects | Comma-separated object names to expose from the live spec (e.g. `companies,people,deals`). Applied on every refresh cycle. |\n| `TWENTY_SPEC_REFRESH_MS` | — | `300000` (5 min) | How often to poll for spec changes. Set to `0` to disable polling. |\n| `TWENTY_OFFLINE` | — | unset | Set to `true` to skip live spec fetching and always use the bundled `openapi/twenty-core.json`. Useful for airgapped environments. |\n| `TWENTY_TIMEOUT_MS` | — | `30000` | Per-request timeout in milliseconds. |\n| `TWENTY_RETRY_BASE_MS` | — | `100` | Exponential backoff base delay (ms). |\n| `TWENTY_RETRY_MAX_MS` | — | `10000` | Maximum backoff delay cap (ms). |\n| `TWENTY_RETRY_JITTER_MS` | — | `100` | Random jitter added to each retry delay (ms). |\n| `TWENTY_IDEMPOTENCY_MAX_ENTRIES` | — | `1000` | LRU cache size for idempotency deduplication. |\n| `TWENTY_IDEMPOTENCY_TTL_MS` | — | `86400000` | Idempotency cache TTL (ms, default 24h). |\n| `LOG_LEVEL` | — | `info` | pino log level: `trace`, `debug`, `info`, `warn`, `error`. Logs go to stderr. |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | — | unset | Set to enable OpenTelemetry tracing. Requires `@opentelemetry/sdk-node` installed. |\n\n---\n\n## Project layout\n\n```\n.\n├── examples/\n│   ├── claude-desktop.json    # Copy-paste config for Claude Desktop\n│   ├── cursor.json            # Copy-paste config for Cursor (~/.cursor/mcp.json)\n│   ├── zed.json               # Copy-paste config for Zed (.zed/settings.json)\n│   └── n8n.json               # Reference snippet for n8n MCP Tool node\n├── openapi/\n│   ├── twenty-core.json       # Cached core REST spec (fetch:spec)\n│   └── twenty-metadata.json   # Cached metadata REST spec (fetch:spec:metadata)\n├── scripts/\n│   ├── gen-metadata-tools.js  # Generator for src/metadata-tools.ts\n│   └── check-spec-drift.js    # SHA256 spec comparison for CI drift detection (v0.4)\n├── src/\n│   ├── index.ts               # Core REST tools + patches + request handlers\n│   ├── spec-loader.ts         # ETag-aware spec fetch with disk fallback (v0.35)\n│   ├── tool-registry.ts       # Runtime tool registry with diff + health (v0.35)\n│   ├── request-executor.ts    # Axios request executor with retries + idempotency (v0.35/v0.4)\n│   ├── retry-strategy.ts      # Exponential backoff, isRetryable, Retry-After (v0.4)\n│   ├── idempotency-cache.ts   # LRU idempotency cache for create operations (v0.4)\n│   ├── logger.ts              # pino structured logger to stderr (v0.4)\n│   ├── telemetry.ts           # Optional OTel tracer hook (v0.4)\n│   ├── metadata-tools.ts      # Generated metadata tools (metadata_* prefix)\n│   ├── graphql-tools.ts       # Hand-written GraphQL escape-hatch tools\n│   ├── file-tools.ts          # Hand-written file upload tool (upload_file)\n│   ├── filter-tools.ts        # build_twenty_filter DSL helper (v0.3)\n│   ├── pagination-tools.ts    # find_all_* auto-pagination wrappers (v0.3)\n│   ├── error-handler.ts       # Structured error formatting + drift detection (v0.3/v0.35)\n│   ├── resources.ts           # MCP resource templates (v0.3)\n│   └── prompts.ts             # MCP guided workflow prompts (v0.3)\n├── tests/                     # Vitest test suite (260 tests)\n├── build/                     # Compiled JS output (gitignored)\n├── Dockerfile                 # Multi-stage build for ghcr.io/drewling/twenty-mcp (v0.5)\n├── smithery.yaml              # Smithery registry manifest (v0.5)\n├── .mcp-so.json               # mcp.so registry manifest (v0.5)\n├── .env.example               # Template for local development\n├── ROADMAP.md                 # Phase plan\n└── package.json\n```\n\n---\n\n## License\n\nMIT.\n","readmeFilename":"README.md"}