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" <