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
32 changes: 31 additions & 1 deletion packages/devai/src/Commands/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,39 @@ static function (string $message) use ($output): void {
$output->writeLine(" - $line");
}

$this->maybePrintClaudeMultiInstanceTip($context->selectedAgents, $output);

return 0;
}

/**
* Surface the Claude Code multi-instance gotcha once Claude Code is among the
* installed agents. Running several Claude Code instances concurrently makes
* them contend on a single global ~/.claude.json, which Claude Code rewrites
* constantly — the contention causes every MCP server (marko-mcp included) to
* disconnect and reconnect in lockstep. This is a Claude Code behavior, not a
* Marko one, so we only point at the documented per-project config-isolation
* workaround rather than touching the user's shell.
*
* @param list<string> $selectedAgents
*/
private function maybePrintClaudeMultiInstanceTip(
array $selectedAgents,
Output $output,
): void {
if (!in_array('claude-code', $selectedAgents, true)) {
return;
}

$output->writeLine('');
$output->writeLine('Tip: if you run multiple Claude Code instances at once, they contend on a');
$output->writeLine('single ~/.claude.json and MCP servers (including marko-mcp) can disconnect and');
$output->writeLine('reconnect repeatedly. To isolate Claude Code config per project, see:');
$output->writeLine(
' https://marko.build/docs/ai-assisted-development/troubleshooting/#multiple-claude-code-instances-disconnect-mcp-servers',
);
}

