mcgl-mcp 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,196 +1,82 @@
1
1
  # mcgl-mcp
2
2
 
3
- MCP-сервер для форума [Minecraft Galaxy](https://forum.minecraft-galaxy.ru/main/). API у форума нет, поэтому сервер ходит на него как браузер: HTML, формы, кука `PHPSESSID`.
3
+ [![npm](https://img.shields.io/npm/v/mcgl-mcp)](https://www.npmjs.com/package/mcgl-mcp)
4
+ [![CI](https://github.com/exi66/mcgl-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/exi66/mcgl-mcp/actions/workflows/ci.yml)
4
5
 
5
- **Текущее состояние:** авторизация игрока и чтение общего форума. Действий от лица игрока пока нет.
6
+ MCP-сервер для [форума Minecraft Galaxy](https://forum.minecraft-galaxy.ru/main/). Подключается к Claude, Cursor и другим AI-ассистентам и даёт им читать форум: разделы, темы, посты, опросы.
6
7
 
7
- ## Инструменты
8
-
9
- Все инструменты публичные: авторизация не нужна, работают в обоих режимах. В локальном режиме с кредами страницы читаются залогиненным.
10
-
11
- | Инструмент | Страница | Что возвращает |
12
- |---|---|---|
13
- | `forum_list_sections` | `/main/` | разделы 1..11 по категориям: описание, число тем и ответов, последнее сообщение |
14
- | `forum_latest_topics` | `/forum/0/{page}` | последние темы всех разделов, у каждой указан раздел |
15
- | `forum_list_topics` | `/forum/{id}/{page}` | темы раздела: закреплена, закрыта (зачёркнута), кто может отвечать (`all`/`vip`/`premium_platinum`/`moderators`), автор, ответы, число страниц, последнее сообщение |
16
- | `forum_get_topic` | `/topic/{id}/{page}` | без `page` — последние 15 постов темы; с `page` — страницы по 15 от начала (`pageSize` 1..50). Всего постов и страниц; в режиме «последние» и на 1-й странице ещё опрос, системные сообщения («перемещено из…») и кто читает сейчас |
17
- | `forum_get_post` | `/post/{id}` | один пост с номером в теме, его тема и страница `forum_get_topic`, на которой он находится |
18
-
19
- В инструментах страницы нумеруются с 1, в URL форума — с 0.
20
-
21
- **Пагинация тем.** Форум показывает на странице столько постов, сколько задано в настройках игрока (у гостя и игроков бывает 15…100). Агенту сто постов за раз дорого, поэтому `forum_get_topic` отдаёт свои страницы фиксированного размера поверх пагинации форума. Номера постов `#N` в теме сквозные, удалённые тоже нумеруются, так что страница `v` — это посты `#(v-1)*15+1 … #v*15`. Без `page` отдаются последние 15 постов: в больших темах обычно нужен конец. Если тема недавно встречалась в списке тем, там уже было число её страниц, и `ForumReader` открывает последнюю страницу форума одним запросом. Если тема с тех пор выросла на новую страницу, это видно по пагинации, и reader идёт обычным путём. Размер страницы форума `ForumReader` узнаёт по первой (или последней) странице темы и перепроверяет по номерам: если игрок поменял настройку, пересчитывает. Разобранные страницы форума кешируются на 60 секунд, чтобы листание темы не скачивало их заново.
22
-
23
- **Недоступность форума.** GET повторяется до 3 раз при сетевой ошибке, таймауте или 5xx, с паузами 1 и 2 секунды. POST не повторяется: повтор может создать дубль поста. После 2 подряд неудачных запросов срабатывает circuit breaker (`src/forum/health.ts`): 30 секунд запросы на форум не идут, инструменты сразу отвечают «форум не отвечает, повтори через N секунд». Затем проходит один пробный запрос. Состояние видно в `/health` HTTP-режима.
24
-
25
- Пост содержит номер, id, автора (ник, id, группа, статус, клан со званием, число ответов), дату, текст и рейтинг. Удалённый пост возвращается как `{deleted: true, number, id}`. Если аккаунт автора удалён или заблокирован (на форуме это `player$0`), у автора стоит `accountDeleted: true`.
26
-
27
- Текст поста получается из отрендеренного bbCode ([справка](https://forum.minecraft-galaxy.ru/wiki/201)):
28
-
29
- - `[quote: ник] … [/quote]`, `[spoiler: заголовок] … [/spoiler]`;
30
- - `[image: url]`, `[video: url]`, `[audio: url]`;
31
- - предметы — `[Золотой блок #41]`, с метадатой — `[Красная шерсть #35:14]`;
32
- - окна крафта, печи и наковальни — `[craft]`: сетка id предметов (`-` — пустой слот, `->` — результат, `x4` — количество) и легенда `124 = Ажурный камень`;
33
- - таблицы — ячейками через `|`, списки — с `- `;
34
- - ссылки — как `текст (url)`, редиректы форума раскрываются.
35
-
36
- Названия предметов берутся из эндпоинта подсказок форума `/object/{id}/2?d={meta}` (`src/forum/objects.ts`). Кеш живёт весь процесс. За один вызов инструмента делается не больше 25 новых запросов, остальные предметы остаются как `[item #id]`. Тот же эндпоинт отдаёт карточку игрока (тип 1) и сообщества (тип 3).
37
-
38
- ### Экономия токенов
8
+ Что можно попросить у ассистента:
39
9
 
40
- - Описания инструментов и служебные маркеры в тексте — на английском и в ASCII. Контент форума остаётся как есть, на русском.
41
- - Ответы — компактный JSON без отступов.
42
- - Флаги `false` не отдаются (`pinned`, `closed`, `deleted`, `accountDeleted` есть только при `true`); об этом сказано в описаниях инструментов.
10
+ - «Что нового на форуме MCGL?»
11
+ - «Перескажи последние сообщения в теме про Гран-при амфибий»
12
+ - «О чём спорят в разделе Предложения?»
13
+ - «Найди в теме 133931 рецепт крафта и объясни его»
43
14
 
44
- Парсеры в `src/forum/parsers/` общие: списки тем и страницы тем одинаково свёрстаны во всём форуме, включая сообщества (`/htopic/N`, `/hpost/N`).
15
+ ## Установка
45
16
 
46
- ## Режимы
47
-
48
- | Режим | Транспорт | Инструменты | Авторизация |
49
- |---|---|---|---|
50
- | Локальный | stdio | публичные + от лица игрока | логин и пароль игрока в env клиента |
51
- | Хост | Streamable HTTP, `POST /public/mcp` | только публичные | нет (пока) |
52
-
53
- ## Авторизация на форуме
54
-
55
- 1. `GET /guilogin/`: из формы `form[action="/login.php"]` берутся скрытые поля `form`, `ref`, `fcode`, `recap`.
56
- 2. `POST /login.php` с этими полями плюс `login` и `pass`.
57
- - Успех: 302 на `/main/`, в шапке появляется ник (`a.button[href="/profilemain/"] .text`).
58
- - Неудача: 302 на `/guilogin/4?f=N` с плашкой `table.alert td.caption` («Введенный логин или пароль неверны! Попыток входа осталось: N…»).
59
- 3. Гостевая страница определяется по кнопке «Войти» в шапке (`a.button[href="/guilogin/"]`). Форма ответа в теме видна и гостю, поэтому по ней судить о логине нельзя.
60
-
61
- Особенности форума:
62
-
63
- - **`fcode` обязателен.** Без него вход отклоняется и списывается попытка.
64
- - **reCAPTCHA v3 сервер не проверяет**, `recap` уходит пустым.
65
- - **У входов есть лимит попыток.** Поэтому после неудачного логина `PlayerSession` больше не пробует войти, пока процесс не перезапустят.
66
- - **Протухшая сессия переустанавливается сама.** Если страница пришла гостевой, бросается `SessionExpiredError`, сессия перелогинивается и повторяет операцию один раз.
67
-
68
- Проверка без MCP (креды из `.env`): `pnpm check:login`. Скрипт логинится, повторно использует сессию и имитирует её протухание. Каждый запуск тратит 2 входа.
69
-
70
- ## Локальный режим
71
-
72
- Нужен Node.js 22+. Ставить отдельно ничего не надо: клиент сам запустит пакет через `npx`.
17
+ Нужен [Node.js](https://nodejs.org) 22 или новее. Отдельно ставить ничего не надо: клиент сам скачает и запустит сервер через `npx`.
73
18
 
74
19
  **Claude Code:**
75
20
 
76
21
  ```bash
77
- claude mcp add mcgl -e FORUM_LOGIN=ник -e FORUM_PASSWORD=пароль -- npx -y mcgl-mcp
22
+ claude mcp add mcgl -- npx -y mcgl-mcp
78
23
  ```
79
24
 
80
- **Claude Desktop, Cursor и другие** (`claude_desktop_config.json`, `.cursor/mcp.json` и т.п.):
25
+ **Claude Desktop, Cursor и другие:** добавьте в конфиг MCP-серверов (`claude_desktop_config.json`, `.cursor/mcp.json` и т.п.):
81
26
 
82
27
  ```json
83
28
  {
84
29
  "mcpServers": {
85
30
  "mcgl": {
86
31
  "command": "npx",
87
- "args": ["-y", "mcgl-mcp"],
88
- "env": {
89
- "FORUM_LOGIN": "ник",
90
- "FORUM_PASSWORD": "пароль"
91
- }
32
+ "args": ["-y", "mcgl-mcp"]
92
33
  }
93
34
  }
94
35
  }
95
36
  ```
96
37
 
97
- `FORUM_LOGIN` и `FORUM_PASSWORD` необязательны: без них доступно только чтение.
98
-
99
- Если на Windows клиент не находит `npx`, укажи `"command": "cmd"` и `"args": ["/c", "npx", "-y", "mcgl-mcp"]`.
38
+ На Windows, если клиент не находит `npx`: `"command": "cmd"`, `"args": ["/c", "npx", "-y", "mcgl-mcp"]`.
100
39
 
101
- С кредами сервер логинится сразу при старте. Результат («Authorized as …» или текст ошибки форума) виден в логах MCP-клиента.
40
+ ### Вход под своим аккаунтом (необязательно)
102
41
 
103
- ### Запуск из исходников
42
+ Сейчас все инструменты работают и без входа. Вход понадобится для действий от вашего имени, например чтобы писать посты: это в планах. Настроить его можно уже сейчас.
104
43
 
105
44
  ```bash
106
- pnpm install
107
- pnpm build
108
- ```
109
-
110
- В конфиге клиента: `"command": "node"`, `"args": ["<путь>/mcgl-mcp/dist/entry/stdio.js"]`.
111
-
112
- ## Публикация в npm
113
-
114
- Публикует GitHub Actions ([.github/workflows/publish.yml](.github/workflows/publish.yml)) по тегу:
115
-
116
- ```bash
117
- git tag v0.1.0
118
- git push origin v0.1.0
119
- ```
120
-
121
- - **Версия берётся из тега.** В `package.json` в репозитории лежит заглушка `0.0.0-dev`, менять её руками не нужно.
122
- - **Пререлизы** (`v1.0.0-beta.1`) публикуются с dist-tag `next`, поэтому `npx mcgl-mcp` продолжает ставить последнюю стабильную версию.
123
- - **Тег, который не является semver,** останавливает workflow с ошибкой.
124
- - **Пакет подписывается `--provenance`:** npm показывает, что он собран этим workflow из этого репозитория.
125
-
126
- Один раз нужно добавить секрет `NPM_TOKEN` в Settings → Secrets and variables → Actions. Токен создаётся на npmjs.com в разделе Access Tokens → Generate New Token → Granular, с правом публикации.
127
-
128
- Что попадёт в пакет: `npm pack --dry-run`. Идут только `dist/**/*.js` (без `dist/dev`), README и LICENSE.
129
-
130
- ## Хост
131
-
132
- ```bash
133
- cp .env.example .env
134
- docker compose up -d --build
45
+ claude mcp add mcgl -e FORUM_LOGIN=ник -e FORUM_PASSWORD=пароль -- npx -y mcgl-mcp
135
46
  ```
136
47
 
137
- Эндпоинт: `https://<домен>/public/mcp`, проверка здоровья: `/health`. `FORUM_LOGIN` и `FORUM_PASSWORD` HTTP-режим не читает.
138
-
139
- ## Устройство
48
+ В JSON-конфиге то же самое задаётся через `"env": { "FORUM_LOGIN": "ник", "FORUM_PASSWORD": "пароль" }`.
140
49
 
141
- ```
142
- src/
143
- entry/stdio.ts локальный режим
144
- entry/http.ts публичный режим
145
- dev/check-login.ts ручная проверка авторизации
146
- env.ts конфиг из переменных окружения
147
- forum/
148
- site.ts URL форума
149
- http.ts fetch + куки + ручные редиректы + повторы GET
150
- health.ts circuit breaker: форум лежит → не ходим 30 с
151
- cookie-jar.ts
152
- rate-limiter.ts минимальный интервал между запросами (общий на процесс)
153
- player-session.ts логин, проверка сессии, перелогин
154
- reader.ts чтение форума (гостем или через сессию игрока)
155
- types.ts формы данных, которые отдают инструменты
156
- parsers/
157
- common.ts общие хелперы, признаки гостя, пользователи, пагинация
158
- topic-list.ts главная, /forum/0, /forum/N (и разделы сообществ)
159
- topic.ts тема и отдельный пост: посты, опрос, шапка
160
- content.ts HTML поста (bbCode) → текст
161
- player.ts форма логина
162
- mcp/
163
- define-tool.ts definePublicTool / definePlayerTool
164
- tools/index.ts реестр инструментов
165
- tools/public/read.ts инструменты чтения
166
- server.ts сборка McpServer
167
- ```
50
+ Пароль хранится только в конфиге клиента на вашем компьютере и отправляется только на форум. Если войти не получилось, текст ошибки форума виден в логах MCP-сервера в клиенте. У форума есть лимит попыток входа, поэтому после неудачи сервер больше не пытается войти: исправьте пароль и перезапустите клиент.
168
51
 
169
- Добавление инструмента: объявить через `definePublicTool` или `definePlayerTool` и положить в массив в `mcp/tools/index.ts`. В player-инструментах обращения к форуму оборачиваются в `ctx.session.withAuth(...)`, а страницы получаются через `ctx.session.getAuthed(...)`.
52
+ ## Инструменты
170
53
 
171
- ## Тесты
54
+ | Инструмент | Что делает |
55
+ | --------------------- | ------------------------------------------------------------------------------------ |
56
+ | `forum_list_sections` | разделы форума по категориям |
57
+ | `forum_latest_topics` | темы со свежими сообщениями во всех разделах |
58
+ | `forum_list_topics` | темы раздела: закреплённые, закрытые, кто может отвечать |
59
+ | `forum_get_topic` | посты темы по 15 штук; по умолчанию — последние, плюс опрос и кто читает тему сейчас |
60
+ | `forum_get_post` | один пост по ссылке `/post/N` и где он находится в теме |
172
61
 
173
- ```bash
174
- pnpm test # все тесты, без сети (их же гоняет CI)
175
- pnpm test:watch
176
- pnpm test:live # smoke-тесты на настоящем форуме, креды игрока из .env необязательны
177
- pnpm fixtures:update # перекачать страницы форума в test/fixtures
178
- ```
62
+ ## Настройки
179
63
 
180
- | Папка | Что проверяется |
181
- |---|---|
182
- | `test/parsers/` | конвертер bbCode → текст на коротких HTML-фрагментах; парсеры страниц на фикстурах |
183
- | `test/forum/` | куки, редиректы, cp1251, лимитер; логин, перелогин, блокировка после неверного пароля; названия предметов (кеш, лимит запросов) |
184
- | `test/mcp/` | MCP целиком: клиент SDK ↔ сервер (`InMemoryTransport`) ↔ фейковый форум |
185
- | `test/live/` | только `pnpm test:live`: живая разметка всё ещё парсится |
64
+ Переменные окружения задаются в конфиге клиента, все необязательные.
186
65
 
187
- Тесты сети работают против фейкового форума: это локальный http-сервер внутри теста (`test/helpers/fake-forum.ts`). Он отдаёт страницы из `test/fixtures/`, поэтому проверяется настоящий `ForumHttp`, без моков `fetch`.
66
+ | Переменная | По умолчанию | Что это |
67
+ | ------------------------------- | ----------------------------------- | ------------------------------------------ |
68
+ | `FORUM_LOGIN`, `FORUM_PASSWORD` | — | вход под своим аккаунтом |
69
+ | `LOG_LEVEL` | `info` | `debug` — подробные логи для отладки |
70
+ | `FORUM_MIN_INTERVAL_MS` | `300` | минимальная пауза между запросами к форуму |
71
+ | `FORUM_TIMEOUT_MS` | `15000` | таймаут одного запроса к форуму |
72
+ | `FORUM_BASE_URL` | `https://forum.minecraft-galaxy.ru` | адрес форума |
188
73
 
189
- Фикстуры — замороженные копии страниц форума, они лежат в git. Если форум поменял вёрстку, запусти `pnpm fixtures:update` и посмотри, какие тесты упали.
74
+ ## Документация
190
75
 
191
- CI ([.github/workflows/ci.yml](.github/workflows/ci.yml)) на push в `main` и на PR прогоняет typecheck, тесты, сборку и проверку состава пакета. Перед публикацией `prepublishOnly` прогоняет то же самое.
76
+ - [Как сервер работает с форумом](docs/forum.md): вход, пагинация тем, bbCode, предметы, недоступность форума.
77
+ - [Разработка](docs/development.md): запуск из исходников, тесты, релизы, свой HTTP-сервер.
192
78
 
193
- `tsconfig.json` проверяет типы в `src`, тестах и скриптах, а `tsconfig.build.json` собирает только `src` в `dist`.
79
+ Нашли ошибку или форум что-то поменял — [заведите issue](https://github.com/exi66/mcgl-mcp/issues/new/choose).
194
80
 
195
81
  ## Лицензия
196
82
 
@@ -119,7 +119,9 @@ export class ForumHttp {
119
119
  }
120
120
  catch (error) {
121
121
  const timeout = error instanceof DOMException && error.name === "TimeoutError";
122
- throw new RetryableError(timeout ? `Forum timed out: ${url}` : `Request to forum failed: ${url}`, { cause: error });
122
+ throw new RetryableError(timeout ? `Forum timed out: ${url}` : `Request to forum failed: ${url}`, {
123
+ cause: error,
124
+ });
123
125
  }
124
126
  }
125
127
  }
@@ -24,7 +24,10 @@ export function postHtmlToText($, el) {
24
24
  node.find(".bc_quote").each((_, block) => {
25
25
  const b = $(block);
26
26
  const title = b.children(".bc_quote_title").first();
27
- const who = title.text().replace(/\s*писал\(а\):\s*$/, "").trim();
27
+ const who = title
28
+ .text()
29
+ .replace(/\s*писал\(а\):\s*$/, "")
30
+ .trim();
28
31
  title.remove();
29
32
  b.prepend(`\n[quote${who ? `: ${who}` : ""}]\n`).append("\n[/quote]\n");
30
33
  b.replaceWith(b.contents());
@@ -9,7 +9,8 @@ const FLAGS_NOTE = " Boolean flags appear only when true.";
9
9
  const page = z.number().int().positive().default(1).describe("1-based page number");
10
10
  const TOPIC_ROW_FIELDS = "Topic fields: pinned; closed (title struck through on the forum, no replies allowed); " +
11
11
  "whoCanReply: all | vip | premium_platinum | moderators (only visible in topic lists, not on the topic page); " +
12
- "pages (topic page count); created/last: author and time." + FLAGS_NOTE;
12
+ "pages (topic page count); created/last: author and time." +
13
+ FLAGS_NOTE;
13
14
  export const listSections = definePublicTool({
14
15
  name: "forum_list_sections",
15
16
  title: "Forum sections",
@@ -22,8 +23,7 @@ export const listSections = definePublicTool({
22
23
  export const latestTopics = definePublicTool({
23
24
  name: "forum_latest_topics",
24
25
  title: "Latest topics",
25
- description: "Recently active topics across all sections, newest first; each row includes its section name. " +
26
- TOPIC_ROW_FIELDS,
26
+ description: "Recently active topics across all sections, newest first; each row includes its section name. " + TOPIC_ROW_FIELDS,
27
27
  inputSchema: { page },
28
28
  annotations: readOnly,
29
29
  handler: async ({ page }, { reader }) => jsonResult(await reader.latestTopics(page)),
@@ -50,10 +50,16 @@ export const getTopic = definePublicTool({
50
50
  "page 1 also include poll (if any), notices (e.g. 'moved from section') and current readers. " +
51
51
  "author.accountDeleted: account removed or banned. " +
52
52
  "Post text markup: [quote: nick]..[/quote], [spoiler: title]..[/spoiler], [image: url], [video: url], " +
53
- "[audio: url], [Item name #id] or [item #id] (#id:meta for variants), [craft] grid of item ids with a legend." + FLAGS_NOTE,
53
+ "[audio: url], [Item name #id] or [item #id] (#id:meta for variants), [craft] grid of item ids with a legend." +
54
+ FLAGS_NOTE,
54
55
  inputSchema: {
55
56
  topicId: z.number().int().positive(),
56
- page: z.number().int().positive().optional().describe("1-based page from the topic start; omit for the newest posts"),
57
+ page: z
58
+ .number()
59
+ .int()
60
+ .positive()
61
+ .optional()
62
+ .describe("1-based page from the topic start; omit for the newest posts"),
57
63
  pageSize: z.number().int().min(1).max(50).default(DEFAULT_PAGE_SIZE).describe("Posts per page"),
58
64
  },
59
65
  annotations: readOnly,
package/package.json CHANGED
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "name": "mcgl-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "MCP server for the Minecraft Galaxy (MCGL) game forum: read sections, topics and posts.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "model-context-protocol",
8
- "minecraft-galaxy",
9
- "mcgl",
10
8
  "forum",
11
9
  "claude"
12
10
  ],
@@ -48,7 +46,9 @@
48
46
  "test": "vitest run",
49
47
  "test:watch": "vitest",
50
48
  "test:live": "vitest run --config vitest.live.config.ts",
51
- "fixtures:update": "tsx --env-file-if-exists=.env scripts/update-fixtures.ts"
49
+ "fixtures:update": "tsx --env-file-if-exists=.env scripts/update-fixtures.ts",
50
+ "format": "prettier --write .",
51
+ "format:check": "prettier --check ."
52
52
  },
53
53
  "dependencies": {
54
54
  "@modelcontextprotocol/sdk": "^1.32.2",
@@ -60,6 +60,7 @@
60
60
  "@modelcontextprotocol/inspector": "^2.15.0",
61
61
  "@types/express": "^5.0.6",
62
62
  "@types/node": "^26.6.6",
63
+ "prettier": "^3.10.1",
63
64
  "tsx": "^4.23.15",
64
65
  "typescript": "^7.0.2",
65
66
  "vitest": "^5.1.0"