Add Claude Code integration with SSE support

- Add Server-Sent Events (SSE) endpoint for Claude Code MCP integration
- Create MCP configuration for Claude Code CLI
- Update tool configuration to support modern OpenAPI format
- Add documentation for Claude Code integration options
- Create CLAUDE.md guide for AI coding agents

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-03-29 12:52:21 +07:00
parent afae299e9c
commit 403fa50b4f
5 changed files with 1188 additions and 548 deletions

27
CLAUDE.md Normal file
View File

@ -0,0 +1,27 @@
# CLAUDE.md - Guide for AI Coding Agents
## Project Overview
Nomad MCP is a service that enables management of HashiCorp Nomad jobs via REST API, with Claude AI integration.
## Commands
- **Run server**: `uvicorn app.main:app --reload --host 0.0.0.0 --port 8000`
- **Tests**: `pytest` (all) or `pytest tests/test_nomad_service.py::test_job_lifecycle` (single)
- **Build docker**: `docker build -t nomad-mcp .`
- **Run docker**: `docker-compose up -d`
## Code Style
- **Imports**: Standard library → Third-party → Local modules (alphabetically)
- **Type annotations**: Required for all function parameters and returns
- **Error handling**: Use try/except with proper logging and HTTP exceptions
- **Logging**: Use Python's logging module with appropriate levels
- **API responses**: Return consistent JSON structures with Pydantic models
- **Docstrings**: Required for all functions and classes
- **Variables**: snake_case for variables, CamelCase for classes
## Structure
- `/app`: Main code (/routers, /schemas, /services)
- `/configs`: Configuration files
- `/static`: Frontend assets
- `/tests`: Test files
Always maintain backward compatibility with existing API endpoints. Follow REST principles.

View File

