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:
- You decided what mattered.
- You copied it into the prompt.
- The model reasoned from that frozen snapshot.
- You ran whatever it suggested.
- 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:
- The model sees available tools and resources.
- It asks for the context it needs.
- The client may ask you to approve sensitive tool calls, depending on configuration.
- The MCP server runs local commands or reads external systems.
- 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:
- You ask the AI client for an infrastructure task.
- The client picks an MCP tool and fills in parameters (
action,vmx_path, and any extras). - The server validates input through Pydantic models.
resolve_vmx_path()maps an inventory id, display name, or absolute path to a real.vmxfile.VmCliRunnerexecutesvmclias an async subprocess with a configurable timeout.vmclitalks to Fusion or Workstation.- The server maps stdout, stderr, and exit code into a
CallToolResult. - 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-testbefore installing updated application code." - "Boot
ubuntu-2604-testand tell me when SSH is ready." - "Deploy the latest application code from my workspace branch."
- "Run the integration test inside
ubuntu-2604-testand 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:
- Pick a narrow capability.
- Wrap the real system with a function.
- Validate inputs.
- Return text or structured content.
- Test with the MCP Inspector.
- Wire it into your client.
- 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:
@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)
@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):
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:
@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:
From the inspector, call vm_list, pick an id from the JSON, then call vm_power_management:
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":
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:
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:
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:
- Open Cursor settings.
- Go to the MCP settings area.
- Confirm the server is discovered.
- Check the available tools.
- 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:
- Open AI Assistant settings.
- Add an MCP server.
- Choose
stdiofor a local server or Streamable HTTP for a hosted one. - Review the tools the server exposes.
- 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:
- Pick one workflow where you keep pasting command output into an AI chat.
- Wrap the read-only commands behind MCP tools.
- Test with the MCP Inspector.
- Connect the server to Cursor, VS Code, Claude Desktop, Claude Code, or JetBrains AI Assistant.
- 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¶
- Model Context Protocol Documentation
- MCP Specification
- MCP Python SDK
- MCP TypeScript SDK
- VS Code MCP Documentation
- Cursor MCP Documentation
- JetBrains AI Assistant MCP Documentation