Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/site/src/components/UseCaseCard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import { ArrowRight } from "@lucide/vue";
import type { CollectionEntry } from "astro:content";

interface Props {
entry: CollectionEntry<"useCases">;
detailed?: boolean;
}

const { entry, detailed = false } = Astro.props;
---

<a
href={`/use-cases/${entry.id}/`}
class:list={[
"group rounded-xl border bg-card p-5 transition-colors hover:border-foreground/40",
detailed && "flex flex-col"
]}
>
<span class="text-xs font-medium tracking-wide text-muted-foreground uppercase">{entry.data.client}</span>
<span class:list={["mt-2 block font-semibold", detailed && "text-lg tracking-tight"]}>{entry.data.title}</span>
<span class:list={["mt-1.5 block text-sm leading-6 text-muted-foreground", detailed && "flex-1"]}>
{entry.data.description}
</span>
{
detailed && (
<span class="mt-4 inline-flex items-center gap-1 text-sm font-medium">
Read
<ArrowRight class="size-4 transition-transform group-hover:translate-x-0.5" />
</span>
)
}
</a>
1 change: 1 addition & 0 deletions apps/site/src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const useCases = defineCollection({
title: z.string(),
description: z.string(),
client: z.string(),
group: z.enum(["client", "agent"]).default("client"),
order: z.number(),
keywords: z.array(z.string()).default([])
})
Expand Down
51 changes: 51 additions & 0 deletions apps/site/src/content/use-cases/3x-ui.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: A VPN panel your agent operates
description: 3x-ui publishes an OpenAPI spec and issues bearer tokens. Load it into the OpenAPI MCP Server behind Junctio and ask for a client, a limit and a link.
client: 3x-ui
group: agent
order: 8
keywords: ["3x-ui api mcp", "3x-ui automation claude", "xray panel agent", "mcp vpn panel"]
---

3x-ui already has everything this needs: a REST API under `/panel/api/`, a generated OpenAPI document, and bearer tokens you create in the panel. So there is no integration to write. One upstream server in Junctio, pointed at the spec, and the panel becomes a set of tools your agent can call from Claude Desktop.

What that buys you: ask for a client on an inbound with thirty days and fifty gigabytes, get the subscription link back. Ask which clients are near their limit. Reset the traffic on one, disable an expired one. The panel is still the source of truth and still where you look when something is wrong.

## The setup

Make a token first, in Settings, Security, API Token. It is a full-admin credential, so treat it like the panel password.

Then add one server with runtime `uvx` and the argument `awslabs.openapi-mcp-server@latest`, carrying this environment:

```
API_NAME=3x-ui
API_BASE_URL=https://panel.example.com
API_SPEC_URL=https://docs.sanaei.dev/openapi.json
AUTH_TYPE=bearer
AUTH_TOKEN=<the token>
```

The spec declares its server as `/`, so `API_BASE_URL` carries the real origin, including the base path if your panel runs under one. If the panel sits on the same host as the gateway and is reached over a private address, add `ALLOW_PRIVATE_NETWORKS=true`. Pinning a copy of the spec on the gateway's volume and pointing `API_SPEC_PATH` at it is the safer choice for a panel you upgrade rarely.

The rest is the usual three clicks: a `vpn` namespace holding this one server, an endpoint with `oauth` auth, then the endpoint URL added as a custom connector in Claude Desktop. The consent screen asks for your admin password once.

## Instructions worth writing

The spec describes shapes, not conventions, and the conventions are what an agent gets wrong. Put them in the namespace instructions, by hand in the Namespaces page or through the [management MCP](/docs/management-mcp/):

- Which inbound new clients go on, when there is more than one.
- The naming convention for clients, and the fact that the identifier has to be unique.
- The units the panel actually uses. Expiry is a millisecond timestamp and the traffic cap is counted in bytes regardless of what the field is called, and an agent that guesses will create a client that expired in 1970.
- How a subscription link is composed from the panel's subscription settings, so the agent hands over a URL rather than a raw config.

[Endpoints](/docs/endpoints/) explains how that text reaches the client and how a per-tool description override works when one operation needs more than the namespace text can carry.

## Keeping it safe

