capsulemcp 1.8.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/http.js +183 -101
- package/dist/index.js +183 -101
- package/package.json +1 -1
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 });
|
|
@@ -1910,7 +1948,7 @@ var PartyWriteBaseSchema = {
|
|
|
1910
1948
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
|
|
1911
1949
|
),
|
|
1912
1950
|
ownerId: positiveId.nullable().optional().describe(
|
|
1913
|
-
"Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on
|
|
1951
|
+
"Pass a user ID to set, or `null` to unassign (verified empirically in v1.6.4 wire-trace \u2014 Capsule accepts `owner: null` on PUT /parties/:id for both persons and organisations). Discover IDs via list_users. WARNING: Capsule's PUT on parties has the same asymmetric owner/team semantic documented in NOTES-ON-CAPSULE-API.md \xA727 for project updates \u2014 setting `owner` while omitting `team` is plausibly clearing-prone. When you supply `ownerId` and omit `teamId`, this connector reads the party's current team and includes it in the PUT body to preserve it across the owner change. Supply `teamId` explicitly to change it."
|
|
1914
1952
|
),
|
|
1915
1953
|
teamId: positiveId.nullable().optional().describe(
|
|
1916
1954
|
"Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
|
|
@@ -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 });
|
|
@@ -2169,7 +2222,7 @@ var createOpportunitySchema = z9.object({
|
|
|
2169
2222
|
"Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
|
|
2170
2223
|
),
|
|
2171
2224
|
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
2172
|
-
fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on
|
|
2225
|
+
fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on party and project creation \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
|
|
2173
2226
|
)
|
|
2174
2227
|
});
|
|
2175
2228
|
async function createOpportunity(input) {
|
|
@@ -2201,10 +2254,10 @@ 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
|
-
"Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as
|
|
2260
|
+
"Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as project updates). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
|
|
2208
2261
|
),
|
|
2209
2262
|
teamId: positiveId.nullable().optional().describe(
|
|
2210
2263
|
"Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
|
|
@@ -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),
|
|
@@ -2294,7 +2361,7 @@ var createProjectSchema = z10.object({
|
|
|
2294
2361
|
),
|
|
2295
2362
|
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2296
2363
|
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2297
|
-
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's
|
|
2364
|
+
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's project create endpoint accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
2298
2365
|
)
|
|
2299
2366
|
});
|
|
2300
2367
|
async function createProject(input) {
|
|
@@ -2326,7 +2393,7 @@ var updateProjectSchema = z10.object({
|
|
|
2326
2393
|
"Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_project { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. A project must always have at least one of {owner, team} set \u2014 `teamId: null` on a project whose owner is already null returns 422 'owner or team is required'."
|
|
2327
2394
|
),
|
|
2328
2395
|
stageId: positiveId.nullable().optional().describe(
|
|
2329
|
-
"Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on
|
|
2396
|
+
"Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on project update and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
|
|
2330
2397
|
),
|
|
2331
2398
|
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2332
2399
|
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
@@ -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
|
}
|
|
@@ -2364,7 +2431,7 @@ var { schema: batchUpdateProjectSchema, handler: batchUpdateProject } = defineBa
|
|
|
2364
2431
|
var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
2365
2432
|
toolName: "delete_project",
|
|
2366
2433
|
pathPrefix: "/kases",
|
|
2367
|
-
confirmHint: "Must be set to true. Permanently deletes the project
|
|
2434
|
+
confirmHint: "Must be set to true. Permanently deletes the project. Consider update_project status='CLOSED' instead. Irreversible."
|
|
2368
2435
|
});
|
|
2369
2436
|
|
|
2370
2437
|
// src/tools/tasks.ts
|
|
@@ -2452,7 +2519,7 @@ var updateTaskSchema = z11.object({
|
|
|
2452
2519
|
"Re-link the task to an opportunity by id, or `null` to orphan it. Mutually exclusive with `partyId` / `projectId` \u2014 see `partyId` for the XOR semantic."
|
|
2453
2520
|
),
|
|
2454
2521
|
projectId: positiveId.nullable().optional().describe(
|
|
2455
|
-
"Re-link the task to a project
|
|
2522
|
+
"Re-link the task to a project by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
|
|
2456
2523
|
)
|
|
2457
2524
|
});
|
|
2458
2525
|
async function updateTask(input) {
|
|
@@ -2494,13 +2561,13 @@ 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,
|
|
2501
2568
|
...listEntriesPagination,
|
|
2502
2569
|
includeLinkedPersons: z12.boolean().optional().describe(
|
|
2503
|
-
"When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422
|
|
2570
|
+
"When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party, opportunity, or project row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
|
|
2504
2571
|
)
|
|
2505
2572
|
});
|
|
2506
2573
|
var PER_PARTY_FETCH_CAP = 100;
|
|
@@ -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) {
|
|
@@ -2736,7 +2803,7 @@ async function listTags(input) {
|
|
|
2736
2803
|
}
|
|
2737
2804
|
var addTagSchema = z15.object({
|
|
2738
2805
|
entity: TagEntity,
|
|
2739
|
-
entityId: positiveId.describe("The party/opportunity/
|
|
2806
|
+
entityId: positiveId.describe("The party/opportunity/project id."),
|
|
2740
2807
|
tagName: z15.string().min(1).describe(
|
|
2741
2808
|
"Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
|
|
2742
2809
|
)
|
|
@@ -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");
|
|
@@ -2752,7 +2819,7 @@ async function addTag(input) {
|
|
|
2752
2819
|
}
|
|
2753
2820
|
var removeTagByIdSchema = z15.object({
|
|
2754
2821
|
entity: TagEntity,
|
|
2755
|
-
entityId: positiveId.describe("The party/opportunity/
|
|
2822
|
+
entityId: positiveId.describe("The party/opportunity/project id."),
|
|
2756
2823
|
tagId: positiveId.describe(
|
|
2757
2824
|
"The tag's id. Read via get_party / get_opportunity / get_project with embed='tags' \u2014 each tag entry in the response has an `id` field. list_tags returns the same ids for the same tags, so either source works; reading via embed first is the safer pattern because it confirms the tag is actually attached to this entity before you try to remove it (otherwise Capsule returns 422 'tag not found to delete'). Removing detaches the tag from this entity only; the tag definition itself persists in the tenant for other entities that share it."
|
|
2758
2825
|
)
|
|
@@ -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
|
+
`/${ENTITY_PATH[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.1",
|
|
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
|
|
@@ -3342,7 +3417,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3342
3417
|
registerTool(
|
|
3343
3418
|
server,
|
|
3344
3419
|
"list_party_projects",
|
|
3345
|
-
"List projects
|
|
3420
|
+
"List projects linked to a given party. Returns the same record shape as get_project, filtered to one party \u2014 use this to answer 'what projects is X involved in?' without enumerating all projects. Accepts optional embed (e.g. 'tags,fields'). For the opportunity-side analogue, use list_party_opportunities.",
|
|
3346
3421
|
listPartyProjectsSchema,
|
|
3347
3422
|
listPartyProjects
|
|
3348
3423
|
);
|
|
@@ -3356,7 +3431,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3356
3431
|
registerTool(
|
|
3357
3432
|
server,
|
|
3358
3433
|
"list_custom_fields",
|
|
3359
|
-
"List custom field DEFINITIONS for an entity type (parties, opportunities, or projects
|
|
3434
|
+
"List custom field DEFINITIONS for an entity type (parties, opportunities, or projects). Returns the schema \u2014 name, type, options for list-type fields, etc. \u2014 NOT the values on any specific record. To read values on a record, use get_party / get_opportunity / get_project with embed=fields.",
|
|
3360
3435
|
listCustomFieldsSchema,
|
|
3361
3436
|
listCustomFields
|
|
3362
3437
|
);
|
|
@@ -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,14 +3573,14 @@ 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
|
);
|
|
3505
3580
|
registerTool(
|
|
3506
3581
|
server,
|
|
3507
3582
|
"list_associated_projects",
|
|
3508
|
-
"List projects
|
|
3583
|
+
"List projects associated with a given opportunity. Returns the same record shape as list_projects, filtered to one opportunity. The inverse direction (project \u2192 opportunity) is on each project's `opportunity` field directly, so this tool is only needed for opportunity \u2192 projects discovery \u2014 use list_party_projects for party \u2192 projects.",
|
|
3509
3584
|
listAssociatedProjectsSchema,
|
|
3510
3585
|
listAssociatedProjects
|
|
3511
3586
|
);
|
|
@@ -3539,38 +3614,45 @@ 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
|
|
3627
|
+
"List projects in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For free-text matching use search_projects; for structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
|
|
3546
3628
|
listProjectsSchema,
|
|
3547
3629
|
listProjects
|
|
3548
3630
|
);
|
|
3549
3631
|
registerTool(
|
|
3550
3632
|
server,
|
|
3551
3633
|
"filter_projects",
|
|
3552
|
-
"Filter projects
|
|
3634
|
+
"Filter projects by structured conditions (date ranges, status, tags, owner). Use this \u2014 not list_projects \u2014 for questions like 'most recent project', 'projects opened this month'. Capsule's API does not support ad-hoc sort, but for 'most recent X' you can filter by a date field and pick the highest-id row \u2014 Capsule IDs are monotonic, so newest id = newest record.",
|
|
3553
3635
|
filterProjectsSchema,
|
|
3554
3636
|
filterProjects
|
|
3555
3637
|
);
|
|
3556
3638
|
registerTool(
|
|
3557
3639
|
server,
|
|
3558
3640
|
"get_project",
|
|
3559
|
-
"Fetch a single project
|
|
3641
|
+
"Fetch a single project by its numeric id. Returns the full record including name, description, status (OPEN/CLOSED), owner, stage, board, opportunityId (if linked), and timestamps. Use embed='tags,fields' to include attached tags and custom field values in one round-trip. For batch fetches of up to 50 projects at once, use get_projects instead. For the project's timeline (notes, captured emails, completed-task records) use list_project_entries.",
|
|
3560
3642
|
getProjectSchema,
|
|
3561
3643
|
getProject
|
|
3562
3644
|
);
|
|
3563
3645
|
registerTool(
|
|
3564
3646
|
server,
|
|
3565
3647
|
"get_projects",
|
|
3566
|
-
"Batch-fetch up to 50 projects
|
|
3648
|
+
"Batch-fetch up to 50 projects by ID. For 1\u201310 ids this is a single Capsule round trip; for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged.",
|
|
3567
3649
|
getProjectsSchema,
|
|
3568
3650
|
getProjects
|
|
3569
3651
|
);
|
|
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
|
);
|
|
@@ -3578,7 +3660,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3578
3660
|
registerTool(
|
|
3579
3661
|
server,
|
|
3580
3662
|
"create_project",
|
|
3581
|
-
"Create a new project
|
|
3663
|
+
"Create a new project in Capsule CRM linked to a party. Requires partyId and name; description, status, owner, and starting board/stage are optional. To pin a project to a specific board+stage on creation, pass stageId (which uniquely identifies a stage within a board). Discover valid ids via list_boards + list_stages. Returns the created project including its assigned id.",
|
|
3582
3664
|
createProjectSchema,
|
|
3583
3665
|
createProject
|
|
3584
3666
|
);
|
|
@@ -3599,7 +3681,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3599
3681
|
registerTool(
|
|
3600
3682
|
server,
|
|
3601
3683
|
"delete_project",
|
|
3602
|
-
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a project
|
|
3684
|
+
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a project. Prefer update_project with status='CLOSED' to close a project while preserving history. Requires confirm=true. Always read the project first with get_project and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the project was already gone.",
|
|
3603
3685
|
deleteProjectSchema,
|
|
3604
3686
|
deleteProject
|
|
3605
3687
|
);
|
|
@@ -3714,7 +3796,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3714
3796
|
registerTool(
|
|
3715
3797
|
server,
|
|
3716
3798
|
"list_project_entries",
|
|
3717
|
-
"List timeline entries (notes, captured emails, completed-task records) for a project
|
|
3799
|
+
"List timeline entries (notes, captured emails, completed-task records) for a project. Returns entries newest-first. Each entry has a type ('note', 'email', 'task'), free-text content, and timestamps. Use this to answer 'what's the latest on project X?' For party or opportunity timelines, use list_party_entries or list_opportunity_entries respectively.",
|
|
3718
3800
|
listProjectEntriesSchema,
|
|
3719
3801
|
listProjectEntries
|
|
3720
3802
|
);
|
|
@@ -3865,14 +3947,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
3865
3947
|
registerTool(
|
|
3866
3948
|
server,
|
|
3867
3949
|
"list_boards",
|
|
3868
|
-
"List all project
|
|
3950
|
+
"List all project boards defined in Capsule. A board is a grouping of stages that projects flow through \u2014 the project equivalent of an opportunity pipeline. Returns each board's id, name, and stages. Use this to discover boardId when creating a project, then pick a starting stage via list_stages. Like pipelines, boards are stable per account.",
|
|
3869
3951
|
listBoardsSchema,
|
|
3870
3952
|
listBoards
|
|
3871
3953
|
);
|
|
3872
3954
|
registerTool(
|
|
3873
3955
|
server,
|
|
3874
3956
|
"list_stages",
|
|
3875
|
-
"List project
|
|
3957
|
+
"List project stages. Without arguments returns every stage across every board (each entry carries a `.board` reference so you can tell them apart). Pass `boardId` to scope the result to one specific board's stages. Use this to discover the numeric `stage.id` that `create_project` / `update_project` consume \u2014 stage names alone won't do, Capsule resolves by id. For opportunity (deal) stages, use `list_pipelines` instead \u2014 opportunities don't have stages in the project sense.",
|
|
3876
3958
|
listStagesSchema,
|
|
3877
3959
|
listStages
|
|
3878
3960
|
);
|
|
@@ -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
|
);
|
|
@@ -3964,21 +4046,21 @@ function createCapsuleMcpServer(opts) {
|
|
|
3964
4046
|
registerTool(
|
|
3965
4047
|
server,
|
|
3966
4048
|
"add_tag",
|
|
3967
|
-
"Attach a tag to a party, opportunity, or project
|
|
4049
|
+
"Attach a tag to a party, opportunity, or project by NAME. Capsule resolves to an existing tag in the tenant or creates a fresh one with this name. Matching is case-insensitive \u2014 'VIP' and 'vip' attach the same tag, preserving the canonical casing from whichever variant was created first. To avoid creating a genuinely-distinct near-duplicate (e.g. 'VIP' vs 'V.I.P.'), call list_tags first and reuse the exact name. Idempotent \u2014 re-attaching an already-attached tag is harmless. To DETACH a tag, use remove_tag_by_id with the tag's id (read via get_party/get_opportunity/get_project with embed='tags').",
|
|
3968
4050
|
addTagSchema,
|
|
3969
4051
|
addTag
|
|
3970
4052
|
);
|
|
3971
4053
|
registerTool(
|
|
3972
4054
|
server,
|
|
3973
4055
|
"remove_tag_by_id",
|
|
3974
|
-
"Detach a tag from a party, opportunity, or project
|
|
4056
|
+
"Detach a tag from a party, opportunity, or project. Atomic \u2014 one PUT to Capsule. Reversible \u2014 no `confirm: true` gate (re-attach with add_tag using the same tag name). The `tagId` parameter is the tag's id, readable via get_party/get_opportunity/get_project with embed='tags' (list_tags returns the same ids and also works, but reading via embed first confirms the tag is actually attached to this entity). The tag definition itself remains in the tenant for other entities that still share it. Idempotent on retry: response is `{removed: true, alreadyRemoved: false, entity, entityId, tagId, ...<updated entity>}` on a fresh detach or `{removed: true, alreadyRemoved: true, entity, entityId, tagId}` if the tag was already detached (Capsule's 422 'tag not found to delete' is caught and converted).",
|
|
3975
4057
|
removeTagByIdSchema,
|
|
3976
4058
|
removeTagById
|
|
3977
4059
|
);
|
|
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
|
);
|