/**
* Offer to install the recommended docs search driver when none is present
* and the session is interactive. Does nothing (falls through gracefully) in
Expand Down Expand Up @@ -124,7 +154,7 @@ private function maybeInstallDocsDriver(
$output->writeLine("Installing $pkg via composer (this may take a moment)…");
$result = $this->commandRunner->run(
'composer',
['require', '--dev', '--no-interaction', '--no-progress', $pkg]
['require', '--dev', '--no-interaction', '--no-progress', $pkg],
);

if ($result['exitCode'] !== 0) {
Expand Down
50 changes: 45 additions & 5 deletions packages/devai/tests/Unit/Commands/InstallCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public function isInteractive(): bool
public function confirm(
string $question,
bool $default,
): bool
{
): bool {
return $this->answer;
}
};
Expand All @@ -66,8 +65,7 @@ public function __construct(
public function run(
string $command,
array $args = [],
): array
{
): array {
$this->calls[] = [$command, $args];

if ($command === 'composer' && ($args[0] ?? '') === 'require') {
Expand Down Expand Up @@ -216,7 +214,7 @@ function readInstallCmdOutput(mixed $stream): string
$cmd->execute(new Input(['marko', 'devai:install']), $output);

expect($runner->calls)->toContain(
['composer', ['require', '--dev', '--no-interaction', '--no-progress', 'marko/docs-fts']]
['composer', ['require', '--dev', '--no-interaction', '--no-progress', 'marko/docs-fts']],
);
});

Expand Down Expand Up @@ -345,6 +343,48 @@ function readInstallCmdOutput(mixed $stream): string
->and($text)->toContain('marko/docs-fts');
});

// ---------------------------------------------------------------------------
// New tests: multi-instance config-isolation tip for Claude Code
// ---------------------------------------------------------------------------

it('prints a multi-instance config-isolation tip when Claude Code is installed', function (): void {
chdir($this->tempRoot);

$cmd = makeInstallCmd(
orchestrator: makeInstallCmdOrchestrator($this->tempRoot),
resolver: new DocsDriverResolver(),
prompter: makeInstallCmdFakePrompter(answer: false, interactive: false),
runner: makeInstallCmdRunner(composerOnPath: true),
);

['stream' => $stream, 'output' => $output] = makeInstallCmdOutput();
$cmd->execute(
new Input(['marko', 'devai:install', '--agents=claude-code', '--no-interaction', '--skip-lsp-deps']),
$output,
);

$text = readInstallCmdOutput($stream);
expect($text)->toContain('multiple Claude Code instances')
->and($text)->toContain('marko.build/docs/ai-assisted-development/troubleshooting');
});

it('does not print the Claude Code tip when only non-Claude agents are installed', function (): void {
chdir($this->tempRoot);

$cmd = makeInstallCmd(
orchestrator: makeInstallCmdOrchestrator($this->tempRoot),
resolver: new DocsDriverResolver(),
prompter: makeInstallCmdFakePrompter(answer: false, interactive: false),
runner: makeInstallCmdRunner(composerOnPath: true),
);

['stream' => $stream, 'output' => $output] = makeInstallCmdOutput();
$cmd->execute(new Input(['marko', 'devai:install', '--agents=codex', '--no-interaction']), $output);

$text = readInstallCmdOutput($stream);
expect($text)->not->toContain('multiple Claude Code instances');
});

it('keeps devai dependent on the marko/docs contract only (no driver in require)', function (): void {
$composerJson = json_decode(
(string) file_get_contents(dirname(__DIR__, 3) . '/composer.json'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,37 @@ The running MCP/LSP server re-checks staleness on every read for `app/` and `mod

The `query_database` tool is only registered when `marko/database` is bound in the container. Install the database package and ensure it is configured before expecting this tool to appear.

### Multiple Claude Code instances disconnect MCP servers

If you run several Claude Code instances at once (multiple terminals or windows) and notice MCP servers — `marko-mcp` included — repeatedly disconnecting and reconnecting, the cause is **not** Marko. Every Claude Code instance shares a single global `~/.claude.json`, and Claude Code rewrites that file constantly (history, tool-usage counters, session state). Concurrent writes to the one file make Claude Code tear down and reconnect its **entire** MCP fleet in lockstep, so all servers flap together. `marko-mcp` is often the one you notice because it boots a PHP process and is the slowest to re-handshake after each bounce.

This is a known Claude Code issue (see [anthropics/claude-code#25768](https://github.com/anthropics/claude-code/issues/25768), [#28829](https://github.com/anthropics/claude-code/issues/28829)), independent of Marko. There is no Marko setting that fixes it — the reliable workaround is to give each project its own Claude Code config via the `CLAUDE_CONFIG_DIR` environment variable so concurrent instances stop contending on one file.

Add this `claude` wrapper to your shell profile (`~/.zshrc` shown; adapt for bash). It gives each project its own isolated `.claude.json` under `~/.claude-profiles/<project>/` while sharing plugins, skills, commands, hooks, and settings via symlinks. Credentials live in the macOS Keychain and are shared automatically — no re-login per project.

```bash
# Per-project Claude Code config profiles — stops concurrent instances from
# contending on a single ~/.claude.json (which causes MCP servers to flap).
claude() {
emulate -L zsh
local src="$HOME/.claude" globalcfg="$HOME/.claude.json" root profile item
root=$(git rev-parse --show-toplevel 2>/dev/null) || root="$PWD"
profile="$HOME/.claude-profiles/${root:t}"
mkdir -p "$profile"
for item in plugins skills commands hooks settings.json settings.local.json statusline-context.sh config ide; do
[[ -e "$src/$item" && ! -e "$profile/$item" ]] && ln -s "$src/$item" "$profile/$item"
done
# Seed the isolated config once from the real global ~/.claude.json so global
# MCP servers and plugin enablement carry over into the profile.
[[ ! -f "$profile/.claude.json" && -f "$globalcfg" ]] && cp "$globalcfg" "$profile/.claude.json"
CLAUDE_CONFIG_DIR="$profile" command claude "$@"
}
```

Open a new terminal (or `source ~/.zshrc`) and confirm isolation with `claude mcp list` from inside a project — your MCP servers should connect, and `~/.claude.json` should no longer be touched by that instance.

> Note: `CLAUDE_CONFIG_DIR` is honored by the Claude Code CLI but ignored by the VS Code extension, which always uses `~/.claude/`.

## LSP problems

### No completions appearing in the editor
Expand Down