Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 11 min read

Minimal Practical Ontology Sample with MCP and Claude. A mechanism to pass Business Definitions, not just data, to AI.

The word ontology has suddenly become familiar in recent years. It appears in data platform product descriptions and in discussions of AI agent design. However, I suspect few people have actually worked with one hands-on

The word ontology has suddenly become familiar in recent years. It appears in data platform product descriptions and in discussions of AI agent design. However, I suspect few people have actually worked with one hands-on. In fact, I was one of them.

What you cannot understand through words alone, you must understand by running it. So I built a minimal ontology-like thing using only the bare essentials, and prepared a program that lets Claude act as the orchestrator. The program itself is published on GitHub. In this article, I will describe what that sample is, what it can and cannot do, and what insights emerged from it.

What Ontology Means Here

Let me first define the scope of the term. This is not philosophical ontology, nor OWL from the Semantic Web. What I am dealing with here is the more practical meaning used in the context of business systems.

In this article, ontology refers to something that declares the following three things:

Object types:
The kinds of things that appear in businessβ€”such as customers and ordersβ€”and their attributes.

Link types:
Relationships between thingsβ€”for example, a customer places an order.

Action types:
Operations permitted on thingsβ€”for example, canceling an order. This includes the conditions under which the action can be executed and what changes as a result.

The third one is the key. If you only have attributes and relationships, that is no different from an ER diagram or a schema. When you include what is allowed to be done in the declaration, the definition of data transforms into the definition of business. While a database says the order table has a status column, an ontology says only unshipped orders can be canceled. This difference is the subject of this article.

Let me touch on how this differs from similar concepts. Database input rules and validation ensure that a single piece of data has the correct formβ€”for example, status can only be open, shipped, or cancelled. Business logic holds judgments like if already shipped, never allow cancellation again inside application code.

Both are very important for actual business execution, but the former only looks at the shape of data, while the latter buries judgments deep in code where they become a black box. An ontology can combine both into a single declarationβ€”only unshipped orders can be canceledβ€”and place it outside the code, in a form readable by both humans and AI. The significant difference is that what is being protected is not the database or the application, but the definition of the business itself. This difference in where things are placed is likely the key point that distinguishes the three.

Overview of the Sample Program

The configuration consists of only four elements.

First is the ontology definition. Object types, link types, and action types are written in a single YAML file. This is the main component; everything else is supporting cast.

Second is the instance store. I use PostgreSQL, but there are only two tables. An objects table that holds all types of objects as type, ID, attributes (JSONB), and a links table that holds relationships as type, from, to. Because I do not create a table for each type, adding a type to the YAML does not change the DDL.

Third is the ontology MCP server. It reads the YAML, wraps the database, and exposes only five generic tools to Claude:

  • describe_ontology: Returns a list of types, links, and actions
  • search_objects: Searches by attributes
  • get_object: Retrieves a single item
  • traverse: Follows links
  • execute_action: Validates preconditions, applies effects if passed, and records an audit log

I do not add tools for each type. If I designed it with separate searches for customers and orders, the tools would grow to fifty once types reached ten. By keeping it to five generic tools, the number stays at five no matter how many types are added.

Fourth is the orchestrator. For this project, I leave this entirely to Claude. Simply by registering the MCP server, Claude can call describe_ontology first, understand what is possible, and then operate accordingly.

With Docker Compose, the database and MCP server start up together.

What This Program Can Do

First, exploration. If you ask What types and actions does this ontology have?, Claude will explain the definition directly. For questions like List the Gold customers, Show me all orders from Yamada Trading, or Whose order is O1003?, it combines searching and link traversal to answer.

❯ Show me all orders from Yamada Trading

I will search for Yamada Trading orders from the ontology MCP server.

⏺ ontology - describe_ontology (MCP)
{
  "version": 1,
  "object_types": {
    "Customer": {
      "description": "A trading customer",
      "key": "customer_id",
      "properties": {
        "name": {
          "type": "string",
          "required": true
        },
        "tier": {
          "type": "enum",
          "required": true,
          "values": [
            "bronze",
            "silver",
            "gold"
          ]
        },
        "email": {
          "type": "string",
          "required": false
        }
      }
    },
    "Order": {
      "description": "An order from a customer",
      "key": "order_id",
      "properties": {
        "amount": {
          "type": "integer",
          "required": true
        },
        "status": {
          "type": "enum",
          "required": true,
          "values": [
            "open",
            "shipped",
            "cancelled"
          ]
        },
        "note": {
          "type": "string",
          "required": false
        }
      }
    }
  },
  "link_types": {
    "places": {
      "description": "A customer places an order",
      "from": "Customer",
      "to": "Order",
      "cardinality": "one_to_many"
    }
  },
  "action_types": {
    "cancel_order": {
      "description": "Cancel an unshipped order",
      "target": "Order",
      "parameters": {
        "reason": {
          "type": "string",
          "required": true
        }
      },
      "preconditions": [
        {
          "property": "status",
          "op": "eq",
          "value": "open"
        }
      ],
      "effects": {
        "status": "cancelled",
        "note": "$params.reason"
      }
    },
    "ship_order": {
      "description": "Mark an order as shipped",
      "target": "Order",
      "parameters": {},
      "preconditions": [
        {
          "property": "status",
          "op": "eq",
          "value": "open"
        }
      ],
      "effects": {
        "status": "shipped"
      }
    },
    "upgrade_tier": {
      "description": "Upgrade a customer tier by one level",
      "target": "Customer",
      "parameters": {},
      "preconditions": [
        {
          "property": "tier",
          "op": "ne",
          "value": "gold"
        }
      ],
      "effects": {
        "tier": "$next_enum"
      }
    }
  }
}

