# TOON Encoder Plugin

> **Document Location:** `plugins/toon_encoder/README.md`
>
> **Related Documents:**
> - [TOON Format Specification](https://github.com/toon-format/spec/blob/main/SPEC.md)
> - Tests: `tests/unit/plugins/toon_encoder/`

A post-invoke plugin that converts JSON tool results to [TOON (Token-Oriented Object Notation)](https://github.com/toon-format/spec) format for reduced token consumption when sending responses to LLM agents.

## Overview

TOON is a compact, human-readable encoding format designed specifically for LLM prompts. It provides lossless serialization of JSON data in a syntax that minimizes tokens by:

- Eliminating quotation marks around keys and simple string values
- Using compact array syntax: `field[N]: val1,val2,val3`
- Using columnar format for homogeneous object arrays
- Removing unnecessary punctuation where context is clear

## Token Savings

### Expected Savings by Data Type

| Data Type | JSON Size | TOON Size | Savings |
|-----------|-----------|-----------|---------|
| Simple object | 150 bytes | 80 bytes | **~47%** |
| Array of 10 objects | 2 KB | 600 bytes | **~70%** |
| Nested data | 5 KB | 2 KB | **~60%** |
| Simple primitives | minimal | minimal | ~0% |

### Verified Example

Real-world test with array of user objects:

```
JSON (231 chars):
{"users": [{"id": 0, "name": "user0", "active": true}, {"id": 1, "name": "user1", "active": true}, ...]}

TOON (109 chars):
users:
  [5]{active,id,name}:
   true,0,user0
   true,1,user1
   true,2,user2
   true,3,user3
   true,4,user4

Savings: 52.8% (122 bytes saved)
```

### Why This Matters

For an agent using ContextForge to federate 10+ MCP servers:
- Each tool call returns JSON responses (database queries, API data, etc.)
- JSON verbosity (quoted keys, quoted strings, punctuation) inflates token count
- **Accumulated impact across many tool calls:**
  - Inference latency (more tokens = longer processing)
  - Context window consumption (less room for reasoning)
  - API costs (for cloud-hosted inference)

## Running Locally

### Prerequisites

- Python 3.11+
- ContextForge repository cloned
- `uv` package manager (or pip)

### Step 1: Set Up Development Environment

```bash
# Clone the repo (if not already)
git clone https://github.com/IBM/mcp-context-forge.git
cd mcp-context-forge

# Create virtual environment and install dependencies
make venv install-dev

# Copy environment template
cp .env.example .env
```

### Step 2: Enable the Plugin

Edit `.env` to enable plugins:
```bash
PLUGINS_ENABLED=true
PLUGINS_CONFIG_FILE=plugins/config.yaml
```

Add the TOON encoder to `plugins/config.yaml`:
```yaml
plugins:
  # ... existing plugins ...

  - name: "ToonEncoder"
    kind: "plugins.toon_encoder.toon_encoder.ToonEncoderPlugin"
    hooks: ["tool_post_invoke"]
    mode: "enforce"
    priority: 900
    config:
      min_size_bytes: 100
```

### Step 3: Start the Gateway

```bash
# Development server with auto-reload
make dev

# Or production server
make serve
```

The gateway will start on `http://localhost:8000` (dev) or `http://localhost:4444` (prod).

### Step 4: Verify Plugin is Loaded

Check the startup logs for:
```
INFO: ToonEncoderPlugin initialized: min_size=100, max_size=1048576, exclude=[]
```

### Step 5: Test the Plugin

```bash
# Run plugin-specific tests
pytest tests/unit/plugins/toon_encoder/ -v

# Quick inline test of TOON encoder
python3 -c "
from plugins.toon_encoder.toon import encode, decode
data = {'users': [{'id': 1, 'name': 'alice'}, {'id': 2, 'name': 'bob'}]}
toon = encode(data)
print('TOON output:')
print(toon)
print()
print('Round-trip:', decode(toon) == data)
"
```

### Step 6: Test with Real Tool Calls

1. Register an MCP server via the Admin UI or API
2. Create a virtual server with tools
3. Invoke a tool that returns JSON
4. Check the response for `annotations.format: "toon"`

```bash
# Example: Call a tool via the API
curl -X POST http://localhost:8000/tools/my-tool/invoke \
  -H "Content-Type: application/json" \
  -d '{"arguments": {}}'

# Response should contain TOON-encoded content with format annotation
```

### Troubleshooting

| Issue | Solution |
|-------|----------|
| Plugin not loading | Check `PLUGINS_ENABLED=true` in `.env` |
| No TOON conversion | Verify JSON response > `min_size_bytes` (default 100) |
| Import errors | Run `make install-dev` to install dependencies |
| Tests failing | Ensure you're in the virtual environment |

---

## Installation

The plugin is included in ContextForge plugins directory. Enable it in your configuration:

### 1. Add to `plugins/config.yaml`

```yaml
plugins:
  - name: "ToonEncoder"
    kind: "plugins.toon_encoder.toon_encoder.ToonEncoderPlugin"
    hooks: ["tool_post_invoke"]
    mode: "enforce"
    priority: 900  # Run late, after other post-processing
    config:
      min_size_bytes: 100        # Only convert JSON larger than this
      max_size_bytes: 1048576    # Skip very large payloads (1MB)
      exclude_tools: []          # Tool names to skip
      include_tools: null        # Whitelist (null = all tools)
      add_format_marker: true    # Add annotation marking content as TOON
      skip_on_error: true        # Continue with JSON if conversion fails
```

### 2. Enable plugins in `.env`

```bash
PLUGINS_ENABLED=true
PLUGINS_CONFIG_FILE=plugins/config.yaml
```

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `min_size_bytes` | int | 100 | Minimum JSON size to trigger conversion |
| `max_size_bytes` | int | 1048576 | Maximum JSON size to convert (prevents OOM) |
| `exclude_tools` | list | [] | Tool names to skip conversion |
| `include_tools` | list | null | Whitelist of tools (if set, only these are converted) |
| `add_format_marker` | bool | true | Add `annotations.format: "toon"` to converted content |
| `skip_on_error` | bool | true | Continue with original JSON if conversion fails |

## Usage Examples

### Example 1: Convert all tool results

```yaml
- name: "ToonEncoder"
  kind: "plugins.toon_encoder.toon_encoder.ToonEncoderPlugin"
  hooks: ["tool_post_invoke"]
  mode: "enforce"
  priority: 900
  config:
    min_size_bytes: 100
```

### Example 2: Only convert specific tools

```yaml
- name: "ToonEncoder"
  kind: "plugins.toon_encoder.toon_encoder.ToonEncoderPlugin"
  hooks: ["tool_post_invoke"]
  mode: "enforce"
  priority: 900
  config:
    include_tools:
      - "search_documents"
      - "query_database"
      - "list_items"
```

### Example 3: Exclude binary/streaming tools

```yaml
- name: "ToonEncoder"
  kind: "plugins.toon_encoder.toon_encoder.ToonEncoderPlugin"
  hooks: ["tool_post_invoke"]
  mode: "enforce"
  priority: 900
  config:
    exclude_tools:
      - "download_file"
      - "stream_data"
      - "get_binary"
```

## TOON Format Reference

TOON (Token-Oriented Object Notation) is designed to minimize tokens while preserving all JSON data.
For the complete specification, see the [official TOON spec](https://github.com/toon-format/spec/blob/main/SPEC.md).

### Primitives

| JSON | TOON | Notes |
|------|------|-------|
| `null` | `null` | Literal |
| `true` | `true` | Literal |
| `false` | `false` | Literal |
| `42` | `42` | Integers as-is |
| `3.14` | `3.14` | Floats as-is |
| `"hello"` | `hello` | Simple strings unquoted |
| `"hello world"` | `hello world` | Spaces OK in values |
| `"has,comma"` | `"has,comma"` | Special chars quoted |
| `""` | `""` | Empty string quoted |

### Objects

**Simple Object:**
```
JSON: {"name": "alice", "age": 30, "active": true}

TOON:
name: alice
age: 30
active: true
```

**Nested Object:**
```
JSON: {"user": {"name": "alice", "email": "alice@example.com"}}

TOON:
user:
  name: alice
  email: alice@example.com
```

**Empty Object:**
```
JSON: {}
TOON: (empty document)
```

### Arrays

**Simple Array (inline):**
```
JSON: [1, 2, 3]
TOON: [3]: 1,2,3
```

**String Array:**
```
JSON: ["apple", "banana", "cherry"]
TOON: [3]: apple,banana,cherry
```

**Empty Array:**
```
JSON: []
TOON: [0]:
```

**Nested Array:**
```
JSON: {"tags": ["python", "fastapi", "mcp"]}

TOON:
tags[3]: python,fastapi,mcp
```

### Columnar Format (Arrays of Objects)

When all objects in an array have identical keys with primitive values, TOON uses
a compact columnar format:

```
JSON:
[
  {"id": 1, "name": "alice", "score": 95},
  {"id": 2, "name": "bob", "score": 87},
  {"id": 3, "name": "charlie", "score": 92}
]

TOON:
[3]{id,name,score}:
 1,alice,95
 2,bob,87
 3,charlie,92
```

The header `[3]{id,name,score}:` specifies:
- `[3]` - Array contains 3 objects
- `{id,name,score}` - Column order (alphabetically sorted)
- Each row contains values in that column order

### Complex Nested Example

```
JSON:
{
  "status": "success",
  "data": {
    "users": [
      {"id": 1, "name": "alice", "roles": ["admin", "user"]},
      {"id": 2, "name": "bob", "roles": ["user"]}
    ],
    "total": 2
  },
  "metadata": {
    "page": 1,
    "per_page": 10
  }
}

TOON:
status: success
data:
  users:
    [2]:
     id: 1
     name: alice
     roles:
       [2]: admin,user
     id: 2
     name: bob
     roles:
       [1]: user
  total: 2
metadata:
  page: 1
  per_page: 10
```

### String Quoting Rules

Strings must be quoted if they:
- Are empty
- Equal `true`, `false`, or `null`
- Look like a number (e.g., `"123"`, `"3.14"`)
- Contain special characters: `,` `:` `[` `]` `{` `}` `"` `\` newlines
- Have leading or trailing whitespace

```
JSON: {"status": "null", "count": "42", "note": "a,b,c"}

TOON:
status: "null"
count: "42"
note: "a,b,c"
```

### Key Quoting Rules

Object keys must be quoted if they don't match `^[A-Za-z_][A-Za-z0-9_.]*$`:

```
JSON: {"valid_key": 1, "has space": 2, "123numeric": 3}

TOON:
valid_key: 1
"has space": 2
"123numeric": 3
```

## Agent Integration (Required)

> **IMPORTANT:** When you enable the TOON encoder plugin, your agent's system prompt **must be updated** to understand TOON format. Without this change, your agent will not be able to parse tool results correctly.

### Why This Is Required

- The gateway converts JSON tool responses to TOON format before sending to your agent
- Standard LLMs are trained on JSON, not TOON
- Your agent needs instructions on how to interpret TOON-formatted data
- Tool results will include `annotations.format: "toon"` to indicate the format

### Recommended System Prompt Addition

Add the following to your agent's system prompt:

```
## Tool Response Format: TOON

Tool results from this gateway use TOON (Token-Oriented Object Notation) format for efficiency.
Results with `annotations.format: "toon"` are TOON-encoded. Parse them as follows:

### TOON Syntax Rules

1. **Objects** - Key-value pairs on separate lines (no braces, no quoted keys):
   ```
   name: alice
   age: 30
   active: true
   ```
   Equivalent JSON: {"name": "alice", "age": 30, "active": true}

2. **Simple Arrays** - Count prefix with comma-separated values:
   ```
   [3]: apple,banana,cherry
   ```
   Equivalent JSON: ["apple", "banana", "cherry"]

3. **Object Arrays (Columnar)** - Count and keys in header, values in rows:
   ```
   [3]{id,name,score}:
    1,alice,95
    2,bob,87
    3,charlie,92
   ```
   Equivalent JSON: [{"id": 1, "name": "alice", "score": 95}, {"id": 2, "name": "bob", "score": 87}, {"id": 3, "name": "charlie", "score": 92}]

4. **Nested Objects** - Indented under parent key:
   ```
   user:
     name: alice
     email: alice@example.com
   settings:
     theme: dark
     notifications: true
   ```

5. **Special Values**:
   - `null` = null/None
   - `true` / `false` = booleans
   - Unquoted numbers = integers or floats
   - Quoted strings `"value"` = strings containing special characters (commas, colons, newlines)

### Parsing Instructions

When you receive a tool result:
1. Check if `annotations.format` equals `"toon"`
2. If yes, parse the text content using TOON rules above
3. If no annotation or format is not "toon", parse as standard JSON

TOON is a lossless format - all data from the original JSON is preserved.
```

### Complete System Prompt Example

Here's a complete example for an agent using ContextForge with TOON:

```
You are a helpful assistant with access to tools via ContextForge.

## Tool Response Format: TOON

Tool results from this gateway use TOON (Token-Oriented Object Notation) format for efficiency.
TOON reduces token usage by 40-70% compared to JSON while preserving all data.

### Quick Reference

| TOON | Meaning |
|------|---------|
| `key: value` | Object property |
| `[N]: a,b,c` | Array of N items |
| `[N]{k1,k2}:` followed by rows | Array of N objects with keys k1,k2 |
| `null`, `true`, `false` | Literal values |
| `"quoted"` | String with special characters |

### Example

TOON:
```
users:
  [2]{id,name,active}:
   1,alice,true
   2,bob,false
total: 2
```

Means:
```json
{
  "users": [
    {"id": 1, "name": "alice", "active": true},
    {"id": 2, "name": "bob", "active": false}
  ],
  "total": 2
}
```

When processing tool results, interpret TOON format and extract the data you need.
```

### Checking Format Annotation

Tool results include a format marker when TOON-encoded:

```python
# Result structure from gateway
{
    "content": [
        {
            "type": "text",
            "text": "name: alice\nage: 30",
            "annotations": {
                "format": "toon"  # <-- This indicates TOON encoding
            }
        }
    ]
}
   ```

## Programmatic Usage

The TOON encoder/decoder can be used directly:

```python
from plugins.toon_encoder.toon import encode, decode, estimate_token_savings

# Encode Python object to TOON
data = {"users": [{"id": 1, "name": "alice"}, {"id": 2, "name": "bob"}]}
toon_str = encode(data)
print(toon_str)
# users:
#   [2]{id,name}:
#    1,alice
#    2,bob

# Decode TOON back to Python
decoded = decode(toon_str)
assert decoded == data

# Estimate savings
import json
json_str = json.dumps(data)
json_len, toon_len, savings_pct = estimate_token_savings(json_str)
print(f"Saved {savings_pct:.1f}% ({json_len - toon_len} bytes)")
```

## Monitoring

The plugin tracks conversion statistics accessible via the `get_stats()` method:

```python
stats = plugin.get_stats()
# {
#     "conversions_attempted": 150,
#     "conversions_successful": 142,
#     "total_bytes_saved": 45230,
#     "success_rate": 94.67
# }
```

Conversion metadata is also included in each response:

```python
result.metadata = {
    "toon_encoded": True,
    "bytes_saved": 1250,
    "savings_percent": 62.5,
    "conversion_time_ms": 0.45
}
```

## Testing

Run the plugin tests:

```bash
# Run TOON encoder tests
pytest tests/unit/plugins/toon_encoder/test_toon.py -v

# Run plugin integration tests
pytest tests/unit/plugins/toon_encoder/test_toon_encoder.py -v

# Run all plugin tests
pytest tests/unit/plugins/toon_encoder/ -v
```

## Limitations

1. **Agent must understand TOON** - The receiving LLM/agent needs to parse TOON format
2. **Not all data benefits** - Very simple data may not see significant savings
3. **No streaming support** - Entire response must be buffered for conversion

### Known Decoder Limitations

The built-in TOON decoder has some edge case limitations (these don't affect the plugin's
primary use case of encoding for LLMs, which parse the format themselves):

- **Quoted keys in single-line objects** - Objects with quoted keys like `"has space": 1`
  are encoded correctly but may not decode properly when on a single line
- **Arrays of arrays** - Nested arrays like `[[1,2], [3,4]]` may have decoding issues
- **Mixed sibling fields after columnar arrays** - When a columnar array is followed by
  primitive fields at the same nesting level, the primitives may not decode

These limitations affect the `decode()` function used for round-trip testing. The `encode()`
function correctly handles all these cases per the TOON specification.

## License

Apache-2.0 - See LICENSE file in repository root.
