@velaro/mcp-server 2.0.8 → 2.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "description": "Velaro MCP server — connect Claude and other AI agents directly to your Velaro account: KB, workflows, bots, conversations, contacts, routing, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,66 @@
1
+ // velaro product-match: the per-site Product Match matrix. Site scope comes from the credential only, never from arguments.
2
+ // Parity module (CLI group `product-match` -> MCP tools). Needs the Product Match package (EnableProductMatch).
3
+
4
+ const enc = encodeURIComponent;
5
+ const need = (a, ...ks) => { for (const k of ks) if (a[k] === undefined || a[k] === null || a[k] === '') throw new Error(k + ' is required'); };
6
+ const pid = (v, n = 'id') => { const x = Number(v); if (!Number.isInteger(x) || x <= 0) throw new Error(n + ' must be a positive integer'); return x; };
7
+
8
+ export const tools = [
9
+ {
10
+ name: 'product_match_lookup',
11
+ description: 'Look up which of the site\'s OWN catalog products matches a part number, SKU, competitor model or discontinued part. Deterministic (no model decides it): only approved matrix rows, only SKUs that exist in the site catalog, never a deny-listed brand. Returns status (matched, own_product, no_match, unapproved, loop, not_in_catalog), the recommendations and whether to hand off to a person. Calls POST /BusinessActions/match_product/Execute.',
12
+ inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The part number, SKU or model as the customer wrote it' } }, required: ['query'] },
13
+ },
14
+ {
15
+ name: 'product_match_list',
16
+ description: 'List the site\'s Product Match matrix rows (source, target SKU, match type, approved). Calls GET /ProductMatch/Entries.',
17
+ inputSchema: { type: 'object', properties: { q: { type: 'string' }, page: { type: 'number' } } },
18
+ },
19
+ {
20
+ name: 'product_match_import',
21
+ description: 'Import a cross-reference matrix from CSV text (header row with a source column such as "Competitor SKU" and a target column such as "Our SKU"; optional "Match Type" exact|equivalent|supersedes, "Approved" yes/no, "Notes"). Rows are NOT approved unless the Approved column says yes. Upserts by source and target. Calls POST /ProductMatch/ImportFile.',
22
+ inputSchema: { type: 'object', properties: { csv: { type: 'string', description: 'The CSV file content' } }, required: ['csv'] },
23
+ },
24
+ {
25
+ name: 'product_match_approve',
26
+ description: 'Approve or revoke one matrix row. Only approved rows are ever recommended. Calls PUT /ProductMatch/Entries/{id}/Approve.',
27
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, approved: { type: 'boolean', description: 'default true' } }, required: ['id'] },
28
+ },
29
+ {
30
+ name: 'product_match_remove',
31
+ description: 'Delete one matrix row. Calls DELETE /ProductMatch/Entries/{id}.',
32
+ inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] },
33
+ },
34
+ {
35
+ name: 'product_match_deny_terms',
36
+ description: 'Read, or replace (when terms is given), the brands and competitors the bot must never name in a Product Match reply. Calls GET or PUT /ProductMatch/DenyTerms.',
37
+ inputSchema: { type: 'object', properties: { terms: { type: 'array', items: { type: 'string' } } } },
38
+ },
39
+ ];
40
+
41
+ export const handlers = {
42
+ product_match_lookup: async (a, ctx) => {
43
+ need(a, 'query');
44
+ return ctx.messagingPost('/BusinessActions/match_product/Execute', { query: String(a.query) });
45
+ },
46
+ product_match_list: async (a, ctx) => {
47
+ const p = new URLSearchParams();
48
+ if (a.q) p.set('q', String(a.q));
49
+ p.set('page', String(a.page ?? 1));
50
+ return ctx.messagingGet('/ProductMatch/Entries?' + p.toString());
51
+ },
52
+ product_match_import: async (a, ctx) => {
53
+ need(a, 'csv');
54
+ return ctx.messagingPost('/ProductMatch/ImportFile', { fileName: 'matrix.csv', contentBase64: Buffer.from(String(a.csv), 'utf8').toString('base64') });
55
+ },
56
+ product_match_approve: async (a, ctx) => {
57
+ return ctx.messagingPut('/ProductMatch/Entries/' + pid(a.id) + '/Approve', { approved: a.approved !== false });
58
+ },
59
+ product_match_remove: async (a, ctx) => {
60
+ return ctx.messagingDel('/ProductMatch/Entries/' + pid(a.id));
61
+ },
62
+ product_match_deny_terms: async (a, ctx) => {
63
+ if (Array.isArray(a.terms)) return ctx.messagingPut('/ProductMatch/DenyTerms', { terms: a.terms.map(String) });
64
+ return ctx.messagingGet('/ProductMatch/DenyTerms');
65
+ },
66
+ };
@@ -0,0 +1,37 @@
1
+ // MCP parity for `velaro report custom-catalog | custom-run` (cli/lib/commands/report.js). Backend: velaro-messaging
2
+ // CustomReportsController (api/v1/custom-reports). Managers and Administrators only. Site from the credential.
3
+
4
+ export const tools = [
5
+ {
6
+ name: 'report_custom_catalog_get',
7
+ description: 'What the custom report builder offers per object (tickets, deals, contacts, companies): group-bys, measures, filters.',
8
+ inputSchema: { type: 'object', properties: {} },
9
+ },
10
+ {
11
+ name: 'report_custom_run',
12
+ description: 'Run a custom report: object, groupBy and measure from report_custom_catalog_get, optional filters and a day range (default 90, max 366).',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ object: { type: 'string', enum: ['tickets', 'deals', 'contacts', 'companies'] },
17
+ groupBy: { type: 'string' },
18
+ measure: { type: 'string' },
19
+ days: { type: 'number' },
20
+ filters: { type: 'object', additionalProperties: { type: 'string' } },
21
+ },
22
+ required: ['object', 'groupBy'],
23
+ },
24
+ },
25
+ ];
26
+
27
+ export const handlers = {
28
+ report_custom_catalog_get: async (_a, ctx) => (await ctx.messagingGet('/api/v1/custom-reports/catalog'))?.data,
29
+ report_custom_run: async (a, ctx) => {
30
+ const end = new Date();
31
+ const start = new Date(end.getTime() - Math.min(366, Math.max(1, a.days ?? 90)) * 86400000);
32
+ const r = await ctx.messagingPost('/api/v1/custom-reports/run', {
33
+ object: a.object, groupBy: a.groupBy, measure: a.measure ?? 'count', start: start.toISOString(), end: end.toISOString(), filters: a.filters ?? {},
34
+ });
35
+ return { rows: r?.data?.rows ?? [], meta: r?.meta ?? {} };
36
+ },
37
+ };
@@ -0,0 +1,229 @@
1
+ // MCP parity for `velaro ticket` (cli/lib/commands/ticket.js). Backend: velaro-messaging help desk parity endpoints
2
+ // (docs/architecture/ticketing-hubspot-parity-plan.md). Site from the credential, never an argument.
3
+
4
+ const TYPES = ['contact', 'company', 'deal', 'call'];
5
+ const ENTITIES = ['ticket', 'company'];
6
+ const FIELD_TYPES = ['Text', 'Number', 'Date', 'Dropdown', 'Checkbox', 'Currency'];
7
+
8
+ const data = (r) => (r && typeof r === 'object' && 'data' in r ? r.data : r);
9
+ const entityBase = (e) => (e === 'company' ? '/api/v1/company-properties' : '/api/v1/ticket-properties');
10
+ const recordBase = (e, id) => (e === 'company' ? `/api/v1/companies/${encodeURIComponent(id)}/properties` : `/api/v1/tickets/${encodeURIComponent(id)}/properties`);
11
+
12
+ const ticketId = { type: 'number', description: 'Ticket id (the ticket conversation id).' };
13
+
14
+ export const tools = [
15
+ {
16
+ name: 'ticket_association_list',
17
+ description: 'List the contacts, companies, deals and calls linked to a ticket. The ticket\'s own contact and that contact\'s company are always listed (primary / from contact). Calls include whether a recording exists.',
18
+ inputSchema: { type: 'object', properties: { ticketId }, required: ['ticketId'] },
19
+ },
20
+ {
21
+ name: 'ticket_association_add',
22
+ description: 'Link a contact, company, deal or call to a ticket (HubSpot-style association). targetId is the contact id, CRM company id, deal id or call record id. Optional label such as "Billing contact".',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ ticketId,
27
+ type: { type: 'string', enum: TYPES },
28
+ targetId: { type: 'number' },
29
+ label: { type: 'string', description: 'Optional association label (64 characters max).' },
30
+ },
31
+ required: ['ticketId', 'type', 'targetId'],
32
+ },
33
+ },
34
+ {
35
+ name: 'ticket_association_remove',
36
+ description: 'Unlink a contact, company, deal or call from a ticket.',
37
+ inputSchema: { type: 'object', properties: { ticketId, type: { type: 'string', enum: TYPES }, targetId: { type: 'number' } }, required: ['ticketId', 'type', 'targetId'] },
38
+ },
39
+ {
40
+ name: 'ticket_company_list',
41
+ description: 'List the tickets of one company: tickets linked to it plus tickets of its contacts. openOnly limits to tickets that are not resolved or closed.',
42
+ inputSchema: { type: 'object', properties: { companyId: { type: 'number' }, openOnly: { type: 'boolean' } }, required: ['companyId'] },
43
+ },
44
+ {
45
+ name: 'ticket_property_list',
46
+ description: 'List the custom property definitions for tickets (or companies): id, label, type, dropdown options, and whether stages may require it.',
47
+ inputSchema: { type: 'object', properties: { entity: { type: 'string', enum: ENTITIES, description: 'Default ticket.' } } },
48
+ },
49
+ {
50
+ name: 'ticket_property_define',
51
+ description: 'Create or update a custom property for tickets (or companies). Administrator only. Pass id to update. Dropdown needs options.',
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: {
55
+ entity: { type: 'string', enum: ENTITIES },
56
+ id: { type: 'number', description: 'Existing property id to update.' },
57
+ label: { type: 'string' },
58
+ fieldType: { type: 'string', enum: FIELD_TYPES },
59
+ options: { type: 'array', items: { type: 'string' } },
60
+ isRequired: { type: 'boolean', description: 'Marks it as one pipeline stages can require.' },
61
+ sortOrder: { type: 'number' },
62
+ },
63
+ required: ['label', 'fieldType'],
64
+ },
65
+ },
66
+ {
67
+ name: 'ticket_property_archive',
68
+ description: 'Archive (remove) a ticket or company property definition. Administrator only. Saved values stay in the database but stop showing.',
69
+ inputSchema: { type: 'object', properties: { entity: { type: 'string', enum: ENTITIES }, id: { type: 'number' } }, required: ['id'] },
70
+ },
71
+ {
72
+ name: 'ticket_property_get',
73
+ description: 'Get the custom property values of one ticket (or company), with each property\'s label and type.',
74
+ inputSchema: { type: 'object', properties: { entity: { type: 'string', enum: ENTITIES }, recordId: { type: 'number' } }, required: ['recordId'] },
75
+ },
76
+ {
77
+ name: 'ticket_property_set',
78
+ description: 'Set custom property values on a ticket (or company). values maps property id to value; an empty string clears it. Values are type-checked (number, date yyyy-mm-dd, true/false, dropdown option).',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ entity: { type: 'string', enum: ENTITIES },
83
+ recordId: { type: 'number' },
84
+ values: { type: 'object', additionalProperties: { type: 'string' } },
85
+ },
86
+ required: ['recordId', 'values'],
87
+ },
88
+ },
89
+ {
90
+ name: 'ticket_pipeline_list',
91
+ description: 'List ticket pipelines with their stages (stage id, name, the ticket status it stands for, required property ids).',
92
+ inputSchema: { type: 'object', properties: {} },
93
+ },
94
+ {
95
+ name: 'ticket_pipeline_save',
96
+ description: 'Create or update a ticket pipeline. Administrator only. Every pipeline needs at least one stage for each status (new, open, pending, resolved, closed). requiredPropertyIds must be ticket property ids.',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {
100
+ id: { type: 'number', description: 'Existing pipeline id to update.' },
101
+ name: { type: 'string' },
102
+ isDefault: { type: 'boolean', description: 'New tickets start in the default pipeline.' },
103
+ stages: {
104
+ type: 'array',
105
+ items: {
106
+ type: 'object',
107
+ properties: {
108
+ id: { type: 'string', description: 'Stable stage id; derived from the name when omitted.' },
109
+ name: { type: 'string' },
110
+ status: { type: 'string', enum: ['new', 'open', 'pending', 'resolved', 'closed'] },
111
+ requiredPropertyIds: { type: 'array', items: { type: 'number' } },
112
+ },
113
+ required: ['name', 'status'],
114
+ },
115
+ },
116
+ },
117
+ required: ['name', 'stages'],
118
+ },
119
+ },
120
+ {
121
+ name: 'ticket_pipeline_board_get',
122
+ description: 'Get a pipeline board: open tickets (and tickets resolved or closed in the last closedDays days) grouped by stage. Newest 500.',
123
+ inputSchema: { type: 'object', properties: { pipelineId: { type: 'number' }, closedDays: { type: 'number' } }, required: ['pipelineId'] },
124
+ },
125
+ {
126
+ name: 'ticket_pipeline_move',
127
+ description: 'Move a ticket to a pipeline stage. If the stage requires properties that are empty, the move is refused and the missing ones are listed. Moving to a resolved or closed stage resolves or closes the ticket.',
128
+ inputSchema: { type: 'object', properties: { ticketId, stageId: { type: 'string' }, pipelineId: { type: 'number' } }, required: ['ticketId', 'stageId'] },
129
+ },
130
+ {
131
+ name: 'ticket_view_list',
132
+ description: 'List Help Desk ticket views: built-in ones (My open, Unassigned, All open, Waiting on customer, SLA breached) and saved ones visible to you.',
133
+ inputSchema: { type: 'object', properties: {} },
134
+ },
135
+ {
136
+ name: 'ticket_view_save',
137
+ description: 'Create or update a saved ticket view. Sharing with a team or everyone needs a manager or administrator. Filters: status, priority, category, teamId, assignedUserId, assignedToMe, unassigned, overdue, slaBreached, tagId, search, propertyFieldId, propertyValue, sort.',
138
+ inputSchema: {
139
+ type: 'object',
140
+ properties: {
141
+ id: { type: 'number' },
142
+ name: { type: 'string' },
143
+ visibility: { type: 'string', enum: ['personal', 'team', 'everyone'] },
144
+ teamId: { type: 'number' },
145
+ filters: { type: 'object' },
146
+ },
147
+ required: ['name'],
148
+ },
149
+ },
150
+ {
151
+ name: 'ticket_view_delete',
152
+ description: 'Delete a saved ticket view you own (or a shared one, as a manager).',
153
+ inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] },
154
+ },
155
+ {
156
+ name: 'ticket_playbook_list',
157
+ description: 'List playbooks (guided scripts with questions) for tickets, calls or deals, with their steps. Step ids are the keys for ticket_playbook_respond answers.',
158
+ inputSchema: { type: 'object', properties: { appliesTo: { type: 'string', enum: ['ticket', 'call', 'deal'] } } },
159
+ },
160
+ {
161
+ name: 'ticket_playbook_respond',
162
+ description: 'Save playbook answers on a ticket, call or deal (answers keyed by step id). Answers mapped to ticket properties update them; an internal note records the answers.',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ playbookId: { type: 'number' },
167
+ recordType: { type: 'string', enum: ['ticket', 'call', 'deal'] },
168
+ recordId: { type: 'number' },
169
+ answers: { type: 'object', additionalProperties: { type: 'string' } },
170
+ },
171
+ required: ['playbookId', 'recordType', 'recordId'],
172
+ },
173
+ },
174
+ {
175
+ name: 'ticket_portal_link_get',
176
+ description: 'Get the customer portal link (customers sign in with an email code, see all their requests, open new ones, search help articles).',
177
+ inputSchema: { type: 'object', properties: {} },
178
+ },
179
+ {
180
+ name: 'ticket_portal_link_rotate',
181
+ description: 'Replace the customer portal link with a new one; the old link stops working. Administrator only.',
182
+ inputSchema: { type: 'object', properties: {} },
183
+ },
184
+ {
185
+ name: 'ticket_sla_policy_list',
186
+ description: 'List SLA policies: priority, first response / next response / resolution minutes, business hours only, schedule, pause while waiting on the customer.',
187
+ inputSchema: { type: 'object', properties: {} },
188
+ },
189
+ ];
190
+
191
+ export const handlers = {
192
+ ticket_association_list: async (a, ctx) => data(await ctx.messagingGet(`/api/v1/tickets/${encodeURIComponent(a.ticketId)}/associations`)),
193
+ ticket_association_add: async (a, ctx) =>
194
+ data(await ctx.messagingPost(`/api/v1/tickets/${encodeURIComponent(a.ticketId)}/associations`, { type: a.type, targetId: a.targetId, label: a.label ?? null })),
195
+ ticket_association_remove: async (a, ctx) =>
196
+ data(await ctx.messagingDel(`/api/v1/tickets/${encodeURIComponent(a.ticketId)}/associations/${encodeURIComponent(a.type)}/${encodeURIComponent(a.targetId)}`)),
197
+ ticket_company_list: async (a, ctx) =>
198
+ data(await ctx.messagingGet(`/api/v1/companies/${encodeURIComponent(a.companyId)}/tickets?openOnly=${a.openOnly ? 'true' : 'false'}`)),
199
+ ticket_property_list: async (a, ctx) => data(await ctx.messagingGet(entityBase(a.entity))),
200
+ ticket_property_define: async (a, ctx) => {
201
+ const body = { label: a.label, fieldType: a.fieldType, options: a.options ?? [], isRequired: !!a.isRequired, sortOrder: a.sortOrder ?? 0 };
202
+ return data(a.id ? await ctx.messagingPut(`${entityBase(a.entity)}/${encodeURIComponent(a.id)}`, body) : await ctx.messagingPost(entityBase(a.entity), body));
203
+ },
204
+ ticket_property_archive: async (a, ctx) => data(await ctx.messagingDel(`${entityBase(a.entity)}/${encodeURIComponent(a.id)}`)),
205
+ ticket_property_get: async (a, ctx) => data(await ctx.messagingGet(recordBase(a.entity, a.recordId))),
206
+ ticket_property_set: async (a, ctx) => data(await ctx.messagingPut(recordBase(a.entity, a.recordId), a.values ?? {})),
207
+ ticket_pipeline_list: async (_a, ctx) => data(await ctx.messagingGet('/api/v1/ticket-pipelines')),
208
+ ticket_pipeline_save: async (a, ctx) => {
209
+ const body = { name: a.name, isDefault: !!a.isDefault, stages: a.stages ?? [] };
210
+ return data(a.id ? await ctx.messagingPut(`/api/v1/ticket-pipelines/${encodeURIComponent(a.id)}`, body) : await ctx.messagingPost('/api/v1/ticket-pipelines', body));
211
+ },
212
+ ticket_pipeline_board_get: async (a, ctx) =>
213
+ data(await ctx.messagingGet(`/api/v1/ticket-pipelines/${encodeURIComponent(a.pipelineId)}/board?closedDays=${encodeURIComponent(a.closedDays ?? 14)}`)),
214
+ ticket_pipeline_move: async (a, ctx) =>
215
+ data(await ctx.messagingPut(`/api/v1/tickets/${encodeURIComponent(a.ticketId)}/stage`, { pipelineId: a.pipelineId ?? null, stageId: a.stageId })),
216
+ ticket_view_list: async (_a, ctx) => data(await ctx.messagingGet('/api/v1/ticket-views')),
217
+ ticket_view_save: async (a, ctx) => {
218
+ const body = { name: a.name, visibility: a.visibility ?? 'personal', teamId: a.teamId ?? null, filters: a.filters ?? {} };
219
+ return data(a.id ? await ctx.messagingPut(`/api/v1/ticket-views/${encodeURIComponent(a.id)}`, body) : await ctx.messagingPost('/api/v1/ticket-views', body));
220
+ },
221
+ ticket_view_delete: async (a, ctx) => data(await ctx.messagingDel(`/api/v1/ticket-views/${encodeURIComponent(a.id)}`)),
222
+ ticket_sla_policy_list: async (_a, ctx) => ({ policies: await ctx.messagingGet('/Ticket/sla-policies') }),
223
+ ticket_playbook_list: async (a, ctx) =>
224
+ data(await ctx.messagingGet(`/api/v1/playbooks${a.appliesTo ? `?appliesTo=${encodeURIComponent(a.appliesTo)}` : ''}`)),
225
+ ticket_playbook_respond: async (a, ctx) =>
226
+ data(await ctx.messagingPost(`/api/v1/playbooks/${encodeURIComponent(a.playbookId)}/responses`, { recordType: a.recordType, recordId: a.recordId, answers: a.answers ?? {} })),
227
+ ticket_portal_link_get: async (_a, ctx) => data(await ctx.messagingGet('/api/v1/ticket-portal')),
228
+ ticket_portal_link_rotate: async (_a, ctx) => data(await ctx.messagingPost('/api/v1/ticket-portal/rotate', {})),
229
+ };