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”.
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:
- Choose whether to allow permissions using a modal when an app requests permissions.
- Unset or change the permission level in System Settings > Privacy & Security > Privacy (only after the user accepted the request from the modal in 1).
- 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: