From 62f0a4a4201c8349033ba4797481d83609610a2b Mon Sep 17 00:00:00 2001 From: sheehanmunim Date: Wed, 19 Aug 2026 15:22:09 -0400 Subject: [PATCH] feat(brand): MT identifiers for the desktop bridge The native messaging host is com.munim.mtcode.desktop and the MCP server is mt-desktop, defined once in contracts so every adapter and the prompt text follow it. Nothing breaks in the middle: the extension tries the new host id and falls back to the old one, and the installers register both manifests (and both registry keys on Windows), so an extension that has not reloaded and a machine that has not re-run the installer both keep working. Wire names that would break installed state stay put - the @t3tools packages, the T3CODE_* environment variables, and the t3-desktop-mcp binary path the native-host wrapper already points at. Co-Authored-By: Claude Opus 5 (1M context) --- apps/desktop/src/computerUse/permissions.ts | 34 +++++++++----- .../provider/CodexDeveloperInstructions.ts | 3 +- .../src/provider/Layers/ClaudeAdapter.ts | 3 +- .../src/provider/Layers/CodexAdapter.ts | 5 +- .../Layers/CodexSessionRuntime.test.ts | 15 +++--- .../provider/Layers/CodexSessionRuntime.ts | 3 +- .../src/provider/Layers/CursorAdapter.ts | 3 +- .../server/src/provider/Layers/GrokAdapter.ts | 3 +- native/t3-chrome-extension/background.js | 22 ++++++--- native/t3-chrome-extension/install.ps1 | 46 +++++++++++-------- native/t3-chrome-extension/install.sh | 12 +++-- native/t3-desktop-mcp-rs/src/main.rs | 2 +- native/t3-desktop-mcp/Sources/main.swift | 2 +- packages/contracts/src/settings.ts | 13 ++++-- 14 files changed, 109 insertions(+), 57 deletions(-) diff --git a/apps/desktop/src/computerUse/permissions.ts b/apps/desktop/src/computerUse/permissions.ts index d61db27e23a5..ae9fe47e2245 100644 --- a/apps/desktop/src/computerUse/permissions.ts +++ b/apps/desktop/src/computerUse/permissions.ts @@ -141,25 +141,35 @@ function chromeExtensionInstalledInPreferences(preferencesPath: string): boolean } } +/** + * Host manifest names, newest first. Installs made before the rename carry + * only the old one, and the installer writes both, so either counts as + * registered. + */ +const NATIVE_HOST_MANIFEST_NAMES = [ + "com.munim.mtcode.desktop.json", + "com.t3tools.t3code.desktop.json", +] as const; + +function anyHostManifest(directory: string): boolean { + return NATIVE_HOST_MANIFEST_NAMES.some((name) => { + try { + return NodeFS.statSync(NodePath.join(directory, name)).isFile(); + } catch { + return false; + } + }); +} + function nativeHostRegistered(root: string): boolean { - const hostPath = NodePath.join(root, "NativeMessagingHosts", "com.t3tools.t3code.desktop.json"); - try { - return NodeFS.statSync(hostPath).isFile(); - } catch { - return false; - } + return anyHostManifest(NodePath.join(root, "NativeMessagingHosts")); } /** Windows registers the host via the registry + a support-dir manifest. */ function nativeHostRegisteredWindows(): boolean { const local = process.env.LOCALAPPDATA; if (!local) return false; - const hostPath = NodePath.join(local, "t3-desktop-mcp", "com.t3tools.t3code.desktop.json"); - try { - return NodeFS.statSync(hostPath).isFile(); - } catch { - return false; - } + return anyHostManifest(NodePath.join(local, "t3-desktop-mcp")); } function resolveChromeExtensionStatus(): { diff --git a/apps/server/src/provider/CodexDeveloperInstructions.ts b/apps/server/src/provider/CodexDeveloperInstructions.ts index 4c9fd543079c..ee8a29946581 100644 --- a/apps/server/src/provider/CodexDeveloperInstructions.ts +++ b/apps/server/src/provider/CodexDeveloperInstructions.ts @@ -1,6 +1,7 @@ import type { ProviderInteractionMode } from "@t3tools/contracts"; import { resolveAppDisplayName } from "../appDisplayName.ts"; +import { DESKTOP_MCP_SERVER_NAME } from "@t3tools/contracts"; const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = ` @@ -43,7 +44,7 @@ const T3_CODE_DESKTOP_TOOL_INSTRUCTIONS = ` ## T3 Code Computer Use -The \`t3-desktop\` MCP server drives this computer's GUI. A pointer overlay shows where you click and type; it does not move the user's mouse. +The \`${DESKTOP_MCP_SERVER_NAME}\` MCP server drives this computer's GUI. A pointer overlay shows where you click and type; it does not move the user's mouse. Prefer these tools for anything on screen: \`list_apps\`, \`get_app_state\`, \`click\`, \`type_text\`, \`press_key\`, \`screenshot\`, and the \`browser_*\` tools for Chrome tabs you own. diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 6b3ac05293e8..f67d938c513b 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -52,6 +52,7 @@ import { ThreadId, TurnId, type UserInputQuestion, + DESKTOP_MCP_SERVER_NAME, } from "@t3tools/contracts"; import { applyClaudePromptEffortPrefix, @@ -4786,7 +4787,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( : {}), ...(desktopMcp ? { - "t3-desktop": { + [DESKTOP_MCP_SERVER_NAME]: { type: "stdio" as const, command: desktopMcp.path, ...(desktopMcp.env.length > 0 diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 6d4782863ce6..bc9128ae5aa4 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -28,6 +28,7 @@ import { ThreadId, type TurnId, ProviderSendTurnInput, + DESKTOP_MCP_SERVER_NAME, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Crypto from "effect/Crypto"; @@ -1891,11 +1892,11 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( .replace(/\r/g, "\\r") .replace(/\t/g, "\\t")}"`; }; - appServerArgs.push("-c", `mcp_servers.t3-desktop.command=${quoteToml(desktopMcp.path)}`); + appServerArgs.push("-c", `mcp_servers.${DESKTOP_MCP_SERVER_NAME}.command=${quoteToml(desktopMcp.path)}`); for (const entry of desktopMcp.env) { appServerArgs.push( "-c", - `mcp_servers.t3-desktop.env.${entry.name}=${quoteToml(entry.value)}`, + `mcp_servers.${DESKTOP_MCP_SERVER_NAME}.env.${entry.name}=${quoteToml(entry.value)}`, ); } } diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 8d5d6a8ee298..cfb509df0c36 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -4,7 +4,7 @@ import { it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import { describe } from "vite-plus/test"; -import { DEFAULT_MODEL, ThreadId } from "@t3tools/contracts"; +import { DEFAULT_MODEL, DESKTOP_MCP_SERVER_NAME, ThreadId } from "@t3tools/contracts"; import * as CodexErrors from "effect-codex-app-server/errors"; import * as CodexRpc from "effect-codex-app-server/rpc"; import * as EffectCodexSchema from "effect-codex-app-server/schema"; @@ -534,14 +534,17 @@ describe("T3 browser developer instructions", () => { ); }); - it("describes Computer Use pointer tools only when t3-desktop is attached", () => { + it("describes Computer Use pointer tools only when the desktop MCP is attached", () => { const withDesktop = codexDefaultModeDeveloperInstructions(false, { desktopToolsAvailable: true, }); - NodeAssert.match(withDesktop, /t3-desktop/); + NodeAssert.match(withDesktop, new RegExp(DESKTOP_MCP_SERVER_NAME)); NodeAssert.match(withDesktop, /pointer overlay/); NodeAssert.match(withDesktop, /get_app_state/); - NodeAssert.doesNotMatch(codexDefaultModeDeveloperInstructions(false), /t3-desktop/); + NodeAssert.doesNotMatch( + codexDefaultModeDeveloperInstructions(false), + new RegExp(DESKTOP_MCP_SERVER_NAME), + ); }); it("marks a home-directory thread as a whole-computer session", () => { @@ -567,8 +570,8 @@ describe("hasConfiguredMcpServer", () => { }); it("matches a named MCP server without treating a sibling as present", () => { - const args = ["-c", "mcp_servers.t3-desktop.command='/usr/bin/t3-desktop-mcp'"]; - NodeAssert.equal(hasConfiguredMcpServerNamed(args, "t3-desktop"), true); + const args = ["-c", `mcp_servers.${DESKTOP_MCP_SERVER_NAME}.command='/usr/bin/t3-desktop-mcp'`]; + NodeAssert.equal(hasConfiguredMcpServerNamed(args, DESKTOP_MCP_SERVER_NAME), true); NodeAssert.equal(hasConfiguredMcpServerNamed(args, "t3-code"), false); }); }); diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index d8112f34e3c6..81337615223b 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -15,6 +15,7 @@ import { RuntimeMode, ThreadId, TurnId, + DESKTOP_MCP_SERVER_NAME, } from "@t3tools/contracts"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { normalizeModelSlug } from "@t3tools/shared/model"; @@ -2195,7 +2196,7 @@ export const makeCodexSessionRuntime = ( // setting, so the prompt describes the tools this turn actually // has even if the setting changed after the session started. browserToolsAvailable: hasConfiguredMcpServerNamed(options.appServerArgs, "t3-code"), - desktopToolsAvailable: hasConfiguredMcpServerNamed(options.appServerArgs, "t3-desktop"), + desktopToolsAvailable: hasConfiguredMcpServerNamed(options.appServerArgs, DESKTOP_MCP_SERVER_NAME), computerHomeWorkspace: isComputerHomeCwd(options.cwd), }); const rawResponse = yield* client.raw.request("turn/start", params); diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts index db6457cc6abf..8bc51955486d 100644 --- a/apps/server/src/provider/Layers/CursorAdapter.ts +++ b/apps/server/src/provider/Layers/CursorAdapter.ts @@ -23,6 +23,7 @@ import { type RuntimeMode, type ThreadId, TurnId, + DESKTOP_MCP_SERVER_NAME, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Crypto from "effect/Crypto"; @@ -582,7 +583,7 @@ export function makeCursorAdapter( ...(desktopMcp ? [ { - name: "t3-desktop", + name: DESKTOP_MCP_SERVER_NAME, command: desktopMcp.path, args: [] as string[], env: [...desktopMcp.env], diff --git a/apps/server/src/provider/Layers/GrokAdapter.ts b/apps/server/src/provider/Layers/GrokAdapter.ts index 47b4a139780e..c76dfa3def36 100644 --- a/apps/server/src/provider/Layers/GrokAdapter.ts +++ b/apps/server/src/provider/Layers/GrokAdapter.ts @@ -12,6 +12,7 @@ import { RuntimeRequestId, type ThreadId, TurnId, + DESKTOP_MCP_SERVER_NAME, } from "@t3tools/contracts"; import * as Clock from "effect/Clock"; import * as Crypto from "effect/Crypto"; @@ -897,7 +898,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte ...(desktopMcp ? [ { - name: "t3-desktop", + name: DESKTOP_MCP_SERVER_NAME, command: desktopMcp.path, args: [] as string[], env: [...desktopMcp.env], diff --git a/native/t3-chrome-extension/background.js b/native/t3-chrome-extension/background.js index 6123e426d32b..5184298b0edf 100644 --- a/native/t3-chrome-extension/background.js +++ b/native/t3-chrome-extension/background.js @@ -10,7 +10,11 @@ // Commands arrive from the desktop app over native messaging; every reply // carries the originating request id. -const HOST = "com.t3tools.t3code.desktop"; +const HOST = "com.munim.mtcode.desktop"; +// Installs made before the rename registered the host under its old name. +// Chrome rejects an unknown host outright, so try the previous id second +// rather than leaving those browsers unable to reach the desktop at all. +const LEGACY_HOST = "com.t3tools.t3code.desktop"; const GROUP_TITLE = "MT Code"; const OWNED_STATE_KEY = "ownedState"; @@ -86,12 +90,18 @@ function ensureStateReady() { function connect() { if (port) return; - try { - port = chrome.runtime.connectNative(HOST); - } catch { - port = null; - return; + // Chrome throws for a host id it has no manifest for, so try the current + // name first and fall back to the pre-rename one. Installs that still carry + // only the old manifest keep working until they run the installer again. + for (const host of [HOST, LEGACY_HOST]) { + try { + port = chrome.runtime.connectNative(host); + break; + } catch { + port = null; + } } + if (!port) return; const sessionPort = port; connectedAt = Date.now(); hadLiveSession = false; diff --git a/native/t3-chrome-extension/install.ps1 b/native/t3-chrome-extension/install.ps1 index 073421c6ccb3..5f9111779feb 100644 --- a/native/t3-chrome-extension/install.ps1 +++ b/native/t3-chrome-extension/install.ps1 @@ -11,7 +11,10 @@ $ErrorActionPreference = 'Stop' $ExtensionId = 'kgdolgnijopbghhomnblabjkmjhnoage' -$HostName = 'com.t3tools.t3code.desktop' +$HostName = 'com.munim.mtcode.desktop' +# Extensions that have not reloaded since the rename still ask for the old id, +# and Chrome refuses a host it has no manifest for. Register both names. +$LegacyHostName = 'com.t3tools.t3code.desktop' $here = Split-Path -Parent $MyInvocation.MyCommand.Path $binary = $env:T3CODE_DESKTOP_MCP_PATH @@ -37,30 +40,37 @@ $utf8NoBom = New-Object System.Text.UTF8Encoding $false $utf8NoBom ) -$manifestPath = Join-Path $support "$HostName.json" -$manifest = [ordered]@{ - name = $HostName - description = 'MT Code desktop control bridge' - path = $wrapper - type = 'stdio' - allowed_origins = @("chrome-extension://$ExtensionId/") +$manifestPaths = @{} +foreach ($name in @($HostName, $LegacyHostName)) { + $path = Join-Path $support "$name.json" + $manifest = [ordered]@{ + name = $name + description = 'MT Code desktop control bridge' + path = $wrapper + type = 'stdio' + allowed_origins = @("chrome-extension://$ExtensionId/") + } + # Chrome rejects native-host manifests with a UTF-8 BOM (PowerShell's UTF8 + # encoding inserts one). Write UTF-8 without BOM explicitly. + [System.IO.File]::WriteAllText( + $path, + ($manifest | ConvertTo-Json -Depth 4), + $utf8NoBom + ) + $manifestPaths[$name] = $path } -# Chrome rejects native-host manifests with a UTF-8 BOM (PowerShell's UTF8 -# encoding inserts one). Write UTF-8 without BOM explicitly. -[System.IO.File]::WriteAllText( - $manifestPath, - ($manifest | ConvertTo-Json -Depth 4), - $utf8NoBom -) +$manifestPath = $manifestPaths[$HostName] # Chrome and Chromium read separate registry trees; register wherever the # browser is actually installed. $installed = 0 foreach ($vendor in @('Google\Chrome', 'Google\Chrome Beta', 'Chromium')) { - $key = "HKCU:\Software\$vendor\NativeMessagingHosts\$HostName" try { - New-Item -Path $key -Force | Out-Null - Set-ItemProperty -Path $key -Name '(default)' -Value $manifestPath + foreach ($name in @($HostName, $LegacyHostName)) { + $key = "HKCU:\Software\$vendor\NativeMessagingHosts\$name" + New-Item -Path $key -Force | Out-Null + Set-ItemProperty -Path $key -Name '(default)' -Value $manifestPaths[$name] + } Write-Host "registered host in: HKCU\Software\$vendor" $installed++ } catch { diff --git a/native/t3-chrome-extension/install.sh b/native/t3-chrome-extension/install.sh index 1caaa28471ec..10862652e567 100755 --- a/native/t3-chrome-extension/install.sh +++ b/native/t3-chrome-extension/install.sh @@ -8,7 +8,11 @@ set -eu EXTENSION_ID="kgdolgnijopbghhomnblabjkmjhnoage" -HOST_NAME="com.t3tools.t3code.desktop" +HOST_NAME="com.munim.mtcode.desktop" +# Extensions that have not reloaded since the rename still ask for the old id, +# and Chrome refuses a host it has no manifest for. Both names are registered +# so neither side has to be updated first. +LEGACY_HOST_NAME="com.t3tools.t3code.desktop" here=$(cd "$(dirname "$0")" && pwd) # macOS builds the Swift package; Linux builds the Rust crate that also covers @@ -60,15 +64,17 @@ for profile in "$@"; do [ -d "$profile" ] || continue dir="$profile/NativeMessagingHosts" mkdir -p "$dir" - cat > "$dir/$HOST_NAME.json" < "$dir/$host_name.json" <