JSON Canvas MCP Server
by Cam10001110101
Verified
- jsoncanvas
"""Core functionality for JSON Canvas."""
from typing import Dict, List, Optional, Union, cast
from .errors import DuplicateIdError, ReferenceError, ValidationError
from .nodes import Node, TextNode, FileNode, LinkNode, GroupNode
from .edges import Edge
class Canvas:
"""A JSON Canvas representation according to the 1.0 specification."""
def __init__(
self,
nodes: Optional[List[Node]] = None,
edges: Optional[List[Edge]] = None
) -> None:
"""Initialize a new Canvas.
Args:
nodes: Optional list of nodes
edges: Optional list of edges
"""
self.nodes = nodes or []
self.edges = edges or []
self._validate_ids()
self._validate_edge_references()
def _validate_ids(self) -> None:
"""Validate that all node and edge IDs are unique.
Raises:
DuplicateIdError: If any duplicate IDs are found
"""
ids = set()
# Check node IDs
for node in self.nodes:
if node.id in ids:
raise DuplicateIdError(f"Duplicate node ID found: {node.id}")
ids.add(node.id)
# Check edge IDs
for edge in self.edges:
if edge.id in ids:
raise DuplicateIdError(f"Duplicate edge ID found: {edge.id}")
ids.add(edge.id)
def _validate_edge_references(self) -> None:
"""Validate that all edge references point to existing nodes.
Raises:
ReferenceError: If an edge references a non-existent node
"""
node_ids = {node.id for node in self.nodes}
for edge in self.edges:
if edge.from_node not in node_ids:
raise ReferenceError(
f"Edge {edge.id} references non-existent from_node: {edge.from_node}"
)
if edge.to_node not in node_ids:
raise ReferenceError(
f"Edge {edge.id} references non-existent to_node: {edge.to_node}"
)
def add_node(self, node: Node) -> None:
"""Add a node to the canvas.
Args:
node: The node to add
Raises:
DuplicateIdError: If a node with the same ID already exists
"""
# Check for duplicate ID
if any(n.id == node.id for n in self.nodes):
raise DuplicateIdError(f"Node with ID {node.id} already exists")
self.nodes.append(node)
def add_edge(self, edge: Edge) -> None:
"""Add an edge to the canvas.
Args:
edge: The edge to add
Raises:
DuplicateIdError: If an edge with the same ID already exists
ReferenceError: If the edge references non-existent nodes
"""
# Check for duplicate ID
if any(e.id == edge.id for e in self.edges):
raise DuplicateIdError(f"Edge with ID {edge.id} already exists")
# Validate node references
node_ids = {node.id for node in self.nodes}
if edge.from_node not in node_ids:
raise ReferenceError(
f"Edge references non-existent from_node: {edge.from_node}"
)
if edge.to_node not in node_ids:
raise ReferenceError(
f"Edge references non-existent to_node: {edge.to_node}"
)
self.edges.append(edge)
def get_node(self, node_id: str) -> Optional[Node]:
"""Get a node by its ID.
Args:
node_id: The ID of the node to get
Returns:
The node if found, None otherwise
"""
for node in self.nodes:
if node.id == node_id:
return node
return None
def get_edge(self, edge_id: str) -> Optional[Edge]:
"""Get an edge by its ID.
Args:
edge_id: The ID of the edge to get
Returns:
The edge if found, None otherwise
"""
for edge in self.edges:
if edge.id == edge_id:
return edge
return None
def remove_node(self, node_id: str) -> Optional[Node]:
"""Remove a node and all its connected edges.
Args:
node_id: The ID of the node to remove
Returns:
The removed node if found, None otherwise
"""
# Find and remove the node
node_index = None
for i, node in enumerate(self.nodes):
if node.id == node_id:
node_index = i
break
if node_index is None:
return None
removed_node = self.nodes.pop(node_index)
# Remove all edges connected to this node
self.edges = [
edge for edge in self.edges
if edge.from_node != node_id and edge.to_node != node_id
]
return removed_node
def remove_edge(self, edge_id: str) -> Optional[Edge]:
"""Remove an edge.
Args:
edge_id: The ID of the edge to remove
Returns:
The removed edge if found, None otherwise
"""
edge_index = None
for i, edge in enumerate(self.edges):
if edge.id == edge_id:
edge_index = i
break
if edge_index is None:
return None
return self.edges.pop(edge_index)
def to_dict(self) -> Dict:
"""Convert the canvas to a dictionary.
Returns:
Dictionary representation of the canvas
"""
canvas_dict: Dict[str, list] = {}
if self.nodes:
canvas_dict["nodes"] = [node.to_dict() for node in self.nodes]
if self.edges:
canvas_dict["edges"] = [edge.to_dict() for edge in self.edges]
return canvas_dict
@classmethod
def from_dict(cls, data: Dict) -> "Canvas":
"""Create a Canvas from a dictionary.
Args:
data: Dictionary representation of a canvas
Returns:
A new Canvas instance
Raises:
ValidationError: If the dictionary is invalid
"""
nodes = []
edges = []
# Parse nodes
for node_data in data.get("nodes", []):
node_type = node_data.get("type")
if node_type == "text":
nodes.append(TextNode(
id=node_data["id"],
x=node_data["x"],
y=node_data["y"],
width=node_data["width"],
height=node_data["height"],
text=node_data["text"],
color=node_data.get("color")
))
elif node_type == "file":
nodes.append(FileNode(
id=node_data["id"],
x=node_data["x"],
y=node_data["y"],
width=node_data["width"],
height=node_data["height"],
file=node_data["file"],
subpath=node_data.get("subpath"),
color=node_data.get("color")
))
elif node_type == "link":
nodes.append(LinkNode(
id=node_data["id"],
x=node_data["x"],
y=node_data["y"],
width=node_data["width"],
height=node_data["height"],
url=node_data["url"],
color=node_data.get("color")
))
elif node_type == "group":
nodes.append(GroupNode(
id=node_data["id"],
x=node_data["x"],
y=node_data["y"],
width=node_data["width"],
height=node_data["height"],
label=node_data.get("label"),
background=node_data.get("background"),
background_style=node_data.get("backgroundStyle"),
color=node_data.get("color")
))
else:
raise ValidationError(f"Invalid node type: {node_type}")
# Parse edges
for edge_data in data.get("edges", []):
edges.append(Edge.from_dict(edge_data))
return cls(nodes=nodes, edges=edges)