@app-connect/core 1.7.8 → 1.7.11
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/connector/developerPortal.js +43 -0
- package/connector/proxy/index.js +10 -3
- package/connector/registry.js +8 -6
- package/handlers/admin.js +44 -21
- package/handlers/auth.js +97 -69
- package/handlers/calldown.js +10 -4
- package/handlers/contact.js +45 -112
- package/handlers/disposition.js +4 -142
- package/handlers/log.js +174 -259
- package/handlers/user.js +19 -6
- package/index.js +310 -122
- package/lib/analytics.js +3 -1
- package/lib/authSession.js +68 -0
- package/lib/callLogComposer.js +498 -420
- package/lib/errorHandler.js +206 -0
- package/lib/jwt.js +2 -0
- package/lib/logger.js +190 -0
- package/lib/oauth.js +21 -12
- package/lib/ringcentral.js +2 -10
- package/lib/sharedSMSComposer.js +471 -0
- package/mcp/SupportedPlatforms.md +12 -0
- package/mcp/lib/validator.js +91 -0
- package/mcp/mcpHandler.js +166 -0
- package/mcp/tools/checkAuthStatus.js +90 -0
- package/mcp/tools/collectAuthInfo.js +86 -0
- package/mcp/tools/createCallLog.js +299 -0
- package/mcp/tools/createMessageLog.js +283 -0
- package/mcp/tools/doAuth.js +185 -0
- package/mcp/tools/findContactByName.js +87 -0
- package/mcp/tools/findContactByPhone.js +96 -0
- package/mcp/tools/getCallLog.js +98 -0
- package/mcp/tools/getHelp.js +39 -0
- package/mcp/tools/getPublicConnectors.js +46 -0
- package/mcp/tools/index.js +58 -0
- package/mcp/tools/logout.js +63 -0
- package/mcp/tools/rcGetCallLogs.js +73 -0
- package/mcp/tools/setConnector.js +64 -0
- package/mcp/tools/updateCallLog.js +122 -0
- package/models/accountDataModel.js +34 -0
- package/models/cacheModel.js +3 -0
- package/package.json +6 -4
- package/releaseNotes.json +36 -0
- package/test/connector/registry.test.js +145 -0
- package/test/handlers/admin.test.js +583 -0
- package/test/handlers/auth.test.js +355 -0
- package/test/handlers/contact.test.js +852 -0
- package/test/handlers/log.test.js +872 -0
- package/test/lib/callLogComposer.test.js +1231 -0
- package/test/lib/debugTracer.test.js +328 -0
- package/test/lib/logger.test.js +206 -0
- package/test/lib/oauth.test.js +359 -0
- package/test/lib/ringcentral.test.js +473 -0
- package/test/lib/sharedSMSComposer.test.js +1084 -0
- package/test/lib/util.test.js +282 -0
- package/test/mcp/tools/collectAuthInfo.test.js +192 -0
- package/test/mcp/tools/createCallLog.test.js +412 -0
- package/test/mcp/tools/createMessageLog.test.js +580 -0
- package/test/mcp/tools/doAuth.test.js +363 -0
- package/test/mcp/tools/findContactByName.test.js +263 -0
- package/test/mcp/tools/findContactByPhone.test.js +284 -0
- package/test/mcp/tools/getCallLog.test.js +286 -0
- package/test/mcp/tools/getPublicConnectors.test.js +128 -0
- package/test/mcp/tools/logout.test.js +169 -0
- package/test/mcp/tools/setConnector.test.js +177 -0
- package/test/mcp/tools/updateCallLog.test.js +346 -0
- package/test/models/accountDataModel.test.js +98 -0
- package/test/models/dynamo/connectorSchema.test.js +189 -0
- package/test/models/models.test.js +539 -0
- package/test/setup.js +176 -176
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server for RC Unified CRM Extension
|
|
3
|
+
*
|
|
4
|
+
* This module provides MCP (Model Context Protocol) interface for the CRM extension.
|
|
5
|
+
* It exposes tools that can be called by AI assistants or other MCP clients.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const tools = require('./tools');
|
|
9
|
+
const logger = require('../lib/logger');
|
|
10
|
+
|
|
11
|
+
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
12
|
+
const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
13
|
+
|
|
14
|
+
async function handleMcpRequest(req, res) {
|
|
15
|
+
try {
|
|
16
|
+
const { method, params, id } = req.body;
|
|
17
|
+
logger.info('Received MCP request:', { method });
|
|
18
|
+
|
|
19
|
+
let response;
|
|
20
|
+
|
|
21
|
+
switch (method) {
|
|
22
|
+
case 'initialize':
|
|
23
|
+
response = {
|
|
24
|
+
jsonrpc: '2.0',
|
|
25
|
+
id,
|
|
26
|
+
result: {
|
|
27
|
+
protocolVersion: '2024-11-05',
|
|
28
|
+
capabilities: {
|
|
29
|
+
tools: {},
|
|
30
|
+
prompts: {}
|
|
31
|
+
},
|
|
32
|
+
serverInfo: {
|
|
33
|
+
name: 'rc-unified-crm-extension',
|
|
34
|
+
version: '1.0.0'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
break;
|
|
39
|
+
case 'tools/list':
|
|
40
|
+
response = {
|
|
41
|
+
jsonrpc: '2.0',
|
|
42
|
+
id,
|
|
43
|
+
result: {
|
|
44
|
+
tools: getTools()
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
break;
|
|
48
|
+
case 'tools/call':
|
|
49
|
+
const { name: toolName, arguments: args } = params;
|
|
50
|
+
const rcAccessToken = req.headers['authorization']?.split('Bearer ')?.[1];
|
|
51
|
+
if (args && rcAccessToken) {
|
|
52
|
+
args.rcAccessToken = rcAccessToken;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const result = await executeTool(toolName, args || {});
|
|
56
|
+
response = {
|
|
57
|
+
jsonrpc: '2.0',
|
|
58
|
+
id,
|
|
59
|
+
result: {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: JSON.stringify(result, null, 2)
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
} catch (toolError) {
|
|
69
|
+
response = {
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
id,
|
|
72
|
+
error: {
|
|
73
|
+
code: JSON_RPC_INTERNAL_ERROR,
|
|
74
|
+
message: `Tool execution failed: ${toolError.message}`,
|
|
75
|
+
data: {
|
|
76
|
+
error: toolError.message,
|
|
77
|
+
stack: process.env.NODE_ENV !== 'production' ? toolError.stack : undefined
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'ping':
|
|
84
|
+
response = {
|
|
85
|
+
jsonrpc: '2.0',
|
|
86
|
+
id,
|
|
87
|
+
result: {}
|
|
88
|
+
};
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
response = {
|
|
92
|
+
jsonrpc: '2.0',
|
|
93
|
+
id,
|
|
94
|
+
error: {
|
|
95
|
+
code: JSON_RPC_METHOD_NOT_FOUND,
|
|
96
|
+
message: `Method not found: ${method}`
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res.status(200).json(response);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error('Error handling MCP request:', { stack: error.stack });
|
|
104
|
+
const errorResponse = {
|
|
105
|
+
jsonrpc: '2.0',
|
|
106
|
+
id: req.body?.id || null,
|
|
107
|
+
error: {
|
|
108
|
+
code: JSON_RPC_INTERNAL_ERROR,
|
|
109
|
+
message: 'Internal server error',
|
|
110
|
+
data: {
|
|
111
|
+
error: error.message,
|
|
112
|
+
stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
res.status(200).json(errorResponse);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all registered MCP tools
|
|
123
|
+
* @returns {Array} Array of tool definitions
|
|
124
|
+
*/
|
|
125
|
+
function getTools() {
|
|
126
|
+
return tools.tools.map(tool => tool.definition);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Execute a specific MCP tool
|
|
131
|
+
* @param {string} toolName - Name of the tool to execute
|
|
132
|
+
* @param {Object} args - Arguments to pass to the tool
|
|
133
|
+
* @returns {Promise<Object>} Tool execution result
|
|
134
|
+
*/
|
|
135
|
+
async function executeTool(toolName, args) {
|
|
136
|
+
// Find the tool by name
|
|
137
|
+
const tool = tools.tools.find(t => t.definition.name === toolName);
|
|
138
|
+
|
|
139
|
+
if (!tool) {
|
|
140
|
+
throw new Error(`Tool not found: ${toolName}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Execute the tool
|
|
144
|
+
return await tool.execute(args);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get a specific tool definition
|
|
149
|
+
* @param {string} toolName - Name of the tool
|
|
150
|
+
* @returns {Object} Tool definition
|
|
151
|
+
*/
|
|
152
|
+
function getToolDefinition(toolName) {
|
|
153
|
+
const tool = tools.tools.find(t => t.definition.name === toolName);
|
|
154
|
+
|
|
155
|
+
if (!tool) {
|
|
156
|
+
throw new Error(`Tool not found: ${toolName}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return tool.definition;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
exports.handleMcpRequest = handleMcpRequest;
|
|
163
|
+
exports.getTools = getTools;
|
|
164
|
+
exports.executeTool = executeTool;
|
|
165
|
+
exports.getToolDefinition = getToolDefinition;
|
|
166
|
+
exports.tools = tools.tools;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const { getAuthSession } = require('../../lib/authSession');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Tool: Check Auth Status
|
|
5
|
+
*
|
|
6
|
+
* Polls the status of an ongoing OAuth authentication session
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const toolDefinition = {
|
|
10
|
+
name: 'checkAuthStatus',
|
|
11
|
+
description: 'Auth flow step.5. Check the status of an ongoing OAuth authentication session. Poll this after user clicks the auth link.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
sessionId: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'The session ID returned from doAuth tool'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
required: ['sessionId']
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute the checkAuthStatus tool
|
|
26
|
+
* @param {Object} args - The tool arguments
|
|
27
|
+
* @param {string} args.sessionId - The session ID to check
|
|
28
|
+
* @returns {Object} Result object with authentication status
|
|
29
|
+
*/
|
|
30
|
+
async function execute(args) {
|
|
31
|
+
try {
|
|
32
|
+
const { sessionId } = args;
|
|
33
|
+
|
|
34
|
+
const session = await getAuthSession(sessionId);
|
|
35
|
+
|
|
36
|
+
if (!session) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: 'Session not found or expired',
|
|
40
|
+
data: {
|
|
41
|
+
status: 'expired'
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
switch (session.status) {
|
|
47
|
+
case 'completed':
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
data: {
|
|
51
|
+
status: 'completed',
|
|
52
|
+
jwtToken: session.jwtToken,
|
|
53
|
+
userInfo: session.userInfo,
|
|
54
|
+
message: 'IMPORTANT: Authentication successful! Keep jwtToken in memory for future use.'
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
case 'failed':
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: 'Authentication failed',
|
|
62
|
+
data: {
|
|
63
|
+
status: 'failed',
|
|
64
|
+
errorMessage: session.errorMessage
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
case 'pending':
|
|
69
|
+
default:
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
data: {
|
|
73
|
+
status: 'pending',
|
|
74
|
+
message: 'Waiting for user to complete authorization. Poll again in a few seconds.'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: error.message || 'Unknown error occurred',
|
|
83
|
+
errorDetails: error.stack
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
exports.definition = toolDefinition;
|
|
89
|
+
exports.execute = execute;
|
|
90
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool: User Authentication
|
|
3
|
+
*
|
|
4
|
+
* This tool authenticates the user with the CRM platform.
|
|
5
|
+
* It uses the platform-specific connector to authenticate the user.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { isManifestValid } = require('../lib/validator');
|
|
9
|
+
|
|
10
|
+
const toolDefinition = {
|
|
11
|
+
name: 'collectAuthInfo',
|
|
12
|
+
description: '(This step is skipped if auth type is "apiKey" or environment type is "fixed", this is a MUST if environment type is "dynamic" or "selectable") Auth flow step.3. Get information that is required for authentication. Next step is calling step.4 "doAuth" tool.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
connectorManifest: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
description: 'connectorManifest variable from above conversation. Must be the full manifest object, not just serverUrl'
|
|
19
|
+
},
|
|
20
|
+
hostname: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'For "dynamic" type environment. User is to login to CRM account then copy and paste the hostname over here.'
|
|
23
|
+
},
|
|
24
|
+
selection: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'For "selectable" type environment. User is to select a name (NOT value) of the options from the selectable list'
|
|
27
|
+
},
|
|
28
|
+
connectorName: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'connectorName variable from above conversation.'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
required: ['connectorManifest', 'connectorName']
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute the collectAuthInfo tool
|
|
39
|
+
* @param {Object} args - The tool arguments
|
|
40
|
+
* @param {string} args.connectorManifest - Connector manifest from conversation or memory.
|
|
41
|
+
* @param {string} args.hostname - For "dynamic" type environment. User is to login to CRM account then copy and paste the hostname over here.
|
|
42
|
+
* @param {string} args.selection - For "selectable" type environment. User is to select a name (NOT value) of the options from the selectable list
|
|
43
|
+
* @param {string} args.connectorName - Connector name from conversation or memory.
|
|
44
|
+
* @returns {Object} Result object with hostname or selection
|
|
45
|
+
*/
|
|
46
|
+
async function execute(args) {
|
|
47
|
+
try {
|
|
48
|
+
const { connectorManifest, hostname, selection, connectorName } = args;
|
|
49
|
+
const { isValid, errors } = isManifestValid({ connectorManifest, connectorName });
|
|
50
|
+
if (!isValid) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: "Invalid connector manifest",
|
|
54
|
+
errorDetails: errors.join(', '),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let result = '';
|
|
58
|
+
switch (connectorManifest.platforms[connectorName].environment.type) {
|
|
59
|
+
case 'selectable':
|
|
60
|
+
result = connectorManifest.platforms[connectorName].environment.selections.find(s => s.name === selection).const;
|
|
61
|
+
break;
|
|
62
|
+
case 'dynamic':
|
|
63
|
+
result = hostname;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const url = new URL(result);
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
data: {
|
|
70
|
+
hostname: url.hostname,
|
|
71
|
+
// Add explicit instruction
|
|
72
|
+
message: "IMPORTANT: Use hostname in the next few authentication steps.",
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: error.message || 'Unknown error occurred',
|
|
80
|
+
errorDetails: error.stack
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
exports.definition = toolDefinition;
|
|
86
|
+
exports.execute = execute;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const jwt = require('../../lib/jwt');
|
|
2
|
+
const connectorRegistry = require('../../connector/registry');
|
|
3
|
+
const logCore = require('../../handlers/log');
|
|
4
|
+
const contactCore = require('../../handlers/contact');
|
|
5
|
+
const util = require('../../lib/util');
|
|
6
|
+
const { CallLogModel } = require('../../models/callLogModel');
|
|
7
|
+
/**
|
|
8
|
+
* MCP Tool: Create Call Log
|
|
9
|
+
*
|
|
10
|
+
* This tool creates a call log in the CRM platform.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const toolDefinition = {
|
|
14
|
+
name: 'createCallLog',
|
|
15
|
+
description: '⚠️ REQUIRES AUTHENTICATION: User must first authenticate using the "auth" tool to obtain a JWT token before using this tool. | Create only one call log in the CRM platform. Returns the created log ID if successful.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
jwtToken: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'JWT token containing userId and platform information. If user does not have this, direct them to use the "auth" tool first.'
|
|
22
|
+
},
|
|
23
|
+
incomingData: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
description: 'Call log data to create',
|
|
26
|
+
properties: {
|
|
27
|
+
logInfo: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
description: 'RingCentral call log information (follows RingCentral Call Log schema)',
|
|
30
|
+
properties: {
|
|
31
|
+
id: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Call log ID from RingCentral'
|
|
34
|
+
},
|
|
35
|
+
sessionId: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Unique session identifier for the call'
|
|
38
|
+
},
|
|
39
|
+
telephonySessionId: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Telephony session ID'
|
|
42
|
+
},
|
|
43
|
+
startTime: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Call start time in ISO 8601 format'
|
|
46
|
+
},
|
|
47
|
+
duration: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
description: 'Call duration in seconds'
|
|
50
|
+
},
|
|
51
|
+
type: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Call type (e.g., Voice)'
|
|
54
|
+
},
|
|
55
|
+
direction: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Call direction: "Inbound" or "Outbound"'
|
|
58
|
+
},
|
|
59
|
+
action: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Call action (e.g., Phone Call)'
|
|
62
|
+
},
|
|
63
|
+
result: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Call result (e.g., Accepted, Missed, Voicemail)'
|
|
66
|
+
},
|
|
67
|
+
to: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
description: 'Recipient information',
|
|
70
|
+
properties: {
|
|
71
|
+
phoneNumber: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Recipient phone number in E.164 format'
|
|
74
|
+
},
|
|
75
|
+
name: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Recipient name'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
from: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
description: 'Caller information',
|
|
84
|
+
properties: {
|
|
85
|
+
phoneNumber: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Caller phone number in E.164 format'
|
|
88
|
+
},
|
|
89
|
+
name: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Caller name'
|
|
92
|
+
},
|
|
93
|
+
location: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Caller location'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
recording: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
description: 'Recording information',
|
|
102
|
+
properties: {
|
|
103
|
+
link: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Recording link URL'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
customSubject: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: 'Custom subject for the call log'
|
|
112
|
+
},
|
|
113
|
+
legs: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
description: 'Call legs information (for multi-party calls)',
|
|
116
|
+
items: {
|
|
117
|
+
type: 'object'
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
accountId: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'RingCentral account ID'
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
required: ['id', 'sessionId', 'direction', 'startTime', 'duration', 'to', 'from']
|
|
126
|
+
},
|
|
127
|
+
note: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'User-entered call note/description'
|
|
130
|
+
},
|
|
131
|
+
aiNote: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'AI-generated summary of the phone call'
|
|
134
|
+
},
|
|
135
|
+
transcript: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
description: 'Call transcript text'
|
|
138
|
+
},
|
|
139
|
+
additionalSubmission: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
description: 'Additional platform-specific custom fields (e.g., deals, matters, nonBillable flags, assigned users)',
|
|
142
|
+
properties: {
|
|
143
|
+
isAssignedToUser: {
|
|
144
|
+
type: 'boolean',
|
|
145
|
+
description: 'Whether to assign to a specific user'
|
|
146
|
+
},
|
|
147
|
+
adminAssignedUserToken: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'JWT token of the assigned user'
|
|
150
|
+
},
|
|
151
|
+
adminAssignedUserRcId: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'RingCentral extension ID of the assigned user'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
required: ['logInfo']
|
|
159
|
+
},
|
|
160
|
+
contactId: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'OPTIONAL: CRM contact ID to associate with the call.'
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
required: ['jwtToken', 'incomingData']
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Execute the createCallLog tool
|
|
171
|
+
* @param {Object} args - The tool arguments
|
|
172
|
+
* @param {string} args.jwtToken - JWT token with user and platform info
|
|
173
|
+
* @param {Object} args.incomingData - Call log data including logInfo (RingCentral call log schema), contactName, contactType, note, aiNote, transcript, and additionalSubmission
|
|
174
|
+
* @param {Object} args.incomingData.logInfo - RingCentral call log information with sessionId, direction, startTime, duration, from, to, result, recording, customSubject, etc.
|
|
175
|
+
* @param {string} [args.incomingData.contactName] - Contact name
|
|
176
|
+
* @param {string} [args.incomingData.contactType] - Contact type in CRM
|
|
177
|
+
* @param {string} [args.incomingData.note] - User-entered call note
|
|
178
|
+
* @param {string} [args.incomingData.aiNote] - AI-generated call summary
|
|
179
|
+
* @param {string} [args.incomingData.transcript] - Call transcript
|
|
180
|
+
* @param {Object} [args.incomingData.additionalSubmission] - Platform-specific custom fields
|
|
181
|
+
* @param {string} args.contactId - OPTIONAL: CRM contact ID to associate with the call
|
|
182
|
+
* @returns {Object} Result object with created log ID
|
|
183
|
+
*/
|
|
184
|
+
async function execute(args) {
|
|
185
|
+
try {
|
|
186
|
+
const { jwtToken, incomingData } = args;
|
|
187
|
+
|
|
188
|
+
if (!jwtToken) {
|
|
189
|
+
throw new Error('Please go to Settings and authorize CRM platform');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!incomingData) {
|
|
193
|
+
throw new Error('Incoming data must be provided');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate logInfo exists
|
|
197
|
+
if (!incomingData.logInfo) {
|
|
198
|
+
throw new Error('incomingData.logInfo is required');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { logInfo } = incomingData;
|
|
202
|
+
|
|
203
|
+
// Check in DB if the call log already exists
|
|
204
|
+
const existingCallLog = await CallLogModel.findOne({
|
|
205
|
+
where: {
|
|
206
|
+
sessionId: logInfo.sessionId
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
if (existingCallLog) {
|
|
210
|
+
throw new Error(`Call log already exists for session ${logInfo.sessionId}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Decode JWT to get userId and platform
|
|
214
|
+
const { id: userId, platform } = jwt.decodeJwt(jwtToken);
|
|
215
|
+
|
|
216
|
+
if (!userId) {
|
|
217
|
+
throw new Error('Invalid JWT token: userId not found');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Get the platform connector module
|
|
221
|
+
const platformModule = connectorRegistry.getConnector(platform);
|
|
222
|
+
|
|
223
|
+
if (!platformModule) {
|
|
224
|
+
throw new Error(`Platform connector not found for: ${platform}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check if createCallLog is implemented
|
|
228
|
+
if (!platformModule.createCallLog) {
|
|
229
|
+
throw new Error(`createCallLog is not implemented for platform: ${platform}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Calculate hashed account ID
|
|
233
|
+
const hashedAccountId = incomingData.logInfo?.accountId
|
|
234
|
+
? util.getHashValue(incomingData.logInfo.accountId, process.env.HASH_KEY)
|
|
235
|
+
: undefined;
|
|
236
|
+
|
|
237
|
+
// Get contact Id from conversation
|
|
238
|
+
let contactId = args.contactId ?? null;
|
|
239
|
+
// Get contact Id from conversation if not provided
|
|
240
|
+
if (!contactId) {
|
|
241
|
+
const callDirection = logInfo.direction;
|
|
242
|
+
const contactNumber = callDirection === 'Inbound' ? logInfo.from.phoneNumber : logInfo.to.phoneNumber;
|
|
243
|
+
const contactInfo = await contactCore.findContact({
|
|
244
|
+
platform,
|
|
245
|
+
userId,
|
|
246
|
+
phoneNumber: contactNumber,
|
|
247
|
+
overridingFormat: '',
|
|
248
|
+
isExtension: 'false'
|
|
249
|
+
});
|
|
250
|
+
const filteredContact = contactInfo.contact?.filter(c => !c.isNewContact);
|
|
251
|
+
if (contactInfo.successful && filteredContact?.length > 0) {
|
|
252
|
+
contactId = filteredContact[0].id;
|
|
253
|
+
incomingData.contactId = contactId;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
error: 'Failed to get contact with number ' + contactNumber
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Call the createCallLog method
|
|
264
|
+
const { successful, logId, returnMessage } = await logCore.createCallLog({
|
|
265
|
+
platform,
|
|
266
|
+
userId,
|
|
267
|
+
incomingData,
|
|
268
|
+
hashedAccountId,
|
|
269
|
+
isFromSSCL: false
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (successful) {
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
data: {
|
|
276
|
+
logId,
|
|
277
|
+
message: returnMessage?.message || 'Call log created successfully'
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
error: returnMessage?.message || 'Failed to create call log',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
error: error.message || 'Unknown error occurred',
|
|
292
|
+
errorDetails: error.stack
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
exports.definition = toolDefinition;
|
|
298
|
+
exports.execute = execute;
|
|
299
|
+
|