Skip to content

Build Your Own Modeling Tool with Linked.Archi

The Build Your Own Metamodel guide ends with a section called "How a Modeling Tool Consumes This" — seven bullet points and a bootstrap query. This article picks up where that one stops. It shows how to build a working modeling tool that loads a Linked.Archi metamodel and provides a palette, canvas, validation, viewpoint filtering, document generation, and AI agent access.

The Cloud Platform Walkthrough provides the worked metamodel example. Everything here uses that metamodel as the running scenario.


What You Are Building

A Linked.Archi-aware modeling tool has seven components:

┌─────────────────────────────────────────────────────────────────────┐
│                        Modeling Tool                                │
│                                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────────────────┐  │
│  │ 1. Metamodel │  │ 2. Palette   │  │ 3. Canvas / Editor        │  │
│  │    Loader    │──│    Builder   │──│    (elements + edges)     │  │
│  └──────┬───────┘  └──────────────┘  └───────────┬───────────────┘  │
│         │                                        │                  │
│  ┌──────┴───────┐  ┌──────────────┐  ┌───────────┴───────────────┐  │
│  │ 4. SHACL     │  │ 5. Viewpoint │  │ 6. Document               │  │
│  │    Validator  │  │    Filter    │  │    Generator              │  │
│  └──────────────┘  └──────────────┘  └───────────────────────────┘  │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │ 7. AI Agent Access (MCP)                                     │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │ RDF Store (Oxigraph / Comunica / rdflib)                     │   │
│  └──────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

You do not need all seven on day one. The minimum viable tool is components 1–3. Add the rest incrementally.


Technology Choices

Two proven stacks, depending on your team:

Component TypeScript / Browser Python / Server
RDF store Oxigraph WASM or Comunica rdflib + Oxigraph (via oxrdflib)
SPARQL engine Oxigraph WASM or Comunica rdflib or Oxigraph server
SHACL validator rdf-validate-shacl pyshacl
Templating Handlebars.js pybars3 or Jinja2
UI framework React / Svelte / vanilla Streamlit / Flask / none (CLI)
Graph visualization D3.js / Cytoscape.js / ReactFlow NetworkX + matplotlib / Graphviz
MCP server @modelcontextprotocol/sdk (TypeScript) mcp (Python)

The Linked.Archi ecosystem uses both: the MCP server is TypeScript + Oxigraph, the static navigator is React + Comunica, and the validation pipeline is Java + RDF4J. Pick what fits your team.

This guide shows both stacks side by side where the patterns differ.


Component 1: Metamodel Loader

The metamodel manifest (arch:Metamodel) is the single entry point. Load it, follow the properties, and you have the complete modeling language.

The Bootstrap Query

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?property ?value WHERE {
    ?metamodel a arch:Metamodel ;
               skos:prefLabel ?label .
    ?metamodel ?property ?value .
    VALUES ?property {
        arch:modelConcepts
        arch:formalRules
        arch:architectureViewpoints
        arch:conceptClassification
        arch:hasDeliverableTemplate
        arch:referenceData
        arch:presentationContextScheme
        arch:derivationRules
        arch:referenceModels
        arch:architectureViewpointsRestrictions
    }
}

For the Cloud Platform example, this returns:

property value
arch:modelConcepts <https://meta.linked.archi/examples/cloudplatform/onto#>
arch:formalRules <https://meta.linked.archi/examples/cloudplatform/shapes#>
arch:architectureViewpoints <https://meta.linked.archi/examples/cloudplatform/viewpoints#>
arch:conceptClassification cptax:CloudPlatformConceptScheme
arch:hasDeliverableTemplate cpdt:ServiceCatalog, cpdt:DeploymentGuide
arch:referenceData cprd:CloudPlatformReferenceData

TypeScript Implementation

import { Store, Parser } from 'n3';
import { QueryEngine } from '@comunica/query-sparql';

async function loadMetamodel(files: string[]): Promise<Store> {
  const store = new Store();
  const parser = new Parser();

  for (const file of files) {
    const ttl = await fs.readFile(file, 'utf-8');
    const quads = parser.parse(ttl);
    store.addQuads(quads);
  }
  return store;
}

