capsulemcp 2.0.0 → 2.0.1

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
@@ -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@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.
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.1"` in `args`. If you're tracking a fork or an unreleased branch, use the GitHub-ref form instead: `"github:soil-dev/capsulemcp#v2.0.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.
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) | `search_projects`, `list_projects`, `filter_projects`, `get_project`, `get_projects`, `list_project_entries` | `create_project`, `update_project`, `delete_project` |
59
+ | Projects | `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` |
package/dist/http.js CHANGED
@@ -1948,7 +1948,7 @@ var PartyWriteBaseSchema = {
1948
1948
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
1949
1949
  ),
1950
1950
  ownerId: positiveId.nullable().optional().describe(
1951
- "Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on /parties has the same asymmetric owner/team semantic documented in NOTES-ON-CAPSULE-API.md \xA727 for /kases \u2014 setting `owner` while omitting `team` is plausibly clearing-prone. When you supply `ownerId` and omit `teamId`, this connector reads the party's current team and includes it in the PUT body to preserve it across the owner change. Supply `teamId` explicitly to change it."
1951
+ "Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on parties has the same asymmetric owner/team semantic documented in NOTES-ON-CAPSULE-API.md \xA727 for project updates \u2014 setting `owner` while omitting `team` is plausibly clearing-prone. When you supply `ownerId` and omit `teamId`, this connector reads the party's current team and includes it in the PUT body to preserve it across the owner change. Supply `teamId` explicitly to change it."
1952
1952
  ),
1953
1953
  teamId: positiveId.nullable().optional().describe(
1954
1954
  "Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
@@ -2222,7 +2222,7 @@ var createOpportunitySchema = z9.object({
2222
2222
  "Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
2223
2223
  ),
2224
2224
  fields: z9.array(CustomFieldWriteSchema).optional().describe(
2225
- fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
2225
+ fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on party and project creation \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
2226
2226
  )
2227
2227
  });
2228
2228
  async function createOpportunity(input) {
@@ -2257,7 +2257,7 @@ var updateOpportunitySchema = z9.object({
2257
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."
2258
2258
  ),
2259
2259
  ownerId: positiveId.nullable().optional().describe(
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)."
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 project updates). 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)."
2261
2261
  ),
2262
2262
  teamId: positiveId.nullable().optional().describe(
2263
2263
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
@@ -2361,7 +2361,7 @@ var createProjectSchema = z10.object({
2361
2361
  ),
2362
2362
  expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2363
2363
  fields: z10.array(CustomFieldWriteSchema).optional().describe(
2364
- fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
2364
+ fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's project create endpoint accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
2365
2365
  )
2366
2366
  });
