Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a1bec6b
Wire up Sparkle so the app actually checks for updates
Arsey Aug 18, 2026
9436cfc
Fix SIGTRAP crash from continuation misuse in connection handling (#151)
sciencemanx Sep 2, 2026
6f90737
Cancel the Bonjour browser on every discovery exit path (#199)
mattt Sep 2, 2026
4e646f8
Exempt connections awaiting approval from the setup timeout (#200)
mattt Sep 2, 2026
45e024b
Fix events_fetch: date-only inputs silently ignored; missing @id on e…
ole-lilienthal Sep 2, 2026
077ff38
Bump Madrid to 0.5.0 and adopt FetchRequest<Message> (#201)
mattt Sep 2, 2026
91fc121
Add read status to messages_fetch (#195)
sebpretzer Sep 2, 2026
0c67edf
Play confirmation sound when copying server command (#181)
Arsey Sep 3, 2026
3165069
Add accessibility labels to service toggle buttons (#182)
Arsey Sep 3, 2026
701e140
Add per-row remove button to trusted clients list (#184)
Arsey Sep 3, 2026
c23262f
Explain trusted clients in Settings (#186)
Arsey Sep 3, 2026
f128fc0
Add Launch at Login setting (#187)
Arsey Sep 3, 2026
b4d3edd
Fix Settings window intermittently not appearing (#188)
Arsey Sep 3, 2026
8dd6a3d
Stop caching Xcode's module cache between CI runs (#202)
mattt Sep 3, 2026
9326064
List enabled services in the connection approval dialog (#185)
Arsey Sep 4, 2026
10997d1
Fix oversized menu panel after toggling MCP server off (#189)
Arsey Sep 4, 2026
bac35e2
Add per-tool toggles and show what each service exposes (#190)
Arsey Sep 4, 2026
1bb7543
Add Call History service (#157)
bratland Sep 4, 2026
d50fa66
Ignore .DS_Store (#204)
mattt Sep 4, 2026
9c1c688
Rotate the Sparkle public key (#203)
mattt Sep 4, 2026
c46245f
Automate releases with GitHub Actions and a Sparkle appcast step (#205)
mattt Sep 4, 2026
837f1a7
Key the CI cache on the Xcode build number (#207)
mattt Sep 4, 2026
087ae5e
Fix reconnect loop that drops Claude Desktop connections (#208)
mattt Sep 5, 2026
e1babfc
Drop the module cache before the CI cache is saved
mattt Sep 5, 2026
6588c0c
Merge pull request #180 from Arsey/fix/wire-up-sparkle-auto-updates
mattt Sep 5, 2026
a408030
Don't drop company-type contacts from search and list (#206)
patp Sep 6, 2026
3aef7ae
Reuse Person(CNContact) for company-toggled cards and sort contacts_l…
mattt Sep 6, 2026
da2dc67
Add an events_delete calendar tool (#209)
Iamlel Sep 6, 2026
43e61bf
Target the exact occurrence in events_delete and reject unknown spans
mattt Sep 6, 2026
ecef7a7
Rename Call History to Phone and add a phone_call tool (#213)
mattt Sep 8, 2026
77b5056
Fix task cancellation and captures for Xcode 27 (#216)
mattt Sep 16, 2026
51883dd
Track one approval window per client so concurrent requests close cor…
mattt Sep 17, 2026
98b5d47
Messages: grant the Messages folder so the write-ahead log is readabl…
patp Sep 17, 2026
5bf6fdc
Grant the CallHistoryDB folder so phone_calls_fetch can read the writ…
mattt Sep 17, 2026
d0fa365
Keep the existing grant when the folder picker is dismissed (#221)
mattt Sep 17, 2026
03e29d6
Reject invalid events_fetch start dates (#223)
mattt Sep 17, 2026
a48258b
Close sockets when client message loops end (#225)
mattt Sep 17, 2026
27f13fd
Direct place-name queries to maps_search (#224)
mattt Sep 17, 2026
036dd5b
Remove unsupported transit directions (#222)
mattt Sep 17, 2026
dfcfdb7
Update README services and generate hero images (#226)
mattt Sep 17, 2026
ad164a9
Release 1.5.0
mattt Sep 17, 2026
9547f12
Verify existing release assets after upload failures (#227)
mattt Sep 17, 2026
b4fdc66
Name the Bonjour service iMCP (#228)
mattt Sep 18, 2026
8bce43a
Fetch editable contacts and serialize contact updates (#231)
mattt Sep 18, 2026
6c0dbfa
Request service permissions from tool handlers (#230)
mattt Sep 18, 2026
17dccd2
Connect over loopback after Bonjour discovery (#229)
mattt Sep 18, 2026
acaa8c1
Replace MenuBarExtraAccess with a local menu panel controller (#233)
mattt Sep 20, 2026
a4096d0
Keep iMCP running when its menu bar item is removed (#234)
mattt Sep 20, 2026
8cfd596
Release 1.5.1
mattt Sep 20, 2026
dfc5c8f
Preserve the menu bar preference when macOS removes the icon (#236)
mattt Sep 21, 2026
5b71979
Release 1.5.2 (#237)
mattt Sep 21, 2026
9afde85
Prevent repeated NECP listener errors (#238)
mattt Sep 21, 2026
cc66b2e
Update Ontology to 0.10.0 (#240)
mattt Sep 22, 2026
296a708
Bump version to 1.5.3 (#241)
mattt Sep 22, 2026
0a46968
Accept connections only over loopback (#242)
mattt Sep 24, 2026
6cb3c82
Bump version to 1.5.4 (#243)
mattt Sep 24, 2026
a0a8583
Merge upstream mattt/iMCP main (1.5.4)
claude Sep 24, 2026
190a3af
Format LAN toggle for swift-format lint
claude Sep 24, 2026
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
20 changes: 18 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,34 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6

# Key the cache on the exact Xcode build,
# so a runner image update can't restore module files
# compiled against an older toolchain.
- name: Read Xcode build number
id: xcode
run: echo "build=$(xcodebuild -version | awk '/Build version/ {print $3}')" >> "${GITHUB_OUTPUT}"

- name: Cache Xcode build artifacts
uses: actions/cache@v5
with:
path: |
~/Library/Developer/Xcode/DerivedData
!~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-xcode-${{ matrix.xcode }}-derived-${{ hashFiles('**/Package.resolved') }}
key: ${{ runner.os }}-xcode-${{ steps.xcode.outputs.build }}-derived-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-xcode-${{ matrix.xcode }}-derived-
${{ runner.os }}-xcode-${{ steps.xcode.outputs.build }}-derived-

- name: Lint
run: swift format lint --strict --recursive .

- name: Build
run: xcodebuild -quiet -scheme iMCP -configuration Debug -destination "platform=macOS" build

# The cache action's exclude pattern has not kept the module cache out of
# the archive, and runner images with the same Xcode build can differ in
# toolchain file timestamps, which makes a restored module cache fatal.
# Remove it before the cache is saved at the end of the job.
- name: Drop the module cache before saving
if: always()
run: rm -rf ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex
211 changes: 211 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
name: Release

# Builds, signs, notarizes, and publishes a release
# with the same Scripts/release.sh steps used locally.
#
# A pushed version tag (for example 1.5.0) runs the full flow.
# Manual runs default to a dry run,
# which does everything except create the GitHub release and upload assets;
# the artifacts are attached to the workflow run instead.

on:
push:
tags: ["[0-9]+.[0-9]+.[0-9]+"]
workflow_dispatch:
inputs:
version:
description: "Version to release (defaults to the tag name when run from a tag)"
required: false
type: string
dry_run:
description: "Skip creating the GitHub release and uploading assets"
required: false
type: boolean
default: true

permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

env:
DEVELOPER_DIR: "/Applications/Xcode_26.0.app/Contents/Developer"
TEAM_ID: TTY35UM57S
SIGNING_CERTIFICATE: "Developer ID Application: dododo, LLC (TTY35UM57S)"
SPARKLE_VERSION: "2.9.6"
SPARKLE_SHA256: "52bf9e88cdd972fc0c81501377a880e90d47031bd8ca5462488f843e2609e192"

jobs:
release:
name: Build, notarize, and publish
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Resolve version
id: version
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
version="${GITHUB_REF_NAME}"
else
version="${INPUT_VERSION}"
fi
if [[ -z "${version}" ]]; then
echo "No version: push a version tag or pass the version input." >&2
exit 1
fi
project_version="$(grep -m1 -o 'MARKETING_VERSION = [0-9.]*' iMCP.xcodeproj/project.pbxproj | awk '{print $3}')"
if [[ "${project_version}" != "${version}" ]]; then
echo "MARKETING_VERSION in the project is ${project_version}, not ${version}." >&2
exit 1
fi
echo "VERSION=${version}" >> "${GITHUB_ENV}"

# Tag pushes always publish; manual runs publish only when dry_run is off.
- name: Note dry run
if: github.event_name == 'workflow_dispatch' && inputs.dry_run
run: echo "DRY_RUN=1" >> "${GITHUB_ENV}"

# A manual publish must run from the version's tag,
# so the release's source matches the binary it ships.
- name: Require the release tag
if: github.event_name == 'workflow_dispatch' && !inputs.dry_run
run: |
if [[ "${GITHUB_REF_TYPE}" != "tag" || "${GITHUB_REF_NAME}" != "${VERSION}" ]]; then
echo "Publishing runs must be started from the ${VERSION} tag, not ${GITHUB_REF_TYPE} ${GITHUB_REF_NAME}." >&2
exit 1
fi

- name: Import signing certificate
env:
CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
keychain="${RUNNER_TEMP}/release.keychain-db"
keychain_password="$(uuidgen)"
certificate="${RUNNER_TEMP}/certificate.p12"
echo "${CERTIFICATE_P12}" | base64 --decode > "${certificate}"
security create-keychain -p "${keychain_password}" "${keychain}"
security set-keychain-settings -lut 21600 "${keychain}"
security unlock-keychain -p "${keychain_password}" "${keychain}"
security import "${certificate}" -P "${CERTIFICATE_PASSWORD}" -A -t cert -f pkcs12 -k "${keychain}"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${keychain_password}" "${keychain}" > /dev/null
security list-keychains -d user -s "${keychain}" login.keychain-db
rm -f "${certificate}"
security find-identity -v -p codesigning "${keychain}"
echo "RELEASE_KEYCHAIN=${keychain}" >> "${GITHUB_ENV}"

- name: Install provisioning profile
env:
PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
run: |
profile="${RUNNER_TEMP}/profile.provisionprofile"
echo "${PROVISIONING_PROFILE}" | base64 --decode > "${profile}"
plist="${RUNNER_TEMP}/profile.plist"
security cms -D -i "${profile}" > "${plist}"
uuid="$(/usr/libexec/PlistBuddy -c "Print UUID" "${plist}")"
for dir in "${HOME}/Library/MobileDevice/Provisioning Profiles" "${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles"; do
mkdir -p "${dir}"
cp "${profile}" "${dir}/${uuid}.provisionprofile"
done
echo "PROVISIONING_PROFILE_UUID=${uuid}" >> "${GITHUB_ENV}"
/usr/libexec/PlistBuddy -c "Print Name" "${plist}"

- name: Write notarization key
env:
APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
run: |
key="${RUNNER_TEMP}/AuthKey.p8"
printf '%s\n' "${APP_STORE_CONNECT_PRIVATE_KEY}" > "${key}"
chmod 600 "${key}"
echo "NOTARY_KEY_FILE=${key}" >> "${GITHUB_ENV}"

- name: Install Sparkle tools
run: |
archive="${RUNNER_TEMP}/Sparkle-${SPARKLE_VERSION}.tar.xz"
curl -sSL -o "${archive}" "https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
echo "${SPARKLE_SHA256} ${archive}" | shasum -a 256 -c -
mkdir -p "${RUNNER_TEMP}/sparkle"
tar -xJf "${archive}" -C "${RUNNER_TEMP}/sparkle" ./bin/generate_appcast ./bin/sign_update
echo "SPARKLE_BIN=${RUNNER_TEMP}/sparkle/bin" >> "${GITHUB_ENV}"

- name: Write Sparkle signing key
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
key="${RUNNER_TEMP}/sparkle_private_key"
printf '%s' "${SPARKLE_PRIVATE_KEY}" > "${key}"
chmod 600 "${key}"
echo "SPARKLE_PRIVATE_KEY_FILE=${key}" >> "${GITHUB_ENV}"

- name: Archive
run: ./Scripts/release.sh archive

- name: Export
run: ./Scripts/release.sh export

- name: Notarize
env:
NOTARY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
NOTARY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
run: ./Scripts/release.sh notarize

- name: Staple
run: |
./Scripts/release.sh staple
xcrun stapler validate dist/export/*.app

- name: Package
run: ./Scripts/release.sh package

- name: Generate appcast
env:
SPARKLE_PUBLIC_KEY: ${{ vars.SPARKLE_PUBLIC_KEY }}
run: ./Scripts/release.sh appcast

# Everything is built and validated before anything is published.
# The release stays a draft until both uploads have succeeded.
- name: Create draft release
env:
GH_TOKEN: ${{ github.token }}
run: ./Scripts/release.sh release

- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
run: ./Scripts/release.sh upload

- name: Upload appcast
env:
GH_TOKEN: ${{ github.token }}
run: ./Scripts/release.sh upload-appcast

- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: ./Scripts/release.sh publish

- name: Attach build products to the workflow run
if: always()
uses: actions/upload-artifact@v4
with:
name: iMCP-${{ env.VERSION }}
path: |
dist/*.zip
dist/*.sha256
dist/appcast/appcast.xml
if-no-files-found: ignore

- name: Clean up secrets
if: always()
run: |
rm -f "${RUNNER_TEMP}/sparkle_private_key" "${RUNNER_TEMP}/AuthKey.p8" "${RUNNER_TEMP}/profile.provisionprofile"
if [[ -n "${RELEASE_KEYCHAIN:-}" && -f "${RELEASE_KEYCHAIN}" ]]; then
security delete-keychain "${RELEASE_KEYCHAIN}"
fi
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# macOS
.DS_Store

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
Expand Down
65 changes: 59 additions & 6 deletions App/App.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,57 @@
import MenuBarExtraAccess
import Sparkle
import SwiftUI

@main
struct App: SwiftUI.App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@Environment(\.openSettings) private var openSettings
@StateObject private var serverController = ServerController()
@AppStorage("isEnabled") private var isEnabled = true
@State private var isMenuPresented = false
@AppStorage("showMenuBarExtra") private var showMenuBarExtra = true

// `startingUpdater: true` makes this the sole owner of update checking
// for the app's lifetime.
// Without it (or without ever constructing an updater at all),
// the SUFeedURL / SUPublicEDKey keys in Info.plist are inert
// and installs never learn about newer releases,
// no matter how long they run.
private let updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)

var body: some Scene {
MenuBarExtra("iMCP", image: #"MenuIcon-\#(isEnabled ? "On" : "Off")"#) {
@Bindable var appDelegate = appDelegate

// The binding lets removal hide the item without terminating the server.
MenuBarExtra(
"iMCP",
image: #"MenuIcon-\#(isEnabled ? "On" : "Off")"#,
isInserted: $appDelegate.isMenuBarExtraInserted
) {
ContentView(
serverManager: serverController,
isEnabled: $isEnabled,
isMenuPresented: $isMenuPresented
updater: updaterController.updater
)
}
.menuBarExtraStyle(.window)
.menuBarExtraAccess(isPresented: $isMenuPresented)
.onChange(of: showMenuBarExtra) { _, showMenuBarExtra in
appDelegate.isMenuBarExtraInserted = showMenuBarExtra
}
.onChange(of: appDelegate.shouldOpenSettings, initial: true) { _, shouldOpenSettings in
guard shouldOpenSettings else { return }
appDelegate.shouldOpenSettings = false
NSApp.activate(ignoringOtherApps: true)
openSettings()
}

Settings {
SettingsView(serverController: serverController)
SettingsView(
serverController: serverController,
isMenuBarExtraInserted: appDelegate.isMenuBarExtraInserted
)
}

.commands {
Expand All @@ -32,3 +64,24 @@ struct App: SwiftUI.App {
}
}
}

@Observable
final class AppDelegate: NSObject, NSApplicationDelegate {
var shouldOpenSettings = false
// System removal must not overwrite the user's saved preference.
var isMenuBarExtraInserted =
UserDefaults.standard.object(forKey: "showMenuBarExtra") as? Bool ?? true

func applicationDidFinishLaunching(_ notification: Notification) {
// An unset preference defaults to showing the menu bar item.
if UserDefaults.standard.object(forKey: "showMenuBarExtra") as? Bool == false {
shouldOpenSettings = true
}
}

func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
// Reopening the app must work even when the menu bar item is absent.
shouldOpenSettings = true
return false
}
}
Loading