Статья #2

MCP-сервер для работы с базами данных 1С

Введение

Model Context Protocol (MCP) позволяет создавать специализированные серверы, которые расширяют возможности Claude Code доступом к внешним источникам данных. Для 1С-разработчика это означает прямой доступ к базам данных 1С прямо из AI-ассистента.

Зачем это нужно

Типичные сценарии:
- Быстрый анализ данных в базе без открытия 1С
- Генерация отчетов на основе реальных данных
- Поиск и анализ проблемных записей
- Выполнение диагностических запросов
- Создание тестовых данных

Реализация MCP-сервера для 1С

Архитектура решения

Claude Code <--MCP--> MCP Server <--COM/REST--> 1C:Enterprise

Вариант 1: Через COM-соединение (Windows)

// mcp-1c-server.js
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

// Для Windows - используем COM через node-activex
const ActiveXObject = require('activex');

class OneCServer {
  constructor() {
    this.server = new Server({
      name: '1c-database-server',
      version: '1.0.0',
    }, {
      capabilities: {
        tools: {},
      },
    });

    this.connection = null;
    this.setupHandlers();
  }

  // Подключение к базе 1С
  connect(connectionString) {
    try {
      const V83 = new ActiveXObject('V83.COMConnector');
      this.connection = V83.Connect(connectionString);
      return true;
    } catch (error) {
      throw new Error(`Ошибка подключения к 1С: ${error.message}`);
    }
  }

  setupHandlers() {
    // Список доступных инструментов
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'execute_query',
          description: 'Выполнить запрос к базе данных 1С',
          inputSchema: {
            type: 'object',
            properties: {
              query: {
                type: 'string',
                description: 'Текст запроса на языке запросов 1С',
              },
              connection_string: {
                type: 'string',
                description: 'Строка подключения к базе',
              },
            },
            required: ['query', 'connection_string'],
          },
        },
        {
          name: 'get_metadata',
          description: 'Получить метаданные конфигурации',
          inputSchema: {
            type: 'object',
            properties: {
              object_type: {
                type: 'string',
                description: 'Тип объекта: Справочник, Документ, РегистрСведений и т.д.',
              },
              connection_string: {
                type: 'string',
                description: 'Строка подключения к базе',
              },
            },
            required: ['connection_string'],
          },
        },
        {
          name: 'execute_code',
          description: 'Выполнить произвольный код на встроенном языке 1С',
          inputSchema: {
            type: 'object',
            properties: {
              code: {
                type: 'string',
                description: 'Код на встроенном языке 1С',
              },
              connection_string: {
                type: 'string',
                description: 'Строка подключения к базе',
              },
            },
            required: ['code', 'connection_string'],
          },
        },
      ],
    }));

    // Обработка вызовов инструментов
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      // Подключаемся к базе
      if (!this.connection) {
        this.connect(args.connection_string);
      }

      switch (name) {
        case 'execute_query':
          return this.executeQuery(args.query);

        case 'get_metadata':
          return this.getMetadata(args.object_type);

        case 'execute_code':
          return this.executeCode(args.code);

        default:
          throw new Error(`Неизвестный инструмент: ${name}`);
      }
    });
  }

  executeQuery(queryText) {
    try {
      const query = this.connection.NewObject('Query');
      query.Text = queryText;
      const result = query.Execute();
      const selection = result.Select();

      const rows = [];
      while (selection.Next()) {
        const row = {};
        for (let i = 0; i < result.Columns.Count(); i++) {
          const column = result.Columns.Get(i);
          row[column.Name] = selection[column.Name];
        }
        rows.push(row);
      }

      return {
        content: [{
          type: 'text',
          text: JSON.stringify(rows, null, 2),
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: 'text',
          text: `Ошибка выполнения запроса: ${error.message}`,
        }],
        isError: true,
      };
    }
  }

  getMetadata(objectType) {
    try {
      const metadata = this.connection.Metadata;
      let metaObjects;

      if (objectType) {
        metaObjects = metadata[objectType];
      } else {
        // Возвращаем общую структуру метаданных
        metaObjects = {
          Справочники: Array.from(metadata.Справочники).map(s => s.Имя),
          Документы: Array.from(metadata.Документы).map(d => d.Имя),
          РегистрыСведений: Array.from(metadata.РегистрыСведений).map(r => r.Имя),
        };
      }

      return {
        content: [{
          type: 'text',
          text: JSON.stringify(metaObjects, null, 2),
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: 'text',
          text: `Ошибка получения метаданных: ${error.message}`,
        }],
        isError: true,
      };
    }
  }

  executeCode(code) {
    try {
      const result = this.connection.Execute(code);
      return {
        content: [{
          type: 'text',
          text: String(result),
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: 'text',
          text: `Ошибка выполнения кода: ${error.message}`,
        }],
        isError: true,
      };
    }
  }

  async run() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
  }
}

