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
package/src/index.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.js — Delegate SF MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Start: node src/index.js
|
|
5
|
+
* Requires tokens.json (run node auth.js first)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
handleQuery,
|
|
14
|
+
handleGetRecord,
|
|
15
|
+
handleSearch,
|
|
16
|
+
handleDescribe,
|
|
17
|
+
handleCreate,
|
|
18
|
+
handleUpdate,
|
|
19
|
+
handleVerifyRecordExists,
|
|
20
|
+
handleGetHygieneScore,
|
|
21
|
+
} from './tools.js';
|
|
22
|
+
|
|
23
|
+
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const TOOLS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'sf_query',
|
|
28
|
+
description: 'Run a SOQL query against Salesforce. Use for reading records with filters. Example: SELECT Id, Name, StageName FROM Opportunity WHERE StageName = \'Prospecting\'',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: { soql: { type: 'string', description: 'The SOQL query to execute' } },
|
|
32
|
+
required: ['soql'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'sf_get_record',
|
|
37
|
+
description: 'Get a single Salesforce record by ID with specific fields.',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
object_type: { type: 'string', description: 'Salesforce object API name (e.g. Opportunity, Contact, Account)' },
|
|
42
|
+
record_id: { type: 'string', description: '18-character Salesforce record ID' },
|
|
43
|
+
fields: { type: 'array', items: { type: 'string' }, description: 'Fields to return. Defaults to Id, Name.' },
|
|
44
|
+
},
|
|
45
|
+
required: ['object_type', 'record_id'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'sf_search',
|
|
50
|
+
description: 'Search across Salesforce objects using SOSL. Use when you have a name or keyword but not a specific ID.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
search_term: { type: 'string', description: 'Text to search for' },
|
|
55
|
+
object_types: { type: 'array', items: { type: 'string' }, description: 'Objects to search. Defaults to Contact, Account, Lead, Opportunity.' },
|
|
56
|
+
fields: { type: 'array', items: { type: 'string' }, description: 'Fields to return per object. Defaults to Id, Name.' },
|
|
57
|
+
},
|
|
58
|
+
required: ['search_term'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'sf_describe',
|
|
63
|
+
description: 'Get field metadata for a Salesforce object — field names, types, required fields, picklist values. Use before creating or updating records to know what fields exist.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: { object_type: { type: 'string', description: 'Salesforce object API name' } },
|
|
67
|
+
required: ['object_type'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'sf_create',
|
|
72
|
+
description: 'Create a new record in Salesforce. Always run sf_verify_exists first for Contact/Lead/Account to prevent duplicates.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
object_type: { type: 'string', description: 'Salesforce object API name' },
|
|
77
|
+
fields: { type: 'object', description: 'Key-value pairs of field API names and values' },
|
|
78
|
+
},
|
|
79
|
+
required: ['object_type', 'fields'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'sf_update',
|
|
84
|
+
description: 'Update an existing Salesforce record. Requires the record ID.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
object_type: { type: 'string', description: 'Salesforce object API name' },
|
|
89
|
+
record_id: { type: 'string', description: '18-character Salesforce record ID' },
|
|
90
|
+
fields: { type: 'object', description: 'Key-value pairs of fields to update' },
|
|
91
|
+
},
|
|
92
|
+
required: ['object_type', 'record_id', 'fields'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'sf_verify_exists',
|
|
97
|
+
description: 'Check if a Contact or Lead already exists before creating one. Prevents duplicates. Matches by email first, then phone, then name+company. Always use before sf_create for people records.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
object_type: { type: 'string', description: 'Contact, Lead, or Account' },
|
|
102
|
+
name: { type: 'string', description: 'Full name to check' },
|
|
103
|
+
email: { type: 'string', description: 'Email address (most reliable match)' },
|
|
104
|
+
phone: { type: 'string', description: 'Phone number' },
|
|
105
|
+
company: { type: 'string', description: 'Company name (used with name match)' },
|
|
106
|
+
},
|
|
107
|
+
required: ['object_type'],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'sf_hygiene_score',
|
|
112
|
+
description: 'Get a hygiene score (0-100) for a Salesforce Account. Checks field completeness, activity recency, stage accuracy, contact coverage, and next step presence. Returns score, grade, strengths, and issues.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: { account_id: { type: 'string', description: '18-character Salesforce Account ID' } },
|
|
116
|
+
required: ['account_id'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// ── Handler map ───────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
const HANDLERS = {
|
|
124
|
+
sf_query: handleQuery,
|
|
125
|
+
sf_get_record: handleGetRecord,
|
|
126
|
+
sf_search: handleSearch,
|
|
127
|
+
sf_describe: handleDescribe,
|
|
128
|
+
sf_create: handleCreate,
|
|
129
|
+
sf_update: handleUpdate,
|
|
130
|
+
sf_verify_exists: handleVerifyRecordExists,
|
|
131
|
+
sf_hygiene_score: handleGetHygieneScore,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ── Server ────────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
const server = new Server(
|
|
137
|
+
{ name: 'delegate-sf-mcp', version: '0.2.0' },
|
|
138
|
+
{ capabilities: { tools: {} } }
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
142
|
+
|
|
143
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
144
|
+
const { name, arguments: args } = req.params;
|
|
145
|
+
|
|
146
|
+
const handler = HANDLERS[name];
|
|
147
|
+
if (!handler) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const text = await handler(args ?? {});
|
|
156
|
+
return { content: [{ type: 'text', text }] };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: 'text', text: `❌ ${err.message}` }],
|
|
160
|
+
isError: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const transport = new StdioServerTransport();
|
|
168
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import jsforce from 'jsforce';
|
|
2
|
+
import { TokenManager } from '../auth/token-manager.js';
|
|
3
|
+
import { FileStorageManager } from '../auth/file-storage.js';
|
|
4
|
+
|
|
5
|
+
export class SalesforceClient {
|
|
6
|
+
constructor(clientId, clientSecret, instanceUrl) {
|
|
7
|
+
this.clientId = clientId;
|
|
8
|
+
this.clientSecret = clientSecret;
|
|
9
|
+
this.instanceUrl = instanceUrl;
|
|
10
|
+
this.tokenManager = new TokenManager(clientId, clientSecret, instanceUrl);
|
|
11
|
+
this.connection = null;
|
|
12
|
+
this.initialized = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the Salesforce client
|
|
17
|
+
*/
|
|
18
|
+
async initialize() {
|
|
19
|
+
if (this.initialized) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Initialize token manager
|
|
25
|
+
const hasTokens = await this.tokenManager.initialize();
|
|
26
|
+
if (!hasTokens) {
|
|
27
|
+
throw new Error('Authentication required - no valid tokens found. Use the salesforce_auth tool to authenticate with Salesforce.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create jsforce connection
|
|
31
|
+
await this.createConnection();
|
|
32
|
+
|
|
33
|
+
this.initialized = true;
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create and configure jsforce connection
|
|
42
|
+
*/
|
|
43
|
+
async createConnection() {
|
|
44
|
+
try {
|
|
45
|
+
const accessToken = await this.tokenManager.getValidAccessToken();
|
|
46
|
+
const tokenInfo = this.tokenManager.getTokenInfo();
|
|
47
|
+
|
|
48
|
+
// Get API version from config file
|
|
49
|
+
const fileStorage = new FileStorageManager();
|
|
50
|
+
const apiConfig = await fileStorage.getApiConfig();
|
|
51
|
+
|
|
52
|
+
this.connection = new jsforce.Connection({
|
|
53
|
+
instanceUrl: tokenInfo.instance_url,
|
|
54
|
+
accessToken: accessToken,
|
|
55
|
+
version: apiConfig.apiVersion
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Test connection
|
|
59
|
+
await this.connection.identity();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new Error(`Failed to create Salesforce connection: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ensure connection is valid, refresh if needed
|
|
67
|
+
*/
|
|
68
|
+
async ensureValidConnection() {
|
|
69
|
+
if (!this.initialized) {
|
|
70
|
+
await this.initialize();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Check if token needs refresh
|
|
75
|
+
if (await this.tokenManager.needsRefresh()) {
|
|
76
|
+
await this.tokenManager.refreshTokens();
|
|
77
|
+
await this.createConnection();
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execute SOQL query
|
|
86
|
+
*/
|
|
87
|
+
async query(soql, options = {}) {
|
|
88
|
+
await this.ensureValidConnection();
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await this.connection.query(soql, options);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
totalSize: result.totalSize,
|
|
95
|
+
done: result.done,
|
|
96
|
+
records: result.records,
|
|
97
|
+
nextRecordsUrl: result.nextRecordsUrl
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Handle authentication errors first
|
|
101
|
+
if (this.isAuthenticationError(error)) {
|
|
102
|
+
throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle specific Salesforce errors
|
|
106
|
+
if (error.name === 'INVALID_QUERY' || error.errorCode === 'INVALID_QUERY') {
|
|
107
|
+
throw new Error(`Invalid SOQL query: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (error.name === 'INVALID_FIELD' || error.errorCode === 'INVALID_FIELD') {
|
|
111
|
+
throw new Error(`Invalid field in query: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (error.name === 'INVALID_TYPE' || error.errorCode === 'INVALID_TYPE') {
|
|
115
|
+
throw new Error(`Invalid object type: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle authentication errors
|
|
119
|
+
if (error.message.includes('Session expired') || error.message.includes('INVALID_SESSION_ID')) {
|
|
120
|
+
await this.tokenManager.refreshTokens();
|
|
121
|
+
await this.createConnection();
|
|
122
|
+
// Retry once after token refresh
|
|
123
|
+
try {
|
|
124
|
+
const retryResult = await this.connection.query(soql, options);
|
|
125
|
+
return {
|
|
126
|
+
totalSize: retryResult.totalSize,
|
|
127
|
+
done: retryResult.done,
|
|
128
|
+
records: retryResult.records,
|
|
129
|
+
nextRecordsUrl: retryResult.nextRecordsUrl
|
|
130
|
+
};
|
|
131
|
+
} catch (retryError) {
|
|
132
|
+
throw new Error(`Query failed after token refresh: ${retryError.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Generic error handling with better message
|
|
137
|
+
const errorMessage = error.message || 'Unknown Salesforce error';
|
|
138
|
+
throw new Error(`Salesforce query error: ${this.formatSalesforceError(error)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a new record
|
|
144
|
+
*/
|
|
145
|
+
async create(sobject, data) {
|
|
146
|
+
await this.ensureValidConnection();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await this.connection.sobject(sobject).create(data);
|
|
150
|
+
|
|
151
|
+
if (result.success) {
|
|
152
|
+
return {
|
|
153
|
+
id: result.id,
|
|
154
|
+
success: true,
|
|
155
|
+
sobject: sobject,
|
|
156
|
+
data: data
|
|
157
|
+
};
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error(`Create failed: ${JSON.stringify(result.errors)}`);
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (this.isAuthenticationError(error)) {
|
|
163
|
+
throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.');
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Create ${sobject} failed: ${this.formatSalesforceError(error)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update an existing record
|
|
171
|
+
*/
|
|
172
|
+
async update(sobject, id, data) {
|
|
173
|
+
await this.ensureValidConnection();
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const result = await this.connection.sobject(sobject).update({
|
|
177
|
+
Id: id,
|
|
178
|
+
...data
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (result.success) {
|
|
182
|
+
return {
|
|
183
|
+
id: result.id,
|
|
184
|
+
success: true,
|
|
185
|
+
sobject: sobject,
|
|
186
|
+
data: data
|
|
187
|
+
};
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error(`Update failed: ${JSON.stringify(result.errors)}`);
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (this.isAuthenticationError(error)) {
|
|
193
|
+
throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.');
|
|
194
|
+
}
|
|
195
|
+
throw new Error(`Update ${sobject} failed: ${this.formatSalesforceError(error)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Delete a record
|
|
201
|
+
*/
|
|
202
|
+
async delete(sobject, id) {
|
|
203
|
+
await this.ensureValidConnection();
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const result = await this.connection.sobject(sobject).destroy(id);
|
|
207
|
+
|
|
208
|
+
if (result.success) {
|
|
209
|
+
return {
|
|
210
|
+
id: result.id,
|
|
211
|
+
success: true,
|
|
212
|
+
sobject: sobject
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
throw new Error(`Delete failed: ${JSON.stringify(result.errors)}`);
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (this.isAuthenticationError(error)) {
|
|
219
|
+
throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.');
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`Delete ${sobject} failed: ${this.formatSalesforceError(error)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Describe an SObject (get schema information)
|
|
227
|
+
*/
|
|
228
|
+
async describe(sobject) {
|
|
229
|
+
await this.ensureValidConnection();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const result = await this.connection.sobject(sobject).describe();
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
name: result.name,
|
|
236
|
+
label: result.label,
|
|
237
|
+
labelPlural: result.labelPlural,
|
|
238
|
+
keyPrefix: result.keyPrefix,
|
|
239
|
+
createable: result.createable,
|
|
240
|
+
updateable: result.updateable,
|
|
241
|
+
deletable: result.deletable,
|
|
242
|
+
queryable: result.queryable,
|
|
243
|
+
fields: result.fields.map(field => ({
|
|
244
|
+
name: field.name,
|
|
245
|
+
label: field.label,
|
|
246
|
+
type: field.type,
|
|
247
|
+
length: field.length,
|
|
248
|
+
required: !field.nillable && !field.defaultedOnCreate,
|
|
249
|
+
createable: field.createable,
|
|
250
|
+
updateable: field.updateable,
|
|
251
|
+
picklistValues: field.picklistValues || [],
|
|
252
|
+
referenceTo: field.referenceTo || [],
|
|
253
|
+
relationshipName: field.relationshipName
|
|
254
|
+
})),
|
|
255
|
+
recordTypeInfos: result.recordTypeInfos || []
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (this.isAuthenticationError(error)) {
|
|
259
|
+
throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.');
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`Describe ${sobject} failed: ${this.formatSalesforceError(error)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get all available SObjects
|
|
267
|
+
*/
|
|
268
|
+
async describeGlobal() {
|
|
269
|
+
await this.ensureValidConnection();
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const result = await this.connection.describeGlobal();
|
|
273
|
+
|
|
274
|
+
const sobjects = result.sobjects
|
|
275
|
+
.filter(sobject => sobject.queryable) // Only queryable objects
|
|
276
|
+
.map(sobject => ({
|
|
277
|
+
name: sobject.name,
|
|
278
|
+
label: sobject.label,
|
|
279
|
+
labelPlural: sobject.labelPlural,
|
|
280
|
+
keyPrefix: sobject.keyPrefix,
|
|
281
|
+
custom: sobject.custom,
|
|
282
|
+
createable: sobject.createable,
|
|
283
|
+
updateable: sobject.updateable,
|
|
284
|
+
deletable: sobject.deletable,
|
|
285
|
+
queryable: sobject.queryable
|
|
286
|
+
}))
|
|
287
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
288
|
+
|
|
289
|
+
return sobjects;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (this.isAuthenticationError(error)) {
|
|
292
|
+
throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.');
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`Global describe failed: ${this.formatSalesforceError(error)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get user information
|
|
300
|
+
*/
|
|
301
|
+
async getUserInfo() {
|
|
302
|
+
await this.ensureValidConnection();
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const identity = await this.connection.identity();
|
|
306
|
+
return {
|
|
307
|
+
id: identity.user_id,
|
|
308
|
+
username: identity.username,
|
|
309
|
+
display_name: identity.display_name,
|
|
310
|
+
email: identity.email,
|
|
311
|
+
organization_id: identity.organization_id,
|
|
312
|
+
urls: identity.urls
|
|
313
|
+
};
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw new Error(`Failed to get user info: ${this.formatSalesforceError(error)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Format Salesforce error messages for better readability
|
|
321
|
+
*/
|
|
322
|
+
formatSalesforceError(error) {
|
|
323
|
+
if (error.name === 'INVALID_FIELD') {
|
|
324
|
+
return `Invalid field: ${error.message}`;
|
|
325
|
+
}
|
|
326
|
+
if (error.name === 'REQUIRED_FIELD_MISSING') {
|
|
327
|
+
return `Required field missing: ${error.message}`;
|
|
328
|
+
}
|
|
329
|
+
if (error.name === 'FIELD_CUSTOM_VALIDATION_EXCEPTION') {
|
|
330
|
+
return `Validation rule failed: ${error.message}`;
|
|
331
|
+
}
|
|
332
|
+
if (error.name === 'INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY') {
|
|
333
|
+
return `Insufficient permissions: ${error.message}`;
|
|
334
|
+
}
|
|
335
|
+
if (error.name === 'INVALID_SESSION_ID') {
|
|
336
|
+
return 'Session expired - please re-authenticate';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return error.message || 'Unknown Salesforce error';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Check if an error is authentication-related
|
|
344
|
+
*/
|
|
345
|
+
isAuthenticationError(error) {
|
|
346
|
+
const authErrorIndicators = [
|
|
347
|
+
'INVALID_SESSION_ID',
|
|
348
|
+
'Session expired',
|
|
349
|
+
'invalid_grant',
|
|
350
|
+
'Authentication failure',
|
|
351
|
+
'Unauthorized',
|
|
352
|
+
'Invalid token',
|
|
353
|
+
'Token expired',
|
|
354
|
+
'Not authenticated',
|
|
355
|
+
'Authentication required',
|
|
356
|
+
'No access token available',
|
|
357
|
+
'refresh token is invalid',
|
|
358
|
+
'Session has expired'
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const errorMessage = error.message || '';
|
|
362
|
+
const errorString = error.toString() || '';
|
|
363
|
+
|
|
364
|
+
return authErrorIndicators.some(indicator =>
|
|
365
|
+
errorMessage.includes(indicator) || errorString.includes(indicator)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Test connection health
|
|
371
|
+
*/
|
|
372
|
+
async testConnection() {
|
|
373
|
+
try {
|
|
374
|
+
await this.ensureValidConnection();
|
|
375
|
+
const identity = await this.connection.identity();
|
|
376
|
+
return {
|
|
377
|
+
connected: true,
|
|
378
|
+
user: identity.username,
|
|
379
|
+
organization: identity.organization_id
|
|
380
|
+
};
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return {
|
|
383
|
+
connected: false,
|
|
384
|
+
error: error.message
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
package/src/sf-client.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sf-client.js — Salesforce connection
|
|
3
|
+
* Reads tokens.json, handles refresh automatically via jsforce v1
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import jsforce from 'jsforce';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const TOKEN_FILE = path.join(__dirname, '..', 'tokens.json');
|
|
13
|
+
|
|
14
|
+
let _conn = null;
|
|
15
|
+
|
|
16
|
+
export function getConnection() {
|
|
17
|
+
if (_conn) return _conn;
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(TOKEN_FILE)) {
|
|
20
|
+
throw new Error('No tokens.json found. Run: node auth.js');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tokens = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
|
|
24
|
+
|
|
25
|
+
const oauth2 = new jsforce.OAuth2({
|
|
26
|
+
loginUrl: tokens.loginUrl,
|
|
27
|
+
clientId: tokens.clientId,
|
|
28
|
+
clientSecret: tokens.clientSecret,
|
|
29
|
+
redirectUri: 'http://localhost:8482/callback',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
_conn = new jsforce.Connection({
|
|
33
|
+
oauth2,
|
|
34
|
+
instanceUrl: tokens.instanceUrl,
|
|
35
|
+
accessToken: tokens.accessToken,
|
|
36
|
+
refreshToken: tokens.refreshToken,
|
|
37
|
+
version: '59.0',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Persist refreshed tokens automatically
|
|
41
|
+
_conn.on('refresh', (newAccessToken) => {
|
|
42
|
+
const updated = { ...tokens, accessToken: newAccessToken };
|
|
43
|
+
fs.writeFileSync(TOKEN_FILE, JSON.stringify(updated, null, 2));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return _conn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Run a SOQL query, return array of records */
|
|
50
|
+
export async function query(soql) {
|
|
51
|
+
const conn = getConnection();
|
|
52
|
+
const result = await conn.query(soql);
|
|
53
|
+
return result.records;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Run SOSL search, return flat array of records across all types */
|
|
57
|
+
export async function search(sosl) {
|
|
58
|
+
const conn = getConnection();
|
|
59
|
+
const result = await conn.search(sosl);
|
|
60
|
+
return result.searchRecords ?? [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Describe a Salesforce object — returns field metadata */
|
|
64
|
+
export async function describe(objectName) {
|
|
65
|
+
const conn = getConnection();
|
|
66
|
+
return await conn.describe(objectName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Create a record */
|
|
70
|
+
export async function createRecord(objectName, fields) {
|
|
71
|
+
const conn = getConnection();
|
|
72
|
+
return await conn.sobject(objectName).create(fields);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Update a record */
|
|
76
|
+
export async function updateRecord(objectName, id, fields) {
|
|
77
|
+
const conn = getConnection();
|
|
78
|
+
return await conn.sobject(objectName).update({ Id: id, ...fields });
|
|
79
|
+
}
|