@loopops/mcp-server 3.13.0 → 3.19.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/account-master.js +9 -1
- package/dist/tools/deploy.d.ts +17 -0
- package/dist/tools/deploy.js +470 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { trpcQuery } from "./api-client.js";
|
|
|
9
9
|
import { registerIdentityTools } from "./tools/identity.js";
|
|
10
10
|
import { registerReportingTools } from "./tools/reporting.js";
|
|
11
11
|
import { registerConfigTools } from "./tools/config.js";
|
|
12
|
+
import { registerDeployTools } from "./tools/deploy.js";
|
|
12
13
|
import { registerEngTools } from "./tools/eng.js";
|
|
13
14
|
import { registerCrmTools } from "./tools/crm.js";
|
|
14
15
|
import { registerEngageTools } from "./tools/engage.js";
|
|
@@ -60,6 +61,7 @@ registerCrmTools(server, allowedSkills);
|
|
|
60
61
|
registerEngageTools(server, allowedSkills);
|
|
61
62
|
registerAccountMasterTools(server, allowedSkills);
|
|
62
63
|
registerPeopleMasterTools(server, allowedSkills);
|
|
64
|
+
registerDeployTools(server, allowedSkills);
|
|
63
65
|
registerTalTools(server, allowedSkills);
|
|
64
66
|
registerScoringTools(server, allowedSkills);
|
|
65
67
|
registerSfdcSyncTools(server, allowedSkills);
|
|
@@ -47,13 +47,21 @@ export function registerAccountMasterTools(server, allowed) {
|
|
|
47
47
|
}, safeTool(async (input) => trpcQuery("mcp.accountLookup", input)));
|
|
48
48
|
}
|
|
49
49
|
if (allowed.has("account_show")) {
|
|
50
|
-
server.tool("account_show", "Show full detail for one Account Master account: identifiers, lifecycle states (enrichment / scoring / deployment), linked legal entities, resolved attributes (the golden record with provenance), latest ICP score, TAL memberships, Salesforce mapping, and recent lifecycle events. Use account_lookup first if you only have a domain or name.", {
|
|
50
|
+
server.tool("account_show", "Show full detail for one Account Master account: identifiers, lifecycle states (enrichment / scoring / deployment), linked legal entities, resolved attributes (the golden record with provenance), latest ICP score, TAL memberships, Salesforce mapping, parent + immediate children, and recent lifecycle events. Use account_lookup first if you only have a domain or name. Use walk_account_hierarchy for the full descendants subtree.", {
|
|
51
51
|
accountId: z
|
|
52
52
|
.string()
|
|
53
53
|
.uuid()
|
|
54
54
|
.describe("Account Master account_id (UUID). Use account_lookup if you don't have it yet."),
|
|
55
55
|
}, safeTool(async ({ accountId }) => trpcQuery("mcp.accountShow", { accountId })));
|
|
56
56
|
}
|
|
57
|
+
if (allowed.has("walk_account_hierarchy")) {
|
|
58
|
+
server.tool("walk_account_hierarchy", "Show one account's place in the parent_account_id graph — ancestors (chain up to the ultimate parent) and the full descendants subtree (BFS, depth-tagged). When you have any one of Disney's selling units, this gives you all of them. Read-only. Cycle-safe (capped at depth 32). Use account_show for the full per-account detail; use account_lookup if you don't have an account_id yet.", {
|
|
59
|
+
accountId: z
|
|
60
|
+
.string()
|
|
61
|
+
.uuid()
|
|
62
|
+
.describe("Account Master account_id (UUID) to walk from. The walk shows ancestors above and descendants below this point."),
|
|
63
|
+
}, safeTool(async ({ accountId }) => trpcQuery("mcp.walkAccountHierarchy", { accountId })));
|
|
64
|
+
}
|
|
57
65
|
if (allowed.has("list_pending_account_matches")) {
|
|
58
66
|
server.tool("list_pending_account_matches", "Show inbound candidates currently in `match_status='needs_review'` — the queue of inbound rows the matcher couldn't confidently resolve. Each entry includes the source, ingestion time, and the normalized attributes the matcher saw, so you can run `account_lookup` against them to investigate.", {
|
|
59
67
|
limit: z
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy — placement-pipeline MCP tools (ops+).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1.2 (this file) ships the first user-placement tool:
|
|
5
|
+
* propose_user_assignments — run the Python placement model on Modal,
|
|
6
|
+
* persist proposals against an open
|
|
7
|
+
* planning_cycle. See docs/DEPLOY-PLAN.md.
|
|
8
|
+
*
|
|
9
|
+
* The review/edit/approve/commit triplet (Phase 1.3–1.5) lands here too
|
|
10
|
+
* as it ships. Quota + account placement (Phases 2–3) get separate
|
|
11
|
+
* groups on top.
|
|
12
|
+
*
|
|
13
|
+
* tRPC procedures live in packages/api/src/routers/mcp.ts; input schemas
|
|
14
|
+
* in packages/api/src/routers/mcp-schemas.ts.
|
|
15
|
+
*/
|
|
16
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
export declare function registerDeployTools(server: McpServer, allowed: Set<string>): void;
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy — placement-pipeline MCP tools (ops+).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1.2 (this file) ships the first user-placement tool:
|
|
5
|
+
* propose_user_assignments — run the Python placement model on Modal,
|
|
6
|
+
* persist proposals against an open
|
|
7
|
+
* planning_cycle. See docs/DEPLOY-PLAN.md.
|
|
8
|
+
*
|
|
9
|
+
* The review/edit/approve/commit triplet (Phase 1.3–1.5) lands here too
|
|
10
|
+
* as it ships. Quota + account placement (Phases 2–3) get separate
|
|
11
|
+
* groups on top.
|
|
12
|
+
*
|
|
13
|
+
* tRPC procedures live in packages/api/src/routers/mcp.ts; input schemas
|
|
14
|
+
* in packages/api/src/routers/mcp-schemas.ts.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { trpcMutation, trpcQuery } from "../api-client.js";
|
|
18
|
+
import { safeTool } from "./_helpers.js";
|
|
19
|
+
export function registerDeployTools(server, allowed) {
|
|
20
|
+
if (allowed.has("propose_user_assignments")) {
|
|
21
|
+
server.tool("propose_user_assignments", [
|
|
22
|
+
"Run the user-placement model for an open planning cycle. The",
|
|
23
|
+
"model walks the active scenario's roster + the current",
|
|
24
|
+
"config/deploy/assignments.yaml and emits one proposal row per AE",
|
|
25
|
+
"across three kinds: 'assign' (planned hire onto a new territory),",
|
|
26
|
+
"'ratify' (existing AE staying put), 'depart' (AE in",
|
|
27
|
+
"assignments.yaml but not in the roster — will be removed at",
|
|
28
|
+
"commit). Patches in hierarchy.yaml not covered by any",
|
|
29
|
+
"assign/ratify proposal surface as 'uncovered territories'",
|
|
30
|
+
"warnings. Re-running on the same cycle supersedes the prior set",
|
|
31
|
+
"of proposals — the partial unique index keeps only one active",
|
|
32
|
+
"proposal per (cycle, user). Pass dryRun:true to preview the",
|
|
33
|
+
"model output without writing to the proposal table.",
|
|
34
|
+
].join(" "), {
|
|
35
|
+
planningCycleId: z
|
|
36
|
+
.string()
|
|
37
|
+
.uuid()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("Planning cycle to write proposals against. Defaults to the org's currently-open cycle if exactly one exists; if multiple open or none, the call returns an error listing the cycles found."),
|
|
40
|
+
scenarioId: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Override which scenario to load (e.g. 'stretch'). Default: 'base'. Must exist in config/design/scenarios/."),
|
|
44
|
+
dryRun: z
|
|
45
|
+
.boolean()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Run the model and return proposals without persisting. Useful for previewing what the model would emit before committing to a cycle. Default: false."),
|
|
48
|
+
branch: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Git branch to read configs from. Default: main."),
|
|
52
|
+
}, safeTool(async (input) => trpcMutation("mcp.proposeUserAssignments", input)));
|
|
53
|
+
}
|
|
54
|
+
if (allowed.has("propose_quotas")) {
|
|
55
|
+
server.tool("propose_quotas", [
|
|
56
|
+
"Run the quota model for an open planning cycle. Produces one",
|
|
57
|
+
"annual quota proposal per (territory, primary measure) by",
|
|
58
|
+
"aggregating the same per-AE per-month productivity vector that",
|
|
59
|
+
"drives /capacity at the AE's actual territory_slug (no ancestor",
|
|
60
|
+
"walk). Two refusals before the model runs:",
|
|
61
|
+
"(1) any user_assignment_proposal in the cycle is non-terminal —",
|
|
62
|
+
"Phase 1 must be fully committed/superseded first;",
|
|
63
|
+
"(2) capacity_config.active_scenario isn't set — quotas need a",
|
|
64
|
+
"promoted capacity plan as their source.",
|
|
65
|
+
"Defaults: scenario = capacity_config.active_scenario; measures =",
|
|
66
|
+
"scenario primary_measures with target_settable=true (secondary",
|
|
67
|
+
"measures don't get quotas). Re-running on the same cycle",
|
|
68
|
+
"supersedes the prior set. Pass dryRun:true to preview without",
|
|
69
|
+
"persisting.",
|
|
70
|
+
].join(" "), {
|
|
71
|
+
planningCycleId: z
|
|
72
|
+
.string()
|
|
73
|
+
.uuid()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Planning cycle to write proposals against. Defaults to the org's currently-open cycle if exactly one exists."),
|
|
76
|
+
scenarioId: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Override which scenario to load. Default: capacity_config.active_scenario."),
|
|
80
|
+
measureIds: z
|
|
81
|
+
.array(z.string())
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Override measure filter. Default: scenario primary_measures with target_settable=true."),
|
|
84
|
+
dryRun: z
|
|
85
|
+
.boolean()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Run the model and return proposals without persisting. Default: false."),
|
|
88
|
+
branch: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Git branch to read configs from. Default: main."),
|
|
92
|
+
}, safeTool(async (input) => trpcMutation("mcp.proposeQuotas", input)));
|
|
93
|
+
}
|
|
94
|
+
if (allowed.has("review_quotas")) {
|
|
95
|
+
server.tool("review_quotas", [
|
|
96
|
+
"List territory_quota_proposal rows for a planning cycle, scoped",
|
|
97
|
+
"to your hierarchy subtree. Reps + managers see proposals for the",
|
|
98
|
+
"patches under territories they cover (per",
|
|
99
|
+
"config/deploy/assignments.yaml); leadership / ops / eng see all.",
|
|
100
|
+
"Default state filter is the active queue ([proposed, in_review,",
|
|
101
|
+
"edited, approved]). Output groups proposals by territory + shows",
|
|
102
|
+
"modeled vs final amount per measure, and rolls up totals at the",
|
|
103
|
+
"target_depth (per capacity_config) with gap-vs-target indicators",
|
|
104
|
+
"when targets.yaml has a matching row. Patches in your scope with",
|
|
105
|
+
"no quota proposal surface in their own warning section. Use the",
|
|
106
|
+
"row id shown for `edit_quota` later.",
|
|
107
|
+
].join(" "), {
|
|
108
|
+
planningCycleId: z
|
|
109
|
+
.string()
|
|
110
|
+
.uuid()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Planning cycle to read from. Defaults to the org's currently-open cycle."),
|
|
113
|
+
state: z
|
|
114
|
+
.array(z.enum([
|
|
115
|
+
"proposed",
|
|
116
|
+
"in_review",
|
|
117
|
+
"edited",
|
|
118
|
+
"approved",
|
|
119
|
+
"committed",
|
|
120
|
+
"superseded",
|
|
121
|
+
]))
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("States to include. Default: [proposed, in_review, edited, approved]."),
|
|
124
|
+
territorySlug: z
|
|
125
|
+
.string()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("Filter to a single territory slug. Combines with the caller's hierarchy-subtree scope."),
|
|
128
|
+
measureId: z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Filter to a single measure id (e.g. 'new_acv'). Default: all measures with rows in this cycle."),
|
|
132
|
+
cursor: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("Pagination cursor. Pass the cursor value from a prior response."),
|
|
136
|
+
limit: z
|
|
137
|
+
.number()
|
|
138
|
+
.int()
|
|
139
|
+
.positive()
|
|
140
|
+
.max(500)
|
|
141
|
+
.optional()
|
|
142
|
+
.describe("Max proposals per page (1–500). Default: 100."),
|
|
143
|
+
branch: z
|
|
144
|
+
.string()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe("Git branch to read configs from. Default: main."),
|
|
147
|
+
}, safeTool(async (input) => trpcQuery("mcp.reviewQuotas", input)));
|
|
148
|
+
}
|
|
149
|
+
if (allowed.has("edit_rep_inputs")) {
|
|
150
|
+
server.tool("edit_rep_inputs", [
|
|
151
|
+
"Record a per-cycle override of a position's effective",
|
|
152
|
+
"characteristics (start_date / segment / profile). Upserts a",
|
|
153
|
+
"rep_input_override row keyed by (cycle, position_id). Manager+",
|
|
154
|
+
"scoped — the position must cover a patch in your hierarchy",
|
|
155
|
+
"subtree. The override is recorded but doesn't immediately",
|
|
156
|
+
"rewrite quotas; run propose_quotas after to refresh modeled",
|
|
157
|
+
"amounts (the loader picks up the override automatically).",
|
|
158
|
+
"Pass null to clear an existing override field. Refuses if the",
|
|
159
|
+
"cycle is closed or if a segment/profile id doesn't exist in",
|
|
160
|
+
"config.",
|
|
161
|
+
].join(" "), {
|
|
162
|
+
positionId: z
|
|
163
|
+
.string()
|
|
164
|
+
.min(1)
|
|
165
|
+
.describe("Phase 1.7 position id (`<plan_year>-<8 hex>`). The position whose effective characteristics you want to override."),
|
|
166
|
+
planningCycleId: z
|
|
167
|
+
.string()
|
|
168
|
+
.uuid()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("Planning cycle to scope the override to. Defaults to the org's currently-open cycle."),
|
|
171
|
+
overrideStartDate: z
|
|
172
|
+
.string()
|
|
173
|
+
.nullable()
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("Override the position's effective ramp date (YYYY-MM-DD). Pass null to clear."),
|
|
176
|
+
overrideSegment: z
|
|
177
|
+
.string()
|
|
178
|
+
.nullable()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Override the position's effective segment id. Pass null to clear."),
|
|
181
|
+
overrideProfile: z
|
|
182
|
+
.string()
|
|
183
|
+
.nullable()
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Override the position's effective ae_profile id. Pass null to clear."),
|
|
186
|
+
notes: z
|
|
187
|
+
.string()
|
|
188
|
+
.min(5)
|
|
189
|
+
.describe("Why you're setting this override — required, min 5 chars. Captured in audit log."),
|
|
190
|
+
branch: z
|
|
191
|
+
.string()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Git branch to read configs from. Default: main."),
|
|
194
|
+
}, safeTool(async (input) => trpcMutation("mcp.editRepInputs", input)));
|
|
195
|
+
}
|
|
196
|
+
if (allowed.has("edit_quota")) {
|
|
197
|
+
server.tool("edit_quota", [
|
|
198
|
+
"Override the modeled annual quota for a (territory, measure)",
|
|
199
|
+
"row. Sets final_amount to the new value, transitions state to",
|
|
200
|
+
"'edited', captures notes + reviewer_id. Manager+ scoped — the",
|
|
201
|
+
"territory must be in your hierarchy subtree. Refuses if the",
|
|
202
|
+
"new amount violates any rule in config/deploy/",
|
|
203
|
+
"quota_guardrails.yaml — adjust the amount or ask ops to relax",
|
|
204
|
+
"the guardrail. Refuses if the cycle is closed or if the",
|
|
205
|
+
"proposal is already committed/superseded.",
|
|
206
|
+
].join(" "), {
|
|
207
|
+
proposalId: z
|
|
208
|
+
.string()
|
|
209
|
+
.uuid()
|
|
210
|
+
.describe("territory_quota_proposal id to edit. Get from review_quotas."),
|
|
211
|
+
newAmount: z
|
|
212
|
+
.number()
|
|
213
|
+
.nonnegative()
|
|
214
|
+
.describe("New annual quota in the measure's native unit. Must be >= 0."),
|
|
215
|
+
notes: z
|
|
216
|
+
.string()
|
|
217
|
+
.min(5)
|
|
218
|
+
.describe("Why you're editing — required, min 5 chars. Captured in audit log."),
|
|
219
|
+
branch: z
|
|
220
|
+
.string()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe("Git branch to read configs from. Default: main."),
|
|
223
|
+
}, safeTool(async (input) => trpcMutation("mcp.editQuota", input)));
|
|
224
|
+
}
|
|
225
|
+
if (allowed.has("approve_quotas")) {
|
|
226
|
+
server.tool("approve_quotas", [
|
|
227
|
+
"Bulk-approve quota proposals for an open planning cycle. State",
|
|
228
|
+
"transitions: proposed | in_review | edited → approved.",
|
|
229
|
+
"Manager+ scoped — defaults to all proposals in the cycle in",
|
|
230
|
+
"approvable states within your hierarchy subtree. Pass",
|
|
231
|
+
"proposalIds to approve a subset. final_amount stays as set",
|
|
232
|
+
"(manager-edited) or defaults to modeled_amount on approve.",
|
|
233
|
+
"Per-proposal audit row.",
|
|
234
|
+
].join(" "), {
|
|
235
|
+
planningCycleId: z
|
|
236
|
+
.string()
|
|
237
|
+
.uuid()
|
|
238
|
+
.optional()
|
|
239
|
+
.describe("Planning cycle to approve in. Defaults to the org's currently-open cycle."),
|
|
240
|
+
proposalIds: z
|
|
241
|
+
.array(z.string().uuid())
|
|
242
|
+
.optional()
|
|
243
|
+
.describe("Specific proposal ids. Default: all approvable in your subtree."),
|
|
244
|
+
notes: z
|
|
245
|
+
.string()
|
|
246
|
+
.optional()
|
|
247
|
+
.describe("Free-form approval note. Captured on every approved proposal's audit row."),
|
|
248
|
+
branch: z
|
|
249
|
+
.string()
|
|
250
|
+
.optional()
|
|
251
|
+
.describe("Git branch to read configs from. Default: main."),
|
|
252
|
+
}, safeTool(async (input) => trpcMutation("mcp.approveQuotas", input)));
|
|
253
|
+
}
|
|
254
|
+
if (allowed.has("revise_quota_proposal")) {
|
|
255
|
+
server.tool("revise_quota_proposal", [
|
|
256
|
+
"Demote an approved quota proposal back to edited so the manager",
|
|
257
|
+
"can revise their final_amount. Ops-only. State `approved →",
|
|
258
|
+
"edited`. final_amount is preserved (manager sees what they",
|
|
259
|
+
"previously approved). Audit row records who sent it back and",
|
|
260
|
+
"why. Refuses on non-approved or closed-cycle proposals.",
|
|
261
|
+
].join(" "), {
|
|
262
|
+
proposalId: z
|
|
263
|
+
.string()
|
|
264
|
+
.uuid()
|
|
265
|
+
.describe("Quota proposal id (UUID). Must be in `approved` state."),
|
|
266
|
+
notes: z
|
|
267
|
+
.string()
|
|
268
|
+
.min(5)
|
|
269
|
+
.describe("Why you're sending this back — required, min 5 chars. Captured in audit log."),
|
|
270
|
+
}, safeTool(async (input) => trpcMutation("mcp.reviseQuotaProposal", input)));
|
|
271
|
+
}
|
|
272
|
+
if (allowed.has("commit_quotas")) {
|
|
273
|
+
server.tool("commit_quotas", [
|
|
274
|
+
"Take the manager-approved set and write the cycle's resolved",
|
|
275
|
+
"quotas to config/deploy/quotas.yaml on GitHub, then transition",
|
|
276
|
+
"every approved row to committed. Ops-only. Default is dryRun:",
|
|
277
|
+
"true (returns the YAML diff without writing); pass dryRun:false",
|
|
278
|
+
"to actually commit. The YAML carries a metadata header pinning",
|
|
279
|
+
"it to source_planning_cycle_id + source_scenario_id +",
|
|
280
|
+
"source_capacity_commit_sha + plan-year window. Pursue/Coach",
|
|
281
|
+
"loops will read this file at runtime for per-rep quota tracking.",
|
|
282
|
+
].join(" "), {
|
|
283
|
+
planningCycleId: z
|
|
284
|
+
.string()
|
|
285
|
+
.uuid()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe("Planning cycle to commit. Defaults to the org's currently-open cycle."),
|
|
288
|
+
dryRun: z
|
|
289
|
+
.boolean()
|
|
290
|
+
.optional()
|
|
291
|
+
.describe("When true (default), return the YAML diff without writing. Pass false to commit + advance state."),
|
|
292
|
+
branch: z
|
|
293
|
+
.string()
|
|
294
|
+
.optional()
|
|
295
|
+
.describe("Branch to commit quotas.yaml to. Default: main."),
|
|
296
|
+
}, safeTool(async (input) => trpcMutation("mcp.commitQuotas", input)));
|
|
297
|
+
}
|
|
298
|
+
if (allowed.has("review_user_assignments")) {
|
|
299
|
+
server.tool("review_user_assignments", [
|
|
300
|
+
"List user-placement proposals for a planning cycle, scoped to",
|
|
301
|
+
"your hierarchy subtree. Reps + managers see proposals for the",
|
|
302
|
+
"patches under territories they cover (per",
|
|
303
|
+
"config/deploy/assignments.yaml); leadership / ops / eng see all.",
|
|
304
|
+
"Default state filter is the active review queue",
|
|
305
|
+
"([proposed, in_review, edited]). Ratify proposals (existing AEs",
|
|
306
|
+
"with no change) appear by default — pass `proposalKind:[assign]`",
|
|
307
|
+
"to skip them. Depart proposals (AEs being removed) surface in",
|
|
308
|
+
"their own warning section. Use the `id` shown for each proposal",
|
|
309
|
+
"as `proposalId` for `edit_user_assignment`.",
|
|
310
|
+
].join(" "), {
|
|
311
|
+
planningCycleId: z
|
|
312
|
+
.string()
|
|
313
|
+
.uuid()
|
|
314
|
+
.optional()
|
|
315
|
+
.describe("Planning cycle to read from. Defaults to the org's currently-open cycle."),
|
|
316
|
+
state: z
|
|
317
|
+
.array(z.enum([
|
|
318
|
+
"proposed",
|
|
319
|
+
"in_review",
|
|
320
|
+
"edited",
|
|
321
|
+
"approved",
|
|
322
|
+
"committed",
|
|
323
|
+
"superseded",
|
|
324
|
+
]))
|
|
325
|
+
.optional()
|
|
326
|
+
.describe("Filter to these proposal states. Default: ['proposed','in_review','edited']."),
|
|
327
|
+
proposalKind: z
|
|
328
|
+
.array(z.enum(["assign", "ratify", "depart"]))
|
|
329
|
+
.optional()
|
|
330
|
+
.describe("Filter by proposal kind. Default: all (assign + ratify + depart). Use ['depart'] to focus on departures, ['assign'] to skip ratifies."),
|
|
331
|
+
territorySlug: z
|
|
332
|
+
.string()
|
|
333
|
+
.optional()
|
|
334
|
+
.describe("Narrow to one territory slug (e.g. 'ams/east/northeast/ny-metro')."),
|
|
335
|
+
cursor: z
|
|
336
|
+
.string()
|
|
337
|
+
.optional()
|
|
338
|
+
.describe("Pagination cursor from a prior response's `next_cursor`. Omit on first page."),
|
|
339
|
+
limit: z
|
|
340
|
+
.number()
|
|
341
|
+
.int()
|
|
342
|
+
.positive()
|
|
343
|
+
.max(500)
|
|
344
|
+
.optional()
|
|
345
|
+
.describe("Max proposals per page (1–500). Default: 100."),
|
|
346
|
+
}, safeTool(async (input) => trpcQuery("mcp.reviewUserAssignments", input)));
|
|
347
|
+
}
|
|
348
|
+
if (allowed.has("approve_user_assignments")) {
|
|
349
|
+
server.tool("approve_user_assignments", [
|
|
350
|
+
"Bulk-approve user-placement proposals you have authority over.",
|
|
351
|
+
"State `proposed | edited → approved`. Sets `final_*` to",
|
|
352
|
+
"`proposed_*` if you didn't override via `edit_user_assignment`.",
|
|
353
|
+
"Per-proposal audit row written. Approval locks the proposal out",
|
|
354
|
+
"of the active review queue; ops will sweep them up via",
|
|
355
|
+
"`commit_user_assignments` to write back to assignments.yaml.",
|
|
356
|
+
"Pass `proposalIds` to approve a subset; default is all",
|
|
357
|
+
"in-scope proposals in proposed/edited state for the cycle.",
|
|
358
|
+
].join(" "), {
|
|
359
|
+
planningCycleId: z
|
|
360
|
+
.string()
|
|
361
|
+
.uuid()
|
|
362
|
+
.optional()
|
|
363
|
+
.describe("Planning cycle. Defaults to the org's currently-open cycle."),
|
|
364
|
+
proposalIds: z
|
|
365
|
+
.array(z.string().uuid())
|
|
366
|
+
.optional()
|
|
367
|
+
.describe("Specific proposal ids to approve. Default: all proposals in your scope in proposed/edited state."),
|
|
368
|
+
notes: z
|
|
369
|
+
.string()
|
|
370
|
+
.optional()
|
|
371
|
+
.describe("Optional approval note (captured in the audit row for every approved proposal)."),
|
|
372
|
+
}, safeTool(async (input) => trpcMutation("mcp.approveUserAssignments", input)));
|
|
373
|
+
}
|
|
374
|
+
if (allowed.has("revise_user_assignment")) {
|
|
375
|
+
server.tool("revise_user_assignment", [
|
|
376
|
+
"Ops only. Send an approved proposal back to the manager for",
|
|
377
|
+
"revisions. State `approved → edited`. Preserves `final_*`",
|
|
378
|
+
"values so the manager can see exactly what they previously",
|
|
379
|
+
"approved before deciding what to revise. Required `notes`",
|
|
380
|
+
"explaining why. Per-proposal audit row written. Cannot be",
|
|
381
|
+
"used on `committed` proposals (those are already in YAML —",
|
|
382
|
+
"run a fresh propose_user_assignments cycle to change them).",
|
|
383
|
+
].join(" "), {
|
|
384
|
+
proposalId: z
|
|
385
|
+
.string()
|
|
386
|
+
.uuid()
|
|
387
|
+
.describe("Proposal id (UUID) to revise. Get from review_user_assignments with `state:[approved]`."),
|
|
388
|
+
notes: z
|
|
389
|
+
.string()
|
|
390
|
+
.min(5)
|
|
391
|
+
.describe("Why you're sending this back — required, captured in the audit log so the manager sees the intent."),
|
|
392
|
+
}, safeTool(async (input) => trpcMutation("mcp.reviseUserAssignment", input)));
|
|
393
|
+
}
|
|
394
|
+
if (allowed.has("close_cycle")) {
|
|
395
|
+
server.tool("close_cycle", [
|
|
396
|
+
"Ops only. Mark a planning cycle done. State `open → closed`.",
|
|
397
|
+
"Every non-`committed` proposal in the cycle (proposed /",
|
|
398
|
+
"in_review / edited / approved) moves to `superseded` —",
|
|
399
|
+
"abandoned at close. Already-`committed` proposals are",
|
|
400
|
+
"untouched (they're already in YAML). After close, the cycle",
|
|
401
|
+
"is read-only history; new propose / edit / approve / revise /",
|
|
402
|
+
"commit calls against this cycle id will be refused. Required",
|
|
403
|
+
"`notes` captured in the cycle-level audit row.",
|
|
404
|
+
].join(" "), {
|
|
405
|
+
planningCycleId: z
|
|
406
|
+
.string()
|
|
407
|
+
.uuid()
|
|
408
|
+
.describe("Planning cycle to close. Must currently be `open`. Closing is irreversible."),
|
|
409
|
+
notes: z
|
|
410
|
+
.string()
|
|
411
|
+
.min(5)
|
|
412
|
+
.describe("Why you're closing the cycle — required, captured in the cycle-level audit row."),
|
|
413
|
+
}, safeTool(async (input) => trpcMutation("mcp.closeCycle", input)));
|
|
414
|
+
}
|
|
415
|
+
if (allowed.has("commit_user_assignments")) {
|
|
416
|
+
server.tool("commit_user_assignments", [
|
|
417
|
+
"Ops only. Write the cycle's approved placements back to",
|
|
418
|
+
"`config/deploy/assignments.yaml` and transition every approved",
|
|
419
|
+
"proposal to `committed`. Default is dry-run — returns the YAML",
|
|
420
|
+
"diff + per-proposal disposition without writing or transitioning.",
|
|
421
|
+
"Pass `dryRun:false` to commit. Mutation rules: `assign` adds (or",
|
|
422
|
+
"mutates) a user entry, `ratify` is a no-op, `depart` removes the",
|
|
423
|
+
"user. After commit succeeds, run `sync_territories` to push the",
|
|
424
|
+
"new YAML state to Salesforce.",
|
|
425
|
+
].join(" "), {
|
|
426
|
+
planningCycleId: z
|
|
427
|
+
.string()
|
|
428
|
+
.uuid()
|
|
429
|
+
.optional()
|
|
430
|
+
.describe("Planning cycle to commit. Defaults to the org's currently-open cycle."),
|
|
431
|
+
dryRun: z
|
|
432
|
+
.boolean()
|
|
433
|
+
.optional()
|
|
434
|
+
.describe("Default true — return diff without writing. Pass `false` to commit."),
|
|
435
|
+
branch: z
|
|
436
|
+
.string()
|
|
437
|
+
.optional()
|
|
438
|
+
.describe("Branch to write assignments.yaml to. Default: main."),
|
|
439
|
+
}, safeTool(async (input) => trpcMutation("mcp.commitUserAssignments", input)));
|
|
440
|
+
}
|
|
441
|
+
if (allowed.has("edit_user_assignment")) {
|
|
442
|
+
server.tool("edit_user_assignment", [
|
|
443
|
+
"Override the agent's pick on a specific user-placement proposal.",
|
|
444
|
+
"Sets `final_*` fields, transitions state to 'edited', captures",
|
|
445
|
+
"your `notes` (required, min 5 chars), and writes an audit_log",
|
|
446
|
+
"row. Validates that the new territory slug is a real patch in",
|
|
447
|
+
"hierarchy.yaml and that you have visibility into both the source",
|
|
448
|
+
"and destination patches. Depart proposals can't be edited via",
|
|
449
|
+
"this tool — to cancel a departure, add the rep back to the active",
|
|
450
|
+
"scenario's roster and re-run propose_user_assignments.",
|
|
451
|
+
].join(" "), {
|
|
452
|
+
proposalId: z
|
|
453
|
+
.string()
|
|
454
|
+
.uuid()
|
|
455
|
+
.describe("Proposal id (UUID) to edit. Get from review_user_assignments."),
|
|
456
|
+
newTerritorySlug: z
|
|
457
|
+
.string()
|
|
458
|
+
.optional()
|
|
459
|
+
.describe("New patch slug. Must be in hierarchy.yaml at the patch level. Omit to keep the proposed slug."),
|
|
460
|
+
newRole: z
|
|
461
|
+
.string()
|
|
462
|
+
.optional()
|
|
463
|
+
.describe("New role id (e.g. 'ae'). Must be in roles.yaml. Omit to keep the proposed role."),
|
|
464
|
+
notes: z
|
|
465
|
+
.string()
|
|
466
|
+
.min(5)
|
|
467
|
+
.describe("Why you're editing — required, captured in the audit log."),
|
|
468
|
+
}, safeTool(async (input) => trpcMutation("mcp.editUserAssignment", input)));
|
|
469
|
+
}
|
|
470
|
+
}
|