Initial commit: Add NanoClaw MCP extension with README and secure tests
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# NanoClaw MCP Extension
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This is an extension for the Pi Coding Agent (`@mariozechner/pi-coding-agent`) that integrates tools from a NanoClaw Model Context Protocol (MCP) server. It allows the Pi agent to dynamically fetch and execute tools exposed by a remote or local NanoClaw server.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
When a new Pi session starts, the extension connects to the NanoClaw MCP server via a `StreamableHTTPClientTransport`. It authenticates using a Bearer token, queries the available tools (`listTools`), and automatically registers them within the Pi environment so the AI can use them.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
The extension is configured using the following environment variables:
|
||||||
|
|
||||||
|
* `NANOCLAW_MCP_TOKEN` **(Required)**: The authentication token for the MCP server. If missing, the extension will skip loading the tools.
|
||||||
|
* `NANOCLAW_MCP_URL` **(Optional)**: The URL of the NanoClaw MCP server endpoint. Defaults to `http://localhost:3002/mcp`.
|
||||||
|
|
||||||
|
## Usage & Example
|
||||||
|
|
||||||
|
1. Ensure the extension is placed in your Pi extensions folder (or configured in your workspace to be loaded).
|
||||||
|
2. Set the necessary environment variables and start Pi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export NANOCLAW_MCP_TOKEN="your-secret-token"
|
||||||
|
# Optional: export NANOCLAW_MCP_URL="http://localhost:3002/mcp"
|
||||||
|
|
||||||
|
pi
|
||||||
|
```
|
||||||
|
|
||||||
|
3. When Pi starts, you will see a notification in the UI:
|
||||||
|
`Connected to nanoclaw MCP server. Loaded X tools: tool_name, another_tool`
|
||||||
|
4. You can now prompt the AI in the chat to use any of the dynamically loaded tools.
|
||||||
|
|
||||||
|
## Connecting via SSH Tunnel
|
||||||
|
|
||||||
|
Often, the NanoClaw MCP server is running on a remote machine (e.g., a production server or a different development environment) and shouldn't be exposed directly to the public internet.
|
||||||
|
|
||||||
|
In this case, you can use an **SSH Tunnel** to securely forward the traffic.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
Forward remote port `3002` to your local port `3002`:
|
||||||
|
```bash
|
||||||
|
ssh -L 3002:localhost:3002 user@remote-nanoclaw-host
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the tunnel is active, you can simply run Pi locally without changing the `NANOCLAW_MCP_URL` (since it defaults to `http://localhost:3002/mcp`). The traffic will be securely routed through the SSH tunnel to the NanoClaw instance.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
|
const token = process.env.NANOCLAW_MCP_TOKEN;
|
||||||
|
const serverUrl = process.env.NANOCLAW_MCP_URL || "http://localhost:3002/mcp";
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
ctx.ui.notify("NANOCLAW_MCP_TOKEN environment variable is missing. MCP tools won't be loaded.", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ui.notify(`Connecting to nanoclaw MCP server at ${serverUrl}...`, "info");
|
||||||
|
try {
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
||||||
|
requestInit: {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new Client(
|
||||||
|
{ name: "pi-mcp-client", version: "1.0.0" },
|
||||||
|
{ capabilities: { tools: {} } }
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.connect(transport);
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
|
||||||
|
for (const tool of tools) {
|
||||||
|
pi.registerTool({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description || `MCP Tool: ${tool.name}`,
|
||||||
|
parameters: Type.Any(),
|
||||||
|
async execute(id, params) {
|
||||||
|
try {
|
||||||
|
const result = await client.callTool({
|
||||||
|
name: tool.name,
|
||||||
|
arguments: params as any
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isError) {
|
||||||
|
throw new Error(result.content.map((c: any) => c.text).join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: result.content.map((c: any) => ({
|
||||||
|
type: "text",
|
||||||
|
text: c.text || JSON.stringify(c)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(`MCP Tool Error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ui.notify(`Connected to nanoclaw MCP server. Loaded ${tools.length} tools: ${tools.map(t => t.name).join(', ')}`, "success");
|
||||||
|
} catch (error: any) {
|
||||||
|
ctx.ui.notify(`Failed to connect to nanoclaw MCP server: ${error.message}`, "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||||
|
import * as EventSource from "eventsource";
|
||||||
|
|
||||||
|
global.EventSource = EventSource;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Connecting...");
|
||||||
|
const transport = new SSEClientTransport(new URL("http://localhost:3002/mcp"), {
|
||||||
|
requestInit: {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer MyToKeN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eventSourceInit: {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer MyToKeN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new Client({ name: "test", version: "1.0.0" }, { capabilities: { tools: {} } });
|
||||||
|
|
||||||
|
await client.connect(transport);
|
||||||
|
console.log("Connected! Fetching tools...");
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
console.log("Tools found:", tools.map(t => t.name));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(console.error);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Connecting with StreamableHTTPClientTransport...");
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL("http://localhost:3002/mcp"), {
|
||||||
|
requestInit: {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer MyToKeN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new Client({ name: "test", version: "1.0.0" }, { capabilities: { tools: {} } });
|
||||||
|
|
||||||
|
await client.connect(transport);
|
||||||
|
console.log("Connected! Fetching tools...");
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
console.log("Tools found:", tools.map(t => t.name));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(console.error);
|
||||||
Generated
+4862
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "nanoclaw-mcp-ext",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"eventsource": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mariozechner/pi-coding-agent": "^0.70.2",
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
|
"typebox": "^1.1.33",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user