Lenticular Zone

Manually granting calendar permissions to Claude for MCP servers on macOS

I had the need to add dates from a screenshot (of a spreadsheet) of events to my calendar, so thought this would be the perfect use case for an MCP server with my local Claude client.

After searching around for “MCP Calendar”, I landed on MCP iCal Server. I followed the instructions, and it worked flawlessly, so I'd like to keep it around to help me manage calendar events on Apple calendar.

Under the hood, the server uses the Python wrapper for Apple's EventKit framework.

The only catch is that I need to start Claude from the CLI to get my terminal emulator to request the calendar permission the first time I use this server:

/Applications/Claude.app/Contents/MacOS/Claude

From macOS's perspective, “A program running within Ghostty would like to access your Calendar”.

Desktop view showing Claude launched from terminal and the macOS permission dialog where 'Ghostty.app' requests Calendar access, demonstrating how terminal launch enables permission requests

Trying the same query when opening Claude from the GUI (by double-clicking the orange Vonnegut-inspired app icon), this server fails to request the permission, and macOS never shows the Calendar permission modal.

I haven't dug into the reason yet, assuming Claude runs the MCP server as a subprocess that somehow can't request OS permissions.

So I wondered where macOS saves these permissions, and whether I can update them manually so that I can grant Claude (and, hopefully, its subprocesses) Calendar access. This might also be useful in the future if I use other local MCP servers that use Apple OS APIs.

I asked Claude for help, and found the ~/Library/Application Support/com.apple.TCC/TCC.db SQLite database.

Running sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db '.schema' returned:

CREATE TABLE admin (key TEXT PRIMARY KEY NOT NULL, value INTEGER NOT NULL);
CREATE TABLE policies (    id        INTEGER    NOT NULL PRIMARY KEY,     bundle_id    TEXT    NOT NULL,     uuid        TEXT    NOT NULL,     display        TEXT    NOT NULL,     UNIQUE (bundle_id, uuid));
CREATE TABLE active_policy (    client        TEXT    NOT NULL,     client_type    INTEGER    NOT NULL,     policy_id    INTEGER NOT NULL,     PRIMARY KEY (client, client_type),     FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE ON UPDATE CASCADE);
CREATE TABLE access (    service        TEXT        NOT NULL,     client         TEXT        NOT NULL,     client_type    INTEGER     NOT NULL,     auth_value     INTEGER     NOT NULL,     auth_reason    INTEGER     NOT NULL,     auth_version   INTEGER     NOT NULL,     csreq          BLOB,     policy_id      INTEGER,     indirect_object_identifier_type    INTEGER,     indirect_object_identifier         TEXT NOT NULL DEFAULT 'UNUSED',     indirect_object_code_identity      BLOB,     flags          INTEGER,     last_modified  INTEGER     NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)),     pid            INTEGER,     pid_version    INTEGER,     boot_uuid      TEXT NOT NULL DEFAULT 'UNUSED',     last_reminded  INTEGER     NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)),     PRIMARY KEY (service, client, client_type, indirect_object_identifier),    FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE ON UPDATE CASCADE);
CREATE TABLE access_overrides (    service        TEXT    NOT NULL PRIMARY KEY);
CREATE TABLE expired (    service        TEXT        NOT NULL,     client         TEXT        NOT NULL,     client_type    INTEGER     NOT NULL,     csreq          BLOB,     last_modified  INTEGER     NOT NULL ,     expired_at     INTEGER     NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)),     PRIMARY KEY (service, client, client_type));
CREATE INDEX active_policy_id ON active_policy(policy_id);

I started getting cold feet, so searched around for help, and found this detailed guide on modifying TCC on macOS by Gregory Parker.

Turns out Claude was on the right track. TCC is short for “transparency, consent and control” - Apple's framework for allowing users to control which apps can access which files and services on macOS. These are the settings you'll find in System Settings > Privacy & Security > Privacy (on macOS 15.4, at least).

The only manual control Apple gives users is to:

  1. Choose whether to allow permissions using a modal when an app requests permissions.
  2. Unset or change the permission level in System Settings > Privacy & Security > Privacy (only after the user accepted the request from the modal in 1).
  3. Reset an app or service's permissions using tccutil, e.g. sudo tccutil reset Camera com.google.Chrome.

