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