⏺ ontology - search_objects (MCP)(type: "Customer", filter: {"name":"Yamada Trading"})
{
  "result": [
    {
      "type": "Customer",
      "id": "C001",
      "name": "Yamada Trading",
      "tier": "gold",
      "email": "[email protected]"
    }
  ]
}

⏺ ontology - traverse (MCP)(type: "Customer", id: "C001", link: "places")
{
  "result": [
    {
      "type": "Order",
      "id": "O1001",
      "amount": 120000,
      "status": "shipped"
    },
    {
      "type": "Order",
      "id": "O1002",
      "amount": 45000,
      "status": "shipped"
    }
  ]
}

Yamada Trading (Customer ID C001, tier gold) has 2 orders.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”
β”‚ Order ID β”‚ Amount  β”‚ Status     β”‚ Note β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€
β”‚ O1001    β”‚ 120,000 β”‚ shipped    β”‚ none β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€
β”‚ O1002    β”‚ 45,000  β”‚ shipped    β”‚ none β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜

The total amount is 165,000. There are no unshipped (open) or cancelled orders.

Next, action execution. If you submit Cancel all unshipped orders from Gold customers due to stock shortage, Claude searches for customers, follows links to gather orders, and executes cancel_order one by one. A human only needs to give this instruction in a single line of natural language.

❯ Cancel all unshipped orders from Gold customers due to stock shortage

I will search for Gold customers and cancel their unshipped (open) orders.

⏺ ontology - search_objects (MCP)(type: "Customer", filter: {"tier":"gold"}, limit: 500)
{
  "result": [
    {
      "type": "Customer",
      "id": "C001",
      "name": "Yamada Trading",
      "tier": "gold",
      "email": "[email protected]"
    },
    {
      "type": "Customer",
      "id": "C003",
      "name": "Suzuki Manufacturing",
      "tier": "gold",
      "email": "[email protected]"
    }
  ]
}

⏺ ontology - traverse (MCP)(type: "Customer", id: "C003", link: "places")
{
  "result": [
    {
      "type": "Order",
      "id": "O1004",
      "amount": 80000,
      "status": "open"
    },
    {
      "type": "Order",
      "id": "O1005",
      "amount": 15000,
      "status": "cancelled"
    }
  ]
}

⏺ ontology - execute_action (MCP)(action: "cancel_order", target_id: "O1004", params: {"reason":"Stock shortage"})
{
  "ok": true,
  "object": {
    "type": "Order",
    "id": "O1004",
    "amount": 80000,
    "status": "cancelled",
    "note": "Stock shortage"
  }
}

There was only 1 unshipped order from Gold customers, and the cancellation is complete.

Status of affected Gold customers and orders

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Customer                β”‚ Order ID β”‚ Amount  β”‚ Before     β”‚ After      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Yamada Trading (C001)   β”‚ O1001    β”‚ 120,000 β”‚ shipped    β”‚ No change  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Yamada Trading (C001)   β”‚ O1002    β”‚ 45,000  β”‚ shipped    β”‚ No change  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Suzuki Manufacturing    β”‚ O1004    β”‚ 80,000  β”‚ open       β”‚ cancelled  β”‚
β”‚ (C003)                  β”‚          β”‚         β”‚            β”‚            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Suzuki Manufacturing    β”‚ O1005    β”‚ 15,000  β”‚ cancelled  β”‚ No change  β”‚
β”‚ (C003)                  β”‚          β”‚         β”‚            β”‚            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The reason "Stock shortage" has been recorded in the note for O1004, and it is also in the audit log. All orders from Yamada Trading were already shipped, so there were no cancellation targets.

And it properly refuses when it understands the situation. For example, if you try to cancel a shipped order, the server-side precondition blocks this action. Claude reports: This order has already been shipped, so it could not be canceled. If you ask for a cancellation without providing a reason, since reason is a required parameter, Claude will ask you for the reason.

❯ Cancel O1001

