npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fractalizer/mcp-core

v5.1.0

Published

Core framework for building MCP tools: base classes, utilities, registry

Readme

@fractalizer/mcp-core

Core framework for building MCP tools: base classes, utilities, registry

npm version License: PolyForm Shield


🎯 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-core

Dependencies:

  • @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 }
): JSONSchema7

Benefits: 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;       // оговорка области применения, встаёт перед риском
}): string

source формулирует вызывающий: у Трекера токен приходит клиентской проекцией 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 object

BatchResultProcessor

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 critical
  • normal — 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 test

With coverage:

npm run test:coverage

Watch mode:

npm run test:watch

🤝 Contributing

See ../../../.github/CONTRIBUTING.md

Architecture rules: ../../../CLAUDE.md


📄 License

PolyForm Shield License 1.0.0


🔗 Links