211 lines
4.7 KiB
Markdown
211 lines
4.7 KiB
Markdown
---
|
|||
|
|
name: add-mcp
|
||
|
|
description: Add an MCP (Model Context Protocol) server to NanoClaw so external agents can chat with the assistant via Streamable HTTP. Includes SSH tunnel setup for secure access.
|
||
|
|
---
|
||
|
|
|
||
|
|
# Add MCP Server
|
||
|
|
|
||
|
|
This skill adds an MCP server to NanoClaw, allowing external agents (Claude Code, pi.dev, custom clients) to have a dialog with the assistant.
|
||
|
|
|
||
|
|
## Phase 1: Pre-flight
|
||
|
|
|
||
|
|
### Check if already applied
|
||
|
|
|
||
|
|
Check if `src/mcp-server.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||
|
|
|
||
|
|
## Phase 2: Apply Code Changes
|
||
|
|
|
||
|
|
### Install dependencies
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm install @modelcontextprotocol/sdk zod
|
||
|
|
```
|
||
|
|
|
||
|
|
### Copy source file
|
||
|
|
|
||
|
|
Copy `src/mcp-server.ts` from the skill directory into the project:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cp ${CLAUDE_SKILL_DIR}/src/mcp-server.ts src/mcp-server.ts
|
||
|
|
```
|
||
|
|
|
||
|
|
### Modify channel barrel file
|
||
|
|
|
||
|
|
Read the intent file at `${CLAUDE_SKILL_DIR}/modify/src/channels/index.ts.intent.md`, then apply it to `src/channels/index.ts`.
|
||
|
|
|
||
|
|
Add this import line (before or after existing channel imports):
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// mcp
|
||
|
|
import '../mcp-server.js';
|
||
|
|
```
|
||
|
|
|
||
|
|
### Modify config.ts
|
||
|
|
|
||
|
|
Add `'MCP_API_KEY'` and `'MCP_PORT'` to the `readEnvFile` array in `src/config.ts`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const envConfig = readEnvFile([
|
||
|
|
// ... existing keys ...
|
||
|
|
'MCP_API_KEY',
|
||
|
|
'MCP_PORT',
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
Add these exports (before or after existing config exports):
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// MCP server for external agent access
|
||
|
|
export const MCP_API_KEY =
|
||
|
|
process.env.MCP_API_KEY || envConfig.MCP_API_KEY || '';
|
||
|
|
export const MCP_PORT = parseInt(
|
||
|
|
process.env.MCP_PORT || envConfig.MCP_PORT || '3002',
|
||
|
|
10,
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Modify index.ts
|
||
|
|
|
||
|
|
Add imports at the top:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { startMcpServer } from './mcp-server.js';
|
||
|
|
import { MCP_API_KEY, MCP_PORT } from './config.js';
|
||
|
|
```
|
||
|
|
|
||
|
|
Note: `MCP_API_KEY` and `MCP_PORT` should be added to the existing config import, not a separate one.
|
||
|
|
|
||
|
|
In the `main()` function, after `startCredentialProxy(...)`, add:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Start external MCP server if API key is configured
|
||
|
|
let mcpServer: ReturnType<typeof startMcpServer> | undefined;
|
||
|
|
if (MCP_API_KEY) {
|
||
|
|
mcpServer = startMcpServer(MCP_PORT, MCP_API_KEY, {
|
||
|
|
registeredGroups: () => registeredGroups,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
In the `shutdown()` function, add before `queue.shutdown(...)`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
(await mcpServer)?.close();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Validate
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm run build
|
||
|
|
```
|
||
|
|
|
||
|
|
Build must succeed before proceeding.
|
||
|
|
|
||
|
|
## Phase 3: Setup
|
||
|
|
|
||
|
|
### Generate API Key
|
||
|
|
|
||
|
|
```bash
|
||
|
|
openssl rand -base64 32
|
||
|
|
```
|
||
|
|
|
||
|
|
### Configure environment
|
||
|
|
|
||
|
|
Add to `.env`:
|
||
|
|
|
||
|
|
```
|
||
|
|
MCP_API_KEY=<generated-key>
|
||
|
|
MCP_PORT=3002
|
||
|
|
```
|
||
|
|
|
||
|
|
### Rebuild and restart
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm run build
|
||
|
|
```
|
||
|
|
|
||
|
|
Then restart the service:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Linux (systemd)
|
||
|
|
sudo systemctl restart nanoclaw
|
||
|
|
|
||
|
|
# macOS (launchd)
|
||
|
|
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||
|
|
```
|
||
|
|
|
||
|
|
### Verify
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Check MCP server is listening
|
||
|
|
ss -tlnp | grep 3002 # Linux
|
||
|
|
lsof -i :3002 # macOS
|
||
|
|
|
||
|
|
# Check logs
|
||
|
|
grep "MCP server listening" logs/nanoclaw.log | tail -1
|
||
|
|
grep "MCP channel connected" logs/nanoclaw.log | tail -1
|
||
|
|
```
|
||
|
|
|
||
|
|
Both log lines should be present.
|
||
|
|
|
||
|
|
### Test
|
||
|
|
|
||
|
|
```bash
|
||
|
|
export MCP_API_KEY="<your-key>"
|
||
|
|
|
||
|
|
curl -s -X POST http://localhost:3002/mcp \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-H "Accept: application/json, text/event-stream" \
|
||
|
|
-H "Authorization: Bearer $MCP_API_KEY" \
|
||
|
|
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
||
|
|
```
|
||
|
|
|
||
|
|
Should return `chat` and `list_groups` tools.
|
||
|
|
|
||
|
|
## Connecting External Agents
|
||
|
|
|
||
|
|
The MCP server binds to `127.0.0.1` only (not publicly accessible). External clients connect via SSH tunnel:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh -L 3002:localhost:3002 user@your-server -N
|
||
|
|
```
|
||
|
|
|
||
|
|
Then configure the MCP client:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"mcpServers": {
|
||
|
|
"nanoclaw": {
|
||
|
|
"type": "streamable-http",
|
||
|
|
"url": "http://localhost:3002/mcp",
|
||
|
|
"headers": {
|
||
|
|
"Authorization": "Bearer <MCP_API_KEY>"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
See the [pi.dev extension](../index.ts) in this repo for a working client implementation.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
- Registers as a NanoClaw **Channel** (like Telegram or WhatsApp)
|
||
|
|
- Uses `mcp:` JID namespace — isolated from all other channels
|
||
|
|
- Same container, workspace, tools, and CLAUDE.md as the target group
|
||
|
|
- Container reuse via GroupQueue (cold start ~2-3 min, warm ~5-10s)
|
||
|
|
- Responses debounced (3s) for multi-part agent output
|
||
|
|
- Bearer token auth + SSH tunnel for security
|
||
|
|
|
||
|
|
## Troubleshooting
|
||
|
|
|
||
|
|
**MCP server not starting:** Check `MCP_API_KEY` is set in `.env` and NanoClaw was rebuilt.
|
||
|
|
|
||
|
|
**401 Unauthorized:** Bearer token doesn't match `MCP_API_KEY`.
|
||
|
|
|
||
|
|
**Connection refused:** SSH tunnel not running, or NanoClaw not running.
|
||
|
|
|
||
|
|
**Slow first response:** Expected — cold start boots a container (~2-3 min). Subsequent calls reuse it.
|
||
|
|
|
||
|
|
**"(no response)":** Container timed out or crashed. Check `docker logs <container-name>`.
|