The rest of my note can be deduced entirely from Gregory's post, but nonetheless, here's how It went.

I would rather not mess with the system-wide TCC database because it seems to require disabling System Integrity Protection SIP, so I started by making a backup of my user's TCC database (FWIW):

cp ~/Library/Application Support/com.apple.TCC/TCC.db .

According to Gregory's post, we need to update the access table. Let's see how Ghostty (my terminal emulator) was added to this table for the calendar (kTCCServiceCalendar) service:

sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db

In the SQLite, run:

.headers on
.mode insert access
SELECT * FROM access WHERE service IS 'kTCCServiceCalendar';

This returned the INSERT statement for Ghostty's calendar access:

INSERT INTO access (
    service,
    client,
    client_type,
    auth_value,
    auth_reason,
    auth_version,
    csreq,
    policy_id,
    indirect_object_identifier_type,
    indirect_object_identifier,
    indirect_object_code_identity,
    flags,
    last_modified,
    pid,
    pid_version,
    boot_uuid,
    last_reminded
) VALUES (
    'kTCCServiceCalendar',
    'com.mitchellh.ghostty',
    0,
    2,
    2,
    2,
    X'fade0c00000000a400000001000000060000000200000015636f6d2e6d69746368656c6c682e67686f73747479000000000000060000000f000000060000000e000000010000000a2a864886f76364060206000000000000000000060000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a3234565a5446364d35560000',
    NULL,
    NULL,
    'UNUSED',
    NULL,
    16,
    1743577768,
    NULL,
    NULL,
    'UNUSED',
    0
);

Let's get the BundleID to use as client code signature request to use as csreq for Claude.app, in the terminal, run:

# 1. Get the Bundle ID
codesign -dr - /Applications/Claude.app | awk -F \" '{print $2}' Executable=/Applications/Claude.app/Contents/MacOS/Claude

#> com.anthropic.claudefordesktop

# 2. Save a code signing request blob as /tmp/csreq.bin
codesign -dr - /Applications/Claude.app  2>&1 | awk -F ' => ' '/designated/{print $2}' | csreq -r- -b /tmp/csreq.bin

# 3. Dump the hexadecimal value of /tmp/csreq.bin
echo "'$(xxd -p /tmp/csreq.bin  | tr -d '\n')'"

#> fade0c00000000ac0000000100000006000000060000000600000006000000020000001e636f6d2e616e7468726f7069632e636c61756465666f726465736b746f7000000000000f0000000e000000010000000a2a864886f763640602060000000000000000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a51364c325346365944570000

(Update: The code signing request field seems optional, but I'm leaving this step in for posterity - Apple might change this)

To add this to the database, we'll use a REPLACE query because the composite primary key contains, among other fields, the values of service and client and we can run this query multiple times to update this rule.

I used a file instead of pasting directly into SQLite, to avoid syntax issues around the binary string.

Caution: This could potentially break your macOS permissions database. Make a backup.

sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db <<EOF
REPLACE INTO access 
VALUES(
 'kTCCServiceCalendar', -- service
 'com.anthropic.claudefordesktop', -- client
 0, -- client_type
 2, -- auth_value (2 = full access, 4 = add only)
 2, -- auth_reason (copying from Ghostty)
 2, -- auth_version (1 = no options in GUI, 2 = options button in GUI)
 X'fade0c00000000ac0000000100000006000000060000000600000006000000020000001e636f6d2e616e7468726f7069632e636c61756465666f726465736b746f7000000000000f0000000e000000010000000a2a864886f763640602060000000000000000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a51364c325346365944570000',
 NULL, -- policy_id
 0, -- indirect_object_identifier_type
 'UNUSED', -- indirect_object_identifier
 NULL, -- indirect_object_code_identity
 16, -- flags (no idea what this does, copied from Ghostty)
 CAST(strftime('%s','now') AS INTEGER), -- last_modified
 NULL, -- pid
 NULL, -- pid_version
 'UNUSED', -- boot_uuid
 0 -- last_reminded
);
EOF

Reopen Claude by clicking the app icon, and the iCal MCP server should work:

Claude desktop app interface showing calendar integration with mcp-ical server, displaying a conversation where Claude checks for events and reports an empty calendar