delegate-sf-mcp 0.2.0
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/.eslintrc.json +20 -0
- package/LICENSE +24 -0
- package/README.md +76 -0
- package/auth.js +148 -0
- package/bin/config-helper.js +51 -0
- package/bin/mcp-salesforce.js +12 -0
- package/bin/setup.js +266 -0
- package/bin/status.js +134 -0
- package/docs/README.md +52 -0
- package/docs/step1.png +0 -0
- package/docs/step2.png +0 -0
- package/docs/step3.png +0 -0
- package/docs/step4.png +0 -0
- package/examples/README.md +35 -0
- package/package.json +16 -0
- package/scripts/README.md +30 -0
- package/src/auth/file-storage.js +447 -0
- package/src/auth/oauth.js +417 -0
- package/src/auth/token-manager.js +207 -0
- package/src/backup/manager.js +949 -0
- package/src/index.js +168 -0
- package/src/salesforce/client.js +388 -0
- package/src/sf-client.js +79 -0
- package/src/tools/auth.js +190 -0
- package/src/tools/backup.js +486 -0
- package/src/tools/create.js +109 -0
- package/src/tools/delegate-hygiene.js +268 -0
- package/src/tools/delegate-validate.js +212 -0
- package/src/tools/delegate-verify.js +143 -0
- package/src/tools/delete.js +72 -0
- package/src/tools/describe.js +132 -0
- package/src/tools/installation-info.js +656 -0
- package/src/tools/learn-context.js +1077 -0
- package/src/tools/learn.js +351 -0
- package/src/tools/query.js +82 -0
- package/src/tools/repair-credentials.js +77 -0
- package/src/tools/setup.js +120 -0
- package/src/tools/time_machine.js +347 -0
- package/src/tools/update.js +138 -0
- package/src/tools.js +214 -0
- package/src/utils/cache.js +120 -0
- package/src/utils/debug.js +52 -0
- package/src/utils/logger.js +19 -0
- package/tokens.json +8 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify_record_exists
|
|
3
|
+
* Fuzzy match before every create suggestion.
|
|
4
|
+
* Priority: email → phone → name + company
|
|
5
|
+
* Returns potential duplicates with confidence score.
|
|
6
|
+
* Non-negotiable — runs before EVERY create. Zero exceptions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const verifyRecordExistsTool = {
|
|
10
|
+
name: 'delegate_verify_record_exists',
|
|
11
|
+
description:
|
|
12
|
+
'Delegate: Check for duplicate records before creating a new Contact, Lead, or Account. ' +
|
|
13
|
+
'Runs fuzzy matching — email first (confidence 95), then phone (80), then name + company (70). ' +
|
|
14
|
+
'Always call this before suggesting salesforce_create for person or account records.',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
objectType: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
enum: ['Contact', 'Lead', 'Account'],
|
|
21
|
+
description: 'Salesforce object type to check for duplicates',
|
|
22
|
+
},
|
|
23
|
+
email: { type: 'string', description: 'Email address — highest confidence match key' },
|
|
24
|
+
phone: { type: 'string', description: 'Phone number — secondary match key' },
|
|
25
|
+
name: { type: 'string', description: 'Full name or account name — tertiary match key' },
|
|
26
|
+
company: { type: 'string', description: 'Company name — used with name for person records' },
|
|
27
|
+
},
|
|
28
|
+
required: ['objectType'],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export async function executeVerifyRecordExists(client, args) {
|
|
33
|
+
const { objectType, email, phone, name, company } = args;
|
|
34
|
+
|
|
35
|
+
if (!email && !phone && !name) {
|
|
36
|
+
return result(false, [], 'Provide at least one of: email, phone, or name');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const matches = [];
|
|
41
|
+
|
|
42
|
+
// ── Pass 1: email (confidence 95) ─────────────────────────────────────────
|
|
43
|
+
if (email) {
|
|
44
|
+
const emailField = objectType === 'Account' ? null : 'Email';
|
|
45
|
+
if (emailField) {
|
|
46
|
+
const emailMatches = await client.query(
|
|
47
|
+
`SELECT Id, Name, ${emailField}, ${objectType === 'Lead' ? 'Company' : 'Account.Name'}
|
|
48
|
+
FROM ${objectType}
|
|
49
|
+
WHERE ${emailField} = '${email.replace(/'/g, "\\'")}'
|
|
50
|
+
LIMIT 5`
|
|
51
|
+
);
|
|
52
|
+
for (const r of emailMatches.records) {
|
|
53
|
+
matches.push({ ...r, _confidence: 95, _matchedOn: 'email' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Pass 2: phone (confidence 80) ─────────────────────────────────────────
|
|
59
|
+
if (phone && matches.length === 0) {
|
|
60
|
+
const cleanPhone = phone.replace(/\D/g, '');
|
|
61
|
+
const phoneField = objectType === 'Account' ? 'Phone' : 'Phone';
|
|
62
|
+
const phoneMatches = await client.query(
|
|
63
|
+
`SELECT Id, Name, ${phoneField}${objectType !== 'Account' ? ', Email' : ''}
|
|
64
|
+
FROM ${objectType}
|
|
65
|
+
WHERE ${phoneField} LIKE '%${cleanPhone.slice(-7)}%'
|
|
66
|
+
LIMIT 5`
|
|
67
|
+
);
|
|
68
|
+
for (const r of phoneMatches.records) {
|
|
69
|
+
if (!matches.find(m => m.Id === r.Id)) {
|
|
70
|
+
matches.push({ ...r, _confidence: 80, _matchedOn: 'phone' });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Pass 3: name + company (confidence 70) ────────────────────────────────
|
|
76
|
+
if (name && matches.length === 0) {
|
|
77
|
+
const nameClean = name.replace(/'/g, "\\'");
|
|
78
|
+
let soql;
|
|
79
|
+
|
|
80
|
+
if (objectType === 'Account') {
|
|
81
|
+
soql = `SELECT Id, Name, Phone, Website FROM Account
|
|
82
|
+
WHERE Name LIKE '%${nameClean}%' LIMIT 5`;
|
|
83
|
+
} else {
|
|
84
|
+
const companyFilter =
|
|
85
|
+
company
|
|
86
|
+
? ` AND ${objectType === 'Lead' ? 'Company' : 'Account.Name'} LIKE '%${company.replace(/'/g, "\\'")}%'`
|
|
87
|
+
: '';
|
|
88
|
+
soql = `SELECT Id, Name, Email, Phone${objectType === 'Contact' ? ', Account.Name' : ', Company'}
|
|
89
|
+
FROM ${objectType}
|
|
90
|
+
WHERE Name LIKE '%${nameClean}%'${companyFilter}
|
|
91
|
+
LIMIT 5`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const nameMatches = await client.query(soql);
|
|
95
|
+
for (const r of nameMatches.records) {
|
|
96
|
+
if (!matches.find(m => m.Id === r.Id)) {
|
|
97
|
+
matches.push({ ...r, _confidence: 70, _matchedOn: 'name' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (matches.length === 0) {
|
|
103
|
+
return result(true, [], null, 'No duplicates found — safe to create');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const formatted = matches.map(m => ({
|
|
107
|
+
id: m.Id,
|
|
108
|
+
name: m.Name,
|
|
109
|
+
confidence: m._confidence,
|
|
110
|
+
matchedOn: m._matchedOn,
|
|
111
|
+
email: m.Email || null,
|
|
112
|
+
phone: m.Phone || null,
|
|
113
|
+
company: m['Account.Name'] || m.Company || null,
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
return result(true, formatted, null,
|
|
117
|
+
`Found ${matches.length} potential duplicate(s). Review before creating.`
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return result(false, [], `Query failed: ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function result(success, matches, error, message) {
|
|
126
|
+
const hasDuplicates = matches.length > 0;
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: 'text',
|
|
131
|
+
text: error
|
|
132
|
+
? `❌ verify_record_exists error: ${error}`
|
|
133
|
+
: hasDuplicates
|
|
134
|
+
? `⚠️ **Potential duplicates found** — ${message}\n\n${JSON.stringify(matches, null, 2)}\n\n` +
|
|
135
|
+
`Confirm with the user: "We found an existing record. Is this the same person?" ` +
|
|
136
|
+
`If yes → update existing. If no → create new and note the distinction.`
|
|
137
|
+
: `✅ ${message} — proceed with create`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
_delegateResult: { success, hasDuplicates, matches },
|
|
141
|
+
isError: !success,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const deleteTool = {
|
|
2
|
+
name: "salesforce_delete",
|
|
3
|
+
description: "Delete a record from any Salesforce object. This action is permanent and cannot be undone.",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
sobject: {
|
|
8
|
+
type: "string",
|
|
9
|
+
description: "SObject API name (e.g., 'Contact', 'Account', 'CustomObject__c')"
|
|
10
|
+
},
|
|
11
|
+
id: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Salesforce record ID to delete (15 or 18 character ID)"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
required: ["sobject", "id"]
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function executeDelete(client, args) {
|
|
21
|
+
try {
|
|
22
|
+
const { sobject, id } = args;
|
|
23
|
+
|
|
24
|
+
if (!sobject || typeof sobject !== 'string') {
|
|
25
|
+
throw new Error('sobject parameter is required and must be a string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!id || typeof id !== 'string') {
|
|
29
|
+
throw new Error('id parameter is required and must be a string');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate ID format (basic check)
|
|
33
|
+
if (id.length !== 15 && id.length !== 18) {
|
|
34
|
+
throw new Error('Invalid Salesforce ID format. ID must be 15 or 18 characters long.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await client.delete(sobject, id);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `✅ Successfully deleted ${sobject} record!\n\n` +
|
|
44
|
+
`Deleted Record ID: ${result.id}\n` +
|
|
45
|
+
`Object Type: ${result.sobject}\n\n` +
|
|
46
|
+
`⚠️ This action is permanent and cannot be undone.\n` +
|
|
47
|
+
`The record has been moved to the Recycle Bin and will be permanently deleted after 15 days.`
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: `❌ Failed to delete ${args.sobject || 'record'}: ${error.message}\n\n` +
|
|
57
|
+
`Common issues:\n` +
|
|
58
|
+
`- Record not found (check the ID)\n` +
|
|
59
|
+
`- Record cannot be deleted (may have dependencies)\n` +
|
|
60
|
+
`- Insufficient permissions\n` +
|
|
61
|
+
`- Record is referenced by other records\n` +
|
|
62
|
+
`- Object deletion is not allowed\n\n` +
|
|
63
|
+
`Record ID provided: ${args.id}\n` +
|
|
64
|
+
`Object Type: ${args.sobject}\n\n` +
|
|
65
|
+
`Tip: Some records cannot be deleted if they are referenced by other records. ` +
|
|
66
|
+
`You may need to delete dependent records first.`
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
isError: true
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export const describeTool = {
|
|
2
|
+
name: "salesforce_describe",
|
|
3
|
+
description: "Get detailed schema information for any Salesforce object, including all fields, data types, and permissions.",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
sobject: {
|
|
8
|
+
type: "string",
|
|
9
|
+
description: "SObject API name to describe (e.g., 'Contact', 'Account', 'CustomObject__c'). Leave empty to get list of all available objects."
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
required: []
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function executeDescribe(client, args) {
|
|
17
|
+
try {
|
|
18
|
+
const { sobject } = args;
|
|
19
|
+
|
|
20
|
+
// If no sobject specified, return list of all available objects
|
|
21
|
+
if (!sobject) {
|
|
22
|
+
const sobjects = await client.describeGlobal();
|
|
23
|
+
|
|
24
|
+
const standardObjects = sobjects.filter(obj => !obj.custom).slice(0, 20);
|
|
25
|
+
const customObjects = sobjects.filter(obj => obj.custom);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `📊 Available Salesforce Objects (${sobjects.length} total)\n\n` +
|
|
32
|
+
`Standard Objects (showing first 20):\n` +
|
|
33
|
+
standardObjects.map(obj =>
|
|
34
|
+
`• ${obj.name} (${obj.label}) - Create: ${obj.createable}, Update: ${obj.updateable}, Delete: ${obj.deletable}`
|
|
35
|
+
).join('\n') +
|
|
36
|
+
(sobjects.filter(obj => !obj.custom).length > 20 ? '\n... and more standard objects\n' : '\n') +
|
|
37
|
+
`\nCustom Objects:\n` +
|
|
38
|
+
(customObjects.length > 0
|
|
39
|
+
? customObjects.map(obj =>
|
|
40
|
+
`• ${obj.name} (${obj.label}) - Create: ${obj.createable}, Update: ${obj.updateable}, Delete: ${obj.deletable}`
|
|
41
|
+
).join('\n')
|
|
42
|
+
: 'No custom objects found'
|
|
43
|
+
) +
|
|
44
|
+
`\n\nTo get detailed information about a specific object, use:\n` +
|
|
45
|
+
`salesforce_describe with sobject parameter (e.g., "Contact", "Account")`
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof sobject !== 'string') {
|
|
52
|
+
throw new Error('sobject parameter must be a string');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const schema = await client.describe(sobject);
|
|
56
|
+
|
|
57
|
+
// Categorize fields
|
|
58
|
+
const requiredFields = schema.fields.filter(f => f.required);
|
|
59
|
+
const createableFields = schema.fields.filter(f => f.createable);
|
|
60
|
+
const updateableFields = schema.fields.filter(f => f.updateable);
|
|
61
|
+
const picklistFields = schema.fields.filter(f => f.type === 'picklist' && f.picklistValues.length > 0);
|
|
62
|
+
const referenceFields = schema.fields.filter(f => f.type === 'reference' && f.referenceTo.length > 0);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: `📋 ${schema.label} (${schema.name}) Schema Information\n\n` +
|
|
69
|
+
`Object Details:\n` +
|
|
70
|
+
`• Label: ${schema.label} (Plural: ${schema.labelPlural})\n` +
|
|
71
|
+
`• API Name: ${schema.name}\n` +
|
|
72
|
+
`• Key Prefix: ${schema.keyPrefix || 'N/A'}\n` +
|
|
73
|
+
`• Permissions: Create: ${schema.createable}, Update: ${schema.updateable}, Delete: ${schema.deletable}, Query: ${schema.queryable}\n` +
|
|
74
|
+
`• Total Fields: ${schema.fields.length}\n\n` +
|
|
75
|
+
|
|
76
|
+
`Required Fields (${requiredFields.length}):\n` +
|
|
77
|
+
(requiredFields.length > 0
|
|
78
|
+
? requiredFields.map(f => `• ${f.name} (${f.label}) - ${f.type}`).join('\n')
|
|
79
|
+
: 'None'
|
|
80
|
+
) + '\n\n' +
|
|
81
|
+
|
|
82
|
+
`Key Fields for Operations:\n` +
|
|
83
|
+
`Createable Fields: ${createableFields.length}\n` +
|
|
84
|
+
`Updateable Fields: ${updateableFields.length}\n\n` +
|
|
85
|
+
|
|
86
|
+
(picklistFields.length > 0
|
|
87
|
+
? `Picklist Fields:\n` +
|
|
88
|
+
picklistFields.map(f =>
|
|
89
|
+
`• ${f.name} (${f.label}): ${f.picklistValues.map(p => p.value).join(', ')}`
|
|
90
|
+
).join('\n') + '\n\n'
|
|
91
|
+
: ''
|
|
92
|
+
) +
|
|
93
|
+
|
|
94
|
+
(referenceFields.length > 0
|
|
95
|
+
? `Reference Fields (Lookups):\n` +
|
|
96
|
+
referenceFields.map(f =>
|
|
97
|
+
`• ${f.name} (${f.label}) → ${f.referenceTo.join(', ')}`
|
|
98
|
+
).join('\n') + '\n\n'
|
|
99
|
+
: ''
|
|
100
|
+
) +
|
|
101
|
+
|
|
102
|
+
`Common Fields for SOQL:\n` +
|
|
103
|
+
schema.fields
|
|
104
|
+
.filter(f => ['Id', 'Name', 'CreatedDate', 'LastModifiedDate'].includes(f.name) ||
|
|
105
|
+
f.name.includes('Email') || f.name.includes('Phone'))
|
|
106
|
+
.map(f => `• ${f.name} (${f.label}) - ${f.type}`)
|
|
107
|
+
.join('\n') +
|
|
108
|
+
|
|
109
|
+
`\n\nExample SOQL Query:\n` +
|
|
110
|
+
`SELECT Id, ${schema.fields.filter(f => f.name !== 'Id').slice(0, 5).map(f => f.name).join(', ')} FROM ${schema.name} LIMIT 10`
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: `❌ Failed to describe ${args.sobject || 'objects'}: ${error.message}\n\n` +
|
|
120
|
+
`Common issues:\n` +
|
|
121
|
+
`- Object name not found (check spelling and API name)\n` +
|
|
122
|
+
`- Insufficient permissions to access object metadata\n` +
|
|
123
|
+
`- Object may not be queryable\n\n` +
|
|
124
|
+
`Tip: Object API names are case-sensitive. Use exact API names like:\n` +
|
|
125
|
+
`- Standard objects: "Account", "Contact", "Opportunity"\n` +
|
|
126
|
+
`- Custom objects: end with "__c" like "CustomObject__c"`
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
isError: true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|