exponential-mcp 0.5.0 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/dist/index.js +330 -0
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -106,6 +106,8 @@ exponential-mcp serve
106
106
  |------|-------------|
107
107
  | `get_workspaces` | List all workspaces |
108
108
  | `get_projects` | List projects (optionally by workspace) |
109
+ | `get_project` | One project in full: linked objectives & key results, DRI, team, dates |
110
+ | `update_project` | Rename, change status/priority, set dates, re-link to OKRs |
109
111
  | `get_actions` | List actions/tasks (filter by project or status; no date filtering) |
110
112
  | `get_todays_actions` | **What's on your plate now** — overdue / today / inbox, across all workspaces |
111
113
  | `get_overdue_triage` | Why the overdue pile is that size: bulk-created cohorts vs real debt |
@@ -114,7 +116,13 @@ exponential-mcp serve
114
116
  | `defer_actions` | Amnesty: clear dates, back to the project backlog untimed |
115
117
  | `reschedule_actions` | Move actions to a new do-date |
116
118
  | `complete_action` | Mark an action as done |
117
- | `get_goals` | List OKRs with progress |
119
+ | `get_goals` | List objectives, flat or as the annual → quarterly tree |
120
+ | `get_key_results` | List key results, grouped by objective or flat |
121
+ | `get_meetings` | List meetings, newest first, with a summary preview (no notes/transcript) |
122
+ | `get_meeting` | One meeting with notes; transcript on request |
123
+ | `create_meeting` | Record a meeting from a transcript or notes |
124
+ | `update_meeting` | Edit title, description, summary, date, or replace notes |
125
+ | `append_meeting_notes` | Add to a meeting's notes without overwriting them |
118
126
  | `search` | Search across everything |
119
127
 
120
128
  ### Asking about the day
package/dist/index.js CHANGED
@@ -106,6 +106,49 @@ function parseDateArg(raw) {
106
106
  }
107
107
  return parsed;
108
108
  }
109
+ const SUMMARY_PREVIEW_CHARS = 400;
110
+ const DEFAULT_MEETING_LIMIT = 30;
111
+ /**
112
+ * A meeting row as it comes off the list read is ~3k characters of metadata
113
+ * plus the full transcript, summary and notes bodies — a single hour-long
114
+ * transcript is easily 50k+ characters, and the server offers no limit or
115
+ * projection. The list tool sends a compact row with a summary preview; the
116
+ * detail tool sends everything except the transcript unless asked.
117
+ */
118
+ function meetingListRow(m) {
119
+ const summary = m.summary ?? null;
120
+ const truncated = summary !== null && summary.length > SUMMARY_PREVIEW_CHARS;
121
+ return {
122
+ id: m.id,
123
+ title: m.title,
124
+ description: m.description,
125
+ meetingDate: m.meetingDate,
126
+ createdAt: m.createdAt,
127
+ durationSeconds: m.durationSeconds ?? null,
128
+ participantCount: m.participantCount ?? null,
129
+ projectId: m.projectId,
130
+ project: m.project ?? null,
131
+ workspaceId: m.workspaceId,
132
+ archivedAt: m.archivedAt,
133
+ source: m.sourceIntegration?.provider ?? null,
134
+ participants: (m.participants ?? []).map((p) => ({ id: p.id, name: p.name, email: p.email })),
135
+ actions: m.actions ?? [],
136
+ summaryPreview: truncated ? summary.slice(0, SUMMARY_PREVIEW_CHARS) + '…' : summary,
137
+ summaryTruncated: truncated,
138
+ hasNotes: Boolean(m.notes),
139
+ hasTranscript: Boolean(m.transcription),
140
+ };
141
+ }
142
+ function meetingDetail(m, includeTranscript) {
143
+ // sentencesJson / analyticsJson are the server's internal processing
144
+ // artefacts; they are not part of the SDK type and never useful to a model.
145
+ const { transcription, sentencesJson, analyticsJson, ...rest } = m;
146
+ return {
147
+ ...rest,
148
+ hasTranscript: Boolean(transcription),
149
+ ...(includeTranscript ? { transcription } : {}),
150
+ };
151
+ }
109
152
  // Tool definitions
110
153
  const TOOLS = [
111
154
  {
@@ -121,6 +164,64 @@ const TOOLS = [
121
164
  }
122
165
  }
123
166
  },
