Skip to content

Commit f1e3b19

Browse files
homeofeclaude
andcommitted
feat: add openclaw_deploy tool for server-side deploy scripts (v1.1.2)
Closes #8 - New tool: openclaw_deploy { service, action: deploy|rollback|status } - Runs deploy-{service}.sh or rollback-{service}.sh on OpenClaw server via SSH - Status action tails ~/deploy/logs/{service}.log (last 30 lines) - New config: OPENCLAW_DEPLOY_SCRIPT_DIR (default: ~/deploy) - 2 unit tests added (53 total) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 970c462 commit f1e3b19

6 files changed

Lines changed: 102 additions & 1 deletion

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ OPENCLAW_GATEWAY_URL=http://localhost:18789
3333
# -------------------------
3434
# OPENCLAW_DEFAULT_AGENT=ops
3535

36+
# -------------------------
37+
# OpenClaw deploy scripts (optional)
38+
# Directory on the OpenClaw server containing deploy-{service}.sh scripts.
39+
# -------------------------
40+
# OPENCLAW_DEPLOY_SCRIPT_DIR=~/deploy
41+
3642
# -------------------------
3743
# Gemini CLI (optional)
3844
# Requires: npm install -g @google/gemini-cli && gemini auth login

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@elvatis_com/elvatis-mcp",
3-
"version": "1.1.1",
3+
"version": "1.1.2",
44
"description": "MCP server for OpenClaw — expose smart home, memory, cron, and more to Claude Desktop, Cursor, Windsurf, and any MCP client",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export interface Config {
4949
localLlmEndpoint?: string;
5050
/** Default model identifier for the local LLM (as shown in LM Studio / Ollama) */
5151
localLlmModel?: string;
52+
// --- OpenClaw deploy ---
53+
/** Directory of deploy scripts on the OpenClaw server (default: ~/deploy) */
54+
deployScriptDir?: string;
5255
// --- Remote Linux server (general SSH, independent of OpenClaw) ---
5356
/** Host for remote_shell, remote_docker, remote_service tools */
5457
remoteHost?: string;
@@ -90,6 +93,8 @@ export function loadConfig(): Config {
9093
// Local LLM (LM Studio default port: 1234, Ollama: 11434, llama.cpp: 8080)
9194
localLlmEndpoint: optional('LOCAL_LLM_ENDPOINT'),
9295
localLlmModel: optional('LOCAL_LLM_MODEL'),
96+
// OpenClaw deploy (optional)
97+
deployScriptDir: optional('OPENCLAW_DEPLOY_SCRIPT_DIR'),
9398
// Remote Linux server (optional, for remote_shell/docker/service tools)
9499
remoteHost: optional('REMOTE_HOST'),
95100
remotePort: parseInt(process.env['REMOTE_PORT'] ?? '22', 10),

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ import {
128128
remoteDockerSchema, handleRemoteDocker,
129129
} from './tools/remote-docker.js';
130130

131+
import {
132+
openclawDeploySchema, handleOpenclawDeploy,
133+
} from './tools/openclaw-deploy.js';
134+
131135
import {
132136
remoteServiceSchema, handleRemoteService,
133137
} from './tools/remote-service.js';
@@ -371,6 +375,12 @@ async function main() {
371375
async (args) => ({ content: [{ type: 'text', text: toText(await handleRemoteShell(args as any, config)) }] })
372376
);
373377

378+
registerTool(server, 'openclaw_deploy',
379+
'Trigger deploy or rollback scripts on the OpenClaw server, or check the last deploy log. Scripts must exist at OPENCLAW_DEPLOY_SCRIPT_DIR (default: ~/deploy).',
380+
openclawDeploySchema.shape,
381+
async (args) => ({ content: [{ type: 'text', text: toText(await handleOpenclawDeploy(args as any, config)) }] })
382+
);
383+
374384
registerTool(server, 'remote_docker',
375385
'Manage Docker containers on the remote Linux server via SSH. Actions: list, logs, start, stop, restart, stats, exec. No Docker API or open port needed.',
376386
remoteDockerSchema.shape,

src/tools/openclaw-deploy.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* openclaw_deploy: trigger deployment scripts on the OpenClaw server via SSH.
3+
*
4+
* Scripts live in OPENCLAW_DEPLOY_SCRIPT_DIR (default: ~/deploy) and follow
5+
* the naming convention: deploy-{service}.sh, rollback-{service}.sh
6+
* Status reads the last 30 lines of ~/deploy/logs/{service}.log if it exists.
7+
*/
8+
9+
import { z } from 'zod';
10+
import { Config } from '../config.js';
11+
import { sshExec, SshConfig } from '../ssh.js';
12+
13+
// --- Schema ---
14+
15+
export const openclawDeploySchema = z.object({
16+
service: z.string().describe('Service name to deploy, e.g. "api", "worker", "frontend"'),
17+
action: z.enum(['deploy', 'rollback', 'status'])
18+
.describe('deploy: run deploy script, rollback: run rollback script, status: show last deploy log'),
19+
});
20+
21+
// --- Handler ---
22+
23+
export async function handleOpenclawDeploy(
24+
args: z.infer<typeof openclawDeploySchema>,
25+
config: Config,
26+
): Promise<{ success: boolean; output?: string; error?: string; service: string; action: string }> {
27+
const cfg: SshConfig = {
28+
host: config.sshHost,
29+
port: config.sshPort,
30+
username: config.sshUser,
31+
keyPath: config.sshKeyPath,
32+
};
33+
34+
const scriptDir = config.deployScriptDir ?? '~/deploy';
35+
const { service, action } = args;
36+
37+
const cmds: Record<string, string> = {
38+
deploy: `bash ${scriptDir}/deploy-${service}.sh 2>&1`,
39+
rollback: `bash ${scriptDir}/rollback-${service}.sh 2>&1`,
40+
status: `if [ -f ${scriptDir}/logs/${service}.log ]; then tail -30 ${scriptDir}/logs/${service}.log; else echo "No log found at ${scriptDir}/logs/${service}.log"; fi`,
41+
};
42+
43+
try {
44+
const output = await sshExec(cfg, cmds[action]!, 120_000);
45+
return { success: true, output: output.trimEnd(), service, action };
46+
} catch (err) {
47+
return {
48+
success: false,
49+
error: err instanceof Error ? err.message : String(err),
50+
service,
51+
action,
52+
};
53+
}
54+
}

tests/unit.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { toRemoteSshCfg } from '../src/tools/remote-shell.js';
2020
import { handleRemoteDocker } from '../src/tools/remote-docker.js';
2121
import { handleRemoteService } from '../src/tools/remote-service.js';
22+
import { handleOpenclawDeploy } from '../src/tools/openclaw-deploy.js';
2223
import type { Config } from '../src/config.js';
2324

2425
// Minimal config stub for heuristic-only tests (no SSH/HTTP needed)
@@ -491,3 +492,28 @@ describe('remote_service', () => {
491492
assert.equal(result.action, 'stop');
492493
});
493494
});
495+
496+
// ============================================================================
497+
// openclaw_deploy — argument validation (no SSH needed)
498+
// ============================================================================
499+
500+
describe('openclaw_deploy', () => {
501+
it('reflects service and action in the response on SSH failure', async () => {
502+
const result = await handleOpenclawDeploy(
503+
{ service: 'api', action: 'status' },
504+
{ ...stubConfig, sshHost: '0.0.0.1' },
505+
);
506+
assert.equal(result.service, 'api');
507+
assert.equal(result.action, 'status');
508+
assert.equal(result.success, false);
509+
});
510+
511+
it('uses default deploy script dir when OPENCLAW_DEPLOY_SCRIPT_DIR is not set', async () => {
512+
const result = await handleOpenclawDeploy(
513+
{ service: 'worker', action: 'status' },
514+
{ ...stubConfig, sshHost: '0.0.0.1', deployScriptDir: undefined },
515+
);
516+
assert.equal(result.success, false);
517+
assert.equal(result.service, 'worker');
518+
});
519+
});

0 commit comments

Comments
 (0)