Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cal-sdk/mcp': minor
---

New package: an MCP server with tools to list calendars and events, find free time across accounts, and create, update and delete events on Google Calendar, Outlook, iCloud and CalDAV.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ const calendar = google({ accessToken: () => getFreshAccessToken() });
- **Errors you can branch on.** Conflicts, rate limits, auth failures and missing events each have their own error class.
- **Types that follow the calendar.** Provider options are checked against the calendar you pass, so a misspelled option fails to compile.

## For AI agents

- **MCP server.** `npx -y @cal-sdk/mcp` gives Claude, Cursor or any MCP client tools to read events, find free time across calendars and book meetings. Add `--demo` to try it without an account. [Setup](https://www.cal-sdk.com/docs/guides/mcp).
- **AI SDK tools.** A few lines turn calsdk functions into tools for an agent, using each user's own token. [Guide](https://www.cal-sdk.com/docs/guides/ai-sdk) and [example](examples/ai-agent).

## Good to know

- **It's a library, not a hosted service.** It runs in your app and calls each provider directly with tokens you pass in. Your users' calendar data doesn't pass through anyone else.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(home)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default async function Post(props: PageProps<'/blog/[slug]'>) {
· <time dateTime={post.date}>{formatPostDate(post.date)}</time>
</p>
{/* Long inline code, like a full property path, would push the page sideways on phones. */}
<article className="prose mt-12 max-w-none border-t border-line pt-10 [&_:not(pre)>code]:[overflow-wrap:anywhere]">
<article className="prose mt-12 max-w-none border-t border-line pt-10">
<MDX components={getMDXComponents()} />
</article>
<div className="mt-16 rounded-2xl border border-line bg-panel p-6">
Expand Down
7 changes: 6 additions & 1 deletion apps/web/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,17 @@ html > body[data-scroll-locked] {
}

/* Inline code is short; splitting `google({ accessToken })` across lines makes it unreadable.
Phones keep wrapping so a long span can't push the page sideways. */
Phones keep wrapping, anywhere if needed, so a long URL or name can't run off the screen. */
@media (min-width: 640px) {
.prose :not(pre) > code {
white-space: nowrap;
}
}
@media (max-width: 639px) {
.prose :not(pre) > code {
overflow-wrap: anywhere;
}
}

/* On phones, wide docs tables scroll inside their own box with the header row in view.
Short tables never reach the height cap. */
Expand Down
113 changes: 113 additions & 0 deletions apps/web/content/docs/guides/ai-sdk.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: AI SDK tools
description: Let an agent built with the Vercel AI SDK read a user's calendar, find free time and book meetings, with each user's own token.
---

An MCP server is the right fit when the calendar belongs to whoever runs the client. In your own product it usually belongs to the user talking to your agent, and you already hold their OAuth token. Then the simplest setup is a few AI SDK tools that call calsdk directly, built per request with that user's provider.

```npm
npm install ai @ai-sdk/anthropic zod @cal-sdk/core @cal-sdk/google
```

## Define the tools

```ts title="calendar-tools.ts"
import { type CalendarProvider, createEvent, findFreeTime, listEvents } from '@cal-sdk/core';
import { tool } from 'ai';
import { z } from 'zod';

const instant = z.string().describe('ISO 8601 date-time with an offset');
const localTime = z.string().describe('Local date-time, e.g. 2026-11-09T14:00:00');

export function calendarTools(calendar: CalendarProvider, timeZone: string) {
return {
listEvents: tool({
description:
'List events between two times, recurring events expanded. If nextPageToken is returned, call again with it as pageToken for the rest.',
inputSchema: z.object({ from: instant, to: instant, pageToken: z.string().optional() }),
execute: async ({ from, to, pageToken }) => {
const { events, nextPageToken } = await listEvents({
calendar,
timeMin: from,
timeMax: to,
pageToken,
});
return {
events: events.map(({ id, title, start, end }) => ({ id, title, start, end })),
nextPageToken,
};
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}),

findFreeTime: tool({
description: 'Find free slots of a given length during working hours.',
inputSchema: z.object({ from: instant, to: instant, minutes: z.number().int() }),
execute: async ({ from, to, minutes }) => {
const { slots } = await findFreeTime({
calendars: [{ calendar }],
timeMin: from,
timeMax: to,
duration: minutes,
workingHours: { start: '09:00', end: '18:00', timeZone },
});
return slots.slice(0, 10);
},
}),

createEvent: tool({
description: 'Book an event and invite attendees.',
inputSchema: z.object({
title: z.string(),
start: localTime,
end: localTime,
attendees: z.array(z.string()).optional(),
}),
execute: async ({ title, start, end, attendees }) => {
const { event } = await createEvent({
calendar,
event: {
title,
start: { dateTime: start, timeZone },
end: { dateTime: end, timeZone },
attendees: attendees?.map((email) => ({ email })),
},
sendUpdates: 'all',
});
return { id: event.id, start: event.start, link: event.htmlLink };
},
}),
};
}
```

Keep the outputs small. A model doesn't need etags or provider metadata, and every field you return costs tokens on each later step.

## Run the agent

```ts
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@cal-sdk/google';
import { generateText, isStepCount } from 'ai';
import { calendarTools } from './calendar-tools';

// "Tomorrow" depends on where the user is, not where the server is.
const today = new Date().toLocaleDateString('en-US', { timeZone: user.timeZone, dateStyle: 'full' });

const { text } = await generateText({
model: anthropic('claude-sonnet-5-5'),
tools: calendarTools(google({ accessToken: user.googleAccessToken }), user.timeZone),
stopWhen: isStepCount(6),
instructions: `You manage the user's calendar. Today is ${today}. All times are in ${user.timeZone}.`,
prompt: 'Find an hour tomorrow afternoon for a planning session with ana@example.com and book it.',
});
```

Swap `google(...)` for `microsoft(...)` or `icloud(...)` and nothing else changes. To check every calendar the user connected before booking, pass them all to `findFreeTime` in `calendars`.

The full example, which runs on an in-memory calendar when no token is set, is in [examples/ai-agent](https://github.com/vkpdeveloper/calsdk/tree/main/examples/ai-agent).

## Things worth deciding

- **Confirm before writing.** For anything that emails attendees, many apps let the model propose a slot and ask the user before calling `createEvent`. The AI SDK's tool approval support fits this well.
- **Time zones.** Give the model the user's zone in the instructions and have it pass local times for events, as above. calsdk turns them into the right offsets, including across daylight saving changes.
- **Read-only first.** If your agent only needs to answer "am I free on Thursday?", give it `listEvents` and `findFreeTime` and request a read-only scope.
111 changes: 111 additions & 0 deletions apps/web/content/docs/guides/mcp.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: MCP server
description: Give Claude, Cursor or any MCP client your Google, Outlook or iCloud calendar, with tools to read events, find free time and book meetings.
---

`@cal-sdk/mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server built on calsdk. Point an MCP client at it and the model can see your schedule, find a slot that works across all your accounts and put a meeting in it. It runs on your machine and talks to the calendar APIs directly: no hosted service sits in between.

