Skip to main content
Glama

Education Data MCP Server

by ckz
index.ts14.7 kB
#!/usr/bin/env node /** * Education Data MCP Server * * This MCP server provides access to the Urban Institute's Education Data API, * similar to the functionality provided by the educationdata R package. * * It allows: * - Retrieving detailed education data via the get_education_data tool * - Retrieving aggregated education data via the get_education_data_summary tool * - Browsing available endpoints via resources */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; // Base URL for the Education Data API const API_BASE_URL = "https://educationdata.urban.org/api/v1"; // Define types for API responses interface ApiEndpoint { level: string; source: string; topic: string; subtopic?: string[]; mainFilters: string[]; yearsAvailable: string; } // In-memory cache of available endpoints let availableEndpoints: ApiEndpoint[] | null = null; /** * Create an MCP server with capabilities for resources and tools */ const server = new Server( { name: "edu-data-mcp-server", version: "0.1.0", }, { capabilities: { resources: {}, tools: {}, }, } ); /** * Fetch available endpoints from the API */ async function fetchAvailableEndpoints(): Promise<ApiEndpoint[]> { if (availableEndpoints) { return availableEndpoints; } try { // This is a simplified approach - in a real implementation, we would parse // the actual API documentation or use an endpoint that returns available endpoints const endpoints: ApiEndpoint[] = [ { level: "schools", source: "ccd", topic: "enrollment", subtopic: ["race", "sex", "race, sex"], mainFilters: ["year", "grade"], yearsAvailable: "1986–2022" }, { level: "schools", source: "ccd", topic: "directory", mainFilters: ["year"], yearsAvailable: "1986–2022" }, { level: "school-districts", source: "ccd", topic: "enrollment", subtopic: ["race", "sex", "race, sex"], mainFilters: ["year", "grade"], yearsAvailable: "1986–2022" }, { level: "college-university", source: "ipeds", topic: "directory", mainFilters: ["year"], yearsAvailable: "1980, 1984–2022" }, // Add more endpoints as needed ]; availableEndpoints = endpoints; return endpoints; } catch (error) { console.error("Error fetching available endpoints:", error); throw new McpError( ErrorCode.InternalError, "Failed to fetch available endpoints" ); } } /** * Handler for listing available endpoints as resources */ server.setRequestHandler(ListResourcesRequestSchema, async () => { const endpoints = await fetchAvailableEndpoints(); return { resources: endpoints.map(endpoint => { const subtopicStr = endpoint.subtopic ? ` (subtopics: ${endpoint.subtopic.join(", ")})` : ""; const uri = `edu-data://endpoints/${endpoint.level}/${endpoint.source}/${endpoint.topic}`; return { uri, mimeType: "application/json", name: `${endpoint.level}/${endpoint.source}/${endpoint.topic}`, description: `Education data endpoint${subtopicStr}. Years: ${endpoint.yearsAvailable}. Main filters: ${endpoint.mainFilters.join(", ")}` }; }) }; }); /** * Handler for resource templates */ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { return { resourceTemplates: [ { uriTemplate: "edu-data://endpoints/{level}/{source}/{topic}", name: "Education Data Endpoint", mimeType: "application/json", description: "Information about a specific education data endpoint" } ] }; }); /** * Handler for reading endpoint details */ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const match = request.params.uri.match(/^edu-data:\/\/endpoints\/([^\/]+)\/([^\/]+)\/([^\/]+)$/); if (!match) { throw new McpError( ErrorCode.InvalidRequest, `Invalid URI format: ${request.params.uri}` ); } const [, level, source, topic] = match; const endpoints = await fetchAvailableEndpoints(); const endpoint = endpoints.find(e => e.level === level && e.source === source && e.topic === topic ); if (!endpoint) { throw new McpError( ErrorCode.InvalidRequest, `Endpoint not found: ${level}/${source}/${topic}` ); } return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(endpoint, null, 2) } ] }; }); /** * Handler for listing available tools */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_education_data", description: "Retrieve education data from the Urban Institute's Education Data API", inputSchema: { type: "object", properties: { level: { type: "string", description: "API data level to query (e.g., 'schools', 'school-districts', 'college-university')" }, source: { type: "string", description: "API data source to query (e.g., 'ccd', 'ipeds', 'crdc')" }, topic: { type: "string", description: "API data topic to query (e.g., 'enrollment', 'directory')" }, subtopic: { type: "array", items: { type: "string" }, description: "Optional list of grouping parameters (e.g., ['race', 'sex'])" }, filters: { type: "object", description: "Optional query filters (e.g., {year: 2008, grade: [9,10,11,12]})" }, add_labels: { type: "boolean", description: "Add variable labels when applicable (default: false)" }, limit: { type: "number", description: "Limit the number of results (default: 100)" } }, required: ["level", "source", "topic"] } }, { name: "get_education_data_summary", description: "Retrieve aggregated education data from the Urban Institute's Education Data API", inputSchema: { type: "object", properties: { level: { type: "string", description: "API data level to query" }, source: { type: "string", description: "API data source to query" }, topic: { type: "string", description: "API data topic to query" }, subtopic: { type: "string", description: "Optional additional parameters (only applicable to certain endpoints)" }, stat: { type: "string", description: "Summary statistic to calculate (e.g., 'sum', 'avg', 'count', 'median')" }, var: { type: "string", description: "Variable to be summarized" }, by: { type: "array", items: { type: "string" }, description: "Variables to group results by" }, filters: { type: "object", description: "Optional query filters" } }, required: ["level", "source", "topic", "stat", "var", "by"] } } ] }; }); /** * Handler for tool calls */ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "get_education_data": { const { level, source, topic, subtopic, filters, add_labels, limit = 100 } = request.params.arguments || {}; if (!level || !source || !topic) { throw new McpError( ErrorCode.InvalidParams, "Missing required parameters: level, source, and topic are required" ); } try { // Construct the API URL let url = `${API_BASE_URL}/${level}/${source}/${topic}`; // Add subtopics if provided if (subtopic && Array.isArray(subtopic) && subtopic.length > 0) { url += `/${subtopic.join("/")}`; } // Add query parameters const queryParams = new URLSearchParams(); queryParams.append("limit", String(limit)); if (add_labels) { queryParams.append("add_labels", "true"); } // Add filters if (filters && typeof filters === "object") { Object.entries(filters).forEach(([key, value]) => { if (Array.isArray(value)) { queryParams.append(key, value.join(",")); } else { queryParams.append(key, String(value)); } }); } // Add mode=R to match the R package behavior queryParams.append("mode", "R"); // Make the API request const response = await axios.get(`${url}?${queryParams.toString()}`); // Return the results return { content: [ { type: "text", text: JSON.stringify(response.data.results || response.data, null, 2) } ] }; } catch (error) { if (axios.isAxiosError(error)) { const statusCode = error.response?.status; const message = error.response?.data?.message || error.message; if (statusCode === 404) { throw new McpError( ErrorCode.InvalidRequest, `Endpoint not found: ${level}/${source}/${topic}` ); } else if (statusCode === 400) { throw new McpError( ErrorCode.InvalidParams, `API error: ${message}` ); } else if (statusCode === 413) { throw new McpError( ErrorCode.InvalidParams, "Your requested query returned too many records. Consider limiting the scope of your query." ); } throw new McpError( ErrorCode.InternalError, `API error (${statusCode}): ${message}` ); } throw new McpError( ErrorCode.InternalError, `Error: ${error instanceof Error ? error.message : String(error)}` ); } } case "get_education_data_summary": { const args = request.params.arguments || {}; const level = String(args.level || ''); const source = String(args.source || ''); const topic = String(args.topic || ''); const subtopic = args.subtopic ? String(args.subtopic) : undefined; const stat = String(args.stat || ''); const variable = String(args.var || ''); const by = args.by || []; const filters = args.filters || {}; if (!level || !source || !topic || !stat || !variable || !by) { throw new McpError( ErrorCode.InvalidParams, "Missing required parameters: level, source, topic, stat, var, and by are required" ); } try { // Construct the API URL let url = `${API_BASE_URL}/${level}/${source}/${topic}`; // Add subtopic if provided if (subtopic) { url += `/${subtopic}`; } // Add summaries endpoint url += "/summaries"; // Add query parameters const queryParams = new URLSearchParams(); queryParams.append("stat", stat); queryParams.append("var", variable); if (Array.isArray(by)) { queryParams.append("by", by.join(",")); } else { queryParams.append("by", String(by)); } // Add filters if (filters && typeof filters === "object") { Object.entries(filters).forEach(([key, value]) => { if (Array.isArray(value)) { queryParams.append(key, value.join(",")); } else { queryParams.append(key, String(value)); } }); } // Add mode=R to match the R package behavior queryParams.append("mode", "R"); // Make the API request const response = await axios.get(`${url}?${queryParams.toString()}`); // Return the results return { content: [ { type: "text", text: JSON.stringify(response.data.results || response.data, null, 2) } ] }; } catch (error) { if (axios.isAxiosError(error)) { const statusCode = error.response?.status; const message = error.response?.data?.message || error.message; if (statusCode === 404) { throw new McpError( ErrorCode.InvalidRequest, `Summary endpoint not found: ${level}/${source}/${topic}${subtopic ? `/${subtopic}` : ''}/summaries` ); } else if (statusCode === 400) { throw new McpError( ErrorCode.InvalidParams, `API error: ${message}` ); } throw new McpError( ErrorCode.InternalError, `API error (${statusCode}): ${message}` ); } throw new McpError( ErrorCode.InternalError, `Error: ${error instanceof Error ? error.message : String(error)}` ); } } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); /** * Start the server using stdio transport */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Education Data MCP server running on stdio"); } main().catch((error) => { console.error("Server error:", 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/ckz/edu_data_mcp_server'

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