pr_reviewer/mcp_servers/hadolint_mcp.py
2026-05-08 23:46:17 +10:00

133 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""
MCP server for Hadolint Dockerfile linter.
"""
import asyncio
import json
import logging
import subprocess
import sys
from typing import Any, Dict, List
import mcp.server.stdio
import mcp.types as types
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Create server instance
server = Server("hadolint-mcp")
@server.list_tools()
async def handle_list_tools() -> List[types.Tool]:
"""
List available tools.
"""
return [
types.Tool(
name="lint_dockerfile",
description="Lint a Dockerfile using Hadolint",
inputSchema={
"type": "object",
"properties": {
"dockerfile_content": {
"type": "string",
"description": "The content of the Dockerfile to lint"
}
},
"required": ["dockerfile_content"]
}
)
]
@server.call_tool()
async def handle_call_tool(
name: str, arguments: Dict[str, Any] | None
) -> List[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
Handle tool calls.
"""
if name != "lint_dockerfile":
raise ValueError(f"Unknown tool: {name}")
if not arguments:
raise ValueError("Missing arguments")
dockerfile_content = arguments.get("dockerfile_content")
if not dockerfile_content:
raise ValueError("Missing dockerfile_content argument")
try:
# Run hadolint on the Dockerfile content
process = await asyncio.create_subprocess_exec(
"hadolint",
"-", # Read from stdin
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, stderr = await process.communicate(input=dockerfile_content.encode())
if process.returncode != 0:
# Hadolint returns non-zero for linting errors, which is expected
# We still return the output as the result
result = stdout.decode() + stderr.decode()
else:
result = stdout.decode()
# If no output, hadolint passed with no issues
if not result.strip():
result = "Hadolint: No issues found."
return [
types.TextContent(
type="text",
text=result
)
]
except FileNotFoundError:
logger.error("Hadolint command not found. Please ensure hadolint is installed and in PATH.")
return [
types.TextContent(
type="text",
text="Error: Hadolint command not found. Please install hadolint."
)
]
except Exception as e:
logger.exception("Error running hadolint")
return [
types.TextContent(
type="text",
text=f"Error running hadolint: {str(e)}"
)
]
async def main():
"""
Run the MCP server.
"""
# Run the server using stdio
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="hadolint-mcp",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
if __name__ == "__main__":
asyncio.run(main())