async function bootstrap(store: Store) {
  const engine = new QueryEngine();
  const result = await engine.queryBindings(`
    PREFIX arch: <https://meta.linked.archi/core#>
    SELECT ?metamodel ?label ?property ?value WHERE {
      ?metamodel a arch:Metamodel ; skos:prefLabel ?label .
      ?metamodel ?property ?value .
      VALUES ?property {
        arch:modelConcepts arch:formalRules
        arch:architectureViewpoints arch:conceptClassification
        arch:hasDeliverableTemplate arch:referenceData
        arch:presentationContextScheme
      }
    }
  `, { sources: [store] });

  const bindings = await result.toArray();
  // Group by property → build a MetamodelDescriptor
  return groupByProperty(bindings);
}

Python Implementation

from rdflib import Graph, Namespace

ARCH = Namespace("https://meta.linked.archi/core#")

def load_metamodel(files: list[str]) -> Graph:
    g = Graph()
    for f in files:
        g.parse(f, format="turtle")
    return g

def bootstrap(g: Graph) -> dict:
    query = """
    PREFIX arch: <https://meta.linked.archi/core#>
    PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
    SELECT ?metamodel ?label ?property ?value WHERE {
        ?metamodel a arch:Metamodel ; skos:prefLabel ?label .
        ?metamodel ?property ?value .
        VALUES ?property {
            arch:modelConcepts arch:formalRules
            arch:architectureViewpoints arch:conceptClassification
            arch:hasDeliverableTemplate arch:referenceData
            arch:presentationContextScheme
        }
    }
    """
    results = g.query(query)
    return group_by_property(results)

What to Load

Load files in dependency order. The metamodel manifest imports the core ontology, which imports nothing. The ontology imports ArchiMate 4.0 (if extending it). Load them all into one graph:

core/core-onto.ttl                              ← always
modelingLanguages/archimate/4.0/archimate4-onto.ttl  ← if extending ArchiMate
examples/custom-metamodel/cloudplatform-onto.ttl      ← your metamodel
examples/custom-metamodel/cloudplatform-metamodel.ttl ← the manifest
examples/custom-metamodel/cloudplatform-tax.ttl       ← taxonomy
examples/custom-metamodel/cloudplatform-shapes.ttl    ← SHACL shapes
examples/custom-metamodel/cloudplatform-viewpoints.ttl
examples/custom-metamodel/cloudplatform-deliverable-templates.ttl
examples/custom-metamodel/cloudplatform-reference-data.ttl

Then load the user's model data on top:

examples/custom-metamodel/example-model.ttl      ← the actual architecture model

Component 2: Palette Builder

The palette is the list of element types and relationship types the user can place on the canvas. It comes entirely from the ontology — no hardcoding.

Discovering Element Types

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?class ?label ?definition ?icon ?parent WHERE {
    ?class rdfs:subClassOf+ arch:Element ;
           skos:prefLabel ?label .
    OPTIONAL { ?class skos:definition ?definition }
    OPTIONAL { ?class arch:prefVisNotation ?icon }
    OPTIONAL { ?class rdfs:subClassOf ?parent .
               ?parent rdfs:subClassOf+ arch:Element }
    FILTER(lang(?label) = "en" || lang(?label) = "")
}
ORDER BY ?parent ?label

For the Cloud Platform metamodel, this returns Microservice, APIGateway, Container, KubernetesCluster, MessageBroker, EventTopic, ManagedDatabase, CacheStore, ObservabilityStack — plus all inherited ArchiMate 4.0 types.

Discovering Relationship Types

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?class ?label ?predicate ?sourceDomain ?targetRange WHERE {
    ?class rdfs:subClassOf arch:QualifiedRelationship ;
           skos:prefLabel ?label ;
           arch:unqualifiedForm ?predicate .
    OPTIONAL { ?predicate arch:domainIncludes ?sourceDomain }
    OPTIONAL { ?predicate arch:rangeIncludes ?targetRange }
    FILTER(lang(?label) = "en" || lang(?label) = "")
}

This tells the tool: "A Dependency can go from a Microservice to an ApplicationComponent." The tool uses this to enable/disable connection handles on the canvas.

Grouping by Taxonomy

The SKOS taxonomy provides palette groups. Elements link to taxonomy concepts via rdfs:seeAlso:

PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?group ?groupLabel ?class ?classLabel WHERE {
    ?class rdfs:seeAlso ?group .
    ?group a skos:Concept ;
           skos:prefLabel ?groupLabel .
    ?class skos:prefLabel ?classLabel .
    FILTER(lang(?groupLabel) = "en" || lang(?groupLabel) = "")
    FILTER(lang(?classLabel) = "en" || lang(?classLabel) = "")
}
ORDER BY ?groupLabel ?classLabel

