Skip to main content
Glama
paulsham

Wiki Analytics Specification MCP Server

by paulsham
tools.js9.02 kB
/** * MCP tool definitions and handlers */ import { events, eventsMap, propertiesMap, propertyGroups, splitMultiLine, getExpandedProperties, } from './data.js'; import { NotFoundError } from './errors.js'; export const tools = { get_event_implementation: { description: 'Get complete event specification with all properties expanded. Use when implementing tracking code.', inputSchema: { type: 'object', properties: { event_name: { type: 'string', description: 'Name of the event to retrieve' } }, required: ['event_name'] }, handler: async (args) => { const event = eventsMap.get(args.event_name); if (!event) { throw new NotFoundError('Event', args.event_name); } const expanded = getExpandedProperties(event); return { event: event.event_name, description: event.event_description, table: event.event_table, notes: event.notes || null, property_groups: expanded.property_groups, additional_properties: expanded.additional_properties }; } }, validate_event_payload: { description: 'Validate a tracking implementation payload against the event spec. Returns errors, warnings, and valid fields.', inputSchema: { type: 'object', properties: { event_name: { type: 'string', description: 'Name of the event to validate against' }, payload: { type: 'object', description: 'The payload object to validate' } }, required: ['event_name', 'payload'] }, handler: async (args) => { const event = eventsMap.get(args.event_name); if (!event) { throw new NotFoundError('Event', args.event_name); } const expanded = getExpandedProperties(event); const errors = []; const warnings = []; const validFields = []; // Collect all expected properties const expectedProps = new Map(); for (const group of expanded.property_groups) { for (const prop of group.properties) { expectedProps.set(prop.name, prop); } } for (const prop of expanded.additional_properties) { expectedProps.set(prop.name, prop); } // Check provided fields for (const [key, value] of Object.entries(args.payload)) { const prop = expectedProps.get(key); if (!prop) { warnings.push({ field: key, issue: 'Unknown property not in spec' }); continue; } // Remove from expected (field was provided, even if invalid) expectedProps.delete(key); // Type validation const actualType = Array.isArray(value) ? 'array' : typeof value; const expectedType = prop.type === 'timestamp' ? 'string' : prop.type; if (expectedType !== actualType && expectedType !== 'unknown') { errors.push({ field: key, issue: 'Type mismatch', expected: prop.type, got: actualType }); continue; } // Constraint validation if (prop.constraints && prop.constraints !== '-') { if (prop.constraints.startsWith('enum:')) { const allowedValues = prop.constraints.substring(5).split(',').map(s => s.trim()); if (!allowedValues.includes(value)) { errors.push({ field: key, issue: 'Invalid enum value', expected: allowedValues, got: value }); continue; } } else if (prop.constraints.startsWith('regex:')) { const pattern = prop.constraints.substring(6).trim(); try { const regex = new RegExp(pattern); if (!regex.test(value)) { errors.push({ field: key, issue: 'Regex validation failed', expected: pattern, got: value }); continue; } } catch (e) { // Invalid regex pattern in spec } } } validFields.push(key); } // Check for missing properties for (const [name] of expectedProps) { warnings.push({ field: name, issue: 'Missing property' }); } return { valid: errors.length === 0, errors, warnings, valid_fields: validFields }; } }, search_events: { description: 'Search for events by name, description, table, or property usage.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search term for event name or description' }, table: { type: 'string', description: 'Filter by event table' }, has_property: { type: 'string', description: 'Filter events that include this property' } } }, handler: async (args) => { let results = [...events]; // Filter by query if (args.query) { const query = args.query.toLowerCase(); results = results.filter(e => e.event_name.toLowerCase().includes(query) || e.event_description.toLowerCase().includes(query) ); } // Filter by table if (args.table) { const table = args.table.toLowerCase(); results = results.filter(e => e.event_table.toLowerCase().includes(table) ); } // Filter by property if (args.has_property) { results = results.filter(e => { const expanded = getExpandedProperties(e); const allProps = [ ...expanded.property_groups.flatMap(g => g.properties.map(p => p.name)), ...expanded.additional_properties.map(p => p.name) ]; return allProps.includes(args.has_property); }); } return results.map(e => { const expanded = getExpandedProperties(e); const propertyCount = expanded.property_groups.reduce((sum, g) => sum + g.properties.length, 0) + expanded.additional_properties.length; return { event_name: e.event_name, description: e.event_description, table: e.event_table, property_count: propertyCount }; }); } }, get_property_details: { description: 'Get property definition and see where it is used across events and property groups.', inputSchema: { type: 'object', properties: { property_name: { type: 'string', description: 'Name of the property to retrieve' } }, required: ['property_name'] }, handler: async (args) => { const prop = propertiesMap.get(args.property_name); if (!prop) { throw new NotFoundError('Property', args.property_name); } // Find usage in property groups const usedInGroups = propertyGroups .filter(g => splitMultiLine(g.properties).includes(args.property_name)) .map(g => g.group_name); // Find usage in events (direct additional properties) const usedInEvents = events .filter(e => splitMultiLine(e.additional_properties).includes(args.property_name)) .map(e => e.event_name); // Find events that use this property via groups const eventsViaGroups = events .filter(e => { const eventGroups = splitMultiLine(e.property_groups); return eventGroups.some(groupName => usedInGroups.includes(groupName)); }) .map(e => e.event_name); return { name: prop.property_name, type: prop.type, constraints: prop.constraints || null, description: prop.description, usage: prop.usage || null, used_in_groups: usedInGroups, used_in_events_directly: usedInEvents, used_in_events_via_groups: eventsViaGroups }; } }, get_related_events: { description: 'Find events in the same table/flow as a given event.', inputSchema: { type: 'object', properties: { event_name: { type: 'string', description: 'Name of the event to find related events for' } }, required: ['event_name'] }, handler: async (args) => { const event = eventsMap.get(args.event_name); if (!event) { throw new NotFoundError('Event', args.event_name); } const relatedEvents = events .filter(e => e.event_table === event.event_table && e.event_name !== args.event_name) .map(e => ({ event_name: e.event_name, description: e.event_description })); return { event: args.event_name, table: event.event_table, related_events: relatedEvents }; } } };

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/paulsham/wiki-mcp-analytics-test-1.1.0'

If you have feedback or need assistance with the MCP directory API, please join our Discord server