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
14 changes: 11 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

- launch semantic-code-mcp tool load_rust_code with path `rust/` at startup.

- When exploring codebase, use semantic-code-mcp that will contains all symbols of the code.

## Project

Oxigraph MCP Tools — a Rust MCP server that exposes an Oxigraph RDF triplestore via stdio JSON-RPC. It provides generic SPARQL/RDF tools plus per-language code-loading tools that parse source code into an RDF knowledge graph.
Expand Down Expand Up @@ -33,10 +35,15 @@ rust/src/
├── tools/
│ ├── sparql.rs # sparql_query, sparql_update — pure sync functions
│ ├── rdf.rs # load_rdf, list_graphs — pure sync functions
│ └── code.rs # load_code dispatcher, load_rust_code wrapper — pure sync functions
│ ├── code.rs # load_code dispatcher, load_rust_code wrapper — pure sync functions
│ ├── git.rs # load_git_history — pure sync function
│ └── ansible.rs # load_inventory, load_ansible — pure sync functions
└── loaders/
├── mod.rs # LanguageLoader trait, LoaderRegistry, discover_files()
└── rust.rs # RustLoader — parses Cargo.toml + syn AST for .rs files
├── rust.rs # RustLoader — parses Cargo.toml + syn AST for .rs files
├── typescript.rs # TypeScriptLoader — parses package.json + oxc AST
├── git.rs # Git commit graph + file changes parser
└── ansible.rs # Ansible inventory/playbook/role parser (INI + YAML)
```

**Key design patterns:**
Expand All @@ -45,8 +52,9 @@ rust/src/
- `LanguageLoader` trait returns `Vec<Quad>` (graph name baked in). `LoaderRegistry` maps language IDs to loaders.
- Language auto-detection: marker files for dirs (`Cargo.toml`, `package.json`, etc.), file extensions for single files.
- RDF namespace: `https://ds-labs.org/code#` (`CODE_NS` in loaders). All code and git loaders write to the default graph.
- Ansible loader uses `https://ds-labs.org/ansible#` (`ANS_NS`) and ICAS `http://www.invincea.com/ontologies/icas/1.0/host#` (`HOST_NS`) namespaces. Standalone tool pattern (not a LanguageLoader).

**Key crates:** oxigraph 0.5.x (store + SPARQL), rmcp 0.16.x (MCP SDK), syn 2 (Rust AST parsing), sparesults 0.3 (SPARQL result serialization), schemars 1 (JSON Schema for tool params).
**Key crates:** oxigraph 0.5.x (store + SPARQL), rmcp 0.16.x (MCP SDK), syn 2 (Rust AST parsing), sparesults 0.3 (SPARQL result serialization), schemars 1 (JSON Schema for tool params), serde_yaml 0.9 (Ansible YAML parsing).

## Test Patterns

Expand Down
178 changes: 168 additions & 10 deletions SPECIFICATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ Claude Code <──stdio──> MCP Server (Rust) <──native API──> O
│ │
│ └── LanguageLoader trait (plugin system)
└── Git history tools
└── load_git_history
├── Git history tools
│ └── load_git_history
└── Ansible infrastructure tools
├── load_inventory
└── load_ansible
```

- **Transport**: stdio (stdin/stdout JSON-RPC)
Expand Down Expand Up @@ -307,7 +311,159 @@ SELECT ?hash ?msg ?fname WHERE {
}
```

## 5. Plugin System — LanguageLoader Trait
## 5. Ansible Infrastructure Loading Tools

### 5.1 Purpose

The Ansible loading tools parse Ansible infrastructure-as-code artifacts — inventory files, playbooks, roles, and variable files — into RDF triples. This enables SPARQL queries over infrastructure topology, deployment automation, and host configuration alongside code and architecture data.

**Cross-ontology integration:** Inventory hosts are typed as ICAS `host:Host`, enabling joins with C4 deployment nodes, monitoring probes, and hardening reports from the ds-reporting ontology stack.

**Single graph model:** Like all other loaders, Ansible triples are stored in the **default graph**.

### 5.2 Namespaces

| Prefix | URI | Source |
|--------|-----|--------|
| `ans:` | `https://ds-labs.org/ansible#` | Ansible-specific classes and properties |
| `host:` | `http://www.invincea.com/ontologies/icas/1.0/host#` | ICAS host identity (shared across ontologies) |

### 5.3 RDF Ontology for Ansible

#### Classes

| Class | Description |
|-------|-------------|
| `host:Host` | Inventory host (ICAS, enables cross-ontology joins) |
| `ans:HostGroup` | Group of hosts (e.g., `[webservers]`) |
| `ans:Inventory` | Inventory file/directory |
| `ans:Variable` | Key-value variable |
| `ans:Playbook` | Playbook YAML file |
| `ans:Play` | Play within a playbook (`- hosts:` block) |
| `ans:Task` | Task within a play or role |
| `ans:Role` | Ansible role |
| `ans:Handler` | Notified handler |
| `ans:Template` | Jinja2 template file |