Result for the Cloud Platform metamodel:

Group Elements
Runtime Services Microservice, APIGateway, ManagedDatabase, CacheStore
Integration MessageBroker, EventTopic
Infrastructure Container, KubernetesCluster
Observability ObservabilityStack

Building the Palette Data Structure

interface PaletteGroup {
  iri: string;
  label: string;
  elements: PaletteItem[];
}

interface PaletteItem {
  iri: string;        // e.g., cp:Microservice
  label: string;      // "Microservice"
  definition: string; // tooltip text
  icon: string;       // SVG URL from arch:prefVisNotation
  parentClass: string; // e.g., am4:ApplicationComponent
}

interface RelationshipType {
  iri: string;           // e.g., cp:Dependency
  label: string;         // "Dependency"
  predicate: string;     // cp:dependsOn
  validSources: string[]; // [cp:Microservice]
  validTargets: string[]; // [am4:ApplicationComponent]
}

Filtering: Show Only Your Custom Types

If you extend ArchiMate 4.0 but only want to show your custom types in the palette (not all 55 ArchiMate types), filter by namespace:

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?class ?label WHERE {
    ?class rdfs:subClassOf+ arch:Element ;
           skos:prefLabel ?label .
    FILTER(STRSTARTS(STR(?class), "https://meta.linked.archi/examples/cloudplatform/"))
}

Or show both, but visually separate them — custom types in the primary palette, inherited types in a collapsible "ArchiMate 4.0" section.


Component 3: Canvas / Editor

The canvas is where users place elements and draw relationships. This is the most UI-intensive component and the one where technology choice matters most.

Library Strengths Best For
ReactFlow React-native, handles, minimap, built-in layout Web-based tools with React
Cytoscape.js Graph-focused, many layouts, extensions Graph-heavy tools
D3.js Full control, force layouts Custom visualizations
JointJS Diagramming-focused, ports, links Traditional diagramming tools

Creating Model Data

When a user drags a Microservice from the palette onto the canvas, the tool creates RDF triples:

@prefix cp:   <https://meta.linked.archi/examples/cloudplatform/onto#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix :     <https://model.example.com/my-platform#> .

:OrderService a cp:Microservice ;
    skos:prefLabel "Order Service"@en ;
    cp:resilienceLevel "critical" ;
    cp:deploymentStrategy "blue-green" ;
    cp:serviceTier "tier-1" .

Creating Relationships

When a user draws an edge from OrderService to PaymentService, the tool creates both the direct triple and the qualified relationship:

# Direct triple — for graph traversal
:OrderService cp:dependsOn :PaymentService .

# Qualified relationship — for metadata and management
:OrderService cp:qualifiedDependsOn :dep-order-payment .
:dep-order-payment a cp:Dependency ;
    arch:source :OrderService ;
    arch:target :PaymentService ;
    skos:prefLabel "Order → Payment dependency"@en .

Connection Validation

Before allowing a user to complete an edge, check whether the relationship type permits that source→target combination. Use the arch:domainIncludes / arch:rangeIncludes data from the palette query:

function canConnect(
  sourceType: string,
  targetType: string,
  relationshipType: RelationshipType,
  classHierarchy: Map<string, string[]>  // class → superclasses
): boolean {
  const sourceValid = relationshipType.validSources.some(
    s => sourceType === s || classHierarchy.get(sourceType)?.includes(s)
  );
  const targetValid = relationshipType.validTargets.some(
    t => targetType === t || classHierarchy.get(targetType)?.includes(t)
  );
  return sourceValid && targetValid;
}

This uses the class hierarchy — a Microservice is a valid source for any relationship that accepts ApplicationComponent, because cp:Microservice rdfs:subClassOf am4:ApplicationComponent.

Property Panel

When the user selects an element, show a property panel. The properties come from the ontology:

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?property ?label ?range WHERE {
    ?property arch:domainIncludes ?elementType ;
              skos:prefLabel ?label .
    OPTIONAL { ?property rdfs:range ?range }
    FILTER(lang(?label) = "en" || lang(?label) = "")
}

For cp:Microservice, this returns resilienceLevel, deploymentStrategy, serviceTier — each with its datatype. The SHACL shapes provide the allowed values (sh:in) for dropdowns.

Extracting Allowed Values from SHACL

