salesflare-mcp-server 1.0.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/API.md +691 -0
- package/CHANGELOG.md +49 -0
- package/CLAUDE.md +117 -0
- package/CONTRIBUTING.md +399 -0
- package/FIX_PLAN.md +70 -0
- package/INSPECTOR.md +191 -0
- package/LICENSE +21 -0
- package/PUBLISH.md +73 -0
- package/README.md +383 -0
- package/dist/auth/api-key-auth.d.ts +75 -0
- package/dist/auth/api-key-auth.d.ts.map +1 -0
- package/dist/auth/api-key-auth.js +103 -0
- package/dist/auth/oauth-auth.d.ts +81 -0
- package/dist/auth/oauth-auth.d.ts.map +1 -0
- package/dist/auth/oauth-auth.js +123 -0
- package/dist/auth/token-manager.d.ts +105 -0
- package/dist/auth/token-manager.d.ts.map +1 -0
- package/dist/auth/token-manager.js +87 -0
- package/dist/client/salesflare-client.d.ts +219 -0
- package/dist/client/salesflare-client.d.ts.map +1 -0
- package/dist/client/salesflare-client.js +484 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/server.d.ts +39 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +140 -0
- package/dist/tools/companies.d.ts +45 -0
- package/dist/tools/companies.d.ts.map +1 -0
- package/dist/tools/companies.js +392 -0
- package/dist/tools/contacts.d.ts +45 -0
- package/dist/tools/contacts.d.ts.map +1 -0
- package/dist/tools/contacts.js +290 -0
- package/dist/tools/deals.d.ts +46 -0
- package/dist/tools/deals.d.ts.map +1 -0
- package/dist/tools/deals.js +442 -0
- package/dist/tools/pipeline.d.ts +43 -0
- package/dist/tools/pipeline.d.ts.map +1 -0
- package/dist/tools/pipeline.js +328 -0
- package/dist/tools/tasks.d.ts +44 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +406 -0
- package/dist/transport/http-transport.d.ts +36 -0
- package/dist/transport/http-transport.d.ts.map +1 -0
- package/dist/transport/http-transport.js +173 -0
- package/dist/transport/stdio-transport.d.ts +37 -0
- package/dist/transport/stdio-transport.d.ts.map +1 -0
- package/dist/transport/stdio-transport.js +129 -0
- package/dist/types/company.d.ts +223 -0
- package/dist/types/company.d.ts.map +1 -0
- package/dist/types/company.js +8 -0
- package/dist/types/contact.d.ts +166 -0
- package/dist/types/contact.d.ts.map +1 -0
- package/dist/types/contact.js +8 -0
- package/dist/types/deal.d.ts +203 -0
- package/dist/types/deal.d.ts.map +1 -0
- package/dist/types/deal.js +8 -0
- package/dist/types/pipeline.d.ts +116 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +8 -0
- package/dist/types/task.d.ts +154 -0
- package/dist/types/task.d.ts.map +1 -0
- package/dist/types/task.js +8 -0
- package/dist/utils/errors.d.ts +128 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +205 -0
- package/dist/utils/validation.d.ts +354 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +716 -0
- package/package.json +49 -0
- package/test-tasks-debug.js +21 -0
- package/test-tasks-params.js +52 -0
- package/test-tools.js +171 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task tools for Salesflare MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Implements CRUD operations for tasks:
|
|
5
|
+
* - salesflare_tasks_list: List tasks with filtering and pagination
|
|
6
|
+
* - salesflare_tasks_create: Create new tasks
|
|
7
|
+
* - salesflare_tasks_update: Update existing tasks (mark complete/incomplete)
|
|
8
|
+
*
|
|
9
|
+
* @module tools/tasks
|
|
10
|
+
*/
|
|
11
|
+
import { SalesflareError, ErrorCode } from '../utils/errors.js';
|
|
12
|
+
/**
|
|
13
|
+
* Task tool definitions with JSON schemas
|
|
14
|
+
*/
|
|
15
|
+
const taskTools = [
|
|
16
|
+
{
|
|
17
|
+
name: 'salesflare_tasks_list',
|
|
18
|
+
description: 'List tasks from Salesflare CRM with optional filtering and pagination. ' +
|
|
19
|
+
'Supports filtering by status (open, completed), assigned_user_id, related contact or deal, ' +
|
|
20
|
+
'overdue status, and date ranges (due_before, due_after). Results sorted by due_date (ascending), ' +
|
|
21
|
+
'then created_at (descending). Overdue tasks are flagged in the response.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
status: { type: 'string', enum: ['open', 'completed'], description: 'Filter by task status' },
|
|
26
|
+
assigned_user_id: { type: 'string', description: 'Filter by assigned user ID (UUID)' },
|
|
27
|
+
related_contact_id: { type: 'string', description: 'Filter by related contact ID (UUID)' },
|
|
28
|
+
related_deal_id: { type: 'string', description: 'Filter by related deal ID (UUID)' },
|
|
29
|
+
is_overdue: { type: 'boolean', description: 'Filter by overdue status' },
|
|
30
|
+
due_before: { type: 'string', description: 'Filter tasks due before this date (ISO 8601)' },
|
|
31
|
+
due_after: { type: 'string', description: 'Filter tasks due after this date (ISO 8601)' },
|
|
32
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
33
|
+
limit: { type: 'number', description: 'Items per page (default: 20, max: 100)' },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'salesflare_tasks_create',
|
|
39
|
+
description: 'Create a new task in Salesflare CRM. Description is required. ' +
|
|
40
|
+
'Optional fields include due_date (ISO 8601), assigned_user_id, and related contact or deal ' +
|
|
41
|
+
'(specify only one, not both). Tasks are created with status "open" by default. ' +
|
|
42
|
+
'Tasks can be linked to either a contact or a deal, but not both.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
description: { type: 'string', description: 'Task description (required)' },
|
|
47
|
+
due_date: { type: 'string', description: 'Due date (ISO 8601 format)' },
|
|
48
|
+
assigned_user_id: { type: 'string', description: 'Assigned user ID (UUID)' },
|
|
49
|
+
related_contact_id: { type: 'string', description: 'Related contact ID (UUID, mutually exclusive with related_deal_id)' },
|
|
50
|
+
related_deal_id: { type: 'string', description: 'Related deal ID (UUID, mutually exclusive with related_contact_id)' },
|
|
51
|
+
status: { type: 'string', enum: ['open', 'completed'], description: 'Task status (default: open)' },
|
|
52
|
+
},
|
|
53
|
+
required: ['description'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'salesflare_tasks_update',
|
|
58
|
+
description: 'Update an existing task in Salesflare CRM. Requires task_id. ' +
|
|
59
|
+
'All other fields are optional - only provided fields will be updated. ' +
|
|
60
|
+
'Status can be changed to mark tasks complete or reopen them. ' +
|
|
61
|
+
'When marking complete, completed_at is set automatically.',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
task_id: { type: 'string', description: 'Task UUID to update' },
|
|
66
|
+
description: { type: 'string', description: 'Task description' },
|
|
67
|
+
due_date: { type: 'string', description: 'Due date (ISO 8601 format)' },
|
|
68
|
+
assigned_user_id: { type: 'string', description: 'Assigned user ID (UUID)' },
|
|
69
|
+
status: { type: 'string', enum: ['open', 'completed'], description: 'Task status' },
|
|
70
|
+
},
|
|
71
|
+
required: ['task_id'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Exported task tools array for unified registration
|
|
77
|
+
*/
|
|
78
|
+
export { taskTools };
|
|
79
|
+
/**
|
|
80
|
+
* Handle task tool calls
|
|
81
|
+
*
|
|
82
|
+
* @param client - Salesflare API client
|
|
83
|
+
* @param name - Tool name
|
|
84
|
+
* @param args - Tool arguments
|
|
85
|
+
* @returns Tool response
|
|
86
|
+
*/
|
|
87
|
+
export async function handleTasksTool(client, name, args) {
|
|
88
|
+
switch (name) {
|
|
89
|
+
case 'salesflare_tasks_list':
|
|
90
|
+
return await handleListTasks(client, args);
|
|
91
|
+
case 'salesflare_tasks_create':
|
|
92
|
+
return await handleCreateTask(client, args);
|
|
93
|
+
case 'salesflare_tasks_update':
|
|
94
|
+
return await handleUpdateTask(client, args);
|
|
95
|
+
default:
|
|
96
|
+
throw new SalesflareError({
|
|
97
|
+
code: ErrorCode.INVALID_INPUT,
|
|
98
|
+
message: `Unknown task tool: ${name}`,
|
|
99
|
+
retryable: false,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Register task-related tools with the MCP server
|
|
105
|
+
*
|
|
106
|
+
* @deprecated Use handleTasksTool instead for unified registration
|
|
107
|
+
* @param server - MCP Server instance
|
|
108
|
+
* @param client - Salesflare API client
|
|
109
|
+
*/
|
|
110
|
+
export function registerTasksTools(server, client) {
|
|
111
|
+
// This function is deprecated - use unified tool registration in server.ts
|
|
112
|
+
// Kept for backward compatibility
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if a task is overdue
|
|
116
|
+
* Task is overdue if status is 'open' and due_date is in the past
|
|
117
|
+
*/
|
|
118
|
+
function isTaskOverdue(status, dueDate) {
|
|
119
|
+
if (status !== 'open' || !dueDate) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const due = new Date(dueDate);
|
|
124
|
+
return due < now;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Format a date for display
|
|
128
|
+
*/
|
|
129
|
+
function formatDate(dateString) {
|
|
130
|
+
if (!dateString)
|
|
131
|
+
return 'No due date';
|
|
132
|
+
try {
|
|
133
|
+
const date = new Date(dateString);
|
|
134
|
+
return date.toLocaleDateString('en-US', {
|
|
135
|
+
month: 'short',
|
|
136
|
+
day: 'numeric',
|
|
137
|
+
year: 'numeric',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return dateString;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Handle salesflare_tasks_list tool call
|
|
146
|
+
* Implements filters per D-06 to D-12
|
|
147
|
+
*/
|
|
148
|
+
async function handleListTasks(client, params) {
|
|
149
|
+
// Build query parameters - only 'limit' is supported by API
|
|
150
|
+
const queryParams = {};
|
|
151
|
+
if (params.limit)
|
|
152
|
+
queryParams.limit = params.limit;
|
|
153
|
+
// Call Salesflare API - returns array directly, not { items: [...] }
|
|
154
|
+
const response = await client.get('/tasks', { params: queryParams });
|
|
155
|
+
// API returns array directly
|
|
156
|
+
const items = Array.isArray(response) ? response : [];
|
|
157
|
+
// Apply client-side filtering if requested
|
|
158
|
+
let filteredItems = items;
|
|
159
|
+
if (params.status) {
|
|
160
|
+
const targetStatus = params.status;
|
|
161
|
+
filteredItems = items.filter((item) => {
|
|
162
|
+
const itemStatus = item.status;
|
|
163
|
+
if (targetStatus === 'completed') {
|
|
164
|
+
return itemStatus === 'completed' || item.completed === true;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
return itemStatus !== 'completed' && item.completed !== true;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Transform to curated response format (D-13)
|
|
172
|
+
const tasks = filteredItems.map((item) => {
|
|
173
|
+
const status = item.status || (item.completed ? 'completed' : 'open');
|
|
174
|
+
const dueDate = item.due_date;
|
|
175
|
+
return {
|
|
176
|
+
id: item.id,
|
|
177
|
+
description: item.description,
|
|
178
|
+
status,
|
|
179
|
+
due_date: dueDate,
|
|
180
|
+
assigned_user: item.assigned_user ? {
|
|
181
|
+
id: item.assigned_user.id,
|
|
182
|
+
name: item.assigned_user.name,
|
|
183
|
+
} : undefined,
|
|
184
|
+
related_contact: item.contact ? {
|
|
185
|
+
id: item.contact.id,
|
|
186
|
+
name: item.contact.name,
|
|
187
|
+
} : undefined,
|
|
188
|
+
related_deal: item.deal ? {
|
|
189
|
+
id: item.deal.id,
|
|
190
|
+
name: item.deal.name,
|
|
191
|
+
} : undefined,
|
|
192
|
+
completed_at: item.completed_at,
|
|
193
|
+
is_overdue: isTaskOverdue(status, dueDate),
|
|
194
|
+
created_at: item.created_at,
|
|
195
|
+
updated_at: item.updated_at,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
const listResponse = {
|
|
199
|
+
tasks,
|
|
200
|
+
total: tasks.length,
|
|
201
|
+
page: 1,
|
|
202
|
+
limit: tasks.length,
|
|
203
|
+
has_more: false,
|
|
204
|
+
};
|
|
205
|
+
// Count overdue tasks
|
|
206
|
+
const overdueCount = tasks.filter(t => t.is_overdue).length;
|
|
207
|
+
// Generate human-readable summary (D-14, D-16)
|
|
208
|
+
let summaryText;
|
|
209
|
+
if (listResponse.tasks.length === 0) {
|
|
210
|
+
summaryText = 'No tasks found matching the criteria.';
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const filterInfo = params.status ? ` (filtered by status: ${params.status})` : '';
|
|
214
|
+
const overdueWarning = overdueCount > 0 ? ` ⚠️ ${overdueCount} overdue` : '';
|
|
215
|
+
summaryText = `Found ${listResponse.total} task(s)${filterInfo}.${overdueWarning}` +
|
|
216
|
+
` Showing ${listResponse.tasks.length} result(s).`;
|
|
217
|
+
// Show first 3 tasks
|
|
218
|
+
const taskPreviews = listResponse.tasks.slice(0, 3).map(task => {
|
|
219
|
+
const overduePrefix = task.is_overdue ? '⚠️ OVERDUE: ' : '';
|
|
220
|
+
return `${overduePrefix}${task.description} (due: ${formatDate(task.due_date)}, status: ${task.status})`;
|
|
221
|
+
});
|
|
222
|
+
summaryText += '\n\n' + taskPreviews.join('\n');
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
content: [
|
|
226
|
+
{ type: 'text', text: summaryText },
|
|
227
|
+
{ type: 'text', text: JSON.stringify(listResponse, null, 2) },
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Handle salesflare_tasks_create tool call
|
|
233
|
+
* Description is required per D-26
|
|
234
|
+
* Validates mutual exclusivity of contact vs deal per D-29
|
|
235
|
+
*/
|
|
236
|
+
async function handleCreateTask(client, params) {
|
|
237
|
+
// Validate mutual exclusivity per D-29
|
|
238
|
+
if (params.related_contact_id && params.related_deal_id) {
|
|
239
|
+
throw new SalesflareError({
|
|
240
|
+
code: ErrorCode.VALIDATION_ERROR,
|
|
241
|
+
message: 'Cannot link task to both contact and deal. Specify only one.',
|
|
242
|
+
details: { fields: ['related_contact_id', 'related_deal_id'] },
|
|
243
|
+
retryable: false,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Build request body
|
|
247
|
+
const requestBody = {
|
|
248
|
+
description: params.description,
|
|
249
|
+
// Note: API doesn't accept 'status' on create, defaults to 'open'
|
|
250
|
+
};
|
|
251
|
+
if (params.due_date)
|
|
252
|
+
requestBody.due_date = params.due_date;
|
|
253
|
+
if (params.assigned_user_id)
|
|
254
|
+
requestBody.assigned_user_id = params.assigned_user_id;
|
|
255
|
+
if (params.related_contact_id)
|
|
256
|
+
requestBody.contact_id = params.related_contact_id;
|
|
257
|
+
if (params.related_deal_id)
|
|
258
|
+
requestBody.deal_id = params.related_deal_id;
|
|
259
|
+
// Call Salesflare API to create task
|
|
260
|
+
const response = await client.post('/tasks', requestBody);
|
|
261
|
+
// Transform to curated response format (D-13)
|
|
262
|
+
const status = response.status;
|
|
263
|
+
const dueDate = response.due_date;
|
|
264
|
+
const task = {
|
|
265
|
+
id: response.id,
|
|
266
|
+
description: response.description,
|
|
267
|
+
status,
|
|
268
|
+
due_date: dueDate,
|
|
269
|
+
assigned_user: response.assigned_user ? {
|
|
270
|
+
id: response.assigned_user.id,
|
|
271
|
+
name: response.assigned_user.name,
|
|
272
|
+
} : undefined,
|
|
273
|
+
related_contact: response.contact ? {
|
|
274
|
+
id: response.contact.id,
|
|
275
|
+
name: response.contact.name,
|
|
276
|
+
} : undefined,
|
|
277
|
+
related_deal: response.deal ? {
|
|
278
|
+
id: response.deal.id,
|
|
279
|
+
name: response.deal.name,
|
|
280
|
+
} : undefined,
|
|
281
|
+
completed_at: response.completed_at,
|
|
282
|
+
is_overdue: isTaskOverdue(status, dueDate),
|
|
283
|
+
created_at: response.created_at,
|
|
284
|
+
updated_at: response.updated_at,
|
|
285
|
+
};
|
|
286
|
+
// Generate human-readable summary (D-14, D-16)
|
|
287
|
+
let summaryText = `✓ Task created: ${task.description}`;
|
|
288
|
+
if (task.due_date) {
|
|
289
|
+
summaryText += ` (due: ${formatDate(task.due_date)})`;
|
|
290
|
+
}
|
|
291
|
+
if (task.assigned_user) {
|
|
292
|
+
summaryText += ` assigned to ${task.assigned_user.name}`;
|
|
293
|
+
}
|
|
294
|
+
if (task.related_contact) {
|
|
295
|
+
summaryText += ` linked to contact: ${task.related_contact.name}`;
|
|
296
|
+
}
|
|
297
|
+
if (task.related_deal) {
|
|
298
|
+
summaryText += ` linked to deal: ${task.related_deal.name}`;
|
|
299
|
+
}
|
|
300
|
+
if (task.is_overdue) {
|
|
301
|
+
summaryText += ' ⚠️ OVERDUE';
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
content: [
|
|
305
|
+
{ type: 'text', text: summaryText },
|
|
306
|
+
{ type: 'text', text: JSON.stringify(task, null, 2) },
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Transform API response to curated Task format
|
|
312
|
+
* Shared transformation logic for create and update
|
|
313
|
+
*/
|
|
314
|
+
function transformTaskResponse(response) {
|
|
315
|
+
const status = response.status;
|
|
316
|
+
const dueDate = response.due_date;
|
|
317
|
+
return {
|
|
318
|
+
id: response.id,
|
|
319
|
+
description: response.description,
|
|
320
|
+
status,
|
|
321
|
+
due_date: dueDate,
|
|
322
|
+
assigned_user: response.assigned_user ? {
|
|
323
|
+
id: response.assigned_user.id,
|
|
324
|
+
name: response.assigned_user.name,
|
|
325
|
+
} : undefined,
|
|
326
|
+
related_contact: response.contact ? {
|
|
327
|
+
id: response.contact.id,
|
|
328
|
+
name: response.contact.name,
|
|
329
|
+
} : undefined,
|
|
330
|
+
related_deal: response.deal ? {
|
|
331
|
+
id: response.deal.id,
|
|
332
|
+
name: response.deal.name,
|
|
333
|
+
} : undefined,
|
|
334
|
+
completed_at: response.completed_at,
|
|
335
|
+
is_overdue: isTaskOverdue(status, dueDate),
|
|
336
|
+
created_at: response.created_at,
|
|
337
|
+
updated_at: response.updated_at,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Handle salesflare_tasks_update tool call
|
|
342
|
+
* Partial update per D-25: only provided fields will be updated
|
|
343
|
+
* Status changes trigger completed_at updates
|
|
344
|
+
*/
|
|
345
|
+
async function handleUpdateTask(client, params) {
|
|
346
|
+
const { task_id, ...updateData } = params;
|
|
347
|
+
// Validate task_id is provided
|
|
348
|
+
if (!task_id) {
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{ type: 'text', text: 'Error [INVALID_INPUT]: task_id is required.' },
|
|
352
|
+
{ type: 'text', text: '{}' },
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// Check if at least one field is provided for update
|
|
357
|
+
const hasUpdateFields = Object.keys(updateData).length > 0;
|
|
358
|
+
if (!hasUpdateFields) {
|
|
359
|
+
return {
|
|
360
|
+
content: [
|
|
361
|
+
{ type: 'text', text: 'Error [INVALID_INPUT]: At least one field must be provided for update.' },
|
|
362
|
+
{ type: 'text', text: '{}' },
|
|
363
|
+
],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// Build PATCH body
|
|
367
|
+
const patchBody = {};
|
|
368
|
+
if (updateData.description !== undefined)
|
|
369
|
+
patchBody.description = updateData.description;
|
|
370
|
+
if (updateData.due_date !== undefined)
|
|
371
|
+
patchBody.due_date = updateData.due_date;
|
|
372
|
+
if (updateData.assigned_user_id !== undefined)
|
|
373
|
+
patchBody.assigned_user_id = updateData.assigned_user_id;
|
|
374
|
+
if (updateData.status !== undefined) {
|
|
375
|
+
patchBody.status = updateData.status;
|
|
376
|
+
// Handle completed_at based on status change
|
|
377
|
+
if (updateData.status === 'completed') {
|
|
378
|
+
patchBody.completed_at = new Date().toISOString();
|
|
379
|
+
}
|
|
380
|
+
else if (updateData.status === 'open') {
|
|
381
|
+
// Clear completed_at when reopening
|
|
382
|
+
patchBody.completed_at = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Call Salesflare API to update task
|
|
386
|
+
const response = await client.patch(`/tasks/${task_id}`, patchBody);
|
|
387
|
+
// Transform to curated response format
|
|
388
|
+
const task = transformTaskResponse(response);
|
|
389
|
+
// Generate human-readable summary
|
|
390
|
+
let summaryText = `✓ Task updated: ${task.description}`;
|
|
391
|
+
if (updateData.status !== undefined) {
|
|
392
|
+
summaryText += ` (status: ${updateData.status})`;
|
|
393
|
+
if (updateData.status === 'completed' && task.completed_at) {
|
|
394
|
+
summaryText += ` completed at ${formatDate(task.completed_at)}`;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (task.is_overdue) {
|
|
398
|
+
summaryText += ' ⚠️ OVERDUE';
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{ type: 'text', text: summaryText },
|
|
403
|
+
{ type: 'text', text: JSON.stringify(task, null, 2) },
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Streamable transport implementation for MCP protocol communication
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP transport with:
|
|
5
|
+
* - Express.js server with CORS support
|
|
6
|
+
* - Health endpoint at GET /health
|
|
7
|
+
* - MCP endpoint at POST /mcp
|
|
8
|
+
* - API key authentication middleware
|
|
9
|
+
* - Request logging to stderr
|
|
10
|
+
* - Graceful shutdown handling
|
|
11
|
+
*
|
|
12
|
+
* Per D-09: Uses Express.js framework
|
|
13
|
+
* Per D-10: Includes CORS, health endpoint, API key validation
|
|
14
|
+
* Per D-11: MCP endpoint at POST /mcp
|
|
15
|
+
* Per D-12: Uses same SALESFLARE_API_KEY as stdio mode
|
|
16
|
+
*
|
|
17
|
+
* @module transport/http-transport
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for HTTP transport
|
|
21
|
+
*/
|
|
22
|
+
export interface HttpTransportConfig {
|
|
23
|
+
/** Port to listen on (default: 3000) */
|
|
24
|
+
port?: number;
|
|
25
|
+
/** Host to bind to (default: localhost) */
|
|
26
|
+
host?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create and start the HTTP transport server
|
|
30
|
+
*
|
|
31
|
+
* @param config - HTTP transport configuration
|
|
32
|
+
* @returns Promise resolving to the HTTP server instance
|
|
33
|
+
* @throws SalesflareError if server fails to start
|
|
34
|
+
*/
|
|
35
|
+
export declare function createHttpTransport(config?: HttpTransportConfig): Promise<void>;
|
|
36
|
+
//# sourceMappingURL=http-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-transport.d.ts","sourceRoot":"","sources":["../../src/transport/http-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA6DD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+GzF"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Streamable transport implementation for MCP protocol communication
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP transport with:
|
|
5
|
+
* - Express.js server with CORS support
|
|
6
|
+
* - Health endpoint at GET /health
|
|
7
|
+
* - MCP endpoint at POST /mcp
|
|
8
|
+
* - API key authentication middleware
|
|
9
|
+
* - Request logging to stderr
|
|
10
|
+
* - Graceful shutdown handling
|
|
11
|
+
*
|
|
12
|
+
* Per D-09: Uses Express.js framework
|
|
13
|
+
* Per D-10: Includes CORS, health endpoint, API key validation
|
|
14
|
+
* Per D-11: MCP endpoint at POST /mcp
|
|
15
|
+
* Per D-12: Uses same SALESFLARE_API_KEY as stdio mode
|
|
16
|
+
*
|
|
17
|
+
* @module transport/http-transport
|
|
18
|
+
*/
|
|
19
|
+
import express from 'express';
|
|
20
|
+
import cors from 'cors';
|
|
21
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
22
|
+
import { createServer } from '../server.js';
|
|
23
|
+
import { SalesflareError, ErrorCode } from '../utils/errors.js';
|
|
24
|
+
/**
|
|
25
|
+
* API key middleware - validates Bearer token
|
|
26
|
+
*
|
|
27
|
+
* Per D-10, D-12: Validates API key from Authorization header
|
|
28
|
+
*/
|
|
29
|
+
function apiKeyMiddleware(req, res, next) {
|
|
30
|
+
const authHeader = req.headers.authorization;
|
|
31
|
+
if (!authHeader) {
|
|
32
|
+
res.status(401).json({
|
|
33
|
+
error: 'Unauthorized',
|
|
34
|
+
message: 'Authorization header required',
|
|
35
|
+
fix: 'Provide Authorization: Bearer <api_key> header',
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Validate Bearer token format
|
|
40
|
+
const parts = authHeader.split(' ');
|
|
41
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
|
|
42
|
+
res.status(401).json({
|
|
43
|
+
error: 'Unauthorized',
|
|
44
|
+
message: 'Invalid Authorization header format. Use: Bearer <api_key>',
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const token = parts[1];
|
|
49
|
+
if (!token || token.length === 0) {
|
|
50
|
+
res.status(401).json({
|
|
51
|
+
error: 'Unauthorized',
|
|
52
|
+
message: 'API key cannot be empty',
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Attach token to request for downstream use
|
|
57
|
+
req.authToken = token;
|
|
58
|
+
next();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Request logging middleware - logs to stderr
|
|
62
|
+
*
|
|
63
|
+
* Per D-15: Consistent with Phase 1 logging (stderr only)
|
|
64
|
+
*/
|
|
65
|
+
function loggingMiddleware(req, res, next) {
|
|
66
|
+
const timestamp = new Date().toISOString();
|
|
67
|
+
console.error(`[${timestamp}] ${req.method} ${req.path} - ${req.ip || 'unknown'}`);
|
|
68
|
+
next();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create and start the HTTP transport server
|
|
72
|
+
*
|
|
73
|
+
* @param config - HTTP transport configuration
|
|
74
|
+
* @returns Promise resolving to the HTTP server instance
|
|
75
|
+
* @throws SalesflareError if server fails to start
|
|
76
|
+
*/
|
|
77
|
+
export async function createHttpTransport(config = {}) {
|
|
78
|
+
const port = config.port || parseInt(process.env.PORT || '3000', 10);
|
|
79
|
+
const host = config.host || process.env.HOST || 'localhost';
|
|
80
|
+
// Create Express app
|
|
81
|
+
const app = express();
|
|
82
|
+
// Enable CORS (configurable per D-10)
|
|
83
|
+
app.use(cors({
|
|
84
|
+
origin: process.env.CORS_ORIGIN || '*',
|
|
85
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
86
|
+
allowedHeaders: ['Authorization', 'Content-Type'],
|
|
87
|
+
}));
|
|
88
|
+
// Parse JSON bodies
|
|
89
|
+
app.use(express.json());
|
|
90
|
+
// Request logging to stderr
|
|
91
|
+
app.use(loggingMiddleware);
|
|
92
|
+
// Health endpoint per D-10
|
|
93
|
+
app.get('/health', (_req, res) => {
|
|
94
|
+
res.json({
|
|
95
|
+
status: 'ok',
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
transport: 'http',
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
// MCP endpoint at POST /mcp per D-11
|
|
101
|
+
app.post('/mcp', apiKeyMiddleware, async (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
// Create MCP server with validated auth token
|
|
104
|
+
// The token was validated by middleware, now set it for the server
|
|
105
|
+
process.env.SALESFLARE_API_KEY = req.authToken;
|
|
106
|
+
const server = createServer({ transport: 'http' });
|
|
107
|
+
// Create streamable HTTP transport
|
|
108
|
+
const transport = new StreamableHTTPServerTransport({
|
|
109
|
+
sessionIdGenerator: () => `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
110
|
+
});
|
|
111
|
+
// Connect server to transport
|
|
112
|
+
await server.connect(transport);
|
|
113
|
+
// Handle the request
|
|
114
|
+
await transport.handleRequest(req, res);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('MCP request error:', error);
|
|
118
|
+
if (!res.headersSent) {
|
|
119
|
+
res.status(500).json({
|
|
120
|
+
error: 'Internal Server Error',
|
|
121
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// 404 handler for undefined routes
|
|
127
|
+
app.use((_req, res) => {
|
|
128
|
+
res.status(404).json({
|
|
129
|
+
error: 'Not Found',
|
|
130
|
+
message: 'Endpoint not found. Use POST /mcp for MCP requests or GET /health for health checks.',
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// Start server
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const server = app.listen(port, host, () => {
|
|
136
|
+
console.error(`HTTP transport listening on http://${host}:${port}`);
|
|
137
|
+
console.error(`Health endpoint: http://${host}:${port}/health`);
|
|
138
|
+
console.error(`MCP endpoint: http://${host}:${port}/mcp`);
|
|
139
|
+
});
|
|
140
|
+
// Handle server errors
|
|
141
|
+
server.on('error', (error) => {
|
|
142
|
+
if (error.code === 'EADDRINUSE') {
|
|
143
|
+
reject(new SalesflareError({
|
|
144
|
+
code: ErrorCode.CONFIG_ERROR,
|
|
145
|
+
message: `Port ${port} is already in use`,
|
|
146
|
+
fix: `Choose a different port with PORT environment variable or stop the process using port ${port}`,
|
|
147
|
+
retryable: false,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
reject(new SalesflareError({
|
|
152
|
+
code: ErrorCode.SERVER_ERROR,
|
|
153
|
+
message: `Failed to start HTTP server: ${error.message}`,
|
|
154
|
+
retryable: false,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// Graceful shutdown
|
|
159
|
+
const shutdown = (signal) => {
|
|
160
|
+
console.error(`Shutting down HTTP server... (${signal})`);
|
|
161
|
+
server.close(() => {
|
|
162
|
+
console.error('HTTP server closed');
|
|
163
|
+
process.exit(0);
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
167
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
168
|
+
// Resolve when server is listening
|
|
169
|
+
server.on('listening', () => {
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio transport implementation for MCP protocol communication
|
|
3
|
+
*
|
|
4
|
+
* Handles MCP protocol communication over stdin/stdout with proper error handling
|
|
5
|
+
* and graceful shutdown. All logging goes to stderr to avoid protocol corruption.
|
|
6
|
+
*
|
|
7
|
+
* @module transport/stdio-transport
|
|
8
|
+
*/
|
|
9
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for stdio transport
|
|
13
|
+
*/
|
|
14
|
+
export interface StdioTransportConfig {
|
|
15
|
+
/** The MCP server instance to connect */
|
|
16
|
+
server: Server;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a new stdio transport instance
|
|
20
|
+
*
|
|
21
|
+
* @param config - Transport configuration with server instance
|
|
22
|
+
* @returns Configured StdioServerTransport instance
|
|
23
|
+
* @throws SalesflareError if stdio is not available
|
|
24
|
+
*/
|
|
25
|
+
export declare function createStdioTransport(config: StdioTransportConfig): StdioServerTransport;
|
|
26
|
+
/**
|
|
27
|
+
* Start the MCP server with stdio transport
|
|
28
|
+
*
|
|
29
|
+
* Connects the server to stdio transport, sets up graceful shutdown,
|
|
30
|
+
* and logs status to stderr.
|
|
31
|
+
*
|
|
32
|
+
* @param config - Transport configuration with server instance
|
|
33
|
+
* @returns Promise that resolves when server is connected and ready
|
|
34
|
+
* @throws SalesflareError if connection fails
|
|
35
|
+
*/
|
|
36
|
+
export declare function startStdioServer(config: StdioTransportConfig): Promise<void>;
|
|
37
|
+
//# sourceMappingURL=stdio-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-transport.d.ts","sourceRoot":"","sources":["../../src/transport/stdio-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAGjF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CA8BvF;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuClF"}
|