@loopops/mcp-server 3.43.1 → 3.44.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.
@@ -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 in config/mcp/skills.yaml bundles these into a workflow
6
- * mode (system prompt + tool emphasis + auto-load context). When a
7
- * user invokes /cpq_coach, the MCP client loads the prompt + Claude
8
- * is told to call `cpq_context` as turn-1.
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 { trpcQuery } from "../api-client.js";
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 in config/mcp/skills.yaml bundles these into a workflow
7
- * mode (system prompt + tool emphasis + auto-load context). When a
8
- * user invokes /cpq_coach, the MCP client loads the prompt + Claude
9
- * is told to call `cpq_context` as turn-1.
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,82 @@ 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("quote_show")) {
29
+ 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).", {
30
+ quoteId: z
31
+ .string()
32
+ .optional()
33
+ .describe("Salesforce Quote Id (starts with 0Q0)."),
34
+ quoteName: z.string().optional().describe("Quote name."),
35
+ opportunityName: z
36
+ .string()
37
+ .optional()
38
+ .describe("Find the most recent Quote on an Opportunity matching this name."),
39
+ }, safeTool(async ({ quoteId, quoteName, opportunityName }) => trpcQuery("mcp.quoteShow", {
40
+ quoteId,
41
+ quoteName,
42
+ opportunityName,
43
+ })));
44
+ }
45
+ if (allowed.has("draft_quote_from_intent")) {
46
+ 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. If the model's parse confidence is below 0.6 (a critical field is ambiguous), this tool returns a clarifying question instead of writing. Quote ships in Status = Draft; the rep approves separately.", {
47
+ opportunityId: z
48
+ .string()
49
+ .regex(/^006/, "expected a Salesforce Opportunity Id")
50
+ .describe("Salesforce Opportunity Id (starts with 006)."),
51
+ intent: z
52
+ .string()
53
+ .min(20)
54
+ .max(2000)
55
+ .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'."),
56
+ termMonths: z
57
+ .union([z.literal(12), z.literal(24), z.literal(36)])
58
+ .default(12),
59
+ region: z.string().default("us-east-1"),
60
+ cloudProvider: z.string().default("AWS"),
61
+ }, safeTool(async ({ opportunityId, intent, termMonths, region, cloudProvider }) => trpcMutation("mcp.draftQuoteFromIntent", {
62
+ opportunityId,
63
+ intent,
64
+ termMonths,
65
+ region,
66
+ cloudProvider,
67
+ })));
68
+ }
69
+ if (allowed.has("update_quote_sizing")) {
70
+ 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.", {
71
+ quoteId: z.string().regex(/^0Q0/),
72
+ tier: z.enum(["Basic", "Scale", "Enterprise"]).optional(),
73
+ termMonths: z.union([z.literal(12), z.literal(24), z.literal(36)]).optional(),
74
+ annualCommit: z.number().nonnegative().optional(),
75
+ replicaCount: z.number().int().positive().optional(),
76
+ ramGb: z.number().int().positive().optional(),
77
+ hoursPerDay: z.number().positive().max(24).optional(),
78
+ storageGb: z.number().nonnegative().optional(),
79
+ clickpipesReplicas: z.number().int().nonnegative().optional(),
80
+ }, safeTool(async (args) => trpcMutation("mcp.updateQuoteSizing", args)));
81
+ }
82
+ if (allowed.has("approve_quote")) {
83
+ 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).", {
84
+ quoteId: z.string().regex(/^0Q0/),
85
+ comments: z.string().max(500).optional(),
86
+ }, safeTool(async ({ quoteId, comments }) => trpcMutation("mcp.approveQuote", { quoteId, comments })));
87
+ }
88
+ if (allowed.has("reject_quote")) {
89
+ 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.", {
90
+ quoteId: z.string().regex(/^0Q0/),
91
+ reason: z
92
+ .string()
93
+ .min(1)
94
+ .max(500)
95
+ .describe("Why the quote is being rejected. Captured on Description."),
96
+ }, safeTool(async ({ quoteId, reason }) => trpcMutation("mcp.rejectQuote", { quoteId, reason })));
97
+ }
18
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "3.43.1",
3
+ "version": "3.44.0",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",