exponential-mcp 0.5.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 +36 -1
- 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 +345 -2
- package/package.json +5 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id, op } from './framework.js';
|
|
3
|
+
const TICKET_STATUSES = [
|
|
4
|
+
'BACKLOG',
|
|
5
|
+
'NEEDS_REFINEMENT',
|
|
6
|
+
'READY_TO_PLAN',
|
|
7
|
+
'COMMITTED',
|
|
8
|
+
'IN_PROGRESS',
|
|
9
|
+
'BLOCKED',
|
|
10
|
+
'QA',
|
|
11
|
+
'DONE',
|
|
12
|
+
'DEPLOYED',
|
|
13
|
+
'ARCHIVED',
|
|
14
|
+
];
|
|
15
|
+
const TICKET_TYPES = ['BUG', 'FEATURE', 'CHORE', 'IMPROVEMENT', 'SPIKE', 'RESEARCH'];
|
|
16
|
+
const ticketId = z.string().describe('Ticket ID (the CUID, not the shortId or number — find it with `search`)');
|
|
17
|
+
const url = (what) => z.string().url().max(500).describe(`${what} URL`);
|
|
18
|
+
const clearable = (schema, what) => schema.nullable().optional().describe(`${what} (null to clear)`);
|
|
19
|
+
export const tickets = {
|
|
20
|
+
name: 'tickets',
|
|
21
|
+
description: 'Tickets: the unit of work in a product backlog, with blocking dependencies and linked actions. Always product-scoped (see `products`); they can point at a feature (`features`), scope (`scopes`), epic (`epics`) and cycle. Ticket comments are in the `comments` tool and labels in `labels`.',
|
|
22
|
+
operations: {
|
|
23
|
+
list: op({
|
|
24
|
+
summary: 'List every ticket in one product with full fields (body included), optionally filtered. No workspace-wide query — call once per product. prUrl/branchName are exact-match filters applied after the fetch.',
|
|
25
|
+
params: z.object({
|
|
26
|
+
productId: id('Product'),
|
|
27
|
+
status: z.enum(TICKET_STATUSES).optional(),
|
|
28
|
+
type: z.enum(TICKET_TYPES).optional(),
|
|
29
|
+
featureId: id('Feature').optional(),
|
|
30
|
+
epicId: id('Epic').optional(),
|
|
31
|
+
cycleId: id('Cycle').optional(),
|
|
32
|
+
assigneeId: id('User').optional(),
|
|
33
|
+
prUrl: z.string().optional().describe('Exact match on the ticket’s PR URL'),
|
|
34
|
+
branchName: z.string().optional().describe('Exact match on the ticket’s branch name'),
|
|
35
|
+
}),
|
|
36
|
+
run: (client, params) => client.tickets.list(params),
|
|
37
|
+
}),
|
|
38
|
+
get: op({
|
|
39
|
+
summary: 'Get one ticket in full: body, product, dependencies (dependsOn / requiredFor), linked actions and comments (newest first).',
|
|
40
|
+
params: z.object({ id: ticketId }),
|
|
41
|
+
run: (client, { id }) => client.tickets.get(id),
|
|
42
|
+
}),
|
|
43
|
+
create: op({
|
|
44
|
+
summary: 'Create a ticket in a product. Number and shortId are assigned server-side.',
|
|
45
|
+
params: z.object({
|
|
46
|
+
productId: id('Product'),
|
|
47
|
+
title: z.string().min(1).max(300),
|
|
48
|
+
body: z.string().optional().describe('Markdown body. Omit with templateId to start from the template’s body'),
|
|
49
|
+
type: z.enum(TICKET_TYPES).optional(),
|
|
50
|
+
status: z.enum(TICKET_STATUSES).optional(),
|
|
51
|
+
priority: z.number().int().min(0).max(4).optional().describe('0–4; lower is higher priority'),
|
|
52
|
+
points: z.number().optional(),
|
|
53
|
+
branchName: z.string().optional(),
|
|
54
|
+
prUrl: url('Pull request').optional(),
|
|
55
|
+
designUrl: url('Design').optional(),
|
|
56
|
+
specUrl: url('Spec').optional(),
|
|
57
|
+
links: z.record(z.string()).optional().describe('Extra named links, label → URL'),
|
|
58
|
+
epicId: id('Epic').optional(),
|
|
59
|
+
featureId: id('Feature').optional(),
|
|
60
|
+
cycleId: id('Cycle').optional(),
|
|
61
|
+
scopeId: id('Feature scope').optional(),
|
|
62
|
+
assigneeId: id('User').optional().describe('User ID; must be a member of the product’s workspace'),
|
|
63
|
+
templateId: id('Ticket template').optional(),
|
|
64
|
+
}),
|
|
65
|
+
run: (client, params) => client.tickets.create(params),
|
|
66
|
+
}),
|
|
67
|
+
update: op({
|
|
68
|
+
summary: 'Update a ticket. Only the fields you pass change.',
|
|
69
|
+
params: z.object({
|
|
70
|
+
id: ticketId,
|
|
71
|
+
title: z.string().min(1).max(300).optional(),
|
|
72
|
+
body: z.string().optional().describe('Markdown body; replaces the whole body'),
|
|
73
|
+
type: z.enum(TICKET_TYPES).optional(),
|
|
74
|
+
status: z.enum(TICKET_STATUSES).optional(),
|
|
75
|
+
priority: clearable(z.number().int().min(0).max(4), 'Priority 0–4, lower is higher'),
|
|
76
|
+
points: clearable(z.number(), 'Story points'),
|
|
77
|
+
branchName: clearable(z.string(), 'Branch name'),
|
|
78
|
+
prUrl: clearable(z.string().url().max(500), 'Pull request URL'),
|
|
79
|
+
designUrl: clearable(z.string().url().max(500), 'Design URL'),
|
|
80
|
+
specUrl: clearable(z.string().url().max(500), 'Spec URL'),
|
|
81
|
+
links: clearable(z.record(z.string()), 'Named links, label → URL; replaces the whole map'),
|
|
82
|
+
epicId: clearable(z.string(), 'Epic ID'),
|
|
83
|
+
featureId: clearable(z.string(), 'Feature ID'),
|
|
84
|
+
cycleId: clearable(z.string(), 'Cycle ID'),
|
|
85
|
+
scopeId: clearable(z.string(), 'Feature scope ID'),
|
|
86
|
+
assigneeId: clearable(z.string(), 'Assignee user ID'),
|
|
87
|
+
}),
|
|
88
|
+
run: (client, params) => client.tickets.update(params),
|
|
89
|
+
}),
|
|
90
|
+
delete: op({
|
|
91
|
+
summary: 'Delete a ticket permanently, with its comments and dependency edges. Linked actions are kept but unlinked.',
|
|
92
|
+
params: z.object({ id: ticketId }),
|
|
93
|
+
run: (client, { id }) => client.tickets.delete(id),
|
|
94
|
+
}),
|
|
95
|
+
search: op({
|
|
96
|
+
summary: 'Quick picker search within one product: matches title, shortId or ticket number and returns slim rows (id, number, shortId, title, status, priority, assignee), most recently updated first. Use `list` for full tickets or field filters.',
|
|
97
|
+
params: z.object({
|
|
98
|
+
productId: id('Product'),
|
|
99
|
+
query: z.string().max(200).optional().describe('Title substring, shortId, or a bare ticket number'),
|
|
100
|
+
excludeTicketId: id('Ticket to leave out of the results').optional(),
|
|
101
|
+
limit: z.number().int().min(1).max(50).optional().describe('Default 20'),
|
|
102
|
+
}),
|
|
103
|
+
run: (client, params) => client.tickets.search(params),
|
|
104
|
+
}),
|
|
105
|
+
add_dependency: op({
|
|
106
|
+
summary: 'Mark ticketId as blocked by dependsOnId. Both must be in the same product; rejects self-dependencies and cycles. Idempotent.',
|
|
107
|
+
params: z.object({
|
|
108
|
+
ticketId: id('Blocked ticket'),
|
|
109
|
+
dependsOnId: id('Blocking ticket'),
|
|
110
|
+
}),
|
|
111
|
+
run: (client, { ticketId, dependsOnId }) => client.tickets.addDependency(ticketId, dependsOnId),
|
|
112
|
+
}),
|
|
113
|
+
remove_dependency: op({
|
|
114
|
+
summary: 'Remove a "ticketId is blocked by dependsOnId" edge. Succeeds even if no such edge exists.',
|
|
115
|
+
params: z.object({
|
|
116
|
+
ticketId: id('Blocked ticket'),
|
|
117
|
+
dependsOnId: id('Blocking ticket'),
|
|
118
|
+
}),
|
|
119
|
+
run: (client, { ticketId, dependsOnId }) => client.tickets.removeDependency(ticketId, dependsOnId),
|
|
120
|
+
}),
|
|
121
|
+
link_action: op({
|
|
122
|
+
summary: 'Attach one of your own actions to a ticket (an action belongs to at most one ticket, so this moves it if already linked).',
|
|
123
|
+
params: z.object({
|
|
124
|
+
ticketId: id('Ticket'),
|
|
125
|
+
actionId: id('Action'),
|
|
126
|
+
}),
|
|
127
|
+
run: (client, { ticketId, actionId }) => client.tickets.linkAction(ticketId, actionId),
|
|
128
|
+
}),
|
|
129
|
+
unlink_action: op({
|
|
130
|
+
summary: 'Detach one of your own actions from whatever ticket it is linked to.',
|
|
131
|
+
params: z.object({ actionId: id('Action') }),
|
|
132
|
+
run: (client, { actionId }) => client.tickets.unlinkAction(actionId),
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { date, id, op } from './framework.js';
|
|
3
|
+
const TIME_ENTRY_STATUSES = ['PROPOSED', 'CONFIRMED'];
|
|
4
|
+
const WRITABLE_SOURCES = ['manual', 'claude-desktop', 'agent-run'];
|
|
5
|
+
const day = z
|
|
6
|
+
.string()
|
|
7
|
+
.describe("Calendar day as YYYY-MM-DD (or an ISO timestamp); resolved to local midnight in the MCP server's timezone");
|
|
8
|
+
const optionalWorkspaceId = z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Workspace ID (see the `workspaces` tool); omit for all workspaces');
|
|
12
|
+
const entryFields = {
|
|
13
|
+
actionId: id('Action'),
|
|
14
|
+
startedAt: date('Start'),
|
|
15
|
+
endedAt: date('End (must be after startedAt)'),
|
|
16
|
+
source: z
|
|
17
|
+
.enum(WRITABLE_SOURCES)
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Defaults to manual. claude-desktop = Daily worklog proposed time; agent-run = unattended agent time'),
|
|
20
|
+
status: z
|
|
21
|
+
.enum(TIME_ENTRY_STATUSES)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Defaults to CONFIRMED for a human; ignored (always PROPOSED) under an agent key'),
|
|
24
|
+
sourceRef: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Idempotency key, e.g. `claude-session:<sessionId>#<segmentIndex>`; makes the write an upsert'),
|
|
28
|
+
note: z.string().max(1000).optional().describe('One line shown in the day view'),
|
|
29
|
+
};
|
|
30
|
+
const partialEntry = z.object(entryFields).partial();
|
|
31
|
+
export const time = {
|
|
32
|
+
name: 'time',
|
|
33
|
+
description: 'Time entries logged against actions (the Daily worklog write path). An entry always belongs to the person whose time it was: under an agent key it is owned by the agent’s owner and forced to PROPOSED until they confirm the day. Actions live in the `actions` tool.',
|
|
34
|
+
operations: {
|
|
35
|
+
log: op({
|
|
36
|
+
summary: 'Log one completed entry with explicit bounds; never touches the running Timer. With sourceRef it upserts per owner (updates a PROPOSED entry, leaves a CONFIRMED one); without, every call creates a row. Outcome: created / updated / left / merged / dropped.',
|
|
37
|
+
params: z.object(entryFields),
|
|
38
|
+
run: (client, params) => client.time.log(params),
|
|
39
|
+
}),
|
|
40
|
+
log_batch: op({
|
|
41
|
+
summary: 'Log many entries in order, one call each; a failure is reported per entry ({ index, success, error }) instead of failing the batch. `defaults` fill fields an entry omits.',
|
|
42
|
+
params: z.object({
|
|
43
|
+
entries: z.array(partialEntry).describe('Entries; each needs actionId, startedAt, endedAt after defaults apply'),
|
|
44
|
+
defaults: partialEntry.optional(),
|
|
45
|
+
}),
|
|
46
|
+
run: (client, { entries, defaults }) => client.time.logBatch(entries, defaults),
|
|
47
|
+
}),
|
|
48
|
+
confirm_day: op({
|
|
49
|
+
summary: 'Flip every PROPOSED entry of yours starting on that day to CONFIRMED, moving the actions’ spent time. Human only — agent keys are FORBIDDEN. Returns { confirmed }.',
|
|
50
|
+
params: z.object({ date: day, workspaceId: optionalWorkspaceId }),
|
|
51
|
+
run: (client, { date, workspaceId }) => client.time.confirmDay(date, workspaceId),
|
|
52
|
+
}),
|
|
53
|
+
day_report: op({
|
|
54
|
+
summary: 'One day’s time as the /time Day tab shows it: entries joined to action/ticket/project/product, attention vs session vs agent-run minutes, per-product and per-action roll-ups. Under an agent key it is the owner’s day.',
|
|
55
|
+
params: z.object({ date: day, workspaceId: optionalWorkspaceId }),
|
|
56
|
+
run: (client, { date, workspaceId }) => client.time.dayReport(date, workspaceId),
|
|
57
|
+
}),
|
|
58
|
+
list: op({
|
|
59
|
+
summary: 'Your raw time entries touching one day, oldest first, each with its status.',
|
|
60
|
+
params: z.object({ date: day, workspaceId: optionalWorkspaceId }),
|
|
61
|
+
run: (client, { date, workspaceId }) => client.time.list(date, workspaceId),
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { op, workspaceId } from './framework.js';
|
|
3
|
+
export const workspaces = {
|
|
4
|
+
name: 'workspaces',
|
|
5
|
+
description: 'Workspaces the user belongs to, and their members. Most other tools take a workspaceId from here; use list_members to resolve people for mentions and assignment.',
|
|
6
|
+
operations: {
|
|
7
|
+
list: op({
|
|
8
|
+
summary: 'List the workspaces you belong to.',
|
|
9
|
+
params: z.object({}),
|
|
10
|
+
run: (client) => client.workspaces.list(),
|
|
11
|
+
}),
|
|
12
|
+
list_members: op({
|
|
13
|
+
summary: 'Everyone in a workspace — direct members plus those with access via a linked team. Each row carries `mentionSyntax` to paste into a comment body.',
|
|
14
|
+
params: z.object({ workspaceId }),
|
|
15
|
+
run: (client, { workspaceId }) => client.workspaces.listMembers(workspaceId),
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import { dirname, join } from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
+
import { DOMAINS, runDomainTool, toolDefinition } from './domains/index.js';
|
|
14
15
|
const LEGACY_CONFIG_PATH = join(homedir(), '.config', 'exponential-mcp', 'config.json');
|
|
15
16
|
// Report the real package version rather than a hand-maintained literal that drifts.
|
|
16
17
|
const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8')).version;
|
|
@@ -106,6 +107,49 @@ function parseDateArg(raw) {
|
|
|
106
107
|
}
|
|
107
108
|
return parsed;
|
|
108
109
|
}
|
|
110
|
+
const SUMMARY_PREVIEW_CHARS = 400;
|
|
111
|
+
const DEFAULT_MEETING_LIMIT = 30;
|
|
112
|
+
/**
|
|
113
|
+
* A meeting row as it comes off the list read is ~3k characters of metadata
|
|
114
|
+
* plus the full transcript, summary and notes bodies — a single hour-long
|
|
115
|
+
* transcript is easily 50k+ characters, and the server offers no limit or
|
|
116
|
+
* projection. The list tool sends a compact row with a summary preview; the
|
|
117
|
+
* detail tool sends everything except the transcript unless asked.
|
|
118
|
+
*/
|
|
119
|
+
function meetingListRow(m) {
|
|
120
|
+
const summary = m.summary ?? null;
|
|
121
|
+
const truncated = summary !== null && summary.length > SUMMARY_PREVIEW_CHARS;
|
|
122
|
+
return {
|
|
123
|
+
id: m.id,
|
|
124
|
+
title: m.title,
|
|
125
|
+
description: m.description,
|
|
126
|
+
meetingDate: m.meetingDate,
|
|
127
|
+
createdAt: m.createdAt,
|
|
128
|
+
durationSeconds: m.durationSeconds ?? null,
|
|
129
|
+
participantCount: m.participantCount ?? null,
|
|
130
|
+
projectId: m.projectId,
|
|
131
|
+
project: m.project ?? null,
|
|
132
|
+
workspaceId: m.workspaceId,
|
|
133
|
+
archivedAt: m.archivedAt,
|
|
134
|
+
source: m.sourceIntegration?.provider ?? null,
|
|
135
|
+
participants: (m.participants ?? []).map((p) => ({ id: p.id, name: p.name, email: p.email })),
|
|
136
|
+
actions: m.actions ?? [],
|
|
137
|
+
summaryPreview: truncated ? summary.slice(0, SUMMARY_PREVIEW_CHARS) + '…' : summary,
|
|
138
|
+
summaryTruncated: truncated,
|
|
139
|
+
hasNotes: Boolean(m.notes),
|
|
140
|
+
hasTranscript: Boolean(m.transcription),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function meetingDetail(m, includeTranscript) {
|
|
144
|
+
// sentencesJson / analyticsJson are the server's internal processing
|
|
145
|
+
// artefacts; they are not part of the SDK type and never useful to a model.
|
|
146
|
+
const { transcription, sentencesJson, analyticsJson, ...rest } = m;
|
|
147
|
+
return {
|
|
148
|
+
...rest,
|
|
149
|
+
hasTranscript: Boolean(transcription),
|
|
150
|
+
...(includeTranscript ? { transcription } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
109
153
|
// Tool definitions
|
|
110
154
|
const TOOLS = [
|
|
111
155
|
{
|
|
@@ -121,6 +165,64 @@ const TOOLS = [
|
|
|
121
165
|
}
|
|
122
166
|
}
|
|
123
167
|
},
|
|
168
|
+
{
|
|
169
|
+
name: 'get_project',
|
|
170
|
+
description: 'Fetch one project with its relations: the objectives and key results it is linked to, DRI, team, dates, description, and linked meetings. get_projects returns none of the OKR links, so this is the tool for "what is this project driving?". It does NOT include the project\'s tasks — use get_actions with projectId for those. Accepts a project ID, a slug, or the slug-id form from app URLs.',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
id: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'Project ID, slug, or the compound slug-id from an app URL'
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
required: ['id']
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'update_project',
|
|
184
|
+
description: 'Update a project: rename, change status or priority, edit the description, set dates, or re-link it to objectives / key results. Only the fields you pass are changed. goalIds and keyResultIds REPLACE the existing links wholesale — read get_project first and pass the full set you want to keep. An empty keyResultIds clears the key-result links; an empty goalIds is ignored by the server.',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
id: { type: 'string', description: 'Project ID' },
|
|
189
|
+
name: { type: 'string', description: 'New name' },
|
|
190
|
+
description: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'New description. Pass an empty string to blank it.'
|
|
193
|
+
},
|
|
194
|
+
status: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
enum: ['ACTIVE', 'ON_HOLD', 'COMPLETED', 'CANCELLED'],
|
|
197
|
+
description: 'New lifecycle status'
|
|
198
|
+
},
|
|
199
|
+
priority: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
enum: ['HIGH', 'MEDIUM', 'LOW', 'NONE'],
|
|
202
|
+
description: 'New priority'
|
|
203
|
+
},
|
|
204
|
+
driId: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
description: 'User ID of the directly responsible individual, or null to clear'
|
|
207
|
+
},
|
|
208
|
+
goalIds: {
|
|
209
|
+
type: 'array',
|
|
210
|
+
items: { type: 'string' },
|
|
211
|
+
description: 'Objective IDs (integers, passed as strings) to link — replaces the current set'
|
|
212
|
+
},
|
|
213
|
+
keyResultIds: {
|
|
214
|
+
type: 'array',
|
|
215
|
+
items: { type: 'string' },
|
|
216
|
+
description: 'Key result IDs to link — replaces the current set; [] clears'
|
|
217
|
+
},
|
|
218
|
+
startDate: { type: 'string', description: 'ISO datetime, or null to clear' },
|
|
219
|
+
endDate: { type: 'string', description: 'ISO datetime, or null to clear' },
|
|
220
|
+
reviewDate: { type: 'string', description: 'ISO datetime, or null to clear' },
|
|
221
|
+
nextActionDate: { type: 'string', description: 'ISO datetime, or null to clear' }
|
|
222
|
+
},
|
|
223
|
+
required: ['id']
|
|
224
|
+
}
|
|
225
|
+
},
|
|
124
226
|
{
|
|
125
227
|
name: 'get_actions',
|
|
126
228
|
description: 'List actions/tasks, optionally filtered by project or status. This is a flat list with no date filtering — to answer "what should I work on today" or "what am I behind on", use get_todays_actions instead.',
|
|
@@ -312,6 +414,114 @@ const TOOLS = [
|
|
|
312
414
|
}
|
|
313
415
|
}
|
|
314
416
|
},
|
|
417
|
+
{
|
|
418
|
+
name: 'get_meetings',
|
|
419
|
+
description: 'List meetings the user can see — ones they own or attended, plus those on their projects and workspaces — newest first. Each row has metadata, participants, linked actions and a short preview of the AI summary; the full summary, notes and transcript are only in get_meeting. Use this to answer "when did I last meet Y" or to find a meeting ID, then get_meeting for what was actually said.',
|
|
420
|
+
inputSchema: {
|
|
421
|
+
type: 'object',
|
|
422
|
+
properties: {
|
|
423
|
+
workspaceId: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
description: 'Optional workspace ID to restrict to one workspace'
|
|
426
|
+
},
|
|
427
|
+
meetingType: {
|
|
428
|
+
type: 'string',
|
|
429
|
+
enum: ['all', 'mine', 'one_on_one'],
|
|
430
|
+
description: 'all (default) — everything visible; mine — meetings the user owns or attended; one_on_one — exactly two participants'
|
|
431
|
+
},
|
|
432
|
+
includeArchived: {
|
|
433
|
+
type: 'boolean',
|
|
434
|
+
description: 'Include archived meetings (default false)'
|
|
435
|
+
},
|
|
436
|
+
limit: {
|
|
437
|
+
type: 'number',
|
|
438
|
+
description: 'Max meetings to return, newest first (default 30)'
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'get_meeting',
|
|
445
|
+
description: 'Fetch one meeting in full: participants with speaker labels, linked actions, the AI summary, and the meeting notes. The raw transcript is omitted unless includeTranscript is true — it can be tens of thousands of characters, so only pull it when the summary and notes are not enough.',
|
|
446
|
+
inputSchema: {
|
|
447
|
+
type: 'object',
|
|
448
|
+
properties: {
|
|
449
|
+
id: { type: 'string', description: 'Meeting ID' },
|
|
450
|
+
includeTranscript: {
|
|
451
|
+
type: 'boolean',
|
|
452
|
+
description: 'Also return the raw transcript text (default false)'
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
required: ['id']
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: 'create_meeting',
|
|
460
|
+
description: 'Record a meeting in Exponential from a transcript or notes the user gives you. transcription is required by the server — if the user only has notes, put the notes text there too. A project-linked meeting inherits its project\'s workspace.',
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: 'object',
|
|
463
|
+
properties: {
|
|
464
|
+
title: { type: 'string', description: 'Meeting title' },
|
|
465
|
+
transcription: {
|
|
466
|
+
type: 'string',
|
|
467
|
+
description: 'Raw transcript text (required, at least one character)'
|
|
468
|
+
},
|
|
469
|
+
notes: { type: 'string', description: 'Meeting notes (Markdown)' },
|
|
470
|
+
description: { type: 'string', description: 'Short description' },
|
|
471
|
+
meetingDate: {
|
|
472
|
+
type: 'string',
|
|
473
|
+
description: 'When the meeting happened, as an ISO datetime (defaults to now)'
|
|
474
|
+
},
|
|
475
|
+
projectId: { type: 'string', description: 'Link to this project' },
|
|
476
|
+
workspaceId: { type: 'string', description: 'Workspace ID (ignored when projectId is set)' },
|
|
477
|
+
participants: {
|
|
478
|
+
type: 'array',
|
|
479
|
+
description: 'Who attended. Each entry needs at least one of userId, contactId, or email.',
|
|
480
|
+
items: {
|
|
481
|
+
type: 'object',
|
|
482
|
+
properties: {
|
|
483
|
+
userId: { type: 'string' },
|
|
484
|
+
contactId: { type: 'string' },
|
|
485
|
+
email: { type: 'string' },
|
|
486
|
+
name: { type: 'string' }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
required: ['title', 'transcription']
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'update_meeting',
|
|
496
|
+
description: 'Edit a meeting\'s title, description, summary, date, or REPLACE its notes wholesale. To add to existing notes without rewriting them, use append_meeting_notes instead. Only the fields you pass are changed.',
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: 'object',
|
|
499
|
+
properties: {
|
|
500
|
+
id: { type: 'string', description: 'Meeting ID' },
|
|
501
|
+
title: { type: 'string', description: 'New title' },
|
|
502
|
+
description: { type: 'string', description: 'New description' },
|
|
503
|
+
summary: { type: 'string', description: 'New summary' },
|
|
504
|
+
notes: { type: 'string', description: 'Replacement notes body (Markdown)' },
|
|
505
|
+
meetingDate: {
|
|
506
|
+
type: 'string',
|
|
507
|
+
description: 'When the meeting happened, as an ISO datetime, or null to clear'
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
required: ['id']
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: 'append_meeting_notes',
|
|
515
|
+
description: 'Append a block of text to a meeting\'s notes, separated by a blank line, creating the notes if there are none yet. This is the safe way to add a follow-up, decision, or action list to a meeting without clobbering what is already there.',
|
|
516
|
+
inputSchema: {
|
|
517
|
+
type: 'object',
|
|
518
|
+
properties: {
|
|
519
|
+
id: { type: 'string', description: 'Meeting ID' },
|
|
520
|
+
text: { type: 'string', description: 'Text to append (Markdown)' }
|
|
521
|
+
},
|
|
522
|
+
required: ['id', 'text']
|
|
523
|
+
}
|
|
524
|
+
},
|
|
315
525
|
{
|
|
316
526
|
name: 'search',
|
|
317
527
|
description: "Global text search across everything the user can access — projects, actions/tasks, goals, workspaces (and more entity types as the API grows). Same coverage as the app's Cmd+K palette. Returns typed results with ids, workspace, and app URL.",
|
|
@@ -360,12 +570,24 @@ async function main() {
|
|
|
360
570
|
});
|
|
361
571
|
// List available tools
|
|
362
572
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
363
|
-
tools: TOOLS,
|
|
573
|
+
tools: [...TOOLS, ...[...DOMAINS.values()].map(toolDefinition)],
|
|
364
574
|
}));
|
|
365
575
|
// Handle tool calls
|
|
366
576
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
367
577
|
const { name, arguments: args } = request.params;
|
|
368
578
|
try {
|
|
579
|
+
const domain = DOMAINS.get(name);
|
|
580
|
+
if (domain) {
|
|
581
|
+
const result = await runDomainTool(domain, client, args);
|
|
582
|
+
return {
|
|
583
|
+
content: [
|
|
584
|
+
{
|
|
585
|
+
type: 'text',
|
|
586
|
+
text: JSON.stringify(result, null, 2),
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
};
|
|
590
|
+
}
|
|
369
591
|
switch (name) {
|
|
370
592
|
case 'get_projects': {
|
|
371
593
|
const projects = await client.projects.list({
|
|
@@ -380,6 +602,45 @@ async function main() {
|
|
|
380
602
|
],
|
|
381
603
|
};
|
|
382
604
|
}
|
|
605
|
+
case 'get_project': {
|
|
606
|
+
const project = await client.projects.get(args?.id);
|
|
607
|
+
return {
|
|
608
|
+
content: [
|
|
609
|
+
{
|
|
610
|
+
type: 'text',
|
|
611
|
+
text: JSON.stringify(project, null, 2),
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
case 'update_project': {
|
|
617
|
+
// `null` clears driId; the string "null" is the JSON-as-text form the
|
|
618
|
+
// model sometimes emits, same as parseDateArg handles for dates.
|
|
619
|
+
const rawDri = args?.driId;
|
|
620
|
+
const driId = rawDri === undefined ? undefined : rawDri === null || rawDri === 'null' ? null : rawDri;
|
|
621
|
+
const project = await client.projects.update({
|
|
622
|
+
id: args?.id,
|
|
623
|
+
name: args?.name,
|
|
624
|
+
description: args?.description,
|
|
625
|
+
status: args?.status,
|
|
626
|
+
priority: args?.priority,
|
|
627
|
+
driId,
|
|
628
|
+
goalIds: args?.goalIds,
|
|
629
|
+
keyResultIds: args?.keyResultIds,
|
|
630
|
+
startDate: parseDateArg(args?.startDate),
|
|
631
|
+
endDate: parseDateArg(args?.endDate),
|
|
632
|
+
reviewDate: parseDateArg(args?.reviewDate),
|
|
633
|
+
nextActionDate: parseDateArg(args?.nextActionDate),
|
|
634
|
+
});
|
|
635
|
+
return {
|
|
636
|
+
content: [
|
|
637
|
+
{
|
|
638
|
+
type: 'text',
|
|
639
|
+
text: `Updated project: ${project.name} (ID: ${project.id})`,
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
};
|
|
643
|
+
}
|
|
383
644
|
case 'get_actions': {
|
|
384
645
|
const status = args?.status;
|
|
385
646
|
const projectId = args?.projectId;
|
|
@@ -546,8 +807,90 @@ async function main() {
|
|
|
546
807
|
],
|
|
547
808
|
};
|
|
548
809
|
}
|
|
810
|
+
case 'get_meetings': {
|
|
811
|
+
const meetings = await client.meetings.list({
|
|
812
|
+
workspaceId: args?.workspaceId,
|
|
813
|
+
meetingType: args?.meetingType,
|
|
814
|
+
includeArchived: args?.includeArchived,
|
|
815
|
+
});
|
|
816
|
+
const limit = Math.max(1, Number(args?.limit) || DEFAULT_MEETING_LIMIT);
|
|
817
|
+
const when = (m) => new Date(m.meetingDate ?? m.createdAt).getTime();
|
|
818
|
+
const rows = [...meetings]
|
|
819
|
+
.sort((a, b) => when(b) - when(a))
|
|
820
|
+
.slice(0, limit)
|
|
821
|
+
.map(meetingListRow);
|
|
822
|
+
return {
|
|
823
|
+
content: [
|
|
824
|
+
{
|
|
825
|
+
type: 'text',
|
|
826
|
+
text: JSON.stringify({ total: meetings.length, returned: rows.length, meetings: rows }, null, 2),
|
|
827
|
+
},
|
|
828
|
+
],
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
case 'get_meeting': {
|
|
832
|
+
const meeting = await client.meetings.get(args?.id);
|
|
833
|
+
return {
|
|
834
|
+
content: [
|
|
835
|
+
{
|
|
836
|
+
type: 'text',
|
|
837
|
+
text: JSON.stringify(meetingDetail(meeting, Boolean(args?.includeTranscript)), null, 2),
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
case 'create_meeting': {
|
|
843
|
+
const meetingDate = parseDateArg(args?.meetingDate);
|
|
844
|
+
const meeting = await client.meetings.create({
|
|
845
|
+
title: args?.title,
|
|
846
|
+
transcription: args?.transcription,
|
|
847
|
+
notes: args?.notes,
|
|
848
|
+
description: args?.description,
|
|
849
|
+
meetingDate: meetingDate ?? undefined,
|
|
850
|
+
projectId: args?.projectId,
|
|
851
|
+
workspaceId: args?.workspaceId,
|
|
852
|
+
participants: args?.participants,
|
|
853
|
+
});
|
|
854
|
+
return {
|
|
855
|
+
content: [
|
|
856
|
+
{
|
|
857
|
+
type: 'text',
|
|
858
|
+
text: `Created meeting: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
case 'update_meeting': {
|
|
864
|
+
const meeting = await client.meetings.update({
|
|
865
|
+
id: args?.id,
|
|
866
|
+
title: args?.title,
|
|
867
|
+
description: args?.description,
|
|
868
|
+
summary: args?.summary,
|
|
869
|
+
notes: args?.notes,
|
|
870
|
+
meetingDate: parseDateArg(args?.meetingDate),
|
|
871
|
+
});
|
|
872
|
+
return {
|
|
873
|
+
content: [
|
|
874
|
+
{
|
|
875
|
+
type: 'text',
|
|
876
|
+
text: `Updated meeting: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
case 'append_meeting_notes': {
|
|
882
|
+
const meeting = await client.meetings.appendNotes(args?.id, args?.text);
|
|
883
|
+
return {
|
|
884
|
+
content: [
|
|
885
|
+
{
|
|
886
|
+
type: 'text',
|
|
887
|
+
text: `Appended to notes of: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
};
|
|
891
|
+
}
|
|
549
892
|
case 'search': {
|
|
550
|
-
const results = await
|
|
893
|
+
const results = await client.search.global({
|
|
551
894
|
query: args?.query,
|
|
552
895
|
workspaceId: args?.workspaceId,
|
|
553
896
|
limit: args?.limit,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exponential-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Exponential - connect Claude to your projects, actions, and
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "MCP server for Exponential - connect Claude to your projects, actions, goals, CRM, tickets and more",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"exponential-mcp": "./dist/cli.js"
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
28
28
|
"commander": "^12.0.0",
|
|
29
|
-
"exponential-sdk": "^1.
|
|
30
|
-
"zod": "^3.22.0"
|
|
29
|
+
"exponential-sdk": "^1.20.1",
|
|
30
|
+
"zod": "^3.22.0",
|
|
31
|
+
"zod-to-json-schema": "^3.25.2"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/node": "^20.0.0",
|