Skip to content
archived Visibility internal Owner erik@uvilo.com Approver _ Created 2026-04-25 Updated 2026-04-26

Bash Refactor Research


1. TypeScript Language & Tooling Standard for Uvilo MCP Servers

Related: R4 (Rewrite in TypeScript), R8 (Create Write_Typescript skill)

1.1 Finding

The Uvilo OS repo currently runs TypeScript only for the Astro site generator (generate_view_pages.ts via npx tsx). All MCP servers are Python. Establishing a TypeScript standard for MCP tools requires deciding: runtime approach (tsx vs compiled), module system (ESM vs CJS), bundling strategy, and project structure.

Runtime approaches evaluated:

ApproachCold startProduction readyDependenciesNotes
npx tsx (esbuild transpile on-the-fly)~100mstsx (dev)Already used in repo. No build step.
node --import tsx~100mstsxSame as above, more explicit.
esbuild bundle → node dist/index.js~10msesbuild (dev)Fastest runtime. Single file output. No tsx at runtime.
tsc compile → node dist/index.js~10mstypescript (dev)Slowest build. Type-checks at compile time.
Node.js native type-stripping (v22.6+)~50ms⚠️ experimentalNoneNo type checking. Too new for debian-slim Railway image.

Module system: ESM is the modern standard and required by @modelcontextprotocol/sdk. CJS is not supported.

Bundling: esbuild can produce a single-file bundle with all dependencies inlined. This eliminates node_modules at runtime, reduces cold start, and simplifies deployment (copy one file). The MCP SDK and zod are tree-shakeable and bundle cleanly.

Prerequisite impact: The LibreChat environment has migrated to debian-slim (not Alpine), and there is no distinction between dev and production environments — it is a living project used as it is being developed. This means there is only one runtime mode to consider, not separate dev/prod configurations.

1.2 Options

Option A: esbuild bundle — single mode

  • Run: esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js → execute with node dist/index.js
  • Build once after changes, run the bundle directly
  • Pros: Fastest runtime (~10ms cold start); single-file deploy; no tsx at runtime; deterministic output
  • Cons: Requires a build step after each source change; esbuild may need --external for native modules (not relevant here — no native deps)

Option B: tsx — single mode

  • Run: npx tsx src/index.ts
  • Pros: No build step — edit and run; simplest workflow
  • Cons: ~90ms overhead per cold start; requires tsx in the image

Option C: tsc compile — single mode

  • Run: tsc && node dist/index.js
  • Pros: Full type checking at compile time; no esbuild dependency
  • Cons: Slowest build (~2s+ for small projects); multiple output files; doesn’t bundle

1.3 Recommendation

Option A: esbuild bundle — single mode.

Since there is no dev/production distinction, a single runtime approach is needed. esbuild produces a single-file bundle that starts in ~10ms — 10x faster than tsx. Erik stated preference for speed: “not afraid to transpile/package code to make running it as fast as possible.” The MCP SDK and zod bundle cleanly with esbuild (no native modules).

The workflow is: edit source → run esbuild → restart the MCP server. Since the environment is a living project, this is the same workflow for all changes. The build step is fast (~50ms for a small project) and can be automated via a simple script.

This becomes the standard captured in the Write_Typescript skill.

1.4 Decision

Option A. Decided by: erik@uvilo.com — 2026-04-26


2. MCP TypeScript SDK — Architecture & API

Related: R4 (Rewrite in TypeScript), R5 (New tool name: forge-bash)

2.1 Finding

The MCP TypeScript SDK (@modelcontextprotocol/sdk) provides two APIs:

  1. High-level McpServer — declarative tool registration via server.registerTool() with Zod schemas. Handles protocol negotiation, capability advertisement, and message routing automatically.

  2. Low-level Server — manual request handler registration. More control, more boilerplate.

The current Python server uses FastMCP (high-level). The TypeScript McpServer is the direct equivalent.

Key SDK patterns for forge-bash:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod/v4';

const server = new McpServer({ name: 'forge-bash', version: '1.0.0' });

