Browse Source

MCP: 17 ArtCraft apps, lazy client start, autodiscover

master
Serge Shutov 1 week ago
parent
commit
fe9c4ebcb2
  1. 51
      README.md
  2. 64
      bridge/mcp-registry.js
  3. 97
      bridge/mcp-tools.js

51
README.md

@ -465,13 +465,58 @@ net use, net user, net localgroup, net share, net stop, net start, net session, @@ -465,13 +465,58 @@ net use, net user, net localgroup, net share, net stop, net start, net session,
## MCP: ArtCraft
Расширение умеет управлять приложениями ArtCraft (VectorCraft, PhotoCraft, FigCraft, DesignCraft, DeckCraft) через протокол MCP. Это не генератор картинок «из головы» — это руки в реальном редакторе: примитивы, пути с кривыми Безье, текст, заливки, обводки, эффекты, pathfinder, экспорт в SVG/PDF/DXF/PNG.
Расширение умеет управлять приложениями ArtCraft через протокол MCP. Это не генератор картинок «из головы» — это руки в реальном редакторе: примитивы, пути с кривыми Безье, текст, заливки, обводки, эффекты, pathfinder, экспорт в SVG/PDF/DXF/PNG.
Поддерживаются все приложения ArtCraft, у которых есть CLI с подкомандой `mcp`:
| Инструмент | Приложение | Профиль |
|---|---|---|
| `vectorcraft_mcp_*` | VectorCraft | векторная графика |
| `photocraft_mcp_*` | PhotoCraft | растровая графика |
| `figcraft_mcp_*` | FigCraft | интерфейсы, макеты |
| `designcraft_mcp_*` | DesignCraft | дизайн |
| `deckcraft_mcp_*` | DeckCraft | презентации |
| `cadcraft_mcp_*` | CADCraft | CAD |
| `craftanything_mcp_*` | Craft Anything | универсальные инструменты |
| `effectcraft_mcp_*` | EffectCraft | эффекты, видео-композиции |
| `filmcraft_mcp_*` | FilmCraft | видео |
| `gridcraft_mcp_*` | GridCraft | сетки |
| `lightcraft_mcp_*` | LightCraft | свет |
| `materialcraft_mcp_*` | MaterialCraft | материалы |
| `pdfcraft_mcp_*` | PdfCraft | PDF (189 инструментов) |
| `solidcraft_mcp_*` | SolidCraft | 3D, CAD-ядро |
| `soundcraft_mcp_*` | SoundCraft | звук |
| `texturecraft_mcp_*` | TextureCraft | текстуры |
| `wordcraft_mcp_*` | WordCraft | текст, документы |
Список не жёсткий: любая новая папка в `V:\artcraft` с единственным `*-cli.exe` подхватывается автоматически (имя инструмента — имя папки в нижнем регистре без не-букв).
### Как это работает
Мост при старте поднимает по одному MCP-клиенту на каждое найденное приложение. Клиент — дочерний процесс `<app>-cli.exe mcp`, общение по stdio строками JSON-RPC 2.0. Никакого mcp-sidecar и сетевых портов между мостом и CLI нет.
Клиент — дочерний процесс `<app>-cli.exe mcp`, общение по stdio строками JSON-RPC 2.0. Никакого mcp-sidecar и сетевых портов между мостом и CLI нет.
**Ленивый старт.** Инструменты всех найденных приложений видны в `/tools` сразу, но CLI-процесс поднимается при первом вызове этого приложения. Это важно при 17 приложениях: eager-старт дал бы ~2.3 ГБ RAM и полминуты ожидания. Параллельные вызовы одного приложения не дублируют старт — второй ждёт первый. Простаивающий клиент закрывается через 5 минут.
Если нужно поднять приложения сразу при старте моста — перечислить их в `config.json`:
```json
"mcpServers": {
"artcraft": {
"root": "V:/artcraft",
"eager": ["vectorcraft", "figcraft"],
"disabled": ["soundcraft"]
}
}
```
`eager` — стартовать сразу, `disabled` — не регистрировать вовсе, `root` — переопределить путь (по умолчанию `V:\artcraft` или `ARTCRAFT_ROOT`).
Discovery двухуровневый:
1. **Явный список** в `mcp-registry.js` — 17 известных приложений с точными именами бинарников.
2. **Автообнаружение** — любая папка в корне с единственным `*-cli.exe`, которой нет в явном списке. Имя инструмента = имя папки в нижнем регистре без не-букв (`Craft Anything` → `craftanything`). В логе такие помечаются звёздочкой.
Discovery: `mcp-registry.js` ищет бинарники в `V:\artcraft\<App>\<app>-cli.exe` (путь настраивается через `config.mcpServers.artcraft.root` или переменную `ARTCRAFT_ROOT`). Найденные приложения регистрируются, ненайденные тихо пропускаются с причиной в логе.
Ненайденные приложения тихо пропускаются с причиной в логе (`not installed: ...`). В `V:\artcraft\SolidCraft` лежат два exe (`solidcraft-cli.exe` и `solvecraft-cli.exe`) — берётся первый, второй игнорируется.
### Два режима работы CLI

