Skip to content

MCP Servers: A Practical Guide to Context on Demand

Model Context Protocol, or MCP, is how an AI client asks for context instead of waiting for you to shuttle it in. Clients can already expose open files, workspace context, and sometimes terminal output. Remote services, databases, CI runs, output from another shell, you still feed that in by hand. MCP gives the client a shared protocol to discover tools, read resources, and reach those systems directly. If the LLM is doing the reasoning, an MCP server is the part that lets it look around and, when you allow it, act on current state.

The model doesn't have to rely only on whatever you remembered to supply it. It can ask for current state, call a tool you exposed, and come back with something closer to evidence.

Less autocomplete. Less guessing. More "I checked x and y, and here is what I found."

MCP is an open protocol for connecting AI applications to the systems around them.

An MCP server can expose a few kinds of things:

  • Tools: actions the model can ask to run, such as read_logs, run_tests, or a query you wrapped instead of exposing raw shell.
  • Resources: readable context, such as repository metadata, service health, runbooks, or log files.
  • Prompts: reusable prompt templates that package common workflows.

An MCP client, such as VS Code or Cursor, connects to one or more servers and surfaces those capabilities to the model.

Old Workflow MCP Workflow
Paste a log into a chat The model calls a read_logs tool
Explain your repository The client provides workspace context
Paste command output The server returns structured tool output
Ask the model to guess state The model asks the system for current state
Manually perform every action The model proposes or invokes approved tool calls

MCP doesn't make a model omniscient. It gives the model a way to ask for context and use the actions you chose to expose. That matters more once you've lived through the manual context loop a few dozen times.

What Exactly Is an MCP Server?

An MCP server is a small program that speaks the protocol. It sits between an AI client and whatever system you actually care about.

Component Role
Client The AI application that hosts the conversation and decides when to call tools.
Server The program that exposes tools, resources, and prompts.
Protocol The JSON-RPC contract they use to talk.

MCP uses JSON-RPC 2.0. You won't hand-write this in normal use because the SDK handles the plumbing, but the shape of a tool call looks like this:

{
  "jsonrpc": "2.0",
  "id": 17,
  "method": "tools/call",
  "params": {
    "name": "read_logs",
    "arguments": {
      "path": "/var/log/app/service.log",
      "lines": 50
    }
  }
}

The server receives the request, runs the tool, and sends the content back to the client that feeds the model:

{
  "jsonrpc": "2.0",
  "id": 17,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "2026-06-26 ERROR connection refused on :5432"
      }
    ]
  }
}

Think of it as an API layer built for LLM clients instead of a human clicking buttons or a service calling REST. Tool descriptions, input schemas, output content, resources, prompts, lifecycle messages, transport rules: the stuff that makes the integration usable, not just possible.

Why Should You Care?

For anything the client doesn't already expose, context was manual labor:

  1. You decided what mattered.
  2. You copied it into the prompt.
  3. The model reasoned from that frozen snapshot.
  4. You ran whatever it suggested.
  5. You copied the results back.

That gets old. The model has no idea what changed after you pasted. It can't tell whether the test failed because the service is down, the token expired, or the environment variable only existed in the shell you had open ten minutes ago.

With MCP, the loop runs the other direction:

  1. The model sees available tools and resources.
  2. It asks for the context it needs.
  3. The client may ask you to approve sensitive tool calls, depending on configuration.
  4. The MCP server runs local commands or reads external systems.
  5. The model reasons from live evidence.

MCP is Context on Demand

MCP isn't just a plugin system. It's a way for agents to pull context when they need it instead of waiting for you to paste it.

Copy-paste makes you the integration layer. MCP moves the lookup work to the machine. You keep approval and judgment.

What you get out of it:

  • Less command output pasted into chat.
  • Answers based on current state, not whatever you remembered to include.
  • Repeated workflows that aren't one-off prompt archaeology.
  • Tool names, schemas, and descriptions you can actually review.
  • One server that works across multiple MCP-aware clients.

Once a model can call tools, those tools are real interfaces. Names matter. Descriptions matter. Input validation, timeouts, permissions, and logs matter.

Example Use Case: An MCP Server for VMware Fusion and Workstation

