Skip to main content
Glama

MCP server for 1C configuration structure

A reference to the metadata of several 1C configurations, to the platform syntax, and to the query language — for agents writing code in BSL. It returns a minimally sufficient slice: resolving a human phrasing into the exact object name, the object structure at the required level of detail, its relationships, descriptions of platform methods taking into account the version of a specific configuration, and query language constructs.

It does not replace grep over project sources: the code lives in files, the server is responsible for slowly changing knowledge about the configuration. The boundary is fixed in docs/data-sources.md.

Status — as of 2026-08-18

Stage

Status

Processing export for 1С

✅ 20 metadata types, 8.3.5 and 8.3.23, XML and JSON

Export format

schema v1

Loader, model, relationship graph, render

✅ 5 configurations, 20,522 objects, 322 thousand edges

Platform help

✅ merged index of three versions, 25,691 elements, since/until boundaries

Query language

shquery_ru.hbk, 127 pages, separate source without versions

Search

✅ 97.1% help, 94.7% query language, 90.5% metadata — see “Measured”

Virtual tables of registers

✅ ready-made query field names (КоличествоОстаток)

Replacement table for old platforms

✅ unavailable is not just forbidden, but replaced with a recipe

Source registry, version mapping

MCP server, 7 tools

✅ streamable-http and stdio

Docker

✅ one container, 354 MB

Search index cache

✅ 12 MB, loaded instead of re-parsing

Benchmark stand

python -m mcp1c.bench, P@k, MRR, lift, label reconciliation

Tests

pytest, 371

Dashboard

✅ registry, sources, query runs, relationship graph, cards, dictionary

Authorization

API_TOKEN for reading, ADMIN_TOKEN for writing

Module index from .cf

Contents

  1. Launch — Docker, dashboard, relationship graph, without Docker

  2. Connecting an agenthow MCP works, if it does not connect, token, client configs: Claude Code, Codex CLI, Cursor, VS Code, Qwen Code, stdio

  3. Toolscall order, sources, query language, platform versions, merging help files, replacements

  4. Data management — sources, dictionary and search keys, CLI, benchmark stand, server manually, where to get data

  5. How it works — modules, measurements, tests

  6. Security — tokens, what is open without them

  7. Documents


1. Launch

Docker (main method)

# 1. Положить исходные данные
mkdir -p data/bootstrap
cp ВыгрузкаКонфигурации.zip                     data/bootstrap/
cp /opt/1cv8/8.3.27.2130/shcntx_ru.hbk          data/bootstrap/

# 2. Поднять
docker compose up -d --build

# 3. Проверить
curl http://localhost:5001/health
{"status":"ok",
 "configurations_total":2,
 "syntax_loaded":true,
 "query_language_loaded":true,
 "configurations":["РозницаДляКазахстана","ЮвелирныйТорговыйДомДляКазахстана"],
 "syntax":["8.3.5.1570","8.3.23.1997","8.3.27"]}

The platform help and the query language are different sources and different fields: syntax_loaded refers only to the former, syntax lists the versions of loaded help files. Configuration names and help versions are returned only to a request that passed the read check; without a token, status, the counter, and two flags remain.

Everything in data/bootstrap/ is indexed at startup: *.zip — configuration exports, *.hbk — platform help. The same file is not parsed twice: verification is by hash.

The ./data directory is mounted into the container as /data. Inside it, the server keeps sources, indexes, cache, and registry.json; paths in the registry are relative, so the directory can be moved between the developer machine and the container.

data/ is entirely outside git — it is a volume, not part of the repository. It is moved by copying the directory. Therefore, after cloning, you need to put the help in yourself: the repository does not contain it and cannot contain it; this is content of the 1С company.

After changing the code, the container must be recreated, not restarted:

docker compose up -d --build --force-recreate

restart will bring up the old container on the old image, and the changes will not apply.

About the port. The server is exposed on 5001, inside the container it listens on 8000 — the 5001:8000 port forwarding in docker-compose.yml. All addresses in this file are external, i.e. 5001. If it is occupied by another service, change the left part of the forwarding, do not touch the right part: EXPOSE and the image healthcheck depend on it.

Dashboard

http://localhost:5001/ — six pages:

Page

What's there

Overview

what is loaded: objects, relationships, platform version, warnings from manifests

Sources

list of loaded items, upload .zip and .hbk, deletion

Queries

running a list of phrasings with a score and ranking reason

Relationships

object neighborhood graph as an image

Card

object composition or description of a platform element — the same as the agent sees

Dictionary

rules with origin; create an alias or a group of synonyms

Relationships — object graph

/graph draws the object neighborhood: color by type, arrow by link direction, edge label on hover. Clicking a node builds a graph around it, dragging moves it, the wheel zooms. The neighbor limit is chosen on the page (15…400), the truncation is called by a number — “shown 30 of 102”.

It answers “what will break if you touch it”: a register surrounded by orange documents immediately tells who moves it.

Depth is always one step. Two steps from a common catalog bring up a thousand objects, three steps — a third of the configuration; further, the connection goes through common mechanisms such as additional attributes, which connect almost everything to everything. You cannot cut them off with a threshold by the number of connections: such a node has 34 of them, while a meaningful Справочник.Пользователи has 323. Therefore, a person expands nodes, not a heuristic — they see where not to go.

The agent intentionally does not have such a tool. The analysis and return conditions are in docs/TASKBOARD.md, section “Deferred”.

A miss is fixed without leaving the browser: on the queries page, each phrase has a link “not that — create an alias”, which leads to the dictionary with the phrase already filled in. The edit takes effect immediately — indexes are not rebuilt, no restart is needed.

