Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions servers/mcp-neo4j-data-modeling/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Changed

### Added
* Add import and export methods to `DataModel` for turtle OWL strings
* Add MCP tools for loading and exporting turtle OWL files

## v0.5.1

Expand Down
1 change: 1 addition & 0 deletions servers/mcp-neo4j-data-modeling/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies = [
"fastmcp>=2.0.0",
"pydantic>=2.10.1",
"starlette>=0.47.0",
"rdflib>=7.0.0",
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any

from pydantic import BaseModel, Field, ValidationInfo, field_validator
from rdflib import Graph, Namespace, RDF, RDFS, OWL, XSD, Literal, URIRef

NODE_COLOR_PALETTE = [
("#e3f2fd", "#1976d2"), # Light Blue / Blue
Expand Down Expand Up @@ -551,6 +552,195 @@ def to_arrows_dict(self) -> dict[str, Any]:
def to_arrows_json_str(self) -> str:
"Convert the data model to an Arrows Data Model JSON string."
return json.dumps(self.to_arrows_dict(), indent=2)

def to_owl_turtle_str(self) -> str:
"""Convert the data model to an OWL Turtle string.

This method creates an OWL ontology from the Neo4j data model:
- Node labels become OWL Classes
- Node properties become OWL DatatypeProperties with the node class as domain
- Relationship types become OWL ObjectProperties with start/end nodes as domain/range
- Relationship properties become OWL DatatypeProperties with the relationship as domain
"""
# Create a new RDF graph
g = Graph()

# Define namespaces
# Use a generic namespace for the ontology
base_ns = Namespace("http://voc.neo4j.com/datamodel#")
g.bind("", base_ns)
g.bind("owl", OWL)
g.bind("rdfs", RDFS)
g.bind("xsd", XSD)

# Create the ontology declaration
ontology_uri = URIRef("http://voc.neo4j.com/datamodel")
g.add((ontology_uri, RDF.type, OWL.Ontology))

# Map Neo4j types to XSD types
type_mapping = {
"STRING": XSD.string,
"INTEGER": XSD.integer,
"FLOAT": XSD.float,
"BOOLEAN": XSD.boolean,
"DATE": XSD.date,
"DATETIME": XSD.dateTime,
"TIME": XSD.time,
"DURATION": XSD.duration,
"LONG": XSD.long,
"DOUBLE": XSD.double,
}

# Process nodes -> OWL Classes
for node in self.nodes:
class_uri = base_ns[node.label]
g.add((class_uri, RDF.type, OWL.Class))

# Add key property as a datatype property
if node.key_property:
prop_uri = base_ns[node.key_property.name]
g.add((prop_uri, RDF.type, OWL.DatatypeProperty))
g.add((prop_uri, RDFS.domain, class_uri))
xsd_type = type_mapping.get(node.key_property.type.upper(), XSD.string)
g.add((prop_uri, RDFS.range, xsd_type))

# Add other properties as datatype properties
for prop in node.properties:
prop_uri = base_ns[prop.name]
g.add((prop_uri, RDF.type, OWL.DatatypeProperty))
g.add((prop_uri, RDFS.domain, class_uri))
xsd_type = type_mapping.get(prop.type.upper(), XSD.string)
g.add((prop_uri, RDFS.range, xsd_type))

# Process relationships -> OWL ObjectProperties
for rel in self.relationships:
rel_uri = base_ns[rel.type]
g.add((rel_uri, RDF.type, OWL.ObjectProperty))
g.add((rel_uri, RDFS.domain, base_ns[rel.start_node_label]))
g.add((rel_uri, RDFS.range, base_ns[rel.end_node_label]))

# If relationship has properties, create datatype properties
if rel.key_property:
prop_uri = base_ns[f"{rel.type}_{rel.key_property.name}"]
g.add((prop_uri, RDF.type, OWL.DatatypeProperty))
g.add((prop_uri, RDFS.domain, rel_uri))
xsd_type = type_mapping.get(rel.key_property.type.upper(), XSD.string)
g.add((prop_uri, RDFS.range, xsd_type))

for prop in rel.properties:
prop_uri = base_ns[f"{rel.type}_{prop.name}"]
g.add((prop_uri, RDF.type, OWL.DatatypeProperty))
g.add((prop_uri, RDFS.domain, rel_uri))
xsd_type = type_mapping.get(prop.type.upper(), XSD.string)
g.add((prop_uri, RDFS.range, xsd_type))

# Serialize to Turtle format
return g.serialize(format="turtle")

@classmethod
def from_owl_turtle_str(cls, owl_turtle_str: str) -> "DataModel":
"""Convert an OWL Turtle string to a Neo4j Data Model.

This method parses an OWL ontology and creates a Neo4j data model:
- OWL Classes become Node labels
- OWL DatatypeProperties with Class domains become Node properties
- OWL ObjectProperties become Relationships
- Property domains and ranges are used to infer Node labels and types
"""
# Parse the Turtle string
g = Graph()
g.parse(data=owl_turtle_str, format="turtle")

# Map XSD types back to Neo4j types
xsd_to_neo4j = {
str(XSD.string): "STRING",
str(XSD.integer): "INTEGER",
str(XSD.float): "FLOAT",
str(XSD.boolean): "BOOLEAN",
str(XSD.date): "DATE",
str(XSD.dateTime): "DATETIME",
str(XSD.time): "TIME",
str(XSD.duration): "DURATION",
str(XSD.long): "LONG",
str(XSD.double): "DOUBLE",
}

# Extract OWL Classes -> Nodes
classes = set()
for s in g.subjects(RDF.type, OWL.Class):
classes.add(str(s).split("#")[-1].split("/")[-1])

# Extract DatatypeProperties
datatype_props = {}
for prop in g.subjects(RDF.type, OWL.DatatypeProperty):
prop_name = str(prop).split("#")[-1].split("/")[-1]
domains = list(g.objects(prop, RDFS.domain))
ranges = list(g.objects(prop, RDFS.range))

domain_name = str(domains[0]).split("#")[-1].split("/")[-1] if domains else None
range_type = xsd_to_neo4j.get(str(ranges[0]), "STRING") if ranges else "STRING"

if domain_name:
if domain_name not in datatype_props:
datatype_props[domain_name] = []
datatype_props[domain_name].append({
"name": prop_name,
"type": range_type
})

# Extract ObjectProperties -> Relationships
object_props = []
for prop in g.subjects(RDF.type, OWL.ObjectProperty):
prop_name = str(prop).split("#")[-1].split("/")[-1]
domains = list(g.objects(prop, RDFS.domain))
ranges = list(g.objects(prop, RDFS.range))

if domains and ranges:
domain_name = str(domains[0]).split("#")[-1].split("/")[-1]
range_name = str(ranges[0]).split("#")[-1].split("/")[-1]

object_props.append({
"type": prop_name,
"start_node_label": domain_name,
"end_node_label": range_name
})

# Create Nodes
nodes = []
for class_name in classes:
props_for_class = datatype_props.get(class_name, [])

# Use the first property as key property, or create a default one
if props_for_class:
key_prop = Property(
name=props_for_class[0]["name"],
type=props_for_class[0]["type"]
)
other_props = [
Property(name=p["name"], type=p["type"])
for p in props_for_class[1:]
]
else:
# Create a default key property
key_prop = Property(name=f"{class_name.lower()}Id", type="STRING")
other_props = []

nodes.append(Node(
label=class_name,
key_property=key_prop,
properties=other_props
))

# Create Relationships
relationships = []
for obj_prop in object_props:
relationships.append(Relationship(
type=obj_prop["type"],
start_node_label=obj_prop["start_node_label"],
end_node_label=obj_prop["end_node_label"]
))

return cls(nodes=nodes, relationships=relationships)

def get_node_cypher_ingest_query_for_many_records(self, node_label: str) -> str:
"Generate a Cypher query to ingest a list of Node records into a Neo4j database."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,18 @@ def list_example_data_models() -> dict[str, Any]:
"total_examples": len(examples),
"usage": "Use the get_example_data_model tool with any of the example names above to get a specific data model",
}

@mcp.tool(name=namespace_prefix + "load_from_owl_turtle")
def load_from_owl_turtle(owl_turtle_str: str) -> DataModel:
"""Load a data model from an OWL Turtle string. Returns a DataModel object."""
logger.info("Loading a data model from an OWL Turtle string.")
return DataModel.from_owl_turtle_str(owl_turtle_str)

@mcp.tool(name=namespace_prefix + "export_to_owl_turtle")
def export_to_owl_turtle(data_model: DataModel) -> str:
"""Export a data model to an OWL Turtle string. Returns a string."""
logger.info("Exporting a data model to an OWL Turtle string.")
return data_model.to_owl_turtle_str()

@mcp.prompt(title="Create New Data Model")
def create_new_data_model(
Expand Down
113 changes: 113 additions & 0 deletions servers/mcp-neo4j-data-modeling/tests/resources/blueplaques.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
@prefix : <http://voc.neo4j.com/blueplaques#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://voc.neo4j.com/blueplaques> a owl:Ontology .

:COMPOSED a owl:ObjectProperty ;
rdfs:domain :Person ;
rdfs:range :MusicalComposition .

:HONORED_BY a owl:ObjectProperty ;
rdfs:domain :Person ;
rdfs:range :Plaque .

:LOCATED_AT a owl:ObjectProperty ;
rdfs:domain :Plaque ;
rdfs:range :Address .

:addressId a owl:DatatypeProperty ;
rdfs:domain :Address ;
rdfs:range xsd:string .

:area a owl:DatatypeProperty ;
rdfs:domain :Address ;
rdfs:range xsd:string .

:birthYear a owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:integer .

:borough a owl:DatatypeProperty ;
rdfs:domain :Address ;
rdfs:range xsd:string .

:compositionId a owl:DatatypeProperty ;
rdfs:domain :MusicalComposition ;
rdfs:range xsd:string .

:deathYear a owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:integer .

:erectionYear a owl:DatatypeProperty ;
rdfs:domain :Plaque ;
rdfs:range xsd:integer .

:genre a owl:DatatypeProperty ;
rdfs:domain :MusicalComposition ;
rdfs:range xsd:string .

:inscription a owl:DatatypeProperty ;
rdfs:domain :Plaque ;
rdfs:range xsd:string .

:material a owl:DatatypeProperty ;
rdfs:domain :Plaque ;
rdfs:range xsd:string .

:name a owl:DatatypeProperty ;
rdfs:domain :Organization,
:Person ;
rdfs:range xsd:string .

:nationality a owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:string .

:organizationType a owl:DatatypeProperty ;
rdfs:domain :Organization ;
rdfs:range xsd:string .

:personId a owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:string .

:plaqueId a owl:DatatypeProperty ;
rdfs:domain :Plaque ;
rdfs:range xsd:string .

:postcode a owl:DatatypeProperty ;
rdfs:domain :Address ;
rdfs:range xsd:string .

:profession a owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:string .

:professionCategory a owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:string .

:streetAddress a owl:DatatypeProperty ;
rdfs:domain :Address ;
rdfs:range xsd:string .

:title a owl:DatatypeProperty ;
rdfs:domain :MusicalComposition ;
rdfs:range xsd:string .

:yearComposed a owl:DatatypeProperty ;
rdfs:domain :MusicalComposition ;
rdfs:range xsd:integer .

:Organization a owl:Class .

:MusicalComposition a owl:Class .

:Address a owl:Class .

:Plaque a owl:Class .

:Person a owl:Class .
Loading