@loopops/mcp-server 3.43.1 → 3.45.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/tools/cpq.d.ts +5 -4
- package/dist/tools/cpq.js +102 -5
- package/package.json +1 -1
package/dist/tools/cpq.d.ts
CHANGED
|
@@ -2,10 +2,11 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* CPQ tools — surface for the cpq_coach skill.
|
|
4
4
|
*
|
|
5
|
-
* The skill
|
|
6
|
-
* mode
|
|
7
|
-
* user invokes /cpq_coach,
|
|
8
|
-
*
|
|
5
|
+
* The skill (config/mcp/skills.yaml) bundles these into a workflow
|
|
6
|
+
* mode with system prompt + tool emphasis + auto-load of cpq_context.
|
|
7
|
+
* When a user invokes /cpq_coach, Claude is told to call cpq_context
|
|
8
|
+
* as turn-1, then helps the rep build / explain / modify / approve
|
|
9
|
+
* quotes from there.
|
|
9
10
|
*
|
|
10
11
|
* Authorization gates: each tool is conditionally registered based on
|
|
11
12
|
* the user's role grant from config/mcp/role-tool-grants.yaml.
|
package/dist/tools/cpq.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { trpcMutation, trpcQuery } from "../api-client.js";
|
|
2
3
|
import { safeTool } from "./_helpers.js";
|
|
3
4
|
/**
|
|
4
5
|
* CPQ tools — surface for the cpq_coach skill.
|
|
5
6
|
*
|
|
6
|
-
* The skill
|
|
7
|
-
* mode
|
|
8
|
-
* user invokes /cpq_coach,
|
|
9
|
-
*
|
|
7
|
+
* The skill (config/mcp/skills.yaml) bundles these into a workflow
|
|
8
|
+
* mode with system prompt + tool emphasis + auto-load of cpq_context.
|
|
9
|
+
* When a user invokes /cpq_coach, Claude is told to call cpq_context
|
|
10
|
+
* as turn-1, then helps the rep build / explain / modify / approve
|
|
11
|
+
* quotes from there.
|
|
10
12
|
*
|
|
11
13
|
* Authorization gates: each tool is conditionally registered based on
|
|
12
14
|
* the user's role grant from config/mcp/role-tool-grants.yaml.
|
|
@@ -15,4 +17,99 @@ export function registerCpqTools(server, allowed) {
|
|
|
15
17
|
if (allowed.has("cpq_context")) {
|
|
16
18
|
server.tool("cpq_context", "Loads the full ClickHouse CPQ context: the playbook (tier guidance, sizing heuristics, commit strategy, discount approach, validation rules, common mistakes, FAQ), the product catalog (16 products with rates), and the discount schedule (commit × term grid). Call this ONCE per CPQ conversation before answering any pricing or quote-building question. The cpq_coach skill auto-loads this.", {}, safeTool(async () => trpcQuery("mcp.cpqContext")));
|
|
17
19
|
}
|
|
20
|
+
if (allowed.has("my_quotes")) {
|
|
21
|
+
server.tool("my_quotes", "List Quotes owned by the authenticated user. Defaults to Draft + Presented; pass `status` to filter. Returns a markdown table with Quote name, opportunity, tier, term, commit, discount, total, and status.", {
|
|
22
|
+
status: z
|
|
23
|
+
.enum(["Draft", "Presented", "Accepted", "Rejected", "Expired"])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Filter by Quote.Status. Omit to see open quotes (Draft + Presented)."),
|
|
26
|
+
}, safeTool(async ({ status }) => trpcQuery("mcp.myQuotes", { status })));
|
|
27
|
+
}
|
|
28
|
+
if (allowed.has("find_opportunity")) {
|
|
29
|
+
server.tool("find_opportunity", "List open Salesforce Opportunities for accounts matching a name (case-insensitive substring). Use this BEFORE draft_quote_from_intent when the rep gave you a customer name but not the 006... Opportunity Id. Returns a table of opp Id / account / opp name / stage / amount / close date. If exactly one match, you can call draft_quote_from_intent with that Id directly. (Alternatively, draft_quote_from_intent itself accepts accountName and resolves internally — use this tool only when you want to show the rep candidates explicitly.)", {
|
|
30
|
+
accountName: z
|
|
31
|
+
.string()
|
|
32
|
+
.min(2)
|
|
33
|
+
.max(200)
|
|
34
|
+
.describe("Customer account name. Case-insensitive substring match."),
|
|
35
|
+
}, safeTool(async ({ accountName }) => trpcQuery("mcp.findOpportunity", { accountName })));
|
|
36
|
+
}
|
|
37
|
+
if (allowed.has("quote_show")) {
|
|
38
|
+
server.tool("quote_show", "Full detail on one Quote: deal shape (tier/region/term/commit/discount), line items with per-line discount, monthly run-rate, annualized run-rate, commit utilization, and AI audit metadata if the quote was generated by an agent. Look up by Quote Id, Quote name, or Opportunity name (uses the most recently modified Quote on a matching opp).", {
|
|
39
|
+
quoteId: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Salesforce Quote Id (starts with 0Q0)."),
|
|
43
|
+
quoteName: z.string().optional().describe("Quote name."),
|
|
44
|
+
opportunityName: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Find the most recent Quote on an Opportunity matching this name."),
|
|
48
|
+
}, safeTool(async ({ quoteId, quoteName, opportunityName }) => trpcQuery("mcp.quoteShow", {
|
|
49
|
+
quoteId,
|
|
50
|
+
quoteName,
|
|
51
|
+
opportunityName,
|
|
52
|
+
})));
|
|
53
|
+
}
|
|
54
|
+
if (allowed.has("draft_quote_from_intent")) {
|
|
55
|
+
server.tool("draft_quote_from_intent", "Generate a Salesforce Quote from a free-form sizing intent. Claude Sonnet 4.6 parses the intent into a structured sizing payload, then the Apex Invocable CpqQuoteBuilder creates the Quote + line items with the negotiated discount applied. Pass EITHER opportunityId OR accountName (you typically know the account name; the tool resolves internally to the single open opp. If multiple opps match the name, the tool returns a disambiguation prompt). If the model's parse confidence is below 0.6 (a critical sizing field is ambiguous), the tool returns a clarifying question instead of writing. Quote ships in Status = Draft; the rep approves separately.", {
|
|
56
|
+
opportunityId: z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(/^006/, "expected a Salesforce Opportunity Id")
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Salesforce Opportunity Id (starts with 006). Provide this OR accountName."),
|
|
61
|
+
accountName: z
|
|
62
|
+
.string()
|
|
63
|
+
.min(2)
|
|
64
|
+
.max(200)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Customer account name (e.g. 'Vercel'). Resolved internally to a single open opp. Provide this OR opportunityId."),
|
|
67
|
+
intent: z
|
|
68
|
+
.string()
|
|
69
|
+
.min(20)
|
|
70
|
+
.max(2000)
|
|
71
|
+
.describe("Free-form sizing description from the rep — e.g. 'Customer wants 5TB analytics, 2 prod replicas at 16GB, always-on, EU compliance, 3-year deal'."),
|
|
72
|
+
termMonths: z
|
|
73
|
+
.union([z.literal(12), z.literal(24), z.literal(36)])
|
|
74
|
+
.default(12),
|
|
75
|
+
region: z.string().default("us-east-1"),
|
|
76
|
+
cloudProvider: z.string().default("AWS"),
|
|
77
|
+
}, safeTool(async ({ opportunityId, accountName, intent, termMonths, region, cloudProvider }) => trpcMutation("mcp.draftQuoteFromIntent", {
|
|
78
|
+
opportunityId,
|
|
79
|
+
accountName,
|
|
80
|
+
intent,
|
|
81
|
+
termMonths,
|
|
82
|
+
region,
|
|
83
|
+
cloudProvider,
|
|
84
|
+
})));
|
|
85
|
+
}
|
|
86
|
+
if (allowed.has("update_quote_sizing")) {
|
|
87
|
+
server.tool("update_quote_sizing", "Modify the sizing on a Draft quote — pass only the fields to change. Internally re-runs CpqQuoteBuilder with the merged payload so the discount cascade is clean; the old Quote is marked Expired and a fresh one created with the same opportunity. Only works on Status = Draft.", {
|
|
88
|
+
quoteId: z.string().regex(/^0Q0/),
|
|
89
|
+
tier: z.enum(["Basic", "Scale", "Enterprise"]).optional(),
|
|
90
|
+
termMonths: z.union([z.literal(12), z.literal(24), z.literal(36)]).optional(),
|
|
91
|
+
annualCommit: z.number().nonnegative().optional(),
|
|
92
|
+
replicaCount: z.number().int().positive().optional(),
|
|
93
|
+
ramGb: z.number().int().positive().optional(),
|
|
94
|
+
hoursPerDay: z.number().positive().max(24).optional(),
|
|
95
|
+
storageGb: z.number().nonnegative().optional(),
|
|
96
|
+
clickpipesReplicas: z.number().int().nonnegative().optional(),
|
|
97
|
+
}, safeTool(async (args) => trpcMutation("mcp.updateQuoteSizing", args)));
|
|
98
|
+
}
|
|
99
|
+
if (allowed.has("approve_quote")) {
|
|
100
|
+
server.tool("approve_quote", "Approve a Draft quote — flips Status to Presented. The CPQ Coach should confirm the discount with the rep first if > 20% (would normally route through ops approval).", {
|
|
101
|
+
quoteId: z.string().regex(/^0Q0/),
|
|
102
|
+
comments: z.string().max(500).optional(),
|
|
103
|
+
}, safeTool(async ({ quoteId, comments }) => trpcMutation("mcp.approveQuote", { quoteId, comments })));
|
|
104
|
+
}
|
|
105
|
+
if (allowed.has("reject_quote")) {
|
|
106
|
+
server.tool("reject_quote", "Reject a quote — sets Status = Rejected with a reason captured on Description. Use when the customer pushed back, the deal shape changed, or the quote was drafted in error.", {
|
|
107
|
+
quoteId: z.string().regex(/^0Q0/),
|
|
108
|
+
reason: z
|
|
109
|
+
.string()
|
|
110
|
+
.min(1)
|
|
111
|
+
.max(500)
|
|
112
|
+
.describe("Why the quote is being rejected. Captured on Description."),
|
|
113
|
+
}, safeTool(async ({ quoteId, reason }) => trpcMutation("mcp.rejectQuote", { quoteId, reason })));
|
|
114
|
+
}
|
|
18
115
|
}
|