2367
2367
  async function createProject(input) {
@@ -2393,7 +2393,7 @@ var updateProjectSchema = z10.object({
2393
2393
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_project { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. A project must always have at least one of {owner, team} set \u2014 `teamId: null` on a project whose owner is already null returns 422 'owner or team is required'."
2394
2394
  ),
2395
2395
  stageId: positiveId.nullable().optional().describe(
2396
- "Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
2396
+ "Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on project update and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
2397
2397
  ),
2398
2398
  expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2399
2399
  fields: z10.array(CustomFieldWriteSchema).optional().describe(
@@ -2431,7 +2431,7 @@ var { schema: batchUpdateProjectSchema, handler: batchUpdateProject } = defineBa
2431
2431
  var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
2432
2432
  toolName: "delete_project",
2433
2433
  pathPrefix: "/kases",
2434
- confirmHint: "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
2434
+ confirmHint: "Must be set to true. Permanently deletes the project. Consider update_project status='CLOSED' instead. Irreversible."
2435
2435
  });
2436
2436
 
2437
2437
  // src/tools/tasks.ts
@@ -2519,7 +2519,7 @@ var updateTaskSchema = z11.object({
2519
2519
  "Re-link the task to an opportunity by id, or `null` to orphan it. Mutually exclusive with `partyId` / `projectId` \u2014 see `partyId` for the XOR semantic."
2520
2520
  ),
2521
2521
  projectId: positiveId.nullable().optional().describe(
2522
- "Re-link the task to a project (kase) by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
2522
+ "Re-link the task to a project by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
2523
2523
  )
2524
2524
  });
2525
2525
  async function updateTask(input) {
@@ -2567,7 +2567,7 @@ var listPartyEntriesSchema = z12.object({
2567
2567
  partyId: positiveId,
2568
2568
  ...listEntriesPagination,
2569
2569
  includeLinkedPersons: z12.boolean().optional().describe(
2570
- "When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422 'entry must be linked to either a party, opportunity or kase'). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
2570
+ "When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party, opportunity, or project row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
2571
2571
  )
2572
2572
  });
2573
2573
  var PER_PARTY_FETCH_CAP = 100;
@@ -2803,7 +2803,7 @@ async function listTags(input) {
2803
2803
  }
2804
2804
  var addTagSchema = z15.object({
2805
2805
  entity: TagEntity,
2806
- entityId: positiveId.describe("The party/opportunity/kase id."),
2806
+ entityId: positiveId.describe("The party/opportunity/project id."),
2807
2807
  tagName: z15.string().min(1).describe(
2808
2808
  "Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
2809
2809
  )
@@ -2819,7 +2819,7 @@ async function addTag(input) {
2819
2819
  }
2820
2820
  var removeTagByIdSchema = z15.object({
2821
2821
  entity: TagEntity,
2822
- entityId: positiveId.describe("The party/opportunity/kase id."),
2822
+ entityId: positiveId.describe("The party/opportunity/project id."),
2823
2823
  tagId: positiveId.describe(
2824
2824
  "The tag's id. Read via get_party / get_opportunity / get_project with embed='tags' \u2014 each tag entry in the response has an `id` field. list_tags returns the same ids for the same tags, so either source works; reading via embed first is the safer pattern because it confirms the tag is actually attached to this entity before you try to remove it (otherwise Capsule returns 422 'tag not found to delete'). Removing detaches the tag from this entity only; the tag definition itself persists in the tenant for other entities that share it."
2825
2825
  )
@@ -3164,7 +3164,7 @@ var getCustomFieldSchema = z21.object({
3164
3164
  });
3165
3165
  async function getCustomField(input) {
3166
3166
  const { data } = await capsuleGetCached(
3167
- `/${input.entity}/fields/definitions/${input.id}`
3167
+ `/${ENTITY_PATH[input.entity]}/fields/definitions/${input.id}`
3168
3168
  );
3169
3169
  return data;
3170
3170
  }
@@ -3357,7 +3357,7 @@ function createCapsuleMcpServer(opts) {
3357
3357
  const server = new McpServer(
3358
3358
  {
3359
3359
  name: "capsulemcp",
3360
- version: "2.0.0",
3360
+ version: "2.0.1",
3361
3361
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
3362
3362
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
3363
3363
  icons: ICONS
@@ -3417,7 +3417,7 @@ function createCapsuleMcpServer(opts) {
3417
3417
  registerTool(
3418
3418
  server,
3419
3419
  "list_party_projects",
3420
- "List projects (cases) linked to a given party. Returns the same record shape as get_project, filtered to one party \u2014 use this to answer 'what cases is X involved in?' without enumerating all projects. Accepts optional embed (e.g. 'tags,fields'). For the opportunity-side analogue, use list_party_opportunities.",
3420
+ "List projects linked to a given party. Returns the same record shape as get_project, filtered to one party \u2014 use this to answer 'what projects is X involved in?' without enumerating all projects. Accepts optional embed (e.g. 'tags,fields'). For the opportunity-side analogue, use list_party_opportunities.",
3421
3421
  listPartyProjectsSchema,
3422
3422
  listPartyProjects
3423
3423
  );
@@ -3431,7 +3431,7 @@ function createCapsuleMcpServer(opts) {
3431
3431
  registerTool(
3432
3432
  server,
3433
3433
  "list_custom_fields",
3434
- "List custom field DEFINITIONS for an entity type (parties, opportunities, or projects/kases). Returns the schema \u2014 name, type, options for list-type fields, etc. \u2014 NOT the values on any specific record. To read values on a record, use get_party / get_opportunity / get_project with embed=fields.",
3434
+ "List custom field DEFINITIONS for an entity type (parties, opportunities, or projects). Returns the schema \u2014 name, type, options for list-type fields, etc. \u2014 NOT the values on any specific record. To read values on a record, use get_party / get_opportunity / get_project with embed=fields.",
3435
3435
  listCustomFieldsSchema,
3436
3436
  listCustomFields
3437
3437
  );
@@ -3580,7 +3580,7 @@ function createCapsuleMcpServer(opts) {
3580
3580
  registerTool(
3581
3581
  server,
3582
3582
  "list_associated_projects",
3583
- "List projects (cases) associated with a given opportunity. Returns the same record shape as list_projects, filtered to one opportunity. The inverse direction (project \u2192 opportunity) is on each project's `opportunity` field directly, so this tool is only needed for opportunity \u2192 projects discovery \u2014 use list_party_projects for party \u2192 projects.",
3583
+ "List projects associated with a given opportunity. Returns the same record shape as list_projects, filtered to one opportunity. The inverse direction (project \u2192 opportunity) is on each project's `opportunity` field directly, so this tool is only needed for opportunity \u2192 projects discovery \u2014 use list_party_projects for party \u2192 projects.",
3584
3584
  listAssociatedProjectsSchema,
3585
3585
  listAssociatedProjects
3586
3586
  );
@@ -3624,28 +3624,28 @@ function createCapsuleMcpServer(opts) {
3624
3624
  registerTool(
3625
3625
  server,
3626
3626
  "list_projects",
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.",
3627
+ "List projects 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.",
3628
3628
  listProjectsSchema,
3629
3629
  listProjects
3630
3630
  );
3631
3631
  registerTool(
3632
3632
  server,
3633
3633
  "filter_projects",
3634
- "Filter projects (cases) by structured conditions (date ranges, status, tags, owner). Use this \u2014 not list_projects \u2014 for questions like 'most recent project', 'projects opened this month'. Capsule's API does not support ad-hoc sort, but for 'most recent X' you can filter by a date field and pick the highest-id row \u2014 Capsule IDs are monotonic, so newest id = newest record.",
3634
+ "Filter projects by structured conditions (date ranges, status, tags, owner). Use this \u2014 not list_projects \u2014 for questions like 'most recent project', 'projects opened this month'. Capsule's API does not support ad-hoc sort, but for 'most recent X' you can filter by a date field and pick the highest-id row \u2014 Capsule IDs are monotonic, so newest id = newest record.",
3635
3635
  filterProjectsSchema,
3636
3636
  filterProjects
3637
3637
  );
3638
3638
  registerTool(
3639
3639
  server,
3640
3640
  "get_project",
3641
- "Fetch a single project (Capsule's term: 'case') by its numeric id. Returns the full record including name, description, status (OPEN/CLOSED), owner, stage, board, opportunityId (if linked), and timestamps. Use embed='tags,fields' to include attached tags and custom field values in one round-trip. For batch fetches of up to 50 projects at once, use get_projects instead. For the project's timeline (notes, captured emails, completed-task records) use list_project_entries.",
3641
+ "Fetch a single project by its numeric id. Returns the full record including name, description, status (OPEN/CLOSED), owner, stage, board, opportunityId (if linked), and timestamps. Use embed='tags,fields' to include attached tags and custom field values in one round-trip. For batch fetches of up to 50 projects at once, use get_projects instead. For the project's timeline (notes, captured emails, completed-task records) use list_project_entries.",
3642
3642
  getProjectSchema,
3643
3643
  getProject
3644
3644
  );
3645
3645
  registerTool(
3646
3646
  server,
3647
3647
  "get_projects",
3648
- "Batch-fetch up to 50 projects (cases) by ID. For 1\u201310 ids this is a single Capsule round trip; for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged.",
3648
+ "Batch-fetch up to 50 projects by ID. For 1\u201310 ids this is a single Capsule round trip; for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged.",
3649
3649
  getProjectsSchema,
3650
3650
  getProjects
3651
3651
  );
@@ -3660,7 +3660,7 @@ function createCapsuleMcpServer(opts) {
3660
3660
  registerTool(
3661
3661
  server,
3662
3662
  "create_project",
3663
- "Create a new project (case) in Capsule CRM linked to a party. Requires partyId and name; description, status, owner, and starting board/stage are optional. To pin a project to a specific board+stage on creation, pass stageId (which uniquely identifies a stage within a board). Discover valid ids via list_boards + list_stages. Returns the created project including its assigned id.",
3663
+ "Create a new project in Capsule CRM linked to a party. Requires partyId and name; description, status, owner, and starting board/stage are optional. To pin a project to a specific board+stage on creation, pass stageId (which uniquely identifies a stage within a board). Discover valid ids via list_boards + list_stages. Returns the created project including its assigned id.",
3664
3664
  createProjectSchema,
3665
3665
  createProject
3666
3666
  );
@@ -3681,7 +3681,7 @@ function createCapsuleMcpServer(opts) {
3681
3681
  registerTool(
3682
3682
  server,
3683
3683
  "delete_project",
3684
- "DESTRUCTIVE & IRREVERSIBLE: permanently delete a project (case). Prefer update_project with status='CLOSED' to close a project while preserving history. Requires confirm=true. Always read the project first with get_project 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 project was already gone.",
3684
+ "DESTRUCTIVE & IRREVERSIBLE: permanently delete a project. Prefer update_project with status='CLOSED' to close a project while preserving history. Requires confirm=true. Always read the project first with get_project 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 project was already gone.",
3685
3685
  deleteProjectSchema,
3686
3686
  deleteProject
3687
3687
  );
@@ -3796,7 +3796,7 @@ function createCapsuleMcpServer(opts) {
3796
3796
  registerTool(
3797
3797
  server,
3798
3798
  "list_project_entries",
3799
- "List timeline entries (notes, captured emails, completed-task records) for a project (case). Returns entries newest-first. Each entry has a type ('note', 'email', 'task'), free-text content, and timestamps. Use this to answer 'what's the latest on case X?' For party or opportunity timelines, use list_party_entries or list_opportunity_entries respectively.",
3799
+ "List timeline entries (notes, captured emails, completed-task records) for a project. Returns entries newest-first. Each entry has a type ('note', 'email', 'task'), free-text content, and timestamps. Use this to answer 'what's the latest on project X?' For party or opportunity timelines, use list_party_entries or list_opportunity_entries respectively.",
3800
3800
  listProjectEntriesSchema,
3801
3801
  listProjectEntries
3802
3802
  );
@@ -3947,14 +3947,14 @@ function createCapsuleMcpServer(opts) {
3947
3947
  registerTool(
3948
3948
  server,
3949
3949
  "list_boards",
3950
- "List all project (case) boards defined in Capsule. A board is a grouping of stages that projects flow through \u2014 the project equivalent of an opportunity pipeline. Returns each board's id, name, and stages. Use this to discover boardId when creating a project, then pick a starting stage via list_stages. Like pipelines, boards are stable per account.",
3950
+ "List all project boards defined in Capsule. A board is a grouping of stages that projects flow through \u2014 the project equivalent of an opportunity pipeline. Returns each board's id, name, and stages. Use this to discover boardId when creating a project, then pick a starting stage via list_stages. Like pipelines, boards are stable per account.",
3951
3951
  listBoardsSchema,
3952
3952
  listBoards
3953
3953
  );
3954
3954
  registerTool(
3955
3955
  server,
3956
3956
  "list_stages",
3957
- "List project (case) stages. Without arguments returns every stage across every board (each entry carries a `.board` reference so you can tell them apart). Pass `boardId` to scope the result to one specific board's stages. Use this to discover the numeric `stage.id` that `create_project` / `update_project` consume \u2014 stage names alone won't do, Capsule resolves by id. For opportunity (deal) stages, use `list_pipelines` instead \u2014 opportunities don't have stages in the project sense.",
3957
+ "List project stages. Without arguments returns every stage across every board (each entry carries a `.board` reference so you can tell them apart). Pass `boardId` to scope the result to one specific board's stages. Use this to discover the numeric `stage.id` that `create_project` / `update_project` consume \u2014 stage names alone won't do, Capsule resolves by id. For opportunity (deal) stages, use `list_pipelines` instead \u2014 opportunities don't have stages in the project sense.",
3958
3958
  listStagesSchema,
3959
3959
  listStages
3960
3960
  );
@@ -4046,14 +4046,14 @@ function createCapsuleMcpServer(opts) {
4046
4046
  registerTool(
4047
4047
  server,
4048
4048
  "add_tag",
4049
- "Attach a tag to a party, opportunity, or project (kase) by NAME. Capsule resolves to an existing tag in the tenant or creates a fresh one with this name. Matching is case-insensitive \u2014 'VIP' and 'vip' attach the same tag, preserving the canonical casing from whichever variant was created first. To avoid creating a genuinely-distinct near-duplicate (e.g. 'VIP' vs 'V.I.P.'), call list_tags first and reuse the exact name. Idempotent \u2014 re-attaching an already-attached tag is harmless. To DETACH a tag, use remove_tag_by_id with the tag's id (read via get_party/get_opportunity/get_project with embed='tags').",
4049
+ "Attach a tag to a party, opportunity, or project by NAME. Capsule resolves to an existing tag in the tenant or creates a fresh one with this name. Matching is case-insensitive \u2014 'VIP' and 'vip' attach the same tag, preserving the canonical casing from whichever variant was created first. To avoid creating a genuinely-distinct near-duplicate (e.g. 'VIP' vs 'V.I.P.'), call list_tags first and reuse the exact name. Idempotent \u2014 re-attaching an already-attached tag is harmless. To DETACH a tag, use remove_tag_by_id with the tag's id (read via get_party/get_opportunity/get_project with embed='tags').",
4050
4050
  addTagSchema,
4051
4051
  addTag
4052
4052
  );
4053
4053
  registerTool(
4054
4054
  server,
4055
4055
  "remove_tag_by_id",
4056
- "Detach a tag from a party, opportunity, or project (kase). Atomic \u2014 one PUT to Capsule. Reversible \u2014 no `confirm: true` gate (re-attach with add_tag using the same tag name). The `tagId` parameter is the tag's id, readable via get_party/get_opportunity/get_project with embed='tags' (list_tags returns the same ids and also works, but reading via embed first confirms the tag is actually attached to this entity). The tag definition itself remains in the tenant for other entities that still share it. Idempotent on retry: response is `{removed: true, alreadyRemoved: false, entity, entityId, tagId, ...<updated entity>}` on a fresh detach or `{removed: true, alreadyRemoved: true, entity, entityId, tagId}` if the tag was already detached (Capsule's 422 'tag not found to delete' is caught and converted).",
4056
+ "Detach a tag from a party, opportunity, or project. Atomic \u2014 one PUT to Capsule. Reversible \u2014 no `confirm: true` gate (re-attach with add_tag using the same tag name). The `tagId` parameter is the tag's id, readable via get_party/get_opportunity/get_project with embed='tags' (list_tags returns the same ids and also works, but reading via embed first confirms the tag is actually attached to this entity). The tag definition itself remains in the tenant for other entities that still share it. Idempotent on retry: response is `{removed: true, alreadyRemoved: false, entity, entityId, tagId, ...<updated entity>}` on a fresh detach or `{removed: true, alreadyRemoved: true, entity, entityId, tagId}` if the tag was already detached (Capsule's 422 'tag not found to delete' is caught and converted).",
4057
4057
  removeTagByIdSchema,
4058
4058
  removeTagById
4059
4059
  );
package/dist/index.js CHANGED
@@ -1445,7 +1445,7 @@ var PartyWriteBaseSchema = {
1445
1445
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
1446
1446
  ),
1447
1447
  ownerId: positiveId.nullable().optional().describe(
1448
- "Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on /parties has the same asymmetric owner/team semantic documented in NOTES-ON-CAPSULE-API.md \xA727 for /kases \u2014 setting `owner` while omitting `team` is plausibly clearing-prone. When you supply `ownerId` and omit `teamId`, this connector reads the party's current team and includes it in the PUT body to preserve it across the owner change. Supply `teamId` explicitly to change it."
1448
+ "Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on parties has the same asymmetric owner/team semantic documented in NOTES-ON-CAPSULE-API.md \xA727 for project updates \u2014 setting `owner` while omitting `team` is plausibly clearing-prone. When you supply `ownerId` and omit `teamId`, this connector reads the party's current team and includes it in the PUT body to preserve it across the owner change. Supply `teamId` explicitly to change it."
1449
1449
  ),
1450
1450
  teamId: positiveId.nullable().optional().describe(
1451
1451
  "Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
@@ -1719,7 +1719,7 @@ var createOpportunitySchema = z8.object({
1719
1719
  "Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
1720
1720
  ),
1721
1721
  fields: z8.array(CustomFieldWriteSchema).optional().describe(
1722
- fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
1722
+ fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on party and project creation \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
1723
1723
  )
1724
1724
  });
1725
1725
  async function createOpportunity(input) {
@@ -1754,7 +1754,7 @@ var updateOpportunitySchema = z8.object({
1754
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."
1755
1755
  ),
1756
1756
  ownerId: positiveId.nullable().optional().describe(
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)."
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 project updates). 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)."
1758
1758
  ),
1759
1759
  teamId: positiveId.nullable().optional().describe(
1760
1760
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
@@ -1858,7 +1858,7 @@ var createProjectSchema = z9.object({
1858
1858
  ),
1859
1859
  expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1860
1860
  fields: z9.array(CustomFieldWriteSchema).optional().describe(
1861
- fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
1861
+ fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's project create endpoint accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
1862
1862
  )
1863
1863
  });
1864
1864
  async function createProject(input) {
@@ -1890,7 +1890,7 @@ var updateProjectSchema = z9.object({
1890
1890
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_project { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. A project must always have at least one of {owner, team} set \u2014 `teamId: null` on a project whose owner is already null returns 422 'owner or team is required'."
1891
1891
  ),
1892
1892
  stageId: positiveId.nullable().optional().describe(
1893
- "Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
1893
+ "Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on project update and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
1894
1894
  ),
1895
1895
  expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1896
1896
  fields: z9.array(CustomFieldWriteSchema).optional().describe(
@@ -1928,7 +1928,7 @@ var { schema: batchUpdateProjectSchema, handler: batchUpdateProject } = defineBa
1928
1928
  var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
1929
1929
  toolName: "delete_project",
1930
1930
  pathPrefix: "/kases",
1931
- confirmHint: "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
1931
+ confirmHint: "Must be set to true. Permanently deletes the project. Consider update_project status='CLOSED' instead. Irreversible."
1932
1932
  });
1933
1933
 
1934
1934
  // src/tools/tasks.ts
@@ -2016,7 +2016,7 @@ var updateTaskSchema = z10.object({
2016
2016
  "Re-link the task to an opportunity by id, or `null` to orphan it. Mutually exclusive with `partyId` / `projectId` \u2014 see `partyId` for the XOR semantic."
2017
2017
  ),
2018
2018
  projectId: positiveId.nullable().optional().describe(
2019
- "Re-link the task to a project (kase) by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
2019
+ "Re-link the task to a project by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
2020
2020
  )
2021
2021
  });
2022
2022
  async function updateTask(input) {
@@ -2064,7 +2064,7 @@ var listPartyEntriesSchema = z11.object({
2064
2064
  partyId: positiveId,
2065
2065
  ...listEntriesPagination,
2066
2066
  includeLinkedPersons: z11.boolean().optional().describe(
2067
- "When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422 'entry must be linked to either a party, opportunity or kase'). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
2067
+ "When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party, opportunity, or project row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
2068
2068
  )
2069
2069
  });
2070
2070
  var PER_PARTY_FETCH_CAP = 100;
@@ -2300,7 +2300,7 @@ async function listTags(input) {
2300
2300
  }
2301
2301
  var addTagSchema = z14.object({
2302
2302
  entity: TagEntity,
2303
- entityId: positiveId.describe("The party/opportunity/kase id."),
2303
+ entityId: positiveId.describe("The party/opportunity/project id."),
2304
2304
  tagName: z14.string().min(1).describe(
2305
2305
  "Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
2306
2306
  )
@@ -2316,7 +2316,7 @@ async function addTag(input) {
2316
2316
  }
2317
2317
  var removeTagByIdSchema = z14.object({
2318
2318
  entity: TagEntity,
2319
- entityId: positiveId.describe("The party/opportunity/kase id."),
2319
+ entityId: positiveId.describe("The party/opportunity/project id."),
2320
2320
  tagId: positiveId.describe(
2321
2321
  "The tag's id. Read via get_party / get_opportunity / get_project with embed='tags' \u2014 each tag entry in the response has an `id` field. list_tags returns the same ids for the same tags, so either source works; reading via embed first is the safer pattern because it confirms the tag is actually attached to this entity before you try to remove it (otherwise Capsule returns 422 'tag not found to delete'). Removing detaches the tag from this entity only; the tag definition itself persists in the tenant for other entities that share it."
2322
2322
  )
@@ -2661,7 +2661,7 @@ var getCustomFieldSchema = z20.object({
2661
2661
  });
2662
2662
  async function getCustomField(input) {
2663
2663
  const { data } = await capsuleGetCached(
2664
- `/${input.entity}/fields/definitions/${input.id}`
2664
+ `/${ENTITY_PATH[input.entity]}/fields/definitions/${input.id}`
2665
2665
  );
2666
2666
  return data;
2667
2667
  }
@@ -2854,7 +2854,7 @@ function createCapsuleMcpServer(opts) {
2854
2854
  const server2 = new McpServer(
2855
2855
  {
2856
2856
  name: "capsulemcp",
2857
- version: "2.0.0",
2857
+ version: "2.0.1",
2858
2858
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2859
2859
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2860
2860
  icons: ICONS
@@ -2914,7 +2914,7 @@ function createCapsuleMcpServer(opts) {
2914
2914
  registerTool(
2915
2915
  server2,
2916
2916
  "list_party_projects",
2917
- "List projects (cases) linked to a given party. Returns the same record shape as get_project, filtered to one party \u2014 use this to answer 'what cases is X involved in?' without enumerating all projects. Accepts optional embed (e.g. 'tags,fields'). For the opportunity-side analogue, use list_party_opportunities.",
2917
+ "List projects linked to a given party. Returns the same record shape as get_project, filtered to one party \u2014 use this to answer 'what projects is X involved in?' without enumerating all projects. Accepts optional embed (e.g. 'tags,fields'). For the opportunity-side analogue, use list_party_opportunities.",
2918
2918
  listPartyProjectsSchema,
2919
2919
  listPartyProjects
2920
2920
  );
@@ -2928,7 +2928,7 @@ function createCapsuleMcpServer(opts) {
2928
2928
  registerTool(
2929
2929
  server2,
2930
2930
  "list_custom_fields",
2931
- "List custom field DEFINITIONS for an entity type (parties, opportunities, or projects/kases). Returns the schema \u2014 name, type, options for list-type fields, etc. \u2014 NOT the values on any specific record. To read values on a record, use get_party / get_opportunity / get_project with embed=fields.",
2931
+ "List custom field DEFINITIONS for an entity type (parties, opportunities, or projects). Returns the schema \u2014 name, type, options for list-type fields, etc. \u2014 NOT the values on any specific record. To read values on a record, use get_party / get_opportunity / get_project with embed=fields.",
2932
2932
  listCustomFieldsSchema,
2933
2933
  listCustomFields
2934
2934
  );
@@ -3077,7 +3077,7 @@ function createCapsuleMcpServer(opts) {
3077
3077
  registerTool(
3078
3078
  server2,
3079
3079
  "list_associated_projects",
3080
- "List projects (cases) associated with a given opportunity. Returns the same record shape as list_projects, filtered to one opportunity. The inverse direction (project \u2192 opportunity) is on each project's `opportunity` field directly, so this tool is only needed for opportunity \u2192 projects discovery \u2014 use list_party_projects for party \u2192 projects.",
3080
+ "List projects associated with a given opportunity. Returns the same record shape as list_projects, filtered to one opportunity. The inverse direction (project \u2192 opportunity) is on each project's `opportunity` field directly, so this tool is only needed for opportunity \u2192 projects discovery \u2014 use list_party_projects for party \u2192 projects.",
3081
3081
  listAssociatedProjectsSchema,
3082
3082
  listAssociatedProjects
3083
3083
  );
@@ -3121,28 +3121,28 @@ function createCapsuleMcpServer(opts) {
3121
3121
  registerTool(
3122
3122
  server2,
3123
3123
  "list_projects",
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.",
3124
+ "List projects 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.",
3125
3125
  listProjectsSchema,
3126
3126
  listProjects
3127
3127
  );
3128
3128
  registerTool(
3129
3129
  server2,
3130
3130
  "filter_projects",
3131
- "Filter projects (cases) by structured conditions (date ranges, status, tags, owner). Use this \u2014 not list_projects \u2014 for questions like 'most recent project', 'projects opened this month'. Capsule's API does not support ad-hoc sort, but for 'most recent X' you can filter by a date field and pick the highest-id row \u2014 Capsule IDs are monotonic, so newest id = newest record.",
3131
+ "Filter projects by structured conditions (date ranges, status, tags, owner). Use this \u2014 not list_projects \u2014 for questions like 'most recent project', 'projects opened this month'. Capsule's API does not support ad-hoc sort, but for 'most recent X' you can filter by a date field and pick the highest-id row \u2014 Capsule IDs are monotonic, so newest id = newest record.",
3132
3132
  filterProjectsSchema,
3133
3133
  filterProjects
3134
3134
  );
3135
3135
  registerTool(
3136
3136
  server2,
3137
3137
  "get_project",
3138
- "Fetch a single project (Capsule's term: 'case') by its numeric id. Returns the full record including name, description, status (OPEN/CLOSED), owner, stage, board, opportunityId (if linked), and timestamps. Use embed='tags,fields' to include attached tags and custom field values in one round-trip. For batch fetches of up to 50 projects at once, use get_projects instead. For the project's timeline (notes, captured emails, completed-task records) use list_project_entries.",
3138
+ "Fetch a single project by its numeric id. Returns the full record including name, description, status (OPEN/CLOSED), owner, stage, board, opportunityId (if linked), and timestamps. Use embed='tags,fields' to include attached tags and custom field values in one round-trip. For batch fetches of up to 50 projects at once, use get_projects instead. For the project's timeline (notes, captured emails, completed-task records) use list_project_entries.",
3139
3139
  getProjectSchema,
3140
3140
  getProject
3141
3141
  );
3142
3142
  registerTool(
3143
3143
  server2,
3144
3144
  "get_projects",
3145
- "Batch-fetch up to 50 projects (cases) by ID. For 1\u201310 ids this is a single Capsule round trip; for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged.",
3145
+ "Batch-fetch up to 50 projects by ID. For 1\u201310 ids this is a single Capsule round trip; for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged.",
3146
3146
  getProjectsSchema,
3147
3147
  getProjects
3148
3148
  );
@@ -3157,7 +3157,7 @@ function createCapsuleMcpServer(opts) {
3157
3157
  registerTool(
3158
3158
  server2,
3159
3159
  "create_project",
3160
- "Create a new project (case) in Capsule CRM linked to a party. Requires partyId and name; description, status, owner, and starting board/stage are optional. To pin a project to a specific board+stage on creation, pass stageId (which uniquely identifies a stage within a board). Discover valid ids via list_boards + list_stages. Returns the created project including its assigned id.",
3160
+ "Create a new project in Capsule CRM linked to a party. Requires partyId and name; description, status, owner, and starting board/stage are optional. To pin a project to a specific board+stage on creation, pass stageId (which uniquely identifies a stage within a board). Discover valid ids via list_boards + list_stages. Returns the created project including its assigned id.",
3161
3161
  createProjectSchema,
3162
3162
  createProject
3163
3163
  );
@@ -3178,7 +3178,7 @@ function createCapsuleMcpServer(opts) {
3178
3178
  registerTool(
3179
3179
  server2,
3180
3180
  "delete_project",
3181
- "DESTRUCTIVE & IRREVERSIBLE: permanently delete a project (case). Prefer update_project with status='CLOSED' to close a project while preserving history. Requires confirm=true. Always read the project first with get_project 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 project was already gone.",
3181
+ "DESTRUCTIVE & IRREVERSIBLE: permanently delete a project. Prefer update_project with status='CLOSED' to close a project while preserving history. Requires confirm=true. Always read the project first with get_project 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 project was already gone.",
3182
3182
  deleteProjectSchema,
3183
3183
  deleteProject
3184
3184
  );
@@ -3293,7 +3293,7 @@ function createCapsuleMcpServer(opts) {
3293
3293
  registerTool(
3294
3294
  server2,
3295
3295
  "list_project_entries",
3296
- "List timeline entries (notes, captured emails, completed-task records) for a project (case). Returns entries newest-first. Each entry has a type ('note', 'email', 'task'), free-text content, and timestamps. Use this to answer 'what's the latest on case X?' For party or opportunity timelines, use list_party_entries or list_opportunity_entries respectively.",
3296
+ "List timeline entries (notes, captured emails, completed-task records) for a project. Returns entries newest-first. Each entry has a type ('note', 'email', 'task'), free-text content, and timestamps. Use this to answer 'what's the latest on project X?' For party or opportunity timelines, use list_party_entries or list_opportunity_entries respectively.",
3297
3297
  listProjectEntriesSchema,
3298
3298
  listProjectEntries
3299
3299
  );
@@ -3444,14 +3444,14 @@ function createCapsuleMcpServer(opts) {
3444
3444
  registerTool(
3445
3445
  server2,
3446
3446
  "list_boards",
3447
- "List all project (case) boards defined in Capsule. A board is a grouping of stages that projects flow through \u2014 the project equivalent of an opportunity pipeline. Returns each board's id, name, and stages. Use this to discover boardId when creating a project, then pick a starting stage via list_stages. Like pipelines, boards are stable per account.",
3447
+ "List all project boards defined in Capsule. A board is a grouping of stages that projects flow through \u2014 the project equivalent of an opportunity pipeline. Returns each board's id, name, and stages. Use this to discover boardId when creating a project, then pick a starting stage via list_stages. Like pipelines, boards are stable per account.",
3448
3448
  listBoardsSchema,
3449
3449
  listBoards
3450
3450
  );
3451
3451
  registerTool(
3452
3452
  server2,
3453
3453
  "list_stages",
3454
- "List project (case) stages. Without arguments returns every stage across every board (each entry carries a `.board` reference so you can tell them apart). Pass `boardId` to scope the result to one specific board's stages. Use this to discover the numeric `stage.id` that `create_project` / `update_project` consume \u2014 stage names alone won't do, Capsule resolves by id. For opportunity (deal) stages, use `list_pipelines` instead \u2014 opportunities don't have stages in the project sense.",
3454
+ "List project stages. Without arguments returns every stage across every board (each entry carries a `.board` reference so you can tell them apart). Pass `boardId` to scope the result to one specific board's stages. Use this to discover the numeric `stage.id` that `create_project` / `update_project` consume \u2014 stage names alone won't do, Capsule resolves by id. For opportunity (deal) stages, use `list_pipelines` instead \u2014 opportunities don't have stages in the project sense.",
3455
3455
  listStagesSchema,
3456
3456
  listStages
3457
3457
  );
@@ -3543,14 +3543,14 @@ function createCapsuleMcpServer(opts) {
3543
3543
  registerTool(
3544
3544
  server2,
3545
3545
  "add_tag",
3546
- "Attach a tag to a party, opportunity, or project (kase) by NAME. Capsule resolves to an existing tag in the tenant or creates a fresh one with this name. Matching is case-insensitive \u2014 'VIP' and 'vip' attach the same tag, preserving the canonical casing from whichever variant was created first. To avoid creating a genuinely-distinct near-duplicate (e.g. 'VIP' vs 'V.I.P.'), call list_tags first and reuse the exact name. Idempotent \u2014 re-attaching an already-attached tag is harmless. To DETACH a tag, use remove_tag_by_id with the tag's id (read via get_party/get_opportunity/get_project with embed='tags').",
3546
+ "Attach a tag to a party, opportunity, or project by NAME. Capsule resolves to an existing tag in the tenant or creates a fresh one with this name. Matching is case-insensitive \u2014 'VIP' and 'vip' attach the same tag, preserving the canonical casing from whichever variant was created first. To avoid creating a genuinely-distinct near-duplicate (e.g. 'VIP' vs 'V.I.P.'), call list_tags first and reuse the exact name. Idempotent \u2014 re-attaching an already-attached tag is harmless. To DETACH a tag, use remove_tag_by_id with the tag's id (read via get_party/get_opportunity/get_project with embed='tags').",
3547
3547
  addTagSchema,
3548
3548
  addTag
3549
3549
  );
3550
3550
  registerTool(
3551
3551
  server2,
3552
3552
  "remove_tag_by_id",
3553
- "Detach a tag from a party, opportunity, or project (kase). Atomic \u2014 one PUT to Capsule. Reversible \u2014 no `confirm: true` gate (re-attach with add_tag using the same tag name). The `tagId` parameter is the tag's id, readable via get_party/get_opportunity/get_project with embed='tags' (list_tags returns the same ids and also works, but reading via embed first confirms the tag is actually attached to this entity). The tag definition itself remains in the tenant for other entities that still share it. Idempotent on retry: response is `{removed: true, alreadyRemoved: false, entity, entityId, tagId, ...<updated entity>}` on a fresh detach or `{removed: true, alreadyRemoved: true, entity, entityId, tagId}` if the tag was already detached (Capsule's 422 'tag not found to delete' is caught and converted).",
3553
+ "Detach a tag from a party, opportunity, or project. Atomic \u2014 one PUT to Capsule. Reversible \u2014 no `confirm: true` gate (re-attach with add_tag using the same tag name). The `tagId` parameter is the tag's id, readable via get_party/get_opportunity/get_project with embed='tags' (list_tags returns the same ids and also works, but reading via embed first confirms the tag is actually attached to this entity). The tag definition itself remains in the tenant for other entities that still share it. Idempotent on retry: response is `{removed: true, alreadyRemoved: false, entity, entityId, tagId, ...<updated entity>}` on a fresh detach or `{removed: true, alreadyRemoved: true, entity, entityId, tagId}` if the tag was already detached (Capsule's 422 'tag not found to delete' is caught and converted).",
3554
3554
  removeTagByIdSchema,
3555
3555
  removeTagById
3556
3556
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capsulemcp",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
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",