Reading is protected by API_TOKEN, writing by ADMIN_TOKEN. While API_TOKEN is not set, anyone who can reach the address can read — including the structure of configurations and customizations. This is acceptable for localhost, but not for a server on a network.

The tokens are separated because the read token lives in every MCP client config and leaks along with it; the agent should not have the right to delete sources. The admin token also works as a read token — no need to keep two headers.

// .mcp.json — как клиент передаёт токен
{"mcpServers": {"1c": {"type": "http", "url": "http://localhost:5001/mcp",
                       "headers": {"X-Api-Token": "..."}}}}

ASCII only: HTTP headers are encoded in latin-1, Cyrillic will not get through in them. /health remains open for healthcheck, but it returns configuration names only by token.

Uploading, deleting, and editing the dictionary require ADMIN_TOKEN — the same as /admin/reload; without it, these endpoints do not exist, rather than “they are closed”. The token is entered once in the form; what goes to the browser is not the token but a session identifier.

It is set via .env next to docker-compose.yml — a template with all variables is in .env.example:

cp .env.example .env
python3 -c "import secrets; print(secrets.token_urlsafe(32))"   # значение
docker compose up -d --force-recreate

The name in the results is a link to the card: for an object, it is attributes with types, tabular sections, and movements; for a platform element, it is the signature, parameters, availability, and version of appearance. The same text the agent receives, with a brief / fields / full toggle. An attribute does not have its own card — the link leads to the owner object.

The “Queries” page answers the question “why did the server return exactly this”: next to each hit there is a reason — точное совпадение, псевдоним из словаря, все слова запроса. It shows how to fix a miss — with a synonym, an alias, or a weight.

Parsing the help takes a few seconds: the page will respond after it, but this does not delay MCP clients — indexing goes into a separate thread.

Without Docker

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
PYTHONPATH=src .venv/bin/python -m mcp1c.server --host 0.0.0.0 --port 5001

2. Connecting an agent

The server implements the MCP protocol using the standard transports of the official SDK, so it suits any MCP client. No wrappers around HTTP are required.

Transport

When

Address

streamable-http

server in Docker or on a separate machine

http://адрес:5001/mcp

stdio

the client starts the process locally itself

sse

only for old clients

--transport sse

Both main transports are verified with the official MCP client: the initialize handshake, tools/list, tools/call, protocol 2025-11-25.

How it works

It is useful to understand before something fails to connect. The address is one — /mcp, there is no endpoint per tool; which tool is called is written in the request body, not in the path.

Next are two different mechanics, and they should not be confused:

Tool descriptions

Data

When

once, at connection

on every call

Who starts

the client, itself, without the model's involvement

the model, by decision

Method

POST initialize, then POST tools/list

POST tools/call

Where it goes

the model's system prompt

the conversation body

Cost

one-time, stays for the whole session

per call

On connection, the client makes a POST initialize — the server responds with the name, version, and the instructions text, and returns mcp-session-id in the header. Then POST tools/list returns the tools at once: name, description, JSON schema of parameters. All of this is placed into the model's context before the person has typed the first word. The model does not go for the description when it needs it — it already has it.

Hence a consequence important when editing descriptions: they take up space in the window for the entire session, regardless of whether the model calls even one tool or none.

There are always seven tools, regardless of what is loaded. The set is a contract, not a variable: the tools are interconnected, and on the working server all three sources are loaded — configurations, platform help, query language. The tools/list contract plus instructions is about 3,900 tokens, and this number does not depend on the state of the registry.

A direct consequence follows from this, which you need to know in advance: if a source is not loaded, you still pay for its tools. Without platform help, search_syntax and get_syntax sit in the context and cost 1,185 tokens, answering "help not connected"; compare_configurations with a single configuration — 262 tokens for the answer "at least two are needed". This is fixed by loading the source, not by filtering tools: filtering was tried on 2026-08-19 and rolled back — details and numbers in «Отложено».

Descriptions are therefore written densely, and details go into the tool's own output: you only pay for them when they are needed.

GET /mcp is not a "wrong POST" but a third method on the same address: it opens a message stream from server to client and requires an already received mcp-session-id. DELETE /mcp closes the session.

If the client does not connect

The response code in the log (docker logs -f mcp1c) names the cause:

Code

What is wrong

406

the client does not send Accept: application/json, text/event-stream — both types are required

400 Missing session ID

the client did not return the mcp-session-id header received during initialize

400 on the very first GET /mcp

the client started the handshake with GET — it speaks the old HTTP+SSE transport, and this address uses streamable-http

404 on /sse

same thing: the old transport is not exposed

401

X-Api-Token not passed when API_TOKEN is set

empty in the log

the client did not send a request at all — the issue is in its config, it never reached the server

A real case: Qwen Code would not connect because its config had the key url — in the Gemini CLI family (Qwen inherits the format) that means the old SSE transport, and the client started with GET, getting 400. With httpUrl — that is, streamable-http — the connection goes through immediately.

Token: what to add to client settings

If API_TOKEN is set on the server, every client must send it as a header. Without the header, /mcp responds 401, and the agent simply will not see the tools.

Either of the two headers works — the server accepts both:

X-Api-Token: <токен>
Authorization: Bearer <токен>

Three things people trip over:

  • ASCII only. HTTP headers are encoded in latin-1; a Cyrillic token will not get through them. Generate like this: python3 -c "import secrets; print(secrets.token_urlsafe(32))".

  • API_TOKEN goes into the client, not ADMIN_TOKEN. The admin one will also be accepted, but the client config goes into git and into backups: a leaked read token gives viewing access, a leaked admin one gives the right to delete sources.

  • stdio requires no token at all. There the client launches the process itself, no network is involved, and there is nothing to check. If the client cannot set headers — this is a working workaround.

To verify that the server sees the token, before any client configuration:

curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  -H 'x-api-token: ВАШ_ТОКЕН' \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
  http://localhost:5001/mcp

200 — token accepted. 401 — wrong token or the header did not get through.

How not to commit a secret

.mcp.json and similar files usually live in the repository. Options:

  1. Variable substitution — if the client supports it (Claude Code does): "X-Api-Token": "${MCP1C_API_TOKEN}", with the variable itself in ~/.zshrc. The variable name goes into git, not the value.

  2. Remove the file from git: git rm --cached .mcp.json && echo ".mcp.json" >> .gitignore.

  3. Keep the setting not in the project but in the client's user config — then the repository is not involved at all.

Claude Code

File .mcp.json in the project root:

{
  "mcpServers": {
    "1c": {
      "type": "http",
      "url": "http://localhost:5001/mcp",
      "headers": { "X-Api-Token": "${MCP1C_API_TOKEN}" }
    }
  }
}

The headers block is only needed if API_TOKEN is set on the server. The value is taken from an environment variable so the file can be kept in the repository:

echo 'export MCP1C_API_TOKEN=ваш_токен' >> ~/.zshrc && source ~/.zshrc

Or via command:

claude mcp add --transport http 1c http://localhost:5001/mcp \
  --header "X-Api-Token: $MCP1C_API_TOKEN"

Codex CLI

~/.codex/config.toml or .codex/config.toml in the project:

[mcp_servers.mcp1c]
url = "http://localhost:5001/mcp"

# Только если задан API_TOKEN. Имя ключа для заголовков у Codex менялось между
# версиями — сверьтесь со своей (`codex --help`, раздел MCP). Не подхватилось —
# используйте stdio, там токен не нужен вовсе.
[mcp_servers.mcp1c.http_headers]
X-Api-Token = "ваш_токен"

Cursor

.cursor/mcp.json:

{
  "mcpServers": {
    "1c": {
      "type": "streamable-http",
      "url": "http://localhost:5001/mcp",
      "headers": { "X-Api-Token": "ваш_токен" }
    }
  }
}

VS Code (Copilot)

.vscode/mcp.json — here the key is called servers:

{
  "servers": {
    "1c": {
      "type": "http",
      "url": "http://localhost:5001/mcp",
      "headers": { "X-Api-Token": "ваш_токен" }
    }
  }
}

Qwen Code

Format from Gemini CLI, and the key selects the transport — this is the only subtlety:

{
  "mcpServers": {
    "1c": {
      "httpUrl": "http://localhost:5001/mcp",
      "headers": { "X-Api-Token": "ваш_токен" }
    }
  }
}

httpUrl — streamable-http, our case. url in this format means the old SSE transport: the client will start the handshake with GET /mcp, get 400 Missing session ID, and will not connect.

Other clients

Windsurf, Antigravity, Cline, Roo Code, console agents — the record format is the same: transport type, URL, and, if API_TOKEN is set, a headers block. The differences are only in the file name and the top-level key (mcpServers or servers) — check the specific client's documentation.

The client cannot set headers — not a dead end: connect via stdio, where no token is needed because there is no network.

Local launch via stdio

When the client must start the server itself. No token is needed here: the process is launched by the client, communication goes through the process channels, not through the network — there is nothing to check and no one to defend against.

{
  "mcpServers": {
    "1c": {
      "command": "python3",
      "args": ["-m", "mcp1c.server", "--transport", "stdio", "--data", "/путь/к/data"],
      "env": { "PYTHONPATH": "/путь/к/проекту/src" }
    }
  }
}

3. Tools

The set is fixed and deliberately small: each tool constantly hangs in the agent's context. A new data source enriches the answers of existing tools rather than adding its own.

Tool

Purpose

list_configurations

what is loaded, which providers are available for each configuration

search_objects(query, config, kind, limit)

human phrasing → exact object name

get_object(full_name, config, detail)

object composition; detail: brief / fields / full

get_related(full_name, config)

movements, links, dependencies — only direct ones

compare_configurations(full_name, configs)

one object in two configurations

search_syntax(query, config, kind, limit)

search across platform help and the query language

get_syntax(name, config, detail)

signature, parameters, availability, version, replacement for the old platform

config is required when more than one configuration is loaded: the server deliberately does not substitute it silently — otherwise the agent would write code against someone else's database, and no one would know.

One name can live in two domains at once: СтрНайти exists both in the platform (since 8.3.6) and in the query language. Then get_syntax lists the same-named entries with a ready address for each, and the call can be repeated with a line from the output:

get_syntax("СтрНайти")                    → Одноимённых элементов: 2
                                            - `Глобальный контекст.СтрНайти` — Метод, с 8.3.6
                                            - `Запрос.СтрНайти` — Функция запроса
get_syntax("Запрос.СтрНайти")             → карточка функции языка запросов

The qualifier Запрос. is needed because a query language element has no owner: it cannot be named via Объект.Член like a platform one.

Call order — and what is lost if you break it

list_configurations → search_objects → get_object → search_syntax → get_syntax

The get_object step cannot be skipped. Search returns only names and counters; everything the code depends on lives in the object card:

  • the kind and periodicity of the register. СрезПоследних exists only for a periodic information register, and 566 of 603 are non-periodic;

  • ready-made field names of virtual tables. In a query, the resource Количество is called КоличествоОстаток, КоличествоОборот, КоличествоПриход — these names are not visible anywhere in the configurator; the platform generates them;

  • the subconto limit, correspondence, schedule resources — without them, fields like СубконтоДт1 have nothing to be named by;

  • unlimited-length strings. Marked right in the type: Строка (неогр. — только через ПОДСТРОКА) versus Строка(200). Such a field cannot be put into a query as is — the platform will not let you compare, group, or order by it. Such fields make up 23% to 38% of string fields in live configurations, so on cards where they occur (2,474 of 20,522), before the field list a caveat with a recipe is printed.

