diff --git a/CHANGELOG.md b/CHANGELOG.md index 813711a..59fd0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `, 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)) diff --git a/dist/dev-app.js b/dist/dev-app.js index be2c9ce..4f20d03 100644 --- a/dist/dev-app.js +++ b/dist/dev-app.js @@ -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}"`); } } } diff --git a/docs/app.md b/docs/app.md index 7ca3676..c0f003e 100644 --- a/docs/app.md +++ b/docs/app.md @@ -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" ``` diff --git a/docs/build.md b/docs/build.md index c11367e..df5367d 100644 --- a/docs/build.md +++ b/docs/build.md @@ -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. diff --git a/docs/mcp.md b/docs/mcp.md index 3ef9855..66eef82 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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 `, and names its app in the instructions and rules: the app it ships in, or `--name `. 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 diff --git a/native2/README.md b/native2/README.md index 4406a4c..cf8e732 100644 --- a/native2/README.md +++ b/native2/README.md @@ -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` | diff --git a/native2/Sources/desktop-tools-mcp/main.swift b/native2/Sources/desktop-tools-mcp/main.swift index 4b1a87d..7031be2 100644 --- a/native2/Sources/desktop-tools-mcp/main.swift +++ b/native2/Sources/desktop-tools-mcp/main.swift @@ -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 `. +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" @@ -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) { @@ -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 @@ -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([]))])) diff --git a/src/dev-app.ts b/src/dev-app.ts index b7b582d..fc95e7c 100644 --- a/src/dev-app.ts +++ b/src/dev-app.ts @@ -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}"`); } } }