@rubytech/create-maxy 1.0.498 → 1.0.500

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.
Files changed (118) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/mcp/dist/index.js +34 -4
  3. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  4. package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +3 -3
  5. package/payload/platform/scripts/logs-read.sh +40 -5
  6. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  7. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/PLUGIN.md +36 -8
  8. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js +229 -153
  9. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js.map +1 -1
  10. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.d.ts +19 -1
  11. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
  12. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.js +99 -3
  13. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.js.map +1 -1
  14. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts +10 -0
  15. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts.map +1 -0
  16. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js +24 -0
  17. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js.map +1 -0
  18. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts +16 -0
  19. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts.map +1 -0
  20. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js +35 -0
  21. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js.map +1 -0
  22. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/key-register.js +1 -1
  23. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/key-register.js.map +1 -1
  24. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts +13 -0
  25. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -0
  26. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js +41 -0
  27. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js.map +1 -0
  28. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts +9 -0
  29. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts.map +1 -0
  30. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js +16 -0
  31. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js.map +1 -0
  32. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts +15 -0
  33. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts.map +1 -0
  34. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js +11 -0
  35. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js.map +1 -0
  36. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts +10 -0
  37. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts.map +1 -0
  38. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js +39 -0
  39. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js.map +1 -0
  40. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts +9 -0
  41. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts.map +1 -0
  42. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js +33 -0
  43. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js.map +1 -0
  44. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts +18 -0
  45. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts.map +1 -0
  46. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js +59 -0
  47. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js.map +1 -0
  48. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts +10 -0
  49. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts.map +1 -0
  50. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js +39 -0
  51. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js.map +1 -0
  52. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts +12 -0
  53. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts.map +1 -0
  54. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js +28 -0
  55. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js.map +1 -0
  56. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts +15 -0
  57. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts.map +1 -0
  58. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js +11 -0
  59. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js.map +1 -0
  60. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts +16 -0
  61. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts.map +1 -0
  62. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js +39 -0
  63. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js.map +1 -0
  64. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts +13 -0
  65. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts.map +1 -0
  66. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js +49 -0
  67. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js.map +1 -0
  68. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts +7 -0
  69. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts.map +1 -0
  70. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js +15 -0
  71. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js.map +1 -0
  72. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts +14 -0
  73. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts.map +1 -0
  74. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js +11 -0
  75. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js.map +1 -0
  76. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts +9 -0
  77. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts.map +1 -0
  78. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js +40 -0
  79. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js.map +1 -0
  80. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts +13 -0
  81. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts.map +1 -0
  82. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js +34 -0
  83. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js.map +1 -0
  84. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts +14 -0
  85. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts.map +1 -0
  86. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js +18 -0
  87. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js.map +1 -0
  88. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/index.ts +335 -158
  89. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/lib/loop-api.ts +140 -3
  90. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/customer-preferences.ts +60 -0
  91. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback.ts +80 -0
  92. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/key-register.ts +1 -1
  93. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-enquiry.ts +105 -0
  94. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-batch.ts +48 -0
  95. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-request.ts +37 -0
  96. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match.ts +78 -0
  97. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-detail.ts +63 -0
  98. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-search.ts +93 -0
  99. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-detail.ts +70 -0
  100. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-listed.ts +67 -0
  101. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-request.ts +37 -0
  102. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-search.ts +80 -0
  103. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/supplier.ts +120 -0
  104. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/team-availability.ts +42 -0
  105. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-create.ts +36 -0
  106. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-detail.ts +70 -0
  107. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-search.ts +74 -0
  108. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-update.ts +48 -0
  109. package/payload/server/public/assets/{ChatInput-BbMvZgRR.js → ChatInput-Bo9T8v0E.js} +1 -1
  110. package/payload/server/public/assets/{admin-BSHc9LPS.js → admin-CkpknqK7.js} +60 -60
  111. package/payload/server/public/assets/{public-DvDzSq3r.js → public-wDhMuZDR.js} +1 -1
  112. package/payload/server/public/index.html +2 -2
  113. package/payload/server/public/public.html +2 -2
  114. package/payload/server/server.js +89 -2
  115. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback-list.ts +0 -54
  116. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-list.ts +0 -52
  117. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/properties-list.ts +0 -52
  118. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewings-list.ts +0 -62
@@ -4,16 +4,30 @@ import { z } from "zod";
4
4
  import { keyRegister } from "./tools/key-register.js";
5
5
  import { keyDeregister } from "./tools/key-deregister.js";
6
6
  import { keyList } from "./tools/key-list.js";
7
- import { propertiesList } from "./tools/properties-list.js";
8
- import { peopleList } from "./tools/people-list.js";
9
- import { viewingsList } from "./tools/viewings-list.js";
10
- import { feedbackList } from "./tools/feedback-list.js";
7
+ import { propertySearch } from "./tools/property-search.js";
8
+ import { propertyDetail } from "./tools/property-detail.js";
9
+ import { propertyListed } from "./tools/property-listed.js";
10
+ import { propertyRequest } from "./tools/property-request.js";
11
+ import { peopleSearch } from "./tools/people-search.js";
12
+ import { peopleDetail } from "./tools/people-detail.js";
13
+ import { viewingSearch } from "./tools/viewing-search.js";
14
+ import { viewingDetail } from "./tools/viewing-detail.js";
15
+ import { viewingCreate } from "./tools/viewing-create.js";
16
+ import { viewingUpdate } from "./tools/viewing-update.js";
17
+ import { feedbackGet, feedbackSubmit } from "./tools/feedback.js";
11
18
  import { teamInfo } from "./tools/team-info.js";
19
+ import { teamAvailability } from "./tools/team-availability.js";
20
+ import { marketingMatchDetail } from "./tools/marketing-match.js";
21
+ import { marketingMatchBatch } from "./tools/marketing-match-batch.js";
22
+ import { marketingMatchRequest } from "./tools/marketing-match-request.js";
23
+ import { marketingEnquiry } from "./tools/marketing-enquiry.js";
24
+ import { customerPreferences } from "./tools/customer-preferences.js";
25
+ import { supplier } from "./tools/supplier.js";
12
26
  import { closeDriver } from "./lib/neo4j.js";
13
27
 
14
28
  const server = new McpServer({
15
29
  name: "maxy-real-agency-loop",
16
- version: "0.1.0",
30
+ version: "0.2.0",
17
31
  });
18
32
 
19
33
  const accountId = process.env.ACCOUNT_ID;
@@ -21,10 +35,33 @@ if (!accountId) {
21
35
  throw new Error("ACCOUNT_ID environment variable is required");
22
36
  }
23
37
 
38
+ const ALL_PERMISSIONS = z.enum([
39
+ "properties", "people", "viewings", "feedback", "team",
40
+ "marketing", "customer", "supplier",
41
+ ]);
42
+
24
43
  console.error(`[loop] server started, account=${accountId}`);
25
44
 
45
+ // Helper: wrap a tool function in standard error handling
46
+ function toolHandler(fn: (p: Record<string, unknown>) => Promise<string>) {
47
+ return async (params: Record<string, unknown>) => {
48
+ try {
49
+ const text = await fn({ ...params, accountId });
50
+ return { content: [{ type: "text" as const, text }] };
51
+ } catch (err) {
52
+ return {
53
+ content: [{
54
+ type: "text" as const,
55
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
56
+ }],
57
+ isError: true,
58
+ };
59
+ }
60
+ };
61
+ }
62
+
26
63
  // ─────────────────────────────────────────────────────────────────
27
- // Key Management Tools
64
+ // Key Management Tools (3)
28
65
  // ─────────────────────────────────────────────────────────────────
29
66
 
30
67
  server.tool(
@@ -34,28 +71,22 @@ server.tool(
34
71
  teamName: z.string().min(1).describe("Human-readable name for this team (e.g. 'Muvin Main Office')"),
35
72
  apiKey: z.string().min(10).describe("Loop API key (X-Api-Key header value)"),
36
73
  permissions: z
37
- .array(z.enum(["properties", "people", "viewings", "feedback", "team"]))
74
+ .array(ALL_PERMISSIONS)
38
75
  .optional()
39
- .describe("Endpoint groups this key can access (default: all)"),
76
+ .describe("Endpoint groups this key can access (default: all 8 groups)"),
40
77
  },
41
78
  async (params) => {
42
79
  try {
43
80
  const result = await keyRegister({ ...params, accountId });
44
-
45
81
  let text = `Team "${params.teamName}" registered successfully.`;
46
- if (result.warning) {
47
- text += `\n\nWarning: ${result.warning}`;
48
- }
49
-
82
+ if (result.warning) text += `\n\nWarning: ${result.warning}`;
50
83
  return { content: [{ type: "text" as const, text }] };
51
84
  } catch (err) {
52
85
  return {
53
- content: [
54
- {
55
- type: "text" as const,
56
- text: `Failed to register key: ${err instanceof Error ? err.message : String(err)}`,
57
- },
58
- ],
86
+ content: [{
87
+ type: "text" as const,
88
+ text: `Failed to register key: ${err instanceof Error ? err.message : String(err)}`,
89
+ }],
59
90
  isError: true,
60
91
  };
61
92
  }
@@ -65,25 +96,17 @@ server.tool(
65
96
  server.tool(
66
97
  "loop-key-deregister",
67
98
  "Remove a registered Loop CRM team key. The key is permanently deleted from the graph.",
68
- {
69
- teamName: z.string().min(1).describe("Name of the team to remove"),
70
- },
99
+ { teamName: z.string().min(1).describe("Name of the team to remove") },
71
100
  async ({ teamName }) => {
72
101
  try {
73
102
  await keyDeregister({ teamName, accountId });
74
- return {
75
- content: [
76
- { type: "text" as const, text: `Team "${teamName}" deregistered.` },
77
- ],
78
- };
103
+ return { content: [{ type: "text" as const, text: `Team "${teamName}" deregistered.` }] };
79
104
  } catch (err) {
80
105
  return {
81
- content: [
82
- {
83
- type: "text" as const,
84
- text: `Failed to deregister: ${err instanceof Error ? err.message : String(err)}`,
85
- },
86
- ],
106
+ content: [{
107
+ type: "text" as const,
108
+ text: `Failed to deregister: ${err instanceof Error ? err.message : String(err)}`,
109
+ }],
87
110
  isError: true,
88
111
  };
89
112
  }
@@ -94,161 +117,315 @@ server.tool(
94
117
  "loop-key-list",
95
118
  "List all registered Loop CRM teams for this account. Shows team names, addresses, and permissions. Never reveals API key values.",
96
119
  {},
97
- async () => {
98
- try {
99
- const text = await keyList({ accountId });
100
- return { content: [{ type: "text" as const, text }] };
101
- } catch (err) {
102
- return {
103
- content: [
104
- {
105
- type: "text" as const,
106
- text: `Failed to list keys: ${err instanceof Error ? err.message : String(err)}`,
107
- },
108
- ],
109
- isError: true,
110
- };
111
- }
112
- }
120
+ toolHandler(async (p) => keyList({ accountId: p.accountId as string }))
113
121
  );
114
122
 
115
123
  // ─────────────────────────────────────────────────────────────────
116
- // Data Query Tools (with cross-team aggregation)
124
+ // People Tools (2) permission: people
117
125
  // ─────────────────────────────────────────────────────────────────
118
126
 
119
127
  server.tool(
120
- "loop-properties-list",
121
- "List properties from Loop CRM. Aggregates across all registered teams unless a specific team is named. Supports filtering by sale or letting type.",
128
+ "loop-people-search",
129
+ "Search people in Loop CRM. Without a role, searches all contacts. With a role (buyers/sellers/renters/landlords), returns role-specific results with rich filters.",
122
130
  {
123
- type: z
124
- .enum(["sale", "letting", "both"])
125
- .optional()
126
- .describe("Filter by property type (default: both)"),
127
- teamName: z.string().optional().describe("Query a specific team only (omit for all teams)"),
128
- limit: z.number().int().optional().describe("Max results (default 200 across all teams)"),
131
+ role: z.enum(["buyers", "sellers", "renters", "landlords"]).optional().describe("Role to search (omit for all people)"),
132
+ searchTerm: z.string().optional().describe("Name or contact search"),
133
+ maxPrice: z.number().optional().describe("Max price (buyers) or max price filter (sellers)"),
134
+ minPrice: z.number().optional().describe("Min price (sellers)"),
135
+ minBeds: z.number().optional().describe("Min bedrooms (buyers/renters)"),
136
+ maxRent: z.number().optional().describe("Max rent (renters only)"),
137
+ searchAreas: z.string().optional().describe("Comma-separated outcodes e.g. 'SN7,OX12' (buyers/renters)"),
138
+ propertyTypes: z.string().optional().describe("Comma-separated property types (buyers/renters)"),
139
+ startDate: z.string().optional().describe("Start date filter (sellers/landlords)"),
140
+ endDate: z.string().optional().describe("End date filter (sellers/landlords)"),
141
+ teamName: z.string().optional().describe("Query a specific team only"),
142
+ limit: z.number().int().optional().describe("Max results (default 200)"),
129
143
  },
130
- async (params) => {
131
- try {
132
- const text = await propertiesList({ ...params, accountId });
133
- return { content: [{ type: "text" as const, text }] };
134
- } catch (err) {
135
- return {
136
- content: [
137
- {
138
- type: "text" as const,
139
- text: `Properties query failed: ${err instanceof Error ? err.message : String(err)}`,
140
- },
141
- ],
142
- isError: true,
143
- };
144
- }
145
- }
144
+ toolHandler(async (p) => peopleSearch(p as Parameters<typeof peopleSearch>[0]))
146
145
  );
147
146
 
148
147
  server.tool(
149
- "loop-people-list",
150
- "List people (contacts) from Loop CRM. Aggregates across all registered teams unless a specific team is named.",
148
+ "loop-people-detail",
149
+ "Get full details for a specific person by ID. Optionally specify a role (buyers/sellers/renters/landlords) for role-specific detail.",
151
150
  {
152
- search: z.string().optional().describe("Search by name or contact details"),
153
- teamName: z.string().optional().describe("Query a specific team only (omit for all teams)"),
154
- limit: z.number().int().optional().describe("Max results (default 200 across all teams)"),
151
+ personId: z.number().int().describe("Person ID"),
152
+ role: z.enum(["buyers", "sellers", "renters", "landlords"]).optional().describe("Role for role-specific detail view"),
153
+ teamName: z.string().optional().describe("Query a specific team only"),
155
154
  },
156
- async (params) => {
157
- try {
158
- const text = await peopleList({ ...params, accountId });
159
- return { content: [{ type: "text" as const, text }] };
160
- } catch (err) {
161
- return {
162
- content: [
163
- {
164
- type: "text" as const,
165
- text: `People query failed: ${err instanceof Error ? err.message : String(err)}`,
166
- },
167
- ],
168
- isError: true,
169
- };
170
- }
171
- }
155
+ toolHandler(async (p) => peopleDetail(p as Parameters<typeof peopleDetail>[0]))
172
156
  );
173
157
 
158
+ // ─────────────────────────────────────────────────────────────────
159
+ // Property Tools (4) — permission: properties
160
+ // ─────────────────────────────────────────────────────────────────
161
+
174
162
  server.tool(
175
- "loop-viewings-list",
176
- "List viewings from Loop CRM. Aggregates across all registered teams unless a specific team is named. Supports sale/letting type and date range filters.",
163
+ "loop-property-search",
164
+ "Search properties in Loop CRM. Queries /property/residential/sales and /property/residential/lettings. Use department to narrow to one type.",
177
165
  {
178
- type: z
179
- .enum(["sale", "letting", "both"])
180
- .optional()
181
- .describe("Filter by viewing type (default: both)"),
182
- teamName: z.string().optional().describe("Query a specific team only (omit for all teams)"),
183
- dateFrom: z.string().optional().describe("Start date (ISO format, e.g. 2026-04-01)"),
184
- dateTo: z.string().optional().describe("End date (ISO format, e.g. 2026-04-30)"),
185
- limit: z.number().int().optional().describe("Max results (default 200 across all teams)"),
166
+ department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
167
+ searchTerm: z.string().optional().describe("Address or keyword search"),
168
+ minPrice: z.number().optional().describe("Minimum price"),
169
+ maxPrice: z.number().optional().describe("Maximum price"),
170
+ minBedrooms: z.number().int().optional().describe("Minimum bedrooms"),
171
+ maxBedrooms: z.number().int().optional().describe("Maximum bedrooms"),
172
+ propertyStatuses: z.string().optional().describe("Comma-separated statuses (e.g. 'forSale,underOffer')"),
173
+ propertyTypes: z.string().optional().describe("Comma-separated property types"),
174
+ teamName: z.string().optional().describe("Query a specific team only"),
175
+ limit: z.number().int().optional().describe("Max results (default 200)"),
186
176
  },
187
- async (params) => {
188
- try {
189
- const text = await viewingsList({ ...params, accountId });
190
- return { content: [{ type: "text" as const, text }] };
191
- } catch (err) {
192
- return {
193
- content: [
194
- {
195
- type: "text" as const,
196
- text: `Viewings query failed: ${err instanceof Error ? err.message : String(err)}`,
197
- },
198
- ],
199
- isError: true,
200
- };
201
- }
202
- }
177
+ toolHandler(async (p) => propertySearch(p as Parameters<typeof propertySearch>[0]))
203
178
  );
204
179
 
205
180
  server.tool(
206
- "loop-feedback-list",
207
- "List viewing feedback from Loop CRM. Aggregates across all registered teams unless a specific team is named.",
181
+ "loop-property-detail",
182
+ "Get full details for a specific property by ID and department. Optionally include a preview hash for the public preview.",
208
183
  {
209
- teamName: z.string().optional().describe("Query a specific team only (omit for all teams)"),
210
- limit: z.number().int().optional().describe("Max results (default 200 across all teams)"),
184
+ propertyId: z.number().int().describe("Property ID"),
185
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
186
+ previewHash: z.number().int().optional().describe("Preview hash for public preview URL"),
187
+ teamName: z.string().optional().describe("Query a specific team only"),
211
188
  },
212
- async (params) => {
213
- try {
214
- const text = await feedbackList({ ...params, accountId });
215
- return { content: [{ type: "text" as const, text }] };
216
- } catch (err) {
217
- return {
218
- content: [
219
- {
220
- type: "text" as const,
221
- text: `Feedback query failed: ${err instanceof Error ? err.message : String(err)}`,
222
- },
223
- ],
224
- isError: true,
225
- };
226
- }
227
- }
189
+ toolHandler(async (p) => propertyDetail(p as Parameters<typeof propertyDetail>[0]))
228
190
  );
229
191
 
192
+ server.tool(
193
+ "loop-property-listed",
194
+ "Get properties listed on a specific channel (Rightmove, Zoopla, OnTheMarket, website). Optionally include sold properties.",
195
+ {
196
+ channel: z.enum(["rightmove", "onTheMarket", "zoopla", "website"]).describe("Listing channel"),
197
+ department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
198
+ includeSold: z.boolean().optional().describe("Include sold gallery (default: false)"),
199
+ teamName: z.string().optional().describe("Query a specific team only"),
200
+ limit: z.number().int().optional().describe("Max results (default 200)"),
201
+ },
202
+ toolHandler(async (p) => propertyListed(p as Parameters<typeof propertyListed>[0]))
203
+ );
204
+
205
+ server.tool(
206
+ "loop-property-request",
207
+ "Submit a viewing, callback, or information request for a property. Requires a specific team.",
208
+ {
209
+ teamName: z.string().min(1).describe("Team to submit the request through"),
210
+ propertyId: z.number().int().describe("Property ID"),
211
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
212
+ action: z.enum(["viewing", "call-back", "information"]).describe("Request type"),
213
+ name: z.string().optional().describe("Requester name"),
214
+ email: z.string().optional().describe("Requester email"),
215
+ phone: z.string().optional().describe("Requester phone"),
216
+ message: z.string().optional().describe("Additional message"),
217
+ },
218
+ toolHandler(async (p) => propertyRequest(p as Parameters<typeof propertyRequest>[0]))
219
+ );
220
+
221
+ // ─────────────────────────────────────────────────────────────────
222
+ // Viewing Tools (4) — permission: viewings
223
+ // ─────────────────────────────────────────────────────────────────
224
+
225
+ server.tool(
226
+ "loop-viewing-search",
227
+ "Search viewings in Loop CRM. Queries /residential/sales/viewings and /residential/lettings/viewings.",
228
+ {
229
+ department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
230
+ searchTerm: z.string().optional().describe("Search viewings"),
231
+ appointmentStartDate: z.string().optional().describe("Filter from date (ISO format)"),
232
+ appointmentEndDate: z.string().optional().describe("Filter to date (ISO format)"),
233
+ status: z.string().optional().describe("Viewing status filter"),
234
+ teamName: z.string().optional().describe("Query a specific team only"),
235
+ limit: z.number().int().optional().describe("Max results (default 200)"),
236
+ },
237
+ toolHandler(async (p) => viewingSearch(p as Parameters<typeof viewingSearch>[0]))
238
+ );
239
+
240
+ server.tool(
241
+ "loop-viewing-detail",
242
+ "Get full details for a specific viewing by ID and department.",
243
+ {
244
+ viewingId: z.number().int().describe("Viewing ID"),
245
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
246
+ teamName: z.string().optional().describe("Query a specific team only"),
247
+ },
248
+ toolHandler(async (p) => viewingDetail(p as Parameters<typeof viewingDetail>[0]))
249
+ );
250
+
251
+ server.tool(
252
+ "loop-viewing-create",
253
+ "Create a new viewing in Loop CRM. Creates the viewing and associated buyer/renter record.",
254
+ {
255
+ teamName: z.string().min(1).describe("Team to create the viewing for"),
256
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
257
+ propertyId: z.number().int().describe("Property ID"),
258
+ date: z.string().describe("Viewing date (YYYY-MM-DD)"),
259
+ time: z.string().describe("Viewing time (HH:mm)"),
260
+ attendeeName: z.string().describe("Attendee full name"),
261
+ attendeeEmail: z.string().optional().describe("Attendee email"),
262
+ attendeePhone: z.string().optional().describe("Attendee phone"),
263
+ },
264
+ toolHandler(async (p) => viewingCreate(p as Parameters<typeof viewingCreate>[0]))
265
+ );
266
+
267
+ server.tool(
268
+ "loop-viewing-update",
269
+ "Add a note or record feedback for a viewing. For feedback, specify the party: buyer/seller (sales) or renter/landlord (lettings).",
270
+ {
271
+ teamName: z.string().min(1).describe("Team that owns the viewing"),
272
+ viewingId: z.number().int().describe("Viewing ID"),
273
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
274
+ action: z.enum(["note", "feedback"]).describe("note or feedback"),
275
+ text: z.string().describe("Note text or feedback content"),
276
+ feedbackParty: z.enum(["buyer", "seller", "renter", "landlord"]).optional().describe("Required for feedback: whose feedback"),
277
+ },
278
+ toolHandler(async (p) => viewingUpdate(p as Parameters<typeof viewingUpdate>[0]))
279
+ );
280
+
281
+ // ─────────────────────────────────────────────────────────────────
282
+ // Feedback Tools (2) — permission: feedback
283
+ // ─────────────────────────────────────────────────────────────────
284
+
285
+ server.tool(
286
+ "loop-feedback-get",
287
+ "Get feedback for a specific viewing. Reads from /feedback/residential/{department}/viewings/{id}.",
288
+ {
289
+ teamName: z.string().min(1).describe("Team that owns the viewing"),
290
+ viewingId: z.number().int().describe("Viewing ID"),
291
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
292
+ },
293
+ toolHandler(async (p) => feedbackGet(p as Parameters<typeof feedbackGet>[0]))
294
+ );
295
+
296
+ server.tool(
297
+ "loop-feedback-submit",
298
+ "Submit feedback for a viewing. Writes to /feedback/residential/{department}/viewings/{id}/feedback.",
299
+ {
300
+ teamName: z.string().min(1).describe("Team that owns the viewing"),
301
+ viewingId: z.number().int().describe("Viewing ID"),
302
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
303
+ feedback: z.string().describe("Feedback text"),
304
+ },
305
+ toolHandler(async (p) => feedbackSubmit(p as Parameters<typeof feedbackSubmit>[0]))
306
+ );
307
+
308
+ // ─────────────────────────────────────────────────────────────────
309
+ // Team Tools (2) — permission: team
310
+ // ─────────────────────────────────────────────────────────────────
311
+
230
312
  server.tool(
231
313
  "loop-team-info",
232
- "Get team details from Loop CRM. Returns team name, address, phone, and email. Aggregates across all registered teams unless a specific team is named.",
314
+ "Get team details from Loop CRM. Returns team name, address, phone, and email.",
233
315
  {
234
316
  teamName: z.string().optional().describe("Query a specific team only (omit for all teams)"),
235
317
  },
236
- async (params) => {
237
- try {
238
- const text = await teamInfo({ ...params, accountId });
239
- return { content: [{ type: "text" as const, text }] };
240
- } catch (err) {
241
- return {
242
- content: [
243
- {
244
- type: "text" as const,
245
- text: `Team info query failed: ${err instanceof Error ? err.message : String(err)}`,
246
- },
247
- ],
248
- isError: true,
249
- };
250
- }
251
- }
318
+ toolHandler(async (p) => teamInfo(p as Parameters<typeof teamInfo>[0]))
319
+ );
320
+
321
+ server.tool(
322
+ "loop-team-availability",
323
+ "Get available time slots for a specific agent on a given date.",
324
+ {
325
+ agentId: z.string().describe("Agent GUID identifier"),
326
+ searchDate: z.string().describe("Date to check availability (YYYY-MM-DD)"),
327
+ teamName: z.string().optional().describe("Query a specific team only"),
328
+ },
329
+ toolHandler(async (p) => teamAvailability(p as Parameters<typeof teamAvailability>[0]))
330
+ );
331
+
332
+ // ─────────────────────────────────────────────────────────────────
333
+ // Marketing Tools (4) — permission: marketing
334
+ // ─────────────────────────────────────────────────────────────────
335
+
336
+ server.tool(
337
+ "loop-marketing-match",
338
+ "Get matching property detail and optionally the team profile for that match.",
339
+ {
340
+ propertyId: z.number().int().describe("Property ID"),
341
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
342
+ includeTeamProfile: z.boolean().optional().describe("Include the team profile for this match (default: false)"),
343
+ teamName: z.string().optional().describe("Query a specific team only"),
344
+ },
345
+ toolHandler(async (p) => marketingMatchDetail(p as Parameters<typeof marketingMatchDetail>[0]))
346
+ );
347
+
348
+ server.tool(
349
+ "loop-marketing-match-batch",
350
+ "Get batch matching results for multiple property IDs.",
351
+ {
352
+ propertyIds: z.array(z.number().int()).describe("Array of property IDs to match"),
353
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
354
+ teamName: z.string().optional().describe("Query a specific team only"),
355
+ },
356
+ toolHandler(async (p) => marketingMatchBatch(p as Parameters<typeof marketingMatchBatch>[0]))
357
+ );
358
+
359
+ server.tool(
360
+ "loop-marketing-match-request",
361
+ "Submit a viewing, information, or callback request for a matching property.",
362
+ {
363
+ teamName: z.string().min(1).describe("Team to submit the request through"),
364
+ propertyId: z.number().int().describe("Matching property ID"),
365
+ department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
366
+ action: z.enum(["viewing", "information", "callback"]).describe("Request type"),
367
+ name: z.string().optional().describe("Requester name"),
368
+ email: z.string().optional().describe("Requester email"),
369
+ phone: z.string().optional().describe("Requester phone"),
370
+ message: z.string().optional().describe("Additional message"),
371
+ },
372
+ toolHandler(async (p) => marketingMatchRequest(p as Parameters<typeof marketingMatchRequest>[0]))
373
+ );
374
+
375
+ server.tool(
376
+ "loop-marketing-enquiry",
377
+ "Marketing enquiry operations: submit seller enquiries, manage auto-responder interactions.",
378
+ {
379
+ teamName: z.string().min(1).describe("Team to submit through"),
380
+ action: z.enum([
381
+ "seller-enquiry", "autoresponder-get", "autoresponder-answers",
382
+ "autoresponder-details", "autoresponder-refer",
383
+ ]).describe("Enquiry action"),
384
+ sellerEnquiryData: z.record(z.string(), z.unknown()).optional().describe("Seller enquiry request body (for seller-enquiry)"),
385
+ autoResponderId: z.number().int().optional().describe("Auto-responder enquiry ID"),
386
+ autoResponderKey: z.string().optional().describe("Auto-responder key (UUID)"),
387
+ answers: z.array(z.unknown()).optional().describe("Auto-responder answers array"),
388
+ details: z.record(z.string(), z.unknown()).optional().describe("Auto-responder details object"),
389
+ },
390
+ toolHandler(async (p) => marketingEnquiry(p as Parameters<typeof marketingEnquiry>[0]))
391
+ );
392
+
393
+ // ─────────────────────────────────────────────────────────────────
394
+ // Customer Tools (1) — permission: customer
395
+ // ─────────────────────────────────────────────────────────────────
396
+
397
+ server.tool(
398
+ "loop-customer-preferences",
399
+ "Read or write customer preferences for a person. Action 'read' returns current preferences, 'write' updates them.",
400
+ {
401
+ teamName: z.string().min(1).describe("Team for this operation"),
402
+ personCode: z.number().int().describe("Person code (ID)"),
403
+ action: z.enum(["read", "write"]).describe("read or write"),
404
+ preferences: z.record(z.string(), z.unknown()).optional().describe("Preferences object (required for write)"),
405
+ },
406
+ toolHandler(async (p) => customerPreferences(p as Parameters<typeof customerPreferences>[0]))
407
+ );
408
+
409
+ // ─────────────────────────────────────────────────────────────────
410
+ // Supplier Tools (1) — permission: supplier
411
+ // ─────────────────────────────────────────────────────────────────
412
+
413
+ server.tool(
414
+ "loop-supplier",
415
+ "Supplier operations: maintenance jobs, quotes, board contractor jobs. Supports listing, completing, and quoting.",
416
+ {
417
+ teamName: z.string().min(1).describe("Team for this operation"),
418
+ action: z.enum([
419
+ "maintenance-jobs", "maintenance-complete", "maintenance-quotes",
420
+ "maintenance-submit-quote", "board-jobs", "board-complete",
421
+ ]).describe("Supplier action"),
422
+ code: z.string().describe("Unique code assigned to the contractor"),
423
+ jobId: z.number().int().optional().describe("Job ID (required for all actions except maintenance-submit-quote)"),
424
+ quoteId: z.number().int().optional().describe("Quote ID (for maintenance-submit-quote)"),
425
+ quoteData: z.record(z.string(), z.unknown()).optional().describe("Quote data (for maintenance-submit-quote)"),
426
+ completionData: z.record(z.string(), z.unknown()).optional().describe("Completion data (for board-complete)"),
427
+ },
428
+ toolHandler(async (p) => supplier(p as Parameters<typeof supplier>[0]))
252
429
  );
253
430
 
254
431
  // ─────────────────────────────────────────────────────────────────