A query written right after search_objects looks correct and fails with "field not found". An example of what only comes from get_object:

## Таблицы запроса
- `РегистрНакопления.ТоварыНаСкладах.Остатки`
  измерения: Склад, Номенклатура, Характеристика
  ресурсы: КоличествоОстаток, РезервОстаток

A second one like it — and it was found by a live miss on 2026-08-18. The agent grouped a query by an unlimited-length string; we returned correct data, but the difference was only readable by the absence of a number in parentheses:

> **Строки неограниченной длины** помечены `(неогр.)`. Платформа не даёт их
> сравнивать, группировать и упорядочивать и не пускает в РАЗЛИЧНЫЕ,
> ОБЪЕДИНИТЬ и агрегатные КОЛИЧЕСТВО, МИНИМУМ, МАКСИМУМ. Ограничивайте
> длину — одинаково в списке выборки и в группировке:
>
>     ПОДСТРОКА(КодСкидки, 1, 100) КАК КодСкидки
>
> Длину подбирайте по смыслу поля: 100 — не универсальное число.

## Реквизиты

- `КодСкидки` — Строка (неогр. — только через ПОДСТРОКА) // Код скидки
- `КодМаркировки` — Строка(200) // Код маркировки

The recipe is in both the caveat and the field string itself, and this is not redundancy. The first revision printed the caveat as the last paragraph of the card. A live agent on 2026-08-18 called get_object with detail=fields, received it in full — and still grouped by such a field. The caveat stood 721 tokens after the field string, and the decision is made where the name is copied. The same lesson was already written on tool descriptions: a rule works where it is read, not where it is more neatly placed.

Each prohibition is verified: aggregates — a quote from the help, the other five — runs against a live database with recorded error texts. The help knows about the restriction in only three of the six aggregate functions and is silent about grouping, ordering, РАЗЛИЧНЫЕ, ОБЪЕДИНИТЬ, and comparison — that is, an agent that honestly read it could not have learned about this. The breakdown by origin is in docs/data-sources.md, section «Оговорки в карточке».

Before calling a platform function on an old configuration — get_syntax. Unavailable items are marked, and the replacement recipe, if recorded, is right there.

Sources are independent

There are three of them, and each connects separately:

Source

File

What it provides

Without it

Configuration metadata

СтруктураКонфигурации_*.zip

objects, attributes, links, movements

search_objects and get_object do not respond

Platform help

shcntx_ru.hbk

methods, properties, signatures, availability, versions

search_syntax says "source not connected"

Query language

shquery_ru.hbk

ВЫБРАТЬ, ЛЕВОЕ СОЕДИНЕНИЕ, ИТОГИ ПО, РАЗНОСТЬДАТ

query language constructs are not found

What is loaded

What works

All three

everything

Only configuration

metadata; syntax responds "source not connected"

Only help

syntax without version filtering, config not needed

Nothing

list_configurations explains what to load

Query language — a separate source

shquery_ru.hbk from the same platform installation directory. 127 pages: 52 functions, 67 keywords, 8 articles. It loads as a regular source and goes into the same search index as the platform help — no separate tool is needed to search; search_syntax finds both.

There are no versions in the file itself — verified across all 129 pages: zero mentions of "8.3.x" and "since version". But the query language changes: release 8.3.20 added 25 functions, among them СтрНайти, Лев, Прав, ВРег, НРег, СтрЗаменить, Окр, Цел, and all of trigonometry.

There's nowhere to take the version from: the platform help for the query language doesn't describe it at all (ПОДСТРОКА — zero matches across 25,511 items). So versions are assigned by the curated table query_versions.py — based on 1C's list "Functions added to the query language starting with release 8.3.20". The remaining 27 functions get no version: they've always been there.

Then the usual filter does its job: a configuration on 8.3.5 won't see these functions, one on 8.3.23 will.

The table is validated against data — by comparing two help files from different platforms. What's absent in the old one and present in the new one appeared between them, and it must have a version:

python3 tools/lab/compare_query_help.py <старая.hbk> <новая.hbk>

Run on 2026-08-19, 8.3.5.1570 against the current one: 29 appeared, 29 covered, 0 false positives. A false positive is the worst kind of error: an item that was already in the old help but is marked with a version will hide from a configuration where it exists.

One instance per server: a repeated load replaces the previous one.

Page tables are shown but not searched. In this help, table cells are marked up as paragraphs inside <TD>, and without separate parsing the card printed the table as a column of values: "Товар / Количество / Номер / Сантехника / 104 / …" for two dozen rows in a row. Now tables are parsed into a separate field — 51 tables across 31 pages out of 127 — and printed in their places in the text: a page with two examples shows each result under its own example. Table contents do not enter the search index.

Tables in this help are of two different natures, and they're parsed differently:

What

How many

How it looks in the card

data table — the result of an example query

51 across 31 pages

as a markdown table

drawn syntax diagram — the grammar of a construct

21 across 17 pages

as a step ladder indented by branching level

They differ by markup, not by CSS class: class=SimplyTable isn't on all of them — 7 real tables go without it. The distinguishing feature is geometry: in a data table all rows are the same width, in a diagram the widths are ragged and there are cells made of a single vertical bar (that's a drawn line, not a value).

Broken markup is called out by name. A page with an unclosed <TABLE> is parsed without tables but isn't lost, and its name lands in the source warnings: as a line in the load output (mcp1c.cli reg-add) and as a separate line on the "Sources" page of the dashboard. Silently handing over a card poorer than usual is not acceptable: that would be indistinguishable from a help file that simply doesn't have it.