The token is full control of the panel and it stays in the gateway, encrypted with `JUNCTIO_SECRET`. The client gets tools, never the credential. Hide the operations that have no business being one sentence away from a chat window: backup and restore, restart, anything under settings or Xray configuration. An endpoint with `oauth` auth means every client passed the consent screen, and revoking that client in Settings ends its access immediately.

The panel controls who reaches the network behind it. Whether that is allowed, and for whom, is your jurisdiction's question and not the gateway's.

## Related

[Any REST API as MCP tools](/use-cases/openapi/) for the general pattern and every environment variable, [Endpoints](/docs/endpoints/) for tool overrides and auth modes, [a custom connector for claude.ai](/use-cases/claude-ai-connector/) for the OAuth flow.
51 changes: 51 additions & 0 deletions apps/site/src/content/use-cases/dokploy-agent.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: A Lovable alternative on a $5 VPS
description: Your agent writes the site and deploys it through the Dokploy MCP behind your gateway. No preview pane, but the code is yours and the bill is a VPS.
client: Dokploy
group: agent
order: 6
keywords: ["lovable alternative self-hosted", "dokploy mcp claude", "deploy from claude.ai", "ai agent deploy vps"]
---

Three pieces, all of which you either already pay for or can rent for the price of a coffee: Dokploy on a small VPS, Junctio next to it, and the Claude or OpenAI subscription in your editor. The gateway holds the Dokploy MCP server and publishes it to your client over OAuth, so the agent that writes the code is also the agent that ships it.

What this is not: there is no preview pane, no visual editor, no click-to-edit on a rendered page. The agent reads build logs, not screenshots. What it is instead: the code lives in your repository, the server is yours, nothing is locked behind a seat price, and the same gateway serves every other tool the agent needs.

## Prerequisites

- Dokploy on the VPS, and Junctio deployed as a Compose service inside it. [Dokploy](/docs/deploy/dokploy/) has the exact YAML, the network it has to join and the Domains tab settings.
- TLS on the gateway's domain, `JUNCTIO_BASE_URL` set to it and `JUNCTIO_TRUST_PROXY=true` behind Traefik. OAuth does not work over plain HTTP.
- A Dokploy API key from Settings, Profile, the API/CLI section.

## The setup

1. **Add the server.** Runtime `npx`, arguments `-y` and `@dokploy/mcp`, environment `DOKPLOY_URL` set to the panel's URL without a path and `DOKPLOY_API_KEY` set to the key. The key is stored encrypted with `JUNCTIO_SECRET` and is never handed to a client.
2. **Create a namespace and cut it down.** The package announces upwards of six hundred tools across every corner of the Dokploy API. An agent does not need SSO, user management, registries or cluster settings, and a tool list that size costs context on every request. Keep projects, applications, Compose, domains, environments, deployments and logs; hide the rest. Hiding is per namespace membership and it is real: a hidden tool is not callable, not merely absent from the listing.
3. **Write the instructions.** The namespace description is what the client reads before it calls anything. Name the project the agent should deploy into, the domain pattern for new applications, and the rule that it asks before deleting or stopping something. [Endpoints](/docs/endpoints/) explains how that text is composed with what each upstream announced.
4. **Create the endpoint.** Auth `oauth` for claude.ai and Claude Desktop, `any` if a Claude Code session on your laptop should also reach it with an API key.
5. **Connect the client.** Add the endpoint URL as a custom connector, approve the consent screen, and the client holds a token that refreshes itself from there on.

## Two ways to write the code

**With a filesystem.** Claude Code or Codex on your machine writes the files, commits and pushes, then calls `dokploy__application-deploy` through the gateway. The editor does what editors are good at and the gateway does the deployment.

**Without one.** claude.ai and Claude Desktop have no filesystem, so put a GitHub MCP server in the same namespace. The agent commits through GitHub and deploys through Dokploy, and both arrive at the client as one endpoint behind one consent screen. This is the configuration that reads most like a hosted app builder: you describe a page in a chat window and a URL comes back.

## A session, roughly

Ask for a landing page for a client. The agent writes the files and pushes them. It calls the create-application tool with the repository and branch, attaches a domain, triggers a deployment, then polls the deployment tool until the build finishes. If the build fails it reads the log, fixes the Dockerfile or the build command, and deploys again. You see each tool call by its prefixed name, `dokploy__application-deploy` and friends, and every one of them is written to the gateway's request log with the key that made it.

