"""
ASCII Art & Unicode Diagrams MCP Server
Lushy Pattern 2 implementation for text-based visual generation.
Uses categorical composition of aesthetic functors with zero rendering dependencies.
Functors:
- box_drawing: Border styles, line weights, corner treatments
- ascii_shading: Character gradients, contrast, patterns
- layout_composition: Canvas size, alignment, spatial organization
"""
from fastmcp import FastMCP
from typing import Dict, Any, List, Optional, Literal
from dataclasses import dataclass
import math
# Initialize MCP server
mcp = FastMCP("ASCII Art Generator")
# ============================================================================
# LAYER 1: Data Structures (Categorical Objects)
# ============================================================================
@dataclass
class BoxDrawingParams:
"""Box drawing aesthetic parameters"""
line_style: Literal['light', 'heavy', 'double', 'rounded']
corner_type: Literal['sharp', 'rounded', 'beveled']
symmetry: Literal['bilateral', 'radial', 'asymmetric']
@dataclass
class ShadingParams:
"""ASCII shading parameters"""
palette_type: Literal['ascii_standard', 'blocks', 'dots', 'density', 'braille']
contrast: float # 0.0-1.0
direction: Literal['horizontal', 'vertical', 'radial', 'diagonal']
dither: bool
@dataclass
class LayoutParams:
"""Layout composition parameters"""
width: int
height: int
horizontal_align: Literal['left', 'center', 'right']
vertical_align: Literal['top', 'middle', 'bottom']
padding: int
# ============================================================================
# LAYER 2: Intentionality (Visual Vocabularies)
# ============================================================================
UNICODE_SETS = {
'light': {
'horizontal': '─', 'vertical': '│',
'top_left': '┌', 'top_right': '┐',
'bottom_left': '└', 'bottom_right': '┘',
'cross': '┼', 't_down': '┬', 't_up': '┴', 't_left': '┤', 't_right': '├',
'intentionality': 'Clean, minimal, professional - optimal for documentation'
},
'heavy': {
'horizontal': '━', 'vertical': '┃',
'top_left': '┏', 'top_right': '┓',
'bottom_left': '┗', 'bottom_right': '┛',
'cross': '╋', 't_down': '┳', 't_up': '┻', 't_left': '┫', 't_right': '┣',
'intentionality': 'Bold, emphatic - draws attention and establishes hierarchy'
},
'double': {
'horizontal': '═', 'vertical': '║',
'top_left': '╔', 'top_right': '╗',
'bottom_left': '╚', 'bottom_right': '╝',
'cross': '╬', 't_down': '╦', 't_up': '╩', 't_left': '╣', 't_right': '╠',
'intentionality': 'Formal, structured - conveys authority and permanence'
},
'rounded': {
'horizontal': '─', 'vertical': '│',
'top_left': '╭', 'top_right': '╮',
'bottom_left': '╰', 'bottom_right': '╯',
'cross': '┼', 't_down': '┬', 't_up': '┴', 't_left': '┤', 't_right': '├',
'intentionality': 'Friendly, approachable - reduces visual tension'
}
}
SHADING_PALETTES = {
'ascii_standard': {
'chars': ' .:-=+*#%@',
'texture': 'grainy',
'intentionality': 'Classic ASCII art - universally compatible'
},
'blocks': {
'chars': ' ░▒▓█',
'texture': 'smooth',
'intentionality': 'Smooth gradients - modern terminal aesthetics'
},
'dots': {
'chars': ' ·∘○●◉',
'texture': 'dotted',
'intentionality': 'Geometric precision - technical diagrams'
},
'density': {
'chars': ' .,;!lI$@',
'texture': 'dense',
'intentionality': 'High detail - complex shading'
},
'braille': {
'chars': '⠀⠁⠃⠇⠏⠟⠿⣿',
'texture': 'fine',
'intentionality': 'Ultra-fine detail - maximum resolution'
}
}
# ============================================================================
# LAYER 3: Rendering Engine
# ============================================================================
class ASCIIArtRenderer:
"""Deterministic ASCII art generation from categorical parameters"""
def __init__(
self,
box_params: BoxDrawingParams,
shading_params: ShadingParams,
layout_params: LayoutParams
):
self.box = box_params
self.shading = shading_params
self.layout = layout_params
self.chars = UNICODE_SETS[box_params.line_style]
self.palette = SHADING_PALETTES[shading_params.palette_type]['chars']
def render_bordered_box(self, title: Optional[str] = None) -> str:
"""Render a bordered box with optional shading"""
lines = []
w = self.layout.width
h = self.layout.height
# Top border
if title:
title_display = f" {title} "
title_len = len(title_display)
left_line = self.chars['horizontal'] * ((w - 2 - title_len) // 2)
right_line = self.chars['horizontal'] * ((w - 2 - title_len + 1) // 2)
top = self.chars['top_left'] + left_line + title_display + right_line + self.chars['top_right']
else:
top = self.chars['top_left'] + (self.chars['horizontal'] * (w - 2)) + self.chars['top_right']
lines.append(top)
# Middle rows with shading
for y in range(h - 2):
row = self.chars['vertical']
for x in range(w - 2):
# Calculate shading value based on direction
shade_value = self._calculate_shade_value(x, y, w - 2, h - 2)
# Apply contrast
shade_value = self._apply_contrast(shade_value)
# Map to character
char_idx = int(shade_value * (len(self.palette) - 1))
char_idx = max(0, min(char_idx, len(self.palette) - 1))
row += self.palette[char_idx]
row += self.chars['vertical']
lines.append(row)
# Bottom border
bottom = self.chars['bottom_left'] + (self.chars['horizontal'] * (w - 2)) + self.chars['bottom_right']
lines.append(bottom)
return '\n'.join(lines)
def _calculate_shade_value(self, x: int, y: int, width: int, height: int) -> float:
"""Calculate shading value 0.0-1.0 based on position and direction"""
center_x = width / 2
center_y = height / 2
if self.shading.direction == 'horizontal':
return x / width if width > 0 else 0
elif self.shading.direction == 'vertical':
return y / height if height > 0 else 0
elif self.shading.direction == 'radial':
dx = (x - center_x) / center_x if center_x > 0 else 0
dy = (y - center_y) / center_y if center_y > 0 else 0
dist = math.sqrt(dx * dx + dy * dy)
return min(dist, 1.0)
elif self.shading.direction == 'diagonal':
return (x + y) / (width + height) if (width + height) > 0 else 0
else:
return 0.5
def _apply_contrast(self, value: float) -> float:
"""Apply contrast adjustment to shade value"""
# Apply contrast curve
if self.shading.contrast < 0.5:
# Reduce contrast - compress to middle
range_compress = self.shading.contrast * 2
return 0.5 + (value - 0.5) * range_compress
else:
# Increase contrast - expand from middle
range_expand = (self.shading.contrast - 0.5) * 2 + 1
if value < 0.5:
return 0.5 - (0.5 - value) * range_expand
else:
return 0.5 + (value - 0.5) * range_expand
return max(0.0, min(1.0, value))
def render_table(self, headers: List[str], rows: List[List[str]]) -> str:
"""Render a data table with borders"""
# Calculate column widths
col_widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = max(col_widths[i], len(str(cell)))
lines = []
# Top border
top = self.chars['top_left']
for i, width in enumerate(col_widths):
top += self.chars['horizontal'] * (width + 2)
if i < len(col_widths) - 1:
top += self.chars['t_down']
top += self.chars['top_right']
lines.append(top)
# Header row
header_row = self.chars['vertical']
for i, header in enumerate(headers):
header_row += f" {header.ljust(col_widths[i])} "
if i < len(headers) - 1:
header_row += self.chars['vertical']
header_row += self.chars['vertical']
lines.append(header_row)
# Header separator
sep = self.chars['t_right']
for i, width in enumerate(col_widths):
sep += self.chars['horizontal'] * (width + 2)
if i < len(col_widths) - 1:
sep += self.chars['cross']
sep += self.chars['t_left']
lines.append(sep)
# Data rows
for row in rows:
data_row = self.chars['vertical']
for i, cell in enumerate(row):
if i < len(col_widths):
data_row += f" {str(cell).ljust(col_widths[i])} "
if i < len(row) - 1:
data_row += self.chars['vertical']
data_row += self.chars['vertical']
lines.append(data_row)
# Bottom border
bottom = self.chars['bottom_left']
for i, width in enumerate(col_widths):
bottom += self.chars['horizontal'] * (width + 2)
if i < len(col_widths) - 1:
bottom += self.chars['t_up']
bottom += self.chars['bottom_right']
lines.append(bottom)
return '\n'.join(lines)
# ============================================================================
# MCP TOOLS
# ============================================================================
@mcp.tool()
def create_ascii_box(
width: int = 50,
height: int = 15,
title: str = "",
line_style: Literal['light', 'heavy', 'double', 'rounded'] = 'light',
shading_palette: Literal['ascii_standard', 'blocks', 'dots', 'density', 'braille'] = 'ascii_standard',
shading_direction: Literal['horizontal', 'vertical', 'radial', 'diagonal'] = 'radial',
contrast: float = 0.7
) -> str:
"""
Create ASCII art box with border and optional shading.
Args:
width: Box width in characters (20-120)
height: Box height in characters (5-50)
title: Optional title text in top border
line_style: Border style (light=minimal, heavy=bold, double=formal, rounded=friendly)
shading_palette: Character set for shading (ascii_standard, blocks, dots, density, braille)
shading_direction: Gradient direction (horizontal, vertical, radial, diagonal)
contrast: Shading contrast 0.0-1.0 (low=subtle, high=dramatic)
Returns:
ASCII art as multi-line string
Example:
create_ascii_box(width=40, height=10, title="Status", line_style='double',
shading_palette='blocks', contrast=0.8)
"""
# Clamp parameters
width = max(20, min(120, width))
height = max(5, min(50, height))
contrast = max(0.0, min(1.0, contrast))
# Create parameter objects
box_params = BoxDrawingParams(
line_style=line_style,
corner_type='sharp' if line_style in ['light', 'heavy', 'double'] else 'rounded',
symmetry='bilateral'
)
shading_params = ShadingParams(
palette_type=shading_palette,
contrast=contrast,
direction=shading_direction,
dither=False
)
layout_params = LayoutParams(
width=width,
height=height,
horizontal_align='center',
vertical_align='middle',
padding=1
)
# Render
renderer = ASCIIArtRenderer(box_params, shading_params, layout_params)
return renderer.render_bordered_box(title if title else None)
@mcp.tool()
def create_ascii_table(
headers: list[str],
rows: list[list[str]],
line_style: Literal['light', 'heavy', 'double', 'rounded'] = 'light'
) -> str:
"""
Create ASCII table with headers and data rows.
Args:
headers: List of column headers
rows: List of rows, each row is a list of cell values
line_style: Border style (light, heavy, double, rounded)
Returns:
ASCII table as multi-line string
Example:
create_ascii_table(
headers=["Name", "Status", "Progress"],
rows=[
["Task 1", "Complete", "100%"],
["Task 2", "Running", "65%"],
["Task 3", "Pending", "0%"]
],
line_style='double'
)
"""
box_params = BoxDrawingParams(
line_style=line_style,
corner_type='sharp' if line_style != 'rounded' else 'rounded',
symmetry='bilateral'
)
shading_params = ShadingParams(
palette_type='ascii_standard',
contrast=0.5,
direction='horizontal',
dither=False
)
layout_params = LayoutParams(
width=80,
height=20,
horizontal_align='left',
vertical_align='top',
padding=1
)
renderer = ASCIIArtRenderer(box_params, shading_params, layout_params)
return renderer.render_table(headers, rows)
@mcp.tool()
def list_ascii_styles() -> dict:
"""
List all available ASCII art styles with their intentionality.
Returns:
Dictionary of styles and their visual/semantic properties
"""
return {
"box_styles": {
style: {
"sample_chars": f"{chars['top_left']}{chars['horizontal']*3}{chars['top_right']}",
"intentionality": chars['intentionality']
}
for style, chars in UNICODE_SETS.items()
},
"shading_palettes": {
name: {
"characters": palette['chars'],
"texture": palette['texture'],
"intentionality": palette['intentionality']
}
for name, palette in SHADING_PALETTES.items()
}
}
# ============================================================================
# Run Server
# ============================================================================
if __name__ == "__main__":
mcp.run()