@telitask/mcp-server 0.1.1 → 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.
@@ -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;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"}
@@ -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 = 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, 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
+ // 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
- // 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')) {
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
- // 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;
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
- return user?.default_persona_id ?? undefined;
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
- // 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`, {
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
- scheduledAt: localTime,
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
- const result = await response.json();
148
- const scheduledCallId = result.id;
149
- if (!params.wait_for_result || !scheduledCallId) {
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: `On-behalf call scheduled to **${contactResult.full_name}** (${contactResult.phone_number}) in ~1 minute.\nBrief: ${params.brief}\nScheduled call ID: ${scheduledCallId ?? 'pending'}`,
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(scheduledCallId);
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 scheduled but timed out waiting for results (10 min max).\nScheduled call ID: ${scheduledCallId}\nUse get_call to check the results later.`,
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 === 'failed' || callResult.status === 'cancelled') {
173
+ if (callResult.status !== 'completed') {
164
174
  return [{
165
175
  type: 'text',
166
- text: `Call to **${contactResult.full_name}** ${callResult.status}.\nScheduled call ID: ${scheduledCallId}`,
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
- if (callResult.triggered_call_id)
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
- // 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')}`;
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/scheduled-calls`, {
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
- const result = await response.json();
351
- const scheduledCallId = result.id;
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 || !scheduledCallId) {
376
+ if (!params.wait_for_result || !callId) {
354
377
  return [{
355
378
  type: 'text',
356
- text: `Your AI assistant will call your phone shortly.${briefMsg}\nScheduled call ID: ${scheduledCallId ?? 'pending'}`,
379
+ text: `Calling your phone now.${briefMsg}\nCall ID: ${callId ?? 'pending'}`,
357
380
  }];
358
381
  }
359
- const callResult = await waitForCallCompletion(scheduledCallId);
382
+ const callResult = await waitForCallCompletion(callId);
360
383
  if (!callResult) {
361
384
  return [{
362
385
  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.`,
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 === 'failed' || callResult.status === 'cancelled') {
389
+ if (callResult.status !== 'completed') {
367
390
  return [{
368
391
  type: 'text',
369
- text: `Call ${callResult.status}.${briefMsg}\nScheduled call ID: ${scheduledCallId}`,
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
- if (callResult.triggered_call_id)
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@telitask/mcp-server",
3
- "version": "0.1.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",
@@ -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",