DEV Community

Cover image for I almost replaced Lovable with a $5 VPS, Dokploy and one MCP gateway
Web Artisan
Web Artisan

Posted on Originally published at junctio.org

I almost replaced Lovable with a $5 VPS, Dokploy and one MCP gateway

Trimming 600 tools to 20 saves agent tokens

Lovable is nice. You describe a site, an agent writes it, you click publish. It also costs $25–50 a month for the tiers that are actually usable, the code lives on their side, and the agent only knows how to do one thing.

I already pay for Claude. I already have a VPS with Dokploy on it. The missing piece was a way for the agent to deploy, and gateways for that already exist — MetaMCP, IBM’s ContextForge, a few others. None of them gave me the combination I wanted: multiple runtimes, one container, an OAuth server of its own, and a UI I don’t mind looking at. So I wrote one.

The setup

  • any $5 VPS. 1 GB of RAM is enough for the gateway, Dokploy itself wants 2 GB
  • Dokploy, which I was already using
  • Junctio, an MCP gateway I wrote, one container next to Dokploy
  • the official @dokploy/mcp server, running inside Junctio as a stdio process
  • AI Agent: Claude (Desktop/Code), ChatGPT, Codex, Cursor, whatever you have

The client talks to one URL. Junctio runs the Dokploy MCP server, keeps the Dokploy API key, and exposes the tools over Streamable HTTP with OAuth.

# Schema
AI Agent App --OAuth--> Junctio --stdio--> @dokploy/mcp --> Dokploy API
                        (API key lives here)
Enter fullscreen mode Exit fullscreen mode

What happens

The prompt is something like: “Build a landing page for X, push it to my GitHub, deploy it on Dokploy under x.example.com”. The agent writes the page, creates the app in Dokploy, points it at the repo, adds the domain and triggers the build. A few minutes end to end, most of it the Docker build.

Every tool call goes through the gateway and lands in the request log: method, tool, upstream, status, timing. Bodies are not stored: arguments and results can carry anything your agent touched, and a log full of that is a leak waiting to happen.

Junctio: Request log

Why a gateway and not just npx @dokploy/mcp in the client

claude.ai has no stdio. Claude Desktop and Claude Code will happily spawn a local stdio server; claude.ai in the browser and ChatGPT connectors will not, and they can’t send an API key header either. A custom connector means a remote MCP server with a real OAuth authorization server behind it: RFC 8414 metadata, dynamic client registration, PKCE, a consent screen. Junctio has that built in. Add the connector URL, approve at the consent screen with the admin password, done. No Cloudflare Worker wrapper, no separate auth service.

~600 tools is too many. The official Dokploy MCP server exposes the whole API, upwards of 600 tools at the moment.. That’s a lot of context, and the agent picks the wrong application-* call half the time. In Junctio I put the server in a namespace and hid everything except about 20: create project, create application, set source, set domain, deploy, read logs. Plus a short instruction block on the namespace (“never delete, ask before creating databases”). The client only sees the small set.

Junctio: Servers (upstream)

The key stays on the server. With the stdio setup every client machine has the Dokploy API key in a mcp.json. With the gateway there is one key, in one place, encrypted at rest. Each client gets its own OAuth client or API key and I can revoke it from the UI.

Junctio: Endpoints

Compose

