/**
* Google Calendar Tools
*
* Two tools demonstrating Layer 2 and Layer 3 service token usage:
*
* - list_calendar_events: Uses shared service token (Layer 2)
* Admin configures a shared Google Calendar, all users access it.
*
* - list_my_calendar_events: Uses user service token (Layer 3)
* Each user connects their own Google Calendar.
*
* Reference: /home/jez/Documents/mcp/archive/google-calendar-mcp-cloudflare
*/
import { z } from 'zod';
import type { ToolDefinition, ToolResult, ToolContext } from './types';
import { getSharedServiceToken, authorizedSharedFetch } from '../lib/shared-services';
import { getUserServiceToken, authorizedUserFetch } from '../lib/user-services';
import type { Env } from '../types';
// Google Calendar API base URL
const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
// Service name for Google Calendar (must match database record)
const GOOGLE_CALENDAR_SERVICE = 'googlecalendar';
/**
* Format a date for the Calendar API.
* Accepts YYYY-MM-DD or RFC3339 format.
*/
function formatDateTime(input: string): string {
if (input.includes('T')) return input; // Already RFC 3339
return `${input}T00:00:00Z`; // YYYY-MM-DD → RFC 3339
}
/**
* Format calendar events for display
*/
function formatEventsResponse(events: CalendarEvent[]): string {
if (!events || events.length === 0) {
return 'No events found.';
}
return events
.map((event, i) => {
const start = event.start?.dateTime || event.start?.date || 'No date';
const end = event.end?.dateTime || event.end?.date || '';
const location = event.location ? `\n Location: ${event.location}` : '';
const attendees = event.attendees?.length
? `\n Attendees: ${event.attendees.map((a) => a.email).join(', ')}`
: '';
return `${i + 1}. ${event.summary || '(No title)'}\n Start: ${start}${end ? `\n End: ${end}` : ''}${location}${attendees}`;
})
.join('\n\n');
}
// Types for Calendar API responses
interface CalendarEvent {
id: string;
summary?: string;
description?: string;
location?: string;
start?: { dateTime?: string; date?: string };
end?: { dateTime?: string; date?: string };
attendees?: Array<{ email: string; responseStatus?: string }>;
htmlLink?: string;
}
interface EventListResponse {
items?: CalendarEvent[];
nextPageToken?: string;
}
// =============================================================================
// LAYER 2: SHARED SERVICE TOOL
// =============================================================================
/**
* List events from the shared Google Calendar.
* Uses the admin-configured shared service token (Layer 2).
*/
export const listCalendarEventsTool: ToolDefinition<{
calendarId: z.ZodDefault<z.ZodOptional<z.ZodString>>;
maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
timeMin: z.ZodOptional<z.ZodString>;
timeMax: z.ZodOptional<z.ZodString>;
query: z.ZodOptional<z.ZodString>;
}> = {
name: 'list_calendar_events',
description:
'List events from the shared Google Calendar. This uses the organization-wide calendar configured by the admin.',
schema: {
calendarId: z.string().optional().default('primary').describe('Calendar ID (default: primary)'),
maxResults: z.number().optional().default(10).describe('Maximum number of events to return'),
timeMin: z.string().optional().describe('Start time filter (YYYY-MM-DD or RFC3339)'),
timeMax: z.string().optional().describe('End time filter (YYYY-MM-DD or RFC3339)'),
query: z.string().optional().describe('Free text search query'),
},
metadata: {
category: 'integration',
tags: ['calendar', 'google', 'shared'],
requiresAuth: 'shared_service:google_calendar',
authScopes: ['https://www.googleapis.com/auth/calendar'],
},
handler: async (args, context): Promise<ToolResult> => {
// Check for environment bindings
if (!context?.env?.DB) {
return {
content: [{ type: 'text', text: 'Error: Database not available.' }],
isError: true,
};
}
const env = context.env as Env;
// Get shared service token
const accessToken = await getSharedServiceToken(env, GOOGLE_CALENDAR_SERVICE);
if (!accessToken) {
return {
content: [
{
type: 'text',
text: 'Google Calendar not configured. Ask an admin to set up the google_calendar shared service.',
},
],
isError: true,
};
}
// Apply defaults (Zod defaults don't apply in direct tool execution)
const calendarId = args.calendarId || 'primary';
const maxResults = args.maxResults ?? 10;
// Build API URL with query parameters
const params = new URLSearchParams({
maxResults: String(maxResults),
singleEvents: 'true',
orderBy: 'startTime',
});
if (args.timeMin) {
params.set('timeMin', formatDateTime(args.timeMin));
} else {
// Default to now
params.set('timeMin', new Date().toISOString());
}
if (args.timeMax) {
params.set('timeMax', formatDateTime(args.timeMax));
}
if (args.query) {
params.set('q', args.query);
}
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[Calendar] API error:`, errorText);
// Include the actual error details for debugging
let errorDetail = '';
try {
const errorJson = JSON.parse(errorText);
errorDetail = errorJson?.error?.message || errorText;
} catch {
errorDetail = errorText;
}
return {
content: [{ type: 'text', text: `Calendar API error: ${response.status} - ${errorDetail}` }],
isError: true,
};
}
const data = (await response.json()) as EventListResponse;
const events = data.items || [];
return {
content: [
{
type: 'text',
text: `Shared Calendar Events (${events.length} found):\n\n${formatEventsResponse(events)}`,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
content: [{ type: 'text', text: `Error fetching calendar: ${message}` }],
isError: true,
};
}
},
};
// =============================================================================
// LAYER 3: USER SERVICE TOOL
// =============================================================================
/**
* List events from the user's personal Google Calendar.
* Uses the user's own connected service token (Layer 3).
*/
export const listMyCalendarEventsTool: ToolDefinition<{
calendarId: z.ZodDefault<z.ZodOptional<z.ZodString>>;
maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
timeMin: z.ZodOptional<z.ZodString>;
timeMax: z.ZodOptional<z.ZodString>;
query: z.ZodOptional<z.ZodString>;
}> = {
name: 'list_my_calendar_events',
description:
'List events from YOUR personal Google Calendar. Requires you to connect your Google Calendar via /dashboard.',
schema: {
calendarId: z.string().optional().default('primary').describe('Calendar ID (default: primary)'),
maxResults: z.number().optional().default(10).describe('Maximum number of events to return'),
timeMin: z.string().optional().describe('Start time filter (YYYY-MM-DD or RFC3339)'),
timeMax: z.string().optional().describe('End time filter (YYYY-MM-DD or RFC3339)'),
query: z.string().optional().describe('Free text search query'),
},
metadata: {
category: 'integration',
tags: ['calendar', 'google', 'personal'],
requiresAuth: 'user_service:google_calendar',
authScopes: ['https://www.googleapis.com/auth/calendar'],
},
handler: async (args, context): Promise<ToolResult> => {
// Check for authentication
if (!context?.props?.id) {
return {
content: [{ type: 'text', text: 'Error: Authentication required.' }],
isError: true,
};
}
// Check for environment bindings
if (!context?.env?.DB) {
return {
content: [{ type: 'text', text: 'Error: Database not available.' }],
isError: true,
};
}
const env = context.env as Env;
const userId = context.props.id;
// Get user's service token
const accessToken = await getUserServiceToken(env, userId, GOOGLE_CALENDAR_SERVICE);
if (!accessToken) {
return {
content: [
{
type: 'text',
text: 'Google Calendar not connected. Visit /dashboard to connect your Google Calendar.',
},
],
isError: true,
};
}
// Apply defaults (Zod defaults don't apply in direct tool execution)
const calendarId = args.calendarId || 'primary';
const maxResults = args.maxResults ?? 10;
// Build API URL with query parameters
const params = new URLSearchParams({
maxResults: String(maxResults),
singleEvents: 'true',
orderBy: 'startTime',
});
if (args.timeMin) {
params.set('timeMin', formatDateTime(args.timeMin));
} else {
// Default to now
params.set('timeMin', new Date().toISOString());
}
if (args.timeMax) {
params.set('timeMax', formatDateTime(args.timeMax));
}
if (args.query) {
params.set('q', args.query);
}
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[Calendar] API error:`, errorText);
// Handle 401 specially - token may be revoked
if (response.status === 401) {
return {
content: [
{
type: 'text',
text: 'Calendar access expired. Please visit /dashboard to reconnect your Google Calendar.',
},
],
isError: true,
};
}
// Parse error details
let errorDetail = '';
try {
const errorJson = JSON.parse(errorText);
errorDetail = errorJson?.error?.message || errorText;
} catch {
errorDetail = errorText;
}
return {
content: [{ type: 'text', text: `Calendar API error: ${response.status} - ${errorDetail}` }],
isError: true,
};
}
const data = (await response.json()) as EventListResponse;
const events = data.items || [];
return {
content: [
{
type: 'text',
text: `Your Calendar Events (${events.length} found):\n\n${formatEventsResponse(events)}`,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
content: [{ type: 'text', text: `Error fetching calendar: ${message}` }],
isError: true,
};
}
},
};
// =============================================================================
// EXPORTS
// =============================================================================
/**
* All calendar tools
*/
export const calendarTools: ToolDefinition[] = [
listCalendarEventsTool as unknown as ToolDefinition,
listMyCalendarEventsTool as unknown as ToolDefinition,
];