unix-disk-mcp 0.1.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.
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import * as readline from "readline";
6
+ import { getConfigPath, getConfigDir } from "../config/index.js";
7
+ const DEFAULT_PROTECTED_PATHS = [
8
+ "/System",
9
+ "/Library",
10
+ "~/.ssh",
11
+ "~/.gnupg",
12
+ ];
13
+ const MCP_CONFIGS = {
14
+ vscode: {
15
+ name: "VS Code",
16
+ path: process.platform === 'darwin'
17
+ ? join(homedir(), "Library", "Application Support", "Code", "User", "mcp.json")
18
+ : join(homedir(), ".config", "Code", "User", "mcp.json"),
19
+ },
20
+ claude: {
21
+ name: "Claude Desktop",
22
+ path: process.platform === 'darwin'
23
+ ? join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
24
+ : join(homedir(), ".config", "Claude", "claude_desktop_config.json"),
25
+ },
26
+ };
27
+ /**
28
+ * Create readline interface for user input
29
+ */
30
+ function createInterface() {
31
+ return readline.createInterface({
32
+ input: process.stdin,
33
+ output: process.stdout,
34
+ });
35
+ }
36
+ /**
37
+ * Ask a question and return the answer
38
+ */
39
+ function ask(rl, question) {
40
+ return new Promise((resolve) => {
41
+ rl.question(question, (answer) => {
42
+ resolve(answer);
43
+ });
44
+ });
45
+ }
46
+ /**
47
+ * Print welcome banner
48
+ */
49
+ function printBanner() {
50
+ console.log("╔════════════════════════════════════════════════════════════════╗");
51
+ console.log("║ Unix Disk MCP - Setup Wizard ║");
52
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
53
+ }
54
+ /**
55
+ * Configure protected paths
56
+ */
57
+ async function configureProtectedPaths(rl) {
58
+ console.log("\n📁 Protected Paths Configuration\n");
59
+ console.log("Protected paths cannot be deleted by AI. These are recursive (includes subdirectories).\n");
60
+ console.log("Default protected paths:");
61
+ DEFAULT_PROTECTED_PATHS.forEach((path, i) => {
62
+ console.log(` ${i + 1}. ${path}`);
63
+ });
64
+ const answer = await ask(rl, "\nUse default protected paths? [Y/n]: ");
65
+ if (answer.toLowerCase() === "n") {
66
+ console.log("\nEnter protected paths (one per line, empty line to finish):");
67
+ const paths = [];
68
+ while (true) {
69
+ const path = await ask(rl, "Path: ");
70
+ if (!path.trim())
71
+ break;
72
+ paths.push(path.trim());
73
+ }
74
+ return paths.length > 0 ? paths : DEFAULT_PROTECTED_PATHS;
75
+ }
76
+ return DEFAULT_PROTECTED_PATHS;
77
+ }
78
+ /**
79
+ * Select MCP client to configure
80
+ */
81
+ async function selectMCPClient(rl) {
82
+ console.log("\n🔧 MCP Client Configuration\n");
83
+ console.log("Which MCP client would you like to configure?\n");
84
+ console.log(" 1. VS Code (Roo Cline)");
85
+ console.log(" 2. Claude Desktop");
86
+ console.log(" 3. Both");
87
+ console.log(" 4. None (manual configuration)\n");
88
+ const answer = await ask(rl, "Select [1-4]: ");
89
+ switch (answer.trim()) {
90
+ case "1":
91
+ return "vscode";
92
+ case "2":
93
+ return "claude";
94
+ case "3":
95
+ return "both";
96
+ case "4":
97
+ default:
98
+ return "none";
99
+ }
100
+ }
101
+ /**
102
+ * Update MCP client config
103
+ */
104
+ function updateMCPConfig(client, configPath) {
105
+ const config = MCP_CONFIGS[client];
106
+ if (!existsSync(config.path)) {
107
+ console.log(`❌ ${config.name} config not found at: ${config.path}`);
108
+ return false;
109
+ }
110
+ try {
111
+ const raw = readFileSync(config.path, "utf-8");
112
+ const data = JSON.parse(raw);
113
+ if (client === "vscode") {
114
+ // VS Code MCP config format
115
+ if (!data.servers) {
116
+ data.servers = {};
117
+ }
118
+ data.servers["unix-disk-mcp"] = {
119
+ type: "stdio",
120
+ command: "unix-disk-mcp",
121
+ };
122
+ }
123
+ else {
124
+ // Claude Desktop config format
125
+ if (!data.mcpServers) {
126
+ data.mcpServers = {};
127
+ }
128
+ data.mcpServers["unix-disk-mcp"] = {
129
+ command: "unix-disk-mcp",
130
+ args: [],
131
+ };
132
+ }
133
+ writeFileSync(config.path, JSON.stringify(data, null, 2));
134
+ console.log(`✅ Updated ${config.name} config`);
135
+ return true;
136
+ }
137
+ catch (error) {
138
+ console.log(`❌ Failed to update ${config.name} config: ${error.message}`);
139
+ return false;
140
+ }
141
+ }
142
+ /**
143
+ * Save configuration
144
+ */
145
+ function saveConfig(config) {
146
+ const configPath = getConfigPath();
147
+ const configDir = getConfigDir();
148
+ // Ensure config directory exists
149
+ if (!existsSync(configDir)) {
150
+ mkdirSync(configDir, { recursive: true });
151
+ }
152
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
153
+ console.log(`\n✅ Configuration saved to: ${configPath}`);
154
+ }
155
+ /**
156
+ * Print manual configuration instructions
157
+ */
158
+ function printManualInstructions() {
159
+ console.log("\n╔════════════════════════════════════════════════════════════════╗");
160
+ console.log("║ Manual Configuration ║");
161
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
162
+ console.log("Add this to your MCP client configuration:\n");
163
+ console.log(JSON.stringify({
164
+ "unix-disk-mcp": {
165
+ command: "unix-disk-mcp",
166
+ args: [],
167
+ },
168
+ }, null, 2));
169
+ console.log("\n\nVS Code (Roo Cline):");
170
+ console.log(` ${MCP_CONFIGS.vscode.path}\n`);
171
+ console.log("Claude Desktop:");
172
+ console.log(` ${MCP_CONFIGS.claude.path}\n`);
173
+ }
174
+ /**
175
+ * Main setup function
176
+ */
177
+ export async function runSetup() {
178
+ printBanner();
179
+ const rl = createInterface();
180
+ try {
181
+ // Configure protected paths
182
+ const protectedPaths = await configureProtectedPaths(rl);
183
+ // Save config
184
+ const config = {
185
+ protected_paths: protectedPaths,
186
+ };
187
+ saveConfig(config);
188
+ // Select and configure MCP client
189
+ const client = await selectMCPClient(rl);
190
+ if (client === "none") {
191
+ printManualInstructions();
192
+ }
193
+ else {
194
+ if (client === "vscode" || client === "both") {
195
+ updateMCPConfig("vscode", getConfigPath());
196
+ }
197
+ if (client === "claude" || client === "both") {
198
+ updateMCPConfig("claude", getConfigPath());
199
+ }
200
+ console.log("\n✅ Setup complete! Restart your MCP client to use the server.\n");
201
+ }
202
+ }
203
+ finally {
204
+ rl.close();
205
+ }
206
+ }
@@ -0,0 +1,11 @@
1
+ export interface Config {
2
+ protected_paths: string[];
3
+ ignore_patterns: string[];
4
+ max_delete_size_gb: number;
5
+ dry_run: boolean;
6
+ }
7
+ export declare function expandPath(path: string): string;
8
+ export declare function loadConfig(): Config;
9
+ export declare function isProtectedPath(path: string, config: Config): boolean;
10
+ export declare function getConfigPath(): string;
11
+ export declare function getConfigDir(): string;
@@ -0,0 +1,59 @@
1
+ import { readFileSync, existsSync, mkdirSync, copyFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { resolve, join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ // Get __dirname equivalent in ES modules
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ // XDG Base Directory paths
9
+ const XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
10
+ const CONFIG_DIR = join(XDG_CONFIG_HOME, "unix-disk-mcp");
11
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
12
+ // Get the directory where the package is installed (for config.sample.json)
13
+ function getPackageRoot() {
14
+ // In development: src/config/index.ts -> project root is 2 levels up
15
+ // In production: dist/config/index.js -> project root is 2 levels up
16
+ return resolve(__dirname, "../..");
17
+ }
18
+ const SAMPLE_CONFIG_PATH = join(getPackageRoot(), "config.sample.json");
19
+ export function expandPath(path) {
20
+ if (path.startsWith("~")) {
21
+ return path.replace("~", homedir());
22
+ }
23
+ return path;
24
+ }
25
+ export function loadConfig() {
26
+ if (!existsSync(CONFIG_DIR)) {
27
+ mkdirSync(CONFIG_DIR, { recursive: true });
28
+ }
29
+ if (!existsSync(CONFIG_PATH)) {
30
+ // Copy sample config on first run
31
+ if (existsSync(SAMPLE_CONFIG_PATH)) {
32
+ copyFileSync(SAMPLE_CONFIG_PATH, CONFIG_PATH);
33
+ console.error(`Created config file: ${CONFIG_PATH}`);
34
+ console.error(`Please review and adjust settings, especially protected_paths.`);
35
+ }
36
+ else {
37
+ console.error(`Config file not found: ${CONFIG_PATH}`);
38
+ console.error(`Sample config not found: ${SAMPLE_CONFIG_PATH}`);
39
+ console.error(`Please create a config file manually or run: unix-disk-mcp setup`);
40
+ process.exit(1);
41
+ }
42
+ }
43
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
44
+ const config = JSON.parse(raw);
45
+ // Expand ~ in paths
46
+ config.protected_paths = config.protected_paths.map(expandPath);
47
+ return config;
48
+ }
49
+ export function isProtectedPath(path, config) {
50
+ const normalizedPath = expandPath(path);
51
+ return config.protected_paths.some((protected_path) => normalizedPath === protected_path ||
52
+ normalizedPath.startsWith(protected_path + "/"));
53
+ }
54
+ export function getConfigPath() {
55
+ return CONFIG_PATH;
56
+ }
57
+ export function getConfigDir() {
58
+ return CONFIG_DIR;
59
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { loadConfig } from "./config/index.js";
3
+ import { createServer } from "./server.js";
4
+ async function main() {
5
+ const config = loadConfig();
6
+ const server = createServer(config);
7
+ const transport = new StdioServerTransport();
8
+ await server.connect(transport);
9
+ }
10
+ main().catch((error) => {
11
+ console.error("Fatal error:", error);
12
+ process.exit(1);
13
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { Config } from "./config/index.js";
3
+ export declare function createServer(config: Config): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,15 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerExplorationTools } from "./tools/exploration.js";
3
+ import { registerDiscoveryTools } from "./tools/discovery.js";
4
+ import { registerStagingTools } from "./tools/staging.js";
5
+ export function createServer(config) {
6
+ const server = new McpServer({
7
+ name: "unix-disk-mcp",
8
+ version: "0.1.0",
9
+ });
10
+ // Register all tools
11
+ registerExplorationTools(server, config);
12
+ registerDiscoveryTools(server, config);
13
+ registerStagingTools(server, config);
14
+ return server;
15
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { Config } from "../config/index.js";
3
+ export declare function registerDiscoveryTools(server: McpServer, config: Config): void;
@@ -0,0 +1,267 @@
1
+ import { z } from "zod";
2
+ import { execSync } from "child_process";
3
+ import { existsSync, readdirSync } from "fs";
4
+ import { join } from "path";
5
+ export function registerDiscoveryTools(server, config) {
6
+ // list_applications
7
+ server.tool("list_applications", "List installed applications with size and last opened date", {}, async () => {
8
+ try {
9
+ if (process.platform !== 'darwin') {
10
+ return {
11
+ content: [
12
+ {
13
+ type: "text",
14
+ text: JSON.stringify({
15
+ success: false,
16
+ error: "list_applications is only supported on macOS",
17
+ code: "PLATFORM_NOT_SUPPORTED",
18
+ note: "Linux application discovery not yet implemented. Use find_large_items on /usr/share/applications or ~/.local/share/applications",
19
+ }),
20
+ },
21
+ ],
22
+ };
23
+ }
24
+ // macOS: Use Spotlight
25
+ const appDirs = ["/Applications", join(process.env.HOME || "", "Applications")];
26
+ const apps = [];
27
+ for (const dir of appDirs) {
28
+ if (!existsSync(dir))
29
+ continue;
30
+ const entries = readdirSync(dir).filter((name) => name.endsWith(".app"));
31
+ for (const name of entries) {
32
+ const appPath = join(dir, name);
33
+ try {
34
+ // Get size using du
35
+ const duOutput = execSync(`du -sk "${appPath}" 2>/dev/null`, {
36
+ encoding: "utf-8",
37
+ });
38
+ const size = parseInt(duOutput.split("\t")[0]) * 1024;
39
+ // Get last opened using mdls (Spotlight metadata)
40
+ let lastOpened = null;
41
+ try {
42
+ const mdlsOutput = execSync(`mdls -name kMDItemLastUsedDate -raw "${appPath}" 2>/dev/null`, { encoding: "utf-8" });
43
+ if (mdlsOutput && !mdlsOutput.includes("null")) {
44
+ lastOpened = new Date(mdlsOutput.trim()).toISOString();
45
+ }
46
+ }
47
+ catch {
48
+ // Spotlight metadata not available
49
+ }
50
+ apps.push({
51
+ name: name.replace(".app", ""),
52
+ path: appPath,
53
+ size,
54
+ last_opened: lastOpened,
55
+ });
56
+ }
57
+ catch {
58
+ apps.push({
59
+ name: name.replace(".app", ""),
60
+ path: appPath,
61
+ size: null,
62
+ last_opened: null,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ // Sort by size descending
68
+ apps.sort((a, b) => (b.size || 0) - (a.size || 0));
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: JSON.stringify({ success: true, data: apps }, null, 2),
74
+ },
75
+ ],
76
+ };
77
+ }
78
+ catch (error) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: JSON.stringify({
84
+ success: false,
85
+ error: error instanceof Error ? error.message : "Unknown error",
86
+ code: "LIST_APPLICATIONS_FAILED",
87
+ }),
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ });
93
+ // list_homebrew
94
+ server.tool("list_homebrew", "List Homebrew packages (formulas and casks)", {
95
+ include_casks: z
96
+ .boolean()
97
+ .optional()
98
+ .default(true)
99
+ .describe("Include casks in the list"),
100
+ }, async ({ include_casks }) => {
101
+ try {
102
+ // Check if Homebrew is installed
103
+ try {
104
+ execSync("which brew", { encoding: "utf-8" });
105
+ }
106
+ catch {
107
+ return {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: JSON.stringify({
112
+ success: false,
113
+ error: "Homebrew is not installed",
114
+ code: "HOMEBREW_NOT_FOUND",
115
+ }),
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ const packages = [];
121
+ // Get formulas
122
+ const formulaOutput = execSync("brew list --formula --versions 2>/dev/null", {
123
+ encoding: "utf-8",
124
+ });
125
+ for (const line of formulaOutput.trim().split("\n")) {
126
+ if (!line)
127
+ continue;
128
+ const parts = line.split(" ");
129
+ packages.push({
130
+ name: parts[0],
131
+ type: "formula",
132
+ version: parts.slice(1).join(" "),
133
+ });
134
+ }
135
+ // Get casks
136
+ if (include_casks) {
137
+ try {
138
+ const caskOutput = execSync("brew list --cask --versions 2>/dev/null", {
139
+ encoding: "utf-8",
140
+ });
141
+ for (const line of caskOutput.trim().split("\n")) {
142
+ if (!line)
143
+ continue;
144
+ const parts = line.split(" ");
145
+ packages.push({
146
+ name: parts[0],
147
+ type: "cask",
148
+ version: parts.slice(1).join(" "),
149
+ });
150
+ }
151
+ }
152
+ catch {
153
+ // No casks installed
154
+ }
155
+ }
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: JSON.stringify({ success: true, data: packages }, null, 2),
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ catch (error) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: JSON.stringify({
171
+ success: false,
172
+ error: error instanceof Error ? error.message : "Unknown error",
173
+ code: "LIST_HOMEBREW_FAILED",
174
+ }),
175
+ },
176
+ ],
177
+ };
178
+ }
179
+ });
180
+ // list_docker
181
+ server.tool("list_docker", "List Docker images, containers, and volumes", {
182
+ resource_type: z
183
+ .enum(["images", "containers", "volumes", "all"])
184
+ .optional()
185
+ .default("all")
186
+ .describe("Type of Docker resources to list"),
187
+ }, async ({ resource_type }) => {
188
+ try {
189
+ // Check if Docker is available
190
+ try {
191
+ execSync("docker info 2>/dev/null", { encoding: "utf-8" });
192
+ }
193
+ catch {
194
+ return {
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: JSON.stringify({
199
+ success: false,
200
+ error: "Docker is not running or not installed",
201
+ code: "DOCKER_NOT_AVAILABLE",
202
+ }),
203
+ },
204
+ ],
205
+ };
206
+ }
207
+ const result = {};
208
+ if (resource_type === "all" || resource_type === "images") {
209
+ const output = execSync('docker images --format "{{.ID}}|{{.Repository}}|{{.Tag}}|{{.Size}}|{{.CreatedAt}}"', { encoding: "utf-8" });
210
+ result.images = output
211
+ .trim()
212
+ .split("\n")
213
+ .filter((line) => line)
214
+ .map((line) => {
215
+ const [id, repository, tag, size, created] = line.split("|");
216
+ return { id, repository, tag, size, created };
217
+ });
218
+ }
219
+ if (resource_type === "all" || resource_type === "containers") {
220
+ const output = execSync('docker ps -a --format "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Size}}"', { encoding: "utf-8" });
221
+ result.containers = output
222
+ .trim()
223
+ .split("\n")
224
+ .filter((line) => line)
225
+ .map((line) => {
226
+ const [id, name, image, status, size] = line.split("|");
227
+ return { id, name, image, status, size };
228
+ });
229
+ }
230
+ if (resource_type === "all" || resource_type === "volumes") {
231
+ const output = execSync('docker volume ls --format "{{.Name}}|{{.Driver}}"', {
232
+ encoding: "utf-8",
233
+ });
234
+ result.volumes = output
235
+ .trim()
236
+ .split("\n")
237
+ .filter((line) => line)
238
+ .map((line) => {
239
+ const [name, driver] = line.split("|");
240
+ return { name, driver, size: null }; // Volume size requires inspection
241
+ });
242
+ }
243
+ return {
244
+ content: [
245
+ {
246
+ type: "text",
247
+ text: JSON.stringify({ success: true, data: result }, null, 2),
248
+ },
249
+ ],
250
+ };
251
+ }
252
+ catch (error) {
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: JSON.stringify({
258
+ success: false,
259
+ error: error instanceof Error ? error.message : "Unknown error",
260
+ code: "LIST_DOCKER_FAILED",
261
+ }),
262
+ },
263
+ ],
264
+ };
265
+ }
266
+ });
267
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { Config } from "../config/index.js";
3
+ export declare function registerExplorationTools(server: McpServer, config: Config): void;