UNPKG

@access-mcp/affinity-groups

Version:

MCP server for ACCESS-CI Affinity Groups API

251 lines (250 loc) 9.84 kB
import { BaseAccessServer, handleApiError, sanitizeGroupId, } from "@access-mcp/shared"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const { version } = require("../package.json"); export class AffinityGroupsServer extends BaseAccessServer { constructor() { super("access-mcp-affinity-groups", version); } getTools() { return [ { name: "search_affinity_groups", description: "Search ACCESS-CI affinity groups. Returns {total, items}.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Group ID (omit to list all)", }, include: { type: "string", enum: ["events", "kb", "all"], description: "What to include with each group result. events = upcoming events for each group; kb = knowledge base articles (also activates query filtering on KB content); all = both events and KB articles", }, query: { type: "string", description: "Filter groups by name and description. When used with include=kb or include=all, also searches knowledge base article content within each group.", }, limit: { type: "number", description: "Max results (default: 20)", default: 20, }, }, }, }, ]; } getResources() { return [ { uri: "accessci://affinity-groups", name: "ACCESS-CI Affinity Groups", description: "Information about ACCESS-CI affinity groups, their events, and knowledge base resources", mimeType: "application/json", }, ]; } async handleToolCall(request) { const { name, arguments: args = {} } = request.params; try { switch (name) { case "search_affinity_groups": return await this.searchAffinityGroupsRouter(args); default: return this.errorResponse(`Unknown tool: ${name}`); } } catch (error) { return this.errorResponse(handleApiError(error)); } } async searchAffinityGroupsRouter(args) { const { id, include, query, limit } = args; if (!id && !query) { return await this.listAffinityGroups(); } // Text search: filter groups by query matching name, description, category if (!id && query) { const allResult = await this.listAffinityGroups(); const content = allResult.content[0]; if (content.type !== "text") return allResult; const allData = JSON.parse(content.text); const terms = query.toLowerCase().split(/\s+/).filter(Boolean); const filtered = (allData.items || []).filter((group) => { const searchable = [ group.name || "", group.description || "", group.category || "", ].join(" ").toLowerCase(); return terms.some((term) => searchable.includes(term)); }); const limited = limit ? filtered.slice(0, limit) : filtered; return { content: [ { type: "text", text: JSON.stringify({ total: limited.length, query, items: limited }, null, 2), }, ], }; } // At this point id is guaranteed to be set (early returns above handle !id cases) const groupId = id; if (include === "all") { const [groupInfo, events, kb] = await Promise.all([ this.getAffinityGroup(groupId), this.getAffinityGroupEvents(groupId), this.getAffinityGroupKB(groupId), ]); const groupContent = groupInfo.content[0]; const eventsContent = events.content[0]; const kbContent = kb.content[0]; const groupText = groupContent.type === "text" ? groupContent.text : ""; const eventsText = eventsContent.type === "text" ? eventsContent.text : ""; const kbText = kbContent.type === "text" ? kbContent.text : ""; return { content: [ { type: "text", text: JSON.stringify({ group: JSON.parse(groupText), events: JSON.parse(eventsText), knowledge_base: JSON.parse(kbText), }), }, ], }; } if (include === "events") { return await this.getAffinityGroupEvents(groupId); } if (include === "kb") { return await this.getAffinityGroupKB(groupId); } return await this.getAffinityGroup(groupId); } async handleResourceRead(request) { const { uri } = request.params; if (uri === "accessci://affinity-groups") { const result = await this.listAffinityGroups(); const content = result.content[0]; const text = content.type === "text" ? content.text : ""; return { contents: [ { uri, mimeType: "application/json", text, }, ], }; } throw new Error(`Unknown resource: ${uri}`); } async getAffinityGroup(groupId) { const sanitizedId = sanitizeGroupId(groupId); const response = await this.httpClient.get(`/1.1/affinity_groups/${sanitizedId}`); // Check if response data exists if (!response.data) { return { content: [ { type: "text", text: JSON.stringify({ total: 0, items: [], }, null, 2), }, ], }; } // Clean up the response data - API returns array with single group const rawGroups = Array.isArray(response.data) ? response.data : [response.data]; const cleanedGroups = rawGroups.map((group) => ({ id: group?.field_group_id || group?.nid, name: group?.title, // Changed from title to name for consistency description: group?.description ?.replace(/<[^>]*>/g, "") .replace(/\\n/g, "\n") .trim(), coordinator: group?.coordinator_name, category: group?.field_affinity_group_category, slack_link: group?.slack_link, support_url: group?.url, ask_ci_forum: group?.field_ask_ci_locale, })); return { content: [ { type: "text", text: JSON.stringify({ total: cleanedGroups.length, items: cleanedGroups, }, null, 2), }, ], }; } async getAffinityGroupEvents(groupId) { const sanitizedId = sanitizeGroupId(groupId); const response = await this.httpClient.get(`/1.1/events/ag/${sanitizedId}`); const events = Array.isArray(response.data) ? response.data : []; return { content: [ { type: "text", text: JSON.stringify({ total: events.length, items: events, }, null, 2), }, ], }; } async getAffinityGroupKB(groupId) { const sanitizedId = sanitizeGroupId(groupId); const response = await this.httpClient.get(`/1.0/kb/${sanitizedId}`); const kbItems = Array.isArray(response.data) ? response.data : []; return { content: [ { type: "text", text: JSON.stringify({ total: kbItems.length, items: kbItems, }, null, 2), }, ], }; } async listAffinityGroups() { const response = await this.httpClient.get("/1.1/affinity_groups/all"); const groups = Array.isArray(response.data) ? response.data.map((group) => ({ id: group.field_group_id || group.nid, name: group.title, // Changed from title to name for consistency description: group.description ?.replace(/<[^>]*>/g, "") .replace(/\\n/g, "\n") .trim(), coordinator: group.coordinator_name, category: group.field_affinity_group_category, })) : []; return { content: [ { type: "text", text: JSON.stringify({ total: groups.length, items: groups, }, null, 2), }, ], }; } }