server.registerTool(
  'run',
  {
    description: 'Execute a shell command...',
    inputSchema: z.object({
      command: z.string().describe('Full command string, including pipes and chain operators.'),
    }),
  },
  async ({ command }) => {
    // execute command, return output
    return { content: [{ type: 'text', text: output }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Subprocess execution in Node.js: child_process.execFile is the right choice — it spawns a shell directly with the command, captures stdout/stderr as buffers, and returns exit code. Unlike spawn, it buffers output automatically (matching the Python subprocess.run with capture_output=True).

import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);
const { stdout, stderr } = await execFileAsync(shellBin, ['-c', command], {
  cwd: '/workspace',
  timeout: 300_000,
  maxBuffer: 50 * 1024 * 1024, // 50MB — handles large output before Layer 2 truncation
  encoding: 'buffer', // returns Buffer for binary detection
});

Railway env var injection: The Python servers read /proc/1/environ to inject Railway env vars. In Node.js, this is equally straightforward:

import { readFileSync } from 'node:fs';
try {
  const env = readFileSync('/proc/1/environ', 'utf8');
  for (const entry of env.split('\0')) {
    const eq = entry.indexOf('=');
    if (eq > 0 && !(entry.slice(0, eq) in process.env)) {
      process.env[entry.slice(0, eq)] = entry.slice(eq + 1);
    }
  }
} catch { /* not on Railway */ }

2.2 Options

Option A: McpServer (high-level) + promisified execFile

  • Pros: Clean API, Zod validation built-in, matches FastMCP pattern, minimal boilerplate
  • Cons: Less control over protocol details (not needed for forge-bash)

Option B: Low-level Server + child_process.spawn with manual buffering

  • Pros: Fine-grained control over streams, could handle very large outputs incrementally
  • Cons: Significantly more code; forge-bash buffers everything anyway (Layer 2 needs full output)

2.3 Recommendation

Option A. McpServer + promisified execFile. forge-bash needs full output for Layer 2 processing (binary detection, overflow truncation, stderr attachment) — streaming provides no benefit. The high-level API matches the Python FastMCP pattern closely, making the migration straightforward.

2.4 Decision

Option A. Decided by: erik@uvilo.com — 2026-04-26


3. Audit Log Design

Related: R2 (Non-blocking audit log)

3.1 Finding

The audit log must record every executed command without blocking or delaying execution. Key design decisions: format, storage location, rotation strategy.

Format: TSV (tab-separated values). Each field is separated by a tab; command strings containing tabs or newlines are escaped (\t\\t, \n\\n). TSV is both machine-parseable and cleanly human-readable — no jq needed, a simple cat or column -t suffices.

Storage location: /tmp/forge-bash/audit-YYYY-MM-DD.tsv — one file per calendar day (UTC). The date-based filename provides automatic rotation without any logic beyond opening the day’s file.

Rotation rationale: Containers may be ephemeral, but they can run for months without restarting. A single append-only file would grow unbounded in that scenario. One file per day keeps each file small and scannable, and old files can be pruned by date if needed.

Entry schema (one line per command):

ts\tcommand\texitCode\tdurationMs

Columns:

  1. ts — ISO 8601 UTC timestamp (e.g., 2026-04-26T14:32:01.234Z)
  2. command — Full command string (tabs/newlines escaped)
  3. exitCode — Process exit code (integer)
  4. durationMs — Execution time in milliseconds (integer)

3.2 Options

Option A: TSV at /tmp/forge-bash/audit-YYYY-MM-DD.tsv, daily rotation

  • Pros: Human-readable; date-based filenames provide automatic rotation; no jq dependency; works well for long-running containers
  • Cons: Tab/newline escaping adds minor complexity

Option B: JSONL at /tmp/forge-bash/audit.jsonl, no rotation

  • Pros: Standard format; queryable with jq; simple
  • Cons: Grows unbounded in long-running containers; less human-readable

3.3 Recommendation

Option A. TSV with daily rotation. Containers can run for months — daily files keep each log scannable and naturally bounded. TSV is directly readable without tooling.

3.4 Decision

Option A. Decided by: erik@uvilo.com — 2026-04-26


4. rm Blocking Strategy

Related: R3 (Always block rm to enforce uvilo-trash)

4.1 Finding

The requirement is simple: unconditionally block rm commands and direct the agent to use uvilo-trash instead. This replaces the entire permission system with a single, targeted guard.

Detection approach: Before executing a command, check if any pipeline segment starts with rm (after stripping env-var assignments). This is the same tokenization approach used by the current permission system — just with a single hardcoded blocklist entry instead of an allowlist.

Edge cases:

  • rm -rf / → blocked ✅
  • rm file.txt → blocked ✅
  • /bin/rm file.txt → should also be blocked (path-qualified rm)
  • echo rm → NOT blocked (rm is an argument, not the command) ✅
  • alias del=rm && del file → NOT blocked (aliasing is undetectable — acceptable gap)
  • find . -delete → NOT blocked (not rm — acceptable; -delete is not rm semantics)

4.2 Options

Option A: Simple token-prefix check — block if first token is rm or ends with /rm

  • Pros: Simple, covers the common cases, negligible overhead
  • Cons: Doesn’t catch aliases or find -delete (acceptable per threat model — environment is trusted)

Option B: Shell AST parsing for comprehensive detection

  • Pros: Catches aliases, functions, and indirect rm invocations
  • Cons: Massive overengineering; requires a shell parser dependency; defeats the “simple targeted guard” intent

4.3 Recommendation

Option A. Simple token-prefix check. The environment is trusted (FORGE.md: “Treat environment as trusted unless an action is explicitly destructive or irreversible”). The rm block is a targeted guard against the most common destructive pattern, not a security boundary. Aliases and find -delete are acceptable gaps.

4.4 Decision

Option A. Decided by: erik@uvilo.com — 2026-04-26


5. Project Structure & File Layout

Related: R4, R5, R8

5.1 Finding

forge-bash needs a home in the repo. The current Python MCP servers live in Forge/Configs-debian/mcp-servers/ — the canonical Configs folder for the debian-slim build. TypeScript MCP servers need a layout with package.json, tsconfig.json, source files, and a build output.

Considerations:

  • Forge/Configs-debian/ is the canonical config folder for the debian-slim build. It contains all custom configuration of the LibreChat environment, including MCP servers.
  • The Python servers are single-file scripts executed directly. TypeScript projects have more structure (src/, dist/, package.json, tsconfig.json).
  • Multiple MCP servers may be migrated to TypeScript in the future (uvilo-trash, typesense-mcp, suprsend).
  • Co-locating TypeScript MCP servers with the existing Python MCP servers in Configs-debian/mcp-servers/ keeps all MCP server code discoverable in one place.

5.2 Options

Option A: Forge/Configs-debian/mcp-servers/forge-bash/ as a subdirectory

  • Pros: Co-located with existing MCP servers; single discoverable location for all MCP server code; consistent with the canonical Configs folder
  • Cons: Mixes Python single-files with TypeScript project dirs; Configs name may not ideally describe source code

Option B: Forge/MCP/forge-bash/ — new top-level directory under Forge

  • Pros: Clear separation from configs; room for future TypeScript MCP servers; proper project structure
  • Cons: New directory; splits MCP server code across two locations; needs sidebar entry

Option C: Forge/MCP/servers/forge-bash/ — servers subdirectory

  • Pros: Room for shared code under Forge/MCP/
  • Cons: Deeper nesting; same split issue as Option B

5.3 Recommendation

Option A: Forge/Configs-debian/mcp-servers/forge-bash/. The canonical Configs folder (Forge/Configs-debian/) is intended for all custom configuration of the LibreChat environment, including MCP servers. Co-locating the TypeScript project alongside the existing Python MCP servers keeps everything in one discoverable place. The Configs name is less than ideal for source code, but the directory’s purpose is clear from its contents.

Proposed structure:

Forge/Configs-debian/mcp-servers/
├── forge-bash/
│   ├── src/
│   │   └── index.ts          # Main entry — McpServer setup, tool registration
│   ├── dist/
│   │   └── index.js          # esbuild output (gitignored)
│   ├── package.json
│   ├── tsconfig.json
│   └── README.md
├── uvilo-shell.py            # Existing Python servers
├── uvilo-trash.py
├── typesense-mcp.py
└── suprsend-mcp-proxy.py

5.4 Decision

Option A. Decided by: erik@uvilo.com — 2026-04-26


6. Layer 1 & Layer 2 Preservation — TypeScript Translation

Related: R6 (Preserve core execution and LLM-presentation layers)

6.1 Finding

The current Python implementation has well-defined Layer 1 (execution) and Layer 2 (output processing) logic. The TypeScript rewrite must preserve exact functional parity.

Layer 1 (Execution) — Python → TypeScript mapping:

PythonTypeScriptNotes
subprocess.run([SHELL_BIN, "-c", command], capture_output=True, timeout=300)execFileAsync(SHELL_BIN, ["-c", command], { timeout: 300_000 })execFile with promisify; maxBuffer set high for overflow handling
shutil.which("bash")which("bash") from node:child_process or manual PATH searchCan use execSync("which bash") or implement PATH lookup
5-minute hard timeoutSame — timeout: 300_000 in execFile optionsIdentical
Working directory: /workspacecwd: "/workspace"Identical

Layer 2 (Output processing) — direct translation:

FeaturePython implementationTypeScript equivalent
Binary guardCheck for \x00 bytes or >10% control chars in first 1024 bytesSame logic on Bufferbuf.includes(0) or byte iteration
Overflow truncation200 lines / 50KB → write to /tmp/uvilo-shell/cmd-N.txtSame thresholds → write to /tmp/forge-bash/cmd-N.txt
Stderr attachmentAppend stderr on non-zero exit codeSame
Metadata footer[exit:N | Xms]Same format
Safe decodedata.decode("utf-8", errors="replace")buf.toString("utf-8") — Node.js replaces invalid sequences with U+FFFD by default

Chain parsing (_split_chain in Python): This 40-line function splits on |, ||, &&, ; while respecting quotes. It needs to be translated to TypeScript. Since the permission system is removed, chain parsing is only needed for rm detection (R3). However, it should still be ported for potential future use and to maintain architectural parity.

6.2 Options

Only one reasonable option: direct translation of Layer 1 and Layer 2 logic to TypeScript, preserving identical behavior and thresholds.

6.3 Recommendation

Direct translation. No behavior changes. The overflow directory changes from /tmp/uvilo-shell/ to /tmp/forge-bash/ to match the new tool name.

6.4 Decision

Agreed. Decided by: erik@uvilo.com — 2026-04-26


7. LibreChat MCP Server Registration

Related: R5 (New tool name: forge-bash)

7.1 Finding

The current uvilo-shell is registered in librechat.yaml as:

uvilo-shell:
  title: "Uvilo Shell"
  description: "Bash execution with two-layer architecture and permissions"
  command: /usr/bin/python3
  args:
    - /workspace/erik/uvilo-os/Forge/Configs/mcp-servers/uvilo-shell.py
  timeout: 330000
  initTimeout: 30000
  serverInstructions: |
    Bash execution in /workspace. Supports chains (| && || ;); all commands
    permission-checked first. 'always' grants write shell-permissions.yaml.

For forge-bash, the registration changes to:

forge-bash:
  title: "Forge Bash"
  description: "Bash execution with two-layer architecture and audit logging"
  command: node
  args:
    - /workspace/erik/uvilo-os/Forge/MCP/forge-bash/dist/index.js
  timeout: 330000
  initTimeout: 30000
  serverInstructions: |
    Bash execution in /workspace. Supports chains (| && || ;).
    rm is blocked — use uvilo-trash instead. All commands are audit-logged.

Coexistence: Both uvilo-shell and forge-bash can be registered simultaneously. The agent system prompt references uvilo-shell — this must be updated to reference forge-bash once validated (part of R9).

Node.js availability: The Dockerfile uses a debian-slim LibreChat image which includes Node.js (the app itself is Node.js). No additional installation needed.

7.2 Options

Option A: Register forge-bash alongside uvilo-shell, update agent prompts later

  • Pros: Safe migration; can validate forge-bash before removing uvilo-shell
  • Cons: Temporarily two shell tools visible to the agent

Option B: Replace uvilo-shell with forge-bash immediately

  • Pros: Clean cutover
  • Cons: No rollback if forge-bash has issues

Option C: Register forge-bash alongside uvilo-shell, update agent prompts now, remove uvilo-shell later

  • Pros: Agent prompts point to the new tool from the start (avoids confusion from two shell tools); uvilo-shell remains available as fallback during validation
  • Cons: Temporarily two shell tools registered; must remember to remove uvilo-shell later

7.3 Recommendation

Option C. Register forge-bash alongside uvilo-shell, update agent prompts to reference forge-bash immediately, and remove uvilo-shell in a separate step after validation. This avoids the ambiguous period where the agent has two shell tools but prompts still reference the old one.

7.4 Decision

Option C. Decided by: erik@uvilo.com — 2026-04-26


8. TypeScript Migration Evaluation for All Forge MCP Tools

Related: R7 (Evaluate TypeScript for all Forge MCP tools)

8.1 Finding

Current Python MCP servers in the repo:

ServerLinesDependenciesComplexityMigration Effort
uvilo-shell~320mcp (FastMCP)High (permission system, chain parsing, Layer 2)Being rewritten (this project)
uvilo-trash~290mcp (FastMCP), pydanticMedium (file ops, timestamp logic, walk/trash/restore)Low-Medium
typesense-mcp~160mcp (FastMCP), pydanticLow (HTTP requests, JSON formatting)Low
suprsend-mcp-proxy~80mcp (FastMCP)Low (proxy to binary)Low
railway_env~20NoneTrivial (read /proc/1/environ)Trivial — shared utility

SDK support: The @modelcontextprotocol/sdk TypeScript SDK is mature (43K+ dependents, 146M+ weekly downloads). It provides McpServer + StdioServerTransport which maps 1:1 to Python’s FastMCP. Zod replaces Pydantic for schema validation. Peer dependency: @cfworker/json-schema.

Dependency implications:

  • Current Python deps: mcp, pydantic — installed via pip install mcp pymongo dnspython in Dockerfile
  • Future TypeScript deps: @modelcontextprotocol/sdk, zod, @cfworker/json-schema — bundled by esbuild into single files, no runtime deps needed
  • Net effect: Fewer runtime dependencies, no pip packages, faster cold start

Build/packaging approach: Each TypeScript MCP server bundles to a single JS file via esbuild. No node_modules at runtime. No pip. The Dockerfile only needs Node.js (already present).

Per-tool migration concerns:

ToolConcernMitigation
uvilo-trashFile system operations (shutil.move, os.walk, os.path)Node.js fs module has equivalent APIs; fs.promises for async
typesense-mcpHTTP requests (urllib.request)Native fetch (Node 18+) or undici (bundled by esbuild)
suprsend-mcpSpawns a binary subprocesschild_process.execFile — same as forge-bash

Cost-benefit:

  • Pro: Single language across all MCP tools; no Python runtime needed; smaller Docker image; faster cold starts
  • Pro: Shared utilities (Railway env injection) across all tools
  • Con: Migration effort for 4 servers (~530 lines total Python)
  • Con: uvilo-trash has the most file-system logic — Node.js fs is more verbose than Python os/shutil

8.2 Options

Option A: Full migration — all Python MCP servers → TypeScript

  • Pros: Single language; no Python in Docker image; consistent tooling
  • Cons: ~2-3 days effort for uvilo-trash, typesense-mcp, suprsend

Option B: Gradual migration — start with forge-bash, migrate others as needed

  • Pros: Lower risk; validate TypeScript approach with forge-bash first
  • Cons: Python + TypeScript coexist temporarily

8.3 Recommendation

Option B: Gradual migration. forge-bash is the proof of concept. Once validated, the other servers can be migrated one at a time. The evaluation confirms it’s feasible and beneficial — the SDK is mature, dependencies bundle cleanly, and Node.js APIs cover all needed functionality. No blocking issues found.

8.4 Decision

Option B. Decided by: erik@uvilo.com — 2026-04-26


9. Write_Typescript Skill Scope

Related: R8 (Create Write_Typescript skill)

9.1 Finding

The Write_Typescript skill must capture conventions and patterns for TypeScript code in Uvilo OS. Based on the research above, the skill should cover:

  1. Runtime & build: esbuild bundle — single mode (no dev/production distinction)
  2. Module system: ESM only ("type": "module" in package.json)
  3. SDK usage: McpServer from @modelcontextprotocol/sdk/server/mcp.js, StdioServerTransport from @modelcontextprotocol/sdk/server/stdio.js
  4. Schema validation: Zod (via zod/v4 import — SDK requirement)
  5. Project structure: src/index.ts entry, dist/index.js output (gitignored)
  6. Naming: kebab-case for directories and file names; PascalCase for TypeScript types/interfaces
  7. Error handling: Return { content: [{ type: 'text', text: error }], isError: true } from tools
  8. Logging: Use console.error() for debug/diagnostic output (never console.log — that’s stdout/MCP protocol)
  9. Railway env: Import from Forge/MCP/shared/railway-env.ts
  10. Build command: esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js
  11. Build command (full): esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js
  12. Type checking: tsc --noEmit (separate from build — esbuild doesn’t type-check)

9.2 Options

Only one option — the skill content is derived from the decisions above. If decisions change, the skill updates accordingly.

9.3 Recommendation

Create the skill after all decisions are approved, incorporating the finalized conventions. Place at Forge/Skills/Write_Typescript/SKILL.md with a references/ directory for extended examples if needed.

9.4 Decision

Agreed. Decided by: erik@uvilo.com — 2026-04-26


10. FORGE.md & Reference Cleanup Scope

Related: R9 (Update FORGE.md and related references)

10.1 Finding

References to the removed permission system exist in:

  1. FORGE.md — “PERMISSION RULE: Never set grant values yourself. When a ⚠️ PERMISSION REQUIRED block appears, STOP and present…”
  2. FORGE.md — Project session workflow step 2: “Check git status for uncommitted shell-permissions.yaml changes; commit if present”
  3. FORGE.md — uvilo-shell MCP server instructions: references to grant parameter and permission protocol
  4. Agent instructions — The system prompt references grant parameter in the tool schema description
  5. shell-permissions.yaml — The entire file becomes obsolete
  6. /end-session command — References committing shell-permissions.yaml changes

Cleanup actions:

  • Remove PERMISSION RULE from FORGE.md
  • Remove step 2 (shell-permissions.yaml check) from project session workflow
  • Update uvilo-shell → forge-bash references in FORGE.md and MCP server instructions
  • Trash Forge/Configs/shell-permissions.yaml and Forge/Configs-debian/shell-permissions.yaml
  • Update /end-session command to remove shell-permissions commit step
  • Update uvilo-shell.py docstrings (or trash the file once forge-bash is validated)

10.2 Options

Only one option — surgical removal of permission-related references.

10.3 Recommendation

Perform cleanup as part of the implementation phase, after forge-bash is validated. This ensures no references are broken during development.

10.4 Decision

Agreed. Decided by: erik@uvilo.com — 2026-04-26


11. Process Improvement — No Abandoned Historical Decisions in Research

11.1 Finding

Research documents should present only the current state of findings and decisions. Including notes about previous drafts (e.g., “Note: The previous draft recommended…”) pollutes the document with abandoned alternatives that were already superseded. Research is not frozen until approval — before approval, the document should be updated in-place to reflect the current best recommendation.

This principle applies to all project research documents, not just this one.

11.2 Recommendation

Update the Project Research skill (Forge/Skills/Project_Research/SKILL.md) to replace the current rule “Research is historical/append-only — never replace existing sections” with:

No historical residue. When a recommendation changes during research, update the Finding and Recommendation sections in-place. Do not append notes about previous drafts or abandoned alternatives. Research documents are not frozen until approved — before approval, they should always reflect the current best thinking. If a decision rationale needs to be preserved, it belongs in Learnings, not in the Research document.

The append-only rule still applies to decisions after approval (use dated revision notes), but not to findings and recommendations during research.

11.3 Decision

Agreed. Decided by: erik@uvilo.com — 2026-04-26