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 +2 -2
- package/dist/http.js +26 -26
- package/dist/index.js +26 -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.0.
|
|
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
|
|
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."
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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."
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|