Whislt the patterns above are generic, we can dive deepers with amore direct example throughout the rest of the post. We'll use VMware Desktop Hypervisors because that's a concrete system outside the editor: inventory, power state, guest commands, and logs that do not arrive through workspace context alone.

MCP gets useful when it starts wrapping tools you already trust.

This isn't "AI replaces infrastructure engineers." It's "stop making engineers shuttle state between CLIs."

Use the established tool when reliability matters. Give the AI a handle narrow enough that you'd actually let it pull.

VMware Fusion and VMware Workstation ship vmcli, a command-line utility for managing local virtual machines. I've been building an MCP server around it for an open source project where the MCP server translates AI-facing tool calls into careful local subprocess calls, with Pydantic validation on the way in.

Disclaimer

This is not an official VMware by Broadcom document. This is a personal blog post.

The information is provided as-is with no warranties and confers no rights.

Please, refer to official documentation for the most up-to-date information.

The tool surface looks like this:

MCP Tool vmcli Module Typical Actions Purpose
vm_list (disk scan) n/a Discover .vmx files in configured folders
vm_discover_capabilities (help parse) n/a Live-discover modules from local --help
vm_power_management Power query, Start, Stop, Pause, Reset Power state and transitions
vm_snapshot_management Snapshot query, Take, Revert, Clone, Delete Snapshot lifecycle
vm_guest_operations Guest run, copyFrom, query, ps Guest commands and file transfer
vm_tools_management Tools Query, Install, Upgrade VMware Tools state
vm_mks_operations MKS captureScreenshot, query Display capture for debugging
vm_lifecycle VM Create Create a new VM from parameters

Power and Snapshot get dedicated handlers with extra argument builders. Everything else registers from a manifest: Chipset, Disk, Ethernet, HGFS, and the rest each become one module-scoped tool (for example vm_disk_operations) with an action field the model must set explicitly.

Architecture

sequenceDiagram
  participant User
  participant Client as LLM Client
  participant Server as MCP Server
  participant VMCLI as vmcli subprocess
  participant Hypervisor as VMware Fusion or Workstation

  User->>Client: Snapshot my lab and start ubuntu-lab-01
  Client->>Server: tools/call vm_snapshot_management
  Server->>VMCLI: vmcli ubuntu-lab-01.vmx Snapshot Take pre-change
  VMCLI->>Hypervisor: Create snapshot
  Hypervisor-->>VMCLI: Snapshot created
  VMCLI-->>Server: stdout and exit code
  Server-->>Client: CallToolResult
  Client->>Server: tools/call vm_power_management
  Server->>VMCLI: vmcli ubuntu-lab-01.vmx Power Start
  VMCLI->>Hypervisor: Start VM
  Hypervisor-->>VMCLI: VM started
  VMCLI-->>Server: stdout and exit code
  Server-->>Client: CallToolResult
  Client-->>User: Snapshot taken and VM started

Walk through without the diagram:

  1. You ask the AI client for an infrastructure task.
  2. The client picks an MCP tool and fills in parameters (action, vmx_path, and any extras).
  3. The server validates input through Pydantic models.
  4. resolve_vmx_path() maps an inventory id, display name, or absolute path to a real .vmx file.
  5. VmCliRunner executes vmcli as an async subprocess with a configurable timeout.
  6. vmcli talks to Fusion or Workstation.
  7. The server maps stdout, stderr, and exit code into a CallToolResult.
  8. The model explains what happened and, when the client requires it, asks before the next risky step.

MCP isn't magic. It's a boundary around tools you already trust.

What This Enables

A prompt like this starts to become realistic:

Quote

"Snapshot my ubuntu-2604-test VM, power it on, wait until it has an IP, SSH in, install the application code from my workspace branch, run the integration tests, and tell me what failed."

That's a chain, not one tool call:

Step Tool
List available virtual machines vm_list
Snapshot existing lab machines vm_snapshot_management with action: Take
Clone or start an Ubuntu lab VM vm_snapshot_management (Clone) or vm_power_management (Start)
Check VMware Tools state vm_tools_management with action: Query
Check SSH readiness vm_guest_operations with a narrow run allowlist
Deploy current workspace custom wrapper or Guest copyTo with fixed paths
Run tests vm_guest_operations run with one approved command
Collect logs vm_guest_operations copyFrom or run for a known path

