From 3a9b98f1a46261e97137ca0ffaa784102dee30fa Mon Sep 17 00:00:00 2001 From: Misha Skvortsov Date: Thu, 23 Apr 2026 16:18:34 +0300 Subject: [PATCH 1/4] feat: add Atomic Chat MCP tool skill Exposes local Atomic Chat models (OpenAI-compatible API at 127.0.0.1:1337/v1) as tools to the container agent. Adds atomic_chat_list_models and atomic_chat_generate alongside the existing Ollama skill. Rebased on current main: - MCP server registered in agent-runner index.ts using bun (no tsc step in-image), sibling path to index.ts, env: {} with ATOMIC_CHAT_* forwarded when set. - allowedTools entry moved to providers/claude.ts TOOL_ALLOWLIST. - SKILL.md: drop obsolete per-group copy step (single RO mount supersedes it); use pnpm build. Made-with: Cursor Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/add-atomic-chat-tool/SKILL.md | 154 ++++++++++++ .env.example | 7 + .../agent-runner/src/atomic-chat-mcp-stdio.ts | 229 ++++++++++++++++++ container/agent-runner/src/index.ts | 8 + .../agent-runner/src/providers/claude.ts | 1 + src/container-runner.ts | 15 +- 6 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/add-atomic-chat-tool/SKILL.md create mode 100644 container/agent-runner/src/atomic-chat-mcp-stdio.ts diff --git a/.claude/skills/add-atomic-chat-tool/SKILL.md b/.claude/skills/add-atomic-chat-tool/SKILL.md new file mode 100644 index 0000000..d995519 --- /dev/null +++ b/.claude/skills/add-atomic-chat-tool/SKILL.md @@ -0,0 +1,154 @@ +--- +name: add-atomic-chat-tool +description: Add Atomic Chat MCP server so the container agent can call local models served by the Atomic Chat desktop app via its OpenAI-compatible API. +--- + +# Add Atomic Chat Integration + +This skill adds a stdio-based MCP server that exposes models running in the local [Atomic Chat](https://github.com/AtomicBot-ai/Atomic-Chat) desktop app as tools for the container agent. Claude remains the orchestrator but can offload work to local models served by Atomic Chat on `http://127.0.0.1:1337/v1` (OpenAI-compatible). + +Tools exposed: +- `atomic_chat_list_models` — list models currently available in Atomic Chat (`GET /v1/models`) +- `atomic_chat_generate` — send a prompt to a specified model and return the response (`POST /v1/chat/completions`) + +Model management (download, delete) is done through the **Atomic Chat desktop UI** — the app is a fork of Jan and manages its own model library. + +## Phase 1: Pre-flight + +### Check if already applied + +Check if `container/agent-runner/src/atomic-chat-mcp-stdio.ts` exists. If it does, skip to Phase 3 (Configure). + +### Check prerequisites + +Verify Atomic Chat is installed and its local API server is running. On the host: + +```bash +curl -s http://127.0.0.1:1337/v1/models | head +``` + +If the request fails: + +1. Install Atomic Chat from the [latest release](https://github.com/AtomicBot-ai/Atomic-Chat/releases) (macOS only for now — `atomic-chat.dmg`). +2. Open the app. +3. Open **Settings → Local API Server** and make sure it's enabled on port `1337`. +4. Go to the **Hub** (or **Models**) tab and download at least one model (e.g. Llama 3.2 3B, Qwen 2.5 Coder 7B). +5. Load the model once by sending any message in Atomic Chat's UI to warm it up. + +## Phase 2: Apply Code Changes + +### Ensure upstream remote + +```bash +git remote -v +``` + +If `upstream` is missing, add it: + +```bash +git remote add upstream https://github.com/qwibitai/nanoclaw.git +``` + +### Merge the skill branch + +```bash +git fetch upstream skill/atomic-chat-tool +git merge upstream/skill/atomic-chat-tool +``` + +This merges in: +- `container/agent-runner/src/atomic-chat-mcp-stdio.ts` (Atomic Chat MCP server, run directly via `bun`) +- Atomic Chat MCP registration in `container/agent-runner/src/index.ts` (`mcpServers.atomic_chat`) +- `mcp__atomic_chat__*` added to `TOOL_ALLOWLIST` in `container/agent-runner/src/providers/claude.ts` +- `[ATOMIC]` log surfacing and `ATOMIC_CHAT_HOST` / `ATOMIC_CHAT_API_KEY` forwarding in `src/container-runner.ts` +- `ATOMIC_CHAT_HOST` / `ATOMIC_CHAT_API_KEY` stubs in `.env.example` + +If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides. + +### Validate code changes + +```bash +pnpm run build +./container/build.sh +``` + +Build must be clean before proceeding. + +## Phase 3: Configure + +### Set Atomic Chat host (optional) + +By default, the MCP server connects to `http://host.docker.internal:1337` (Docker Desktop) with a fallback to `localhost`. To use a custom host, add to `.env`: + +```bash +ATOMIC_CHAT_HOST=http://your-atomic-chat-host:1337 +``` + +### Set API key (optional) + +Atomic Chat does **not require authentication** when running locally — leave this unset. Only set it if you've put Atomic Chat behind a reverse proxy that enforces auth: + +```bash +ATOMIC_CHAT_API_KEY=sk-... +``` + +### Restart the service + +```bash +launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS +# Linux: systemctl --user restart nanoclaw +``` + +## Phase 4: Verify + +### Test inference + +Tell the user: + +> Send a message like: "use atomic chat to tell me the capital of France" +> +> The agent should use `atomic_chat_list_models` to find available models, then `atomic_chat_generate` to get a response. + +### Check logs if needed + +```bash +tail -f logs/nanoclaw.log | grep -i atomic +``` + +Look for: +- `[ATOMIC] Listing models...` — list request started +- `[ATOMIC] Found N models` — models discovered +- `[ATOMIC] >>> Generating with ` — generation started +- `[ATOMIC] <<< Done: | Xs | N tokens | M chars` — generation completed + +## Troubleshooting + +### Agent says "Atomic Chat is not installed" or tries to run a CLI + +The agent is looking for a CLI that doesn't exist instead of using the MCP tools. This means: +1. The MCP server wasn't registered — check `container/agent-runner/src/index.ts` has the `atomic_chat` entry in `mcpServers` +2. The allowlist wasn't updated — check `container/agent-runner/src/providers/claude.ts` includes `mcp__atomic_chat__*` in `TOOL_ALLOWLIST` +3. The container wasn't rebuilt — run `./container/build.sh` + +### "Failed to connect to Atomic Chat" + +1. Verify the host API is reachable: `curl http://127.0.0.1:1337/v1/models` +2. Confirm the Local API Server is enabled in Atomic Chat's settings +3. Check Docker can reach the host: `docker run --rm curlimages/curl curl -s http://host.docker.internal:1337/v1/models` +4. If using a custom host, check `ATOMIC_CHAT_HOST` in `.env` + +### `model not found` / 404 on generate + +The model ID passed to `atomic_chat_generate` must exactly match one of the IDs returned by `atomic_chat_list_models`. Ask the agent to list models first, then pick one from that list. + +### Slow first response + +Atomic Chat lazy-loads models into memory on first use. The initial call may take longer while the model warms up. Subsequent calls against the same model are fast. + +### Agent doesn't use Atomic Chat tools + +The agent may not know about the tools. Try being explicit: "use the atomic_chat_generate tool with llama3.2-3b-instruct to answer: ..." + +### Context window or output size issues + +Atomic Chat respects each model's native context length. If you hit limits, pass `max_tokens` explicitly when calling `atomic_chat_generate`, or switch to a model with a larger context window in the Atomic Chat UI. diff --git a/.env.example b/.env.example index e69de29..61f2074 100644 --- a/.env.example +++ b/.env.example @@ -0,0 +1,7 @@ +# Atomic Chat MCP tool (skill/atomic-chat-tool) +# Override the host where Atomic Chat exposes its OpenAI-compatible API. +# Default: http://host.docker.internal:1337 (with fallback to localhost) +# ATOMIC_CHAT_HOST=http://host.docker.internal:1337 + +# Optional API key. Leave unset for a local Atomic Chat install — it does not require auth. +# ATOMIC_CHAT_API_KEY= diff --git a/container/agent-runner/src/atomic-chat-mcp-stdio.ts b/container/agent-runner/src/atomic-chat-mcp-stdio.ts new file mode 100644 index 0000000..0198644 --- /dev/null +++ b/container/agent-runner/src/atomic-chat-mcp-stdio.ts @@ -0,0 +1,229 @@ +/** + * Atomic Chat MCP Server for NanoClaw + * Exposes local Atomic Chat models (OpenAI-compatible, /v1) as tools for the container agent. + * Uses host.docker.internal to reach the host's Atomic Chat desktop app from Docker. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; + +import fs from 'fs'; +import path from 'path'; + +const ATOMIC_CHAT_HOST = + process.env.ATOMIC_CHAT_HOST || 'http://host.docker.internal:1337'; +const ATOMIC_CHAT_API_KEY = process.env.ATOMIC_CHAT_API_KEY || ''; +const ATOMIC_CHAT_STATUS_FILE = '/workspace/ipc/atomic_chat_status.json'; + +function log(msg: string): void { + console.error(`[ATOMIC] ${msg}`); +} + +function writeStatus(status: string, detail?: string): void { + try { + const data = { status, detail, timestamp: new Date().toISOString() }; + const tmpPath = `${ATOMIC_CHAT_STATUS_FILE}.tmp`; + fs.mkdirSync(path.dirname(ATOMIC_CHAT_STATUS_FILE), { recursive: true }); + fs.writeFileSync(tmpPath, JSON.stringify(data)); + fs.renameSync(tmpPath, ATOMIC_CHAT_STATUS_FILE); + } catch { + /* best-effort */ + } +} + +async function atomicFetch( + apiPath: string, + options?: RequestInit, +): Promise { + const url = `${ATOMIC_CHAT_HOST}${apiPath}`; + const headers: Record = { + ...((options?.headers as Record) || {}), + }; + if (ATOMIC_CHAT_API_KEY) { + headers.Authorization = `Bearer ${ATOMIC_CHAT_API_KEY}`; + } + const finalOptions: RequestInit = { ...options, headers }; + try { + return await fetch(url, finalOptions); + } catch (err) { + // Fallback to localhost if host.docker.internal fails + if (ATOMIC_CHAT_HOST.includes('host.docker.internal')) { + const fallbackUrl = url.replace('host.docker.internal', 'localhost'); + return await fetch(fallbackUrl, finalOptions); + } + throw err; + } +} + +const server = new McpServer({ + name: 'atomic_chat', + version: '1.0.0', +}); + +server.tool( + 'atomic_chat_list_models', + 'List all models available in the local Atomic Chat desktop app. Use this to see which models are loaded before calling atomic_chat_generate.', + {}, + async () => { + log('Listing models...'); + writeStatus('listing', 'Listing available models'); + try { + const res = await atomicFetch('/v1/models'); + if (!res.ok) { + return { + content: [ + { + type: 'text' as const, + text: `Atomic Chat API error: ${res.status} ${res.statusText}`, + }, + ], + isError: true, + }; + } + + const data = (await res.json()) as { + data?: Array<{ id: string; owned_by?: string }>; + }; + const models = data.data || []; + + if (models.length === 0) { + return { + content: [ + { + type: 'text' as const, + text: 'No models available. Open Atomic Chat on the host and download a model from the Hub.', + }, + ], + }; + } + + const list = models + .map((m) => `- ${m.id}${m.owned_by ? ` (${m.owned_by})` : ''}`) + .join('\n'); + + log(`Found ${models.length} models`); + return { + content: [ + { type: 'text' as const, text: `Available models:\n${list}` }, + ], + }; + } catch (err) { + return { + content: [ + { + type: 'text' as const, + text: `Failed to connect to Atomic Chat at ${ATOMIC_CHAT_HOST}: ${err instanceof Error ? err.message : String(err)}`, + }, + ], + isError: true, + }; + } + }, +); + +server.tool( + 'atomic_chat_generate', + 'Send a prompt to a local Atomic Chat model and get a response. Good for cheaper/faster tasks like summarization, translation, or general queries. Use atomic_chat_list_models first to see available models.', + { + model: z + .string() + .describe( + 'The model ID as returned by atomic_chat_list_models (e.g. "llama3.2-3b-instruct")', + ), + prompt: z.string().describe('The prompt to send to the model'), + system: z + .string() + .optional() + .describe('Optional system prompt to set model behavior'), + temperature: z + .number() + .optional() + .describe('Sampling temperature (0.0–2.0). Defaults to model default.'), + max_tokens: z + .number() + .optional() + .describe('Maximum number of tokens to generate in the response.'), + }, + async (args) => { + log(`>>> Generating with ${args.model} (${args.prompt.length} chars)...`); + writeStatus('generating', `Generating with ${args.model}`); + try { + const messages: Array<{ role: string; content: string }> = []; + if (args.system) { + messages.push({ role: 'system', content: args.system }); + } + messages.push({ role: 'user', content: args.prompt }); + + const body: Record = { + model: args.model, + messages, + stream: false, + }; + if (args.temperature !== undefined) body.temperature = args.temperature; + if (args.max_tokens !== undefined) body.max_tokens = args.max_tokens; + + const startedAt = Date.now(); + const res = await atomicFetch('/v1/chat/completions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const errorText = await res.text(); + return { + content: [ + { + type: 'text' as const, + text: `Atomic Chat error (${res.status}): ${errorText}`, + }, + ], + isError: true, + }; + } + + const data = (await res.json()) as { + choices?: Array<{ message?: { content?: string } }>; + usage?: { + prompt_tokens?: number; + completion_tokens?: number; + total_tokens?: number; + }; + }; + + const response = data.choices?.[0]?.message?.content ?? ''; + const elapsedSec = ((Date.now() - startedAt) / 1000).toFixed(1); + const completionTokens = data.usage?.completion_tokens; + + const meta = `\n\n[${args.model} | ${elapsedSec}s${ + completionTokens !== undefined ? ` | ${completionTokens} tokens` : '' + }]`; + + log( + `<<< Done: ${args.model} | ${elapsedSec}s | ${ + completionTokens ?? '?' + } tokens | ${response.length} chars`, + ); + writeStatus( + 'done', + `${args.model} | ${elapsedSec}s | ${completionTokens ?? '?'} tokens`, + ); + + return { content: [{ type: 'text' as const, text: response + meta }] }; + } catch (err) { + return { + content: [ + { + type: 'text' as const, + text: `Failed to call Atomic Chat: ${err instanceof Error ? err.message : String(err)}`, + }, + ], + isError: true, + }; + } + }, +); + +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 236be4c..2093f9a 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -79,6 +79,14 @@ async function main(): Promise { args: ['run', mcpServerPath], env: {}, }, + atomic_chat: { + command: 'bun', + args: ['run', path.join(__dirname, 'atomic-chat-mcp-stdio.ts')], + env: { + ...(process.env.ATOMIC_CHAT_HOST ? { ATOMIC_CHAT_HOST: process.env.ATOMIC_CHAT_HOST } : {}), + ...(process.env.ATOMIC_CHAT_API_KEY ? { ATOMIC_CHAT_API_KEY: process.env.ATOMIC_CHAT_API_KEY } : {}), + }, + }, }; for (const [name, serverConfig] of Object.entries(config.mcpServers)) { diff --git a/container/agent-runner/src/providers/claude.ts b/container/agent-runner/src/providers/claude.ts index fbb077c..d633c0f 100644 --- a/container/agent-runner/src/providers/claude.ts +++ b/container/agent-runner/src/providers/claude.ts @@ -55,6 +55,7 @@ const TOOL_ALLOWLIST = [ 'Skill', 'NotebookEdit', 'mcp__nanoclaw__*', + 'mcp__atomic_chat__*', ]; interface SDKUserMessage { diff --git a/src/container-runner.ts b/src/container-runner.ts index 71e2064..d92f5ac 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -139,7 +139,12 @@ async function spawnContainer(session: Session): Promise { // Log stderr container.stderr?.on('data', (data) => { for (const line of data.toString().trim().split('\n')) { - if (line) log.debug(line, { container: agentGroup.folder }); + if (!line) continue; + if (line.includes('[ATOMIC]')) { + log.info(line, { container: agentGroup.folder }); + } else { + log.debug(line, { container: agentGroup.folder }); + } } }); @@ -396,6 +401,14 @@ async function buildContainerArgs( // Everything NanoClaw-specific is in container.json (read by runner at startup). args.push('-e', `TZ=${TIMEZONE}`); + // Atomic Chat MCP tool: forward host overrides if set (default is host.docker.internal:1337). + if (process.env.ATOMIC_CHAT_HOST) { + args.push('-e', `ATOMIC_CHAT_HOST=${process.env.ATOMIC_CHAT_HOST}`); + } + if (process.env.ATOMIC_CHAT_API_KEY) { + args.push('-e', `ATOMIC_CHAT_API_KEY=${process.env.ATOMIC_CHAT_API_KEY}`); + } + // Provider-contributed env vars (e.g. XDG_DATA_HOME, OPENCODE_*, NO_PROXY). if (providerContribution.env) { for (const [key, value] of Object.entries(providerContribution.env)) { From 97e356d243d7285b00bb2f5ca236349e9b98a02c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Apr 2026 13:21:49 +0000 Subject: [PATCH 2/4] chore: bump version to 2.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09053c4..aa63756 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "2.0.5", + "version": "2.0.6", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "packageManager": "pnpm@10.33.0", From dd5bc85b02656fdaf8304c91518da151b139204f Mon Sep 17 00:00:00 2001 From: gavrielc Date: Thu, 23 Apr 2026 16:29:10 +0300 Subject: [PATCH 3/4] refactor(skill/atomic-chat-tool): ship MCP file in skill folder, revert src edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial /add-atomic-chat-tool merge added src edits directly to main. That conflicts with the utility-skill pattern used elsewhere (e.g. /claw): the skill folder should ship the file and SKILL.md should instruct copy + idempotent edits at install time, not a git merge that carries src diffs. - Move container/agent-runner/src/atomic-chat-mcp-stdio.ts → .claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts - Revert the atomic_chat mcpServers entry in agent-runner index.ts - Revert mcp__atomic_chat__* from TOOL_ALLOWLIST in providers/claude.ts - Revert ATOMIC_CHAT_* env forwarding and [ATOMIC] log elevation in src/container-runner.ts - Empty .env.example back out - Rewrite SKILL.md: copy the shipped file, then apply deterministic Edits (index.ts, providers/claude.ts, container-runner.ts, .env.example) with exact before/after snippets the installer agent can match. Main is now back to its pre-PR state for the tool; /add-atomic-chat-tool re-applies everything at install time. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/add-atomic-chat-tool/SKILL.md | 137 +++++++++++++++--- .../atomic-chat-mcp-stdio.ts | 0 .env.example | 7 - container/agent-runner/src/index.ts | 8 - .../agent-runner/src/providers/claude.ts | 1 - src/container-runner.ts | 15 +- 6 files changed, 114 insertions(+), 54 deletions(-) rename {container/agent-runner/src => .claude/skills/add-atomic-chat-tool}/atomic-chat-mcp-stdio.ts (100%) diff --git a/.claude/skills/add-atomic-chat-tool/SKILL.md b/.claude/skills/add-atomic-chat-tool/SKILL.md index d995519..6a6d858 100644 --- a/.claude/skills/add-atomic-chat-tool/SKILL.md +++ b/.claude/skills/add-atomic-chat-tool/SKILL.md @@ -13,6 +13,8 @@ Tools exposed: Model management (download, delete) is done through the **Atomic Chat desktop UI** — the app is a fork of Jan and manages its own model library. +The skill ships the MCP server source in this folder and copies it into the agent-runner tree at install time, then wires it up with small edits to `index.ts`, `providers/claude.ts`, and `container-runner.ts`. No branch merge — all edits are additive and idempotent. + ## Phase 1: Pre-flight ### Check if already applied @@ -37,42 +39,128 @@ If the request fails: ## Phase 2: Apply Code Changes -### Ensure upstream remote +### Copy the MCP server source ```bash -git remote -v +cp .claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts container/agent-runner/src/atomic-chat-mcp-stdio.ts ``` -If `upstream` is missing, add it: +### Register the MCP server in the agent-runner + +Edit `container/agent-runner/src/index.ts`. Find the `mcpServers` object that currently looks like this: + +```ts + const mcpServers: Record }> = { + nanoclaw: { + command: 'bun', + args: ['run', mcpServerPath], + env: {}, + }, + }; +``` + +Add an `atomic_chat` entry alongside `nanoclaw`: + +```ts + const mcpServers: Record }> = { + nanoclaw: { + command: 'bun', + args: ['run', mcpServerPath], + env: {}, + }, + atomic_chat: { + command: 'bun', + args: ['run', path.join(__dirname, 'atomic-chat-mcp-stdio.ts')], + env: { + ...(process.env.ATOMIC_CHAT_HOST ? { ATOMIC_CHAT_HOST: process.env.ATOMIC_CHAT_HOST } : {}), + ...(process.env.ATOMIC_CHAT_API_KEY ? { ATOMIC_CHAT_API_KEY: process.env.ATOMIC_CHAT_API_KEY } : {}), + }, + }, + }; +``` + +### Add the tool glob to the allowlist + +Edit `container/agent-runner/src/providers/claude.ts`. Find `'mcp__nanoclaw__*',` in the `TOOL_ALLOWLIST` array and add `'mcp__atomic_chat__*',` on the following line: + +```ts + 'mcp__nanoclaw__*', + 'mcp__atomic_chat__*', +]; +``` + +### Forward host env vars into the container + +Edit `src/container-runner.ts` in `buildContainerArgs`. Find the `TZ` env line: + +```ts + args.push('-e', `TZ=${TIMEZONE}`); +``` + +Add ATOMIC_CHAT forwarding right after it: + +```ts + args.push('-e', `TZ=${TIMEZONE}`); + + // Atomic Chat MCP tool: forward host overrides if set (default is host.docker.internal:1337). + if (process.env.ATOMIC_CHAT_HOST) { + args.push('-e', `ATOMIC_CHAT_HOST=${process.env.ATOMIC_CHAT_HOST}`); + } + if (process.env.ATOMIC_CHAT_API_KEY) { + args.push('-e', `ATOMIC_CHAT_API_KEY=${process.env.ATOMIC_CHAT_API_KEY}`); + } +``` + +### Surface `[ATOMIC]` log lines at info level + +In the same file, find the stderr logger: + +```ts + container.stderr?.on('data', (data) => { + for (const line of data.toString().trim().split('\n')) { + if (line) log.debug(line, { container: agentGroup.folder }); + } + }); +``` + +Replace it with: + +```ts + container.stderr?.on('data', (data) => { + for (const line of data.toString().trim().split('\n')) { + if (!line) continue; + if (line.includes('[ATOMIC]')) { + log.info(line, { container: agentGroup.folder }); + } else { + log.debug(line, { container: agentGroup.folder }); + } + } + }); +``` + +### Add env-var stubs to `.env.example` + +Append to `.env.example`: ```bash -git remote add upstream https://github.com/qwibitai/nanoclaw.git +# Atomic Chat MCP tool (.claude/skills/add-atomic-chat-tool) +# Override the host where Atomic Chat exposes its OpenAI-compatible API. +# Default: http://host.docker.internal:1337 (with fallback to localhost) +# ATOMIC_CHAT_HOST=http://host.docker.internal:1337 + +# Optional API key. Leave unset for a local Atomic Chat install — it does not require auth. +# ATOMIC_CHAT_API_KEY= ``` -### Merge the skill branch - -```bash -git fetch upstream skill/atomic-chat-tool -git merge upstream/skill/atomic-chat-tool -``` - -This merges in: -- `container/agent-runner/src/atomic-chat-mcp-stdio.ts` (Atomic Chat MCP server, run directly via `bun`) -- Atomic Chat MCP registration in `container/agent-runner/src/index.ts` (`mcpServers.atomic_chat`) -- `mcp__atomic_chat__*` added to `TOOL_ALLOWLIST` in `container/agent-runner/src/providers/claude.ts` -- `[ATOMIC]` log surfacing and `ATOMIC_CHAT_HOST` / `ATOMIC_CHAT_API_KEY` forwarding in `src/container-runner.ts` -- `ATOMIC_CHAT_HOST` / `ATOMIC_CHAT_API_KEY` stubs in `.env.example` - -If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides. - ### Validate code changes ```bash pnpm run build +pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit ./container/build.sh ``` -Build must be clean before proceeding. +All three must be clean before proceeding. ## Phase 3: Configure @@ -126,9 +214,10 @@ Look for: ### Agent says "Atomic Chat is not installed" or tries to run a CLI The agent is looking for a CLI that doesn't exist instead of using the MCP tools. This means: -1. The MCP server wasn't registered — check `container/agent-runner/src/index.ts` has the `atomic_chat` entry in `mcpServers` -2. The allowlist wasn't updated — check `container/agent-runner/src/providers/claude.ts` includes `mcp__atomic_chat__*` in `TOOL_ALLOWLIST` -3. The container wasn't rebuilt — run `./container/build.sh` +1. The MCP server wasn't copied — check `container/agent-runner/src/atomic-chat-mcp-stdio.ts` exists +2. The MCP server wasn't registered — check `container/agent-runner/src/index.ts` has the `atomic_chat` entry in `mcpServers` +3. The allowlist wasn't updated — check `container/agent-runner/src/providers/claude.ts` includes `mcp__atomic_chat__*` in `TOOL_ALLOWLIST` +4. The container wasn't rebuilt — run `./container/build.sh` ### "Failed to connect to Atomic Chat" diff --git a/container/agent-runner/src/atomic-chat-mcp-stdio.ts b/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts similarity index 100% rename from container/agent-runner/src/atomic-chat-mcp-stdio.ts rename to .claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts diff --git a/.env.example b/.env.example index 61f2074..e69de29 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +0,0 @@ -# Atomic Chat MCP tool (skill/atomic-chat-tool) -# Override the host where Atomic Chat exposes its OpenAI-compatible API. -# Default: http://host.docker.internal:1337 (with fallback to localhost) -# ATOMIC_CHAT_HOST=http://host.docker.internal:1337 - -# Optional API key. Leave unset for a local Atomic Chat install — it does not require auth. -# ATOMIC_CHAT_API_KEY= diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 2093f9a..236be4c 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -79,14 +79,6 @@ async function main(): Promise { args: ['run', mcpServerPath], env: {}, }, - atomic_chat: { - command: 'bun', - args: ['run', path.join(__dirname, 'atomic-chat-mcp-stdio.ts')], - env: { - ...(process.env.ATOMIC_CHAT_HOST ? { ATOMIC_CHAT_HOST: process.env.ATOMIC_CHAT_HOST } : {}), - ...(process.env.ATOMIC_CHAT_API_KEY ? { ATOMIC_CHAT_API_KEY: process.env.ATOMIC_CHAT_API_KEY } : {}), - }, - }, }; for (const [name, serverConfig] of Object.entries(config.mcpServers)) { diff --git a/container/agent-runner/src/providers/claude.ts b/container/agent-runner/src/providers/claude.ts index d633c0f..fbb077c 100644 --- a/container/agent-runner/src/providers/claude.ts +++ b/container/agent-runner/src/providers/claude.ts @@ -55,7 +55,6 @@ const TOOL_ALLOWLIST = [ 'Skill', 'NotebookEdit', 'mcp__nanoclaw__*', - 'mcp__atomic_chat__*', ]; interface SDKUserMessage { diff --git a/src/container-runner.ts b/src/container-runner.ts index d92f5ac..71e2064 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -139,12 +139,7 @@ async function spawnContainer(session: Session): Promise { // Log stderr container.stderr?.on('data', (data) => { for (const line of data.toString().trim().split('\n')) { - if (!line) continue; - if (line.includes('[ATOMIC]')) { - log.info(line, { container: agentGroup.folder }); - } else { - log.debug(line, { container: agentGroup.folder }); - } + if (line) log.debug(line, { container: agentGroup.folder }); } }); @@ -401,14 +396,6 @@ async function buildContainerArgs( // Everything NanoClaw-specific is in container.json (read by runner at startup). args.push('-e', `TZ=${TIMEZONE}`); - // Atomic Chat MCP tool: forward host overrides if set (default is host.docker.internal:1337). - if (process.env.ATOMIC_CHAT_HOST) { - args.push('-e', `ATOMIC_CHAT_HOST=${process.env.ATOMIC_CHAT_HOST}`); - } - if (process.env.ATOMIC_CHAT_API_KEY) { - args.push('-e', `ATOMIC_CHAT_API_KEY=${process.env.ATOMIC_CHAT_API_KEY}`); - } - // Provider-contributed env vars (e.g. XDG_DATA_HOME, OPENCODE_*, NO_PROXY). if (providerContribution.env) { for (const [key, value] of Object.entries(providerContribution.env)) { From 438dedad77c27adf648d20cf1f4a508eb806d3a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Apr 2026 13:30:51 +0000 Subject: [PATCH 4/4] chore: bump version to 2.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa63756..77920c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "2.0.6", + "version": "2.0.7", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "packageManager": "pnpm@10.33.0",