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:
| Approach | Cold start | Production ready | Dependencies | Notes |
|---|---|---|---|---|
npx tsx (esbuild transpile on-the-fly) | ~100ms | ✅ | tsx (dev) | Already used in repo. No build step. |
node --import tsx | ~100ms | ✅ | tsx | Same as above, more explicit. |
esbuild bundle → node dist/index.js | ~10ms | ✅ | esbuild (dev) | Fastest runtime. Single file output. No tsx at runtime. |
tsc compile → node dist/index.js | ~10ms | ✅ | typescript (dev) | Slowest build. Type-checks at compile time. |
| Node.js native type-stripping (v22.6+) | ~50ms | ⚠️ experimental | None | No 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 withnode 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
--externalfor 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:
-
High-level
McpServer— declarative tool registration viaserver.registerTool()with Zod schemas. Handles protocol negotiation, capability advertisement, and message routing automatically. -
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:
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).
Railway env var injection: The Python servers read /proc/1/environ to inject Railway env vars. In Node.js, this is equally straightforward:
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):
Columns:
ts— ISO 8601 UTC timestamp (e.g.,2026-04-26T14:32:01.234Z)command— Full command string (tabs/newlines escaped)exitCode— Process exit code (integer)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
jqdependency; 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;-deleteis 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;
Configsname 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:
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:
| Python | TypeScript | Notes |
|---|---|---|
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 search | Can use execSync("which bash") or implement PATH lookup |
| 5-minute hard timeout | Same — timeout: 300_000 in execFile options | Identical |
Working directory: /workspace | cwd: "/workspace" | Identical |
Layer 2 (Output processing) — direct translation:
| Feature | Python implementation | TypeScript equivalent |
|---|---|---|
| Binary guard | Check for \x00 bytes or >10% control chars in first 1024 bytes | Same logic on Buffer — buf.includes(0) or byte iteration |
| Overflow truncation | 200 lines / 50KB → write to /tmp/uvilo-shell/cmd-N.txt | Same thresholds → write to /tmp/forge-bash/cmd-N.txt |
| Stderr attachment | Append stderr on non-zero exit code | Same |
| Metadata footer | [exit:N | Xms] | Same format |
| Safe decode | data.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:
For forge-bash, the registration changes to:
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:
| Server | Lines | Dependencies | Complexity | Migration Effort |
|---|---|---|---|---|
| uvilo-shell | ~320 | mcp (FastMCP) | High (permission system, chain parsing, Layer 2) | Being rewritten (this project) |
| uvilo-trash | ~290 | mcp (FastMCP), pydantic | Medium (file ops, timestamp logic, walk/trash/restore) | Low-Medium |
| typesense-mcp | ~160 | mcp (FastMCP), pydantic | Low (HTTP requests, JSON formatting) | Low |
| suprsend-mcp-proxy | ~80 | mcp (FastMCP) | Low (proxy to binary) | Low |
| railway_env | ~20 | None | Trivial (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 viapip install mcp pymongo dnspythonin 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:
| Tool | Concern | Mitigation |
|---|---|---|
| uvilo-trash | File system operations (shutil.move, os.walk, os.path) | Node.js fs module has equivalent APIs; fs.promises for async |
| typesense-mcp | HTTP requests (urllib.request) | Native fetch (Node 18+) or undici (bundled by esbuild) |
| suprsend-mcp | Spawns a binary subprocess | child_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
fsis more verbose than Pythonos/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:
- Runtime & build: esbuild bundle — single mode (no dev/production distinction)
- Module system: ESM only (
"type": "module"in package.json) - SDK usage:
McpServerfrom@modelcontextprotocol/sdk/server/mcp.js,StdioServerTransportfrom@modelcontextprotocol/sdk/server/stdio.js - Schema validation: Zod (via
zod/v4import — SDK requirement) - Project structure:
src/index.tsentry,dist/index.jsoutput (gitignored) - Naming: kebab-case for directories and file names; PascalCase for TypeScript types/interfaces
- Error handling: Return
{ content: [{ type: 'text', text: error }], isError: true }from tools - Logging: Use
console.error()for debug/diagnostic output (neverconsole.log— that’s stdout/MCP protocol) - Railway env: Import from
Forge/MCP/shared/railway-env.ts - Build command:
esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js - Build command (full):
esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js - 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:
- FORGE.md — “PERMISSION RULE: Never set grant values yourself. When a ⚠️ PERMISSION REQUIRED block appears, STOP and present…”
- FORGE.md — Project session workflow step 2: “Check
git statusfor uncommittedshell-permissions.yamlchanges; commit if present” - FORGE.md — uvilo-shell MCP server instructions: references to
grantparameter and permission protocol - Agent instructions — The system prompt references
grantparameter in the tool schema description - shell-permissions.yaml — The entire file becomes obsolete
/end-sessioncommand — 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.yamlandForge/Configs-debian/shell-permissions.yaml - Update
/end-sessioncommand 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