167
+ {
168
+ name: 'get_project',
169
+ 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.',
170
+ inputSchema: {
171
+ type: 'object',
172
+ properties: {
173
+ id: {
174
+ type: 'string',
175
+ description: 'Project ID, slug, or the compound slug-id from an app URL'
176
+ }
177
+ },
178
+ required: ['id']
179
+ }
180
+ },
181
+ {
182
+ name: 'update_project',
183
+ 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.',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ id: { type: 'string', description: 'Project ID' },
188
+ name: { type: 'string', description: 'New name' },
189
+ description: {
190
+ type: 'string',
191
+ description: 'New description. Pass an empty string to blank it.'
192
+ },
193
+ status: {
194
+ type: 'string',
195
+ enum: ['ACTIVE', 'ON_HOLD', 'COMPLETED', 'CANCELLED'],
196
+ description: 'New lifecycle status'
197
+ },
198
+ priority: {
199
+ type: 'string',
200
+ enum: ['HIGH', 'MEDIUM', 'LOW', 'NONE'],
201
+ description: 'New priority'
202
+ },
203
+ driId: {
204
+ type: 'string',
205
+ description: 'User ID of the directly responsible individual, or null to clear'
206
+ },
207
+ goalIds: {
208
+ type: 'array',
209
+ items: { type: 'string' },
210
+ description: 'Objective IDs (integers, passed as strings) to link — replaces the current set'
211
+ },
212
+ keyResultIds: {
213
+ type: 'array',
214
+ items: { type: 'string' },
215
+ description: 'Key result IDs to link — replaces the current set; [] clears'
216
+ },
217
+ startDate: { type: 'string', description: 'ISO datetime, or null to clear' },
218
+ endDate: { type: 'string', description: 'ISO datetime, or null to clear' },
219
+ reviewDate: { type: 'string', description: 'ISO datetime, or null to clear' },
220
+ nextActionDate: { type: 'string', description: 'ISO datetime, or null to clear' }
221
+ },
222
+ required: ['id']
223
+ }
224
+ },
124
225
  {
125
226
  name: 'get_actions',
126
227
  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 +413,114 @@ const TOOLS = [
312
413
  }
313
414
  }
314
415
  },
416
+ {
417
+ name: 'get_meetings',
418
+ 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.',
419
+ inputSchema: {
420
+ type: 'object',
421
+ properties: {
422
+ workspaceId: {
423
+ type: 'string',
424
+ description: 'Optional workspace ID to restrict to one workspace'
425
+ },
426
+ meetingType: {
427
+ type: 'string',
428
+ enum: ['all', 'mine', 'one_on_one'],
429
+ description: 'all (default) — everything visible; mine — meetings the user owns or attended; one_on_one — exactly two participants'
430
+ },
431
+ includeArchived: {
432
+ type: 'boolean',
433
+ description: 'Include archived meetings (default false)'
434
+ },
435
+ limit: {
436
+ type: 'number',
437
+ description: 'Max meetings to return, newest first (default 30)'
438
+ }
439
+ }
440
+ }
441
+ },
442
+ {
443
+ name: 'get_meeting',
444
+ 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.',
445
+ inputSchema: {
446
+ type: 'object',
447
+ properties: {
448
+ id: { type: 'string', description: 'Meeting ID' },
449
+ includeTranscript: {
450
+ type: 'boolean',
451
+ description: 'Also return the raw transcript text (default false)'
452
+ }
453
+ },
454
+ required: ['id']
455
+ }
456
+ },
457
+ {
458
+ name: 'create_meeting',
459
+ 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.',
460
+ inputSchema: {
461
+ type: 'object',
462
+ properties: {
463
+ title: { type: 'string', description: 'Meeting title' },
464
+ transcription: {
465
+ type: 'string',
466
+ description: 'Raw transcript text (required, at least one character)'
467
+ },
468
+ notes: { type: 'string', description: 'Meeting notes (Markdown)' },
469
+ description: { type: 'string', description: 'Short description' },
470
+ meetingDate: {
471
+ type: 'string',
472
+ description: 'When the meeting happened, as an ISO datetime (defaults to now)'
473
+ },
474
+ projectId: { type: 'string', description: 'Link to this project' },
475
+ workspaceId: { type: 'string', description: 'Workspace ID (ignored when projectId is set)' },
476
+ participants: {
477
+ type: 'array',
478
+ description: 'Who attended. Each entry needs at least one of userId, contactId, or email.',
479
+ items: {
480
+ type: 'object',
481
+ properties: {
482
+ userId: { type: 'string' },
483
+ contactId: { type: 'string' },
484
+ email: { type: 'string' },
485
+ name: { type: 'string' }
486
+ }
487
+ }
488
+ }
489
+ },
490
+ required: ['title', 'transcription']
491
+ }
492
+ },
493
+ {
494
+ name: 'update_meeting',
495
+ 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.',
496
+ inputSchema: {
497
+ type: 'object',
498
+ properties: {
499
+ id: { type: 'string', description: 'Meeting ID' },
500
+ title: { type: 'string', description: 'New title' },
501
+ description: { type: 'string', description: 'New description' },
502
+ summary: { type: 'string', description: 'New summary' },
503
+ notes: { type: 'string', description: 'Replacement notes body (Markdown)' },
504
+ meetingDate: {
505
+ type: 'string',
506
+ description: 'When the meeting happened, as an ISO datetime, or null to clear'
507
+ }
508
+ },
509
+ required: ['id']
510
+ }
511
+ },
512
+ {
513
+ name: 'append_meeting_notes',
514
+ 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.',
515
+ inputSchema: {
516
+ type: 'object',
517
+ properties: {
518
+ id: { type: 'string', description: 'Meeting ID' },
519
+ text: { type: 'string', description: 'Text to append (Markdown)' }
520
+ },
521
+ required: ['id', 'text']
522
+ }
523
+ },
315
524
  {
316
525
  name: 'search',
317
526
  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.",
@@ -380,6 +589,45 @@ async function main() {
380
589
  ],
381
590
  };
