@access-mcp/affinity-groups
Version:
MCP server for ACCESS-CI Affinity Groups API
251 lines (250 loc) • 9.84 kB
JavaScript
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),
},
],
};
}
}