Skip to content
Back to skills

2878 Virtual Mcp Server Explained 8511e4e0

ASecurity

This document explains how Virtual MCP Servers work using diagrams and examples. For detailed implementation specifics, see [virtual-mcp-server.md](virtual-mcp-server.md). ---

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmentgogitapidatabasebackend

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 2878-virtual-mcp-server-explained_8511e4e0 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 2878 Virtual Mcp Server Explained 8511e4e0?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 2878 Virtual Mcp Server Explained 8511e4e0
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2878-virtual-mcp-server-explained-8511e4e0/badge)](https://www.skillsdirectory.com/skills/tools-only-2878-virtual-mcp-server-explained-8511e4e0)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Virtual MCP Server - How It Works

This document explains how Virtual MCP Servers work using diagrams and examples. For detailed implementation specifics, see [virtual-mcp-server.md](virtual-mcp-server.md).

---

## What Problem Are We Solving?

Consider a typical development setup: you have separate MCP servers for GitHub (code search, PRs), Slack (messaging), and Jira (issue tracking). Your AI agent needs tools from all three, which means:

- Managing three separate connections
- Handling three different sessions
- Dealing with tool name conflicts (both GitHub and Jira have a `search` tool)

A Virtual MCP Server solves this by providing a **single endpoint** that aggregates tools from multiple backends. Your agent connects once and gets access to all the tools it needs.

```
WITHOUT Virtual Server:              WITH Virtual Server:

  You                                  You
   |                                    |
   +---> GitHub Server                  |
   |        |-> search                  v
   |        |-> create_pr         +------------+
   |                              |  Virtual   |
   +---> Slack Server             |  Server    |
   |        |-> send_message      +-----+------+
   |        |-> list_channels           |
   |                              +-----+-----+-----+
   +---> Jira Server              |           |     |
            |-> create_issue      v           v     v
            |-> search_issues   GitHub     Slack   Jira
                                Server     Server  Server
```

**Benefits:**
- Your app only connects to ONE server instead of many
- You can pick exactly which tools you want from each backend
- You can rename tools to avoid confusion (like "github_search" vs "jira_search")
- You can control who has access to which tools

---

## The Big Picture

Request flow when a client connects to a Virtual MCP Server:

```
+----------------+                    +------------------+
|   Your App     |                    |   MCP Gateway    |
|                |                    |                  |
|  "I want to    | ---(1) Request --> |  Nginx receives  |
|   search on    |                    |  your request    |
|   GitHub"      |                    +--------+---------+
|                |                             |
|                |                             v
|                |                    +------------------+
|                |                    |  Lua Router      |
|                |                    |  (the brain)     |
|                |                    |                  |
|                |                    |  "Ah, this tool  |
|                |                    |   belongs to     |
|                |                    |   GitHub backend"|
|                |                    +--------+---------+
|                |                             |
|                |                             v
|                |                    +------------------+
|                |                    |  GitHub Backend  |
|                |                    |                  |
|                | <--(4) Response -- |  (does the       |
|                |                    |   actual work)   |
+----------------+                    +------------------+
```

Each component is described below.

---

## The Three Key Players

### 1. Nginx (Reverse Proxy)

Nginx receives incoming requests and handles:

- JWT authentication via `auth_request` subrequest
- Path-based routing to determine which virtual server
- Invoking the Lua content handler for MCP protocol processing

```
Request arrives at /virtual/dev-tools
                |
                v
        +---------------+
        |    Nginx      |
        |               |
        |  1. Check JWT |  <-- "Is this token valid?"
        |  2. Read path |  <-- "Which virtual server?"
        |  3. Call Lua  |  <-- "Hand off to the router"
        +---------------+
```

### 2. Lua Router (Content Handler)

The Lua router (`virtual_router.lua`) runs as an nginx content handler. It:

- Reads tool-to-backend mappings from JSON config files
- Translates tool aliases back to original names
- Manages session multiplexing across backends
- Issues concurrent subrequests for aggregation methods

### 3. Backend Servers

The actual MCP servers (GitHub, Slack, Jira, etc.) that execute tool calls. The virtual server coordinates requests but delegates all execution to backends.

---

## How Tool Mapping Works

This is the core mechanism. The process works as follows:

### Step 1: Configuration is Created

When someone creates a virtual server, they specify which tools to include:

```
Virtual Server: "dev-tools"
Path: /virtual/dev-tools

Tool Mappings:
  +------------------+------------------+------------------+
  | Tool Name        | Backend Server   | Alias            |
  +------------------+------------------+------------------+
  | search           | /github          | github_search    |
  | search           | /jira            | jira_search      |
  | send_message     | /slack           | (none - use as-is)|
  +------------------+------------------+------------------+
```

Both GitHub and Jira have a tool called "search". Aliases resolve this naming conflict.

### Step 2: Mapping File is Generated

The system writes a JSON file that the Lua router will read:

```
File: /etc/nginx/lua/virtual_mappings/dev-tools.json

{
  "tool_backend_map": {
    "github_search": {
      "original_name": "search",
      "backend_location": "/_backend/github"
    },
    "jira_search": {
      "original_name": "search",
      "backend_location": "/_backend/jira"
    },
    "send_message": {
      "original_name": "send_message",
      "backend_location": "/_backend/slack"
    }
  }
}
```

This file is a lookup table mapping tool names to their backend locations.

### Step 3: Request Comes In

When your app calls a tool:

```
Your app sends:
{
  "method": "tools/call",
  "params": {
    "name": "github_search",      <-- The alias you see
    "arguments": { "query": "bug fixes" }
  }
}
```

### Step 4: Lua Router Translates

The Lua router:
1. Reads the mapping file
2. Looks up "github_search"
3. Finds: backend is "/_backend/github", original name is "search"
4. Rewrites the request:

```
Forwarded to /_backend/github:
{
  "method": "tools/call",
  "params": {
    "name": "search",             <-- Original name the backend knows
    "arguments": { "query": "bug fixes" }
  }
}
```

### Step 5: Response Goes Back

The GitHub backend responds. The Lua router passes it back to your app unchanged.

---

## The Complete Request Flow (Sequence Diagram)

Sequence diagram for a `tools/call` request:

```
Your App          Nginx           Lua Router        Backend
   |                |                  |               |
   |  POST /virtual/dev-tools          |               |
   |  tools/call: github_search        |               |
   |--------------->|                  |               |
   |                |                  |               |
   |                |  auth_request    |               |
   |                |  (check JWT)     |               |
   |                |----------------->|               |
   |                |  OK + scopes     |               |
   |                |<-----------------|               |
   |                |                  |               |
   |                |  content_by_lua  |               |
   |                |----------------->|               |
   |                |                  |               |
   |                |      Read mapping file           |
   |                |      "github_search" ->          |
   |                |        backend: /_backend/github |
   |                |        original: search          |
   |                |                  |               |
   |                |      Check session cache         |
   |                |      (do we have a session       |
   |                |       with this backend?)        |
   |                |                  |               |
   |                |      Rewrite tool name           |
   |                |      github_search -> search     |
   |                |                  |               |
   |                |                  | POST to       |
   |                |                  | /_backend/github
   |                |                  |-------------->|
   |                |                  |               |
   |                |                  |   Response    |
   |                |                  |<--------------|
   |                |                  |               |
   |<----------------------------------|               |
   |      Response                     |               |
```

---

## Session Management (The Tricky Part)

Each backend server requires its own session.

When your app connects to the virtual server, it gets ONE session ID:

```
Your app <---> Virtual Server (session: vs-abc123)
```

But behind the scenes, the virtual server maintains SEPARATE sessions with each backend:

```
Virtual Server:
  +-- Session with GitHub: sess-gh-001
  +-- Session with Slack:  sess-sl-002
  +-- Session with Jira:   sess-jr-003
```

The Lua router keeps track of this mapping so you don't have to.

### The Two-Tier Cache

Looking up sessions from the database on every request would be slow. So we use two levels of caching:

```
Request: "What's the GitHub session for client vs-abc123?"

        +-------------------+
        |  Level 1 Cache    |  <-- Super fast (in nginx memory)
        |  (Shared Dict)    |      TTL: 30 seconds
        |                   |
        |  "Do I have it?"  |
        +--------+----------+
                 |
        MISS     |
                 v
        +-------------------+
        |  Level 2 Cache    |  <-- Fast (MongoDB lookup)
        |  (MongoDB)        |      TTL: 1 hour
        |                   |
        |  "Check database" |
        +--------+----------+
                 |
        MISS     |
                 v
        +-------------------+
        |  Create New       |  <-- Send "initialize" to backend
        |  Session          |      Store in both caches
        +-------------------+
```

**Why two levels?**
- Level 1 is in memory - no network call, extremely fast
- Level 2 is in MongoDB - survives server restarts
- If the server restarts, we lose Level 1 but Level 2 still has sessions

---

## Listing Tools (Aggregation)

When your app asks "what tools do you have?", the virtual server needs to ask ALL backends:

```
Your app asks: tools/list

Lua Router:
  +-- Ask GitHub: "What tools do you have?"
  |     Response: [search, create_pr, list_repos]
  |
  +-- Ask Slack: "What tools do you have?"
  |     Response: [send_message, list_channels]
  |
  +-- Ask Jira: "What tools do you have?"
        Response: [create_issue, search_issues]

Lua Router combines them:
  [github_search, create_pr, list_repos,     <-- Applied aliases
   send_message, list_channels,
   jira_search, create_issue]                 <-- Renamed "search" to "jira_search"
```

**Important optimization:** These backend calls happen IN PARALLEL, not one after another. This makes the aggregation fast.

```
Time ------>

Sequential (slow):
  [GitHub call]---[Slack call]---[Jira call]---Done

Parallel (fast):
  [GitHub call]-----
  [Slack call]------+---Done
  [Jira call]-------
```

---

## What the Nginx Config Looks Like

When a virtual server is enabled, the system generates two things:

### 1. A Location Block (for routing)

```nginx
location /virtual/dev-tools {
    # Tell Lua which virtual server this is
    set $virtual_server_id "dev-tools";

    # Check authentication first
    auth_request /validate;

    # Run the Lua router
    content_by_lua_file /etc/nginx/lua/virtual_router.lua;
}
```

### 2. Internal Backend Locations

```nginx
# These are marked "internal" - only Lua can use them
# Regular users can't access them directly

location /_backend/github {
    internal;
    proxy_pass https://github-mcp.example.com/mcp;
}

location /_backend/slack {
    internal;
    proxy_pass https://slack-mcp.example.com/mcp;
}
```

The Lua router uses these internal locations to talk to backends.

---

## Error Handling

What happens when things go wrong?

### Backend is Down

```
Lua Router tries to call /_backend/github
  |
  v
Connection fails or returns error
  |
  v
Lua Router returns error to your app:
{
  "error": {
    "code": -32000,
    "message": "Backend server unreachable: /github"
  }
}
```

### Session Expired

```
Lua Router uses cached session sess-gh-001
  |
  v
GitHub returns: "400 Bad Request - Invalid session"
  |
  v
Lua Router:
  1. Delete sess-gh-001 from both caches
  2. Send new "initialize" to GitHub
  3. Get new session: sess-gh-002
  4. Cache it in both levels
  5. Retry the original request with new session
```

### User Lacks Permission

```
User has scopes: ["mcp-access"]
Tool "create_pr" requires: ["github-write"]
  |
  v
Lua Router checks scopes... DENIED
  |
  v
Response: 403 Forbidden
{
  "error": "Missing required scope: github-write"
}
```

---

## Access Control in Simple Terms

Access control works at two levels:

### Level 1: Server Access

To use the virtual server at all, you need certain scopes:

```
Virtual Server: /virtual/dev-tools
Required Scopes: ["mcp-access"]

User with scopes ["mcp-access"] -> Allowed in
User with scopes ["other-stuff"] -> Blocked at the door
```

### Level 2: Tool Access

Individual tools can require additional scopes:

```
Tool: create_pr
Required Scopes: ["github-write"]

User with ["mcp-access", "github-read"] -> Can't use this tool
User with ["mcp-access", "github-write"] -> Can use this tool
```

When listing tools, the Lua router hides tools the user can't access:

```
Full tool list:    [search, create_pr, delete_repo]
User scopes:       ["mcp-access", "github-read"]

Filtered list:     [search]  <-- Only shows tools user can actually use
```

---

## How Changes Are Applied

When you create or update a virtual server:

```
1. You call the API: POST /api/virtual-servers
          |
          v
2. Service validates the configuration
   - Does each backend server exist?
   - Does each tool exist on its backend?
   - Are all alias names unique?
          |
          v
3. Configuration saved to MongoDB
          |
          v
4. Nginx config regenerated
   - New location block written
   - New mapping JSON file written
          |
          v
5. Nginx reloaded
   - nginx -s reload
   - New config takes effect immediately
          |
          v
6. Virtual server is live!
```

---

## Quick Reference

### Files You Should Know

| File | What It Does |
|------|-------------|
| `virtual_router.lua` | The Lua brain that routes requests |
| `nginx_service.py` | Generates nginx config + mapping files |
| `virtual_server_service.py` | Business logic and validation |
| `virtual_server_routes.py` | REST API endpoints |
| `/etc/nginx/lua/virtual_mappings/*.json` | Tool mapping files read by Lua |

### Key Concepts

| Term | Plain English |
|------|--------------|
| Virtual Server | A fake server that coordinates real servers |
| Tool Mapping | "This tool comes from that backend" |
| Alias | A renamed tool to avoid confusion |
| Backend Location | Where to forward requests (internal nginx path) |
| Session Multiplexing | One client session, many backend sessions |
| Scope | A permission string that controls access |

### Common Operations

| What You Want | What Happens |
|---------------|--------------|
| List tools | Asks all backends in parallel, combines results |
| Call a tool | Looks up backend, translates name, forwards request |
| Initialize | Creates client session, backend sessions are lazy |
| Ping | Responds immediately, no backend calls |

---

## Summary

1. **Virtual servers aggregate tools** from multiple backends into one endpoint
2. **Nginx routes requests** to the Lua router based on path
3. **Lua router reads mapping files** to know which tool goes where
4. **Aliases solve naming conflicts** when two backends have same tool names
5. **Sessions are cached in two levels** for speed and reliability
6. **Access control works at server and tool level** using scopes
7. **Backend calls happen in parallel** when listing tools

The virtual server acts as a coordinator - all tool execution happens on the backend servers. The virtual server's role is to present a unified endpoint to clients.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.