Migrate to uvilo-mono Research
1. Agentic Inference on Single-Server Runtime
1.1 Finding
ChatPostHandler.ts is the inference entry point used by both uvilo-ai and bot-craft. It uses Vercel AI SDK’s streamText() → toUIMessageStreamResponse() for SSE streaming. Its dependencies:
| Dependency | Location | Works on Railway? |
|---|---|---|
getServerAuthSession() | @dakoda/database/server | ✅ Uses Better Auth getSession — cookie-based for browsers, but needs API-key support for programmatic access |
getEnhancedExtendedPrisma() | @dakoda/database/server | ✅ Neon pooled connection — works anywhere |
getAiTools() | @dakoda/ai-tool/aiToolRegistry | ✅ In-process registry — works anywhere |
getLanguageModel() | @dakoda/bot/server | ✅ OpenAI provider — works anywhere |
getModelParameters() | @dakoda/bot/server | ✅ Reads convo fields — works anywhere |
computeBotSystemPrompt() | @dakoda/bot/server | ✅ DB reads + scripting — works anywhere |
request.signal | Next.js Request | ⚠️ Needs long-lived HTTP connection — NOT compatible with Vercel serverless 10–60s timeout |
onFinish callback | Vercel AI SDK | ⚠️ Same timeout constraint |
Next.js can run in standalone mode (next start) on Railway, which gives long-lived HTTP connections with no serverless timeout. This is exactly how bot-craft is already configured (it has start script). The request.signal and onFinish work fine in standalone mode.
The real blocker is auth: ChatPostHandler uses getServerAuthSession() which requires a browser cookie session. For programmatic access (forge-spawn), we need API-key auth.
1.2 Options
| # | Option | Pros | Cons |
|---|---|---|---|
| A | Convert BotCraft → Forge runtime on Railway standalone | Already has app/api/ai/chat/route.ts wired to ChatPostHandler; shares all @erikdakoda/* packages; same Neon DB + Better Auth; clean separation (uvilo-ai = SaaS on Vercel, bot-craft = Forge on Railway) | Must strip/repurpose UI code; must add API-key auth path; must add MCP server lifecycle, per-Bot tool binding, Forge system prompts |
| B | Deploy second uvilo-ai instance on Railway standalone | Zero code changes to inference path | Duplicates entire SaaS app for one workload; two copies of same codebase to maintain; confusing architecture |
| C | Extract inference into plain Express/Fastify on Railway | Full control; lightweight; no Next.js overhead | Must replicate auth, DB access, tool registry, message persistence outside monorepo context; duplicates infrastructure |
| D | Vercel Sandbox / Hybrid | — | Sandbox is for code execution, not API serving; Vercel timeout still applies; defeats the purpose |
1.3 Recommendation
Option A — Convert BotCraft → Forge runtime. It already has ChatPostHandler wired at app/api/ai/chat/route.ts, shares all packages, and provides clean architectural separation. The only additions needed are: (1) API-key auth path, (2) external MCP server lifecycle management, (3) per-Bot MCP tool binding, (4) Forge system prompts. All are additive — no inference code changes needed.
Option B is a viable fallback if BotCraft’s UI code creates excessive drag, but stripping pages and adding routes is simpler than maintaining two copies of uvilo-ai.
Options C and D are not viable.
1.4 Decision
Selected: Option A — Convert BotCraft → Forge runtime
Decided by: erik@uvilo.com — 2026-05-14
1.5 Deploying BotCraft Next.js on Railway
To run BotCraft as a long-lived Next.js server on Railway (Option A), the following deployment changes are required:
| Item | Current State | Required Change |
|---|---|---|
next.config.ts output | 'export' for static builds only | Add output: 'standalone' as the default (non-static) output mode |
| Dockerfile | None — currently deployed to Vercel | Create a Dockerfile for Railway; Nixpacks does not handle pnpm monorepo standalone output well |
| Start command | next start | node .next/standalone/apps/bot-craft/server.js (standalone output produces a self-contained server.js) |
| PORT handling | Vercel manages this | Railway injects PORT env var; standalone server.js reads it automatically |
| Static assets | Part of Vercel build | Copy .next/static and public into the Docker image beside the standalone output |
Dockerfile outline:
Key environment variables for Railway:
NEON_URL— Postgres connection stringAUTH_SECRET— Better Auth secretNEXT_PUBLIC_BASE_URL— BotCraft’s own Railway URLBETTER_AUTH_URL— Same asNEXT_PUBLIC_BASE_URL- Any API keys needed by AI tools (e.g.,
GOOGLE_API_KEY,TAVILY_API_KEY)
Monorepo standalone quirk: Next.js standalone mode traces monorepo workspace dependencies and copies them into .next/standalone/ with the full path structure (apps/bot-craft/, packages/*/). The Dockerfile must preserve this directory structure when copying the standalone output.
Health check: Railway can use the default Next.js response on GET / or a dedicated /api/health endpoint (to be added).
1.6 Decision
Selected: Option A — Convert BotCraft → Forge runtime (deployment via standalone Dockerfile on Railway)
2. API-Key Auth for Programmatic Access (R5)
Already implemented. No further work needed.
getServerAuthSession() (called by ChatPostHandler) already resolves dual auth:
Authorization: Bearer <key>→ validated againstApiKeytable viaresolveBearerApiKeyAuth()- No
Authorizationheader → falls back to Better Auth cookie session
Supporting infrastructure already exists:
ApiKeymodel (Better Auth plugin, in@erikdakoda/auth/models/ApiKey.zmodel)McpToolsAuthenticateHandler(in@erikdakoda/mcp)GenerateUserApiTokenHandler(admin-only key minting from user grid)apiKeyAuthSessionToExecutionUser()(maps API-key session to execution context)
3. External MCP Server Lifecycle Management (R2)
3.1 Finding
LibreChat configures 16 MCP servers in librechat.yaml with per-agent tool selection via UI checkboxes. uvilo-mono has:
- Native AI Tools (
@erikdakoda/ai-tool): In-process tools registered at startup, available to all Bots. Works perfectly. - External MCP proxy (
apps/uvilo-mcp): A stdio MCP server that proxies to uvilo-ai’s HTTP MCP API. Used for external consumers — does NOT attach external MCP servers to Bots. - MCP HTTP API (
@erikdakoda/mcp): Handlers for listing/inoking tools via HTTP. Used by uvilo-mcp.
What’s missing: the ability to attach arbitrary external MCP servers (stdio or SSE) to a Bot and have the system manage their lifecycle (start, health-check, stop) and discover their tools at inference time.
Vercel AI SDK natively supports MCP tool discovery via @ai-sdk/mcp-filesystem and related packages. The streamText() call in ChatPostHandler can accept MCP tools alongside native AI tools.
3.2 Options
| # | Option | Pros | Cons |
|---|---|---|---|
| A | DB-stored MCP server configs + process manager | Config lives in DB (no YAML); per-Bot binding; can be managed via API/UI; process lifecycle managed by a singleton service on the Railway server | Requires building process manager (spawn/health/restart); stdio MCP servers are long-lived processes |
| B | YAML/file-based MCP config (like LibreChat) | Familiar pattern; simple to implement | Harder to manage programmatically; doesn’t fit uvilo-mono’s DB-first architecture |
| C | Use Vercel AI SDK’s MCP integration directly | SDK handles client-side MCP connection | Still need server-side process lifecycle; SDK’s MCP support is designed for connecting to already-running servers |
3.3 Recommendation
Option A — DB-stored MCP server configs + in-process lifecycle manager. Store server configs (command, args, env, timeout, instructions) in a new McpServer model. Add a per-Bot join table BotMcpServer for binding. Build an McpServerManager singleton that spawns stdio processes on demand, tracks health, and discovers tools. At inference time, ChatPostHandler collects both native AI tools and MCP tools from the Bot’s bound servers.
The McpServerManager should:
- Lazy-start servers on first tool invocation
- 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
3.4 Decision
Selected: Option A — DB-stored configs + process manager
Decided by: erik@uvilo.com — 2026-05-14
4. Sub-Agent Spawning API (R3)
4.1 Finding
The current forge-spawn MCP server works by:
- Authenticating to LibreChat via email/password → session cookie
- Creating a conversation via LibreChat’s REST API
- Sending a message via LibreChat’s UI Chat API
- Polling for completion (disconnect mode) or monitoring for stalls (Ralph Wiggum mode)
After migration, we need to replace the LibreChat API calls with calls to uvilo-mono’s API. The key endpoints needed:
| Operation | Current (LibreChat) | Target (uvilo-mono) |
|---|---|---|
| Create convo | POST /api/convos | POST /api/convos (exists in @erikdakoda/convo) |
| Send message | POST /api/ask (SSE stream) | POST /api/ai/chat (SSE stream) |
| Poll status | Parse SSE events | Parse SSE events (same protocol) |
| Abort | POST /api/edit/abort | Need new abort endpoint |
4.2 Options
| # | Option | Pros | Cons |
|---|---|---|---|
| A | Rewrite forge-spawn to call uvilo-mono API | Direct; uses existing convo/chat endpoints; API-key auth already exists | Must implement abort endpoint; must adapt SSE parsing to Vercel AI SDK’s stream format |
| B | Build spawn into uvilo-mono as a native feature | Tighter integration; could expose as AI Tool | Scope creep; conflates runtime with orchestration |
| C | Keep forge-spawn but target uvilo-mono | Minimal change to orchestration layer | Same as A but acknowledges forge-spawn remains an MCP server |
4.3 Recommendation
Option C — Keep forge-spawn as MCP server, rewrite internals to target uvilo-mono API. The forge-spawn MCP server already has the right architecture (spawn, check_job, abort_job). We just replace the LibreChat HTTP client with a uvilo-mono HTTP client. Key changes:
- Replace email/password auth with API-key auth (
Authorization: Bearer <key>) - Replace LibreChat convo creation with uvilo-mono convo API
- Replace LibreChat chat API with
POST /api/ai/chat(same SSE protocol via Vercel AI SDK) - Add abort endpoint to uvilo-mono (
POST /api/ai/chat/abort) - Adapt SSE event parsing to Vercel AI SDK’s
UIMessageStreamformat
4.4 Decision
Selected: Option C — Rewrite forge-spawn internals
Decided by: erik@uvilo.com — 2026-05-14
5. Runtime Model Override (R4)
5.1 Finding
Bots currently bind to a single integration + modelName at creation time. ChatPostHandler reads these from the convo’s associated Bot. LibreChat works around this by creating duplicate modelSpecs (e.g., “Forge Sonnet 4.6” and “Forge GLM 5.1 [Master]” are the same agent with different models).
5.2 Options
| # | Option | Pros | Cons |
|---|---|---|---|
| A | Add modelOverride param on chat API | Clean; no Bot duplication; user picks model at conversation start | Requires updating ChatPostHandler to accept override; requires UI support |
| B | Accept Bot duplication (same as LibreChat workaround) | No code changes; already proven | Creates many duplicate Bots; harder to manage; not scalable as models increase |
| C | Add model variants to Bot definition | Single Bot with multiple model options; structured | More complex Bot model; UI must support variant selection |
5.3 Recommendation
Option A — Add modelOverride param on chat/convo creation API. This is the simplest approach and matches how users expect to interact: pick an agent, then pick a model. The override is passed as an optional parameter to POST /api/ai/chat (or POST /api/convos), and ChatPostHandler uses it instead of the Bot’s default model when present.
5.4 Decision
Selected: Option A — Model override param
Decided by: erik@uvilo.com — 2026-05-14
6. Bot-to-Agent Parity Mapping (R1)
6.1 Finding
Forge’s 19 LibreChat agents need to map to 19 uvilo-mono Bots. The mapping is straightforward:
| LibreChat Agent | Bot Name | Model | Role |
|---|---|---|---|
| Forge Chat (default) | Forge Chat | GPT 5.5 | Primary chat |
| Forge GLM 5.1 [Master] | Forge GLM 5.1 | GLM 5.1 | Master model |
| Forge GLM 5 Turbo | Forge GLM 5 Turbo | GLM 5 | Fast model |
| Forge Opus 4.6 | Forge Opus | Opus 4.6 | High-reasoning |
| Forge Sonnet 4.6 | Forge Sonnet | Sonnet 4.6 | Balanced |
| Forge GPT 5.5 | Forge GPT 5.5 | GPT 5.5 | Latest GPT |
| Forge GPT 5.4 | Forge GPT 5.4 | GPT 5.4 | GPT 5.4 |
| Forge GPT 5.1 | Forge GPT 5.1 | GPT 5.1 | GPT 5.1 |
| Forge GPT 5.4 Nano | Forge GPT 5.4 Nano | GPT 5.4 Nano | Lightweight |
| Forge Gemini 3.1 Pro | Forge Gemini Pro | Gemini 3.1 Pro | Google Pro |
| Forge Gemini 3.2 Flash | Forge Gemini Flash | Gemini 3.2 Flash | Google Flash |
| Forge Page Worker | Forge Page Worker | Sonnet 4.6 | Page handling |
| Forge Project Worker | Forge Project Worker | Sonnet 4.6 | Project work |
| Forge Project Evaluator | Forge Project Evaluator | GLM 5.1 | Evaluation |
| Forge Project Thinker | Forge Project Thinker | GLM 5.1 | Planning |
| Forge Project Runner GLM 5.1 [Master] | Forge Project Runner | GLM 5.1 | Project execution |
| Forge Project Runner Sonnet 4.6 | Forge Project Runner | Sonnet 4.6 | Project execution |
| Forge Task Runner GLM 5.1 [Master] | Forge Task Runner | GLM 5.1 | Task execution |
| Forge Task Runner Sonnet 4.6 | Forge Task Runner | Sonnet 4.6 | Task execution |
Note: 4 agents are duplicates differing only in model (Project Runner x2, Task Runner x2). With R4 (model override), these collapse to 2 unique Bots with model variants.
6.2 Options
Only one viable approach: create Bots via seed script or API, matching each agent’s system prompt, model, and tool bindings.
6.3 Recommendation
Create a seed script that:
- Creates 15 unique Bots (after collapsing model-duplicates via R5)
- Sets each Bot’s system prompt (composed from child prompts matching current
Forge_Chat_Prompt.mdstructure) - Binds the correct MCP servers per Bot (matching current LibreChat per-agent tool config)
- Sets default model parameters (temperature, reasoning effort, verbosity)
6.4 Decision
Selected: Seed script approach
Decided by: erik@uvilo.com — 2026-05-14
7. Deployment Architecture (R0)
7.1 Finding
Forge currently runs on Railway as a single service containing:
- LibreChat (Node.js server)
- 5 custom MCP servers (forge-bash, forge-discovery, forge-spawn, uvilo-typesense, uvilo-shell)
- 11 third-party MCP servers (npx-based: filesystem, vercel, github, neon, notion, linear, railway, context7, playwright; python-based: suprsend)
After migration, the architecture becomes:
- BotCraft app (Next.js standalone) — the Forge runtime, deployed as a Railway service
- Custom MCP servers — still running within the same container or as separate Railway services
- Third-party MCP servers — managed by the new
McpServerManagerinside BotCraft
7.2 Options
| # | Option | Pros | Cons |
|---|---|---|---|
| A | All MCP servers as BotCraft-managed processes | Single container; simple networking; matches LibreChat’s model | Higher memory usage; longer startup; all processes share one container |
| B | Custom MCP servers as Railway services; third-party managed by BotCraft | Custom servers are long-lived and benefit from independent scaling; third-party are ephemeral | More complex deployment; networking between services |
| C | All MCP servers as separate Railway services | Full isolation; independent scaling | Most complex; 16+ services; high cold-start overhead |
7.3 Recommendation
Option A — All MCP servers managed by BotCraft. This matches LibreChat’s proven model. The McpServerManager spawns stdio processes on demand and manages their lifecycle. Custom MCP servers (forge-bash, forge-discovery, etc.) are always-on; third-party servers (npx-based) start on first use and idle-timeout. This keeps the deployment simple — one Railway service, one container, same pattern as today.
The forge-spawn MCP server is a special case: it makes HTTP calls to the BotCraft API (itself). It doesn’t need process management — it’s just an MCP server that calls back to the app. It can remain a standalone process managed by McpServerManager.
7.4 Decision
Selected: Option A — All MCP servers managed by BotCraft
Decided by: erik@uvilo.com — 2026-05-14