attio-mcp
README.md
# attio-mcp
Code Mode MCP server for [Attio CRM](https://attio.com). 130+ endpoints through 3 tools.
> Built by [Trial and Error Inc](https://trialanderror.ai). Inspired by the [Code Mode pattern](https://blog.cloudflare.com/mcp-code-mode) from Cloudflare.
## Why Code Mode?
| | Traditional MCP | Code Mode |
|---|---|---|
| Tools | 35+ (one per operation) | 3 (search, execute, context) |
| Token cost | ~8,000 tokens for tool defs | ~800 tokens |
| New endpoints | Update every tool definition | Zero changes |
| Maintenance | Per-tool mapping | Just update the spec |
The LLM writes JavaScript to query the API spec and call endpoints directly. No tool-per-endpoint mapping. No bloat.
## Quick Start
```bash
git clone https://github.com/TrialAndErrorAI/attio-mcp.git
cd attio-mcp
npm install
npm run build
```
Create `.env`:
```
ATTIO_API_KEY=your_api_key_here
```
Get your API key from [Attio Settings > Developers > API Keys](https://app.attio.com/settings/developers).
### Claude Code
Add to your project's `.mcp.json`:
```json
{
"mcpServers": {
"attio": {
"command": "node",
"args": ["/path/to/attio-mcp/dist/index.js"],
"env": {
"ATTIO_API_KEY": "your_api_key_here"
}
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"attio": {
"command": "node",
"args": ["/path/to/attio-mcp/dist/index.js"],
"env": {
"ATTIO_API_KEY": "your_api_key_here"
}
}
}
}
```
## Tools
### `search(code)`
Write JavaScript to explore the Attio API specification. Find endpoints, check parameters, discover schemas.
```javascript
// Find all endpoints related to notes
const notes = Object.entries(spec.paths)
.filter(([p]) => p.includes('notes'))
.map(([path, methods]) => ({
path,
methods: Object.entries(methods).map(([m, op]) => ({
method: m.toUpperCase(),
summary: op.summary
}))
}));
return notes;
```
### `execute(code)`
Write JavaScript to call the Attio API. Authentication is automatic.
```javascript
// Find a company and create a note
const companies = await api.request({
method: 'POST',
path: '/v2/objects/companies/records/query',
params: { filter: { name: { $contains: 'Acme' } }, limit: 1 }
});
const companyId = companies.data[0].id.record_id;
await api.request({
method: 'POST',
path: '/v2/notes',
params: {
data: {
parent_object: 'companies',
parent_record_id: companyId,
title: 'Meeting Notes',
content: 'Discussed Q1 roadmap...'
}
}
});
return { success: true, company: companies.data[0].values.name };
```
### `context(topic)`
Look up Attio documentation by topic. Self-educate on the domain model before writing code.
```
context("lists") → docs about lists, entries, stages
context("attributes") → attribute types (select, status, text, etc.)
context("categories") → all top-level doc groupings
context("all") → full documentation index
```
### `test_connection()`
Verify your API key works and see server stats.
## Architecture
```
src/
├── auth/api-key.ts # API key auth (15 lines)
├── api/client.ts # HTTP client with rate limiting
├── spec/
│ ├── openapi.json # Attio's OpenAPI spec (embedded)
│ └── loader.ts # Pre-resolves $refs for agent traversal
├── context/
│ ├── llms.txt # Attio's documentation index
│ └── loader.ts # Topic-based doc search
├── executor/sandbox.ts # Node.js vm sandbox (proven pattern)
├── server/mcp-server.ts # 4 tools: search, execute, context, test_connection
└── index.ts # Entry point
```
## Security
- API key is injected via environment variables, never exposed to LLM-generated code
- Code execution runs in Node.js `vm` sandbox — no filesystem, no network, no env access
- Only injected globals (`spec`, `api`) are available inside the sandbox
- Output truncated at 40,000 characters to prevent context bloat
## License
MIT — Trial and Error Inc
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues