capsulemcp 1.8.1 → 2.0.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/README.md CHANGED
@@ -4,10 +4,10 @@
4
4
 
5
5
  A [Model Context Protocol](https://modelcontextprotocol.io) server for [Capsule CRM](https://capsulecrm.com). Connect Claude (Desktop, Code, or web Projects via Custom Connector) to your CRM and let it answer natural-language questions across the full record graph: contacts, organisations, opportunities, projects, tasks, and timeline activity. Beyond the basics it covers structured filters with field/operator conditions, saved searches with sort, workflow tracks (templates and instances), file attachments (read + write), audit of deleted records, and batch fetches up to 50 records per call.
6
6
 
7
- - **88 tools** across the Capsule resource graph (49 in read-only mode) — full read coverage plus careful, confirm-gated writes; 6 batched-write tools (`batch_*`) for mass-update workflows
7
+ - **89 tools** across the Capsule resource graph (50 in read-only mode) — full read coverage plus careful, confirm-gated writes; 6 batched-write tools (`batch_*`) for mass-update workflows
8
8
  - **Two transports**: stdio for local installs (Claude Desktop / Code), HTTP+OAuth for hosted Custom Connectors
9
9
  - **Read-only mode** as a one-env-var flag; works alongside read-scoped Capsule tokens
10
- - **MCP tool annotations**: 49 read tools carry `readOnlyHint: true`, 8 destructive ones carry `destructiveHint: true` — clients that honor these hints can auto-approve safe reads while still prompting for writes/destructive calls
10
+ - **MCP tool annotations**: 50 read tools carry `readOnlyHint: true`, 8 destructive ones carry `destructiveHint: true` — clients that honor these hints can auto-approve safe reads while still prompting for writes/destructive calls
11
11
  - **Apache 2.0**
12
12
 
13
13
  ## Pick your install
@@ -48,7 +48,7 @@ For most individual users the install is a single JSON snippet pasted into Claud
48
48
 
49
49
  3. Restart Claude Desktop. The Capsule tools appear in the tool picker.
50
50
 
51
- That's it. The first launch fetches the package from npm (a few seconds); subsequent launches are instant from the npx cache. To pin a specific version, use `"capsulemcp@1.8.1"` in `args`. If you're tracking a fork or an unreleased branch, use the GitHub-ref form instead: `"github:soil-dev/capsulemcp#v1.8.1"` — same arguments, just installs from a git clone rather than the npm registry. See [INSTALL.md](INSTALL.md) for the Claude Code path, manual install, and troubleshooting.
51
+ That's it. The first launch fetches the package from npm (a few seconds); subsequent launches are instant from the npx cache. To pin a specific version, use `"capsulemcp@2.0.0"` in `args`. If you're tracking a fork or an unreleased branch, use the GitHub-ref form instead: `"github:soil-dev/capsulemcp#v2.0.0"` — same arguments, just installs from a git clone rather than the npm registry. See [INSTALL.md](INSTALL.md) for the Claude Code path, manual install, and troubleshooting.
52
52
 
53
53
  ## Tools
54
54
 
@@ -56,7 +56,7 @@ That's it. The first launch fetches the package from npm (a few seconds); subseq
56
56
  |---|---|---|
57
57
  | Parties (people/orgs) | `search_parties`, `filter_parties`, `get_party`, `get_parties`, `list_employees`, `list_party_opportunities`, `list_party_projects`, `list_party_entries` | `create_party`, `update_party`, `delete_party`, `add_party_email_address`, `remove_party_email_address_by_id`, `add_party_phone_number`, `remove_party_phone_number_by_id`, `add_party_address`, `remove_party_address_by_id`, `add_party_website`, `remove_party_website_by_id` |
58
58
  | Opportunities | `search_opportunities`, `filter_opportunities`, `get_opportunity`, `get_opportunities`, `list_opportunity_entries`, `list_associated_projects` | `create_opportunity`, `update_opportunity`, `delete_opportunity` |
59
- | Projects (cases) | `list_projects`, `filter_projects`, `get_project`, `get_projects`, `list_project_entries` | `create_project`, `update_project`, `delete_project` |
59
+ | Projects (cases) | `search_projects`, `list_projects`, `filter_projects`, `get_project`, `get_projects`, `list_project_entries` | `create_project`, `update_project`, `delete_project` |
60
60
  | Additional parties (multi-party deals) | `list_additional_parties` | `add_additional_party`, `remove_additional_party` |
61
61
  | Tasks | `list_tasks`, `get_task`, `get_tasks` | `create_task`, `update_task`, `complete_task`, `delete_task` |
62
62
  | Entries (notes / captured emails) | `get_entry`, `list_entries` | `add_note`, `update_entry`, `delete_entry` |
@@ -64,12 +64,12 @@ That's it. The first launch fetches the package from npm (a few seconds); subseq
64
64
  | Audit (deleted records) | `list_deleted_parties`, `list_deleted_opportunities`, `list_deleted_projects` | — |
65
65
  | Pipelines & milestones (opportunities) | `list_pipelines`, `list_milestones` | — |
66
66
  | Boards & stages (projects) | `list_boards`, `list_stages` | — |
67
- | Tracks (workflow instances) | `list_track_definitions`, `list_entity_tracks`, `show_track` | `apply_track`, `update_track`, `remove_track` |
67
+ | Tracks (workflow instances) | `list_track_definitions`, `list_entity_tracks`, `get_track` | `apply_track`, `update_track`, `remove_track` |
68
68
  | Saved filters | `list_saved_filters`, `run_saved_filter` | — |
69
69
  | Custom fields (schema) | `list_custom_fields`, `get_custom_field` | — |
70
70
  | Tags | `list_tags` | `add_tag`, `remove_tag_by_id`, `delete_tag_definition` |
71
71
  | Users & teams | `list_users`, `get_current_user`, `list_teams` | — |
72
- | Reference metadata | `list_lostreasons`, `list_activitytypes`, `list_categories`, `list_goals`, `get_site` | — |
72
+ | Reference metadata | `list_lost_reasons`, `list_activity_types`, `list_categories`, `list_goals`, `get_site` | — |
73
73
 
74
74
  Most record-list tools default `perPage=25`; reference-data tools default `perPage=100` so small accounts usually fit in one response. All paginated tools cap `perPage` at 100 and return a `nextPage` cursor when more results exist. Many GET tools accept an `embed` parameter (e.g. `tags,fields`) — see Capsule's API docs for the full list per resource.
75
75
 
package/dist/http.js CHANGED
@@ -156,6 +156,26 @@ function invalidateByPrefix(pathPrefix, trigger) {
156
156
  }
157
157
  }
158
158
 
159
+ // src/capsule/normalize.ts
160
+ var KEY_RENAMES = {
161
+ kase: "project",
162
+ kases: "projects",
163
+ restrictedKases: "restrictedProjects"
164
+ };
165
+ function normalizeProjectKeys(value) {
166
+ if (Array.isArray(value)) {
167
+ return value.map(normalizeProjectKeys);
168
+ }
169
+ if (value !== null && typeof value === "object") {
170
+ const out = {};
171
+ for (const [key, v] of Object.entries(value)) {
172
+ out[KEY_RENAMES[key] ?? key] = normalizeProjectKeys(v);
173
+ }
174
+ return out;
175
+ }
176
+ return value;
177
+ }
178
+
159
179
  // src/capsule/client.ts
160
180
  var DEFAULT_BASE_URL = "https://api.capsulecrm.com/api/v2";