## Where it stops

- **No live preview.** The agent cannot see the rendered page. Give it a screenshot yourself, or add a Playwright server to the namespace and let it take one.
- **Builds take what they take.** A small VPS building a Next.js image is not instant, and the agent waits with you.
- **Frameworks are Dokploy's business.** Whatever Nixpacks detects, or whatever your Dockerfile says. Nothing here changes that.
- **Rollbacks are manual in practice.** The tools exist, but knowing which deployment to go back to is still your call.

## Keeping it safe

The Dokploy key is full control over that panel, which means the host. It lives in the gateway, encrypted, and the agent gets a set of tools rather than the key itself. Bind the endpoint to OAuth so every client authorizes through the consent screen, and revoke the client in Settings when the work is done; its tokens die with it. If you also switch on the [management MCP](/docs/management-mcp/), remember it can add stdio servers, which is equivalent to shell access on the gateway host.

## Related

[Dokploy](/docs/deploy/dokploy/) for the deployment itself, [Endpoints](/docs/endpoints/) for auth modes and tool overrides, [a custom connector for claude.ai](/use-cases/claude-ai-connector/) for the OAuth side in detail.
58 changes: 58 additions & 0 deletions apps/site/src/content/use-cases/openapi.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Any REST API as MCP tools, in an evening
description: Point the OpenAPI MCP Server at a spec, let an agent write the tool descriptions through the management MCP, hand the client one endpoint.
client: OpenAPI
group: agent
order: 7
keywords: ["openapi to mcp", "rest api as mcp server", "awslabs openapi-mcp-server", "mcp gateway openapi"]
---

The OpenAPI MCP Server from AWS Labs reads an OpenAPI document and turns every operation in it into a tool. That is the whole trick, and on its own it produces tools that are exactly as good as the summaries in the spec, which is to say usually not good enough for an agent to use well.

Behind Junctio it becomes something better. The gateway runs the Python process and keeps the API credential, so the client never sees either. The tool descriptions and the namespace instructions are rewritten in the gateway, by hand or by an agent, and the upstream spec is left untouched. What reaches the client is a Streamable HTTP endpoint with a curated set of tools.

## The setup

Add a server with runtime `uvx` and one argument, `awslabs.openapi-mcp-server@latest`. The environment does the rest:

| Variable | Meaning |
|---|---|
| `API_NAME` | Short name for the API. It shows up in tool names and prompts. |
| `API_BASE_URL` | Where requests go. The spec's own server URL is often relative or wrong. |
| `API_SPEC_URL` | URL of the OpenAPI document. Use `API_SPEC_PATH` instead for a copy on the gateway's volume. |
| `AUTH_TYPE` | `none`, `bearer`, `basic` or `api_key`. |
| `AUTH_TOKEN` | The bearer token, when `AUTH_TYPE` is `bearer`. |
| `AUTH_API_KEY`, `AUTH_API_KEY_NAME`, `AUTH_API_KEY_IN` | The key, the parameter name and whether it rides in a header, the query string or a cookie. |
| `AUTH_USERNAME`, `AUTH_PASSWORD` | Basic credentials. |
| `INCLUDE_TAGS`, `EXCLUDE_TAGS` | Cut a large spec down at the source, before it becomes tools. |
| `ALLOW_PRIVATE_NETWORKS` | Needed when the API is on the same host or Docker network as the gateway. |

From there it is an ordinary upstream: put it in a namespace, create an endpoint, connect a client. [Upstream servers](/docs/upstream-servers/) covers the runtime side, including why `uvx` needs an exec-capable `TMPDIR` and why the image already gives it one.

## Let the agent finish the job

This is the part a bare OpenAPI server cannot do. Connect the [management MCP](/docs/management-mcp/) from Claude Code or Codex and ask it to work on the namespace you just created:

