@shellexec/mcp 1.0.0
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/README.md +91 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/package.json +22 -0
- package/src/index.ts +237 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ShellExec MCP Server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for [ShellExec](https://app.shellexec.sh). Lets AI assistants like Claude, Cursor, and Windsurf manage your servers through ShellExec.
|
|
4
|
+
|
|
5
|
+
## Available Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `list_agents` | List all agents with status |
|
|
10
|
+
| `get_agent` | Get details for a specific agent |
|
|
11
|
+
| `execute_command` | Run a command on one agent |
|
|
12
|
+
| `execute_on_all` | Run a command on all online agents |
|
|
13
|
+
| `get_allowlist` | View allowed commands |
|
|
14
|
+
| `get_command_history` | View recent command history |
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
### Claude Desktop / Claude Code
|
|
19
|
+
|
|
20
|
+
Add to your Claude config (`~/.claude.json` or Claude Desktop settings):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"shellexec": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "shellexec-mcp", "--api-key", "sk-your-api-key"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Cursor
|
|
34
|
+
|
|
35
|
+
Add to `.cursor/mcp.json` in your project:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"shellexec": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "shellexec-mcp", "--api-key", "sk-your-api-key"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Environment variables
|
|
49
|
+
|
|
50
|
+
Alternatively, use environment variables:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"shellexec": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "shellexec-mcp"],
|
|
58
|
+
"env": {
|
|
59
|
+
"SHELLEXEC_API_KEY": "sk-your-api-key",
|
|
60
|
+
"SHELLEXEC_API_URL": "https://app.shellexec.sh"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage Examples
|
|
68
|
+
|
|
69
|
+
Once configured, you can ask your AI assistant:
|
|
70
|
+
|
|
71
|
+
- *"List my servers"*
|
|
72
|
+
- *"Check the uptime on all agents"*
|
|
73
|
+
- *"Run df -h on web-1"*
|
|
74
|
+
- *"What commands have been run today?"*
|
|
75
|
+
- *"Show me the disk usage across all servers"*
|
|
76
|
+
- *"What commands are in the allowlist?"*
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
| Option | Env Var | CLI Flag | Default |
|
|
81
|
+
|--------|---------|----------|---------|
|
|
82
|
+
| API Key | `SHELLEXEC_API_KEY` | `--api-key` | Required |
|
|
83
|
+
| API URL | `SHELLEXEC_API_URL` | `--api-url` | `https://app.shellexec.sh` |
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm install
|
|
89
|
+
npm run build
|
|
90
|
+
SHELLEXEC_API_KEY=sk-... node dist/index.js
|
|
91
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Configuration
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const API_KEY = process.env.SHELLEXEC_API_KEY || process.argv.find((_, i, a) => a[i - 1] === "--api-key") || "";
|
|
11
|
+
const API_BASE = process.env.SHELLEXEC_API_URL || process.argv.find((_, i, a) => a[i - 1] === "--api-url") || "https://app.shellexec.sh";
|
|
12
|
+
if (!API_KEY) {
|
|
13
|
+
console.error("Error: SHELLEXEC_API_KEY environment variable or --api-key argument required");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// API client
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
async function api(path, options = {}) {
|
|
20
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
21
|
+
...options,
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"Authorization": `Bearer ${API_KEY}`,
|
|
25
|
+
...(options.headers || {}),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const text = await res.text().catch(() => res.statusText);
|
|
30
|
+
throw new Error(`ShellExec API error ${res.status}: ${text}`);
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// MCP Server
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
const server = new mcp_js_1.McpServer({
|
|
38
|
+
name: "shellexec",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
});
|
|
41
|
+
// --- list_agents ---
|
|
42
|
+
server.tool("list_agents", "List all ShellExec agents with their hostname, UUID, online status, and last seen time", {}, async () => {
|
|
43
|
+
const data = await api("/api/agents");
|
|
44
|
+
if (data.agents.length === 0) {
|
|
45
|
+
return { content: [{ type: "text", text: "No agents found." }] };
|
|
46
|
+
}
|
|
47
|
+
const lines = data.agents.map(a => `${a.online ? "●" : "○"} ${a.hostname || "(no hostname)"} — ${a.uuid} — ${a.online ? "online" : "offline"} — last seen: ${a.last_seen || "never"}`);
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: `${data.agents.length} agent(s):\n\n${lines.join("\n")}` }],
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
// --- execute_command ---
|
|
53
|
+
server.tool("execute_command", "Execute a shell command on a specific agent. The command must be in the allowlist. Returns output, exit code, and whether sudo was used.", {
|
|
54
|
+
agent_uuid: zod_1.z.string().describe("UUID of the agent to execute the command on"),
|
|
55
|
+
command: zod_1.z.string().describe("The shell command to execute (must be in the allowlist)"),
|
|
56
|
+
}, async ({ agent_uuid, command }) => {
|
|
57
|
+
const data = await api(`/api/agents/${agent_uuid}/exec`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
body: JSON.stringify({ command }),
|
|
60
|
+
});
|
|
61
|
+
let text = `Exit code: ${data.exit_code}`;
|
|
62
|
+
if (data.used_sudo)
|
|
63
|
+
text += " (ran with sudo)";
|
|
64
|
+
text += "\n";
|
|
65
|
+
if (data.output)
|
|
66
|
+
text += `\nOutput:\n${data.output}`;
|
|
67
|
+
if (data.error && data.error !== "command not allowed")
|
|
68
|
+
text += `\nError:\n${data.error}`;
|
|
69
|
+
if (data.error === "command not allowed")
|
|
70
|
+
text += "\nCommand not in allowlist — add it via the ShellExec dashboard first.";
|
|
71
|
+
if (data.sudo_hint)
|
|
72
|
+
text += `\n\nSudo permissions:\n${data.sudo_hint}`;
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: text.trim() }],
|
|
75
|
+
isError: data.exit_code !== 0,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
// --- execute_on_all ---
|
|
79
|
+
server.tool("execute_on_all", "Execute a shell command on ALL online agents simultaneously. Returns results from each agent.", {
|
|
80
|
+
command: zod_1.z.string().describe("The shell command to execute on all online agents"),
|
|
81
|
+
}, async ({ command }) => {
|
|
82
|
+
const data = await api("/api/agents/exec-all", {
|
|
83
|
+
method: "POST",
|
|
84
|
+
body: JSON.stringify({ command }),
|
|
85
|
+
});
|
|
86
|
+
if (data.results.length === 0) {
|
|
87
|
+
return { content: [{ type: "text", text: "No online agents found." }] };
|
|
88
|
+
}
|
|
89
|
+
const sections = data.results.map(r => {
|
|
90
|
+
let section = `--- ${r.hostname || r.agent_uuid} (exit ${r.exit_code}${r.used_sudo ? ", sudo" : ""}, ${r.duration_ms}ms) ---`;
|
|
91
|
+
if (r.output)
|
|
92
|
+
section += `\n${r.output.trimEnd()}`;
|
|
93
|
+
if (r.error && r.error !== "command not allowed")
|
|
94
|
+
section += `\nError: ${r.error}`;
|
|
95
|
+
if (r.error === "command not allowed")
|
|
96
|
+
section += `\nDENIED — command not in allowlist`;
|
|
97
|
+
return section;
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: sections.join("\n\n") }],
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
// --- get_agent ---
|
|
104
|
+
server.tool("get_agent", "Get detailed information about a specific agent by UUID", {
|
|
105
|
+
agent_uuid: zod_1.z.string().describe("UUID of the agent"),
|
|
106
|
+
}, async ({ agent_uuid }) => {
|
|
107
|
+
const data = await api(`/api/agents/${agent_uuid}`);
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `Hostname: ${data.hostname}\nUUID: ${data.uuid}\nStatus: ${data.online ? "Online" : "Offline"}\nLast seen: ${data.last_seen || "never"}`,
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
// --- get_allowlist ---
|
|
116
|
+
server.tool("get_allowlist", "Get the list of commands that agents are allowed to execute. Supports glob wildcards.", {}, async () => {
|
|
117
|
+
const data = await api("/api/allowlist");
|
|
118
|
+
if (data.commands.length === 0) {
|
|
119
|
+
return { content: [{ type: "text", text: "Allowlist is empty. No commands can be executed. Add commands via the ShellExec dashboard." }] };
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: `Allowed commands (${data.commands.length}):\n\n${data.commands.map(c => ` ${c}`).join("\n")}` }],
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
// --- get_command_history ---
|
|
126
|
+
server.tool("get_command_history", "View recent command execution history with output, exit codes, and timing", {
|
|
127
|
+
limit: zod_1.z.number().optional().default(10).describe("Number of recent commands to return (default 10, max 100)"),
|
|
128
|
+
agent_uuid: zod_1.z.string().optional().describe("Filter by agent UUID (optional)"),
|
|
129
|
+
search: zod_1.z.string().optional().describe("Search commands by keyword (optional)"),
|
|
130
|
+
}, async ({ limit, agent_uuid, search }) => {
|
|
131
|
+
const params = new URLSearchParams();
|
|
132
|
+
params.set("limit", String(Math.min(limit || 10, 100)));
|
|
133
|
+
if (agent_uuid)
|
|
134
|
+
params.set("agent_uuid", agent_uuid);
|
|
135
|
+
if (search)
|
|
136
|
+
params.set("search", search);
|
|
137
|
+
const data = await api(`/api/commands?${params.toString()}`);
|
|
138
|
+
if (data.commands.length === 0) {
|
|
139
|
+
return { content: [{ type: "text", text: "No commands found." }] };
|
|
140
|
+
}
|
|
141
|
+
const lines = data.commands.map(c => {
|
|
142
|
+
const ts = new Date(c.executed_at).toLocaleString();
|
|
143
|
+
const host = c.agent_hostname || c.agent_uuid.substring(0, 8);
|
|
144
|
+
const sudo = c.used_sudo ? " [sudo]" : "";
|
|
145
|
+
const denied = c.exit_code === -1 && c.error === "command not allowed" ? " [DENIED]" : "";
|
|
146
|
+
let line = `[${ts}] ${host} $ ${c.command} → exit ${c.exit_code}${sudo}${denied} (${c.duration_ms}ms)`;
|
|
147
|
+
if (c.output)
|
|
148
|
+
line += `\n ${c.output.trimEnd().split("\n").join("\n ")}`;
|
|
149
|
+
return line;
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: `${data.total} total commands. Showing ${data.commands.length}:\n\n${lines.join("\n\n")}` }],
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Start
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
async function main() {
|
|
159
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
160
|
+
await server.connect(transport);
|
|
161
|
+
}
|
|
162
|
+
main().catch((err) => {
|
|
163
|
+
console.error("Fatal error:", err);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shellexec/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for ShellExec — remote command execution via AI assistants",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shellexec-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["mcp", "shellexec", "remote-execution", "claude", "ai-tools"],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.5.0",
|
|
20
|
+
"@types/node": "^22.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Configuration
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const API_KEY = process.env.SHELLEXEC_API_KEY || process.argv.find((_, i, a) => a[i - 1] === "--api-key") || "";
|
|
12
|
+
const API_BASE = process.env.SHELLEXEC_API_URL || process.argv.find((_, i, a) => a[i - 1] === "--api-url") || "https://app.shellexec.sh";
|
|
13
|
+
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
console.error("Error: SHELLEXEC_API_KEY environment variable or --api-key argument required");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// API client
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
async function api<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
24
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
25
|
+
...options,
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"Authorization": `Bearer ${API_KEY}`,
|
|
29
|
+
...(options.headers as Record<string, string> || {}),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const text = await res.text().catch(() => res.statusText);
|
|
35
|
+
throw new Error(`ShellExec API error ${res.status}: ${text}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return res.json() as Promise<T>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// MCP Server
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const server = new McpServer({
|
|
46
|
+
name: "shellexec",
|
|
47
|
+
version: "1.0.0",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// --- list_agents ---
|
|
51
|
+
server.tool(
|
|
52
|
+
"list_agents",
|
|
53
|
+
"List all ShellExec agents with their hostname, UUID, online status, and last seen time",
|
|
54
|
+
{},
|
|
55
|
+
async () => {
|
|
56
|
+
const data = await api<{ agents: Array<{ uuid: string; hostname: string; online: boolean; last_seen: string }> }>("/api/agents");
|
|
57
|
+
|
|
58
|
+
if (data.agents.length === 0) {
|
|
59
|
+
return { content: [{ type: "text", text: "No agents found." }] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lines = data.agents.map(a =>
|
|
63
|
+
`${a.online ? "●" : "○"} ${a.hostname || "(no hostname)"} — ${a.uuid} — ${a.online ? "online" : "offline"} — last seen: ${a.last_seen || "never"}`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: `${data.agents.length} agent(s):\n\n${lines.join("\n")}` }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// --- execute_command ---
|
|
73
|
+
server.tool(
|
|
74
|
+
"execute_command",
|
|
75
|
+
"Execute a shell command on a specific agent. The command must be in the allowlist. Returns output, exit code, and whether sudo was used.",
|
|
76
|
+
{
|
|
77
|
+
agent_uuid: z.string().describe("UUID of the agent to execute the command on"),
|
|
78
|
+
command: z.string().describe("The shell command to execute (must be in the allowlist)"),
|
|
79
|
+
},
|
|
80
|
+
async ({ agent_uuid, command }) => {
|
|
81
|
+
const data = await api<{
|
|
82
|
+
output: string; error: string; exit_code: number;
|
|
83
|
+
used_sudo?: boolean; sudo_hint?: string;
|
|
84
|
+
}>(`/api/agents/${agent_uuid}/exec`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: JSON.stringify({ command }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let text = `Exit code: ${data.exit_code}`;
|
|
90
|
+
if (data.used_sudo) text += " (ran with sudo)";
|
|
91
|
+
text += "\n";
|
|
92
|
+
if (data.output) text += `\nOutput:\n${data.output}`;
|
|
93
|
+
if (data.error && data.error !== "command not allowed") text += `\nError:\n${data.error}`;
|
|
94
|
+
if (data.error === "command not allowed") text += "\nCommand not in allowlist — add it via the ShellExec dashboard first.";
|
|
95
|
+
if (data.sudo_hint) text += `\n\nSudo permissions:\n${data.sudo_hint}`;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: text.trim() }],
|
|
99
|
+
isError: data.exit_code !== 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// --- execute_on_all ---
|
|
105
|
+
server.tool(
|
|
106
|
+
"execute_on_all",
|
|
107
|
+
"Execute a shell command on ALL online agents simultaneously. Returns results from each agent.",
|
|
108
|
+
{
|
|
109
|
+
command: z.string().describe("The shell command to execute on all online agents"),
|
|
110
|
+
},
|
|
111
|
+
async ({ command }) => {
|
|
112
|
+
const data = await api<{
|
|
113
|
+
results: Array<{
|
|
114
|
+
agent_uuid: string; hostname: string;
|
|
115
|
+
output: string; error: string; exit_code: number;
|
|
116
|
+
used_sudo?: boolean; duration_ms: number;
|
|
117
|
+
}>;
|
|
118
|
+
}>("/api/agents/exec-all", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: JSON.stringify({ command }),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (data.results.length === 0) {
|
|
124
|
+
return { content: [{ type: "text", text: "No online agents found." }] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sections = data.results.map(r => {
|
|
128
|
+
let section = `--- ${r.hostname || r.agent_uuid} (exit ${r.exit_code}${r.used_sudo ? ", sudo" : ""}, ${r.duration_ms}ms) ---`;
|
|
129
|
+
if (r.output) section += `\n${r.output.trimEnd()}`;
|
|
130
|
+
if (r.error && r.error !== "command not allowed") section += `\nError: ${r.error}`;
|
|
131
|
+
if (r.error === "command not allowed") section += `\nDENIED — command not in allowlist`;
|
|
132
|
+
return section;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: sections.join("\n\n") }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// --- get_agent ---
|
|
142
|
+
server.tool(
|
|
143
|
+
"get_agent",
|
|
144
|
+
"Get detailed information about a specific agent by UUID",
|
|
145
|
+
{
|
|
146
|
+
agent_uuid: z.string().describe("UUID of the agent"),
|
|
147
|
+
},
|
|
148
|
+
async ({ agent_uuid }) => {
|
|
149
|
+
const data = await api<{
|
|
150
|
+
uuid: string; hostname: string; online: boolean; last_seen: string;
|
|
151
|
+
}>(`/api/agents/${agent_uuid}`);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `Hostname: ${data.hostname}\nUUID: ${data.uuid}\nStatus: ${data.online ? "Online" : "Offline"}\nLast seen: ${data.last_seen || "never"}`,
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// --- get_allowlist ---
|
|
163
|
+
server.tool(
|
|
164
|
+
"get_allowlist",
|
|
165
|
+
"Get the list of commands that agents are allowed to execute. Supports glob wildcards.",
|
|
166
|
+
{},
|
|
167
|
+
async () => {
|
|
168
|
+
const data = await api<{ commands: string[] }>("/api/allowlist");
|
|
169
|
+
|
|
170
|
+
if (data.commands.length === 0) {
|
|
171
|
+
return { content: [{ type: "text", text: "Allowlist is empty. No commands can be executed. Add commands via the ShellExec dashboard." }] };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: `Allowed commands (${data.commands.length}):\n\n${data.commands.map(c => ` ${c}`).join("\n")}` }],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// --- get_command_history ---
|
|
181
|
+
server.tool(
|
|
182
|
+
"get_command_history",
|
|
183
|
+
"View recent command execution history with output, exit codes, and timing",
|
|
184
|
+
{
|
|
185
|
+
limit: z.number().optional().default(10).describe("Number of recent commands to return (default 10, max 100)"),
|
|
186
|
+
agent_uuid: z.string().optional().describe("Filter by agent UUID (optional)"),
|
|
187
|
+
search: z.string().optional().describe("Search commands by keyword (optional)"),
|
|
188
|
+
},
|
|
189
|
+
async ({ limit, agent_uuid, search }) => {
|
|
190
|
+
const params = new URLSearchParams();
|
|
191
|
+
params.set("limit", String(Math.min(limit || 10, 100)));
|
|
192
|
+
if (agent_uuid) params.set("agent_uuid", agent_uuid);
|
|
193
|
+
if (search) params.set("search", search);
|
|
194
|
+
|
|
195
|
+
const data = await api<{
|
|
196
|
+
commands: Array<{
|
|
197
|
+
id: number; agent_uuid: string; agent_hostname?: string;
|
|
198
|
+
command: string; output: string; error: string;
|
|
199
|
+
exit_code: number; used_sudo: boolean;
|
|
200
|
+
duration_ms: number; executed_at: string;
|
|
201
|
+
}>;
|
|
202
|
+
total: number;
|
|
203
|
+
}>(`/api/commands?${params.toString()}`);
|
|
204
|
+
|
|
205
|
+
if (data.commands.length === 0) {
|
|
206
|
+
return { content: [{ type: "text", text: "No commands found." }] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const lines = data.commands.map(c => {
|
|
210
|
+
const ts = new Date(c.executed_at).toLocaleString();
|
|
211
|
+
const host = c.agent_hostname || c.agent_uuid.substring(0, 8);
|
|
212
|
+
const sudo = c.used_sudo ? " [sudo]" : "";
|
|
213
|
+
const denied = c.exit_code === -1 && c.error === "command not allowed" ? " [DENIED]" : "";
|
|
214
|
+
let line = `[${ts}] ${host} $ ${c.command} → exit ${c.exit_code}${sudo}${denied} (${c.duration_ms}ms)`;
|
|
215
|
+
if (c.output) line += `\n ${c.output.trimEnd().split("\n").join("\n ")}`;
|
|
216
|
+
return line;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `${data.total} total commands. Showing ${data.commands.length}:\n\n${lines.join("\n\n")}` }],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Start
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
async function main() {
|
|
230
|
+
const transport = new StdioServerTransport();
|
|
231
|
+
await server.connect(transport);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
main().catch((err) => {
|
|
235
|
+
console.error("Fatal error:", err);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|