Try it first on two in-memory calendars, no account needed:

```bash
npx -y @cal-sdk/mcp --demo
```

## Tools

| Tool | What it does |
| --- | --- |
| `list_calendars` | Calendars on every connected account, with ids and access |
| `list_events` | Events in a window, recurring events expanded into occurrences |
| `find_free_time` | Slots when every chosen calendar is free, across accounts |
| `create_event` | A new event, optionally recurring, with attendees or a meeting link |
| `update_event` | Change some fields, for one occurrence or the whole series |
| `delete_event` | Remove an event, one occurrence or the rest of a series |

Connect more than one account and each tool takes an `account` argument (`google`, `microsoft`, `icloud` or `caldav`). `find_free_time` takes a `calendars` list instead, with an `account` and optional `calendarIds` in each entry. Left out, it checks the primary calendar of every account, and it fails instead of guessing when one can't be read, so the model never books over a meeting it couldn't see.

## Add it to your client

<Tabs items={['Claude Code', 'Claude Desktop', 'Cursor']}>
<Tab value="Claude Code">

```bash
claude mcp add calendar \
-e ICLOUD_APPLE_ID=me@icloud.com -e ICLOUD_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
-- npx -y @cal-sdk/mcp
```

</Tab>
<Tab value="Claude Desktop">

