Add ticketed events, Jitsi meeting integration, social features, and calendar system
- Ticketed events: full CRUD, ticket tiers (free/paid/donation), Stripe checkout, QR-based check-in scanner, public event pages, ticket confirmation emails - Event formats: IN_PERSON/ONLINE/HYBRID with auto Jitsi meeting room lifecycle, ticket-gated meeting access, moderator JWT tokens, feature-flag guarded - Social engagement: challenges with scoring/leaderboards, referral tracking, volunteer spotlight, impact stories, campaign celebrations, wall of fame - Social calendar: personal calendar layers, shared calendar items with recurrence, scheduling polls, mobile day view - MCP server: events tool pack with full admin CRUD + meeting token generation - Unified calendar: eventFormat-aware tags, online event indicators - Updated docs site, pangolin configs, and various admin UI improvements Bunker Admin
This commit is contained in:
@@ -17,6 +17,7 @@ import { smsPack } from './tools/packs/sms.js';
|
||||
import { paymentsPack } from './tools/packs/payments.js';
|
||||
import { mediaPack } from './tools/packs/media.js';
|
||||
import { adminPack } from './tools/packs/admin.js';
|
||||
import { eventsPack } from './tools/packs/events.js';
|
||||
|
||||
// Tier 3 composite workflows
|
||||
import { dailyBriefing } from './tools/composite/daily-briefing.js';
|
||||
@@ -76,8 +77,9 @@ export async function createServer(config: ServerConfig) {
|
||||
registry.registerPack(paymentsPack);
|
||||
registry.registerPack(mediaPack);
|
||||
registry.registerPack(adminPack);
|
||||
registry.registerPack(eventsPack);
|
||||
|
||||
console.error('[MCP] Server initialized with core tools + 5 on-demand packs');
|
||||
console.error('[MCP] Server initialized with core tools + 6 on-demand packs');
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import type { ApiClient } from '../../api-client.js';
|
||||
|
||||
async function handler(_args: Record<string, unknown>, api: ApiClient): Promise<string> {
|
||||
// Fire multiple requests in parallel for speed
|
||||
const [summary, connectivity, shifts, activity] = await Promise.allSettled([
|
||||
const [summary, connectivity, shifts, events, activity] = await Promise.allSettled([
|
||||
api.request('GET', '/api/dashboard/summary'),
|
||||
api.request('GET', '/api/dashboard/connectivity'),
|
||||
api.request('GET', '/api/dashboard/upcoming-shifts'),
|
||||
api.request('GET', '/api/ticketed-events/admin', { status: 'PUBLISHED', limit: 5 }),
|
||||
api.request('GET', '/api/dashboard/activity', { limit: 10 }),
|
||||
]);
|
||||
|
||||
@@ -34,6 +35,13 @@ async function handler(_args: Record<string, unknown>, api: ApiClient): Promise<
|
||||
sections.push('## Upcoming Shifts\n(Failed to fetch)');
|
||||
}
|
||||
|
||||
// Upcoming ticketed events
|
||||
if (events.status === 'fulfilled') {
|
||||
sections.push('## Upcoming Ticketed Events\n' + JSON.stringify(events.value, null, 2));
|
||||
} else {
|
||||
sections.push('## Upcoming Ticketed Events\n(Failed to fetch)');
|
||||
}
|
||||
|
||||
// Recent activity
|
||||
if (activity.status === 'fulfilled') {
|
||||
sections.push('## Recent Activity\n' + JSON.stringify(activity.value, null, 2));
|
||||
@@ -48,7 +56,7 @@ export const dailyBriefing: CompositeToolDef = {
|
||||
name: 'daily_briefing',
|
||||
description:
|
||||
'Get a comprehensive daily briefing: platform summary (campaign/user/location counts), ' +
|
||||
'service health checks, upcoming volunteer shifts, and recent activity feed. ' +
|
||||
'service health checks, upcoming volunteer shifts, upcoming ticketed events, and recent activity feed. ' +
|
||||
'Use this as the first call to understand the current state of everything.',
|
||||
inputSchema: {},
|
||||
handler,
|
||||
|
||||
295
mcp-server/src/tools/packs/events.ts
Normal file
295
mcp-server/src/tools/packs/events.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { z } from 'zod';
|
||||
import type { AnyToolDef, ToolDef, CompositeToolDef, ToolPack } from '../../tool-registry.js';
|
||||
import type { ApiClient } from '../../api-client.js';
|
||||
|
||||
// ---------- Standard CRUD Tools ----------
|
||||
|
||||
const eventTools: ToolDef[] = [
|
||||
// --- Admin Event Management ---
|
||||
{
|
||||
name: 'events_list',
|
||||
description:
|
||||
'List ticketed events. Filter by status (DRAFT, PENDING_APPROVAL, PUBLISHED, CANCELLED, COMPLETED) ' +
|
||||
'or search by title. Returns paginated results with tier counts and ticket stats.',
|
||||
inputSchema: {
|
||||
page: z.coerce.number().int().positive().default(1).describe('Page number'),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20).describe('Items per page'),
|
||||
status: z.enum(['DRAFT', 'PENDING_APPROVAL', 'PUBLISHED', 'CANCELLED', 'COMPLETED']).optional()
|
||||
.describe('Filter by event status'),
|
||||
search: z.string().optional().describe('Search events by title'),
|
||||
},
|
||||
method: 'GET',
|
||||
path: '/api/ticketed-events/admin',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_get',
|
||||
description:
|
||||
'Get full details of a ticketed event by ID, including ticket tiers, ' +
|
||||
'venue info, visibility settings, and Gancio sync status.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
},
|
||||
method: 'GET',
|
||||
path: '/api/ticketed-events/admin/:id',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_create',
|
||||
description:
|
||||
'Create a new ticketed event. Starts in DRAFT status. Include date (YYYY-MM-DD), ' +
|
||||
'startTime/endTime (HH:MM), venue info, and optionally initial ticket tiers. ' +
|
||||
'Set visibility to PUBLIC, UNLISTED, or PRIVATE.',
|
||||
inputSchema: {
|
||||
title: z.string().min(1).max(200).describe('Event title'),
|
||||
description: z.string().max(2000).optional().describe('Event description'),
|
||||
date: z.string().describe('Event date (YYYY-MM-DD)'),
|
||||
startTime: z.string().describe('Start time (HH:MM)'),
|
||||
endTime: z.string().describe('End time (HH:MM)'),
|
||||
doorsOpenTime: z.string().optional().describe('Doors open time (HH:MM)'),
|
||||
eventFormat: z.enum(['IN_PERSON', 'ONLINE', 'HYBRID']).optional().describe('Event format (default: IN_PERSON). ONLINE/HYBRID auto-create a Jitsi meeting room.'),
|
||||
venueName: z.string().max(200).optional().describe('Venue name'),
|
||||
venueAddress: z.string().max(500).optional().describe('Venue address'),
|
||||
visibility: z.enum(['PUBLIC', 'UNLISTED', 'PRIVATE']).optional().describe('Event visibility (default: PUBLIC)'),
|
||||
maxAttendees: z.number().int().positive().optional().describe('Maximum total attendees'),
|
||||
organizerName: z.string().max(200).optional().describe('Organizer display name'),
|
||||
organizerEmail: z.string().email().optional().describe('Organizer contact email'),
|
||||
},
|
||||
method: 'POST',
|
||||
path: '/api/ticketed-events/admin',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_update',
|
||||
description:
|
||||
'Update a ticketed event. Only provided fields are changed. ' +
|
||||
'Cannot change status directly — use events_publish, events_cancel, or events_complete.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
title: z.string().min(1).max(200).optional().describe('Event title'),
|
||||
description: z.string().max(2000).optional().describe('Event description'),
|
||||
date: z.string().optional().describe('Event date (YYYY-MM-DD)'),
|
||||
startTime: z.string().optional().describe('Start time (HH:MM)'),
|
||||
endTime: z.string().optional().describe('End time (HH:MM)'),
|
||||
doorsOpenTime: z.string().optional().describe('Doors open time (HH:MM)'),
|
||||
eventFormat: z.enum(['IN_PERSON', 'ONLINE', 'HYBRID']).optional().describe('Event format. Changing to ONLINE/HYBRID auto-creates a Jitsi room.'),
|
||||
venueName: z.string().max(200).optional().describe('Venue name'),
|
||||
venueAddress: z.string().max(500).optional().describe('Venue address'),
|
||||
visibility: z.enum(['PUBLIC', 'UNLISTED', 'PRIVATE']).optional().describe('Event visibility'),
|
||||
maxAttendees: z.number().int().positive().optional().describe('Maximum total attendees'),
|
||||
},
|
||||
method: 'PUT',
|
||||
path: '/api/ticketed-events/admin/:id',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_delete',
|
||||
description:
|
||||
'Delete a ticketed event. Only DRAFT events can be deleted. ' +
|
||||
'Published or completed events must be cancelled instead.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
},
|
||||
method: 'DELETE',
|
||||
path: '/api/ticketed-events/admin/:id',
|
||||
tier: 2,
|
||||
},
|
||||
|
||||
// --- Event Status Transitions ---
|
||||
{
|
||||
name: 'events_publish',
|
||||
description:
|
||||
'Publish a DRAFT event, making it visible to the public. ' +
|
||||
'Requires at least one active ticket tier. Also syncs to Gancio community calendar.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
},
|
||||
method: 'POST',
|
||||
path: '/api/ticketed-events/admin/:id/publish',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_cancel',
|
||||
description:
|
||||
'Cancel a published event. All VALID tickets are cancelled. ' +
|
||||
'Removed from public listings and community calendar.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
},
|
||||
method: 'POST',
|
||||
path: '/api/ticketed-events/admin/:id/cancel',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_complete',
|
||||
description:
|
||||
'Mark a published event as completed (e.g., after the event date has passed).',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
},
|
||||
method: 'POST',
|
||||
path: '/api/ticketed-events/admin/:id/complete',
|
||||
tier: 2,
|
||||
},
|
||||
|
||||
// --- Ticket Tier Management ---
|
||||
{
|
||||
name: 'events_add_tier',
|
||||
description:
|
||||
'Add a ticket tier to an event. Types: FREE (no charge), PAID (fixed price in cents CAD), ' +
|
||||
'DONATION (minimum donation amount). Set maxQuantity for limited availability.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
name: z.string().min(1).max(100).describe('Tier name (e.g., "General Admission", "VIP")'),
|
||||
tierType: z.enum(['PAID', 'FREE', 'DONATION']).describe('Ticket type'),
|
||||
priceCAD: z.number().int().min(0).default(0).describe('Price in cents CAD (e.g., 2500 = $25.00)'),
|
||||
maxQuantity: z.number().int().positive().optional().describe('Maximum tickets for this tier'),
|
||||
maxPerOrder: z.number().int().min(1).max(100).default(10).describe('Max tickets per order'),
|
||||
description: z.string().max(500).optional().describe('Tier description'),
|
||||
sortOrder: z.number().int().default(0).describe('Display order (lower = first)'),
|
||||
},
|
||||
method: 'POST',
|
||||
path: '/api/ticketed-events/admin/:id/tiers',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_update_tier',
|
||||
description: 'Update a ticket tier. Only provided fields are changed.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
tierId: z.string().describe('Tier ID'),
|
||||
name: z.string().min(1).max(100).optional().describe('Tier name'),
|
||||
tierType: z.enum(['PAID', 'FREE', 'DONATION']).optional().describe('Ticket type'),
|
||||
priceCAD: z.number().int().min(0).optional().describe('Price in cents CAD'),
|
||||
maxQuantity: z.number().int().positive().optional().describe('Maximum tickets'),
|
||||
maxPerOrder: z.number().int().min(1).max(100).optional().describe('Max per order'),
|
||||
isActive: z.boolean().optional().describe('Enable or disable this tier'),
|
||||
},
|
||||
method: 'PUT',
|
||||
path: '/api/ticketed-events/admin/:id/tiers/:tierId',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_delete_tier',
|
||||
description: 'Remove a ticket tier. Only possible if no tickets have been sold for this tier.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
tierId: z.string().describe('Tier ID'),
|
||||
},
|
||||
method: 'DELETE',
|
||||
path: '/api/ticketed-events/admin/:id/tiers/:tierId',
|
||||
tier: 2,
|
||||
},
|
||||
|
||||
// --- Ticket & Check-in Management ---
|
||||
{
|
||||
name: 'events_tickets',
|
||||
description:
|
||||
'List tickets sold for an event. Includes holder info, status (VALID, CHECKED_IN, CANCELLED, REFUNDED), ' +
|
||||
'tier assignment, and check-in timestamp.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
page: z.coerce.number().int().positive().default(1).describe('Page number'),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20).describe('Items per page'),
|
||||
},
|
||||
method: 'GET',
|
||||
path: '/api/ticketed-events/admin/:id/tickets',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_checkins',
|
||||
description:
|
||||
'Get check-in audit log for an event. Shows who was checked in, by whom, ' +
|
||||
'the method used (QR, MANUAL, CODE), and timestamps.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
page: z.coerce.number().int().positive().default(1).describe('Page number'),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20).describe('Items per page'),
|
||||
},
|
||||
method: 'GET',
|
||||
path: '/api/ticketed-events/admin/:id/checkins',
|
||||
tier: 2,
|
||||
},
|
||||
{
|
||||
name: 'events_cancel_ticket',
|
||||
description: 'Cancel a specific ticket. Decrements sold count and frees capacity.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
ticketId: z.string().describe('Ticket ID'),
|
||||
},
|
||||
method: 'POST',
|
||||
path: '/api/ticketed-events/admin/:id/tickets/:ticketId/cancel',
|
||||
tier: 2,
|
||||
},
|
||||
|
||||
// --- Meeting ---
|
||||
{
|
||||
name: 'events_meeting_token',
|
||||
description:
|
||||
'Generate a moderator JWT token for joining an event\'s Jitsi meeting room. ' +
|
||||
'Returns the token and a ready-to-use Jitsi URL. Only works for ONLINE/HYBRID events.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
},
|
||||
method: 'POST',
|
||||
path: '/api/ticketed-events/admin/:id/meeting-token',
|
||||
tier: 2,
|
||||
},
|
||||
];
|
||||
|
||||
// ---------- Composite Tool: Event Stats ----------
|
||||
|
||||
async function eventStatsHandler(args: Record<string, unknown>, api: ApiClient): Promise<string> {
|
||||
const id = args.id as string;
|
||||
if (!id) throw new Error('Event ID is required');
|
||||
|
||||
const [event, stats, availability] = await Promise.allSettled([
|
||||
api.request('GET', `/api/ticketed-events/admin/${id}`),
|
||||
api.request('GET', `/api/ticketed-events/admin/${id}/stats`),
|
||||
api.request('GET', `/api/ticketed-events/${(await api.request('GET', `/api/ticketed-events/admin/${id}`) as any).slug}/availability`),
|
||||
]);
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
if (event.status === 'fulfilled') {
|
||||
const e = event.value as any;
|
||||
sections.push(
|
||||
`## Event: ${e.title}\n` +
|
||||
`- **Status:** ${e.status}\n` +
|
||||
`- **Date:** ${e.date?.split('T')[0]} ${e.startTime}–${e.endTime}\n` +
|
||||
`- **Venue:** ${e.venueName || 'TBD'}\n` +
|
||||
`- **Visibility:** ${e.visibility}\n` +
|
||||
`- **Capacity:** ${e.currentAttendees}/${e.maxAttendees || '∞'}\n`
|
||||
);
|
||||
}
|
||||
|
||||
if (stats.status === 'fulfilled') {
|
||||
sections.push('## Ticket Stats\n' + JSON.stringify(stats.value, null, 2));
|
||||
}
|
||||
|
||||
if (availability.status === 'fulfilled') {
|
||||
sections.push('## Tier Availability\n' + JSON.stringify(availability.value, null, 2));
|
||||
}
|
||||
|
||||
return sections.join('\n\n');
|
||||
}
|
||||
|
||||
const eventStats: CompositeToolDef = {
|
||||
name: 'events_stats',
|
||||
description:
|
||||
'Get comprehensive event statistics: ticket sales, revenue, check-in counts, ' +
|
||||
'per-tier availability, and capacity info. Combines event detail, stats, and availability.',
|
||||
inputSchema: {
|
||||
id: z.string().describe('Event ID'),
|
||||
},
|
||||
handler: eventStatsHandler,
|
||||
tier: 3,
|
||||
};
|
||||
|
||||
// ---------- Pack Export ----------
|
||||
|
||||
export const eventsPack: ToolPack = {
|
||||
name: 'events',
|
||||
description: 'Ticketed events management: create events, manage ticket tiers, track sales and check-ins',
|
||||
tools: [...eventTools, eventStats],
|
||||
};
|
||||
Reference in New Issue
Block a user