#### Properties

| Property | Domain → Range | Description |
|----------|----------------|-------------|
| `host:hostName` | Host → xsd:string | Hostname from inventory |
| `ans:ansibleHost` | Host → xsd:string | `ansible_host` connection address |
| `ans:memberOf` | Host → HostGroup | Host-to-group membership |
| `ans:hasHost` | HostGroup → Host | Group contains host |
| `ans:childGroup` | HostGroup → HostGroup | Group hierarchy |
| `ans:hasVariable` | Host/Group/Role → Variable | Variable attachment |
| `ans:variableName` | Variable → xsd:string | Variable key |
| `ans:variableValue` | Variable → xsd:string | Variable value |
| `ans:hasPlay` | Playbook → Play | Play containment |
| `ans:targetHosts` | Play → xsd:string | Hosts pattern |
| `ans:hasTask` | Play/Role → Task | Task containment |
| `ans:module` | Task/Handler → xsd:string | Ansible module name |
| `ans:usesRole` | Play → Role | Role inclusion |
| `ans:dependsOn` | Role → Role | Role dependency |
| `ans:hasHandler` | Play/Role → Handler | Handler containment |
| `ans:hasTemplate` | Role → Template | Template containment |
| `ans:name` | any → xsd:string | Entity name |
| `ans:sourceFile` | any → xsd:string | Source file path (relative) |

#### URI Patterns

| Entity | Pattern | Example |
|--------|---------|---------|
| Host | `ans:host/<hostname>` | `ans:host/web01` |
| Group | `ans:group/<name>` | `ans:group/webservers` |
| Variable | `ans:var/<owner_type>/<owner>/<key>` | `ans:var/host/web01/http_port` |
| Playbook | `ans:playbook/<rel_path>` | `ans:playbook/site.yml` |
| Play | `ans:play/<playbook>/<idx>` | `ans:play/site.yml/0` |
| Task | `ans:task/<context>/<idx>` | `ans:task/site.yml/0/3` |
| Role | `ans:role/<name>` | `ans:role/nginx` |
| Handler | `ans:handler/<context>/<slug>` | `ans:handler/nginx/restart_nginx` |
| Template | `ans:template/<role>/<filename>` | `ans:template/nginx/nginx.conf.j2` |

### 5.4 `load_inventory`

Load an Ansible inventory into the RDF store.

**Input:**
| Parameter | Type | Required | Description |
|---|---|---|---|
| `path` | string | yes | Path to an inventory file (INI or YAML) or inventory directory |

**Behavior:**
- Auto-detects INI vs YAML format from content
- Parses INI sections: `[group]`, `[group:children]`, `[group:vars]`, inline host vars
- Parses YAML inventory structure: `all.hosts`, `all.children`, `all.vars`
- Expands host range patterns (e.g., `web[01:05]` → `web01..web05`)
- Loads `host_vars/` and `group_vars/` directories if present
- Complex variable values are serialized as JSON strings

**Output:**
- Success: summary of hosts, groups, and variables loaded
- Failure: parse error with details

### 5.5 `load_ansible`

Load a full Ansible project into the RDF store — inventory, playbooks, roles, tasks, handlers, and templates.

**Input:**
| Parameter | Type | Required | Description |
|---|---|---|---|
| `path` | string | yes | Path to an Ansible project directory |
| `inventory_path` | string | no | Path to inventory file/directory. Default: auto-detect from common locations |

**Behavior:**
1. Loads inventory (tries `inventory/`, `hosts`, `hosts.yml`, etc.)
2. Loads `host_vars/` and `group_vars/` at project root
3. Parses playbook YAML files at project root (files starting with `-` or `---`)
4. Parses roles under `roles/` directory (tasks, handlers, templates, defaults, meta/dependencies)
5. Creates stub nodes for referenced but not-found roles
6. Task module detection: filters known task keywords; remaining key is the module name

**Output:**
- Success: summary of all entities loaded (hosts, groups, variables, playbooks, plays, tasks, roles, handlers, templates, triple count)
- Failure: parse error with details

**Implementation approach:**
- Standalone tool pattern (like `tools/git.rs` + `loaders/git.rs`), NOT a LanguageLoader
- Uses `serde_yaml` 0.9 with dynamic `serde_yaml::Value` (Ansible YAML is too polymorphic for typed structs)
- Custom INI parser handling Ansible-specific syntax
- Pure sync functions consistent with other tool implementations