Add this to `claude_desktop_config.json` and restart the app:

```json
{
"mcpServers": {
"calendar": {
"command": "npx",
"args": ["-y", "@cal-sdk/mcp"],
"env": {
"GOOGLE_CLIENT_ID": "...",
"GOOGLE_CLIENT_SECRET": "...",
"GOOGLE_REFRESH_TOKEN": "..."
}
}
}
}
```

</Tab>
<Tab value="Cursor">

Add this to `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` for every project:

```json
{
"mcpServers": {
"calendar": {
"command": "npx",
"args": ["-y", "@cal-sdk/mcp"],
"env": { "MICROSOFT_CLIENT_ID": "...", "MICROSOFT_REFRESH_TOKEN": "..." }
}
}
}
```

</Tab>
</Tabs>

## Connect your calendars

Set the variables for each calendar you want. Any mix works.

| Calendar | Variables |
| --- | --- |
| Google | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REFRESH_TOKEN` (or a short-lived `GOOGLE_ACCESS_TOKEN`) |
| Outlook | `MICROSOFT_CLIENT_ID`, `MICROSOFT_REFRESH_TOKEN`, plus `MICROSOFT_CLIENT_SECRET` for confidential apps (or `MICROSOFT_ACCESS_TOKEN`) |
| iCloud | `ICLOUD_APPLE_ID`, `ICLOUD_APP_PASSWORD` |
| Any CalDAV server | `CALDAV_URL`, `CALDAV_USERNAME`, `CALDAV_PASSWORD` |

With a refresh token the server gets a new access token whenever the old one is about to expire, so it keeps working after the first hour.

**iCloud** is the quickest: make an app-specific password at [account.apple.com](https://account.apple.com) under Sign-In and Security, and use it with your Apple ID.

**Google** needs an OAuth client of your own. Create one in the Google Cloud console (type "Web application", with `https://developers.google.com/oauthplayground` as a redirect URI) and enable the Google Calendar API. Then open the [OAuth Playground](https://developers.google.com/oauthplayground), tick "Use your own OAuth credentials" in the settings, authorize the `https://www.googleapis.com/auth/calendar` scope and exchange the code for a refresh token. While the app's publishing status is "Testing", Google expires refresh tokens after 7 days, so publish it (it can stay unverified for your own account) to keep the token.

**Outlook** needs an app registration in Microsoft Entra with the delegated `Calendars.ReadWrite` permission. Get a refresh token with any OAuth tool that requests `offline_access Calendars.ReadWrite`. Set `MICROSOFT_TENANT` if the app is single-tenant, and `MICROSOFT_SCOPE` if you granted something other than `Calendars.ReadWrite`.

## Use it from code

The server is also a function, so you can host it yourself, for example over HTTP with your users' tokens:

```ts
import { google } from '@cal-sdk/google';
import { createCalendarServer } from '@cal-sdk/mcp';

const server = createCalendarServer({
work: google({ accessToken: () => getTokenFor(userId) }),
});
```

Building an agent with the AI SDK instead? You may not need MCP at all: see [AI SDK tools](/docs/guides/ai-sdk).
4 changes: 3 additions & 1 deletion apps/web/content/docs/guides/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"free-time",
"sync",
"provider-options",
"testing"
"testing",
"mcp",
"ai-sdk"
]
}
7 changes: 6 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@
}
},
{
"includes": ["packages/*/src/**", "!packages/provider-tests/**", "!**/*.test.ts"],
"includes": [
"packages/*/src/**",
"!packages/provider-tests/**",
"!**/*.test.ts",
"!packages/mcp/src/cli.ts"
],
"linter": {
"rules": {
"correctness": {
Expand Down
Loading
Loading