⏺ ontology - get_object (MCP)(type: "Order", id: "O1001")
{
  "type": "Order",
  "id": "O1001",
  "amount": 120000,
  "status": "shipped"
}

O1001 cannot be canceled. The current status is shipped.

The cancel_order action has a precondition defined that it can only be applied to orders with status open. Even if executed, it would be rejected due to precondition failure, so no changes have been made.

The current state of O1001 is as follows:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Order ID β”‚ Customer              β”‚ Amount  β”‚ Status     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ O1001    β”‚ Yamada Trading (C001) β”‚ 120,000 β”‚ shipped    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

If you need to reverse a shipped order, this ontology does not have actions defined for returns or shipment reversal, so you would need to handle it through a different business workflow.

What I like most is this refusal. The anxiety about letting AI touch business data usually boils down to Will it do something unnecessary? In this mechanism, the means to do unnecessary things simply does not exist. There is no tool to directly rewrite the status; state can only change through actions. It is not safe because the AI is smartβ€”it is safe because the logic side is built to be safe.

What This Program Cannot Do

Let me also clarify what it cannot do. This is a mix of things deliberately omitted to define the scope of the demo and things I simply have not gotten around to.

  • Creating and deleting objects. Register a new customer is not possible. Sample data is loaded via SQL.
  • Creating and deleting links. Link this order to Sato Industries is also not possible.
  • Range searches and partial matching. For orders over 100,000 yen, Claude would have to fetch everything and filter it himself. This breaks down as the number of records grows.
  • Preconditions spanning multiple objects. Conditions that look at linked itemsβ€”like cancellation allowed only if the customer is Goldβ€”cannot be written.
  • Transactions for batch execution. Execution is one item at a time, so if something fails midway, previous items are not rolled back.
  • Permissions. Who executed something is a fixed value. Who can do what is not implemented.

This list is also the list of what to do next. Range searches and preconditions that look at linked items in particular can be added just by slightly expanding the YAML vocabulary. I will fill these in as I feel this is missing while building.

Insights Gained from This Program

It is a small sample, but running it revealed three things.

First: What should be given to AI is not data but the definition of the world. If you give tables and SQL, the AI can do anything. Being able to do anything also means not knowing what it is allowed to do. When you give types and actions, the AI stops hesitating. Watching Claude read the result of describe_ontology and then proceed with exactly the steps I expected is just like someone reading a manual before starting workβ€”literally someone who reads instructions and then acts.

Second: Five generic tools are enough. Initially I considered a design that created tools for each type. I did not do so because if I had to rewrite code every time I added a type to the YAML, there would be no point in separating it into YAML. In fact, I added a Product type and a restock action to the YAML, restarted the container, and Claude began handling the new type. Separating definitions from code makes changes easy and fast, and improves manageability.

Third: Audit logs should be built in from the start as part of actions, not added later. execute_action always leaves one line, whether it succeeds or is blocked by a precondition. Who, on what type, tried to do what, and what happened. In a world where AI operates, no one can explain why this happened without this record. I have not implemented it yet, but once who is added here, it will become nearly the complete audit record needed.

What Is Good About Ontology

Summarizing the discussion so far, the goodness of ontology lies in constraints.

I believe that turning routine work into programs is the original purpose of computers. Having written programs for nearly forty years, that thinking itself has not changed at all. What changed with the arrival of AI is that non-routine parts can now also be delegated to computers. For example: which customers, which orders, in what sequence to process. Things that humans normally think about and do manually can now be thought about by AI.

However, delegation requires boundaries. What is allowed, and what is not. Communicating those boundaries through natural language prompts is unreliable. Rather than asking Please do not cancel shipped orders, it is more certain to build it so cancellation is impossible. An ontology is a mechanism that writes those boundaries as declarations and has the system enforce them.

There is another good thing. When you write boundaries, the business becomes visible. To write a single line like only unshipped orders can be canceled, you need to confirm with the business people. Conversations emerge: Are there cases where shipped orders can also be canceled? Is that a different action called a return? This conversation is the most important and most easily skipped part when building systems. A single YAML can also become a tool to draw out that conversation. Also, this YAML itself could be called a business design document.

I believe the job of an architect is not to choose technology, but to express business as structure. An ontology is a place to put that structure in a form AI can read. That is why, even in the age of AIβ€”no, precisely because it is the age of AIβ€”I believe this tool will be effective.

Something to Try

If you have business data at hand, forget the table definitions for now and write out these three things on paper:

  • What kinds of things appear there?
  • What words connect things to things?
  • For those things, who, under what conditions, is allowed to do what?

If you can write the third one, that is already an ontology. If you cannot write it, that is where the business is ambiguous. Either way, you gain something.

The sample is on GitHub. Start it up with Docker Compose and ask Claude: Cancel all unshipped orders from Gold customers. And watch the shipped orders get properly refused. All the value of this mechanism is packed into that moment.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.