**Example SPARQL queries after loading:**
```sparql
# Find all hosts and their groups
PREFIX ans: <https://ds-labs.org/ansible#>
PREFIX host: <http://www.invincea.com/ontologies/icas/1.0/host#>
SELECT ?hostname ?group WHERE {
?h a host:Host ; host:hostName ?hostname ; ans:memberOf ?g .
?g ans:name ?group .
}

# Find all tasks using the apt module
PREFIX ans: <https://ds-labs.org/ansible#>
SELECT ?taskName ?playbook WHERE {
?t a ans:Task ; ans:name ?taskName ; ans:module "apt" ; ans:sourceFile ?playbook .
}

# Find role dependencies
PREFIX ans: <https://ds-labs.org/ansible#>
SELECT ?role ?dep WHERE {
?r a ans:Role ; ans:name ?role ; ans:dependsOn ?d .
?d ans:name ?dep .
}

# Cross-ontology: find hosts that are both in Ansible inventory and C4 deployment
PREFIX ans: <https://ds-labs.org/ansible#>
PREFIX host: <http://www.invincea.com/ontologies/icas/1.0/host#>
SELECT ?hostname ?group ?addr WHERE {
?h a host:Host ; host:hostName ?hostname ; ans:memberOf ?g ; ans:ansibleHost ?addr .
?g ans:name ?group .
}
```

## 6. Plugin System — LanguageLoader Trait

New language support is added by implementing the `LanguageLoader` trait:

Expand Down Expand Up @@ -336,7 +492,7 @@ pub trait LanguageLoader: Send + Sync {
- The generic `load_code` tool dispatches to the appropriate loader based on the `language` parameter or auto-detection from file extensions.
- Adding a new language requires implementing the trait and registering it — no changes to the MCP tool interface.

## 6. Project Structure
## 7. Project Structure

```
oxigraph-code/
Expand All @@ -356,22 +512,24 @@ oxigraph-code/
│ ├── sparql.rs # sparql_query, sparql_update
│ ├── rdf.rs # load_rdf, list_graphs
│ ├── code.rs # load_code (generic dispatcher)
│ └── git.rs # load_git_history
│ ├── git.rs # load_git_history
│ └── ansible.rs # load_inventory, load_ansible
└── loaders/
├── mod.rs # LanguageLoader trait, registry, auto-detection
├── rust.rs # Rust loader (load_rust_code)
├── python.rs # Python loader (load_python_code)
├── typescript.rs # TypeScript loader (load_ts_code)
└── git.rs # Git history loader (commit graph, file changes)
├── git.rs # Git history loader (commit graph, file changes)
└── ansible.rs # Ansible inventory/playbook/role parser
```

## 7. Configuration
## 8. Configuration

| Variable | Default | Description |
|---|---|---|
| `OXIGRAPH_STORE_PATH` | `./oxigraph_data` | Path to the on-disk RocksDB store directory |

## 8. Claude Code Integration
## 9. Claude Code Integration

Register the server in Claude Code's configuration (`~/.claude.json` or project-level `.mcp.json`):

Expand All @@ -388,7 +546,7 @@ Register the server in Claude Code's configuration (`~/.claude.json` or project-
}
```

## 9. Error Handling
## 10. Error Handling

All tools follow the MCP error convention:
- Tool execution errors return `isError: true` with a descriptive text message
Expand All @@ -397,7 +555,7 @@ All tools follow the MCP error convention:
- Code parse errors include the source file path, line number, and error details
- Store errors (corruption, lock contention) are surfaced as-is from Oxigraph

## 10. Constraints and Limitations
## 11. Constraints and Limitations

- **Single server**: one Rust binary serves all tools. No separate Python/TypeScript server implementations.
- **File loading**: only local file paths are supported. No HTTP/URL fetching (use SPARQL `LOAD <url>` via `sparql_update` for remote sources where supported).
Expand Down
110 changes: 110 additions & 0 deletions plans/ansible-loader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Plan: Ansible & Inventory Loader for Oxigraph MCP

## Context

The Oxigraph MCP server currently loads code (Rust, TypeScript) and git history into an RDF knowledge graph. We need to extend it to load **Ansible infrastructure-as-code** artifacts — inventory files, playbooks, roles, and variable files — so that infrastructure topology, deployment automation, and host configuration can be queried via SPARQL alongside code and architecture data.

The ontology mapping uses **ICAS `host:Host`** for inventory hosts (enabling joins with C4 deployment nodes, monitoring probes, and hardening reports from the ds-reporting ontology stack) plus a new **`ans:` namespace** for Ansible-specific concepts (playbooks, roles, tasks, groups).

## Architecture

Standalone tool pattern (like `tools/git.rs` + `loaders/git.rs`), NOT a LanguageLoader.

**New files:**
- `rust/src/loaders/ansible.rs` — Parsing logic, quad generation
- `rust/src/tools/ansible.rs` — Tool functions (`load_inventory`, `load_ansible`)

**Modified files:**
- `rust/src/loaders/mod.rs` — Add `pub mod ansible;`
- `rust/src/tools/mod.rs` — Add `pub mod ansible;`
- `rust/src/main.rs` — Add param structs + 2 async tool handlers
- `rust/Cargo.toml` — Add `serde_yaml = "0.9"`
- `SPECIFICATIONS.md` — Document new ontology & tools

## Namespaces

| Prefix | URI | Source |
|--------|-----|--------|
| `ans:` | `https://ds-labs.org/ansible#` | **NEW** — Ansible-specific classes/properties |
| `host:` | `http://www.invincea.com/ontologies/icas/1.0/host#` | Existing ICAS — shared host identity |

