Files
agent-news/mcp_server/bin/agent_news_mcp.dart
matt2dev 7bfe0dfe0c Fix HOME volume mount, genericize topic, add progress statuses, extract lexicon
- Volume was mounted at ~/.claude only, but claude-code writes auth to
  ~/.claude.json too — that file lived on the ephemeral container layer and
  was lost on every recreate. Now the whole $HOME is on the named volume,
  and claude-config moved outside HOME (/app) so it isn't shadowed by it.
- Default /news topic was hardcoded to AI; now it's a configurable
  DEFAULT_TOPIC (plain "главные новости дня"), topic is fully free-form.
- Prompt now instructs Claude to send interim " ..." progress updates via
  send_to_telegram while researching, not just the final digest — a bare
  request like "call this tool" was already taking 3+ minutes end-to-end,
  so silence read as broken.
- All user-facing/prompt text moved into bot/lib/lexicon.dart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 21:53:38 +03:00

84 lines
2.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'dart:io';
import 'package:mcp_dart/mcp_dart.dart';
final _token = Platform.environment['TELEGRAM_BOT_TOKEN'] ?? '';
final _http = HttpClient();
CallToolResult _ok(String text) => CallToolResult(content: [TextContent(text: text)]);
CallToolResult _err(String text) =>
CallToolResult(content: [TextContent(text: 'Ошибка: $text')], isError: true);
List<String> _splitMessage(String text) {
const maxLen = 4000;
if (text.length <= maxLen) return [text];
final chunks = <String>[];
var start = 0;
while (start < text.length) {
final end = (start + maxLen).clamp(0, text.length);
chunks.add(text.substring(start, end));
start = end;
}
return chunks;
}
Future<void> _sendMessage(int chatId, String text) async {
for (final chunk in _splitMessage(text)) {
final req = await _http.postUrl(
Uri.parse('https://api.telegram.org/bot$_token/sendMessage'),
);
req.headers.contentType = ContentType.json;
req.write(jsonEncode({
'chat_id': chatId,
'text': chunk,
'parse_mode': 'Markdown',
}));
final res = await req.close();
final body = await res.transform(utf8.decoder).join();
if (res.statusCode >= 400) {
throw Exception('Telegram sendMessage → ${res.statusCode}: $body');
}
}
}
void main() {
final server = McpServer(
const Implementation(name: 'agent-news', version: '0.1.0'),
options: const McpServerOptions(
capabilities: ServerCapabilities(tools: ServerCapabilitiesTools()),
),
);
server.registerTool(
'send_to_telegram',
description:
'Отправляет текст (Markdown) в указанный Telegram-чат. Единственный '
'способ доставить результат пользователю. Можно и нужно вызывать '
'несколько раз за сессию: сначала короткими статусами по ходу работы '
'("⏳ ищу источники…"), в конце — с финальным оформленным текстом.',
inputSchema: JsonSchema.object(
properties: {
'chat_id': JsonSchema.integer(description: 'ID чата Telegram, куда отправить сообщение'),
'text': JsonSchema.string(description: 'Готовый текст в Markdown-разметке Telegram'),
},
required: ['chat_id', 'text'],
),
callback: (args, _) async {
if (_token.isEmpty) {
return _err('TELEGRAM_BOT_TOKEN не задан в окружении MCP-сервера');
}
try {
final chatId = args['chat_id'] as int;
final text = args['text'].toString();
await _sendMessage(chatId, text);
return _ok('Отправлено в чат $chatId (${text.length} символов).');
} catch (e) {
return _err('$e');
}
},
);
server.connect(StdioServerTransport());
}