Add standalone MCP server for Claude Desktop integration
- Create dedicated MCP server with Nomad job management tools - Add Claude Desktop configuration for MCP server connection - Update requirements with mcp dependency for standalone server 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
12
claude_desktop_config.json
Normal file
12
claude_desktop_config.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"nomad-mcp": {
|
||||||
|
"command": "python",
|
||||||
|
"args": ["/Users/nkohl/Documents/Code/nomad_mcp/mcp_server.py"],
|
||||||
|
"env": {
|
||||||
|
"NOMAD_ADDR": "http://localhost:4646",
|
||||||
|
"PYTHONPATH": "/Users/nkohl/Documents/Code/nomad_mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
432
mcp_server.py
Normal file
432
mcp_server.py
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Nomad MCP Server for Claude Desktop App
|
||||||
|
Provides MCP tools for managing HashiCorp Nomad jobs
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from mcp.server import NotificationOptions, Server
|
||||||
|
from mcp.server.models import InitializationOptions
|
||||||
|
import mcp.server.stdio
|
||||||
|
import mcp.types as types
|
||||||
|
|
||||||
|
from app.services.nomad_client import NomadService
|
||||||
|
from app.schemas.claude_api import ClaudeJobSpecification
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("nomad-mcp")
|
||||||
|
|
||||||
|
# Create the server instance
|
||||||
|
server = Server("nomad-mcp")
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def handle_list_tools() -> List[types.Tool]:
|
||||||
|
"""List available tools for Nomad management."""
|
||||||
|
return [
|
||||||
|
types.Tool(
|
||||||
|
name="list_nomad_jobs",
|
||||||
|
description="List all Nomad jobs in a namespace",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nomad namespace",
|
||||||
|
"default": "development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="get_job_status",
|
||||||
|
description="Get the status of a specific Nomad job",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"job_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the job to check"
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nomad namespace",
|
||||||
|
"default": "development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["job_id"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="stop_job",
|
||||||
|
description="Stop a running Nomad job",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"job_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the job to stop"
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nomad namespace",
|
||||||
|
"default": "development"
|
||||||
|
},
|
||||||
|
"purge": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to purge the job",
|
||||||
|
"default": False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["job_id"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="restart_job",
|
||||||
|
description="Restart a Nomad job",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"job_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the job to restart"
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nomad namespace",
|
||||||
|
"default": "development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["job_id"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="create_job",
|
||||||
|
description="Create a new Nomad job",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"job_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique ID for the job"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display name for the job"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Job type (service, batch, etc.)",
|
||||||
|
"default": "service"
|
||||||
|
},
|
||||||
|
"datacenters": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "List of datacenters to run the job in",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"default": ["jm"]
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nomad namespace",
|
||||||
|
"default": "development"
|
||||||
|
},
|
||||||
|
"docker_image": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Docker image to run"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of instances to run",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"cpu": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "CPU allocation in MHz",
|
||||||
|
"default": 100
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Memory allocation in MB",
|
||||||
|
"default": 128
|
||||||
|
},
|
||||||
|
"ports": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Port mappings",
|
||||||
|
"items": {"type": "object"},
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
"env_vars": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Environment variables for the container",
|
||||||
|
"default": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["job_id", "name", "docker_image"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="get_job_logs",
|
||||||
|
description="Get logs for a Nomad job",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"job_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the job to get logs for"
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nomad namespace",
|
||||||
|
"default": "development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["job_id"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextContent]:
|
||||||
|
"""Handle tool calls from Claude."""
|
||||||
|
try:
|
||||||
|
# Create Nomad service instance
|
||||||
|
nomad_service = NomadService()
|
||||||
|
namespace = arguments.get("namespace", "development")
|
||||||
|
nomad_service.namespace = namespace
|
||||||
|
|
||||||
|
if name == "list_nomad_jobs":
|
||||||
|
jobs = nomad_service.list_jobs()
|
||||||
|
simplified_jobs = []
|
||||||
|
for job in jobs:
|
||||||
|
simplified_jobs.append({
|
||||||
|
"id": job.get("ID"),
|
||||||
|
"name": job.get("Name"),
|
||||||
|
"status": job.get("Status"),
|
||||||
|
"type": job.get("Type"),
|
||||||
|
"namespace": namespace
|
||||||
|
})
|
||||||
|
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(simplified_jobs, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
elif name == "get_job_status":
|
||||||
|
job_id = arguments.get("job_id")
|
||||||
|
if not job_id:
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text="Error: job_id is required"
|
||||||
|
)]
|
||||||
|
|
||||||
|
job = nomad_service.get_job(job_id)
|
||||||
|
allocations = nomad_service.get_allocations(job_id)
|
||||||
|
|
||||||
|
latest_alloc = None
|
||||||
|
if allocations:
|
||||||
|
sorted_allocations = sorted(
|
||||||
|
allocations,
|
||||||
|
key=lambda a: a.get("CreateTime", 0),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
latest_alloc = sorted_allocations[0]
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"job_id": job_id,
|
||||||
|
"status": job.get("Status", "unknown"),
|
||||||
|
"message": f"Job {job_id} is {job.get('Status', 'unknown')}",
|
||||||
|
"details": {
|
||||||
|
"job": job,
|
||||||
|
"latest_allocation": latest_alloc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(result, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
elif name == "stop_job":
|
||||||
|
job_id = arguments.get("job_id")
|
||||||
|
purge = arguments.get("purge", False)
|
||||||
|
|
||||||
|
if not job_id:
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text="Error: job_id is required"
|
||||||
|
)]
|
||||||
|
|
||||||
|
result = nomad_service.stop_job(job_id, purge=purge)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"success": True,
|
||||||
|
"job_id": job_id,
|
||||||
|
"status": "stopped",
|
||||||
|
"message": f"Job {job_id} has been stopped" + (" and purged" if purge else ""),
|
||||||
|
"details": result
|
||||||
|
}
|
||||||
|
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(response, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
elif name == "restart_job":
|
||||||
|
job_id = arguments.get("job_id")
|
||||||
|
|
||||||
|
if not job_id:
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text="Error: job_id is required"
|
||||||
|
)]
|
||||||
|
|
||||||
|
# Get current job spec
|
||||||
|
job_spec = nomad_service.get_job(job_id)
|
||||||
|
|
||||||
|
# Stop and restart
|
||||||
|
nomad_service.stop_job(job_id)
|
||||||
|
result = nomad_service.start_job(job_spec)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"success": True,
|
||||||
|
"job_id": job_id,
|
||||||
|
"status": "restarted",
|
||||||
|
"message": f"Job {job_id} has been restarted",
|
||||||
|
"details": result
|
||||||
|
}
|
||||||
|
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(response, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
elif name == "create_job":
|
||||||
|
# Validate required arguments
|
||||||
|
required_args = ["job_id", "name", "docker_image"]
|
||||||
|
for arg in required_args:
|
||||||
|
if not arguments.get(arg):
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Error: {arg} is required"
|
||||||
|
)]
|
||||||
|
|
||||||
|
# Create job specification
|
||||||
|
job_spec = ClaudeJobSpecification(**arguments)
|
||||||
|
|
||||||
|
# Set namespace
|
||||||
|
if job_spec.namespace:
|
||||||
|
nomad_service.namespace = job_spec.namespace
|
||||||
|
|
||||||
|
# Convert to Nomad format and start
|
||||||
|
nomad_job_spec = job_spec.to_nomad_job_spec()
|
||||||
|
result = nomad_service.start_job(nomad_job_spec)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"success": True,
|
||||||
|
"job_id": job_spec.job_id,
|
||||||
|
"status": "started",
|
||||||
|
"message": f"Job {job_spec.job_id} has been created and started",
|
||||||
|
"details": result
|
||||||
|
}
|
||||||
|
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(response, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
elif name == "get_job_logs":
|
||||||
|
job_id = arguments.get("job_id")
|
||||||
|
|
||||||
|
if not job_id:
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text="Error: job_id is required"
|
||||||
|
)]
|
||||||
|
|
||||||
|
# Get allocations
|
||||||
|
allocations = nomad_service.get_allocations(job_id)
|
||||||
|
if not allocations:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"job_id": job_id,
|
||||||
|
"message": f"No allocations found for job {job_id}",
|
||||||
|
"logs": None
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Get latest allocation
|
||||||
|
sorted_allocations = sorted(
|
||||||
|
allocations,
|
||||||
|
key=lambda a: a.get("CreateTime", 0),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
latest_alloc = sorted_allocations[0]
|
||||||
|
alloc_id = latest_alloc.get("ID")
|
||||||
|
|
||||||
|
# Get task name
|
||||||
|
task_name = None
|
||||||
|
if "TaskStates" in latest_alloc:
|
||||||
|
task_states = latest_alloc["TaskStates"]
|
||||||
|
if task_states:
|
||||||
|
task_name = next(iter(task_states.keys()))
|
||||||
|
|
||||||
|
if not task_name:
|
||||||
|
task_name = "app" # Default task name
|
||||||
|
|
||||||
|
# Get logs
|
||||||
|
stdout_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stdout")
|
||||||
|
stderr_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stderr")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"job_id": job_id,
|
||||||
|
"allocation_id": alloc_id,
|
||||||
|
"task_name": task_name,
|
||||||
|
"message": f"Retrieved logs for job {job_id}",
|
||||||
|
"logs": {
|
||||||
|
"stdout": stdout_logs,
|
||||||
|
"stderr": stderr_logs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(result, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
else:
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Error: Unknown tool '{name}'"
|
||||||
|
)]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in tool '{name}': {str(e)}")
|
||||||
|
return [types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Error: {str(e)}"
|
||||||
|
)]
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main entry point for the MCP server."""
|
||||||
|
# Run the server using stdio transport
|
||||||
|
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(
|
||||||
|
read_stream,
|
||||||
|
write_stream,
|
||||||
|
InitializationOptions(
|
||||||
|
server_name="nomad-mcp",
|
||||||
|
server_version="1.0.0",
|
||||||
|
capabilities=server.get_capabilities(
|
||||||
|
notification_options=NotificationOptions(),
|
||||||
|
experimental_capabilities={},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
@ -7,4 +7,5 @@ httpx
|
|||||||
python-multipart
|
python-multipart
|
||||||
pyyaml
|
pyyaml
|
||||||
requests
|
requests
|
||||||
fastapi_mcp
|
fastapi_mcp
|
||||||
|
mcp
|
Reference in New Issue
Block a user