capsulemcp 1.6.2 → 1.6.5
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 +109 -37
- package/dist/index.js +109 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A [Model Context Protocol](https://modelcontextprotocol.io) server for [Capsule CRM](https://capsulecrm.com). Connect Claude (Desktop, Code, or web Projects via Custom Connector) to your CRM and let it answer natural-language questions across the full record graph: contacts, organisations, opportunities, projects, tasks, and timeline activity. Beyond the basics it covers structured filters with field/operator conditions, saved searches with sort, workflow tracks (templates and instances), file attachments (read + write), audit of deleted records, and batch fetches up to 50 records per call.
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **87 tools** across the Capsule resource graph (49 in read-only mode) — full read coverage plus careful, confirm-gated writes; 6 batched-write tools (`batch_*`) for mass-update workflows
|
|
8
8
|
- **Two transports**: stdio for local installs (Claude Desktop / Code), HTTP+OAuth for hosted Custom Connectors
|
|
9
9
|
- **Read-only mode** as a one-env-var flag; works alongside read-scoped Capsule tokens
|
|
10
10
|
- **MCP tool annotations**: 49 read tools carry `readOnlyHint: true`, 7 destructive ones carry `destructiveHint: true` — clients that honor these hints can auto-approve safe reads while still prompting for writes/destructive calls
|
|
@@ -48,7 +48,7 @@ For most individual users the install is a single JSON snippet pasted into Claud
|
|
|
48
48
|
|
|
49
49
|
3. Restart Claude Desktop. The Capsule tools appear in the tool picker.
|
|
50
50
|
|
|
51
|
-
That's it. The first launch fetches the package from npm (a few seconds); subsequent launches are instant from the npx cache. To pin a specific version, use `"capsulemcp@1.6.
|
|
51
|
+
That's it. The first launch fetches the package from npm (a few seconds); subsequent launches are instant from the npx cache. To pin a specific version, use `"capsulemcp@1.6.5"` in `args`. If you're tracking a fork or an unreleased branch, use the GitHub-ref form instead: `"github:soil-dev/capsulemcp#v1.6.5"` — 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
|
|
package/dist/http.js
CHANGED
|
@@ -1555,6 +1555,16 @@ function defineDelete(args) {
|
|
|
1555
1555
|
return { schema, handler };
|
|
1556
1556
|
}
|
|
1557
1557
|
|
|
1558
|
+
// src/tools/preserve-refs.ts
|
|
1559
|
+
async function readEntityRefs(path, responseKey) {
|
|
1560
|
+
const { data } = await capsuleGet(path);
|
|
1561
|
+
const entity = data[responseKey];
|
|
1562
|
+
return {
|
|
1563
|
+
teamId: entity?.team?.id ?? void 0,
|
|
1564
|
+
stageId: entity?.stage?.id ?? void 0
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1558
1568
|
// src/tools/custom-field-helpers.ts
|
|
1559
1569
|
import { z as z6 } from "zod";
|
|
1560
1570
|
var CustomFieldWriteSchema = z6.object({
|
|
@@ -1730,8 +1740,11 @@ var PartyWriteBaseSchema = {
|
|
|
1730
1740
|
websites: z7.array(WebsiteSchema).optional().describe(
|
|
1731
1741
|
"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."
|
|
1732
1742
|
),
|
|
1733
|
-
ownerId: positiveId.optional().describe(
|
|
1734
|
-
"
|
|
1743
|
+
ownerId: positiveId.nullable().optional().describe(
|
|
1744
|
+
"Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on /parties has the same asymmetric owner/team semantic documented in NOTES-ON-CAPSULE-API.md \xA727 for /kases \u2014 setting `owner` while omitting `team` is plausibly clearing-prone. When you supply `ownerId` and omit `teamId`, this connector reads the party's current team and includes it in the PUT body to preserve it across the owner change. Supply `teamId` explicitly to change it."
|
|
1745
|
+
),
|
|
1746
|
+
teamId: positiveId.nullable().optional().describe(
|
|
1747
|
+
"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)."
|
|
1735
1748
|
)
|
|
1736
1749
|
};
|
|
1737
1750
|
var createPartySchema = z7.object({
|
|
@@ -1744,13 +1757,25 @@ var createPartySchema = z7.object({
|
|
|
1744
1757
|
organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
|
|
1745
1758
|
// organisation
|
|
1746
1759
|
name: z7.string().optional(),
|
|
1747
|
-
...PartyWriteBaseSchema
|
|
1760
|
+
...PartyWriteBaseSchema,
|
|
1761
|
+
ownerId: positiveId.optional().describe(
|
|
1762
|
+
"Assign to user ID. Defaults to the API-token owner when omitted. To create a team-owned party with no specific user, first create the party, then call update_party with `ownerId: null` and `teamId`."
|
|
1763
|
+
),
|
|
1764
|
+
teamId: positiveId.optional().describe(
|
|
1765
|
+
"Assign to team ID (discover via list_teams). Omit to leave team unset on create. To clear an existing team or create a team-owned party with no specific owner, use update_party after creation."
|
|
1766
|
+
),
|
|
1767
|
+
fields: z7.array(CustomFieldWriteSchema).optional().describe(
|
|
1768
|
+
fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
|
|
1769
|
+
)
|
|
1748
1770
|
});
|
|
1749
1771
|
async function createParty(input) {
|
|
1750
|
-
const { ownerId, organisationId, ...rest } = input;
|
|
1772
|
+
const { ownerId, teamId, organisationId, fields, ...rest } = input;
|
|
1751
1773
|
const body = { ...rest };
|
|
1752
1774
|
setRef(body, "owner", ownerId);
|
|
1775
|
+
setRef(body, "team", teamId);
|
|
1753
1776
|
setRef(body, "organisation", organisationId);
|
|
1777
|
+
const mappedFields = mapFieldsForBody(fields);
|
|
1778
|
+
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1754
1779
|
return capsulePost("/parties", { party: body });
|
|
1755
1780
|
}
|
|
1756
1781
|
var updatePartySchema = z7.object({
|
|
@@ -1760,16 +1785,25 @@ var updatePartySchema = z7.object({
|
|
|
1760
1785
|
title: z7.string().optional(),
|
|
1761
1786
|
jobTitle: z7.string().optional(),
|
|
1762
1787
|
name: z7.string().optional(),
|
|
1788
|
+
organisationId: positiveId.nullable().optional().describe(
|
|
1789
|
+
"For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
|
|
1790
|
+
),
|
|
1763
1791
|
fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
|
|
1764
1792
|
...PartyWriteBaseSchema
|
|
1765
1793
|
});
|
|
1766
1794
|
async function updateParty(input) {
|
|
1767
|
-
const { id, ownerId, fields, ...rest } = input;
|
|
1795
|
+
const { id, ownerId, teamId, organisationId, fields, ...rest } = input;
|
|
1768
1796
|
const body = {};
|
|
1769
1797
|
for (const [k, v] of Object.entries(rest)) {
|
|
1770
1798
|
if (v !== void 0) body[k] = v;
|
|
1771
1799
|
}
|
|
1772
|
-
|
|
1800
|
+
let resolvedTeamId = teamId;
|
|
1801
|
+
if (ownerId !== void 0 && teamId === void 0) {
|
|
1802
|
+
({ teamId: resolvedTeamId } = await readEntityRefs(`/parties/${id}`, "party"));
|
|
1803
|
+
}
|
|
1804
|
+
setNullableRef(body, "owner", ownerId);
|
|
1805
|
+
setNullableRef(body, "team", resolvedTeamId);
|
|
1806
|
+
setNullableRef(body, "organisation", organisationId);
|
|
1773
1807
|
const mappedFields = mapFieldsForBody(fields);
|
|
1774
1808
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1775
1809
|
return capsulePut(`/parties/${id}`, { party: body });
|
|
@@ -1936,18 +1970,6 @@ async function removePartyWebsiteById(input) {
|
|
|
1936
1970
|
|
|
1937
1971
|
// src/tools/opportunities.ts
|
|
1938
1972
|
import { z as z8 } from "zod";
|
|
1939
|
-
|
|
1940
|
-
// src/tools/preserve-refs.ts
|
|
1941
|
-
async function readEntityRefs(path, responseKey) {
|
|
1942
|
-
const { data } = await capsuleGet(path);
|
|
1943
|
-
const entity = data[responseKey];
|
|
1944
|
-
return {
|
|
1945
|
-
teamId: entity?.team?.id ?? void 0,
|
|
1946
|
-
stageId: entity?.stage?.id ?? void 0
|
|
1947
|
-
};
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
// src/tools/opportunities.ts
|
|
1951
1973
|
var OpportunityValueSchema = z8.object({
|
|
1952
1974
|
amount: z8.number().nonnegative(),
|
|
1953
1975
|
currency: z8.string({
|
|
@@ -2018,14 +2040,17 @@ var createOpportunitySchema = z8.object({
|
|
|
2018
2040
|
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2019
2041
|
probability: z8.number().int().min(0).max(100).optional(),
|
|
2020
2042
|
ownerId: positiveId.optional().describe(
|
|
2021
|
-
"Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it.
|
|
2043
|
+
"Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. To clear owner later, call update_opportunity with `ownerId: null`. Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
|
|
2022
2044
|
),
|
|
2023
2045
|
teamId: positiveId.optional().describe(
|
|
2024
2046
|
"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)."
|
|
2047
|
+
),
|
|
2048
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
2049
|
+
fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
|
|
2025
2050
|
)
|
|
2026
2051
|
});
|
|
2027
2052
|
async function createOpportunity(input) {
|
|
2028
|
-
const { partyId, milestoneId, ownerId, teamId, ...rest } = input;
|
|
2053
|
+
const { partyId, milestoneId, ownerId, teamId, fields, ...rest } = input;
|
|
2029
2054
|
const body = {
|
|
2030
2055
|
...rest,
|
|
2031
2056
|
party: { id: partyId },
|
|
@@ -2033,11 +2058,16 @@ async function createOpportunity(input) {
|
|
|
2033
2058
|
};
|
|
2034
2059
|
setRef(body, "owner", ownerId);
|
|
2035
2060
|
setRef(body, "team", teamId);
|
|
2061
|
+
const mappedFields = mapFieldsForBody(fields);
|
|
2062
|
+
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2036
2063
|
return capsulePost("/opportunities", { opportunity: body });
|
|
2037
2064
|
}
|
|
2038
2065
|
var updateOpportunitySchema = z8.object({
|
|
2039
2066
|
id: positiveId,
|
|
2040
2067
|
name: z8.string().min(1).optional(),
|
|
2068
|
+
partyId: positiveId.optional().describe(
|
|
2069
|
+
"Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_project.partyId`."
|
|
2070
|
+
),
|
|
2041
2071
|
milestoneId: positiveId.optional().describe(
|
|
2042
2072
|
"Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
|
|
2043
2073
|
),
|
|
@@ -2050,8 +2080,8 @@ var updateOpportunitySchema = z8.object({
|
|
|
2050
2080
|
lostReasonId: positiveId.optional().describe(
|
|
2051
2081
|
"Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lostreasons."
|
|
2052
2082
|
),
|
|
2053
|
-
ownerId: positiveId.optional().describe(
|
|
2054
|
-
"Reassign owner
|
|
2083
|
+
ownerId: positiveId.nullable().optional().describe(
|
|
2084
|
+
"Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
|
|
2055
2085
|
),
|
|
2056
2086
|
teamId: positiveId.nullable().optional().describe(
|
|
2057
2087
|
"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."
|
|
@@ -2059,17 +2089,18 @@ var updateOpportunitySchema = z8.object({
|
|
|
2059
2089
|
fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
|
|
2060
2090
|
});
|
|
2061
2091
|
async function updateOpportunity(input) {
|
|
2062
|
-
const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
2092
|
+
const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
2063
2093
|
const body = {};
|
|
2064
2094
|
for (const [k, v] of Object.entries(rest)) {
|
|
2065
2095
|
if (v !== void 0) body[k] = v;
|
|
2066
2096
|
}
|
|
2097
|
+
setRef(body, "party", partyId);
|
|
2067
2098
|
setRef(body, "milestone", milestoneId);
|
|
2068
2099
|
let resolvedTeamId = teamId;
|
|
2069
2100
|
if (ownerId !== void 0 && teamId === void 0) {
|
|
2070
2101
|
({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
|
|
2071
2102
|
}
|
|
2072
|
-
|
|
2103
|
+
setNullableRef(body, "owner", ownerId);
|
|
2073
2104
|
setNullableRef(body, "team", resolvedTeamId);
|
|
2074
2105
|
setRef(body, "lostReason", lostReasonId);
|
|
2075
2106
|
const mappedFields = mapFieldsForBody(fields);
|
|
@@ -2153,10 +2184,13 @@ var createProjectSchema = z9.object({
|
|
|
2153
2184
|
stageId: positiveId.optional().describe(
|
|
2154
2185
|
"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."
|
|
2155
2186
|
),
|
|
2156
|
-
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
|
|
2187
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2188
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
2189
|
+
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
2190
|
+
)
|
|
2157
2191
|
});
|
|
2158
2192
|
async function createProject(input) {
|
|
2159
|
-
const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
|
|
2193
|
+
const { partyId, ownerId, teamId, status, stageId, fields, ...rest } = input;
|
|
2160
2194
|
const body = {
|
|
2161
2195
|
...rest,
|
|
2162
2196
|
status: status ?? "OPEN",
|
|
@@ -2165,6 +2199,8 @@ async function createProject(input) {
|
|
|
2165
2199
|
setRef(body, "owner", ownerId);
|
|
2166
2200
|
setRef(body, "team", teamId);
|
|
2167
2201
|
if (stageId) body["stage"] = stageId;
|
|
2202
|
+
const mappedFields = mapFieldsForBody(fields);
|
|
2203
|
+
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2168
2204
|
return capsulePost("/kases", { kase: body });
|
|
2169
2205
|
}
|
|
2170
2206
|
var updateProjectSchema = z9.object({
|
|
@@ -2172,14 +2208,17 @@ var updateProjectSchema = z9.object({
|
|
|
2172
2208
|
name: z9.string().min(1).optional(),
|
|
2173
2209
|
description: z9.string().optional(),
|
|
2174
2210
|
status: z9.enum(["OPEN", "CLOSED"]).optional(),
|
|
2211
|
+
partyId: positiveId.optional().describe(
|
|
2212
|
+
"Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_opportunity.partyId`."
|
|
2213
|
+
),
|
|
2175
2214
|
ownerId: positiveId.nullable().optional().describe(
|
|
2176
2215
|
"Reassign owner: pass a user ID to set, or `null` to unassign (matches the 'Unassign' option in Capsule's web UI). When you supply `ownerId` and omit `teamId` and/or `stageId`, the connector fetches the project's current omitted fields and includes them in the PUT body \u2014 this preserves them across the owner change (without it, Capsule's PUT would clear team; stage carry is defensive against the symmetric clear). Supply `teamId` and/or `stageId` explicitly on the same call to change them instead. `teamId: null` clears the team as part of an owner change. Constraints (Capsule enforces, 422 on violation): owner must be a member of the team if both are set; a project must always have at least one of {owner, team} set (cannot clear both)."
|
|
2177
2216
|
),
|
|
2178
2217
|
teamId: positiveId.nullable().optional().describe(
|
|
2179
2218
|
"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'."
|
|
2180
2219
|
),
|
|
2181
|
-
stageId: positiveId.optional().describe(
|
|
2182
|
-
"Move the project to this stage (board column). 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."
|
|
2220
|
+
stageId: positiveId.nullable().optional().describe(
|
|
2221
|
+
"Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
|
|
2183
2222
|
),
|
|
2184
2223
|
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2185
2224
|
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
@@ -2187,11 +2226,12 @@ var updateProjectSchema = z9.object({
|
|
|
2187
2226
|
)
|
|
2188
2227
|
});
|
|
2189
2228
|
async function updateProject(input) {
|
|
2190
|
-
const { id, ownerId, teamId, stageId, fields, ...rest } = input;
|
|
2229
|
+
const { id, partyId, ownerId, teamId, stageId, fields, ...rest } = input;
|
|
2191
2230
|
const body = {};
|
|
2192
2231
|
for (const [k, v] of Object.entries(rest)) {
|
|
2193
2232
|
if (v !== void 0) body[k] = v;
|
|
2194
2233
|
}
|
|
2234
|
+
setRef(body, "party", partyId);
|
|
2195
2235
|
let resolvedTeamId = teamId;
|
|
2196
2236
|
let resolvedStageId = stageId;
|
|
2197
2237
|
if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
|
|
@@ -2201,11 +2241,18 @@ async function updateProject(input) {
|
|
|
2201
2241
|
}
|
|
2202
2242
|
setNullableRef(body, "owner", ownerId);
|
|
2203
2243
|
setNullableRef(body, "team", resolvedTeamId);
|
|
2204
|
-
if (resolvedStageId) body["stage"] =
|
|
2244
|
+
if (resolvedStageId === null) body["stage"] = null;
|
|
2245
|
+
else if (resolvedStageId !== void 0) body["stage"] = resolvedStageId;
|
|
2205
2246
|
const mappedFields = mapFieldsForBody(fields);
|
|
2206
2247
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2207
2248
|
return capsulePut(`/kases/${id}`, { kase: body });
|
|
2208
2249
|
}
|
|
2250
|
+
var { schema: batchUpdateProjectSchema, handler: batchUpdateProject } = defineBatch({
|
|
2251
|
+
toolName: "batch_update_project",
|
|
2252
|
+
itemSchema: updateProjectSchema,
|
|
2253
|
+
itemDescription: "Array of 1\u201350 update_project inputs. Each item is the same shape as a single update_project call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget (~4000 req/h). Mirrors batch_update_party and batch_update_opportunity \u2014 same shape across the three entity types.",
|
|
2254
|
+
itemHandler: updateProject
|
|
2255
|
+
});
|
|
2209
2256
|
var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
2210
2257
|
toolName: "delete_project",
|
|
2211
2258
|
pathPrefix: "/kases",
|
|
@@ -2303,15 +2350,33 @@ var updateTaskSchema = z10.object({
|
|
|
2303
2350
|
),
|
|
2304
2351
|
ownerId: positiveId.optional().describe(
|
|
2305
2352
|
"Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
|
|
2353
|
+
),
|
|
2354
|
+
partyId: positiveId.nullable().optional().describe(
|
|
2355
|
+
"Re-link the task to a party by id, or `null` to orphan it. Mutually exclusive with `opportunityId` / `projectId` \u2014 Capsule enforces 'task can be related to at most one entity' server-side (422 if two parent-refs are set at once, verified in v1.6.3 wire-trace). To swap parent type atomically, pass the old one as `null` and the new one as an id in the same call. NOTE: orphaning is unique to tasks \u2014 `update_opportunity.partyId` and `update_project.partyId` are NOT nullable (Capsule rejects with 422 'party is required'). Tasks are the only entity in Capsule's data model that can exist without any parent."
|
|
2356
|
+
),
|
|
2357
|
+
opportunityId: positiveId.nullable().optional().describe(
|
|
2358
|
+
"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."
|
|
2359
|
+
),
|
|
2360
|
+
projectId: positiveId.nullable().optional().describe(
|
|
2361
|
+
"Re-link the task to a project (kase) by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
|
|
2306
2362
|
)
|
|
2307
2363
|
});
|
|
2308
2364
|
async function updateTask(input) {
|
|
2309
|
-
const { id, ownerId, ...rest } = input;
|
|
2365
|
+
const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
2366
|
+
const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
|
|
2367
|
+
if (setCount > 1) {
|
|
2368
|
+
throw new Error(
|
|
2369
|
+
"update_task: provide at most one of partyId, opportunityId, or projectId (Capsule rejects multi-parent tasks with 422 'task can be related to at most one entity')"
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2310
2372
|
const body = {};
|
|
2311
2373
|
for (const [k, v] of Object.entries(rest)) {
|
|
2312
2374
|
if (v !== void 0) body[k] = v;
|
|
2313
2375
|
}
|
|
2314
2376
|
setRef(body, "owner", ownerId);
|
|
2377
|
+
setNullableRef(body, "party", partyId);
|
|
2378
|
+
setNullableRef(body, "opportunity", opportunityId);
|
|
2379
|
+
setNullableRef(body, "kase", projectId);
|
|
2315
2380
|
return capsulePut(`/tasks/${id}`, { task: body });
|
|
2316
2381
|
}
|
|
2317
2382
|
var completeTaskSchema = z10.object({
|
|
@@ -3080,7 +3145,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3080
3145
|
const server = new McpServer(
|
|
3081
3146
|
{
|
|
3082
3147
|
name: "capsulemcp",
|
|
3083
|
-
version: "1.6.
|
|
3148
|
+
version: "1.6.5",
|
|
3084
3149
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
3085
3150
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
3086
3151
|
icons: ICONS
|
|
@@ -3176,14 +3241,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
3176
3241
|
registerTool(
|
|
3177
3242
|
server,
|
|
3178
3243
|
"create_party",
|
|
3179
|
-
"Create a new person or organisation in Capsule CRM. For type='person', firstName or lastName is required (one suffices); the `name` field is silently ignored. For type='organisation', `name` is required and firstName/lastName/title/jobTitle are silently ignored. Passing organisationId pointing at a non-organisation party (e.g. another person's id) returns 404 'organisation not found' \u2014 Capsule filters lookups by type.",
|
|
3244
|
+
"Create a new person or organisation in Capsule CRM. For type='person', firstName or lastName is required (one suffices); the `name` field is silently ignored. For type='organisation', `name` is required and firstName/lastName/title/jobTitle are silently ignored. Passing organisationId pointing at a non-organisation party (e.g. another person's id) returns 404 'organisation not found' \u2014 Capsule filters lookups by type. Accepts `ownerId` and `teamId` to set ownership at create time; both are optional and Capsule defaults `owner` to the API-token user when omitted (team has no default).",
|
|
3180
3245
|
createPartySchema,
|
|
3181
3246
|
createParty
|
|
3182
3247
|
);
|
|
3183
3248
|
registerTool(
|
|
3184
3249
|
server,
|
|
3185
3250
|
"update_party",
|
|
3186
|
-
"Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId). Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
|
|
3251
|
+
"Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId, teamId, organisationId). `ownerId` and `teamId` both accept `null` to unassign \u2014 the combination `{ownerId: null, teamId: <id>}` puts a party into 'team-owned, no specific user' state (the common pattern when transferring ownership to a team after a user departs). For PERSON parties, organisationId links to an organisation or null unlinks; for ORGANISATION parties Capsule silently ignores organisationId. Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
|
|
3187
3252
|
updatePartySchema,
|
|
3188
3253
|
updateParty
|
|
3189
3254
|
);
|
|
@@ -3318,7 +3383,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3318
3383
|
registerTool(
|
|
3319
3384
|
server,
|
|
3320
3385
|
"update_opportunity",
|
|
3321
|
-
"Update fields on an existing opportunity. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side.",
|
|
3386
|
+
"Update fields on an existing opportunity, including the parent-reference field `partyId` to reassign the opp to a different primary party. `ownerId` and `teamId` both accept `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 brings update_opportunity into parity with update_party and update_project). The combination `{ownerId: null, teamId: <id>}` puts an opportunity into 'team-owned, no specific user' state, matching the pattern available on parties and projects. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side. Capsule requires every opportunity to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required' (Unlike `update_task.partyId` which IS nullable \u2014 tasks can be orphaned in Capsule's model).",
|
|
3322
3387
|
updateOpportunitySchema,
|
|
3323
3388
|
updateOpportunity
|
|
3324
3389
|
);
|
|
@@ -3383,10 +3448,17 @@ function createCapsuleMcpServer(opts) {
|
|
|
3383
3448
|
registerTool(
|
|
3384
3449
|
server,
|
|
3385
3450
|
"update_project",
|
|
3386
|
-
"Update fields on an existing project. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning.",
|
|
3451
|
+
"Update fields on an existing project, including the parent-reference field `partyId` to reassign the project to a different primary party. `ownerId`, `teamId`, and `stageId` all accept `null` to unassign (the latter removes the project from all stages \u2014 verified empirically in v1.6.5 wire-trace). Constraint: a project must always have at least one of {owner, team} set, so `teamId: null` on a project with no owner returns 422. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning. Capsule requires every project to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required' (Unlike `update_task.partyId` which IS nullable \u2014 tasks can be orphaned in Capsule's model).",
|
|
3387
3452
|
updateProjectSchema,
|
|
3388
3453
|
updateProject
|
|
3389
3454
|
);
|
|
3455
|
+
registerBatchTool(
|
|
3456
|
+
server,
|
|
3457
|
+
"batch_update_project",
|
|
3458
|
+
"Update 1\u201350 projects in parallel. Same input shape as update_project but wrapped in an `items` array. Use this \u2014 not N sequential update_project calls \u2014 for mass stage transitions (e.g. move a board column of projects to a new stage), bulk owner reassignments after a personnel change, or batch closures. Mirrors batch_update_party and batch_update_opportunity \u2014 identical fan-out shape across the three entity types. Connector fans out parallel HTTP requests, default cap 5 (CAPSULE_MCP_BATCH_CONCURRENCY). Returns { results: [{ok, ...} per item], summary: {total, succeeded, failed} }. Partial failures possible; Capsule has no rollback.",
|
|
3459
|
+
batchUpdateProjectSchema,
|
|
3460
|
+
batchUpdateProject
|
|
3461
|
+
);
|
|
3390
3462
|
registerTool(
|
|
3391
3463
|
server,
|
|
3392
3464
|
"delete_project",
|
|
@@ -3462,7 +3534,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3462
3534
|
registerTool(
|
|
3463
3535
|
server,
|
|
3464
3536
|
"update_task",
|
|
3465
|
-
"Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), and `
|
|
3537
|
+
"Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), `ownerId`, and the parent-reference fields `partyId`, `opportunityId`, `projectId`. Pass a parent id to re-link the task, or null on a parent field to orphan/unlink it; at most one parent id may be set in a single call, though null+id swaps are allowed. Only the fields you provide are changed. To mark a task done, prefer the dedicated `complete_task` tool \u2014 it's idempotent (a no-op success on an already-completed task) and semantically clearer than `update_task status=COMPLETED`. Capsule rejects directly setting status=PENDING (which exists only internally for track-driven tasks); use OPEN or COMPLETED. Completed tasks remain fully editable \u2014 Capsule does not enforce closed-record immutability.",
|
|
3466
3538
|
updateTaskSchema,
|
|
3467
3539
|
updateTask
|
|
3468
3540
|
);
|
package/dist/index.js
CHANGED
|
@@ -1052,6 +1052,16 @@ function defineDelete(args) {
|
|
|
1052
1052
|
return { schema, handler };
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
|
+
// src/tools/preserve-refs.ts
|
|
1056
|
+
async function readEntityRefs(path, responseKey) {
|
|
1057
|
+
const { data } = await capsuleGet(path);
|
|
1058
|
+
const entity = data[responseKey];
|
|
1059
|
+
return {
|
|
1060
|
+
teamId: entity?.team?.id ?? void 0,
|
|
1061
|
+
stageId: entity?.stage?.id ?? void 0
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1055
1065
|
// src/tools/custom-field-helpers.ts
|
|
1056
1066
|
import { z as z5 } from "zod";
|
|
1057
1067
|
var CustomFieldWriteSchema = z5.object({
|
|
@@ -1227,8 +1237,11 @@ var PartyWriteBaseSchema = {
|
|
|
1227
1237
|
websites: z6.array(WebsiteSchema).optional().describe(
|
|
1228
1238
|
"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."
|
|
1229
1239
|
),
|
|
1230
|
-
ownerId: positiveId.optional().describe(
|
|
1231
|
-
"
|
|
1240
|
+
ownerId: positiveId.nullable().optional().describe(
|
|
1241
|
+
"Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on /parties has the same asymmetric owner/team semantic documented in NOTES-ON-CAPSULE-API.md \xA727 for /kases \u2014 setting `owner` while omitting `team` is plausibly clearing-prone. When you supply `ownerId` and omit `teamId`, this connector reads the party's current team and includes it in the PUT body to preserve it across the owner change. Supply `teamId` explicitly to change it."
|
|
1242
|
+
),
|
|
1243
|
+
teamId: positiveId.nullable().optional().describe(
|
|
1244
|
+
"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)."
|
|
1232
1245
|
)
|
|
1233
1246
|
};
|
|
1234
1247
|
var createPartySchema = z6.object({
|
|
@@ -1241,13 +1254,25 @@ var createPartySchema = z6.object({
|
|
|
1241
1254
|
organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
|
|
1242
1255
|
// organisation
|
|
1243
1256
|
name: z6.string().optional(),
|
|
1244
|
-
...PartyWriteBaseSchema
|
|
1257
|
+
...PartyWriteBaseSchema,
|
|
1258
|
+
ownerId: positiveId.optional().describe(
|
|
1259
|
+
"Assign to user ID. Defaults to the API-token owner when omitted. To create a team-owned party with no specific user, first create the party, then call update_party with `ownerId: null` and `teamId`."
|
|
1260
|
+
),
|
|
1261
|
+
teamId: positiveId.optional().describe(
|
|
1262
|
+
"Assign to team ID (discover via list_teams). Omit to leave team unset on create. To clear an existing team or create a team-owned party with no specific owner, use update_party after creation."
|
|
1263
|
+
),
|
|
1264
|
+
fields: z6.array(CustomFieldWriteSchema).optional().describe(
|
|
1265
|
+
fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
|
|
1266
|
+
)
|
|
1245
1267
|
});
|
|
1246
1268
|
async function createParty(input) {
|
|
1247
|
-
const { ownerId, organisationId, ...rest } = input;
|
|
1269
|
+
const { ownerId, teamId, organisationId, fields, ...rest } = input;
|
|
1248
1270
|
const body = { ...rest };
|
|
1249
1271
|
setRef(body, "owner", ownerId);
|
|
1272
|
+
setRef(body, "team", teamId);
|
|
1250
1273
|
setRef(body, "organisation", organisationId);
|
|
1274
|
+
const mappedFields = mapFieldsForBody(fields);
|
|
1275
|
+
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1251
1276
|
return capsulePost("/parties", { party: body });
|
|
1252
1277
|
}
|
|
1253
1278
|
var updatePartySchema = z6.object({
|
|
@@ -1257,16 +1282,25 @@ var updatePartySchema = z6.object({
|
|
|
1257
1282
|
title: z6.string().optional(),
|
|
1258
1283
|
jobTitle: z6.string().optional(),
|
|
1259
1284
|
name: z6.string().optional(),
|
|
1285
|
+
organisationId: positiveId.nullable().optional().describe(
|
|
1286
|
+
"For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
|
|
1287
|
+
),
|
|
1260
1288
|
fields: z6.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
|
|
1261
1289
|
...PartyWriteBaseSchema
|
|
1262
1290
|
});
|
|
1263
1291
|
async function updateParty(input) {
|
|
1264
|
-
const { id, ownerId, fields, ...rest } = input;
|
|
1292
|
+
const { id, ownerId, teamId, organisationId, fields, ...rest } = input;
|
|
1265
1293
|
const body = {};
|
|
1266
1294
|
for (const [k, v] of Object.entries(rest)) {
|
|
1267
1295
|
if (v !== void 0) body[k] = v;
|
|
1268
1296
|
}
|
|
1269
|
-
|
|
1297
|
+
let resolvedTeamId = teamId;
|
|
1298
|
+
if (ownerId !== void 0 && teamId === void 0) {
|
|
1299
|
+
({ teamId: resolvedTeamId } = await readEntityRefs(`/parties/${id}`, "party"));
|
|
1300
|
+
}
|
|
1301
|
+
setNullableRef(body, "owner", ownerId);
|
|
1302
|
+
setNullableRef(body, "team", resolvedTeamId);
|
|
1303
|
+
setNullableRef(body, "organisation", organisationId);
|
|
1270
1304
|
const mappedFields = mapFieldsForBody(fields);
|
|
1271
1305
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1272
1306
|
return capsulePut(`/parties/${id}`, { party: body });
|
|
@@ -1433,18 +1467,6 @@ async function removePartyWebsiteById(input) {
|
|
|
1433
1467
|
|
|
1434
1468
|
// src/tools/opportunities.ts
|
|
1435
1469
|
import { z as z7 } from "zod";
|
|
1436
|
-
|
|
1437
|
-
// src/tools/preserve-refs.ts
|
|
1438
|
-
async function readEntityRefs(path, responseKey) {
|
|
1439
|
-
const { data } = await capsuleGet(path);
|
|
1440
|
-
const entity = data[responseKey];
|
|
1441
|
-
return {
|
|
1442
|
-
teamId: entity?.team?.id ?? void 0,
|
|
1443
|
-
stageId: entity?.stage?.id ?? void 0
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// src/tools/opportunities.ts
|
|
1448
1470
|
var OpportunityValueSchema = z7.object({
|
|
1449
1471
|
amount: z7.number().nonnegative(),
|
|
1450
1472
|
currency: z7.string({
|
|
@@ -1515,14 +1537,17 @@ var createOpportunitySchema = z7.object({
|
|
|
1515
1537
|
expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1516
1538
|
probability: z7.number().int().min(0).max(100).optional(),
|
|
1517
1539
|
ownerId: positiveId.optional().describe(
|
|
1518
|
-
"Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it.
|
|
1540
|
+
"Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. To clear owner later, call update_opportunity with `ownerId: null`. Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
|
|
1519
1541
|
),
|
|
1520
1542
|
teamId: positiveId.optional().describe(
|
|
1521
1543
|
"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)."
|
|
1544
|
+
),
|
|
1545
|
+
fields: z7.array(CustomFieldWriteSchema).optional().describe(
|
|
1546
|
+
fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
|
|
1522
1547
|
)
|
|
1523
1548
|
});
|
|
1524
1549
|
async function createOpportunity(input) {
|
|
1525
|
-
const { partyId, milestoneId, ownerId, teamId, ...rest } = input;
|
|
1550
|
+
const { partyId, milestoneId, ownerId, teamId, fields, ...rest } = input;
|
|
1526
1551
|
const body = {
|
|
1527
1552
|
...rest,
|
|
1528
1553
|
party: { id: partyId },
|
|
@@ -1530,11 +1555,16 @@ async function createOpportunity(input) {
|
|
|
1530
1555
|
};
|
|
1531
1556
|
setRef(body, "owner", ownerId);
|
|
1532
1557
|
setRef(body, "team", teamId);
|
|
1558
|
+
const mappedFields = mapFieldsForBody(fields);
|
|
1559
|
+
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1533
1560
|
return capsulePost("/opportunities", { opportunity: body });
|
|
1534
1561
|
}
|
|
1535
1562
|
var updateOpportunitySchema = z7.object({
|
|
1536
1563
|
id: positiveId,
|
|
1537
1564
|
name: z7.string().min(1).optional(),
|
|
1565
|
+
partyId: positiveId.optional().describe(
|
|
1566
|
+
"Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_project.partyId`."
|
|
1567
|
+
),
|
|
1538
1568
|
milestoneId: positiveId.optional().describe(
|
|
1539
1569
|
"Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
|
|
1540
1570
|
),
|
|
@@ -1547,8 +1577,8 @@ var updateOpportunitySchema = z7.object({
|
|
|
1547
1577
|
lostReasonId: positiveId.optional().describe(
|
|
1548
1578
|
"Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lostreasons."
|
|
1549
1579
|
),
|
|
1550
|
-
ownerId: positiveId.optional().describe(
|
|
1551
|
-
"Reassign owner
|
|
1580
|
+
ownerId: positiveId.nullable().optional().describe(
|
|
1581
|
+
"Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
|
|
1552
1582
|
),
|
|
1553
1583
|
teamId: positiveId.nullable().optional().describe(
|
|
1554
1584
|
"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."
|
|
@@ -1556,17 +1586,18 @@ var updateOpportunitySchema = z7.object({
|
|
|
1556
1586
|
fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
|
|
1557
1587
|
});
|
|
1558
1588
|
async function updateOpportunity(input) {
|
|
1559
|
-
const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
1589
|
+
const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
1560
1590
|
const body = {};
|
|
1561
1591
|
for (const [k, v] of Object.entries(rest)) {
|
|
1562
1592
|
if (v !== void 0) body[k] = v;
|
|
1563
1593
|
}
|
|
1594
|
+
setRef(body, "party", partyId);
|
|
1564
1595
|
setRef(body, "milestone", milestoneId);
|
|
1565
1596
|
let resolvedTeamId = teamId;
|
|
1566
1597
|
if (ownerId !== void 0 && teamId === void 0) {
|
|
1567
1598
|
({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
|
|
1568
1599
|
}
|
|
1569
|
-
|
|
1600
|
+
setNullableRef(body, "owner", ownerId);
|
|
1570
1601
|
setNullableRef(body, "team", resolvedTeamId);
|
|
1571
1602
|
setRef(body, "lostReason", lostReasonId);
|
|
1572
1603
|
const mappedFields = mapFieldsForBody(fields);
|
|
@@ -1650,10 +1681,13 @@ var createProjectSchema = z8.object({
|
|
|
1650
1681
|
stageId: positiveId.optional().describe(
|
|
1651
1682
|
"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."
|
|
1652
1683
|
),
|
|
1653
|
-
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
|
|
1684
|
+
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1685
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
1686
|
+
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
1687
|
+
)
|
|
1654
1688
|
});
|
|
1655
1689
|
async function createProject(input) {
|
|
1656
|
-
const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
|
|
1690
|
+
const { partyId, ownerId, teamId, status, stageId, fields, ...rest } = input;
|
|
1657
1691
|
const body = {
|
|
1658
1692
|
...rest,
|
|
1659
1693
|
status: status ?? "OPEN",
|
|
@@ -1662,6 +1696,8 @@ async function createProject(input) {
|
|
|
1662
1696
|
setRef(body, "owner", ownerId);
|
|
1663
1697
|
setRef(body, "team", teamId);
|
|
1664
1698
|
if (stageId) body["stage"] = stageId;
|
|
1699
|
+
const mappedFields = mapFieldsForBody(fields);
|
|
1700
|
+
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1665
1701
|
return capsulePost("/kases", { kase: body });
|
|
1666
1702
|
}
|
|
1667
1703
|
var updateProjectSchema = z8.object({
|
|
@@ -1669,14 +1705,17 @@ var updateProjectSchema = z8.object({
|
|
|
1669
1705
|
name: z8.string().min(1).optional(),
|
|
1670
1706
|
description: z8.string().optional(),
|
|
1671
1707
|
status: z8.enum(["OPEN", "CLOSED"]).optional(),
|
|
1708
|
+
partyId: positiveId.optional().describe(
|
|
1709
|
+
"Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_opportunity.partyId`."
|
|
1710
|
+
),
|
|
1672
1711
|
ownerId: positiveId.nullable().optional().describe(
|
|
1673
1712
|
"Reassign owner: pass a user ID to set, or `null` to unassign (matches the 'Unassign' option in Capsule's web UI). When you supply `ownerId` and omit `teamId` and/or `stageId`, the connector fetches the project's current omitted fields and includes them in the PUT body \u2014 this preserves them across the owner change (without it, Capsule's PUT would clear team; stage carry is defensive against the symmetric clear). Supply `teamId` and/or `stageId` explicitly on the same call to change them instead. `teamId: null` clears the team as part of an owner change. Constraints (Capsule enforces, 422 on violation): owner must be a member of the team if both are set; a project must always have at least one of {owner, team} set (cannot clear both)."
|
|
1674
1713
|
),
|
|
1675
1714
|
teamId: positiveId.nullable().optional().describe(
|
|
1676
1715
|
"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'."
|
|
1677
1716
|
),
|
|
1678
|
-
stageId: positiveId.optional().describe(
|
|
1679
|
-
"Move the project to this stage (board column). 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."
|
|
1717
|
+
stageId: positiveId.nullable().optional().describe(
|
|
1718
|
+
"Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
|
|
1680
1719
|
),
|
|
1681
1720
|
expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
1682
1721
|
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
@@ -1684,11 +1723,12 @@ var updateProjectSchema = z8.object({
|
|
|
1684
1723
|
)
|
|
1685
1724
|
});
|
|
1686
1725
|
async function updateProject(input) {
|
|
1687
|
-
const { id, ownerId, teamId, stageId, fields, ...rest } = input;
|
|
1726
|
+
const { id, partyId, ownerId, teamId, stageId, fields, ...rest } = input;
|
|
1688
1727
|
const body = {};
|
|
1689
1728
|
for (const [k, v] of Object.entries(rest)) {
|
|
1690
1729
|
if (v !== void 0) body[k] = v;
|
|
1691
1730
|
}
|
|
1731
|
+
setRef(body, "party", partyId);
|
|
1692
1732
|
let resolvedTeamId = teamId;
|
|
1693
1733
|
let resolvedStageId = stageId;
|
|
1694
1734
|
if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
|
|
@@ -1698,11 +1738,18 @@ async function updateProject(input) {
|
|
|
1698
1738
|
}
|
|
1699
1739
|
setNullableRef(body, "owner", ownerId);
|
|
1700
1740
|
setNullableRef(body, "team", resolvedTeamId);
|
|
1701
|
-
if (resolvedStageId) body["stage"] =
|
|
1741
|
+
if (resolvedStageId === null) body["stage"] = null;
|
|
1742
|
+
else if (resolvedStageId !== void 0) body["stage"] = resolvedStageId;
|
|
1702
1743
|
const mappedFields = mapFieldsForBody(fields);
|
|
1703
1744
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1704
1745
|
return capsulePut(`/kases/${id}`, { kase: body });
|
|
1705
1746
|
}
|
|
1747
|
+
var { schema: batchUpdateProjectSchema, handler: batchUpdateProject } = defineBatch({
|
|
1748
|
+
toolName: "batch_update_project",
|
|
1749
|
+
itemSchema: updateProjectSchema,
|
|
1750
|
+
itemDescription: "Array of 1\u201350 update_project inputs. Each item is the same shape as a single update_project call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget (~4000 req/h). Mirrors batch_update_party and batch_update_opportunity \u2014 same shape across the three entity types.",
|
|
1751
|
+
itemHandler: updateProject
|
|
1752
|
+
});
|
|
1706
1753
|
var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
1707
1754
|
toolName: "delete_project",
|
|
1708
1755
|
pathPrefix: "/kases",
|
|
@@ -1800,15 +1847,33 @@ var updateTaskSchema = z9.object({
|
|
|
1800
1847
|
),
|
|
1801
1848
|
ownerId: positiveId.optional().describe(
|
|
1802
1849
|
"Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
|
|
1850
|
+
),
|
|
1851
|
+
partyId: positiveId.nullable().optional().describe(
|
|
1852
|
+
"Re-link the task to a party by id, or `null` to orphan it. Mutually exclusive with `opportunityId` / `projectId` \u2014 Capsule enforces 'task can be related to at most one entity' server-side (422 if two parent-refs are set at once, verified in v1.6.3 wire-trace). To swap parent type atomically, pass the old one as `null` and the new one as an id in the same call. NOTE: orphaning is unique to tasks \u2014 `update_opportunity.partyId` and `update_project.partyId` are NOT nullable (Capsule rejects with 422 'party is required'). Tasks are the only entity in Capsule's data model that can exist without any parent."
|
|
1853
|
+
),
|
|
1854
|
+
opportunityId: positiveId.nullable().optional().describe(
|
|
1855
|
+
"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."
|
|
1856
|
+
),
|
|
1857
|
+
projectId: positiveId.nullable().optional().describe(
|
|
1858
|
+
"Re-link the task to a project (kase) by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
|
|
1803
1859
|
)
|
|
1804
1860
|
});
|
|
1805
1861
|
async function updateTask(input) {
|
|
1806
|
-
const { id, ownerId, ...rest } = input;
|
|
1862
|
+
const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
1863
|
+
const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
|
|
1864
|
+
if (setCount > 1) {
|
|
1865
|
+
throw new Error(
|
|
1866
|
+
"update_task: provide at most one of partyId, opportunityId, or projectId (Capsule rejects multi-parent tasks with 422 'task can be related to at most one entity')"
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1807
1869
|
const body = {};
|
|
1808
1870
|
for (const [k, v] of Object.entries(rest)) {
|
|
1809
1871
|
if (v !== void 0) body[k] = v;
|
|
1810
1872
|
}
|
|
1811
1873
|
setRef(body, "owner", ownerId);
|
|
1874
|
+
setNullableRef(body, "party", partyId);
|
|
1875
|
+
setNullableRef(body, "opportunity", opportunityId);
|
|
1876
|
+
setNullableRef(body, "kase", projectId);
|
|
1812
1877
|
return capsulePut(`/tasks/${id}`, { task: body });
|
|
1813
1878
|
}
|
|
1814
1879
|
var completeTaskSchema = z9.object({
|
|
@@ -2577,7 +2642,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2577
2642
|
const server2 = new McpServer(
|
|
2578
2643
|
{
|
|
2579
2644
|
name: "capsulemcp",
|
|
2580
|
-
version: "1.6.
|
|
2645
|
+
version: "1.6.5",
|
|
2581
2646
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
2582
2647
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
2583
2648
|
icons: ICONS
|
|
@@ -2673,14 +2738,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
2673
2738
|
registerTool(
|
|
2674
2739
|
server2,
|
|
2675
2740
|
"create_party",
|
|
2676
|
-
"Create a new person or organisation in Capsule CRM. For type='person', firstName or lastName is required (one suffices); the `name` field is silently ignored. For type='organisation', `name` is required and firstName/lastName/title/jobTitle are silently ignored. Passing organisationId pointing at a non-organisation party (e.g. another person's id) returns 404 'organisation not found' \u2014 Capsule filters lookups by type.",
|
|
2741
|
+
"Create a new person or organisation in Capsule CRM. For type='person', firstName or lastName is required (one suffices); the `name` field is silently ignored. For type='organisation', `name` is required and firstName/lastName/title/jobTitle are silently ignored. Passing organisationId pointing at a non-organisation party (e.g. another person's id) returns 404 'organisation not found' \u2014 Capsule filters lookups by type. Accepts `ownerId` and `teamId` to set ownership at create time; both are optional and Capsule defaults `owner` to the API-token user when omitted (team has no default).",
|
|
2677
2742
|
createPartySchema,
|
|
2678
2743
|
createParty
|
|
2679
2744
|
);
|
|
2680
2745
|
registerTool(
|
|
2681
2746
|
server2,
|
|
2682
2747
|
"update_party",
|
|
2683
|
-
"Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId). Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
|
|
2748
|
+
"Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId, teamId, organisationId). `ownerId` and `teamId` both accept `null` to unassign \u2014 the combination `{ownerId: null, teamId: <id>}` puts a party into 'team-owned, no specific user' state (the common pattern when transferring ownership to a team after a user departs). For PERSON parties, organisationId links to an organisation or null unlinks; for ORGANISATION parties Capsule silently ignores organisationId. Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
|
|
2684
2749
|
updatePartySchema,
|
|
2685
2750
|
updateParty
|
|
2686
2751
|
);
|
|
@@ -2815,7 +2880,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2815
2880
|
registerTool(
|
|
2816
2881
|
server2,
|
|
2817
2882
|
"update_opportunity",
|
|
2818
|
-
"Update fields on an existing opportunity. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side.",
|
|
2883
|
+
"Update fields on an existing opportunity, including the parent-reference field `partyId` to reassign the opp to a different primary party. `ownerId` and `teamId` both accept `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 brings update_opportunity into parity with update_party and update_project). The combination `{ownerId: null, teamId: <id>}` puts an opportunity into 'team-owned, no specific user' state, matching the pattern available on parties and projects. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side. Capsule requires every opportunity to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required' (Unlike `update_task.partyId` which IS nullable \u2014 tasks can be orphaned in Capsule's model).",
|
|
2819
2884
|
updateOpportunitySchema,
|
|
2820
2885
|
updateOpportunity
|
|
2821
2886
|
);
|
|
@@ -2880,10 +2945,17 @@ function createCapsuleMcpServer(opts) {
|
|
|
2880
2945
|
registerTool(
|
|
2881
2946
|
server2,
|
|
2882
2947
|
"update_project",
|
|
2883
|
-
"Update fields on an existing project. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning.",
|
|
2948
|
+
"Update fields on an existing project, including the parent-reference field `partyId` to reassign the project to a different primary party. `ownerId`, `teamId`, and `stageId` all accept `null` to unassign (the latter removes the project from all stages \u2014 verified empirically in v1.6.5 wire-trace). Constraint: a project must always have at least one of {owner, team} set, so `teamId: null` on a project with no owner returns 422. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning. Capsule requires every project to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required' (Unlike `update_task.partyId` which IS nullable \u2014 tasks can be orphaned in Capsule's model).",
|
|
2884
2949
|
updateProjectSchema,
|
|
2885
2950
|
updateProject
|
|
2886
2951
|
);
|
|
2952
|
+
registerBatchTool(
|
|
2953
|
+
server2,
|
|
2954
|
+
"batch_update_project",
|
|
2955
|
+
"Update 1\u201350 projects in parallel. Same input shape as update_project but wrapped in an `items` array. Use this \u2014 not N sequential update_project calls \u2014 for mass stage transitions (e.g. move a board column of projects to a new stage), bulk owner reassignments after a personnel change, or batch closures. Mirrors batch_update_party and batch_update_opportunity \u2014 identical fan-out shape across the three entity types. Connector fans out parallel HTTP requests, default cap 5 (CAPSULE_MCP_BATCH_CONCURRENCY). Returns { results: [{ok, ...} per item], summary: {total, succeeded, failed} }. Partial failures possible; Capsule has no rollback.",
|
|
2956
|
+
batchUpdateProjectSchema,
|
|
2957
|
+
batchUpdateProject
|
|
2958
|
+
);
|
|
2887
2959
|
registerTool(
|
|
2888
2960
|
server2,
|
|
2889
2961
|
"delete_project",
|
|
@@ -2959,7 +3031,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2959
3031
|
registerTool(
|
|
2960
3032
|
server2,
|
|
2961
3033
|
"update_task",
|
|
2962
|
-
"Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), and `
|
|
3034
|
+
"Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), `ownerId`, and the parent-reference fields `partyId`, `opportunityId`, `projectId`. Pass a parent id to re-link the task, or null on a parent field to orphan/unlink it; at most one parent id may be set in a single call, though null+id swaps are allowed. Only the fields you provide are changed. To mark a task done, prefer the dedicated `complete_task` tool \u2014 it's idempotent (a no-op success on an already-completed task) and semantically clearer than `update_task status=COMPLETED`. Capsule rejects directly setting status=PENDING (which exists only internally for track-driven tasks); use OPEN or COMPLETED. Completed tasks remain fully editable \u2014 Capsule does not enforce closed-record immutability.",
|
|
2963
3035
|
updateTaskSchema,
|
|
2964
3036
|
updateTask
|
|
2965
3037
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capsulemcp",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
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",
|