@loopops/mcp-server 3.39.0 → 3.40.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.
@@ -371,6 +371,7 @@ export function registerConfigTools(server, allowed) {
371
371
  if (allowed.has("preview_routing")) {
372
372
  server.tool("preview_routing", "Simulate Route loop resolution for a hypothetical lead. Walks config/route/rules.yaml, dispatches to territory/pool/queue/user/target_account handler, and returns the resolved owner + audit trail. Useful for testing rule changes before merging. Requires at least billing geography or email.", {
373
373
  email: z.string().optional().describe("Lead email — also extracts the domain for target_account lookup."),
374
+ company_name: z.string().optional().describe("Lead.Company. Drives Account Master fuzzy-name matching."),
374
375
  billing_country: z.string().optional().describe("ISO country code (US, GB, JP, etc.)."),
375
376
  billing_state: z.string().optional().describe("ISO state code (CA, NY, etc.)."),
376
377
  billing_city: z.string().optional().describe("City name."),
@@ -379,6 +380,24 @@ export function registerConfigTools(server, allowed) {
379
380
  source: z.string().optional().describe("Lead source (Web, Partner Referral, etc.)."),
380
381
  }, safeTool(async (input) => trpcMutation("mcp.previewRouting", input)));
381
382
  }
383
+ if (allowed.has("preview_contact_routing")) {
384
+ server.tool("preview_contact_routing", "Simulate Route loop resolution for a hypothetical Contact. Resolves the account (existing AccountId → Account Master matcher → SOQL domain fallback), walks the territory cascade for an owner, and applies the open-opportunity attach policy (single_only / most_recent / never). Useful for validating contact-routing setup without writing to SF.", {
385
+ email: z.string().optional().describe("Contact email — feeds AM matcher + SOQL fallback when no AccountId."),
386
+ company_name: z.string().optional().describe("Account name or company hint. Drives AM fuzzy_name when no AccountId."),
387
+ account_id: z.string().optional().describe("Existing SF Account.Id, if the contact is already linked. Bypasses matching."),
388
+ billing_country: z.string().optional().describe("ISO country code from Contact.MailingCountry."),
389
+ billing_state: z.string().optional().describe("ISO state code from Contact.MailingState."),
390
+ billing_city: z.string().optional().describe("City from Contact.MailingCity."),
391
+ hierarchy_mode: z
392
+ .enum(["exact", "ultimate_parent", "best_child_by_country"])
393
+ .optional()
394
+ .describe("Hierarchy walk after AM match. Default: exact."),
395
+ opp_attach_policy: z
396
+ .enum(["single_only", "most_recent", "never"])
397
+ .optional()
398
+ .describe("Open-opp attach policy. Default: most_recent (LeanData default)."),
399
+ }, safeTool(async (input) => trpcMutation("mcp.previewContactRouting", input)));
400
+ }
382
401
  // list_pending_territories + assign_territories retired 2026-05-10.
383
402
  // Account placement now uses the Phase 3 propose_account_assignments
384
403
  // chain (with commit_cycle for directory-shape scenarios).
@@ -455,13 +455,18 @@ export function registerDeployTools(server, allowed) {
455
455
  }
456
456
  if (allowed.has("propose_account_assignments")) {
457
457
  server.tool("propose_account_assignments", [
458
- "Ops/eng. Run the Phase 3 account-placement model for an open",
459
- "planning cycle. Continuity-preservation policy from",
460
- "config/deploy/account_placement.yaml is honored unless overridden.",
461
- "Three refusal gates: (1) Phase 1 (user → territory) must be fully",
462
- "committed/superseded; (2) capacity_config.active_scenario must",
463
- "be set; (3) a planning scope snapshot must exist for the cycle",
464
- "(run snapshot_planning_scope first). Default behavior: accounts",
458
+ "Manager+. Run the Phase 3 account-placement model for an open",
459
+ "planning cycle. Scope-aware: managers re-run for accounts in",
460
+ "their hierarchy subtree (sync; persist scoped to their accounts;",
461
+ "approved + committed rows preserved across the run). Ops/eng",
462
+ "run org-wide (async-by-default; poll via",
463
+ "propose_account_assignments_status). Continuity-preservation",
464
+ "policy from config/deploy/account_placement.yaml is honored",
465
+ "unless overridden. Three refusal gates: (1) Phase 1 (user →",
466
+ "territory) must be fully committed/superseded; (2)",
467
+ "capacity_config.active_scenario must be set; (3) a planning",
468
+ "scope snapshot must exist for the cycle (run",
469
+ "snapshot_planning_scope first). Default behavior: accounts",
465
470
  "with active current owners auto-ratify their current territory",
466
471
  "(continuity); only new or owner-departed accounts go through",
467
472
  "full match-rule + composition + balance modeling. Pass",
@@ -472,7 +477,7 @@ export function registerDeployTools(server, allowed) {
472
477
  preserveCurrentOwnershipOverride: z.boolean().optional().describe("Override config/deploy/account_placement.yaml's preserve_current_ownership policy."),
473
478
  accountIds: z.array(z.string().uuid()).optional().describe("Optional restriction; only model these accounts."),
474
479
  scenarioId: z.string().optional().describe("Override active scenario."),
475
- async: z.boolean().optional().describe("Run the placement model as a Modal background job (bypasses Vercel's 5-min function ceiling). Returns immediately with a job_id; poll status via propose_account_assignments_status. Recommended for cycles with >500 accounts. Default: false."),
480
+ async: z.boolean().optional().describe("Run the placement model as a Modal background job (bypasses Vercel's 5-min function ceiling). Returns immediately with a job_id; poll status via propose_account_assignments_status. Recommended for cycles with >500 accounts. Default: false. NOTE: silently downgraded to sync for manager calls — async scoping isn't plumbed through the Modal worker yet."),
476
481
  branch: z.string().optional(),
477
482
  }, safeTool(async (input) => trpcMutation("mcp.proposeAccountAssignments", input)));
478
483
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "3.39.0",
3
+ "version": "3.40.0",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",