mirlo-mcp 1.0.3 → 1.1.0

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/client.d.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  export declare class MirloClient {
5
5
  private readonly baseUrl;
6
6
  private readonly apiKey;
7
- constructor(apiKey: string, baseUrl?: string);
7
+ private orgId;
8
+ constructor(apiKey: string, baseUrl?: string, orgId?: string);
9
+ private getMessagesBaseUrl;
8
10
  private request;
9
11
  listDeals(filters?: Record<string, string | number | boolean>): Promise<any>;
10
12
  getDeal(id: string): Promise<any>;
@@ -29,6 +31,8 @@ export declare class MirloClient {
29
31
  createTask(data: Record<string, unknown>): Promise<any>;
30
32
  completeTask(id: string): Promise<any>;
31
33
  listPipelines(): Promise<any>;
34
+ private resolveOrgId;
35
+ private messagesRequest;
32
36
  sendMessage(organizationAddress: string, contactAddress: string, text: string, service?: string): Promise<any>;
33
37
  sendTemplate(organizationAddress: string, to: string, metaTemplateId: string, components?: any[]): Promise<any>;
34
38
  listConversations(filters?: Record<string, string>): Promise<any>;
package/dist/client.js CHANGED
@@ -4,12 +4,20 @@
4
4
  export class MirloClient {
5
5
  baseUrl;
6
6
  apiKey;
7
- constructor(apiKey, baseUrl = 'https://api.mirlo.com/v1') {
7
+ orgId = null;
8
+ constructor(apiKey, baseUrl = 'https://api.mirlo.com/v1', orgId) {
8
9
  this.apiKey = apiKey;
9
10
  this.baseUrl = baseUrl;
11
+ this.orgId = orgId || null;
12
+ }
13
+ getMessagesBaseUrl() {
14
+ // Messages API is v2, not v1
15
+ return this.baseUrl.replace('/v1', '/v2/messages');
10
16
  }
11
17
  async request(method, path, body) {
12
- const url = `${this.baseUrl}${path}`;
18
+ // CRM API uses org in path: /v1/crm/organizations/{orgId}/...
19
+ const orgId = await this.resolveOrgId();
20
+ const url = `${this.baseUrl}/crm/organizations/${orgId}${path}`;
13
21
  const res = await fetch(url, {
14
22
  method,
15
23
  headers: {
@@ -27,88 +35,136 @@ export class MirloClient {
27
35
  // ── CRM: Deals ──
28
36
  async listDeals(filters) {
29
37
  const params = filters ? '?' + new URLSearchParams(Object.entries(filters).map(([k, v]) => [k, String(v)])).toString() : '';
30
- return this.request('GET', `/crm/deals${params}`);
38
+ return this.request('GET', `/deals${params}`);
31
39
  }
32
40
  async getDeal(id) {
33
- return this.request('GET', `/crm/deals/${id}`);
41
+ return this.request('GET', `/deals/${id}`);
34
42
  }
35
43
  async createDeal(data) {
36
- return this.request('POST', '/crm/deals', data);
44
+ return this.request('POST', '/deals', data);
37
45
  }
38
46
  async updateDeal(id, data) {
39
- return this.request('PATCH', `/crm/deals/${id}`, data);
47
+ return this.request('PATCH', `/deals/${id}`, data);
40
48
  }
41
49
  async moveDealStage(id, stageId) {
42
- return this.request('PATCH', `/crm/deals/${id}/stage`, { stage_id: stageId });
50
+ return this.request('PATCH', `/deals/${id}/stage`, { stage_id: stageId });
43
51
  }
44
52
  async deleteDeal(id) {
45
- return this.request('DELETE', `/crm/deals/${id}`);
53
+ return this.request('DELETE', `/deals/${id}`);
46
54
  }
47
55
  // ── CRM: Contacts ──
48
56
  async listContacts(filters) {
49
57
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
50
- return this.request('GET', `/crm/contacts${params}`);
58
+ return this.request('GET', `/contacts${params}`);
51
59
  }
52
60
  async getContact(id) {
53
- return this.request('GET', `/crm/contacts/${id}`);
61
+ return this.request('GET', `/contacts/${id}`);
54
62
  }
55
63
  async createContact(data) {
56
- return this.request('POST', '/crm/contacts', data);
64
+ return this.request('POST', '/contacts', data);
57
65
  }
58
66
  async updateContact(id, data) {
59
- return this.request('PATCH', `/crm/contacts/${id}`, data);
67
+ return this.request('PATCH', `/contacts/${id}`, data);
60
68
  }
61
69
  // ── CRM: Accounts ──
62
70
  async listAccounts(filters) {
63
71
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
64
- return this.request('GET', `/crm/accounts${params}`);
72
+ return this.request('GET', `/accounts${params}`);
65
73
  }
66
74
  async getAccount(id) {
67
- return this.request('GET', `/crm/accounts/${id}`);
75
+ return this.request('GET', `/accounts/${id}`);
68
76
  }
69
77
  async createAccount(data) {
70
- return this.request('POST', '/crm/accounts', data);
78
+ return this.request('POST', '/accounts', data);
71
79
  }
72
80
  async updateAccount(id, data) {
73
- return this.request('PATCH', `/crm/accounts/${id}`, data);
81
+ return this.request('PATCH', `/accounts/${id}`, data);
74
82
  }
75
83
  // ── CRM: Tickets ──
76
84
  async listTickets(filters) {
77
85
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
78
- return this.request('GET', `/crm/tickets${params}`);
86
+ return this.request('GET', `/tickets${params}`);
79
87
  }
80
88
  async getTicket(id) {
81
- return this.request('GET', `/crm/tickets/${id}`);
89
+ return this.request('GET', `/tickets/${id}`);
82
90
  }
83
91
  async createTicket(data) {
84
- return this.request('POST', '/crm/tickets', data);
92
+ return this.request('POST', '/tickets', data);
85
93
  }
86
94
  async updateTicket(id, data) {
87
- return this.request('PATCH', `/crm/tickets/${id}`, data);
95
+ return this.request('PATCH', `/tickets/${id}`, data);
88
96
  }
89
97
  async addTicketNote(ticketId, data) {
90
- return this.request('POST', `/crm/tickets/${ticketId}/notes`, data);
98
+ return this.request('POST', `/tickets/${ticketId}/notes`, data);
91
99
  }
92
100
  // ── CRM: Tasks ──
93
101
  async listTasks(filters) {
94
102
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
95
- return this.request('GET', `/crm/tasks${params}`);
103
+ return this.request('GET', `/tasks${params}`);
96
104
  }
97
105
  async createTask(data) {
98
- return this.request('POST', '/crm/tasks', data);
106
+ return this.request('POST', '/tasks', data);
99
107
  }
100
108
  async completeTask(id) {
101
- return this.request('PATCH', `/crm/tasks/${id}/complete`);
109
+ return this.request('PATCH', `/tasks/${id}/complete`);
102
110
  }
103
111
  // ── CRM: Pipelines ──
104
112
  async listPipelines() {
105
- return this.request('GET', '/crm/pipelines');
113
+ return this.request('GET', '/pipelines');
106
114
  }
107
115
  // ── Messaging ──
108
- // Messages API is v2 with org in path
116
+ // Messages API is v2 at /v2/messages/organizations/{orgId}/...
117
+ async resolveOrgId() {
118
+ if (this.orgId)
119
+ return this.orgId;
120
+ // Resolve org from API key by calling a lightweight endpoint
121
+ // Try the old-style path first (without org in path) as bootstrap
122
+ try {
123
+ const res = await fetch(`${this.baseUrl}/crm/accounts?limit=1`, {
124
+ headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
125
+ });
126
+ if (res.ok) {
127
+ const data = await res.json();
128
+ if (data?.data?.[0]?.organization_id) {
129
+ this.orgId = data.data[0].organization_id;
130
+ return this.orgId;
131
+ }
132
+ }
133
+ }
134
+ catch { }
135
+ // Try contacts
136
+ try {
137
+ const res = await fetch(`${this.baseUrl}/crm/contacts?limit=1`, {
138
+ headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
139
+ });
140
+ if (res.ok) {
141
+ const data = await res.json();
142
+ if (data?.data?.[0]?.organization_id) {
143
+ this.orgId = data.data[0].organization_id;
144
+ return this.orgId;
145
+ }
146
+ }
147
+ }
148
+ catch { }
149
+ throw new Error('Could not resolve organization_id. Provide --org-id=UUID or ensure your API key has CRM data.');
150
+ }
151
+ async messagesRequest(method, path, body) {
152
+ const orgId = await this.resolveOrgId();
153
+ const msgBase = this.getMessagesBaseUrl();
154
+ const url = `${msgBase}/organizations/${orgId}${path}`;
155
+ const res = await fetch(url, {
156
+ method,
157
+ headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
158
+ ...(body ? { body: JSON.stringify(body) } : {}),
159
+ });
160
+ if (!res.ok) {
161
+ const text = await res.text();
162
+ throw new Error(`Mirlo Messages API ${method} ${path} → ${res.status}: ${text.slice(0, 200)}`);
163
+ }
164
+ return res.json();
165
+ }
109
166
  async sendMessage(organizationAddress, contactAddress, text, service = 'whatsapp') {
110
- // v2 messages endpoint — org resolved from API key
111
- return this.request('POST', '/messages', {
167
+ return this.messagesRequest('POST', '/messages', {
112
168
  organization_address: organizationAddress,
113
169
  service,
114
170
  contact_address: contactAddress,
@@ -117,7 +173,7 @@ export class MirloClient {
117
173
  });
118
174
  }
119
175
  async sendTemplate(organizationAddress, to, metaTemplateId, components) {
120
- return this.request('POST', '/messages/send-template', {
176
+ return this.messagesRequest('POST', '/messages/send-template', {
121
177
  organization_address: organizationAddress,
122
178
  to,
123
179
  meta_template_id: metaTemplateId,
@@ -126,10 +182,10 @@ export class MirloClient {
126
182
  }
127
183
  async listConversations(filters) {
128
184
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
129
- return this.request('GET', `/conversations${params}`);
185
+ return this.messagesRequest('GET', `/conversations${params}`);
130
186
  }
131
187
  async listMessages(conversationId, limit = 20) {
132
- return this.request('GET', `/conversations/${conversationId}/messages?limit=${limit}`);
188
+ return this.messagesRequest('GET', `/conversations/${conversationId}/messages?limit=${limit}`);
133
189
  }
134
190
  // ── Voice ──
135
191
  async listCalls(filters) {
package/dist/index.js CHANGED
@@ -34,10 +34,17 @@ function getBaseUrl() {
34
34
  return process.env.MIRLO_BASE_URL || 'https://api.mirlo.com/v1';
35
35
  }
36
36
  // ── Main ──
37
+ function getOrgId() {
38
+ const cliArg = process.argv.find((a) => a.startsWith('--org-id='));
39
+ if (cliArg)
40
+ return cliArg.split('=')[1];
41
+ return process.env.MIRLO_ORG_ID || undefined;
42
+ }
37
43
  async function main() {
38
44
  const apiKey = getApiKey();
39
45
  const baseUrl = getBaseUrl();
40
- const client = new MirloClient(apiKey, baseUrl);
46
+ const orgId = getOrgId();
47
+ const client = new MirloClient(apiKey, baseUrl, orgId);
41
48
  const server = new McpServer({
42
49
  name: 'mirlo',
43
50
  version: '1.0.0',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirlo-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Mirlo MCP Server — CRM, Messaging & Voice tools for Claude, GPT, and other LLMs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/client.ts CHANGED
@@ -5,14 +5,23 @@
5
5
  export class MirloClient {
6
6
  private readonly baseUrl: string;
7
7
  private readonly apiKey: string;
8
+ private orgId: string | null = null;
8
9
 
9
- constructor(apiKey: string, baseUrl = 'https://api.mirlo.com/v1') {
10
+ constructor(apiKey: string, baseUrl = 'https://api.mirlo.com/v1', orgId?: string) {
10
11
  this.apiKey = apiKey;
11
12
  this.baseUrl = baseUrl;
13
+ this.orgId = orgId || null;
14
+ }
15
+
16
+ private getMessagesBaseUrl(): string {
17
+ // Messages API is v2, not v1
18
+ return this.baseUrl.replace('/v1', '/v2/messages');
12
19
  }
13
20
 
14
21
  private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
15
- const url = `${this.baseUrl}${path}`;
22
+ // CRM API uses org in path: /v1/crm/organizations/{orgId}/...
23
+ const orgId = await this.resolveOrgId();
24
+ const url = `${this.baseUrl}/crm/organizations/${orgId}${path}`;
16
25
  const res = await fetch(url, {
17
26
  method,
18
27
  headers: {
@@ -34,117 +43,164 @@ export class MirloClient {
34
43
 
35
44
  async listDeals(filters?: Record<string, string | number | boolean>) {
36
45
  const params = filters ? '?' + new URLSearchParams(Object.entries(filters).map(([k, v]) => [k, String(v)])).toString() : '';
37
- return this.request<any>('GET', `/crm/deals${params}`);
46
+ return this.request<any>('GET', `/deals${params}`);
38
47
  }
39
48
 
40
49
  async getDeal(id: string) {
41
- return this.request<any>('GET', `/crm/deals/${id}`);
50
+ return this.request<any>('GET', `/deals/${id}`);
42
51
  }
43
52
 
44
53
  async createDeal(data: Record<string, unknown>) {
45
- return this.request<any>('POST', '/crm/deals', data);
54
+ return this.request<any>('POST', '/deals', data);
46
55
  }
47
56
 
48
57
  async updateDeal(id: string, data: Record<string, unknown>) {
49
- return this.request<any>('PATCH', `/crm/deals/${id}`, data);
58
+ return this.request<any>('PATCH', `/deals/${id}`, data);
50
59
  }
51
60
 
52
61
  async moveDealStage(id: string, stageId: string) {
53
- return this.request<any>('PATCH', `/crm/deals/${id}/stage`, { stage_id: stageId });
62
+ return this.request<any>('PATCH', `/deals/${id}/stage`, { stage_id: stageId });
54
63
  }
55
64
 
56
65
  async deleteDeal(id: string) {
57
- return this.request<any>('DELETE', `/crm/deals/${id}`);
66
+ return this.request<any>('DELETE', `/deals/${id}`);
58
67
  }
59
68
 
60
69
  // ── CRM: Contacts ──
61
70
 
62
71
  async listContacts(filters?: Record<string, string>) {
63
72
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
64
- return this.request<any>('GET', `/crm/contacts${params}`);
73
+ return this.request<any>('GET', `/contacts${params}`);
65
74
  }
66
75
 
67
76
  async getContact(id: string) {
68
- return this.request<any>('GET', `/crm/contacts/${id}`);
77
+ return this.request<any>('GET', `/contacts/${id}`);
69
78
  }
70
79
 
71
80
  async createContact(data: Record<string, unknown>) {
72
- return this.request<any>('POST', '/crm/contacts', data);
81
+ return this.request<any>('POST', '/contacts', data);
73
82
  }
74
83
 
75
84
  async updateContact(id: string, data: Record<string, unknown>) {
76
- return this.request<any>('PATCH', `/crm/contacts/${id}`, data);
85
+ return this.request<any>('PATCH', `/contacts/${id}`, data);
77
86
  }
78
87
 
79
88
  // ── CRM: Accounts ──
80
89
 
81
90
  async listAccounts(filters?: Record<string, string>) {
82
91
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
83
- return this.request<any>('GET', `/crm/accounts${params}`);
92
+ return this.request<any>('GET', `/accounts${params}`);
84
93
  }
85
94
 
86
95
  async getAccount(id: string) {
87
- return this.request<any>('GET', `/crm/accounts/${id}`);
96
+ return this.request<any>('GET', `/accounts/${id}`);
88
97
  }
89
98
 
90
99
  async createAccount(data: Record<string, unknown>) {
91
- return this.request<any>('POST', '/crm/accounts', data);
100
+ return this.request<any>('POST', '/accounts', data);
92
101
  }
93
102
 
94
103
  async updateAccount(id: string, data: Record<string, unknown>) {
95
- return this.request<any>('PATCH', `/crm/accounts/${id}`, data);
104
+ return this.request<any>('PATCH', `/accounts/${id}`, data);
96
105
  }
97
106
 
98
107
  // ── CRM: Tickets ──
99
108
 
100
109
  async listTickets(filters?: Record<string, string>) {
101
110
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
102
- return this.request<any>('GET', `/crm/tickets${params}`);
111
+ return this.request<any>('GET', `/tickets${params}`);
103
112
  }
104
113
 
105
114
  async getTicket(id: string) {
106
- return this.request<any>('GET', `/crm/tickets/${id}`);
115
+ return this.request<any>('GET', `/tickets/${id}`);
107
116
  }
108
117
 
109
118
  async createTicket(data: Record<string, unknown>) {
110
- return this.request<any>('POST', '/crm/tickets', data);
119
+ return this.request<any>('POST', '/tickets', data);
111
120
  }
112
121
 
113
122
  async updateTicket(id: string, data: Record<string, unknown>) {
114
- return this.request<any>('PATCH', `/crm/tickets/${id}`, data);
123
+ return this.request<any>('PATCH', `/tickets/${id}`, data);
115
124
  }
116
125
 
117
126
  async addTicketNote(ticketId: string, data: Record<string, unknown>) {
118
- return this.request<any>('POST', `/crm/tickets/${ticketId}/notes`, data);
127
+ return this.request<any>('POST', `/tickets/${ticketId}/notes`, data);
119
128
  }
120
129
 
121
130
  // ── CRM: Tasks ──
122
131
 
123
132
  async listTasks(filters?: Record<string, string>) {
124
133
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
125
- return this.request<any>('GET', `/crm/tasks${params}`);
134
+ return this.request<any>('GET', `/tasks${params}`);
126
135
  }
127
136
 
128
137
  async createTask(data: Record<string, unknown>) {
129
- return this.request<any>('POST', '/crm/tasks', data);
138
+ return this.request<any>('POST', '/tasks', data);
130
139
  }
131
140
 
132
141
  async completeTask(id: string) {
133
- return this.request<any>('PATCH', `/crm/tasks/${id}/complete`);
142
+ return this.request<any>('PATCH', `/tasks/${id}/complete`);
134
143
  }
135
144
 
136
145
  // ── CRM: Pipelines ──
137
146
 
138
147
  async listPipelines() {
139
- return this.request<any>('GET', '/crm/pipelines');
148
+ return this.request<any>('GET', '/pipelines');
140
149
  }
141
150
 
142
151
  // ── Messaging ──
143
- // Messages API is v2 with org in path
152
+ // Messages API is v2 at /v2/messages/organizations/{orgId}/...
153
+
154
+ private async resolveOrgId(): Promise<string> {
155
+ if (this.orgId) return this.orgId;
156
+ // Resolve org from API key by calling a lightweight endpoint
157
+ // Try the old-style path first (without org in path) as bootstrap
158
+ try {
159
+ const res = await fetch(`${this.baseUrl}/crm/accounts?limit=1`, {
160
+ headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
161
+ });
162
+ if (res.ok) {
163
+ const data = await res.json();
164
+ if (data?.data?.[0]?.organization_id) {
165
+ this.orgId = data.data[0].organization_id;
166
+ return this.orgId!;
167
+ }
168
+ }
169
+ } catch {}
170
+ // Try contacts
171
+ try {
172
+ const res = await fetch(`${this.baseUrl}/crm/contacts?limit=1`, {
173
+ headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
174
+ });
175
+ if (res.ok) {
176
+ const data = await res.json();
177
+ if (data?.data?.[0]?.organization_id) {
178
+ this.orgId = data.data[0].organization_id;
179
+ return this.orgId!;
180
+ }
181
+ }
182
+ } catch {}
183
+ throw new Error('Could not resolve organization_id. Provide --org-id=UUID or ensure your API key has CRM data.');
184
+ }
185
+
186
+ private async messagesRequest<T>(method: string, path: string, body?: unknown): Promise<T> {
187
+ const orgId = await this.resolveOrgId();
188
+ const msgBase = this.getMessagesBaseUrl();
189
+ const url = `${msgBase}/organizations/${orgId}${path}`;
190
+ const res = await fetch(url, {
191
+ method,
192
+ headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
193
+ ...(body ? { body: JSON.stringify(body) } : {}),
194
+ });
195
+ if (!res.ok) {
196
+ const text = await res.text();
197
+ throw new Error(`Mirlo Messages API ${method} ${path} → ${res.status}: ${text.slice(0, 200)}`);
198
+ }
199
+ return res.json() as T;
200
+ }
144
201
 
145
202
  async sendMessage(organizationAddress: string, contactAddress: string, text: string, service = 'whatsapp') {
146
- // v2 messages endpoint — org resolved from API key
147
- return this.request<any>('POST', '/messages', {
203
+ return this.messagesRequest<any>('POST', '/messages', {
148
204
  organization_address: organizationAddress,
149
205
  service,
150
206
  contact_address: contactAddress,
@@ -154,7 +210,7 @@ export class MirloClient {
154
210
  }
155
211
 
156
212
  async sendTemplate(organizationAddress: string, to: string, metaTemplateId: string, components?: any[]) {
157
- return this.request<any>('POST', '/messages/send-template', {
213
+ return this.messagesRequest<any>('POST', '/messages/send-template', {
158
214
  organization_address: organizationAddress,
159
215
  to,
160
216
  meta_template_id: metaTemplateId,
@@ -164,11 +220,11 @@ export class MirloClient {
164
220
 
165
221
  async listConversations(filters?: Record<string, string>) {
166
222
  const params = filters ? '?' + new URLSearchParams(filters).toString() : '';
167
- return this.request<any>('GET', `/conversations${params}`);
223
+ return this.messagesRequest<any>('GET', `/conversations${params}`);
168
224
  }
169
225
 
170
226
  async listMessages(conversationId: string, limit = 20) {
171
- return this.request<any>('GET', `/conversations/${conversationId}/messages?limit=${limit}`);
227
+ return this.messagesRequest<any>('GET', `/conversations/${conversationId}/messages?limit=${limit}`);
172
228
  }
173
229
 
174
230
  // ── Voice ──
package/src/index.ts CHANGED
@@ -40,10 +40,17 @@ function getBaseUrl(): string {
40
40
 
41
41
  // ── Main ──
42
42
 
43
+ function getOrgId(): string | undefined {
44
+ const cliArg = process.argv.find((a: string) => a.startsWith('--org-id='));
45
+ if (cliArg) return cliArg.split('=')[1];
46
+ return process.env.MIRLO_ORG_ID || undefined;
47
+ }
48
+
43
49
  async function main() {
44
50
  const apiKey = getApiKey();
45
51
  const baseUrl = getBaseUrl();
46
- const client = new MirloClient(apiKey, baseUrl);
52
+ const orgId = getOrgId();
53
+ const client = new MirloClient(apiKey, baseUrl, orgId);
47
54
 
48
55
  const server = new McpServer({
49
56
  name: 'mirlo',