diff --git a/PORTABILITY.md b/PORTABILITY.md new file mode 100644 index 0000000..a9a0e1f --- /dev/null +++ b/PORTABILITY.md @@ -0,0 +1,173 @@ +# Portability Guide: Can Others Use This? + +## TL;DR + +**Core functionality (chat, agent_chat)**: ✅ Fully portable +**SSH execution**: ⚠️ Requires configuration per user +**95% savings claims**: ⚠️ Depends on use case and local LLM quality + +--- + +## What's Portable Out of the Box + +### 1. Basic Chat with Local LLM +Anyone with a llama.cpp server can use: +```javascript +mcp__llama-local__chat({ message: "Hello" }) +``` +**Requirements:** Just `LLAMA_SERVER_URL` environment variable. + +### 2. Agent Chat (Autonomous Loop) +The core agent architecture works for anyone: +```javascript +mcp__llama-local__agent_chat({ task: "Analyze this data..." }) +``` +**Requirements:** Same as above. + +### 3. Health Check +```javascript +mcp__llama-local__health_check() +``` +**Requirements:** None beyond basic setup. + +--- + +## What Requires Configuration + +### SSH Execution + +The `ssh_exec` tool requires users to configure their own hosts. + +**Option 1: GPG-encrypted credentials (recommended)** +```bash +# Create ~/.claude/credentials.json +{ + "ssh_hosts": { + "192.168.1.100": { "user": "admin", "password": "secret" }, + "myserver.local": { "user": "root" } + } +} + +# Encrypt it +gpg -c ~/.claude/credentials.json +rm ~/.claude/credentials.json +``` + +**Option 2: Environment variables** +```bash +export SSH_HOST_192_168_1_100='{"user":"admin","password":"secret"}' +``` + +**Option 3: SSH keys (no password needed)** +If user has SSH keys configured, just specify the user: +```json +{ "192.168.1.100": { "user": "admin" } } +``` + +--- + +## Minimum Requirements + +| Component | Minimum | Recommended | +|-----------|---------|-------------| +| **Node.js** | 18+ | 20+ | +| **llama.cpp server** | Any version | Recent (for performance) | +| **Local LLM** | 7B (basic tasks) | 32B+ (complex analysis) | +| **Context window** | 8K | 32K-128K | +| **RAM** | 8GB (7B Q4) | 64GB+ (70B+ models) | +| **Claude Code/Desktop** | Any with MCP | Latest | + +--- + +## What Won't Transfer + +### 1. Specific Host Configurations +My SSH hosts (192.168.0.165, etc.) won't work for others. They need to configure their own. + +### 2. Token Savings Numbers +The 95% savings depend on: +- Task type (data-heavy = more savings) +- Local LLM quality (better model = better summaries) +- Use patterns (lots of log analysis = high ROI) + +Others may see 50-95% depending on their use case. + +### 3. Specific Prompting Patterns +The prompts that work well for my infrastructure debugging may need adjustment for different domains. + +--- + +## Quick Start for New Users + +```bash +# 1. Clone and build +git clone https://github.com/lambertmt/llama-mcp-server +cd llama-mcp-server +git checkout feature/agent-tool-calling +npm install && npm run build + +# 2. Configure Claude Code (~/.claude/config.json) +{ + "mcpServers": { + "llama-local": { + "command": "node", + "args": ["/absolute/path/to/dist/index.js"], + "env": { + "LLAMA_SERVER_URL": "http://localhost:8080" + } + } + } +} + +# 3. Start your llama.cpp server +./llama-server -m your-model.gguf -c 32768 --port 8080 + +# 4. (Optional) Configure SSH hosts +# Create ~/.claude/credentials.json with your hosts +# Then: gpg -c ~/.claude/credentials.json && rm ~/.claude/credentials.json + +# 5. Test +# In Claude Code: "Use llama-local to check if the server is healthy" +``` + +--- + +## Artifacts Others Can Use Directly + +| Artifact | Location | Portable? | +|----------|----------|-----------| +| MCP Server code | `/src/index.ts` | ✅ Yes | +| Agent loop implementation | `agent_chat` handler | ✅ Yes | +| JSON parsing utilities | `parseToolCall`, etc. | ✅ Yes | +| Health check script | `/test-scripts/health_check.sh` | ⚠️ Adapt paths | +| Video script template | `/video-script.md` | ✅ Yes (as template) | +| Reddit/blog posts | `/posts/` | ✅ Yes (as templates) | + +--- + +## Recommended Model Sizes by Task + +| Task | Minimum Model | Notes | +|------|---------------|-------| +| Simple commands | 7B | Basic SSH + summary | +| Log analysis | 13B-32B | Needs pattern recognition | +| Security audit | 32B+ | Complex reasoning | +| Multi-step debugging | 70B+ | Best results | + +--- + +## Known Issues for Portability + +1. **Windows**: SSH execution uses `sshpass` which is Linux/Mac. Windows users need WSL or alternative. + +2. **Firewalls**: SSH hosts must be reachable from where the MCP server runs. + +3. **Model quality**: Smaller models may not follow the strict output format, causing parsing failures. + +4. **Token counting**: The `tokens_used` in responses is from the local LLM's tokenizer, not Claude's. Comparisons are approximate. + +--- + +## Contributing + +If you adapt this for a different use case (Kubernetes, cloud providers, databases), PRs welcome! diff --git a/README.md b/README.md index 55d1b03..e6961b7 100644 --- a/README.md +++ b/README.md @@ -1,430 +1,324 @@ -# LibreModel MCP Server 🤖 +# LibreModel MCP Server -A Model Context Protocol (MCP) server that bridges Claude Desktop with your local LLM instance running via llama-server. +A Model Context Protocol (MCP) server that bridges Claude Desktop/Claude Code with your local LLM instance running via llama-server. ## Features -- 💬 **Full conversation support** with Local Model through Claude Desktop -- 🎛️ **Complete parameter control** (temperature, max_tokens, top_p, top_k) -- ✅ **Health monitoring** and server status checks -- 🧪 **Built-in testing tools** for different capabilities -- 📊 **Performance metrics** and token usage tracking -- 🔧 **Easy configuration** via environment variables +- **Autonomous agent execution** - local LLM executes tools directly, no Claude middleman +- **Massive Claude token savings** - up to 95% reduction on analysis tasks +- **Built-in SSH execution** - agent can run commands on remote servers +- **Full conversation support** with local LLMs through Claude +- **GPG-encrypted credentials** - secure SSH host configuration +- **Unlimited local tokens** - designed for large context models (128K+) -## Quick Start +## Why This Matters: Claude Token Savings - npm install @openconstruct/llama-mcp-server +When Claude analyzes large outputs (logs, disk usage, etc.), every character burns API tokens. This MCP server offloads that work to your **free local LLM**. +### How the Token Math Works -A Model Context Protocol (MCP) server that bridges Claude Desktop with your local LLM instance running via llama-server. +**Claude Direct** (no agent) - security audit example: +- Raw SSH output: ~44,000 chars ≈ 11,000 tokens +- Conversation overhead: ~800 tokens +- **Total Claude tokens: ~11,800** -## Features +**Claude w/ Agent** - same task: +- Task request to agent: ~100 tokens +- Agent's summary response: ~700 tokens +- **Total Claude tokens: ~800** -- 💬 **Full conversation support** with LibreModel through Claude Desktop -- 🎛️ **Complete parameter control** (temperature, max_tokens, top_p, top_k) -- ✅ **Health monitoring** and server status checks -- 🧪 **Built-in testing tools** for different capabilities -- 📊 **Performance metrics** and token usage tracking -- 🔧 **Easy configuration** via environment variables +**Local LLM** (inside agent, FREE): +- Processes ~44,000 chars raw output: ~11,000 tokens +- Analysis and formatting: ~1,000 tokens +- **Total local tokens: ~12,000** -## Quick Start +The tokens don't disappear - they move from Claude (paid) to your local LLM (free). The work gets done, you just don't pay for it. -### 1. Install Dependencies - -```bash -cd llama-mcp -npm install -``` +### Actual Test Results -### 2. Build the Server +| Task | Claude (Direct) | Claude (w/ Agent) | Local LLM (free) | Savings | +|------|-----------------|-------------------|------------------|---------| +| **Debugging workflow (7 calls)** | **~56,000** | **~4,100** | **~35,000** | **93%** | +| **Security audit** | **~11,800** | **~800** | **~11,000** | **93%** | +| **Docker logs analysis** | **~10,500** | **~500** | **~10,000** | **95%** | +| System health check | ~5,500 | ~1,500 | ~4,000 | 73% | +| Log analysis (journalctl) | ~4,000 | ~800 | ~3,200 | 80% | +| Code gen (w/ exploration) | ~2,700 | ~1,700 | ~1,000 | 37% | +| Disk analysis | ~1,500 | ~500 | ~1,000 | 65% | +| Code gen (small input) | ~1,550 | ~1,600 | ~1,500 | 0% | +| Simple query (hostname) | ~500 | ~300 | ~200 | 40% | -```bash -npm run build -``` +**When it doesn't help:** Code generation with small inputs (0% savings) - the output dominates token count either way. The agent shines when raw data is large. -### 3. Start Your LibreModel +### Real-World Example: Debugging Nextcloud Talk -Make sure llama-server is running with your model: +A complete debugging session - Nextcloud Talk returning HTTP 400 errors: -```bash -./llama-server -m lm37.gguf -c 2048 --port 8080 ``` +7 agent calls over ~10 minutes: + 1. Check signaling + parse logs → "Config OK, no errors" + 2. Check rate limits + DB → "Rate limiting on, perms OK" + 3. Enable debug, get stack trace → "SSL cert not trusted" + 4. Add cert to trust store → "HTTP 201 - fixed!" -### 4. Configure Claude Desktop - -Add this to your Claude Desktop configuration (`~/.config/claude/claude_desktop_config.json`): - -```json -{ - "mcpServers": { - "libremodel": { - "command": "node", - "args": ["/home/jerr/llama-mcp/dist/index.js"] - } - } -} +Total Claude tokens (direct): ~56,000 +Total Claude tokens (w/ agent): ~4,100 +Tokens saved: ~52,000 (93%) ``` -### 5. Restart Claude Desktop +Claude stayed strategic (decided what to check), agent did tactical execution (SSH, log parsing, DB queries). -Claude will now have access to LibreModel through MCP! +### Real Test: Security Audit -## Usage - -Once configured, you can use these tools in Claude Desktop: - -### 💬 `chat` - Main conversation tool -``` -Use the chat tool to ask LibreModel: "What is your name and what can you do?" ``` +Task: "Analyze SSH logs, sudo usage, and check for suspicious activity on 192.168.0.165" -### 🧪 `quick_test` - Test LibreModel capabilities -``` -Run a quick_test with type "creative" to see if LibreModel can write poetry -``` +Agent internally executed: + - ssh_exec: "journalctl -u sshd -n 200; journalctl _COMM=sudo -n 100; ss -tuln" + - Raw output: 43,821 characters (Claude NEVER saw this) + - Local LLM tokens: ~11,000 (free) -### 🏥 `health_check` - Monitor server status +Claude received: Security summary with severity ratings (~800 tokens) +Savings: 93% reduction in Claude API tokens ``` -Use health_check to see if LibreModel is running properly -``` - -## Configuration -Set environment variables to customize behavior: +## Installation ```bash -export LLAMA_SERVER_URL="http://localhost:8080" # Default llama-server URL +npm install @openconstruct/llama-mcp-server ``` -## Available Tools - -| Tool | Description | Parameters | -|------|-------------|------------| -| `chat` | Converse with MOdel | `message`, `temperature`, `max_tokens`, `top_p`, `top_k`, `system_prompt` | -| `quick_test` | Run predefined capability tests | `test_type` (hello/math/creative/knowledge) | -| `health_check` | Check server health and status | None | - -## Resources - -- **Configuration**: View current server settings -- **Instructions**: Detailed usage guide and setup instructions - -## Development - -```bash -# Install dependencies -npm install # LibreModel MCP Server 🤖 - -A Model Context Protocol (MCP) server that bridges Claude Desktop with your local LLM instance running via llama-server. - -## Features - -- 💬 **Full conversation support** with LibreModel through Claude Desktop -- 🎛️ **Complete parameter control** (temperature, max_tokens, top_p, top_k) -- ✅ **Health monitoring** and server status checks -- 🧪 **Built-in testing tools** for different capabilities -- 📊 **Performance metrics** and token usage tracking -- 🔧 **Easy configuration** via environment variables - -## Quick Start - -### 1. Install Dependencies +Or clone and build from source: ```bash -cd llama-mcp +git clone https://github.com/lambertmt/llama-mcp-server.git +cd llama-mcp-server npm install -``` - -### 2. Build the Server - -```bash npm run build ``` -### 3. Start Your LibreModel +## Quick Start -Make sure llama-server is running with your model: +### 1. Start Your LLM Server ```bash -./llama-server -m lm37.gguf -c 2048 --port 8080 +# Example with llama.cpp server (128K context for full analysis capability) +./llama-server -m your-model.gguf -c 131072 --port 8080 ``` -### 4. Configure Claude Desktop +### 2. Configure Claude Code -Add this to your Claude Desktop configuration (`~/.config/claude/claude_desktop_config.json`): +Add to `~/.claude.json`: ```json { "mcpServers": { - "libremodel": { + "llama-local": { + "type": "stdio", "command": "node", - "args": ["/home/jerr/llama-mcp/dist/index.js"] + "args": ["/path/to/llama-mcp-server/dist/index.js"], + "env": { + "LLAMA_SERVER_URL": "http://localhost:8080", + "GPG_PASSPHRASE": "your-gpg-passphrase" + } } } } ``` -### 5. Restart Claude Desktop - -Claude will now have access to LibreModel through MCP! - -## Usage +### 3. Configure SSH Hosts (Optional) -Once configured, you can use these tools in Claude Desktop: +Create `~/.claude/credentials.json.gpg` with your SSH hosts: -### 💬 `chat` - Main conversation tool -``` -Use the chat tool to ask LibreModel: "What is your name and what can you do?" -``` - -### 🧪 `quick_test` - Test LibreModel capabilities -``` -Run a quick_test with type "creative" to see if LibreModel can write poetry -``` - -### 🏥 `health_check` - Monitor server status -``` -Use health_check to see if LibreModel is running properly +```json +{ + "ssh_hosts": { + "192.168.0.165": { "user": "admin", "password": "secret" }, + "192.168.0.13": { "user": "root" } + } +} ``` -## Configuration - -Set environment variables to customize behavior: - -```bash -export LLAMA_SERVER_URL="http://localhost:8080" # Default llama-server URL -``` +Encrypt with: `gpg -c ~/.claude/credentials.json` ## Available Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `chat` | Converse with MOdel | `message`, `temperature`, `max_tokens`, `top_p`, `top_k`, `system_prompt` | -| `quick_test` | Run predefined capability tests | `test_type` (hello/math/creative/knowledge) | -| `health_check` | Check server health and status | None | - -## Resources - -- **Configuration**: View current server settings -- **Instructions**: Detailed usage guide and setup instructions - -## Development - -```bash -# Install dependencies -npm install openconstruct/llama-mcp-server - - -# Development mode (auto-rebuild) -npm run dev - -# Build for production -npm run build - -# Start the server directly -npm start -``` - -## Architecture - -``` -Claude Desktop ←→ LLama MCP Server ←→ llama-server API ←→ Local Model -``` - -The MCP server acts as a bridge, translating MCP protocol messages into llama-server API calls and formatting responses for Claude Desktop. - -## Troubleshooting - -**"Cannot reach LLama server"** -- Ensure llama-server is running on the configured port -- Check that the model is loaded and responding -- Verify firewall/network settings - -**"Tool not found in Claude Desktop"** -- Restart Claude Desktop after configuration changes -- Check that the path to `index.js` is correct and absolute -- Verify the MCP server builds without errors - -**Poor response quality** -- Adjust temperature and sampling parameters -- Try different system prompts - -## License - -CC0-1.0 - Public Domain. Use freely! - ---- - -Built with ❤️ for open-source AI and the LibreModel project. by Claude Sonnet4 - - -# Development mode (auto-rebuild) -npm run dev - -# Build for production -npm run build - -# Start the server directly -npm start -``` - -## Architecture - -``` -Claude Desktop ←→ LLama MCP Server ←→ llama-server API ←→ Local Model +| Tool | Description | +|------|-------------| +| `agent_chat` | **Autonomous agent** - executes tools internally, returns only final answer | +| `ssh_exec` | Execute commands on remote servers (also available as agent built-in) | +| `chat` | Simple conversation with the local model | +| `health_check` | Check llama-server status | +| `quick_test` | Run capability tests | + +## Autonomous Agent (`agent_chat`) + +The killer feature. One call to Claude, the local LLM handles everything internally. + +### How It Works + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Claude (Orchestrator) │ +│ │ +│ 1. Sends task to agent_chat │ +│ 2. Waits... │ +│ 3. Receives final_answer (only the analysis, not raw data) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ MCP Server (Autonomous Agent Loop) │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Local LLM reasons about task │ │ +│ │ ↓ │ │ +│ │ Requests tool: ssh_exec("df -h") │ │ +│ │ ↓ │ │ +│ │ MCP executes SSH internally (Claude never sees) │ │ +│ │ ↓ │ │ +│ │ Local LLM analyzes 15KB of output │ │ +│ │ ↓ │ │ +│ │ Returns concise final answer │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Usage + +```typescript +// One call - agent handles everything +agent_chat({ + task: "Check disk usage on 192.168.0.165 and report partitions over 50% full" +}) +``` + +**Response:** +```json +{ + "type": "final_answer", + "conversation_id": "conv_abc123", + "content": "Partitions over 50%:\n- /boot: 53%\n- /mnt/nas-music: 67%\n- /mnt/nas-backup: 67%", + "tokens_used": 253, + "tools_executed": [ + { + "tool": "ssh_exec", + "args": { "host": "192.168.0.165", "command": "df -h" }, + "result_length": 1243 + } + ] +} ``` -The MCP server acts as a bridge, translating MCP protocol messages into llama-server API calls and formatting responses for Claude Desktop. +Note: `result_length: 1243` - that's 1,243 characters Claude **never had to process**. -## Troubleshooting - -**"Cannot reach LLama server"** -- Ensure llama-server is running on the configured port -- Check that the model is loaded and responding -- Verify firewall/network settings - -**"Tool not found in Claude Desktop"** -- Restart Claude Desktop after configuration changes -- Check that the path to `index.js` is correct and absolute -- Verify the MCP server builds without errors - -**Poor response quality** -- Adjust temperature and sampling parameters -- Try different system prompts - -## License +### Parameters -CC0-1.0 - Public Domain. Use freely! +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `task` | string | required | The task for the agent | +| `auto_execute` | boolean | `true` | Execute built-in tools internally | +| `max_iterations` | number | `10` | Max tool execution loops | +| `temperature` | number | `0.3` | Lower = more focused | +| `context` | string | `""` | Additional context/instructions | ---- +### Strict Output Format -Built with ❤️ for open-source AI and the LibreModel project. by Claude Sonnet4 +The agent follows strict formatting rules: +- **Tool calls**: Pure JSON only, no surrounding text +- **Final answers**: Plain text only, no JSON wrapping +- No "let me think" or "I'll analyze" preamble -### 1. Install Dependencies +## SSH Execution -```bash -cd llama-mcp -npm install -``` +Built-in SSH support for infrastructure management. -### 2. Build the Server +### Direct Usage -```bash -npm run build +```typescript +ssh_exec({ + host: "192.168.0.165", + command: "docker ps" +}) ``` -### 3. Start Your LibreModel +### As Agent Tool -Make sure llama-server is running with your model: +The agent automatically has access to `ssh_exec` for configured hosts: -```bash -./llama-server -m lm37.gguf -c 2048 --port 8080 +```typescript +agent_chat({ + task: "Check memory usage on all servers and identify any issues" +}) +// Agent will autonomously SSH to hosts and analyze results ``` -### 4. Configure Claude Desktop - -Add this to your Claude Desktop configuration (`~/.config/claude/claude_desktop_config.json`): - -```json -{ - "mcpServers": { - "libremodel": { - "command": "node", - "args": ["/home/jerr/llama-mcp/dist/index.js"] - } - } -} -``` +## Configuration -### 5. Restart Claude Desktop +### Environment Variables -Claude will now have access to LibreModel through MCP! +| Variable | Description | +|----------|-------------| +| `LLAMA_SERVER_URL` | llama-server endpoint (default: `http://localhost:8080`) | +| `GPG_PASSPHRASE` | Passphrase for encrypted credentials file | +| `DEBUG_MCP` | Set to `1` for detailed logging | -## Usage +### Credentials File -Once configured, you can use these tools in Claude Desktop: +SSH hosts can be configured via: +1. `~/.claude/credentials.json.gpg` (encrypted, recommended) +2. `~/.claude/credentials.json` (plaintext) +3. Environment variables: `SSH_HOST_192_168_0_165='{"user":"admin"}'` -### 💬 `chat` - Main conversation tool -``` -Use the chat tool to ask LibreModel: "What is your name and what can you do?" -``` +## Architecture -### 🧪 `quick_test` - Test LibreModel capabilities ``` -Run a quick_test with type "creative" to see if LibreModel can write poetry +Claude ←→ MCP Protocol ←→ llama-mcp-server ←→ llama-server ←→ Local LLM + │ + └──→ SSH (internal execution) ``` -### 🏥 `health_check` - Monitor server status -``` -Use health_check to see if LibreModel is running properly -``` +## Performance Comparison -## Configuration +Tested with GPT-OSS 120B (Q8) on AMD Strix Halo, 128K context: -Set environment variables to customize behavior: +| Scenario | Time | Claude Tokens | Local Tokens | +|----------|------|---------------|--------------| +| Docker logs (Claude direct) | ~45s | ~10,500 | 0 | +| Docker logs (autonomous agent) | ~45s | ~500 | ~10,000 | +| Security audit (Claude direct) | ~60s | ~11,800 | 0 | +| Security audit (autonomous agent) | ~60s | ~800 | ~11,000 | -```bash -export LLAMA_SERVER_URL="http://localhost:8080" # Default llama-server URL -``` +**Result**: Same speed, up to 95% Claude token reduction. The work shifts to your free local LLM. -## Available Tools +## Troubleshooting -| Tool | Description | Parameters | -|------|-------------|------------| -| `chat` | Converse with MOdel | `message`, `temperature`, `max_tokens`, `top_p`, `top_k`, `system_prompt` | -| `quick_test` | Run predefined capability tests | `test_type` (hello/math/creative/knowledge) | -| `health_check` | Check server health and status | None | +**"Cannot reach server"** +- Verify llama-server is running: `curl http://localhost:8080/health` +- Check firewall allows the port -## Resources +**Agent not executing tools** +- Ensure `auto_execute: true` (default) +- Check SSH hosts are configured in credentials file +- Enable `DEBUG_MCP=1` for detailed logs -- **Configuration**: View current server settings -- **Instructions**: Detailed usage guide and setup instructions +**Tool calls malformed** +- Lower temperature to 0.1-0.3 +- Ensure model supports instruction following +- Check logs for JSON parsing errors ## Development ```bash -# Install dependencies npm install - -# Development mode (auto-rebuild) -npm run dev - -# Build for production npm run build - -# Start the server directly -npm start -``` - -## Architecture - -``` -Claude Desktop ←→ LLama MCP Server ←→ llama-server API ←→ Local Model +DEBUG_MCP=1 npm start # Run with logging ``` -The MCP server acts as a bridge, translating MCP protocol messages into llama-server API calls and formatting responses for Claude Desktop. - -## Troubleshooting - -**"Cannot reach LLama server"** -- Ensure llama-server is running on the configured port -- Check that the model is loaded and responding -- Verify firewall/network settings - -**"Tool not found in Claude Desktop"** -- Restart Claude Desktop after configuration changes -- Check that the path to `index.js` is correct and absolute -- Verify the MCP server builds without errors - -**Poor response quality** -- Adjust temperature and sampling parameters -- Try different system prompts - ## License CC0-1.0 - Public Domain. Use freely! --- -Built with ❤️ for open-source AI and the LibreModel project. by Claude Sonnet4 - +Built for open-source AI infrastructure. Reduce your Claude API costs by up to 95% on analysis tasks. diff --git a/dist/index.js b/dist/index.js index 9342290..a0adc7b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2,13 +2,104 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; +import { exec, execSync } from "child_process"; +import { promisify } from "util"; +import fs from "fs"; +import path from "path"; +import os from "os"; +const execAsync = promisify(exec); +function decryptGPGFile(filePath) { + const passphrase = process.env.GPG_PASSPHRASE; + if (!passphrase) { + console.error("GPG_PASSPHRASE env var required to decrypt", filePath); + return null; + } + try { + // Use gpg with passphrase from env var (--batch for non-interactive) + const result = execSync(`gpg --batch --yes --passphrase-fd 0 --decrypt "${filePath}"`, { + input: passphrase, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"] + }); + return result; + } + catch (e) { + console.error(`Failed to decrypt ${filePath}:`, e); + return null; + } +} +function loadCredentialsFile(filePath) { + if (!fs.existsSync(filePath)) { + return null; + } + try { + let content; + if (filePath.endsWith(".gpg")) { + // Decrypt GPG-encrypted file + const decrypted = decryptGPGFile(filePath); + if (!decrypted) + return null; + content = decrypted; + console.error(`Decrypted credentials from ${filePath}`); + } + else { + // Plain JSON file + content = fs.readFileSync(filePath, "utf-8"); + } + return JSON.parse(content); + } + catch (e) { + console.error(`Failed to load credentials from ${filePath}:`, e); + return null; + } +} +function loadSSHHosts() { + const hosts = {}; + // Try loading from credentials file (check for .gpg first, then plain) + const baseCredentialsFile = process.env.CREDENTIALS_FILE || path.join(os.homedir(), ".claude", "credentials.json"); + const gpgFile = baseCredentialsFile.endsWith(".gpg") ? baseCredentialsFile : baseCredentialsFile + ".gpg"; + let credentialsFile = baseCredentialsFile; + if (fs.existsSync(gpgFile) && process.env.GPG_PASSPHRASE) { + credentialsFile = gpgFile; // Prefer encrypted if available and passphrase set + } + const parsed = loadCredentialsFile(credentialsFile); + if (parsed?.ssh_hosts) { + Object.assign(hosts, parsed.ssh_hosts); + console.error(`Loaded ${Object.keys(parsed.ssh_hosts).length} SSH hosts from ${credentialsFile}`); + } + // Try loading from SSH_HOSTS_FILE (flat format) + const hostsFile = process.env.SSH_HOSTS_FILE; + if (hostsFile) { + const hostsParsed = loadCredentialsFile(hostsFile); + if (hostsParsed) { + Object.assign(hosts, hostsParsed); + } + } + // Load from individual env vars (SSH_HOST_192_168_0_165, etc.) + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith("SSH_HOST_") && value) { + try { + const ip = key.replace("SSH_HOST_", "").replace(/_/g, "."); + const parsed = JSON.parse(value); + hosts[ip] = parsed; + } + catch (e) { + console.error(`Warning: Could not parse ${key}:`, e); + } + } + } + return hosts; +} +const SSH_HOSTS = loadSSHHosts(); class LibreModelMCPServer { server; config; + conversations = new Map(); + CONVERSATION_TTL = 30 * 60 * 1000; // 30 minutes constructor() { this.server = new McpServer({ name: "libremodel-mcp-server", - version: "1.0.0" + version: "1.1.0" }); this.config = { url: process.env.LLAMA_SERVER_URL || "http://localhost:8080", @@ -20,6 +111,252 @@ class LibreModelMCPServer { }; this.setupTools(); this.setupResources(); + // Cleanup old conversations periodically + setInterval(() => this.cleanupConversations(), 5 * 60 * 1000); + } + cleanupConversations() { + const now = Date.now(); + for (const [id, conv] of this.conversations) { + if (now - conv.createdAt > this.CONVERSATION_TTL) { + this.conversations.delete(id); + } + } + } + generateConversationId() { + return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + buildToolsPrompt(tools) { + if (tools.length === 0) + return ""; + let prompt = `## OUTPUT FORMAT RULES (STRICT) + +You are an autonomous agent. Follow these rules EXACTLY: + +### RULE 1: Tool Calls +When you need to use a tool, output ONLY a JSON object. Nothing else. +Format: {"tool": "tool_name", "arguments": {"param": "value"}} + +CORRECT: +{"tool": "ssh_exec", "arguments": {"host": "192.168.0.165", "command": "df -h"}} + +WRONG (do NOT do these): +- Let me check... {"tool": ...} (NO text before JSON) +- {"tool": ...} Let me analyze (NO text after JSON) +- I'll use ssh_exec to check (NO explaining, just output JSON) + +### RULE 2: Final Answers +When you have enough information to answer, provide a DIRECT answer. +Do NOT output JSON. Just write the answer clearly and concisely. + +CORRECT: +The disk usage shows /dev/sda1 is at 85% capacity. This is above the 80% threshold. + +WRONG: +{"answer": "The disk is at 85%"} (NO JSON for answers) + +### RULE 3: After Tool Results +When you receive tool results, either: +- Call another tool (output JSON only) +- Provide your final answer (plain text only) + +## AVAILABLE TOOLS + +`; + // Build tool descriptions + for (const tool of tools) { + prompt += `### ${tool.name}\n`; + prompt += `${tool.description}\n`; + if (tool.parameters && Object.keys(tool.parameters).length > 0) { + prompt += `Parameters:\n`; + for (const [name, param] of Object.entries(tool.parameters)) { + const required = param.required ? "(required)" : "(optional)"; + prompt += ` - ${name}: ${param.type} ${required}${param.description ? ` - ${param.description}` : ""}\n`; + } + } + prompt += `\n`; + } + // Add example + const exampleTool = tools.find(t => t.name === "ssh_exec") || tools[0]; + if (exampleTool) { + prompt += `## EXAMPLE INTERACTION + +User: Check the uptime on 192.168.0.165 +Assistant: {"tool": "ssh_exec", "arguments": {"host": "192.168.0.165", "command": "uptime"}} +Tool result: 16:30:00 up 5 days, 3:22, 2 users, load average: 0.15, 0.10, 0.08 +Assistant: The server 192.168.0.165 has been running for 5 days and 3 hours. Current load is low (0.15). + +`; + } + return prompt; + } + buildAgentPrompt(conversation) { + let prompt = ""; + // System section with context and tools + if (conversation.context) { + prompt += `## Context\n${conversation.context}\n`; + } + prompt += this.buildToolsPrompt(conversation.tools); + prompt += "\n---\n\n"; + // Conversation history + for (const msg of conversation.messages) { + if (msg.role === "user") { + prompt += `Human: ${msg.content}\n\n`; + } + else if (msg.role === "assistant") { + prompt += `Assistant: ${msg.content}\n\n`; + } + else if (msg.role === "tool") { + prompt += `Tool (${msg.tool_name}) result:\n${msg.content}\n\n`; + } + } + prompt += "Assistant:"; + return prompt; + } + debug(msg, data) { + if (process.env.DEBUG_MCP) { + const timestamp = new Date().toISOString(); + if (data !== undefined) { + console.error(`[${timestamp}] [MCP] ${msg}:`, typeof data === 'string' ? data : JSON.stringify(data, null, 2)); + } + else { + console.error(`[${timestamp}] [MCP] ${msg}`); + } + } + } + async executeSSHCommand(host, command) { + const hostConfig = SSH_HOSTS[host]; + if (!hostConfig) { + return `Error: Unknown host ${host}. Available: ${Object.keys(SSH_HOSTS).join(", ")}`; + } + try { + const port = hostConfig.port || 22; + const portArg = port !== 22 ? `-p ${port}` : ""; + const escapedCommand = command.replace(/"/g, '\\"'); + let sshCommand; + if (hostConfig.password) { + sshCommand = `sshpass -p '${hostConfig.password}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${host} "${escapedCommand}"`; + } + else { + sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${host} "${escapedCommand}"`; + } + const { stdout, stderr } = await execAsync(sshCommand, { + timeout: 30000, + maxBuffer: 1024 * 1024 + }); + return stdout || stderr || "(no output)"; + } + catch (error) { + return `Error: ${error instanceof Error ? error.message : String(error)}`; + } + } + parseToolCall(content) { + this.debug("parseToolCall input (first 500 chars)", content.slice(0, 500)); + // Strategy 1: Look for JSON in code block + const jsonBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/); + if (jsonBlockMatch) { + this.debug("Found JSON code block", jsonBlockMatch[1]); + const parsed = this.tryParseToolJson(jsonBlockMatch[1]); + if (parsed) { + this.debug("Parsed tool call from code block", parsed); + return parsed; + } + } + // Strategy 2: Extract all balanced JSON objects and check for tool calls + const jsonObjects = this.extractJsonObjects(content); + this.debug(`Found ${jsonObjects.length} JSON objects`); + for (const jsonStr of jsonObjects) { + if (jsonStr.includes('"tool"')) { + this.debug("Attempting to parse JSON with 'tool' key", jsonStr); + const parsed = this.tryParseToolJson(jsonStr); + if (parsed) { + this.debug("Successfully parsed tool call", parsed); + return parsed; + } + else { + this.debug("Failed to parse as tool call"); + } + } + } + this.debug("No tool call found in content"); + return null; + } + extractJsonObjects(content) { + const results = []; + let i = 0; + while (i < content.length) { + if (content[i] === '{') { + const jsonStr = this.extractBalancedJson(content, i); + if (jsonStr) { + results.push(jsonStr); + i += jsonStr.length; + } + else { + i++; + } + } + else { + i++; + } + } + return results; + } + extractBalancedJson(content, startIdx) { + if (content[startIdx] !== '{') + return null; + let depth = 0; + let inString = false; + let escape = false; + for (let i = startIdx; i < content.length; i++) { + const char = content[i]; + if (escape) { + escape = false; + continue; + } + if (char === '\\' && inString) { + escape = true; + continue; + } + if (char === '"' && !escape) { + inString = !inString; + continue; + } + if (!inString) { + if (char === '{') { + depth++; + } + else if (char === '}') { + depth--; + if (depth === 0) { + return content.slice(startIdx, i + 1); + } + } + } + } + return null; // Unbalanced + } + tryParseToolJson(jsonStr) { + // Try parsing strategies in order of safety + const strategies = [ + jsonStr, // Try as-is first + jsonStr.replace(/,\s*}/g, '}'), // Fix trailing commas + jsonStr.replace(/,\s*}/g, '}') // Fix trailing commas in arrays too + .replace(/,\s*\]/g, ']'), + ]; + for (const attempt of strategies) { + try { + const parsed = JSON.parse(attempt); + if (parsed.tool && typeof parsed.tool === "string") { + return { + name: parsed.tool, + arguments: parsed.arguments || {} + }; + } + } + catch (e) { + // Try next strategy + } + } + return null; } setupTools() { // Main chat tool @@ -163,6 +500,293 @@ class LibreModelMCPServer { }; } }); + // SSH execution tool for infrastructure access + this.server.registerTool("ssh_exec", { + title: "Execute SSH Command", + description: "Execute a shell command on a remote server via SSH. Supports primary (.165), secondary (.13), HA (.148), Pi-hole (.239), and Proxmox (.75).", + inputSchema: { + host: z.string().describe("Server IP address (e.g., 192.168.0.165)"), + command: z.string().describe("Shell command to execute"), + timeout: z.number().min(1000).max(60000).default(30000).describe("Command timeout in ms (default: 30000)") + } + }, async (args) => { + try { + const hostConfig = SSH_HOSTS[args.host]; + if (!hostConfig) { + const knownHosts = Object.keys(SSH_HOSTS); + const helpText = knownHosts.length > 0 + ? `Configured hosts: ${knownHosts.join(", ")}` + : "No hosts configured. Set SSH_HOSTS_FILE or SSH_HOST_ env vars."; + return { + content: [{ + type: "text", + text: `**SSH Error:** Unknown host ${args.host}\n\n${helpText}\n\nConfigure via:\n- SSH_HOSTS_FILE=/path/to/hosts.json\n- SSH_HOST_192_168_0_1='{"user":"admin","password":"secret"}'` + }], + isError: true + }; + } + const port = hostConfig.port || 22; + const portArg = port !== 22 ? `-p ${port}` : ""; + const escapedCommand = args.command.replace(/"/g, '\\"'); + let sshCommand; + if (hostConfig.password) { + // Use sshpass for password auth + sshCommand = `sshpass -p '${hostConfig.password}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${args.host} "${escapedCommand}"`; + } + else { + // Key-based auth + sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${args.host} "${escapedCommand}"`; + } + const { stdout, stderr } = await execAsync(sshCommand, { + timeout: args.timeout || 30000, + maxBuffer: 1024 * 1024 // 1MB buffer + }); + const output = stdout || stderr || "(no output)"; + return { + content: [{ + type: "text", + text: `**SSH to ${args.host}:**\n\`\`\`\n$ ${args.command}\n${output.trim()}\n\`\`\`` + }] + }; + } + catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return { + content: [{ + type: "text", + text: `**SSH Error on ${args.host}:**\n${errorMsg}` + }], + isError: true + }; + } + }); + // Agent chat tool with tool-calling support and autonomous execution + this.server.registerTool("agent_chat", { + title: "Agent Chat with Tool Calling", + description: "Start or continue an agent conversation where the model can request tool calls. The orchestrating system (e.g., Claude) executes tools and feeds results back.", + inputSchema: { + task: z.string().describe("The task or message for the agent"), + tools: z.array(z.object({ + name: z.string(), + description: z.string(), + parameters: z.record(z.object({ + type: z.string(), + description: z.string().optional(), + required: z.boolean().optional() + })).optional() + })).default([]).describe("Tool definitions the agent can request"), + context: z.string().default("").describe("RAG context or background information"), + conversation_id: z.string().optional().describe("ID to resume an existing conversation"), + tool_result: z.object({ + tool_name: z.string(), + result: z.string() + }).optional().describe("Result from a previously requested tool call"), + temperature: z.number().min(0.0).max(2.0).default(0.3).describe("Lower temperature for more focused agent behavior"), + auto_execute: z.boolean().default(true).describe("Auto-execute built-in tools (ssh_exec) without returning to caller"), + max_iterations: z.number().min(1).max(20).default(10).describe("Max tool execution iterations before returning") + } + }, async (args) => { + try { + this.debug("agent_chat called", { + task: args.task?.slice(0, 100), + conversation_id: args.conversation_id, + tools: args.tools?.map(t => t.name), + has_tool_result: !!args.tool_result, + auto_execute: args.auto_execute + }); + let conversation; + // Resume or create conversation + if (args.conversation_id && this.conversations.has(args.conversation_id)) { + conversation = this.conversations.get(args.conversation_id); + this.debug("Resuming conversation", conversation.id); + // Add tool result if provided (for manual tool execution mode) + if (args.tool_result) { + this.debug("Adding tool result", { tool: args.tool_result.tool_name, result_length: args.tool_result.result.length }); + conversation.messages.push({ + role: "tool", + content: args.tool_result.result, + tool_name: args.tool_result.tool_name + }); + } + else if (args.task) { + conversation.messages.push({ + role: "user", + content: args.task + }); + } + } + else { + // Create new conversation with ssh_exec as built-in tool + const id = args.conversation_id || this.generateConversationId(); + this.debug("Creating new conversation", id); + // Add ssh_exec as built-in tool if not already provided + const providedTools = args.tools || []; + const hasSSH = providedTools.some(t => t.name === "ssh_exec"); + const allTools = hasSSH ? providedTools : [ + ...providedTools, + { + name: "ssh_exec", + description: "Execute a shell command on a remote server via SSH. Available hosts: " + Object.keys(SSH_HOSTS).join(", "), + parameters: { + host: { type: "string", description: "Server IP address", required: true }, + command: { type: "string", description: "Shell command to execute", required: true } + } + } + ]; + conversation = { + id, + messages: [{ role: "user", content: args.task }], + tools: allTools, + context: args.context || "", + createdAt: Date.now() + }; + this.conversations.set(id, conversation); + } + // Agentic loop - execute tools until final answer or max iterations + const autoExecute = args.auto_execute !== false; + const maxIterations = args.max_iterations || 5; + let totalTokens = 0; + let toolsExecuted = []; + for (let iteration = 0; iteration < maxIterations; iteration++) { + this.debug(`Agentic loop iteration ${iteration + 1}/${maxIterations}`); + // Build prompt and call model + const prompt = this.buildAgentPrompt(conversation); + const requestBody = { + prompt, + temperature: args.temperature || 0.3, + n_predict: -1, // Unlimited - local tokens are free + top_p: 0.95, + top_k: 40, + stop: ["Human:", "\nHuman:", "User:", "\nUser:"], + stream: false + }; + this.debug("Calling LLM", { iteration: iteration + 1 }); + const response = await fetch(`${this.config.url}/completion`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody) + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + const data = await response.json(); + const content = data.content?.trim() || ""; + totalTokens += data.tokens_predicted || 0; + this.debug("LLM response", { tokens: data.tokens_predicted, total: totalTokens }); + // Add assistant response to conversation + conversation.messages.push({ + role: "assistant", + content + }); + // Check if model requested a tool call + const toolCall = this.parseToolCall(content); + if (toolCall) { + this.debug("Tool call detected", { tool: toolCall.name, args: toolCall.arguments }); + // Check if we can auto-execute this tool + const isBuiltIn = toolCall.name === "ssh_exec"; + if (autoExecute && isBuiltIn) { + // Execute ssh_exec internally + this.debug("Auto-executing ssh_exec"); + const result = await this.executeSSHCommand(toolCall.arguments.host, toolCall.arguments.command); + toolsExecuted.push({ + tool: toolCall.name, + args: toolCall.arguments, + result_length: result.length + }); + // Add tool result to conversation and continue loop + conversation.messages.push({ + role: "tool", + content: result, + tool_name: toolCall.name + }); + this.debug("Tool result added, continuing loop"); + continue; // Continue to next iteration + } + else { + // Return tool call to caller for manual execution + const agentResponse = { + type: "tool_call", + conversation_id: conversation.id, + tool_call: toolCall, + tokens_used: totalTokens + }; + return { + content: [{ + type: "text", + text: JSON.stringify(agentResponse, null, 2) + }] + }; + } + } + else { + // Final answer - no tool call + this.debug("Final answer reached", { iterations: iteration + 1, tools_executed: toolsExecuted.length }); + const agentResponse = { + type: "final_answer", + conversation_id: conversation.id, + content, + tokens_used: totalTokens + }; + if (toolsExecuted.length > 0) { + agentResponse.tools_executed = toolsExecuted; + } + return { + content: [{ + type: "text", + text: JSON.stringify(agentResponse, null, 2) + }] + }; + } + } + // Max iterations reached + this.debug("Max iterations reached", { iterations: maxIterations }); + return { + content: [{ + type: "text", + text: JSON.stringify({ + type: "max_iterations", + conversation_id: conversation.id, + message: `Reached max iterations (${maxIterations}) without final answer`, + tools_executed: toolsExecuted, + tokens_used: totalTokens + }, null, 2) + }] + }; + } + catch (error) { + return { + content: [{ + type: "text", + text: JSON.stringify({ + type: "error", + error: error instanceof Error ? error.message : String(error) + }, null, 2) + }], + isError: true + }; + } + }); + // List active conversations (for debugging) + this.server.registerTool("list_conversations", { + title: "List Agent Conversations", + description: "List all active agent conversations (for debugging)", + inputSchema: {} + }, async () => { + const convs = Array.from(this.conversations.values()).map(c => ({ + id: c.id, + message_count: c.messages.length, + tools_count: c.tools.length, + age_seconds: Math.round((Date.now() - c.createdAt) / 1000) + })); + return { + content: [ + { + type: "text", + text: `**Active Conversations:** ${convs.length}\n\n\`\`\`json\n${JSON.stringify(convs, null, 2)}\n\`\`\`` + } + ] + }; + }); } setupResources() { // Server configuration resource diff --git a/dist/index.js.map b/dist/index.js.map index 132e72f..4e399c2 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/posts/reddit-post.md b/posts/reddit-post.md new file mode 100644 index 0000000..6e81fe2 --- /dev/null +++ b/posts/reddit-post.md @@ -0,0 +1,104 @@ +# Reddit Post: r/LocalLLaMA + +## Title Options (pick one): +- "My implementation of local LLM delegation for Claude Code - with built-in SSH and real token measurements" +- "Local LLM + Claude Code for infrastructure debugging - 52,000 tokens saved in one session" +- "Added autonomous SSH execution to the local LLM delegation pattern - here's what I measured" + +--- + +## Post Body: + +**TL;DR:** I know this pattern exists (CC Token Saver, Ollama Claude, Rubber Duck, etc.), but I wanted an implementation focused on infrastructure/DevOps with built-in SSH execution. Here's what I built and the actual token savings I measured. + +### Prior Art + +This isn't new. Several projects do local LLM delegation for Claude: +- [CC Token Saver](https://github.com/csabakecskemeti/cc_token_saver_mcp) - Delegates simple tasks to local LLM +- [Ollama Claude](https://mcpmarket.com/server/ollama-claude) - Same pattern with Ollama +- [Rubber Duck MCP](https://www.reddit.com/r/ClaudeAI/comments/1n9vxfp/claude_mcp_rubber_duck_context_window_saver/) - Claims 93% savings (I got similar numbers) + +### What's Different About Mine + +I wanted something specifically for **infrastructure debugging** with: +1. **Built-in SSH execution** - Agent runs commands directly, not just LLM delegation +2. **Autonomous tool loop** - Agent decides what commands to run, executes them, analyzes results +3. **Raw data stays local** - 40K chars of logs never touch Claude's context + +``` +Claude: "Debug why Nextcloud Talk returns 400" + ↓ +Agent: [SSHs to server, checks logs, runs DB queries - all internally] +Agent: [Analyzes 40K chars locally] + ↓ +Claude: [Receives "SSL cert not trusted" - 800 tokens] +``` + +### Actual Measurements + +I did systematic testing instead of just claiming savings: + +| Task | Claude Direct | Claude w/ Agent | Local LLM | Savings | +|------|---------------|-----------------|-----------|---------| +| Debugging workflow (7 calls) | ~56,000 | ~4,100 | ~35,000 | **93%** | +| Security audit (44K chars) | ~11,800 | ~800 | ~11,000 | **93%** | +| Docker logs (40K chars) | ~10,500 | ~500 | ~10,000 | **95%** | +| Code gen (small input) | ~1,550 | ~1,600 | ~1,500 | **0%** | + +The 0% case matters - when raw data is small, no benefit. This shines on **data-heavy tasks**. + +### Real Debugging Session + +Nextcloud Talk returning HTTP 400. Seven agent calls over 10 minutes: +1. Check signaling + logs → "Config OK" +2. Check rate limits + DB → "Permissions OK" +3. Enable debug, get stack trace → "SSL cert not trusted" +4. Add cert, verify fix → "HTTP 201 - working!" + +Claude stayed strategic (what to check), agent did tactical work (SSH, parsing, queries). + +### Setup + +- **Local LLM**: llama.cpp server (I use 120B Q8 on Strix Halo, but 32B+ should work) +- **Context**: 32K minimum, 128K recommended for large logs +- **MCP Server**: Node.js + +### Code + +GitHub: https://github.com/lambertmt/llama-mcp-server +Branch: `feature/agent-tool-calling` + +Key file: `src/index.ts` - the `agent_chat` handler runs the autonomous loop with internal SSH execution. + +### When to Use This vs Other Options + +| Use Case | Best Tool | +|----------|-----------| +| Simple code tasks | CC Token Saver, Ollama Claude | +| Research/docs | Rubber Duck | +| **Infrastructure debugging** | This (built-in SSH) | +| General delegation | Ultimate MCP Server | + +### Limitations + +- Doesn't help when output dominates (code gen from small inputs) +- Requires decent local LLM (7B struggles with complex analysis) +- SSH hosts need pre-configuration +- Linux/Mac only (uses sshpass) + +--- + +**Questions welcome.** Especially interested if anyone has other infrastructure-focused use cases. + +--- + +## Suggested Subreddits: +- **r/LocalLLaMA** (primary - focus on local LLM agent architecture) +- **r/ClaudeAI** (focus on Claude Code integration, mention prior art) +- **r/selfhosted** (focus on infrastructure monitoring use case) +- **r/homelab** (focus on the debugging workflow) + +## Cross-post adjustments: +- r/ClaudeAI: Emphasize this builds on existing work (CC Token Saver etc.) +- r/selfhosted: Lead with the Nextcloud debugging story +- r/homelab: Focus on multi-server monitoring diff --git a/posts/substack-post.md b/posts/substack-post.md new file mode 100644 index 0000000..270e119 --- /dev/null +++ b/posts/substack-post.md @@ -0,0 +1,272 @@ +# Substack / Blog Post + +## Title: Building on the Local LLM Delegation Pattern: An Infrastructure-Focused Implementation + +### Subtitle: How I added autonomous SSH execution to save 52,000 tokens on a single debugging session + +--- + +Every time you ask Claude to analyze server logs, parse configuration files, or debug a production issue, you're burning API tokens. A lot of them. + +I just finished a debugging session where Claude helped me fix a Nextcloud Talk issue. If I'd done it the normal way - copying logs into Claude, asking questions, pasting more output - it would have cost me around **56,000 tokens**. + +Instead, I spent **4,100 tokens**. A 93% reduction. + +Before I explain how, let me be clear: **this pattern isn't new**. + +--- + +## Prior Art: Others Have Done This + +Several projects already delegate work from Claude to local LLMs: + +- **[CC Token Saver](https://github.com/csabakecskemeti/cc_token_saver_mcp)** - Intelligently delegates simple tasks to your local LLM +- **[Ollama Claude](https://mcpmarket.com/server/ollama-claude)** - Offloads code generation and review to Ollama +- **[Rubber Duck MCP](https://www.reddit.com/r/ClaudeAI/comments/1n9vxfp/claude_mcp_rubber_duck_context_window_saver/)** - Delegates research to cheaper LLMs (claims 93% savings) +- **[Ultimate MCP Server](https://github.com/Dicklesworthstone/ultimate_mcp_server)** - Delegates from Claude to Gemini Flash + +The core insight is the same across all of these: let Claude orchestrate while cheaper/free models do the grunt work. + +**What I built is a variation focused on infrastructure debugging**, with built-in SSH execution and detailed token measurements. If you're doing DevOps work, this might be more useful than the general-purpose tools. + +--- + +## The Problem With Raw Data + +When Claude analyzes anything substantial - 200 lines of logs, disk usage across servers, a stack trace - every character of that output goes into Claude's context window. You pay for all of it. + +Even if you're using an MCP server to connect Claude to external tools, the data still flows through: + +``` +You → Claude → MCP Server → Tool → MCP Server → Claude → You +``` + +Claude sees everything. Claude charges for everything. + +## The Insight: Delegate the Grunt Work + +What if Claude could delegate the data-heavy work to a local LLM that runs for free? + +Not replace Claude - Claude is still better at reasoning, planning, and knowing what questions to ask. But let a local model handle the tactical execution: + +- SSH into servers +- Parse verbose logs +- Run database queries +- Filter and summarize output + +Then return just the summary to Claude. + +## The Architecture + +I modified an MCP server to run an **autonomous agent loop**. Here's how it works: + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Claude Code │ "Check server health" │ Local LLM │ +│ (Orchestrator) │ ────────────────────────►│ (Agent) │ +│ │ │ │ +│ Decides what │ │ Executes SSH │ +│ to check next │ │ Parses output │ +│ │◄─────────────────────────│ Summarizes │ +│ │ "Memory at 92%, 10 │ │ +│ │ containers healthy" │ │ +└─────────────────┘ └─────────────────┘ +``` + +The key: **Claude never sees the raw SSH output**. That stays entirely within the local LLM's context. + +## Real Numbers + +I ran systematic tests across different task types: + +| Task | Claude (Direct) | Claude (w/ Agent) | Local LLM | Savings | +|------|-----------------|-------------------|-----------|---------| +| Debugging workflow (7 calls) | ~56,000 | ~4,100 | ~35,000 | **93%** | +| Security audit | ~11,800 | ~800 | ~11,000 | **93%** | +| Docker logs analysis | ~10,500 | ~500 | ~10,000 | **95%** | +| System health check | ~5,500 | ~1,500 | ~4,000 | 73% | +| Log analysis | ~4,000 | ~800 | ~3,200 | 80% | +| Code generation (small input) | ~1,550 | ~1,600 | ~1,500 | **0%** | + +Notice the last row. When raw data is small, there's no benefit - the output size dominates. This architecture shines when you're processing **large amounts of data**. + +## A Real Debugging Session + +Let me walk through an actual example. + +Nextcloud Talk was returning HTTP 400 errors when sending messages. Here's how the debugging went: + +**Call 1: Initial triage** +- Claude: "Check the Talk container logs and signaling configuration" +- Agent: SSHs to server, runs docker logs, parses 15K chars +- Returns to Claude: "Signaling configured correctly, no errors in logs" +- *Tokens saved: ~14,000* + +**Call 2: Narrowing down** +- Claude: "Check rate limiting and user permissions in the database" +- Agent: Runs SQL queries, checks config +- Returns: "Rate limiting enabled, user has correct permissions" +- *Tokens saved: ~7,000* + +**Call 3: The breakthrough** +- Claude: "Enable debug mode and capture the exact error" +- Agent: Modifies config, triggers error, parses 3KB stack trace +- Returns: "SSL certificate problem: self-signed certificate not trusted" +- *Tokens saved: ~8,000* + +**Call 4: The fix** +- Claude: "Add the cert to the container's trust store and test" +- Agent: Runs update-ca-certificates, tests API +- Returns: "HTTP 201 - message sent successfully" +- *Tokens saved: ~4,000* + +**Total: 52,000 tokens saved.** Ten minutes of debugging. Actual production issue resolved. + +## The Orchestration Pattern + +This works because Claude and the local LLM have complementary strengths: + +**Claude (Orchestrator):** +- Knows Nextcloud architecture +- Decides what to investigate next +- Interprets findings in context +- Knows when the problem is solved + +**Local LLM (Agent):** +- Executes SSH commands +- Parses verbose output +- Filters noise from signal +- Returns concise summaries + +Claude stays strategic. The agent stays tactical. + +## When This Doesn't Work + +Transparency matters. This approach has limitations: + +1. **Small input, large output**: Code generation from minimal context shows 0% savings - the generated code dominates token count either way. + +2. **Complex reasoning required**: If the raw data requires Claude-level reasoning to interpret, the agent can't help. + +3. **Local LLM quality**: A 7B model might struggle with nuanced log analysis. I use 32B-120B models. + +4. **Setup overhead**: You need a local LLM running, hosts configured for SSH, the MCP server installed. + +## The Setup + +**Requirements:** +- llama.cpp server (or compatible) running your preferred model +- Claude Code or Claude Desktop with MCP support +- Node.js for the MCP server + +**My setup:** +- AMD Strix Halo server (192.168.0.165) +- GPT-OSS 120B (Q8 quantization) +- 128K context window +- SSH access to infrastructure hosts + +**Installation:** +```bash +git clone https://github.com/lambertmt/llama-mcp-server +cd llama-mcp-server +git checkout feature/agent-tool-calling +npm install && npm run build +``` + +**Claude Code config:** +```json +{ + "mcpServers": { + "llama-local": { + "command": "node", + "args": ["/path/to/llama-mcp-server/dist/index.js"], + "env": { + "LLAMA_SERVER_URL": "http://your-server:8080" + } + } + } +} +``` + +## The Code + +The key addition is `agent_chat` - an MCP tool that runs an autonomous loop: + +```typescript +// Simplified version +async function agentChat(task: string) { + while (iterations < maxIterations) { + // Ask local LLM what to do + const response = await callLocalLLM(task, conversationHistory); + + // If it wants to run a command, execute internally + if (response.type === "tool_call" && response.tool === "ssh_exec") { + const result = await executeSSH(response.args); + conversationHistory.push({ role: "tool", content: result }); + continue; // Loop - Claude never sees this + } + + // Otherwise, return the final answer to Claude + return response.content; + } +} +``` + +The magic is the `continue` - when the agent executes a tool, the result stays in the agent's context and the loop continues. Only the final summary reaches Claude. + +## Which Tool Should You Use? + +| Your Use Case | Best Option | +|---------------|-------------| +| Simple code tasks (generation, review) | [CC Token Saver](https://github.com/csabakecskemeti/cc_token_saver_mcp), [Ollama Claude](https://mcpmarket.com/server/ollama-claude) | +| Research and document parsing | [Rubber Duck MCP](https://www.reddit.com/r/ClaudeAI/comments/1n9vxfp/claude_mcp_rubber_duck_context_window_saver/) | +| **Infrastructure debugging (logs, SSH)** | This implementation | +| Multi-model delegation | [Ultimate MCP Server](https://github.com/Dicklesworthstone/ultimate_mcp_server) | + +This implementation makes sense if you: + +- Run Claude Code for infrastructure/DevOps work +- Have a local LLM setup (or want to build one) +- Process large outputs regularly (logs, monitoring, debugging) +- Want built-in SSH execution without additional setup + +It doesn't make sense if you: + +- Only do small-context tasks (use CC Token Saver instead) +- Don't have infrastructure to run a local LLM +- Need every response to have Claude-level reasoning +- Just need code generation delegation (Ollama Claude is simpler) + +## The Broader Ecosystem + +The local LLM delegation pattern is maturing. What started as individual experiments is becoming a standard approach: + +1. **Task classification** - Route simple tasks to cheap/free models +2. **Context isolation** - Keep verbose output away from expensive APIs +3. **Orchestration** - Let the best model coordinate + +My contribution is a point solution for infrastructure work. The ecosystem has options for most use cases now. + +## Code + +The code is open source: [github.com/lambertmt/llama-mcp-server](https://github.com/lambertmt/llama-mcp-server) (branch: `feature/agent-tool-calling`) + +--- + +*The pattern isn't new, but the measurements might be useful. If you've measured token savings with other implementations, I'd love to compare notes.* + +--- + +## Publishing Notes + +**Platforms:** +- Substack (if you have one) +- Dev.to (good for technical content) +- Medium (broader reach, but paywalled) +- Personal blog + Hacker News submission + +**Hacker News title:** +"Show HN: Infrastructure-focused local LLM delegation for Claude Code - with real token measurements" + +**Tags:** +#LLM #Claude #LocalAI #DevOps #OpenSource #MCP diff --git a/src/index.ts b/src/index.ts index 9c2a7f3..4498d5e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; +import { exec, execSync } from "child_process"; +import { promisify } from "util"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +const execAsync = promisify(exec); interface LlamaServerConfig { url: string; @@ -13,6 +20,154 @@ interface LlamaServerConfig { stopSequences: string[]; } +// SSH configuration for infrastructure access +// Configure via: +// 1. CREDENTIALS_FILE env var (JSON with ssh_hosts key) - default: ~/.claude/credentials.json +// Supports GPG-encrypted files (.gpg extension) - requires GPG_PASSPHRASE env var +// 2. SSH_HOSTS_FILE env var (JSON with flat host mapping) +// 3. SSH_HOST_ env vars: SSH_HOST_192_168_0_165='{"user":"admin","password":"secret"}' +interface SSHHost { + user: string; + password?: string; // Optional - if not provided, uses key-based auth + port?: number; // Optional - defaults to 22 + description?: string; +} + +function decryptGPGFile(filePath: string): string | null { + const passphrase = process.env.GPG_PASSPHRASE; + + if (!passphrase) { + console.error("GPG_PASSPHRASE env var required to decrypt", filePath); + return null; + } + + try { + // Use gpg with passphrase from env var (--batch for non-interactive) + const result = execSync( + `gpg --batch --yes --passphrase-fd 0 --decrypt "${filePath}"`, + { + input: passphrase, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"] + } + ); + return result; + } catch (e) { + console.error(`Failed to decrypt ${filePath}:`, e); + return null; + } +} + +function loadCredentialsFile(filePath: string): any | null { + if (!fs.existsSync(filePath)) { + return null; + } + + try { + let content: string; + + if (filePath.endsWith(".gpg")) { + // Decrypt GPG-encrypted file + const decrypted = decryptGPGFile(filePath); + if (!decrypted) return null; + content = decrypted; + console.error(`Decrypted credentials from ${filePath}`); + } else { + // Plain JSON file + content = fs.readFileSync(filePath, "utf-8"); + } + + return JSON.parse(content); + } catch (e) { + console.error(`Failed to load credentials from ${filePath}:`, e); + return null; + } +} + +function loadSSHHosts(): Record { + const hosts: Record = {}; + + // Try loading from credentials file (check for .gpg first, then plain) + const baseCredentialsFile = process.env.CREDENTIALS_FILE || path.join(os.homedir(), ".claude", "credentials.json"); + const gpgFile = baseCredentialsFile.endsWith(".gpg") ? baseCredentialsFile : baseCredentialsFile + ".gpg"; + + let credentialsFile = baseCredentialsFile; + if (fs.existsSync(gpgFile) && process.env.GPG_PASSPHRASE) { + credentialsFile = gpgFile; // Prefer encrypted if available and passphrase set + } + + const parsed = loadCredentialsFile(credentialsFile); + if (parsed?.ssh_hosts) { + Object.assign(hosts, parsed.ssh_hosts); + console.error(`Loaded ${Object.keys(parsed.ssh_hosts).length} SSH hosts from ${credentialsFile}`); + } + + // Try loading from SSH_HOSTS_FILE (flat format) + const hostsFile = process.env.SSH_HOSTS_FILE; + if (hostsFile) { + const hostsParsed = loadCredentialsFile(hostsFile); + if (hostsParsed) { + Object.assign(hosts, hostsParsed); + } + } + + // Load from individual env vars (SSH_HOST_192_168_0_165, etc.) + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith("SSH_HOST_") && value) { + try { + const ip = key.replace("SSH_HOST_", "").replace(/_/g, "."); + const parsed = JSON.parse(value); + hosts[ip] = parsed; + } catch (e) { + console.error(`Warning: Could not parse ${key}:`, e); + } + } + } + + return hosts; +} + +const SSH_HOSTS = loadSSHHosts(); + +// Agent tool-calling interfaces +interface ToolDefinition { + name: string; + description: string; + parameters?: Record; +} + +interface ToolCall { + name: string; + arguments: Record; +} + +interface AgentMessage { + role: "user" | "assistant" | "tool"; + content: string; + tool_call?: ToolCall; + tool_name?: string; +} + +interface AgentConversation { + id: string; + messages: AgentMessage[]; + tools: ToolDefinition[]; + context: string; + createdAt: number; +} + +interface AgentResponse { + type: "tool_call" | "final_answer"; + conversation_id: string; + content?: string; + tool_call?: ToolCall; + tokens_used?: number; +} + interface LlamaCompletionRequest { prompt: string; temperature: number; @@ -43,11 +198,13 @@ interface LlamaCompletionResponse { class LibreModelMCPServer { private server: McpServer; private config: LlamaServerConfig; + private conversations: Map = new Map(); + private readonly CONVERSATION_TTL = 30 * 60 * 1000; // 30 minutes constructor() { this.server = new McpServer({ name: "libremodel-mcp-server", - version: "1.0.0" + version: "1.1.0" }); this.config = { @@ -61,6 +218,275 @@ class LibreModelMCPServer { this.setupTools(); this.setupResources(); + + // Cleanup old conversations periodically + setInterval(() => this.cleanupConversations(), 5 * 60 * 1000); + } + + private cleanupConversations() { + const now = Date.now(); + for (const [id, conv] of this.conversations) { + if (now - conv.createdAt > this.CONVERSATION_TTL) { + this.conversations.delete(id); + } + } + } + + private generateConversationId(): string { + return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + private buildToolsPrompt(tools: ToolDefinition[]): string { + if (tools.length === 0) return ""; + + let prompt = `## OUTPUT FORMAT RULES (STRICT) + +You are an autonomous agent. Follow these rules EXACTLY: + +### RULE 1: Tool Calls +When you need to use a tool, output ONLY a JSON object. Nothing else. +Format: {"tool": "tool_name", "arguments": {"param": "value"}} + +CORRECT: +{"tool": "ssh_exec", "arguments": {"host": "192.168.0.165", "command": "df -h"}} + +WRONG (do NOT do these): +- Let me check... {"tool": ...} (NO text before JSON) +- {"tool": ...} Let me analyze (NO text after JSON) +- I'll use ssh_exec to check (NO explaining, just output JSON) + +### RULE 2: Final Answers +When you have enough information to answer, provide a DIRECT answer. +Do NOT output JSON. Just write the answer clearly and concisely. + +CORRECT: +The disk usage shows /dev/sda1 is at 85% capacity. This is above the 80% threshold. + +WRONG: +{"answer": "The disk is at 85%"} (NO JSON for answers) + +### RULE 3: After Tool Results +When you receive tool results, either: +- Call another tool (output JSON only) +- Provide your final answer (plain text only) + +## AVAILABLE TOOLS + +`; + + // Build tool descriptions + for (const tool of tools) { + prompt += `### ${tool.name}\n`; + prompt += `${tool.description}\n`; + if (tool.parameters && Object.keys(tool.parameters).length > 0) { + prompt += `Parameters:\n`; + for (const [name, param] of Object.entries(tool.parameters)) { + const required = param.required ? "(required)" : "(optional)"; + prompt += ` - ${name}: ${param.type} ${required}${param.description ? ` - ${param.description}` : ""}\n`; + } + } + prompt += `\n`; + } + + // Add example + const exampleTool = tools.find(t => t.name === "ssh_exec") || tools[0]; + if (exampleTool) { + prompt += `## EXAMPLE INTERACTION + +User: Check the uptime on 192.168.0.165 +Assistant: {"tool": "ssh_exec", "arguments": {"host": "192.168.0.165", "command": "uptime"}} +Tool result: 16:30:00 up 5 days, 3:22, 2 users, load average: 0.15, 0.10, 0.08 +Assistant: The server 192.168.0.165 has been running for 5 days and 3 hours. Current load is low (0.15). + +`; + } + + return prompt; + } + + private buildAgentPrompt(conversation: AgentConversation): string { + let prompt = ""; + + // System section with context and tools + if (conversation.context) { + prompt += `## Context\n${conversation.context}\n`; + } + prompt += this.buildToolsPrompt(conversation.tools); + prompt += "\n---\n\n"; + + // Conversation history + for (const msg of conversation.messages) { + if (msg.role === "user") { + prompt += `Human: ${msg.content}\n\n`; + } else if (msg.role === "assistant") { + prompt += `Assistant: ${msg.content}\n\n`; + } else if (msg.role === "tool") { + prompt += `Tool (${msg.tool_name}) result:\n${msg.content}\n\n`; + } + } + + prompt += "Assistant:"; + return prompt; + } + + private debug(msg: string, data?: any) { + if (process.env.DEBUG_MCP) { + const timestamp = new Date().toISOString(); + if (data !== undefined) { + console.error(`[${timestamp}] [MCP] ${msg}:`, typeof data === 'string' ? data : JSON.stringify(data, null, 2)); + } else { + console.error(`[${timestamp}] [MCP] ${msg}`); + } + } + } + + private async executeSSHCommand(host: string, command: string): Promise { + const hostConfig = SSH_HOSTS[host]; + if (!hostConfig) { + return `Error: Unknown host ${host}. Available: ${Object.keys(SSH_HOSTS).join(", ")}`; + } + + try { + const port = hostConfig.port || 22; + const portArg = port !== 22 ? `-p ${port}` : ""; + const escapedCommand = command.replace(/"/g, '\\"'); + + let sshCommand: string; + if (hostConfig.password) { + sshCommand = `sshpass -p '${hostConfig.password}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${host} "${escapedCommand}"`; + } else { + sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${host} "${escapedCommand}"`; + } + + const { stdout, stderr } = await execAsync(sshCommand, { + timeout: 30000, + maxBuffer: 1024 * 1024 + }); + + return stdout || stderr || "(no output)"; + } catch (error) { + return `Error: ${error instanceof Error ? error.message : String(error)}`; + } + } + + private parseToolCall(content: string): ToolCall | null { + this.debug("parseToolCall input (first 500 chars)", content.slice(0, 500)); + + // Strategy 1: Look for JSON in code block + const jsonBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/); + if (jsonBlockMatch) { + this.debug("Found JSON code block", jsonBlockMatch[1]); + const parsed = this.tryParseToolJson(jsonBlockMatch[1]); + if (parsed) { + this.debug("Parsed tool call from code block", parsed); + return parsed; + } + } + + // Strategy 2: Extract all balanced JSON objects and check for tool calls + const jsonObjects = this.extractJsonObjects(content); + this.debug(`Found ${jsonObjects.length} JSON objects`); + for (const jsonStr of jsonObjects) { + if (jsonStr.includes('"tool"')) { + this.debug("Attempting to parse JSON with 'tool' key", jsonStr); + const parsed = this.tryParseToolJson(jsonStr); + if (parsed) { + this.debug("Successfully parsed tool call", parsed); + return parsed; + } else { + this.debug("Failed to parse as tool call"); + } + } + } + + this.debug("No tool call found in content"); + return null; + } + + private extractJsonObjects(content: string): string[] { + const results: string[] = []; + let i = 0; + + while (i < content.length) { + if (content[i] === '{') { + const jsonStr = this.extractBalancedJson(content, i); + if (jsonStr) { + results.push(jsonStr); + i += jsonStr.length; + } else { + i++; + } + } else { + i++; + } + } + + return results; + } + + private extractBalancedJson(content: string, startIdx: number): string | null { + if (content[startIdx] !== '{') return null; + + let depth = 0; + let inString = false; + let escape = false; + + for (let i = startIdx; i < content.length; i++) { + const char = content[i]; + + if (escape) { + escape = false; + continue; + } + + if (char === '\\' && inString) { + escape = true; + continue; + } + + if (char === '"' && !escape) { + inString = !inString; + continue; + } + + if (!inString) { + if (char === '{') { + depth++; + } else if (char === '}') { + depth--; + if (depth === 0) { + return content.slice(startIdx, i + 1); + } + } + } + } + + return null; // Unbalanced + } + + private tryParseToolJson(jsonStr: string): ToolCall | null { + // Try parsing strategies in order of safety + const strategies = [ + jsonStr, // Try as-is first + jsonStr.replace(/,\s*}/g, '}'), // Fix trailing commas + jsonStr.replace(/,\s*}/g, '}') // Fix trailing commas in arrays too + .replace(/,\s*\]/g, ']'), + ]; + + for (const attempt of strategies) { + try { + const parsed = JSON.parse(attempt); + if (parsed.tool && typeof parsed.tool === "string") { + return { + name: parsed.tool, + arguments: parsed.arguments || {} + }; + } + } catch (e) { + // Try next strategy + } + } + return null; } private setupTools() { @@ -213,6 +639,321 @@ class LibreModelMCPServer { }; } }); + + // SSH execution tool for infrastructure access + this.server.registerTool("ssh_exec", { + title: "Execute SSH Command", + description: "Execute a shell command on a remote server via SSH. Supports primary (.165), secondary (.13), HA (.148), Pi-hole (.239), and Proxmox (.75).", + inputSchema: { + host: z.string().describe("Server IP address (e.g., 192.168.0.165)"), + command: z.string().describe("Shell command to execute"), + timeout: z.number().min(1000).max(60000).default(30000).describe("Command timeout in ms (default: 30000)") + } + }, async (args) => { + try { + const hostConfig = SSH_HOSTS[args.host]; + if (!hostConfig) { + const knownHosts = Object.keys(SSH_HOSTS); + const helpText = knownHosts.length > 0 + ? `Configured hosts: ${knownHosts.join(", ")}` + : "No hosts configured. Set SSH_HOSTS_FILE or SSH_HOST_ env vars."; + return { + content: [{ + type: "text", + text: `**SSH Error:** Unknown host ${args.host}\n\n${helpText}\n\nConfigure via:\n- SSH_HOSTS_FILE=/path/to/hosts.json\n- SSH_HOST_192_168_0_1='{"user":"admin","password":"secret"}'` + }], + isError: true + }; + } + + const port = hostConfig.port || 22; + const portArg = port !== 22 ? `-p ${port}` : ""; + const escapedCommand = args.command.replace(/"/g, '\\"'); + + let sshCommand: string; + if (hostConfig.password) { + // Use sshpass for password auth + sshCommand = `sshpass -p '${hostConfig.password}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${args.host} "${escapedCommand}"`; + } else { + // Key-based auth + sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${portArg} '${hostConfig.user}'@${args.host} "${escapedCommand}"`; + } + + const { stdout, stderr } = await execAsync(sshCommand, { + timeout: args.timeout || 30000, + maxBuffer: 1024 * 1024 // 1MB buffer + }); + + const output = stdout || stderr || "(no output)"; + return { + content: [{ + type: "text", + text: `**SSH to ${args.host}:**\n\`\`\`\n$ ${args.command}\n${output.trim()}\n\`\`\`` + }] + }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return { + content: [{ + type: "text", + text: `**SSH Error on ${args.host}:**\n${errorMsg}` + }], + isError: true + }; + } + }); + + // Agent chat tool with tool-calling support and autonomous execution + this.server.registerTool("agent_chat", { + title: "Agent Chat with Tool Calling", + description: "Start or continue an agent conversation where the model can request tool calls. The orchestrating system (e.g., Claude) executes tools and feeds results back.", + inputSchema: { + task: z.string().describe("The task or message for the agent"), + tools: z.array(z.object({ + name: z.string(), + description: z.string(), + parameters: z.record(z.object({ + type: z.string(), + description: z.string().optional(), + required: z.boolean().optional() + })).optional() + })).default([]).describe("Tool definitions the agent can request"), + context: z.string().default("").describe("RAG context or background information"), + conversation_id: z.string().optional().describe("ID to resume an existing conversation"), + tool_result: z.object({ + tool_name: z.string(), + result: z.string() + }).optional().describe("Result from a previously requested tool call"), + temperature: z.number().min(0.0).max(2.0).default(0.3).describe("Lower temperature for more focused agent behavior"), + auto_execute: z.boolean().default(true).describe("Auto-execute built-in tools (ssh_exec) without returning to caller"), + max_iterations: z.number().min(1).max(20).default(10).describe("Max tool execution iterations before returning") + } + }, async (args) => { + try { + this.debug("agent_chat called", { + task: args.task?.slice(0, 100), + conversation_id: args.conversation_id, + tools: args.tools?.map(t => t.name), + has_tool_result: !!args.tool_result, + auto_execute: args.auto_execute + }); + + let conversation: AgentConversation; + + // Resume or create conversation + if (args.conversation_id && this.conversations.has(args.conversation_id)) { + conversation = this.conversations.get(args.conversation_id)!; + this.debug("Resuming conversation", conversation.id); + + // Add tool result if provided (for manual tool execution mode) + if (args.tool_result) { + this.debug("Adding tool result", { tool: args.tool_result.tool_name, result_length: args.tool_result.result.length }); + conversation.messages.push({ + role: "tool", + content: args.tool_result.result, + tool_name: args.tool_result.tool_name + }); + } else if (args.task) { + conversation.messages.push({ + role: "user", + content: args.task + }); + } + } else { + // Create new conversation with ssh_exec as built-in tool + const id = args.conversation_id || this.generateConversationId(); + this.debug("Creating new conversation", id); + + // Add ssh_exec as built-in tool if not already provided + const providedTools = args.tools || []; + const hasSSH = providedTools.some(t => t.name === "ssh_exec"); + const allTools = hasSSH ? providedTools : [ + ...providedTools, + { + name: "ssh_exec", + description: "Execute a shell command on a remote server via SSH. Available hosts: " + Object.keys(SSH_HOSTS).join(", "), + parameters: { + host: { type: "string", description: "Server IP address", required: true }, + command: { type: "string", description: "Shell command to execute", required: true } + } + } + ]; + + conversation = { + id, + messages: [{ role: "user", content: args.task }], + tools: allTools, + context: args.context || "", + createdAt: Date.now() + }; + this.conversations.set(id, conversation); + } + + // Agentic loop - execute tools until final answer or max iterations + const autoExecute = args.auto_execute !== false; + const maxIterations = args.max_iterations || 5; + let totalTokens = 0; + let toolsExecuted: Array<{ tool: string; args: any; result_length: number }> = []; + + for (let iteration = 0; iteration < maxIterations; iteration++) { + this.debug(`Agentic loop iteration ${iteration + 1}/${maxIterations}`); + + // Build prompt and call model + const prompt = this.buildAgentPrompt(conversation); + + const requestBody: LlamaCompletionRequest = { + prompt, + temperature: args.temperature || 0.3, + n_predict: -1, // Unlimited - local tokens are free + top_p: 0.95, + top_k: 40, + stop: ["Human:", "\nHuman:", "User:", "\nUser:"], + stream: false + }; + + this.debug("Calling LLM", { iteration: iteration + 1 }); + const response = await fetch(`${this.config.url}/completion`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json() as LlamaCompletionResponse; + const content = data.content?.trim() || ""; + totalTokens += data.tokens_predicted || 0; + this.debug("LLM response", { tokens: data.tokens_predicted, total: totalTokens }); + + // Add assistant response to conversation + conversation.messages.push({ + role: "assistant", + content + }); + + // Check if model requested a tool call + const toolCall = this.parseToolCall(content); + + if (toolCall) { + this.debug("Tool call detected", { tool: toolCall.name, args: toolCall.arguments }); + + // Check if we can auto-execute this tool + const isBuiltIn = toolCall.name === "ssh_exec"; + + if (autoExecute && isBuiltIn) { + // Execute ssh_exec internally + this.debug("Auto-executing ssh_exec"); + const result = await this.executeSSHCommand( + toolCall.arguments.host as string, + toolCall.arguments.command as string + ); + + toolsExecuted.push({ + tool: toolCall.name, + args: toolCall.arguments, + result_length: result.length + }); + + // Add tool result to conversation and continue loop + conversation.messages.push({ + role: "tool", + content: result, + tool_name: toolCall.name + }); + this.debug("Tool result added, continuing loop"); + continue; // Continue to next iteration + } else { + // Return tool call to caller for manual execution + const agentResponse: AgentResponse = { + type: "tool_call", + conversation_id: conversation.id, + tool_call: toolCall, + tokens_used: totalTokens + }; + + return { + content: [{ + type: "text", + text: JSON.stringify(agentResponse, null, 2) + }] + }; + } + } else { + // Final answer - no tool call + this.debug("Final answer reached", { iterations: iteration + 1, tools_executed: toolsExecuted.length }); + + const agentResponse: AgentResponse & { tools_executed?: typeof toolsExecuted } = { + type: "final_answer", + conversation_id: conversation.id, + content, + tokens_used: totalTokens + }; + + if (toolsExecuted.length > 0) { + agentResponse.tools_executed = toolsExecuted; + } + + return { + content: [{ + type: "text", + text: JSON.stringify(agentResponse, null, 2) + }] + }; + } + } + + // Max iterations reached + this.debug("Max iterations reached", { iterations: maxIterations }); + return { + content: [{ + type: "text", + text: JSON.stringify({ + type: "max_iterations", + conversation_id: conversation.id, + message: `Reached max iterations (${maxIterations}) without final answer`, + tools_executed: toolsExecuted, + tokens_used: totalTokens + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: "text", + text: JSON.stringify({ + type: "error", + error: error instanceof Error ? error.message : String(error) + }, null, 2) + }], + isError: true + }; + } + }); + + // List active conversations (for debugging) + this.server.registerTool("list_conversations", { + title: "List Agent Conversations", + description: "List all active agent conversations (for debugging)", + inputSchema: {} + }, async () => { + const convs = Array.from(this.conversations.values()).map(c => ({ + id: c.id, + message_count: c.messages.length, + tools_count: c.tools.length, + age_seconds: Math.round((Date.now() - c.createdAt) / 1000) + })); + + return { + content: [ + { + type: "text", + text: `**Active Conversations:** ${convs.length}\n\n\`\`\`json\n${JSON.stringify(convs, null, 2)}\n\`\`\`` + } + ] + }; + }); } private setupResources() { diff --git a/test-scripts/health_check.sh b/test-scripts/health_check.sh new file mode 100644 index 0000000..1296ff6 --- /dev/null +++ b/test-scripts/health_check.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# health_check.sh – simple health‑check for a server running Docker, systemd services and network sockets +# Generated by local LLM agent (GPT-OSS 120B) +# ------------------------------------------------------------------------- +# Exit on any unexpected error (but we handle most errors ourselves) +set -euo pipefail + +# ------------------------------------------------------------------------- +# Helper functions +# ------------------------------------------------------------------------- +log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; } +warn() { log "WARN: $*"; } +error() { log "ERROR: $*"; } + +# ------------------------------------------------------------------------- +# 1. Docker containers – list and verify they are running +# ------------------------------------------------------------------------- +log "Checking Docker containers …" +if ! command -v docker >/dev/null 2>&1; then + warn "Docker CLI not found – skipping Docker checks." +else + # Get container names and status (running / exited / …) + mapfile -t containers < <(docker ps --format '{{.Names}} {{.Status}}') + if [[ ${#containers[@]} -eq 0 ]]; then + warn "No Docker containers are running." + else + for line in "${containers[@]}"; do + name=$(awk '{print $1}' <<<"$line") + status=$(awk '{$1=""; print $0}' <<<"$line" | xargs) + if [[ $status != Up* ]]; then + warn "Container '$name' is NOT running (status: $status)." + else + log "Container '$name' is healthy ($status)." + fi + done + fi +fi + +# ------------------------------------------------------------------------- +# 2. Systemd services – list a subset of critical services and verify they are active +# ------------------------------------------------------------------------- +log "Checking critical systemd services …" +# Define a list of services you consider essential. +critical_services=( + docker + sshd + keepalived +) + +for svc in "${critical_services[@]}"; do + if systemctl is-active --quiet "$svc.service"; then + log "Service '$svc' is active." + else + warn "Service '$svc' is NOT active." + fi +done + +# ------------------------------------------------------------------------- +# 3. Listening sockets – verify that expected ports are open +# ------------------------------------------------------------------------- +log "Checking listening sockets …" +# Expected ports (protocol:port). Add/remove as needed. +declare -A expected_ports=( + ["tcp:22"]="sshd" + ["tcp:80"]="nginx" + ["tcp:443"]="nginx" +) + +# Get current listening sockets +listening_ports=$(ss -tuln | awk 'NR>1 {print $1, $5}') + +check_port() { + local proto=$1 port=$2 + echo "$listening_ports" | grep -q "$proto.*:$port\$" && return 0 + echo "$listening_ports" | grep -q "$proto.*:$port " && return 0 + return 1 +} + +for key in "${!expected_ports[@]}"; do + IFS=':' read -r proto port <<<"$key" + if check_port "$proto" "$port"; then + log "Listening $proto port $port (${expected_ports[$key]})." + else + warn "Expected $proto port $port (${expected_ports[$key]}) is NOT listening." + fi +done + +# ------------------------------------------------------------------------- +# 4. Resource usage +# ------------------------------------------------------------------------- +log "Checking resource usage …" +disk_usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%') +mem_usage=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}') +load_avg=$(uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | xargs) + +log "Disk usage: ${disk_usage}%" +log "Memory usage: ${mem_usage}%" +log "Load average (1m): ${load_avg}" + +if [[ $disk_usage -gt 90 ]]; then + warn "Disk usage is critical (${disk_usage}%)" +elif [[ $disk_usage -gt 80 ]]; then + warn "Disk usage is high (${disk_usage}%)" +fi + +if [[ $mem_usage -gt 90 ]]; then + warn "Memory usage is critical (${mem_usage}%)" +elif [[ $mem_usage -gt 80 ]]; then + warn "Memory usage is high (${mem_usage}%)" +fi + +# ------------------------------------------------------------------------- +# 5. Summary +# ------------------------------------------------------------------------- +log "Health-check completed." diff --git a/video-script.md b/video-script.md new file mode 100644 index 0000000..6fe77d6 --- /dev/null +++ b/video-script.md @@ -0,0 +1,318 @@ +# Video Script: Local LLM Delegation for Infrastructure Debugging - Real Token Measurements + +**Target Length**: 5-7 minutes +**Tone**: Technical, honest, practical +**Audience**: Claude Code users, AI developers, home lab enthusiasts + +--- + +## INTRO (0:00 - 0:45) + +**[HOOK - Text on screen or talking head]** + +"I saved 52,000 Claude tokens on a single debugging session by delegating work to my local LLM." + +**[Pause]** + +"Now, this pattern isn't new. Projects like CC Token Saver, Ollama Claude, and Rubber Duck MCP have been doing local LLM delegation for a while." + +**[Show logos/links of prior art]** + +"What I built is an implementation focused on infrastructure debugging - with built-in SSH execution and detailed token measurements. If you're doing DevOps work with Claude Code, this might be useful." + +"Let me show you the actual numbers." + +--- + +## THE PROBLEM (0:30 - 1:30) + +**[Show diagram: Claude processing large log file]** + +"Here's the problem. When you ask Claude to analyze something big - like 200 lines of server logs, or disk usage across multiple systems - every single character of that output gets loaded into Claude's context." + +**[Show token counter animation: 0 → 15,000]** + +"That's 15,000 tokens just to look at some logs. And you're paying for every one of them." + +**[Show typical workflow diagram]** + +"Even if you're using an MCP server to connect Claude to a local LLM, the data still flows through Claude's context. You ask Claude, Claude asks the local LLM, the local LLM responds, Claude processes the response, Claude gives you an answer." + +"The raw data touches Claude's context twice. That's expensive." + +--- + +## THE SOLUTION (1:30 - 2:30) + +**[Show new architecture diagram]** + +"What if the local LLM could execute tools directly, without Claude ever seeing the raw output?" + +**[Animated diagram showing autonomous agent loop]** + +"Here's what I built. An autonomous agent that runs entirely inside the MCP server." + +"Step 1: Claude sends a task to the agent - 'check the server health'" + +"Step 2: The local LLM decides what tools to call - in this case, SSH to run some commands" + +"Step 3: The MCP server executes those commands internally. Claude never sees the 15,000 characters of output." + +"Step 4: The local LLM analyzes the results and produces a concise summary" + +"Step 5: Only that summary goes back to Claude" + +**[Show comparison: 11,800 tokens vs 800 tokens]** + +"That's a 93% reduction in Claude tokens. The other 11,000 tokens? They run on your local LLM - completely free." + +--- + +## REAL TEST RESULTS (2:30 - 3:30) + +**[Show terminal or results table]** + +"Let me show you the actual token math from my testing." + +**[Show bullet breakdown - animate each line]** + +"Here's how a security audit breaks down:" + +**Claude Direct** (no agent): +- Raw SSH output: ~44,000 chars ≈ 11,000 tokens +- Conversation overhead: ~800 tokens +- **Total Claude tokens: ~11,800** + +**Claude with Agent**: +- Task request to agent: ~100 tokens +- Agent's summary response: ~700 tokens +- **Total Claude tokens: ~800** + +**Local LLM** (inside agent - FREE): +- Processes ~44,000 chars raw output: ~11,000 tokens +- Analysis and formatting: ~1,000 tokens +- **Total local tokens: ~12,000** + +"The tokens don't disappear - they shift from paid to free. 11,000 tokens move to your local LLM." + +**[Table appears on screen - sorted by Claude Direct tokens, highest first]** + +| Task | Claude (Direct) | Claude (w/ Agent) | Local LLM (free) | Savings | +|------|-----------------|-------------------|------------------|---------| +| **Debugging workflow (7 calls)** | **~56,000** | **~4,100** | **~35,000** | **93%** | +| **Security audit** | **~11,800** | **~800** | **~11,000** | **93%** | +| **Docker logs analysis** | **~10,500** | **~500** | **~10,000** | **95%** | +| System health check | ~5,500 | ~1,500 | ~4,000 | 73% | +| Log analysis (journalctl) | ~4,000 | ~800 | ~3,200 | 80% | +| Code gen (w/ exploration) | ~2,700 | ~1,700 | ~1,000 | 37% | +| Disk analysis | ~1,500 | ~500 | ~1,000 | 65% | +| Code gen (small input) | ~1,550 | ~1,600 | ~1,500 | 0% | +| Simple query | ~500 | ~300 | ~200 | 40% | + +"See the pattern? The tokens shift from Claude to your local LLM. Big raw data = big savings. But notice code gen with small inputs - zero savings. The output size dominates, so no benefit there. This works best when you're processing large amounts of data." + +**[Show actual agent response]** + +"Here's a real response from the security audit. I asked it to analyze SSH logs, sudo usage, and check for suspicious activity." + +**[Show JSON response with tools_executed]** + +```json +{ + "type": "final_answer", + "content": "Security Audit Summary... No failed logins, + direct root SSH from internal IP (Medium severity), + repeated sudo auth failures (Medium)...", + "tokens_used": 1115, + "tools_executed": [{ + "tool": "ssh_exec", + "result_length": 43821 + }] +} +``` + +"43,821 characters of security logs. Claude never saw any of it. I just got a severity-rated summary with recommendations." + +--- + +## REAL-WORLD DEBUGGING (3:30 - 4:30) + +**[Show Nextcloud Talk error message]** + +"But here's where it gets really powerful. Let me show you a real debugging session." + +"Nextcloud Talk was returning HTTP 400 errors. Instead of me manually SSHing around and copying logs into Claude, I used the agent." + +**[Show orchestration diagram]** + +``` +Claude Code (Orchestrator) Local LLM Agent (Executor) + │ │ + │ "Check signaling + logs" │ + ├───────────────────────────────────►│ SSH, parse 15K chars + │◄───────────────────────────────────┤ "Signaling OK, no errors" + │ │ + │ "Check rate limits + permissions" │ + ├───────────────────────────────────►│ SSH, DB query + │◄───────────────────────────────────┤ "Rate limiting on, perms OK" + │ │ + │ "Enable debug, get exact error" │ + ├───────────────────────────────────►│ SSH, parse stack trace + │◄───────────────────────────────────┤ "SSL cert not trusted" + │ │ + │ "Add cert, test API" │ + ├───────────────────────────────────►│ SSH, apply fix + │◄───────────────────────────────────┤ "HTTP 201 - fixed!" +``` + +"Seven agent calls. Each one focused on a specific question. Claude decided what to check next, the agent did the grunt work." + +**[Show token breakdown table]** + +| Debugging Phase | CC Direct | CC w/ Agent | Saved | +|-----------------|-----------|-------------|-------| +| Signaling + log analysis | ~15,000 | ~800 | 95% | +| Config checks | ~8,000 | ~600 | 92% | +| Rate limit investigation | ~6,000 | ~500 | 92% | +| Permissions diagnosis | ~10,000 | ~600 | 94% | +| Debug + stack trace | ~8,000 | ~700 | 91% | +| CA cert investigation | ~5,000 | ~500 | 90% | +| Apply + verify fix | ~4,000 | ~400 | 90% | +| **Total** | **~56,000** | **~4,100** | **93%** | + +"56,000 tokens if I'd done this manually with Claude. 4,100 with the agent. That's 52,000 tokens saved on one debugging session." + +"And the issue? Self-signed SSL cert wasn't in the container's trust store. Ten minutes, problem solved." + +--- + +## HOW IT WORKS (4:30 - 5:30) + +**[Show code or configuration]** + +"Setting this up is straightforward." + +"First, you need a local LLM running via llama-server. I'm using a 120B parameter model with 128K context, but smaller models work too." + +**[Show config snippet]** + +```json +{ + "mcpServers": { + "llama-local": { + "command": "node", + "args": ["/path/to/llama-mcp-server/dist/index.js"], + "env": { + "LLAMA_SERVER_URL": "http://your-server:8080" + } + } + } +} +``` + +"Then you just call agent_chat with your task. One call. The agent handles everything." + +**[Show usage example]** + +``` +agent_chat({ + task: "Analyze the last 200 journal lines and identify any errors" +}) +``` + +"The agent automatically has access to SSH for your configured hosts. It figures out what commands to run, executes them, analyzes the results, and gives you a clean summary." + +--- + +## KEY FEATURES (5:30 - 6:00) + +**[Bullet points appearing on screen]** + +"Some key features of this implementation:" + +- "**Autonomous execution** - the agentic loop runs inside the MCP server" +- "**Built-in SSH** - agent can run commands on your infrastructure" +- "**GPG-encrypted credentials** - secure storage for SSH passwords" +- "**Strict output formatting** - the agent follows rules for clean JSON tool calls and plain text answers" +- "**Unlimited local tokens** - no artificial limits, use your full context window" +- "**Debug logging** - set DEBUG_MCP=1 to see exactly what's happening" + +--- + +## CALL TO ACTION (6:00 - 6:30) + +**[Show GitHub link]** + +"The code is open source. Link in the description." + +"github.com/lambertmt/llama-mcp-server - branch: feature/agent-tool-calling" + +"If you're running Claude Code with a local LLM setup, try this out. The token savings are real, and it makes Claude way more useful for infrastructure tasks." + +**[Closing]** + +"If you found this useful, let me know in the comments. And if you're using this for something cool, I'd love to hear about it." + +"Thanks for watching." + +--- + +## B-ROLL / VISUAL SUGGESTIONS + +1. **Terminal recordings**: Show actual agent_chat calls and responses +2. **Architecture diagrams**: Animate the flow of data (tools like Excalidraw or Mermaid) +3. **Token counter**: Animated number going up (Claude direct) vs staying low (autonomous agent) +4. **Split screen**: Claude direct on left (lots of text scrolling) vs agent on right (clean summary) +5. **Code highlights**: Zoom into key parts of the config and usage examples + +--- + +## THUMBNAIL SUGGESTIONS + +Option A: "52K tokens saved" with debugging terminal screenshot +Option B: Split image - Claude Direct vs Agent (token counters) +Option C: "Infrastructure + Local LLM" with server icons + +--- + +## DESCRIPTION / METADATA + +**Title Options:** +- "Local LLM Delegation for Infrastructure Debugging - Real Token Measurements" +- "52,000 Tokens Saved: Infrastructure Debugging with Local LLM Agents" +- "Building on CC Token Saver: SSH Execution for Claude Code" + +**Description:** +``` +I implemented local LLM delegation for infrastructure debugging with built-in +SSH execution. Here are my actual token measurements. + +This pattern isn't new - CC Token Saver, Ollama Claude, and Rubber Duck MCP +do similar things. My implementation focuses on DevOps use cases with +autonomous SSH execution. + +Real results from a Nextcloud debugging session: +- Claude Direct: ~56,000 tokens +- With Agent: ~4,100 tokens +- Savings: 93% + +Prior Art: +- CC Token Saver: https://github.com/csabakecskemeti/cc_token_saver_mcp +- Ollama Claude: https://mcpmarket.com/server/ollama-claude +- Rubber Duck: https://reddit.com/r/ClaudeAI/comments/1n9vxfp/ + +My Implementation: +GitHub: https://github.com/lambertmt/llama-mcp-server +Branch: feature/agent-tool-calling + +Tested with: +- Claude Code (Opus 4.5) +- GPT-OSS 120B (Q8) via llama.cpp +- AMD Strix Halo server with 128K context + +#ClaudeAI #LocalLLM #MCP #DevOps #OpenSource +``` + +**Tags:** +claude, claude code, anthropic, local llm, llama.cpp, mcp, model context protocol, +ai agents, autonomous agents, token optimization, api costs, open source ai