=== 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.
318 lines
7.5 KiB
TypeScript
318 lines
7.5 KiB
TypeScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { z } from 'zod';
|
|
import type { MetaApiClient } from './client/meta-client.js';
|
|
import type { ToolCategory, LoadTier, ToolAnnotations } from './types/meta-api.js';
|
|
|
|
/**
|
|
* Tool definition with metadata
|
|
*/
|
|
export interface ToolDefinition {
|
|
name: string;
|
|
description: string;
|
|
inputSchema: z.ZodType<unknown>;
|
|
annotations?: ToolAnnotations;
|
|
_meta?: {
|
|
labels: {
|
|
category: string;
|
|
access: "read" | "write" | "delete";
|
|
complexity: "simple" | "complex" | "batch";
|
|
};
|
|
};
|
|
handler: (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }>;
|
|
}
|
|
|
|
/**
|
|
* Module registry for lazy loading
|
|
*/
|
|
interface ToolModule {
|
|
category: ToolCategory;
|
|
tier: LoadTier;
|
|
loaded: boolean;
|
|
register: (registry: ToolRegistry) => Promise<void> | void;
|
|
}
|
|
|
|
export class ToolRegistry {
|
|
private tools = new Map<string, ToolDefinition>();
|
|
private client: MetaApiClient;
|
|
|
|
constructor(client: MetaApiClient) {
|
|
this.client = client;
|
|
}
|
|
|
|
/**
|
|
* Register a tool
|
|
*/
|
|
registerTool(tool: ToolDefinition): void {
|
|
this.tools.set(tool.name, tool);
|
|
console.error(`[MCP] Registered tool: ${tool.name}`);
|
|
}
|
|
|
|
/**
|
|
* Get all registered tools
|
|
*/
|
|
getTools(): ToolDefinition[] {
|
|
return Array.from(this.tools.values());
|
|
}
|
|
|
|
/**
|
|
* Get a specific tool
|
|
*/
|
|
getTool(name: string): ToolDefinition | undefined {
|
|
return this.tools.get(name);
|
|
}
|
|
|
|
/**
|
|
* Get the API client
|
|
*/
|
|
getClient(): MetaApiClient {
|
|
return this.client;
|
|
}
|
|
}
|
|
|
|
export class ModuleRegistry {
|
|
private modules = new Map<ToolCategory, ToolModule>();
|
|
private toolRegistry: ToolRegistry;
|
|
|
|
constructor(toolRegistry: ToolRegistry) {
|
|
this.toolRegistry = toolRegistry;
|
|
}
|
|
|
|
/**
|
|
* Register a tool module
|
|
*/
|
|
registerModule(module: ToolModule): void {
|
|
this.modules.set(module.category, module);
|
|
}
|
|
|
|
/**
|
|
* Load a module if not already loaded
|
|
*/
|
|
async loadModule(category: ToolCategory): Promise<void> {
|
|
const module = this.modules.get(category);
|
|
|
|
if (!module) {
|
|
throw new Error(`Module ${category} not registered`);
|
|
}
|
|
|
|
if (module.loaded) {
|
|
return;
|
|
}
|
|
|
|
console.error(`[MCP] Loading module: ${category}`);
|
|
await module.register(this.toolRegistry);
|
|
module.loaded = true;
|
|
console.error(`[MCP] Module loaded: ${category}`);
|
|
}
|
|
|
|
/**
|
|
* Get module status
|
|
*/
|
|
getModuleStatus(): Record<string, { tier: LoadTier; loaded: boolean }> {
|
|
const status: Record<string, { tier: LoadTier; loaded: boolean }> = {};
|
|
|
|
for (const [category, module] of this.modules.entries()) {
|
|
status[category] = {
|
|
tier: module.tier,
|
|
loaded: module.loaded,
|
|
};
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Check if a module is loaded
|
|
*/
|
|
isModuleLoaded(category: ToolCategory): boolean {
|
|
return this.modules.get(category)?.loaded || false;
|
|
}
|
|
|
|
/**
|
|
* Get tool registry
|
|
*/
|
|
getToolRegistry(): ToolRegistry {
|
|
return this.toolRegistry;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create gateway tool for lazy-loaded advanced modules
|
|
*/
|
|
export function createGatewayTool(
|
|
category: ToolCategory,
|
|
registry: ModuleRegistry,
|
|
description: string
|
|
): ToolDefinition {
|
|
return {
|
|
name: `${category}_tools`,
|
|
description,
|
|
inputSchema: z.object({}),
|
|
annotations: {
|
|
title: `Load ${category} module`,
|
|
readOnlyHint: true,
|
|
openWorldHint: false,
|
|
},
|
|
handler: async () => {
|
|
// Load the module if not loaded
|
|
if (!registry.isModuleLoaded(category)) {
|
|
await registry.loadModule(category);
|
|
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Module '${category}' has been loaded. Available tools are now registered. Please list tools again to see them.`,
|
|
}],
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Module '${category}' is already loaded.`,
|
|
}],
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create and configure the MCP server
|
|
*/
|
|
export function createMcpServer(toolRegistry: ToolRegistry): Server {
|
|
const server = new Server(
|
|
{
|
|
name: '@clawdbot/meta-ads-mcp',
|
|
version: '1.0.0',
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
// Handle tools/list
|
|
server.setRequestHandler('tools/list' as never, async () => {
|
|
const tools = toolRegistry.getTools();
|
|
|
|
return {
|
|
tools: tools.map(tool => {
|
|
// Convert Zod schema to JSON Schema
|
|
const inputSchema = zodToJsonSchema(tool.inputSchema);
|
|
|
|
return {
|
|
name: tool.name,
|
|
description: tool.description,
|
|
inputSchema,
|
|
_meta: tool._meta,
|
|
annotations: tool.annotations,
|
|
};
|
|
}),
|
|
};
|
|
});
|
|
|
|
// Handle tools/call
|
|
server.setRequestHandler('tools/call' as never, async (request: { params: { name: string; arguments: Record<string, unknown> } }) => {
|
|
const { name, arguments: args } = request.params;
|
|
|
|
const tool = toolRegistry.getTool(name);
|
|
if (!tool) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Unknown tool: ${name}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
try {
|
|
// Validate input
|
|
const validatedArgs = tool.inputSchema.parse(args);
|
|
|
|
// Execute handler
|
|
return await tool.handler(validatedArgs as Record<string, unknown>);
|
|
} catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
});
|
|
|
|
return server;
|
|
}
|
|
|
|
/**
|
|
* Convert Zod schema to JSON Schema (simplified)
|
|
*/
|
|
function zodToJsonSchema(schema: z.ZodType<unknown>): Record<string, unknown> {
|
|
// This is a simplified conversion. For production, use @anatine/zod-to-openapi or similar
|
|
if (schema instanceof z.ZodObject) {
|
|
const shape = (schema as z.ZodObject<z.ZodRawShape>).shape;
|
|
const properties: Record<string, unknown> = {};
|
|
const required: string[] = [];
|
|
|
|
for (const [key, value] of Object.entries(shape)) {
|
|
if (value instanceof z.ZodString) {
|
|
properties[key] = {
|
|
type: 'string',
|
|
description: value.description,
|
|
};
|
|
} else if (value instanceof z.ZodNumber) {
|
|
properties[key] = {
|
|
type: 'number',
|
|
description: value.description,
|
|
};
|
|
} else if (value instanceof z.ZodBoolean) {
|
|
properties[key] = {
|
|
type: 'boolean',
|
|
description: value.description,
|
|
};
|
|
} else if (value instanceof z.ZodArray) {
|
|
properties[key] = {
|
|
type: 'array',
|
|
description: value.description,
|
|
items: {},
|
|
};
|
|
} else if (value instanceof z.ZodEnum) {
|
|
properties[key] = {
|
|
type: 'string',
|
|
enum: value.options,
|
|
description: value.description,
|
|
};
|
|
} else {
|
|
properties[key] = {
|
|
type: 'object',
|
|
description: value.description,
|
|
};
|
|
}
|
|
|
|
if (!value.isOptional()) {
|
|
required.push(key);
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: 'object',
|
|
properties,
|
|
required,
|
|
};
|
|
}
|
|
|
|
return { type: 'object' };
|
|
}
|
|
|
|
/**
|
|
* Connect server to stdio transport
|
|
*/
|
|
export async function connectServer(server: Server): Promise<void> {
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error('[MCP] Meta Ads MCP server running on stdio');
|
|
}
|