Skip to main content
Glama
index.ts16.8 kB
#!/usr/bin/env node /** * Slate MCP Server * Connects Technolutions Slate with Claude for enrollment demographics reporting */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, TextContent, } from '@modelcontextprotocol/sdk/types.js'; import { SlateClient, FunnelData } from './slate-client.js'; import { generateDemographicsSummary, generateDiversityMetrics, generateYearOverYearComparison, formatDemographicsReport, } from './demographics.js'; import { SlateConfig, StudentRecord, EnrollmentDemographicsSummary, DiversityMetrics, YearOverYearComparison, } from './types.js'; // Load configuration from environment variables function loadConfig(): SlateConfig { const baseUrl = process.env.SLATE_BASE_URL; const username = process.env.SLATE_USERNAME; const password = process.env.SLATE_PASSWORD; const apiKey = process.env.SLATE_API_KEY; if (!baseUrl) { throw new Error('SLATE_BASE_URL environment variable is required'); } if (!apiKey && (!username || !password)) { throw new Error( 'Either SLATE_API_KEY or both SLATE_USERNAME and SLATE_PASSWORD are required' ); } return { baseUrl: baseUrl.replace(/\/$/, ''), // Remove trailing slash username: username || '', password: password || '', apiKey, }; } // Define available tools const tools: Tool[] = [ { name: 'get_enrollment_demographics', description: 'Get a comprehensive demographics summary for enrolled students for a specific entry year. Returns data on gender, ethnicity, geographic distribution, first-generation status, and more.', inputSchema: { type: 'object', properties: { entry_year: { type: 'number', description: 'The entry year for the cohort (e.g., 2024)', }, entry_term: { type: 'string', description: 'The entry term (e.g., "Fall", "Spring"). Defaults to "Fall".', default: 'Fall', }, }, required: ['entry_year'], }, }, { name: 'get_diversity_metrics', description: 'Get detailed diversity metrics for an enrolled cohort, including diversity index, URM percentage, gender balance, and geographic diversity.', inputSchema: { type: 'object', properties: { entry_year: { type: 'number', description: 'The entry year for the cohort (e.g., 2024)', }, entry_term: { type: 'string', description: 'The entry term (e.g., "Fall", "Spring"). Defaults to "Fall".', default: 'Fall', }, }, required: ['entry_year'], }, }, { name: 'get_enrollment_funnel', description: 'Get enrollment funnel data showing the progression from prospects to enrolled students for a specific year.', inputSchema: { type: 'object', properties: { entry_year: { type: 'number', description: 'The entry year for the cohort (e.g., 2024)', }, entry_term: { type: 'string', description: 'The entry term (e.g., "Fall", "Spring"). Defaults to "Fall".', default: 'Fall', }, }, required: ['entry_year'], }, }, { name: 'compare_enrollment_years', description: 'Compare enrollment demographics across multiple years to identify trends and changes.', inputSchema: { type: 'object', properties: { years: { type: 'array', items: { type: 'number' }, description: 'Array of years to compare (e.g., [2022, 2023, 2024])', }, entry_term: { type: 'string', description: 'The entry term to compare (e.g., "Fall"). Defaults to "Fall".', default: 'Fall', }, }, required: ['years'], }, }, { name: 'get_demographics_report', description: 'Generate a formatted, human-readable demographics report for a specific enrollment year.', inputSchema: { type: 'object', properties: { entry_year: { type: 'number', description: 'The entry year for the cohort (e.g., 2024)', }, entry_term: { type: 'string', description: 'The entry term (e.g., "Fall", "Spring"). Defaults to "Fall".', default: 'Fall', }, }, required: ['entry_year'], }, }, { name: 'get_demographics_by_category', description: 'Get enrollment counts broken down by a specific demographic category.', inputSchema: { type: 'object', properties: { entry_year: { type: 'number', description: 'The entry year for the cohort (e.g., 2024)', }, category: { type: 'string', enum: [ 'gender', 'ethnicity', 'state', 'country', 'admit_type', 'major', 'college', 'first_generation', 'international', ], description: 'The demographic category to break down by', }, entry_term: { type: 'string', description: 'The entry term (e.g., "Fall", "Spring"). Defaults to "Fall".', default: 'Fall', }, }, required: ['entry_year', 'category'], }, }, { name: 'search_students', description: 'Search for students matching specific criteria. Returns demographic information for matching students.', inputSchema: { type: 'object', properties: { entry_year: { type: 'number', description: 'Filter by entry year', }, entry_term: { type: 'string', description: 'Filter by entry term', }, state: { type: 'string', description: 'Filter by home state', }, country: { type: 'string', description: 'Filter by home country', }, major: { type: 'string', description: 'Filter by intended major (partial match)', }, first_generation: { type: 'boolean', description: 'Filter by first-generation status', }, international: { type: 'boolean', description: 'Filter by international status', }, limit: { type: 'number', description: 'Maximum number of results to return (default: 100)', default: 100, }, }, }, }, ]; // Create the server const server = new Server( { name: 'slate-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); let slateClient: SlateClient | null = null; // Initialize the Slate client function getSlateClient(): SlateClient { if (!slateClient) { const config = loadConfig(); slateClient = new SlateClient(config); } return slateClient; } // Handle list tools request server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const client = getSlateClient(); let result: unknown; switch (name) { case 'get_enrollment_demographics': { const entryYear = args?.entry_year as number; const entryTerm = (args?.entry_term as string) || 'Fall'; const students = await client.getEnrolledStudents(entryYear, entryTerm); const summary = generateDemographicsSummary(students, entryYear, entryTerm); result = summary; break; } case 'get_diversity_metrics': { const entryYear = args?.entry_year as number; const entryTerm = (args?.entry_term as string) || 'Fall'; const students = await client.getEnrolledStudents(entryYear, entryTerm); const metrics = generateDiversityMetrics(students, entryYear); result = metrics; break; } case 'get_enrollment_funnel': { const entryYear = args?.entry_year as number; const entryTerm = (args?.entry_term as string) || 'Fall'; const funnel = await client.getFunnelData(entryYear, entryTerm); result = { ...funnel, conversionRates: { inquiryToApplicant: funnel.inquiries > 0 ? `${((funnel.applicants / funnel.inquiries) * 100).toFixed(1)}%` : 'N/A', applicantToAdmit: funnel.applicants > 0 ? `${((funnel.admitted / funnel.applicants) * 100).toFixed(1)}%` : 'N/A', admitToDeposit: funnel.admitted > 0 ? `${((funnel.deposited / funnel.admitted) * 100).toFixed(1)}%` : 'N/A', depositToEnroll: funnel.deposited > 0 ? `${((funnel.enrolled / funnel.deposited) * 100).toFixed(1)}%` : 'N/A', overallYield: funnel.admitted > 0 ? `${((funnel.enrolled / funnel.admitted) * 100).toFixed(1)}%` : 'N/A', }, }; break; } case 'compare_enrollment_years': { const years = args?.years as number[]; const entryTerm = (args?.entry_term as string) || 'Fall'; const studentsByYear = new Map<number, StudentRecord[]>(); for (const year of years) { const students = await client.getEnrolledStudents(year, entryTerm); studentsByYear.set(year, students); } const comparison = generateYearOverYearComparison(studentsByYear); result = comparison; break; } case 'get_demographics_report': { const entryYear = args?.entry_year as number; const entryTerm = (args?.entry_term as string) || 'Fall'; const students = await client.getEnrolledStudents(entryYear, entryTerm); const summary = generateDemographicsSummary(students, entryYear, entryTerm); const report = formatDemographicsReport(summary); result = { report, rawData: summary }; break; } case 'get_demographics_by_category': { const entryYear = args?.entry_year as number; const category = args?.category as string; const entryTerm = (args?.entry_term as string) || 'Fall'; const students = await client.getEnrolledStudents(entryYear, entryTerm); const enrolled = students.filter( (s) => s.enrollmentStatus === 'enrolled' || s.enrollmentStatus === 'deposited' ); let breakdown: Record<string, number> = {}; const total = enrolled.length; switch (category) { case 'gender': breakdown = aggregateStudents(enrolled, (s) => s.demographics.gender || 'Not Reported'); break; case 'ethnicity': breakdown = aggregateStudents(enrolled, (s) => s.demographics.ethnicity || 'Not Reported'); break; case 'state': breakdown = aggregateStudents(enrolled, (s) => s.geographicInfo.state || 'Not Reported'); break; case 'country': breakdown = aggregateStudents(enrolled, (s) => s.geographicInfo.country || 'Not Reported'); break; case 'admit_type': breakdown = aggregateStudents(enrolled, (s) => s.admitType); break; case 'major': breakdown = aggregateStudents(enrolled, (s) => s.academicInfo.intendedMajor || 'Undeclared'); break; case 'college': breakdown = aggregateStudents(enrolled, (s) => s.academicInfo.intendedCollege || 'Not Specified'); break; case 'first_generation': breakdown = { 'First Generation': enrolled.filter((s) => s.demographics.firstGeneration === true).length, 'Not First Generation': enrolled.filter((s) => s.demographics.firstGeneration === false).length, 'Not Reported': enrolled.filter((s) => s.demographics.firstGeneration === undefined).length, }; break; case 'international': breakdown = { International: enrolled.filter((s) => s.geographicInfo.isInternational).length, Domestic: enrolled.filter((s) => !s.geographicInfo.isInternational).length, }; break; } // Calculate percentages const withPercentages = Object.entries(breakdown) .map(([key, count]) => ({ category: key, count, percentage: total > 0 ? Number(((count / total) * 100).toFixed(1)) : 0, })) .sort((a, b) => b.count - a.count); result = { entryYear, entryTerm, category, total, breakdown: withPercentages, }; break; } case 'search_students': { const entryYear = args?.entry_year as number | undefined; const entryTerm = args?.entry_term as string | undefined; const state = args?.state as string | undefined; const country = args?.country as string | undefined; const major = args?.major as string | undefined; const firstGeneration = args?.first_generation as boolean | undefined; const international = args?.international as boolean | undefined; const limit = (args?.limit as number) || 100; // Get students - if no year specified, get current year const year = entryYear || new Date().getFullYear(); let students = await client.getEnrolledStudents(year, entryTerm); // Apply filters if (state) { students = students.filter( (s) => s.geographicInfo.state?.toLowerCase() === state.toLowerCase() ); } if (country) { students = students.filter( (s) => s.geographicInfo.country?.toLowerCase() === country.toLowerCase() ); } if (major) { students = students.filter((s) => s.academicInfo.intendedMajor?.toLowerCase().includes(major.toLowerCase()) ); } if (firstGeneration !== undefined) { students = students.filter((s) => s.demographics.firstGeneration === firstGeneration); } if (international !== undefined) { students = students.filter((s) => s.geographicInfo.isInternational === international); } // Limit results and anonymize const limitedStudents = students.slice(0, limit).map((s) => ({ id: s.id, entryYear: s.entryYear, entryTerm: s.entryTerm, admitType: s.admitType, enrollmentStatus: s.enrollmentStatus, demographics: { gender: s.demographics.gender, ethnicity: s.demographics.ethnicity, firstGeneration: s.demographics.firstGeneration, }, academicInfo: { intendedMajor: s.academicInfo.intendedMajor, intendedCollege: s.academicInfo.intendedCollege, }, geographicInfo: { state: s.geographicInfo.state, country: s.geographicInfo.country, isInternational: s.geographicInfo.isInternational, }, })); result = { totalMatching: students.length, returned: limitedStudents.length, students: limitedStudents, }; break; } default: throw new Error(`Unknown tool: ${name}`); } const content: TextContent[] = [ { type: 'text', text: JSON.stringify(result, null, 2), }, ]; return { content }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); // Helper function for aggregation function aggregateStudents( students: StudentRecord[], keyFn: (s: StudentRecord) => string ): Record<string, number> { const result: Record<string, number> = {}; for (const student of students) { const key = keyFn(student); result[key] = (result[key] || 0) + 1; } return result; } // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Slate MCP Server started'); } main().catch((error) => { console.error('Failed to start server:', error); process.exit(1); });

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/pwfarmer87/Slate-MCP'

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