From ce4466736a02fc84bd847a49a6a4d9c6044695ce Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Tue, 3 Nov 2026 19:35:26 -0300 Subject: [PATCH 1/7] Add getlocalhost mcp serve: run the app's localhost mcp for any MCP client --- packages/cli/bin/getlocalhost.js | 2 +- packages/cli/src/cli.js | 55 ++++++++++- packages/cli/test/fixture.js | 10 +- packages/cli/test/serve.test.js | 160 +++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 packages/cli/test/serve.test.js diff --git a/packages/cli/bin/getlocalhost.js b/packages/cli/bin/getlocalhost.js index 387827a..a56bbb9 100755 --- a/packages/cli/bin/getlocalhost.js +++ b/packages/cli/bin/getlocalhost.js @@ -1,4 +1,4 @@ #!/usr/bin/env node import { main } from '../src/cli.js'; -process.exitCode = main(process.argv.slice(2)); +process.exitCode = await main(process.argv.slice(2)); diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 6a886b4..74d5613 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -1,6 +1,8 @@ // `npx @getlocalhost/cli mcp install|uninstall|status`: connects coding agents // to `localhost mcp` from a terminal, as Settings → Agents does in the app. +// `mcp serve` runs that server itself, for MCP clients configured with npx. +import { spawn } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; @@ -20,13 +22,15 @@ Commands: to your agents. Every other server and setting stays. mcp uninstall Remove them, and nothing else mcp status Show where each agent stands + mcp serve Run the app's localhost mcp on stdin and stdout, for any + MCP client: npx -y @getlocalhost/cli mcp serve Options: --client claude-code, claude-desktop, cursor or codex; claude for both Claude apps. Repeat for more than one. Without it: every agent found on this computer --read-only Leave out the tools that add, edit or remove Projects and - Services (localhost mcp --read-only) + Services (localhost mcp --read-only). For install and serve --dry-run Print what would change, and change nothing -h, --help Show this -V, --version Show this package's version @@ -63,16 +67,18 @@ export function parse(argv) { } else if (argument.startsWith('-')) return { error: `Unknown option ${argument}.` }; else words.push(argument); } - if (words[0] !== 'mcp' || words.length !== 2 || !['install', 'uninstall', 'status'].includes(words[1])) { + if (words[0] !== 'mcp' || words.length !== 2 || !['install', 'uninstall', 'status', 'serve'].includes(words[1])) { return { error: words.length === 0 ? USAGE : `Unknown command “${words.join(' ')}”.\n\n${USAGE}` }; } - if (options.readOnly && words[1] !== 'install') return { error: '--read-only is for mcp install.' }; + if (options.readOnly && !['install', 'serve'].includes(words[1])) return { error: '--read-only is for mcp install and mcp serve.' }; + if (words[1] === 'serve' && (options.clients.length > 0 || options.dryRun)) return { error: 'mcp serve takes only --read-only.' }; return { command: words[1], ...options }; } /** - * Runs the command line, writing to `io.out` and `io.err`; returns the exit code. `io` also carries what tests replace: `platform`, `home`, - * `env`, and the `candidates`, `probe`, `spotlight`, `claude` and `runClaude` of `findApp` and `Agents`. + * Runs the command line, writing to `io.out` and `io.err`; returns the exit code, or for `mcp serve` a promise of it. `io` also carries what + * tests replace: `platform`, `home`, `env`, the `candidates`, `probe`, `spotlight`, `claude` and `runClaude` of `findApp` and `Agents`, and + * the `spawn`, `signals` and `raise` of `serve`. */ export function main(argv, io = {}) { const out = io.out ?? ((line) => process.stdout.write(`${line}\n`)); @@ -92,6 +98,7 @@ export function main(argv, io = {}) { const env = io.env ?? process.env; const context = { platform, home, env, probe: io.probe, spotlight: io.spotlight, candidates: io.candidates }; const app = findApp(context); + if (parsed.command === 'serve') return serve(app, parsed, { ...io, err, platform, env }); const files = new Files({ dryRun: parsed.dryRun }); const agents = new Agents({ ...context, @@ -168,6 +175,44 @@ export function main(argv, io = {}) { return failed ? 1 : 0; } +const FORWARDED = ['SIGINT', 'SIGTERM', 'SIGHUP']; + +/** + * Runs the app's `localhost mcp` with this process's stdin and stdout, which are the client's JSON-RPC channel: nothing else may write to + * stdout, so every problem goes to stderr. Resolves to the exit code to leave with, once the server has ended. + */ +function serve(app, parsed, { err, platform, env, spawn: start = spawn, signals = process, raise = (signal) => process.kill(process.pid, signal) }) { + const refusal = refuse(app, parsed, platform, env); + if (refusal) { + refusal.forEach((line) => err(line)); + return Promise.resolve(1); + } + return new Promise((resolve) => { + const child = start(app.command, ['mcp', ...(parsed.readOnly ? ['--read-only'] : [])], { stdio: 'inherit', windowsHide: true }); + // On Windows the server shares the console, which sends it Ctrl-C itself, and a forwarded signal would only terminate it. Listening + // keeps this process alive until the server has ended, either way. + const forward = (signal) => platform !== 'win32' && child.kill(signal); + for (const signal of FORWARDED) signals.on(signal, forward); + let done = false; + const finish = (code) => { + if (done) return; + done = true; + for (const signal of FORWARDED) signals.off(signal, forward); + resolve(code); + }; + child.on('error', (error) => { + err(`Couldn’t run ${app.command}: ${error.message}`); + finish(1); + }); + child.on('exit', (code, signal) => { + if (signal === null) return finish(code ?? 1); + // Ended the way the server did, so whoever started this sees the signal; 128 + its number if it doesn't end this process. + finish(128 + (os.constants.signals[signal] ?? 0)); + raise(signal); + }); + }); +} + /** What happened to one agent, in a sentence. */ function summary(parsed, agents, agent, before, { server, changed }) { const where = display(agent.config, agents.home); diff --git a/packages/cli/test/fixture.js b/packages/cli/test/fixture.js index 2893a6c..0133a31 100644 --- a/packages/cli/test/fixture.js +++ b/packages/cli/test/fixture.js @@ -11,9 +11,9 @@ export const SKILL = '---\nname: localhost-mcp\ndescription: test\n---\n\nUse lo /** * `agents`: the agents' folders to create. `app`: whether the app is installed; `readOnly`: whether its command has `mcp --read-only`. - * `claude`: the `claude` command Claude Code is changed with (`null`: edit the file). + * `claude`: the `claude` command Claude Code is changed with (`null`: edit the file). `io`: more of `main`'s `io`, such as `serve`'s `spawn`. */ -export function fixture({ agents = ['.claude', '.cursor', '.codex'], app = true, readOnly = true, claude = null, platform = process.platform } = {}) { +export function fixture({ agents = ['.claude', '.cursor', '.codex'], app = true, readOnly = true, claude = null, platform = process.platform, io = {} } = {}) { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'getlocalhost-')); const env = { PATH: '', @@ -39,7 +39,7 @@ export function fixture({ agents = ['.claude', '.cursor', '.codex'], app = true, file, read: (...parts) => (fs.existsSync(file(...parts)) ? fs.readFileSync(file(...parts), 'utf8') : null), write: (parts, text) => write(file(...parts), text), - /** Runs the command line; `{ code, out, err }`, each a string. */ + /** Runs the command line; `{ code, out, err }`, each a string, or for `mcp serve` a promise of it. */ run(...argv) { const out = []; const err = []; @@ -60,8 +60,10 @@ export function fixture({ agents = ['.claude', '.cursor', '.codex'], app = true, runClaude: (claudeCommand, args) => claudeCalls.push([claudeCommand, ...args]), out: (line) => out.push(line), err: (line) => err.push(line), + ...io, }); - return { code, out: out.join('\n'), err: err.join('\n') }; + const result = (exit) => ({ code: exit, out: out.join('\n'), err: err.join('\n') }); + return code instanceof Promise ? code.then(result) : result(code); }, }; } diff --git a/packages/cli/test/serve.test.js b/packages/cli/test/serve.test.js new file mode 100644 index 0000000..502d569 --- /dev/null +++ b/packages/cli/test/serve.test.js @@ -0,0 +1,160 @@ +// `mcp serve`: runs the app's `localhost mcp` for MCP clients started with +// npx, keeping stdout for the server alone. +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { EventEmitter } from 'node:events'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { test } from 'node:test'; + +import { parse } from '../src/cli.js'; +import { fixture } from './fixture.js'; + +/** A `spawn` that records its calls and hands back a server the test ends, and the signals sent to it. */ +function fakeSpawn() { + const calls = []; + const spawnServer = (command, args, options) => { + const child = new EventEmitter(); + child.killed = []; + child.kill = (signal) => child.killed.push(signal); + calls.push({ command, args, options, child }); + return child; + }; + return { calls, spawn: spawnServer }; +} + +/** Runs `mcp serve` against a fixture, with a fake `spawn`, signals and re-raise. */ +function serve({ argv = [], app = true, readOnly = true, platform = 'linux', io = {} } = {}) { + const spawned = fakeSpawn(); + const signals = new EventEmitter(); + const raised = []; + const home = fixture({ app, readOnly, platform, io: { spawn: spawned.spawn, signals, raise: (signal) => raised.push(signal), ...io } }); + const running = home.run('mcp', 'serve', ...argv); + return { home, running, calls: spawned.calls, signals, raised }; +} + +test('parses mcp serve, and --read-only only for install and serve', () => { + assert.deepEqual(parse(['mcp', 'serve']), { command: 'serve', clients: [], readOnly: false, dryRun: false }); + assert.deepEqual(parse(['mcp', 'serve', '--read-only']), { command: 'serve', clients: [], readOnly: true, dryRun: false }); + assert.equal(parse(['mcp', 'install', '--read-only']).readOnly, true); + for (const command of ['uninstall', 'status']) assert.match(parse(['mcp', command, '--read-only']).error, /--read-only is for mcp install and mcp serve/); + assert.match(parse(['mcp', 'serve', '--client', 'cursor']).error, /only --read-only/); + assert.match(parse(['mcp', 'serve', '--dry-run']).error, /only --read-only/); +}); + +test('runs the app’s localhost mcp with the client’s stdin and stdout', async () => { + const { home, running, calls } = serve(); + assert.equal(calls.length, 1); + assert.equal(calls[0].command, home.command); + assert.deepEqual(calls[0].args, ['mcp']); + assert.deepEqual(calls[0].options, { stdio: 'inherit', windowsHide: true }); + calls[0].child.emit('exit', 0, null); + assert.deepEqual(await running, { code: 0, out: '', err: '' }); +}); + +test('passes --read-only on', async () => { + const { running, calls } = serve({ argv: ['--read-only'] }); + assert.deepEqual(calls[0].args, ['mcp', '--read-only']); + calls[0].child.emit('exit', 0, null); + assert.equal((await running).code, 0); +}); + +test('without the app, says how to install it on stderr only, and fails', async () => { + for (const platform of ['darwin', 'win32', 'linux']) { + const { running, calls } = serve({ app: false, platform }); + const { code, out, err } = await running; + assert.equal(code, 1); + assert.equal(out, '', platform); + assert.match(err, /The localhost app isn’t installed/); + assert.match(err, /https:\/\/www\.getlocalhost\.app/); + assert.equal(calls.length, 0); + } +}); + +test('--read-only fails when the app’s command doesn’t have it', async () => { + const { running, calls } = serve({ argv: ['--read-only'], readOnly: false }); + const { code, out, err } = await running; + assert.equal(code, 1); + assert.equal(out, ''); + assert.match(err, /localhost 1\.2\.3 has no `localhost mcp --read-only` yet/); + assert.equal(calls.length, 0); +}); + +test('exits with the server’s exit code', async () => { + const { running, calls } = serve(); + calls[0].child.emit('exit', 3, null); + assert.equal((await running).code, 3); +}); + +test('forwards SIGINT, SIGTERM and SIGHUP to the server, and stops listening once it ends', async () => { + const { running, calls, signals } = serve(); + for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) signals.emit(signal, signal); + assert.deepEqual(calls[0].child.killed, ['SIGINT', 'SIGTERM', 'SIGHUP']); + calls[0].child.emit('exit', 0, null); + await running; + assert.equal(signals.listenerCount('SIGTERM'), 0); +}); + +test('on Windows, leaves Ctrl-C to the console the server shares', async () => { + // The Linux fixture's command, found as if this were Windows: a Windows path wouldn't be one on this computer. + const linuxCommand = (context) => [path.join(context.env.LOCALHOST_STORAGE_ROOT, 'bin', 'localhost')]; + const { running, calls, signals } = serve({ io: { platform: 'win32', candidates: linuxCommand } }); + signals.emit('SIGINT', 'SIGINT'); + assert.deepEqual(calls[0].child.killed, []); + calls[0].child.emit('exit', 0, null); + assert.equal((await running).code, 0); +}); + +test('ends the way the server did when a signal killed it', async () => { + const { running, calls, raised } = serve(); + calls[0].child.emit('exit', null, 'SIGTERM'); + assert.equal((await running).code, 128 + os.constants.signals.SIGTERM); + assert.deepEqual(raised, ['SIGTERM']); +}); + +test('a server that can’t be started is an error on stderr', async () => { + const { home, running, calls } = serve(); + calls[0].child.emit('error', new Error('spawn EACCES')); + const { code, out, err } = await running; + assert.equal(code, 1); + assert.equal(out, ''); + assert.equal(err, `Couldn’t run ${home.command}: spawn EACCES`); +}); + +// A real server: a `localhost` script that answers --version and --help as the app's does, and echoes stdin as `mcp`. +test('end to end: stdout carries only the server’s output', { skip: process.platform === 'win32' && 'the fake command is a shell script' }, async () => { + const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'getlocalhost-serve-')); + const command = path.join(folder, 'localhost'); + fs.writeFileSync( + command, + [ + '#!/bin/sh', + 'case "$1" in', + ' --version) echo "localhost 9.9.9" ;;', + ' --help) echo " mcp [--read-only] Serve Projects" ;;', + ' mcp) echo "args: $*" >&2; exec /bin/cat ;;', + ' *) exit 64 ;;', + 'esac', + '', + ].join('\n'), + { mode: 0o755 }, + ); + const cli = new URL('../src/cli.js', import.meta.url).href; + const script = `import { main } from ${JSON.stringify(cli)}; +process.exitCode = await main(['mcp', 'serve', ...process.argv.slice(1)], { candidates: () => [${JSON.stringify(command)}], spotlight: () => [] });`; + const request = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n'; + + for (const argv of [[], ['--read-only']]) { + const child = spawn(process.execPath, ['--input-type=module', '-e', script, '--', ...argv], { stdio: ['pipe', 'pipe', 'pipe'] }); + let out = ''; + let err = ''; + child.stdout.on('data', (chunk) => (out += chunk)); + child.stderr.on('data', (chunk) => (err += chunk)); + child.stdin.end(request); + const code = await new Promise((resolve) => child.on('close', resolve)); + assert.equal(code, 0, err); + assert.equal(out, request); + assert.equal(err.trim(), `args: ${['mcp', ...argv].join(' ')}`); + } +}); From 47a72ae0337925a9448fc66203a1975a1fb8ab6a Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Tue, 3 Nov 2026 19:35:26 -0300 Subject: [PATCH 2/7] Add server.json, glama.json, .mcp.json and an introspection Dockerfile; CLI 0.2.0 with mcpName --- .dockerignore | 2 ++ .github/workflows/ci.yml | 4 ++-- .mcp.json | 8 ++++++++ Dockerfile | 27 +++++++++++++++++++++++++++ glama.json | 4 ++++ packages/cli/package.json | 5 +++-- packages/cli/test/server-json.test.js | 24 ++++++++++++++++++++++++ server.json | 27 +++++++++++++++++++++++++++ 8 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 .mcp.json create mode 100644 Dockerfile create mode 100644 glama.json create mode 100644 packages/cli/test/server-json.test.js create mode 100644 server.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c2cbff6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +# The Dockerfile downloads a release and copies nothing from the repository. +* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2788b8..61a3e1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,8 @@ jobs: CORE: '^(core/|skills/|\.github/workflows/ci\.yml$)' MACOS: '^(apps/macos/|scripts/build-core\.sh$)' DESKTOP: '^(apps/desktop/|scripts/(test-updater-scripts|updater-manifest|appcast|sparkle-public-key)\.sh$)' - # The package's tests read the skill and the site's install links. - CLI: '^(packages/cli/|skills/|site/src/lib/(site|release)\.ts$|site/src/components/InstallDialog\.astro$|site/astro\.config\.mjs$|\.github/workflows/ci\.yml$)' + # The package's tests read the skill, the site's install links and the MCP Registry's server.json. + CLI: '^(packages/cli/|skills/|server\.json$|\.mcp\.json$|site/src/lib/(site|release)\.ts$|site/src/components/InstallDialog\.astro$|site/astro\.config\.mjs$|\.github/workflows/ci\.yml$)' run: | if [[ $GITHUB_EVENT_NAME == pull_request ]]; then files=$(git diff --name-only HEAD^1 HEAD) diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..92ff5e8 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "localhost": { + "command": "npx", + "args": ["-y", "@getlocalhost/cli", "mcp", "serve"] + } + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b2a4177 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# For MCP directories' introspection only, such as Glama's: it runs +# `localhost mcp` so they can list its tools. Inside a container it sees no +# Projects and can't start or stop anything on your computer: for real use, +# install the app (https://www.getlocalhost.app) and connect your agents with +# `npx -y @getlocalhost/cli mcp install`. +# +# The `localhost` command from the latest release's .deb, or from VERSION: +# docker build --platform=linux/amd64 -t localhost-mcp . +# docker build --platform=linux/amd64 --build-arg VERSION=0.3.0 -t localhost-mcp . +# The Linux app is amd64 only. +FROM debian:bookworm-slim + +ARG VERSION=latest +RUN set -eu; \ + apt-get update; \ + apt-get install -y --no-install-recommends curl ca-certificates; \ + if [ "$VERSION" = latest ]; then \ + VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/marcellosso/localhost/releases/latest | sed 's|.*/tag/v||'); \ + fi; \ + curl -fsSL -o /tmp/localhost.deb "https://github.com/marcellosso/localhost/releases/download/v${VERSION}/localhost-app_${VERSION}_amd64.deb"; \ + dpkg-deb -x /tmp/localhost.deb /tmp/localhost; \ + install -m 0755 /tmp/localhost/usr/bin/localhost /usr/local/bin/localhost; \ + localhost --version; \ + apt-get purge -y --auto-remove curl; \ + rm -rf /tmp/localhost /tmp/localhost.deb /var/lib/apt/lists/* + +ENTRYPOINT ["localhost", "mcp"] diff --git a/glama.json b/glama.json new file mode 100644 index 0000000..c8f03aa --- /dev/null +++ b/glama.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://glama.ai/mcp/schemas/server.json", + "maintainers": ["marcellosso"] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 8947d25..e7277d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,8 @@ { "name": "@getlocalhost/cli", - "version": "0.1.0", - "description": "Connect Claude Code, Claude Desktop, Cursor and Codex to localhost mcp, the MCP server of the localhost app, from a terminal.", + "version": "0.2.0", + "mcpName": "io.github.marcellosso/localhost", + "description": "Connect Claude Code, Claude Desktop, Cursor and Codex to localhost mcp, the MCP server of the localhost app, from a terminal, or run it from any MCP client with npx.", "keywords": [ "localhost", "mcp", diff --git a/packages/cli/test/server-json.test.js b/packages/cli/test/server-json.test.js new file mode 100644 index 0000000..cf8d466 --- /dev/null +++ b/packages/cli/test/server-json.test.js @@ -0,0 +1,24 @@ +// The MCP Registry's entry, server.json at the repository's root, names this +// package at the version npm has: publish both together. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { test } from 'node:test'; + +const read = (file) => JSON.parse(fs.readFileSync(new URL(file, import.meta.url), 'utf8')); +const pkg = read('../package.json'); + +test('server.json is this package, at its version, running mcp serve', () => { + const server = read('../../../server.json'); + assert.equal(server.name, pkg.mcpName); + assert.equal(server.version, pkg.version); + assert.equal(server.packages.length, 1); + const [entry] = server.packages; + assert.equal(entry.identifier, pkg.name); + assert.equal(entry.version, pkg.version); + assert.deepEqual(entry.packageArguments.map((argument) => argument.value), ['mcp', 'serve']); + assert.ok(server.description.length <= 100, 'the registry takes 100 characters at most'); +}); + +test('the example .mcp.json runs mcp serve', () => { + assert.deepEqual(read('../../../.mcp.json').mcpServers.localhost.args, ['-y', pkg.name, 'mcp', 'serve']); +}); diff --git a/server.json b/server.json new file mode 100644 index 0000000..f5d40df --- /dev/null +++ b/server.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.marcellosso/localhost", + "title": "localhost app", + "description": "Start, stop and read logs of your local dev servers from Claude Code, Cursor and Codex.", + "websiteUrl": "https://www.getlocalhost.app/docs/mcp", + "repository": { + "url": "https://github.com/marcellosso/localhost", + "source": "github", + "subfolder": "packages/cli" + }, + "version": "0.2.0", + "packages": [ + { + "registryType": "npm", + "registryBaseUrl": "https://registry.npmjs.org", + "identifier": "@getlocalhost/cli", + "version": "0.2.0", + "runtimeHint": "npx", + "transport": { "type": "stdio" }, + "packageArguments": [ + { "type": "positional", "value": "mcp" }, + { "type": "positional", "value": "serve" } + ] + } + ] +} From ef43b6ffc395531246a1ae58f2ff00f21cd99cee Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Tue, 3 Nov 2026 19:35:26 -0300 Subject: [PATCH 3/7] =?UTF-8?q?Add=20a=20400=C3=97400=20logo=20from=20the?= =?UTF-8?q?=20app=20icon=20for=20MCP=20directories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/logo-400.png | Bin 0 -> 46806 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/logo-400.png diff --git a/assets/logo-400.png b/assets/logo-400.png new file mode 100644 index 0000000000000000000000000000000000000000..802f9cd5285169f089a62d4b37826b0839d5d300 GIT binary patch literal 46806 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 G_x}LiC}Hsc literal 0 HcmV?d00001 From c97a0cb2a5aff1f361e0080aaf23770585979964 Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Tue, 3 Nov 2026 19:35:26 -0300 Subject: [PATCH 4/7] Document mcp serve, the MCP server's install commands and publishing to the MCP Registry --- AGENTS.md | 4 +++- CONTRIBUTING.md | 16 +++++++++++++-- README.md | 38 ++++++++++++++++++++++++++++++++++++ packages/cli/README.md | 17 +++++++++++++++- site/src/content/docs/mcp.md | 17 +++++++++++++++- site/src/lib/agents.ts | 2 +- 6 files changed, 88 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f869f89..0b3a26d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,12 +37,14 @@ apps/macos/ the macOS app apps/desktop/ the Linux and Windows app (Tauri 2) src/ React + TypeScript UI; src/bindings.ts is generated, never edited src-tauri/ Rust shell hosting the core -packages/cli/ `npx @getlocalhost/cli mcp install`: connects agents from a terminal (Node, no dependencies) +packages/cli/ `npx @getlocalhost/cli mcp install`: connects agents from a terminal; `mcp serve` runs the app's server (Node, no dependencies) skills/ agent skills, e.g. using localhost's MCP server site/ the website (Astro): landing page, docs, changelog; reads docs/ and the desktop tokens scripts/ build-core.sh, build-release.sh, signing and installer artwork packaging/homebrew/ the Homebrew cask's template; the tap is marcellosso/homebrew-tap +assets/ brand images for listings, e.g. the 400×400 logo docs/configuration.md the Project file format (user-facing) +server.json, glama.json MCP directory listings, with the example .mcp.json and the introspection-only Dockerfile ``` ## Vocabulary diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63e0ddf..6f69624 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,7 +120,7 @@ Checking the app for Windows from macOS or Linux works like the core's check abo 2. Run the checks for what you touched (AGENTS.md, Verifying a change), then build and run the app. 3. Open a PR describing what changed and why, how you checked it, and anything you left out. Link the issue it addresses. -CI runs the core's tests on Linux, macOS and Windows, the Swift layer's tests, an unsigned app build, and the desktop app's checks (TypeScript, Vitest, clippy, its Rust tests, up-to-date bindings), and the npm package's tests; all must pass. On a PR each job runs only when files it depends on changed, and is skipped otherwise: the core's jobs for `core/` and `skills/` (the core embeds the localhost-mcp skill), the macOS job for those and `apps/macos/`, the desktop job for those and `apps/desktop/`, the npm package's (`packages/cli/`, on Linux, macOS and Windows) for it, `skills/` and the site's install links, and each for the scripts it runs. The site builds only for `site/`, `docs/`, `.github/assets/` and the desktop tokens, and Vercel's preview of a PR follows the same paths (`site/scripts/ignore-build.sh`), so other PRs show it as Ignored; production on `main` and the release's deploy hook always build. Changing a workflow runs everything in it, and every push to `main` runs everything; release packages are built only by a release tag, and the npm package is published only by running its workflow by hand. A skipped job counts as passing, and the `CI result` check fails if any CI job failed. The macOS job also skips draft PRs (macOS minutes cost the most), so mark yours ready for review when you want the full run. CI on a PR from a fork starts once a maintainer approves the run, and every PR needs a maintainer's approving review before it merges. Commits are kept as written (merge commits, no squash), so make each commit message say what it does in one line. +CI runs the core's tests on Linux, macOS and Windows, the Swift layer's tests, an unsigned app build, and the desktop app's checks (TypeScript, Vitest, clippy, its Rust tests, up-to-date bindings), and the npm package's tests; all must pass. On a PR each job runs only when files it depends on changed, and is skipped otherwise: the core's jobs for `core/` and `skills/` (the core embeds the localhost-mcp skill), the macOS job for those and `apps/macos/`, the desktop job for those and `apps/desktop/`, the npm package's (`packages/cli/`, on Linux, macOS and Windows) for it, `skills/`, the root `server.json` and `.mcp.json`, and the site's install links, and each for the scripts it runs. The site builds only for `site/`, `docs/`, `.github/assets/` and the desktop tokens, and Vercel's preview of a PR follows the same paths (`site/scripts/ignore-build.sh`), so other PRs show it as Ignored; production on `main` and the release's deploy hook always build. Changing a workflow runs everything in it, and every push to `main` runs everything; release packages are built only by a release tag, and the npm package is published only by running its workflow by hand. A skipped job counts as passing, and the `CI result` check fails if any CI job failed. The macOS job also skips draft PRs (macOS minutes cost the most), so mark yours ready for review when you want the full run. CI on a PR from a fork starts once a maintainer approves the run, and every PR needs a maintainer's approving review before it merges. Commits are kept as written (merge commits, no squash), so make each commit message say what it does in one line. Releases are cut by tagging `main`; see [RELEASING.md](RELEASING.md). @@ -130,7 +130,7 @@ By contributing, you agree your contributions are licensed under the MIT license `@getlocalhost/cli` (`packages/cli/`) is published to npm from CI by the [Publish the npm package](.github/workflows/publish-cli.yml) workflow, run by hand. npm trusts that workflow through trusted publishing, so there is no npm token. With each app release: -1. Bump `version` in `packages/cli/package.json` and merge it into `main`. npm never accepts a version twice. +1. Bump `version` in `packages/cli/package.json`, and both versions in the root `server.json` to match (the package's tests check it), and merge it into `main`. npm never accepts a version twice. 2. Run the workflow on `main`, first as a dry run: ```sh @@ -139,3 +139,15 @@ By contributing, you agree your contributions are licensed under the MIT license ``` It runs the package's tests, fails if the version is already on npm, checks the package includes the skill, and publishes it with provenance. A dry run does all but the publishing, from any branch; publishing runs only from `main`. + +## Publishing to the MCP Registry + +The root `server.json` lists `@getlocalhost/cli` in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.marcellosso/localhost`, run as `npx -y @getlocalhost/cli mcp serve`. The registry checks that npm has that version and that its `package.json` names the server in `mcpName`, so publish it after the npm package, with [`mcp-publisher`](https://github.com/modelcontextprotocol/registry/releases) (`brew install mcp-publisher`): + +```sh +mcp-publisher validate +mcp-publisher login github # as marcellosso, who owns the io.github.marcellosso namespace +mcp-publisher publish +``` + +The other files at the root are for MCP directories too: `glama.json` names the maintainers for Glama, the `Dockerfile` lets directories that inspect a server's tools run `localhost mcp` from the latest release, and `.mcp.json` is the example configuration. Claude Code reads that `.mcp.json` as a project server: it asks before using it, and once allowed it replaces a user-scope `localhost` server of the same name in this repository. Decline it, or list it in `disabledMcpjsonServers` in `.claude/settings.local.json`, to keep the app's own entry. diff --git a/README.md b/README.md index 663b5ae..bcee54b 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,44 @@ It writes the same entries as **Connect** for every agent it finds, and Claude D Other MCP clients take the same `command` and `args`. On Windows it covers Projects on Windows itself; a Project in a WSL distro runs only from the app, and agents installed inside a WSL distro are set up by hand. The skill is in [skills/localhost-mcp/SKILL.md](skills/localhost-mcp/SKILL.md): copy the `localhost-mcp` folder to `~/.claude/skills/`, `~/.cursor/skills/` or `~/.agents/skills/`. More in the [MCP docs](https://www.getlocalhost.app/docs/mcp), the [skill docs](https://www.getlocalhost.app/docs/skills) and [localhost for AI coding agents](https://www.getlocalhost.app/agents). +### MCP server + +`npx -y @getlocalhost/cli mcp serve` runs `localhost mcp` from any MCP client's configuration: it finds the command that comes with the app and hands it the client's stdin and stdout (`mcp serve --read-only` runs `localhost mcp --read-only`). It needs the localhost app: install it from [getlocalhost.app](https://www.getlocalhost.app). Without it, the server exits and says on stderr how to install it. + +Connect every agent on this computer at once: + +```sh +npx -y @getlocalhost/cli mcp install +npx -y @getlocalhost/cli mcp install --read-only # without the tools that add, edit or remove +``` + +Or add the server to one agent: + +- **Claude Code**: `claude mcp add --scope user localhost -- npx -y @getlocalhost/cli mcp serve` +- **Codex**: `codex mcp add localhost -- npx -y @getlocalhost/cli mcp serve`, or in `~/.codex/config.toml`: + + ```toml + [mcp_servers.localhost] + command = "npx" + args = ["-y", "@getlocalhost/cli", "mcp", "serve"] + ``` + +- **Cursor, Claude Desktop and other clients**: in `~/.cursor/mcp.json` (or `.cursor/mcp.json` in one project), `claude_desktop_config.json`, or the client's own file: + + ```json + { + "mcpServers": { + "localhost": { "command": "npx", "args": ["-y", "@getlocalhost/cli", "mcp", "serve"] } + } + } + ``` + +Tools: `list_projects`, `list_services`, `status`, `start_service`, `stop_service`, `start_project`, `stop_project`, `logs`, `ports`, `create_project`, `update_project`, `delete_project`, `create_service`, `update_service` and `delete_service`; `--read-only` leaves out the last six. + +[`server.json`](server.json) describes this server for the [MCP Registry](https://registry.modelcontextprotocol.io), and [`.mcp.json`](.mcp.json) is the configuration above. The [`Dockerfile`](Dockerfile) is only for MCP directories that inspect a server's tools: in a container, `localhost mcp` sees no Projects and can't start or stop anything on your computer. + + + ## FAQ ### What is localhost app? diff --git a/packages/cli/README.md b/packages/cli/README.md index 52933a7..2666943 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -27,16 +27,31 @@ It writes what **Settings → Agents** in the app writes, and the app shows thes npx @getlocalhost/cli mcp install # add the server and the skill npx @getlocalhost/cli mcp uninstall # remove them, and nothing else npx @getlocalhost/cli mcp status # where each agent stands +npx @getlocalhost/cli mcp serve # run the server, for an MCP client's configuration ``` | Option | | |---|---| | `--client ` | `claude-code`, `claude-desktop`, `cursor` or `codex`; `claude` for both Claude apps. Repeat it, or separate with commas, for more than one. Without it: every agent found on this computer. | -| `--read-only` | The server runs `localhost mcp --read-only`, without the tools that add, edit or remove Projects and Services. Needs a localhost app that has it (from the release after 0.3.0). Installing again without it keeps a read-only server read-only. | +| `--read-only` | For `mcp install` and `mcp serve`. The server runs `localhost mcp --read-only`, without the tools that add, edit or remove Projects and Services. Needs a localhost app that has it (0.4.0 or later). Installing again without it keeps a read-only server read-only. | | `--dry-run` | Prints what would change, as a diff, and changes nothing. | Then start a new Claude Code session, quit and reopen Claude Desktop, check **Cursor Settings → MCP**, or restart Codex. +## Any MCP client + +`mcp serve` is the server itself, for any client that starts MCP servers over stdio: + +```json +{ + "mcpServers": { + "localhost": { "command": "npx", "args": ["-y", "@getlocalhost/cli", "mcp", "serve"] } + } +} +``` + +It finds the app's `localhost` command as `mcp install` does and runs `localhost mcp` with the client's stdin and stdout, passing on Ctrl-C and other signals and exiting as the server does. `mcp serve --read-only` runs `localhost mcp --read-only`. It writes nothing to stdout itself, since that is the client's channel: without the app, or with an app whose command doesn't run, it says why on stderr and exits with 1. `claude mcp add --scope user localhost -- npx -y @getlocalhost/cli mcp serve` and `codex mcp add localhost -- npx -y @getlocalhost/cli mcp serve` add it from a terminal. + More in the [MCP docs](https://www.getlocalhost.app/docs/mcp). Node 18 or later; no dependencies. ## Development diff --git a/site/src/content/docs/mcp.md b/site/src/content/docs/mcp.md index 3e975d9..55d94ff 100644 --- a/site/src/content/docs/mcp.md +++ b/site/src/content/docs/mcp.md @@ -55,8 +55,9 @@ It writes what **Connect** writes, to the same files and with the same rules (on | `npx @getlocalhost/cli mcp install` | Adds the `localhost` server and the skill. | | `npx @getlocalhost/cli mcp uninstall` | Removes them, and nothing else. | | `npx @getlocalhost/cli mcp status` | Says where each agent stands, as **Settings → Agents** does. | +| `npx -y @getlocalhost/cli mcp serve` | Runs the server itself, for an agent's configuration: see [With npx](#with-npx). | | `--client ` | Only `claude-code`, `claude-desktop`, `cursor` or `codex` (`claude` for both Claude apps). Repeat it for more than one. | -| `--read-only` | Sets up `localhost mcp --read-only`, without the tools that add, edit or remove Projects and Services ([Read-only](#read-only)). Installing again without it keeps a read-only server read-only. | +| `--read-only` | For `mcp install` and `mcp serve`: sets up `localhost mcp --read-only`, without the tools that add, edit or remove Projects and Services ([Read-only](#read-only)). Installing again without it keeps a read-only server read-only. | | `--dry-run` | Prints what would change, as a diff, and changes nothing. | The package doesn't contain the app or the `localhost` command: agents run the one inside the app, [where it is](#keeping-it-current) on each OS. Without the app it changes nothing and says how to install it: Homebrew or the disk image on macOS, the installer on Windows, the packages on Linux (see [Install](/docs/install)). It needs Node 18 or later, and only edits files: no network, apart from `npx` downloading it. @@ -103,6 +104,20 @@ args = ["mcp"] Building localhost yourself? `cargo install --path core/localhost-cli` in a clone of the repository puts `localhost` in `~/.cargo/bin`. It must come from the same version as the app, which otherwise says the two speak to the core differently. +### With npx + +Instead of the command's path, a client can run `npx -y @getlocalhost/cli mcp serve`, the same in every client and on every OS: + +```json +{ + "mcpServers": { + "localhost": { "command": "npx", "args": ["-y", "@getlocalhost/cli", "mcp", "serve"] } + } +} +``` + +From a terminal: `claude mcp add --scope user localhost -- npx -y @getlocalhost/cli mcp serve`, or `codex mcp add localhost -- npx -y @getlocalhost/cli mcp serve`. It finds the app's command [where it is](#keeping-it-current) and runs `localhost mcp` with the client's input and output; `mcp serve --read-only` runs `localhost mcp --read-only`. The app must be installed: without it, the server exits at once and says how to install it, on standard error, as the client's log shows. Each start goes through `npx`, so the full path above starts faster and works offline. + ## Tools Projects and Services are named by their **Key**, as `list_projects` and `list_services` show them. A name works too. diff --git a/site/src/lib/agents.ts b/site/src/lib/agents.ts index 931dd8e..c751545 100644 --- a/site/src/lib/agents.ts +++ b/site/src/lib/agents.ts @@ -32,7 +32,7 @@ export const agentsFaq: { question: string; answer: string[] }[] = [ { question: 'Which coding agents does it work with?', answer: [ - 'Any MCP client that starts a server over standard input and output. The app connects Claude Code, Cursor and Codex in one click, in Settings → Agents; from a terminal, `npx @getlocalhost/cli mcp install` connects those and Claude Desktop. Other clients take the same `command` and `args` in their own configuration file.', + 'Any MCP client that starts a server over standard input and output. The app connects Claude Code, Cursor and Codex in one click, in Settings → Agents; from a terminal, `npx @getlocalhost/cli mcp install` connects those and Claude Desktop. Other clients take the same `command` and `args` in their own configuration file, or run `npx -y @getlocalhost/cli mcp serve`, which finds the app’s command for them.', ], }, { From 2fdcdd40507c5e392e39ed5540691caff16d9dac Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Tue, 3 Nov 2026 19:35:26 -0300 Subject: [PATCH 5/7] Add the 0.4.0 changelog, unreleased --- site/src/content/changelog/0.4.0.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 site/src/content/changelog/0.4.0.md diff --git a/site/src/content/changelog/0.4.0.md b/site/src/content/changelog/0.4.0.md new file mode 100644 index 0000000..ca27a6f --- /dev/null +++ b/site/src/content/changelog/0.4.0.md @@ -0,0 +1,12 @@ +--- +version: 0.4.0 +title: Agents add and edit your Projects, and connect from a terminal +date: 2026-11-03 +released: false +--- + +- **Agents add, edit and remove Projects and Services.** `localhost mcp` has six new tools, 15 in all: `create_project`, `update_project`, `delete_project`, `create_service`, `update_service` and `delete_service`. Each change is checked as the app's editors check it (Keys, ports, URLs, working directories, Dependency cycles) and shows in the open app at once, without a restart. Removing needs `confirm: true`, stops what is running first and never touches a Project's folder. Agents set plain variables only, and every Secret stays as it was. A command is stored as given, never run when it's added. See [Tools](/docs/mcp#tools). +- **`localhost mcp --read-only`** leaves those six tools out: the agent can still list, start, stop and inspect. **Settings → Agents** shows a read-only server as connected, and **Update** keeps it read-only. See [Read-only](/docs/mcp#read-only). +- **Connect your agents from a terminal.** With the app installed, `npx @getlocalhost/cli mcp install` writes what **Connect** writes for Claude Code, Cursor and Codex, and Claude Desktop's `claude_desktop_config.json` too, on macOS and Windows. `mcp uninstall` removes it, `mcp status` shows where each agent stands, `--client` picks one, `--read-only` sets up a read-only server and `--dry-run` prints the diff instead. Without the app, it changes nothing and says how to install it. See [Connect from a terminal](/docs/mcp#connect-from-a-terminal). +- **`npx -y @getlocalhost/cli mcp serve`** runs the server from any MCP client's configuration: it finds the command that comes with the app and runs `localhost mcp`, so the same `command` and `args` work in every client and on every OS. See [With npx](/docs/mcp#with-npx). +- **Install with Homebrew on macOS**: `brew install --cask marcellosso/tap/localhost` installs the app and links its `localhost` command into Homebrew's `bin`. The app still updates itself. See [Install](/docs/install#homebrew-macos). From 5a3566bccc83d42cbe77c07ccbd6393b1208452c Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Tue, 3 Nov 2026 19:41:57 -0300 Subject: [PATCH 6/7] Find the serve tests' fixture command on any OS, and run a real server end to end on Windows too --- packages/cli/test/serve.test.js | 62 ++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/packages/cli/test/serve.test.js b/packages/cli/test/serve.test.js index 502d569..e2de307 100644 --- a/packages/cli/test/serve.test.js +++ b/packages/cli/test/serve.test.js @@ -24,12 +24,16 @@ function fakeSpawn() { return { calls, spawn: spawnServer }; } -/** Runs `mcp serve` against a fixture, with a fake `spawn`, signals and re-raise. */ -function serve({ argv = [], app = true, readOnly = true, platform = 'linux', io = {} } = {}) { +/** + * Runs `mcp serve` as on `platform`, with a fake `spawn`, signals and re-raise. The fixture's command is where this computer's app would + * put it, a real path here, and is the one candidate, so only what `serve` does differs by `platform`. + */ +function serve({ argv = [], app = true, readOnly = true, platform = 'linux' } = {}) { const spawned = fakeSpawn(); const signals = new EventEmitter(); const raised = []; - const home = fixture({ app, readOnly, platform, io: { spawn: spawned.spawn, signals, raise: (signal) => raised.push(signal), ...io } }); + const io = { platform, candidates: () => [home.command], spawn: spawned.spawn, signals, raise: (signal) => raised.push(signal) }; + const home = fixture({ app, readOnly, io }); const running = home.run('mcp', 'serve', ...argv); return { home, running, calls: spawned.calls, signals, raised }; } @@ -97,9 +101,7 @@ test('forwards SIGINT, SIGTERM and SIGHUP to the server, and stops listening onc }); test('on Windows, leaves Ctrl-C to the console the server shares', async () => { - // The Linux fixture's command, found as if this were Windows: a Windows path wouldn't be one on this computer. - const linuxCommand = (context) => [path.join(context.env.LOCALHOST_STORAGE_ROOT, 'bin', 'localhost')]; - const { running, calls, signals } = serve({ io: { platform: 'win32', candidates: linuxCommand } }); + const { running, calls, signals } = serve({ platform: 'win32' }); signals.emit('SIGINT', 'SIGINT'); assert.deepEqual(calls[0].child.killed, []); calls[0].child.emit('exit', 0, null); @@ -122,8 +124,42 @@ test('a server that can’t be started is an error on stderr', async () => { assert.equal(err, `Couldn’t run ${home.command}: spawn EACCES`); }); -// A real server: a `localhost` script that answers --version and --help as the app's does, and echoes stdin as `mcp`. -test('end to end: stdout carries only the server’s output', { skip: process.platform === 'win32' && 'the fake command is a shell script' }, async () => { +/** Runs `node -e script -- ...argv` in `cwd`, writing `input` to its stdin: `{ code, out, err }`. */ +async function runNode(script, argv, { cwd, input }) { + const child = spawn(process.execPath, ['--input-type=module', '-e', script, '--', ...argv], { cwd, stdio: ['pipe', 'pipe', 'pipe'] }); + let out = ''; + let err = ''; + child.stdout.on('data', (chunk) => (out += chunk)); + child.stderr.on('data', (chunk) => (err += chunk)); + child.stdin.end(input); + const code = await new Promise((resolve) => child.on('close', resolve)); + return { code, out, err }; +} + +const cli = new URL('../src/cli.js', import.meta.url).href; +const request = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n'; + +// A real server on every OS: Node itself as the app's command, which `mcp` makes run the script named mcp in the working folder. It echoes +// stdin and exits with 3 once stdin ends. +test('end to end: runs the server with the client’s stdin and stdout, and exits as it does', async () => { + const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'getlocalhost-serve-')); + fs.writeFileSync( + path.join(folder, 'mcp'), + "process.stderr.write(`args: ${['mcp', ...process.argv.slice(2)].join(' ')}\\n`);\nprocess.stdin.on('end', () => (process.exitCode = 3));\nprocess.stdin.pipe(process.stdout);\n", + ); + const script = `import { main } from ${JSON.stringify(cli)}; +const probe = (command, args) => ({ output: args[0] === '--version' ? 'localhost 9.9.9\\n' : ' mcp [--read-only] Serve Projects\\n' }); +process.exitCode = await main(['mcp', 'serve', ...process.argv.slice(1)], { candidates: () => [process.execPath], spotlight: () => [], probe });`; + for (const argv of [[], ['--read-only']]) { + const { code, out, err } = await runNode(script, argv, { cwd: folder, input: request }); + assert.equal(code, 3, err); + assert.equal(out, request); + assert.equal(err.trim(), `args: ${['mcp', ...argv].join(' ')}`); + } +}); + +// The app's command as findApp checks it: a `localhost` script that answers --version and --help as the app's does, and echoes stdin as `mcp`. +test('end to end: finds and runs a localhost command', { skip: process.platform === 'win32' && 'the fake command is a shell script; the test above runs on Windows' }, async () => { const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'getlocalhost-serve-')); const command = path.join(folder, 'localhost'); fs.writeFileSync( @@ -140,19 +176,11 @@ test('end to end: stdout carries only the server’s output', { skip: process.pl ].join('\n'), { mode: 0o755 }, ); - const cli = new URL('../src/cli.js', import.meta.url).href; const script = `import { main } from ${JSON.stringify(cli)}; process.exitCode = await main(['mcp', 'serve', ...process.argv.slice(1)], { candidates: () => [${JSON.stringify(command)}], spotlight: () => [] });`; - const request = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n'; for (const argv of [[], ['--read-only']]) { - const child = spawn(process.execPath, ['--input-type=module', '-e', script, '--', ...argv], { stdio: ['pipe', 'pipe', 'pipe'] }); - let out = ''; - let err = ''; - child.stdout.on('data', (chunk) => (out += chunk)); - child.stderr.on('data', (chunk) => (err += chunk)); - child.stdin.end(request); - const code = await new Promise((resolve) => child.on('close', resolve)); + const { code, out, err } = await runNode(script, argv, { input: request }); assert.equal(code, 0, err); assert.equal(out, request); assert.equal(err.trim(), `args: ${['mcp', ...argv].join(' ')}`); From 272dbb42347a02668c9ae9cfceb203543696e751 Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Tue, 3 Nov 2026 19:43:21 -0300 Subject: [PATCH 7/7] Phrase the README's MCP server section as a question, mention mcp serve on /agents, and pin the Dockerfile to 0.4.0 --- Dockerfile | 8 +++++--- README.md | 6 ++++-- site/src/content/docs/mcp.md | 2 ++ site/src/pages/agents.astro | 3 +++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index b2a4177..a45052b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,15 @@ # install the app (https://www.getlocalhost.app) and connect your agents with # `npx -y @getlocalhost/cli mcp install`. # -# The `localhost` command from the latest release's .deb, or from VERSION: +# The `localhost` command from release VERSION's .deb; VERSION=latest takes the latest release: # docker build --platform=linux/amd64 -t localhost-mcp . -# docker build --platform=linux/amd64 --build-arg VERSION=0.3.0 -t localhost-mcp . +# docker build --platform=linux/amd64 --build-arg VERSION=latest -t localhost-mcp . # The Linux app is amd64 only. +# +# Bump VERSION with each release, so directories list that release's tools. FROM debian:bookworm-slim -ARG VERSION=latest +ARG VERSION=0.4.0 RUN set -eu; \ apt-get update; \ apt-get install -y --no-install-recommends curl ca-certificates; \ diff --git a/README.md b/README.md index bcee54b..8ff4a77 100644 --- a/README.md +++ b/README.md @@ -329,7 +329,9 @@ It writes the same entries as **Connect** for every agent it finds, and Claude D Other MCP clients take the same `command` and `args`. On Windows it covers Projects on Windows itself; a Project in a WSL distro runs only from the app, and agents installed inside a WSL distro are set up by hand. The skill is in [skills/localhost-mcp/SKILL.md](skills/localhost-mcp/SKILL.md): copy the `localhost-mcp` folder to `~/.claude/skills/`, `~/.cursor/skills/` or `~/.agents/skills/`. More in the [MCP docs](https://www.getlocalhost.app/docs/mcp), the [skill docs](https://www.getlocalhost.app/docs/skills) and [localhost for AI coding agents](https://www.getlocalhost.app/agents). -### MCP server +### How do I add the localhost MCP server to Claude Code, Cursor or Codex? + +localhost app's MCP server, `localhost mcp`, is a stdio Model Context Protocol server that lets Claude Code, Cursor and Codex start, stop and read the logs of your local dev servers on macOS, Windows and Linux. `npx -y @getlocalhost/cli mcp serve` runs `localhost mcp` from any MCP client's configuration: it finds the command that comes with the app and hands it the client's stdin and stdout (`mcp serve --read-only` runs `localhost mcp --read-only`). It needs the localhost app: install it from [getlocalhost.app](https://www.getlocalhost.app). Without it, the server exits and says on stderr how to install it. @@ -361,7 +363,7 @@ Or add the server to one agent: } ``` -Tools: `list_projects`, `list_services`, `status`, `start_service`, `stop_service`, `start_project`, `stop_project`, `logs`, `ports`, `create_project`, `update_project`, `delete_project`, `create_service`, `update_service` and `delete_service`; `--read-only` leaves out the last six. +Tools: `list_projects`, `list_services`, `status`, `start_service`, `stop_service`, `start_project`, `stop_project`, `logs`, `ports`, `create_project`, `update_project`, `delete_project`, `create_service`, `update_service` and `delete_service`; `--read-only` leaves out the last six. The six that add, edit or remove need the localhost app 0.4.0 or later. [`server.json`](server.json) describes this server for the [MCP Registry](https://registry.modelcontextprotocol.io), and [`.mcp.json`](.mcp.json) is the configuration above. The [`Dockerfile`](Dockerfile) is only for MCP directories that inspect a server's tools: in a container, `localhost mcp` sees no Projects and can't start or stop anything on your computer. diff --git a/site/src/content/docs/mcp.md b/site/src/content/docs/mcp.md index 55d94ff..051a092 100644 --- a/site/src/content/docs/mcp.md +++ b/site/src/content/docs/mcp.md @@ -106,6 +106,8 @@ Building localhost yourself? `cargo install --path core/localhost-cli` in a clon ### With npx +`npx -y @getlocalhost/cli mcp serve` is the same MCP server, run through npm, so one configuration works in every client and on every OS. + Instead of the command's path, a client can run `npx -y @getlocalhost/cli mcp serve`, the same in every client and on every OS: ```json diff --git a/site/src/pages/agents.astro b/site/src/pages/agents.astro index c642635..373dbf2 100644 --- a/site/src/pages/agents.astro +++ b/site/src/pages/agents.astro @@ -275,6 +275,9 @@ args = ["mcp"]`;

{codexConfig}
+

+ Or, on any OS: npx -y @getlocalhost/cli mcp serve as the command, which finds the app’s command for you. +

Every platform’s path, and the skill by hand: Set it up by hand.