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
34 changes: 22 additions & 12 deletions apps/desktop/src/computerUse/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(): {
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
@@ -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 = `

Expand Down Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
ThreadId,
TurnId,
type UserInputQuestion,
DESKTOP_MCP_SERVER_NAME,
} from "@t3tools/contracts";
import {
applyClaudePromptEffortPrefix,
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)}`,
);
}
}
Expand Down
15 changes: 9 additions & 6 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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);
});
});
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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],
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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],
Expand Down
22 changes: 16 additions & 6 deletions native/t3-chrome-extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand Down
46 changes: 28 additions & 18 deletions native/t3-chrome-extension/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
12 changes: 9 additions & 3 deletions native/t3-chrome-extension/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,15 +64,17 @@ for profile in "$@"; do
[ -d "$profile" ] || continue
dir="$profile/NativeMessagingHosts"
mkdir -p "$dir"
cat > "$dir/$HOST_NAME.json" <<EOF
for host_name in "$HOST_NAME" "$LEGACY_HOST_NAME"; do
cat > "$dir/$host_name.json" <<EOF
{
"name": "$HOST_NAME",
"name": "$host_name",
"description": "MT Code desktop control bridge",
"path": "$wrapper",
"type": "stdio",
"allowed_origins": ["chrome-extension://$EXTENSION_ID/"]
}
EOF
done
echo "registered host in: $profile"
installed=$((installed + 1))
done
Expand Down
2 changes: 1 addition & 1 deletion native/t3-desktop-mcp-rs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use serde_json::{Value, json};
use platform::{Desktop, DesktopError, Point, ScrollDirection};

const PROTOCOL_VERSION: &str = "2024-11-05";
const SERVER_NAME: &str = "t3-desktop";
const SERVER_NAME: &str = "mt-desktop";
const SERVER_VERSION: &str = "0.1.0";

/// Keeps the agent pointer up for the duration of a `tools/call`, then
Expand Down
2 changes: 1 addition & 1 deletion native/t3-desktop-mcp/Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2443,7 +2443,7 @@ while let line = readLine(strippingNewline: true) {
respond(id: id ?? NSNull(), result: [
"protocolVersion": "2024-11-05",
"capabilities": ["tools": ["listChanged": false]],
"serverInfo": ["name": "t3-desktop", "version": "0.1.0"],
"serverInfo": ["name": "mt-desktop", "version": "0.1.0"],
])

case "tools/list":
Expand Down
13 changes: 10 additions & 3 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,9 +771,16 @@ export const BackgroundActivitySettings = Schema.Struct({
}).pipe(Schema.withDecodingDefault(Effect.succeed({})));
export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type;

/** Local desktop / browser computer-use MCP (`t3-desktop`). */
/**
* MCP server name for the local desktop/browser control server. Spawn-time
* only - providers pass it when they launch, so nothing on disk carries the
* old name and renaming it costs nothing but a rebuild.
*/
export const DESKTOP_MCP_SERVER_NAME = "mt-desktop";

/** Local desktop / browser computer-use MCP (`mt-desktop`). */
export const DesktopControlSettings = Schema.Struct({
/** When false, providers do not inject the t3-desktop MCP server. Default on. */
/** When false, providers do not inject the mt-desktop MCP server. Default on. */
enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
/** Show the agent pointer overlay while controlling the desktop. */
agentCursorEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
Expand Down Expand Up @@ -928,7 +935,7 @@ export const ServerSettings = Schema.Struct({
Schema.withDecodingDefault(Effect.succeed({})),
),
observability: ObservabilitySettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
// Local computer-use MCP (`t3-desktop`): agents can drive the desktop and an
// Local computer-use MCP (`mt-desktop`): agents can drive the desktop and an
// agent-owned Chrome tab group. Off disables injection even when the binary
// is present. Sub-flags are passed through to the MCP process as env.
desktopControl: DesktopControlSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
Expand Down
Loading