how-to-create-oculus-tool
How to Create an Oculus Tool
TL;DR: Write a Python fence with a label β It becomes a callable MCP tool.
The Pattern
- Create a fence with a label:
```python[my-tool-name] - Use
configto receive parameters - Set
resultto return data - Document inline so the tool is self-describing
That's it. The fence IS the tool.
Live Example: Magic 8-Ball
This fence demonstrates the pattern. It's a working tool you can call right now.
config
tool
{
"question": "Will things work out?",
"answer": "Signs point to yes.",
"confidence": 0.6,
"advice": "Promising signs",
"oracle": "π±"
}How It Works
The Label Makes It Callable
```python[magic-8-ball] β This label registers it as a tool
...code...
``` β Tool is now callable as "magic-8-ball"Params Become Config
When you call the tool:
oculus_execute_fence({
fence_id: "magic-8-ball",
params: { question: "Should I refactor?" } // β These become 'config'
})Inside the fence, access via:
question = config.get('question', 'default value')Result Is Your Return Value
Whatever you assign to result gets returned:
result = {"answer": "Yes!", "data": [...]}Try It Now
question: Is this tutorial helpful?β Fence Execution Error: No fence found in section 'tool' of node 'how-to-create-oculus-tool'
Creating Your Own Tool
Step 1: Create the Node
# Via CLI
oculus create my-cool-tool
# Via MCP
oculus_create_node({ slug: "my-cool-tool", content: "# My Cool Tool\n\n## tool\n\n```python[my-cool-tool]\nresult = {'hello': 'world'}\n```" })Step 2: Write the Fence
# Label your fence: ```python[my-cool-tool]
"""
Brief description of what this tool does.
USAGE:
oculus_execute_fence({ fence_id: "my-cool-tool", params: {...} })
PARAMS:
param1 (type): Description
param2 (type): Description [optional, default: x]
RETURNS:
{ field1: ..., field2: ... }
"""
# Read params
param1 = config.get('param1', 'default')
param2 = config.get('param2', 'default')
# Do work
output = do_something(param1, param2)
# Return result
result = {
"status": "success",
"data": output
}Step 3: Trigger Index Rebuild
The fence index updates automatically on poke, but you can force it:
# Just poke any section to trigger re-index
oculus_poke({ slug: "my-cool-tool", path: "config", value: "", context: "trigger re-index" })Step 4: Call Your Tool
oculus_execute_fence({
fence_id: "my-cool-tool",
params: { param1: "value1" },
context: "Testing my new tool"
})Self-Documentation Pattern
The best tools document themselves. Include a docstring at the top:
# Label: ```python[my-tool]
"""
π§ Tool Name
One-line description.
USAGE:
oculus_execute_fence({ fence_id: "my-tool", params: {...} })
PARAMS:
name (string): What it's for
count (int): How many [optional, default: 10]
RETURNS:
{ result: ..., metadata: ... }
EXAMPLE:
{ name: "test", count: 5 } β { result: "processed 5 items" }
"""This docstring appears in:
- The fence metadata (
oculus_get_fence) - The node content (human-readable)
- Error messages (when params are wrong)
Addressing Formats
Your tool can be called multiple ways:
| Format | Example | When to Use |
|---|---|---|
| Label | magic-8-ball |
Simple, global (must be unique) |
| slug:label | how-to-create-oculus-tool:magic-8-ball |
Explicit, avoids conflicts |
| slug:index | how-to-create-oculus-tool:1 |
When you know the position |
Quick Reference
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FENCE TOOL ANATOMY β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ```python[tool-name] β Label = tool identifier β
β """ β
β Docstring = self-documentation β
β """ β
β β
β # Read params β
β x = config.get('param', 'default') β params β config β
β β
β # Do work β
β output = process(x) β
β β
β # Return β
β result = {"data": output} β result β response β
β ``` β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββWorking with Data in Fences
When your tool needs to read/write data stored in Oculus nodes, you have two options: native graph functions (preferred) or subprocess calls.
Native Graph Functions (Recommended)
Python fences have direct access to the graph through built-in functions. No imports needed!
Available Functions
| Function | Signature | Description |
|---|---|---|
config |
dict | Parameters passed to the fence |
peek |
peek(slug, path, format='raw') |
Read value from graph |
poke |
poke(slug, path, value, operation='set', fence_type=None) |
Write value to graph |
execute |
execute(fence_id, params=None, force=False) |
Execute another fence |
get_node_content |
get_node_content(slug) |
Get raw markdown of a node |
context |
FenceExecutionContext | Execution context object |
peek(slug, path, format='raw')
Read values directly from the graph:
# Read a YAML value
location = peek("adventure-game", "data.yaml.location")
# Read entire section
config = peek("my-node", "config.yaml")
# Returns None if not found
value = peek("node", "missing.path") # β Nonepoke(slug, path, value, operation='set', fence_type=None)
Write values directly to the graph:
# Set a value
poke("adventure-game", "data.yaml.location", "cave-dark")
# Append to a list
poke("my-node", "log.yaml.entries", new_entry, operation='append')
# Write with fence wrapper
poke("my-node", "config", {"key": "value"}, fence_type='yaml')execute(fence_id, params=None, force=False)
Call other fences from within a fence:
# Execute by label
result = execute("magic-8-ball", params={"question": "Will it work?"})
# Execute by slug:label
data = execute("my-node:my-tool", params={"count": 5})
# Force execute even if execute bit is false
output = execute("disabled-fence", force=True)get_node_content(slug)
Fetch raw markdown content:
# Get a node's content
content = get_node_content("my-documentation")
# Extract title
for line in content.split('\n'):
if line.startswith('# '):
title = line[2:].strip()
breakExample: Reading List Tool
Here's a complete example using native functions:
"""
π Add to Reading List
PARAMS:
topic (string): The reading list topic name
slug (string): The node slug to add
"""
topic = config.get('topic')
slug_to_add = config.get('slug')
# Native peek - direct graph access!
current_list = peek("required-reading", f"readings.yaml.{topic}") or []
if slug_to_add not in current_list:
current_list.append(slug_to_add)
# Native poke - direct graph access!
poke("required-reading", f"readings.yaml.{topic}", current_list)
result = {"success": True, "count": len(current_list)}Subprocess Method (Legacy)
For cases where native functions aren't available, use CLI subprocess calls.
Reading YAML Data (peek CLI)
Key insight: YAML fences are data, not executable code. Use --level sprout to get parsed YAML without trying to execute it.
import json
import subprocess
# Address format: slug:section.fence.key
address = 'my-node:my-section.yaml.some-key'
proc = subprocess.run(
['oculus', 'peek', address, '--level', 'sprout', '--format', 'json'],
capture_output=True, text=True
)
if proc.returncode == 0:
data = json.loads(proc.stdout.strip())Address formats:
slug:section.yaml- entire YAML fenceslug:section.yaml.key- specific key in YAMLslug:section.yaml.nested.path- nested value
Cache levels:
| Level | Use Case |
|---|---|
sprout |
YAML/JSON data fences (parsed, not executed) |
clouds |
Python fences (executed, default) |
seed |
Raw markdown, no processing |
Writing Data (poke CLI)
import json
import subprocess
new_list = ["item1", "item2", "item3"]
# Note: VALUE is positional, not --value flag!
proc = subprocess.run(
['oculus', 'poke', f'my-node:my-section.yaml.my-key',
json.dumps(new_list),
'--context', 'Updating list from tool'],
capture_output=True, text=True
)Poke CLI syntax: oculus poke PATH VALUE --context CONTEXT
- PATH can be
slug:section.pathformat - VALUE is a positional argument (not
--valueflag!) - Always include
--contextfor audit trail
See Also
- cmd-fence - CLI fence commands
- fence-test-data - Test suite
- jenkins-job-config - Real-world example
Slots
North
slots:
- context: []
slug: oculus-cli
- context:
- Linking tutorial to CLI docs
slug: oculus-cliSouth
slots:
- context: []
slug: cmd-fence
- context:
- Linking frontend guide to backend guide
slug: how-to-wire-stuffy-to-oculus
- context:
- Linking whimsical example tool to tutorial
slug: cosmic-fortune
- context:
- Linking tutorial to main how-to guide
slug: bolawa-20260107Config
East
slots:
- context:
- Practical demonstration of fences-as-functions
slug: structural-isomorphism-thesis