cowork-os 0.3.21 → 0.3.23
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/README.md +293 -6
- package/connectors/README.md +20 -0
- package/connectors/asana-mcp/README.md +24 -0
- package/connectors/asana-mcp/dist/index.js +427 -0
- package/connectors/asana-mcp/package.json +15 -0
- package/connectors/asana-mcp/src/index.ts +553 -0
- package/connectors/asana-mcp/tsconfig.json +13 -0
- package/connectors/hubspot-mcp/README.md +35 -0
- package/connectors/hubspot-mcp/dist/index.js +454 -0
- package/connectors/hubspot-mcp/package.json +15 -0
- package/connectors/hubspot-mcp/src/index.ts +562 -0
- package/connectors/hubspot-mcp/tsconfig.json +13 -0
- package/connectors/jira-mcp/README.md +49 -0
- package/connectors/jira-mcp/dist/index.js +588 -0
- package/connectors/jira-mcp/package.json +15 -0
- package/connectors/jira-mcp/src/index.ts +711 -0
- package/connectors/jira-mcp/tsconfig.json +13 -0
- package/connectors/linear-mcp/README.md +22 -0
- package/connectors/linear-mcp/dist/index.js +402 -0
- package/connectors/linear-mcp/package.json +15 -0
- package/connectors/linear-mcp/src/index.ts +522 -0
- package/connectors/linear-mcp/tsconfig.json +13 -0
- package/connectors/okta-mcp/README.md +24 -0
- package/connectors/okta-mcp/dist/index.js +411 -0
- package/connectors/okta-mcp/package.json +15 -0
- package/connectors/okta-mcp/src/index.ts +520 -0
- package/connectors/okta-mcp/tsconfig.json +13 -0
- package/connectors/salesforce-mcp/README.md +47 -0
- package/connectors/salesforce-mcp/dist/index.js +584 -0
- package/connectors/salesforce-mcp/package.json +15 -0
- package/connectors/salesforce-mcp/src/index.ts +722 -0
- package/connectors/salesforce-mcp/tsconfig.json +13 -0
- package/connectors/servicenow-mcp/README.md +26 -0
- package/connectors/servicenow-mcp/dist/index.js +400 -0
- package/connectors/servicenow-mcp/package.json +15 -0
- package/connectors/servicenow-mcp/src/index.ts +500 -0
- package/connectors/servicenow-mcp/tsconfig.json +13 -0
- package/connectors/templates/mcp-connector/README.md +31 -0
- package/connectors/templates/mcp-connector/package.json +15 -0
- package/connectors/templates/mcp-connector/src/index.ts +330 -0
- package/connectors/templates/mcp-connector/tsconfig.json +13 -0
- package/connectors/zendesk-mcp/README.md +40 -0
- package/connectors/zendesk-mcp/dist/index.js +431 -0
- package/connectors/zendesk-mcp/package.json +15 -0
- package/connectors/zendesk-mcp/src/index.ts +543 -0
- package/connectors/zendesk-mcp/tsconfig.json +13 -0
- package/dist/electron/electron/agent/daemon.js +25 -0
- package/dist/electron/electron/agent/executor.js +181 -26
- package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
- package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
- package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
- package/dist/electron/electron/agent/llm/index.js +11 -1
- package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
- package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
- package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
- package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
- package/dist/electron/electron/agent/llm/provider-factory.js +318 -4
- package/dist/electron/electron/agent/llm/types.js +66 -1
- package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
- package/dist/electron/electron/agent/tools/box-tools.js +231 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
- package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
- package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
- package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
- package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
- package/dist/electron/electron/agent/tools/registry.js +541 -0
- package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
- package/dist/electron/electron/agent/tools/x-tools.js +1 -1
- package/dist/electron/electron/gateway/index.js +1 -0
- package/dist/electron/electron/gateway/router.js +123 -143
- package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
- package/dist/electron/electron/ipc/handlers.js +627 -158
- package/dist/electron/electron/main.js +63 -0
- package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
- package/dist/electron/electron/memory/MemoryService.js +1 -1
- package/dist/electron/electron/preload.js +74 -1
- package/dist/electron/electron/settings/box-manager.js +54 -0
- package/dist/electron/electron/settings/dropbox-manager.js +54 -0
- package/dist/electron/electron/settings/google-drive-manager.js +54 -0
- package/dist/electron/electron/settings/notion-manager.js +56 -0
- package/dist/electron/electron/settings/onedrive-manager.js +54 -0
- package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
- package/dist/electron/electron/utils/box-api.js +153 -0
- package/dist/electron/electron/utils/dropbox-api.js +144 -0
- package/dist/electron/electron/utils/env-migration.js +19 -0
- package/dist/electron/electron/utils/google-drive-api.js +152 -0
- package/dist/electron/electron/utils/notion-api.js +103 -0
- package/dist/electron/electron/utils/onedrive-api.js +113 -0
- package/dist/electron/electron/utils/sharepoint-api.js +109 -0
- package/dist/electron/electron/utils/validation.js +82 -3
- package/dist/electron/electron/utils/x-cli.js +1 -1
- package/dist/electron/shared/channelMessages.js +284 -3
- package/dist/electron/shared/llm-provider-catalog.js +198 -0
- package/dist/electron/shared/types.js +88 -1
- package/package.json +12 -2
- package/src/electron/agent/executor.ts +205 -28
- package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
- package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
- package/src/electron/agent/llm/groq-provider.ts +39 -0
- package/src/electron/agent/llm/index.ts +5 -0
- package/src/electron/agent/llm/kimi-provider.ts +39 -0
- package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
- package/src/electron/agent/llm/openai-compatible.ts +133 -0
- package/src/electron/agent/llm/openai-oauth.ts +2 -1
- package/src/electron/agent/llm/openrouter-provider.ts +2 -1
- package/src/electron/agent/llm/provider-factory.ts +414 -6
- package/src/electron/agent/llm/types.ts +90 -1
- package/src/electron/agent/llm/xai-provider.ts +39 -0
- package/src/electron/agent/tools/box-tools.ts +239 -0
- package/src/electron/agent/tools/builtin-settings.ts +34 -0
- package/src/electron/agent/tools/dropbox-tools.ts +237 -0
- package/src/electron/agent/tools/google-drive-tools.ts +228 -0
- package/src/electron/agent/tools/notion-tools.ts +330 -0
- package/src/electron/agent/tools/onedrive-tools.ts +217 -0
- package/src/electron/agent/tools/registry.ts +565 -0
- package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
- package/src/electron/agent/tools/shell-tools.ts +11 -3
- package/src/electron/agent/tools/x-tools.ts +1 -1
- package/src/electron/database/SecureSettingsRepository.ts +7 -1
- package/src/electron/gateway/index.ts +1 -0
- package/src/electron/gateway/router.ts +134 -149
- package/src/electron/ipc/canvas-handlers.ts +10 -0
- package/src/electron/ipc/handlers.ts +673 -153
- package/src/electron/main.ts +35 -0
- package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
- package/src/electron/memory/MemoryService.ts +5 -1
- package/src/electron/preload.ts +167 -4
- package/src/electron/settings/box-manager.ts +58 -0
- package/src/electron/settings/dropbox-manager.ts +58 -0
- package/src/electron/settings/google-drive-manager.ts +58 -0
- package/src/electron/settings/notion-manager.ts +60 -0
- package/src/electron/settings/onedrive-manager.ts +58 -0
- package/src/electron/settings/sharepoint-manager.ts +58 -0
- package/src/electron/utils/box-api.ts +184 -0
- package/src/electron/utils/dropbox-api.ts +171 -0
- package/src/electron/utils/env-migration.ts +22 -0
- package/src/electron/utils/google-drive-api.ts +183 -0
- package/src/electron/utils/notion-api.ts +126 -0
- package/src/electron/utils/onedrive-api.ts +137 -0
- package/src/electron/utils/sharepoint-api.ts +132 -0
- package/src/electron/utils/validation.ts +102 -1
- package/src/electron/utils/x-cli.ts +1 -1
- package/src/renderer/App.tsx +20 -2
- package/src/renderer/components/BoxSettings.tsx +203 -0
- package/src/renderer/components/BrowserView.tsx +101 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
- package/src/renderer/components/CanvasPreview.tsx +68 -1
- package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
- package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
- package/src/renderer/components/ConnectorsSettings.tsx +397 -0
- package/src/renderer/components/DropboxSettings.tsx +202 -0
- package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
- package/src/renderer/components/MCPSettings.tsx +56 -0
- package/src/renderer/components/MainContent.tsx +270 -34
- package/src/renderer/components/NotionSettings.tsx +231 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
- package/src/renderer/components/OnboardingModal.tsx +70 -1
- package/src/renderer/components/OneDriveSettings.tsx +212 -0
- package/src/renderer/components/Settings.tsx +611 -8
- package/src/renderer/components/SharePointSettings.tsx +224 -0
- package/src/renderer/components/Sidebar.tsx +25 -9
- package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
- package/src/renderer/styles/index.css +438 -25
- package/src/shared/channelMessages.ts +367 -4
- package/src/shared/llm-provider-catalog.ts +217 -0
- package/src/shared/types.ts +226 -1
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
|
|
3
|
+
// ==================== MCP Types ====================
|
|
4
|
+
|
|
5
|
+
type JSONRPCId = string | number;
|
|
6
|
+
|
|
7
|
+
type JSONRPCRequest = {
|
|
8
|
+
jsonrpc: '2.0';
|
|
9
|
+
id: JSONRPCId;
|
|
10
|
+
method: string;
|
|
11
|
+
params?: Record<string, any>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type JSONRPCNotification = {
|
|
15
|
+
jsonrpc: '2.0';
|
|
16
|
+
method: string;
|
|
17
|
+
params?: Record<string, any>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type JSONRPCResponse = {
|
|
21
|
+
jsonrpc: '2.0';
|
|
22
|
+
id: JSONRPCId;
|
|
23
|
+
result?: any;
|
|
24
|
+
error?: { code: number; message: string; data?: any };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type MCPToolProperty = {
|
|
28
|
+
type: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
enum?: string[];
|
|
31
|
+
default?: any;
|
|
32
|
+
items?: MCPToolProperty;
|
|
33
|
+
properties?: Record<string, MCPToolProperty>;
|
|
34
|
+
required?: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type MCPTool = {
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object';
|
|
42
|
+
properties?: Record<string, MCPToolProperty>;
|
|
43
|
+
required?: string[];
|
|
44
|
+
additionalProperties?: boolean;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type MCPServerInfo = {
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
protocolVersion?: string;
|
|
52
|
+
capabilities?: {
|
|
53
|
+
tools?: { listChanged?: boolean };
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const PROTOCOL_VERSION = '2024-11-05';
|
|
58
|
+
|
|
59
|
+
const MCP_METHODS = {
|
|
60
|
+
INITIALIZE: 'initialize',
|
|
61
|
+
INITIALIZED: 'notifications/initialized',
|
|
62
|
+
SHUTDOWN: 'shutdown',
|
|
63
|
+
TOOLS_LIST: 'tools/list',
|
|
64
|
+
TOOLS_CALL: 'tools/call',
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
const MCP_ERROR_CODES = {
|
|
68
|
+
PARSE_ERROR: -32700,
|
|
69
|
+
INVALID_REQUEST: -32600,
|
|
70
|
+
METHOD_NOT_FOUND: -32601,
|
|
71
|
+
INVALID_PARAMS: -32602,
|
|
72
|
+
INTERNAL_ERROR: -32603,
|
|
73
|
+
SERVER_NOT_INITIALIZED: -32002,
|
|
74
|
+
} as const;
|
|
75
|
+
|
|
76
|
+
// ==================== Salesforce Client ====================
|
|
77
|
+
|
|
78
|
+
type SalesforceConfig = {
|
|
79
|
+
instanceUrl?: string;
|
|
80
|
+
accessToken?: string;
|
|
81
|
+
apiVersion: string;
|
|
82
|
+
loginUrl: string;
|
|
83
|
+
clientId?: string;
|
|
84
|
+
clientSecret?: string;
|
|
85
|
+
refreshToken?: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type RateLimitInfo = {
|
|
89
|
+
used: number;
|
|
90
|
+
limit: number;
|
|
91
|
+
remaining: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type RequestMeta = {
|
|
95
|
+
durationMs: number;
|
|
96
|
+
rateLimit?: RateLimitInfo;
|
|
97
|
+
vendorRequestId?: string;
|
|
98
|
+
apiVersion: string;
|
|
99
|
+
instanceUrl?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type RequestResult = {
|
|
103
|
+
data: any;
|
|
104
|
+
meta: RequestMeta;
|
|
105
|
+
nextCursor?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
class SalesforceClient {
|
|
109
|
+
private config: SalesforceConfig;
|
|
110
|
+
|
|
111
|
+
constructor(config: SalesforceConfig) {
|
|
112
|
+
this.config = config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async health(): Promise<RequestResult> {
|
|
116
|
+
return this.requestJson('GET', 'limits');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async listObjects(): Promise<RequestResult> {
|
|
120
|
+
return this.requestJson('GET', 'sobjects');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async describeObject(objectName: string): Promise<RequestResult> {
|
|
124
|
+
return this.requestJson('GET', `sobjects/${encodeURIComponent(objectName)}/describe`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async getRecord(objectName: string, recordId: string, fields?: string[]): Promise<RequestResult> {
|
|
128
|
+
const query = fields && fields.length > 0 ? `?fields=${encodeURIComponent(fields.join(','))}` : '';
|
|
129
|
+
return this.requestJson('GET', `sobjects/${encodeURIComponent(objectName)}/${encodeURIComponent(recordId)}${query}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async query(soql: string, cursor?: string): Promise<RequestResult> {
|
|
133
|
+
if (cursor) {
|
|
134
|
+
return this.requestJson('GET', cursor, undefined, true);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const encoded = encodeURIComponent(soql);
|
|
138
|
+
return this.requestJson('GET', `query?q=${encoded}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async createRecord(objectName: string, fields: Record<string, any>): Promise<RequestResult> {
|
|
142
|
+
return this.requestJson('POST', `sobjects/${encodeURIComponent(objectName)}`, fields);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async updateRecord(objectName: string, recordId: string, fields: Record<string, any>): Promise<RequestResult> {
|
|
146
|
+
return this.requestJson('PATCH', `sobjects/${encodeURIComponent(objectName)}/${encodeURIComponent(recordId)}`, fields);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private getBaseUrl(): string {
|
|
150
|
+
if (!this.config.instanceUrl) {
|
|
151
|
+
throw new Error('SALESFORCE_INSTANCE_URL is required');
|
|
152
|
+
}
|
|
153
|
+
return `${this.config.instanceUrl.replace(/\/$/, '')}/services/data/v${this.config.apiVersion}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async requestJson(
|
|
157
|
+
method: string,
|
|
158
|
+
path: string,
|
|
159
|
+
body?: any,
|
|
160
|
+
absolutePath = false
|
|
161
|
+
): Promise<RequestResult> {
|
|
162
|
+
const start = Date.now();
|
|
163
|
+
|
|
164
|
+
const url = absolutePath
|
|
165
|
+
? this.buildAbsoluteUrl(path)
|
|
166
|
+
: `${this.getBaseUrl()}/${path.replace(/^\//, '')}`;
|
|
167
|
+
|
|
168
|
+
const token = await this.ensureAccessToken();
|
|
169
|
+
const res = await fetch(url, {
|
|
170
|
+
method,
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: `Bearer ${token}`,
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
'User-Agent': 'CoWork-Salesforce-Connector/0.1.0',
|
|
175
|
+
},
|
|
176
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (res.status === 401 && this.canRefresh()) {
|
|
180
|
+
await this.refreshAccessToken();
|
|
181
|
+
return this.requestJson(method, path, body, absolutePath);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const durationMs = Date.now() - start;
|
|
185
|
+
const rateLimit = parseRateLimit(res.headers.get('sforce-limit-info'));
|
|
186
|
+
const vendorRequestId = res.headers.get('sforce-request-id') || undefined;
|
|
187
|
+
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
const message = await this.extractErrorMessage(res);
|
|
190
|
+
throw new Error(message);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let data: any = null;
|
|
194
|
+
if (res.status !== 204) {
|
|
195
|
+
data = await res.json();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const nextCursor = data?.nextRecordsUrl;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
data,
|
|
202
|
+
meta: {
|
|
203
|
+
durationMs,
|
|
204
|
+
rateLimit,
|
|
205
|
+
vendorRequestId,
|
|
206
|
+
apiVersion: this.config.apiVersion,
|
|
207
|
+
instanceUrl: this.config.instanceUrl,
|
|
208
|
+
},
|
|
209
|
+
nextCursor,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private buildAbsoluteUrl(path: string): string {
|
|
214
|
+
const trimmed = path.trim();
|
|
215
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
216
|
+
return trimmed;
|
|
217
|
+
}
|
|
218
|
+
if (!this.config.instanceUrl) {
|
|
219
|
+
throw new Error('SALESFORCE_INSTANCE_URL is required');
|
|
220
|
+
}
|
|
221
|
+
return `${this.config.instanceUrl.replace(/\/$/, '')}${trimmed.startsWith('/') ? '' : '/'}${trimmed}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async extractErrorMessage(res: Response): Promise<string> {
|
|
225
|
+
const text = await res.text();
|
|
226
|
+
if (!text) {
|
|
227
|
+
return `Salesforce API error (status ${res.status})`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(text);
|
|
232
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
233
|
+
const first = parsed[0];
|
|
234
|
+
const code = first.errorCode ? ` (${first.errorCode})` : '';
|
|
235
|
+
return `${first.message || 'Salesforce API error'}${code}`;
|
|
236
|
+
}
|
|
237
|
+
if (parsed.error && parsed.error_description) {
|
|
238
|
+
return `${parsed.error_description} (${parsed.error})`;
|
|
239
|
+
}
|
|
240
|
+
if (parsed.message) {
|
|
241
|
+
return parsed.message;
|
|
242
|
+
}
|
|
243
|
+
return JSON.stringify(parsed);
|
|
244
|
+
} catch {
|
|
245
|
+
return text;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private canRefresh(): boolean {
|
|
250
|
+
return Boolean(
|
|
251
|
+
this.config.clientId &&
|
|
252
|
+
this.config.clientSecret &&
|
|
253
|
+
this.config.refreshToken &&
|
|
254
|
+
this.config.loginUrl
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async ensureAccessToken(): Promise<string> {
|
|
259
|
+
if (this.config.accessToken) {
|
|
260
|
+
return this.config.accessToken;
|
|
261
|
+
}
|
|
262
|
+
if (!this.canRefresh()) {
|
|
263
|
+
throw new Error('SALESFORCE_ACCESS_TOKEN is required (or provide refresh credentials)');
|
|
264
|
+
}
|
|
265
|
+
await this.refreshAccessToken();
|
|
266
|
+
if (!this.config.accessToken) {
|
|
267
|
+
throw new Error('Failed to obtain Salesforce access token');
|
|
268
|
+
}
|
|
269
|
+
return this.config.accessToken;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async refreshAccessToken(): Promise<void> {
|
|
273
|
+
if (!this.canRefresh()) {
|
|
274
|
+
throw new Error('Missing refresh credentials');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const params = new URLSearchParams({
|
|
278
|
+
grant_type: 'refresh_token',
|
|
279
|
+
client_id: this.config.clientId as string,
|
|
280
|
+
client_secret: this.config.clientSecret as string,
|
|
281
|
+
refresh_token: this.config.refreshToken as string,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const url = `${this.config.loginUrl.replace(/\/$/, '')}/services/oauth2/token`;
|
|
285
|
+
const res = await fetch(url, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: {
|
|
288
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
289
|
+
},
|
|
290
|
+
body: params.toString(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (!res.ok) {
|
|
294
|
+
const message = await this.extractErrorMessage(res);
|
|
295
|
+
throw new Error(`Salesforce OAuth refresh failed: ${message}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const data = await res.json();
|
|
299
|
+
if (!data.access_token) {
|
|
300
|
+
throw new Error('Salesforce OAuth refresh returned no access_token');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.config.accessToken = data.access_token;
|
|
304
|
+
if (data.instance_url) {
|
|
305
|
+
this.config.instanceUrl = data.instance_url;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseRateLimit(header: string | null): RateLimitInfo | undefined {
|
|
311
|
+
if (!header) return undefined;
|
|
312
|
+
const match = header.match(/api-usage=(\d+)\/(\d+)/i);
|
|
313
|
+
if (!match) return undefined;
|
|
314
|
+
const used = Number(match[1]);
|
|
315
|
+
const limit = Number(match[2]);
|
|
316
|
+
if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
used,
|
|
321
|
+
limit,
|
|
322
|
+
remaining: Math.max(0, limit - used),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ==================== MCP Stdio Server ====================
|
|
327
|
+
|
|
328
|
+
type ToolProvider = {
|
|
329
|
+
getTools(): MCPTool[];
|
|
330
|
+
executeTool(name: string, args: Record<string, any>): Promise<any>;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
class StdioMCPServer {
|
|
334
|
+
private initialized = false;
|
|
335
|
+
private rl: readline.Interface | null = null;
|
|
336
|
+
|
|
337
|
+
constructor(
|
|
338
|
+
private toolProvider: ToolProvider,
|
|
339
|
+
private serverInfo: MCPServerInfo
|
|
340
|
+
) {}
|
|
341
|
+
|
|
342
|
+
start(): void {
|
|
343
|
+
this.rl = readline.createInterface({
|
|
344
|
+
input: process.stdin,
|
|
345
|
+
output: process.stdout,
|
|
346
|
+
terminal: false,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
this.rl.on('line', (line) => this.handleLine(line));
|
|
350
|
+
this.rl.on('close', () => this.stop());
|
|
351
|
+
|
|
352
|
+
process.on('SIGINT', () => this.stop());
|
|
353
|
+
process.on('SIGTERM', () => this.stop());
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
stop(): void {
|
|
357
|
+
if (this.rl) {
|
|
358
|
+
this.rl.close();
|
|
359
|
+
this.rl = null;
|
|
360
|
+
}
|
|
361
|
+
process.exit(0);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private handleLine(line: string): void {
|
|
365
|
+
const trimmed = line.trim();
|
|
366
|
+
if (!trimmed) return;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const message = JSON.parse(trimmed);
|
|
370
|
+
this.handleMessage(message);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
this.sendError(0, MCP_ERROR_CODES.PARSE_ERROR, 'Parse error');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async handleMessage(message: any): Promise<void> {
|
|
377
|
+
if ('id' in message && message.id !== null) {
|
|
378
|
+
await this.handleRequest(message as JSONRPCRequest);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if ('method' in message) {
|
|
383
|
+
await this.handleNotification(message as JSONRPCNotification);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async handleRequest(request: JSONRPCRequest): Promise<void> {
|
|
388
|
+
const { id, method, params } = request;
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
let result: any;
|
|
392
|
+
|
|
393
|
+
switch (method) {
|
|
394
|
+
case MCP_METHODS.INITIALIZE:
|
|
395
|
+
result = this.handleInitialize(params);
|
|
396
|
+
break;
|
|
397
|
+
case MCP_METHODS.TOOLS_LIST:
|
|
398
|
+
this.requireInitialized();
|
|
399
|
+
result = this.handleToolsList();
|
|
400
|
+
break;
|
|
401
|
+
case MCP_METHODS.TOOLS_CALL:
|
|
402
|
+
this.requireInitialized();
|
|
403
|
+
result = await this.handleToolsCall(params);
|
|
404
|
+
break;
|
|
405
|
+
case MCP_METHODS.SHUTDOWN:
|
|
406
|
+
result = this.handleShutdown();
|
|
407
|
+
break;
|
|
408
|
+
default:
|
|
409
|
+
throw this.createError(MCP_ERROR_CODES.METHOD_NOT_FOUND, `Method not found: ${method}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.sendResult(id, result);
|
|
413
|
+
} catch (error: any) {
|
|
414
|
+
if (error.code !== undefined) {
|
|
415
|
+
this.sendError(id, error.code, error.message, error.data);
|
|
416
|
+
} else {
|
|
417
|
+
this.sendError(id, MCP_ERROR_CODES.INTERNAL_ERROR, error?.message || 'Internal error');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async handleNotification(notification: JSONRPCNotification): Promise<void> {
|
|
423
|
+
const { method } = notification;
|
|
424
|
+
|
|
425
|
+
if (method === MCP_METHODS.INITIALIZED) {
|
|
426
|
+
this.initialized = true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private handleInitialize(_params: any): {
|
|
431
|
+
protocolVersion: string;
|
|
432
|
+
capabilities: MCPServerInfo['capabilities'];
|
|
433
|
+
serverInfo: MCPServerInfo;
|
|
434
|
+
} {
|
|
435
|
+
if (this.initialized) {
|
|
436
|
+
throw this.createError(MCP_ERROR_CODES.INVALID_REQUEST, 'Already initialized');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
441
|
+
capabilities: this.serverInfo.capabilities,
|
|
442
|
+
serverInfo: this.serverInfo,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private handleToolsList(): { tools: MCPTool[] } {
|
|
447
|
+
return { tools: this.toolProvider.getTools() };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private async handleToolsCall(params: any): Promise<any> {
|
|
451
|
+
const { name, arguments: args } = params || {};
|
|
452
|
+
if (!name) {
|
|
453
|
+
throw this.createError(MCP_ERROR_CODES.INVALID_PARAMS, 'Tool name is required');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const result = await this.toolProvider.executeTool(name, args || {});
|
|
458
|
+
|
|
459
|
+
if (typeof result === 'string') {
|
|
460
|
+
return { content: [{ type: 'text', text: result }] };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (result && typeof result === 'object') {
|
|
464
|
+
if (result.content && Array.isArray(result.content)) {
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return { content: [{ type: 'text', text: String(result) }] };
|
|
471
|
+
} catch (error: any) {
|
|
472
|
+
return {
|
|
473
|
+
content: [{ type: 'text', text: `Error: ${error?.message || 'Tool failed'}` }],
|
|
474
|
+
isError: true,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private handleShutdown(): Record<string, never> {
|
|
480
|
+
setImmediate(() => this.stop());
|
|
481
|
+
return {};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private sendResult(id: JSONRPCId, result: any): void {
|
|
485
|
+
const response: JSONRPCResponse = { jsonrpc: '2.0', id, result };
|
|
486
|
+
this.sendMessage(response);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private sendError(id: JSONRPCId, code: number, message: string, data?: any): void {
|
|
490
|
+
const response: JSONRPCResponse = {
|
|
491
|
+
jsonrpc: '2.0',
|
|
492
|
+
id,
|
|
493
|
+
error: { code, message, data },
|
|
494
|
+
};
|
|
495
|
+
this.sendMessage(response);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private sendMessage(message: JSONRPCResponse | JSONRPCNotification): void {
|
|
499
|
+
process.stdout.write(JSON.stringify(message) + '\n');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private requireInitialized(): void {
|
|
503
|
+
if (!this.initialized) {
|
|
504
|
+
throw this.createError(MCP_ERROR_CODES.SERVER_NOT_INITIALIZED, 'Server not initialized');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private createError(code: number, message: string, data?: any): { code: number; message: string; data?: any } {
|
|
509
|
+
return { code, message, data };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ==================== Tool Definitions ====================
|
|
514
|
+
|
|
515
|
+
const CONNECTOR_PREFIX = 'salesforce';
|
|
516
|
+
const DEFAULT_API_VERSION = '60.0';
|
|
517
|
+
|
|
518
|
+
const tools: MCPTool[] = [
|
|
519
|
+
{
|
|
520
|
+
name: `${CONNECTOR_PREFIX}.health`,
|
|
521
|
+
description: 'Check connector health and authentication status',
|
|
522
|
+
inputSchema: {
|
|
523
|
+
type: 'object',
|
|
524
|
+
properties: {
|
|
525
|
+
requestId: { type: 'string', description: 'Optional request id for tracing' },
|
|
526
|
+
},
|
|
527
|
+
additionalProperties: false,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: `${CONNECTOR_PREFIX}.list_objects`,
|
|
532
|
+
description: 'List available Salesforce objects',
|
|
533
|
+
inputSchema: {
|
|
534
|
+
type: 'object',
|
|
535
|
+
properties: {
|
|
536
|
+
requestId: { type: 'string', description: 'Optional request id for tracing' },
|
|
537
|
+
},
|
|
538
|
+
additionalProperties: false,
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
name: `${CONNECTOR_PREFIX}.describe_object`,
|
|
543
|
+
description: 'Describe a Salesforce object and its fields',
|
|
544
|
+
inputSchema: {
|
|
545
|
+
type: 'object',
|
|
546
|
+
properties: {
|
|
547
|
+
object: { type: 'string', description: 'Salesforce object name (e.g., Account)', },
|
|
548
|
+
requestId: { type: 'string', description: 'Optional request id for tracing' },
|
|
549
|
+
},
|
|
550
|
+
required: ['object'],
|
|
551
|
+
additionalProperties: false,
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: `${CONNECTOR_PREFIX}.get_record`,
|
|
556
|
+
description: 'Fetch a Salesforce record by id',
|
|
557
|
+
inputSchema: {
|
|
558
|
+
type: 'object',
|
|
559
|
+
properties: {
|
|
560
|
+
object: { type: 'string', description: 'Salesforce object name (e.g., Account)' },
|
|
561
|
+
id: { type: 'string', description: 'Salesforce record id' },
|
|
562
|
+
fields: {
|
|
563
|
+
type: 'array',
|
|
564
|
+
description: 'Optional list of fields to include',
|
|
565
|
+
items: { type: 'string' },
|
|
566
|
+
},
|
|
567
|
+
requestId: { type: 'string', description: 'Optional request id for tracing' },
|
|
568
|
+
},
|
|
569
|
+
required: ['object', 'id'],
|
|
570
|
+
additionalProperties: false,
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: `${CONNECTOR_PREFIX}.search_records`,
|
|
575
|
+
description: 'Run a SOQL query',
|
|
576
|
+
inputSchema: {
|
|
577
|
+
type: 'object',
|
|
578
|
+
properties: {
|
|
579
|
+
soql: { type: 'string', description: 'SOQL query to execute (required if cursor is not provided)' },
|
|
580
|
+
limit: { type: 'number', description: 'Optional LIMIT to append if not present' },
|
|
581
|
+
cursor: { type: 'string', description: 'Pagination cursor (nextRecordsUrl); if provided, soql is optional' },
|
|
582
|
+
requestId: { type: 'string', description: 'Optional request id for tracing' },
|
|
583
|
+
},
|
|
584
|
+
additionalProperties: false,
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: `${CONNECTOR_PREFIX}.create_record`,
|
|
589
|
+
description: 'Create a Salesforce record',
|
|
590
|
+
inputSchema: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
properties: {
|
|
593
|
+
object: { type: 'string', description: 'Salesforce object name (e.g., Account)' },
|
|
594
|
+
fields: { type: 'object', description: 'Field map for creation' },
|
|
595
|
+
idempotencyKey: { type: 'string', description: 'Optional idempotency key (best-effort)' },
|
|
596
|
+
requestId: { type: 'string', description: 'Optional request id for tracing' },
|
|
597
|
+
},
|
|
598
|
+
required: ['object', 'fields'],
|
|
599
|
+
additionalProperties: false,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: `${CONNECTOR_PREFIX}.update_record`,
|
|
604
|
+
description: 'Update a Salesforce record',
|
|
605
|
+
inputSchema: {
|
|
606
|
+
type: 'object',
|
|
607
|
+
properties: {
|
|
608
|
+
object: { type: 'string', description: 'Salesforce object name (e.g., Account)' },
|
|
609
|
+
id: { type: 'string', description: 'Salesforce record id' },
|
|
610
|
+
fields: { type: 'object', description: 'Field map for update' },
|
|
611
|
+
idempotencyKey: { type: 'string', description: 'Optional idempotency key (best-effort)' },
|
|
612
|
+
requestId: { type: 'string', description: 'Optional request id for tracing' },
|
|
613
|
+
},
|
|
614
|
+
required: ['object', 'id', 'fields'],
|
|
615
|
+
additionalProperties: false,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
const config: SalesforceConfig = {
|
|
621
|
+
instanceUrl: process.env.SALESFORCE_INSTANCE_URL,
|
|
622
|
+
accessToken: process.env.SALESFORCE_ACCESS_TOKEN,
|
|
623
|
+
apiVersion: process.env.SALESFORCE_API_VERSION || DEFAULT_API_VERSION,
|
|
624
|
+
loginUrl: process.env.SALESFORCE_LOGIN_URL || 'https://login.salesforce.com',
|
|
625
|
+
clientId: process.env.SALESFORCE_CLIENT_ID,
|
|
626
|
+
clientSecret: process.env.SALESFORCE_CLIENT_SECRET,
|
|
627
|
+
refreshToken: process.env.SALESFORCE_REFRESH_TOKEN,
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const client = new SalesforceClient(config);
|
|
631
|
+
|
|
632
|
+
const handlers: Record<string, (args: Record<string, any>) => Promise<any>> = {
|
|
633
|
+
[`${CONNECTOR_PREFIX}.health`]: async (args) => {
|
|
634
|
+
const result = await client.health();
|
|
635
|
+
return buildEnvelope(result, args.requestId);
|
|
636
|
+
},
|
|
637
|
+
[`${CONNECTOR_PREFIX}.list_objects`]: async (args) => {
|
|
638
|
+
const result = await client.listObjects();
|
|
639
|
+
return buildEnvelope(result, args.requestId);
|
|
640
|
+
},
|
|
641
|
+
[`${CONNECTOR_PREFIX}.describe_object`]: async (args) => {
|
|
642
|
+
const result = await client.describeObject(args.object);
|
|
643
|
+
return buildEnvelope(result, args.requestId);
|
|
644
|
+
},
|
|
645
|
+
[`${CONNECTOR_PREFIX}.get_record`]: async (args) => {
|
|
646
|
+
const result = await client.getRecord(args.object, args.id, args.fields);
|
|
647
|
+
return buildEnvelope(result, args.requestId);
|
|
648
|
+
},
|
|
649
|
+
[`${CONNECTOR_PREFIX}.search_records`]: async (args) => {
|
|
650
|
+
const soql = args.soql ? normalizeSoql(args.soql, args.limit) : '';
|
|
651
|
+
if (!args.cursor && !soql) {
|
|
652
|
+
throw new Error('search_records requires either soql or cursor');
|
|
653
|
+
}
|
|
654
|
+
const result = await client.query(soql, args.cursor);
|
|
655
|
+
return buildEnvelope(result, args.requestId);
|
|
656
|
+
},
|
|
657
|
+
[`${CONNECTOR_PREFIX}.create_record`]: async (args) => {
|
|
658
|
+
const warnings = args.idempotencyKey
|
|
659
|
+
? ['Salesforce does not natively support idempotency keys; handled best-effort.']
|
|
660
|
+
: [];
|
|
661
|
+
const result = await client.createRecord(args.object, args.fields || {});
|
|
662
|
+
return buildEnvelope(result, args.requestId, warnings);
|
|
663
|
+
},
|
|
664
|
+
[`${CONNECTOR_PREFIX}.update_record`]: async (args) => {
|
|
665
|
+
const warnings = args.idempotencyKey
|
|
666
|
+
? ['Salesforce does not natively support idempotency keys; handled best-effort.']
|
|
667
|
+
: [];
|
|
668
|
+
const result = await client.updateRecord(args.object, args.id, args.fields || {});
|
|
669
|
+
return buildEnvelope(result, args.requestId, warnings);
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const toolProvider: ToolProvider = {
|
|
674
|
+
getTools: () => tools,
|
|
675
|
+
executeTool: async (name, args) => {
|
|
676
|
+
const handler = handlers[name];
|
|
677
|
+
if (!handler) {
|
|
678
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
679
|
+
}
|
|
680
|
+
return handler(args);
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const serverInfo: MCPServerInfo = {
|
|
685
|
+
name: 'Salesforce Connector',
|
|
686
|
+
version: '0.1.0',
|
|
687
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
688
|
+
capabilities: {
|
|
689
|
+
tools: { listChanged: false },
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const server = new StdioMCPServer(toolProvider, serverInfo);
|
|
694
|
+
server.start();
|
|
695
|
+
|
|
696
|
+
function buildEnvelope(result: RequestResult, requestId?: string, warnings: string[] = []): any {
|
|
697
|
+
return {
|
|
698
|
+
ok: true,
|
|
699
|
+
data: result.data,
|
|
700
|
+
meta: {
|
|
701
|
+
requestId,
|
|
702
|
+
durationMs: result.meta.durationMs,
|
|
703
|
+
rateLimit: result.meta.rateLimit,
|
|
704
|
+
vendorRequestId: result.meta.vendorRequestId,
|
|
705
|
+
apiVersion: result.meta.apiVersion,
|
|
706
|
+
instanceUrl: result.meta.instanceUrl,
|
|
707
|
+
},
|
|
708
|
+
nextCursor: result.nextCursor,
|
|
709
|
+
warnings,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function normalizeSoql(soql: string, limit?: number): string {
|
|
714
|
+
if (!limit || !Number.isFinite(limit) || limit <= 0) {
|
|
715
|
+
return soql;
|
|
716
|
+
}
|
|
717
|
+
const hasLimit = /\blimit\b/i.test(soql);
|
|
718
|
+
if (hasLimit) {
|
|
719
|
+
return soql;
|
|
720
|
+
}
|
|
721
|
+
return `${soql} LIMIT ${Math.floor(limit)}`;
|
|
722
|
+
}
|