Migrate to uvilo-mono Plan 1
Scope: Migrate custom MCP servers from uvilo-os into uvilo-mono as standalone apps, build the McpServerManager process lifecycle manager inside BotCraft, integrate MCP tools into the Vercel AI SDK inference path, and implement deferred tool discovery. Covers Research §3 (External MCP Server Lifecycle Management) and §7 (Deployment Architecture).
Source Repository Locations
All current MCP server source code lives in uvilo-os at Forge/Configs/MCP_Servers/:
| Server | Language | Source Path | Build |
|---|---|---|---|
| forge-bash | TypeScript | Forge/Configs/MCP_Servers/forge-bash/src/index.ts | esbuild → dist/index.js |
| forge-discovery | TypeScript | Forge/Configs/MCP_Servers/forge-discovery/src/index.ts | esbuild → dist/index.js |
| uvilo-typesense | Python | Forge/Configs/MCP_Servers/typesense-mcp.py | Interpreted |
| uvilo-filesystem | npx package | @modelcontextprotocol/server-filesystem (npm) | npx -y |
| uvilo-trash | Python | Forge/Configs/MCP_Servers/uvilo-trash.py | Interpreted |
| uvilo-shell | Python | Forge/Configs/MCP_Servers/uvilo-shell.py | Interpreted (OUTDATED — drop) |
| forge-spawn | TypeScript | Forge/Configs/MCP_Servers/forge-spawn/src/index.ts | esbuild (OUTDATED — drop, reimplement later) |
Target monorepo: uvilo-mono at /workspace/erik/uvilo-mono/ (GitHub: ErikDakoda/uvilo-mono)
Current uvilo-mono app layout: apps/bot-craft, apps/uvilo-ai, apps/uvilo-mcp
Package namespace: packages/@erikdakoda/* (e.g., @erikdakoda/mcp, @erikdakoda/ai-tool, @erikdakoda/bot, @erikdakoda/inference)
Architecture Decisions (from Research §3 and §7)
-
DB-stored MCP server configs + in-process lifecycle manager (§3 Option A) — Store server configs (command, args, env, timeout, instructions) in a new
McpServermodel. Add a per-Bot join tableBotMcpServerfor binding. Build anMcpServerManagersingleton inside BotCraft that spawns stdio processes on demand. -
All MCP servers managed by BotCraft (§7 Option A) — The
McpServerManagerspawns stdio processes on demand, tracks health, discovers tools. Custom MCP servers (forge-bash, forge-discovery, etc.) are always-on; third-party servers start on first use and idle-timeout. One Railway service, one container. -
Vercel AI SDK
createMCPClient— Use@ai-sdk/mcp’screateMCPClientwith stdio transport to connect to MCP servers. At inference time,ChatPostHandlercollects both native AI tools and MCP tools from the Bot’s bound servers.
Task 1 — Set Up MCP App Infrastructure in uvilo-mono
Before any MCP server can be migrated, the monorepo must have app scaffolding, shared build config, and turbo pipeline entries for each MCP server app.
1.1 Add MCP server apps to pnpm workspace
Edit pnpm-workspace.yaml to add the new app entries:
1.2 Create each app directory with standard structure
Each MCP server app follows this structure:
1.3 Shared package.json template
Each MCP server app’s package.json should follow this pattern (adapted from the existing forge-bash/forge-discovery configs):
1.4 Add turbo build tasks
Edit turbo.json to add build tasks for each MCP server app:
1.5 Add root-level convenience scripts
Add to root package.json scripts:
Task 2 — Migrate forge-bash
Direct port of the existing TypeScript forge-bash MCP server. Minimal changes — adapt import paths and ensure it works as a monorepo app.
2.1 Copy source
Source: uvilo-os/Forge/Configs/MCP_Servers/forge-bash/src/index.ts (277 lines)
Create: uvilo-mono/apps/forge-bash/src/index.ts
2.2 Adapt the source
The existing code uses these imports:
Changes needed:
- Change
zod/v4import tozod(the monorepo uses zod v3, not v4):import { z } from 'zod' - Keep all other code as-is. The server logic (chain parsing, rm detection, audit logging, layer-2 output processing, Railway env injection) requires no changes.
2.3 Tool signature (preserve exactly)
The server registers one tool: run
Key behaviors to preserve:
- Supports
|,&&,||,;operators - Blocks
rm— returns error directing user to uvilo-trash - Audit logs to
/tmp/forge-bash/audit-{date}.tsv - Overflow: truncates at 200 lines / 50KB, writes full output to
/tmp/forge-bash/cmd-N.txt - Binary guard: detects null bytes / high control-char ratio
- Railway env injection: reads
/proc/1/environat startup - Shell detection:
which bashfallback to/bin/sh - Timeout: 300s (5 min), max buffer: 50MB
2.4 Verify
After building, test manually:
Task 3 — Migrate forge-discovery
Direct port of the existing TypeScript forge-discovery MCP server. Adapt for monorepo.
3.1 Copy source
Source: uvilo-os/Forge/Configs/MCP_Servers/forge-discovery/src/index.ts (464 lines)
Create: uvilo-mono/apps/forge-discovery/src/index.ts
3.2 Adapt the source
Changes needed:
- Change
zod/v4import tozod(same as forge-bash) - The
REPO_PATHconstant currently defaults to/workspace/erik/uvilo-os— keep as-is since the server will still read from the uvilo-os repo path on Railway. This is configurable viaprocess.env.REPO_PATH. - Keep all tool registrations and logic as-is.
3.3 Tool signatures (preserve exactly)
The server registers these tools:
| Tool | Input | Description |
|---|---|---|
list_departments | (none) | List all departments with README summary |
list_projects | { department: string } | List projects in a department |
get_project_phase | { project: string, department: string } | Get current project phase |
get_project_files | { project: string, department: string } | List project files with metadata |
list_skills | { department?: string } | List available skills |
get_skill_details | { skill: string, department?: string } | Get full SKILL.md content |
find | { query: string } | Fuzzy search across departments, projects, skills |
3.4 Verify
Task 4 — Rewrite uvilo-typesense as forge-typesense
Rewrite the Python
typesense-mcp.py(183 lines) as TypeScript. Rename toforge-typesense.
4.1 Reference implementation
Source: uvilo-os/Forge/Configs/MCP_Servers/typesense-mcp.py
The Python implementation provides 2 tools: search_knowledge and get_file_summary. Both call the Typesense multi_search endpoint.
4.2 Create TypeScript implementation
Create: uvilo-mono/apps/forge-typesense/src/index.ts
Environment variables:
TYPESENSE_URL— Typesense server URL (required)TYPESENSE_SEARCH_KEY— API key for search (required)TYPESENSE_COLLECTION— Collection name (default:"uvilo")
Implementation approach:
- Use Node.js built-in
fetch(Node 22+) instead ofurllib— no extra HTTP dependency needed - Use
McpServerfrom@modelcontextprotocol/sdk/server/mcp.js - Use
StdioServerTransportfrom@modelcontextprotocol/sdk/server/stdio.js - Use
zfromzodfor tool input schemas
4.3 Tool: search_knowledge
Preserve the exact same input parameters and output format as the Python version:
Filter construction: Build filter_by string from non-null parameters: department:=Forge && project:=Taxonomy etc.
Search parameters:
Output format (preserve exactly):
4.4 Tool: get_file_summary
Input: { path: string } — relative file path
Searches Typesense with query_by: "path" and filter_by: "path:={path}". Returns title + summary from the stored document. If no summary, returns message about re-indexing.
4.5 Typesense HTTP call
4.6 Verify
Task 5 — Create forge-filesystem
Custom TypeScript replacement for the npx
@modelcontextprotocol/server-filesystempackage, plus integration of the uvilo-trash Python server’s trash functionality.
5.1 Reference implementations
- Current filesystem server:
@modelcontextprotocol/server-filesystem(npx package) — providesread_text_file,write_file,edit_file,list_directory,search_files - Current trash server:
uvilo-os/Forge/Configs/MCP_Servers/uvilo-trash.py(493 lines) — providestrash_file,trash_directory,untrash_file,untrash_directory,list_trash,empty_trash
5.2 Create TypeScript implementation
Create: uvilo-mono/apps/forge-filesystem/src/index.ts
Environment variables:
WORKSPACE_ROOT— Root directory for filesystem access (default:/workspace)REPO_ROOT— Repository root for trash operations (default:/workspace/erik/uvilo-os)
5.3 Filesystem tools (port from @modelcontextprotocol/server-filesystem)
These 5 tools replicate the standard filesystem MCP server. Use Node.js built-in fs module:
| Tool | Input | Description |
|---|---|---|
read_text_file | { path: string, head?: number, tail?: number } | Read file contents (optionally first/last N lines) |
write_file | { path: string, content: string } | Create or overwrite file |
edit_file | { path: string, edits: Array<{oldText: string, newText: string}>, dryRun?: boolean } | Line-based edits with diff output |
list_directory | { path: string } | List files/dirs with [FILE]/[DIR] prefixes |
search_files | { path: string, pattern: string, excludePatterns?: string[] } | Glob-style recursive file search |
Security: Validate that all paths resolve within WORKSPACE_ROOT. Reject path traversal (e.g., ../ escaping the root).
5.4 Trash tools (port from uvilo-trash.py)
Port the 6 trash tools from the Python implementation. Key logic to preserve:
| Tool | Input | Description |
|---|---|---|
trash_file | { path: string } | Move file to .trash/ with timestamp suffix |
trash_directory | { path: string } | Move directory to .trash/ with timestamp suffix |
untrash_file | { path: string } | Restore file from trash |
untrash_directory | { path: string } | Restore directory from trash |
list_trash | (none) | List items in trash with metadata |
empty_trash | (none) | Permanently delete all trash items (irreversible) |
Key trash logic to preserve:
- Trash root:
{REPO_ROOT}/.trash/ - Timestamp format:
__YYYY-MM-DDTHH-MM-SSappended before file extension - Protected paths:
.git,.trash,.internal/node_modules,.internal/dist,.internal/.astro - Original directory structure preserved in trash (e.g.,
Architecture/Old_Doc.md→.trash/Architecture/Old_Doc__2026-03-14T18-30-00.md) - Cleanup empty parent directories after untrash
empty_trashis irreversible — include a confirmation step
5.5 Verify
Task 6 — Integrate Playwright MCP
Add Playwright MCP as a managed server within the monorepo build. Unlike the custom servers, Playwright is a third-party npm package — we need to ensure it’s available in the Docker image.
6.1 Add Playwright as a BotCraft dependency
Since Playwright MCP will be managed by McpServerManager inside the BotCraft container, add it as a dependency of bot-craft:
6.2 Playwright MCP configuration in DB seed
When seeding MCP server configs (Task 7), include:
6.3 Note on Playwright skill
Reference the existing skill: Forge/Skills/Playwright_MCP/SKILL.md — this contains Forge-specific usage patterns. The tool descriptions from the MCP server itself should be sufficient; the skill content is for Forge’s system prompt.
Task 7 — Build McpServerManager
Core infrastructure: a singleton service inside BotCraft that manages the lifecycle of MCP server processes. This is the heart of Research §3 Option A.
7.1 Create new package: @erikdakoda/mcp-lifecycle
Create: uvilo-mono/packages/@erikdakoda/mcp-lifecycle/
This is a new workspace package (not an app) because it’s imported by BotCraft’s server-side code, not deployed as a standalone process.
Package structure:
7.2 Dependencies
7.3 McpProcess — individual process wrapper
Each McpProcess wraps:
- A
child_process.ChildProcess(the stdio MCP server) - A Vercel AI SDK
MCPClientcreated viacreateMCPClientwithStdioClientTransport - Health-checking (ping on
initialize) - Idle timer management
Key methods:
start()— Spawn process, create MCP client, callinitialize, discover toolsstop()— Close MCP client, kill processgetTools()— Return cached tool definitions from this serverhealthCheck()— Ping the server to verify it’s responsivegetState()— Return currentProcessState
7.4 McpServerManager — singleton
Lifecycle rules (from Research §3.3):
- Lazy-start servers on first tool invocation for that Bot’s session
- Keep servers alive for the duration of the inference session
- Support explicit start/stop via API
- Health-check idle servers and shut them down after a configurable timeout (default: 60s)
7.5 Vercel AI SDK integration
Use @ai-sdk/mcp’s createMCPClient for each managed process:
Important: The StdioClientTransport from @modelcontextprotocol/sdk spawns the process internally. The McpProcess wrapper must coordinate with this — either let the transport manage the process, or manage it externally and pass the already-running process. The simplest approach: let StdioClientTransport handle process spawning, and track the process reference for health checks and idle shutdown.
7.6 DB models
Add two new models via the existing ZenStack/Prisma schema:
McpServer model:
BotMcpServer join table:
7.7 Seed script
Create a seed script (packages/@erikdakoda/mcp-lifecycle/seed/) that populates McpServer and BotMcpServer for the 4 custom + Playwright servers:
Note: Env vars with ${VAR} syntax should be resolved at runtime by McpServerManager using process.env.
7.8 Verify
- Unit test:
McpProcesscan start and stop a simple echo MCP server - Integration test:
McpServerManagercan load configs, start always-on servers, and discover tools - The seed script runs without errors
Task 8 — Integrate MCP Tools into ChatPostHandler
Wire
McpServerManagerinto the inference path so thatChatPostHandlerpasses MCP tools alongside native AI tools tostreamText().
8.1 Current inference path
The inference flow is:
apps/bot-craft/src/app/api/ai/chat/route.ts→ importsPOSTfrom@dakoda/inference/api-handlers/ChatPostHandlerChatPostHandlercallsgetAiTools()for native AI toolsChatPostHandlercallsstreamText()from Vercel AI SDK with the collected tools
8.2 Modification point
In packages/@erikdakoda/inference/api-handlers/ChatPostHandler.ts, after collecting native AI tools, also collect MCP tools:
8.3 MCP client lifecycle within a request
Critical: MCP clients should NOT be created/destroyed per request. The McpServerManager maintains persistent connections. Tools are discovered once (on server start or first use) and cached. The mcpClient.tools() returns tool definitions that are compatible with Vercel AI SDK’s tools parameter.
However, the createMCPClient returns an MCPClient that needs to stay alive for the tool invocations to work. The McpProcess wrapper keeps the client alive as long as the process is running.
8.4 Tool invocation flow
When streamText() calls an MCP tool:
- Vercel AI SDK routes the call through the
MCPClient’s tool handler MCPClientsends atools/callrequest over stdio to the MCP server process- The MCP server executes the tool and returns the result
- Result flows back through
streamText()to the LLM
This works automatically with Vercel AI SDK’s MCP integration — no custom invocation code needed.
8.5 Verify
- Start BotCraft locally with MCP servers running
- Send a chat message that triggers an MCP tool call
- Verify the tool executes and returns results
Task 9 — Build Deferred Tool Discovery (ToolSearch)
Implement a provider-agnostic tool discovery mechanism similar to LibreChat’s ToolSearch, since not all MCP tools should be loaded into every inference call (token budget concerns).
9.1 Vercel AI SDK analysis
The Vercel AI SDK 6 provides:
createMCPClient→mcpClient.tools()— discovers ALL tools from a server- Anthropic has a built-in
toolSearchBm25tool, but it’s Anthropic-specific - No built-in provider-agnostic deferred tool discovery
Conclusion: We must build a custom deferred tool discovery mechanism.
9.2 Design: tool_search AI Tool
Create a native AI tool (@erikdakoda/ai-tool) that serves as a tool catalog:
9.3 Implementation approach
-
Tool catalog cache: At
McpServerManagerstartup (or on first tool discovery), store all MCP tool definitions (name, description, input schema) in an in-memory index. -
BM25 search: Implement a simple BM25 ranking over tool names and descriptions. This can use a lightweight library or a custom implementation.
-
Deferred loading pattern:
- At inference time, only inject the
tool_searchtool (plus native AI tools) intostreamText() - When the LLM calls
tool_search, return matching tool definitions - The LLM then uses the discovered tools in subsequent steps
- Vercel AI SDK’s
maxStepshandles multi-step tool use automatically
- At inference time, only inject the
-
Alternative approach — partial tool loading: Instead of fully deferred, load tool names + descriptions for all MCP tools (cheap in tokens), but only load full input schemas when the tool is actually called. This requires a custom tool wrapper.
9.4 Integration with Vercel AI SDK
The key challenge: Vercel AI SDK’s streamText() takes a static tools object. Tools cannot be added mid-stream.
Solution options:
Option A — tool_search returns tool definitions, LLM requests them explicitly:
- The
tool_searchtool returns tool metadata (name, description, parameter summary) - On next user turn, the LLM can use the discovered tools
- Limitation: requires an extra turn to discover tools
Option B — Eager load all tool definitions but defer execution:
- Pass all tool definitions to
streamText()(names, descriptions, schemas) - This consumes tokens but is the standard Vercel AI SDK pattern
- No deferred execution needed — all tools are available immediately
Option C — Custom middleware that injects tools on demand:
- Use
prepareStepcallback instreamText()to add tools dynamically - When the LLM calls
tool_search, inject the matching tools into the next step - This is the closest to LibreChat’s ToolSearch behavior
Recommendation: Option C — Use Vercel AI SDK’s prepareStep or experimental_toolCallStreaming to dynamically add tools. If the SDK doesn’t support dynamic tool injection mid-stream, fall back to Option A (two-step discovery).
9.5 Verify
tool_search("filesystem")returns forge-filesystem toolstool_search("bash", { mcp_server: "forge-bash" })returns only theruntool- LLM can discover and use tools across multiple steps
Task 10 — Update BotCraft Dockerfile and Deployment
Bundle all MCP server apps into the BotCraft Docker image for Railway deployment.
10.1 Current deployment state
BotCraft is deployed as a Next.js standalone app on Railway. The Dockerfile from Research §1.5 already exists conceptually.
10.2 Dockerfile modifications
Add MCP server build steps to the Dockerfile:
10.3 Railway environment variables
Add to the Railway BotCraft service:
TYPESENSE_URL— Typesense server URLTYPESENSE_SEARCH_KEY— Typesense search API key- All existing variables from Research §1.5
10.4 Health check
Add a /api/health endpoint to BotCraft that also checks MCP server status:
Task 11 — Remove Outdated Servers and Clean Up
Drop uvilo-shell and forge-spawn from the configuration. They are replaced by forge-bash (already covers shell) and will be reimplemented differently (out of scope).
11.1 Servers to drop
- uvilo-shell — replaced by forge-bash (which already provides bash execution with rm-blocking and audit logging)
- forge-spawn — will be reimplemented as a different architecture later (out of scope for this Plan)
11.2 Actions
- Do NOT migrate
uvilo-shell.pyorforge-spawn/to uvilo-mono - Remove
uvilo-shellandforge-spawnentries from any DB seed data - Remove
uvilo-shellandforge-spawnfrom the LibreChatlibrechat.yaml(when decommissioning LibreChat) - The
uvilo-trash.pyPython server is subsumed intoforge-filesystem(Task 5) — do NOT migrate as a separate app
11.3 Verify
- No references to
uvilo-shellorforge-spawnin the new monorepo codebase forge-filesystemincludes all trash functionality
Dependency Order
Tasks 2–6 can be done in parallel after Task 1 is complete. Task 7 depends on all MCP server apps existing. Tasks 8–11 are sequential.