@telitask/mcp-server 0.1.0 → 0.1.2
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 +41 -16
- package/dist/tools/calls.d.ts.map +1 -1
- package/dist/tools/calls.js +92 -70
- package/package.json +22 -6
|
@@ -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,13 +1,13 @@
|
|
|
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';
|
|
8
8
|
import { registerCallTools } from './tools/calls.js';
|
|
9
9
|
import { registerResources } from './resources/index.js';
|
|
10
|
-
const DEFAULT_DASHBOARD_URL = 'https://
|
|
10
|
+
const DEFAULT_DASHBOARD_URL = 'https://telitask.ai';
|
|
11
11
|
async function runLogin() {
|
|
12
12
|
const dashboardUrl = process.env.TELITASK_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
|
|
13
13
|
console.error(`Authenticating with TeliTask at ${dashboardUrl}...`);
|
|
@@ -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;AAyJzE,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,55 @@
|
|
|
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, 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
|
-
if (
|
|
22
|
-
return {
|
|
22
|
+
// Terminal non-completed statuses — return immediately with whatever data is available
|
|
23
|
+
if (TERMINAL_STATUSES.includes(call.status)) {
|
|
24
|
+
return {
|
|
25
|
+
status: call.status,
|
|
26
|
+
summary: call.call_summary,
|
|
27
|
+
transcript: call.transcript,
|
|
28
|
+
duration_seconds: call.duration_seconds,
|
|
29
|
+
};
|
|
23
30
|
}
|
|
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')) {
|
|
31
|
+
// Call completed — wait up to 60s for summary/transcript to be generated
|
|
32
|
+
if (call.status === 'completed') {
|
|
33
|
+
if (call.call_summary && call.transcript) {
|
|
33
34
|
return {
|
|
34
35
|
status: call.status,
|
|
35
36
|
summary: call.call_summary,
|
|
36
37
|
transcript: call.transcript,
|
|
37
38
|
duration_seconds: call.duration_seconds,
|
|
38
|
-
triggered_call_id: scheduledCall.triggered_call_id,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
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;
|
|
41
|
+
if (!completedSince)
|
|
42
|
+
completedSince = Date.now();
|
|
43
|
+
if (Date.now() - completedSince >= POST_COMPLETION_TIMEOUT_MS) {
|
|
44
|
+
return {
|
|
45
|
+
status: call.status,
|
|
46
|
+
summary: call.call_summary,
|
|
47
|
+
transcript: call.transcript,
|
|
48
|
+
duration_seconds: call.duration_seconds,
|
|
49
|
+
};
|
|
55
50
|
}
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
|
52
|
+
continue;
|
|
56
53
|
}
|
|
57
54
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
58
55
|
}
|
|
@@ -107,7 +104,17 @@ async function resolvePersonaId(explicitId, userId) {
|
|
|
107
104
|
.select('default_persona_id')
|
|
108
105
|
.eq('id', userId)
|
|
109
106
|
.single();
|
|
110
|
-
|
|
107
|
+
if (user?.default_persona_id)
|
|
108
|
+
return user.default_persona_id;
|
|
109
|
+
// Fallback: use first active persona if no default is set
|
|
110
|
+
const { data: personas } = await supabase
|
|
111
|
+
.from('user_personas')
|
|
112
|
+
.select('id')
|
|
113
|
+
.eq('user_id', userId)
|
|
114
|
+
.eq('is_active', true)
|
|
115
|
+
.order('created_at', { ascending: true })
|
|
116
|
+
.limit(1);
|
|
117
|
+
return personas?.[0]?.id ?? undefined;
|
|
111
118
|
}
|
|
112
119
|
export async function handleMakeCall(params) {
|
|
113
120
|
await refreshSessionIfNeeded();
|
|
@@ -121,49 +128,52 @@ export async function handleMakeCall(params) {
|
|
|
121
128
|
if (!personaId) {
|
|
122
129
|
return [{ type: 'text', text: 'No persona specified and no default persona set. Please provide a persona_id.' }];
|
|
123
130
|
}
|
|
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`, {
|
|
131
|
+
// Initiate an on_behalf call instantly via the voice server
|
|
132
|
+
const response = await fetch(`${voiceServerUrl}/api/calls/initiate`, {
|
|
130
133
|
method: 'POST',
|
|
131
134
|
headers: {
|
|
132
135
|
'Content-Type': 'application/json',
|
|
133
136
|
Authorization: `Bearer ${accessToken}`,
|
|
134
137
|
},
|
|
135
138
|
body: JSON.stringify({
|
|
139
|
+
userId,
|
|
136
140
|
personaId,
|
|
141
|
+
phoneNumber: contactResult.phone_number,
|
|
137
142
|
callType: 'on_behalf',
|
|
138
|
-
contactId: contactResult.id,
|
|
139
143
|
purpose: params.brief,
|
|
140
|
-
|
|
144
|
+
contactId: contactResult.id,
|
|
141
145
|
}),
|
|
142
146
|
});
|
|
143
147
|
if (!response.ok) {
|
|
144
148
|
const errBody = await response.text().catch(() => '');
|
|
145
149
|
return [{ type: 'text', text: `Failed to initiate call: ${response.status} ${errBody}` }];
|
|
146
150
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
let result;
|
|
152
|
+
try {
|
|
153
|
+
result = await response.json();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [{ type: 'text', text: `Call to **${contactResult.full_name}** initiated but received malformed response from voice server.` }];
|
|
157
|
+
}
|
|
158
|
+
const callId = result.callId;
|
|
159
|
+
if (!params.wait_for_result || !callId) {
|
|
150
160
|
return [{
|
|
151
161
|
type: 'text',
|
|
152
|
-
text: `
|
|
162
|
+
text: `Calling **${contactResult.full_name}** (${contactResult.phone_number}) now.\nBrief: ${params.brief}\nCall ID: ${callId ?? 'pending'}`,
|
|
153
163
|
}];
|
|
154
164
|
}
|
|
155
165
|
// Wait for the call to complete and return results
|
|
156
|
-
const callResult = await waitForCallCompletion(
|
|
166
|
+
const callResult = await waitForCallCompletion(callId);
|
|
157
167
|
if (!callResult) {
|
|
158
168
|
return [{
|
|
159
169
|
type: 'text',
|
|
160
|
-
text: `Call to **${contactResult.full_name}** was
|
|
170
|
+
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
171
|
}];
|
|
162
172
|
}
|
|
163
|
-
if (callResult.status
|
|
173
|
+
if (callResult.status !== 'completed') {
|
|
164
174
|
return [{
|
|
165
175
|
type: 'text',
|
|
166
|
-
text: `Call to **${contactResult.full_name}** ${callResult.status}.\
|
|
176
|
+
text: `Call to **${contactResult.full_name}** ${callResult.status.replace('_', ' ')}.\nCall ID: ${callId}`,
|
|
167
177
|
}];
|
|
168
178
|
}
|
|
169
179
|
const duration = callResult.duration_seconds ? `${Math.round(callResult.duration_seconds / 60)} min` : 'unknown';
|
|
@@ -176,8 +186,7 @@ export async function handleMakeCall(params) {
|
|
|
176
186
|
parts.push(`\n### Summary\n${callResult.summary}`);
|
|
177
187
|
if (callResult.transcript)
|
|
178
188
|
parts.push(`\n### Transcript\n${callResult.transcript}`);
|
|
179
|
-
|
|
180
|
-
parts.push(`\nCall ID: ${callResult.triggered_call_id}`);
|
|
189
|
+
parts.push(`\nCall ID: ${callId}`);
|
|
181
190
|
return [{ type: 'text', text: parts.join('\n') }];
|
|
182
191
|
}
|
|
183
192
|
export async function handleScheduleCall(params) {
|
|
@@ -324,18 +333,26 @@ export async function handleCallMe(params) {
|
|
|
324
333
|
if (!personaId) {
|
|
325
334
|
return [{ type: 'text', text: 'No persona specified and no default persona set. Please provide a persona_id.' }];
|
|
326
335
|
}
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
336
|
+
// Get user's phone number
|
|
337
|
+
const supabase = getAuthenticatedClient();
|
|
338
|
+
const { data: userProfile } = await supabase
|
|
339
|
+
.from('users')
|
|
340
|
+
.select('phone_number')
|
|
341
|
+
.eq('id', userId)
|
|
342
|
+
.single();
|
|
343
|
+
if (!userProfile?.phone_number) {
|
|
344
|
+
return [{ type: 'text', text: 'No phone number on your profile. Please add one in the dashboard settings.' }];
|
|
345
|
+
}
|
|
346
|
+
// Initiate a direct call to the user's phone instantly
|
|
331
347
|
const body = {
|
|
348
|
+
userId,
|
|
332
349
|
personaId,
|
|
350
|
+
phoneNumber: userProfile.phone_number,
|
|
333
351
|
callType: 'direct',
|
|
334
|
-
scheduledAt: localTime,
|
|
335
352
|
};
|
|
336
353
|
if (params.brief)
|
|
337
354
|
body.purpose = params.brief;
|
|
338
|
-
const response = await fetch(`${voiceServerUrl}/api/
|
|
355
|
+
const response = await fetch(`${voiceServerUrl}/api/calls/initiate`, {
|
|
339
356
|
method: 'POST',
|
|
340
357
|
headers: {
|
|
341
358
|
'Content-Type': 'application/json',
|
|
@@ -347,26 +364,32 @@ export async function handleCallMe(params) {
|
|
|
347
364
|
const errBody = await response.text().catch(() => '');
|
|
348
365
|
return [{ type: 'text', text: `Failed to initiate call: ${response.status} ${errBody}` }];
|
|
349
366
|
}
|
|
350
|
-
|
|
351
|
-
|
|
367
|
+
let result;
|
|
368
|
+
try {
|
|
369
|
+
result = await response.json();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return [{ type: 'text', text: 'Call initiated but received malformed response from voice server.' }];
|
|
373
|
+
}
|
|
374
|
+
const callId = result.callId;
|
|
352
375
|
const briefMsg = params.brief ? `\nBrief: ${params.brief}` : '';
|
|
353
|
-
if (!params.wait_for_result || !
|
|
376
|
+
if (!params.wait_for_result || !callId) {
|
|
354
377
|
return [{
|
|
355
378
|
type: 'text',
|
|
356
|
-
text: `
|
|
379
|
+
text: `Calling your phone now.${briefMsg}\nCall ID: ${callId ?? 'pending'}`,
|
|
357
380
|
}];
|
|
358
381
|
}
|
|
359
|
-
const callResult = await waitForCallCompletion(
|
|
382
|
+
const callResult = await waitForCallCompletion(callId);
|
|
360
383
|
if (!callResult) {
|
|
361
384
|
return [{
|
|
362
385
|
type: 'text',
|
|
363
|
-
text: `Call was
|
|
386
|
+
text: `Call was initiated but timed out waiting for results (10 min max).${briefMsg}\nCall ID: ${callId}\nUse get_call to check later.`,
|
|
364
387
|
}];
|
|
365
388
|
}
|
|
366
|
-
if (callResult.status
|
|
389
|
+
if (callResult.status !== 'completed') {
|
|
367
390
|
return [{
|
|
368
391
|
type: 'text',
|
|
369
|
-
text: `Call ${callResult.status}.${briefMsg}\
|
|
392
|
+
text: `Call ${callResult.status.replace('_', ' ')}.${briefMsg}\nCall ID: ${callId}`,
|
|
370
393
|
}];
|
|
371
394
|
}
|
|
372
395
|
const duration = callResult.duration_seconds ? `${Math.round(callResult.duration_seconds / 60)} min` : 'unknown';
|
|
@@ -380,8 +403,7 @@ export async function handleCallMe(params) {
|
|
|
380
403
|
parts.push(`\n### Summary\n${callResult.summary}`);
|
|
381
404
|
if (callResult.transcript)
|
|
382
405
|
parts.push(`\n### Transcript\n${callResult.transcript}`);
|
|
383
|
-
|
|
384
|
-
parts.push(`\nCall ID: ${callResult.triggered_call_id}`);
|
|
406
|
+
parts.push(`\nCall ID: ${callId}`);
|
|
385
407
|
return [{ type: 'text', text: parts.join('\n') }];
|
|
386
408
|
}
|
|
387
409
|
export function registerCallTools(server) {
|
package/package.json
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telitask/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "TeliTask MCP server — manage contacts, tasks, and calls from AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/Telitask/
|
|
10
|
-
"directory": "packages/mcp-server"
|
|
9
|
+
"url": "https://github.com/Telitask/telitask-ai"
|
|
11
10
|
},
|
|
12
|
-
"
|
|
11
|
+
"homepage": "https://github.com/Telitask/telitask-ai#mcp-server",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Telitask/telitask-ai/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"telitask",
|
|
18
|
+
"ai-assistant",
|
|
19
|
+
"voice",
|
|
20
|
+
"calls",
|
|
21
|
+
"contacts",
|
|
22
|
+
"tasks"
|
|
23
|
+
],
|
|
13
24
|
"bin": {
|
|
14
25
|
"telitask-mcp": "./dist/index.js"
|
|
15
26
|
},
|
|
16
27
|
"main": "./dist/index.js",
|
|
17
28
|
"types": "./dist/index.d.ts",
|
|
18
|
-
"files": [
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
19
32
|
"publishConfig": {
|
|
20
33
|
"access": "public"
|
|
21
34
|
},
|
|
@@ -27,7 +40,10 @@
|
|
|
27
40
|
"typecheck": "tsc --noEmit",
|
|
28
41
|
"test": "vitest run",
|
|
29
42
|
"test:watch": "vitest watch",
|
|
30
|
-
"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"
|
|
31
47
|
},
|
|
32
48
|
"dependencies": {
|
|
33
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|