PREFIX sh:   <http://www.w3.org/ns/shacl#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?property ?allowedValue WHERE {
    ?shape sh:targetClass ?elementType ;
           sh:property ?propShape .
    ?propShape sh:path ?property ;
               sh:in/rdf:rest*/rdf:first ?allowedValue .
}

For cp:resilienceLevel on cp:Microservice, this returns: "critical", "high", "medium", "low" — ready for a dropdown.


Component 4: Live SHACL Validation

SHACL shapes turn the metamodel's rules into real-time feedback — red squiggles when a required property is missing, warnings when a relationship has an invalid target.

How It Works

  1. The user edits the model (adds an element, changes a property, draws an edge).
  2. The tool serializes the current model graph.
  3. The SHACL engine validates the model graph against the shapes graph.
  4. Validation results are mapped back to canvas elements as errors/warnings.

TypeScript: rdf-validate-shacl

import SHACLValidator from 'rdf-validate-shacl';
import { DataFactory } from 'n3';

async function validate(
  dataStore: Store,
  shapesStore: Store
): Promise<ValidationResult[]> {
  const validator = new SHACLValidator(shapesStore, { factory: DataFactory });
  const report = validator.validate(dataStore);

  return report.results.map(r => ({
    focusNode: r.focusNode?.value,       // the element IRI
    path: r.path?.value,                  // the property that failed
    message: r.message?.[0]?.value,       // human-readable message
    severity: r.severity?.value,          // sh:Violation, sh:Warning, sh:Info
    sourceShape: r.sourceShape?.value,    // which shape triggered this
  }));
}

Python: pyshacl

from pyshacl import validate as shacl_validate

def validate(data_graph, shapes_graph):
    conforms, results_graph, results_text = shacl_validate(
        data_graph,
        shacl_graph=shapes_graph,
        inference='none',
        abort_on_first=False,
    )
    # Parse results_graph for individual violations
    return parse_validation_results(results_graph)

Mapping Results to the Canvas

Each validation result has a focusNode — the IRI of the element that failed. Map this back to the canvas node and show the error:

function applyValidationResults(
  canvas: CanvasState,
  results: ValidationResult[]
) {
  // Clear previous markers
  canvas.clearAllMarkers();

  for (const result of results) {
    const node = canvas.findNodeByIRI(result.focusNode);
    if (!node) continue;

    if (result.severity.endsWith('Violation')) {
      node.addMarker('error', result.message);
    } else if (result.severity.endsWith('Warning')) {
      node.addMarker('warning', result.message);
    } else {
      node.addMarker('info', result.message);
    }
  }
}

When to Validate

  • On every edit — if the model is small (< 500 elements). SHACL validation is fast for small graphs.
  • On save — if the model is large. Debounce to avoid blocking the UI.
  • On demand — a "Validate" button that runs the full check.

For the CI/CD pipeline, the existing .scripts/validate.sh handles batch validation using RDF4J:

.scripts/validate.sh --shacl example-model.ttl cloudplatform-shapes.ttl

What the Cloud Platform Shapes Catch

The cloudplatform-shapes.ttl enforces rules like:

  • Every Microservice must have a skos:prefLabel, resilienceLevel
  • resilienceLevel must be one of: critical, high, medium, low
  • deploymentStrategy must be one of: blue-green, canary, rolling, recreate
  • Every Container must have a containerImage and positive replicaCount
  • Every Dependency must have arch:source and arch:target
  • Critical microservices must be tier-1 (conditional rule)

Component 5: Viewpoint Filter

Viewpoints restrict which element and relationship types are visible in a given view. When the user opens a "Service Dependency View", only the relevant types appear in the palette and on the canvas.

Discovering Viewpoints

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?viewpoint ?label ?definition ?viewType WHERE {
    ?viewpoint a arch:Viewpoint ;
               skos:prefLabel ?label .
    OPTIONAL { ?viewpoint skos:definition ?definition }
    OPTIONAL { ?viewpoint arch:viewType ?viewType }
    FILTER(lang(?label) = "en" || lang(?label) = "")
}

For the Cloud Platform metamodel, this returns four viewpoints: Service Dependency, Deployment, Data Flow, Platform Overview.

Getting Allowed Concepts for a Viewpoint

PREFIX arch: <https://meta.linked.archi/core#>

SELECT ?concept WHERE {
    <viewpoint-iri> arch:includesConcept ?concept .
}