services:
  junctio:
    image: ghcr.io/k2so-dev/junctio:latest
    pull_policy: always
    restart: unless-stopped
    environment:
      JUNCTIO_SECRET: ${JUNCTIO_SECRET:?set JUNCTIO_SECRET, e.g. openssl rand -hex 32}
      JUNCTIO_BASE_URL: ${JUNCTIO_BASE_URL:?set JUNCTIO_BASE_URL, e.g. https://mcp.example.com}
      JUNCTIO_TRUST_PROXY: "true"
      JUNCTIO_ADMIN_TOKEN: ${JUNCTIO_ADMIN_TOKEN:-}
      LOG_LEVEL: ${LOG_LEVEL:-info}
    volumes:
      - junctio-data:/data
      - junctio-cache:/cache
    tmpfs:
      - /tmp
    read_only: true
    networks:
      - dokploy-network

networks:
  dokploy-network:
    external: true

volumes:
  junctio-data:
  junctio-cache:
Enter fullscreen mode Exit fullscreen mode

Add it in Dokploy as a Compose service with the Raw provider. No published ports: Dokploy routes through its own Traefik, so the service joins dokploy-network and gets its domain on the Domains tab, pointed at container port 3000 with Let’s Encrypt on. junctio-cache holds the npx and uvx package caches — throwaway, but without it every cold start is slow. Outside Dokploy, put Caddy in front instead; either way OAuth needs TLS, while an API-key endpoint works fine over plain http on localhost.

Then in the UI: add server > runtime npx (or bunx) > package @dokploy/mcp > env DOKPLOY_URL, DOKPLOY_API_KEY. Create a namespace, hide tools, create an endpoint with OAuth on. Paste the URL into your agent app as a custom connector.

Full walkthrough: https://junctio.pages.dev/use-cases/claude-ai-connector/

What this is not

It is not Lovable. No live preview, no visual editor, no “click this button and change its color”. You get a chat and a deploy. If you want to iterate on UI visually, this is worse.

It is also not safer by default. You are giving an agent write access to your deployment platform. I hid the destructive tools and the agent still once tried to redeploy the wrong app because two had similar names. Keep a namespace per project, keep the tool set small, read the logs. Junctio can also run a scheduled vulnerability check on the servers it launches through npx, bunx, node or uvx, which matters when you install packages from npm with one click. It’s off by default, it’s advisory data rather than analysis, and container images, custom commands and remote servers are marked “not audited” because the gateway can’t tell what’s inside them:

Junctio: Security

Cost

VPS $5 a month. Dokploy free. Junctio free, MIT. AI subscription I already had.

Memory: Junctio idles at about 60 MB. A running stdio server adds its own footprint, @dokploy/mcp is another 60 MB or so. Idle servers get stopped after a timeout and restarted on the next call, so most of the time you’re back at 60 MB. It’s TypeScript on Bun, not Go, and I’m fine with that. I can maintain it.

What else this unlocks

The same trick works with anything that has an OpenAPI spec: run an OpenAPI-to-MCP server inside the gateway, hide the tools you don’t want, write instructions for the rest, and the agent can manage your shop, your DNS, your VPN panel. The gateway also browses the public MCP registry, so adding the next server is a click:

Junctio: Explore
Junctio: Explore other sources

Repo: https://github.com/k2so-dev/junctio
Docs: https://junctio.pages.dev/docs/

Top comments (10)

Collapse
 
reidmarlow profile image
Reid Marlow •

Trimming the 600 tools down to 20 is the part that actually saves this. When an agent has 600 deployment endpoints in its prompt, it spends tokens hallucinating parameters or picking similar routes, and any network blip triggers weird retries.

I keep a similar rule for local runner tools: never expose the raw management API to an agent loop. A narrow whitelist of idempotent endpoints keeps the model from guessing, and keeping the API key out of local mcp.json files is just basic hygiene.

Collapse
 
k2sodev profile image
Web Artisan •

This is nowhere near as smooth as Lovable, I know. But wait until the database grows: with Lovable that's a Supabase bill on top of the seat. Here it's bumping the VPS one tier. The two numbers don't compare.

Collapse
 
jkming profile image
jkming •

Trimming 600 tools to 20 is doing more work than the rest of the setup combined. A full API surface in context doesn't just cost tokens, it makes the agent pick wrong — your "wrong application-* call half the time" matches what everyone running an unfiltered MCP server ends up seeing. A small verb set plus a "never delete" instruction on the namespace is the right shape.

Good call on not logging bodies. Tool arguments carry whatever the agent just touched — env values, connection strings — and a plaintext request log quietly becomes the most sensitive file on the box.

One thing I'd want to know: can an OAuth client be scoped to a subset of the 20 tools? E.g. a connector that can deploy and read logs but not create databases. That's the line I'd want between my own client and one handed to a teammate.

Collapse
 
k2sodev profile image
Web Artisan •

Yes, that’s what Namespaces are for—configuring tools—and only then do you connect Endpoints (specifically OAuth) to the Namespaces.

Collapse
 
jithox profile image
jithox •

One MCP gateway is a clean pattern. The next layer I want on any gateway is explicit permissions, budgets, and a pass/fail receipt per tool call so the agent stays controllable.

Collapse
 
vlad_z_16b6320e21f32bee0d profile image
Vlad Z •

A $5 VPS is a good trade for a personal setup, and the numbers that don't show in the $5 are backups, bandwidth overage and the hours spent keeping it patched

Curious where you'd draw the line for moving the same setup onto something managed

Collapse
 
k2sodev profile image
Web Artisan •

True, $5 is not the full cost. It's everyone's choice - pay more and get convenience, or pay less and keep full control. I'll move to managed when downtime starts costing me more than the managed plan.

Collapse
 
pushpendraagrawal profile image
Pushpendra Agrawal •

we hit the same wall building an MCP marketplace at viaSocket. one big tool that maps 1:1 to an API surface (your 600 endpoints) reads great in docs and terribly in an agent's context window. what worked for us was collapsing each app down to one action that can do more than its name suggests, instead of exposing every CRUD call separately. cuts tokens and cuts the wrong-call problem you're describing. curious if you've measured how much of the "picks the wrong application-* call" problem goes away once you're down to ~20 vs the full 600, or if it's still mostly the namespace instructions doing the work.

Collapse
 
k2sodev profile image
Web Artisan •

I haven't measured it. The only real mistake I saw in the log was the agent hitting the wrong app because two had similar names, so the tool name wasn't the problem there. And I probably overstated the 600 thing. If the client loads tools lazily and the model just searches the list when it needs something, the top models handle all 600 fine. I never had trouble with that. Trimming helps when the client dumps every schema into the context at once.
By the way, you don't have to trim by hand. The gateway has a management MCP server, so you can point an agent at it and say "hide everything except what's needed to deploy a site".
Merging tools isn't there, right now you can only hide or rename them per namespace. One tool that calls several upstream ones is a good idea though, I'll think about it.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.