The hypervisor-specific tools handle inventory, power, snapshots, guest ops, and Tools state. Higher-level wrappers like deploy-from-workspace are things you'd add once the basic server is boring and reliable. Keep that boundary clear so each tool stays auditable.

A narrow Guest run that only accepts one approved test command beats a general remote execution tool. A deploy helper that runs one known script beats letting the model assemble a deployment from half-read context.

For a local lab, the prompts are direct:

  • "Take a snapshot of ubuntu-2604-test before installing updated application code."
  • "Boot ubuntu-2604-test and tell me when SSH is ready."
  • "Deploy the latest application code from my workspace branch."
  • "Run the integration test inside ubuntu-2604-test and pull the service logs."

The value isn't that the model memorized vmcli syntax. It's that it can combine editor context, repo scripts, VM state, logs, and test output in one loop.

How MCP Servers Are Developed

Most MCP servers are programs with a protocol library and a transport.

Transports you'll run into first:

Transport Best For Notes
stdio Local tools The client launches the server as a subprocess and talks over stdin/stdout.
Streamable HTTP Local or remote services A single HTTP endpoint supports request/response and streaming behavior.
HTTP with SSE Existing or legacy servers Still around; Streamable HTTP is the direction most new work follows.

For local developer tooling, start with stdio. Simple, fast, keeps secrets on the machine. Your IDE starts the server when it needs it.

SDKs most people reach for first:

Language Package Fit
Python mcp Fast local tools, infrastructure wrappers, internal automation.
TypeScript @modelcontextprotocol/* packages Node, Bun, Deno, web service integrations, editor-adjacent tools.
Others Community SDKs Check protocol version and maintenance before you bet on them.

The loop:

  1. Pick a narrow capability.
  2. Wrap the real system with a function.
  3. Validate inputs.
  4. Return text or structured content.
  5. Test with the MCP Inspector.
  6. Wire it into your client.
  7. Add guardrails before you add mutating actions.

Inside the Server

The sections above explain how MCP servers are built in general. What follows walks through the vmcli server from the example.

The server in src/mcp_example/ follows that loop. Layout:

File Role
server.py FastMCP entry point, lifespan, logging
tools.py Tool registration
handlers.py Shared invoke logic for module tools
inventory.py .vmx discovery and alias resolution
vmcli.py CommandBuilder and async VmCliRunner
schemas.py Pydantic models per vmcli module
platform_detector.py Resolve vmcli binary path on macOS, Windows, Linux
config.py Environment-based settings
manifest.json Cached module/command metadata from local discovery

Entry Point and Shared Context

server.py resolves vmcli once at startup and shares a VmCliRunner across tool calls:

src/mcp_example/server.py
@asynccontextmanager
async def lifespan(_app: FastMCP) -> AsyncIterator[None]:
    """Resolve vmcli once and share VmCliRunner across tool invocations."""
    binary = resolve_vmcli()
    token = set_app_context(AppContext(runner=VmCliRunner(binary)))
    logger.info("Using vmcli at %s", binary)
    try:
        yield
    finally:
        reset_app_context(token)


mcp = FastMCP("mcp-example", lifespan=lifespan)

resolve_vmcli() checks VMCLI_PATH, then platform defaults (/Applications/VMware Fusion.app/Contents/Public/vmcli on macOS, Workstation paths on Windows and Linux), then PATH.

Inventory without Asking the Hypervisor

vm_list scans disk for .vmx files. It doesn't call vmcli for discovery:

inventory.py (trimmed)
src/mcp_example/inventory.py
@dataclass(frozen=True)
class VmEntry:
    id: str
    display_name: str
    vmx_path: str


def discover_vms(*, refresh: bool = False) -> list[VmEntry]:
    # walks VM_SEARCH_PATHS + platform default folders for *.vmx
    ...


def resolve_vmx_path(vmx_path: str) -> Path:
    """Resolve absolute .vmx path, or inventory id / display_name."""
    candidate = Path(vmx_path).expanduser()
    if candidate.is_file():
        return candidate.resolve()

    needle = vmx_path.strip().lower()
    for entry in discover_vms():
        if entry.id == vmx_path or entry.display_name.lower() == needle:
            return Path(entry.vmx_path)
    raise FileNotFoundError(...)

Each VM gets a stable 12-character id from the resolved path hash. The model can pass that id, the display name, or an absolute .vmx path into any tool that needs vmx_path.

Subprocess Execution

vmcli.py builds argv lists and runs them through asyncio.create_subprocess_exec with a timeout from VMCLI_TIMEOUT_SECONDS (default 120 seconds):

src/mcp_example/vmcli.py
class CommandBuilder:
    def build(self) -> list[str]:
        core: list[str] = []
        if self.vmx_path is not None and self.vmx_position == "first":
            core.append(str(self.vmx_path))
        core.extend([self.module, self.action])
        ...
        return core

Query actions automatically get -f json (or yaml/toml via VMCLI_OUTPUT_FORMAT).

Tool Registration

tools.py registers inventory tools plus module-grouped handlers. Power and Snapshot get extra argument builders for common flags:

src/mcp_example/tools.py
@mcp.tool()
async def vm_list() -> str:
    """List virtual machines discovered on disk (.vmx inventory)."""
    return await vm_list_text()


@mcp.tool()
async def vm_power_management(params: PowerParams) -> CallToolResult:
    """Power module: query, Start, Stop, Pause, Reset, Suspend, Unpause."""
    return await invoke_power(params)


@mcp.tool()
async def vm_snapshot_management(params: SnapshotParams) -> CallToolResult:
    """Snapshot module: query, Take, Delete, Revert, Clone."""
    return await invoke_snapshot(params)

PowerParams and SnapshotParams in schemas.py validate required fields before anything hits the subprocess. Snapshot Take accepts snapshot_name, description, and include_memory. Power Stop defaults to a soft shutdown via stop_op_type.

Configuration

Environment variables the server actually reads:

Variable Default Purpose
VMCLI_PATH platform Override path to vmcli binary
VM_SEARCH_PATHS (empty) Extra folders to scan for .vmx files
VMCLI_TIMEOUT_SECONDS 120 Subprocess timeout
VMCLI_OUTPUT_FORMAT json Format flag for query actions
LOG_LEVEL WARNING Server logging

Test with the MCP Inspector

Install the package (PyPI or editable from source), then:

npx -y @modelcontextprotocol/inspector mcp-example

From the inspector, call vm_list, pick an id from the JSON, then call vm_power_management:

{
  "action": "query",
  "vmx_path": "ubuntu-lab-01"
}

Debug one layer at a time. Inventory problems and vmcli problems show up in different places.

Adding Mutation Safely

Read-only path first: vm_list and vm_power_management with action: query. Once that works, vm_snapshot_management with action: Take is the first mutating tool I'd wire up. That's the moment to review approval behavior in your MCP client. Snapshots are reversible, but they still change local infrastructure.

IDE Integration

MCP is most useful when your AI client already knows your workspace.

Your editor knows the files you're touching. MCP reaches the systems outside the editor. The model sits between them and tries to connect the dots.

VS Code

GitHub Copilot in VS Code supports MCP through mcp.json. Put workspace config in .vscode/mcp.json if you want it in source control. For a personal setup, run MCP: Open User Configuration from the Command Palette.

If you're coming from Cursor, watch the key name. VS Code expects "servers", not "mcpServers":

.vscode/mcp.json
{
  "servers": {
    "mcp-example": {
      "type": "stdio",
      "command": "mcp-example"
    }
  }
}

The usual fields apply: command, args, env, and whatever auto-approval policy your client enforces.

Add one server, run MCP: List Servers, confirm the tools show up, send a read-only prompt, then add mutating tools one at a time.

Cursor

Cursor is a common MCP entry point right now because editor context, agent mode, and MCP config live in the same place. Good place to test local tooling.

Cursor reads MCP configuration from mcp.json. For a project-level setup:

.cursor/mcp.json

A system running a VMware desktop hypervisor might look like this. The same "mcpServers" shape works for any local stdio server; swap command and env for your tool:

.cursor/mcp.json
{
  "mcpServers": {
    "mcp-example": {
      "command": "mcp-example",
      "env": {
        "VMCLI_PATH": "/Applications/VMware Fusion.app/Contents/Public/vmcli"
      }
    }
  }
}
.cursor/mcp.json
{
  "mcpServers": {
    "mcp-example": {
      "command": "mcp-example",
      "env": {
        "VMCLI_PATH": "/usr/bin/vmcli"
      }
    }
  }
}
.cursor/mcp.json
{
  "mcpServers": {
    "mcp-example": {
      "command": "mcp-example",
      "env": {
        "VMCLI_PATH": "C:/Program Files (x86)/VMware/VMware Workstation/vmcli.exe"
      }
    }
  }
}

If vmcli is already on your default install path, you can omit VMCLI_PATH and let platform_detector.py find it.

After you add the file:

  1. Open Cursor settings.
  2. Go to the MCP settings area.
  3. Confirm the server is discovered.
  4. Check the available tools.
  5. Start with a read-only prompt.
Use the mcp-example MCP server to list my local
virtual machines and summarize their power state.
Don't make changes.

For infrastructure servers, keep mutating tools out of auto-approval. Require confirmation before a model snapshots, deletes, restarts, migrates, or deploys anything.

Don't Start with Broad Tools

Avoid tools named run_command, shell, or execute until you have a real permission model.

Start with narrow module tools: vm_list, vm_power_management with query, vm_snapshot_management with query, then Take and Revert. Specific tools are easier to review and harder for the model to misuse.

JetBrains

JetBrains AI Assistant supports MCP servers over stdio, Streamable HTTP, and legacy SSE. JetBrains also documents the inverse pattern where the IDE itself acts as an MCP server for external clients.

JetBrains IDEs often know things your terminal doesn't:

  • inspections
  • run configurations
  • VCS roots
  • project structure
  • database connections in DataGrip or IDEs with database tooling

If you live in JetBrains IDEs:

  1. Open AI Assistant settings.
  2. Add an MCP server.
  3. Choose stdio for a local server or Streamable HTTP for a hosted one.
  4. Review the tools the server exposes.
  5. Keep write-capable infrastructure tools behind confirmation.

Third-party JetBrains plugins need the same scrutiny as any other MCP client: transport support, configuration shape, logs, tool approval behavior. Check those before connecting anything sensitive.

Security Rules That Will Save You Pain

MCP gives models access to tools. Tools do work. Work can break things. Treat the server as an automation boundary, not a chat feature.

Rules I apply consistently:

  • Read-only tools first.
  • Mutating tools get explicit names and narrow arguments.
  • Validate every input.
  • Clamp counts, time ranges, and output sizes.
  • Set subprocess timeouts.
  • Return stderr when commands fail.
  • Secrets in environment variables or a secret manager, not in tool args.
  • Limit server scope to the systems it actually needs.
  • Human approval for destructive actions.
  • Log tool calls when the server touches infrastructure.

For infrastructure, split tools by risk before you expand the interface:

Risk Examples Approval posture
Read-only vm_list, power query, Tools query Usually safe to allow.
Reversible mutation start/stop VM, snapshot take/revert Ask first.
Destructive mutation delete VM, delete snapshot, wipe disk Require explicit confirmation or don't expose.
Broad execution unrestricted local command, unrestricted Guest run Avoid unless sandboxed and audited.

The model isn't the security boundary. Your server is.

How to Get Started Right Now

Fastest useful path:

  1. Pick one workflow where you keep pasting command output into an AI chat.
  2. Wrap the read-only commands behind MCP tools.
  3. Test with the MCP Inspector.
  4. Connect the server to Cursor, VS Code, Claude Desktop, Claude Code, or JetBrains AI Assistant.
  5. Add write-capable tools only after the read-only flow is stable.

A first server doesn't need to impress anyone. Pick one workflow you keep explaining by hand:

  • Read-only log tail for a known path on disk.
  • CI run status checker for one repository or pipeline.
  • VMware local lab inventory server.
  • VMware snapshot helper for a fixed set of VMs.
  • VMware guest readiness checker that waits for Tools, an IP, and SSH or WinRM.

Remove one annoying context handoff from your day. Then remove another.

MCP gives AI a path into the working environment without making every developer serve as the context shuttle. Build the small server first. Keep the tools narrow. Make the model prove what it sees before you allow mutating actions.

References