exponential-mcp 0.6.0 → 0.7.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 +27 -0
- package/dist/cli.js +0 -0
- package/dist/domains/actions.d.ts +2 -0
- package/dist/domains/actions.js +157 -0
- package/dist/domains/areas.d.ts +2 -0
- package/dist/domains/areas.js +22 -0
- package/dist/domains/comments.d.ts +2 -0
- package/dist/domains/comments.js +165 -0
- package/dist/domains/contacts.d.ts +2 -0
- package/dist/domains/contacts.js +105 -0
- package/dist/domains/deals.d.ts +2 -0
- package/dist/domains/deals.js +77 -0
- package/dist/domains/decisions.d.ts +2 -0
- package/dist/domains/decisions.js +160 -0
- package/dist/domains/epics.d.ts +2 -0
- package/dist/domains/epics.js +54 -0
- package/dist/domains/features.d.ts +2 -0
- package/dist/domains/features.js +89 -0
- package/dist/domains/framework.d.ts +34 -0
- package/dist/domains/framework.js +94 -0
- package/dist/domains/goals.d.ts +2 -0
- package/dist/domains/goals.js +121 -0
- package/dist/domains/index.d.ts +5 -0
- package/dist/domains/index.js +48 -0
- package/dist/domains/keyResults.d.ts +2 -0
- package/dist/domains/keyResults.js +112 -0
- package/dist/domains/labels.d.ts +2 -0
- package/dist/domains/labels.js +77 -0
- package/dist/domains/meetings.d.ts +2 -0
- package/dist/domains/meetings.js +90 -0
- package/dist/domains/organizations.d.ts +2 -0
- package/dist/domains/organizations.js +38 -0
- package/dist/domains/pages.d.ts +2 -0
- package/dist/domains/pages.js +44 -0
- package/dist/domains/products.d.ts +2 -0
- package/dist/domains/products.js +57 -0
- package/dist/domains/projects.d.ts +2 -0
- package/dist/domains/projects.js +57 -0
- package/dist/domains/requirements.d.ts +2 -0
- package/dist/domains/requirements.js +39 -0
- package/dist/domains/scopes.d.ts +2 -0
- package/dist/domains/scopes.js +42 -0
- package/dist/domains/stories.d.ts +2 -0
- package/dist/domains/stories.js +46 -0
- package/dist/domains/tickets.d.ts +2 -0
- package/dist/domains/tickets.js +135 -0
- package/dist/domains/time.d.ts +2 -0
- package/dist/domains/time.js +64 -0
- package/dist/domains/workspaces.d.ts +2 -0
- package/dist/domains/workspaces.js +18 -0
- package/dist/index.js +15 -2
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -44,6 +44,9 @@ Once connected, Claude can:
|
|
|
44
44
|
- **Complete actions** – "Mark the report task as done"
|
|
45
45
|
- **View OKRs** – "What are my Q1 goals?"
|
|
46
46
|
- **Search** – "Find anything related to Kenya"
|
|
47
|
+
- **Work the CRM** – "Log that I emailed Keily today" / "Move the Onionpress deal to Proposal"
|
|
48
|
+
- **Run the product backlog** – tickets, features, epics, stories, pages and decisions
|
|
49
|
+
- **Track time** – log, confirm and report on the day
|
|
47
50
|
|
|
48
51
|
## Manual Setup
|
|
49
52
|
|
|
@@ -125,6 +128,30 @@ exponential-mcp serve
|
|
|
125
128
|
| `append_meeting_notes` | Add to a meeting's notes without overwriting them |
|
|
126
129
|
| `search` | Search across everything |
|
|
127
130
|
|
|
131
|
+
### Domain tools
|
|
132
|
+
|
|
133
|
+
Alongside the tools above, every part of the [Exponential SDK](https://www.npmjs.com/package/exponential-sdk)
|
|
134
|
+
is exposed as one tool per domain, so the MCP server can do anything the SDK
|
|
135
|
+
and CLI can:
|
|
136
|
+
|
|
137
|
+
`actions` · `projects` · `workspaces` · `goals` · `key_results` · `meetings` ·
|
|
138
|
+
`time` · `decisions` · `contacts` · `organizations` · `deals` · `products` ·
|
|
139
|
+
`features` · `scopes` · `requirements` · `areas` · `stories` · `epics` ·
|
|
140
|
+
`tickets` · `labels` · `pages` · `comments`
|
|
141
|
+
|
|
142
|
+
Each takes `{ operation, params }`. The tool description lists every operation
|
|
143
|
+
with a one-line signature; `operation: "describe"` returns the full parameter
|
|
144
|
+
schema for one operation (`params: { operation: "create" }`) or all of them.
|
|
145
|
+
Invalid params come back as an error that includes the expected schema.
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{ "operation": "add_interaction",
|
|
149
|
+
"params": { "contactId": "…", "type": "EMAIL", "direction": "OUTBOUND", "subject": "Intro" } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`comments` covers comments on actions, features, pages, goals and tickets — pass
|
|
153
|
+
`target` to choose which.
|
|
154
|
+
|
|
128
155
|
### Asking about the day
|
|
129
156
|
|
|
130
157
|
Use **`get_todays_actions`**, not `get_actions`, for anything about today,
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { date, id, op } from './framework.js';
|
|
3
|
+
const KANBAN_STATUSES = ['BACKLOG', 'TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE', 'CANCELLED'];
|
|
4
|
+
const ACTION_STATUSES = ['ACTIVE', 'COMPLETED', 'CANCELLED', 'DELETED', 'DRAFT'];
|
|
5
|
+
const PRIORITIES = [
|
|
6
|
+
'Quick',
|
|
7
|
+
'Scheduled',
|
|
8
|
+
'1st Priority',
|
|
9
|
+
'2nd Priority',
|
|
10
|
+
'3rd Priority',
|
|
11
|
+
'4th Priority',
|
|
12
|
+
'5th Priority',
|
|
13
|
+
'Errand',
|
|
14
|
+
'Remember',
|
|
15
|
+
'Watch',
|
|
16
|
+
'Someday Maybe',
|
|
17
|
+
];
|
|
18
|
+
const optionalWorkspaceId = z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Workspace ID (see the `workspaces` tool); omit for all workspaces');
|
|
22
|
+
const clearableDate = (what) => date(what).nullable().optional().describe(`${what} (ISO 8601; null to clear)`);
|
|
23
|
+
export const actions = {
|
|
24
|
+
name: 'actions',
|
|
25
|
+
description: 'Actions: the personal and project to-dos on the user’s plate, with due/scheduled dates, priority and a kanban column. Projects live in the `projects` tool, logged time in `time`, and meetings (which can spawn actions) in `meetings`.',
|
|
26
|
+
operations: {
|
|
27
|
+
list: op({
|
|
28
|
+
summary: 'List your open actions (completed/cancelled hidden unless `status` is set). With projectId, returns that project’s actions and ignores `status`; with status alone, returns only actions you created in that kanban column.',
|
|
29
|
+
params: z.object({
|
|
30
|
+
projectId: id('Project').optional(),
|
|
31
|
+
status: z.enum(KANBAN_STATUSES).optional().describe('Kanban column to filter by'),
|
|
32
|
+
assigneeId: id('User').optional(),
|
|
33
|
+
}),
|
|
34
|
+
run: (client, params) => client.actions.list(params),
|
|
35
|
+
}),
|
|
36
|
+
get_today: op({
|
|
37
|
+
summary: 'What is actually on the user’s plate today, split into overdue / today / inbox — the /today page’s own partition. Prefer this for "what should I do today". Each group is capped at 50 rows; `count` is the true total.',
|
|
38
|
+
params: z.object({ workspaceId: optionalWorkspaceId }),
|
|
39
|
+
run: (client, { workspaceId }) => client.actions.getTodaysActions(workspaceId),
|
|
40
|
+
}),
|
|
41
|
+
get_due_today: op({
|
|
42
|
+
summary: 'Narrow slice: only actions whose dueDate is today. Excludes overdue and scheduled-but-undated work — use get_today for "what is on my plate".',
|
|
43
|
+
params: z.object({ workspaceId: optionalWorkspaceId }),
|
|
44
|
+
run: (client, { workspaceId }) => client.actions.getToday(workspaceId),
|
|
45
|
+
}),
|
|
46
|
+
get_overdue_triage: op({
|
|
47
|
+
summary: 'Explain the overdue pile: cohorts (actions sharing one exact timestamp, i.e. a bulk-dated plan — usually bulk_defer them) vs loose, individually-dated debt.',
|
|
48
|
+
params: z.object({ workspaceId: optionalWorkspaceId }),
|
|
49
|
+
run: (client, { workspaceId }) => client.actions.getOverdueTriage(workspaceId),
|
|
50
|
+
}),
|
|
51
|
+
bulk_reschedule: op({
|
|
52
|
+
summary: 'Move actions to a new date: sets both scheduledStart and dueDate to `dueDate`. Pass null to clear both (bulk_defer says that more clearly). Actions you cannot access are silently skipped.',
|
|
53
|
+
params: z.object({
|
|
54
|
+
actionIds: z.array(id('Action')),
|
|
55
|
+
dueDate: date('New date').nullable().describe('New do/due date (ISO 8601), or null to clear'),
|
|
56
|
+
}),
|
|
57
|
+
run: (client, { actionIds, dueDate }) => client.actions.bulkReschedule(actionIds, dueDate),
|
|
58
|
+
}),
|
|
59
|
+
bulk_defer: op({
|
|
60
|
+
summary: 'Amnesty: clear the dates on actions so they fall back to their project backlog untimed (1–200 ids). Kanban column is untouched. For work that was never really due — typically an overdue cohort.',
|
|
61
|
+
params: z.object({
|
|
62
|
+
actionIds: z.array(id('Action')).min(1).max(200),
|
|
63
|
+
}),
|
|
64
|
+
run: (client, { actionIds }) => client.actions.bulkDefer(actionIds),
|
|
65
|
+
}),
|
|
66
|
+
list_by_date_range: op({
|
|
67
|
+
summary: 'Your ACTIVE actions whose dueDate falls in [start, end) — end is exclusive. The workspace filter matches via the action’s project, so project-less actions drop out when it is set.',
|
|
68
|
+
params: z.object({
|
|
69
|
+
start: date('Range start (inclusive)'),
|
|
70
|
+
end: date('Range end (exclusive)'),
|
|
71
|
+
workspaceId: optionalWorkspaceId,
|
|
72
|
+
}),
|
|
73
|
+
run: (client, { start, end, workspaceId }) => client.actions.getByDateRange(start, end, workspaceId),
|
|
74
|
+
}),
|
|
75
|
+
list_kanban: op({
|
|
76
|
+
summary: 'Kanban-board actions (those with a kanban column), ordered by column then board order. Only returns actions you created.',
|
|
77
|
+
params: z.object({
|
|
78
|
+
projectId: id('Project').optional(),
|
|
79
|
+
status: z.enum(KANBAN_STATUSES).optional().describe('Kanban column to filter by'),
|
|
80
|
+
assigneeId: id('User').optional(),
|
|
81
|
+
}),
|
|
82
|
+
run: (client, params) => client.actions.getKanban(params),
|
|
83
|
+
}),
|
|
84
|
+
list_project_actions: op({
|
|
85
|
+
summary: 'All actions in one project, including completed ones (unlike list). FORBIDDEN if you lack project access.',
|
|
86
|
+
params: z.object({
|
|
87
|
+
projectId: id('Project'),
|
|
88
|
+
assigneeId: id('User').optional(),
|
|
89
|
+
}),
|
|
90
|
+
run: (client, { projectId, assigneeId }) => client.actions.getProjectActions(projectId, assigneeId),
|
|
91
|
+
}),
|
|
92
|
+
create: op({
|
|
93
|
+
summary: 'Create an action. Priority defaults to "Quick" and status to ACTIVE.',
|
|
94
|
+
params: z.object({
|
|
95
|
+
name: z.string(),
|
|
96
|
+
description: z.string().optional(),
|
|
97
|
+
projectId: id('Project').optional(),
|
|
98
|
+
workspaceId: optionalWorkspaceId.describe('Workspace ID (see the `workspaces` tool)'),
|
|
99
|
+
dueDate: date('Due date').optional(),
|
|
100
|
+
scheduledStart: date('Scheduled start (do-date)').optional(),
|
|
101
|
+
scheduledEnd: date('Scheduled end').optional(),
|
|
102
|
+
duration: z.number().optional().describe('Planned duration in minutes'),
|
|
103
|
+
priority: z.enum(PRIORITIES).optional(),
|
|
104
|
+
status: z.enum(ACTION_STATUSES).optional(),
|
|
105
|
+
epicId: id('Epic').optional(),
|
|
106
|
+
effortEstimate: z.number().optional(),
|
|
107
|
+
blockedByIds: z.array(id('Action')).optional().describe('IDs of actions this one is blocked by'),
|
|
108
|
+
}),
|
|
109
|
+
run: (client, params) => client.actions.create(params),
|
|
110
|
+
}),
|
|
111
|
+
update: op({
|
|
112
|
+
summary: 'Update an action. Only the fields you pass change. Complete it with status COMPLETED or kanbanStatus DONE.',
|
|
113
|
+
params: z.object({
|
|
114
|
+
id: id('Action'),
|
|
115
|
+
name: z.string().optional(),
|
|
116
|
+
description: z.string().optional(),
|
|
117
|
+
projectId: id('Project').optional().describe('Project ID to move the action to'),
|
|
118
|
+
workspaceId: z.string().nullable().optional().describe('Workspace ID (null to clear)'),
|
|
119
|
+
dueDate: clearableDate('Due date'),
|
|
120
|
+
scheduledStart: clearableDate('Scheduled start (do-date)'),
|
|
121
|
+
scheduledEnd: clearableDate('Scheduled end'),
|
|
122
|
+
duration: z.number().nullable().optional().describe('Planned duration in minutes (null to clear)'),
|
|
123
|
+
priority: z.enum(PRIORITIES).optional(),
|
|
124
|
+
status: z.enum(ACTION_STATUSES).optional(),
|
|
125
|
+
kanbanStatus: z.enum(KANBAN_STATUSES).optional(),
|
|
126
|
+
epicId: z.string().nullable().optional().describe('Epic ID (null to unlink)'),
|
|
127
|
+
effortEstimate: z.number().nullable().optional().describe('Effort estimate (null to clear)'),
|
|
128
|
+
blockedByIds: z
|
|
129
|
+
.array(id('Action'))
|
|
130
|
+
.optional()
|
|
131
|
+
.describe('Replaces the full set of blocking action IDs; omit to leave untouched'),
|
|
132
|
+
}),
|
|
133
|
+
run: (client, params) => client.actions.update(params),
|
|
134
|
+
}),
|
|
135
|
+
upsert_by_source: op({
|
|
136
|
+
summary: 'Idempotent create keyed on (workspaceId, sourceType, sourceId): refreshes name/links if found, else creates. Returns { action, outcome: "created" | "updated" }.',
|
|
137
|
+
params: z.object({
|
|
138
|
+
sourceType: z.string().describe('External source kind, e.g. `claude-session` for the Daily worklog'),
|
|
139
|
+
sourceId: z.string().describe('External id within sourceType (e.g. the conversation id)'),
|
|
140
|
+
name: z.string(),
|
|
141
|
+
workspaceId: z.string().describe('Workspace ID (see the `workspaces` tool)'),
|
|
142
|
+
description: z.string().optional(),
|
|
143
|
+
projectId: z
|
|
144
|
+
.string()
|
|
145
|
+
.nullable()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe('Project ID; must be in workspaceId or the call is NOT_FOUND'),
|
|
148
|
+
ticketId: z
|
|
149
|
+
.string()
|
|
150
|
+
.nullable()
|
|
151
|
+
.optional()
|
|
152
|
+
.describe('Ticket ID; its product must be in workspaceId or the call is NOT_FOUND'),
|
|
153
|
+
}),
|
|
154
|
+
run: (client, params) => client.actions.upsertBySource(params),
|
|
155
|
+
}),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id, op } from './framework.js';
|
|
3
|
+
export const areas = {
|
|
4
|
+
name: 'areas',
|
|
5
|
+
description: 'Areas: per-product buckets a feature is filed under (exactly one or none). Products are in `products`; file a feature under an area via `features` create/update `areaId`.',
|
|
6
|
+
operations: {
|
|
7
|
+
list: op({
|
|
8
|
+
summary: 'List a product’s areas in display order, with feature counts.',
|
|
9
|
+
params: z.object({ productId: id('Product') }),
|
|
10
|
+
run: (client, params) => client.areas.list(params),
|
|
11
|
+
}),
|
|
12
|
+
create: op({
|
|
13
|
+
summary: 'Create an area at the end of a product’s list. Names are unique per product.',
|
|
14
|
+
params: z.object({
|
|
15
|
+
productId: id('Product'),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
description: z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
run: (client, params) => client.areas.create(params),
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { op } from './framework.js';
|
|
3
|
+
const TARGETS = ['action', 'feature', 'page', 'goal', 'ticket'];
|
|
4
|
+
const target = z.enum(TARGETS).describe('Kind of entity the comment is on');
|
|
5
|
+
const targetId = z
|
|
6
|
+
.union([z.string(), z.number()])
|
|
7
|
+
.describe('ID of the entity being commented on: the action/feature/page/ticket ID, or the goal’s integer ID');
|
|
8
|
+
const commentId = z.string().describe('Comment ID');
|
|
9
|
+
const body = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.describe('Comment text (markdown). Mention members with @[Name](userId) — see the `workspaces` tool’s members');
|
|
13
|
+
function unsupported(operation, t, supported) {
|
|
14
|
+
throw new Error(`comments.${operation} is not supported for target "${t}". Supported targets: ${supported}.`);
|
|
15
|
+
}
|
|
16
|
+
function goalIdOf(value) {
|
|
17
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
18
|
+
if (!Number.isInteger(n)) {
|
|
19
|
+
throw new Error(`Goal targetId must be an integer, got ${JSON.stringify(value)}.`);
|
|
20
|
+
}
|
|
21
|
+
return n;
|
|
22
|
+
}
|
|
23
|
+
/** Feature-only / goal-only add fields, rejected loudly on other targets rather than silently dropped. */
|
|
24
|
+
function assertOnly(t, allowed, fields) {
|
|
25
|
+
if (t === allowed)
|
|
26
|
+
return;
|
|
27
|
+
const present = Object.entries(fields)
|
|
28
|
+
.filter(([, v]) => v !== undefined)
|
|
29
|
+
.map(([k]) => k);
|
|
30
|
+
if (present.length) {
|
|
31
|
+
throw new Error(`${present.join(', ')} can only be used with target "${allowed}", not "${t}".`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function listTicketComments(client, ticketId) {
|
|
35
|
+
// There is no standalone ticket-comment list endpoint; the detail read carries them.
|
|
36
|
+
const ticket = await client.tickets.get(ticketId);
|
|
37
|
+
return ticket.comments ?? [];
|
|
38
|
+
}
|
|
39
|
+
export const comments = {
|
|
40
|
+
name: 'comments',
|
|
41
|
+
description: 'Comments on actions, features (PRDs), knowledge pages, goals (objectives) and tickets — pick the entity with `target`. Threaded replies and anchored-thread resolve/unresolve exist only on features. Editing and deleting are author-only.',
|
|
42
|
+
operations: {
|
|
43
|
+
list: op({
|
|
44
|
+
summary: 'List comments on an entity. All targets. For tickets this reads the ticket detail (newest first) — there is no separate list endpoint.',
|
|
45
|
+
params: z.object({ target, targetId }),
|
|
46
|
+
run: (client, { target: t, targetId: tid }) => {
|
|
47
|
+
switch (t) {
|
|
48
|
+
case 'action':
|
|
49
|
+
return client.actionComments.list(String(tid));
|
|
50
|
+
case 'feature':
|
|
51
|
+
return client.featureComments.list(String(tid));
|
|
52
|
+
case 'page':
|
|
53
|
+
return client.pageComments.list(String(tid));
|
|
54
|
+
case 'goal':
|
|
55
|
+
return client.goalComments.list(goalIdOf(tid));
|
|
56
|
+
case 'ticket':
|
|
57
|
+
return listTicketComments(client, String(tid));
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
add: op({
|
|
62
|
+
summary: 'Add a top-level comment. All targets; scopeId/threadId/quotedText are feature-only, parentUpdateId is goal-only.',
|
|
63
|
+
params: z.object({
|
|
64
|
+
target,
|
|
65
|
+
targetId,
|
|
66
|
+
body,
|
|
67
|
+
scopeId: z.string().optional().describe('Feature only: post to this scope’s activity feed instead of the feature’s'),
|
|
68
|
+
threadId: z.string().optional().describe('Feature only: anchor to a span of the PRD body; omit for a doc-level comment'),
|
|
69
|
+
quotedText: z.string().optional().describe('Feature only: the anchored text being quoted'),
|
|
70
|
+
parentUpdateId: z.string().optional().describe('Goal only: thread under this goal update instead of the goal itself'),
|
|
71
|
+
}),
|
|
72
|
+
run: (client, { target: t, targetId: tid, body, scopeId, threadId, quotedText, parentUpdateId }) => {
|
|
73
|
+
assertOnly(t, 'feature', { scopeId, threadId, quotedText });
|
|
74
|
+
assertOnly(t, 'goal', { parentUpdateId });
|
|
75
|
+
switch (t) {
|
|
76
|
+
case 'action':
|
|
77
|
+
return client.actionComments.add({ actionId: String(tid), content: body });
|
|
78
|
+
case 'feature':
|
|
79
|
+
return client.featureComments.create({ featureId: String(tid), body, scopeId, threadId, quotedText });
|
|
80
|
+
case 'page':
|
|
81
|
+
return client.pageComments.create({ pageId: String(tid), body });
|
|
82
|
+
case 'goal':
|
|
83
|
+
return client.goalComments.add({ goalId: goalIdOf(tid), content: body, parentUpdateId });
|
|
84
|
+
case 'ticket':
|
|
85
|
+
return client.tickets.addComment({ ticketId: String(tid), content: body });
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
update: op({
|
|
90
|
+
summary: 'Replace a comment’s text. All targets; author-only.',
|
|
91
|
+
params: z.object({ target, commentId, body }),
|
|
92
|
+
run: (client, { target: t, commentId, body }) => {
|
|
93
|
+
switch (t) {
|
|
94
|
+
case 'action':
|
|
95
|
+
return client.actionComments.update({ commentId, content: body });
|
|
96
|
+
case 'feature':
|
|
97
|
+
return client.featureComments.update({ commentId, body });
|
|
98
|
+
case 'page':
|
|
99
|
+
return client.pageComments.update({ commentId, body });
|
|
100
|
+
case 'goal':
|
|
101
|
+
return client.goalComments.update({ commentId, content: body });
|
|
102
|
+
case 'ticket':
|
|
103
|
+
return client.tickets.updateComment({ id: commentId, content: body });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
delete: op({
|
|
108
|
+
summary: 'Delete a comment permanently. All targets; author-only. On features, deleting a root also deletes its replies.',
|
|
109
|
+
params: z.object({ target, commentId }),
|
|
110
|
+
run: (client, { target: t, commentId }) => {
|
|
111
|
+
switch (t) {
|
|
112
|
+
case 'action':
|
|
113
|
+
return client.actionComments.delete(commentId);
|
|
114
|
+
case 'feature':
|
|
115
|
+
return client.featureComments.delete(commentId);
|
|
116
|
+
case 'page':
|
|
117
|
+
return client.pageComments.delete(commentId);
|
|
118
|
+
case 'goal':
|
|
119
|
+
return client.goalComments.delete(commentId);
|
|
120
|
+
case 'ticket':
|
|
121
|
+
return client.tickets.deleteComment(commentId);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
reply: op({
|
|
126
|
+
summary: 'Reply to a comment. Feature target only. Replies to replies still hang off the root comment.',
|
|
127
|
+
params: z.object({
|
|
128
|
+
target,
|
|
129
|
+
commentId: z.string().describe('ID of the comment being replied to'),
|
|
130
|
+
body,
|
|
131
|
+
}),
|
|
132
|
+
run: (client, { target: t, commentId, body }) => {
|
|
133
|
+
if (t !== 'feature')
|
|
134
|
+
unsupported('reply', t, 'feature');
|
|
135
|
+
return client.featureComments.reply({ parentId: commentId, body });
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
resolve: op({
|
|
139
|
+
summary: 'Mark an anchored comment thread resolved. Feature target only; only anchored threads have a threadId.',
|
|
140
|
+
params: z.object({
|
|
141
|
+
target,
|
|
142
|
+
targetId,
|
|
143
|
+
threadId: z.string().describe('The anchored thread’s threadId (from the comment), not a comment ID'),
|
|
144
|
+
}),
|
|
145
|
+
run: (client, { target: t, targetId: tid, threadId }) => {
|
|
146
|
+
if (t !== 'feature')
|
|
147
|
+
unsupported('resolve', t, 'feature');
|
|
148
|
+
return client.featureComments.resolve({ featureId: String(tid), threadId });
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
unresolve: op({
|
|
152
|
+
summary: 'Reopen a resolved anchored comment thread. Feature target only.',
|
|
153
|
+
params: z.object({
|
|
154
|
+
target,
|
|
155
|
+
targetId,
|
|
156
|
+
threadId: z.string().describe('The anchored thread’s threadId (from the comment), not a comment ID'),
|
|
157
|
+
}),
|
|
158
|
+
run: (client, { target: t, targetId: tid, threadId }) => {
|
|
159
|
+
if (t !== 'feature')
|
|
160
|
+
unsupported('unresolve', t, 'feature');
|
|
161
|
+
return client.featureComments.unresolve({ featureId: String(tid), threadId });
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id, op, workspaceId } from './framework.js';
|
|
3
|
+
const INTERACTION_TYPES = [
|
|
4
|
+
'EMAIL',
|
|
5
|
+
'TELEGRAM',
|
|
6
|
+
'PHONE_CALL',
|
|
7
|
+
'MEETING',
|
|
8
|
+
'NOTE',
|
|
9
|
+
'LINKEDIN',
|
|
10
|
+
'OTHER',
|
|
11
|
+
];
|
|
12
|
+
/** Fields shared by create and update; update additionally accepts null to clear. */
|
|
13
|
+
const contactFields = {
|
|
14
|
+
firstName: z.string().optional(),
|
|
15
|
+
lastName: z.string().optional(),
|
|
16
|
+
about: z.string().optional().describe('Free-text notes about the person'),
|
|
17
|
+
profileType: z.string().optional(),
|
|
18
|
+
skills: z.array(z.string()).optional(),
|
|
19
|
+
tags: z.array(z.string()).optional(),
|
|
20
|
+
organizationName: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Link to an organization by name, creating it if needed. Ignored when organizationId is set'),
|
|
24
|
+
};
|
|
25
|
+
const clearable = (what) => z.string().nullable().optional().describe(`${what} (null to clear)`);
|
|
26
|
+
export const contacts = {
|
|
27
|
+
name: 'contacts',
|
|
28
|
+
description: 'CRM contacts (people) in a workspace, with their interaction history. Organizations live in the `organizations` tool and deals in `deals`. Personal fields are encrypted at rest; list/search runs server-side.',
|
|
29
|
+
operations: {
|
|
30
|
+
list: op({
|
|
31
|
+
summary: 'List contacts, newest first. Paginate with the returned nextCursor.',
|
|
32
|
+
params: z.object({
|
|
33
|
+
workspaceId,
|
|
34
|
+
search: z.string().optional().describe('Match on name, email or organization'),
|
|
35
|
+
tags: z.array(z.string()).optional(),
|
|
36
|
+
organizationId: id('Organization').optional(),
|
|
37
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
38
|
+
cursor: z.string().optional(),
|
|
39
|
+
}),
|
|
40
|
+
run: (client, params) => client.contacts.list(params),
|
|
41
|
+
}),
|
|
42
|
+
get: op({
|
|
43
|
+
summary: 'Get one contact, optionally with its interaction timeline.',
|
|
44
|
+
params: z.object({
|
|
45
|
+
id: id('Contact'),
|
|
46
|
+
includeInteractions: z.boolean().optional(),
|
|
47
|
+
}),
|
|
48
|
+
run: (client, { id, includeInteractions }) => client.contacts.get(id, { includeInteractions }),
|
|
49
|
+
}),
|
|
50
|
+
create: op({
|
|
51
|
+
summary: 'Create a contact.',
|
|
52
|
+
params: z.object({
|
|
53
|
+
workspaceId,
|
|
54
|
+
...contactFields,
|
|
55
|
+
email: z.string().nullable().optional(),
|
|
56
|
+
phone: z.string().optional(),
|
|
57
|
+
linkedIn: z.string().optional(),
|
|
58
|
+
telegram: z.string().optional(),
|
|
59
|
+
twitter: z.string().optional(),
|
|
60
|
+
github: z.string().optional(),
|
|
61
|
+
bluesky: z.string().optional(),
|
|
62
|
+
organizationId: id('Organization').optional(),
|
|
63
|
+
}),
|
|
64
|
+
run: (client, params) => client.contacts.create(params),
|
|
65
|
+
}),
|
|
66
|
+
update: op({
|
|
67
|
+
summary: 'Update a contact. Only the fields you pass change.',
|
|
68
|
+
params: z.object({
|
|
69
|
+
id: id('Contact'),
|
|
70
|
+
...contactFields,
|
|
71
|
+
email: clearable('Email'),
|
|
72
|
+
phone: clearable('Phone'),
|
|
73
|
+
linkedIn: clearable('LinkedIn URL'),
|
|
74
|
+
telegram: clearable('Telegram handle'),
|
|
75
|
+
twitter: clearable('Twitter/X handle'),
|
|
76
|
+
github: clearable('GitHub handle'),
|
|
77
|
+
bluesky: clearable('Bluesky handle'),
|
|
78
|
+
organizationId: clearable('Organization ID'),
|
|
79
|
+
}),
|
|
80
|
+
run: (client, params) => client.contacts.update(params),
|
|
81
|
+
}),
|
|
82
|
+
delete: op({
|
|
83
|
+
summary: 'Delete a contact permanently.',
|
|
84
|
+
params: z.object({ id: id('Contact') }),
|
|
85
|
+
run: (client, { id }) => client.contacts.delete(id),
|
|
86
|
+
}),
|
|
87
|
+
add_interaction: op({
|
|
88
|
+
summary: 'Log an interaction (email, call, meeting, note…) on a contact’s timeline.',
|
|
89
|
+
params: z.object({
|
|
90
|
+
contactId: id('Contact'),
|
|
91
|
+
type: z.enum(INTERACTION_TYPES),
|
|
92
|
+
direction: z.enum(['INBOUND', 'OUTBOUND']),
|
|
93
|
+
subject: z.string().optional(),
|
|
94
|
+
notes: z.string().optional(),
|
|
95
|
+
metadata: z.record(z.unknown()).optional(),
|
|
96
|
+
}),
|
|
97
|
+
run: (client, params) => client.contacts.addInteraction(params),
|
|
98
|
+
}),
|
|
99
|
+
enrich: op({
|
|
100
|
+
summary: 'Queue a web-search enrichment job for a contact. Idempotent: a job already in flight is not re-queued.',
|
|
101
|
+
params: z.object({ id: id('Contact') }),
|
|
102
|
+
run: (client, { id }) => client.contacts.enrich(id),
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { date, id, op, workspaceId } from './framework.js';
|
|
3
|
+
const probability = z.number().int().min(0).max(100);
|
|
4
|
+
const value = z.number().min(0);
|
|
5
|
+
export const deals = {
|
|
6
|
+
name: 'deals',
|
|
7
|
+
description: 'CRM deals moving through the workspace’s sales pipeline stages, plus the pipeline and its stages. Deals link to people in `contacts` and companies in `organizations`. All operations target the workspace’s default (oldest active) pipeline.',
|
|
8
|
+
operations: {
|
|
9
|
+
pipeline: op({
|
|
10
|
+
summary: 'Get the workspace’s default pipeline with its stages, or null if there is none you can access.',
|
|
11
|
+
params: z.object({ workspaceId }),
|
|
12
|
+
run: (client, { workspaceId }) => client.pipelines.get(workspaceId),
|
|
13
|
+
}),
|
|
14
|
+
stages: op({
|
|
15
|
+
summary: 'List the pipeline’s stages in order, with deal counts. Use a stage ID for create/move.',
|
|
16
|
+
params: z.object({ workspaceId }),
|
|
17
|
+
run: (client, { workspaceId }) => client.pipelines.getStages(workspaceId),
|
|
18
|
+
}),
|
|
19
|
+
list: op({
|
|
20
|
+
summary: 'List every deal in the pipeline (unpaginated), ordered by position within stage.',
|
|
21
|
+
params: z.object({ workspaceId }),
|
|
22
|
+
run: (client, { workspaceId }) => client.pipelines.listDeals(workspaceId),
|
|
23
|
+
}),
|
|
24
|
+
get: op({
|
|
25
|
+
summary: 'Get one deal with its stage, contact, organization, assignee and latest 50 activities.',
|
|
26
|
+
params: z.object({ id: id('Deal') }),
|
|
27
|
+
run: (client, { id }) => client.pipelines.getDeal(id),
|
|
28
|
+
}),
|
|
29
|
+
create: op({
|
|
30
|
+
summary: 'Create a deal at the end of a stage. Logs a CREATED activity.',
|
|
31
|
+
params: z.object({
|
|
32
|
+
workspaceId,
|
|
33
|
+
stageId: id('Pipeline stage'),
|
|
34
|
+
title: z.string(),
|
|
35
|
+
description: z.string().optional(),
|
|
36
|
+
value: value.optional(),
|
|
37
|
+
currency: z.string().optional().describe('ISO currency code; default USD'),
|
|
38
|
+
probability: probability.optional().describe('Win probability, 0–100'),
|
|
39
|
+
expectedCloseDate: date('Expected close date').optional(),
|
|
40
|
+
contactId: id('Contact').optional(),
|
|
41
|
+
organizationId: id('Organization').optional(),
|
|
42
|
+
assignedToId: id('User').optional(),
|
|
43
|
+
}),
|
|
44
|
+
run: (client, params) => client.pipelines.createDeal(params),
|
|
45
|
+
}),
|
|
46
|
+
update: op({
|
|
47
|
+
summary: 'Update a deal’s fields (not its stage — use move). Only the fields you pass change; a value change is logged as an activity.',
|
|
48
|
+
params: z.object({
|
|
49
|
+
id: id('Deal'),
|
|
50
|
+
title: z.string().optional(),
|
|
51
|
+
description: z.string().nullable().optional().describe('null to clear'),
|
|
52
|
+
value: value.nullable().optional().describe('null to clear'),
|
|
53
|
+
currency: z.string().optional(),
|
|
54
|
+
probability: probability.nullable().optional().describe('Win probability, 0–100 (null to clear)'),
|
|
55
|
+
expectedCloseDate: date('Expected close date').nullable().optional().describe('ISO 8601 date or datetime (null to clear)'),
|
|
56
|
+
contactId: z.string().nullable().optional().describe('Contact ID (null to clear)'),
|
|
57
|
+
organizationId: z.string().nullable().optional().describe('Organization ID (null to clear)'),
|
|
58
|
+
assignedToId: z.string().nullable().optional().describe('User ID (null to clear)'),
|
|
59
|
+
}),
|
|
60
|
+
run: (client, params) => client.pipelines.updateDeal(params),
|
|
61
|
+
}),
|
|
62
|
+
move: op({
|
|
63
|
+
summary: 'Move a deal to a stage and position. Entering a won/lost stage sets closedAt (leaving one clears it); a stage change is logged. Other deals are not re-ordered.',
|
|
64
|
+
params: z.object({
|
|
65
|
+
id: id('Deal'),
|
|
66
|
+
stageId: id('Target pipeline stage'),
|
|
67
|
+
stageOrder: z.number().int().min(0).describe('0-based position within the target stage'),
|
|
68
|
+
}),
|
|
69
|
+
run: (client, params) => client.pipelines.moveDeal(params),
|
|
70
|
+
}),
|
|
71
|
+
delete: op({
|
|
72
|
+
summary: 'Delete a deal permanently, with its activity history.',
|
|
73
|
+
params: z.object({ id: id('Deal') }),
|
|
74
|
+
run: (client, { id }) => client.pipelines.deleteDeal(id),
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
};
|