@imboard.ai/mcp-server 0.1.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.
@@ -0,0 +1,47 @@
1
+ import { resourceIdParam, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
2
+
3
+ export const registerNotificationTools: RegisterToolsFn = (server, client) => {
4
+ server.tool(
5
+ 'list_notifications',
6
+ 'Lists notifications for the authenticated user.',
7
+ {},
8
+ async () => {
9
+ try {
10
+ const response = await client.get('/api/notification/');
11
+ return formatResult({ data: response.data });
12
+ } catch (error) {
13
+ return handleToolError(error);
14
+ }
15
+ },
16
+ );
17
+
18
+ server.tool(
19
+ 'mark_all_notifications_read',
20
+ 'Marks all notifications as read.',
21
+ {},
22
+ async () => {
23
+ try {
24
+ const response = await client.put('/api/notification/read/all');
25
+ return formatResult({ data: response.data });
26
+ } catch (error) {
27
+ return handleToolError(error);
28
+ }
29
+ },
30
+ );
31
+
32
+ server.tool(
33
+ 'mark_notification_read',
34
+ 'Marks a single notification as read.',
35
+ {
36
+ notificationId: resourceIdParam.describe('The notification ID'),
37
+ },
38
+ async ({ notificationId }) => {
39
+ try {
40
+ const response = await client.put(`/api/notification/read/${notificationId}`);
41
+ return formatResult({ data: response.data });
42
+ } catch (error) {
43
+ return handleToolError(error);
44
+ }
45
+ },
46
+ );
47
+ };
@@ -0,0 +1,414 @@
1
+ import { z } from 'zod';
2
+ import { boardIdParam, resourceIdParam, paginationParams, buildQueryParams, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
3
+
4
+ export const registerReportTools: RegisterToolsFn = (server, client) => {
5
+ server.tool(
6
+ 'list_board_reports',
7
+ 'Lists reports for a board. Supports filtering by status.',
8
+ {
9
+ boardId: boardIdParam,
10
+ ...paginationParams,
11
+ status: z.enum(['draft', 'publishedForReview', 'finalized', 'archived', 'ongoing']).optional()
12
+ .describe('Filter by report status'),
13
+ },
14
+ async ({ boardId, ...rest }) => {
15
+ try {
16
+ const params = buildQueryParams(rest, ['status']);
17
+ const result = await client.getCollection(
18
+ `/api/boards/${boardId}/reports`,
19
+ params,
20
+ );
21
+ return formatResult({ data: result.data, meta: result.meta });
22
+ } catch (error) {
23
+ return handleToolError(error);
24
+ }
25
+ },
26
+ );
27
+
28
+ server.tool(
29
+ 'get_report',
30
+ 'Returns details for a specific board report including title, status, and publication date.',
31
+ {
32
+ boardId: boardIdParam,
33
+ reportId: resourceIdParam.describe('The report ID'),
34
+ },
35
+ async ({ boardId, reportId }) => {
36
+ try {
37
+ const result = await client.get(
38
+ `/api/boards/${boardId}/reports/${reportId}`,
39
+ );
40
+ return formatResult({ data: result.data });
41
+ } catch (error) {
42
+ return handleToolError(error);
43
+ }
44
+ },
45
+ );
46
+
47
+ server.tool(
48
+ 'create_report',
49
+ 'Creates a new report for a board.',
50
+ {
51
+ boardId: boardIdParam,
52
+ name: z.string().describe('Report name'),
53
+ description: z.string().optional().describe('Report description'),
54
+ meetingIds: z.array(z.string()).optional().describe('Meeting IDs to associate with'),
55
+ periodStart: z.string().optional().describe('Period start date (YYYY-MM-DD)'),
56
+ periodEnd: z.string().optional().describe('Period end date (YYYY-MM-DD)'),
57
+ },
58
+ async ({ boardId, name, description, meetingIds, periodStart, periodEnd }) => {
59
+ try {
60
+ const body: Record<string, unknown> = { name };
61
+ if (description !== undefined) body.description = description;
62
+ if (meetingIds !== undefined) body.meetingIds = meetingIds;
63
+ if (periodStart !== undefined) body.periodStart = periodStart;
64
+ if (periodEnd !== undefined) body.periodEnd = periodEnd;
65
+ const result = await client.post(`/api/reports/${boardId}`, body);
66
+ return formatResult({ data: result.data });
67
+ } catch (error) {
68
+ return handleToolError(error);
69
+ }
70
+ },
71
+ );
72
+
73
+ server.tool(
74
+ 'update_report',
75
+ 'Updates report details (name, description, status, period dates).',
76
+ {
77
+ boardId: boardIdParam,
78
+ reportId: resourceIdParam.describe('The report ID'),
79
+ name: z.string().optional().describe('Report name'),
80
+ description: z.string().optional().describe('Report description'),
81
+ status: z.string().optional().describe('Report status'),
82
+ meetingIds: z.array(z.string()).optional().describe('Meeting IDs to associate with'),
83
+ periodStart: z.string().optional().describe('Period start date (YYYY-MM-DD)'),
84
+ periodEnd: z.string().optional().describe('Period end date (YYYY-MM-DD)'),
85
+ },
86
+ async ({ boardId, reportId, name, description, status, meetingIds, periodStart, periodEnd }) => {
87
+ try {
88
+ const body: Record<string, unknown> = {};
89
+ if (name !== undefined) body.name = name;
90
+ if (description !== undefined) body.description = description;
91
+ if (status !== undefined) body.status = status;
92
+ if (meetingIds !== undefined) body.meetingIds = meetingIds;
93
+ if (periodStart !== undefined) body.periodStart = periodStart;
94
+ if (periodEnd !== undefined) body.periodEnd = periodEnd;
95
+ const result = await client.put(`/api/reports/${boardId}/${reportId}`, body);
96
+ return formatResult({ data: result.data });
97
+ } catch (error) {
98
+ return handleToolError(error);
99
+ }
100
+ },
101
+ );
102
+
103
+ server.tool(
104
+ 'promote_report_status',
105
+ 'Promotes report status (draft -> review -> published).',
106
+ {
107
+ boardId: boardIdParam,
108
+ reportId: resourceIdParam.describe('The report ID'),
109
+ status: z.string().describe('Target status (e.g. publishedForReview, finalized)'),
110
+ },
111
+ async ({ boardId, reportId, status }) => {
112
+ try {
113
+ const result = await client.put(`/api/reports/${boardId}/${reportId}/promote`, { status });
114
+ return formatResult({ data: result.data });
115
+ } catch (error) {
116
+ return handleToolError(error);
117
+ }
118
+ },
119
+ );
120
+
121
+ server.tool(
122
+ 'create_followup_report',
123
+ 'Creates a follow-up report from an existing report, copying dashboards and settings.',
124
+ {
125
+ boardId: boardIdParam,
126
+ reportId: resourceIdParam.describe('The source report ID'),
127
+ name: z.string().describe('Name for the follow-up report'),
128
+ description: z.string().optional().describe('Report description'),
129
+ meetingIds: z.array(z.string()).optional().describe('Meeting IDs to associate with'),
130
+ periodStart: z.string().optional().describe('Period start date (YYYY-MM-DD)'),
131
+ periodEnd: z.string().optional().describe('Period end date (YYYY-MM-DD)'),
132
+ },
133
+ async ({ boardId, reportId, name, description, meetingIds, periodStart, periodEnd }) => {
134
+ try {
135
+ const body: Record<string, unknown> = { name };
136
+ if (description !== undefined) body.description = description;
137
+ if (meetingIds !== undefined) body.meetingIds = meetingIds;
138
+ if (periodStart !== undefined) body.periodStart = periodStart;
139
+ if (periodEnd !== undefined) body.periodEnd = periodEnd;
140
+ const result = await client.post(`/api/reports/${boardId}/${reportId}/followup`, body);
141
+ return formatResult({ data: result.data });
142
+ } catch (error) {
143
+ return handleToolError(error);
144
+ }
145
+ },
146
+ );
147
+
148
+ server.tool(
149
+ 'create_dashboard',
150
+ 'Creates a custom dashboard in a report.',
151
+ {
152
+ boardId: boardIdParam,
153
+ reportId: resourceIdParam.describe('The report ID'),
154
+ customName: z.string().describe('Dashboard name (1-100 characters)'),
155
+ displayOrder: z.number().optional().describe('Display order position'),
156
+ },
157
+ async ({ boardId, reportId, customName, displayOrder }) => {
158
+ try {
159
+ const body: Record<string, unknown> = { customName };
160
+ if (displayOrder !== undefined) body.displayOrder = displayOrder;
161
+ const result = await client.post(`/api/reports/${boardId}/${reportId}/dashboards`, body);
162
+ return formatResult({ data: result.data });
163
+ } catch (error) {
164
+ return handleToolError(error);
165
+ }
166
+ },
167
+ );
168
+
169
+ server.tool(
170
+ 'create_dashboard_version',
171
+ 'Creates a new version of a dashboard with updated content.',
172
+ {
173
+ boardId: boardIdParam,
174
+ reportId: resourceIdParam.describe('The report ID'),
175
+ dashboardId: resourceIdParam.describe('The dashboard ID'),
176
+ content: z.record(z.string(), z.unknown()).describe('Dashboard content object (must include _type field)'),
177
+ notes: z.array(z.string()).optional().describe('Version notes'),
178
+ },
179
+ async ({ boardId, reportId, dashboardId, content, notes }) => {
180
+ try {
181
+ const body: Record<string, unknown> = { content };
182
+ if (notes !== undefined) body.notes = notes;
183
+ const result = await client.post(
184
+ `/api/reports/${boardId}/${reportId}/dashboards/${dashboardId}/versions`,
185
+ body,
186
+ );
187
+ return formatResult({ data: result.data });
188
+ } catch (error) {
189
+ return handleToolError(error);
190
+ }
191
+ },
192
+ );
193
+
194
+ server.tool(
195
+ 'update_dashboard_review_status',
196
+ 'Updates the review status of a dashboard.',
197
+ {
198
+ boardId: boardIdParam,
199
+ reportId: resourceIdParam.describe('The report ID'),
200
+ dashboardId: resourceIdParam.describe('The dashboard ID'),
201
+ newStatus: z.string().describe('New review status'),
202
+ },
203
+ async ({ boardId, reportId, dashboardId, newStatus }) => {
204
+ try {
205
+ const result = await client.put(
206
+ `/api/reports/${boardId}/${reportId}/dashboards/${dashboardId}/review-status`,
207
+ { newStatus },
208
+ );
209
+ return formatResult({ data: result.data });
210
+ } catch (error) {
211
+ return handleToolError(error);
212
+ }
213
+ },
214
+ );
215
+
216
+ server.tool(
217
+ 'get_dashboard_chat',
218
+ 'Gets discussion comments on a dashboard.',
219
+ {
220
+ boardId: boardIdParam,
221
+ reportId: resourceIdParam.describe('The report ID'),
222
+ dashboardId: resourceIdParam.describe('The dashboard ID'),
223
+ chatType: z.enum(['internal', 'boardFeedback']).optional().describe('Chat type filter'),
224
+ },
225
+ async ({ boardId, reportId, dashboardId, chatType }) => {
226
+ try {
227
+ const params: Record<string, string> = {};
228
+ if (chatType !== undefined) params.chatType = chatType;
229
+ const result = await client.get(
230
+ `/api/reports/${boardId}/${reportId}/dashboards/${dashboardId}/chat`,
231
+ params,
232
+ );
233
+ return formatResult({ data: result.data });
234
+ } catch (error) {
235
+ return handleToolError(error);
236
+ }
237
+ },
238
+ );
239
+
240
+ server.tool(
241
+ 'post_dashboard_chat',
242
+ 'Posts a comment on a dashboard.',
243
+ {
244
+ boardId: boardIdParam,
245
+ reportId: resourceIdParam.describe('The report ID'),
246
+ dashboardId: resourceIdParam.describe('The dashboard ID'),
247
+ message: z.string().describe('Comment text (1-5000 characters)'),
248
+ chatType: z.enum(['internal', 'boardFeedback']).describe('Chat type: internal or boardFeedback'),
249
+ recipientId: z.string().optional().describe('Recipient user ID (for directed feedback)'),
250
+ },
251
+ async ({ boardId, reportId, dashboardId, message, chatType, recipientId }) => {
252
+ try {
253
+ const body: Record<string, unknown> = { message, chatType };
254
+ if (recipientId !== undefined) body.recipientId = recipientId;
255
+ const result = await client.post(
256
+ `/api/reports/${boardId}/${reportId}/dashboards/${dashboardId}/chat`,
257
+ body,
258
+ );
259
+ return formatResult({ data: result.data });
260
+ } catch (error) {
261
+ return handleToolError(error);
262
+ }
263
+ },
264
+ );
265
+
266
+ server.tool(
267
+ 'get_board_feedback',
268
+ 'Gets board-level feedback on a report.',
269
+ {
270
+ boardId: boardIdParam,
271
+ reportId: resourceIdParam.describe('The report ID'),
272
+ recipientId: z.string().optional().describe('Filter by recipient user ID'),
273
+ },
274
+ async ({ boardId, reportId, recipientId }) => {
275
+ try {
276
+ const params: Record<string, string> = {};
277
+ if (recipientId !== undefined) params.recipientId = recipientId;
278
+ const result = await client.get(
279
+ `/api/reports/${boardId}/${reportId}/board-feedback`,
280
+ params,
281
+ );
282
+ return formatResult({ data: result.data });
283
+ } catch (error) {
284
+ return handleToolError(error);
285
+ }
286
+ },
287
+ );
288
+
289
+ server.tool(
290
+ 'post_board_feedback',
291
+ 'Posts board-level feedback on a report.',
292
+ {
293
+ boardId: boardIdParam,
294
+ reportId: resourceIdParam.describe('The report ID'),
295
+ message: z.string().describe('Feedback text (1-5000 characters)'),
296
+ recipientId: resourceIdParam.describe('Recipient user ID'),
297
+ },
298
+ async ({ boardId, reportId, message, recipientId }) => {
299
+ try {
300
+ const result = await client.post(
301
+ `/api/reports/${boardId}/${reportId}/board-feedback`,
302
+ { message, recipientId },
303
+ );
304
+ return formatResult({ data: result.data });
305
+ } catch (error) {
306
+ return handleToolError(error);
307
+ }
308
+ },
309
+ );
310
+
311
+ server.tool(
312
+ 'trigger_report_audit',
313
+ 'Triggers an AI audit of a report.',
314
+ {
315
+ boardId: boardIdParam,
316
+ reportId: resourceIdParam.describe('The report ID'),
317
+ includeAiAnalysis: z.boolean().optional().describe('Include AI analysis (default: true)'),
318
+ },
319
+ async ({ boardId, reportId, includeAiAnalysis }) => {
320
+ try {
321
+ const body: Record<string, unknown> = {};
322
+ if (includeAiAnalysis !== undefined) body.includeAiAnalysis = includeAiAnalysis;
323
+ const result = await client.post(`/api/reports/${boardId}/${reportId}/audit`, body);
324
+ return formatResult({ data: result.data });
325
+ } catch (error) {
326
+ return handleToolError(error);
327
+ }
328
+ },
329
+ );
330
+
331
+ server.tool(
332
+ 'list_report_audits',
333
+ 'Lists audit history for a report.',
334
+ {
335
+ boardId: boardIdParam,
336
+ reportId: resourceIdParam.describe('The report ID'),
337
+ },
338
+ async ({ boardId, reportId }) => {
339
+ try {
340
+ const result = await client.get(`/api/reports/${boardId}/${reportId}/audits`);
341
+ return formatResult({ data: result.data });
342
+ } catch (error) {
343
+ return handleToolError(error);
344
+ }
345
+ },
346
+ );
347
+
348
+ server.tool(
349
+ 'get_report_audit',
350
+ 'Gets details of a specific report audit.',
351
+ {
352
+ boardId: boardIdParam,
353
+ reportId: resourceIdParam.describe('The report ID'),
354
+ auditId: resourceIdParam.describe('The audit ID'),
355
+ },
356
+ async ({ boardId, reportId, auditId }) => {
357
+ try {
358
+ const result = await client.get(`/api/reports/${boardId}/${reportId}/audits/${auditId}`);
359
+ return formatResult({ data: result.data });
360
+ } catch (error) {
361
+ return handleToolError(error);
362
+ }
363
+ },
364
+ );
365
+
366
+ server.tool(
367
+ 'get_historical_metrics',
368
+ 'Gets historical KPI data for a board. Supports filtering by period type, date range, and number of periods.',
369
+ {
370
+ boardId: boardIdParam,
371
+ periodType: z.enum(['monthly', 'quarterly', 'yearly']).optional().describe('Period type (default: monthly)'),
372
+ periods: z.number().int().min(1).max(48).optional().describe('Number of periods (1-48)'),
373
+ startDate: z.string().optional().describe('Start date (ISO 8601)'),
374
+ endDate: z.string().optional().describe('End date (ISO 8601)'),
375
+ includeDerived: z.boolean().optional().describe('Include derived metrics'),
376
+ },
377
+ async ({ boardId, periodType, periods, startDate, endDate, includeDerived }) => {
378
+ try {
379
+ const params: Record<string, string> = {};
380
+ if (periodType !== undefined) params.periodType = periodType;
381
+ if (periods !== undefined) params.periods = String(periods);
382
+ if (startDate !== undefined) params.startDate = startDate;
383
+ if (endDate !== undefined) params.endDate = endDate;
384
+ if (includeDerived !== undefined) params.includeDerived = String(includeDerived);
385
+ const result = await client.get(`/api/boards/${boardId}/historical-metrics`, params);
386
+ return formatResult({ data: result.data });
387
+ } catch (error) {
388
+ return handleToolError(error);
389
+ }
390
+ },
391
+ );
392
+
393
+ server.tool(
394
+ 'get_metrics_comparison',
395
+ 'Compares metrics across time periods for a board.',
396
+ {
397
+ boardId: boardIdParam,
398
+ metric: z.string().describe('Metric key to compare'),
399
+ basePeriod: z.string().describe('Base period (e.g. 2025-01)'),
400
+ comparisonPeriod: z.string().describe('Comparison period (e.g. 2025-02)'),
401
+ periodType: z.enum(['monthly', 'quarterly', 'yearly']).optional().describe('Period type'),
402
+ },
403
+ async ({ boardId, metric, basePeriod, comparisonPeriod, periodType }) => {
404
+ try {
405
+ const params: Record<string, string> = { metric, basePeriod, comparisonPeriod };
406
+ if (periodType !== undefined) params.periodType = periodType;
407
+ const result = await client.get(`/api/boards/${boardId}/metrics-comparison`, params);
408
+ return formatResult({ data: result.data });
409
+ } catch (error) {
410
+ return handleToolError(error);
411
+ }
412
+ },
413
+ );
414
+ };
@@ -0,0 +1,82 @@
1
+ import { z } from 'zod';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { ImboardApiClient } from '../api-client/imboardApiClient.js';
4
+ import { ImboardApiError, ImboardApiTimeoutError, ImboardApiNetworkError } from '../api-client/errors.js';
5
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
+ import { logger } from '../utils/logging.js';
7
+
8
+ // --- Shared params & types ---
9
+
10
+ /** Reusable Zod schema for MongoDB ObjectId path parameters — prevents path traversal. */
11
+ const objectIdSchema = z.string().regex(/^[a-f0-9]{24}$/, 'Must be a 24-character hex ID');
12
+
13
+ export const boardIdParam = objectIdSchema.describe('The board ID');
14
+ export const resourceIdParam = objectIdSchema;
15
+
16
+ export const paginationParams = {
17
+ limit: z.number().int().min(1).max(100).optional().describe('Number of results per page (1-100, default 25)'),
18
+ cursor: z.string().optional().describe('Pagination cursor from a previous response'),
19
+ sort: z.enum(['updatedAt']).optional().describe('Sort field (default: updatedAt)'),
20
+ order: z.enum(['asc', 'desc']).optional().describe('Sort order (default: desc)'),
21
+ };
22
+
23
+ export type RegisterToolsFn = (server: McpServer, client: ImboardApiClient) => void;
24
+
25
+ export function buildQueryParams(
26
+ args: Record<string, unknown>,
27
+ filterKeys: string[],
28
+ ): Record<string, string> {
29
+ const params: Record<string, string> = {};
30
+
31
+ for (const key of ['limit', 'cursor', 'sort', 'order', ...filterKeys]) {
32
+ const value = args[key];
33
+ if (value !== undefined && value !== null) {
34
+ params[key] = String(value);
35
+ }
36
+ }
37
+
38
+ return params;
39
+ }
40
+
41
+ export function formatResult(data: unknown): CallToolResult {
42
+ return {
43
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
44
+ };
45
+ }
46
+
47
+ // --- Unified error handling ---
48
+
49
+ function errorResult(code: string, message: string, extra?: Record<string, unknown>): CallToolResult {
50
+ const payload: Record<string, unknown> = { error: code, message, ...extra };
51
+ return {
52
+ isError: true,
53
+ content: [{ type: 'text', text: JSON.stringify(payload) }],
54
+ };
55
+ }
56
+
57
+ export function handleToolError(error: unknown): CallToolResult {
58
+ if (error instanceof ImboardApiError) {
59
+ logger.warn(`API error: ${error.code} ${error.status}`, { requestId: error.requestId });
60
+ const message = error.status >= 500 ? 'Internal server error' : error.message;
61
+ const extra: Record<string, unknown> = { status: error.status };
62
+ if (error.requestId) extra.requestId = error.requestId;
63
+ if (error.details !== undefined && error.details !== null) extra.details = error.details;
64
+ return errorResult(error.code, message, extra);
65
+ }
66
+
67
+ if (error instanceof ImboardApiTimeoutError) {
68
+ logger.warn(`API timeout: ${error.message}`);
69
+ return errorResult('TIMEOUT', error.message);
70
+ }
71
+
72
+ if (error instanceof ImboardApiNetworkError) {
73
+ logger.warn(`Network error: ${error.message}`);
74
+ return errorResult('NETWORK_ERROR', error.message);
75
+ }
76
+
77
+ logger.error('Unexpected tool error', {
78
+ error: error instanceof Error ? error.message : String(error),
79
+ stack: error instanceof Error ? error.stack : undefined,
80
+ });
81
+ return errorResult('INTERNAL_ERROR', 'An unexpected error occurred');
82
+ }
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+ import { boardIdParam, resourceIdParam, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
3
+
4
+ export const registerSlotTools: RegisterToolsFn = (server, client) => {
5
+ server.tool(
6
+ 'list_meeting_slots',
7
+ 'Lists proposed time slots with vote tallies for a meeting. Each slot has feedbacks showing how each member voted.',
8
+ {
9
+ boardId: boardIdParam,
10
+ meetingId: resourceIdParam.describe('The meeting ID'),
11
+ },
12
+ async ({ boardId, meetingId }) => {
13
+ try {
14
+ const result = await client.getCollection(
15
+ `/api/boards/${boardId}/meetings/${meetingId}/slots`,
16
+ );
17
+ return formatResult({ data: result.data, meta: result.meta });
18
+ } catch (error) {
19
+ return handleToolError(error);
20
+ }
21
+ },
22
+ );
23
+
24
+ server.tool(
25
+ 'create_meeting_slot',
26
+ 'Proposes a new time slot for a meeting. The creator is automatically marked as "ok". All other board members start as "didNotVote".',
27
+ {
28
+ boardId: boardIdParam,
29
+ meetingId: resourceIdParam.describe('The meeting ID'),
30
+ startTime: z.string().describe('Proposed start time as ISO-8601 UTC string (e.g. "2025-07-15T14:00:00.000Z")'),
31
+ timezone: z.string().describe('IANA timezone for display (e.g. "America/New_York")'),
32
+ durationInMinutes: z.number().int().min(15).max(180).optional().describe('Duration in minutes (15-180, default 120)'),
33
+ },
34
+ async ({ boardId, meetingId, startTime, timezone, durationInMinutes }) => {
35
+ try {
36
+ const body: Record<string, unknown> = { startTime, timezone };
37
+ if (durationInMinutes !== undefined) body.durationInMinutes = durationInMinutes;
38
+
39
+ const result = await client.post(
40
+ `/api/boards/${boardId}/meetings/${meetingId}/slots`,
41
+ body,
42
+ );
43
+ return formatResult({ data: result.data });
44
+ } catch (error) {
45
+ return handleToolError(error);
46
+ }
47
+ },
48
+ );
49
+
50
+ server.tool(
51
+ 'vote_on_slot',
52
+ 'Casts or updates the authenticated user\'s vote on a proposed meeting time slot.',
53
+ {
54
+ boardId: boardIdParam,
55
+ meetingId: resourceIdParam.describe('The meeting ID'),
56
+ slotId: resourceIdParam.describe('The proposed slot ID'),
57
+ vote: z.enum(['ok', 'challenging', 'cant']).describe('Vote: "ok" (can attend), "challenging" (maybe), "cant" (cannot attend)'),
58
+ comment: z.string().max(500).optional().describe('Optional comment explaining the vote'),
59
+ },
60
+ async ({ boardId, meetingId, slotId, vote, comment }) => {
61
+ try {
62
+ const body: Record<string, unknown> = { vote };
63
+ if (comment !== undefined) body.comment = comment;
64
+
65
+ const result = await client.put(
66
+ `/api/boards/${boardId}/meetings/${meetingId}/slots/${slotId}/vote`,
67
+ body,
68
+ );
69
+ return formatResult({ data: result.data });
70
+ } catch (error) {
71
+ return handleToolError(error);
72
+ }
73
+ },
74
+ );
75
+
76
+ server.tool(
77
+ 'delete_meeting_slot',
78
+ 'Removes a proposed time slot from a meeting.',
79
+ {
80
+ boardId: boardIdParam,
81
+ meetingId: resourceIdParam.describe('The meeting ID'),
82
+ slotId: resourceIdParam.describe('The proposed slot ID'),
83
+ },
84
+ async ({ boardId, meetingId, slotId }) => {
85
+ try {
86
+ const result = await client.delete(
87
+ `/api/boards/${boardId}/meetings/${meetingId}/slots/${slotId}`,
88
+ );
89
+ return formatResult({ data: result.data });
90
+ } catch (error) {
91
+ return handleToolError(error);
92
+ }
93
+ },
94
+ );
95
+
96
+ server.tool(
97
+ 'confirm_meeting_slot',
98
+ 'Confirms a proposed slot as the meeting\'s scheduled time. Copies the slot\'s start time, duration, and timezone to the meeting.',
99
+ {
100
+ boardId: boardIdParam,
101
+ meetingId: resourceIdParam.describe('The meeting ID'),
102
+ slotId: resourceIdParam.describe('The proposed slot ID to confirm'),
103
+ },
104
+ async ({ boardId, meetingId, slotId }) => {
105
+ try {
106
+ const result = await client.post(
107
+ `/api/boards/${boardId}/meetings/${meetingId}/slots/${slotId}/confirm`,
108
+ );
109
+ return formatResult({ data: result.data });
110
+ } catch (error) {
111
+ return handleToolError(error);
112
+ }
113
+ },
114
+ );
115
+ };