64
bridge/mcp-registry.js

@ -3,12 +3,26 @@ @@ -3,12 +3,26 @@
const fs = require('fs');
const path = require('path');
// Явный список: имя инструмента -> папка, бинарь, аргументы.
// Имя обязано быть [a-z]+ — на него завязан regex в tools.js (^[a-z]+_mcp_(tools|call)$).
const DEFAULT_APPS = {
vectorcraft: { dir: 'VectorCraft', bin: 'vectorcraft-cli.exe', args: ['mcp'] },
photocraft: { dir: 'PhotoCraft', bin: 'photocraft-cli.exe', args: ['mcp'] },
figcraft: { dir: 'FigCraft', bin: 'figcraft-cli.exe', args: ['mcp'] },
designcraft: { dir: 'DesignCraft', bin: 'designcraft-cli.exe', args: ['mcp'] },
deckcraft: { dir: 'DeckCraft', bin: 'deckcraft-cli.exe', args: ['mcp'] },
vectorcraft: { dir: 'VectorCraft', bin: 'vectorcraft-cli.exe', args: ['mcp'] },
photocraft: { dir: 'PhotoCraft', bin: 'photocraft-cli.exe', args: ['mcp'] },
figcraft: { dir: 'FigCraft', bin: 'figcraft-cli.exe', args: ['mcp'] },
designcraft: { dir: 'DesignCraft', bin: 'designcraft-cli.exe', args: ['mcp'] },
deckcraft: { dir: 'DeckCraft', bin: 'deckcraft-cli.exe', args: ['mcp'] },
cadcraft: { dir: 'CADCraft', bin: 'cadcraft-cli.exe', args: ['mcp'] },
craftanything: { dir: 'Craft Anything', bin: 'craft-anything-cli.exe', args: ['mcp'] },
effectcraft: { dir: 'EffectCraft', bin: 'effectcraft-cli.exe', args: ['mcp'] },
filmcraft: { dir: 'FilmCraft', bin: 'filmcraft-cli.exe', args: ['mcp'] },
gridcraft: { dir: 'GridCraft', bin: 'gridcraft-cli.exe', args: ['mcp'] },
lightcraft: { dir: 'LightCraft', bin: 'lightcraft-cli.exe', args: ['mcp'] },
materialcraft: { dir: 'MaterialCraft', bin: 'materialcraft-cli.exe', args: ['mcp'] },
pdfcraft: { dir: 'PdfCraft', bin: 'pdfcraft-cli.exe', args: ['mcp'] },
solidcraft: { dir: 'SolidCraft', bin: 'solidcraft-cli.exe', args: ['mcp'] },
soundcraft: { dir: 'SoundCraft', bin: 'soundcraft-cli.exe', args: ['mcp'] },
texturecraft: { dir: 'TextureCraft', bin: 'texturecraft-cli.exe', args: ['mcp'] },
wordcraft: { dir: 'WordCraft', bin: 'wordcraft-cli.exe', args: ['mcp'] },
};
function resolveRoot(config) {
@ -16,20 +30,54 @@ function resolveRoot(config) { @@ -16,20 +30,54 @@ function resolveRoot(config) {
return (c && c.root) || process.env.ARTCRAFT_ROOT || 'V:\\artcraft';
}
function resolveDisabled(config) {
const c = config && config.mcpServers && config.mcpServers.artcraft;
const list = (c && c.disabled) || [];
return Array.isArray(list) ? list.map(String) : [];
}
function discover(root, spec) {
const full = path.join(root, spec.dir, spec.bin);
try { return fs.existsSync(full) ? full : null; } catch { return null; }
}
function toolNameFromDir(dirName) {
return String(dirName).toLowerCase().replace(/[^a-z]/g, '');
}
// Автообнаружение: любая папка в root с единственным *-cli.exe, которой нет в списке.
// Нужно, чтобы новая установка ArtCraft подхватывалась без правки кода.
function autodiscover(root, known) {
const found = {};
let entries = [];
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return found; }
for (const it of entries) {
if (!it.isDirectory()) continue;
const dir = path.join(root, it.name);
let exes = [];
try { exes = fs.readdirSync(dir).filter(n => /-cli\.exe$/i.test(n)); } catch { continue; }
if (exes.length !== 1) continue;
const bin = path.join(dir, exes[0]);
if (Object.values(known).some(s => path.join(root, s.dir, s.bin).toLowerCase() === bin.toLowerCase())) continue;
const name = toolNameFromDir(it.name);
if (!name || known[name] || found[name]) continue;
found[name] = { dir: it.name, bin: exes[0], args: ['mcp'], auto: true };
}
return found;
}
function buildSpecs(config) {
const root = resolveRoot(config);
const disabled = resolveDisabled(config);
const known = { ...DEFAULT_APPS, ...autodiscover(root, DEFAULT_APPS) };
const out = {};
for (const [name, spec] of Object.entries(DEFAULT_APPS)) {
for (const [name, spec] of Object.entries(known)) {
if (disabled.includes(name)) { out[name] = { name, enabled: false, reason: 'disabled in config' }; continue; }
const bin = discover(root, spec);
if (!bin) { out[name] = { name, enabled: false, reason: 'binary not found: ' + path.join(root, spec.dir, spec.bin) }; continue; }
out[name] = { name, enabled: true, bin, args: spec.args, cwd: path.dirname(bin) };
out[name] = { name, enabled: true, bin, args: spec.args, cwd: path.dirname(bin), auto: !!spec.auto };
}
return { root, apps: out };
}
module.exports = { DEFAULT_APPS, resolveRoot, buildSpecs };
module.exports = { DEFAULT_APPS, resolveRoot, resolveDisabled, buildSpecs, toolNameFromDir };

97
bridge/mcp-tools.js

@ -1,52 +1,110 @@ @@ -1,52 +1,110 @@
'use strict';
// MCP-менеджер: держит McpClient на каждое приложение ArtCraft, отдаёт TOOL_DEFS и проксирует вызовы.
// Ленивый старт: клиент поднимается при первом вызове приложения, не на старте моста.
const { McpClient } = require('./mcp-client');
const { buildSpecs } = require('./mcp-registry');
const IDLE_MS = 300000;
const REQUEST_TIMEOUT_MS = 60000;
class McpManager {
constructor(config) {
this.config = config;
const { root, apps } = buildSpecs(config);
this.root = root;
this.apps = apps;
this.root = null;
this.apps = {};
this.enabled = [];
this.clients = {};
this.starting = {};
this.lastError = {};
this.ready = false;
}
async init() {
const enabled = Object.values(this.apps).filter(a => a.enabled);
if (enabled.length === 0) {
const { root, apps } = buildSpecs(this.config);
this.root = root;
this.apps = apps;
this.enabled = Object.values(apps).filter(a => a.enabled);
if (this.enabled.length === 0) {
console.log('[mcp] no ArtCraft apps found under ' + this.root);
this.ready = true;
return;
}
for (const spec of enabled) {
const c = new McpClient(spec.name, {
const names = this.enabled.map(a => a.name + (a.auto ? '*' : ''));
console.log('[mcp] discovered ' + this.enabled.length + ' app(s) under ' + this.root + ': ' + names.join(', '));
const missing = Object.values(apps).filter(a => !a.enabled && a.reason !== 'disabled in config').map(a => a.name);
if (missing.length) console.log('[mcp] not installed: ' + missing.join(', '));
const eager = this._eagerList();
if (eager.length) {
for (const name of eager) {
try { await this.ensure(name); }
catch (e) { console.warn('[mcp] eager ' + name + ' failed: ' + e.message); }
}
}
this.ready = true;
}
_eagerList() {
const c = this.config && this.config.mcpServers && this.config.mcpServers.artcraft;
const list = (c && c.eager) || [];
if (!Array.isArray(list)) return [];
return list.map(String).filter(n => this.apps[n] && this.apps[n].enabled);
}
_spec(name) {
const spec = this.apps[name];
if (!spec) throw new Error('MCP app unknown: ' + name);
if (!spec.enabled) throw new Error('MCP app not available: ' + name + (spec.reason ? ' (' + spec.reason + ')' : ''));
return spec;
}
_client(spec) {
let c = this.clients[spec.name];
if (!c) {
c = new McpClient(spec.name, {
bin: spec.bin,
args: spec.args,
cwd: spec.cwd,
idleMs: 300000,
requestTimeoutMs: 60000,
idleMs: IDLE_MS,
requestTimeoutMs: REQUEST_TIMEOUT_MS,
});
this.clients[spec.name] = c;
}
return c;
}
// Поднять клиент и загрузить список инструментов. Параллельные вызовы не дублируют старт.
async ensure(name) {
const spec = this._spec(name);
const c = this._client(spec);
if (c.running && c.tools) return c;
if (this.starting[name]) return this.starting[name];
const p = (async () => {
try {
await c.start();
await c.listTools();
this.clients[spec.name] = c;
console.log('[mcp] ' + spec.name + ' ready, tools: ' + (c.tools ? c.tools.length : 0));
const tools = await c.listTools();
this.lastError[name] = null;
console.log('[mcp] ' + name + ' ready, tools: ' + (tools ? tools.length : 0));
return c;
} catch (e) {
console.warn('[mcp] ' + spec.name + ' failed: ' + e.message);
this.lastError[name] = e.message;
console.warn('[mcp] ' + name + ' failed: ' + e.message);
try { c.close(); } catch {}
throw e;
} finally {
delete this.starting[name];
}
}
this.ready = true;
})();
this.starting[name] = p;
return p;
}
toolDefs() {
const defs = [];
for (const name of Object.keys(this.clients)) {
for (const spec of this.enabled) {
const name = spec.name;
defs.push({
name: name + '_mcp_tools',
description: 'List MCP tools of ArtCraft ' + name + ' (33+ tools, filterable)',
description: 'List MCP tools of ArtCraft ' + name + ' (filterable, brief)',
params: { filter: 'string?', brief: 'bool?' },
});
defs.push({
@ -63,9 +121,7 @@ class McpManager { @@ -63,9 +121,7 @@ class McpManager {
if (!m) return null;
const app = m[1];
const op = m[2];
const c = this.clients[app];
if (!c) throw new Error('MCP app not ready: ' + app + (this.apps[app] && this.apps[app].reason ? ' (' + this.apps[app].reason + ')' : ''));
await c.start();
const c = await this.ensure(app);
if (op === 'tools') {
let tools = (await c.listTools()).slice();
if (args && args.filter) {
@ -85,6 +141,7 @@ class McpManager { @@ -85,6 +141,7 @@ class McpManager {
closeAll() {
for (const c of Object.values(this.clients)) { try { c.close(); } catch {} }
this.clients = {};
this.starting = {};
}
}

Loading…
Cancel
Save