@access-mcp/shared
Version:
Shared utilities for ACCESS-CI MCP servers
144 lines (143 loc) • 4.48 kB
JavaScript
export function sanitizeGroupId(groupId) {
if (!groupId) {
throw new Error("groupId parameter is required and cannot be null or undefined");
}
return groupId.replace(/[^a-zA-Z0-9.-]/g, "");
}
export function formatApiUrl(version, endpoint) {
return `/${version}/${endpoint}`;
}
export function handleApiError(error) {
const axiosError = error;
if (axiosError.response?.data?.message) {
return axiosError.response.data.message;
}
if (axiosError.response?.status) {
return `API error: ${axiosError.response.status} ${axiosError.response.statusText}`;
}
if (error instanceof Error) {
return error.message;
}
return "Unknown API error";
}
/**
* Add helpful next steps to a successful response
*/
export function addNextSteps(data, nextSteps) {
return {
data,
next_steps: nextSteps,
};
}
/**
* Create an LLM-friendly error response with suggestions
*/
export function createLLMError(error, errorType, options = {}) {
return {
error,
error_type: errorType,
...options,
};
}
/**
* Add discovery suggestions when returning empty results
*/
export function addDiscoverySuggestions(data, discoverySteps) {
if (data.length === 0) {
return {
data,
count: 0,
next_steps: discoverySteps,
suggestions: [
"No results found. Try the suggested next steps to discover available options.",
],
};
}
return {
data,
count: data.length,
};
}
/**
* Common next step templates for cross-server consistency
*/
export const CommonNextSteps = {
discoverResources: {
action: "discover_resources",
description: "Find available compute resources to filter by",
tool: "search_resources",
parameters: { include_resource_ids: true },
},
narrowResults: (currentCount, suggestedFilters) => ({
action: "narrow_results",
description: `Currently showing ${currentCount} results. Add filters to narrow down: ${suggestedFilters.join(", ")}`,
}),
exploreRelated: (relatedTool, description) => ({
action: "explore_related",
description,
tool: relatedTool,
}),
refineSearch: (suggestions) => ({
action: "refine_search",
description: `Try these refinements: ${suggestions.join(", ")}`,
}),
};
/**
* Resolve a human-readable name to a resource ID.
*
* @param input - The input string (name or ID)
* @param searchFn - A function that searches for resources by name and returns matches
* @returns ResolveResult with either the resolved ID or an error message
*
* @example
* ```ts
* const result = await resolveResourceId("Anvil", async (query) => {
* const resources = await searchResources({ query });
* return resources.map(r => ({ id: r.id, name: r.name }));
* });
*
* if (result.success) {
* console.log(result.id); // "anvil.purdue.access-ci.org"
* } else {
* console.log(result.error); // "Multiple resources match..."
* }
* ```
*/
export async function resolveResourceId(input, searchFn) {
// If it already looks like a full resource ID (contains dots), return as-is
if (input.includes(".")) {
return { success: true, id: input };
}
// Search for the resource by name
const items = await searchFn(input);
if (items.length === 0) {
return {
success: false,
error: `No resource found matching '${input}'`,
suggestion: "Use the search tool to find valid resource names.",
};
}
// Find exact name match first (case-insensitive)
const inputLower = input.toLowerCase();
const exactMatch = items.find((item) => item.name?.toLowerCase() === inputLower);
if (exactMatch && exactMatch.id) {
return { success: true, id: exactMatch.id };
}
// Multiple partial matches - ask user to be more specific
if (items.length > 1) {
const names = items.map((i) => i.name).join(", ");
return {
success: false,
error: `Multiple resources match '${input}': ${names}`,
suggestion: "Please specify the exact resource name.",
};
}
// Single partial match - use it
if (items[0].id) {
return { success: true, id: items[0].id };
}
return {
success: false,
error: `Could not resolve resource '${input}'`,
};
}