@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.
@@ -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;AAiBD,wBAAgB,eAAe,IAAI,cAAc,GAAG,IAAI,CAUvD;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,cAAc,GAAG,IAAI,CAOlE;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC"}
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"}
@@ -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
- const CREDENTIALS_PATH = path.join(os.homedir(), '.telitask', 'mcp-credentials.json');
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(CREDENTIALS_PATH))
22
+ if (!fs.existsSync(getCredentialsPath()))
19
23
  return null;
20
- const raw = fs.readFileSync(CREDENTIALS_PATH, 'utf-8');
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(CREDENTIALS_PATH);
35
+ const dir = path.dirname(getCredentialsPath());
32
36
  if (!fs.existsSync(dir)) {
33
37
  fs.mkdirSync(dir, { recursive: true });
34
38
  }
35
- fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2));
36
- fs.chmodSync(CREDENTIALS_PATH, 0o600);
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(CREDENTIALS_PATH)) {
40
- fs.unlinkSync(CREDENTIALS_PATH);
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
- if (!credentials) {
26
- console.error('Not authenticated. Run `telitask-mcp login` to connect your TeliTask account.');
27
- process.exit(1);
28
- }
29
- const server = new McpServer({ name: 'telitask', version: '0.1.0' }, {
30
- instructions: 'TeliTask MCP server — manage contacts, tasks, and make phone calls. ' +
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
- registerContactTools(server);
35
- registerTaskTools(server);
36
- registerCallTools(server);
37
- registerResources(server);
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
- clearCredentials();
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;AAiJzE,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,CA+E5C;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,CA2E5C;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6EzD"}
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"}
@@ -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 = 30_000; // 30 seconds
4
+ const POLL_INTERVAL_MS = 10_000; // 10 seconds
5
5
  const MAX_POLL_DURATION_MS = 10 * 60_000; // 10 minutes
6
- async function waitForCallCompletion(scheduledCallId) {
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
- // Check the scheduled call for its triggered_call_id
12
- const { data: scheduledCall } = await supabase
13
- .from('scheduled_calls')
14
- .select('status, triggered_call_id')
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 (!scheduledCall)
20
+ if (!call)
19
21
  return null;
20
- // If the scheduled call failed or was cancelled, return early
21
- if (scheduledCall.status === 'failed' || scheduledCall.status === 'cancelled') {
22
- return { status: scheduledCall.status, summary: null, transcript: null, duration_seconds: null, triggered_call_id: null };
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
- // If the call was triggered, check the actual call record
25
- if (scheduledCall.triggered_call_id) {
26
- const { data: call } = await supabase
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: call.call_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
- // Call completed — wait for summary to be generated (async post-call processing)
42
- if (call && call.status === 'completed') {
43
- if (call.call_summary && call.transcript) {
44
- return {
45
- status: call.status,
46
- summary: call.call_summary,
47
- transcript: call.transcript,
48
- duration_seconds: call.duration_seconds,
49
- triggered_call_id: scheduledCall.triggered_call_id,
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
- return user?.default_persona_id ?? undefined;
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
- // Schedule an on_behalf call 2 minutes from now
125
- // The scheduled calls system handles on_behalf calls with contact context and purpose
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
- scheduledAt: localTime,
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
- const result = await response.json();
148
- const scheduledCallId = result.id;
149
- if (!params.wait_for_result || !scheduledCallId) {
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: `On-behalf call scheduled to **${contactResult.full_name}** (${contactResult.phone_number}) in ~1 minute.\nBrief: ${params.brief}\nScheduled call ID: ${scheduledCallId ?? 'pending'}`,
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(scheduledCallId);
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 scheduled but timed out waiting for results (10 min max).\nScheduled call ID: ${scheduledCallId}\nUse get_call to check the results later.`,
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 === 'failed' || callResult.status === 'cancelled') {
175
+ if (callResult.status !== 'completed') {
164
176
  return [{
165
177
  type: 'text',
166
- text: `Call to **${contactResult.full_name}** ${callResult.status}.\nScheduled call ID: ${scheduledCallId}`,
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
- if (callResult.triggered_call_id)
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
- // Schedule a direct call to the user's phone 10 seconds from now
328
- // Using scheduled calls so we can pass a purpose/brief — BullMQ picks it up almost instantly
329
- const d = new Date(Date.now() + 10_000);
330
- 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')}`;
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/scheduled-calls`, {
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
- const result = await response.json();
351
- const scheduledCallId = result.id;
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 || !scheduledCallId) {
378
+ if (!params.wait_for_result || !callId) {
354
379
  return [{
355
380
  type: 'text',
356
- text: `Your AI assistant will call your phone shortly.${briefMsg}\nScheduled call ID: ${scheduledCallId ?? 'pending'}`,
381
+ text: `Calling your phone now.${briefMsg}\nCall ID: ${callId ?? 'pending'}`,
357
382
  }];
358
383
  }
359
- const callResult = await waitForCallCompletion(scheduledCallId);
384
+ const callResult = await waitForCallCompletion(callId);
360
385
  if (!callResult) {
361
386
  return [{
362
387
  type: 'text',
363
- text: `Call was scheduled but timed out waiting for results (10 min max).${briefMsg}\nScheduled call ID: ${scheduledCallId}\nUse get_call to check later.`,
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 === 'failed' || callResult.status === 'cancelled') {
391
+ if (callResult.status !== 'completed') {
367
392
  return [{
368
393
  type: 'text',
369
- text: `Call ${callResult.status}.${briefMsg}\nScheduled call ID: ${scheduledCallId}`,
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
- if (callResult.triggered_call_id)
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.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",