const server = new OneCServer();
server.run().catch(console.error);

Вариант 2: Через REST API (кросс-платформенный)

// mcp-1c-rest-server.js
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import axios from 'axios';

class OneC_REST_Server {
  constructor(baseUrl, auth) {
    this.baseUrl = baseUrl; // http://localhost/base/odata/standard.odata
    this.auth = auth; // { username: 'admin', password: 'pass' }
    this.server = new Server({
      name: '1c-rest-server',
      version: '1.0.0',
    }, {
      capabilities: { tools: {} },
    });

    this.setupHandlers();
  }

  setupHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'odata_query',
          description: 'Выполнить OData запрос к базе 1С',
          inputSchema: {
            type: 'object',
            properties: {
              entity: { type: 'string', description: 'Имя сущности (например, Catalog_Номенклатура)' },
              filter: { type: 'string', description: 'Фильтр OData' },
              select: { type: 'string', description: 'Поля для выборки' },
              top: { type: 'number', description: 'Количество записей' },
            },
            required: ['entity'],
          },
        },
      ],
    }));

    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      if (name === 'odata_query') {
        return this.executeODataQuery(args);
      }
    });
  }

  async executeODataQuery({ entity, filter, select, top }) {
    try {
      const params = new URLSearchParams();
      if (filter) params.append('$filter', filter);
      if (select) params.append('$select', select);
      if (top) params.append('$top', top.toString());

      const url = `${this.baseUrl}/${entity}?${params.toString()}`;
      const response = await axios.get(url, {
        auth: this.auth,
        headers: { 'Accept': 'application/json' },
      });

      return {
        content: [{
          type: 'text',
          text: JSON.stringify(response.data.value, null, 2),
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: 'text',
          text: `Ошибка OData запроса: ${error.message}`,
        }],
        isError: true,
      };
    }
  }
}

Конфигурация для Claude Code

Добавьте в файл конфигурации MCP (обычно ~/Library/Application Support/Claude/claude_desktop_config.json на macOS):

{
  "mcpServers": {
    "1c-database": {
      "command": "node",
      "args": ["/path/to/mcp-1c-server.js"],
      "env": {
        "CONNECTION_STRING": "File=\"C:\\Bases\\MyBase\";Usr=\"Admin\";Pwd=\"\";"
      }
    }
  }
}

Примеры использования

Анализ данных

Команда в Claude Code:
"Используя MCP-сервер 1С, выполни запрос:
ВЫБРАТЬ
  Номенклатура.Наименование,
  СУММА(ДвиженияНоменклатуры.Количество) КАК Количество
ИЗ
  РегистрНакопления.ДвиженияНоменклатуры КАК ДвиженияНоменклатуры
СГРУППИРОВАТЬ ПО
  Номенклатура
УПОРЯДОЧИТЬ ПО
  Количество УБЫВ
ПЕРВЫЕ 10"

Генерация отчета

"На основе данных из базы 1С создай Excel отчет с топ-10 товаров по продажам за последний месяц"

Диагностика проблем

"Найди в базе 1С все документы реализации, у которых сумма документа не совпадает с суммой табличной части"

Безопасность

Важно:
- Храните строки подключения в переменных окружения
- Используйте read-only подключения для аналитики
- Ограничьте права пользователя базы данных
- Логируйте все запросы к базе
- Не используйте в production без дополнительной защиты

Расширенные возможности

Кеширование метаданных

class CachedMetadataServer extends OneCServer {
  constructor() {
    super();
    this.metadataCache = new Map();
  }

  async getMetadata(objectType) {
    const cacheKey = objectType || 'all';

    if (this.metadataCache.has(cacheKey)) {
      return this.metadataCache.get(cacheKey);
    }

    const result = await super.getMetadata(objectType);
    this.metadataCache.set(cacheKey, result);

    return result;
  }
}

Интеграция с логированием

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: '1c-mcp-server.log' }),
  ],
});

// Логируем каждый запрос
executeQuery(queryText) {
  logger.info('Executing query', { query: queryText });
  // ... выполнение запроса
}

Следующие шаги

  • Добавьте поддержку HTTP-соединений
  • Реализуйте пул соединений для нескольких баз
  • Создайте веб-интерфейс для управления сервером
  • Интегрируйте с системой мониторинга

Интересно узнать больше?

Посмотрите другие статьи о том, как AI ускоряет разработку 1С

Перейти к блогу