The Service Dependency viewpoint includes: cp:Microservice, cp:APIGateway, am4:ApplicationComponent, cp:Dependency. Only these types appear in the palette when this viewpoint is active.

Filtering the Palette

function filterPalette(
  fullPalette: PaletteGroup[],
  allowedConcepts: Set<string>,
  classHierarchy: Map<string, string[]>
): PaletteGroup[] {
  return fullPalette
    .map(group => ({
      ...group,
      elements: group.elements.filter(el =>
        allowedConcepts.has(el.iri) ||
        classHierarchy.get(el.iri)?.some(parent => allowedConcepts.has(parent))
      ),
    }))
    .filter(group => group.elements.length > 0);
}

Filtering the Canvas

Elements already on the canvas that don't belong to the active viewpoint can be dimmed or hidden:

function applyViewpointFilter(
  canvas: CanvasState,
  allowedConcepts: Set<string>
) {
  for (const node of canvas.nodes) {
    const isAllowed = allowedConcepts.has(node.type) ||
      classHierarchy.get(node.type)?.some(p => allowedConcepts.has(p));
    node.setOpacity(isAllowed ? 1.0 : 0.2);
  }
}

Viewpoint Metadata for the UI

Viewpoints also carry stakeholder and concern information — useful for a viewpoint picker:

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?viewpoint ?vpLabel ?stakeholder ?shLabel ?concern ?cLabel WHERE {
    ?viewpoint a arch:Viewpoint ;
               skos:prefLabel ?vpLabel .
    OPTIONAL {
        ?viewpoint arch:targetsStakeholder ?stakeholder .
        ?stakeholder skos:prefLabel ?shLabel .
    }
    OPTIONAL {
        ?viewpoint arch:viewpointFramesConcern ?concern .
        ?concern skos:prefLabel ?cLabel .
    }
}

This lets you build a viewpoint picker that says: "Service Dependency View — for Developers and Architects — addresses Dependency Management concern."


Component 6: Document Generator

The document generation pipeline is fully declarative — the ontology defines what to extract, how to structure it, and how to render it. The tool contains no domain-specific logic.

For the full specification, see the Deliverable Generator Spec. This section shows how to implement it.

The Pipeline

arch:DeliverableTemplate
  └─ arch:TemplateSection (ordered by arch:sectionOrder)
       ├─ arch:sectionQuery      → SPARQL SELECT
       ├─ arch:sectionResultKey  → JSON key name
       └─ arch:sectionViewpoint  → which viewpoint this section covers

arch:templateResource → path to .md.hbs Handlebars template

Step 1: Discover Template Sections

PREFIX arch: <https://meta.linked.archi/core#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?section ?order ?title ?resultKey ?query WHERE {
    <template-iri> arch:templateHasSection ?section .
    ?section arch:sectionOrder ?order ;
             skos:prefLabel ?title ;
             arch:sectionResultKey ?resultKey ;
             arch:sectionQuery ?query .
}
ORDER BY ?order

Step 2: Execute Queries and Assemble Context

import Handlebars from 'handlebars';

async function generateDocument(
  templateIRI: string,
  store: Store,
  meta: Record<string, string>
): Promise<string> {
  // 1. Get sections
  const sections = await querySections(store, templateIRI);

  // 2. Execute each section's SPARQL query
  const context: Record<string, any> = { meta };
  for (const section of sections) {
    const results = await executeSPARQL(store, section.query);
    context[section.resultKey] = results;
  }

  // 3. Load and render the Handlebars template
  const templatePath = await getTemplateResource(store, templateIRI);
  const templateSource = await fs.readFile(templatePath, 'utf-8');
  const template = Handlebars.compile(templateSource);

  return template(context);
}

Step 3: Render

The Cloud Platform metamodel includes two deliverable templates:

Service Catalog — lists all microservices with their properties:

# Service Catalog

Generated: {{meta.date}}

## Microservices

