@antidrift/mcp-attio 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +142 -0
- package/connectors/attio.mjs +341 -0
- package/package.json +29 -0
- package/server.mjs +65 -0
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const configDir = join(homedir(), '.antidrift');
|
|
13
|
+
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
|
|
16
|
+
function ask(q) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(q, (answer) => { resolve(answer); });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const command = process.argv[2];
|
|
24
|
+
|
|
25
|
+
if (command === 'add' || command === 'setup' || !command) {
|
|
26
|
+
await setup();
|
|
27
|
+
} else if (command === 'status') {
|
|
28
|
+
status();
|
|
29
|
+
} else if (command === 'reset') {
|
|
30
|
+
const configPath = join(configDir, 'attio.json');
|
|
31
|
+
if (existsSync(configPath)) {
|
|
32
|
+
const { rmSync } = await import('fs');
|
|
33
|
+
rmSync(configPath);
|
|
34
|
+
console.log(' Credentials cleared. Run this command again to reconnect.\n');
|
|
35
|
+
} else {
|
|
36
|
+
console.log(' No credentials to clear.\n');
|
|
37
|
+
}
|
|
38
|
+
rl.close();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
} else {
|
|
41
|
+
console.log(`
|
|
42
|
+
@antidrift/mcp-attio — Attio CRM for Claude
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
npx @antidrift/mcp-attio Connect Attio
|
|
46
|
+
npx @antidrift/mcp-attio status Check connection status
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function setup() {
|
|
54
|
+
console.log(`
|
|
55
|
+
┌─────────────────────────────┐
|
|
56
|
+
│ antidrift │
|
|
57
|
+
│ Attio CRM │
|
|
58
|
+
└─────────────────────────────┘
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
const configPath = join(configDir, 'attio.json');
|
|
62
|
+
if (existsSync(configPath)) {
|
|
63
|
+
console.log(' Already authorized — updating server files.\n');
|
|
64
|
+
writeMcpConfig();
|
|
65
|
+
console.log(' ✓ Attio updated. Restart your agent to pick up changes.\n');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(' To get your API key:\n');
|
|
70
|
+
console.log(' 1. Go to https://app.attio.com');
|
|
71
|
+
console.log(' 2. Settings (bottom left) → Developers → API Keys');
|
|
72
|
+
console.log(' 3. Create a new key with read/write access');
|
|
73
|
+
console.log(' 4. Copy the key and paste it below\n');
|
|
74
|
+
|
|
75
|
+
const apiKey = await ask(' API key: ');
|
|
76
|
+
|
|
77
|
+
if (!apiKey.trim()) {
|
|
78
|
+
console.log(' No key provided.\n');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const masked = '*'.repeat(Math.max(0, apiKey.trim().length - 5)) + apiKey.trim().slice(-5);
|
|
83
|
+
console.log(` Key: ${masked}\n`);
|
|
84
|
+
|
|
85
|
+
// Verify the key works
|
|
86
|
+
console.log(' Verifying...');
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch('https://api.attio.com/v2/self', {
|
|
89
|
+
headers: { 'Authorization': `Bearer ${apiKey.trim()}` }
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
console.log(` ✓ Connected to workspace: ${data.data?.workspace?.name || 'OK'}\n`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.log(` ✗ Invalid key or connection failed: ${err.message}\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
mkdirSync(configDir, { recursive: true });
|
|
100
|
+
writeFileSync(configPath, JSON.stringify({ apiKey: apiKey.trim() }, null, 2));
|
|
101
|
+
writeMcpConfig();
|
|
102
|
+
console.log(' ✓ Attio connected (people, companies, deals, tasks, notes)');
|
|
103
|
+
console.log(' Restart your agent to use it.\n');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function status() {
|
|
108
|
+
const hasConfig = existsSync(join(configDir, 'attio.json'));
|
|
109
|
+
const icon = hasConfig ? '✓' : '○';
|
|
110
|
+
console.log(`\n ${icon} Attio CRM — ${hasConfig ? 'connected' : 'not connected'}`);
|
|
111
|
+
console.log(' People, companies, deals, tasks, notes\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeMcpConfig() {
|
|
115
|
+
const cwd = process.cwd();
|
|
116
|
+
const serverDir = join(cwd, '.mcp-servers', 'attio');
|
|
117
|
+
const pkgDir = join(__dirname, '..');
|
|
118
|
+
|
|
119
|
+
// Copy server files to brain
|
|
120
|
+
mkdirSync(join(serverDir, 'connectors'), { recursive: true });
|
|
121
|
+
cpSync(join(pkgDir, 'server.mjs'), join(serverDir, 'server.mjs'));
|
|
122
|
+
cpSync(join(pkgDir, 'connectors', 'attio.mjs'), join(serverDir, 'connectors', 'attio.mjs'));
|
|
123
|
+
|
|
124
|
+
// Write .mcp.json pointing to local copy
|
|
125
|
+
const mcpPath = join(cwd, '.mcp.json');
|
|
126
|
+
let config = {};
|
|
127
|
+
|
|
128
|
+
if (existsSync(mcpPath)) {
|
|
129
|
+
try { config = JSON.parse(readFileSync(mcpPath, 'utf8')); } catch {}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
133
|
+
|
|
134
|
+
config.mcpServers['antidrift-attio'] = {
|
|
135
|
+
command: 'node',
|
|
136
|
+
args: [join('.mcp-servers', 'attio', 'server.mjs')]
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
const config = JSON.parse(readFileSync(join(homedir(), '.antidrift', 'attio.json'), 'utf8'));
|
|
6
|
+
|
|
7
|
+
async function attio(method, path, body) {
|
|
8
|
+
const res = await fetch(`https://api.attio.com/v2${path}`, {
|
|
9
|
+
method,
|
|
10
|
+
headers: {
|
|
11
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
12
|
+
'Content-Type': 'application/json'
|
|
13
|
+
},
|
|
14
|
+
body: body ? JSON.stringify(body) : undefined
|
|
15
|
+
});
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const err = await res.text();
|
|
18
|
+
throw new Error(`Attio API ${res.status}: ${err}`);
|
|
19
|
+
}
|
|
20
|
+
return res.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatPerson(record) {
|
|
24
|
+
const vals = record.values || {};
|
|
25
|
+
const name = vals.name?.[0]?.full_name || vals.name?.[0]?.first_name || 'Unknown';
|
|
26
|
+
const email = vals.email_addresses?.[0]?.email_address || '';
|
|
27
|
+
const company = vals.company?.[0]?.target_object_id ? vals.company[0].target_record_id : '';
|
|
28
|
+
let line = `👤 ${name}`;
|
|
29
|
+
if (email) line += ` • ${email}`;
|
|
30
|
+
line += ` [id: ${record.id.record_id}]`;
|
|
31
|
+
return line;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatCompany(record) {
|
|
35
|
+
const vals = record.values || {};
|
|
36
|
+
const name = vals.name?.[0]?.value || 'Unknown';
|
|
37
|
+
const domain = vals.domains?.[0]?.domain || '';
|
|
38
|
+
let line = `🏢 ${name}`;
|
|
39
|
+
if (domain) line += ` • ${domain}`;
|
|
40
|
+
line += ` [id: ${record.id.record_id}]`;
|
|
41
|
+
return line;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatDeal(record) {
|
|
45
|
+
const vals = record.values || {};
|
|
46
|
+
const name = vals.name?.[0]?.value || 'Unknown';
|
|
47
|
+
const stage = vals.stage?.[0]?.status?.title || '';
|
|
48
|
+
const value = vals.value?.[0]?.currency_value || '';
|
|
49
|
+
let line = `💰 ${name}`;
|
|
50
|
+
if (stage) line += ` • ${stage}`;
|
|
51
|
+
if (value) line += ` • $${value}`;
|
|
52
|
+
line += ` [id: ${record.id.record_id}]`;
|
|
53
|
+
return line;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const tools = [
|
|
57
|
+
{
|
|
58
|
+
name: 'attio_list_people',
|
|
59
|
+
description: 'List people in Attio CRM.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
limit: { type: 'number', description: 'Max results (default 20)' }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
handler: async ({ limit = 20 }) => {
|
|
67
|
+
const res = await attio('POST', '/objects/people/records/query', { limit });
|
|
68
|
+
if (!res.data?.length) return 'No people found.';
|
|
69
|
+
return res.data.map(formatPerson).join('\n');
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'attio_search_people',
|
|
74
|
+
description: 'Search for people in Attio by name or email.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
query: { type: 'string', description: 'Name or email to search for' }
|
|
79
|
+
},
|
|
80
|
+
required: ['query']
|
|
81
|
+
},
|
|
82
|
+
handler: async ({ query }) => {
|
|
83
|
+
const res = await attio('POST', '/objects/people/records/query', {
|
|
84
|
+
filter: {
|
|
85
|
+
or: [
|
|
86
|
+
{ attribute: 'name', condition: 'contains', value: query },
|
|
87
|
+
{ attribute: 'email_addresses', condition: 'contains', value: query }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (!res.data?.length) return `No people matching "${query}".`;
|
|
92
|
+
return res.data.map(formatPerson).join('\n');
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'attio_get_person',
|
|
97
|
+
description: 'Get full details for a person by record ID.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
recordId: { type: 'string', description: 'The person record ID' }
|
|
102
|
+
},
|
|
103
|
+
required: ['recordId']
|
|
104
|
+
},
|
|
105
|
+
handler: async ({ recordId }) => {
|
|
106
|
+
const res = await attio('GET', `/objects/people/records/${recordId}`);
|
|
107
|
+
const vals = res.data.values || {};
|
|
108
|
+
const lines = [];
|
|
109
|
+
const name = vals.name?.[0]?.full_name || 'Unknown';
|
|
110
|
+
lines.push(`👤 ${name}`);
|
|
111
|
+
if (vals.email_addresses?.length) lines.push(`📧 ${vals.email_addresses.map(e => e.email_address).join(', ')}`);
|
|
112
|
+
if (vals.phone_numbers?.length) lines.push(`📞 ${vals.phone_numbers.map(p => p.phone_number).join(', ')}`);
|
|
113
|
+
if (vals.job_title?.[0]?.value) lines.push(`💼 ${vals.job_title[0].value}`);
|
|
114
|
+
if (vals.description?.[0]?.value) lines.push(`📝 ${vals.description[0].value}`);
|
|
115
|
+
lines.push(`[id: ${recordId}]`);
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'attio_create_person',
|
|
121
|
+
description: 'Create a new person in Attio.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
email: { type: 'string', description: 'Email address' },
|
|
126
|
+
firstName: { type: 'string', description: 'First name' },
|
|
127
|
+
lastName: { type: 'string', description: 'Last name' },
|
|
128
|
+
jobTitle: { type: 'string', description: 'Job title (optional)' }
|
|
129
|
+
},
|
|
130
|
+
required: ['email']
|
|
131
|
+
},
|
|
132
|
+
handler: async ({ email, firstName, lastName, jobTitle }) => {
|
|
133
|
+
const values = {
|
|
134
|
+
email_addresses: [{ email_address: email }]
|
|
135
|
+
};
|
|
136
|
+
if (firstName || lastName) {
|
|
137
|
+
values.name = [{ first_name: firstName || '', last_name: lastName || '' }];
|
|
138
|
+
}
|
|
139
|
+
if (jobTitle) values.job_title = [{ value: jobTitle }];
|
|
140
|
+
|
|
141
|
+
const res = await attio('POST', '/objects/people/records', { data: { values } });
|
|
142
|
+
return `✅ Created ${firstName || ''} ${lastName || ''} (${email}) [id: ${res.data.id.record_id}]`;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'attio_list_companies',
|
|
147
|
+
description: 'List companies in Attio CRM.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
limit: { type: 'number', description: 'Max results (default 20)' }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
handler: async ({ limit = 20 }) => {
|
|
155
|
+
const res = await attio('POST', '/objects/companies/records/query', { limit });
|
|
156
|
+
if (!res.data?.length) return 'No companies found.';
|
|
157
|
+
return res.data.map(formatCompany).join('\n');
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'attio_search_companies',
|
|
162
|
+
description: 'Search for companies in Attio by name or domain.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
query: { type: 'string', description: 'Company name or domain to search for' }
|
|
167
|
+
},
|
|
168
|
+
required: ['query']
|
|
169
|
+
},
|
|
170
|
+
handler: async ({ query }) => {
|
|
171
|
+
const res = await attio('POST', '/objects/companies/records/query', {
|
|
172
|
+
filter: {
|
|
173
|
+
or: [
|
|
174
|
+
{ attribute: 'name', condition: 'contains', value: query },
|
|
175
|
+
{ attribute: 'domains', condition: 'contains', value: query }
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
if (!res.data?.length) return `No companies matching "${query}".`;
|
|
180
|
+
return res.data.map(formatCompany).join('\n');
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'attio_create_company',
|
|
185
|
+
description: 'Create a new company in Attio.',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
name: { type: 'string', description: 'Company name' },
|
|
190
|
+
domain: { type: 'string', description: 'Website domain (optional)' }
|
|
191
|
+
},
|
|
192
|
+
required: ['name']
|
|
193
|
+
},
|
|
194
|
+
handler: async ({ name, domain }) => {
|
|
195
|
+
const values = { name: [{ value: name }] };
|
|
196
|
+
if (domain) values.domains = [{ domain }];
|
|
197
|
+
|
|
198
|
+
const res = await attio('POST', '/objects/companies/records', { data: { values } });
|
|
199
|
+
return `✅ Created ${name} [id: ${res.data.id.record_id}]`;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'attio_list_deals',
|
|
204
|
+
description: 'List deals in Attio CRM.',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
limit: { type: 'number', description: 'Max results (default 20)' }
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
handler: async ({ limit = 20 }) => {
|
|
212
|
+
const res = await attio('POST', '/objects/deals/records/query', { limit });
|
|
213
|
+
if (!res.data?.length) return 'No deals found.';
|
|
214
|
+
return res.data.map(formatDeal).join('\n');
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'attio_update_record',
|
|
219
|
+
description: 'Update fields on a person, company, or deal in Attio.',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
objectType: { type: 'string', description: 'Object type: "people", "companies", or "deals"' },
|
|
224
|
+
recordId: { type: 'string', description: 'The record ID to update' },
|
|
225
|
+
values: { type: 'object', description: 'Field values to update, e.g. {"job_title": [{"value": "CTO"}]}' }
|
|
226
|
+
},
|
|
227
|
+
required: ['objectType', 'recordId', 'values']
|
|
228
|
+
},
|
|
229
|
+
handler: async ({ objectType, recordId, values }) => {
|
|
230
|
+
const res = await attio('PATCH', `/objects/${objectType}/records/${recordId}`, { data: { values } });
|
|
231
|
+
return `✅ Updated ${objectType} record ${recordId}`;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'attio_move_deal',
|
|
236
|
+
description: 'Move a deal to a different pipeline stage in Attio.',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
recordId: { type: 'string', description: 'The deal record ID' },
|
|
241
|
+
stage: { type: 'string', description: 'The stage name to move to (e.g. "Qualified", "Proposal", "Closed Won")' }
|
|
242
|
+
},
|
|
243
|
+
required: ['recordId', 'stage']
|
|
244
|
+
},
|
|
245
|
+
handler: async ({ recordId, stage }) => {
|
|
246
|
+
const res = await attio('PATCH', `/objects/deals/records/${recordId}`, {
|
|
247
|
+
data: { values: { stage: [{ status: { title: stage } }] } }
|
|
248
|
+
});
|
|
249
|
+
return `✅ Deal moved to "${stage}"`;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'attio_create_task',
|
|
254
|
+
description: 'Create a task in Attio, optionally linked to a record.',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {
|
|
258
|
+
content: { type: 'string', description: 'Task description' },
|
|
259
|
+
deadlineAt: { type: 'string', description: 'Deadline as ISO date string (optional)' },
|
|
260
|
+
linkedRecordId: { type: 'string', description: 'Record ID to link the task to (optional)' },
|
|
261
|
+
linkedObjectType: { type: 'string', description: 'Object type of linked record: "people", "companies", or "deals" (optional)' }
|
|
262
|
+
},
|
|
263
|
+
required: ['content']
|
|
264
|
+
},
|
|
265
|
+
handler: async ({ content, deadlineAt, linkedRecordId, linkedObjectType }) => {
|
|
266
|
+
const body = {
|
|
267
|
+
data: {
|
|
268
|
+
content: [{ type: 'paragraph', children: [{ text: content }] }],
|
|
269
|
+
format: 'plaintext',
|
|
270
|
+
is_completed: false
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
if (deadlineAt) body.data.deadline_at = deadlineAt;
|
|
274
|
+
if (linkedRecordId && linkedObjectType) {
|
|
275
|
+
body.data.linked_records = [{ target_object: linkedObjectType, target_record_id: linkedRecordId }];
|
|
276
|
+
}
|
|
277
|
+
const res = await attio('POST', '/tasks', body);
|
|
278
|
+
return `✅ Task created: "${content}"${deadlineAt ? ` (due: ${deadlineAt})` : ''}`;
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'attio_list_tasks',
|
|
283
|
+
description: 'List tasks in Attio.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
limit: { type: 'number', description: 'Max results (default 20)' }
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
handler: async ({ limit = 20 }) => {
|
|
291
|
+
const res = await attio('GET', `/tasks?limit=${limit}`);
|
|
292
|
+
if (!res.data?.length) return 'No tasks found.';
|
|
293
|
+
return res.data.map(t => {
|
|
294
|
+
const status = t.is_completed ? '✅' : '⬜';
|
|
295
|
+
const content = t.content_plaintext || 'No description';
|
|
296
|
+
const deadline = t.deadline_at ? ` (due: ${new Date(t.deadline_at).toLocaleDateString()})` : '';
|
|
297
|
+
return `${status} ${content}${deadline} [id: ${t.id.task_id}]`;
|
|
298
|
+
}).join('\n');
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'attio_complete_task',
|
|
303
|
+
description: 'Mark a task as completed in Attio.',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
taskId: { type: 'string', description: 'The task ID to complete' }
|
|
308
|
+
},
|
|
309
|
+
required: ['taskId']
|
|
310
|
+
},
|
|
311
|
+
handler: async ({ taskId }) => {
|
|
312
|
+
await attio('PATCH', `/tasks/${taskId}`, { data: { is_completed: true } });
|
|
313
|
+
return `✅ Task marked as completed`;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'attio_add_note',
|
|
318
|
+
description: 'Add a note to a person or company in Attio.',
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: {
|
|
322
|
+
objectType: { type: 'string', description: 'Object type: "people" or "companies"' },
|
|
323
|
+
recordId: { type: 'string', description: 'The record ID' },
|
|
324
|
+
title: { type: 'string', description: 'Note title' },
|
|
325
|
+
content: { type: 'string', description: 'Note content' }
|
|
326
|
+
},
|
|
327
|
+
required: ['objectType', 'recordId', 'title', 'content']
|
|
328
|
+
},
|
|
329
|
+
handler: async ({ objectType, recordId, title, content }) => {
|
|
330
|
+
const res = await attio('POST', '/notes', {
|
|
331
|
+
data: {
|
|
332
|
+
title,
|
|
333
|
+
content: [{ type: 'paragraph', children: [{ text: content }] }],
|
|
334
|
+
parent_object: objectType === 'people' ? 'people' : 'companies',
|
|
335
|
+
parent_record_id: recordId
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return `✅ Note added: "${title}"`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@antidrift/mcp-attio",
|
|
3
|
+
"version": "0.7.3",
|
|
4
|
+
"description": "Attio CRM connector for antidrift — people, companies, deals, notes",
|
|
5
|
+
"main": "server.mjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"antidrift-mcp-attio": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.mjs",
|
|
11
|
+
"connectors/",
|
|
12
|
+
"bin/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"antidrift",
|
|
16
|
+
"mcp",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"attio",
|
|
19
|
+
"crm"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/probeo-io/antidrift.git",
|
|
26
|
+
"directory": "packages/mcp-attio"
|
|
27
|
+
},
|
|
28
|
+
"author": "Probeo <hello@probeo.io>"
|
|
29
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import { tools as attioTools } from './connectors/attio.mjs';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
|
|
9
|
+
const allTools = [];
|
|
10
|
+
|
|
11
|
+
if (existsSync(join(homedir(), '.antidrift', 'attio.json'))) {
|
|
12
|
+
allTools.push(...attioTools);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
16
|
+
|
|
17
|
+
function send(msg) {
|
|
18
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
rl.on('line', async (line) => {
|
|
22
|
+
let req;
|
|
23
|
+
try { req = JSON.parse(line); } catch { return; }
|
|
24
|
+
|
|
25
|
+
const { id, method, params } = req;
|
|
26
|
+
|
|
27
|
+
if (method === 'initialize') {
|
|
28
|
+
send({
|
|
29
|
+
jsonrpc: '2.0', id,
|
|
30
|
+
result: {
|
|
31
|
+
protocolVersion: '2024-11-05',
|
|
32
|
+
capabilities: { tools: {} },
|
|
33
|
+
serverInfo: { name: 'antidrift-attio', version: '0.1.0' }
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
} else if (method === 'notifications/initialized') {
|
|
37
|
+
// no response needed
|
|
38
|
+
} else if (method === 'tools/list') {
|
|
39
|
+
send({
|
|
40
|
+
jsonrpc: '2.0', id,
|
|
41
|
+
result: {
|
|
42
|
+
tools: allTools.map(t => ({
|
|
43
|
+
name: t.name,
|
|
44
|
+
description: t.description,
|
|
45
|
+
inputSchema: t.inputSchema
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} else if (method === 'tools/call') {
|
|
50
|
+
const tool = allTools.find(t => t.name === params.name);
|
|
51
|
+
if (!tool) {
|
|
52
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${params.name}` } });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await tool.handler(params.arguments || {});
|
|
58
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true } });
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${method}` } });
|
|
64
|
+
}
|
|
65
|
+
});
|