@ -1,249 +1,295 @@
# Claude Integration with Nomad MCP # Claude Integration with Nomad MCP
This document explains how to configure Claude to connect to the Nomad MCP service and manage jobs. This document explains how to configure Claude to connect to the Nomad MCP service and manage jobs.
## Overview ## Overview
The Nomad MCP service provides a simplified REST API specifically designed for Claude to interact with Nomad jobs. This API allows Claude to: The Nomad MCP service provides a simplified REST API specifically designed for Claude to interact with Nomad jobs. This API allows Claude to:
1. List all jobs in a namespace 1. List all jobs in a namespace
2. Get the status of a specific job 2. Get the status of a specific job
3. Start, stop, and restart jobs 3. Start, stop, and restart jobs
4. Create new jobs with a simplified specification 4. Create new jobs with a simplified specification
5. Retrieve logs from jobs 5. Retrieve logs from jobs
## API Endpoints ## API Endpoints
The Claude-specific API is available at the `/api/claude` prefix. The following endpoints are available: The Claude-specific API is available at the `/api/claude` prefix. The following endpoints are available:
### List Jobs ### List Jobs
``` ```
GET /api/claude/list-jobs?namespace=development GET /api/claude/list-jobs?namespace=development
``` ```
Returns a list of all jobs in the specified namespace with their IDs, names, statuses, and types. Returns a list of all jobs in the specified namespace with their IDs, names, statuses, and types.
### Manage Jobs ### Manage Jobs
``` ```
POST /api/claude/jobs POST /api/claude/jobs
``` ```
Manages existing jobs with operations like status check, stop, and restart. Manages existing jobs with operations like status check, stop, and restart.
Request body: Request body:
```json ```json
{ {
"job_id": "example-job", "job_id": "example-job",
"action": "status|stop|restart", "action": "status|stop|restart",
"namespace": "development", "namespace": "development",
"purge": false "purge": false
} }
``` ```
### Create Jobs ### Create Jobs
``` ```
POST /api/claude/create-job POST /api/claude/create-job
``` ```
Creates a new job with a simplified specification. Creates a new job with a simplified specification.
Request body: Request body:
```json ```json
{ {
"job_id": "example-job", "job_id": "example-job",
"name": "Example Job", "name": "Example Job",
"type": "service", "type": "service",
"datacenters": ["jm"], "datacenters": ["jm"],
"namespace": "development", "namespace": "development",
"docker_image": "nginx:latest", "docker_image": "nginx:latest",
"count": 1, "count": 1,
"cpu": 100, "cpu": 100,
"memory": 128, "memory": 128,
"ports": [ "ports": [
{ {
"Label": "http", "Label": "http",
"Value": 0, "Value": 0,
"To": 80 "To": 80
} }
], ],
"env_vars": { "env_vars": {
"ENV_VAR1": "value1", "ENV_VAR1": "value1",
"ENV_VAR2": "value2" "ENV_VAR2": "value2"
} }
} }
``` ```
### Get Job Logs ### Get Job Logs
``` ```
GET /api/claude/job-logs/{job_id}?namespace=development GET /api/claude/job-logs/{job_id}?namespace=development
``` ```
Retrieves logs from the latest allocation of the specified job. Retrieves logs from the latest allocation of the specified job.
## Configuring Claude Desktop Application ## Claude Integration Options
To configure Claude to connect to the Nomad MCP service, follow these steps: There are three main ways to integrate Claude with the Nomad MCP service:
### 1. Set Up API Access ### Option 1: Using Claude Desktop/Web UI Tool Configuration
Claude needs to be configured with the base URL of your Nomad MCP service. This is typically: Use this approach for Claude Web or Desktop applications with the tool configuration feature.
``` #### 1. Set Up API Access
http://your-server-address:8000
``` Configure the base URL of your Nomad MCP service:
```
### 2. Create a Claude Tool Configuration http://your-server-address:8000
```
In the Claude desktop application, you can create a custom tool configuration that allows Claude to interact with the Nomad MCP API. Here's a sample configuration:
#### 2. Create a Claude Tool Configuration
```json
{ Create a tool configuration file (see sample in `claude_nomad_tool.json`):
"tools": [
{ ```json
"name": "nomad_mcp", {
"description": "Manage Nomad jobs through the MCP service", "tools": [
"api_endpoints": [ {
{ "name": "nomad_mcp",
"name": "list_jobs", "description": "Manage Nomad jobs through the MCP service",
"description": "List all jobs in a namespace", "api_endpoints": [
"method": "GET", {
"url": "http://your-server-address:8000/api/claude/list-jobs", "name": "list_jobs",
"params": [ "description": "List all jobs in a namespace",
{ "method": "GET",
"name": "namespace", "url": "http://your-server-address:8000/api/claude/list-jobs",
"type": "string", "params": [
"description": "Nomad namespace", {
"required": false, "name": "namespace",
"default": "development" "type": "string",
} "description": "Nomad namespace",
] "required": false,
}, "default": "development"
{ }
"name": "manage_job", ]
"description": "Manage a job (status, stop, restart)", },
"method": "POST", // Other endpoints...
"url": "http://your-server-address:8000/api/claude/jobs", ]
"body": { }
"job_id": "string", ]
"action": "string", }
"namespace": "string", ```
"purge": "boolean"
} #### 3. Import the Tool Configuration
},
{ 1. Open the Claude web or desktop application
"name": "create_job", 2. Go to Settings > Tools
"description": "Create a new job", 3. Click "Import Tool Configuration"
"method": "POST", 4. Select the JSON file with the configuration
"url": "http://your-server-address:8000/api/claude/create-job", 5. Click "Save"
"body": {
"job_id": "string", ### Option 2: Using Claude Code CLI with OpenAPI Tool
"name": "string",
"type": "string", For Claude Code CLI integration with the OpenAPI tool approach:
"datacenters": "array",
"namespace": "string", #### 1. Install Claude Code CLI
"docker_image": "string",
"count": "integer", ```bash
"cpu": "integer", npm install -g @anthropic-ai/claude-code
"memory": "integer", ```
"ports": "array",
"env_vars": "object" #### 2. Create an OpenAPI Specification File
}
}, Use the updated `claude_nomad_tool.json` file which follows the OpenAPI specification format:
{
"name": "get_job_logs", ```json
"description": "Get logs for a job", {
"method": "GET", "schema_version": "v1",
"url": "http://your-server-address:8000/api/claude/job-logs/{job_id}", "name": "nomad_mcp",
"params": [ "description": "Manage Nomad jobs through the MCP service",
{ "authentication": {
"name": "namespace", "type": "none"
"type": "string", },
"description": "Nomad namespace", "api": {
"required": false, "type": "openapi",
"default": "development" "url": "",
} "endpoints": [
] {
} "path": "/api/claude/list-jobs",
] "method": "GET",
} "description": "List all jobs in a namespace",
] "parameters": [
} {
``` "name": "namespace",
"in": "query",
### 3. Import the Tool Configuration "description": "Nomad namespace",
"required": false,
1. Open the Claude desktop application "schema": {
2. Go to Settings > Tools "type": "string",
3. Click "Import Tool Configuration" "default": "development"
4. Select the JSON file with the above configuration }
5. Click "Save" }
]
### 4. Test the Connection },
// Other endpoints...
You can test the connection by asking Claude to list all jobs: ]
}
``` }
Please list all jobs in the development namespace using the Nomad MCP service. ```
```
#### 3. Register the Tool with Claude Code
Claude should use the configured tool to make an API request to the Nomad MCP service and return the list of jobs.
```bash
## Example Prompts for Claude claude-code tools register --specification=/path/to/claude_nomad_tool.json --base-url=http://your-server-address:8000
```
Here are some example prompts you can use with Claude to interact with the Nomad MCP service:
### Option 3: Using Claude Code CLI with SSE (Server-Sent Events)
### List Jobs
For a more interactive experience, you can use Claude Code's MCP (Model Context Protocol) with SSE transport:
```
Please list all jobs in the development namespace. #### 1. Install Claude Code CLI
```
```bash
### Check Job Status npm install -g @anthropic-ai/claude-code
```
```
What is the status of the job "example-job"? #### 2. Start Your Nomad MCP Service
```
Make sure your Nomad MCP service is running and accessible.
### Start a New Job
#### 3. Add the MCP Configuration to Claude Code
```
Please create a new job with the following specifications: Use the `claude_code_mcp.json` configuration file with the Claude Code CLI:
- Job ID: test-nginx
- Docker image: nginx:latest ```bash
- Memory: 256MB claude-code mcp add nomad_mcp /path/to/claude_code_mcp.json
- CPU: 200MHz ```
- Port mapping: HTTP port 80
``` This configuration uses the SSE endpoint at `/api/claude/mcp/stream` to create a streaming connection between Claude Code and your service.
### Stop a Job #### 4. Launch Claude Code with the MCP Provider
``` ```bash
Please stop the job "test-nginx" and purge it from Nomad. claude-code --mcp nomad_mcp
``` ```
### Get Job Logs The SSE integration provides a more responsive experience with streaming updates and better error handling compared to the regular tool integration.
``` ## Test the Connection
Show me the logs for the job "example-job".
``` You can test the connection by asking Claude to list all jobs:
## Troubleshooting ```
Please list all jobs in the development namespace using the Nomad MCP service.
If Claude is unable to connect to the Nomad MCP service, check the following: ```
1. Ensure the Nomad MCP service is running and accessible from Claude's network Claude should use the configured tool to make an API request to the Nomad MCP service and return the list of jobs.
2. Verify the base URL in the tool configuration is correct
3. Check that the Nomad MCP service has proper connectivity to the Nomad server ## Example Prompts for Claude
4. Review the logs of the Nomad MCP service for any errors
Here are some example prompts you can use with Claude to interact with the Nomad MCP service:
## Security Considerations
### List Jobs
The Claude API integration does not include authentication by default. If you need to secure the API:
```
1. Add an API key requirement to the FastAPI application Please list all jobs in the development namespace.
2. Include the API key in the Claude tool configuration ```
### Check Job Status
```
What is the status of the job "example-job"?
```
### Start a New Job
```
Please create a new job with the following specifications:
- Job ID: test-nginx
- Docker image: nginx:latest
- Memory: 256MB
- CPU: 200MHz
- Port mapping: HTTP port 80
```
### Stop a Job
```
Please stop the job "test-nginx" and purge it from Nomad.
```
### Get Job Logs
```
Show me the logs for the job "example-job".
```
## Troubleshooting
If Claude is unable to connect to the Nomad MCP service, check the following:
1. Ensure the Nomad MCP service is running and accessible from Claude's network
2. Verify the base URL in the tool configuration is correct
3. Check that the Nomad MCP service has proper connectivity to the Nomad server
4. Review the logs of the Nomad MCP service for any errors
5. Make sure CORS is enabled in your FastAPI application (already configured in this application)
## Security Considerations
The Claude API integration does not include authentication by default. If you need to secure the API:
1. Add an API key requirement to the FastAPI application
2. Include the API key in the Claude tool configuration
3. Consider using HTTPS for all communications between Claude and the Nomad MCP service 3. Consider using HTTPS for all communications between Claude and the Nomad MCP service

View File

@ -1,230 +1,460 @@
from fastapi import APIRouter, HTTPException, Body, Query, Depends from fastapi import APIRouter, HTTPException, Body, Query, Depends, Request
from typing import Dict, Any, List, Optional from fastapi.responses import StreamingResponse
import logging from typing import Dict, Any, List, Optional, AsyncGenerator
import json import logging
import json
from app.services.nomad_client import NomadService import asyncio
from app.schemas.claude_api import ClaudeJobRequest, ClaudeJobSpecification, ClaudeJobResponse
from app.services.nomad_client import NomadService
router = APIRouter() from app.schemas.claude_api import ClaudeJobRequest, ClaudeJobSpecification, ClaudeJobResponse
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/jobs", response_model=ClaudeJobResponse) logger = logging.getLogger(__name__)
async def manage_job(request: ClaudeJobRequest):
""" @router.post("/jobs", response_model=ClaudeJobResponse)
Endpoint for Claude to manage Nomad jobs with a simplified interface. async def manage_job(request: ClaudeJobRequest):
"""
This endpoint handles job operations like start, stop, restart, and status checks. Endpoint for Claude to manage Nomad jobs with a simplified interface.
"""
try: This endpoint handles job operations like start, stop, restart, and status checks.
# Create a Nomad service instance with the specified namespace """
nomad_service = NomadService() try:
if request.namespace: # Create a Nomad service instance with the specified namespace
nomad_service.namespace = request.namespace nomad_service = NomadService()
if request.namespace:
# Handle different actions nomad_service.namespace = request.namespace
if request.action.lower() == "status":
# Get job status # Handle different actions
job = nomad_service.get_job(request.job_id) if request.action.lower() == "status":
# Get job status
# Get allocations for more detailed status job = nomad_service.get_job(request.job_id)
allocations = nomad_service.get_allocations(request.job_id)
latest_alloc = None # Get allocations for more detailed status
if allocations: allocations = nomad_service.get_allocations(request.job_id)
# Sort allocations by creation time (descending) latest_alloc = None
sorted_allocations = sorted( if allocations:
allocations, # Sort allocations by creation time (descending)
key=lambda a: a.get("CreateTime", 0), sorted_allocations = sorted(
reverse=True allocations,
) key=lambda a: a.get("CreateTime", 0),
latest_alloc = sorted_allocations[0] reverse=True
)
return ClaudeJobResponse( latest_alloc = sorted_allocations[0]
success=True,
job_id=request.job_id, return ClaudeJobResponse(
status=job.get("Status", "unknown"), success=True,
message=f"Job {request.job_id} is {job.get('Status', 'unknown')}", job_id=request.job_id,
details={ status=job.get("Status", "unknown"),
"job": job, message=f"Job {request.job_id} is {job.get('Status', 'unknown')}",
"latest_allocation": latest_alloc details={
} "job": job,
) "latest_allocation": latest_alloc
}
elif request.action.lower() == "stop": )
# Stop the job
result = nomad_service.stop_job(request.job_id, purge=request.purge) elif request.action.lower() == "stop":
# Stop the job
return ClaudeJobResponse( result = nomad_service.stop_job(request.job_id, purge=request.purge)
success=True,
job_id=request.job_id, return ClaudeJobResponse(
status="stopped", success=True,
message=f"Job {request.job_id} has been stopped" + (" and purged" if request.purge else ""), job_id=request.job_id,
details=result status="stopped",
) message=f"Job {request.job_id} has been stopped" + (" and purged" if request.purge else ""),
details=result
elif request.action.lower() == "restart": )
# Get the current job specification
job_spec = nomad_service.get_job(request.job_id) elif request.action.lower() == "restart":
# Get the current job specification
# Stop the job job_spec = nomad_service.get_job(request.job_id)
nomad_service.stop_job(request.job_id)
# Stop the job
# Start the job with the original specification nomad_service.stop_job(request.job_id)
result = nomad_service.start_job(job_spec)
# Start the job with the original specification
return ClaudeJobResponse( result = nomad_service.start_job(job_spec)
success=True,
job_id=request.job_id, return ClaudeJobResponse(
status="restarted", success=True,
message=f"Job {request.job_id} has been restarted", job_id=request.job_id,
details=result status="restarted",
) message=f"Job {request.job_id} has been restarted",
details=result
else: )
# Unknown action
raise HTTPException(status_code=400, detail=f"Unknown action: {request.action}") else:
# Unknown action
except Exception as e: raise HTTPException(status_code=400, detail=f"Unknown action: {request.action}")
logger.error(f"Error managing job {request.job_id}: {str(e)}")
return ClaudeJobResponse( except Exception as e:
success=False, logger.error(f"Error managing job {request.job_id}: {str(e)}")
job_id=request.job_id, return ClaudeJobResponse(
status="error", success=False,
message=f"Error: {str(e)}", job_id=request.job_id,
details=None status="error",
) message=f"Error: {str(e)}",
details=None
@router.post("/create-job", response_model=ClaudeJobResponse) )
async def create_job(job_spec: ClaudeJobSpecification):
""" @router.post("/create-job", response_model=ClaudeJobResponse)
Endpoint for Claude to create a new Nomad job with a simplified interface. async def create_job(job_spec: ClaudeJobSpecification):
"""
This endpoint allows creating a job with minimal configuration. Endpoint for Claude to create a new Nomad job with a simplified interface.
"""
try: This endpoint allows creating a job with minimal configuration.
# Create a Nomad service instance with the specified namespace """
nomad_service = NomadService() try:
if job_spec.namespace: # Create a Nomad service instance with the specified namespace
nomad_service.namespace = job_spec.namespace nomad_service = NomadService()
if job_spec.namespace:
# Convert the simplified job spec to Nomad format nomad_service.namespace = job_spec.namespace
nomad_job_spec = job_spec.to_nomad_job_spec()
# Convert the simplified job spec to Nomad format
# Start the job nomad_job_spec = job_spec.to_nomad_job_spec()
result = nomad_service.start_job(nomad_job_spec)
# Start the job
return ClaudeJobResponse( result = nomad_service.start_job(nomad_job_spec)
success=True,
job_id=job_spec.job_id, return ClaudeJobResponse(
status="started", success=True,
message=f"Job {job_spec.job_id} has been created and started", job_id=job_spec.job_id,
details=result status="started",
) message=f"Job {job_spec.job_id} has been created and started",
details=result
except Exception as e: )
logger.error(f"Error creating job {job_spec.job_id}: {str(e)}")
return ClaudeJobResponse( except Exception as e:
success=False, logger.error(f"Error creating job {job_spec.job_id}: {str(e)}")
job_id=job_spec.job_id, return ClaudeJobResponse(
status="error", success=False,
message=f"Error: {str(e)}", job_id=job_spec.job_id,
details=None status="error",
) message=f"Error: {str(e)}",
details=None
@router.get("/list-jobs", response_model=List[Dict[str, Any]]) )
async def list_jobs(namespace: str = Query("development")):
""" @router.get("/list-jobs", response_model=List[Dict[str, Any]])
List all jobs in the specified namespace. async def list_jobs(namespace: str = Query("development")):
"""
Returns a simplified list of jobs with their IDs and statuses. List all jobs in the specified namespace.
"""
try: Returns a simplified list of jobs with their IDs and statuses.
# Create a Nomad service instance with the specified namespace """
nomad_service = NomadService() try:
nomad_service.namespace = namespace # Create a Nomad service instance with the specified namespace
nomad_service = NomadService()
# Get all jobs nomad_service.namespace = namespace
jobs = nomad_service.list_jobs()
# Get all jobs
# Return a simplified list jobs = nomad_service.list_jobs()
simplified_jobs = []
for job in jobs: # Return a simplified list
simplified_jobs.append({ simplified_jobs = []
"id": job.get("ID"), for job in jobs:
"name": job.get("Name"), simplified_jobs.append({
"status": job.get("Status"), "id": job.get("ID"),
"type": job.get("Type"), "name": job.get("Name"),
"namespace": namespace "status": job.get("Status"),
}) "type": job.get("Type"),
"namespace": namespace
return simplified_jobs })
except Exception as e: return simplified_jobs
logger.error(f"Error listing jobs: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error listing jobs: {str(e)}") except Exception as e:
logger.error(f"Error listing jobs: {str(e)}")
@router.get("/job-logs/{job_id}", response_model=Dict[str, Any]) raise HTTPException(status_code=500, detail=f"Error listing jobs: {str(e)}")
async def get_job_logs(job_id: str, namespace: str = Query("development")):
""" @router.get("/job-logs/{job_id}", response_model=Dict[str, Any])
Get logs for a job. async def get_job_logs(job_id: str, namespace: str = Query("development")):
"""
Returns logs from the latest allocation of the job. Get logs for a job.
"""
try: Returns logs from the latest allocation of the job.
# Create a Nomad service instance with the specified namespace """
nomad_service = NomadService() try:
nomad_service.namespace = namespace # Create a Nomad service instance with the specified namespace
nomad_service = NomadService()
# Get allocations for the job nomad_service.namespace = namespace
allocations = nomad_service.get_allocations(job_id)
if not allocations: # Get allocations for the job
return { allocations = nomad_service.get_allocations(job_id)
"success": False, if not allocations:
"job_id": job_id, return {
"message": f"No allocations found for job {job_id}", "success": False,
"logs": None "job_id": job_id,
} "message": f"No allocations found for job {job_id}",
"logs": None
# Sort allocations by creation time (descending) }
sorted_allocations = sorted(
allocations, # Sort allocations by creation time (descending)
key=lambda a: a.get("CreateTime", 0), sorted_allocations = sorted(
reverse=True allocations,
) key=lambda a: a.get("CreateTime", 0),
latest_alloc = sorted_allocations[0] reverse=True
alloc_id = latest_alloc.get("ID") )
latest_alloc = sorted_allocations[0]
# Get the task name from the allocation alloc_id = latest_alloc.get("ID")
task_name = None
if "TaskStates" in latest_alloc: # Get the task name from the allocation
task_states = latest_alloc["TaskStates"] task_name = None
if task_states: if "TaskStates" in latest_alloc:
task_name = next(iter(task_states.keys())) task_states = latest_alloc["TaskStates"]
if task_states:
if not task_name: task_name = next(iter(task_states.keys()))
task_name = "app" # Default task name
if not task_name:
# Get logs for the allocation task_name = "app" # Default task name
stdout_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stdout")
stderr_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stderr") # Get logs for the allocation
stdout_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stdout")
return { stderr_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stderr")
"success": True,
"job_id": job_id, return {
"allocation_id": alloc_id, "success": True,
"task_name": task_name, "job_id": job_id,
"message": f"Retrieved logs for job {job_id}", "allocation_id": alloc_id,
"logs": { "task_name": task_name,
"stdout": stdout_logs, "message": f"Retrieved logs for job {job_id}",
"stderr": stderr_logs "logs": {
} "stdout": stdout_logs,
} "stderr": stderr_logs
}
except Exception as e: }
logger.error(f"Error getting logs for job {job_id}: {str(e)}")
return { except Exception as e:
"success": False, logger.error(f"Error getting logs for job {job_id}: {str(e)}")
"job_id": job_id, return {
"message": f"Error getting logs: {str(e)}", "success": False,
"logs": None "job_id": job_id,
} "message": f"Error getting logs: {str(e)}",
"logs": None
}
@router.post("/mcp/stream")
async def mcp_stream(request: Request):
"""
MCP streaming endpoint for Claude Code using Server-Sent Events (SSE).
This endpoint allows Claude Code to connect using the SSE transport mode.
It receives requests from Claude Code and streams responses back.
"""
async def event_generator() -> AsyncGenerator[str, None]:
# Read request data
request_data = await request.json()
logger.info(f"Received MCP request: {request_data}")
# Extract the request type and content
request_type = request_data.get("type", "unknown")
request_content = request_data.get("content", {})
request_id = request_data.get("id", "unknown")
# Send initial acknowledgment
yield f"data: {json.dumps({'id': request_id, 'type': 'ack', 'content': 'Processing request'})}\n\n"
try:
# Process different request types
if request_type == "nomad_list_jobs":
# Handle list jobs request
namespace = request_content.get("namespace", "development")
# Create Nomad service and get jobs
nomad_service = NomadService()
nomad_service.namespace = namespace
jobs = nomad_service.list_jobs()
# Format job information
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
})
# Send result
yield f"data: {json.dumps({'id': request_id, 'type': 'result', 'content': simplified_jobs})}\n\n"
elif request_type == "nomad_job_status":
# Handle job status request
job_id = request_content.get("job_id")
namespace = request_content.get("namespace", "development")
if not job_id:
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': 'Job ID is required'})}\n\n"
else:
# Create Nomad service and get job status
nomad_service = NomadService()
nomad_service.namespace = namespace
job = nomad_service.get_job(job_id)
# Get latest allocation
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]
# Send result
yield f"data: {json.dumps({'id': request_id, 'type': 'result', 'content': {
'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
}
}})}\n\n"
elif request_type == "nomad_job_action":
# Handle job action request (stop, restart)
job_id = request_content.get("job_id")
action = request_content.get("action")
namespace = request_content.get("namespace", "development")
purge = request_content.get("purge", False)
if not job_id or not action:
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': 'Job ID and action are required'})}\n\n"
else:
# Create Nomad service
nomad_service = NomadService()
nomad_service.namespace = namespace
if action.lower() == "stop":
# Stop the job
result = nomad_service.stop_job(job_id, purge=purge)
yield f"data: {json.dumps({'id': request_id, 'type': 'result', 'content': {
'job_id': job_id,
'status': 'stopped',
'message': f'Job {job_id} has been stopped' + (' and purged' if purge else ''),
'details': result
}})}\n\n"
elif action.lower() == "restart":
# Restart the job
job_spec = nomad_service.get_job(job_id)
nomad_service.stop_job(job_id)
result = nomad_service.start_job(job_spec)
yield f"data: {json.dumps({'id': request_id, 'type': 'result', 'content': {
'job_id': job_id,
'status': 'restarted',
'message': f'Job {job_id} has been restarted',
'details': result
}})}\n\n"
else:
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': f'Unknown action: {action}'})}\n\n"
elif request_type == "nomad_create_job":
# Handle create job request
try:
# Convert request content to job spec
job_spec = ClaudeJobSpecification(**request_content)
# Create Nomad service
nomad_service = NomadService()
if job_spec.namespace:
nomad_service.namespace = job_spec.namespace
# Convert to Nomad format and start job
nomad_job_spec = job_spec.to_nomad_job_spec()
result = nomad_service.start_job(nomad_job_spec)
yield f"data: {json.dumps({'id': request_id, 'type': 'result', 'content': {
'job_id': job_spec.job_id,
'status': 'started',
'message': f'Job {job_spec.job_id} has been created and started',
'details': result
}})}\n\n"
except Exception as e:
logger.error(f"Error creating job: {str(e)}")
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': f'Error creating job: {str(e)}'})}\n\n"
elif request_type == "nomad_job_logs":
# Handle job logs request
job_id = request_content.get("job_id")
namespace = request_content.get("namespace", "development")
if not job_id:
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': 'Job ID is required'})}\n\n"
else:
try:
# Create Nomad service
nomad_service = NomadService()
nomad_service.namespace = namespace
# Get job allocations
allocations = nomad_service.get_allocations(job_id)
if not allocations:
yield f"data: {json.dumps({'id': request_id, 'type': 'result', 'content': {
'success': False,
'job_id': job_id,
'message': f'No allocations found for job {job_id}',
'logs': None
}})}\n\n"
else:
# Get latest allocation and logs
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")
yield f"data: {json.dumps({'id': request_id, 'type': 'result', 'content': {
'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
}
}})}\n\n"
except Exception as e:
logger.error(f"Error getting logs for job {job_id}: {str(e)}")
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': f'Error getting logs: {str(e)}'})}\n\n"
else:
# Unknown request type
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': f'Unknown request type: {request_type}'})}\n\n"
# Send completion message
yield f"data: {json.dumps({'id': request_id, 'type': 'done'})}\n\n"
except Exception as e:
logger.error(f"Error processing MCP request: {str(e)}")
yield f"data: {json.dumps({'id': request_id, 'type': 'error', 'content': f'Internal server error: {str(e)}'})}\n\n"
yield f"data: {json.dumps({'id': request_id, 'type': 'done'})}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Needed for Nginx
}
)

249
claude_code_mcp.json Normal file
View File

@ -0,0 +1,249 @@
{
"name": "nomad_mcp_sse",
"version": "1.0.0",
"description": "Nomad MCP service for Claude Code using SSE",
"transport": {
"type": "sse",
"url": "http://localhost:8000/api/claude/mcp/stream"
},
"authentication": {
"type": "none"
},
"capabilities": [
{
"type": "function",
"function": {
"name": "list_nomad_jobs",
"description": "List all Nomad jobs in a namespace",
"parameters": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Nomad namespace",
"default": "development"
}
}
},
"required": []
},
"implementation": {
"type": "nomad_list_jobs",
"content": {
"namespace": "${parameters.namespace}"
}
}
},
{
"type": "function",
"function": {
"name": "get_job_status",
"description": "Get the status of a specific Nomad job",
"parameters": {
"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"]
},
"implementation": {
"type": "nomad_job_status",
"content": {
"job_id": "${parameters.job_id}",
"namespace": "${parameters.namespace}"
}
}
},
{
"type": "function",
"function": {
"name": "stop_job",
"description": "Stop a running Nomad job",
"parameters": {
"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"]
},
"implementation": {
"type": "nomad_job_action",
"content": {
"job_id": "${parameters.job_id}",
"action": "stop",
"namespace": "${parameters.namespace}",
"purge": "${parameters.purge}"
}
}
},
{
"type": "function",
"function": {
"name": "restart_job",
"description": "Restart a Nomad job",
"parameters": {
"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"]
},
"implementation": {
"type": "nomad_job_action",
"content": {
"job_id": "${parameters.job_id}",
"action": "restart",
"namespace": "${parameters.namespace}"
}
}
},
{
"type": "function",
"function": {
"name": "create_job",
"description": "Create a new Nomad job",
"parameters": {
"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"]
},
"implementation": {
"type": "nomad_create_job",
"content": {
"job_id": "${parameters.job_id}",
"name": "${parameters.name}",
"type": "${parameters.type}",
"datacenters": "${parameters.datacenters}",
"namespace": "${parameters.namespace}",
"docker_image": "${parameters.docker_image}",
"count": "${parameters.count}",
"cpu": "${parameters.cpu}",
"memory": "${parameters.memory}",
"ports": "${parameters.ports}",
"env_vars": "${parameters.env_vars}"
}
}
},
{
"type": "function",
"function": {
"name": "get_job_logs",
"description": "Get logs for a Nomad job",
"parameters": {
"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"]
},
"implementation": {
"type": "nomad_job_logs",
"content": {
"job_id": "${parameters.job_id}",
"namespace": "${parameters.namespace}"
}
}
}
]
}

View File

@ -1,71 +1,159 @@
{ {
"tools": [ "schema_version": "v1",
{ "name": "nomad_mcp",
"name": "nomad_mcp", "description": "Manage Nomad jobs through the MCP service",
"description": "Manage Nomad jobs through the MCP service", "authentication": {
"api_endpoints": [ "type": "none"
{ },
"name": "list_jobs", "api": {
"description": "List all jobs in a namespace", "type": "openapi",
"method": "GET", "url": "",
"url": "http://127.0.0.1:8000/api/claude/list-jobs", "endpoints": [
"params": [ {
{ "path": "/api/claude/list-jobs",
"name": "namespace", "method": "GET",
"type": "string", "description": "List all jobs in a namespace",
"description": "Nomad namespace", "parameters": [
"required": false, {
"default": "development" "name": "namespace",
} "in": "query",
] "description": "Nomad namespace",
}, "required": false,
{ "schema": {
"name": "manage_job", "type": "string",
"description": "Manage a job (status, stop, restart)", "default": "development"
"method": "POST", }
"url": "http://127.0.0.1:8000/api/claude/jobs", }
"body": { ]
"job_id": "string", },
"action": "string", {
"namespace": "string", "path": "/api/claude/jobs",
"purge": "boolean" "method": "POST",
} "description": "Manage a job (status, stop, restart)",
}, "request_body": {
{ "required": true,
"name": "create_job", "content": {
"description": "Create a new job", "application/json": {
"method": "POST", "schema": {
"url": "http://127.0.0.1:8000/api/claude/create-job", "type": "object",
"body": { "required": ["job_id", "action"],
"job_id": "string", "properties": {
"name": "string", "job_id": {
"type": "string", "type": "string",
"datacenters": "array", "description": "ID of the job to manage"
"namespace": "string", },
"docker_image": "string", "action": {
"count": "integer", "type": "string",
"cpu": "integer", "description": "Action to perform: status, stop, or restart"
"memory": "integer", },
"ports": "array", "namespace": {
"env_vars": "object" "type": "string",
} "description": "Nomad namespace"
}, },
{ "purge": {
"name": "get_job_logs", "type": "boolean",
"description": "Get logs for a job", "description": "Whether to purge the job when stopping"
"method": "GET", }
"url": "http://127.0.0.1:8000/api/claude/job-logs/{job_id}", }
"params": [ }
{ }
"name": "namespace", }
"type": "string", }
"description": "Nomad namespace", },
"required": false, {
"default": "development" "path": "/api/claude/create-job",
} "method": "POST",
] "description": "Create a new job",
} "request_body": {
] "required": true,
} "content": {
] "application/json": {
"schema": {
"type": "object",
"required": ["job_id", "name", "type", "docker_image"],
"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.)"
},
"datacenters": {
"type": "array",
"description": "List of datacenters to run the job in",
"items": {
"type": "string"
}
},
"namespace": {
"type": "string",
"description": "Nomad namespace"
},
"docker_image": {
"type": "string",
"description": "Docker image to run"
},
"count": {
"type": "integer",
"description": "Number of instances to run"
},
"cpu": {
"type": "integer",
"description": "CPU allocation in MHz"
},
"memory": {
"type": "integer",
"description": "Memory allocation in MB"
},
"ports": {
"type": "array",
"description": "Port mappings",
"items": {
"type": "object"
}
},
"env_vars": {
"type": "object",
"description": "Environment variables for the container"
}
}
}
}
}
}
},
{
"path": "/api/claude/job-logs/{job_id}",
"method": "GET",
"description": "Get logs for a job",
"parameters": [
{
"name": "job_id",
"in": "path",
"description": "ID of the job to get logs for",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "namespace",
"in": "query",
"description": "Nomad namespace",
"required": false,
"schema": {
"type": "string",
"default": "development"
}
}
]
}
]
}
} }