@velaro/mcp-server 1.0.2 → 2.0.1
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 +3 -1
- package/parity/actions.customer.js +39 -0
- package/parity/activity.customer.js +33 -0
- package/parity/ai-field-extraction.customer.js +78 -0
- package/parity/callback.customer.js +22 -0
- package/parity/check.customer.js +31 -0
- package/parity/deployment.customer.js +45 -0
- package/parity/endpoints.customer.js +87 -0
- package/parity/ingest.customer.js +22 -0
- package/parity/keyword-intelligence.customer.js +33 -0
- package/parity/knowledge-advisor.customer.js +26 -0
- package/parity/loader.js +32 -0
- package/parity/quality.customer.js +122 -0
- package/parity/rule.customer.js +48 -0
- package/parity/sentiment.customer.js +41 -0
- package/parity/site-builder.customer.js +12 -0
- package/parity/skill-routing.customer.js +79 -0
- package/parity/squarespace.customer.js +44 -0
- package/parity/woocommerce.customer.js +94 -0
- package/parity/zoom-phone.customer.js +49 -0
- package/server.js +365 -3974
- package/server.json +2 -2
- package/tool-annotations.js +8 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// MCP parity for `velaro rule` (cli/lib/commands/rule.js). Site comes from the credential; no siteId argument.
|
|
2
|
+
export const tools = [
|
|
3
|
+
{
|
|
4
|
+
name: 'rule_list',
|
|
5
|
+
description: 'List the routing rules for your site (id, name, priority, trigger), sorted by priority.',
|
|
6
|
+
inputSchema: { type: 'object', properties: {} },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'rule_create',
|
|
10
|
+
description: 'Create an enabled routing rule that sends matching visitors to a team (trigger e.g. pageUrl or country; lower priority number wins).',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
name: { type: 'string', description: 'Rule name' },
|
|
15
|
+
trigger: { type: 'string', description: 'Trigger type (e.g. pageUrl, country)' },
|
|
16
|
+
value: { type: 'string', description: 'Trigger value to match' },
|
|
17
|
+
teamId: { type: 'number', description: 'Team ID to route to' },
|
|
18
|
+
priority: { type: 'number', description: 'Rule priority (lower = higher priority). Default 0.' },
|
|
19
|
+
},
|
|
20
|
+
required: ['name', 'trigger', 'value', 'teamId'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'rule_delete',
|
|
25
|
+
description: 'Delete a routing rule by id.',
|
|
26
|
+
inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Rule ID' } }, required: ['id'] },
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const handlers = {
|
|
31
|
+
rule_list: async (_a, ctx) => {
|
|
32
|
+
const rules = (await ctx.messagingGet('/Rules/List')) || [];
|
|
33
|
+
return [...rules].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
34
|
+
},
|
|
35
|
+
rule_create: async (a, ctx) => {
|
|
36
|
+
for (const k of ['name', 'trigger', 'value']) if (!a[k]) throw new Error(`${k} is required`);
|
|
37
|
+
if (!Number.isInteger(Number(a.teamId))) throw new Error('teamId is required');
|
|
38
|
+
return ctx.messagingPost('/Rules', {
|
|
39
|
+
name: a.name, trigger: a.trigger, value: a.value, teamId: Number(a.teamId), priority: Number(a.priority ?? 0), isEnabled: true,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
rule_delete: async (a, ctx) => {
|
|
43
|
+
if (!Number.isInteger(Number(a.id))) throw new Error('id is required');
|
|
44
|
+
// RulesController.DeleteAsync takes the rule in the BODY (DELETE /Rules {id}); same as the CLI.
|
|
45
|
+
await ctx.messagingRequest('DELETE', '/Rules', { id: Number(a.id) });
|
|
46
|
+
return { deleted: Number(a.id) };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// MCP parity for `velaro sentiment` (cli/lib/commands/quality-reports.js). Site from credential; deployment/team verified server-side.
|
|
2
|
+
const ROUTES = {
|
|
3
|
+
overview: 'sentiment-overview',
|
|
4
|
+
breakdown: 'sentiment-breakdown',
|
|
5
|
+
'conversation-overview': 'conversation-sentiment-overview',
|
|
6
|
+
'conversation-breakdown': 'conversation-sentiment-breakdown',
|
|
7
|
+
'contact-overview': 'contact-sentiment-overview',
|
|
8
|
+
'agent-breakdown': 'agent-sentiment-breakdown',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const tools = [
|
|
12
|
+
{
|
|
13
|
+
name: 'sentiment_report',
|
|
14
|
+
description: 'Sentiment report for your site, optionally scoped to one website (deploymentId) or team. kind selects overview, breakdown, conversation-overview, conversation-breakdown, contact-overview or agent-breakdown. Defaults to the last 30 days.',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
kind: { type: 'string', enum: Object.keys(ROUTES), description: 'Report kind (default overview)' },
|
|
19
|
+
from: { type: 'string', description: 'Start date/time (ISO). Default 30 days before "to".' },
|
|
20
|
+
to: { type: 'string', description: 'End date/time (ISO). Default now.' },
|
|
21
|
+
groupBy: { type: 'string', description: 'Grouping (default day)' },
|
|
22
|
+
deploymentId: { type: 'number', description: 'Website (deployment row id)' },
|
|
23
|
+
teamId: { type: 'number', description: 'Team id' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const handlers = {
|
|
30
|
+
sentiment_report: async (a, ctx) => {
|
|
31
|
+
const kind = a.kind || 'overview';
|
|
32
|
+
if (!ROUTES[kind]) throw new Error(`kind must be one of ${Object.keys(ROUTES).join(', ')}`);
|
|
33
|
+
const end = a.to ? new Date(a.to) : new Date();
|
|
34
|
+
const start = a.from ? new Date(a.from) : new Date(end.getTime() - 30 * 86400000);
|
|
35
|
+
if (isNaN(start) || isNaN(end)) throw new Error('from/to must be valid dates');
|
|
36
|
+
const body = { Start: start.toISOString(), End: end.toISOString(), GroupBy: a.groupBy || 'day' };
|
|
37
|
+
if (a.deploymentId != null) body.DeploymentId = Number(a.deploymentId);
|
|
38
|
+
if (a.teamId != null) body.TeamId = Number(a.teamId);
|
|
39
|
+
return ctx.messagingPost(`/Reports/${ROUTES[kind]}`, body);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// MCP parity for `velaro site-builder connect-info`. The site is the caller's own (credential), never an argument.
|
|
2
|
+
export const tools = [
|
|
3
|
+
{
|
|
4
|
+
name: 'site_builder_connect_info',
|
|
5
|
+
description: 'Get your own site ID and chat snippet for pasting into a website builder.',
|
|
6
|
+
inputSchema: { type: 'object', properties: {} },
|
|
7
|
+
},
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const handlers = {
|
|
11
|
+
site_builder_connect_info: async (_a, ctx) => ctx.messagingGet('/integrations/site-builder/connect-info'),
|
|
12
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// MCP parity for `velaro skill-routing` (cli/lib/commands/skill-routing.js). Site from the credential (velaro-messaging
|
|
2
|
+
// /SkillRouting, gated by the EnableSkillBasedRouting entitlement). Requirements are disabled by default; PUT is partial.
|
|
3
|
+
const RESOURCES = ['Skills', 'Assignments', 'Requirements'];
|
|
4
|
+
const idList = (v) => (v == null ? undefined : (Array.isArray(v) ? v : String(v).split(',')).map((x) => Number(x)).filter((n) => !Number.isNaN(n)));
|
|
5
|
+
const strip = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
6
|
+
const resource = (a) => {
|
|
7
|
+
const r = RESOURCES.find((x) => x.toLowerCase() === String(a.resource || '').toLowerCase());
|
|
8
|
+
if (!r) throw new Error(`resource must be one of ${RESOURCES.join(', ')}`);
|
|
9
|
+
return r;
|
|
10
|
+
};
|
|
11
|
+
const bodyFor = (r, a) => {
|
|
12
|
+
if (r === 'Skills') return strip({ Name: a.name, Description: a.description });
|
|
13
|
+
if (r === 'Assignments') return strip({ WorkspaceUserId: a.agent, AgentSkillId: a.skill, Proficiency: a.proficiency });
|
|
14
|
+
return strip({
|
|
15
|
+
DeploymentId: a.deployment, TeamId: a.team, RequiredSkillIds: idList(a.required), PreferredSkillIds: idList(a.preferred),
|
|
16
|
+
OverflowWaitSeconds: a.overflowWait, IsEnabled: a.enabled,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const fieldProps = {
|
|
20
|
+
resource: { type: 'string', enum: RESOURCES, description: 'Skills (catalog), Assignments (agent skills) or Requirements (per deployment/team)' },
|
|
21
|
+
};
|
|
22
|
+
const props = () => ({
|
|
23
|
+
resource: fieldProps.resource,
|
|
24
|
+
name: { type: 'string', description: 'Skills: skill name' },
|
|
25
|
+
description: { type: 'string', description: 'Skills: description' },
|
|
26
|
+
agent: { type: 'number', description: 'Assignments: workspaceUserId' },
|
|
27
|
+
skill: { type: 'number', description: 'Assignments: agentSkillId' },
|
|
28
|
+
proficiency: { type: 'number', description: 'Assignments: 1-5' },
|
|
29
|
+
deployment: { type: 'number', description: 'Requirements: deployment id' },
|
|
30
|
+
team: { type: 'number', description: 'Requirements: team id' },
|
|
31
|
+
required: { type: 'string', description: 'Requirements: comma-separated required skill ids' },
|
|
32
|
+
preferred: { type: 'string', description: 'Requirements: comma-separated preferred skill ids' },
|
|
33
|
+
overflowWait: { type: 'number', description: 'Requirements: seconds before falling back to all agents' },
|
|
34
|
+
enabled: { type: 'boolean', description: 'Requirements: turn on/off (default off)' },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const tools = [
|
|
38
|
+
{
|
|
39
|
+
name: 'skill_routing_list',
|
|
40
|
+
description: 'List skill-based routing rows for your site: the skill catalog, agent skill assignments, or per-deployment/team requirements.',
|
|
41
|
+
inputSchema: { type: 'object', properties: { resource: fieldProps.resource }, required: ['resource'] },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'skill_routing_create',
|
|
45
|
+
description: 'Create a skill-based routing row (skill, agent skill assignment, or requirement). Requirements are created disabled unless enabled is true.',
|
|
46
|
+
inputSchema: { type: 'object', properties: props(), required: ['resource'] },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'skill_routing_update',
|
|
50
|
+
description: 'Partially update a skill-based routing row by id (omitted fields are unchanged).',
|
|
51
|
+
inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Row id' }, ...props() }, required: ['resource', 'id'] },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'skill_routing_delete',
|
|
55
|
+
description: 'Delete a skill-based routing row by id.',
|
|
56
|
+
inputSchema: { type: 'object', properties: { resource: fieldProps.resource, id: { type: 'number', description: 'Row id' } }, required: ['resource', 'id'] },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'skill_routing_simulate',
|
|
60
|
+
description: 'Dry-run skill routing for a team. Never assigns an agent and never writes the decision log.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: { team: { type: 'number', description: 'Team id' }, waited: { type: 'number', description: 'Seconds the conversation has waited' } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const needId = (a) => {
|
|
69
|
+
if (!Number.isInteger(Number(a.id))) throw new Error('id is required');
|
|
70
|
+
return Number(a.id);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const handlers = {
|
|
74
|
+
skill_routing_list: async (a, ctx) => ctx.messagingGet(`/SkillRouting/${resource(a)}`),
|
|
75
|
+
skill_routing_create: async (a, ctx) => { const r = resource(a); return ctx.messagingPost(`/SkillRouting/${r}`, bodyFor(r, a)); },
|
|
76
|
+
skill_routing_update: async (a, ctx) => { const r = resource(a); return ctx.messagingPut(`/SkillRouting/${r}/${needId(a)}`, bodyFor(r, a)); },
|
|
77
|
+
skill_routing_delete: async (a, ctx) => ctx.messagingDel(`/SkillRouting/${resource(a)}/${needId(a)}`),
|
|
78
|
+
skill_routing_simulate: async (a, ctx) => ctx.messagingPost('/SkillRouting/Simulate', { TeamId: a.team ?? null, WaitedSeconds: a.waited ?? null }),
|
|
79
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// MCP parity for `velaro squarespace` (cli/lib/commands/squarespace.js). Site from the credential. Read-only integration.
|
|
2
|
+
export const tools = [
|
|
3
|
+
{
|
|
4
|
+
name: 'squarespace_get_status',
|
|
5
|
+
description: 'Show the Squarespace Commerce connection status for your site (connected, store URL, whether an API key is on file).',
|
|
6
|
+
inputSchema: { type: 'object', properties: {} },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'squarespace_configure',
|
|
10
|
+
description: 'Connect a Squarespace store using a Commerce API key (storeUrl is optional, reference only). The key is stored server-side and never returned.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
apiKey: { type: 'string', description: 'Squarespace Commerce API key' },
|
|
15
|
+
storeUrl: { type: 'string', description: 'Store URL (optional)' },
|
|
16
|
+
},
|
|
17
|
+
required: ['apiKey'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'squarespace_disconnect',
|
|
22
|
+
description: 'Disconnect the Squarespace store from your site.',
|
|
23
|
+
inputSchema: { type: 'object', properties: {} },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'squarespace_test_connection',
|
|
27
|
+
description: 'Test the current Squarespace connection without changing anything.',
|
|
28
|
+
inputSchema: { type: 'object', properties: {} },
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const handlers = {
|
|
33
|
+
squarespace_get_status: async (_a, ctx) => ctx.api('GET', '/Squarespace/Config'),
|
|
34
|
+
squarespace_configure: async (a, ctx) => {
|
|
35
|
+
if (!a.apiKey) throw new Error('apiKey is required');
|
|
36
|
+
return ctx.api('POST', '/Squarespace/Config', { apiKey: a.apiKey, storeUrl: a.storeUrl || '' });
|
|
37
|
+
},
|
|
38
|
+
squarespace_disconnect: async (_a, ctx) => {
|
|
39
|
+
const cfg = await ctx.api('GET', '/Squarespace/Config');
|
|
40
|
+
if (!cfg?.connected) return { success: true, message: 'Squarespace is not connected.' };
|
|
41
|
+
return ctx.api('DELETE', `/Squarespace/Config/${cfg.id}`);
|
|
42
|
+
},
|
|
43
|
+
squarespace_test_connection: async (_a, ctx) => ctx.api('POST', '/Squarespace/TestConnection', {}),
|
|
44
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// MCP parity for `velaro woocommerce` (cli/lib/commands/woocommerce.js). Site from the credential; the CLI's optional --site
|
|
2
|
+
// override is deliberately NOT exposed (tenant isolation).
|
|
3
|
+
const lim = (v) => Math.min(50, Math.max(1, Number(v) || 10));
|
|
4
|
+
|
|
5
|
+
export const tools = [
|
|
6
|
+
{
|
|
7
|
+
name: 'woocommerce_get_status',
|
|
8
|
+
description: 'Show the WooCommerce connection and billing status for your site.',
|
|
9
|
+
inputSchema: { type: 'object', properties: {} },
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'woocommerce_start_connect',
|
|
13
|
+
description: 'Start the WooCommerce store connection flow. Returns an authorization URL the store owner must open to approve Velaro.',
|
|
14
|
+
inputSchema: { type: 'object', properties: { storeUrl: { type: 'string', description: 'Store URL, e.g. https://mystore.com' } }, required: ['storeUrl'] },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'woocommerce_disconnect',
|
|
18
|
+
description: 'Disconnect the WooCommerce store from your site.',
|
|
19
|
+
inputSchema: { type: 'object', properties: {} },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'woocommerce_list_skills',
|
|
23
|
+
description: 'List the WooCommerce AI skills available to your site.',
|
|
24
|
+
inputSchema: { type: 'object', properties: {} },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'woocommerce_search_orders',
|
|
28
|
+
description: 'Search WooCommerce orders by customer email or order ID.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
email: { type: 'string', description: 'Customer email address' },
|
|
33
|
+
orderId: { type: 'number', description: 'Order ID' },
|
|
34
|
+
limit: { type: 'number', description: 'Max results (default 10, max 50)' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'woocommerce_search_products',
|
|
40
|
+
description: 'Search the WooCommerce product catalog.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: { query: { type: 'string', description: 'Search term' }, limit: { type: 'number', description: 'Max results (default 10, max 50)' } },
|
|
44
|
+
required: ['query'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'woocommerce_logs',
|
|
49
|
+
description: 'View WooCommerce integration activity logs for your site, optionally filtered by severity or text.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
severity: { type: 'string', description: 'Error, Warning or Info' },
|
|
54
|
+
search: { type: 'string', description: 'Text to find in messages' },
|
|
55
|
+
pageSize: { type: 'number', description: 'Entries to return (default 25, max 100)' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const handlers = {
|
|
62
|
+
woocommerce_get_status: async (_a, ctx) => (await ctx.messagingGet('/WooCommerce/billing/status')) ?? { connected: false },
|
|
63
|
+
woocommerce_start_connect: async (a, ctx) => {
|
|
64
|
+
if (!a.storeUrl) throw new Error('storeUrl is required');
|
|
65
|
+
const r = await ctx.messagingGet(`/WooCommerce/auth/start?storeUrl=${encodeURIComponent(a.storeUrl)}`);
|
|
66
|
+
if (!r?.authUrl) throw new Error('Server did not return an authUrl; check the store URL.');
|
|
67
|
+
return r;
|
|
68
|
+
},
|
|
69
|
+
woocommerce_disconnect: async (_a, ctx) => {
|
|
70
|
+
await ctx.messagingPost('/WooCommerce/auth/disconnect', {});
|
|
71
|
+
return { disconnected: true };
|
|
72
|
+
},
|
|
73
|
+
woocommerce_list_skills: async (_a, ctx) => {
|
|
74
|
+
const data = await ctx.api('GET', '/AI/Skills');
|
|
75
|
+
const skills = Array.isArray(data) ? data : (data?.skills ?? []);
|
|
76
|
+
return skills.filter((s) => s.category === 'WooCommerce');
|
|
77
|
+
},
|
|
78
|
+
woocommerce_search_orders: async (a, ctx) => {
|
|
79
|
+
const q = a.orderId ?? a.email;
|
|
80
|
+
if (!q) throw new Error('email or orderId is required');
|
|
81
|
+
return ctx.messagingGet(`/WooCommerce/data/orders?email=${encodeURIComponent(q)}&limit=${lim(a.limit)}`);
|
|
82
|
+
},
|
|
83
|
+
woocommerce_search_products: async (a, ctx) => {
|
|
84
|
+
if (!a.query) throw new Error('query is required');
|
|
85
|
+
return ctx.messagingGet(`/WooCommerce/data/products?query=${encodeURIComponent(a.query)}&limit=${lim(a.limit)}`);
|
|
86
|
+
},
|
|
87
|
+
woocommerce_logs: async (a, ctx) => {
|
|
88
|
+
const p = new URLSearchParams();
|
|
89
|
+
if (a.severity) p.set('severity', a.severity);
|
|
90
|
+
if (a.search) p.set('search', a.search);
|
|
91
|
+
p.set('pageSize', String(Math.min(100, Math.max(1, Number(a.pageSize) || 25))));
|
|
92
|
+
return ctx.api('GET', `/Integration/Logs/WooCommerce?${p}`);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// MCP parity for `velaro zoom-phone` (cli/lib/commands/zoom-phone.js). Site from the credential.
|
|
2
|
+
export const tools = [
|
|
3
|
+
{
|
|
4
|
+
name: 'zoom_phone_get_status',
|
|
5
|
+
description: 'Show the voice provider, Zoom account_id and whether a webhook secret is saved for your site (the secret is never returned).',
|
|
6
|
+
inputSchema: { type: 'object', properties: {} },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'zoom_phone_list_numbers',
|
|
10
|
+
description: 'List the Zoom Phone numbers assigned on the connected Zoom account.',
|
|
11
|
+
inputSchema: { type: 'object', properties: {} },
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'zoom_phone_set_config',
|
|
15
|
+
description: 'Save the Zoom account_id and/or webhook secret for Zoom Phone screen-pop. Refuses if the site already uses a different voice provider.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
accountId: { type: 'string', description: 'Zoom account_id (from a webhook payload or Account Profile)' },
|
|
20
|
+
webhookSecret: { type: 'string', description: 'Secret token from the Zoom Event Subscription' },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const handlers = {
|
|
27
|
+
zoom_phone_get_status: async (_a, ctx) => {
|
|
28
|
+
const c = await ctx.messagingGet('/voice/admin/config');
|
|
29
|
+
if (!c?.configured) return { configured: false };
|
|
30
|
+
return { configured: true, providerSlug: c.providerSlug, zoomAccountId: c.zoomAccountId || null, hasWebhookSecret: !!c.hasWebhookSecret };
|
|
31
|
+
},
|
|
32
|
+
zoom_phone_list_numbers: async (_a, ctx) => {
|
|
33
|
+
const r = await ctx.messagingGet('/ZoomPhoneCti/numbers');
|
|
34
|
+
return { phoneNumbers: r?.phoneNumbers || [] };
|
|
35
|
+
},
|
|
36
|
+
zoom_phone_set_config: async (a, ctx) => {
|
|
37
|
+
if (!a.accountId && !a.webhookSecret) throw new Error('Pass accountId and/or webhookSecret.');
|
|
38
|
+
const current = await ctx.messagingGet('/voice/admin/config');
|
|
39
|
+
if (current?.configured && current.providerSlug && current.providerSlug !== 'zoom') {
|
|
40
|
+
throw new Error(`Site already uses voice provider "${current.providerSlug}". Switch providers in the admin UI before configuring Zoom Phone.`);
|
|
41
|
+
}
|
|
42
|
+
await ctx.messagingPost('/voice/admin/config', {
|
|
43
|
+
providerSlug: 'zoom',
|
|
44
|
+
zoomAccountId: a.accountId ? String(a.accountId).trim() : undefined,
|
|
45
|
+
webhookSecret: a.webhookSecret || undefined,
|
|
46
|
+
});
|
|
47
|
+
return handlers.zoom_phone_get_status({}, ctx);
|
|
48
|
+
},
|
|
49
|
+
};
|