@telitask/mcp-server 0.1.1 → 0.1.3
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/dist/auth/token-store.d.ts.map +1 -1
- package/dist/auth/token-store.js +12 -8
- package/dist/index.js +40 -15
- package/dist/tools/calls.d.ts.map +1 -1
- package/dist/tools/calls.js +95 -71
- package/package.json +5 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAqBD,wBAAgB,eAAe,IAAI,cAAc,GAAG,IAAI,CAUvD;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,cAAc,GAAG,IAAI,CAOlE;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC"}
|
package/dist/auth/token-store.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
|
-
|
|
4
|
+
function getCredentialsPath() {
|
|
5
|
+
const dashboardUrl = process.env.TELITASK_DASHBOARD_URL ?? '';
|
|
6
|
+
const suffix = dashboardUrl.includes('staging') ? '-staging' : '';
|
|
7
|
+
return path.join(os.homedir(), '.telitask', `mcp-credentials${suffix}.json`);
|
|
8
|
+
}
|
|
5
9
|
function isValidCredentials(value) {
|
|
6
10
|
if (typeof value !== 'object' || value === null)
|
|
7
11
|
return false;
|
|
@@ -15,9 +19,9 @@ function isValidCredentials(value) {
|
|
|
15
19
|
}
|
|
16
20
|
export function readCredentials() {
|
|
17
21
|
try {
|
|
18
|
-
if (!fs.existsSync(
|
|
22
|
+
if (!fs.existsSync(getCredentialsPath()))
|
|
19
23
|
return null;
|
|
20
|
-
const raw = fs.readFileSync(
|
|
24
|
+
const raw = fs.readFileSync(getCredentialsPath(), 'utf-8');
|
|
21
25
|
const parsed = JSON.parse(raw);
|
|
22
26
|
if (!isValidCredentials(parsed))
|
|
23
27
|
return null;
|
|
@@ -28,15 +32,15 @@ export function readCredentials() {
|
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
export function writeCredentials(credentials) {
|
|
31
|
-
const dir = path.dirname(
|
|
35
|
+
const dir = path.dirname(getCredentialsPath());
|
|
32
36
|
if (!fs.existsSync(dir)) {
|
|
33
37
|
fs.mkdirSync(dir, { recursive: true });
|
|
34
38
|
}
|
|
35
|
-
fs.writeFileSync(
|
|
36
|
-
fs.chmodSync(
|
|
39
|
+
fs.writeFileSync(getCredentialsPath(), JSON.stringify(credentials, null, 2));
|
|
40
|
+
fs.chmodSync(getCredentialsPath(), 0o600);
|
|
37
41
|
}
|
|
38
42
|
export function clearCredentials() {
|
|
39
|
-
if (fs.existsSync(
|
|
40
|
-
fs.unlinkSync(
|
|
43
|
+
if (fs.existsSync(getCredentialsPath())) {
|
|
44
|
+
fs.unlinkSync(getCredentialsPath());
|
|
41
45
|
}
|
|
42
46
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { readCredentials } from './auth/token-store.js';
|
|
4
|
+
import { readCredentials, clearCredentials } from './auth/token-store.js';
|
|
5
5
|
import { runOAuthLogin } from './auth/oauth.js';
|
|
6
6
|
import { registerContactTools } from './tools/contacts.js';
|
|
7
7
|
import { registerTaskTools } from './tools/tasks.js';
|
|
@@ -20,21 +20,46 @@ async function runLogin() {
|
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
function registerAuthTools(server) {
|
|
24
|
+
const dashboardUrl = process.env.TELITASK_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
|
|
25
|
+
server.tool('login', 'Authenticate with TeliTask. Opens a browser for you to authorize. After login, use /mcp → Reconnect to load all tools.', {}, async () => {
|
|
26
|
+
try {
|
|
27
|
+
await runOAuthLogin(dashboardUrl);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: 'Authenticated successfully! Now use /mcp → Reconnect to load all tools.' }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text', text: `Authentication failed: ${err instanceof Error ? err.message : err}` }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
server.tool('logout', 'Clear stored credentials and log out. After calling this, use /mcp → Reconnect to re-authenticate.', {}, async () => {
|
|
39
|
+
clearCredentials();
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: 'Logged out. Credentials cleared.\n\nUse the `login` tool or /mcp → Reconnect to re-authenticate.' }],
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
23
45
|
async function runServer() {
|
|
24
46
|
const credentials = readCredentials();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'Use make_call to initiate calls, schedule_call for future calls. ' +
|
|
32
|
-
'When the user mentions a contact by name, use list_contacts to find them first.',
|
|
47
|
+
const server = new McpServer({ name: 'telitask', version: '0.1.1' }, {
|
|
48
|
+
instructions: credentials
|
|
49
|
+
? 'TeliTask MCP server — manage contacts, tasks, and make phone calls. ' +
|
|
50
|
+
'Use make_call to initiate calls, schedule_call for future calls. ' +
|
|
51
|
+
'When the user mentions a contact by name, use list_contacts to find them first.'
|
|
52
|
+
: 'TeliTask MCP server — not authenticated. Use the `login` tool to connect your TeliTask account.',
|
|
33
53
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
// Always register auth tools
|
|
55
|
+
registerAuthTools(server);
|
|
56
|
+
// Only register feature tools when authenticated
|
|
57
|
+
if (credentials) {
|
|
58
|
+
registerContactTools(server);
|
|
59
|
+
registerTaskTools(server);
|
|
60
|
+
registerCallTools(server);
|
|
61
|
+
registerResources(server);
|
|
62
|
+
}
|
|
38
63
|
const transport = new StdioServerTransport();
|
|
39
64
|
await server.connect(transport);
|
|
40
65
|
}
|
|
@@ -46,8 +71,8 @@ if (command === 'login') {
|
|
|
46
71
|
});
|
|
47
72
|
}
|
|
48
73
|
else if (command === 'logout') {
|
|
49
|
-
const { clearCredentials } = await import('./auth/token-store.js');
|
|
50
|
-
|
|
74
|
+
const { clearCredentials: clear } = await import('./auth/token-store.js');
|
|
75
|
+
clear();
|
|
51
76
|
console.error('Logged out. Credentials removed.');
|
|
52
77
|
}
|
|
53
78
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calls.d.ts","sourceRoot":"","sources":["../../src/tools/calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"calls.d.ts","sourceRoot":"","sources":["../../src/tools/calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4JzE,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAgF5C;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAwC5C;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAgC5C;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAuC5C;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAwC5C;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAyF5C;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6EzD"}
|
package/dist/tools/calls.js
CHANGED
|
@@ -1,58 +1,57 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { getAuthenticatedClient, refreshSessionIfNeeded, getUserId, getVoiceServerUrl, getAccessToken, } from '../lib/supabase.js';
|
|
3
3
|
import { escapeIlikePattern } from '../lib/utils.js';
|
|
4
|
-
const POLL_INTERVAL_MS =
|
|
4
|
+
const POLL_INTERVAL_MS = 10_000; // 10 seconds
|
|
5
5
|
const MAX_POLL_DURATION_MS = 10 * 60_000; // 10 minutes
|
|
6
|
-
|
|
6
|
+
const POST_COMPLETION_TIMEOUT_MS = 60_000; // 60 seconds to wait for summary after call ends
|
|
7
|
+
const TERMINAL_STATUSES = ['failed', 'missed', 'cancelled', 'no_answer', 'busy'];
|
|
8
|
+
async function waitForCallCompletion(callId) {
|
|
7
9
|
const supabase = getAuthenticatedClient();
|
|
8
10
|
const userId = getUserId();
|
|
9
11
|
const startTime = Date.now();
|
|
12
|
+
let completedSince = null;
|
|
10
13
|
while (Date.now() - startTime < MAX_POLL_DURATION_MS) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.eq('id', scheduledCallId)
|
|
14
|
+
const { data: call } = await supabase
|
|
15
|
+
.from('calls')
|
|
16
|
+
.select('status, call_summary, summary, transcript, duration_seconds')
|
|
17
|
+
.eq('id', callId)
|
|
16
18
|
.eq('user_id', userId)
|
|
17
19
|
.single();
|
|
18
|
-
if (!
|
|
20
|
+
if (!call)
|
|
19
21
|
return null;
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// Use whichever summary field is available (voice server writes to `summary`, on-behalf writes to `call_summary`)
|
|
23
|
+
const callSummary = call.call_summary || call.summary;
|
|
24
|
+
// Terminal non-completed statuses — return immediately with whatever data is available
|
|
25
|
+
if (TERMINAL_STATUSES.includes(call.status)) {
|
|
26
|
+
return {
|
|
27
|
+
status: call.status,
|
|
28
|
+
summary: callSummary,
|
|
29
|
+
transcript: call.transcript,
|
|
30
|
+
duration_seconds: call.duration_seconds,
|
|
31
|
+
};
|
|
23
32
|
}
|
|
24
|
-
//
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
.from('calls')
|
|
28
|
-
.select('status, call_summary, transcript, duration_seconds')
|
|
29
|
-
.eq('id', scheduledCall.triggered_call_id)
|
|
30
|
-
.eq('user_id', userId)
|
|
31
|
-
.single();
|
|
32
|
-
if (call && (call.status === 'failed' || call.status === 'no_answer')) {
|
|
33
|
+
// Call completed — wait up to 60s for summary/transcript to be generated
|
|
34
|
+
if (call.status === 'completed') {
|
|
35
|
+
if (callSummary && call.transcript) {
|
|
33
36
|
return {
|
|
34
37
|
status: call.status,
|
|
35
|
-
summary:
|
|
38
|
+
summary: callSummary,
|
|
36
39
|
transcript: call.transcript,
|
|
37
40
|
duration_seconds: call.duration_seconds,
|
|
38
|
-
triggered_call_id: scheduledCall.triggered_call_id,
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
// Summary not ready yet — poll faster since call is done
|
|
53
|
-
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
|
54
|
-
continue;
|
|
43
|
+
if (!completedSince)
|
|
44
|
+
completedSince = Date.now();
|
|
45
|
+
if (Date.now() - completedSince >= POST_COMPLETION_TIMEOUT_MS) {
|
|
46
|
+
return {
|
|
47
|
+
status: call.status,
|
|
48
|
+
summary: callSummary,
|
|
49
|
+
transcript: call.transcript,
|
|
50
|
+
duration_seconds: call.duration_seconds,
|
|
51
|
+
};
|
|
55
52
|
}
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
|
54
|
+
continue;
|
|
56
55
|
}
|
|
57
56
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
58
57
|
}
|
|
@@ -107,7 +106,17 @@ async function resolvePersonaId(explicitId, userId) {
|
|
|
107
106
|
.select('default_persona_id')
|
|
108
107
|
.eq('id', userId)
|
|
109
108
|
.single();
|
|
110
|
-
|
|
109
|
+
if (user?.default_persona_id)
|
|
110
|
+
return user.default_persona_id;
|
|
111
|
+
// Fallback: use first active persona if no default is set
|
|
112
|
+
const { data: personas } = await supabase
|
|
113
|
+
.from('user_personas')
|
|
114
|
+
.select('id')
|
|
115
|
+
.eq('user_id', userId)
|
|
116
|
+
.eq('is_active', true)
|
|
117
|
+
.order('created_at', { ascending: true })
|
|
118
|
+
.limit(1);
|
|
119
|
+
return personas?.[0]?.id ?? undefined;
|
|
111
120
|
}
|
|
112
121
|
export async function handleMakeCall(params) {
|
|
113
122
|
await refreshSessionIfNeeded();
|
|
@@ -121,49 +130,52 @@ export async function handleMakeCall(params) {
|
|
|
121
130
|
if (!personaId) {
|
|
122
131
|
return [{ type: 'text', text: 'No persona specified and no default persona set. Please provide a persona_id.' }];
|
|
123
132
|
}
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
// Format as local time (YYYY-MM-DDTHH:mm:ss) — the voice server expects local, not UTC
|
|
127
|
-
const d = new Date(Date.now() + 2 * 60_000);
|
|
128
|
-
const localTime = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}T${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
|
129
|
-
const response = await fetch(`${voiceServerUrl}/api/scheduled-calls`, {
|
|
133
|
+
// Initiate an on_behalf call instantly via the voice server
|
|
134
|
+
const response = await fetch(`${voiceServerUrl}/api/calls/initiate`, {
|
|
130
135
|
method: 'POST',
|
|
131
136
|
headers: {
|
|
132
137
|
'Content-Type': 'application/json',
|
|
133
138
|
Authorization: `Bearer ${accessToken}`,
|
|
134
139
|
},
|
|
135
140
|
body: JSON.stringify({
|
|
141
|
+
userId,
|
|
136
142
|
personaId,
|
|
143
|
+
phoneNumber: contactResult.phone_number,
|
|
137
144
|
callType: 'on_behalf',
|
|
138
|
-
contactId: contactResult.id,
|
|
139
145
|
purpose: params.brief,
|
|
140
|
-
|
|
146
|
+
contactId: contactResult.id,
|
|
141
147
|
}),
|
|
142
148
|
});
|
|
143
149
|
if (!response.ok) {
|
|
144
150
|
const errBody = await response.text().catch(() => '');
|
|
145
151
|
return [{ type: 'text', text: `Failed to initiate call: ${response.status} ${errBody}` }];
|
|
146
152
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
let result;
|
|
154
|
+
try {
|
|
155
|
+
result = await response.json();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return [{ type: 'text', text: `Call to **${contactResult.full_name}** initiated but received malformed response from voice server.` }];
|
|
159
|
+
}
|
|
160
|
+
const callId = result.callId;
|
|
161
|
+
if (!params.wait_for_result || !callId) {
|
|
150
162
|
return [{
|
|
151
163
|
type: 'text',
|
|
152
|
-
text: `
|
|
164
|
+
text: `Calling **${contactResult.full_name}** (${contactResult.phone_number}) now.\nBrief: ${params.brief}\nCall ID: ${callId ?? 'pending'}`,
|
|
153
165
|
}];
|
|
154
166
|
}
|
|
155
167
|
// Wait for the call to complete and return results
|
|
156
|
-
const callResult = await waitForCallCompletion(
|
|
168
|
+
const callResult = await waitForCallCompletion(callId);
|
|
157
169
|
if (!callResult) {
|
|
158
170
|
return [{
|
|
159
171
|
type: 'text',
|
|
160
|
-
text: `Call to **${contactResult.full_name}** was
|
|
172
|
+
text: `Call to **${contactResult.full_name}** was initiated but timed out waiting for results (10 min max).\nCall ID: ${callId}\nUse get_call to check the results later.`,
|
|
161
173
|
}];
|
|
162
174
|
}
|
|
163
|
-
if (callResult.status
|
|
175
|
+
if (callResult.status !== 'completed') {
|
|
164
176
|
return [{
|
|
165
177
|
type: 'text',
|
|
166
|
-
text: `Call to **${contactResult.full_name}** ${callResult.status}.\
|
|
178
|
+
text: `Call to **${contactResult.full_name}** ${callResult.status.replace('_', ' ')}.\nCall ID: ${callId}`,
|
|
167
179
|
}];
|
|
168
180
|
}
|
|
169
181
|
const duration = callResult.duration_seconds ? `${Math.round(callResult.duration_seconds / 60)} min` : 'unknown';
|
|
@@ -176,8 +188,7 @@ export async function handleMakeCall(params) {
|
|
|
176
188
|
parts.push(`\n### Summary\n${callResult.summary}`);
|
|
177
189
|
if (callResult.transcript)
|
|
178
190
|
parts.push(`\n### Transcript\n${callResult.transcript}`);
|
|
179
|
-
|
|
180
|
-
parts.push(`\nCall ID: ${callResult.triggered_call_id}`);
|
|
191
|
+
parts.push(`\nCall ID: ${callId}`);
|
|
181
192
|
return [{ type: 'text', text: parts.join('\n') }];
|
|
182
193
|
}
|
|
183
194
|
export async function handleScheduleCall(params) {
|
|
@@ -324,18 +335,26 @@ export async function handleCallMe(params) {
|
|
|
324
335
|
if (!personaId) {
|
|
325
336
|
return [{ type: 'text', text: 'No persona specified and no default persona set. Please provide a persona_id.' }];
|
|
326
337
|
}
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
338
|
+
// Get user's phone number
|
|
339
|
+
const supabase = getAuthenticatedClient();
|
|
340
|
+
const { data: userProfile } = await supabase
|
|
341
|
+
.from('users')
|
|
342
|
+
.select('phone_number')
|
|
343
|
+
.eq('id', userId)
|
|
344
|
+
.single();
|
|
345
|
+
if (!userProfile?.phone_number) {
|
|
346
|
+
return [{ type: 'text', text: 'No phone number on your profile. Please add one in the dashboard settings.' }];
|
|
347
|
+
}
|
|
348
|
+
// Initiate a direct call to the user's phone instantly
|
|
331
349
|
const body = {
|
|
350
|
+
userId,
|
|
332
351
|
personaId,
|
|
352
|
+
phoneNumber: userProfile.phone_number,
|
|
333
353
|
callType: 'direct',
|
|
334
|
-
scheduledAt: localTime,
|
|
335
354
|
};
|
|
336
355
|
if (params.brief)
|
|
337
356
|
body.purpose = params.brief;
|
|
338
|
-
const response = await fetch(`${voiceServerUrl}/api/
|
|
357
|
+
const response = await fetch(`${voiceServerUrl}/api/calls/initiate`, {
|
|
339
358
|
method: 'POST',
|
|
340
359
|
headers: {
|
|
341
360
|
'Content-Type': 'application/json',
|
|
@@ -347,26 +366,32 @@ export async function handleCallMe(params) {
|
|
|
347
366
|
const errBody = await response.text().catch(() => '');
|
|
348
367
|
return [{ type: 'text', text: `Failed to initiate call: ${response.status} ${errBody}` }];
|
|
349
368
|
}
|
|
350
|
-
|
|
351
|
-
|
|
369
|
+
let result;
|
|
370
|
+
try {
|
|
371
|
+
result = await response.json();
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return [{ type: 'text', text: 'Call initiated but received malformed response from voice server.' }];
|
|
375
|
+
}
|
|
376
|
+
const callId = result.callId;
|
|
352
377
|
const briefMsg = params.brief ? `\nBrief: ${params.brief}` : '';
|
|
353
|
-
if (!params.wait_for_result || !
|
|
378
|
+
if (!params.wait_for_result || !callId) {
|
|
354
379
|
return [{
|
|
355
380
|
type: 'text',
|
|
356
|
-
text: `
|
|
381
|
+
text: `Calling your phone now.${briefMsg}\nCall ID: ${callId ?? 'pending'}`,
|
|
357
382
|
}];
|
|
358
383
|
}
|
|
359
|
-
const callResult = await waitForCallCompletion(
|
|
384
|
+
const callResult = await waitForCallCompletion(callId);
|
|
360
385
|
if (!callResult) {
|
|
361
386
|
return [{
|
|
362
387
|
type: 'text',
|
|
363
|
-
text: `Call was
|
|
388
|
+
text: `Call was initiated but timed out waiting for results (10 min max).${briefMsg}\nCall ID: ${callId}\nUse get_call to check later.`,
|
|
364
389
|
}];
|
|
365
390
|
}
|
|
366
|
-
if (callResult.status
|
|
391
|
+
if (callResult.status !== 'completed') {
|
|
367
392
|
return [{
|
|
368
393
|
type: 'text',
|
|
369
|
-
text: `Call ${callResult.status}.${briefMsg}\
|
|
394
|
+
text: `Call ${callResult.status.replace('_', ' ')}.${briefMsg}\nCall ID: ${callId}`,
|
|
370
395
|
}];
|
|
371
396
|
}
|
|
372
397
|
const duration = callResult.duration_seconds ? `${Math.round(callResult.duration_seconds / 60)} min` : 'unknown';
|
|
@@ -380,8 +405,7 @@ export async function handleCallMe(params) {
|
|
|
380
405
|
parts.push(`\n### Summary\n${callResult.summary}`);
|
|
381
406
|
if (callResult.transcript)
|
|
382
407
|
parts.push(`\n### Transcript\n${callResult.transcript}`);
|
|
383
|
-
|
|
384
|
-
parts.push(`\nCall ID: ${callResult.triggered_call_id}`);
|
|
408
|
+
parts.push(`\nCall ID: ${callId}`);
|
|
385
409
|
return [{ type: 'text', text: parts.join('\n') }];
|
|
386
410
|
}
|
|
387
411
|
export function registerCallTools(server) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telitask/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "TeliTask MCP server — manage contacts, tasks, and calls from AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
41
|
"test": "vitest run",
|
|
42
42
|
"test:watch": "vitest watch",
|
|
43
|
-
"clean": "rm -rf dist"
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"release": "./scripts/publish.sh",
|
|
45
|
+
"release:minor": "./scripts/publish.sh minor",
|
|
46
|
+
"release:major": "./scripts/publish.sh major"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|