A Swift 6 MCP (Model Context Protocol) server that provides AI assistants with access to macOS applications and services.
Apple Bridge exposes 7 macOS domains through MCP tools:
| Domain | Tools | Access Method |
|---|---|---|
| Calendar | calendar_list, calendar_create, calendar_update, calendar_delete |
EventKit |
| Reminders | reminders_list, reminders_create, reminders_update, reminders_complete, reminders_delete |
EventKit |
| Contacts | contacts_search, contacts_me |
AppleScript |
| Notes | notes_search, notes_list, notes_create |
AppleScript |
| Messages | messages_read, messages_send, messages_unread |
AppleScript (partial) |
mail_search, mail_unread, mail_send |
AppleScript | |
| Maps | maps_search, maps_nearby, maps_directions, maps_open |
MapKit |
- macOS 13.0 (Ventura) or later
- Swift 6.0+
- Xcode 16.0+ (for development)
git clone https://github.com/boutquin/apple-bridge.git
cd apple-bridge
swift build -c releaseThe executable will be at .build/release/apple-bridge.
Apple Bridge requires various macOS permissions depending on which domains you use:
| Permission | Required For | Grant Location |
|---|---|---|
| Calendar | Calendar domain | System Settings → Privacy & Security → Calendars |
| Reminders | Reminders domain | System Settings → Privacy & Security → Reminders |
| Automation (Contacts) | Contacts domain | System Settings → Privacy & Security → Automation |
| Automation (Notes) | Notes domain | System Settings → Privacy & Security → Automation |
| Automation (Messages) | Messages domain (chats, send) | System Settings → Privacy & Security → Automation |
| Full Disk Access | Messages domain (read, unread) | System Settings → Privacy & Security → Full Disk Access |
| Automation (Mail) | Mail domain | System Settings → Privacy & Security → Automation |
| Location Services | Maps domain | System Settings → Privacy & Security → Location Services |
Note: Messages read/unread requires Full Disk Access because the Messages.app scripting dictionary does not expose individual messages. All other AppleScript domains (Contacts, Notes, Messages chats/send, Mail) only need Automation permission.
Add to your claude_desktop_config.json:
{
"mcpServers": {
"apple-bridge": {
"command": "/path/to/apple-bridge"
}
}
}swift build # Debug build
swift build -c release # Release buildApple Bridge has several test categories:
# Run all unit tests (no permissions required)
swift test
# Run specific test suites
swift test --filter CoreTests
swift test --filter AdapterTests
swift test --filter MCPServerTests
swift test --filter E2ETestsSystem tests verify real macOS integration and require proper permissions. They are disabled by default to allow CI to pass without macOS permissions.
# Enable and run system tests
export APPLE_BRIDGE_SYSTEM_TESTS=1
swift test --filter SystemTests
# Or use the convenience script
./scripts/run-system-tests.sh
# With coverage reporting
./scripts/run-system-tests.sh --coverageBefore running system tests, ensure:
- Calendar access granted to Terminal/IDE
- Automation access granted to Terminal/IDE (for Contacts, Notes, Messages, Mail)
- Full Disk Access granted to Terminal/IDE (only for Messages read/unread tests)
- Apple Mail configured with at least one account
- Apple Maps available
Some tests verify graceful degradation when permissions are denied. These require special setup:
# 1. REMOVE Full Disk Access from the apple-bridge binary
# 2. Set both environment variables
export APPLE_BRIDGE_SYSTEM_TESTS=1
export APPLE_BRIDGE_MANUAL_QA=1
# 3. Run FDA tests
swift test --filter FDA
# 4. Re-grant Full Disk Access after testingapple-bridge/
├── Sources/
│ ├── apple-bridge/ # Main executable
│ ├── Core/ # Domain models, service protocols, errors
│ ├── Adapters/ # macOS framework adapters (see Architecture)
│ └── MCPServer/ # MCP protocol implementation and handlers
├── Tests/
│ ├── CoreTests/ # Unit tests for Core
│ ├── AdapterTests/ # Unit tests for Adapters
│ ├── MCPServerTests/ # Unit tests for MCP handlers
│ ├── E2ETests/ # End-to-end protocol tests
│ ├── SystemTests/ # Real macOS integration tests
│ └── TestUtilities/ # Shared test helpers
└── scripts/
└── run-system-tests.sh # System test runner
The TestUtilities module provides shared testing infrastructure:
- Factory Functions:
makeTestEvent(),makeTestReminder(), etc. - Mock Services:
MockCalendarService,MockNotesService, etc. - ProcessRunner: Shared executable runner for E2E and System tests
Apple Bridge uses a clean three-layer architecture that separates concerns and enables comprehensive testing:
┌─────────────────────────────────────────────────────────────────┐
│ MCP Handlers │
│ (MCPServer/Handlers/) │
│ Receives MCP tool calls, validates arguments, returns JSON │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Service Layer │
│ (Core/Services/) │
│ Domain protocols (CalendarService, NotesService, etc.) │
│ Uses domain models (CalendarEvent, Note, Message, etc.) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Adapter Layer │
│ (Adapters/) │
│ Adapter protocols + Real implementations │
│ Uses DTOs (CalendarEventData, NoteData, MessageData, etc.) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ macOS Frameworks │
│ EventKit, AppleScript, MapKit │
└─────────────────────────────────────────────────────────────────┘
Each domain uses the Adapter Pattern to decouple the service layer from specific macOS framework implementations. This provides:
- Testability: Mock adapters can be injected for unit testing without macOS permissions
- Flexibility: Alternative implementations can be swapped (e.g., MapKit vs AppleScript for Maps)
- Clean Boundaries: DTOs provide Sendable-safe data transfer across actor boundaries
- Implementation Hiding: Service layer doesn't know if data comes from SQLite, EventKit, or AppleScript
| Domain | Adapter Protocol | DTOs | Implementation |
|---|---|---|---|
| Calendar | CalendarAdapterProtocol |
CalendarEventData, CalendarData |
EventKitAdapter |
| Reminders | CalendarAdapterProtocol |
ReminderData, ReminderListData |
EventKitAdapter |
| Contacts | ContactsAdapterProtocol |
ContactData |
AppleScriptContactsAdapter |
| Notes | NotesAdapterProtocol |
NoteData, NoteFolderData |
AppleScriptNotesAdapter |
| Messages | MessagesAdapterProtocol |
MessageData, ChatData |
AppleScriptMessagesAdapter |
MailAdapterProtocol |
EmailData, MailboxData |
AppleScriptMailAdapter |
|
| Maps | MapsAdapterProtocol |
LocationData |
MapKitAdapter |
| Domain | Read | Write | Adapter |
|---|---|---|---|
| Calendar | EventKit | EventKit | EventKitAdapter |
| Reminders | EventKit | EventKit | EventKitAdapter |
| Contacts | AppleScript | Read-only | AppleScriptContactsAdapter |
| Notes | AppleScript | AppleScript | AppleScriptNotesAdapter |
| Messages | AppleScript (chats only) | AppleScript | AppleScriptMessagesAdapter |
| AppleScript | AppleScript | AppleScriptMailAdapter |
|
| Maps | MapKit | MapKit | MapKitAdapter |
Data Transfer Objects (DTOs) follow these principles:
- Sendable: All DTOs are
Sendablefor safe actor boundary crossing - Codable: DTOs are
Codablefor serialization when needed - Equatable: DTOs are
Equatablefor testing assertions - Implementation-Agnostic: No framework-specific prefixes (e.g.,
CalendarEventDatanotEKEventData) - Lightweight: Only essential fields, no framework dependencies
Example DTO:
public struct CalendarEventData: Sendable, Equatable {
public let id: String
public let title: String
public let startDate: Date
public let endDate: Date
public let calendarId: String
public let location: String?
public let notes: String?
}Services delegate to adapters and convert between domain models and DTOs:
// Service implementation
public struct NotesSQLiteService: NotesService {
private let adapter: any NotesAdapterProtocol
public func get(id: String) async throws -> Note {
let noteData = try await adapter.fetchNote(id: id)
return toNote(noteData) // Convert DTO to domain model
}
private func toNote(_ data: NoteData) -> Note {
Note(id: data.id, title: data.title, body: data.body, ...)
}
}Each domain has a corresponding mock adapter for unit testing:
| Domain | Mock Adapter | Location |
|---|---|---|
| Calendar/Reminders | MockEventKitAdapter |
Tests/AdapterTests/Mocks/ |
| Contacts | MockContactsAdapter |
Tests/AdapterTests/Mocks/ |
| Notes | MockNotesAdapter |
Tests/AdapterTests/Mocks/ |
| Messages | MockMessagesAdapter |
Tests/AdapterTests/Mocks/ |
MockMailAdapter |
Tests/AdapterTests/Mocks/ |
|
| Maps | MockMapsAdapter |
Tests/AdapterTests/Mocks/ |
Mock adapters are actors that:
- Store stubbed data for return values
- Track method calls for verification
- Allow error injection for testing error paths
Example usage:
func testSearchNotes() async throws {
let mockAdapter = MockNotesAdapter()
await mockAdapter.setStubNotes([
NoteData(id: "1", title: "Meeting Notes", body: "...", modifiedAt: "...")
])
let service = NotesSQLiteService(adapter: mockAdapter)
let results = try await service.search(query: "Meeting", limit: 10, includeBody: true)
XCTAssertEqual(results.items.count, 1)
XCTAssertEqual(results.items[0].title, "Meeting Notes")
}Sources/Adapters/
├── EventKitAdapter/
│ ├── CalendarAdapterProtocol.swift # Protocol + Calendar/Reminder DTOs
│ ├── EventKitAdapter.swift # EventKit implementation
│ ├── EventKitCalendarService.swift # CalendarService using adapter
│ └── EventKitRemindersService.swift # RemindersService using adapter
├── ContactsAdapter/
│ ├── ContactsAdapterProtocol.swift # Protocol + ContactData DTO
│ ├── ContactsAdapter.swift # Contacts framework implementation (requires signed binary)
│ ├── AppleScriptContactsAdapter.swift # AppleScript implementation (default)
│ └── ContactsFrameworkService.swift # ContactsService using adapter
├── NotesAdapter/
│ ├── NotesAdapterProtocol.swift # Protocol + NoteData DTO
│ ├── SQLiteNotesAdapter.swift # SQLite implementation (requires Full Disk Access)
│ └── AppleScriptNotesAdapter.swift # AppleScript implementation (default)
├── MessagesAdapter/
│ ├── MessagesAdapterProtocol.swift # Protocol + MessageData/ChatData DTOs
│ ├── HybridMessagesAdapter.swift # SQLite (read) + AppleScript (send) (requires FDA)
│ └── AppleScriptMessagesAdapter.swift # AppleScript implementation (default, partial)
├── MailAdapter/
│ ├── MailAdapterProtocol.swift # Protocol + EmailData DTO
│ └── AppleScriptMailAdapter.swift # AppleScript implementation
├── MapsAdapter/
│ ├── MapsAdapterProtocol.swift # Protocol + LocationData DTO
│ ├── MapKitAdapter.swift # MapKit implementation
│ └── MapsKitService.swift # MapsService using MapKitAdapter
├── SQLiteAdapter/
│ ├── SQLiteConnection.swift # Low-level SQLite wrapper
│ ├── SchemaValidation.swift # Database schema validation
│ ├── NotesSQLiteService.swift # NotesService using NotesAdapter
│ └── MessagesSQLiteService.swift # MessagesService using MessagesAdapter
└── AppleScriptAdapter/
├── AppleScriptRunner.swift # Actor for script execution
└── MailAppleScriptService.swift # MailService using MailAdapter
Apple Bridge is fully Swift 6 compliant with strict concurrency checking:
- All service protocols are
Sendable - All adapter protocols are
Sendable - All DTOs are
Sendable - Adapters use actors where needed for thread safety
- No data races or Sendable violations
Errors follow a consistent pattern:
ToolErrorfor MCP-level errors (invalid arguments, unknown tool)ValidationErrorfor domain validation (not found, missing required fields)PermissionErrorfor access issues (calendar denied, full disk access required)AppleScriptErrorfor AppleScript execution failuresSQLiteErrorfor database errors
All errors include remediation instructions when applicable.
Apple Bridge implements MCP specification 2024-11-05:
- JSON-RPC 2.0 over stdio
- Tool definitions with JSON Schema validation
- Cursor-based pagination for list operations
"Permission denied" for Calendar/Reminders:
- Open System Settings → Privacy & Security
- Find the relevant section (Calendars or Reminders)
- Enable access for your terminal application or Claude Desktop
"AppleScript error" for Contacts/Notes/Messages/Mail:
- Open System Settings → Privacy & Security → Automation
- Enable access to the relevant app (Contacts.app, Notes.app, Messages.app, Mail.app)
- You may need to run a command once to trigger the permission prompt
"Full Disk Access required" for Messages read/unread: The Messages.app scripting dictionary does not expose individual messages, so reading message history requires direct database access via Full Disk Access:
- Open System Settings → Privacy & Security → Full Disk Access
- Add the
apple-bridgeexecutable (requires a properly signed binary) - Restart the application after granting access
Note: Ad-hoc signed binaries cannot reliably hold Full Disk Access TCC entries. A signed binary (Apple Developer certificate) is required for Messages read/unread functionality.
Server doesn't respond:
- Ensure you're using the correct path to the
apple-bridgeexecutable - Check that the executable has execute permissions:
chmod +x apple-bridge - Verify no other process is using stdin/stdout
Tool returns empty results:
- Verify the relevant macOS app has data (e.g., Calendar has events)
- Check that permissions are granted in System Settings
Mail operations are slow or time out:
- Mail uses indexed reverse iteration to avoid loading all messages at once
- Very large inboxes (100K+ messages) may still take a few seconds for search
- Ensure Mail.app is not in an unresponsive state
Tests fail with permission errors:
- Unit tests don't require permissions and should always pass
- System tests require
APPLE_BRIDGE_SYSTEM_TESTS=1and proper permissions - See the "System Tests" section above for setup instructions
To see detailed logging, run with stderr visible:
# View logs while running
./apple-bridge 2>apple-bridge.log &
tail -f apple-bridge.logLogs go to stderr only; stdout is reserved for MCP protocol messages.
MIT License - see LICENSE file for details.