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.
Files changed (73) hide show
  1. package/API.md +691 -0
  2. package/CHANGELOG.md +49 -0
  3. package/CLAUDE.md +117 -0
  4. package/CONTRIBUTING.md +399 -0
  5. package/FIX_PLAN.md +70 -0
  6. package/INSPECTOR.md +191 -0
  7. package/LICENSE +21 -0
  8. package/PUBLISH.md +73 -0
  9. package/README.md +383 -0
  10. package/dist/auth/api-key-auth.d.ts +75 -0
  11. package/dist/auth/api-key-auth.d.ts.map +1 -0
  12. package/dist/auth/api-key-auth.js +103 -0
  13. package/dist/auth/oauth-auth.d.ts +81 -0
  14. package/dist/auth/oauth-auth.d.ts.map +1 -0
  15. package/dist/auth/oauth-auth.js +123 -0
  16. package/dist/auth/token-manager.d.ts +105 -0
  17. package/dist/auth/token-manager.d.ts.map +1 -0
  18. package/dist/auth/token-manager.js +87 -0
  19. package/dist/client/salesflare-client.d.ts +219 -0
  20. package/dist/client/salesflare-client.d.ts.map +1 -0
  21. package/dist/client/salesflare-client.js +484 -0
  22. package/dist/index.d.ts +15 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +82 -0
  25. package/dist/server.d.ts +39 -0
  26. package/dist/server.d.ts.map +1 -0
  27. package/dist/server.js +140 -0
  28. package/dist/tools/companies.d.ts +45 -0
  29. package/dist/tools/companies.d.ts.map +1 -0
  30. package/dist/tools/companies.js +392 -0
  31. package/dist/tools/contacts.d.ts +45 -0
  32. package/dist/tools/contacts.d.ts.map +1 -0
  33. package/dist/tools/contacts.js +290 -0
  34. package/dist/tools/deals.d.ts +46 -0
  35. package/dist/tools/deals.d.ts.map +1 -0
  36. package/dist/tools/deals.js +442 -0
  37. package/dist/tools/pipeline.d.ts +43 -0
  38. package/dist/tools/pipeline.d.ts.map +1 -0
  39. package/dist/tools/pipeline.js +328 -0
  40. package/dist/tools/tasks.d.ts +44 -0
  41. package/dist/tools/tasks.d.ts.map +1 -0
  42. package/dist/tools/tasks.js +406 -0
  43. package/dist/transport/http-transport.d.ts +36 -0
  44. package/dist/transport/http-transport.d.ts.map +1 -0
  45. package/dist/transport/http-transport.js +173 -0
  46. package/dist/transport/stdio-transport.d.ts +37 -0
  47. package/dist/transport/stdio-transport.d.ts.map +1 -0
  48. package/dist/transport/stdio-transport.js +129 -0
  49. package/dist/types/company.d.ts +223 -0
  50. package/dist/types/company.d.ts.map +1 -0
  51. package/dist/types/company.js +8 -0
  52. package/dist/types/contact.d.ts +166 -0
  53. package/dist/types/contact.d.ts.map +1 -0
  54. package/dist/types/contact.js +8 -0
  55. package/dist/types/deal.d.ts +203 -0
  56. package/dist/types/deal.d.ts.map +1 -0
  57. package/dist/types/deal.js +8 -0
  58. package/dist/types/pipeline.d.ts +116 -0
  59. package/dist/types/pipeline.d.ts.map +1 -0
  60. package/dist/types/pipeline.js +8 -0
  61. package/dist/types/task.d.ts +154 -0
  62. package/dist/types/task.d.ts.map +1 -0
  63. package/dist/types/task.js +8 -0
  64. package/dist/utils/errors.d.ts +128 -0
  65. package/dist/utils/errors.d.ts.map +1 -0
  66. package/dist/utils/errors.js +205 -0
  67. package/dist/utils/validation.d.ts +354 -0
  68. package/dist/utils/validation.d.ts.map +1 -0
  69. package/dist/utils/validation.js +716 -0
  70. package/package.json +49 -0
  71. package/test-tasks-debug.js +21 -0
  72. package/test-tasks-params.js +52 -0
  73. 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"}