Skip to main content
Glama

Federated Directory MCP

by FedWiebe
index.js16.5 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; const API_BASE_URL = "https://api.federated.directory/v2"; class FederatedDirectoryServer { constructor() { this.apiToken = process.env.FEDERATED_DIRECTORY_API_TOKEN; this.enableLogging = process.env.FEDERATED_DIRECTORY_DEBUG === "true"; if (!this.apiToken) { console.error( "ERROR: FEDERATED_DIRECTORY_API_TOKEN environment variable is required", ); process.exit(1); } if (this.enableLogging) { console.error("DEBUG: Logging enabled"); console.error( "DEBUG: API Token (masked):", this.maskToken(this.apiToken), ); } this.server = new Server( { name: "federated-directory-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); this.setupToolHandlers(); this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } maskToken(token) { if (!token || token.length < 8) return "***"; return token.substring(0, 4) + "..." + token.substring(token.length - 4); } logRequest(url, headers) { if (!this.enableLogging) return; console.error("\n========== API REQUEST =========="); console.error("URL:", url); console.error("Method: GET"); console.error("Headers:", { ...headers, Authorization: `Bearer ${this.maskToken(this.apiToken)}`, }); console.error("=================================\n"); } logResponse(status, statusText, data) { if (!this.enableLogging) return; console.error("\n========== API RESPONSE =========="); console.error("Status:", status, statusText); console.error("Response Data:", JSON.stringify(data, null, 2)); console.error("==================================\n"); } logError(error) { if (!this.enableLogging) return; console.error("\n========== ERROR =========="); console.error("Error:", error.message); console.error("Stack:", error.stack); console.error("===========================\n"); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "search_contacts_by_name", description: "Search for contacts in the Federated Directory by name. Searches across displayName, givenName, and familyName fields.", inputSchema: { type: "object", properties: { name: { type: "string", description: "The name to search for (partial matches supported)", }, limit: { type: "number", description: "Maximum number of results to return (default: 10, max: 100)", default: 10, }, attributes: { type: "string", description: "Comma-separated list of attributes to return (e.g., 'userName,displayName,emails'). If not specified, returns all attributes.", }, }, required: ["name"], }, }, { name: "search_contacts_by_email", description: "Search for contacts in the Federated Directory by email address. Supports partial email matching.", inputSchema: { type: "object", properties: { email: { type: "string", description: "The email address to search for (partial matches supported)", }, limit: { type: "number", description: "Maximum number of results to return (default: 10, max: 100)", default: 10, }, attributes: { type: "string", description: "Comma-separated list of attributes to return (e.g., 'userName,displayName,emails'). If not specified, returns all attributes.", }, }, required: ["email"], }, }, { name: "get_contact_by_id", description: "Get detailed information for a specific contact by their ID.", inputSchema: { type: "object", properties: { id: { type: "string", description: "The unique ID of the contact", }, attributes: { type: "string", description: "Comma-separated list of attributes to return (e.g., 'userName,displayName,emails,photos'). If not specified, returns all attributes.", }, }, required: ["id"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "search_contacts_by_name") { return await this.searchContactsByName(request.params.arguments); } else if (request.params.name === "search_contacts_by_email") { return await this.searchContactsByEmail(request.params.arguments); } else if (request.params.name === "get_contact_by_id") { return await this.getContactById(request.params.arguments); } throw new Error(`Unknown tool: ${request.params.name}`); }); } async searchContactsByName(args) { try { const { name, limit = 10, attributes } = args; if (!name || typeof name !== "string" || name.trim().length === 0) { throw new Error( "Name parameter is required and must be a non-empty string", ); } const searchLimit = Math.min(Math.max(1, limit), 100); // SCIM filter for name search (searches displayName, givenName, and familyName) const filter = `displayName co "${name.trim()}" or name.givenName co "${name.trim()}" or name.familyName co "${name.trim()}"`; let url = `${API_BASE_URL}/Users?filter=${encodeURIComponent(filter)}&count=${searchLimit}`; if ( attributes && typeof attributes === "string" && attributes.trim().length > 0 ) { url += `&attributes=${encodeURIComponent(attributes.trim())}`; } const headers = { Accept: "application/scim+json", Authorization: `Bearer ${this.apiToken}`, "User-Agent": "MCP-FederatedDirectory/1.0", }; this.logRequest(url, headers); const response = await fetch(url, { method: "GET", headers: headers, }); const responseText = await response.text(); let data; try { data = JSON.parse(responseText); } catch (parseError) { console.error( "Failed to parse JSON response. Raw response:", responseText.substring(0, 500), ); throw new Error( `Invalid JSON response from API. Status: ${response.status}. Response preview: ${responseText.substring(0, 200)}`, ); } this.logResponse(response.status, response.statusText, data); if (!response.ok) { if (response.status === 401) { throw new Error( "Authentication failed. Please check your API token.", ); } throw new Error( `API request failed: ${response.status} ${response.statusText}`, ); } // SCIM response format const resources = data.Resources || []; return { content: [ { type: "text", text: JSON.stringify( { success: true, count: resources.length, totalResults: data.totalResults, query: { name, limit: searchLimit }, results: resources.map((user) => ({ id: user.id, username: user.userName, displayName: user.displayName, firstName: user.name?.givenName, lastName: user.name?.familyName, email: user.emails?.[0]?.value, emails: user.emails, profileUrl: user.profileUrl, avatarUrl: user.photos?.[0]?.value, active: user.active, })), }, null, 2, ), }, ], }; } catch (error) { this.logError(error); return { content: [ { type: "text", text: JSON.stringify( { success: false, error: error.message, }, null, 2, ), }, ], isError: true, }; } } async getContactById(args) { try { const { id, attributes } = args; if (!id || typeof id !== "string" || id.trim().length === 0) { throw new Error( "ID parameter is required and must be a non-empty string", ); } let url = `${API_BASE_URL}/Users/${encodeURIComponent(id.trim())}`; if ( attributes && typeof attributes === "string" && attributes.trim().length > 0 ) { url += `?attributes=${encodeURIComponent(attributes.trim())}`; } const headers = { Accept: "application/scim+json", Authorization: `Bearer ${this.apiToken}`, "User-Agent": "MCP-FederatedDirectory/1.0", }; this.logRequest(url, headers); const response = await fetch(url, { method: "GET", headers: headers, }); const responseText = await response.text(); let data; try { data = JSON.parse(responseText); } catch (parseError) { console.error( "Failed to parse JSON response. Raw response:", responseText.substring(0, 500), ); throw new Error( `Invalid JSON response from API. Status: ${response.status}. Response preview: ${responseText.substring(0, 200)}`, ); } this.logResponse(response.status, response.statusText, data); if (!response.ok) { if (response.status === 401) { throw new Error( "Authentication failed. Please check your API token.", ); } if (response.status === 404) { throw new Error(`Contact with ID '${id}' not found.`); } throw new Error( `API request failed: ${response.status} ${response.statusText}`, ); } // Return full SCIM user object return { content: [ { type: "text", text: JSON.stringify( { success: true, query: { id, attributes }, result: { id: data.id, username: data.userName, displayName: data.displayName, name: data.name, emails: data.emails, phoneNumbers: data.phoneNumbers, photos: data.photos, profileUrl: data.profileUrl, active: data.active, addresses: data.addresses, locale: data.locale, timezone: data.timezone, // Include any other fields that were returned ...Object.keys(data).reduce((acc, key) => { if ( ![ "id", "userName", "displayName", "name", "emails", "phoneNumbers", "photos", "profileUrl", "active", "addresses", "locale", "timezone", ].includes(key) ) { acc[key] = data[key]; } return acc; }, {}), }, }, null, 2, ), }, ], }; } catch (error) { this.logError(error); return { content: [ { type: "text", text: JSON.stringify( { success: false, error: error.message, }, null, 2, ), }, ], isError: true, }; } } async searchContactsByEmail(args) { try { const { email, limit = 10, attributes } = args; if (!email || typeof email !== "string" || email.trim().length === 0) { throw new Error( "Email parameter is required and must be a non-empty string", ); } const searchLimit = Math.min(Math.max(1, limit), 100); // SCIM filter for email search const filter = `emails.value co "${email.trim()}"`; let url = `${API_BASE_URL}/Users?filter=${encodeURIComponent(filter)}&count=${searchLimit}`; if ( attributes && typeof attributes === "string" && attributes.trim().length > 0 ) { url += `&attributes=${encodeURIComponent(attributes.trim())}`; } const headers = { Accept: "application/scim+json", Authorization: `Bearer ${this.apiToken}`, "User-Agent": "MCP-FederatedDirectory/1.0", }; this.logRequest(url, headers); const response = await fetch(url, { method: "GET", headers: headers, }); const responseText = await response.text(); let data; try { data = JSON.parse(responseText); } catch (parseError) { console.error( "Failed to parse JSON response. Raw response:", responseText.substring(0, 500), ); throw new Error( `Invalid JSON response from API. Status: ${response.status}. Response preview: ${responseText.substring(0, 200)}`, ); } this.logResponse(response.status, response.statusText, data); if (!response.ok) { if (response.status === 401) { throw new Error( "Authentication failed. Please check your API token.", ); } throw new Error( `API request failed: ${response.status} ${response.statusText}`, ); } // SCIM response format const resources = data.Resources || []; return { content: [ { type: "text", text: JSON.stringify( { success: true, count: resources.length, totalResults: data.totalResults, query: { email, limit: searchLimit }, results: resources.map((user) => ({ id: user.id, username: user.userName, displayName: user.displayName, firstName: user.name?.givenName, lastName: user.name?.familyName, email: user.emails?.[0]?.value, emails: user.emails, profileUrl: user.profileUrl, avatarUrl: user.photos?.[0]?.value, active: user.active, })), }, null, 2, ), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify( { success: false, error: error.message, }, null, 2, ), }, ], isError: true, }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Federated Directory MCP server running on stdio"); } } const server = new FederatedDirectoryServer(); server.run();

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/FedWiebe/MCP'

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