| Service | Resilience | Deployment | Tier |
|---------|-----------|------------|------|
{{#each microservices}}
| {{this.label}} | {{this.resilience}} | {{this.deployment}} | {{this.tier}} |
{{/each}}

## Dependencies

| Source | Target | Description |
|--------|--------|-------------|
{{#each dependencies}}
| {{this.sourceLabel}} | {{this.targetLabel}} | {{this.description}} |
{{/each}}

Deployment Guide — infrastructure topology:

# Deployment Guide

## Kubernetes Clusters

{{#each clusters}}
### {{this.label}}

- Region: {{this.region}}
- Node count: {{this.nodeCount}}

#### Containers

| Container | Image | Replicas |
|-----------|-------|----------|
{{#each this.containers}}
| {{this.label}} | {{this.image}} | {{this.replicas}} |
{{/each}}

{{/each}}

Output Formats

The arch:templateHasFormat property declares supported formats. For Markdown → HTML, use a Markdown renderer. For Markdown → PDF, pipe through Pandoc or WeasyPrint:

# Markdown → HTML
npx marked service-catalog.md -o service-catalog.html

# Markdown → PDF
pandoc service-catalog.md -o service-catalog.pdf

Component 7: AI Agent Access (MCP)

The Model Context Protocol (MCP) exposes the knowledge graph to AI agents. The Linked.Archi MCP server (ai-enablement/linked-archi-mcp/) provides a reference implementation using TypeScript + Oxigraph.

What the MCP Server Exposes

Tool What it does
query-graph Execute arbitrary SPARQL against the model
list-elements List all elements with type and label
get-element Get full details for one element
get-relationships Get all relationships for an element
graph-stats Summary statistics (element counts by type, relationship counts)
Prompt What it does
impact-analysis Trace how a change to one element propagates
architecture-overview Generate a natural-language summary of the model
explore-ontology Explain the metamodel structure

Building Your Own MCP Server

If your modeling tool has its own graph store, you can expose it via MCP:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const server = new McpServer({
  name: 'my-architecture-tool',
  version: '1.0.0',
});

server.tool('query-graph', { query: z.string() }, async ({ query }) => {
  const results = await sparqlEngine.query(query);
  return { content: [{ type: 'text', text: JSON.stringify(results) }] };
});

server.tool('list-elements', {}, async () => {
  const results = await sparqlEngine.query(`
    PREFIX arch: <https://meta.linked.archi/core#>
    PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
    SELECT ?element ?label ?type WHERE {
      ?element a ?type ; skos:prefLabel ?label .
      ?type rdfs:subClassOf+ arch:Element .
    }
  `);
  return { content: [{ type: 'text', text: JSON.stringify(results) }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);

Why This Matters

With MCP, an AI agent can:

  • "What microservices depend on the Payment Service?" → query-graph with a SPARQL query
  • "Show me all critical services without a deployment strategy" → query-graph with a SHACL-like filter
  • "Generate an impact analysis for removing the Order Database" → impact-analysis prompt
  • "What's the overall health of this architecture?" → architecture-overview prompt

The agent queries the same graph the tool uses — no separate export or sync needed.


Presentation Contexts: Stakeholder-Specific Rendering

Presentation contexts are SKOS concepts that define how the same model looks for different audiences. They are not a separate component — they influence how components 2, 3, 5, and 6 behave.

How They Work

The metamodel declares a arch:presentationContextScheme — a SKOS ConceptScheme with concepts like Executive, Architect, Developer, Operations, Governance.

Each context implies:

Aspect Executive Developer Architect
Palette Business elements only Application + Technology All domains
Detail level Labels only Full properties Full properties + relationships
Viewpoints Strategy, Capability Service Dependency, Deployment All viewpoints
Documents Executive Summary Service Catalog, Deployment Guide Architecture Definition
Visual style Simplified icons, large labels Technical icons, code-style Full ArchiMate notation

Querying Presentation Contexts

PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?context ?label ?definition WHERE {
    ?context skos:topConceptOf ?scheme ;
             skos:prefLabel ?label ;
             skos:definition ?definition .
    FILTER(lang(?label) = "en" || lang(?label) = "")
}

Applying a Context

The tool reads the context's skos:definition to determine filtering rules. A more structured approach adds custom properties to each context:

:DeveloperContext
    a skos:Concept ;
    skos:prefLabel "Developer / Engineering"@en ;
    arch:contextIncludesDomain :ApplicationDomain, :TechnologyDomain ;
    arch:contextDefaultViewpoint :ServiceDependencyViewpoint ;
    arch:contextDetailLevel "full" ;
    arch:contextDeliverableTemplate :ServiceCatalog, :DeploymentGuide .

The tool queries these properties and adjusts the palette, default viewpoint, and available documents accordingly.


Putting It All Together: The Minimal Viable Tool

Here is the simplest possible tool — a CLI that loads a metamodel, validates a model, and generates a document:

// minimal-tool.ts
import { Store, Parser } from 'n3';
import SHACLValidator from 'rdf-validate-shacl';
import Handlebars from 'handlebars';
import { QueryEngine } from '@comunica/query-sparql';

async function main() {
  const store = new Store();
  const parser = new Parser();
  const engine = new QueryEngine();

  // 1. Load metamodel + model
  const files = [
    'core/core-onto.ttl',
    'examples/custom-metamodel/cloudplatform-onto.ttl',
    'examples/custom-metamodel/cloudplatform-metamodel.ttl',
    'examples/custom-metamodel/cloudplatform-shapes.ttl',
    'examples/custom-metamodel/cloudplatform-deliverable-templates.ttl',
    'examples/custom-metamodel/example-model.ttl',
  ];
  for (const file of files) {
    const ttl = await Bun.file(file).text();
    store.addQuads(parser.parse(ttl));
  }

  // 2. Validate
  const shapesStore = new Store();
  const shapesTtl = await Bun.file(
    'examples/custom-metamodel/cloudplatform-shapes.ttl'
  ).text();
  shapesStore.addQuads(parser.parse(shapesTtl));

  const validator = new SHACLValidator(shapesStore);
  const report = validator.validate(store);
  if (!report.conforms) {
    console.error('Validation errors:');
    for (const r of report.results) {
      console.error(`  ${r.focusNode?.value}: ${r.message?.[0]?.value}`);
    }
  } else {
    console.log('Model is valid.');
  }

  // 3. Generate document
  const queryResult = await engine.queryBindings(`
    PREFIX cp:   <https://meta.linked.archi/examples/cloudplatform/onto#>
    PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
    SELECT ?label ?resilience ?deployment ?tier WHERE {
      ?ms a cp:Microservice ;
          skos:prefLabel ?label ;
          cp:resilienceLevel ?resilience .
      OPTIONAL { ?ms cp:deploymentStrategy ?deployment }
      OPTIONAL { ?ms cp:serviceTier ?tier }
    } ORDER BY ?label
  `, { sources: [store] });

  const microservices = (await queryResult.toArray()).map(b => ({
    label: b.get('label')?.value,
    resilience: b.get('resilience')?.value,
    deployment: b.get('deployment')?.value ?? '—',
    tier: b.get('tier')?.value ?? '—',
  }));

  const template = Handlebars.compile(`# Service Catalog

| Service | Resilience | Deployment | Tier |
|---------|-----------|------------|------|
{{#each microservices}}
| {{this.label}} | {{this.resilience}} | {{this.deployment}} | {{this.tier}} |
{{/each}}
`);

  const output = template({ microservices });
  console.log(output);
}

main();

Run it:

npx tsx minimal-tool.ts

Output:

# Service Catalog

| Service | Resilience | Deployment | Tier |
|---------|-----------|------------|------|
| API Gateway | high | blue-green | tier-1 |
| Inventory Service | medium | rolling | tier-2 |
| Order Service | critical | blue-green | tier-1 |
| Payment Service | critical | canary | tier-1 |
| Shipping Service | medium | rolling | tier-2 |

From here, add a web UI (ReactFlow for the canvas, a sidebar for the palette, a panel for properties) and you have a visual modeling tool.


Incremental Build Path

Phase Components Effort What You Get
1. CLI Loader + Validator + Generator 1–2 days Validate models and generate documents from the command line
2. Browser + Palette + Canvas (ReactFlow) 1–2 weeks Visual editor with drag-and-drop elements and edges
3. Smart Editor + Viewpoint Filter + Property Panel 1 week Viewpoint-filtered palette, property editing with dropdowns from SHACL
4. Full Tool + Presentation Contexts + MCP 1 week Stakeholder-specific views and AI agent integration

Each phase produces a usable tool. You don't need to build everything before shipping.


How This Relates to Existing Linked.Archi Tools

The Linked.Archi toolbox already implements several of these components:

Existing Tool Components It Implements
Static Navigator (React + Comunica) Palette (type facets), Canvas (D3 graph), SPARQL workbench
MCP Server (TypeScript + Oxigraph) AI Agent Access (all 5 tools + 3 prompts)
Validation Script (Java + RDF4J) SHACL Validator (batch mode)
Deliverable Generator Document Generator (full pipeline spec)

A new modeling tool can reuse these directly or use them as reference implementations.


References