NNTP MCP-сервер: мониторинг новостных групп и ответы в тредах.

Сценарий: скрипт запускается периодически, scan_group находит новые
сообщения с момента прошлого запуска, возвращает треды целиком (с контекстом),
LLM принимает решение, post_reply отправляет ответ.

Состояние хранится в nntp_mcp_state.json рядом со скриптом.
This commit is contained in:
ysv committed 2026-10-26 17:12:23 +03:00
1 parent 8b4659cffb
commit 367f024cbd
4 files changed
+486

No files matched your search

+66
View File
@@ -427,6 +427,72 @@ def get_latest_messages(group_name: str, count: int = 10) -> list[dict]:
except Exception as e:
return [{"error": f"Ошибка NNTP при получении последних сообщений: {e}"}]
@mcp.tool()
def get_thread_root(message_id: str) -> dict:
"""
Находит Message-ID первого (корневого) сообщения в ветке обсуждения.
:param message_id: Message-ID любого сообщения из ветки (например, '<abc@example.com>')
:return: Словарь с полями root_message_id, subject, author или error
"""
try:
with nntplib.NNTP(NNTP_SERVER, port=PORT, timeout=10) as s:
# Получаем исходное сообщение
resp, info = s.article(message_id)
raw_bytes = b"\n".join(info.lines)
msg = email.message_from_bytes(raw_bytes, policy=default)
# Проверяем заголовок References - там все Message-ID в цепочке
references = msg.get("References", "").strip()
if references:
# References содержит список ID через пробел, первый - корень ветки
ref_ids = re.findall(r'<[^>]+>', references)
if ref_ids:
root_id = ref_ids[0]
# Получаем информацию о корневом сообщении
try:
resp_root, info_root = s.article(root_id)
raw_root = b"\n".join(info_root.lines)
root_msg = email.message_from_bytes(raw_root, policy=default)
return {
"root_message_id": root_id,
"subject": decode_mime_header(root_msg.get("Subject", "[Без темы]")),
"author": decode_mime_header(root_msg.get("From", "[Неизвестно]")),
"date": root_msg.get("Date", "")
}
except Exception:
# Если корневое сообщение недоступно, возвращаем хотя бы его ID
return {
"root_message_id": root_id,
"subject": "[Сообщение недоступно]",
"author": "[Неизвестно]",
"date": ""
}
# Если References нет, проверяем In-Reply-To и поднимаемся рекурсивно
in_reply_to = msg.get("In-Reply-To", "").strip()
if in_reply_to:
# Извлекаем Message-ID из In-Reply-To
match = re.search(r'<[^>]+>', in_reply_to)
if match:
parent_id = match.group(0)
# Рекурсивно ищем корень через родительское сообщение
return get_thread_root(parent_id)
# Если ни References, ни In-Reply-To нет - это и есть корень
return {
"root_message_id": message_id,
"subject": decode_mime_header(msg.get("Subject", "[Без темы]")),
"author": decode_mime_header(msg.get("From", "[Неизвестно]")),
"date": msg.get("Date", "")
}
except Exception as e:
return {"error": f"Не удалось найти корень ветки для {message_id}: {e}"}
if __name__ == "__main__":