- List the tools and read what each one announces. Specs written for humans leave summaries empty or cryptic.
- Rewrite the descriptions that say nothing. A per-namespace override replaces what the upstream announced, and clearing it goes back to the original.
- Hide the operations that should never be called from a chat window: anything that deletes in bulk, anything that rotates credentials.
- Write the namespace instructions. The conventions that are obvious to you and invisible in the spec go here: which identifiers to use, which fields are in milliseconds, the order operations are normally done in. The `preview_namespace_instructions` tool returns the composed text so the agent can check its own work.

Everything it writes is stored in the gateway. Replace the upstream later and the wording survives.

## What this fits

- **A self-hosted shop.** Medusa, Saleor and WooCommerce all publish a spec. The agent creates products, edits prices and answers questions about stock, in the same session where it writes the storefront.
- **A CMS or an internal admin API.** The one with twelve endpoints that nobody wants to build a client for.
- **A panel.** [3x-ui](/use-cases/3x-ui/) is the worked example: a VPN panel the agent operates through its own spec.

## Where it stops

- **One spec, one server.** `ADDITIONAL_SPECS` takes more, but a server per API keeps the prefixes and the audit trail legible.
- **No OAuth to the upstream.** This server speaks bearer, basic and API key. When the API behind it needs an OAuth dance, use a native MCP server and Junctio's own [upstream OAuth](/docs/upstream-servers/), which refreshes tokens before they expire.
- **A bad spec makes bad tools.** If the document lies about its parameters, the agent inherits the lie. Overrides fix descriptions, not schemas.
- **Large specs cost context.** Three hundred tools in one namespace is a slow client. Use the tag filters, then hide what survives them.

## Related

[Upstream servers](/docs/upstream-servers/) for runtimes and credentials, [Endpoints](/docs/endpoints/) for prefixes, overrides and instructions, [Management MCP](/docs/management-mcp/) for the agent that does the curation.
31 changes: 19 additions & 12 deletions apps/site/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import Base from "@/layouts/Base.astro";
import CopyCommand from "@/components/CopyCommand.vue";
import GatewayFlow from "@/components/GatewayFlow.vue";
import TokenTimeline from "@/components/TokenTimeline.vue";
import UseCaseCard from "@/components/UseCaseCard.astro";
import { SITE } from "@/lib/site";
import { ArrowRight, Box, EyeOff, KeyRound, Layers, RefreshCw, ShieldCheck, Terminal } from "@lucide/vue";
import { getCollection } from "astro:content";

const useCases = (await getCollection("useCases")).sort((a, b) => a.data.order - b.data.order);
const clientCases = useCases.filter((entry) => entry.data.group === "client");
const agentCases = useCases.filter((entry) => entry.data.group === "agent");

const features = [
{
Expand Down Expand Up @@ -164,6 +167,21 @@ const jsonLd = {
</div>
</section>

<section class="border-t">
<div class="mx-auto max-w-6xl px-4 py-20 sm:px-6">
<div class="max-w-2xl">
<h2 class="text-2xl font-semibold tracking-tight">Give the agent hands</h2>
<p class="mt-2 text-muted-foreground">
The gateway is the OAuth door for claude.ai, Claude Desktop, Codex and Claude Code. Behind it, anything with
an API becomes tools the agent can call: a deploy platform, a shop, a VPN panel.
</p>
</div>
<div class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{agentCases.map((entry) => <UseCaseCard entry={entry} />)}
</div>
</div>
</section>

<section class="border-t bg-muted/30">
<div class="mx-auto max-w-6xl px-4 py-20 sm:px-6">
<div class="flex items-end justify-between gap-4">
Expand All @@ -174,18 +192,7 @@ const jsonLd = {
<a href="/use-cases/" class="hidden text-sm font-medium hover:underline sm:inline">All use cases</a>
</div>
<div class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{
useCases.slice(0, 6).map((entry) => (
<a
href={`/use-cases/${entry.id}/`}
class="group rounded-xl border bg-card p-5 transition-colors hover:border-foreground/40"
>
<span class="text-xs font-medium tracking-wide text-muted-foreground uppercase">{entry.data.client}</span>
<span class="mt-2 block font-semibold">{entry.data.title}</span>
<span class="mt-1.5 block text-sm leading-6 text-muted-foreground">{entry.data.description}</span>
</a>
))
}
{clientCases.map((entry) => <UseCaseCard entry={entry} />)}
</div>
</div>
</section>
Expand Down
Loading
Loading