382
591
  }
592
+ case 'get_project': {
593
+ const project = await client.projects.get(args?.id);
594
+ return {
595
+ content: [
596
+ {
597
+ type: 'text',
598
+ text: JSON.stringify(project, null, 2),
599
+ },
600
+ ],
601
+ };
602
+ }
603
+ case 'update_project': {
604
+ // `null` clears driId; the string "null" is the JSON-as-text form the
605
+ // model sometimes emits, same as parseDateArg handles for dates.
606
+ const rawDri = args?.driId;
607
+ const driId = rawDri === undefined ? undefined : rawDri === null || rawDri === 'null' ? null : rawDri;
608
+ const project = await client.projects.update({
609
+ id: args?.id,
610
+ name: args?.name,
611
+ description: args?.description,
612
+ status: args?.status,
613
+ priority: args?.priority,
614
+ driId,
615
+ goalIds: args?.goalIds,
616
+ keyResultIds: args?.keyResultIds,
617
+ startDate: parseDateArg(args?.startDate),
618
+ endDate: parseDateArg(args?.endDate),
619
+ reviewDate: parseDateArg(args?.reviewDate),
620
+ nextActionDate: parseDateArg(args?.nextActionDate),
621
+ });
622
+ return {
623
+ content: [
624
+ {
625
+ type: 'text',
626
+ text: `Updated project: ${project.name} (ID: ${project.id})`,
627
+ },
628
+ ],
629
+ };
630
+ }
383
631
  case 'get_actions': {
384
632
  const status = args?.status;
385
633
  const projectId = args?.projectId;
@@ -546,6 +794,88 @@ async function main() {
546
794
  ],
547
795
  };
548
796
  }
797
+ case 'get_meetings': {
798
+ const meetings = await client.meetings.list({
799
+ workspaceId: args?.workspaceId,
800
+ meetingType: args?.meetingType,
801
+ includeArchived: args?.includeArchived,
802
+ });
803
+ const limit = Math.max(1, Number(args?.limit) || DEFAULT_MEETING_LIMIT);
804
+ const when = (m) => new Date(m.meetingDate ?? m.createdAt).getTime();
805
+ const rows = [...meetings]
806
+ .sort((a, b) => when(b) - when(a))
807
+ .slice(0, limit)
808
+ .map(meetingListRow);
809
+ return {
810
+ content: [
811
+ {
812
+ type: 'text',
813
+ text: JSON.stringify({ total: meetings.length, returned: rows.length, meetings: rows }, null, 2),
814
+ },
815
+ ],
816
+ };
817
+ }
818
+ case 'get_meeting': {
819
+ const meeting = await client.meetings.get(args?.id);
820
+ return {
821
+ content: [
822
+ {
823
+ type: 'text',
824
+ text: JSON.stringify(meetingDetail(meeting, Boolean(args?.includeTranscript)), null, 2),
825
+ },
826
+ ],
827
+ };
828
+ }
829
+ case 'create_meeting': {
830
+ const meetingDate = parseDateArg(args?.meetingDate);
831
+ const meeting = await client.meetings.create({
832
+ title: args?.title,
833
+ transcription: args?.transcription,
834
+ notes: args?.notes,
835
+ description: args?.description,
836
+ meetingDate: meetingDate ?? undefined,
837
+ projectId: args?.projectId,
838
+ workspaceId: args?.workspaceId,
839
+ participants: args?.participants,
840
+ });
841
+ return {
842
+ content: [
843
+ {
844
+ type: 'text',
845
+ text: `Created meeting: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
846
+ },
847
+ ],
848
+ };
849
+ }
850
+ case 'update_meeting': {
851
+ const meeting = await client.meetings.update({
852
+ id: args?.id,
853
+ title: args?.title,
854
+ description: args?.description,
855
+ summary: args?.summary,
856
+ notes: args?.notes,
857
+ meetingDate: parseDateArg(args?.meetingDate),
858
+ });
859
+ return {
860
+ content: [
861
+ {
862
+ type: 'text',
863
+ text: `Updated meeting: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
864
+ },
865
+ ],
866
+ };
867
+ }
868
+ case 'append_meeting_notes': {
869
+ const meeting = await client.meetings.appendNotes(args?.id, args?.text);
870
+ return {
871
+ content: [
872
+ {
873
+ type: 'text',
874
+ text: `Appended to notes of: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
875
+ },
876
+ ],
877
+ };
878
+ }
549
879
  case 'search': {
550
880
  const results = await trpcClient.search.global.query({
551
881
  query: args?.query,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exponential-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for Exponential - connect Claude to your projects, actions, and goals",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.30.0",
28
28
  "commander": "^12.0.0",
29
- "exponential-sdk": "^1.12.0",
29
+ "exponential-sdk": "^1.16.0",
30
30
  "zod": "^3.22.0"
31
31
  },
32
32
  "devDependencies": {