Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate, NS
func applicationWillFinishLaunching(_ notification: Notification) {
// Prevent focus steal on launch — no Dock icon, no Cmd+Tab entry
NSApp.setActivationPolicy(.prohibited)

// Disable macOS automatic text substitutions app-wide (issue #538).
// Smart-dash substitution rewrites "--" as an em-dash "—", which
// silently corrupts CLI flags typed into server Command/Arguments/Env
// fields (e.g. "--flag" → "—flag"), producing broken configs. Done
// before any window (and thus any NSTextView field editor) is created
// so every text field inherits the disabled state. See
// TextSubstitution.disableAutomaticTextSubstitutions.
TextSubstitution.disableAutomaticTextSubstitutions()
}

func applicationDidFinishLaunching(_ notification: Notification) {
Expand Down
43 changes: 43 additions & 0 deletions native/macos/MCPProxy/MCPProxy/TextSubstitution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// TextSubstitution.swift
// MCPProxy
//
// Disables macOS automatic text substitutions (smart dashes, smart quotes,
// text replacement, spelling autocorrection, autocapitalization) for the whole
// app. See issue #538: smart-dash substitution rewrites "--" as an em-dash
// "—", silently corrupting CLI flags typed into server Command/Arguments/Env
// fields (e.g. "--flag" → "—flag"), producing broken configs.
//
// None of this app's text fields hold prose — they hold commands, flags,
// paths, URLs, KEY=VALUE pairs, names, and tokens — so every automatic
// substitution is unwanted everywhere. SwiftUI exposes no modifier for smart
// dashes (`.autocorrectionDisabled()` does not affect dash substitution), so
// the reliable lever is the AppKit defaults the field editor (NSTextView)
// reads at creation time.

import AppKit

enum TextSubstitution {
/// The automatic-substitution default keys NSTextView consults when a field
/// editor is created. Writing them to the standard (application) defaults
/// domain overrides the user's system-wide NSGlobalDomain setting for this
/// app only.
static let substitutionDefaultsKeys = [
"NSAutomaticDashSubstitutionEnabled",
"NSAutomaticQuoteSubstitutionEnabled",
"NSAutomaticTextReplacementEnabled",
"NSAutomaticSpellingCorrectionEnabled",
"NSAutomaticCapitalizationEnabled",
]

/// Disable all automatic text substitutions app-wide. MUST be called before
/// any window / text field editor is created (e.g. from
/// `applicationWillFinishLaunching`) so every NSTextView inherits the
/// disabled state.
static func disableAutomaticTextSubstitutions(
defaults: UserDefaults = .standard
) {
for key in substitutionDefaultsKeys {
defaults.set(false, forKey: key)
}
}
}
2 changes: 1 addition & 1 deletion native/macos/MCPProxy/MCPProxyTests/ModelsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ final class ModelsTests: XCTestCase {
XCTAssertEqual(stats.tokenMetrics?.totalServerToolListSize, 120000)
XCTAssertEqual(stats.tokenMetrics?.averageQueryResultSize, 5000)
XCTAssertEqual(stats.tokenMetrics?.savedTokens, 115000)
XCTAssertEqual(stats.tokenMetrics?.savedTokensPercentage, 95.83, accuracy: 0.01)
XCTAssertEqual(try XCTUnwrap(stats.tokenMetrics?.savedTokensPercentage), 95.83, accuracy: 0.01)
XCTAssertEqual(stats.tokenMetrics?.perServerToolListSizes?["github"], 80000)
XCTAssertEqual(stats.tokenMetrics?.perServerToolListSizes?["gitlab"], 40000)
}
Expand Down
72 changes: 72 additions & 0 deletions native/macos/MCPProxy/MCPProxyTests/TextSubstitutionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import XCTest
import AppKit
@testable import MCPProxy

/// Regression tests for issue #538 — "Editor autocorrect creates broken
/// configs". macOS smart-dash substitution turns "--" into "—" in the tray's
/// Add Server / config text fields. `TextSubstitution.disableAutomatic...`
/// must turn every automatic substitution off so a freshly created NSTextView
/// field editor (the type SwiftUI TextField/TextEditor use on macOS) has dash
/// substitution disabled.
final class TextSubstitutionTests: XCTestCase {

/// After calling the helper, the substitution defaults are all false.
@MainActor
func test_disableAutomaticTextSubstitutions_setsAllDefaultsFalse() {
let suiteName = "TextSubstitutionTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer { defaults.removePersistentDomain(forName: suiteName) }

// Pre-condition: simulate a machine where the user has smart dashes ON.
for key in TextSubstitution.substitutionDefaultsKeys {
defaults.set(true, forKey: key)
}

TextSubstitution.disableAutomaticTextSubstitutions(defaults: defaults)

for key in TextSubstitution.substitutionDefaultsKeys {
XCTAssertFalse(
defaults.bool(forKey: key),
"\(key) must be disabled after disableAutomaticTextSubstitutions() (issue #538)"
)
}
}

/// The dash-substitution key (the one that produces the em-dash bug) is
/// among the keys the helper disables.
func test_substitutionKeys_includeDashSubstitution() {
XCTAssertTrue(
TextSubstitution.substitutionDefaultsKeys.contains("NSAutomaticDashSubstitutionEnabled"),
"Dash substitution is the key that causes '--' → '—' (issue #538)"
)
}

/// End-to-end mechanism check: with the substitution defaults written to
/// the STANDARD domain (what the app does at launch) BEFORE the field
/// editor exists, a freshly created NSTextView reports dash substitution
/// disabled. This is the property AppKit consults before rewriting "--".
@MainActor
func test_freshNSTextView_hasDashSubstitutionDisabled_afterHelper() {
// Capture and restore the real standard-domain values so the test is
// side-effect free regardless of the host machine's settings.
let keys = TextSubstitution.substitutionDefaultsKeys
let saved = keys.map { ($0, UserDefaults.standard.object(forKey: $0)) }
defer {
for (key, value) in saved {
if let value { UserDefaults.standard.set(value, forKey: key) }
else { UserDefaults.standard.removeObject(forKey: key) }
}
}

// Model the buggy condition, then apply the fix to the standard domain.
UserDefaults.standard.set(true, forKey: "NSAutomaticDashSubstitutionEnabled")
TextSubstitution.disableAutomaticTextSubstitutions()

// A field editor created now must inherit the disabled state.
let textView = NSTextView(frame: .zero)
XCTAssertFalse(
textView.isAutomaticDashSubstitutionEnabled,
"A fresh NSTextView must have smart-dash substitution OFF so '--' is preserved (issue #538)"
)
}
}
Loading