Half the names match platform names (57 of 127) — ГОД, МЕСЯЦ, ПРЕДСТАВЛЕНИЕ exist in both. So that a query question doesn't lead to a platform method, phrasings like "in a query", "in the query text", "in a result set" give query language items a gentle boost. Gentle on purpose: with a confident lead, the platform item stays first — "how to set a parameter in a query" could also be about Запрос.УстановитьПараметр.

The config parameter is required if more than one configuration is loaded. Nothing is substituted by default: a silent choice leads to the agent writing code against someone else's configuration, and nobody notices.

The answer depends on the platform version

The same call, two configurations:

get_syntax("СтрШаблон", config="Розница")          → 8.3.23
# Метод: Глобальный контекст.СтрШаблон
с версии платформы 8.3.6
Доступность: ТонкийКлиент, ВебКлиент, Сервер, ТолстыйКлиент, …

get_syntax("СтрШаблон", config="Ювелирный")        → 8.3.5
# `Глобальный контекст.СтрШаблон` недоступен в этой конфигурации
Элемент существует, но появился в 8.3.6, а конфигурация работает на 8.3.5.1570.
Использовать нельзя — код не скомпилируется.

For platform 8.3.5, 6,539 items were removed from the output; for 8.3.23 — 874. Not as a warning but as filtering: the agent will skip a warning, but a method absent from the output — no.

The Availability field (server / thin client / web client / mobile) must be read: calling a server method from a client context doesn't compile.

Help files of several versions merge into one index

One fresh help file on an old configuration lies. Measured on 8.3.5: 199 items the server would declare nonexistent, 117 it would return with a foreign signature (ЗаписьXML.ОткрытьФайл on 8.3.5 takes two parameters, in 8.3.27 — three), 410 — with foreign availability. All of these are compilation errors, not inaccuracies.

So help files of different versions are placed side by side and merged into one index with since and until boundaries, and the answer is assembled for the version of the specific configuration. You need as many help files as there are platforms among the loaded configurations — two extreme intermediate ones don't replace them.

The cost is measured and small: merging three versions yields 25,691 keys against 24,777 for one, that is, less than a percent. A separate container per version also works and remains the fallback path, but as the primary one it lost on the numbers — 300–450 MB and its own address for each version.

The server itself names which help files are missing and which are extra — in the list_configurations output.

Replacement instead of prohibition

Saying "the function doesn't exist" is half the answer. The second half is what to replace it with, and that can't be derived from the help: the deprecation note is on 15 pages out of 25 thousand.

So there's a replacement table (replacements.py), currently 6 entries — string functions that appeared in 8.3.6. Instead of a prohibition, get_syntax returns a recipe:

get_syntax("СтрРазделить", config="Ювелирный")     → 8.3.5
# `СтрРазделить` недоступна: появилась в 8.3.6

Замена: РазложитьСтрокуВМассивПодстрок(<Строка>, <Разделитель>)
Оговорка: разделитель у `СтрРазделить` — набор символов, каждый из которых
самостоятельный разделитель; у замены это одна строка целиком.

A caveat is mandatory. A replacement is almost never equivalent, and silently slipping in a similar function is worse than offering no hint at all.

The table is filled from live cases, not blindly: there's no point in inventing workarounds for functions nobody has asked about.


4. Data management

Adding a source

# в Docker
docker compose exec mcp1c python -m mcp1c.cli reg-add /data/bootstrap/Выгрузка.zip --data /data
docker compose exec mcp1c python -m mcp1c.cli reg-add /data/bootstrap/shcntx_ru.hbk --data /data

# без Docker
PYTHONPATH=src python3 -m mcp1c.cli reg-add Выгрузка.zip

Simpler: put the file in data/bootstrap/ — it will be picked up at the next start.

Applying changes without a restart

A running server keeps the registry in memory, so after reg-add it needs a nudge. Either a restart (docker compose restart mcp1c, about 2 seconds), or the admin endpoint:

# включается переменной ADMIN_TOKEN; без неё маршрут отключён
ADMIN_TOKEN=секрет docker compose up -d
curl -X POST -H "x-admin-token: секрет" http://localhost:5001/admin/reload

Dictionary: how people speak vs. what things are called

The main difficulty of search is the gap between human words and names in the configuration. "Заказ клиента" — but the object is called ЗаказПокупателя. The dictionary lives in data/dictionary.json and is edited without rebuilding the image.

Two mechanisms, and they're different.

Word synonyms — common to all configurations:

python3 -m mcp1c.cli dict-synonyms клиент покупатель заказчик

Object aliases — a direct statement "when I say this, I mean these objects", with weight above any textual match. Two dozen typical phrases ("файлы", "товары", "клиенты", "сотрудники", "задачи") are built in and work right away; if the object isn't in the configuration, the alias isn't applied. Your own are added bound to a configuration:

python3 -m mcp1c.cli dict-alias "справочник физлиц" \
    Справочник.ФизическиеЛица Справочник.Пользователи \
    --config РозницаДляКазахстана
«справочник физлиц»
    Справочник.ФизическиеЛица     псевдоним из словаря
    Справочник.Пользователи       псевдоним из словаря

Object existence is checked at addition time — an alias for a typo is useless. View contents: dict-show, delete: dict-alias «фраза» --remove.

Changes are applied by restarting the container or POST /admin/reload — no need to rebuild the image.

Query language search keys — a third mechanism, and it's edited only in code (search_keys.py, in git with review). The gap here is of a different nature: a person doesn't call a construct by a foreign word, but describes a task. "Количество дней между двумя датами" against РАЗНОСТЬДАТ, "убрать повторы" against РАЗЛИЧНЫЕ — zero common words, and a synonym won't help, there's nothing to replace.

