@yeepay/awesome-components-mcp
Version:
MCP server providing access to awesome-components documentation and integration guides with dual-mode operation: direct fetch and GitLab MCP instruction generation
362 lines (301 loc) • 10.4 kB
text/typescript
/**
* Get Components Guide MCP Tool
*
* This tool retrieves comprehensive integration guides for specific components
* with intelligent path resolution and security validation.
*/
import { z } from 'zod';
import { config } from '../config.js';
import { GitLabError } from '../services/gitlabClient.js';
/**
* Schema for get components guide tool parameters
*/
export const getComponentsGuideSchema = z.object({
path: z.string().min(1).describe('Path to the component\'s llms.txt file (relative or absolute URL)')
});
export type GetComponentsGuideParams = z.infer<typeof getComponentsGuideSchema>;
/**
* Validates if a path is safe and doesn't contain dangerous characters
*/
function isValidPath(path: string): boolean {
// Check for empty path
if (!path || path.trim().length === 0) {
return false;
}
// Check for dangerous characters and patterns
const dangerousPatterns = [
/\.\./, // Directory traversal
/<script/i, // Script injection
/<\/script/i, // Script injection
/javascript:/i, // JavaScript protocol
/data:/i, // Data protocol
/vbscript:/i, // VBScript protocol
/[<>]/, // HTML/XML tags
/[\x00-\x1f]/, // Control characters
];
return !dangerousPatterns.some(pattern => pattern.test(path));
}
/**
* Determines if a URL is external (not matching the internal GitLab host)
*/
function isExternalUrl(path: string, internalHost?: string): boolean {
// If path doesn't start with http/https, it's a relative path (internal)
if (!path.startsWith('http://') && !path.startsWith('https://')) {
return false;
}
// If no internal host is configured, treat all absolute URLs as external
if (!internalHost) {
return true;
}
try {
const url = new URL(path);
return url.hostname !== internalHost;
} catch (e) {
// Malformed URL, treat as external to be safe
return true;
}
}
/**
* Extracts the title from content (first markdown header or filename)
*/
function extractTitle(content: string, path: string): string {
// Try to find the first markdown header
const headerMatch = content.match(/^#\s+(.+)$/m);
if (headerMatch) {
return headerMatch[1].trim();
}
// Fall back to the filename or path
const filename = path.split('/').pop() || path;
return filename.replace(/\.(txt|md|markdown)$/i, '');
}
/**
* Formats the guide content with metadata
*/
function formatGuideContent(content: string, path: string): string {
const title = extractTitle(content, path);
const contentLength = content.length;
return `# Component Guide: ${title}
**Source Path:** \`${path}\`
**Content Length:** ${contentLength} characters
**Retrieved:** ${new Date().toISOString()}
---
${content}`;
}
/**
* Fetches content from external URL using HTTP GET
*/
async function fetchExternalUrl(url: string): Promise<string> {
try {
console.log(`Fetching external URL: ${url}`);
const response = await fetch(url, {
headers: {
'User-Agent': 'awesome-components-mcp/1.0.0'
}
});
if (!response.ok) {
throw new GitLabError(
`Failed to fetch external URL (status: ${response.status})`,
response.status,
url
);
}
const content = await response.text();
console.log(`Successfully fetched ${content.length} characters from external URL`);
return content;
} catch (error) {
if (error instanceof GitLabError) {
throw error;
}
throw new GitLabError(
`Network error while fetching external URL: ${error instanceof Error ? error.message : 'Unknown error'}`,
undefined,
url
);
}
}
/**
* Interface for gitlab-mcp instruction
*/
interface GitLabMcpInstruction {
action_type: 'mcp_call';
tool_name: string;
parameters: {
project_id: string;
file_path: string;
ref: string;
};
}
/**
* Validates GitLab MCP configuration
*/
function validateGitLabMcpConfig(): { isValid: boolean; error?: string } {
if (!config.internalGuidesProjectId) {
return {
isValid: false,
error: 'GITLAB_PROJECT_ID is required for GitLab MCP instruction mode'
};
}
return { isValid: true };
}
/**
* Get Components Guide MCP Tool Implementation
*/
export async function getComponentsGuideTool(
params: GetComponentsGuideParams
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
try {
const { path } = params;
console.log(`Getting component guide for path: ${path}`);
// Validate the path for security
if (!isValidPath(path)) {
const errorResponse = `# Error: Invalid Component Guide Path
The provided path contains invalid characters or format: \`${path}\`
**Security Note:** Paths cannot contain:
- Directory traversal patterns (\`../\`)
- Script injection attempts (\`<script>\`)
- Control characters or HTML tags
- Protocol handlers (javascript:, data:, etc.)
**Valid Examples:**
- \`yeepay/dynamicpassword/llms.txt\`
- \`common/authentication/guide.md\`
- \`https://external-gitlab.com/project/-/raw/main/component/llms.txt\`
**Suggestions:**
- Use relative paths from the repository root
- Use absolute GitLab raw file URLs for external repositories
- Ensure the path points to a valid documentation file`;
return {
content: [{
type: 'text',
text: errorResponse
}],
isError: true
};
}
// Check if this is an external URL
if (isExternalUrl(path, config.internalGitlabHost)) {
console.log(`External URL detected: ${path}`);
// For external URLs, use direct HTTP GET request
const content = await fetchExternalUrl(path);
const formattedContent = formatGuideContent(content, path);
return {
content: [{
type: 'text',
text: formattedContent
}]
};
}
// For relative paths and internal URLs, generate gitlab-mcp instruction
console.log(`Relative/internal path detected, generating gitlab-mcp instruction: ${path}`);
// Validate GitLab MCP configuration
const configValidation = validateGitLabMcpConfig();
if (!configValidation.isValid) {
throw new Error(`GitLab MCP Configuration Error: ${configValidation.error}`);
}
// Derive file path for GitLab
let filePathForGitlab = path;
// If it's an internal URL, extract the path
if (path.startsWith('http://') || path.startsWith('https://')) {
try {
const url = new URL(path);
filePathForGitlab = url.pathname;
if (filePathForGitlab.startsWith('/')) {
filePathForGitlab = filePathForGitlab.substring(1);
}
} catch (e) {
// If URL parsing fails, use the original path
}
}
// Generate JSON instruction for LLM to call gitlab-mcp
// project_id should be URL encoded only if it's a string (not a numeric ID)
// and only if it's not already encoded
let projectId = config.internalGuidesProjectId;
if (typeof projectId === 'string' && isNaN(Number(projectId))) {
// Check if it's already URL encoded by looking for % characters
if (!projectId.includes('%')) {
projectId = encodeURIComponent(projectId);
}
}
const instruction: GitLabMcpInstruction = {
action_type: 'mcp_call',
tool_name: 'get_file_contents',
parameters: {
project_id: projectId!,
file_path: filePathForGitlab,
ref: 'main'
}
};
const instructionOutput = `# Component Guide - GitLab MCP Instruction
To retrieve the component guide for \`${path}\`, please call the gitlab-mcp server with the following instruction:
\`\`\`json
${JSON.stringify(instruction, null, 2)}
\`\`\`
**Instructions for LLM:**
1. Use the above JSON instruction to call the gitlab-mcp server
2. Call the \`get_file_contents\` tool with the provided parameters
3. The response will contain the component guide content
**Configuration:**
- Project ID: \`${config.internalGuidesProjectId}\`
- File Path: \`${filePathForGitlab}\`
- Branch: \`main\`
- Original Path: \`${path}\``;
return {
content: [{
type: 'text',
text: instructionOutput
}]
};
} catch (error) {
console.error('Get components guide failed:', error);
let errorMessage = 'Unknown error occurred';
let statusInfo = '';
let suggestions = `**Suggestions:**
- Check if the component path is correct
- Verify the file exists in the repository
- Ensure you have proper access permissions
- Try using the components_discovery tool to find available components`;
if (error instanceof GitLabError) {
errorMessage = `GitLab Error: ${error.message}`;
if (error.statusCode) {
statusInfo = `\n**Status Code:** ${error.statusCode}`;
// Provide specific suggestions based on status code
if (error.statusCode === 404) {
suggestions = `**Suggestions:**
- Verify the component path is correct: \`${params.path}\`
- Check if the file exists in the repository
- Use the components_discovery tool to see available components
- Ensure the file extension is correct (e.g., llms.txt, guide.md)`;
} else if (error.statusCode === 403) {
suggestions = `**Suggestions:**
- This repository may be private and require authentication
- Check your GitLab Personal Access Token configuration
- Verify your token has read access to this repository
- Contact the repository administrator for access`;
}
}
if (error.url) {
statusInfo += `\n**URL:** ${error.url}`;
}
} else if (error instanceof Error) {
errorMessage = `Error: ${error.message}`;
} else {
errorMessage = `Error: ${String(error)}`;
}
const errorTitle = error instanceof GitLabError && error.statusCode === 404
? 'Component Guide Not Found'
: 'Failed to Retrieve Component Guide';
const errorResponse = `# Error: ${errorTitle}
${errorMessage}${statusInfo}
${suggestions}
**Configuration:**
- GitLab Base URL: ${config.urls.gitlabBase}
- Authentication: ${config.auth.gitlabToken ? 'Configured' : 'Not configured'}
- Requested Path: \`${params.path}\``;
return {
content: [{
type: 'text',
text: errorResponse
}],
isError: true
};
}
}