=== NEW SERVERS ADDED (7) === - servers/closebot — 119 tools, 14 modules, 4,656 lines TS (Stage 7) - servers/google-console — Google Search Console MCP (Stage 7) - servers/meta-ads — Meta/Facebook Ads MCP (Stage 8) - servers/twilio — Twilio communications MCP (Stage 8) - servers/competitor-research — Competitive intel MCP (Stage 6) - servers/n8n-apps — n8n workflow MCP apps (Stage 6) - servers/reonomy — Commercial real estate MCP (Stage 1) === FACTORY INFRASTRUCTURE ADDED === - infra/factory-tools — mcp-jest, mcp-validator, mcp-add, MCP Inspector - 60 test configs, 702 auto-generated test cases - All 30 servers score 100/100 protocol compliance - infra/command-center — Pipeline state, operator playbook, dashboard config - infra/factory-reviews — Automated eval reports === DOCS ADDED === - docs/MCP-FACTORY.md — Factory overview - docs/reports/ — 5 pipeline evaluation reports - docs/research/ — Browser MCP research === RULES ESTABLISHED === - CONTRIBUTING.md — All MCP work MUST go in this repo - README.md — Full inventory of 37 servers + infra docs - .gitignore — Updated for Python venvs TOTAL: 37 MCP servers + full factory pipeline in one repo. This is now the single source of truth for all MCP work.
214 lines
5.7 KiB
TypeScript
214 lines
5.7 KiB
TypeScript
/**
|
|
* Tool registry with annotations, safety tiers, and pack management.
|
|
* Supports MCP tool annotations spec + custom metadata for lazy loading.
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
|
|
// Safety tiers — controls confirmation behavior
|
|
export type SafetyTier = 'green' | 'yellow' | 'red';
|
|
|
|
// Tool annotation metadata
|
|
export interface ToolAnnotations {
|
|
/** Tool only reads data, no side effects */
|
|
readOnlyHint?: boolean;
|
|
/** Tool may perform destructive actions (delete, release) */
|
|
destructiveHint?: boolean;
|
|
/** Tool is idempotent — safe to retry */
|
|
idempotentHint?: boolean;
|
|
/** Tool may cost real money (Twilio charges) */
|
|
costHint?: boolean;
|
|
/** Human-readable cost estimate */
|
|
estimatedCost?: string;
|
|
}
|
|
|
|
// Our extended tool metadata
|
|
export interface ToolMeta {
|
|
name: string;
|
|
description: string;
|
|
category: string;
|
|
pack: string;
|
|
safety: SafetyTier;
|
|
annotations: ToolAnnotations;
|
|
inputSchema: z.ZodType<any>;
|
|
_meta?: {
|
|
labels: {
|
|
category: string;
|
|
access: "read" | "write" | "delete";
|
|
complexity: "simple" | "complex" | "batch";
|
|
};
|
|
};
|
|
isApp?: boolean;
|
|
/** Tier 1 = always loaded, Tier 2 = lazy loaded */
|
|
tier: 1 | 2;
|
|
}
|
|
|
|
export interface RegisteredTool extends ToolMeta {
|
|
handler: (params: any) => Promise<ToolResult>;
|
|
}
|
|
|
|
export interface ToolResult {
|
|
content: Array<{
|
|
type: 'text' | 'resource';
|
|
text?: string;
|
|
resource?: {
|
|
uri: string;
|
|
mimeType: string;
|
|
text: string;
|
|
};
|
|
}>;
|
|
isError?: boolean;
|
|
structuredContent?: {
|
|
type: 'html';
|
|
html: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Central registry for all tools across all packs.
|
|
*/
|
|
export class ToolRegistry {
|
|
private tools = new Map<string, RegisteredTool>();
|
|
private activePacks = new Set<string>();
|
|
private onToolsChanged?: () => void;
|
|
|
|
/** Set callback for when tools list changes (lazy loading) */
|
|
setOnToolsChanged(callback: () => void) {
|
|
this.onToolsChanged = callback;
|
|
}
|
|
|
|
/** Register a single tool */
|
|
register(tool: RegisteredTool): void {
|
|
this.tools.set(tool.name, tool);
|
|
}
|
|
|
|
/** Register all tools from a pack */
|
|
registerPack(packName: string, tools: RegisteredTool[]): void {
|
|
for (const tool of tools) {
|
|
this.tools.set(tool.name, { ...tool, pack: packName });
|
|
}
|
|
this.activePacks.add(packName);
|
|
}
|
|
|
|
/** Activate a lazy-loaded pack and notify client */
|
|
activatePack(packName: string, tools: RegisteredTool[]): void {
|
|
if (this.activePacks.has(packName)) return; // Already loaded
|
|
this.registerPack(packName, tools);
|
|
this.onToolsChanged?.();
|
|
}
|
|
|
|
/** Deactivate a pack (unload tools) */
|
|
deactivatePack(packName: string): void {
|
|
if (!this.activePacks.has(packName)) return;
|
|
for (const [name, tool] of this.tools) {
|
|
if (tool.pack === packName) {
|
|
this.tools.delete(name);
|
|
}
|
|
}
|
|
this.activePacks.delete(packName);
|
|
this.onToolsChanged?.();
|
|
}
|
|
|
|
/** Get a tool by name */
|
|
get(name: string): RegisteredTool | undefined {
|
|
return this.tools.get(name);
|
|
}
|
|
|
|
/** Get all currently registered tools (for tools/list response) */
|
|
getAll(): RegisteredTool[] {
|
|
return Array.from(this.tools.values());
|
|
}
|
|
|
|
/** Get tools for a specific pack */
|
|
getByPack(packName: string): RegisteredTool[] {
|
|
return Array.from(this.tools.values()).filter(t => t.pack === packName);
|
|
}
|
|
|
|
/** Get all active pack names */
|
|
getActivePacks(): string[] {
|
|
return Array.from(this.activePacks);
|
|
}
|
|
|
|
/** Check if a pack is loaded */
|
|
isPackActive(packName: string): boolean {
|
|
return this.activePacks.has(packName);
|
|
}
|
|
|
|
/** Convert to MCP tools/list format */
|
|
toMCPToolsList(): Array<{
|
|
name: string;
|
|
description: string;
|
|
inputSchema: any;
|
|
_meta?: any;
|
|
annotations?: Record<string, any>;
|
|
}> {
|
|
return this.getAll().map(tool => {
|
|
const schema = tool.inputSchema instanceof z.ZodType
|
|
? zodToJsonSchema(tool.inputSchema)
|
|
: tool.inputSchema;
|
|
|
|
return {
|
|
name: tool.name,
|
|
description: tool.description,
|
|
inputSchema: schema,
|
|
_meta: tool._meta,
|
|
annotations: {
|
|
...tool.annotations,
|
|
category: tool.category,
|
|
safety: tool.safety,
|
|
},
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Convert Zod schema to JSON Schema (simplified) */
|
|
function zodToJsonSchema(schema: z.ZodType<any>): Record<string, any> {
|
|
if (schema instanceof z.ZodObject) {
|
|
const shape = schema.shape;
|
|
const properties: Record<string, any> = {};
|
|
const required: string[] = [];
|
|
|
|
for (const [key, value] of Object.entries(shape)) {
|
|
const zodValue = value as z.ZodType<any>;
|
|
properties[key] = zodFieldToJsonSchema(zodValue);
|
|
if (!(zodValue instanceof z.ZodOptional)) {
|
|
required.push(key);
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: 'object',
|
|
properties,
|
|
...(required.length > 0 ? { required } : {}),
|
|
};
|
|
}
|
|
|
|
return { type: 'object' };
|
|
}
|
|
|
|
function zodFieldToJsonSchema(field: z.ZodType<any>): Record<string, any> {
|
|
if (field instanceof z.ZodOptional) {
|
|
return zodFieldToJsonSchema(field.unwrap());
|
|
}
|
|
if (field instanceof z.ZodString) {
|
|
return { type: 'string', description: field.description };
|
|
}
|
|
if (field instanceof z.ZodNumber) {
|
|
return { type: 'number', description: field.description };
|
|
}
|
|
if (field instanceof z.ZodBoolean) {
|
|
return { type: 'boolean', description: field.description };
|
|
}
|
|
if (field instanceof z.ZodArray) {
|
|
return { type: 'array', items: zodFieldToJsonSchema(field.element), description: field.description };
|
|
}
|
|
if (field instanceof z.ZodEnum) {
|
|
return { type: 'string', enum: field.options, description: field.description };
|
|
}
|
|
if (field instanceof z.ZodObject) {
|
|
return zodToJsonSchema(field);
|
|
}
|
|
return { type: 'string' };
|
|
}
|