So 116 pages out of 127 have phrasings attached that people use to ask for them, and they enter the search index as a separate field. At runtime they cost nothing. Result on a live set: 57.9% → 94.7% in first place, with no regression across 61 thousand automated queries.

The keys are invented by us, not exported, and from that come three constraints:

  • they live as a separate layer in git, not attached to the parsed item;

  • they don't reach the agent's answer — the answer is still assembled only from the help, the keys work solely on getting to the right article;

  • they're bound to pages by identifier, and if the help produces a different set of pages, the discrepancy is called out at load time rather than silently showing up as degraded search.

The full rule is in docs/data-sources.md, section "Сгенерированные слои поверх источников".

Viewing what's loaded

docker compose exec mcp1c python -m mcp1c.cli reg-list --data /data
РозницаДляКазахстана  2.3.10.5  платформа 8.3.23.1997
  объектов 5637, связей 44034, загружено 2026-08-18T12:22:16+00:00
  метаданные : да
  синтаксис  : справка 8.3.27, новее конфигурации, скрыто 874
  модули     : не подключены
  язык запросов: подключён, 127 страниц

There may be no configurations at all — the server still works in that case if at least one help file is loaded: search_syntax and get_syntax answer, and config doesn't need to be specified. reg-list in this case lists what's connected and returns 0:

Конфигурации не загружены. Подключено:
  язык запросов, 127 страниц
Работают search_syntax и get_syntax, без фильтра по версии.

On a completely empty registry — "Ничего не загружено." and exit code 1. Any command that needs a configuration will say right there what exactly is missing and how each thing is obtained.

Debugging without an agent — mcp1c.cli

The CLI goes to the same registry and the same functions as the MCP tools. If it answers correctly — the problem is in the client setup, not the server.

Commands fall into three groups. Registry — the same thing the agent sees:

PYTHONPATH=src python3 -m mcp1c.cli reg-list  [--data data]
PYTHONPATH=src python3 -m mcp1c.cli reg-add   Выгрузка.zip     [--data data]
PYTHONPATH=src python3 -m mcp1c.cli reg-add   shcntx_ru.hbk    [--data data]
PYTHONPATH=src python3 -m mcp1c.cli reg-search "чек ккм"  --config РозницаДляКазахстана
PYTHONPATH=src python3 -m mcp1c.cli reg-search "разделить строку" --syntax --limit 5

reg-search without --syntax searches metadata; with it — the help and query language.

Directly against a file, without the registry — to look at an export before it goes to the server:

PYTHONPATH=src python3 -m mcp1c.cli info    Выгрузка.zip
PYTHONPATH=src python3 -m mcp1c.cli stats   Выгрузка.zip
PYTHONPATH=src python3 -m mcp1c.cli show    Выгрузка.zip Документ.ЧекККМ --detail full
PYTHONPATH=src python3 -m mcp1c.cli related Выгрузка.zip Документ.ЧекККМ --depth 2
PYTHONPATH=src python3 -m mcp1c.cli find    Выгрузка.zip реализация --limit 10

The path is a ZIP or an unpacked directory; the format is determined by the manifest.

Search dictionary — synonyms are common, aliases are bound to a configuration:

PYTHONPATH=src python3 -m mcp1c.cli dict-show                       # правила и их происхождение
PYTHONPATH=src python3 -m mcp1c.cli dict-show --all --config Розница...
PYTHONPATH=src python3 -m mcp1c.cli dict-synonyms чек ккм касса     # группа взаимозаменяемых слов
PYTHONPATH=src python3 -m mcp1c.cli dict-synonyms чек ккм --remove
PYTHONPATH=src python3 -m mcp1c.cli dict-alias "справочник физлиц" Справочник.ФизическиеЛица
PYTHONPATH=src python3 -m mcp1c.cli dict-alias "справочник физлиц" --remove

dict-show shows the origin of each rule — that's where the analysis of "why search behaves this way" begins.

Measuring search quality — mcp1c.bench

A separate stand, because "it got better" without numbers is an opinion.

PYTHONPATH=src .venv/bin/python -m mcp1c.bench \
    --data data --config РозницаДляКазахстана \
    --auto --sets query-language,roznica-metadata --check-notes

Key

What it does

--sets имя,имя

