@fractalizer/mcp-core
v5.1.0
Published
Core framework for building MCP tools: base classes, utilities, registry
Maintainers
Readme
@fractalizer/mcp-core
Core framework for building MCP tools: base classes, utilities, registry
🎯 Purpose
Principle: Core provides reusable base classes and utilities for building MCP tools
Generic Design: BaseTool<TFacade> is facade-agnostic — works with any API facade
Architecture rule: Core depends ONLY on @fractalizer/mcp-infrastructure
⚠️ Scope Restrictions
Этот пакет содержит ТОЛЬКО инфраструктуру для MCP tools:
| ✅ Входит в scope | ❌ НЕ входит в scope |
|-------------------|---------------------|
| BaseTool и связанные типы | HTTP клиенты → @fractalizer/mcp-infrastructure |
| ToolRegistry и фильтрация | CLI логика → @fractalizer/mcp-cli |
| Schema → Definition генерация | Discovery инструментов — на стороне MCP клиента |
| Утилиты для результатов tools | Бизнес-логика серверов → packages/servers/* |
| Общие Zod-схемы для tools | Logging, caching → @fractalizer/mcp-infrastructure |
Правило: Если функционал не связан напрямую с созданием/регистрацией/выполнением MCP tools — ему здесь не место
📦 Installation
npm install @fractalizer/mcp-coreDependencies:
@fractalizer/mcp-infrastructure(HTTP, logging, config)@modelcontextprotocol/sdk(MCP protocol)zod(validation)
📁 Structure
src/
├── definition/ # Schema-to-Definition generation
│ ├── schema-to-definition.ts # Auto-generate definitions from Zod
│ ├── definition-validator.ts # ToolDefinition validation
│ └── zod-json-schema-adapter.ts # Zod → JSON Schema converter
├── tool-registry/ # Tool registration and filtering
│ ├── tool-registry.ts # Tool registration and routing
│ ├── tool-filter.service.ts # Category/subcategory filtering
│ ├── tool-sorter.ts # Priority-based sorting
│ ├── tool-access-policy.ts # Single source of truth: tools/list visibility == tools/call callability
│ ├── tools-list-projection.ts # ToolDefinition[] → wire shape of tools/list
│ └── types.ts # Registry types
├── mcp-server-adapter/ # MCP server lifecycle + transport (createMcpServerAdapter)
│ ├── create-mcp-server-adapter.ts # Server instance, request handlers, stdio transport, signal handlers
│ ├── normalize-tool-name.ts # Strip server-name prefix some MCP clients add to tool calls
│ ├── tools-metrics.ts # tools/list size/token metrics + logging
│ └── tool-call-error-response.ts # Uniform tools/call error envelope
├── tools/
│ ├── base/ # Base classes for tools
│ │ ├── base-tool.ts # Generic BaseTool<TSchema>
│ │ └── tool-metadata.ts # StaticToolMetadata interface
│ └── common/ # Common utilities
│ ├── schemas/ # Reusable Zod schemas
│ └── utils/ # Tool utilities
├── utils/ # General utilities
│ ├── response-field-filter.ts # Filter response fields
│ ├── batch-result-processor.ts # Process batch results
│ └── result-logger.ts # Log tool results
└── index.ts # Public exports🔧 Core Components
BaseTool
Generic base class for creating MCP tools that work with any API facade.
Key features:
- ✅ Generic
<TFacade>— facade-agnostic design - ✅ Automatic parameter validation (Zod schemas via
getParamsSchema()) - ✅ Auto-generates MCP definitions from schema (DRY principle)
- ✅ Built-in logging, error handling
- ✅ Integration with ToolRegistry
Implementation: src/tools/base/base-tool.ts
Abstract interface:
abstract class BaseTool<TFacade = unknown> {
static readonly METADATA: StaticToolMetadata; // Required!
constructor(facade: TFacade, logger: Logger);
getDefinition(): ToolDefinition; // Auto-generated from schema
getParamsSchema?(): ZodSchema; // Optional: enables auto-generation
abstract execute(params: unknown): Promise<ToolResult>;
}Reference implementation: See any tool in packages/servers/yandex-tracker/src/tools/api/ (e.g., issues/get/get-issues.tool.ts)
generateDefinitionFromSchema()
✅ Auto-generates MCP definitions from Zod schemas (eliminates schema-definition mismatch)
Key features:
- ✅ Single source of truth (Zod schema with
.describe()) - ✅ Uses Zod v4 native
schema.toJSONSchema()API - ✅ Automatic required/optional field detection
- ✅ Physically impossible to create schema ↔ definition mismatch
Implementation: src/definition/schema-to-definition.ts
Function signature:
function generateDefinitionFromSchema(
schema: ZodSchema,
options?: { includeDescriptions?: boolean }
): JSONSchema7Benefits: DRY principle, type-safe, no separate *.definition.ts files
Migration guide: ../../../ARCHITECTURE.md → "Schema-to-Definition Generator"
buildOptimisticLockDescription()
Единая формулировка подсказки об оптимистичной блокировке для .describe() полей
version/revision. Профилактика работает только ДО вызова: описание параметра модель
читает, формируя аргументы, тогда как предупреждение VERSION_NOT_PROVIDED приходит,
когда запись уже применена.
function buildOptimisticLockDescription(options: {
paramName: 'version' | 'revision';
source: string; // клауза «где взять значение» целиком
conflict: 'silent-overwrite' | 'unspecified';
lead?: string; // оговорка области применения, встаёт перед риском
}): stringsource формулирует вызывающий: у Трекера токен приходит клиентской проекцией
fields, у Вики он базовое поле ответа — зашитая во фреймворк клауза про fields
была бы ложью для одного из двух серверов. conflict различает НАБЛЮДЁННОЕ
поведение API, а не форму поля схемы.
Implementation: src/definition/optimistic-lock-description.ts
BaseDefinition (Deprecated)
⚠️ Deprecated in v2.0 — use generateDefinitionFromSchema() instead.
Problem with old approach: Manual sync between Zod validation schema and MCP definition → frequent mismatch bugs.
ToolRegistry
Tool registration and routing — maps tool names to handlers, provides filtering and sorting.
Key features:
- ✅ Lazy initialization (tools created on-demand via DI container)
- ✅ Priority-based sorting: critical → high → normal → low
- ✅ Category filtering (via
ToolFilterService) - ✅ Type-safe tool registration
- ✅ Error handling for unknown tools
Implementation: src/tool-registry/tool-registry.ts
Class signature:
class ToolRegistry {
constructor(
container: Container,
logger: Logger,
toolClasses: ReadonlyArray<ToolClass>,
filterService?: ToolFilterService
);
getDefinitions(): ToolDefinition[]; // Filtered & sorted
executeToolByName(name: string, params: ToolCallParams): Promise<ToolResult>;
}createMcpServerAdapter()
MCP server lifecycle + transport, shared by all servers (yandex-tracker,
yandex-wiki) instead of near-identical server.ts code. Serves both protocol eras over stdio
(serveStdio, legacy 2025-06-18 initialize handshake and 2026-07-28 server/discover) from the
same tools/list/tools/call handlers — no initialize handler of our own: the SDK's built-in one
negotiates protocolVersion and stamps serverInfo/resultType/_meta/cache hints, so there is no
hardcoded version anywhere in this repo. tools/list visibility is filtered through
ToolRegistry.getVisibleDefinitions() — the same ToolAccessPolicy instance execute() checks for
tools/call, so the two can never disagree, in either era.
Implementation: src/mcp-server-adapter/create-mcp-server-adapter.ts
import { createMcpServerAdapter } from '@fractalizer/mcp-core';
const adapter = createMcpServerAdapter({
serverName: MCP_SERVER_NAME,
serverDisplayName: MCP_SERVER_DISPLAY_NAME, // optional
version: getPackageVersion(),
toolRegistry, // already carries its own ToolAccessPolicy
logger,
});
await adapter.start();🛠️ Utilities
ResponseFieldFilter
Filter response fields to reduce token usage (80-90% savings).
Usage:
import { ResponseFieldFilter } from '@fractalizer/mcp-core';
const data = {
id: '123',
name: 'Item',
description: 'Long description...',
metadata: { /* ... */ },
};
// Filter to specific fields
const filtered = ResponseFieldFilter.filter(data, ['id', 'name']);
// Result: { id: '123', name: 'Item' }
// No fields specified = return all
const all = ResponseFieldFilter.filter(data);
// Result: full data objectBatchResultProcessor
Process batch operation results (fulfilled + rejected).
Usage:
import { BatchResultProcessor } from '@fractalizer/mcp-core';
const results: BatchResult<string, Item> = [
{ status: 'fulfilled', value: { id: '1', name: 'Item 1' } },
{ status: 'rejected', reason: new Error('Not found') },
];
// Separate successful and failed
const { successful, failed } = BatchResultProcessor.separateResults(results);
// Format for response
const formatted = BatchResultProcessor.formatBatchResponse(
successful,
failed,
'items'
);
// Result: { items: [...], errors: [...] }ResultLogger
Log tool execution results with structured output.
Usage:
import { ResultLogger } from '@fractalizer/mcp-core';
// Log successful result
ResultLogger.logSuccess(logger, 'get_item', result, { itemId: '123' });
// Log error
ResultLogger.logError(logger, 'get_item', error, { itemId: '123' });validateRedactionAllowlist()
Барьер соответствия METADATA.redactionAllowlist схеме параметров. Подключается
вручную в scripts/validate-tool-registration.ts каждого сервера — сам себя он нигде
не запускает (чек-лист: MCP_SERVER_CHECKLIST.md §8.3).
import { validateRedactionAllowlist } from '@fractalizer/mcp-core';
const errors = validateRedactionAllowlist(TOOL_CLASSES); // [] — расхождений нетОбещает: каждый ключ allow-list называет параметр, реально существующий в
getDefinition().inputSchema, на любой глубине вложенности. Опечатка, переименованный
параметр и допуск, переживший удаление параметра, роняют проверку. Нечитаемая схема при
непустом allow-list — тоже ошибка, а не пропуск.
НЕ проверяет:
- тип допущенного параметра — сверяется только ИМЯ. Допуск на параметр со свободным
пользовательским текстом пройдёт зелёным, инертный допуск на нередактируемый параметр —
тоже. Осмысленность допусков проверяет ревью и отдельный смоук на стороне сервера
(образец —
tests/smoke/tool-redaction-allowlist.smoke.test.tsу yandex-tracker); - обратное направление — параметр вне allow-list штатен по построению;
- путь до параметра — совпадение имени на любой глубине принимается за существование,
как и в самом
redactParams.
Границы и формы JSON Schema, которые знает обходчик, — JSDoc
src/tools/base/redaction-allowlist-validator.ts.
computeToolSchemaFingerprint()
Отпечаток схемы параметров инструмента — способ привязать утверждение, записанное
о инструменте однажды, к состоянию его кода (потребитель — реестр живых наблюдений
yandex-tracker, tests/coverage-exceptions/live-observations.ts).
const fingerprint = computeToolSchemaFingerprint(GetIssuesTool); // 'e170b50752de'Схема берётся тем же единственным способом, что и у барьера допусков —
readToolInputSchema (getDefinition().inputSchema). Отпечаток детерминирован:
порядок ключей на него не влияет, порядок элементов массива — влияет (в схеме он
значим). 12 hex-символов — чтобы строка читалась в диффе; коллизии моделью угроз не
являются.
НЕ ловит — всё за пределами схемы параметров, и таких классов два:
- маршрут (URL, метод, форма тела запроса) живёт в операции API, и его смена без смены схемы отпечатком не видна;
- форма ответа —
outputSchemaи DTO разбора в отпечаток не входят вовсе, хотя утверждение «эффект правдив» читается именно через ответ (живой прогон трекера 26.08.2026:PATCH .../permissionsотдаёт{self, version}при типе-массиве).
Названный риск: отпечаток зависит от адаптера zod→JSON Schema и версии zod — их
смена меняет ВСЕ отпечатки разом, и массовая пере-штамповка была бы ложью (см. JSDoc
tool-schema-fingerprint.ts). computeSchemaFingerprint(schema) считает то же по
готовой схеме.
🚨 Critical Rules
1. BaseTool is generic — facade-agnostic
// ✅ CORRECT (generic facade)
class MyTool extends BaseTool<MyApiFacade> {
async execute(params: Params) {
// Use this.facade (type: MyApiFacade)
return this.facade.someMethod();
}
}
// ❌ WRONG (hardcoded facade type in BaseTool)
class BaseTool {
constructor(private facade: SpecificFacade) {} // BAD!
}2. Always validate params with Zod
// ✅ CORRECT
const validated = this.validateParams(ParamsSchema, params);
// ❌ WRONG (no validation)
const id = params.id; // Unsafe!3. Use METADATA for tool discovery
// ✅ CORRECT — Extended metadata with categorization
static readonly METADATA: ToolMetadata = {
name: 'my_tool',
description: '[Category/Action] Brief description',
category: 'api', // REQUIRED
subcategory: 'read', // Optional (read/write/workflow)
priority: 'critical', // Optional (critical/high/normal/low)
tags: ['tag1', 'tag2'], // Optional (for categorization)
requiresExplicitUserConsent: false, // or true for write operations
inputSchema: {...}
};
// ❌ WRONG (missing required category)
static readonly METADATA = {
name: 'my_tool',
description: 'Tool description',
// Missing category — will fail type check
};Tool Categorization & Priority:
Priority levels (for sorting in tools/list):
critical— Frequently used, key operations (shown first)high— Important but not criticalnormal— Regular operations (default)low— Rarely used, debug tools (shown last)
Tools are sorted: critical → high → normal → low → alphabetically
Description convention: [Category/Subcategory] Brief description
- Keep descriptions concise (≤80 chars)
- Use category prefix for structure
- Details go in inputSchema parameter descriptions
Example:
static readonly METADATA: ToolMetadata = {
name: 'create_issue',
description: '[Issues/Write] Create new issue',
category: 'issues',
subcategory: 'write',
priority: 'critical',
tags: ['create', 'new', 'write', 'issue'],
inputSchema: {...}
};4. Filter fields before returning
// ✅ CORRECT (filter to save tokens)
const filtered = ResponseFieldFilter.filter(data, params.fields);
return { content: [{ type: 'text', text: JSON.stringify(filtered) }] };
// ❌ WRONG (return everything)
return { content: [{ type: 'text', text: JSON.stringify(data) }] };📖 API Reference
Exports
// Base classes
export { BaseTool } from './tools/base/base-tool.js';
export { BaseDefinition } from './tools/base/base-definition.js';
export type { StaticToolMetadata } from './tools/base/tool-metadata.js';
export { validateRedactionAllowlist } from './tools/base/redaction-allowlist-validator.js';
export { readToolInputSchema } from './tools/base/tool-input-schema.js';
export type { ToolClassLike } from './tools/base/tool-input-schema.js';
export {
computeSchemaFingerprint,
computeToolSchemaFingerprint,
} from './tools/base/tool-schema-fingerprint.js';
// Utilities
export { ResponseFieldFilter } from './utils/response-field-filter.js';
export { BatchResultProcessor } from './utils/batch-result-processor.js';
export { ResultLogger } from './utils/result-logger.js';
// Registry
export { ToolRegistry } from './tool-registry.js';
// Common utilities
export { buildToolName, SafetyWarningBuilder } from './tools/common/utils/index.js';
// Types
export type { BatchResult, FulfilledResult, RejectedResult } from '@fractalizer/mcp-infrastructure';🧪 Testing
Run tests:
cd packages/framework/core
npm run testWith coverage:
npm run test:coverageWatch mode:
npm run test:watch🤝 Contributing
See ../../../.github/CONTRIBUTING.md
Architecture rules: ../../../CLAUDE.md
📄 License
PolyForm Shield License 1.0.0
🔗 Links
- Monorepo root: ../../../README.md
- Architecture: ../../../ARCHITECTURE.md
- Infrastructure package: ../infrastructure/README.md
