capsulemcp 2.0.0 → 2.1.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 +2 -2
- package/dist/http.js +32 -26
- package/dist/index.js +32 -26
- package/package.json +1 -1
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.
|
|
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.1.0"` in `args`. If you're tracking a fork or an unreleased branch, use the GitHub-ref form instead: `"github:soil-dev/capsulemcp#v2.1.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
|
|
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
|
|
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
|
|
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
|
|
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."
|
|
@@ -2360,8 +2360,11 @@ var createProjectSchema = z10.object({
|
|
|
2360
2360
|
"Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
|
|
2361
2361
|
),
|
|
2362
2362
|
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2363
|
+
startOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
2364
|
+
"Project start date, YYYY-MM-DD. Verified empirically (v2.0.1 wire probe): Capsule's POST /kases accepts and stores it; reads back as `startOn` on the project."
|
|
2365
|
+
),
|
|
2363
2366
|
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2364
|
-
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's
|
|
2367
|
+
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
2368
|
)
|
|
2366
2369
|
});
|
|
2367
2370
|
async function createProject(input) {
|
|
@@ -2393,9 +2396,12 @@ var updateProjectSchema = z10.object({
|
|
|
2393
2396
|
"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
2397
|
),
|
|
2395
2398
|
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
|
|
2399
|
+
"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
2400
|
),
|
|
2398
2401
|
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2402
|
+
startOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional().describe(
|
|
2403
|
+
"Set the project start date (YYYY-MM-DD), or `null` to clear it. Verified empirically (v2.0.1 wire probe): PUT accepts both the set and the null-clear. `undefined` leaves the field untouched."
|
|
2404
|
+
),
|
|
2399
2405
|
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2400
2406
|
fieldsArrayDescriptor("get_project") + " 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."
|
|
2401
2407
|
)
|
|
@@ -2431,7 +2437,7 @@ var { schema: batchUpdateProjectSchema, handler: batchUpdateProject } = defineBa
|
|
|
2431
2437
|
var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
2432
2438
|
toolName: "delete_project",
|
|
2433
2439
|
pathPrefix: "/kases",
|
|
2434
|
-
confirmHint: "Must be set to true. Permanently deletes the project
|
|
2440
|
+
confirmHint: "Must be set to true. Permanently deletes the project. Consider update_project status='CLOSED' instead. Irreversible."
|
|
2435
2441
|
});
|
|
2436
2442
|
|
|
2437
2443
|
// src/tools/tasks.ts
|
|
@@ -2519,7 +2525,7 @@ var updateTaskSchema = z11.object({
|
|
|
2519
2525
|
"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
2526
|
),
|
|
2521
2527
|
projectId: positiveId.nullable().optional().describe(
|
|
2522
|
-
"Re-link the task to a project
|
|
2528
|
+
"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
2529
|
)
|
|
2524
2530
|
});
|
|
2525
2531
|
async function updateTask(input) {
|
|
@@ -2567,7 +2573,7 @@ var listPartyEntriesSchema = z12.object({
|
|
|
2567
2573
|
partyId: positiveId,
|
|
2568
2574
|
...listEntriesPagination,
|
|
2569
2575
|
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
|
|
2576
|
+
"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
2577
|
)
|
|
2572
2578
|
});
|
|
2573
2579
|
var PER_PARTY_FETCH_CAP = 100;
|
|
@@ -2803,7 +2809,7 @@ async function listTags(input) {
|
|
|
2803
2809
|
}
|
|
2804
2810
|
var addTagSchema = z15.object({
|
|
2805
2811
|
entity: TagEntity,
|
|
2806
|
-
entityId: positiveId.describe("The party/opportunity/
|
|
2812
|
+
entityId: positiveId.describe("The party/opportunity/project id."),
|
|
2807
2813
|
tagName: z15.string().min(1).describe(
|
|
2808
2814
|
"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
2815
|
)
|
|
@@ -2819,7 +2825,7 @@ async function addTag(input) {
|
|
|
2819
2825
|
}
|
|
2820
2826
|
var removeTagByIdSchema = z15.object({
|
|
2821
2827
|
entity: TagEntity,
|
|
2822
|
-
entityId: positiveId.describe("The party/opportunity/
|
|
2828
|
+
entityId: positiveId.describe("The party/opportunity/project id."),
|
|
2823
2829
|
tagId: positiveId.describe(
|
|
2824
2830
|
"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
2831
|
)
|
|
@@ -3164,7 +3170,7 @@ var getCustomFieldSchema = z21.object({
|
|
|
3164
3170
|
});
|
|
3165
3171
|
async function getCustomField(input) {
|
|
3166
3172
|
const { data } = await capsuleGetCached(
|
|
3167
|
-
`/${input.entity}/fields/definitions/${input.id}`
|
|
3173
|
+
`/${ENTITY_PATH[input.entity]}/fields/definitions/${input.id}`
|
|
3168
3174
|
);
|
|
3169
3175
|
return data;
|
|
3170
3176
|
}
|
|
@@ -3357,7 +3363,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3357
3363
|
const server = new McpServer(
|
|
3358
3364
|
{
|
|
3359
3365
|
name: "capsulemcp",
|
|
3360
|
-
version: "2.
|
|
3366
|
+
version: "2.1.0",
|
|
3361
3367
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
3362
3368
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
3363
3369
|
icons: ICONS
|
|
@@ -3417,7 +3423,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3417
3423
|
registerTool(
|
|
3418
3424
|
server,
|
|
3419
3425
|
"list_party_projects",
|
|
3420
|
-
"List projects
|
|
3426
|
+
"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
3427
|
listPartyProjectsSchema,
|
|
3422
3428
|
listPartyProjects
|
|
3423
3429
|
);
|
|
@@ -3431,7 +3437,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3431
3437
|
registerTool(
|
|
3432
3438
|
server,
|
|
3433
3439
|
"list_custom_fields",
|
|
3434
|
-
"List custom field DEFINITIONS for an entity type (parties, opportunities, or projects
|
|
3440
|
+
"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
3441
|
listCustomFieldsSchema,
|
|
3436
3442
|
listCustomFields
|
|
3437
3443
|
);
|
|
@@ -3580,7 +3586,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3580
3586
|
registerTool(
|
|
3581
3587
|
server,
|
|
3582
3588
|
"list_associated_projects",
|
|
3583
|
-
"List projects
|
|
3589
|
+
"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
3590
|
listAssociatedProjectsSchema,
|
|
3585
3591
|
listAssociatedProjects
|
|
3586
3592
|
);
|
|
@@ -3624,28 +3630,28 @@ function createCapsuleMcpServer(opts) {
|
|
|
3624
3630
|
registerTool(
|
|
3625
3631
|
server,
|
|
3626
3632
|
"list_projects",
|
|
3627
|
-
"List projects
|
|
3633
|
+
"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
3634
|
listProjectsSchema,
|
|
3629
3635
|
listProjects
|
|
3630
3636
|
);
|
|
3631
3637
|
registerTool(
|
|
3632
3638
|
server,
|
|
3633
3639
|
"filter_projects",
|
|
3634
|
-
"Filter projects
|
|
3640
|
+
"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
3641
|
filterProjectsSchema,
|
|
3636
3642
|
filterProjects
|
|
3637
3643
|
);
|
|
3638
3644
|
registerTool(
|
|
3639
3645
|
server,
|
|
3640
3646
|
"get_project",
|
|
3641
|
-
"Fetch a single project
|
|
3647
|
+
"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
3648
|
getProjectSchema,
|
|
3643
3649
|
getProject
|
|
3644
3650
|
);
|
|
3645
3651
|
registerTool(
|
|
3646
3652
|
server,
|
|
3647
3653
|
"get_projects",
|
|
3648
|
-
"Batch-fetch up to 50 projects
|
|
3654
|
+
"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
3655
|
getProjectsSchema,
|
|
3650
3656
|
getProjects
|
|
3651
3657
|
);
|
|
@@ -3660,7 +3666,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3660
3666
|
registerTool(
|
|
3661
3667
|
server,
|
|
3662
3668
|
"create_project",
|
|
3663
|
-
"Create a new project
|
|
3669
|
+
"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
3670
|
createProjectSchema,
|
|
3665
3671
|
createProject
|
|
3666
3672
|
);
|
|
@@ -3681,7 +3687,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3681
3687
|
registerTool(
|
|
3682
3688
|
server,
|
|
3683
3689
|
"delete_project",
|
|
3684
|
-
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a project
|
|
3690
|
+
"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
3691
|
deleteProjectSchema,
|
|
3686
3692
|
deleteProject
|
|
3687
3693
|
);
|
|
@@ -3796,7 +3802,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3796
3802
|
registerTool(
|
|
3797
3803
|
server,
|
|
3798
3804
|
"list_project_entries",
|
|
3799
|
-
"List timeline entries (notes, captured emails, completed-task records) for a project
|
|
3805
|
+
"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
3806
|
listProjectEntriesSchema,
|
|
3801
3807
|
listProjectEntries
|
|
3802
3808
|
);
|
|
@@ -3947,14 +3953,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
3947
3953
|
registerTool(
|
|
3948
3954
|
server,
|
|
3949
3955
|
"list_boards",
|
|
3950
|
-
"List all project
|
|
3956
|
+
"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
3957
|
listBoardsSchema,
|
|
3952
3958
|
listBoards
|
|
3953
3959
|
);
|
|
3954
3960
|
registerTool(
|
|
3955
3961
|
server,
|
|
3956
3962
|
"list_stages",
|
|
3957
|
-
"List project
|
|
3963
|
+
"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
3964
|
listStagesSchema,
|
|
3959
3965
|
listStages
|
|
3960
3966
|
);
|
|
@@ -4046,14 +4052,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
4046
4052
|
registerTool(
|
|
4047
4053
|
server,
|
|
4048
4054
|
"add_tag",
|
|
4049
|
-
"Attach a tag to a party, opportunity, or project
|
|
4055
|
+
"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
4056
|
addTagSchema,
|
|
4051
4057
|
addTag
|
|
4052
4058
|
);
|
|
4053
4059
|
registerTool(
|
|
4054
4060
|
server,
|
|
4055
4061
|
"remove_tag_by_id",
|
|
4056
|
-
"Detach a tag from a party, opportunity, or project
|
|
4062
|
+
"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
4063
|
removeTagByIdSchema,
|
|
4058
4064
|
removeTagById
|
|
4059
4065
|
);
|
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
|
|
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
|
|
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
|
|
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."
|
|
@@ -1857,8 +1857,11 @@ var createProjectSchema = z9.object({
|
|
|
1857
1857
|
"Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
|
|
1858
1858
|
),
|
|
1859
1859
|
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1860
|
+
startOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
1861
|
+
"Project start date, YYYY-MM-DD. Verified empirically (v2.0.1 wire probe): Capsule's POST /kases accepts and stores it; reads back as `startOn` on the project."
|
|
1862
|
+
),
|
|
1860
1863
|
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
1861
|
-
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's
|
|
1864
|
+
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
1865
|
)
|
|
1863
1866
|
});
|
|
1864
1867
|
async function createProject(input) {
|
|
@@ -1890,9 +1893,12 @@ var updateProjectSchema = z9.object({
|
|
|
1890
1893
|
"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
1894
|
),
|
|
1892
1895
|
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
|
|
1896
|
+
"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
1897
|
),
|
|
1895
1898
|
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1899
|
+
startOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional().describe(
|
|
1900
|
+
"Set the project start date (YYYY-MM-DD), or `null` to clear it. Verified empirically (v2.0.1 wire probe): PUT accepts both the set and the null-clear. `undefined` leaves the field untouched."
|
|
1901
|
+
),
|
|
1896
1902
|
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
1897
1903
|
fieldsArrayDescriptor("get_project") + " 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."
|
|
1898
1904
|
)
|
|
@@ -1928,7 +1934,7 @@ var { schema: batchUpdateProjectSchema, handler: batchUpdateProject } = defineBa
|
|
|
1928
1934
|
var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
1929
1935
|
toolName: "delete_project",
|
|
1930
1936
|
pathPrefix: "/kases",
|
|
1931
|
-
confirmHint: "Must be set to true. Permanently deletes the project
|
|
1937
|
+
confirmHint: "Must be set to true. Permanently deletes the project. Consider update_project status='CLOSED' instead. Irreversible."
|
|
1932
1938
|
});
|
|
1933
1939
|
|
|
1934
1940
|
// src/tools/tasks.ts
|
|
@@ -2016,7 +2022,7 @@ var updateTaskSchema = z10.object({
|
|
|
2016
2022
|
"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
2023
|
),
|
|
2018
2024
|
projectId: positiveId.nullable().optional().describe(
|
|
2019
|
-
"Re-link the task to a project
|
|
2025
|
+
"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
2026
|
)
|
|
2021
2027
|
});
|
|
2022
2028
|
async function updateTask(input) {
|
|
@@ -2064,7 +2070,7 @@ var listPartyEntriesSchema = z11.object({
|
|
|
2064
2070
|
partyId: positiveId,
|
|
2065
2071
|
...listEntriesPagination,
|
|
2066
2072
|
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
|
|
2073
|
+
"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
2074
|
)
|
|
2069
2075
|
});
|
|
2070
2076
|
var PER_PARTY_FETCH_CAP = 100;
|
|
@@ -2300,7 +2306,7 @@ async function listTags(input) {
|
|
|
2300
2306
|
}
|
|
2301
2307
|
var addTagSchema = z14.object({
|
|
2302
2308
|
entity: TagEntity,
|
|
2303
|
-
entityId: positiveId.describe("The party/opportunity/
|
|
2309
|
+
entityId: positiveId.describe("The party/opportunity/project id."),
|
|
2304
2310
|
tagName: z14.string().min(1).describe(
|
|
2305
2311
|
"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
2312
|
)
|
|
@@ -2316,7 +2322,7 @@ async function addTag(input) {
|
|
|
2316
2322
|
}
|
|
2317
2323
|
var removeTagByIdSchema = z14.object({
|
|
2318
2324
|
entity: TagEntity,
|
|
2319
|
-
entityId: positiveId.describe("The party/opportunity/
|
|
2325
|
+
entityId: positiveId.describe("The party/opportunity/project id."),
|
|
2320
2326
|
tagId: positiveId.describe(
|
|
2321
2327
|
"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
2328
|
)
|
|
@@ -2661,7 +2667,7 @@ var getCustomFieldSchema = z20.object({
|
|
|
2661
2667
|
});
|
|
2662
2668
|
async function getCustomField(input) {
|
|
2663
2669
|
const { data } = await capsuleGetCached(
|
|
2664
|
-
`/${input.entity}/fields/definitions/${input.id}`
|
|
2670
|
+
`/${ENTITY_PATH[input.entity]}/fields/definitions/${input.id}`
|
|
2665
2671
|
);
|
|
2666
2672
|
return data;
|
|
2667
2673
|
}
|
|
@@ -2854,7 +2860,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2854
2860
|
const server2 = new McpServer(
|
|
2855
2861
|
{
|
|
2856
2862
|
name: "capsulemcp",
|
|
2857
|
-
version: "2.
|
|
2863
|
+
version: "2.1.0",
|
|
2858
2864
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
2859
2865
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
2860
2866
|
icons: ICONS
|
|
@@ -2914,7 +2920,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2914
2920
|
registerTool(
|
|
2915
2921
|
server2,
|
|
2916
2922
|
"list_party_projects",
|
|
2917
|
-
"List projects
|
|
2923
|
+
"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
2924
|
listPartyProjectsSchema,
|
|
2919
2925
|
listPartyProjects
|
|
2920
2926
|
);
|
|
@@ -2928,7 +2934,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2928
2934
|
registerTool(
|
|
2929
2935
|
server2,
|
|
2930
2936
|
"list_custom_fields",
|
|
2931
|
-
"List custom field DEFINITIONS for an entity type (parties, opportunities, or projects
|
|
2937
|
+
"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
2938
|
listCustomFieldsSchema,
|
|
2933
2939
|
listCustomFields
|
|
2934
2940
|
);
|
|
@@ -3077,7 +3083,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3077
3083
|
registerTool(
|
|
3078
3084
|
server2,
|
|
3079
3085
|
"list_associated_projects",
|
|
3080
|
-
"List projects
|
|
3086
|
+
"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
3087
|
listAssociatedProjectsSchema,
|
|
3082
3088
|
listAssociatedProjects
|
|
3083
3089
|
);
|
|
@@ -3121,28 +3127,28 @@ function createCapsuleMcpServer(opts) {
|
|
|
3121
3127
|
registerTool(
|
|
3122
3128
|
server2,
|
|
3123
3129
|
"list_projects",
|
|
3124
|
-
"List projects
|
|
3130
|
+
"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
3131
|
listProjectsSchema,
|
|
3126
3132
|
listProjects
|
|
3127
3133
|
);
|
|
3128
3134
|
registerTool(
|
|
3129
3135
|
server2,
|
|
3130
3136
|
"filter_projects",
|
|
3131
|
-
"Filter projects
|
|
3137
|
+
"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
3138
|
filterProjectsSchema,
|
|
3133
3139
|
filterProjects
|
|
3134
3140
|
);
|
|
3135
3141
|
registerTool(
|
|
3136
3142
|
server2,
|
|
3137
3143
|
"get_project",
|
|
3138
|
-
"Fetch a single project
|
|
3144
|
+
"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
3145
|
getProjectSchema,
|
|
3140
3146
|
getProject
|
|
3141
3147
|
);
|
|
3142
3148
|
registerTool(
|
|
3143
3149
|
server2,
|
|
3144
3150
|
"get_projects",
|
|
3145
|
-
"Batch-fetch up to 50 projects
|
|
3151
|
+
"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
3152
|
getProjectsSchema,
|
|
3147
3153
|
getProjects
|
|
3148
3154
|
);
|
|
@@ -3157,7 +3163,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3157
3163
|
registerTool(
|
|
3158
3164
|
server2,
|
|
3159
3165
|
"create_project",
|
|
3160
|
-
"Create a new project
|
|
3166
|
+
"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
3167
|
createProjectSchema,
|
|
3162
3168
|
createProject
|
|
3163
3169
|
);
|
|
@@ -3178,7 +3184,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3178
3184
|
registerTool(
|
|
3179
3185
|
server2,
|
|
3180
3186
|
"delete_project",
|
|
3181
|
-
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a project
|
|
3187
|
+
"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
3188
|
deleteProjectSchema,
|
|
3183
3189
|
deleteProject
|
|
3184
3190
|
);
|
|
@@ -3293,7 +3299,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3293
3299
|
registerTool(
|
|
3294
3300
|
server2,
|
|
3295
3301
|
"list_project_entries",
|
|
3296
|
-
"List timeline entries (notes, captured emails, completed-task records) for a project
|
|
3302
|
+
"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
3303
|
listProjectEntriesSchema,
|
|
3298
3304
|
listProjectEntries
|
|
3299
3305
|
);
|
|
@@ -3444,14 +3450,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
3444
3450
|
registerTool(
|
|
3445
3451
|
server2,
|
|
3446
3452
|
"list_boards",
|
|
3447
|
-
"List all project
|
|
3453
|
+
"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
3454
|
listBoardsSchema,
|
|
3449
3455
|
listBoards
|
|
3450
3456
|
);
|
|
3451
3457
|
registerTool(
|
|
3452
3458
|
server2,
|
|
3453
3459
|
"list_stages",
|
|
3454
|
-
"List project
|
|
3460
|
+
"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
3461
|
listStagesSchema,
|
|
3456
3462
|
listStages
|
|
3457
3463
|
);
|
|
@@ -3543,14 +3549,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
3543
3549
|
registerTool(
|
|
3544
3550
|
server2,
|
|
3545
3551
|
"add_tag",
|
|
3546
|
-
"Attach a tag to a party, opportunity, or project
|
|
3552
|
+
"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
3553
|
addTagSchema,
|
|
3548
3554
|
addTag
|
|
3549
3555
|
);
|
|
3550
3556
|
registerTool(
|
|
3551
3557
|
server2,
|
|
3552
3558
|
"remove_tag_by_id",
|
|
3553
|
-
"Detach a tag from a party, opportunity, or project
|
|
3559
|
+
"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
3560
|
removeTagByIdSchema,
|
|
3555
3561
|
removeTagById
|
|
3556
3562
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capsulemcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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",
|