mcp-1c
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-1cShow me the structure of the document 'ПоступлениеТоваровУслуг'."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 | |
Loader, model, relationship graph, render | ✅ 5 configurations, 20,522 objects, 322 thousand edges |
Platform help | ✅ merged index of three versions, 25,691 elements, |
Query language | ✅ |
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 | ✅ |
Tests | ✅ |
Dashboard | ✅ registry, sources, query runs, relationship graph, cards, dictionary |
Authorization | ✅ |
Module index from | ⬜ |
Contents
Launch — Docker, dashboard, relationship graph, without Docker
Connecting an agent — how MCP works, if it does not connect, token, client configs: Claude Code, Codex CLI, Cursor, VS Code, Qwen Code, stdio
Tools — call order, sources, query language, platform versions, merging help files, replacements
Data management — sources, dictionary and search keys, CLI, benchmark stand, server manually, where to get data
How it works — modules, measurements, tests
Security — tokens, what is open without them
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-recreaterestart 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 |
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-recreateThe 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 50012. 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 |
|
stdio | the client starts the process locally itself | — |
| only for old clients |
|
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 |
|
|
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 |
| the client does not send |
| the client did not return the |
| the client started the handshake with |
| same thing: the old transport is not exposed |
|
|
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_TOKENgoes into the client, notADMIN_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.stdiorequires 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/mcp200 — 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:
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.Remove the file from git:
git rm --cached .mcp.json && echo ".mcp.json" >> .gitignore.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 ~/.zshrcOr 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 |
| what is loaded, which providers are available for each configuration |
| human phrasing → exact object name |
| object composition; |
| movements, links, dependencies — only direct ones |
| one object in two configurations |
| search across platform help and the query language |
| 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_syntaxThe 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
СубконтоДт1have 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 |
| objects, attributes, links, movements |
|
Platform help |
| methods, properties, signatures, availability, versions |
|
Query language |
|
| 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, |
Nothing |
|
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 Выгрузка.zipSimpler: 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/reloadDictionary: 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 5reg-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 10The 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 "справочник физлиц" --removedict-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-notesKey | What it does |
| manual sets from |
| automated sets from the help: exact names and same-named ones |
| configuration; required if several are loaded |
| output depth, default 10 |
| write a run for comparison; by convention |
| compare with a previous run — names by name who changed places |
| 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 -> 4Sets 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.hbkThe 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 |
| 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 |
| description of the built-in language | not a 1С container at all |
| query language | same |
| configurator help | it is a container, but there are no syntax-assistant pages inside |
| 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-notesSet | 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 |
| read: MCP tools and dashboard pages | configuration structure is open to everyone |
| write: loading and deleting sources, dictionary editing, | 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_TOKENwhenever you expose it beyond your own machine; network access is not enough.The entire
data/directory is in.gitignore— both.hbkfiles 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 withgit filter-repo, and the rule was reformulated by directory rather than by extensions: you should check not “is this.hbk?” but “does this live indata/?”.
7. Documents
File | About |
rules for working on the project | |
what has been done and what has been learned about 1С | |
plans, priorities, and rejected proposals with reasons | |
export format contract | |
what we take from which source | |
structure of the query language source | |
dashboard structure | |
review of alternatives and what was taken from it | |
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.
This server cannot be installed
Maintenance
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
Get up-to-date, version-specific documentation and code examples from official sources directly in…
RU INN/OGRN, banks, geo, WHOIS. Agent self-registers via register_agent. 20 free/day.
RedM / RDR3 docs MCP server: native lookups, semantic search, VORP, RSGCore, oxmysql.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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