161
181
  function baseUrl() {
@@ -427,7 +447,8 @@ async function throwForStatus(res) {
427
447
  }
428
448
  async function handleResponse(res) {
429
449
  await throwForStatus(res);
430
- return mapAbort(res.json());
450
+ const body = await mapAbort(res.json());
451
+ return normalizeProjectKeys(body);
431
452
  }
432
453
  function buildUrl(path, params) {
433
454
  const url = new URL(`${baseUrl()}${path}`);
@@ -1154,6 +1175,7 @@ var CORE_TOOLS = /* @__PURE__ */ new Set([
1154
1175
  "create_opportunity",
1155
1176
  "update_opportunity",
1156
1177
  // Projects
1178
+ "search_projects",
1157
1179
  "filter_projects",
1158
1180
  "list_projects",
1159
1181
  "get_project",
@@ -1651,10 +1673,6 @@ function defineBatch(args) {
1651
1673
  return { schema, handler };
1652
1674
  }
1653
1675
 
1654
- // src/tools/descriptions.ts
1655
- var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
1656
- var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1657
-
1658
1676
  // src/tools/define-delete.ts
1659
1677
  import { z as z6 } from "zod";
1660
1678
 
@@ -1680,6 +1698,26 @@ var paginationFieldsNoDefaults = {
1680
1698
  page: z5.number().int().positive().optional(),
1681
1699
  perPage: z5.number().int().min(1).max(100).optional()
1682
1700
  };
1701
+ var ENTITY_PATH = {
1702
+ parties: "parties",
1703
+ opportunities: "opportunities",
1704
+ projects: "kases"
1705
+ };
1706
+ function embedParam(allowed) {
1707
+ return z5.string().superRefine((value, ctx) => {
1708
+ const tokens = value.split(",").map((t) => t.trim());
1709
+ for (const token of tokens) {
1710
+ if (token === "" || !allowed.includes(token)) {
1711
+ ctx.addIssue({
1712
+ code: "custom",
1713
+ message: `Unknown embed token '${token}'. Valid tokens: ${allowed.join(", ")} (comma-separated). Capsule silently ignores unknown tokens, so this is rejected client-side to prevent silently-missing data.`
1714
+ });
1715
+ }
1716
+ }
1717
+ }).describe(`Comma-separated embeds. Valid tokens: ${allowed.join(", ")}.`).optional();
1718
+ }
1719
+ var RECORD_EMBEDS = ["tags", "fields", "missingImportantFields"];
1720
+ var ENTRY_EMBEDS = ["attachments", "participants"];
1683
1721
 
1684
1722
  // src/capsule/idempotent.ts
1685
1723
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
@@ -1844,7 +1882,7 @@ var WebsiteSchema = z8.object({
1844
1882
  }).superRefine(validateWebsiteAddress);
1845
1883
  var searchPartiesSchema = z8.object({
1846
1884
  q: z8.string().optional().describe("Free-text search query"),
1847
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1885
+ embed: embedParam(RECORD_EMBEDS),
1848
1886
  ...paginationFields
1849
1887
  });
1850
1888
  async function searchParties(input) {
@@ -1858,7 +1896,7 @@ async function searchParties(input) {
1858
1896
  }
1859
1897
  var getPartySchema = z8.object({
1860
1898
  id: positiveId.describe("Party ID"),
1861
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1899
+ embed: embedParam(RECORD_EMBEDS)
1862
1900
  });
1863
1901
  async function getParty(input) {
1864
1902
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1870,7 +1908,7 @@ var getPartiesSchema = z8.object({
1870
1908
  ids: z8.array(positiveId).min(1).max(50).describe(
1871
1909
  "Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
1872
1910
  ),
1873
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1911
+ embed: embedParam(RECORD_EMBEDS)
1874
1912
  });
1875
1913
  async function getParties(input) {
1876
1914
  return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
@@ -1936,6 +1974,21 @@ var createPartySchema = z8.object({
1936
1974
  fields: z8.array(CustomFieldWriteSchema).optional().describe(
1937
1975
  fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
1938
1976
  )
1977
+ }).superRefine((data, ctx) => {
1978
+ if (data.type === "person" && !data.firstName && !data.lastName) {
1979
+ ctx.addIssue({
1980
+ code: "custom",
1981
+ path: ["firstName"],
1982
+ message: "create_party: a person requires firstName and/or lastName"
1983
+ });
1984
+ }
1985
+ if (data.type === "organisation" && !data.name) {
1986
+ ctx.addIssue({
1987
+ code: "custom",
1988
+ path: ["name"],
1989
+ message: "create_party: an organisation requires name"
1990
+ });
1991
+ }
1939
1992
  });
1940
1993
  async function createParty(input) {
1941
1994
  const { ownerId, teamId, organisationId, fields, ...rest } = input;
@@ -1986,7 +2039,7 @@ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch(
1986
2039
  var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1987
2040
  toolName: "delete_party",
1988
2041
  pathPrefix: "/parties",
1989
- confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
2042
+ confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects. Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1990
2043
  });
1991
2044
  function definePartySubResourceRemove(opts) {
1992
2045
  const shape = {
@@ -2121,7 +2174,7 @@ var OpportunityValueSchema = z9.object({
2121
2174
  });
2122
2175
  var searchOpportunitiesSchema = z9.object({
2123
2176
  q: z9.string().optional().describe("Free-text search query"),
2124
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2177
+ embed: embedParam(RECORD_EMBEDS),
2125
2178
  ...paginationFields
2126
2179
  });
2127
2180
  async function searchOpportunities(input) {
@@ -2135,7 +2188,7 @@ async function searchOpportunities(input) {
2135
2188
  }
2136
2189
  var getOpportunitySchema = z9.object({
2137
2190
  id: positiveId,
2138
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2191
+ embed: embedParam(RECORD_EMBEDS)
2139
2192
  });
2140
2193
  async function getOpportunity(input) {
2141
2194
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -2147,7 +2200,7 @@ var getOpportunitiesSchema = z9.object({
2147
2200
  ids: z9.array(positiveId).min(1).max(50).describe(
2148
2201
  "Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
2149
2202
  ),
2150
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2203
+ embed: embedParam(RECORD_EMBEDS)
2151
2204
  });
2152
2205
  async function getOpportunities(input) {
2153
2206
  return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
@@ -2201,7 +2254,7 @@ var updateOpportunitySchema = z9.object({
2201
2254
  "Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
2202
2255
  ),
2203
2256
  lostReasonId: positiveId.optional().describe(
2204
- "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lostreasons."
2257
+ "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lost_reasons."
2205
2258
  ),
2206
2259
  ownerId: positiveId.nullable().optional().describe(
2207
2260
  "Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
@@ -2246,9 +2299,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
2246
2299
 
2247
2300
  // src/tools/projects.ts
2248
2301
  import { z as z10 } from "zod";
2302
+ var searchProjectsSchema = z10.object({
2303
+ q: z10.string().optional().describe("Free-text search query"),
2304
+ embed: embedParam(RECORD_EMBEDS),
2305
+ ...paginationFields
2306
+ });
2307
+ async function searchProjects(input) {
2308
+ const path = input.q ? "/kases/search" : "/kases";
2309
+ return capsuleGetList(path, {
2310
+ q: input.q,
2311
+ embed: input.embed,
2312
+ page: input.page,
2313
+ perPage: input.perPage
2314
+ });
2315
+ }
2249
2316
  var listProjectsSchema = z10.object({
2250
2317
  status: z10.enum(["OPEN", "CLOSED"]).optional(),
2251
- embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2318
+ embed: embedParam(RECORD_EMBEDS),
2252
2319
  ...paginationFields
2253
2320
  });
2254
2321
  async function listProjects(input) {
@@ -2261,7 +2328,7 @@ async function listProjects(input) {
2261
2328
  }
2262
2329
  var getProjectSchema = z10.object({
2263
2330
  id: positiveId,
2264
- embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2331
+ embed: embedParam(RECORD_EMBEDS)
2265
2332
  });
2266
2333
  async function getProject(input) {
2267
2334
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -2273,10 +2340,10 @@ var getProjectsSchema = z10.object({
2273
2340
  ids: z10.array(positiveId).min(1).max(50).describe(
2274
2341
  "Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
2275
2342
  ),
2276
- embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2343
+ embed: embedParam(RECORD_EMBEDS)
2277
2344
  });
2278
2345
  async function getProjects(input) {
2279
- return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
2346
+ return chunkedMultiGet("/kases", "projects", input.ids, { embed: input.embed });
2280
2347
  }
2281
2348
  var createProjectSchema = z10.object({
2282
2349
  name: z10.string().min(1),
@@ -2343,7 +2410,7 @@ async function updateProject(input) {
2343
2410
  let resolvedTeamId = teamId;
2344
2411
  let resolvedStageId = stageId;
2345
2412
  if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
2346
- const current = await readEntityRefs(`/kases/${id}`, "kase");
2413
+ const current = await readEntityRefs(`/kases/${id}`, "project");
2347
2414
  if (teamId === void 0) resolvedTeamId = current.teamId;
2348
2415
  if (stageId === void 0) resolvedStageId = current.stageId;
2349
2416
  }
@@ -2494,7 +2561,7 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
2494
2561
  import { z as z12 } from "zod";
2495
2562
  var listEntriesPagination = {
2496
2563
  ...paginationFields,
2497
- embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2564
+ embed: embedParam(ENTRY_EMBEDS)
2498
2565
  };
2499
2566
  var listPartyEntriesSchema = z12.object({
2500
2567
  partyId: positiveId,
@@ -2602,7 +2669,7 @@ async function listProjectEntries(input) {
2602
2669
  }
2603
2670
  var getEntrySchema = z12.object({
2604
2671
  id: positiveId,
2605
- embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2672
+ embed: embedParam(ENTRY_EMBEDS)
2606
2673
  });
2607
2674
  async function getEntry(input) {
2608
2675
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2715,16 +2782,16 @@ import { z as z15 } from "zod";
2715
2782
  var TAG_LIST_PATH = {
2716
2783
  parties: "/parties/tags",
2717
2784
  opportunities: "/opportunities/tags",
2718
- kases: "/kases/tags"
2785
+ projects: "/kases/tags"
2719
2786
  };
2720
2787
  var ENTITY_TO_WRAPPER = {
2721
2788
  parties: "party",
2722
2789
  opportunities: "opportunity",
2723
- kases: "kase"
2790
+ projects: "kase"
2724
2791
  };
2725
- var TagEntity = z15.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2792
+ var TagEntity = z15.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
2726
2793
  var listTagsSchema = z15.object({
2727
- entity: z15.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2794
+ entity: z15.enum(["parties", "opportunities", "projects"]).describe("The resource type to list tags for"),
2728
2795
  ...paginationFieldsNoDefaults
2729
2796
  });
2730
2797
  async function listTags(input) {
@@ -2744,7 +2811,7 @@ var addTagSchema = z15.object({
2744
2811
  async function addTag(input) {
2745
2812
  const { entity, entityId, tagName } = input;
2746
2813
  const wrapper = ENTITY_TO_WRAPPER[entity];
2747
- const result = await capsulePut(`/${entity}/${entityId}`, {
2814
+ const result = await capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
2748
2815
  [wrapper]: { tags: [{ name: tagName }] }
2749
2816
  });
2750
2817
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
@@ -2761,7 +2828,7 @@ async function removeTagById(input) {
2761
2828
  const { entity, entityId, tagId } = input;
2762
2829
  const wrapper = ENTITY_TO_WRAPPER[entity];
2763
2830
  const result = await idempotentWithResult(
2764
- () => capsulePut(`/${entity}/${entityId}`, {
2831
+ () => capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
2765
2832
  [wrapper]: { tags: [{ id: tagId, _delete: true }] }
2766
2833
  }),
2767
2834
  (result2) => ({
@@ -2796,7 +2863,7 @@ async function deleteTagDefinition(input) {
2796
2863
  throw new Error("delete_tag_definition requires confirm: true");
2797
2864
  }
2798
2865
  const result = await idempotent(
2799
- () => capsuleDelete(`/${entity}/tags/${tagId}`),
2866
+ () => capsuleDelete(`/${ENTITY_PATH[entity]}/tags/${tagId}`),
2800
2867
  () => ({ deleted: true, alreadyDeleted: false, entity, tagId }),
2801
2868
  () => ({ deleted: true, alreadyDeleted: true, entity, tagId })
2802
2869
  );
@@ -2850,7 +2917,7 @@ var FilterInputSchema = z17.object({
2850
2917
  conditions: z17.array(FilterConditionSchema).min(1).describe(
2851
2918
  "Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
2852
2919
  ),
2853
- embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2920
+ embed: embedParam(RECORD_EMBEDS),
2854
2921
  ...paginationFields
2855
2922
  });
2856
2923
  async function runFilter(entityPath, input) {
@@ -2941,7 +3008,7 @@ var listEmployeesSchema = z19.object({
2941
3008
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2942
3009
  ),
2943
3010
  ...paginationFields,
2944
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
3011
+ embed: embedParam(RECORD_EMBEDS)
2945
3012
  });
2946
3013
  async function listEmployees(input) {
2947
3014
  return capsuleGetList(`/parties/${input.partyId}/people`, {
@@ -2984,19 +3051,22 @@ async function listDeletedProjects(input) {
2984
3051
 
2985
3052
  // src/tools/relationships.ts
2986
3053
  import { z as z20 } from "zod";
2987
- var RelationshipEntity = z20.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
3054
+ var RelationshipEntity = z20.enum(["opportunities", "projects"]).describe("Which entity has the additional-party links.");
2988
3055
  var listAdditionalPartiesSchema = z20.object({
2989
3056
  entity: RelationshipEntity,
2990
3057
  entityId: positiveId.describe("ID of the opportunity or project."),
2991
- embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3058
+ embed: embedParam(RECORD_EMBEDS),
2992
3059
  ...paginationFields
2993
3060
  });
2994
3061
  async function listAdditionalParties(input) {
2995
- return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
2996
- embed: input.embed,
2997
- page: input.page,
2998
- perPage: input.perPage
2999
- });
3062
+ return capsuleGetList(
3063
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/parties`,
3064
+ {
3065
+ embed: input.embed,
3066
+ page: input.page,
3067
+ perPage: input.perPage
3068
+ }
3069
+ );
3000
3070
  }
3001
3071
  var addAdditionalPartySchema = z20.object({
3002
3072
  entity: RelationshipEntity,
@@ -3007,7 +3077,9 @@ var addAdditionalPartySchema = z20.object({
3007
3077
  });
3008
3078
  async function addAdditionalParty(input) {
3009
3079
  try {
3010
- await capsulePostNoContent(`/${input.entity}/${input.entityId}/parties/${input.partyId}`);
3080
+ await capsulePostNoContent(
3081
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`
3082
+ );
3011
3083
  return {
3012
3084
  linked: true,
3013
3085
  alreadyLinked: false,
@@ -3044,7 +3116,7 @@ async function removeAdditionalParty(input) {
3044
3116
  throw new Error("remove_additional_party requires confirm: true");
3045
3117
  }
3046
3118
  return idempotent(
3047
- () => capsuleDelete(`/${input.entity}/${input.entityId}/parties/${input.partyId}`),
3119
+ () => capsuleDelete(`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`),
3048
3120
  () => ({
3049
3121
  removed: true,
3050
3122
  alreadyRemoved: false,
@@ -3063,7 +3135,7 @@ async function removeAdditionalParty(input) {
3063
3135
  }
3064
3136
  var listAssociatedProjectsSchema = z20.object({
3065
3137
  opportunityId: positiveId,
3066
- embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3138
+ embed: embedParam(RECORD_EMBEDS),
3067
3139
  ...paginationFields
3068
3140
  });
3069
3141
  async function listAssociatedProjects(input) {
@@ -3076,49 +3148,49 @@ async function listAssociatedProjects(input) {
3076
3148
 
3077
3149
  // src/tools/custom-fields.ts
3078
3150
  import { z as z21 } from "zod";
3079
- var CustomFieldEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
3151
+ var CustomFieldEntity = z21.enum(["parties", "opportunities", "projects"]).describe("Which entity type's custom field schema to inspect.");
3080
3152
  var listCustomFieldsSchema = z21.object({
3081
3153
  entity: CustomFieldEntity
3082
3154
  });
3083
3155
  async function listCustomFields(input) {
3084
3156
  const { data } = await capsuleGetCached(
3085
- `/${input.entity}/fields/definitions`
3157
+ `/${ENTITY_PATH[input.entity]}/fields/definitions`
3086
3158
  );
3087
3159
  return data;
3088
3160
  }
3089
3161
  var getCustomFieldSchema = z21.object({
3090
3162
  entity: CustomFieldEntity,
3091
- fieldId: positiveId.describe("Custom field definition id.")
3163
+ id: positiveId.describe("Custom field definition id.")
3092
3164
  });
3093
3165
  async function getCustomField(input) {
3094
3166
  const { data } = await capsuleGetCached(
3095
- `/${input.entity}/fields/definitions/${input.fieldId}`
3167
+ `/${input.entity}/fields/definitions/${input.id}`
3096
3168
  );
3097
3169
  return data;
3098
3170
  }
3099
3171
 
3100
3172
  // src/tools/tracks.ts
3101
3173
  import { z as z22 } from "zod";
3102
- var TrackEntity = z22.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
3174
+ var TrackEntity = z22.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
3103
3175
  var listEntityTracksSchema = z22.object({
3104
3176
  entity: TrackEntity,
3105
3177
  entityId: positiveId
3106
3178
  });
3107
3179
  async function listEntityTracks(input) {
3108
3180
  const { data } = await capsuleGet(
3109
- `/${input.entity}/${input.entityId}/tracks`
3181
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/tracks`
3110
3182
  );
3111
3183
  return data;
3112
3184
  }
3113
- var showTrackSchema = z22.object({
3114
- trackId: positiveId
3185
+ var getTrackSchema = z22.object({
3186
+ id: positiveId
3115
3187
  });
3116
- async function showTrack(input) {
3117
- const { data } = await capsuleGet(`/tracks/${input.trackId}`);
3188
+ async function getTrack(input) {
3189
+ const { data } = await capsuleGet(`/tracks/${input.id}`);
3118
3190
  return data;
3119
3191
  }
3120
3192
  var applyTrackSchema = z22.object({
3121
- entity: z22.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
3193
+ entity: z22.enum(["opportunities", "projects"]).describe("Which entity to apply the track to."),
3122
3194
  entityId: positiveId,
3123
3195
  trackDefinitionId: positiveId.describe(
3124
3196
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
@@ -3137,7 +3209,7 @@ async function applyTrack(input) {
3137
3209
  return capsulePost("/tracks", { track });
3138
3210
  }
3139
3211
  var updateTrackSchema = z22.object({
3140
- trackId: positiveId,
3212
+ id: positiveId,
3141
3213
  fields: z22.record(z22.string(), z22.unknown()).describe(
3142
3214
  "Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
3143
3215
  )
@@ -3146,12 +3218,12 @@ async function updateTrack(input) {
3146
3218
  if (Object.keys(input.fields).length === 0) {
3147
3219
  throw new Error("update_track: provide at least one field in `fields`");
3148
3220
  }
3149
- return capsulePut(`/tracks/${input.trackId}`, {
3221
+ return capsulePut(`/tracks/${input.id}`, {
3150
3222
  track: input.fields
3151
3223
  });
3152
3224
  }
3153
3225
  var removeTrackSchema = z22.object({
3154
- trackId: positiveId,
3226
+ id: positiveId,
3155
3227
  confirm: confirmFlag().describe(
3156
3228
  "Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
3157
3229
  )
@@ -3161,9 +3233,9 @@ async function removeTrack(input) {
3161
3233
  throw new Error("remove_track requires confirm: true");
3162
3234
  }
3163
3235
  return idempotent(
3164
- () => capsuleDelete(`/tracks/${input.trackId}`),
3165
- () => ({ removed: true, alreadyRemoved: false, trackId: input.trackId }),
3166
- () => ({ removed: true, alreadyRemoved: true, trackId: input.trackId })
3236
+ () => capsuleDelete(`/tracks/${input.id}`),
3237
+ () => ({ removed: true, alreadyRemoved: false, id: input.id }),
3238
+ () => ({ removed: true, alreadyRemoved: true, id: input.id })
3167
3239
  );
3168
3240
  }
3169
3241
 
@@ -3250,28 +3322,31 @@ async function uploadAttachment(input) {
3250
3322
 
3251
3323
  // src/tools/saved-filters.ts
3252
3324
  import { z as z24 } from "zod";
3253
- var EntitySchema = z24.enum(["parties", "opportunities", "kases"]).describe(
3254
- "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
3255
- );
3325
+ var EntitySchema = z24.enum(["parties", "opportunities", "projects"]).describe("Which entity type the filter operates over.");
3256
3326
  var listSavedFiltersSchema = z24.object({
3257
3327
  entity: EntitySchema
3258
3328
  });
3259
3329
  async function listSavedFilters(input) {
3260
- const { data } = await capsuleGetCached(`/${input.entity}/filters`);
3330
+ const { data } = await capsuleGetCached(
3331
+ `/${ENTITY_PATH[input.entity]}/filters`
3332
+ );
3261
3333
  return data;
3262
3334
  }
3263
3335
  var runSavedFilterSchema = z24.object({
3264
3336
  entity: EntitySchema,
3265
3337
  id: positiveId.describe("The saved filter id (from list_saved_filters)."),
3266
- embed: z24.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3338
+ embed: embedParam(RECORD_EMBEDS),
3267
3339
  ...paginationFields
3268
3340
  });
3269
3341
  async function runSavedFilter(input) {
3270
- return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
3271
- page: input.page,
3272
- perPage: input.perPage,
3273
- embed: input.embed
3274
- });
3342
+ return capsuleGetList(
3343
+ `/${ENTITY_PATH[input.entity]}/filters/${input.id}/results`,
3344
+ {
3345
+ page: input.page,
3346
+ perPage: input.perPage,
3347
+ embed: input.embed
3348
+ }
3349
+ );
3275
3350
  }
3276
3351
 
3277
3352
  // src/server.ts
@@ -3282,7 +3357,7 @@ function createCapsuleMcpServer(opts) {
3282
3357
  const server = new McpServer(
3283
3358
  {
3284
3359
  name: "capsulemcp",
3285
- version: "1.8.1",
3360
+ version: "2.0.0",
3286
3361
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
3287
3362
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
3288
3363
  icons: ICONS
@@ -3399,7 +3474,7 @@ function createCapsuleMcpServer(opts) {
3399
3474
  registerTool(
3400
3475
  server,
3401
3476
  "delete_party",
3402
- "DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects (kases). Deleting an organisation does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via show_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
3477
+ "DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects. Deleting an organisation does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via get_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
3403
3478
  deletePartySchema,
3404
3479
  deleteParty
3405
3480
  );
@@ -3498,7 +3573,7 @@ function createCapsuleMcpServer(opts) {
3498
3573
  registerTool(
3499
3574
  server,
3500
3575
  "list_additional_parties",
3501
- "List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'kases' (Capsule's term for projects).",
3576
+ "List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'projects'.",
3502
3577
  listAdditionalPartiesSchema,
3503
3578
  listAdditionalParties
3504
3579
  );
@@ -3539,10 +3614,17 @@ function createCapsuleMcpServer(opts) {
3539
3614
  deleteOpportunity
3540
3615
  );
3541
3616
  }
3617
+ registerTool(
3618
+ server,
3619
+ "search_projects",
3620
+ "Free-text search projects in Capsule CRM (matches name and description). Returns results in Capsule's default order (no sort parameter is supported here). Omit `q` to list all projects. For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3621
+ searchProjectsSchema,
3622
+ searchProjects
3623
+ );
3542
3624
  registerTool(
3543
3625
  server,
3544
3626
  "list_projects",
3545
- "List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3627
+ "List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For free-text matching use search_projects; for structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3546
3628
  listProjectsSchema,
3547
3629
  listProjects
3548
3630
  );
@@ -3570,7 +3652,7 @@ function createCapsuleMcpServer(opts) {
3570
3652
  registerTool(
3571
3653
  server,
3572
3654
  "list_deleted_projects",
3573
- "Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedKases` key for records the integration user can't read fully.",
3655
+ "Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedProjects` key for records the integration user can't read fully.",
3574
3656
  listDeletedProjectsSchema,
3575
3657
  listDeletedProjects
3576
3658
  );
@@ -3885,14 +3967,14 @@ function createCapsuleMcpServer(opts) {
3885
3967
  );
3886
3968
  registerTool(
3887
3969
  server,
3888
- "list_lostreasons",
3970
+ "list_lost_reasons",
3889
3971
  "List all configured opportunity-loss reasons (e.g. 'Poor Qualification', 'Lost to competitor', 'Price too high'). Returns each reason's id and name; the set is account-configured rather than a fixed enum, so call this to discover valid ids before referencing a lostReason in update_opportunity when closing a deal as lost. Useful for analysing closed-lost opportunities by reason.",
3890
3972
  listLostReasonsSchema,
3891
3973
  listLostReasons
3892
3974
  );
3893
3975
  registerTool(
3894
3976
  server,
3895
- "list_activitytypes",
3977
+ "list_activity_types",
3896
3978
  "List all configured activity types (e.g. Call, Meeting, Email). These are the categories used when logging timeline entries via add_note. Returns each type's id and name. The set is account-configured rather than a fixed enum, so call this to discover valid values before referencing an activityType in entry creation.",
3897
3979
  listActivityTypesSchema,
3898
3980
  listActivityTypes
@@ -3920,10 +4002,10 @@ function createCapsuleMcpServer(opts) {
3920
4002
  );
3921
4003
  registerTool(
3922
4004
  server,
3923
- "show_track",
4005
+ "get_track",
3924
4006
  "Fetch a single track instance by id. Returns the minimal Capsule projection: id, description, trackDateOn, direction, and the array of tasks attached to the track. Capsule's GET /tracks/{id} does NOT include a trackDefinition link, an entity reference, or a completion field \u2014 to find the entity a track is applied to, use list_entity_tracks (which lists track instances by their parent entity); to check completion, the track-tasks' own statuses are the proxy.",
3925
- showTrackSchema,
3926
- showTrack
4007
+ getTrackSchema,
4008
+ getTrack
3927
4009
  );
3928
4010
  registerTool(
3929
4011
  server,
@@ -3956,7 +4038,7 @@ function createCapsuleMcpServer(opts) {
3956
4038
  registerTool(
3957
4039
  server,
3958
4040
  "list_tags",
3959
- "List all tags available for a given entity type (parties, opportunities, or kases). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
4041
+ "List all tags available for a given entity type (parties, opportunities, or projects). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
3960
4042
  listTagsSchema,
3961
4043
  listTags
3962
4044
  );
@@ -3978,7 +4060,7 @@ function createCapsuleMcpServer(opts) {
3978
4060
  registerTool(
3979
4061
  server,
3980
4062
  "delete_tag_definition",
3981
- "DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / kases). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
4063
+ "DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / projects). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
3982
4064
  deleteTagDefinitionSchema,
3983
4065
  deleteTagDefinition
3984
4066
  );
package/dist/index.js CHANGED
@@ -138,6 +138,26 @@ function invalidateByPrefix(pathPrefix, trigger) {
138
138
  }
139
139
  }
140
140
 
141
+ // src/capsule/normalize.ts
142
+ var KEY_RENAMES = {
143
+ kase: "project",
144
+ kases: "projects",
145
+ restrictedKases: "restrictedProjects"
146
+ };
147
+ function normalizeProjectKeys(value) {
148
+ if (Array.isArray(value)) {
149
+ return value.map(normalizeProjectKeys);
150
+ }
151
+ if (value !== null && typeof value === "object") {
152
+ const out = {};
153
+ for (const [key, v] of Object.entries(value)) {
154
+ out[KEY_RENAMES[key] ?? key] = normalizeProjectKeys(v);
155
+ }
156
+ return out;
157
+ }
158
+ return value;
159
+ }
160
+
141
161
  // src/capsule/client.ts
142
162
  var DEFAULT_BASE_URL = "https://api.capsulecrm.com/api/v2";
143
163
  function baseUrl() {
@@ -409,7 +429,8 @@ async function throwForStatus(res) {
409
429
  }
410
430
  async function handleResponse(res) {
411
431
  await throwForStatus(res);
412
- return mapAbort(res.json());
432
+ const body = await mapAbort(res.json());
433
+ return normalizeProjectKeys(body);
413
434
  }
414
435
  function buildUrl(path, params) {
415
436
  const url = new URL(`${baseUrl()}${path}`);
@@ -651,6 +672,7 @@ var CORE_TOOLS = /* @__PURE__ */ new Set([
651
672
  "create_opportunity",
652
673
  "update_opportunity",
653
674
  // Projects
675
+ "search_projects",
654
676
  "filter_projects",
655
677
  "list_projects",
656
678
  "get_project",
@@ -1148,10 +1170,6 @@ function defineBatch(args) {
1148
1170
  return { schema, handler };
1149
1171
  }
1150
1172
 
1151
- // src/tools/descriptions.ts
1152
- var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
1153
- var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1154
-
1155
1173
  // src/tools/define-delete.ts
1156
1174
  import { z as z5 } from "zod";
1157
1175
 
@@ -1177,6 +1195,26 @@ var paginationFieldsNoDefaults = {
1177
1195
  page: z4.number().int().positive().optional(),
1178
1196
  perPage: z4.number().int().min(1).max(100).optional()
1179
1197
  };
1198
+ var ENTITY_PATH = {
1199
+ parties: "parties",
1200
+ opportunities: "opportunities",
1201
+ projects: "kases"
1202
+ };
1203
+ function embedParam(allowed) {
1204
+ return z4.string().superRefine((value, ctx) => {
1205
+ const tokens = value.split(",").map((t) => t.trim());
1206
+ for (const token of tokens) {
1207
+ if (token === "" || !allowed.includes(token)) {
1208
+ ctx.addIssue({
1209
+ code: "custom",
1210
+ message: `Unknown embed token '${token}'. Valid tokens: ${allowed.join(", ")} (comma-separated). Capsule silently ignores unknown tokens, so this is rejected client-side to prevent silently-missing data.`
1211
+ });
1212
+ }
1213
+ }
1214
+ }).describe(`Comma-separated embeds. Valid tokens: ${allowed.join(", ")}.`).optional();
1215
+ }
1216
+ var RECORD_EMBEDS = ["tags", "fields", "missingImportantFields"];
1217
+ var ENTRY_EMBEDS = ["attachments", "participants"];
1180
1218
 
1181
1219
  // src/capsule/idempotent.ts
1182
1220
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
@@ -1341,7 +1379,7 @@ var WebsiteSchema = z7.object({
1341
1379
  }).superRefine(validateWebsiteAddress);
1342
1380
  var searchPartiesSchema = z7.object({
1343
1381
  q: z7.string().optional().describe("Free-text search query"),
1344
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1382
+ embed: embedParam(RECORD_EMBEDS),
1345
1383
  ...paginationFields
1346
1384
  });
1347
1385
  async function searchParties(input) {
@@ -1355,7 +1393,7 @@ async function searchParties(input) {
1355
1393
  }
1356
1394
  var getPartySchema = z7.object({
1357
1395
  id: positiveId.describe("Party ID"),
1358
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1396
+ embed: embedParam(RECORD_EMBEDS)
1359
1397
  });
1360
1398
  async function getParty(input) {
1361
1399
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1367,7 +1405,7 @@ var getPartiesSchema = z7.object({
1367
1405
  ids: z7.array(positiveId).min(1).max(50).describe(
1368
1406
  "Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
1369
1407
  ),
1370
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1408
+ embed: embedParam(RECORD_EMBEDS)
1371
1409
  });
1372
1410
  async function getParties(input) {
1373
1411
  return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
@@ -1433,6 +1471,21 @@ var createPartySchema = z7.object({
1433
1471
  fields: z7.array(CustomFieldWriteSchema).optional().describe(
1434
1472
  fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
1435
1473
  )
1474
+ }).superRefine((data, ctx) => {
1475
+ if (data.type === "person" && !data.firstName && !data.lastName) {
1476
+ ctx.addIssue({
1477
+ code: "custom",
1478
+ path: ["firstName"],
1479
+ message: "create_party: a person requires firstName and/or lastName"
1480
+ });
1481
+ }
1482
+ if (data.type === "organisation" && !data.name) {
1483
+ ctx.addIssue({
1484
+ code: "custom",
1485
+ path: ["name"],
1486
+ message: "create_party: an organisation requires name"
1487
+ });
1488
+ }
1436
1489
  });
1437
1490
  async function createParty(input) {
1438
1491
  const { ownerId, teamId, organisationId, fields, ...rest } = input;
@@ -1483,7 +1536,7 @@ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch(
1483
1536
  var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1484
1537
  toolName: "delete_party",
1485
1538
  pathPrefix: "/parties",
1486
- confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1539
+ confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects. Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1487
1540
  });
1488
1541
  function definePartySubResourceRemove(opts) {
1489
1542
  const shape = {
@@ -1618,7 +1671,7 @@ var OpportunityValueSchema = z8.object({
1618
1671
  });
1619
1672
  var searchOpportunitiesSchema = z8.object({
1620
1673
  q: z8.string().optional().describe("Free-text search query"),
1621
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1674
+ embed: embedParam(RECORD_EMBEDS),
1622
1675
  ...paginationFields
1623
1676
  });
1624
1677
  async function searchOpportunities(input) {
@@ -1632,7 +1685,7 @@ async function searchOpportunities(input) {
1632
1685
  }
1633
1686
  var getOpportunitySchema = z8.object({
1634
1687
  id: positiveId,
1635
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1688
+ embed: embedParam(RECORD_EMBEDS)
1636
1689
  });
1637
1690
  async function getOpportunity(input) {
1638
1691
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1644,7 +1697,7 @@ var getOpportunitiesSchema = z8.object({
1644
1697
  ids: z8.array(positiveId).min(1).max(50).describe(
1645
1698
  "Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1646
1699
  ),
1647
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1700
+ embed: embedParam(RECORD_EMBEDS)
1648
1701
  });
1649
1702
  async function getOpportunities(input) {
1650
1703
  return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
@@ -1698,7 +1751,7 @@ var updateOpportunitySchema = z8.object({
1698
1751
  "Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
1699
1752
  ),
1700
1753
  lostReasonId: positiveId.optional().describe(
1701
- "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lostreasons."
1754
+ "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lost_reasons."
1702
1755
  ),
1703
1756
  ownerId: positiveId.nullable().optional().describe(
1704
1757
  "Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
@@ -1743,9 +1796,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
1743
1796
 
1744
1797
  // src/tools/projects.ts
1745
1798
  import { z as z9 } from "zod";
1799
+ var searchProjectsSchema = z9.object({
1800
+ q: z9.string().optional().describe("Free-text search query"),
1801
+ embed: embedParam(RECORD_EMBEDS),
1802
+ ...paginationFields
1803
+ });
1804
+ async function searchProjects(input) {
1805
+ const path = input.q ? "/kases/search" : "/kases";
1806
+ return capsuleGetList(path, {
1807
+ q: input.q,
1808
+ embed: input.embed,
1809
+ page: input.page,
1810
+ perPage: input.perPage
1811
+ });
1812
+ }
1746
1813
  var listProjectsSchema = z9.object({
1747
1814
  status: z9.enum(["OPEN", "CLOSED"]).optional(),
1748
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1815
+ embed: embedParam(RECORD_EMBEDS),
1749
1816
  ...paginationFields
1750
1817
  });
1751
1818
  async function listProjects(input) {
@@ -1758,7 +1825,7 @@ async function listProjects(input) {
1758
1825
  }
1759
1826
  var getProjectSchema = z9.object({
1760
1827
  id: positiveId,
1761
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1828
+ embed: embedParam(RECORD_EMBEDS)
1762
1829
  });
1763
1830
  async function getProject(input) {
1764
1831
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -1770,10 +1837,10 @@ var getProjectsSchema = z9.object({
1770
1837
  ids: z9.array(positiveId).min(1).max(50).describe(
1771
1838
  "Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1772
1839
  ),
1773
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1840
+ embed: embedParam(RECORD_EMBEDS)
1774
1841
  });
1775
1842
  async function getProjects(input) {
1776
- return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
1843
+ return chunkedMultiGet("/kases", "projects", input.ids, { embed: input.embed });
1777
1844
  }
1778
1845
  var createProjectSchema = z9.object({
1779
1846
  name: z9.string().min(1),
@@ -1840,7 +1907,7 @@ async function updateProject(input) {
1840
1907
  let resolvedTeamId = teamId;
1841
1908
  let resolvedStageId = stageId;
1842
1909
  if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
1843
- const current = await readEntityRefs(`/kases/${id}`, "kase");
1910
+ const current = await readEntityRefs(`/kases/${id}`, "project");
1844
1911
  if (teamId === void 0) resolvedTeamId = current.teamId;
1845
1912
  if (stageId === void 0) resolvedStageId = current.stageId;
1846
1913
  }
@@ -1991,7 +2058,7 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
1991
2058
  import { z as z11 } from "zod";
1992
2059
  var listEntriesPagination = {
1993
2060
  ...paginationFields,
1994
- embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2061
+ embed: embedParam(ENTRY_EMBEDS)
1995
2062
  };
1996
2063
  var listPartyEntriesSchema = z11.object({
1997
2064
  partyId: positiveId,
@@ -2099,7 +2166,7 @@ async function listProjectEntries(input) {
2099
2166
  }
2100
2167
  var getEntrySchema = z11.object({
2101
2168
  id: positiveId,
2102
- embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2169
+ embed: embedParam(ENTRY_EMBEDS)
2103
2170
  });
2104
2171
  async function getEntry(input) {
2105
2172
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2212,16 +2279,16 @@ import { z as z14 } from "zod";
2212
2279
  var TAG_LIST_PATH = {
2213
2280
  parties: "/parties/tags",
2214
2281
  opportunities: "/opportunities/tags",
2215
- kases: "/kases/tags"
2282
+ projects: "/kases/tags"
2216
2283
  };
2217
2284
  var ENTITY_TO_WRAPPER = {
2218
2285
  parties: "party",
2219
2286
  opportunities: "opportunity",
2220
- kases: "kase"
2287
+ projects: "kase"
2221
2288
  };
2222
- var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2289
+ var TagEntity = z14.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
2223
2290
  var listTagsSchema = z14.object({
2224
- entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2291
+ entity: z14.enum(["parties", "opportunities", "projects"]).describe("The resource type to list tags for"),
2225
2292
  ...paginationFieldsNoDefaults
2226
2293
  });
2227
2294
  async function listTags(input) {
@@ -2241,7 +2308,7 @@ var addTagSchema = z14.object({
2241
2308
  async function addTag(input) {
2242
2309
  const { entity, entityId, tagName } = input;
2243
2310
  const wrapper = ENTITY_TO_WRAPPER[entity];
2244
- const result = await capsulePut(`/${entity}/${entityId}`, {
2311
+ const result = await capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
2245
2312
  [wrapper]: { tags: [{ name: tagName }] }
2246
2313
  });
2247
2314
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
@@ -2258,7 +2325,7 @@ async function removeTagById(input) {
2258
2325
  const { entity, entityId, tagId } = input;
2259
2326
  const wrapper = ENTITY_TO_WRAPPER[entity];
2260
2327
  const result = await idempotentWithResult(
2261
- () => capsulePut(`/${entity}/${entityId}`, {
2328
+ () => capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
2262
2329
  [wrapper]: { tags: [{ id: tagId, _delete: true }] }
2263
2330
  }),
2264
2331
  (result2) => ({
@@ -2293,7 +2360,7 @@ async function deleteTagDefinition(input) {
2293
2360
  throw new Error("delete_tag_definition requires confirm: true");
2294
2361
  }
2295
2362
  const result = await idempotent(
2296
- () => capsuleDelete(`/${entity}/tags/${tagId}`),
2363
+ () => capsuleDelete(`/${ENTITY_PATH[entity]}/tags/${tagId}`),
2297
2364
  () => ({ deleted: true, alreadyDeleted: false, entity, tagId }),
2298
2365
  () => ({ deleted: true, alreadyDeleted: true, entity, tagId })
2299
2366
  );
@@ -2347,7 +2414,7 @@ var FilterInputSchema = z16.object({
2347
2414
  conditions: z16.array(FilterConditionSchema).min(1).describe(
2348
2415
  "Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
2349
2416
  ),
2350
- embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2417
+ embed: embedParam(RECORD_EMBEDS),
2351
2418
  ...paginationFields
2352
2419
  });
2353
2420
  async function runFilter(entityPath, input) {
@@ -2438,7 +2505,7 @@ var listEmployeesSchema = z18.object({
2438
2505
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2439
2506
  ),
2440
2507
  ...paginationFields,
2441
- embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2508
+ embed: embedParam(RECORD_EMBEDS)
2442
2509
  });
2443
2510
  async function listEmployees(input) {
2444
2511
  return capsuleGetList(`/parties/${input.partyId}/people`, {
@@ -2481,19 +2548,22 @@ async function listDeletedProjects(input) {
2481
2548
 
2482
2549
  // src/tools/relationships.ts
2483
2550
  import { z as z19 } from "zod";
2484
- var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2551
+ var RelationshipEntity = z19.enum(["opportunities", "projects"]).describe("Which entity has the additional-party links.");
2485
2552
  var listAdditionalPartiesSchema = z19.object({
2486
2553
  entity: RelationshipEntity,
2487
2554
  entityId: positiveId.describe("ID of the opportunity or project."),
2488
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2555
+ embed: embedParam(RECORD_EMBEDS),
2489
2556
  ...paginationFields
2490
2557
  });
2491
2558
  async function listAdditionalParties(input) {
2492
- return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
2493
- embed: input.embed,
2494
- page: input.page,
2495
- perPage: input.perPage
2496
- });
2559
+ return capsuleGetList(
2560
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/parties`,
2561
+ {
2562
+ embed: input.embed,
2563
+ page: input.page,
2564
+ perPage: input.perPage
2565
+ }
2566
+ );
2497
2567
  }
2498
2568
  var addAdditionalPartySchema = z19.object({
2499
2569
  entity: RelationshipEntity,
@@ -2504,7 +2574,9 @@ var addAdditionalPartySchema = z19.object({
2504
2574
  });
2505
2575
  async function addAdditionalParty(input) {
2506
2576
  try {
2507
- await capsulePostNoContent(`/${input.entity}/${input.entityId}/parties/${input.partyId}`);
2577
+ await capsulePostNoContent(
2578
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`
2579
+ );
2508
2580
  return {
2509
2581
  linked: true,
2510
2582
  alreadyLinked: false,
@@ -2541,7 +2613,7 @@ async function removeAdditionalParty(input) {
2541
2613
  throw new Error("remove_additional_party requires confirm: true");
2542
2614
  }
2543
2615
  return idempotent(
2544
- () => capsuleDelete(`/${input.entity}/${input.entityId}/parties/${input.partyId}`),
2616
+ () => capsuleDelete(`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`),
2545
2617
  () => ({
2546
2618
  removed: true,
2547
2619
  alreadyRemoved: false,
@@ -2560,7 +2632,7 @@ async function removeAdditionalParty(input) {
2560
2632
  }
2561
2633
  var listAssociatedProjectsSchema = z19.object({
2562
2634
  opportunityId: positiveId,
2563
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2635
+ embed: embedParam(RECORD_EMBEDS),
2564
2636
  ...paginationFields
2565
2637
  });
2566
2638
  async function listAssociatedProjects(input) {
@@ -2573,49 +2645,49 @@ async function listAssociatedProjects(input) {
2573
2645
 
2574
2646
  // src/tools/custom-fields.ts
2575
2647
  import { z as z20 } from "zod";
2576
- var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2648
+ var CustomFieldEntity = z20.enum(["parties", "opportunities", "projects"]).describe("Which entity type's custom field schema to inspect.");
2577
2649
  var listCustomFieldsSchema = z20.object({
2578
2650
  entity: CustomFieldEntity
2579
2651
  });
2580
2652
  async function listCustomFields(input) {
2581
2653
  const { data } = await capsuleGetCached(
2582
- `/${input.entity}/fields/definitions`
2654
+ `/${ENTITY_PATH[input.entity]}/fields/definitions`
2583
2655
  );
2584
2656
  return data;
2585
2657
  }
2586
2658
  var getCustomFieldSchema = z20.object({
2587
2659
  entity: CustomFieldEntity,
2588
- fieldId: positiveId.describe("Custom field definition id.")
2660
+ id: positiveId.describe("Custom field definition id.")
2589
2661
  });
2590
2662
  async function getCustomField(input) {
2591
2663
  const { data } = await capsuleGetCached(
2592
- `/${input.entity}/fields/definitions/${input.fieldId}`
2664
+ `/${input.entity}/fields/definitions/${input.id}`
2593
2665
  );
2594
2666
  return data;
2595
2667
  }
2596
2668
 
2597
2669
  // src/tools/tracks.ts
2598
2670
  import { z as z21 } from "zod";
2599
- var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2671
+ var TrackEntity = z21.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
2600
2672
  var listEntityTracksSchema = z21.object({
2601
2673
  entity: TrackEntity,
2602
2674
  entityId: positiveId
2603
2675
  });
2604
2676
  async function listEntityTracks(input) {
2605
2677
  const { data } = await capsuleGet(
2606
- `/${input.entity}/${input.entityId}/tracks`
2678
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/tracks`
2607
2679
  );
2608
2680
  return data;
2609
2681
  }
2610
- var showTrackSchema = z21.object({
2611
- trackId: positiveId
2682
+ var getTrackSchema = z21.object({
2683
+ id: positiveId
2612
2684
  });
2613
- async function showTrack(input) {
2614
- const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2685
+ async function getTrack(input) {
2686
+ const { data } = await capsuleGet(`/tracks/${input.id}`);
2615
2687
  return data;
2616
2688
  }
2617
2689
  var applyTrackSchema = z21.object({
2618
- entity: z21.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2690
+ entity: z21.enum(["opportunities", "projects"]).describe("Which entity to apply the track to."),
2619
2691
  entityId: positiveId,
2620
2692
  trackDefinitionId: positiveId.describe(
2621
2693
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
@@ -2634,7 +2706,7 @@ async function applyTrack(input) {
2634
2706
  return capsulePost("/tracks", { track });
2635
2707
  }
2636
2708
  var updateTrackSchema = z21.object({
2637
- trackId: positiveId,
2709
+ id: positiveId,
2638
2710
  fields: z21.record(z21.string(), z21.unknown()).describe(
2639
2711
  "Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
2640
2712
  )
@@ -2643,12 +2715,12 @@ async function updateTrack(input) {
2643
2715
  if (Object.keys(input.fields).length === 0) {
2644
2716
  throw new Error("update_track: provide at least one field in `fields`");
2645
2717
  }
2646
- return capsulePut(`/tracks/${input.trackId}`, {
2718
+ return capsulePut(`/tracks/${input.id}`, {
2647
2719
  track: input.fields
2648
2720
  });
2649
2721
  }
2650
2722
  var removeTrackSchema = z21.object({
2651
- trackId: positiveId,
2723
+ id: positiveId,
2652
2724
  confirm: confirmFlag().describe(
2653
2725
  "Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
2654
2726
  )
@@ -2658,9 +2730,9 @@ async function removeTrack(input) {
2658
2730
  throw new Error("remove_track requires confirm: true");
2659
2731
  }
2660
2732
  return idempotent(
2661
- () => capsuleDelete(`/tracks/${input.trackId}`),
2662
- () => ({ removed: true, alreadyRemoved: false, trackId: input.trackId }),
2663
- () => ({ removed: true, alreadyRemoved: true, trackId: input.trackId })
2733
+ () => capsuleDelete(`/tracks/${input.id}`),
2734
+ () => ({ removed: true, alreadyRemoved: false, id: input.id }),
2735
+ () => ({ removed: true, alreadyRemoved: true, id: input.id })
2664
2736
  );
2665
2737
  }
2666
2738
 
@@ -2747,28 +2819,31 @@ async function uploadAttachment(input) {
2747
2819
 
2748
2820
  // src/tools/saved-filters.ts
2749
2821
  import { z as z23 } from "zod";
2750
- var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
2751
- "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
2752
- );
2822
+ var EntitySchema = z23.enum(["parties", "opportunities", "projects"]).describe("Which entity type the filter operates over.");
2753
2823
  var listSavedFiltersSchema = z23.object({
2754
2824
  entity: EntitySchema
2755
2825
  });
2756
2826
  async function listSavedFilters(input) {
2757
- const { data } = await capsuleGetCached(`/${input.entity}/filters`);
2827
+ const { data } = await capsuleGetCached(
2828
+ `/${ENTITY_PATH[input.entity]}/filters`
2829
+ );
2758
2830
  return data;
2759
2831
  }
2760
2832
  var runSavedFilterSchema = z23.object({
2761
2833
  entity: EntitySchema,
2762
2834
  id: positiveId.describe("The saved filter id (from list_saved_filters)."),
2763
- embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2835
+ embed: embedParam(RECORD_EMBEDS),
2764
2836
  ...paginationFields
2765
2837
  });
2766
2838
  async function runSavedFilter(input) {
2767
- return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
2768
- page: input.page,
2769
- perPage: input.perPage,
2770
- embed: input.embed
2771
- });
2839
+ return capsuleGetList(
2840
+ `/${ENTITY_PATH[input.entity]}/filters/${input.id}/results`,
2841
+ {
2842
+ page: input.page,
2843
+ perPage: input.perPage,
2844
+ embed: input.embed
2845
+ }
2846
+ );
2772
2847
  }
2773
2848
 
2774
2849
  // src/server.ts
@@ -2779,7 +2854,7 @@ function createCapsuleMcpServer(opts) {
2779
2854
  const server2 = new McpServer(
2780
2855
  {
2781
2856
  name: "capsulemcp",
2782
- version: "1.8.1",
2857
+ version: "2.0.0",
2783
2858
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2784
2859
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2785
2860
  icons: ICONS
@@ -2896,7 +2971,7 @@ function createCapsuleMcpServer(opts) {
2896
2971
  registerTool(
2897
2972
  server2,
2898
2973
  "delete_party",
2899
- "DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects (kases). Deleting an organisation does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via show_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
2974
+ "DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects. Deleting an organisation does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via get_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
2900
2975
  deletePartySchema,
2901
2976
  deleteParty
2902
2977
  );
@@ -2995,7 +3070,7 @@ function createCapsuleMcpServer(opts) {
2995
3070
  registerTool(
2996
3071
  server2,
2997
3072
  "list_additional_parties",
2998
- "List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'kases' (Capsule's term for projects).",
3073
+ "List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'projects'.",
2999
3074
  listAdditionalPartiesSchema,
3000
3075
  listAdditionalParties
3001
3076
  );
@@ -3036,10 +3111,17 @@ function createCapsuleMcpServer(opts) {
3036
3111
  deleteOpportunity
3037
3112
  );
3038
3113
  }
3114
+ registerTool(
3115
+ server2,
3116
+ "search_projects",
3117
+ "Free-text search projects in Capsule CRM (matches name and description). Returns results in Capsule's default order (no sort parameter is supported here). Omit `q` to list all projects. For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3118
+ searchProjectsSchema,
3119
+ searchProjects
3120
+ );
3039
3121
  registerTool(
3040
3122
  server2,
3041
3123
  "list_projects",
3042
- "List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3124
+ "List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For free-text matching use search_projects; for structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3043
3125
  listProjectsSchema,
3044
3126
  listProjects
3045
3127
  );
@@ -3067,7 +3149,7 @@ function createCapsuleMcpServer(opts) {
3067
3149
  registerTool(
3068
3150
  server2,
3069
3151
  "list_deleted_projects",
3070
- "Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedKases` key for records the integration user can't read fully.",
3152
+ "Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedProjects` key for records the integration user can't read fully.",
3071
3153
  listDeletedProjectsSchema,
3072
3154
  listDeletedProjects
3073
3155
  );
@@ -3382,14 +3464,14 @@ function createCapsuleMcpServer(opts) {
3382
3464
  );
3383
3465
  registerTool(
3384
3466
  server2,
3385
- "list_lostreasons",
3467
+ "list_lost_reasons",
3386
3468
  "List all configured opportunity-loss reasons (e.g. 'Poor Qualification', 'Lost to competitor', 'Price too high'). Returns each reason's id and name; the set is account-configured rather than a fixed enum, so call this to discover valid ids before referencing a lostReason in update_opportunity when closing a deal as lost. Useful for analysing closed-lost opportunities by reason.",
3387
3469
  listLostReasonsSchema,
3388
3470
  listLostReasons
3389
3471
  );
3390
3472
  registerTool(
3391
3473
  server2,
3392
- "list_activitytypes",
3474
+ "list_activity_types",
3393
3475
  "List all configured activity types (e.g. Call, Meeting, Email). These are the categories used when logging timeline entries via add_note. Returns each type's id and name. The set is account-configured rather than a fixed enum, so call this to discover valid values before referencing an activityType in entry creation.",
3394
3476
  listActivityTypesSchema,
3395
3477
  listActivityTypes
@@ -3417,10 +3499,10 @@ function createCapsuleMcpServer(opts) {
3417
3499
  );
3418
3500
  registerTool(
3419
3501
  server2,
3420
- "show_track",
3502
+ "get_track",
3421
3503
  "Fetch a single track instance by id. Returns the minimal Capsule projection: id, description, trackDateOn, direction, and the array of tasks attached to the track. Capsule's GET /tracks/{id} does NOT include a trackDefinition link, an entity reference, or a completion field \u2014 to find the entity a track is applied to, use list_entity_tracks (which lists track instances by their parent entity); to check completion, the track-tasks' own statuses are the proxy.",
3422
- showTrackSchema,
3423
- showTrack
3504
+ getTrackSchema,
3505
+ getTrack
3424
3506
  );
3425
3507
  registerTool(
3426
3508
  server2,
@@ -3453,7 +3535,7 @@ function createCapsuleMcpServer(opts) {
3453
3535
  registerTool(
3454
3536
  server2,
3455
3537
  "list_tags",
3456
- "List all tags available for a given entity type (parties, opportunities, or kases). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
3538
+ "List all tags available for a given entity type (parties, opportunities, or projects). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
3457
3539
  listTagsSchema,
3458
3540
  listTags
3459
3541
  );
@@ -3475,7 +3557,7 @@ function createCapsuleMcpServer(opts) {
3475
3557
  registerTool(
3476
3558
  server2,
3477
3559
  "delete_tag_definition",
3478
- "DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / kases). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
3560
+ "DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / projects). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
3479
3561
  deleteTagDefinitionSchema,
3480
3562
  deleteTagDefinition
3481
3563
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capsulemcp",
3
- "version": "1.8.1",
3
+ "version": "2.0.0",
4
4
  "description": "Model Context Protocol server for Capsule CRM. Lets Claude (Desktop, Code, or web Projects via Custom Connector) read and write your CRM in plain English. Covers contacts, opportunities, projects, tasks, timeline activity, structured filters, saved filters with sort, workflow tracks, file attachments, audit, and batch fetches.",
5
5
  "keywords": [
6
6
  "mcp",