exponential-mcp 0.6.0 → 0.8.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/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 +40 -13
- package/dist/runTools.d.ts +29 -0
- package/dist/runTools.js +98 -0
- package/package.json +5 -4
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { date, id, op, workspaceId } from './framework.js';
|
|
3
|
+
const DECISION_STATUSES = ['OPEN', 'PROPOSED', 'ACCEPTED', 'SUPERSEDED', 'DEPRECATED'];
|
|
4
|
+
const DECISION_SOURCES = ['MEETING', 'MANUAL', 'AGENT'];
|
|
5
|
+
const decisionId = id('Decision');
|
|
6
|
+
const meetingId = id('Meeting (transcription session)');
|
|
7
|
+
const clearable = (what) => z.string().nullable().optional().describe(`${what} (null to clear)`);
|
|
8
|
+
/** Scope links shared by create and update; all nullable to clear. */
|
|
9
|
+
const scopeFields = {
|
|
10
|
+
productId: clearable('Product ID'),
|
|
11
|
+
projectId: clearable('Project ID'),
|
|
12
|
+
goalId: z.number().int().nullable().optional().describe('Objective (goal) integer ID (null to clear)'),
|
|
13
|
+
keyResultId: clearable('Key result ID'),
|
|
14
|
+
};
|
|
15
|
+
export const decisions = {
|
|
16
|
+
name: 'decisions',
|
|
17
|
+
description: 'The Decision Log: decisions and open questions (an open question is a decision with status OPEN — there is no separate model), optionally sourced from a meeting with transcript evidence. Meetings are in `meetings`; decisions can be linked to `tickets` and `features` as "implemented by".',
|
|
18
|
+
operations: {
|
|
19
|
+
list: op({
|
|
20
|
+
summary: 'Confirmed decisions in a workspace, newest decided first. Never includes drafts (see list_for_meeting).',
|
|
21
|
+
params: z.object({
|
|
22
|
+
workspaceId,
|
|
23
|
+
statuses: z.array(z.enum(DECISION_STATUSES)).optional(),
|
|
24
|
+
sources: z.array(z.enum(DECISION_SOURCES)).optional(),
|
|
25
|
+
productId: z.string().optional().describe('Product ID, or the literal "workspace" for decisions with no product'),
|
|
26
|
+
includeWorkspaceWide: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('With a real productId: also include decisions that have no product'),
|
|
30
|
+
projectId: id('Project').optional(),
|
|
31
|
+
search: z.string().max(200).optional().describe('Free text over statement and body'),
|
|
32
|
+
number: z.number().int().min(1).optional().describe('One decision by its workspace sequence number — D-0003 is 3'),
|
|
33
|
+
limit: z.number().int().min(1).max(500).optional().describe('Unset returns everything visible'),
|
|
34
|
+
}),
|
|
35
|
+
run: (client, params) => client.decisions.list(params),
|
|
36
|
+
}),
|
|
37
|
+
get: op({
|
|
38
|
+
summary: 'Get one decision with its deciders, evidence, links and supersession chain.',
|
|
39
|
+
params: z.object({ workspaceId, decisionId }),
|
|
40
|
+
run: (client, { workspaceId, decisionId }) => client.decisions.get(workspaceId, decisionId),
|
|
41
|
+
}),
|
|
42
|
+
list_for_meeting: op({
|
|
43
|
+
summary: 'Decisions and open questions logged from one meeting. Includes drafts if you can edit the meeting. No workspaceId needed.',
|
|
44
|
+
params: z.object({ transcriptionSessionId: meetingId }),
|
|
45
|
+
run: (client, { transcriptionSessionId }) => client.decisions.listForMeeting(transcriptionSessionId),
|
|
46
|
+
}),
|
|
47
|
+
list_for_adr: op({
|
|
48
|
+
summary: 'Decisions formalised as one ADR ("Decided in").',
|
|
49
|
+
params: z.object({ workspaceId, adrDocumentId: id('ADR document') }),
|
|
50
|
+
run: (client, { workspaceId, adrDocumentId }) => client.decisions.listForAdr(workspaceId, adrDocumentId),
|
|
51
|
+
}),
|
|
52
|
+
extract_drafts: op({
|
|
53
|
+
summary: 'AI-extract DRAFT decisions from a meeting’s notes and transcript for review. Idempotent: existing drafts come back; a meeting with confirmed decisions reports alreadyPublished.',
|
|
54
|
+
params: z.object({ transcriptionSessionId: meetingId }),
|
|
55
|
+
run: (client, { transcriptionSessionId }) => client.decisions.extractDrafts(transcriptionSessionId),
|
|
56
|
+
}),
|
|
57
|
+
create: op({
|
|
58
|
+
summary: 'Log a decision (status defaults to PROPOSED). Pass status "OPEN" for an open question and transcriptionSessionId to attach it to a meeting.',
|
|
59
|
+
params: z.object({
|
|
60
|
+
workspaceId,
|
|
61
|
+
statement: z.string().max(500).describe('The decision itself, or the open question'),
|
|
62
|
+
body: z.string().max(20000).nullable().optional().describe('Markdown detail — context, alternatives, consequences'),
|
|
63
|
+
status: z
|
|
64
|
+
.enum(DECISION_STATUSES)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('SUPERSEDED and DEPRECATED are rejected here — use set_status'),
|
|
67
|
+
source: z.enum(DECISION_SOURCES).optional(),
|
|
68
|
+
decidedAt: date('When it was decided').nullable().optional(),
|
|
69
|
+
ownerId: z.string().nullable().optional().describe('User ID of the owner'),
|
|
70
|
+
transcriptionSessionId: z
|
|
71
|
+
.string()
|
|
72
|
+
.nullable()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Meeting it came out of. Requires edit access to that meeting'),
|
|
75
|
+
occurrenceId: z.string().nullable().optional().describe('Recurring-meeting occurrence ID'),
|
|
76
|
+
...scopeFields,
|
|
77
|
+
deciders: z
|
|
78
|
+
.array(z.object({
|
|
79
|
+
userId: z.string().nullable().optional().describe('Omit for external participants'),
|
|
80
|
+
name: z.string(),
|
|
81
|
+
email: z.string().nullable().optional(),
|
|
82
|
+
}))
|
|
83
|
+
.max(50)
|
|
84
|
+
.optional(),
|
|
85
|
+
evidence: z
|
|
86
|
+
.array(z.object({
|
|
87
|
+
turnIndex: z.number().int(),
|
|
88
|
+
speaker: z.string().nullable().optional(),
|
|
89
|
+
startTime: z.number().nullable().optional(),
|
|
90
|
+
text: z.string(),
|
|
91
|
+
}))
|
|
92
|
+
.max(50)
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('Quoted transcript turns. Requires transcriptionSessionId. Turns whose text does not match the transcript are silently dropped; speaker/startTime are overwritten from the transcript'),
|
|
95
|
+
}),
|
|
96
|
+
run: (client, params) => client.decisions.create(params),
|
|
97
|
+
}),
|
|
98
|
+
update: op({
|
|
99
|
+
summary: 'Edit a decision’s content and scope. Only the fields you pass change; status goes through set_status.',
|
|
100
|
+
params: z.object({
|
|
101
|
+
workspaceId,
|
|
102
|
+
decisionId,
|
|
103
|
+
statement: z.string().max(500).optional(),
|
|
104
|
+
body: clearable('Markdown body'),
|
|
105
|
+
decidedAt: date('When it was decided').nullable().optional(),
|
|
106
|
+
ownerId: clearable('Owner user ID'),
|
|
107
|
+
...scopeFields,
|
|
108
|
+
adrDocumentId: clearable('ADR document this decision is formalised in'),
|
|
109
|
+
}),
|
|
110
|
+
run: (client, params) => client.decisions.update(params),
|
|
111
|
+
}),
|
|
112
|
+
set_status: op({
|
|
113
|
+
summary: 'Lifecycle transition — e.g. answer an open question by moving OPEN → ACCEPTED.',
|
|
114
|
+
params: z.object({
|
|
115
|
+
workspaceId,
|
|
116
|
+
decisionId,
|
|
117
|
+
status: z.enum(DECISION_STATUSES),
|
|
118
|
+
supersededById: z
|
|
119
|
+
.string()
|
|
120
|
+
.nullable()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe('Decision ID that replaces this one; expected when moving to SUPERSEDED'),
|
|
123
|
+
}),
|
|
124
|
+
run: (client, params) => client.decisions.setStatus(params),
|
|
125
|
+
}),
|
|
126
|
+
link_ticket: op({
|
|
127
|
+
summary: 'Mark a decision as "implemented by" a ticket. Idempotent: an existing link is returned.',
|
|
128
|
+
params: z.object({ workspaceId, decisionId, ticketId: id('Ticket') }),
|
|
129
|
+
run: (client, { workspaceId, decisionId, ticketId }) => client.decisions.linkTicket(workspaceId, decisionId, ticketId),
|
|
130
|
+
}),
|
|
131
|
+
link_feature: op({
|
|
132
|
+
summary: 'Mark a decision as "implemented by" a feature. Idempotent: an existing link is returned.',
|
|
133
|
+
params: z.object({ workspaceId, decisionId, featureId: id('Feature') }),
|
|
134
|
+
run: (client, { workspaceId, decisionId, featureId }) => client.decisions.linkFeature(workspaceId, decisionId, featureId),
|
|
135
|
+
}),
|
|
136
|
+
unlink: op({
|
|
137
|
+
summary: 'Remove one ticket/feature link.',
|
|
138
|
+
params: z.object({
|
|
139
|
+
workspaceId,
|
|
140
|
+
linkId: z.string().describe('DecisionLink ID from the decision’s links — not the ticket or feature ID'),
|
|
141
|
+
}),
|
|
142
|
+
run: (client, { workspaceId, linkId }) => client.decisions.unlink(workspaceId, linkId),
|
|
143
|
+
}),
|
|
144
|
+
confirm_draft: op({
|
|
145
|
+
summary: 'Publish a DRAFT decision into the log.',
|
|
146
|
+
params: z.object({ workspaceId, decisionId }),
|
|
147
|
+
run: (client, { workspaceId, decisionId }) => client.decisions.confirmDraft(workspaceId, decisionId),
|
|
148
|
+
}),
|
|
149
|
+
reject_draft: op({
|
|
150
|
+
summary: 'Reject a DRAFT decision. Confirmed decisions cannot be rejected — deprecate or supersede via set_status.',
|
|
151
|
+
params: z.object({ workspaceId, decisionId }),
|
|
152
|
+
run: (client, { workspaceId, decisionId }) => client.decisions.rejectDraft(workspaceId, decisionId),
|
|
153
|
+
}),
|
|
154
|
+
delete_draft: op({
|
|
155
|
+
summary: 'Hard-delete a draft or rejected decision. Refused for confirmed decisions — the log keeps its history.',
|
|
156
|
+
params: z.object({ workspaceId, decisionId }),
|
|
157
|
+
run: (client, { workspaceId, decisionId }) => client.decisions.deleteDraft(workspaceId, decisionId),
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { date, id, op, workspaceId } from './framework.js';
|
|
3
|
+
const EPIC_STATUSES = ['OPEN', 'IN_PROGRESS', 'DONE', 'CANCELLED'];
|
|
4
|
+
const EPIC_PRIORITIES = ['HIGH', 'MEDIUM', 'LOW', 'NONE'];
|
|
5
|
+
export const epics = {
|
|
6
|
+
name: 'epics',
|
|
7
|
+
description: 'Epics: large bodies of work in a workspace that group tickets (`tickets`) and actions (`actions`). Each new epic belongs to a product (`products`); older ones may have none.',
|
|
8
|
+
operations: {
|
|
9
|
+
list: op({
|
|
10
|
+
summary: 'List the epics in a workspace (all products), ordered by status then name.',
|
|
11
|
+
params: z.object({
|
|
12
|
+
workspaceId,
|
|
13
|
+
status: z.enum(EPIC_STATUSES).optional(),
|
|
14
|
+
}),
|
|
15
|
+
run: (client, params) => client.epics.list(params),
|
|
16
|
+
}),
|
|
17
|
+
get: op({
|
|
18
|
+
summary: 'Get one epic with its owner, tickets and actions.',
|
|
19
|
+
params: z.object({ id: id('Epic') }),
|
|
20
|
+
run: (client, { id }) => client.epics.get(id),
|
|
21
|
+
}),
|
|
22
|
+
create: op({
|
|
23
|
+
summary: 'Create an epic owned by you. Priority defaults to MEDIUM.',
|
|
24
|
+
params: z.object({
|
|
25
|
+
workspaceId,
|
|
26
|
+
productId: id('Product').describe('Product ID; must be in the same workspace'),
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
description: z.string().optional(),
|
|
29
|
+
priority: z.enum(EPIC_PRIORITIES).optional(),
|
|
30
|
+
startDate: date('Start date').optional(),
|
|
31
|
+
targetDate: date('Target date').optional(),
|
|
32
|
+
}),
|
|
33
|
+
run: (client, params) => client.epics.create(params),
|
|
34
|
+
}),
|
|
35
|
+
update: op({
|
|
36
|
+
summary: 'Update an epic. Only the fields you pass change.',
|
|
37
|
+
params: z.object({
|
|
38
|
+
id: id('Epic'),
|
|
39
|
+
name: z.string().min(1).optional(),
|
|
40
|
+
description: z.string().nullable().optional().describe('null to clear'),
|
|
41
|
+
status: z.enum(EPIC_STATUSES).optional(),
|
|
42
|
+
priority: z.enum(EPIC_PRIORITIES).optional(),
|
|
43
|
+
startDate: date('Start date').nullable().optional().describe('ISO 8601 date or datetime; null to clear'),
|
|
44
|
+
targetDate: date('Target date').nullable().optional().describe('ISO 8601 date or datetime; null to clear'),
|
|
45
|
+
}),
|
|
46
|
+
run: (client, params) => client.epics.update(params),
|
|
47
|
+
}),
|
|
48
|
+
delete: op({
|
|
49
|
+
summary: 'Delete an epic permanently (only its owner or a workspace owner/admin may). Its tickets and actions are kept but unlinked.',
|
|
50
|
+
params: z.object({ id: id('Epic') }),
|
|
51
|
+
run: (client, { id }) => client.epics.delete(id),
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id, op } from './framework.js';
|
|
3
|
+
const FEATURE_STATUSES = [
|
|
4
|
+
'IDEA',
|
|
5
|
+
'DEFINED',
|
|
6
|
+
'IN_PROGRESS',
|
|
7
|
+
'SHIPPED',
|
|
8
|
+
'DEPRECATED',
|
|
9
|
+
'ARCHIVED',
|
|
10
|
+
];
|
|
11
|
+
const priority = z.number().int().min(0).max(4).optional().describe('0–4; lower is higher priority');
|
|
12
|
+
const goalId = z.number().int().describe('Objective (goal) ID — an integer, unlike most IDs');
|
|
13
|
+
export const features = {
|
|
14
|
+
name: 'features',
|
|
15
|
+
description: 'Product features: long-lived capabilities of a product (see `products`), filed under an area (`areas`). A feature’s shippable increments are in `scopes`, its "shall" statements in `requirements`, and its spec documents are Knowledge pages (`pages`) linked here.',
|
|
16
|
+
operations: {
|
|
17
|
+
list: op({
|
|
18
|
+
summary: 'List a product’s features, newest first, with goal, area, lean key-result links and scope/requirement/ticket counts.',
|
|
19
|
+
params: z.object({
|
|
20
|
+
productId: id('Product'),
|
|
21
|
+
status: z.enum(FEATURE_STATUSES).optional(),
|
|
22
|
+
}),
|
|
23
|
+
run: (client, params) => client.features.list(params),
|
|
24
|
+
}),
|
|
25
|
+
get: op({
|
|
26
|
+
summary: 'Get one feature with its scopes, requirements and key-result progress.',
|
|
27
|
+
params: z.object({ id: id('Feature') }),
|
|
28
|
+
run: (client, { id }) => client.features.get(id),
|
|
29
|
+
}),
|
|
30
|
+
create: op({
|
|
31
|
+
summary: 'Create a feature in a product. Status defaults to IDEA.',
|
|
32
|
+
params: z.object({
|
|
33
|
+
productId: id('Product'),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
description: z.string().optional().describe('Markdown PRD body'),
|
|
36
|
+
vision: z.string().optional(),
|
|
37
|
+
status: z.enum(FEATURE_STATUSES).optional(),
|
|
38
|
+
effort: z.number().optional(),
|
|
39
|
+
priority,
|
|
40
|
+
goalId: goalId.optional(),
|
|
41
|
+
areaId: id('Area').optional().describe('Area ID; must belong to the same product'),
|
|
42
|
+
}),
|
|
43
|
+
run: (client, params) => client.features.create(params),
|
|
44
|
+
}),
|
|
45
|
+
update: op({
|
|
46
|
+
summary: 'Update a feature. Only the fields you pass change; a feature that was ever live cannot be ARCHIVED (deprecate it instead).',
|
|
47
|
+
params: z.object({
|
|
48
|
+
id: id('Feature'),
|
|
49
|
+
name: z.string().optional(),
|
|
50
|
+
description: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Markdown PRD body; replaces the whole body (the rich doc is re-derived server-side)'),
|
|
54
|
+
vision: z.string().optional(),
|
|
55
|
+
status: z.enum(FEATURE_STATUSES).optional(),
|
|
56
|
+
effort: z.number().optional(),
|
|
57
|
+
priority,
|
|
58
|
+
goalId: goalId.nullable().optional().describe('Objective (goal) ID, an integer (null to clear)'),
|
|
59
|
+
areaId: z.string().nullable().optional().describe('Area ID (null to clear)'),
|
|
60
|
+
}),
|
|
61
|
+
run: (client, params) => client.features.update(params),
|
|
62
|
+
}),
|
|
63
|
+
delete: op({
|
|
64
|
+
summary: 'Delete a feature permanently, cascading its scopes, requirements, user stories, comments and page links; linked tickets are kept but unlinked.',
|
|
65
|
+
params: z.object({ id: id('Feature') }),
|
|
66
|
+
run: (client, { id }) => client.features.delete(id),
|
|
67
|
+
}),
|
|
68
|
+
link_page: op({
|
|
69
|
+
summary: 'Link a Knowledge page (PRD, spec, research) to a feature. Idempotent: re-linking updates the scope pin. Page must be in the feature’s workspace.',
|
|
70
|
+
params: z.object({
|
|
71
|
+
featureId: id('Feature'),
|
|
72
|
+
pageId: id('Page'),
|
|
73
|
+
scopeId: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Pin the page to one of this feature’s scopes; omitting it on a re-link clears an existing pin'),
|
|
77
|
+
}),
|
|
78
|
+
run: (client, params) => client.features.linkPage(params),
|
|
79
|
+
}),
|
|
80
|
+
unlink_page: op({
|
|
81
|
+
summary: 'Unlink a page from a feature. The page itself is not deleted; no-op if not linked.',
|
|
82
|
+
params: z.object({
|
|
83
|
+
featureId: id('Feature'),
|
|
84
|
+
pageId: id('Page'),
|
|
85
|
+
}),
|
|
86
|
+
run: (client, { featureId, pageId }) => client.features.unlinkPage(featureId, pageId),
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain tools: one MCP tool per SDK namespace, taking `{ operation, params }`.
|
|
3
|
+
*
|
|
4
|
+
* Each operation declares its params once, as a zod object. That single schema
|
|
5
|
+
* drives the one-line signature in the tool description, the full JSON Schema
|
|
6
|
+
* returned by `operation: "describe"`, and runtime validation — so the three
|
|
7
|
+
* cannot drift apart. A validation failure returns the schema it expected, so
|
|
8
|
+
* the model can correct itself on the next call without a separate lookup.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
import type { ExponentialClient } from 'exponential-sdk';
|
|
13
|
+
export interface Operation<S extends z.AnyZodObject = z.AnyZodObject> {
|
|
14
|
+
/** One line: what the operation does and anything surprising about it. */
|
|
15
|
+
summary: string;
|
|
16
|
+
params: S;
|
|
17
|
+
run: (client: ExponentialClient, params: z.infer<S>) => Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface Domain {
|
|
20
|
+
/** Tool name, e.g. `contacts`. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** One or two sentences: what the domain is and how it relates to others. */
|
|
23
|
+
description: string;
|
|
24
|
+
operations: Record<string, Operation<any>>;
|
|
25
|
+
}
|
|
26
|
+
/** Identity helper that keeps `params` inferred per operation. */
|
|
27
|
+
export declare function op<S extends z.AnyZodObject>(operation: Operation<S>): Operation<S>;
|
|
28
|
+
/** Shared param building blocks. */
|
|
29
|
+
export declare const id: (what: string) => z.ZodString;
|
|
30
|
+
export declare const workspaceId: z.ZodString;
|
|
31
|
+
/** ISO date or datetime; coerced to a Date before it reaches the SDK. */
|
|
32
|
+
export declare const date: (what: string) => z.ZodDate;
|
|
33
|
+
export declare function toolDefinition(domain: Domain): Tool;
|
|
34
|
+
export declare function runDomainTool(domain: Domain, client: ExponentialClient, args: Record<string, unknown> | undefined): Promise<unknown>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain tools: one MCP tool per SDK namespace, taking `{ operation, params }`.
|
|
3
|
+
*
|
|
4
|
+
* Each operation declares its params once, as a zod object. That single schema
|
|
5
|
+
* drives the one-line signature in the tool description, the full JSON Schema
|
|
6
|
+
* returned by `operation: "describe"`, and runtime validation — so the three
|
|
7
|
+
* cannot drift apart. A validation failure returns the schema it expected, so
|
|
8
|
+
* the model can correct itself on the next call without a separate lookup.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
12
|
+
/** Identity helper that keeps `params` inferred per operation. */
|
|
13
|
+
export function op(operation) {
|
|
14
|
+
return operation;
|
|
15
|
+
}
|
|
16
|
+
/** Shared param building blocks. */
|
|
17
|
+
export const id = (what) => z.string().describe(`${what} ID`);
|
|
18
|
+
export const workspaceId = z.string().describe('Workspace ID (see the `workspaces` tool)');
|
|
19
|
+
/** ISO date or datetime; coerced to a Date before it reaches the SDK. */
|
|
20
|
+
export const date = (what) => z.coerce.date().describe(`${what} (ISO 8601 date or datetime)`);
|
|
21
|
+
const DESCRIBE = 'describe';
|
|
22
|
+
function signature(name, params) {
|
|
23
|
+
const keys = Object.entries(params.shape).map(([key, schema]) => (schema.isOptional() ? `${key}?` : key));
|
|
24
|
+
return `${name}(${keys.join(', ')})`;
|
|
25
|
+
}
|
|
26
|
+
function paramsJsonSchema(params) {
|
|
27
|
+
// Cast: zodToJsonSchema's generics recurse too deep on a bare AnyZodObject (TS2589).
|
|
28
|
+
const { $schema: _ignored, ...schema } = zodToJsonSchema(params, {
|
|
29
|
+
target: 'jsonSchema7',
|
|
30
|
+
$refStrategy: 'none',
|
|
31
|
+
});
|
|
32
|
+
return schema;
|
|
33
|
+
}
|
|
34
|
+
export function toolDefinition(domain) {
|
|
35
|
+
const operations = Object.keys(domain.operations);
|
|
36
|
+
const lines = Object.entries(domain.operations).map(([name, operation]) => `- ${signature(name, operation.params)} — ${operation.summary}`);
|
|
37
|
+
return {
|
|
38
|
+
name: domain.name,
|
|
39
|
+
description: [
|
|
40
|
+
domain.description,
|
|
41
|
+
'',
|
|
42
|
+
'Call with { operation, params }. Operations (? = optional param):',
|
|
43
|
+
...lines,
|
|
44
|
+
`- ${DESCRIBE}(operation?) — full parameter schema for one operation, or all of them.`,
|
|
45
|
+
].join('\n'),
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
operation: { type: 'string', enum: [...operations, DESCRIBE] },
|
|
50
|
+
params: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
description: 'Parameters for the operation, as listed in the tool description.',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ['operation'],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function describe(domain, only) {
|
|
60
|
+
const names = typeof only === 'string' && only ? [only] : Object.keys(domain.operations);
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
const operation = domain.operations[name];
|
|
64
|
+
if (!operation) {
|
|
65
|
+
throw new Error(`Unknown operation "${name}" for ${domain.name}. Valid: ${Object.keys(domain.operations).join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
out[name] = { summary: operation.summary, params: paramsJsonSchema(operation.params) };
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
export async function runDomainTool(domain, client, args) {
|
|
72
|
+
const operationName = args?.operation;
|
|
73
|
+
const rawParams = (args?.params ?? {});
|
|
74
|
+
if (operationName === DESCRIBE) {
|
|
75
|
+
return describe(domain, rawParams.operation);
|
|
76
|
+
}
|
|
77
|
+
const operation = typeof operationName === 'string' ? domain.operations[operationName] : undefined;
|
|
78
|
+
if (!operation) {
|
|
79
|
+
throw new Error(`Unknown operation "${String(operationName)}" for ${domain.name}. Valid: ${[
|
|
80
|
+
...Object.keys(domain.operations),
|
|
81
|
+
DESCRIBE,
|
|
82
|
+
].join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
const parsed = operation.params.safeParse(rawParams);
|
|
85
|
+
if (!parsed.success) {
|
|
86
|
+
const issues = parsed.error.issues
|
|
87
|
+
.map((issue) => `${issue.path.join('.') || 'params'}: ${issue.message}`)
|
|
88
|
+
.join('; ');
|
|
89
|
+
throw new Error(`Invalid params for ${domain.name}.${operationName}: ${issues}\nExpected: ${JSON.stringify(paramsJsonSchema(operation.params))}`);
|
|
90
|
+
}
|
|
91
|
+
const result = await operation.run(client, parsed.data);
|
|
92
|
+
// Void SDK calls (deletes) would otherwise serialize to nothing.
|
|
93
|
+
return result === undefined ? { success: true } : result;
|
|
94
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { date, id, op, workspaceId } from './framework.js';
|
|
3
|
+
const GOAL_STATUSES = ['planned', 'active', 'completed', 'archived', 'on-hold'];
|
|
4
|
+
/** `goals.update` rejects `on-hold`; only `set_status` accepts it. */
|
|
5
|
+
const WRITABLE_STATUSES = ['planned', 'active', 'completed', 'archived'];
|
|
6
|
+
/** Objectives are keyed by integer id, unlike most cuid-keyed entities. */
|
|
7
|
+
const goalId = (what = 'Objective (goal)') => z.number().int().describe(`${what} ID — an integer`);
|
|
8
|
+
const period = z.string().describe('Free-form OKR period, e.g. "Q3-2026", "H1-2027", "Annual-2026" (see `periods`)');
|
|
9
|
+
const clearable = (what) => z.string().nullable().optional().describe(`${what} (null to clear)`);
|
|
10
|
+
export const goals = {
|
|
11
|
+
name: 'goals',
|
|
12
|
+
description: 'Objectives (`Goal`) — the qualitative half of an OKR, keyed by integer id and nestable up to 5 levels. Their measurable key results live in the `key_results` tool; discussion on an objective is in `comments` (target "goal").',
|
|
13
|
+
operations: {
|
|
14
|
+
list: op({
|
|
15
|
+
summary: 'List objectives with their linked projects. With workspaceId: every member’s; without: only your own.',
|
|
16
|
+
params: z.object({
|
|
17
|
+
workspaceId: workspaceId.optional().describe('Workspace ID for the workspace-wide list; omit for only your own'),
|
|
18
|
+
period: period.optional(),
|
|
19
|
+
status: z.enum(GOAL_STATUSES).optional(),
|
|
20
|
+
}),
|
|
21
|
+
run: (client, params) => client.goals.list(params),
|
|
22
|
+
}),
|
|
23
|
+
tree: op({
|
|
24
|
+
summary: 'Objectives nested parent → child (up to 5 levels), each with projects and key results. Only roots at top level.',
|
|
25
|
+
params: z.object({
|
|
26
|
+
workspaceId: workspaceId.optional(),
|
|
27
|
+
status: z.enum(GOAL_STATUSES).optional(),
|
|
28
|
+
}),
|
|
29
|
+
run: (client, params) => client.goals.tree(params),
|
|
30
|
+
}),
|
|
31
|
+
get: op({
|
|
32
|
+
summary: 'Get one objective.',
|
|
33
|
+
params: z.object({ id: goalId() }),
|
|
34
|
+
run: (client, { id }) => client.goals.get(id),
|
|
35
|
+
}),
|
|
36
|
+
list_by_project: op({
|
|
37
|
+
summary: 'Objectives linked to a given project.',
|
|
38
|
+
params: z.object({ projectId: id('Project') }),
|
|
39
|
+
run: (client, { projectId }) => client.goals.listByProject(projectId),
|
|
40
|
+
}),
|
|
41
|
+
create: op({
|
|
42
|
+
summary: 'Create an objective, optionally nested under a parent and linked to one project.',
|
|
43
|
+
params: z.object({
|
|
44
|
+
title: z.string(),
|
|
45
|
+
description: z.string().optional(),
|
|
46
|
+
whyThisGoal: z.string().optional(),
|
|
47
|
+
notes: z.string().optional(),
|
|
48
|
+
dueDate: date('Due date').optional(),
|
|
49
|
+
period: period.optional(),
|
|
50
|
+
status: z.enum(WRITABLE_STATUSES).optional(),
|
|
51
|
+
lifeDomainId: z.number().int().optional(),
|
|
52
|
+
projectId: id('Project').optional().describe('Link the objective to one project on creation'),
|
|
53
|
+
outcomeIds: z.array(z.string()).optional(),
|
|
54
|
+
driUserId: id('Directly responsible user').optional(),
|
|
55
|
+
workspaceId: workspaceId.optional(),
|
|
56
|
+
parentGoalId: goalId('Parent objective').optional().describe('Nest under this objective (integer ID). Max nesting depth is 5'),
|
|
57
|
+
icon: z.string().nullable().optional(),
|
|
58
|
+
iconColor: z.string().nullable().optional(),
|
|
59
|
+
}),
|
|
60
|
+
run: (client, params) => client.goals.create(params),
|
|
61
|
+
}),
|
|
62
|
+
update: op({
|
|
63
|
+
summary: 'Partial update: omitted fields are untouched, null clears. Prefer set_status / set_parent when that is all you change.',
|
|
64
|
+
params: z.object({
|
|
65
|
+
id: goalId(),
|
|
66
|
+
title: z.string().optional(),
|
|
67
|
+
description: clearable('Description'),
|
|
68
|
+
whyThisGoal: clearable('Why this goal'),
|
|
69
|
+
notes: clearable('Notes'),
|
|
70
|
+
dueDate: date('Due date').nullable().optional().describe('Due date, ISO 8601 (null to clear)'),
|
|
71
|
+
period: clearable('Free-form OKR period, e.g. "Q3-2026"'),
|
|
72
|
+
status: z.enum(WRITABLE_STATUSES).optional().describe('"on-hold" is only accepted by set_status'),
|
|
73
|
+
lifeDomainId: z.number().int().nullable().optional(),
|
|
74
|
+
projectId: clearable('Replace the project links with this one project'),
|
|
75
|
+
projectIds: z.array(z.string()).optional().describe('Replace the project links wholesale; [] clears them'),
|
|
76
|
+
outcomeIds: z.array(z.string()).optional(),
|
|
77
|
+
driUserId: clearable('Directly responsible user ID'),
|
|
78
|
+
workspaceId: clearable('Workspace ID'),
|
|
79
|
+
parentGoalId: z.number().int().nullable().optional().describe('Parent objective integer ID (null to detach)'),
|
|
80
|
+
displayOrder: z.number().int().optional(),
|
|
81
|
+
icon: z.string().nullable().optional(),
|
|
82
|
+
iconColor: z.string().nullable().optional(),
|
|
83
|
+
}),
|
|
84
|
+
run: (client, params) => client.goals.update(params),
|
|
85
|
+
}),
|
|
86
|
+
set_status: op({
|
|
87
|
+
summary: 'Status-only write; the only path that accepts "on-hold". Moving to "completed" also records a workspace milestone.',
|
|
88
|
+
params: z.object({
|
|
89
|
+
id: goalId(),
|
|
90
|
+
status: z.enum(GOAL_STATUSES),
|
|
91
|
+
}),
|
|
92
|
+
run: (client, params) => client.goals.setStatus(params),
|
|
93
|
+
}),
|
|
94
|
+
set_parent: op({
|
|
95
|
+
summary: 'Re-parent an objective (or detach with null). Writes only parentGoalId; rejects self-parenting, cycles and depth > 5.',
|
|
96
|
+
params: z.object({
|
|
97
|
+
id: goalId(),
|
|
98
|
+
parentGoalId: z.number().int().nullable().describe('New parent objective integer ID, or null to detach'),
|
|
99
|
+
}),
|
|
100
|
+
run: (client, params) => client.goals.setParent(params),
|
|
101
|
+
}),
|
|
102
|
+
delete: op({
|
|
103
|
+
summary: 'Delete an objective permanently. Its key results are deleted with it; child objectives are detached, not deleted.',
|
|
104
|
+
params: z.object({ id: goalId() }),
|
|
105
|
+
run: (client, { id }) => client.goals.delete(id),
|
|
106
|
+
}),
|
|
107
|
+
periods: op({
|
|
108
|
+
summary: 'The conventional period strings (quarters, halves, annual) for this year and next.',
|
|
109
|
+
params: z.object({}),
|
|
110
|
+
run: (client) => client.goals.periods(),
|
|
111
|
+
}),
|
|
112
|
+
stats: op({
|
|
113
|
+
summary: 'Aggregate objective/key-result counts, status breakdown and average progress.',
|
|
114
|
+
params: z.object({
|
|
115
|
+
workspaceId: workspaceId.optional(),
|
|
116
|
+
period: period.optional(),
|
|
117
|
+
}),
|
|
118
|
+
run: (client, params) => client.goals.stats(params),
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { actions } from './actions.js';
|
|
2
|
+
import { areas } from './areas.js';
|
|
3
|
+
import { comments } from './comments.js';
|
|
4
|
+
import { contacts } from './contacts.js';
|
|
5
|
+
import { deals } from './deals.js';
|
|
6
|
+
import { decisions } from './decisions.js';
|
|
7
|
+
import { epics } from './epics.js';
|
|
8
|
+
import { features } from './features.js';
|
|
9
|
+
import { goals } from './goals.js';
|
|
10
|
+
import { keyResults } from './keyResults.js';
|
|
11
|
+
import { labels } from './labels.js';
|
|
12
|
+
import { meetings } from './meetings.js';
|
|
13
|
+
import { organizations } from './organizations.js';
|
|
14
|
+
import { pages } from './pages.js';
|
|
15
|
+
import { products } from './products.js';
|
|
16
|
+
import { projects } from './projects.js';
|
|
17
|
+
import { requirements } from './requirements.js';
|
|
18
|
+
import { scopes } from './scopes.js';
|
|
19
|
+
import { stories } from './stories.js';
|
|
20
|
+
import { tickets } from './tickets.js';
|
|
21
|
+
import { time } from './time.js';
|
|
22
|
+
import { workspaces } from './workspaces.js';
|
|
23
|
+
export { runDomainTool, toolDefinition } from './framework.js';
|
|
24
|
+
/** Every domain tool, keyed by tool name. */
|
|
25
|
+
export const DOMAINS = new Map([
|
|
26
|
+
actions,
|
|
27
|
+
projects,
|
|
28
|
+
workspaces,
|
|
29
|
+
goals,
|
|
30
|
+
keyResults,
|
|
31
|
+
meetings,
|
|
32
|
+
time,
|
|
33
|
+
decisions,
|
|
34
|
+
contacts,
|
|
35
|
+
organizations,
|
|
36
|
+
deals,
|
|
37
|
+
products,
|
|
38
|
+
features,
|
|
39
|
+
scopes,
|
|
40
|
+
requirements,
|
|
41
|
+
areas,
|
|
42
|
+
stories,
|
|
43
|
+
epics,
|
|
44
|
+
tickets,
|
|
45
|
+
labels,
|
|
46
|
+
pages,
|
|
47
|
+
comments,
|
|
48
|
+
].map((domain) => [domain.name, domain]));
|