@loopops/mcp-server 3.4.0 → 3.6.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/index.js +2 -0
- package/dist/tools/scoring.d.ts +11 -0
- package/dist/tools/scoring.js +102 -0
- package/dist/tools/tal.js +24 -70
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { registerEngTools } from "./tools/eng.js";
|
|
|
13
13
|
import { registerCrmTools } from "./tools/crm.js";
|
|
14
14
|
import { registerEngageTools } from "./tools/engage.js";
|
|
15
15
|
import { registerAccountMasterTools } from "./tools/account-master.js";
|
|
16
|
+
import { registerScoringTools } from "./tools/scoring.js";
|
|
16
17
|
import { registerTalTools } from "./tools/tal.js";
|
|
17
18
|
// Read our own package.json at runtime so the version baked into MCP
|
|
18
19
|
// initialize-handshake `serverInfo.version` matches the published npm
|
|
@@ -57,5 +58,6 @@ registerCrmTools(server, allowedSkills);
|
|
|
57
58
|
registerEngageTools(server, allowedSkills);
|
|
58
59
|
registerAccountMasterTools(server, allowedSkills);
|
|
59
60
|
registerTalTools(server, allowedSkills);
|
|
61
|
+
registerScoringTools(server, allowedSkills);
|
|
60
62
|
const transport = new StdioServerTransport();
|
|
61
63
|
await server.connect(transport);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICP scoring MCP tool wrappers (Phase 3.4 + 3.5).
|
|
3
|
+
*
|
|
4
|
+
* Tools (ops + eng):
|
|
5
|
+
* sync_account_master_icp_models — apply config/account_master/icp_models.yaml → DB
|
|
6
|
+
* score_account — score one account immediately
|
|
7
|
+
* score_distribution — band counts per active model
|
|
8
|
+
* score_explain — per-feature breakdown for one account's score
|
|
9
|
+
*/
|
|
10
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
export declare function registerScoringTools(server: McpServer, allowed: Set<string>): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICP scoring MCP tool wrappers (Phase 3.4 + 3.5).
|
|
3
|
+
*
|
|
4
|
+
* Tools (ops + eng):
|
|
5
|
+
* sync_account_master_icp_models — apply config/account_master/icp_models.yaml → DB
|
|
6
|
+
* score_account — score one account immediately
|
|
7
|
+
* score_distribution — band counts per active model
|
|
8
|
+
* score_explain — per-feature breakdown for one account's score
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { trpcMutation, trpcQuery } from "../api-client.js";
|
|
12
|
+
import { safeTool } from "./_helpers.js";
|
|
13
|
+
const accountIdentifierShape = {
|
|
14
|
+
accountId: z
|
|
15
|
+
.string()
|
|
16
|
+
.uuid()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Account Master account_id (UUID). Use account_lookup if you don't have it."),
|
|
19
|
+
domain: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Account's primary domain (e.g. 'acme.com')."),
|
|
24
|
+
sfAccountId: z
|
|
25
|
+
.string()
|
|
26
|
+
.min(3)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Salesforce Account ID."),
|
|
29
|
+
accountName: z
|
|
30
|
+
.string()
|
|
31
|
+
.min(1)
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Company name (matched after normalization)."),
|
|
34
|
+
};
|
|
35
|
+
export function registerScoringTools(server, allowed) {
|
|
36
|
+
if (allowed.has("sync_account_master_icp_models")) {
|
|
37
|
+
server.tool("sync_account_master_icp_models", [
|
|
38
|
+
"Apply config/account_master/icp_models.yaml to the DB. Reads YAML from GitHub, validates,",
|
|
39
|
+
"upserts icp_model rows by (model_name, model_version), and (by default) marks every",
|
|
40
|
+
"enriched account as pending_scoring so the score worker picks up the new model on its",
|
|
41
|
+
"next run.",
|
|
42
|
+
"",
|
|
43
|
+
"WRITE path for ICP model definitions. To create or edit a model:",
|
|
44
|
+
" 1. update_account_master_config file=icp_models (commits new YAML to main)",
|
|
45
|
+
" 2. sync_account_master_icp_models (apply YAML → DB; this tool)",
|
|
46
|
+
" 3. score_account (optional) (rescore a specific account immediately)",
|
|
47
|
+
"",
|
|
48
|
+
"Models in DB but absent from YAML are LEFT UNTOUCHED — to retire a model, set",
|
|
49
|
+
"is_active: false in YAML. Permanent deletion requires explicit DB action so a typo",
|
|
50
|
+
"doesn't orphan account_score rows.",
|
|
51
|
+
"",
|
|
52
|
+
"Idempotent. Safe to call after every update_account_master_config. Pass dryRun: true to",
|
|
53
|
+
"preview before applying.",
|
|
54
|
+
].join("\n"), {
|
|
55
|
+
branch: z.string().optional().describe("Branch to read YAML from. Default: main."),
|
|
56
|
+
rescoreEnriched: z
|
|
57
|
+
.boolean()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("After applying model changes, mark every enriched account as pending_scoring. Default: true."),
|
|
60
|
+
dryRun: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Read + validate + diff against DB without writing. Default: false."),
|
|
64
|
+
}, safeTool(async (input) => trpcMutation("mcp.syncAccountMasterIcpModels", input)));
|
|
65
|
+
}
|
|
66
|
+
if (allowed.has("score_account")) {
|
|
67
|
+
server.tool("score_account", [
|
|
68
|
+
"Score one account immediately against every active ICP model. Resolves the account by",
|
|
69
|
+
"any of accountId / domain / sfAccountId / accountName.",
|
|
70
|
+
"",
|
|
71
|
+
"Use after a manual override or attribute backfill when you want the golden-record-driven",
|
|
72
|
+
"score to update without waiting for the next scheduled run. Inserts a fresh account_score",
|
|
73
|
+
"row (scoring is append-only) and applies the lifecycle transition (scored or",
|
|
74
|
+
"scoring_ineligible). Use score_explain afterward to see the per-feature breakdown.",
|
|
75
|
+
].join("\n"), {
|
|
76
|
+
...accountIdentifierShape,
|
|
77
|
+
forceRescore: z
|
|
78
|
+
.boolean()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("If true, marks the account pending_scoring even when it's currently in another scoring state. Default: true (the operator's intuition when calling this tool is 'rescore now')."),
|
|
81
|
+
}, safeTool(async (input) => trpcMutation("mcp.scoreAccount", input)));
|
|
82
|
+
}
|
|
83
|
+
if (allowed.has("score_distribution")) {
|
|
84
|
+
server.tool("score_distribution", "Band distribution per active ICP model. For each account, counts the most-recent score against each model. Useful for spotting model-tuning opportunities (e.g. 'all accounts are band C → bump weights') or comparing two model versions side-by-side during a migration.", {
|
|
85
|
+
modelName: z
|
|
86
|
+
.string()
|
|
87
|
+
.min(1)
|
|
88
|
+
.optional()
|
|
89
|
+
.describe("Filter to one model. Default: every active model."),
|
|
90
|
+
}, safeTool(async (input) => trpcQuery("mcp.scoreDistribution", input)));
|
|
91
|
+
}
|
|
92
|
+
if (allowed.has("score_explain")) {
|
|
93
|
+
server.tool("score_explain", "Per-feature points breakdown for one account's most-recent score against a model. Shows feature → category → weight → input value → points → source status, so you can answer 'why did Snowflake get 87?' or 'why is Acme scoring_ineligible?'", {
|
|
94
|
+
...accountIdentifierShape,
|
|
95
|
+
modelName: z
|
|
96
|
+
.string()
|
|
97
|
+
.min(1)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Model to explain. Default: the active model with the most-recent score."),
|
|
100
|
+
}, safeTool(async (input) => trpcQuery("mcp.scoreExplain", input)));
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/tools/tal.js
CHANGED
|
@@ -15,25 +15,6 @@
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import { trpcMutation, trpcQuery } from "../api-client.js";
|
|
17
17
|
import { safeTool } from "./_helpers.js";
|
|
18
|
-
// Local replicas of the criteria DSL types — kept narrow so the MCP
|
|
19
|
-
// tool description doesn't have to ship the full zod schema. The
|
|
20
|
-
// authoritative validator lives in mcp-schemas.ts; this is just for
|
|
21
|
-
// tool-input typing on the client side.
|
|
22
|
-
const conditionSchema = z.lazy(() => z.union([
|
|
23
|
-
z.object({
|
|
24
|
-
operator: z.enum(["AND", "OR", "NOT"]),
|
|
25
|
-
conditions: z.array(conditionSchema),
|
|
26
|
-
}),
|
|
27
|
-
z.object({
|
|
28
|
-
type: z.enum([
|
|
29
|
-
"attribute",
|
|
30
|
-
"score",
|
|
31
|
-
"lifecycle_state",
|
|
32
|
-
"member_of",
|
|
33
|
-
"exclusion",
|
|
34
|
-
]),
|
|
35
|
-
}).passthrough(),
|
|
36
|
-
]));
|
|
37
18
|
const talIdentifierShape = {
|
|
38
19
|
talName: z
|
|
39
20
|
.string()
|
|
@@ -81,63 +62,36 @@ export function registerTalTools(server, allowed) {
|
|
|
81
62
|
.describe("If true, classify changes but skip writes. Useful for previewing the impact of a criteria edit. Default: false."),
|
|
82
63
|
}, safeTool(async (input) => trpcMutation("mcp.evaluateTalNow", input)));
|
|
83
64
|
}
|
|
84
|
-
if (allowed.has("
|
|
85
|
-
server.tool("
|
|
86
|
-
"
|
|
87
|
-
"
|
|
65
|
+
if (allowed.has("sync_account_master_tals")) {
|
|
66
|
+
server.tool("sync_account_master_tals", [
|
|
67
|
+
"Apply config/account_master/tals.yaml to the DB. Reads the YAML from GitHub, validates,",
|
|
68
|
+
"upserts target_account_list rows by tal_name, and (by default) re-evaluates each affected",
|
|
69
|
+
"TAL so membership reflects criteria changes immediately.",
|
|
88
70
|
"",
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"(tal_name — sugar for NOT member_of).",
|
|
71
|
+
"This is the WRITE path for TAL definitions. To create or edit a TAL:",
|
|
72
|
+
" 1. update_account_master_config file=tals (commits new YAML to main)",
|
|
73
|
+
" 2. sync_account_master_tals (apply YAML → DB; this tool)",
|
|
74
|
+
" 3. evaluate_tal_now (optional) (run sooner than the cron's next fire)",
|
|
94
75
|
"",
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
'
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
' {"type": "lifecycle_state", "lifecycle_type": "enrichment", "state": "enriched"},',
|
|
102
|
-
' {"type": "exclusion", "tal_name": "do_not_contact"}',
|
|
103
|
-
" ]",
|
|
104
|
-
"}",
|
|
76
|
+
"TALs in DB but absent from YAML are LEFT UNTOUCHED — to remove a TAL, set is_active: false",
|
|
77
|
+
"in YAML. Permanent deletion requires explicit DB action so an accidental YAML edit",
|
|
78
|
+
"doesn't orphan target_account_list_membership rows.",
|
|
79
|
+
"",
|
|
80
|
+
"Idempotent. Safe to call after every update_account_master_config. Pass dryRun: true to",
|
|
81
|
+
"preview before applying.",
|
|
105
82
|
].join("\n"), {
|
|
106
|
-
|
|
107
|
-
.string()
|
|
108
|
-
.min(1)
|
|
109
|
-
.max(200)
|
|
110
|
-
.regex(/^[a-z0-9_]+$/, "tal_name must be snake_case")
|
|
111
|
-
.describe("Snake-case identifier (e.g. 'enterprise_expansion_targets'). Must be unique."),
|
|
112
|
-
description: z
|
|
113
|
-
.string()
|
|
114
|
-
.min(1)
|
|
115
|
-
.describe("One-sentence description of what this list is for and who consumes it."),
|
|
116
|
-
purpose: z
|
|
117
|
-
.enum([
|
|
118
|
-
"signal_scanner",
|
|
119
|
-
"abm_advertising",
|
|
120
|
-
"outbound_sequence",
|
|
121
|
-
"csm_book",
|
|
122
|
-
"event_invite",
|
|
123
|
-
"exclusion",
|
|
124
|
-
])
|
|
125
|
-
.describe("What the TAL is for. Drives which consumers read it."),
|
|
126
|
-
consumerSystem: z
|
|
83
|
+
branch: z
|
|
127
84
|
.string()
|
|
128
85
|
.optional()
|
|
129
|
-
.describe("
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.enum(["frozen", "weekly", "daily", "hourly"])
|
|
86
|
+
.describe("Branch to read YAML from. Default: main. Use a feature branch if you committed via update_account_master_config to a feature branch and want to test before merging."),
|
|
87
|
+
evaluate: z
|
|
88
|
+
.boolean()
|
|
133
89
|
.optional()
|
|
134
|
-
.describe("
|
|
135
|
-
|
|
136
|
-
.
|
|
137
|
-
.int()
|
|
138
|
-
.positive()
|
|
90
|
+
.describe("After applying TAL changes, re-evaluate every affected TAL (criteria/is_active/max_size diffs) so membership picks up the new definition immediately. Default: true."),
|
|
91
|
+
dryRun: z
|
|
92
|
+
.boolean()
|
|
139
93
|
.optional()
|
|
140
|
-
.describe("
|
|
141
|
-
}, safeTool(async (input) => trpcMutation("mcp.
|
|
94
|
+
.describe("Read + validate + diff against DB without writing. Useful for previewing a TAL edit before applying."),
|
|
95
|
+
}, safeTool(async (input) => trpcMutation("mcp.syncAccountMasterTals", input)));
|
|
142
96
|
}
|
|
143
97
|
}
|