Skip to content

Merge upstream mattt/iMCP (1.5.4) - #1

Merged
rvanderfeer merged 58 commits into
mainfrom
claude/gifted-pascal-q4r0s6
Sep 24, 2026
Merged

rvanderfeer merged 58 commits into
mainfrom
claude/gifted-pascal-q4r0s6

Conversation

@rvanderfeer

@rvanderfeer rvanderfeer commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Merges 56 commits from mattt/iMCP main, up to release 1.5.4.

Conflict resolutions

  • ServerController.swift: keeps the LAN toggle (allowLANConnections) and adds upstream's per-tool disable feature. When LAN is off, the listener also sets upstream's loopback requiredLocalEndpoint (Accept connections only over loopback mattt/iMCP#242), so only this Mac can connect. When LAN is on, that restriction is removed. If the setting is applied before the listener has started, it replaces the idle listener instead of starting it early.
  • SettingsView.swift: the Network Access section sits alongside upstream's Show in Menu Bar and Launch at Login settings. Its toggle is reformatted to pass swift format lint --strict.
  • release.sh: keeps the local install command next to the new appcast, upload-appcast and publish commands.
  • project.pbxproj: keeps the personal team, the com.primadeluxe.iMCP bundle ID and automatic signing, and takes upstream's version 1.5.4 (build 16).

Known limitation

The upstream CLI always connects to the loopback address after Bonjour discovery (CLI/BonjourDiscovery.swift). LAN mode therefore does nothing for imcp-server running on another machine.

Testing

  • CI passes on 190a3af: lint and a Debug xcodebuild build on macOS 26 / Xcode 26.0.
  • Not tested at runtime. The LAN toggle and the new upstream features have not been tried in the running app.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GAFz1WodA51dwyzn4eyjbc

Arsey and others added 30 commits August 18, 2026 20:52
Info.plist has declared SUFeedURL/SUPublicEDKey (and the entitlements
reserve Sparkle's -spks/-spki XPC mach ports) since day one, but no
target ever linked the Sparkle package or instantiated an updater. The
result: every install is permanently frozen at whatever version it
first shipped, silently, with no error and no "Check for Updates" menu
item to surface the problem.

This is why users can stay on known-crashy builds indefinitely: my own
installed copy was v1.4.0 (Jan 2026), three months and one release
behind the fix for the Bonjour-disconnect crash in mattt#166/1.4.1, with no
way to find out short of reading raw Claude Desktop MCP logs.

- Add the Sparkle SPM package to the iMCP app target.
- Construct SPUStandardUpdaterController(startingUpdater: true, ...) in
  App.swift so Sparkle's normal periodic background check actually
  runs, using the existing SUFeedURL/SUPublicEDKey.
- Add a "Check for Updates..." item to the menu bar dropdown, wired to
  updater.checkForUpdates() and disabled while a check is unavailable.

Verified: package graph resolves, `xcodebuild build -scheme iMCP`
succeeds, the built app launches and stays up with Sparkle.framework
loaded, `imcp-serverTests` (incl. ServiceGroupConfigurationTests) still
pass, and `swift format lint --strict --recursive .` is clean.

Note: https://downloads.imcp.app/appcast.xml (the configured SUFeedURL)
currently fails to resolve in DNS (NXDOMAIN), so this alone won't help
until that feed is live — flagging separately in the PR description.
…ttt#151)

The MCP SDK's NetworkTransport has reconnection logic that creates
unstructured tasks holding CheckedContinuation references. For
server-side incoming connections, this reconnection path can cause
double-resume when connections are cancelled, crashing the app.

Disable reconnection and heartbeats for server-accepted connections
since they are client features — if a client disconnects, it should
reconnect itself. Also make removeConnection idempotent to prevent
stop() being called twice on the same connection.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Cancel the Bonjour browser on every discovery exit path

imcp-server retries discovery in a loop, creating a new NWBrowser each
time. The 30-second timeout path never cancelled the previous browser,
so every timed-out attempt left a DNSServiceBrowse connection to
mDNSResponder open. On a machine where the app wasn't running, that
leaked one connection every ~35 seconds until the daemon ran out of
descriptors and DNS failed for every process.

Move discovery into BonjourDiscovery.discoverEndpoint, which cancels the
browser in a defer, and share the file with the CLI test target so the
behavior is covered by a test against a real NWBrowser. Also drop a
module-level browser that was never started and a write-only property.

Fixes mattt#192. Fixes mattt#174.

* Use a valid absent service type and require the timeout error in the test
* Exempt connections awaiting approval from the setup timeout

handleNewConnection races connection setup against a 10-second stall
timer, but setup includes waiting for the user to answer the approval
dialog. A first connection from a new client was therefore torn down
before anyone could click Allow, which is why the first attempt always
failed and the retry (now trusted) succeeded.

Track connections whose setup is blocked on the approval handler and
skip them in the timeout check. The handshake before the prompt is
still covered, and a denied dialog still closes the connection.

Fixes mattt#193.

* Pause the setup timer during approval and restart it afterwards

A one-shot timer that merely skipped connections awaiting approval could
still close a connection approved just before the deadline, and stopped
enforcing anything once approval finished. Keep a timer per connection:
cancel it when the approval handler starts, give the connection a fresh
budget when approval returns, and clear it when setup ends.
…vents (mattt#175)

* Set @id on calendar events from EKEvent.eventIdentifier

Without an identifier, Event values produced from EKEvent encoded as
JSON-LD blank nodes. Two events with the same name/dates/calendar
serialised to byte-identical objects, and downstream JSON-LD-aware
consumers collapsed them into a single node. Symptom: events_fetch
appeared to silently filter out duplicate-named events.

Populate Event.identifier from EKEvent.eventIdentifier in both
events_fetch and events_create so each event carries a stable @id,
matching what Ontology already does for Person (CNContact.identifier)
and ItemList (EKCalendar.calendarIdentifier).

* Fix date-only inputs being misread as having a timezone suffix

ISO8601DateFormatter.lenientDate(fromISO8601String:) ran a regex to
decide whether the input already carried a timezone offset and thus
should not be re-parsed in local time. The regex
#"([Zz]|[+-]\\d{2}(:?\\d{2})?)$"# matches the trailing -DD of any
bare yyyy-MM-dd date — "2026-06-15" looks like it ends with a -15
offset — so date-only inputs returned nil instead of falling through
to the "yyyy-MM-dd" DateFormatter fallback.

In events_fetch this manifested as a silent regression: when the
caller passed e.g. start="2026-06-02", end="2026-06-15", parsing
failed for both, hasStart/hasEnd stayed false, and the tool fell back
to its 'now → now + 1 week' default range. The user saw events from
the current week and assumed events past that point had been filtered
out (in this case two same-name events where only the earlier one was
in the default range).

Gate the timezone-suffix check on dateString.count > 10 so it can't
fire on a bare yyyy-MM-dd. The day component can no longer collide
with the offset branch of the regex; date-only inputs now reach the
"yyyy-MM-dd" fallback as intended.
Madrid 0.4.0 deprecated fetchMessages(with:in:limit:), and the project
treats warnings as errors, so any bump past 0.1.0 failed the build at
the call in Messages.swift. Move that call to fetch(_:) with an
equivalent predicate, and pin 0.5.0, which reads the write-ahead log
and exposes Message.chatID for the follow-up Messages PRs.
* Bump Madrid to 0.4.0

iMCP has been pinned to Madrid 0.1.0. Madrid has since shipped 0.2.0,
0.3.0, and 0.4.0. This bumps the pin to 0.4.0 with no behaviour change,
so that later work can use APIs added in those releases (notably
`Message.readAt` / `Message.isRead` from 0.3.0, for mattt#154).

The version requirement was already `upToNextMajorVersion` from 0.1.0,
which 0.4.0 satisfies, so this is mechanically a re-resolve. The declared
minimum is raised alongside the lockfile to keep the two in agreement.

Source compatibility was verified by compiling iMCP's Madrid call sites
(`Database()`, `Database(path:)`, `fetchParticipant(matching:)`,
`fetchMessages(with:in:limit:)`, and the `Message` properties read in
`messages_fetch`) against 0.4.0. They compile unchanged.

Behaviour was checked against 0.1.0 for the two cases that could have
differed, and matches in both:

- With no participants, 0.1.0 skipped the participant condition. In
  0.4.0 the deprecated `fetchMessages` shim likewise omits the predicate
  for an empty set, and `.and([])` compiles to an absent WHERE clause
  rather than a false one.
- Message ordering was `ORDER BY m.date DESC` in 0.1.0, and the 0.4.0
  default sort is `[.date(.descending), .id(.descending)]` — the same
  order, with an added tiebreak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add read status to messages_fetch

Closes mattt#154.

Adds an optional `isRead` filter to `messages_fetch`, and reports read
status on every message it returns:

- `isRead` (input, optional): if true, fetch read messages; if false,
  unread incoming; if omitted, fetch all.
- `isRead` (output): present on every message.
- `dateRead` (output): ISO 8601, omitted when the message is unread.

The tri-state input mirrors `reminders_fetch`'s `completed` parameter
rather than introducing a single-purpose `unreadOnly` flag, so the same
parameter also expresses "only messages I have read".

Filtering for unread excludes outgoing messages. Read receipts populate a
read date on messages you sent, so without that guard "unread" would also
report your own messages the recipient has not read yet.

Read status is derived from Madrid's `Message.readAt`, which is the read
timestamp `chat.db` records rather than a dedicated unread flag. A message
marked read without a recorded timestamp therefore counts as unread, so
this is a superset of genuinely-unread and may not match the Messages app
badge exactly.

Known limitation: `messages_fetch` fetches `max(limit, 1024)` candidates
and filters them down to `limit`, so the filter only sees the most recent
1024 messages. A large *read* backlog can push unread messages outside
that window. Filtering in SQL instead would remove the limitation, but
Madrid has no read-status predicate to compose; mattt/Madrid#17 proposes
one. The tool's schema would not change if the filtering later moves into
the query.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: mattt <mattt@me.com>
"Copy server command to clipboard" silently copies with no feedback,
so there's no way to tell the click registered. Play the existing
system Pop sound cue on copy, matching the confirmation pattern
already used elsewhere in the app.


Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
VoiceOver announced each service's icon and name as two separate,
unlabeled elements with no indication of on/off state. Add an
accessibility label and value so each toggle reads as e.g.
"Calendar, Enabled".


Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Removing a single trusted client required a right-click context menu
or select-then-Delete, neither of which is discoverable, while bulk
removal already had a visible "Remove All" button. Add a visible
per-row remove button alongside the existing context menu.


Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The Trusted Clients section didn't say how entries get there, that
trusted clients connect silently apart from a notification, or that
trust is keyed to the client's self-reported name. Spell all three out:
an actionable empty state, a fuller section caption, and a footnote on
name-based identity.


Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Registers the app as a login item via SMAppService with a toggle in
Settings > General. State is re-read on appear so changes made in
System Settings > Login Items stay in sync, and the toggle reverts if
registration fails.


Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fix Settings window intermittently not appearing

iMCP is an accessory app (LSUIElement = YES, no Dock icon). SwiftUI's
openSettings() routes through a responder-chain action that only
reliably lands when the app is already frontmost, which is ambiguous
right as the custom menu bar popover dismisses — causing the window
to randomly fail to open. Every other window-showing action in this
file (About iMCP, connection approval) already works around this by
calling NSApp.activate(ignoringOtherApps: true) first; Settings was
the one path missing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

* Trim comment to one line

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Clang module files record the mtime of the SDK headers they were built
from. When the runner image updates its Xcode, a restored
ModuleCache.noindex is rejected wholesale ("has been modified since the
module file was built"), which is what broke main after mattt#188 landed.
Keep caching DerivedData and the SwiftPM cache, but let the module cache
rebuild; it is cheap.
The dialog said "This will give the client access to enabled
services" without naming them, so users had no way to know what
they were actually granting access to. Pass the currently enabled
service names through to the dialog and list them explicitly.


Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: mattt <mattt@me.com>
MenuBarExtra's window-style panel keeps its stale taller frame when
the Services section collapses, leaving the compact content vertically
centered in an oversized translucent panel — and reopening the menu
doesn't fix it, since the panel is only sized at first presentation.

Track the content's measured height and resize the panel window to
match, both on content changes and each time the menu is presented.
The window is looked up by class name directly because
MenuBarExtraAccess's introspection fails to find it on macOS 26.
The collapse animation is removed so the frame can't lag the content,
and the content is top-aligned as a fallback.


Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add disabled-tools state and enforce it in the MCP server

A JSON-encoded set of disabled tool names is persisted the same way as
trusted clients and pushed into ServerNetworkManager the same way as
service bindings. ListTools omits disabled tools and CallTool rejects
them, so a client holding a stale list still cannot invoke one.
Connected clients get tools/listChanged on every change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

* Guard disabled-tools pushes against out-of-order delivery

Each write to ServerController.disabledTools fired an unstructured Task
carrying a snapshot to the actor. Two rapid writes could reach the
actor out of order, and the actor's equality guard would accept the
stale snapshot, leaving live ListTools/CallTool enforcement diverged
from the persisted value until the next toggle. A monotonic generation
counter is now attached to each push; the actor discards any delivery
whose generation is not newer than the last one it applied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

* Add Services section to Settings with per-tool toggles

Each service lists its tools with human-readable titles, descriptions,
and a Read-only badge from the tool annotations, plus a switch per tool
backed by the disabled-tools set. The service master toggle is the same
binding the menu uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

* Fix accessibility and initial paint of service tool toggles

Two Important findings from the Task 2 review, both in
App/Views/ServicesSettingsView.swift:

1. `.accessibilityElement(children: .combine)` on each tool row merged
   the label and the interactive Toggle into a single non-actionable AX
   element, so VoiceOver users could not flip a tool's switch. Removed
   the combine modifier and gave both the per-tool and per-service
   Toggle real accessibility labels (tool title / service name) with
   `.labelsHidden()` to keep the visual layout unchanged while exposing
   a named, actionable control to assistive technology.

2. Freshly rendered/scrolled switch rows could transiently paint "off"
   while the persisted value was "on" (view identity was reused across
   rows with different underlying state). Per the controller's ruling,
   mitigated by keying each switch's view identity to its current value
   via `.id("\(name)-\(currentValue)")`, so a newly materialized row
   constructs its switch with the right state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

* List each service's tools in its menu row tooltip

Hovering a service row now shows what enabling it exposes, using the
tool annotation titles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

* Route Settings service toggles through activation and client notification

The Settings service toggle (ServicesSettingsView.serviceBinding) wrote
config.binding.wrappedValue directly, bypassing the activation flow that
the menu path (ServiceToggleView) already performs. Enabling a service
like Calendar from Settings never triggered the macOS permission prompt,
and there was no revert-to-off if activation failed or was denied.

It also never pushed the updated bindings to ServerNetworkManager, so
connected MCP clients were not notified via tools/listChanged when a
service was toggled from Settings (only the menu scene's ContentView
.onChange did this).

Add ServerController.setService(_:enabled:), which updates the binding,
calls config.service.activate() when enabling an unactivated service
(reverting on failure), and pushes currentServiceBindings to the network
manager so notifyToolListChanged fires for connected clients. Route
ServicesSettingsView.serviceBinding's setter through it, removing the
now-redundant manual objectWillChange.send() from the view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLDiNzrDds62dnD9NoCEJV

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: mattt <mattt@me.com>
* Add Call History service for phone call log access

Adds a new MCP service that reads the macOS CallHistory database
(synced from iPhone) at ~/Library/Application Support/CallHistoryDB/
CallHistory.storedata. Exposes a callhistory_fetch tool with filtering
by participant, date range, and call type (incoming/outgoing/missed).

Follows the same patterns as the existing Messages service: direct
SQLite access, security-scoped bookmark persistence, and file picker
fallback for sandboxed environments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix CallHistory SQL schema and CLI graceful shutdown

- CallHistory: Use ZADDRESS and ZNAME columns directly from ZCALLRECORD
  instead of JOINing on a non-existent ZHANDLE foreign key. The macOS
  CallHistory database stores phone numbers inline, not via a handle table.
- CallHistory: Include contact name in results when available.
- CLI: Add successTerminationBehavior: .gracefullyShutdownGroup to prevent
  ServiceGroup from crashing with a fatal error on normal disconnect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Make call_type an enum in the schema

* Fix formatting for swift-format lint

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: mattt <mattt@me.com>
The private half of the previous key was not available,
so update checks are signed with a new pair.
No shipped build contains Sparkle yet,
so nothing in the field verifies against the old key.
…tt#205)

* Automate releases with GitHub Actions and a Sparkle appcast step

Scripts/release.sh gains an appcast command
that signs the release zip with the Sparkle EdDSA key,
generates appcast.xml,
and uploads it next to the release asset,
refusing to continue when the app's SUPublicEDKey
doesn't match the signing key.
notarytool can now take an App Store Connect API key
instead of a keychain profile,
uploads and release creation honor DRY_RUN,
and the release zip is built after stapling
so the shipped bundle carries the ticket.

The Release workflow runs the same steps on a version tag push,
importing the Developer ID certificate and provisioning profile
from repository secrets.
Manual runs default to a dry run
and attach the build products to the workflow run.

* Publish releases only after every asset is in place

Create the GitHub release as a draft,
upload the zip and the appcast,
then publish it and mark it latest,
so a failure partway through never leaves a half-built release public.
Publishing requires the version tag at HEAD,
and manual workflow runs that publish must start from that tag.
Capture the notarization credential helper's output
so its validation failure stops the script.

* Sign archives with the Developer ID identity and make publishing rerunnable

The project's Release configuration signs with an Apple Development identity,
which a release machine doesn't have,
so archive with the configured Developer ID certificate,
routing the provisioning profile to the app target alone
because the embedded CLI has its own bundle identifier.
Reuse an existing draft release on a rerun
and refuse to touch one that is already published.
Stop the local release flow after the tag push,
since the Release workflow publishes from there.

* Require the release tag on origin before creating a release

Check that the tag has been pushed and matches the local tag,
and pass --verify-tag so gh never mints a tag from the default branch.

* Push only the release tag

Every pushed version tag starts a release,
so pushing all local tags could publish more than one.
A runner image update changes the toolchain
but not the cache key,
so the restored module cache no longer matched
and every build failed until the caches were deleted by hand.
Reading the build number from xcodebuild
makes the key change with the image.
When the MCP client closed stdin, the stdin handler returned without an error and MCPService browsed Bonjour again immediately, so each new session died at once and the helper spun until it was killed. stdin EOF now fails the session, a clean return terminates the helper so the client can relaunch it, and the connection is cancelled before the task group unwinds so a pending receive can't keep it alive. The menubar app registers the approval handler before advertising the service, and StdioProxy and MCPService move out of the CLI entry point.
Wire up Sparkle so the app actually checks for updates
…ist by the user's default order

Flip contactType on a mutable copy instead of re-implementing the mapping,
so company-toggled cards keep URLs, birthday, social profiles, IM, and relations.
Cards with only an organization name stay excluded.
Route contacts_me through the same helper.
Follow-up to mattt#206.
Every occurrence of a recurring event shares one identifier,
and event(withIdentifier:) resolves it to the first occurrence,
so thisEvent could remove the wrong one
and futureEvents could remove the whole series.
An optional start date now refetches the exact occurrence,
recurring events without one are rejected,
and an unknown span throws instead of defaulting to thisEvent.
Follow-up to mattt#209.
* Rename Call History service to Phone

* Use green for the Phone service to match the app icon

* Add a phone_call tool that dials a number via tel: URL

* Model the phone_call result as a CommunicateAction type

* Move call history SQLite access into CallHistoryDatabase and CallRecord types

* Validate phone tool arguments and request database access only on fetch

* Incorporate review feedback

* Reject * and # in phone_call numbers since the Phone app will not dial them
mattt and others added 27 commits September 17, 2026 10:00
…rectly (mattt#219)

A second client's request replaced the first window's reference, leaving the first dialog stuck on screen and its client marked pending forever.
mattt#198)

The service asked for chat.db alone. Messages keeps that database in WAL
mode: every new message is committed to chat.db-wal first and only reaches
chat.db when SQLite checkpoints the log, every few MB of writes. With a
grant on the single file the log was unreadable, so Madrid opened the
database with immutable=1 and messages_fetch could not see anything newer
than the last checkpoint, often hours behind, then everything at once.

The file picker now asks for ~/Library/Messages (directories only,
constrained to a folder named Messages) and the bookmark covers the
folder, which lets SQLite read chat.db together with its -wal and -shm
companions (Madrid's Database.AccessMode.live). The security scope now
stays open until the fetch is done, since SQLite opens the log lazily.

A bookmark stored by an earlier version (chat.db alone) keeps working in
immutable mode; the first fetch of a launch offers to re-select the folder,
and declining is remembered for that launch.


Claude-Session: https://claude.ai/code/session_0187kUP2mjhwMfJkwRbP5DG7

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…e-ahead log (mattt#220)

* Grant the CallHistoryDB folder so phone_calls_fetch can read the write-ahead log

The store is a WAL-mode database, and a grant on the file alone leaves its -wal and -shm companions unreadable, so every read failed with "authorization denied".

* Trim the call history README note
Dismissing the folder picker threw invalidFileSelected. During a Messages
upgrade from a file grant, that error reached the service toggle and turned
Messages off, although the old grant still worked and the alert's Cancel
button already kept it. The picker now returns nil on dismissal; Messages
keeps the old grant during an upgrade and otherwise reports
userDeclinedAccess, and Phone reports the same instead of a wrong selection.

The once-per-launch warning about a file-only Messages grant was a plain
check-and-set on a shared flag that concurrent tool calls could race.
It is now guarded by a lock, with the log call outside it.
The SDK ends its message loop when a client disconnects but leaves the socket open until the health poll detects it. Await loop completion to release the connection immediately, and guard against repeated stops during app shutdown.
* Request service permissions from tool handlers

* Share location authorization across concurrent requests
)

* Replace MenuBarExtraAccess with a local menu panel controller

The app used the package only to dismiss the menu and to resize its panel.
A small internal controller now gets the panel window from an embedded
NSView, orders it out for menu commands, and resizes it to fit its content.

* Dismiss the menu with SwiftUI's dismiss action

Ordering the panel window out hides it while MenuBarExtra still considers
the status item presented, so the next click on the icon does nothing.
The menu buttons now call the dismiss action from the environment.

* Shorten the menu dismissal comment
* Keep iMCP running when its menu bar item is removed

* Reflow updater comment with semantic line breaks

* Open Settings on launch when the menu bar item is hidden
Also points the Ontology, Madrid, and JSONSchema dependencies at their mattt/ repositories instead of the loopwork-ai redirects. Re-resolving JSONSchema at its new location picks up 1.3.1, a patch release within the existing range.
Resolve conflicts with the local fork changes:
- LAN mode: keep the allowLANConnections toggle; loopback-only mode now
  also sets upstream's requiredLocalEndpoint (mattt#242). Before the listener
  starts, the setting replaces the idle listener instead of starting it.
- Settings: keep the Network Access section alongside upstream's
  menu bar / launch-at-login settings.
- release.sh: keep the local `install` command next to the new appcast
  and publish commands.
- project.pbxproj: keep personal team and bundle ID, take upstream
  version 1.5.4 (build 16).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAFz1WodA51dwyzn4eyjbc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAFz1WodA51dwyzn4eyjbc
@rvanderfeer
rvanderfeer marked this pull request as ready for review September 24, 2026 19:41
@rvanderfeer
rvanderfeer merged commit 23480f0 into main Sep 24, 2026
1 check passed
@rvanderfeer
rvanderfeer deleted the claude/gifted-pascal-q4r0s6 branch September 24, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants