GCX1 — компактный формат передачи данных для MCP-инструментов
Медиана −27,4% экономии tiktoken по сравнению с JSON, 100% целостность при round-trip, на 20 репрезентативных MCP-ответах. Готово к использованию, стабильно, референсные декодеры на Go и TypeScript.
Ответы MCP-инструментов в большинстве своём табличные: вызов search_symbols возвращает N строк одной формы; find_usages — N рёбер; analyze — N находок. JSON — плохой формат для табличных данных: за каждое имя ключа на каждой строке платите вы, а токены платит LLM. При 20-строчном результате поиска "file_path", "start_line", "kind" повторяются двадцать раз.
GCX1 появился, чтобы исправить это для Gortex MCP-сервера, а потом выяснилось, что формат не привязан к конкретному инструменту. Теперь он подключается опционально на уровне отдельного вызова через format: "gcx". Вот как это устроено, почему экономия такая, и где она не работает.
Конкретный пример #
Вызов search_symbols, возвращающий 5 результатов.
JSON (720 байт, 181 tiktoken-токен):
[
{"id":"internal/mcp/server.go::NewServer","kind":"function","name":"NewServer","file_path":"internal/mcp/server.go","start_line":62},
{"id":"internal/mcp/server.go::Server.Start","kind":"method","name":"Start","file_path":"internal/mcp/server.go","start_line":118},
{"id":"internal/mcp/server.go::Server.Shutdown","kind":"method","name":"Shutdown","file_path":"internal/mcp/server.go","start_line":140},
{"id":"internal/mcp/tools_core.go::registerCoreTools","kind":"method","name":"registerCoreTools","file_path":"internal/mcp/tools_core.go","start_line":307},
{"id":"internal/mcp/tools_coding.go::registerCodingTools","kind":"method","name":"registerCodingTools","file_path":"internal/mcp/tools_coding.go","start_line":1}
]
GCX1 (522 байта, 125 токенов — −27,5% / −30,9%):
GCX1 tool=search_symbols fields=id,kind,name,path,line,sig rows=5 total=5 truncated=false
internal/mcp/server.go::NewServer function NewServer internal/mcp/server.go 62
internal/mcp/server.go::Server.Start method Start internal/mcp/server.go 118
internal/mcp/server.go::Server.Shutdown method Shutdown internal/mcp/server.go 140
internal/mcp/tools_core.go::registerCoreTools method registerCoreTools internal/mcp/tools_core.go 307
internal/mcp/tools_coding.go::registerCodingTools method registerCodingTools internal/mcp/tools_coding.go 1
Та же информация. Заголовок объявляет порядок колонок один раз; каждая строка — одна строка с разделителями-табуляциями.
Устройство формата #
Четыре ключевых решения.
1. Ориентация на токенизатор, а не на байты.
GCX1 проектировался под tiktoken cl100k_base — токенизатор, который используют Claude и модели класса GPT-4. Табуляция (\t) — один токен. Перенос строки — один токен. "file_path":" — 4 токена. Убрать 20 копий этого одного повторяющегося ключа — сэкономить 80 токенов, а не просто байты. Бенчмарк измеряет именно tiktoken, потому что это число в счёте пользователя — не сырые UTF-8-байты, которые gzip и так уплощает.
2. Round-trip по конструкции.
Каждый GCX1-пейлоад декодируется в эквивалентное JSON-значение и кодируется обратно в побайтово идентичный GCX. Агенты, работающие с JSON, не ломаются. Реализации декодеров: pkg/wire (Go) и @gortex/wire (TypeScript на npm). 20/20 round-trip в бенчмарке — не выборочное утверждение.
3. Табуляция, а не запятая.
Сигнатуры символов содержат запятые ((int, string)) и скобки. Табуляций они не содержат. CSV потребовал бы кавычек почти на каждой строке; TSV — нет. Алфавит экранирования: два байта — \t, \n, \\. Всё остальное проходит без экранирования.
4. Версионирование и безопасный fallback.
Литеральный префикс GCX1 есть в каждом заголовке. Декодер, встретивший в будущем GCX2, обязан перейти к JSON, повторив MCP-вызов без format: "gcx". Расположение полей для каждого объявленного инструмента заморожено на весь срок жизни GCX1 — добавление колонки к инструменту является ломающим изменением и выйдет как GCX2, а не тихой дрейфом схемы.
Грамматика #
payload = section { section } ;
section = header row-line { row-line | comment } ;
header = "GCX1" SP "tool=" token { SP key-value } SP "fields=" field-list LF ;
row-line = value { TAB value } LF | LF ;
value = { "\\\\" | "\\t" | "\\n" | any-byte-except-TAB-LF-BACKSLASH } ;
Многосекционные пейлоады конкатенируются последовательно — инструменты вроде get_callers выдают две секции (.nodes и .edges), что избавляет от денормализации графа в дублирование строк.
Полная спецификация: docs/wire-format.md
Бенчмарк — полный скорборд #
Воспроизводимо из bench/wire-format/ командой go run ./bench/wire-format. Стенд захватывает сырые UTF-8-байты, tiktoken-токены, gzip-сжатые байты и целостность round-trip по 20 репрезентативным ответам инструментов.
| кейс | токенов JSON | токенов GCX | Δ% |
|---|---|---|---|
| search_symbols (small) | 181 | 125 | −30,9% |
| search_symbols (large) | 1068 | 742 | −30,5% |
| batch_symbols | 335 | 241 | −28,1% |
| find_usages (large) | 569 | 351 | −38,3% |
| analyze_hotspots | 506 | 318 | −37,2% |
| smart_context | 471 | 299 | −36,5% |
| analyze_dead_code | 288 | 198 | −31,2% |
| find_implementations | 224 | 157 | −29,9% |
| get_callers | 750 | 532 | −29,1% |
| get_editing_context | 233 | 171 | −26,6% |
| get_file_summary | 567 | 431 | −24,0% |
| find_usages (small) | 335 | 255 | −23,9% |
| contracts.list | 580 | 463 | −20,2% |
| get_test_targets | 311 | 262 | −15,8% |
| get_repo_outline | 237 | 224 | −5,5% |
| get_symbol_source (small) | 257 | 255 | −0,8% |
| get_symbol_source (large) | 534 | 532 | −0,4% |
| find_cycles | 87 | 90 | +3,4% |
| graph_stats | 162 | 174 | +7,4% |
Медиана: −27,4% токенов. Round-trip: 20/20.
Где GCX1 не помогает #
Три кейса дают нейтральный или отрицательный результат — и это структурные причины, а не баги.
graph_stats (+7,4%). Один скалярный объект с ~6 ключами. Заголовок GCX1 стоит фиксированно; при меньше ~5 строках заголовок съедает экономию. Для этого JSON лучше.
find_cycles (+3,4%). 3–4 строки коротких скаляров. Та же проблема.
get_symbol_source (~0%). Пейлоад доминируется полем с исходным кодом. Ни одна кодировка исходник не сжимает.
Выигрыш приходит от табличных пейлоадов с повторяющейся формой. Большинство ответов MCP-инструментов именно такие — но не все. Формат подключается опционально на уровне вызова именно по этой причине: энкодеры переходят обратно к JSON для форм, где GCX1 проигрывает.
GCX1 vs TOON #
TOON (Token-Oriented Object Notation) решает смежную задачу — сравнение здесь уместно.
TOON — это универсальная компактная кодировка для LLM-ввода: отступы в стиле YAML, скобочная нотация [N] для длин массивов и фигурные скобки {id,name,value} для заголовков полей с CSV-строками под ними. Формат обрабатывает вложенные структуры, частично табличные массивы и смешанные данные — его бенчмарки показывают 33–59% снижения токенов по сравнению с JSON в зависимости от формы данных.
GCX1 уже. Он ориентирован именно на ответы MCP-инструментов, несёт в заголовке метаданные уровня инструмента (tool=, rows=, total=, truncated=) и плоский по конструкции. Многосекционные пейлоады (.nodes + .edges) обрабатывают граф без вложенности.
| TOON | GCX1 | |
|---|---|---|
| Область применения | Универсальный LLM-ввод | Ответы MCP-инструментов |
| Вложенные структуры | Да | Нет (многосекционность вместо) |
| Разделитель | Запятая или табуляция | Только табуляция |
| Декларация длины массива | Да ([N]) | Через rows=N в заголовке |
| Метаданные инструмента | Нет | Да (tool=, total=, truncated=) |
| Round-trip | Не является целью проектирования | Да, по конструкции |
| Версионирование + fallback | Нет | Префикс GCX1, декодер переходит на JSON при GCX2 |
| Заявленная экономия | 30–60% (разнородные данные) | 27,4% медиана (формы MCP-инструментов) |
| Интеграция с MCP | Сторонний сервер | Нативный format: "gcx" на уровне вызова |
Выбор только табуляции в GCX1 — наиболее важное структурное различие. Сигнатуры кода — доминирующий строковый тип в ответах Gortex — содержат запятые (func(int, string) error), но не содержат табуляций. TOON поддерживает табуляцию как альтернативный разделитель, однако по умолчанию используется запятая с кавычками — а это накладные расходы именно на те значения, которые чаще всего встречаются в пейлоадах анализа кода. Алфавит экранирования GCX1 — два байта (\t, \n, \\), и ничто в сигнатуре символа его не триггерит.
Второе существенное различие — round-trip. TOON проектировался как формат, который отправляется LLM: модель его читает, не downstream-инструмент. Пейлоады GCX1 должны декодироваться обратно в типизированные строки, чтобы вывод одного инструмента мог стать вводом другого без повторного вызова. Это MCP-специфичное требование, которое TOON не обязан учитывать.
Если вы строите общую агентную систему, передающую разнородный JSON — записи пользователей, логи событий, конфигурационные объекты, — TOON стоит измерить. Если вы строите MCP-сервер, возвращающий табличные данные о символах, накладные расходы на вложенность TOON и разделитель по умолчанию — затраты, которые вам не нужны.
Попробовать #
Со стороны MCP-сервера — любой Go-сервер.
go get github.com/zzet/gortex/pkg/wire
pkg/wire — отдельный Go-модуль, MIT-лицензия, только стандартная библиотека, 250 строк. Остальная часть Gortex распространяется под отдельной source-available лицензией; wire-пакет — MIT, так что его можно добавить в любой MCP-сервер без проблем с лицензированием.
Со стороны агента — TypeScript.
npm install @gortex/wire
import { decode } from "@gortex/wire";
const rows = decode(mcpResponseText);
// rows имеет ту же форму, что и JSON.parse
Со стороны агента — Go.
Тот же пакет pkg/wire; wire.Decode(payload) возвращает типизированные строки.
Воспроизвести бенчмарк.
git clone https://github.com/zzet/gortex
cd gortex
go run ./bench/wire-format
cat bench/wire-format/scorecard.md
Набор фикстур находится в bench/wire-format/cases/. Каждая — YAML-файл с канонической JSON-нагрузкой. Добавьте фикстуры своего инструмента, запустите стенд и посмотрите, что GCX1 даёт на ваших формах.
Почему это появилось #
GCX1 появился для Gortex — MCP-сервера анализа кода, который индексирует репозитории в граф знаний и предоставляет его Claude Code, Cursor, Windsurf, Copilot и 11 другим AI-агентам. Сервер отдаёт много табличных данных — символы, вызывающий код, контракты, зависимости — и первое, что хотелось срезать, был токенный счёт в аккаунте каждого пользователя. Режим compact: true с текстовым выводом существовал, но не поддерживал round-trip: результат нельзя было подать обратно в другой инструмент. Нужно было что-то, что несёт схему один раз, а не повторяет её в каждой строке.
Что дальше #
GCX1 остаётся стабильным. Расположение полей для каждого инструмента заморожено на весь срок жизни GCX1. Добавление колонки — ломающее изменение, которое выйдет как GCX2.
GCX1-stream зарезервирован. Потоковая передача строка за строкой через SSE / chunked HTTP, тот же заголовок, та же грамматика строк. Пока не выпущено.
GCX2 при необходимости. Если кому-то понадобятся строки на основе CBOR или MessagePack для бинарных пейлоадов — это следующая мажорная версия. GCX1 — только текст, чтобы агенты могли читать сырые пейлоады при отладке. Это не обсуждается для v1.
Если вы строите MCP-сервер: возьмите самый большой табличный ответ, измерьте его через tiktoken, затем прогоните через энкодер и измерьте снова. Если форма табличная — вы оставляете 25–35% на столе.
Спецификация: docs/wire-format.md
Бенчмарк: bench/wire-format
Go-реализация: pkg/wire (MIT)
TypeScript-декодер: @gortex/wire на npm