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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Notable changes to Desktop Tools, newest first. Each release's GitHub release no

## [Unreleased]

### Added

- The app's MCP server takes `--name <app>`, the app its instructions and rules name, and by default names the app it ships in, so the development app's server says **Desktop Tools Dev**. Pointed at `Desktop Tools Dev.app/Contents/MacOS/desktop-tools-mcp`, an agent uses the development app without Node; `npm run dev-app` prints that setup. Run outside an app with no host serving, the server now fails with `HOST_NOT_RUNNING` and the socket it tried. ([#237](https://github.com/githubnext/desktop-tools/issues/237))

### Changed

- Tool descriptions, schemas and the rules they share are declared in Swift, in `native2/Sources/DesktopProtocol/ContractTools.swift`, which the app's MCP server serves. `@githubnext/desktop-tools/tools` and the key and wait-condition lists in `/protocol` are generated from those declarations (`npm run contract`) and export the same values as before. `desktopRequest` no longer checks the exclusive argument forms of `desktop_clipboard_write` and `desktop_dialog_answer` itself: the host refuses them with the same text. `DESKTOP_EXPECT_OPS` lists its ops in tool order. ([#237](https://github.com/githubnext/desktop-tools/issues/237))
Expand Down
5 changes: 4 additions & 1 deletion dist/dev-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
console.log(devApp);
if (!values["no-open"]) {
const state = launch();
console.log(`Serving ${join(state, "desktop.sock")}. Use it with: DESKTOP_TOOLS_STATE=${state} node dist/mcp.js`);
const server = join(devApp, "Contents", "MacOS", "desktop-tools-mcp");
// The app's own server finds its state folder from its bundle ID; only a custom one has to be passed on.
const env = process.env.DESKTOP_TOOLS_STATE ? `-e DESKTOP_TOOLS_STATE=${state} ` : "";
console.log(`Serving ${join(state, "desktop.sock")}. Add its MCP server with: claude mcp add desktop-dev ${env}-- "${server}"`);
}
}
}
4 changes: 2 additions & 2 deletions docs/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ node dist/app.js --out build --identity "Developer ID Application: … (TEAM)" -

## Development

Run development builds with `npm run dev-app`, described in [Install and build](build.md#development-app). It installs one `Desktop Tools Dev.app` (`dev.githubnext.desktop-tools.dev`) at a fixed path and serves `~/.local/state/desktop-tools-dev/desktop.sock`, so it never relinks the installed app's `~/.local/state/desktop-tools/desktop-client` or takes its socket. Point the MCP server at it:
Run development builds with `npm run dev-app`, described in [Install and build](build.md#development-app). It installs one `Desktop Tools Dev.app` (`dev.githubnext.desktop-tools.dev`) at a fixed path and serves `~/.local/state/desktop-tools-dev/desktop.sock`, so it never relinks the installed app's `~/.local/state/desktop-tools/desktop-client` or takes its socket. Point an agent at its own MCP server, which needs no Node:

```sh
DESKTOP_TOOLS_STATE=~/.local/state/desktop-tools-dev node dist/mcp.js
claude mcp add desktop-dev -- "$HOME/Library/Application Support/desktop-tools/dev/Desktop Tools Dev.app/Contents/MacOS/desktop-tools-mcp"
```
2 changes: 1 addition & 1 deletion docs/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ npm run dev-app # build, sign, install and open
npm run dev-app -- --stop # quit it
```

The app bundles the Swift MCP server, `Desktop Tools Dev.app/Contents/MacOS/desktop-tools-mcp`, which finds the app's socket and opens the app in the background when nothing serves it. `DESKTOP_TOOLS_STATE=~/.local/state/desktop-tools-dev node dist/mcp.js` hands over to it.
The app bundles the Swift MCP server, `Desktop Tools Dev.app/Contents/MacOS/desktop-tools-mcp`, which finds the app's socket and opens the app in the background when nothing serves it. It names itself **Desktop Tools Dev** in its instructions, and needs no Node: `npm run dev-app` prints the `claude mcp add` line for it.

Every checkout and worktree installs the same `~/Library/Application Support/desktop-tools/dev/Desktop Tools Dev.app`. It has its own bundle identity, `dev.githubnext.desktop-tools.dev`, nested under the release app's, and the name **Desktop Tools Dev**. Building quits the running copy, replaces the bundle in place and opens it again through LaunchServices with `open`. Launching its executable directly can attribute permission checks to the launching terminal or agent instead of the app.

Expand Down
2 changes: 2 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The [README](https://github.com/githubnext/desktop-tools#set-up-for-an-agent) ha

The app bundles its own MCP server (since 0.17.0), `Desktop Tools.app/Contents/MacOS/desktop-tools-mcp`, written in Swift. Run with no options, `dist/mcp.js` finds it beside the client the app links and hands stdio over to it, so an `npx` setup needs no change. Any option keeps the Node server, which forwards each call through the host's `desktop-tools-client` as [`createDesktop`](node.md) does; `--wire 2` is accepted for that, and `--wire 1`, the removed Peekaboo stack, is refused. An app from before 0.17.0 has no server of its own and its client speaks the Peekaboo stack's wire, which the Node server no longer does, so every call fails, ending with both versions when they differ: updating the `npx` tag doesn't update the app.

The app's server needs no Node. It serves the socket in its app's state folder (`DESKTOP_TOOLS_STATE`, else the folder its bundle ID chooses), or `--socket <path>`, and names its app in the instructions and rules: the app it ships in, or `--name <app>`. So the [development app](build.md#development-app)'s server, `Desktop Tools Dev.app/Contents/MacOS/desktop-tools-mcp`, serves `~/.local/state/desktop-tools-dev` as **Desktop Tools Dev** with no options.

To use another signed host's grants instead, pass its client, socket, and name. The host must embed the Swift runtime from `native2/`; one that still embeds the Peekaboo stack's can't be served. For Ace Canary:

```sh
Expand Down
2 changes: 1 addition & 1 deletion native2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The desktop tools' only native code: a Swift MCP server, host and embedding runt
| `DesktopHost` | The tools, the snapshot store, the socket server, admission (`Admission.swift`: approved callers, pause, own-UI guard) and the runtime hooks |
| `desktop-tools-host` | A standalone host, run from a terminal for development; it uses the terminal's macOS grants |
| `desktop-tools-app` | The Desktop Tools menu bar app, which loads `libDesktopTools.dylib` |
| `desktop-tools-mcp` | The stdio MCP server, which forwards each call to the host |
| `desktop-tools-mcp` | The stdio MCP server, which forwards each call to the host: its app's socket, or `--socket`, naming its app, or `--name`, in the instructions. `--contract` prints the tool declarations instead |
| `desktop-tools-client` | One tool call per run, or every call over one connection with `--serve`, for `createDesktop` and `dist/mcp.js` with options (docs/node.md) |
| `DesktopTools` | `libDesktopTools.dylib`, the menu bar app's embedding runtime with the C ABI in docs/embed.md: it serves the host's `Server` and draws the agent cursor, window outline and activity list through `DesktopHost/Hooks.swift` |

Expand Down
22 changes: 11 additions & 11 deletions native2/Sources/desktop-tools-mcp/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ if arguments.contains("--contract") {
}
/// The app this server ships in, whose host it serves through, and opens in the background when nothing serves.
let app = HostApp.containing()
let socketPath = arguments.firstIndex(of: "--socket").flatMap { arguments.dropFirst($0 - arguments.startIndex + 1).first }
/// The value after `option`, as in `--socket <path>`.
func option(_ name: String) -> String? {
arguments.firstIndex(of: name).flatMap { arguments.dropFirst($0 - arguments.startIndex + 1).first }
}
let socketPath = option("--socket")
?? ((app?.stateFolder ?? defaultStateFolder()) as NSString).appendingPathComponent("desktop.sock")
/// The app named in the instructions and rules, whose menu pauses input: --name, else the app this server ships in.
let appName = option("--name") ?? app?.name ?? "Desktop Tools"
// The package version the app was built from; a server outside an app has none.
let serverVersion = app.flatMap { Bundle(url: $0.url)?.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String } ?? "0.0.0"

Expand All @@ -31,14 +37,8 @@ func agentName(_ info: JSON?) -> String? {
return String(title.prefix(40))
}

let tools: JSON = .array((Contract.tools.array ?? []).map { tool in
.object([
("name", tool["name"] ?? .null),
("description", tool["description"] ?? .null),
("inputSchema", tool["inputSchema"] ?? .null),
("annotations", tool["annotations"] ?? .null),
])
})
let tools = Contract.listed(place: "this Mac", app: appName)
let instructions = Contract.instructions(app: appName)

let output = NSLock()
func send(_ value: JSON) {
Expand Down Expand Up @@ -206,7 +206,7 @@ func call(_ key: String, tool: String, arguments: JSON) async -> JSON {
)
} catch let error as WireError {
if case .system("connect", _) = error {
return errorResult("The Desktop Tools 2 host isn't serving at \(socketPath). Start it with: native2/.build/debug/desktop-tools-host")
return errorResult("HOST_NOT_RUNNING: Nothing is serving \(socketPath), so nothing ran. This server runs outside an app: start a host with native2/.build/debug/desktop-tools-host, or pass --socket.")
}
// The request may have reached the host, which can stop or restart mid-call. An action may then have taken
// effect, as a desktop_open whose host stopped after Xcode opened the file did (#244), so it is unknown, never
Expand Down Expand Up @@ -247,7 +247,7 @@ func handle(_ message: JSON) async {
("protocolVersion", JSON(versions.contains(requested) ? requested : versions[0])),
("capabilities", .object([("tools", .object([]))])),
("serverInfo", .object([("name", "desktop-tools"), ("version", JSON(serverVersion))])),
("instructions", JSON(Contract.instructions)),
("instructions", JSON(instructions)),
]))]))
case "ping":
send(.object([("jsonrpc", "2.0"), ("id", id), ("result", .object([]))]))
Expand Down
5 changes: 4 additions & 1 deletion src/dev-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
console.log(devApp);
if (!values["no-open"]) {
const state = launch();
console.log(`Serving ${join(state, "desktop.sock")}. Use it with: DESKTOP_TOOLS_STATE=${state} node dist/mcp.js`);
const server = join(devApp, "Contents", "MacOS", "desktop-tools-mcp");
// The app's own server finds its state folder from its bundle ID; only a custom one has to be passed on.
const env = process.env.DESKTOP_TOOLS_STATE ? `-e DESKTOP_TOOLS_STATE=${state} ` : "";
console.log(`Serving ${join(state, "desktop.sock")}. Add its MCP server with: claude mcp add desktop-dev ${env}-- "${server}"`);
}
}
}
Loading