All triples go to the **default graph** (consistent with all other loaders).

## Ontology

### Classes

| Class | Description |
|-------|-------------|
| `host:Host` | Inventory host (ICAS, enables cross-ontology joins) |
| `ans:HostGroup` | Group of hosts (`[webservers]`) |
| `ans:Inventory` | Inventory file/directory |
| `ans:Variable` | Key-value variable |
| `ans:Playbook` | Playbook YAML file |
| `ans:Play` | Play within a playbook (`- hosts:` block) |
| `ans:Task` | Task within a play or role |
| `ans:Role` | Ansible role |
| `ans:Handler` | Notified handler |
| `ans:Template` | Jinja2 template file |

### Key Properties

| Property | Domain → Range | Description |
|----------|----------------|-------------|
| `host:hostName` | Host → string | Hostname from inventory |
| `ans:ansibleHost` | Host → string | `ansible_host` connection address |
| `ans:memberOf` | Host → HostGroup | Host-to-group membership |
| `ans:hasHost` | HostGroup → Host | Group contains host |
| `ans:childGroup` | HostGroup → HostGroup | Group hierarchy |
| `ans:hasVariable` | Host/Group → Variable | Variable attachment |
| `ans:variableName` / `ans:variableValue` | Variable → string | Key-value pair |
| `ans:hasPlay` | Playbook → Play | Play containment |
| `ans:targetHosts` | Play → string | Hosts pattern |
| `ans:hasTask` | Play/Role → Task | Task containment |
| `ans:module` | Task → string | Ansible module name |
| `ans:usesRole` | Play → Role | Role inclusion |
| `ans:dependsOn` | Role → Role | Role dependency |
| `ans:name` | any → string | Entity name |
| `ans:sourceFile` | any → string | Source file path |

### URI Patterns

| Entity | Pattern | Example |
|--------|---------|---------|
| Host | `ans:host/<hostname>` | `ans:host/web01` |
| Group | `ans:group/<name>` | `ans:group/webservers` |
| Variable | `ans:var/<owner>/<key>` | `ans:var/host/web01/http_port` |
| Playbook | `ans:playbook/<rel_path>` | `ans:playbook/site.yml` |
| Play | `ans:play/<playbook>/<idx>` | `ans:play/site.yml/0` |
| Task | `ans:task/<ctx>/<idx>` | `ans:task/site.yml/0/3` |
| Role | `ans:role/<name>` | `ans:role/nginx` |
| Handler | `ans:handler/<ctx>/<slug>` | `ans:handler/nginx/restart_nginx` |

## Two MCP Tools

### `load_inventory`
- **Input**: `path` (file or directory)
- **Parses**: INI inventory, YAML inventory, `host_vars/`, `group_vars/`
- **Output**: `host:Host`, `ans:HostGroup`, `ans:Variable` triples

### `load_ansible`
- **Input**: `path` (project dir), optional `inventory_path`
- **Parses**: inventory + playbooks + roles
- **Output**: All of the above plus `ans:Playbook`, `ans:Play`, `ans:Task`, `ans:Role`, `ans:Handler`, `ans:Template`

## Implementation Steps

1. Add `serde_yaml = "0.9"` to Cargo.toml
2. Create `loaders/ansible.rs` — namespace helpers, error types, result structs, intermediate data model
3. INI inventory parser + quad generation
4. YAML inventory parser
5. `host_vars/` and `group_vars/` directory parsing
6. Create `tools/ansible.rs` — `load_inventory()` function
7. Wire `load_inventory` tool handler in `main.rs` (+ mod.rs updates)
8. Tests: INI parsing, YAML parsing, range expansion, host/group vars, ICAS bridge
9. Playbook parser (plays, tasks)
10. Role parser (tasks, handlers, defaults, templates, meta/dependencies)
11. `load_ansible_project_quads()` orchestrator
12. `load_ansible()` tool function + handler in main.rs
13. Tests: playbook, role, full project
14. Update SPECIFICATIONS.md
Loading