@romiluz/clawmongo 0.1.0-rc.2 → 0.1.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/branding.js +129 -0
- package/dist/cli/commands/init.js +173 -0
- package/dist/cli/commands/registry.js +15 -4
- package/dist/main.js +11 -2
- package/dist/modules/tool-runtime/executors/bash.js +234 -0
- package/dist/modules/tool-runtime/executors/index.js +4 -0
- package/dist/modules/tool-runtime/executors/server-tools.js +70 -0
- package/dist/modules/tool-runtime/executors/think.js +79 -0
- package/dist/modules/tool-runtime/mcp/connection.js +178 -0
- package/dist/modules/tool-runtime/mcp/index.js +11 -0
- package/dist/modules/tool-runtime/mcp/tool.js +72 -0
- package/dist/modules/tool-runtime/mcp/types.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMongo CLI Branding
|
|
3
|
+
*
|
|
4
|
+
* ASCII art logo, colors, and branding elements.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/branding
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* ClawMongo ASCII art logo.
|
|
10
|
+
*/
|
|
11
|
+
export const LOGO = `
|
|
12
|
+
\x1b[36m _____ _ __ __
|
|
13
|
+
/ ____| | | \\/ |
|
|
14
|
+
| | | | __ ___ __| \\ / | ___ _ __ __ _ ___
|
|
15
|
+
| | | |/ _\` \\ \\ /\\ / /| |\\/| |/ _ \\| '_ \\ / _\` |/ _ \\
|
|
16
|
+
| |____| | (_| |\\ V V / | | | | (_) | | | | (_| | (_) |
|
|
17
|
+
\\_____|_|\\__,_| \\_/\\_/ |_| |_|\\___/|_| |_|\\__, |\\___/
|
|
18
|
+
__/ |
|
|
19
|
+
|___/ \x1b[0m`;
|
|
20
|
+
/**
|
|
21
|
+
* Tagline displayed under the logo.
|
|
22
|
+
*/
|
|
23
|
+
export const TAGLINE = "\x1b[33m MongoDB is the brain, OpenClaw is the heart.\x1b[0m";
|
|
24
|
+
/**
|
|
25
|
+
* Short tagline for compact displays.
|
|
26
|
+
*/
|
|
27
|
+
export const TAGLINE_SHORT = "\x1b[2mMongoDB-native agentic AI assistant\x1b[0m";
|
|
28
|
+
/**
|
|
29
|
+
* Version banner with logo.
|
|
30
|
+
*/
|
|
31
|
+
export function getVersionBanner(version) {
|
|
32
|
+
return `${LOGO}
|
|
33
|
+
${TAGLINE}
|
|
34
|
+
|
|
35
|
+
\x1b[2mVersion ${version}\x1b[0m
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Welcome message for first-run experience.
|
|
40
|
+
*/
|
|
41
|
+
export function getWelcomeMessage(version) {
|
|
42
|
+
return `${LOGO}
|
|
43
|
+
${TAGLINE}
|
|
44
|
+
|
|
45
|
+
\x1b[32m✨ Welcome to ClawMongo v${version}!\x1b[0m
|
|
46
|
+
|
|
47
|
+
ClawMongo is a MongoDB-native fork of OpenClaw that leverages
|
|
48
|
+
MongoDB's capabilities (Atlas Search, Vector Search, \$rankFusion)
|
|
49
|
+
while maintaining full parity with OpenClaw's features.
|
|
50
|
+
|
|
51
|
+
\x1b[36m📚 Quick Start:\x1b[0m
|
|
52
|
+
|
|
53
|
+
\x1b[33mclawmongo init\x1b[0m Set up your configuration
|
|
54
|
+
\x1b[33mclawmongo chat\x1b[0m Start interactive chat
|
|
55
|
+
\x1b[33mclawmongo send\x1b[0m Send a one-shot message
|
|
56
|
+
\x1b[33mclawmongo health\x1b[0m Check system status
|
|
57
|
+
|
|
58
|
+
\x1b[36m🔧 Configuration:\x1b[0m
|
|
59
|
+
|
|
60
|
+
Set these environment variables:
|
|
61
|
+
|
|
62
|
+
\x1b[2mANTHROPIC_API_KEY\x1b[0m Your Anthropic API key
|
|
63
|
+
\x1b[2mCLAWMONGO_MONGODB_URI\x1b[0m MongoDB connection string (optional)
|
|
64
|
+
\x1b[2mVOYAGE_API_KEY\x1b[0m Voyage AI key for embeddings (optional)
|
|
65
|
+
|
|
66
|
+
\x1b[36m📖 Documentation:\x1b[0m
|
|
67
|
+
|
|
68
|
+
https://github.com/romiluz13/ClawMongo
|
|
69
|
+
|
|
70
|
+
\x1b[2mRun 'clawmongo --help' for all available commands.\x1b[0m
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Compact header for command output.
|
|
75
|
+
*/
|
|
76
|
+
export function getCompactHeader(version) {
|
|
77
|
+
return `\x1b[36m🐾 ClawMongo\x1b[0m v${version}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Success message with emoji.
|
|
81
|
+
*/
|
|
82
|
+
export function successMessage(text) {
|
|
83
|
+
return `\x1b[32m✓\x1b[0m ${text}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Error message with emoji.
|
|
87
|
+
*/
|
|
88
|
+
export function errorMessage(text) {
|
|
89
|
+
return `\x1b[31m✗\x1b[0m ${text}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Warning message with emoji.
|
|
93
|
+
*/
|
|
94
|
+
export function warnMessage(text) {
|
|
95
|
+
return `\x1b[33m⚠\x1b[0m ${text}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Info message with emoji.
|
|
99
|
+
*/
|
|
100
|
+
export function infoMessage(text) {
|
|
101
|
+
return `\x1b[36mℹ\x1b[0m ${text}`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Step indicator for multi-step processes.
|
|
105
|
+
*/
|
|
106
|
+
export function stepIndicator(current, total, text) {
|
|
107
|
+
return `\x1b[36m[${current}/${total}]\x1b[0m ${text}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Progress bar.
|
|
111
|
+
*/
|
|
112
|
+
export function progressBar(percent, width = 30) {
|
|
113
|
+
const filled = Math.round((percent / 100) * width);
|
|
114
|
+
const empty = width - filled;
|
|
115
|
+
const bar = "\x1b[32m" + "█".repeat(filled) + "\x1b[2m" + "░".repeat(empty) + "\x1b[0m";
|
|
116
|
+
return `${bar} ${percent.toFixed(0)}%`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Box drawing for important messages.
|
|
120
|
+
*/
|
|
121
|
+
export function boxMessage(title, lines) {
|
|
122
|
+
const maxLen = Math.max(title.length, ...lines.map(l => l.length));
|
|
123
|
+
const top = `┌${"─".repeat(maxLen + 2)}┐`;
|
|
124
|
+
const titleLine = `│ \x1b[1m${title.padEnd(maxLen)}\x1b[0m │`;
|
|
125
|
+
const sep = `├${"─".repeat(maxLen + 2)}┤`;
|
|
126
|
+
const content = lines.map(l => `│ ${l.padEnd(maxLen)} │`).join("\n");
|
|
127
|
+
const bottom = `└${"─".repeat(maxLen + 2)}┘`;
|
|
128
|
+
return `${top}\n${titleLine}\n${sep}\n${content}\n${bottom}`;
|
|
129
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMongo Init Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive setup wizard for first-time configuration.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/commands/init
|
|
7
|
+
*/
|
|
8
|
+
import * as readline from "readline";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { ExitCode } from "./types.js";
|
|
12
|
+
import { createOutput } from "./output.js";
|
|
13
|
+
import { LOGO, TAGLINE, successMessage, errorMessage, stepIndicator, boxMessage } from "../branding.js";
|
|
14
|
+
/**
|
|
15
|
+
* Prompt user for input with optional default.
|
|
16
|
+
*/
|
|
17
|
+
async function prompt(rl, question, defaultValue, isSecret = false) {
|
|
18
|
+
const defaultHint = defaultValue ? ` \x1b[2m(${isSecret ? "****" : defaultValue})\x1b[0m` : "";
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
rl.question(` ${question}${defaultHint}: `, (answer) => {
|
|
21
|
+
resolve(answer.trim() || defaultValue || "");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Prompt for yes/no confirmation.
|
|
27
|
+
*/
|
|
28
|
+
async function confirm(rl, question, defaultYes = true) {
|
|
29
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
rl.question(` ${question} ${hint}: `, (answer) => {
|
|
32
|
+
const a = answer.trim().toLowerCase();
|
|
33
|
+
if (a === "")
|
|
34
|
+
resolve(defaultYes);
|
|
35
|
+
else
|
|
36
|
+
resolve(a === "y" || a === "yes");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Select from a list of options using arrow keys.
|
|
42
|
+
*/
|
|
43
|
+
async function select(rl, question, options, defaultIndex = 0) {
|
|
44
|
+
console.log(`\n ${question}\n`);
|
|
45
|
+
let selectedIndex = defaultIndex;
|
|
46
|
+
// For simplicity, use numbered selection (arrow keys require raw mode)
|
|
47
|
+
options.forEach((opt, i) => {
|
|
48
|
+
const marker = i === defaultIndex ? "\x1b[36m→\x1b[0m" : " ";
|
|
49
|
+
console.log(` ${marker} ${i + 1}. ${opt.label}`);
|
|
50
|
+
});
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
rl.question(`\n Enter number (1-${options.length}): `, (answer) => {
|
|
53
|
+
const num = parseInt(answer.trim(), 10);
|
|
54
|
+
if (num >= 1 && num <= options.length) {
|
|
55
|
+
const selected = options[num - 1];
|
|
56
|
+
resolve(selected ? selected.value : options[0]?.value ?? "");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const defaultOpt = options[defaultIndex];
|
|
60
|
+
resolve(defaultOpt ? defaultOpt.value : options[0]?.value ?? "");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Write config to .env file.
|
|
67
|
+
*/
|
|
68
|
+
function writeEnvFile(config, filePath) {
|
|
69
|
+
const lines = [
|
|
70
|
+
"# ClawMongo Configuration",
|
|
71
|
+
"# Generated by 'clawmongo init'",
|
|
72
|
+
"",
|
|
73
|
+
];
|
|
74
|
+
if (config.anthropicApiKey) {
|
|
75
|
+
lines.push(`ANTHROPIC_API_KEY=${config.anthropicApiKey}`);
|
|
76
|
+
}
|
|
77
|
+
if (config.mongodbUri) {
|
|
78
|
+
lines.push(`CLAWMONGO_MONGODB_URI=${config.mongodbUri}`);
|
|
79
|
+
}
|
|
80
|
+
if (config.voyageApiKey) {
|
|
81
|
+
lines.push(`VOYAGE_API_KEY=${config.voyageApiKey}`);
|
|
82
|
+
}
|
|
83
|
+
if (config.openaiApiKey) {
|
|
84
|
+
lines.push(`OPENAI_API_KEY=${config.openaiApiKey}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
fs.writeFileSync(filePath, lines.join("\n"));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Init command - interactive setup wizard.
|
|
91
|
+
*/
|
|
92
|
+
export const initCommand = {
|
|
93
|
+
name: "init",
|
|
94
|
+
aliases: ["setup", "configure"],
|
|
95
|
+
description: "Interactive setup wizard",
|
|
96
|
+
usage: "clawmongo init [--force]",
|
|
97
|
+
async execute(ctx) {
|
|
98
|
+
const out = createOutput({ json: ctx.json });
|
|
99
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
100
|
+
// Check if .env already exists
|
|
101
|
+
if (fs.existsSync(envPath) && !ctx.flags.force) {
|
|
102
|
+
out.writeln(errorMessage(".env file already exists. Use --force to overwrite."));
|
|
103
|
+
return ExitCode.ERROR;
|
|
104
|
+
}
|
|
105
|
+
// Show welcome banner
|
|
106
|
+
console.log(LOGO);
|
|
107
|
+
console.log(TAGLINE);
|
|
108
|
+
console.log("\n \x1b[1m✨ Welcome to ClawMongo Setup!\x1b[0m\n");
|
|
109
|
+
console.log(" This wizard will help you configure ClawMongo.");
|
|
110
|
+
console.log(" Press Enter to accept defaults, or type your values.\n");
|
|
111
|
+
const rl = readline.createInterface({
|
|
112
|
+
input: process.stdin,
|
|
113
|
+
output: process.stdout
|
|
114
|
+
});
|
|
115
|
+
const config = {};
|
|
116
|
+
try {
|
|
117
|
+
// Step 1: Anthropic API Key
|
|
118
|
+
console.log(stepIndicator(1, 4, "\x1b[1mAnthropic API Key\x1b[0m"));
|
|
119
|
+
console.log(" \x1b[2mRequired for Claude AI. Get yours at https://console.anthropic.com\x1b[0m\n");
|
|
120
|
+
config.anthropicApiKey = await prompt(rl, "ANTHROPIC_API_KEY", process.env.ANTHROPIC_API_KEY, true);
|
|
121
|
+
// Step 2: MongoDB URI
|
|
122
|
+
console.log("\n" + stepIndicator(2, 4, "\x1b[1mMongoDB Connection\x1b[0m"));
|
|
123
|
+
console.log(" \x1b[2mOptional. Enables persistent storage, Atlas Search, and vector search.\x1b[0m\n");
|
|
124
|
+
const useMongo = await confirm(rl, "Configure MongoDB?", true);
|
|
125
|
+
if (useMongo) {
|
|
126
|
+
config.mongodbUri = await prompt(rl, "CLAWMONGO_MONGODB_URI", process.env.CLAWMONGO_MONGODB_URI);
|
|
127
|
+
}
|
|
128
|
+
// Step 3: Embedding Provider
|
|
129
|
+
console.log("\n" + stepIndicator(3, 4, "\x1b[1mEmbedding Provider\x1b[0m"));
|
|
130
|
+
console.log(" \x1b[2mOptional. Enables semantic search and RAG capabilities.\x1b[0m\n");
|
|
131
|
+
const embeddingProvider = await select(rl, "Select embedding provider:", [
|
|
132
|
+
{ value: "none", label: "None (skip for now)" },
|
|
133
|
+
{ value: "voyage", label: "Voyage AI (recommended)" },
|
|
134
|
+
{ value: "openai", label: "OpenAI" }
|
|
135
|
+
], 0);
|
|
136
|
+
if (embeddingProvider === "voyage") {
|
|
137
|
+
config.voyageApiKey = await prompt(rl, "VOYAGE_API_KEY", process.env.VOYAGE_API_KEY, true);
|
|
138
|
+
}
|
|
139
|
+
else if (embeddingProvider === "openai") {
|
|
140
|
+
config.openaiApiKey = await prompt(rl, "OPENAI_API_KEY", process.env.OPENAI_API_KEY, true);
|
|
141
|
+
}
|
|
142
|
+
// Step 4: Confirm and write
|
|
143
|
+
console.log("\n" + stepIndicator(4, 4, "\x1b[1mConfirm Configuration\x1b[0m\n"));
|
|
144
|
+
const summary = [
|
|
145
|
+
`Anthropic API Key: ${config.anthropicApiKey ? "✓ Set" : "✗ Not set"}`,
|
|
146
|
+
`MongoDB URI: ${config.mongodbUri ? "✓ Set" : "✗ Not set"}`,
|
|
147
|
+
`Voyage API Key: ${config.voyageApiKey ? "✓ Set" : "✗ Not set"}`,
|
|
148
|
+
`OpenAI API Key: ${config.openaiApiKey ? "✓ Set" : "✗ Not set"}`
|
|
149
|
+
];
|
|
150
|
+
console.log(boxMessage("Configuration Summary", summary));
|
|
151
|
+
console.log("");
|
|
152
|
+
const shouldWrite = await confirm(rl, "Write configuration to .env?", true);
|
|
153
|
+
if (shouldWrite) {
|
|
154
|
+
writeEnvFile(config, envPath);
|
|
155
|
+
console.log("\n" + successMessage(`Configuration saved to ${envPath}`));
|
|
156
|
+
console.log("\n \x1b[36m🚀 You're all set! Try these commands:\x1b[0m\n");
|
|
157
|
+
console.log(" \x1b[33mclawmongo health\x1b[0m Check system status");
|
|
158
|
+
console.log(" \x1b[33mclawmongo chat\x1b[0m Start interactive chat");
|
|
159
|
+
console.log(" \x1b[33mclawmongo send\x1b[0m Send a message\n");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log("\n" + errorMessage("Setup cancelled."));
|
|
163
|
+
}
|
|
164
|
+
rl.close();
|
|
165
|
+
return ExitCode.SUCCESS;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
rl.close();
|
|
169
|
+
out.error(err instanceof Error ? err.message : String(err));
|
|
170
|
+
return ExitCode.ERROR;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
@@ -74,15 +74,26 @@ export function generateHelp(command, prefix = "") {
|
|
|
74
74
|
export function generateGlobalHelp(registry, version) {
|
|
75
75
|
const commands = registry.list();
|
|
76
76
|
const lines = [];
|
|
77
|
-
|
|
78
|
-
lines.push("
|
|
77
|
+
// ASCII Logo
|
|
78
|
+
lines.push("\x1b[36m _____ _ __ __ ");
|
|
79
|
+
lines.push(" / ____| | | \\/ | ");
|
|
80
|
+
lines.push(" | | | | __ ___ __| \\ / | ___ _ __ __ _ ___ ");
|
|
81
|
+
lines.push(" | | | |/ _` \\ \\ /\\ / /| |\\/| |/ _ \\| '_ \\ / _` |/ _ \\ ");
|
|
82
|
+
lines.push(" | |____| | (_| |\\ V V / | | | | (_) | | | | (_| | (_) |");
|
|
83
|
+
lines.push(" \\_____|_|\\__,_| \\_/\\_/ |_| |_|\\___/|_| |_|\\__, |\\___/ ");
|
|
84
|
+
lines.push(" __/ | ");
|
|
85
|
+
lines.push(" |___/ \x1b[0m");
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push("\x1b[33m MongoDB is the brain, OpenClaw is the heart.\x1b[0m");
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push(` \x1b[2mVersion ${version}\x1b[0m`);
|
|
79
90
|
lines.push("");
|
|
80
91
|
lines.push("Usage: clawmongo <command> [options]");
|
|
81
92
|
lines.push("");
|
|
82
93
|
lines.push("Commands:");
|
|
83
94
|
for (const cmd of commands) {
|
|
84
95
|
const aliasStr = cmd.aliases ? ` (${cmd.aliases.join(", ")})` : "";
|
|
85
|
-
lines.push(` ${cmd.name.padEnd(12)}${aliasStr.padEnd(8)} ${cmd.description}`);
|
|
96
|
+
lines.push(` \x1b[33m${cmd.name.padEnd(12)}\x1b[0m${aliasStr.padEnd(8)} ${cmd.description}`);
|
|
86
97
|
}
|
|
87
98
|
lines.push("");
|
|
88
99
|
lines.push("Options:");
|
|
@@ -91,6 +102,6 @@ export function generateGlobalHelp(registry, version) {
|
|
|
91
102
|
lines.push(" --json Output as JSON");
|
|
92
103
|
lines.push(" -V, --verbose Verbose output");
|
|
93
104
|
lines.push("");
|
|
94
|
-
lines.push("
|
|
105
|
+
lines.push("\x1b[2mRun 'clawmongo <command> --help' for command-specific help.\x1b[0m");
|
|
95
106
|
return lines.join("\n");
|
|
96
107
|
}
|
package/dist/main.js
CHANGED
|
@@ -19,13 +19,17 @@ import { cronCommand } from "./cli/commands/cron.js";
|
|
|
19
19
|
import { backupCommand } from "./cli/commands/backup.js";
|
|
20
20
|
import { securityCommand } from "./cli/commands/security.js";
|
|
21
21
|
import { benchmarkCommand } from "./cli/commands/benchmark.js";
|
|
22
|
-
|
|
22
|
+
import { initCommand } from "./cli/commands/init.js";
|
|
23
|
+
// Import branding
|
|
24
|
+
import { getVersionBanner, getWelcomeMessage } from "./cli/branding.js";
|
|
25
|
+
const VERSION = "0.1.0-rc.4";
|
|
23
26
|
/**
|
|
24
27
|
* Build the command registry.
|
|
25
28
|
*/
|
|
26
29
|
function buildRegistry() {
|
|
27
30
|
const registry = createRegistry();
|
|
28
31
|
// Register all commands
|
|
32
|
+
registry.register(initCommand); // Setup wizard first
|
|
29
33
|
registry.register(healthCommand);
|
|
30
34
|
registry.register(statusCommand);
|
|
31
35
|
registry.register(doctorCommand);
|
|
@@ -51,7 +55,12 @@ async function main() {
|
|
|
51
55
|
const out = createOutput({ json: hasFlag(parsed.flags, "json") });
|
|
52
56
|
// Handle --version
|
|
53
57
|
if (hasFlag(parsed.flags, "version") || hasFlag(parsed.flags, "v")) {
|
|
54
|
-
out.writeln(
|
|
58
|
+
out.writeln(getVersionBanner(VERSION));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Handle no command - show welcome message
|
|
62
|
+
if (!parsed.command && !hasFlag(parsed.flags, "help")) {
|
|
63
|
+
out.writeln(getWelcomeMessage(VERSION));
|
|
55
64
|
return;
|
|
56
65
|
}
|
|
57
66
|
// Handle --help with no command
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Session Tool
|
|
3
|
+
*
|
|
4
|
+
* Persistent bash shell sessions with restart capability.
|
|
5
|
+
* Ported from OpenClaw's computer-use-demo/tools/bash.py.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Persistent sessions that maintain state
|
|
9
|
+
* - Automatic session creation
|
|
10
|
+
* - Restart capability for stuck sessions
|
|
11
|
+
* - Timeout handling
|
|
12
|
+
* - Tenant-scoped session management
|
|
13
|
+
*
|
|
14
|
+
* @module tool-runtime/executors/bash
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes
|
|
19
|
+
const OUTPUT_DELAY_MS = 200;
|
|
20
|
+
const SENTINEL = "<<exit>>";
|
|
21
|
+
/**
|
|
22
|
+
* Session registry - maps tenant to their active bash session
|
|
23
|
+
*/
|
|
24
|
+
const sessionRegistry = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* Get or create a bash session for a tenant
|
|
27
|
+
*/
|
|
28
|
+
function getSession(tenantId) {
|
|
29
|
+
return sessionRegistry.get(tenantId);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a new bash session
|
|
33
|
+
*/
|
|
34
|
+
async function createSession(tenantId) {
|
|
35
|
+
// Kill existing session if any
|
|
36
|
+
const existing = sessionRegistry.get(tenantId);
|
|
37
|
+
if (existing?.process) {
|
|
38
|
+
existing.process.kill();
|
|
39
|
+
}
|
|
40
|
+
const sessionId = `bash-${randomUUID().slice(0, 8)}`;
|
|
41
|
+
const proc = spawn("/bin/bash", [], {
|
|
42
|
+
shell: false,
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
44
|
+
});
|
|
45
|
+
const session = {
|
|
46
|
+
id: sessionId,
|
|
47
|
+
process: proc,
|
|
48
|
+
tenantId,
|
|
49
|
+
started: true,
|
|
50
|
+
timedOut: false,
|
|
51
|
+
stdout: "",
|
|
52
|
+
stderr: "",
|
|
53
|
+
createdAt: new Date()
|
|
54
|
+
};
|
|
55
|
+
sessionRegistry.set(tenantId, session);
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run a command in a bash session
|
|
60
|
+
*/
|
|
61
|
+
async function runCommand(session, command, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
62
|
+
if (!session.started) {
|
|
63
|
+
return { stdout: "", stderr: "", error: "Session has not started" };
|
|
64
|
+
}
|
|
65
|
+
if (session.process.exitCode !== null) {
|
|
66
|
+
return {
|
|
67
|
+
stdout: "",
|
|
68
|
+
stderr: "",
|
|
69
|
+
error: `Bash has exited with return code ${session.process.exitCode}. Tool must be restarted.`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (session.timedOut) {
|
|
73
|
+
return {
|
|
74
|
+
stdout: "",
|
|
75
|
+
stderr: "",
|
|
76
|
+
error: `Session timed out and must be restarted`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Clear buffers
|
|
80
|
+
session.stdout = "";
|
|
81
|
+
session.stderr = "";
|
|
82
|
+
// Setup output collection
|
|
83
|
+
const stdoutHandler = (data) => {
|
|
84
|
+
session.stdout += data.toString();
|
|
85
|
+
};
|
|
86
|
+
const stderrHandler = (data) => {
|
|
87
|
+
session.stderr += data.toString();
|
|
88
|
+
};
|
|
89
|
+
session.process.stdout?.on("data", stdoutHandler);
|
|
90
|
+
session.process.stderr?.on("data", stderrHandler);
|
|
91
|
+
// Send command with sentinel
|
|
92
|
+
session.process.stdin?.write(`${command}; echo '${SENTINEL}'\n`);
|
|
93
|
+
// Wait for output with sentinel
|
|
94
|
+
try {
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
const timeout = setTimeout(() => {
|
|
97
|
+
session.timedOut = true;
|
|
98
|
+
reject(new Error(`Bash timed out after ${timeoutMs}ms and must be restarted`));
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
const checkInterval = setInterval(() => {
|
|
101
|
+
if (session.stdout.includes(SENTINEL)) {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
clearInterval(checkInterval);
|
|
104
|
+
resolve();
|
|
105
|
+
}
|
|
106
|
+
}, OUTPUT_DELAY_MS);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
session.process.stdout?.removeListener("data", stdoutHandler);
|
|
111
|
+
session.process.stderr?.removeListener("data", stderrHandler);
|
|
112
|
+
return {
|
|
113
|
+
stdout: "",
|
|
114
|
+
stderr: "",
|
|
115
|
+
error: err instanceof Error ? err.message : String(err)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
session.process.stdout?.removeListener("data", stdoutHandler);
|
|
119
|
+
session.process.stderr?.removeListener("data", stderrHandler);
|
|
120
|
+
// Strip sentinel from output
|
|
121
|
+
let output = session.stdout;
|
|
122
|
+
const sentinelIdx = output.indexOf(SENTINEL);
|
|
123
|
+
if (sentinelIdx !== -1) {
|
|
124
|
+
output = output.slice(0, sentinelIdx);
|
|
125
|
+
}
|
|
126
|
+
if (output.endsWith("\n")) {
|
|
127
|
+
output = output.slice(0, -1);
|
|
128
|
+
}
|
|
129
|
+
let stderr = session.stderr;
|
|
130
|
+
if (stderr.endsWith("\n")) {
|
|
131
|
+
stderr = stderr.slice(0, -1);
|
|
132
|
+
}
|
|
133
|
+
return { stdout: output, stderr };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Validate bash tool parameters
|
|
137
|
+
*/
|
|
138
|
+
function validateBashParams(params) {
|
|
139
|
+
const command = params.command;
|
|
140
|
+
const restart = params.restart;
|
|
141
|
+
if (restart === true) {
|
|
142
|
+
return { restart: true };
|
|
143
|
+
}
|
|
144
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
145
|
+
return { error: "command is required (or set restart: true)" };
|
|
146
|
+
}
|
|
147
|
+
return { command: command.trim(), restart: false };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Bash tool executor
|
|
151
|
+
*/
|
|
152
|
+
export async function bashToolExecutor(params, context) {
|
|
153
|
+
const startTime = performance.now();
|
|
154
|
+
const tenantId = context.tenantId ?? "default";
|
|
155
|
+
// Handle restart
|
|
156
|
+
if (params.restart) {
|
|
157
|
+
await createSession(tenantId);
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
output: { system: "Tool has been restarted.", stdout: "", stderr: "" },
|
|
161
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
162
|
+
sideEffects: [`bash:restart:${context.toolCallId}`]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Get or create session
|
|
166
|
+
let session = getSession(tenantId);
|
|
167
|
+
if (!session) {
|
|
168
|
+
session = await createSession(tenantId);
|
|
169
|
+
}
|
|
170
|
+
// Run command
|
|
171
|
+
const result = await runCommand(session, params.command ?? "");
|
|
172
|
+
if (result.error) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
output: null,
|
|
176
|
+
error: result.error,
|
|
177
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
178
|
+
sideEffects: []
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
output: { stdout: result.stdout, stderr: result.stderr },
|
|
184
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
185
|
+
sideEffects: [`bash:exec:${context.toolCallId}`]
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create bash executor with validated params
|
|
190
|
+
*/
|
|
191
|
+
export function createBashExecutor(rawParams, context) {
|
|
192
|
+
const validation = validateBashParams(rawParams);
|
|
193
|
+
if ("error" in validation) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
output: null,
|
|
197
|
+
error: validation.error,
|
|
198
|
+
durationMs: 0,
|
|
199
|
+
sideEffects: []
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return bashToolExecutor(validation, context);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Cleanup bash sessions for a tenant
|
|
206
|
+
*/
|
|
207
|
+
export function cleanupBashSessions(tenantId) {
|
|
208
|
+
const session = sessionRegistry.get(tenantId);
|
|
209
|
+
if (session?.process) {
|
|
210
|
+
session.process.kill();
|
|
211
|
+
}
|
|
212
|
+
sessionRegistry.delete(tenantId);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Tool definition for the bash tool
|
|
216
|
+
*/
|
|
217
|
+
export const bashToolDefinition = {
|
|
218
|
+
name: "bash",
|
|
219
|
+
description: "Run commands in a persistent bash shell. The session persists between calls. " +
|
|
220
|
+
"Set restart: true to restart the shell if it times out or exits.",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
command: {
|
|
225
|
+
type: "string",
|
|
226
|
+
description: "The bash command to run."
|
|
227
|
+
},
|
|
228
|
+
restart: {
|
|
229
|
+
type: "boolean",
|
|
230
|
+
description: "Set to true to restart the bash session."
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
@@ -8,3 +8,7 @@ export { createExecExecutor, execToolExecutor } from "./exec.js";
|
|
|
8
8
|
export { cleanupTenantProcesses, createProcessExecutor, getProcessCount, processToolExecutor } from "./process.js";
|
|
9
9
|
export { createEditExecutor, createReadExecutor, createWriteExecutor, editToolExecutor, readToolExecutor, writeToolExecutor } from "./filesystem.js";
|
|
10
10
|
export { createSessionsListExecutor, createSessionStatusExecutor, sessionsListExecutor, sessionStatusExecutor } from "./session.js";
|
|
11
|
+
// New tools for OpenClaw parity
|
|
12
|
+
export { createThinkExecutor, thinkToolExecutor, thinkToolDefinition } from "./think.js";
|
|
13
|
+
export { createBashExecutor, bashToolExecutor, bashToolDefinition, cleanupBashSessions } from "./bash.js";
|
|
14
|
+
export { createCodeExecutionServerTool, createWebSearchServerTool, getDefaultServerTools, getServerToolBetaHeader, isServerTool, SERVER_TOOL_TYPES } from "./server-tools.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Tools - Anthropic Server-Side Tool Definitions
|
|
3
|
+
*
|
|
4
|
+
* These are special tools executed by Anthropic's servers, not locally.
|
|
5
|
+
* They use a different registration format and don't need local executors.
|
|
6
|
+
*
|
|
7
|
+
* Ported from OpenClaw's anthropic-quickstarts:
|
|
8
|
+
* - code_execution.py → CodeExecutionServerTool
|
|
9
|
+
* - web_search.py → WebSearchServerTool
|
|
10
|
+
*
|
|
11
|
+
* @module tool-runtime/executors/server-tools
|
|
12
|
+
*/
|
|
13
|
+
export function createCodeExecutionServerTool(options = {}) {
|
|
14
|
+
const name = options.name ?? "code_execution";
|
|
15
|
+
return {
|
|
16
|
+
type: "code_execution_20250522",
|
|
17
|
+
name
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function createWebSearchServerTool(options = {}) {
|
|
21
|
+
const name = options.name ?? "web_search";
|
|
22
|
+
const tool = {
|
|
23
|
+
type: "web_search_20250305",
|
|
24
|
+
name
|
|
25
|
+
};
|
|
26
|
+
if (options.maxUses !== undefined) {
|
|
27
|
+
tool.max_uses = options.maxUses;
|
|
28
|
+
}
|
|
29
|
+
if (options.allowedDomains !== undefined) {
|
|
30
|
+
tool.allowed_domains = options.allowedDomains;
|
|
31
|
+
}
|
|
32
|
+
if (options.blockedDomains !== undefined) {
|
|
33
|
+
tool.blocked_domains = options.blockedDomains;
|
|
34
|
+
}
|
|
35
|
+
if (options.userLocation !== undefined) {
|
|
36
|
+
tool.user_location = options.userLocation;
|
|
37
|
+
}
|
|
38
|
+
return tool;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Server tool type constants
|
|
42
|
+
*/
|
|
43
|
+
export const SERVER_TOOL_TYPES = {
|
|
44
|
+
CODE_EXECUTION: "code_execution_20250522",
|
|
45
|
+
WEB_SEARCH: "web_search_20250305"
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Check if a tool definition is a server tool
|
|
49
|
+
*/
|
|
50
|
+
export function isServerTool(toolDef) {
|
|
51
|
+
const type = toolDef.type;
|
|
52
|
+
return (type === SERVER_TOOL_TYPES.CODE_EXECUTION ||
|
|
53
|
+
type === SERVER_TOOL_TYPES.WEB_SEARCH);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get required beta header for server tools
|
|
57
|
+
*/
|
|
58
|
+
export function getServerToolBetaHeader() {
|
|
59
|
+
// The code execution tool requires this beta header
|
|
60
|
+
return "code-execution-2025-05-22";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Default server tools preset - both code execution and web search
|
|
64
|
+
*/
|
|
65
|
+
export function getDefaultServerTools() {
|
|
66
|
+
return [
|
|
67
|
+
createCodeExecutionServerTool(),
|
|
68
|
+
createWebSearchServerTool()
|
|
69
|
+
];
|
|
70
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Think Tool Executor
|
|
3
|
+
*
|
|
4
|
+
* Internal reasoning tool for the agent to think through problems
|
|
5
|
+
* without executing external actions. Ported from OpenClaw's think.py.
|
|
6
|
+
*
|
|
7
|
+
* This tool allows the model to:
|
|
8
|
+
* - Reason about complex problems step by step
|
|
9
|
+
* - Store intermediate thoughts in conversation history
|
|
10
|
+
* - Plan before taking actions
|
|
11
|
+
*
|
|
12
|
+
* @module tool-runtime/executors/think
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Validate think tool parameters
|
|
16
|
+
*/
|
|
17
|
+
function validateThinkParams(params) {
|
|
18
|
+
const thought = params.thought;
|
|
19
|
+
if (typeof thought !== "string" || thought.trim().length === 0) {
|
|
20
|
+
return { error: "thought is required and must be a non-empty string" };
|
|
21
|
+
}
|
|
22
|
+
return { thought: thought.trim() };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Think tool executor
|
|
26
|
+
*
|
|
27
|
+
* Simply acknowledges the thought, allowing the model to use it
|
|
28
|
+
* for chain-of-thought reasoning without external side effects.
|
|
29
|
+
*/
|
|
30
|
+
export const thinkToolExecutor = async (params, _context) => {
|
|
31
|
+
const startTime = performance.now();
|
|
32
|
+
// The think tool doesn't do anything external -
|
|
33
|
+
// it just acknowledges the thought so the model can reason
|
|
34
|
+
const result = {
|
|
35
|
+
message: "Thinking complete!",
|
|
36
|
+
thought: params.thought
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
output: result,
|
|
41
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
42
|
+
sideEffects: [] // No side effects - purely internal reasoning
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Create think executor with validated params
|
|
47
|
+
*/
|
|
48
|
+
export function createThinkExecutor(rawParams, context) {
|
|
49
|
+
const validation = validateThinkParams(rawParams);
|
|
50
|
+
if ("error" in validation) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
output: null,
|
|
54
|
+
error: validation.error,
|
|
55
|
+
durationMs: 0,
|
|
56
|
+
sideEffects: []
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return thinkToolExecutor(validation, context);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Tool definition for the think tool (for registration)
|
|
63
|
+
*/
|
|
64
|
+
export const thinkToolDefinition = {
|
|
65
|
+
name: "think",
|
|
66
|
+
description: "Use this tool to think about something. It will not obtain new information " +
|
|
67
|
+
"or change the database, but just append the thought to the log. " +
|
|
68
|
+
"Use it when complex reasoning or some cache memory is needed.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
thought: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "A thought to think about."
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ["thought"]
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Connection Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages connections to MCP (Model Context Protocol) tool servers.
|
|
5
|
+
* Supports both STDIO and SSE connection types.
|
|
6
|
+
*
|
|
7
|
+
* @module tool-runtime/mcp/connection
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
/**
|
|
12
|
+
* MCP Connection - manages a single MCP server connection
|
|
13
|
+
*/
|
|
14
|
+
export class MCPConnection {
|
|
15
|
+
id;
|
|
16
|
+
config;
|
|
17
|
+
process = null;
|
|
18
|
+
connected = false;
|
|
19
|
+
tools = [];
|
|
20
|
+
error;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.id = `mcp-${randomUUID().slice(0, 8)}`;
|
|
23
|
+
this.config = config;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Connect to the MCP server
|
|
27
|
+
*/
|
|
28
|
+
async connect() {
|
|
29
|
+
if (this.connected)
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
if (this.config.type === "stdio") {
|
|
33
|
+
await this.connectStdio();
|
|
34
|
+
}
|
|
35
|
+
else if (this.config.type === "sse") {
|
|
36
|
+
await this.connectSSE();
|
|
37
|
+
}
|
|
38
|
+
this.connected = true;
|
|
39
|
+
this.error = undefined;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Connect via STDIO
|
|
48
|
+
*/
|
|
49
|
+
async connectStdio() {
|
|
50
|
+
const config = this.config;
|
|
51
|
+
this.process = spawn(config.command, config.args ?? [], {
|
|
52
|
+
env: { ...process.env, ...config.env },
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
54
|
+
});
|
|
55
|
+
// Wait for connection to be established
|
|
56
|
+
await new Promise((resolve, reject) => {
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
reject(new Error("MCP connection timeout"));
|
|
59
|
+
}, 10000);
|
|
60
|
+
this.process?.stdout?.once("data", () => {
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
this.process?.on("error", (err) => {
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
reject(err);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Connect via SSE (placeholder - full implementation needs SSE client)
|
|
72
|
+
*/
|
|
73
|
+
async connectSSE() {
|
|
74
|
+
// SSE implementation would use EventSource or similar
|
|
75
|
+
// For now, throw not implemented
|
|
76
|
+
throw new Error("SSE MCP connections not yet implemented - use STDIO");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Disconnect from the MCP server
|
|
80
|
+
*/
|
|
81
|
+
disconnect() {
|
|
82
|
+
if (this.process) {
|
|
83
|
+
this.process.kill();
|
|
84
|
+
this.process = null;
|
|
85
|
+
}
|
|
86
|
+
this.connected = false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* List available tools from the server
|
|
90
|
+
*/
|
|
91
|
+
async listTools() {
|
|
92
|
+
if (!this.connected) {
|
|
93
|
+
throw new Error("Not connected to MCP server");
|
|
94
|
+
}
|
|
95
|
+
// In full implementation, this would send list_tools request
|
|
96
|
+
// For now, return cached tools
|
|
97
|
+
return this.tools;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Call a tool on the MCP server
|
|
101
|
+
*/
|
|
102
|
+
async callTool(name, args) {
|
|
103
|
+
if (!this.connected) {
|
|
104
|
+
throw new Error("Not connected to MCP server");
|
|
105
|
+
}
|
|
106
|
+
// In full implementation, this would:
|
|
107
|
+
// 1. Send JSON-RPC request to server
|
|
108
|
+
// 2. Wait for response
|
|
109
|
+
// 3. Parse and return result
|
|
110
|
+
// Placeholder response
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `MCP tool ${name} called with args: ${JSON.stringify(args)}`
|
|
115
|
+
}]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get current connection state
|
|
120
|
+
*/
|
|
121
|
+
getState() {
|
|
122
|
+
return {
|
|
123
|
+
id: this.id,
|
|
124
|
+
config: this.config,
|
|
125
|
+
connected: this.connected,
|
|
126
|
+
tools: this.tools,
|
|
127
|
+
error: this.error
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* MCP Connection Manager - manages multiple MCP connections
|
|
133
|
+
*/
|
|
134
|
+
export class MCPConnectionManager {
|
|
135
|
+
connections = new Map();
|
|
136
|
+
/**
|
|
137
|
+
* Create and connect to an MCP server
|
|
138
|
+
*/
|
|
139
|
+
async connect(config) {
|
|
140
|
+
const connection = new MCPConnection(config);
|
|
141
|
+
await connection.connect();
|
|
142
|
+
this.connections.set(connection.id, connection);
|
|
143
|
+
return connection;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Disconnect from an MCP server
|
|
147
|
+
*/
|
|
148
|
+
disconnect(connectionId) {
|
|
149
|
+
const connection = this.connections.get(connectionId);
|
|
150
|
+
if (connection) {
|
|
151
|
+
connection.disconnect();
|
|
152
|
+
this.connections.delete(connectionId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Disconnect all MCP servers
|
|
157
|
+
*/
|
|
158
|
+
disconnectAll() {
|
|
159
|
+
for (const connection of this.connections.values()) {
|
|
160
|
+
connection.disconnect();
|
|
161
|
+
}
|
|
162
|
+
this.connections.clear();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get a connection by ID
|
|
166
|
+
*/
|
|
167
|
+
get(connectionId) {
|
|
168
|
+
return this.connections.get(connectionId);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* List all connections
|
|
172
|
+
*/
|
|
173
|
+
list() {
|
|
174
|
+
return Array.from(this.connections.values()).map(c => c.getState());
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Singleton manager instance
|
|
178
|
+
export const mcpManager = new MCPConnectionManager();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) Module
|
|
3
|
+
*
|
|
4
|
+
* Enables ClawMongo to connect to external MCP tool servers.
|
|
5
|
+
* Provides full parity with OpenClaw's MCP tool integration.
|
|
6
|
+
*
|
|
7
|
+
* @module tool-runtime/mcp
|
|
8
|
+
*/
|
|
9
|
+
export * from "./types.js";
|
|
10
|
+
export { MCPConnection, MCPConnectionManager, mcpManager } from "./connection.js";
|
|
11
|
+
export { createMCPTool, createMCPToolExecutor, mcpToolToDefinition } from "./tool.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool - Wrapper for MCP server tools
|
|
3
|
+
*
|
|
4
|
+
* Creates executable tool instances from MCP server tool definitions.
|
|
5
|
+
* Ported from OpenClaw's mcp_tool.py.
|
|
6
|
+
*
|
|
7
|
+
* @module tool-runtime/mcp/tool
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Create an MCP tool from a server tool definition
|
|
11
|
+
*/
|
|
12
|
+
export function createMCPTool(definition, connection) {
|
|
13
|
+
return {
|
|
14
|
+
name: definition.name,
|
|
15
|
+
description: definition.description ?? `MCP tool: ${definition.name}`,
|
|
16
|
+
inputSchema: definition.inputSchema,
|
|
17
|
+
connection,
|
|
18
|
+
async execute(args) {
|
|
19
|
+
try {
|
|
20
|
+
const result = await connection.callTool(definition.name, args);
|
|
21
|
+
// Extract text content from result
|
|
22
|
+
if (result.content && result.content.length > 0) {
|
|
23
|
+
for (const item of result.content) {
|
|
24
|
+
if (item.type === "text" && item.text) {
|
|
25
|
+
return item.text;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return "No text content in MCP tool response";
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return `Error executing MCP tool ${definition.name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create an executor for an MCP tool
|
|
39
|
+
*/
|
|
40
|
+
export function createMCPToolExecutor(tool) {
|
|
41
|
+
return async (params, context) => {
|
|
42
|
+
const startTime = performance.now();
|
|
43
|
+
try {
|
|
44
|
+
const result = await tool.execute(params);
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
output: result,
|
|
48
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
49
|
+
sideEffects: [`mcp:${tool.name}:${context.toolCallId}`]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
output: null,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err),
|
|
57
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
58
|
+
sideEffects: []
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convert MCP tool to standard tool definition format
|
|
65
|
+
*/
|
|
66
|
+
export function mcpToolToDefinition(tool) {
|
|
67
|
+
return {
|
|
68
|
+
name: tool.name,
|
|
69
|
+
description: tool.description,
|
|
70
|
+
input_schema: tool.inputSchema
|
|
71
|
+
};
|
|
72
|
+
}
|