diff --git a/packages/bash-mcp/src/index.ts b/packages/bash-mcp/src/index.ts index 028f925..1b318fa 100644 --- a/packages/bash-mcp/src/index.ts +++ b/packages/bash-mcp/src/index.ts @@ -20,16 +20,25 @@ function getSession(sessionId: string): Bash { return sessions.get(sessionId)!; } -const server = new McpServer({ - name: '@capsule-run/bash-mcp', - version: '0.1.3', -}); +const server = new McpServer( + { + name: '@capsule-run/bash-mcp', + version: '0.1.4', + }, + { + instructions: ` + You are operating inside a sandboxed Bash environment (WebAssembly). + To discover available commands, run: cat /workspace/manual.md + Sessions are isolated: each session_id has its own cwd, env vars, and filesystem state. + Use the same session_id across calls to maintain state within a workflow, or use an existing session_id to resume. + `, + }, +); server.registerTool( 'run', { - description: - 'Run a bash command inside the sandboxed Capsule environment. Returns stdout, stderr, exit code, filesystem diff, and current shell state (cwd + env).', + description: 'Execute a Bash command in the sandboxed environment', inputSchema: { command: z.string().describe('The bash command to execute.'), session_id: z @@ -68,7 +77,7 @@ server.registerTool( 'reset', { description: - 'Reset the sandboxed filesystem and shell state (cwd, env vars) for a session back to their initial values.', + "Reset a session's filesystem and shell state (cwd, env vars) to their initial values. Useful to start fresh without creating a new session.", inputSchema: { session_id: z .string() @@ -96,7 +105,7 @@ server.registerTool( server.registerTool( 'sessions', { - description: 'List all active shell sessions.', + description: 'List all active session IDs and their current state.', }, async () => { return { diff --git a/packages/bash/src/core/filesystem.ts b/packages/bash/src/core/filesystem.ts index 2cb7f7b..15b1302 100644 --- a/packages/bash/src/core/filesystem.ts +++ b/packages/bash/src/core/filesystem.ts @@ -1,9 +1,12 @@ +import { CommandManual } from '@capsule-run/bash-types'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const require = createRequire(import.meta.url); export class Filesystem { constructor(private readonly workspace: string) {} @@ -15,17 +18,40 @@ export class Filesystem { fs.mkdirSync(path.join(this.workspace, dir), { recursive: true }); } - const availableCommandsList = fs - .readdirSync(path.resolve(__dirname, '../commands')) - .join('\n') - .trim(); + let commandManuals = ''; + const commandsDir = path.resolve(__dirname, '../commands'); + + for (const command of fs.readdirSync(commandsDir)) { + const commandDir = path.join(commandsDir, command); + + if (!fs.statSync(commandDir).isDirectory()) { + continue; + } + + const commandPath = path.join(commandDir, `${command}.handler`); + + const mod = require(commandPath); + const man = mod.manual as CommandManual; + + commandManuals += `- + +Command: ${man.name} +Usage: ${man.usage} +Description: ${man.description}${ + man.options + ? `\nOptions:\n${Object.entries(man.options) + .map(([key, value]) => `${key} : ${value}`) + .join('\n')}` + : '' + }\n\n`; + } const files: Record = { 'etc/resolv.conf': 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n', 'etc/os-release': 'NAME="Capsule OS"\nVERSION="1.0"\nID=capsule\n', 'etc/passwd': 'root:x:0:0:root:/root:/bin/bash\n', 'proc/cpuinfo': 'processor\t: 0\nvendor_id\t: CapsuleVirtualCPU\n', - 'workspace/README.md': `You are operating inside a sandboxed bash. Here the list of available commands:\n${availableCommandsList}`, + 'workspace/manual.md': `# Capsule Bash Manual\n\n## Available commands:\n${commandManuals}`, }; for (const [relativePath, content] of Object.entries(files)) {