import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { PCO_MODULES, CHARACTER_LIMIT } from "../constants.js";
import {
apiGet, handleApiError, buildPaginationParams,
getTotalCount, ensureArray
} from "../services/api.js";
import {
ResponseFormat, ResponseFormatSchema, PaginationSchema,
formatDate, formatDateTime, buildPaginationMeta, truncateIfNeeded
} from "../schemas/common.js";
import type { PcoCheckIn, ToolResult } from "../types.js";
const BASE = PCO_MODULES.checkins;
export function registerCheckInsTools(server: McpServer): void {
// ─── List Check-Ins ──────────────────────────────────────────────────────
server.registerTool(
"pco_list_checkins",
{
title: "List Check-Ins",
description: `List check-in records in Planning Center Check-Ins.
Args:
- where_created_after (string, optional): Filter check-ins created after this date (YYYY-MM-DD)
- where_created_before (string, optional): Filter check-ins created before this date (YYYY-MM-DD)
- limit (number): Max results (1-100, default 25)
- offset (number): Pagination offset (default 0)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns: List of check-in records with name, security code, kind, and timestamps.
Error: Returns "Error: ..." if the request fails.`,
inputSchema: z.object({
where_created_after: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
.describe("Filter check-ins created after this date (YYYY-MM-DD)"),
where_created_before: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
.describe("Filter check-ins created before this date (YYYY-MM-DD)"),
...PaginationSchema.shape,
response_format: ResponseFormatSchema,
}).strict(),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params): Promise<ToolResult> => {
try {
const queryParams: Record<string, string | number | boolean | undefined> = {
...buildPaginationParams(params.limit, params.offset),
};
if (params.where_created_after) queryParams["where[created_at][gte]"] = params.where_created_after;
if (params.where_created_before) queryParams["where[created_at][lte]"] = params.where_created_before;
const response = await apiGet<PcoCheckIn>(`${BASE}/check_ins`, queryParams);
const checkIns = ensureArray(response.data) as PcoCheckIn[];
const total = getTotalCount(response);
const meta = buildPaginationMeta(total, checkIns.length, params.offset);
if (checkIns.length === 0) {
return { content: [{ type: "text", text: "No check-ins found." }] };
}
if (params.response_format === ResponseFormat.JSON) {
const output = {
...meta,
check_ins: checkIns.map(c => ({ id: c.id, ...c.attributes })),
};
return {
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
structuredContent: output,
};
}
const lines = [`# Check-Ins (${total} total, showing ${checkIns.length})`, ""];
for (const c of checkIns) {
const a = c.attributes;
const name = [a.first_name, a.last_name].filter(Boolean).join(" ") || "(unnamed)";
lines.push(`## ${name} (ID: ${c.id})`);
if (a.kind) lines.push(`- **Kind**: ${a.kind}`);
if (a.number != null) lines.push(`- **Number**: ${a.number}`);
if (a.security_code) lines.push(`- **Security Code**: ${a.security_code}`);
if (a.medical_notes) lines.push(`- **Medical Notes**: ${a.medical_notes}`);
if (a.emergency_contact_name) lines.push(`- **Emergency Contact**: ${a.emergency_contact_name}${a.emergency_contact_phone_number ? ` (${a.emergency_contact_phone_number})` : ""}`);
if (a.created_at) lines.push(`- **Checked In**: ${formatDateTime(a.created_at)}`);
if (a.checked_out_at) lines.push(`- **Checked Out**: ${formatDateTime(a.checked_out_at)}`);
lines.push("");
}
if (meta.has_more) {
lines.push(`*More results available — use offset ${meta.next_offset}.*`);
}
const text = truncateIfNeeded(lines.join("\n"), CHARACTER_LIMIT, "Use date filters or pagination to narrow results.");
return { content: [{ type: "text", text }] };
} catch (error) {
return { content: [{ type: "text", text: handleApiError(error) }] };
}
}
);
// ─── List Check-In Events ────────────────────────────────────────────────
server.registerTool(
"pco_list_checkin_events",
{
title: "List Check-In Events",
description: `List events (attendance tracking sessions) in Planning Center Check-Ins.
Args:
- limit (number): Max results (1-100, default 25)
- offset (number): Pagination offset (default 0)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns: List of check-in events with name, frequency, and enable settings.
Error: Returns "Error: ..." if the request fails.`,
inputSchema: z.object({
...PaginationSchema.shape,
response_format: ResponseFormatSchema,
}).strict(),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params): Promise<ToolResult> => {
try {
const response = await apiGet(`${BASE}/events`, buildPaginationParams(params.limit, params.offset));
const events = ensureArray(response.data);
const total = getTotalCount(response);
const meta = buildPaginationMeta(total, events.length, params.offset);
if (events.length === 0) {
return { content: [{ type: "text", text: "No check-in events found." }] };
}
if (params.response_format === ResponseFormat.JSON) {
const output = { ...meta, events: events.map(e => ({ id: e.id, ...e.attributes })) };
return {
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
structuredContent: output,
};
}
const lines = [`# Check-In Events (${total} total, showing ${events.length})`, ""];
for (const ev of events) {
const a = ev.attributes as Record<string, unknown>;
lines.push(`## ${String(a.name ?? "(unnamed)")} (ID: ${ev.id})`);
if (a.frequency) lines.push(`- **Frequency**: ${a.frequency}`);
if (a.enable_services_integration !== undefined) {
lines.push(`- **Services Integration**: ${a.enable_services_integration ? "Enabled" : "Disabled"}`);
}
if (a.created_at) lines.push(`- **Created**: ${formatDate(String(a.created_at))}`);
if (a.updated_at) lines.push(`- **Updated**: ${formatDate(String(a.updated_at))}`);
lines.push("");
}
if (meta.has_more) {
lines.push(`*More results available — use offset ${meta.next_offset}.*`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
} catch (error) {
return { content: [{ type: "text", text: handleApiError(error) }] };
}
}
);
// ─── List Check-In Locations ─────────────────────────────────────────────
server.registerTool(
"pco_list_checkin_locations",
{
title: "List Check-In Locations",
description: `List locations (rooms) for a check-in event in Planning Center Check-Ins.
Args:
- event_id (string): The check-in event ID (get this from pco_list_checkin_events)
- limit (number): Max results (1-100, default 25)
- offset (number): Pagination offset (default 0)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns: List of locations with name, capacity, and check-in count.
Error: Returns "Error: Resource not found" if the event ID is invalid.`,
inputSchema: z.object({
event_id: z.string().min(1).describe("The check-in event ID"),
...PaginationSchema.shape,
response_format: ResponseFormatSchema,
}).strict(),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params): Promise<ToolResult> => {
try {
const response = await apiGet(
`${BASE}/events/${params.event_id}/locations`,
buildPaginationParams(params.limit, params.offset)
);
const locations = ensureArray(response.data);
const total = getTotalCount(response);
const meta = buildPaginationMeta(total, locations.length, params.offset);
if (locations.length === 0) {
return { content: [{ type: "text", text: "No locations found for this check-in event." }] };
}
if (params.response_format === ResponseFormat.JSON) {
const output = { ...meta, locations: locations.map(l => ({ id: l.id, ...l.attributes })) };
return {
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
structuredContent: output,
};
}
const lines = [`# Check-In Locations (${total} total, showing ${locations.length})`, ""];
for (const loc of locations) {
const a = loc.attributes as Record<string, unknown>;
lines.push(`- **${String(a.name ?? "(unnamed)")}** (ID: ${loc.id})${a.max_occupancy != null ? ` — max: ${a.max_occupancy}` : ""}${a.current_count != null ? `, current: ${a.current_count}` : ""}`);
}
if (meta.has_more) {
lines.push("", `*More results available — use offset ${meta.next_offset}.*`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
} catch (error) {
return { content: [{ type: "text", text: handleApiError(error) }] };
}
}
);
}