manual sets from tests/queries/*.json, without extension

--auto

automated sets from the help: exact names and same-named ones

--config

configuration; required if several are loaded

--limit

output depth, default 10

--save путь

write a run for comparison; by convention data/bench/ГГГГ-ММ-ДД.json

--baseline путь

compare with a previous run — names by name who changed places

--check-notes

verify the notes in the set against what place the query took

Prints P@1/P@3/P@5/P@10, MRR, the share of "foreign domain first" and the median gap between the first and second result. There are deliberately no thresholds in asserts: query sets aren't tests, percentages would break on every dictionary edit. A nonzero exit code happens only on a mismatch of notes — that's not search quality, that's lying in the file.

Comparing two runs looks like this (regressions first):

=== сравнение с прошлым прогоном ===
  - «как прибавить месяц к дате в запросе»: 1 -> промах
  - «как отсортировать результат запроса»: 1 -> 5
  + «в чем разница между внутренним и левым соединением»: 5 -> 4

Sets don't go into the image (tests/ in .dockerignore) — run from a working copy, not from the container.

The server manually — mcp1c.server

PYTHONPATH=src python3 -m mcp1c.server --data data          # streamable-http на :8000/mcp
PYTHONPATH=src python3 -m mcp1c.server --transport stdio    # локальному клиенту
PYTHONPATH=src python3 -m mcp1c.server --host 0.0.0.0 --port 5001

--transport sse exists in the code and works, but isn't exposed: the SDK raises one transport per process, and all our state is in memory — a second transport would cost about as much as the first. SSE itself in MCP is declared deprecated in favor of streamable-http.

Where the source data comes from

Configuration structure — via the processor from exporter-1c/. Four module variants for regular and managed forms, XML and JSON; the XML variants are compatible with 8.3.5. Two processors are already built and open as-is: ВыгрузкаСтруктурыКонфигурации_ОбычнаяФорма_XML.epf (8.3.5 and above) and ВыгрузкаСтруктурыКонфигурации_УправляемаяФорма_XML_JSON.epf (8.3.6 and above, the format is chosen on the form).

Platform help — the file shcntx_ru.hbk from the 1C installation directory:

/opt/1cv8/<версия>/shcntx_ru.hbk
C:\Program Files\1cv8\<версия>\bin\shcntx_ru.hbk

The name must match exactly. The same directory contains hundreds of .hbk files — 38 different help files, each in two dozen languages. Similar to the one we need:

File

What it is

Why it does not fit

shcntx_root.hbk

the same help, language-independent part

25,508 elements, but not a single description: only the page tree and English identifiers, with no since-versions

shlang_ru.hbk

description of the built-in language

not a 1С container at all

shquery_ru.hbk

query language

same

config_ru.hbk

configurator help

it is a container, but there are no syntax-assistant pages inside

1cv8_ru.hbk

user guide

not a container

You cannot tell them apart by size: shcntx_root.hbk weighs 33 MB versus 39 MB for the one we need. The _ru suffix is the language, _root is the common part without texts.

Wrong file — the server explains exactly why and leaves the existing help in place.

One help from the newest available platform is enough: each element carries its since-version, and for old configurations the excess is filtered out. If the version is not in the path, it is derived from the data itself.

Help from old platforms is also accepted — they are marked up differently (sections in div instead of p), and this is accounted for. Useful if you are setting up a separate server for old deployments: the 8.3.5 help gives 18,936 elements and does not contain СтрНайти, СтрРазделить, ЗаписьJSON — they did not exist in 8.3.5. But such help does not report its version: there are no “since version” marks in it, because back then everything was current. Therefore the version is taken from the file or directory name — put it as 8.3.5.1570.hbk or in data/hbk/8.3.5.1570/, otherwise matching against the configuration will not work.


5. How it works

src/mcp1c/
  v8container.py     контейнер 1С — общий для .hbk, .cf, .epf
  syntax_parser.py   разбор справки платформы
  syntax_model.py    модель элемента справки, виды, границы версий
  syntax_merge.py    слияние справок разных версий в один индекс
  query_parser.py    разбор справки по языку запросов (shquery_ru.hbk)
  replacements.py    чем заменить функцию, которой нет в старой платформе
  virtual_tables.py  таблицы запроса регистров и имена их полей
  loader.py          чтение выгрузок, XML и JSON в одну модель
  model.py           модель конфигурации
  graph.py           граф связей
  graph_view.py      окрестность объекта для картинки на дашборде
  search.py          лексический поиск
  search_keys.py     формулировки, которыми спрашивают язык запросов
  synonyms.py        встроенный словарь: как говорят против того, как названо
  dictionary.py      локальный словарь поверх встроенного
  index_cache.py     кэш поисковых индексов, расходный
  store.py           чтение и запись разобранных справок
  render.py          markdown-карточки объектов и элементов
  registry.py        реестр источников, сопоставление версий
  tools.py           семь инструментов, без зависимости от MCP
  server.py          протокольный слой (единственная внешняя зависимость)
  dashboard.py       веб-интерфейс: реестр, запросы, словарь
  cli.py             отладочный CLI
  bench.py           стенд замеров качества поиска

One model for two formats. XML and JSON are different serializations of the same schema; the loader reduces both to the same dictionary. Verified: both exports produce the same set of 30 keys.

The loader builds the graph, not 1С. Edges are derived from attribute types, document movements, input bases, owners, subscription handlers, and scheduled job methods. The rules can be changed without re-exporting.

Weak edges. Attributes like ЗначениеДоступа list hundreds of types and connect almost everything to everything. Such links are marked weak and hidden by default — otherwise the useful ones drown in them.

Detail levels. The full description of Документ.ЧекККМ (50 attributes, 17 tabular sections) eats the entire context. brief is a couple of lines, fields is the composition, full is with links.

No external databases. Five configurations with help are held in the memory of one process — 628 MB, cold start from disk 9.4 s. Elasticsearch, a vector store, and a graph database were considered and rejected with numbers: the breakdown is in docs/TASKBOARD.md, section “Deferred”. In short: half a million documents is too little for ES, and the whole cost of a vector is not in the store but in the encoder model at runtime (+185–620 MB to the image for torch, query encoding 16–32 ms versus the current 0.18–1.4 ms for the entire search).

Measured on real data — 2026-08-18

What is loaded on the working server:

Configuration

Platform

Objects

Edges

Accounting for Kazakhstan

8.3.27.1936

3,492

84,426

Document Management CORP

8.3.27.1936

4,596

50,554

Payroll and HR Management

8.3.27.1936

5,181

100,136

Retail for Kazakhstan

8.3.23.1997

5,637

58,345

Jewelry Trading House

8.3.5.1570

1,616

29,288

Total

20,522

322,749

Plus the platform help — a merged index of three versions (8.3.5, 8.3.23, 8.3.27), 25,691 elements, and the query language — 127 pages as a separate source.

Start from cache — 9.4 s on all of this. The first start is longer: sources are parsed, indexes are built and placed in data/index/cache/ (12 MB), parsed help files in data/index/syntax/ (11 MB). After that they are loaded from there.

The cache is derived and expendable: it is tied to the Python version, the package code fingerprint, and the source hash. If anything does not match, the indexes are rebuilt. The directory can be deleted at any time; it will rebuild itself.

Live container memory — 628 MB. Index postings are stored as numpy arrays; the payload remains dictionary-based and is freed immediately after freezing. The image is 354 MB.

Module texts — reconnaissance, no provider yet

The modules provider is not done; the server exposes no code tools. The cost was measured in advance on the “Retail” 2.3.10.5 export to files (2,063 MB, 33,188 files, 7,878 modules, 136,909 procedures):

Layer

On disk

In memory

procedure signatures and addresses

18.1 MB

65 MB

search across all procedures

473 MB

search only over exported ones (49,068)

156 MB

forms: 3,194 files, 69,769 elements

5.8 MB

44 MB

Search latency is 0.4–1.2 ms. Corpus parsing is 7–10 s.

There are two different file exports, and the second was measured separately — “Jewelry Trading House” 10.5.1.3 on 8.3.5: flat layout, modules in .txt, ordinary-form code inside binary .Form containers. 2,603 modules, 33,555 procedures, parsing 1.1 s, search over all 94 MB with a median of 0.2 ms. This format does not contain form structures, and some common modules are shipped compiled — there is no source in them at all.

Measurement scripts live in tools/lab/; they are exploratory and will be thrown away when a real provider appears. For now they reproduce every figure above:

python3 tools/lab/measure_modules.py <каталог выгрузки в файлы>
python3 tools/lab/measure_resident.py <каталог> <файл индекса> собрать
python3 tools/lab/measure_search.py <файл индекса> [экспортные]
python3 tools/lab/measure_forms.py <каталог>
python3 tools/lab/measure_flat.py <каталог плоской выгрузки>

Full breakdown, including the structure of configuration extensions, is in docs/modules-and-extensions-2026-08-18.md.

Search quality

Measured by a benchmark, reproduced with a single command:

PYTHONPATH=src .venv/bin/python -m mcp1c.bench \
    --data data --config РозницаДляКазахстана \
    --auto --sets query-language,roznica-metadata --check-notes

Set

Queries

P@1

P@3

P@5

MRR

Gap

Query language

19

94.7%

94.7%

100%

0.958

35.0%

Retail metadata

21

90.5%

95.2%

95.2%

0.934

94.0%

Exact help names

50,926

97.1%

98.3%

98.7%

0.978

91.7%

Same-named

10,544

98.8%

99.8%

99.9%

0.993

93.8%

The first two sets are manual, from real misses. The second two are built from the data itself: an element name as the query, and the same element as the expected answer.

“Gap” is how far the first result pulled ahead of the second, by median. It answers the question “hit confidently or by luck”: 35% for the query language versus 91.7% for the help means those wins hold three times more weakly, and a ranking tweak can overturn them without moving a single percentage point of P@1.

Search latency is 0.18–1.4 ms per query depending on the set.

Query sets are not included in the image (tests/ in .dockerignore): you must measure from a working copy, not from the container.

Tests

.venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest          # 371 тест, ~2 с

They do not depend on the contents of data/: there are no proprietary exports in the repository; everything needed is assembled synthetically in tests/conftest.py.

Search quality is not checked by tests — it is measured by the benchmark (mcp1c.bench, see “Measured”). Percentage thresholds would break on every dictionary edit, so the benchmark prints numbers and a human makes the decision. pytest checks observable behavior: “index was not rebuilt”, “output matched”, “startup did not crash”.


6. Security

Two tokens, both set via environment variables. Until a token is set, the corresponding access is open to anyone who can reach the address.

Variable

What it protects

Not set

API_TOKEN

read: MCP tools and dashboard pages

configuration structure is open to everyone

ADMIN_TOKEN

write: loading and deleting sources, dictionary editing, /admin/reload

these routes are disabled and respond 404

The difference between “open” and “disabled” is intentional. Reading without a token works — on your own machine this is convenient and harmless. Writing without a token does not work at all: one bad dictionary edit silently breaks search for everyone connected to the shared server.

The token is passed in a header — either X-Api-Token or Authorization: Bearer <токен>. The admin token also works for reading: otherwise the owner would have to keep two headers in the client instead of one.

The token must be in Latin characters. HTTP headers are encoded in latin-1, and a Cyrillic token physically never reaches the server: it works through the browser login form, but not through a client header.

Two paths bypass the check: /health (the container healthcheck uses it, and it exposes no more than read access) and /login — otherwise the login form would be behind the very authorization it issues.

Two more rules, not about tokens:

  • The MCP endpoint returns the entire configuration structure. Set API_TOKEN whenever you expose it beyond your own machine; network access is not enough.

  • The entire data/ directory is in .gitignore — both .hbk files with exports and parsed indexes. The help index is the same 1С company content, just unpacked. Once it got in there and sat for 20 commits; the history was rewritten with git filter-repo, and the rule was reformulated by directory rather than by extensions: you should check not “is this .hbk?” but “does this live in data/?”.


7. Documents

File

About

AGENTS.md

rules for working on the project

CHANGELOG.md

what has been done and what has been learned about 1С

docs/TASKBOARD.md

plans, priorities, and rejected proposals with reasons

docs/schema-v1.md

export format contract

docs/data-sources.md

what we take from which source

docs/query-language-design.md

structure of the query language source

docs/dashboard-design.md

dashboard structure

docs/market-review-2026-08-17.md

review of alternatives and what was taken from it

exporter-1c/README.md

export processing for 1С

The “Deferred” section in the task board is rejected proposals with numbers: external DB, vectors, graph DB, SSE to the outside, lazy loading. Before proposing any of this again, it is worth reading: such decisions are overturned by new measurements, not by new considerations.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AzeevAN/mcp-1c'

If you have feedback or need assistance with the MCP directory API, please join our Discord server