Skip to content

Commit 27fb485

Browse files
authored
Add files via upload
1 parent eeaf45c commit 27fb485

File tree

3 files changed

+477
-119
lines changed

3 files changed

+477
-119
lines changed

vsphere_mcp_pro/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
vSphere-MCP-Pro: A secure MCP server for VMware vCenter 8.0+.
3+
4+
This package provides a Model Context Protocol (MCP) server for VMware vCenter
5+
operations including VM lifecycle management, snapshots, and inventory discovery.
6+
"""
7+
8+
from .vsphere_client import VsphereApiError, VsphereClient, VsphereClientPool
9+
from .config import AppConfig, load_config
10+
from .authz import Authorizer, TokenBucketLimiter
11+
from .audit import Auditor, AuditEvent
12+
13+
__version__ = "0.2.0"
14+
15+
__all__ = [
16+
# Client
17+
"VsphereClient",
18+
"VsphereClientPool",
19+
"VsphereApiError",
20+
# Config
21+
"AppConfig",
22+
"load_config",
23+
# Auth
24+
"Authorizer",
25+
"TokenBucketLimiter",
26+
# Audit
27+
"Auditor",
28+
"AuditEvent",
29+
]

vsphere_mcp_pro/server.py

Lines changed: 120 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,7 @@
1010
from .audit import AuditEvent, Auditor
1111
from .authz import Authorizer, CallerContext, TokenBucketLimiter, set_caller
1212
from .config import AppConfig, load_config
13-
from .vsphere_client import VsphereClient
14-
15-
16-
def _choose_host(cfg: AppConfig, hostname: Optional[str]) -> str:
17-
host = (hostname or cfg.vsphere.host).strip()
18-
if cfg.vsphere.allowed_hosts and host not in cfg.vsphere.allowed_hosts:
19-
raise PermissionError(f"Hostname '{host}' not in allowed set")
20-
return host
13+
from .vsphere_client import VsphereClientPool
2114

2215

2316
def _with_guard(tool_name: str, destructive: bool = False):
@@ -28,6 +21,7 @@ def wrapper(*args, **kwargs):
2821
auditor: Auditor = kwargs.pop("_auditor")
2922
limiter: TokenBucketLimiter = kwargs.pop("_limiter")
3023
authz: Authorizer = kwargs.pop("_authz")
24+
pool: VsphereClientPool = kwargs.pop("_pool")
3125

3226
token = kwargs.pop("token", None)
3327
confirm = kwargs.get("confirm", False)
@@ -50,7 +44,7 @@ def wrapper(*args, **kwargs):
5044
if destructive and not confirm:
5145
raise PermissionError("Destructive operation: set confirm=True to proceed")
5246

53-
result = fn(*args, **kwargs)
47+
result = fn(*args, _pool=pool, **kwargs)
5448
ok = True
5549
if isinstance(result, dict) and "meta" in result and "host" in result["meta"]:
5650
host_used = result["meta"]["host"]
@@ -73,128 +67,159 @@ def build_server(cfg: AppConfig) -> FastMCP:
7367
auditor = Auditor(cfg.server.audit_log_path)
7468
authz = Authorizer(cfg.auth)
7569
limiter = TokenBucketLimiter(cfg.ratelimit)
76-
77-
def inject(fn, name: str, destructive: bool = False):
78-
wrapped = _with_guard(name, destructive)(fn)
79-
mcp.tool(name=name)(functools.partial(wrapped, _cfg=cfg, _auditor=auditor, _limiter=limiter, _authz=authz))
80-
81-
def client_for(hostname: Optional[str]) -> VsphereClient:
82-
host = _choose_host(cfg, hostname)
83-
local_cfg = cfg.model_copy(deep=True)
84-
local_cfg.vsphere.host = host
85-
c = VsphereClient(local_cfg.vsphere)
86-
c.login()
87-
return c
88-
89-
@inject
90-
def list_vms(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
91-
c = client_for(hostname)
70+
pool = VsphereClientPool(cfg)
71+
72+
def inject(name: str, destructive: bool = False):
73+
"""Decorator factory that wraps a tool with auth, rate limiting, and auditing."""
74+
def decorator(fn):
75+
wrapped = _with_guard(name, destructive)(fn)
76+
bound = functools.partial(
77+
wrapped,
78+
_cfg=cfg,
79+
_auditor=auditor,
80+
_limiter=limiter,
81+
_authz=authz,
82+
_pool=pool,
83+
)
84+
mcp.tool(name=name)(bound)
85+
return fn
86+
return decorator
87+
88+
# --- VM Discovery ---
89+
90+
@inject("list_vms")
91+
def list_vms(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
92+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
93+
c = _pool.get(hostname)
9294
vms = c.list_vms()
93-
return {"ok": True, "meta": {"host": c._cfg.host}, "count": len(vms), "vms": vms}
95+
return {"ok": True, "meta": {"host": c.host}, "count": len(vms), "vms": vms}
9496

95-
@inject
96-
def get_vm_details(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
97-
c = client_for(hostname)
97+
@inject("get_vm_details")
98+
def get_vm_details(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
99+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
100+
c = _pool.get(hostname)
98101
vm = c.get_vm(vm_id)
99-
return {"ok": True, "meta": {"host": c._cfg.host}, "vm": vm}
102+
return {"ok": True, "meta": {"host": c.host}, "vm": vm}
103+
104+
# --- Inventory Discovery ---
100105

101-
@inject
102-
def list_hosts(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
103-
c = client_for(hostname)
106+
@inject("list_hosts")
107+
def list_hosts(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
108+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
109+
c = _pool.get(hostname)
104110
data = c.list_hosts()
105-
return {"ok": True, "meta": {"host": c._cfg.host}, "count": len(data), "hosts": data}
111+
return {"ok": True, "meta": {"host": c.host}, "count": len(data), "hosts": data}
106112

107-
@inject
108-
def list_datastores(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
109-
c = client_for(hostname)
113+
@inject("list_datastores")
114+
def list_datastores(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
115+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
116+
c = _pool.get(hostname)
110117
data = c.list_datastores()
111-
return {"ok": True, "meta": {"host": c._cfg.host}, "count": len(data), "datastores": data}
118+
return {"ok": True, "meta": {"host": c.host}, "count": len(data), "datastores": data}
112119

113-
@inject
114-
def list_networks(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
115-
c = client_for(hostname)
120+
@inject("list_networks")
121+
def list_networks(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
122+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
123+
c = _pool.get(hostname)
116124
data = c.list_networks()
117-
return {"ok": True, "meta": {"host": c._cfg.host}, "count": len(data), "networks": data}
125+
return {"ok": True, "meta": {"host": c.host}, "count": len(data), "networks": data}
118126

119-
@inject
120-
def list_datacenters(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
121-
c = client_for(hostname)
127+
@inject("list_datacenters")
128+
def list_datacenters(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
129+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
130+
c = _pool.get(hostname)
122131
data = c.list_datacenters()
123-
return {"ok": True, "meta": {"host": c._cfg.host}, "count": len(data), "datacenters": data}
132+
return {"ok": True, "meta": {"host": c.host}, "count": len(data), "datacenters": data}
133+
134+
@inject("get_datastore_usage")
135+
def get_datastore_usage(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
136+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
137+
c = _pool.get(hostname)
138+
dss = c.list_datastores()
139+
return {"ok": True, "meta": {"host": c.host}, "count": len(dss), "datastores": dss}
140+
141+
@inject("get_resource_utilization_summary")
142+
def get_resource_utilization_summary(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
143+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
144+
c = _pool.get(hostname)
145+
return {"ok": True, "meta": {"host": c.host}, "summary": {
146+
"vms": len(c.list_vms()),
147+
"hosts": len(c.list_hosts()),
148+
"datastores": len(c.list_datastores()),
149+
"networks": len(c.list_networks()),
150+
"datacenters": len(c.list_datacenters()),
151+
}}
124152

125-
@inject
126-
def power_on_vm(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
127-
c = client_for(hostname)
153+
# --- Power Operations ---
154+
155+
@inject("power_on_vm")
156+
def power_on_vm(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
157+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
158+
c = _pool.get(hostname)
128159
data = c.power_start(vm_id)
129-
return {"ok": True, "meta": {"host": c._cfg.host}, "result": data}
160+
return {"ok": True, "meta": {"host": c.host}, "result": data}
130161

131-
@inject
132-
def power_off_vm(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
133-
c = client_for(hostname)
162+
@inject("power_off_vm")
163+
def power_off_vm(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
164+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
165+
c = _pool.get(hostname)
134166
data = c.power_stop(vm_id)
135-
return {"ok": True, "meta": {"host": c._cfg.host}, "result": data}
167+
return {"ok": True, "meta": {"host": c.host}, "result": data}
136168

137-
@inject
138-
def restart_vm(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
139-
c = client_for(hostname)
169+
@inject("restart_vm")
170+
def restart_vm(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
171+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
172+
c = _pool.get(hostname)
140173
data = c.power_reset(vm_id)
141-
return {"ok": True, "meta": {"host": c._cfg.host}, "result": data}
174+
return {"ok": True, "meta": {"host": c.host}, "result": data}
175+
176+
# --- Snapshot Operations ---
142177

143-
@inject
144-
def list_vm_snapshots(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
145-
c = client_for(hostname)
178+
@inject("list_vm_snapshots")
179+
def list_vm_snapshots(vm_id: str, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
180+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
181+
c = _pool.get(hostname)
146182
data = c.list_snapshots(vm_id)
147-
return {"ok": True, "meta": {"host": c._cfg.host}, "count": (len(data) if isinstance(data, list) else None), "snapshots": data}
183+
return {"ok": True, "meta": {"host": c.host}, "count": (len(data) if isinstance(data, list) else None), "snapshots": data}
148184

149-
@inject
185+
@inject("create_vm_snapshot")
150186
def create_vm_snapshot(vm_id: str, snapshot_name: str, description: str = "", memory: bool = False, quiesce: bool = False,
151-
hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
152-
c = client_for(hostname)
187+
hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
188+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
189+
c = _pool.get(hostname)
153190
data = c.create_snapshot(vm_id, snapshot_name, description=description, memory=memory, quiesce=quiesce)
154-
return {"ok": True, "meta": {"host": c._cfg.host}, "result": data}
191+
return {"ok": True, "meta": {"host": c.host}, "result": data}
155192

156-
@inject
193+
@inject("delete_vm_snapshot", destructive=True)
157194
def delete_vm_snapshot(vm_id: str, snapshot_id: str, confirm: bool = False,
158-
hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
159-
c = client_for(hostname)
195+
hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
196+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
197+
c = _pool.get(hostname)
160198
data = c.delete_snapshot(vm_id, snapshot_id)
161-
return {"ok": True, "meta": {"host": c._cfg.host}, "result": data}
199+
return {"ok": True, "meta": {"host": c.host}, "result": data}
200+
201+
# --- Destructive Operations (require confirm=True) ---
162202

163-
@inject
164-
def delete_vm(vm_id: str, confirm: bool = False, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
165-
c = client_for(hostname)
203+
@inject("delete_vm", destructive=True)
204+
def delete_vm(vm_id: str, confirm: bool = False, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
205+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
206+
c = _pool.get(hostname)
166207
data = c.delete_vm(vm_id)
167-
return {"ok": True, "meta": {"host": c._cfg.host}, "result": data}
208+
return {"ok": True, "meta": {"host": c.host}, "result": data}
168209

169-
@inject
210+
@inject("modify_vm_resources", destructive=True)
170211
def modify_vm_resources(vm_id: str, cpu_count: Optional[int] = None, memory_gb: Optional[int] = None,
171-
confirm: bool = False, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
212+
confirm: bool = False, hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None,
213+
*, _pool: VsphereClientPool) -> Dict[str, Any]:
172214
if cpu_count is None and memory_gb is None:
173215
raise ValueError("cpu_count or memory_gb required")
174-
c = client_for(hostname)
216+
c = _pool.get(hostname)
175217
res = {}
176218
if cpu_count is not None:
177219
res["cpu"] = c.set_cpu(vm_id, cpu_count)
178220
if memory_gb is not None:
179221
res["memory"] = c.set_memory(vm_id, int(memory_gb * 1024))
180-
return {"ok": True, "meta": {"host": c._cfg.host}, "result": res}
181-
182-
@inject
183-
def get_datastore_usage(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
184-
c = client_for(hostname)
185-
dss = c.list_datastores()
186-
return {"ok": True, "meta": {"host": c._cfg.host}, "count": len(dss), "datastores": dss}
187-
188-
@inject
189-
def get_resource_utilization_summary(hostname: Optional[str] = None, verbose: bool = False, token: Optional[str] = None) -> Dict[str, Any]:
190-
c = client_for(hostname)
191-
return {"ok": True, "meta": {"host": c._cfg.host}, "summary": {
192-
"vms": len(c.list_vms()),
193-
"hosts": len(c.list_hosts()),
194-
"datastores": len(c.list_datastores()),
195-
"networks": len(c.list_networks()),
196-
"datacenters": len(c.list_datacenters()),
197-
}}
222+
return {"ok": True, "meta": {"host": c.host}, "result": res}
198223

199224
return mcp
200225

0 commit comments

Comments
 (0)