Skip to main content
Glama
DeanWard

HAL (HTTP API Layer)

index.ts57.8 kB
#!/usr/bin/env node import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // @ts-ignore import SwaggerParser from "@apidevtools/swagger-parser"; import { readFileSync, existsSync } from "fs"; // @ts-ignore import * as YAML from "yaml"; // Environment configuration const SWAGGER_FILE_PATH = process.env.HAL_SWAGGER_FILE?.trim(); const API_BASE_URL = process.env.HAL_API_BASE_URL?.trim(); // Secrets management interface SecretInfo { value: string; namespace?: string; allowedUrls?: string[]; templateKey: string; // Store the original template key for redaction } interface SecretsStore { [key: string]: SecretInfo; } let secrets: SecretsStore = {}; // Function to redact secrets from text function redactSecretsFromText(text: string): string { if (!text || typeof text !== 'string') { return text; } let redactedText = text; // Replace all secret values with [REDACTED] for (const [templateKey, secretInfo] of Object.entries(secrets)) { if (secretInfo.value && secretInfo.value.length > 0) { // Use a global regex to replace all instances of the secret value const regex = new RegExp(escapeRegExp(secretInfo.value), 'g'); redactedText = redactedText.replace(regex, '[REDACTED]'); } } return redactedText; } // Helper function to escape special regex characters function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Parse namespace and key from environment variable name function parseSecretKey(envKey: string): { namespace: string | undefined, key: string, templateKey: string } { const withoutPrefix = envKey.replace('HAL_SECRET_', ''); const firstUnderscoreIndex = withoutPrefix.indexOf('_'); if (firstUnderscoreIndex === -1) { // No namespace, just a key const key = withoutPrefix.toLowerCase(); return { namespace: undefined, key, templateKey: key }; } const namespace = withoutPrefix.substring(0, firstUnderscoreIndex); const key = withoutPrefix.substring(firstUnderscoreIndex + 1); // Convert namespace: AZURE-STORAGE -> azure.storage const namespaceTemplate = namespace.toLowerCase().replace(/-/g, '.'); // Convert key: ACCESS_KEY -> access_key const keyTemplate = key.toLowerCase(); const templateKey = namespace ? `${namespaceTemplate}.${keyTemplate}` : keyTemplate; return { namespace, key: keyTemplate, templateKey }; } // Load URL restrictions from environment variables function loadUrlRestrictions(): Record<string, string[]> { const restrictions: Record<string, string[]> = {}; for (const [key, value] of Object.entries(process.env)) { if (key.startsWith('HAL_ALLOW_') && value) { const namespace = key.replace('HAL_ALLOW_', ''); const urls = value.split(',').map(url => url.trim()).filter(url => url.length > 0); restrictions[namespace] = urls; } } return restrictions; } // Check if a URL matches any of the allowed patterns function isUrlAllowed(url: string, allowedPatterns: string[]): boolean { for (const pattern of allowedPatterns) { if (matchesUrlPattern(url, pattern)) { return true; } } return false; } // Match URL against a pattern (supports * wildcards) function matchesUrlPattern(url: string, pattern: string): boolean { // Convert pattern to regex // Escape special regex characters except * const escapedPattern = pattern .replace(/[.+?^${}()|[\]\\]/g, '\\$&') .replace(/\*/g, '.*'); const regex = new RegExp(`^${escapedPattern}$`, 'i'); return regex.test(url); } // Load global URL whitelist/blacklist from environment variables function loadGlobalUrlFilters(): { whitelist: string[] | null; blacklist: string[] | null } { const whitelistEnv = process.env.HAL_WHITELIST_URLS; const blacklistEnv = process.env.HAL_BLACKLIST_URLS; const whitelist = whitelistEnv ? whitelistEnv.split(',').map(url => url.trim()).filter(url => url.length > 0) : null; const blacklist = blacklistEnv ? blacklistEnv.split(',').map(url => url.trim()).filter(url => url.length > 0) : null; return { whitelist, blacklist }; } // Check if a URL is allowed based on global whitelist/blacklist rules function isUrlAllowedGlobal(url: string): { allowed: boolean; reason?: string } { const { whitelist, blacklist } = loadGlobalUrlFilters(); // If both whitelist and blacklist are provided, prioritize whitelist if (whitelist && blacklist) { console.error('Warning: Both HAL_WHITELIST_URLS and HAL_BLACKLIST_URLS are set. Whitelist takes precedence.'); } // Check whitelist first (if present, only whitelisted URLs are allowed) if (whitelist) { for (const pattern of whitelist) { if (matchesUrlPattern(url, pattern)) { return { allowed: true }; } } return { allowed: false, reason: `URL '${url}' is not in the whitelist. Allowed patterns: ${whitelist.join(', ')}` }; } // Check blacklist (if present, blacklisted URLs are denied) if (blacklist) { for (const pattern of blacklist) { if (matchesUrlPattern(url, pattern)) { return { allowed: false, reason: `URL '${url}' is blacklisted. Blocked patterns: ${blacklist.join(', ')}` }; } } return { allowed: true }; } // If neither whitelist nor blacklist is set, allow all URLs return { allowed: true }; } // Load secrets from environment variables with HAL_SECRET_ prefix function loadSecrets(): void { secrets = {}; const urlRestrictions = loadUrlRestrictions(); for (const [key, value] of Object.entries(process.env)) { if (key.startsWith('HAL_SECRET_') && value) { const { namespace, key: secretKey, templateKey } = parseSecretKey(key); const secretInfo: SecretInfo = { value, namespace, allowedUrls: namespace ? urlRestrictions[namespace] : undefined, templateKey }; secrets[templateKey] = secretInfo; } } if (Object.keys(secrets).length > 0) { console.error(`Loaded ${Object.keys(secrets).length} secrets from environment variables`); // Log namespace restrictions (without secret values) const restrictedCount = Object.values(secrets).filter(s => s.allowedUrls).length; if (restrictedCount > 0) { console.error(`${restrictedCount} secrets have URL restrictions`); } } } // Template substitution function with URL validation function substituteSecrets(template: string, targetUrl?: string): string { if (!template || typeof template !== 'string') { return template; } return template.replace(/\{secrets\.([^}]+)\}/g, (match, secretKey) => { const key = secretKey.toLowerCase(); const secretInfo = secrets[key]; if (!secretInfo) { console.error(`Warning: Secret '${secretKey}' not found. Available secrets: ${Object.keys(secrets).join(', ')}`); return match; // Return original placeholder if secret not found } // Check URL restrictions if they exist if (secretInfo.allowedUrls && targetUrl) { if (!isUrlAllowed(targetUrl, secretInfo.allowedUrls)) { const namespace = secretInfo.namespace || 'unknown'; console.error(`Error: Secret '${secretKey}' (namespace: ${namespace}) is not allowed for URL '${targetUrl}'. Allowed patterns: ${secretInfo.allowedUrls.join(', ')}`); throw new Error(`Secret '${secretKey}' is not allowed for URL '${targetUrl}'`); } } return secretInfo.value; }); } // Helper function to recursively substitute secrets in objects with URL validation function substituteSecretsInObject(obj: any, targetUrl?: string): any { if (typeof obj === 'string') { return substituteSecrets(obj, targetUrl); } else if (Array.isArray(obj)) { return obj.map(item => substituteSecretsInObject(item, targetUrl)); } else if (obj && typeof obj === 'object') { const result: any = {}; for (const [key, value] of Object.entries(obj)) { result[key] = substituteSecretsInObject(value, targetUrl); } return result; } return obj; } // Types for OpenAPI/Swagger interface OpenAPISpec { openapi?: string; swagger?: string; info: { title: string; version: string; description?: string; }; servers?: Array<{ url: string; description?: string; }>; paths: Record<string, Record<string, any>>; components?: { schemas?: Record<string, any>; securitySchemes?: Record<string, any>; }; } // Create the HAL MCP server const server = new McpServer({ name: "hal-mcp", version: "1.0.0" }); // Helper function to convert OpenAPI parameter to Zod schema function createZodSchemaFromParameter(param: any): z.ZodTypeAny { const { type, format, enum: enumValues, required } = param; let schema: z.ZodTypeAny; switch (type) { case 'string': if (format === 'email') { schema = z.string().email(); } else if (format === 'uri' || format === 'url') { schema = z.string().url(); } else if (enumValues) { schema = z.enum(enumValues); } else { schema = z.string(); } break; case 'number': case 'integer': schema = z.number(); break; case 'boolean': schema = z.boolean(); break; case 'array': schema = z.array(z.string()); // Simplified array handling break; default: schema = z.string(); } return required ? schema : schema.optional(); } // Helper function to create Zod schema from OpenAPI parameters function createInputSchema(operation: any): Record<string, z.ZodTypeAny> { const schema: Record<string, z.ZodTypeAny> = {}; // Handle parameters (query, path, header) if (operation.parameters) { for (const param of operation.parameters) { const zodSchema = createZodSchemaFromParameter({ ...param.schema, required: param.required }); schema[param.name] = zodSchema; } } // Handle request body if (operation.requestBody) { const content = operation.requestBody.content; if (content) { if (content['application/json']) { schema.body = z.string().optional(); } else if (content['application/x-www-form-urlencoded']) { schema.body = z.string().optional(); } else { schema.body = z.string().optional(); } } } // Always include headers as optional schema.headers = z.record(z.string()).optional(); return schema; } // Helper function to make HTTP request async function makeHttpRequest( method: string, url: string, options: { headers?: Record<string, string>; body?: string; queryParams?: Record<string, any>; } = {} ) { try { const { headers = {}, body, queryParams = {} } = options; // First, substitute secrets in URL to get the final URL for validation // We need to do this in two passes to handle URL restrictions properly const processedUrl = substituteSecrets(url, url); // Now substitute secrets in headers, body, and query parameters using the processed URL const processedHeaders = substituteSecretsInObject(headers, processedUrl); const processedBody = body ? substituteSecrets(body, processedUrl) : body; const processedQueryParams = substituteSecretsInObject(queryParams, processedUrl); // Build URL with query parameters const urlObj = new URL(processedUrl); Object.entries(processedQueryParams).forEach(([key, value]) => { if (value !== undefined && value !== null) { urlObj.searchParams.set(key, String(value)); } }); const finalUrl = urlObj.toString(); // Check global URL whitelist/blacklist const urlCheck = isUrlAllowedGlobal(finalUrl); if (!urlCheck.allowed) { throw new Error(urlCheck.reason || 'URL is not allowed'); } const defaultHeaders = { 'User-Agent': 'HAL-MCP/1.0.0', ...processedHeaders }; // Add Content-Type for methods that typically send data if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && processedBody && !('Content-Type' in processedHeaders)) { (defaultHeaders as any)['Content-Type'] = 'application/json'; } const response = await fetch(finalUrl, { method: method.toUpperCase(), headers: defaultHeaders, body: processedBody }); const contentType = response.headers.get('content-type') || 'text/plain'; let content: string; // HEAD requests don't have a body by design if (method.toUpperCase() === 'HEAD') { content = '(No body - HEAD request)'; } else { try { if (contentType.includes('application/json')) { const text = await response.text(); if (text.trim()) { content = JSON.stringify(JSON.parse(text), null, 2); } else { content = '(Empty response)'; } } else { content = await response.text(); } } catch (parseError) { // If JSON parsing fails, try to get text try { content = await response.text(); } catch (textError) { content = '(Unable to parse response)'; } } } // Redact secrets from response headers and content before returning const redactedHeaders = Array.from(response.headers.entries()) .map(([key, value]) => `${key}: ${redactSecretsFromText(value)}`) .join('\n'); const redactedContent = redactSecretsFromText(content); return { content: [{ type: "text" as const, text: `Status: ${response.status} ${response.statusText}\n\nHeaders:\n${redactedHeaders}\n\nBody:\n${redactedContent}` }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const redactedErrorMessage = redactSecretsFromText(errorMessage); return { content: [{ type: "text" as const, text: `Error making ${method.toUpperCase()} request: ${redactedErrorMessage}` }], isError: true }; } } // Function to register tools from OpenAPI spec async function registerSwaggerTools(spec: OpenAPISpec) { const baseUrl = API_BASE_URL || (spec.servers?.[0]?.url) || ''; for (const [path, pathItem] of Object.entries(spec.paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) { continue; } const operationId = operation.operationId || `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`; const toolName = `swagger_${operationId}`; const inputSchema = createInputSchema(operation); server.registerTool( toolName, { title: operation.summary || `${method.toUpperCase()} ${path}`, description: operation.description || `Execute ${method.toUpperCase()} request to ${path}`, inputSchema: inputSchema }, async (params: any) => { const { headers, body, ...queryParams } = params; // Replace path parameters let resolvedPath = path; for (const [key, value] of Object.entries(queryParams)) { const pathParam = `{${key}}`; if (resolvedPath.includes(pathParam)) { resolvedPath = resolvedPath.replace(pathParam, String(value)); delete queryParams[key]; } } const fullUrl = baseUrl.endsWith('/') || resolvedPath.startsWith('/') ? `${baseUrl}${resolvedPath}` : `${baseUrl}/${resolvedPath}`; return makeHttpRequest(method, fullUrl, { headers, body, queryParams }); } ); } } } // Helper function to fetch spec content from URL async function fetchSpecContent(url: string, filePath: string): Promise<{ content: string; isYaml: boolean } | null> { console.log(`Loading Swagger specification from URL: ${url}`); const response = await fetch(url); if (!response.ok) { console.error(`Failed to fetch Swagger spec from URL: ${response.status} ${response.statusText}`); return null; } const content = await response.text(); const contentType = response.headers.get('content-type') || ''; const isYaml = contentType.includes('yaml') || contentType.includes('yml') || filePath.endsWith('.yaml') || filePath.endsWith('.yml'); return { content, isYaml }; } // Function to load and parse Swagger/OpenAPI file async function loadSwaggerSpec(): Promise<OpenAPISpec | null> { if (!SWAGGER_FILE_PATH) { return null; } try { let specContent: string; let isYaml = false; // Determine spec source and content if (SWAGGER_FILE_PATH.startsWith('http://') || SWAGGER_FILE_PATH.startsWith('https://')) { // Direct URL const result = await fetchSpecContent(SWAGGER_FILE_PATH, SWAGGER_FILE_PATH); if (!result) return null; specContent = result.content; isYaml = result.isYaml; } else if (SWAGGER_FILE_PATH.startsWith('/') && API_BASE_URL) { // Relative path combined with API_BASE_URL const result = await fetchSpecContent(`${API_BASE_URL}${SWAGGER_FILE_PATH}`, SWAGGER_FILE_PATH); if (!result) return null; specContent = result.content; isYaml = result.isYaml; } else { // Local file path if (!existsSync(SWAGGER_FILE_PATH)) { console.error(`Swagger file not found: ${SWAGGER_FILE_PATH}`); return null; } specContent = readFileSync(SWAGGER_FILE_PATH, 'utf-8'); isYaml = SWAGGER_FILE_PATH.endsWith('.yaml') || SWAGGER_FILE_PATH.endsWith('.yml'); } let spec: OpenAPISpec; if (isYaml) { spec = YAML.parse(specContent); } else { spec = JSON.parse(specContent); } // Validate the spec using swagger-parser try { const parsedSpec = await (SwaggerParser as any).validate(spec); return parsedSpec as OpenAPISpec; } catch (parseError) { console.error('Swagger validation failed, using raw spec:', parseError); return spec; } } catch (error) { console.error('Error loading Swagger spec:', error); return null; } } // Register original HTTP tools (always available) server.registerTool( "http-get", { title: "HTTP GET Request", description: "Make an HTTP GET request to a specified URL. Supports secret substitution using {secrets.key} syntax where 'key' corresponds to HAL_SECRET_KEY environment variables.", inputSchema: { url: z.string().url(), headers: z.record(z.string()).optional() } }, async ({ url, headers = {} }: { url: string; headers?: Record<string, string> }) => { return makeHttpRequest('GET', url, { headers }); } ); server.registerTool( "http-post", { title: "HTTP POST Request", description: "Make an HTTP POST request to a specified URL with optional body and headers. Supports secret substitution using {secrets.key} syntax in URL, headers, and body where 'key' corresponds to HAL_SECRET_KEY environment variables.", inputSchema: { url: z.string().url(), body: z.string().optional(), headers: z.record(z.string()).optional(), contentType: z.string().default('application/json') } }, async ({ url, body, headers = {}, contentType }: { url: string; body?: string; headers?: Record<string, string>; contentType: string }) => { const requestHeaders = { 'Content-Type': contentType, ...headers }; return makeHttpRequest('POST', url, { headers: requestHeaders, body }); } ); server.registerTool( "http-put", { title: "HTTP PUT Request", description: "Make an HTTP PUT request to a specified URL with optional body and headers. Supports secret substitution using {secrets.key} syntax in URL, headers, and body where 'key' corresponds to HAL_SECRET_KEY environment variables.", inputSchema: { url: z.string().url(), body: z.string().optional(), headers: z.record(z.string()).optional(), contentType: z.string().default('application/json') } }, async ({ url, body, headers = {}, contentType }: { url: string; body?: string; headers?: Record<string, string>; contentType: string }) => { const requestHeaders = { 'Content-Type': contentType, ...headers }; return makeHttpRequest('PUT', url, { headers: requestHeaders, body }); } ); server.registerTool( "http-patch", { title: "HTTP PATCH Request", description: "Make an HTTP PATCH request to a specified URL with optional body and headers. Supports secret substitution using {secrets.key} syntax in URL, headers, and body where 'key' corresponds to HAL_SECRET_KEY environment variables.", inputSchema: { url: z.string().url(), body: z.string().optional(), headers: z.record(z.string()).optional(), contentType: z.string().default('application/json') } }, async ({ url, body, headers = {}, contentType }: { url: string; body?: string; headers?: Record<string, string>; contentType: string }) => { const requestHeaders = { 'Content-Type': contentType, ...headers }; return makeHttpRequest('PATCH', url, { headers: requestHeaders, body }); } ); server.registerTool( "http-delete", { title: "HTTP DELETE Request", description: "Make an HTTP DELETE request to a specified URL with optional headers. Supports secret substitution using {secrets.key} syntax in URL and headers where 'key' corresponds to HAL_SECRET_KEY environment variables.", inputSchema: { url: z.string().url(), headers: z.record(z.string()).optional() } }, async ({ url, headers = {} }: { url: string; headers?: Record<string, string> }) => { return makeHttpRequest('DELETE', url, { headers }); } ); server.registerTool( "http-head", { title: "HTTP HEAD Request", description: "Make an HTTP HEAD request to a specified URL with optional headers (returns only headers, no body). Supports secret substitution using {secrets.key} syntax in URL and headers where 'key' corresponds to HAL_SECRET_KEY environment variables.", inputSchema: { url: z.string().url(), headers: z.record(z.string()).optional() } }, async ({ url, headers = {} }: { url: string; headers?: Record<string, string> }) => { return makeHttpRequest('HEAD', url, { headers }); } ); server.registerTool( "http-options", { title: "HTTP OPTIONS Request", description: "Make an HTTP OPTIONS request to a specified URL to check available methods and headers. Supports secret substitution using {secrets.key} syntax in URL and headers where 'key' corresponds to HAL_SECRET_KEY environment variables.", inputSchema: { url: z.string().url(), headers: z.record(z.string()).optional() } }, async ({ url, headers = {} }: { url: string; headers?: Record<string, string> }) => { return makeHttpRequest('OPTIONS', url, { headers }); } ); // Register secrets listing tool server.registerTool( "list-secrets", { title: "List Available Secrets", description: "Get a list of available secret keys that can be used with {secrets.key} syntax. Only shows the key names, never the actual secret values.", inputSchema: {} }, async () => { try { const secretKeys = Object.keys(secrets); if (secretKeys.length === 0) { return { content: [{ type: "text" as const, text: "No secrets are currently configured. To add secrets, set environment variables with the HAL_SECRET_ prefix.\n\nExample:\n HAL_SECRET_API_KEY=your_api_key\n HAL_SECRET_TOKEN=your_token\n\nThen use them in requests like: {secrets.api_key} or {secrets.token}\n\nFor namespaced secrets with URL restrictions:\n HAL_SECRET_MICROSOFT_API_KEY=your_api_key\n HAL_ALLOW_MICROSOFT=\"https://azure.microsoft.com/*\"\n Usage: {secrets.microsoft.api_key}" }] }; } let response = `Available secrets (${secretKeys.length} total):\n\n`; // Group secrets by namespace const namespacedSecrets: Record<string, string[]> = {}; const unrestrictedSecrets: string[] = []; for (const [key, secretInfo] of Object.entries(secrets)) { if (secretInfo.namespace) { const namespaceTemplate = secretInfo.namespace.toLowerCase().replace(/-/g, '.'); if (!namespacedSecrets[namespaceTemplate]) { namespacedSecrets[namespaceTemplate] = []; } namespacedSecrets[namespaceTemplate].push(key); } else { unrestrictedSecrets.push(key); } } // Show unrestricted secrets first if (unrestrictedSecrets.length > 0) { response += "**Unrestricted Secrets** (can be used with any URL):\n"; unrestrictedSecrets.forEach((key, index) => { response += `${index + 1}. {secrets.${key}}\n`; }); response += "\n"; } // Show namespaced secrets with their restrictions for (const [namespace, keys] of Object.entries(namespacedSecrets)) { const firstKey = keys[0]; const secretInfo = secrets[firstKey]; response += `**Namespace: ${namespace}**\n`; if (secretInfo.allowedUrls && secretInfo.allowedUrls.length > 0) { response += `Restricted to URLs: ${secretInfo.allowedUrls.join(', ')}\n`; } else { response += "No URL restrictions (can be used with any URL)\n"; } response += "Secrets:\n"; keys.forEach((key, index) => { response += `${index + 1}. {secrets.${key}}\n`; }); response += "\n"; } response += "**Usage examples:**\n"; const exampleKey = secretKeys[0] || 'token'; response += `- URL: "https://api.example.com/data?token={secrets.${exampleKey}}"\n`; response += `- Header: {"Authorization": "Bearer {secrets.${exampleKey}}"}\n`; response += `- Body: {"username": "{secrets.${secretKeys.find(k => k.includes('user')) || 'username'}}"}\n\n`; response += "**Security Notes:**\n"; response += "- Only the key names are shown here. The actual secret values are never exposed to the AI.\n"; response += "- Secrets are substituted securely at request time.\n"; const restrictedCount = Object.values(secrets).filter(s => s.allowedUrls).length; if (restrictedCount > 0) { response += `- ${restrictedCount} secrets have URL restrictions for enhanced security.\n`; response += "- If a secret is restricted, it will only work with URLs matching its allowed patterns.\n"; } return { content: [{ type: "text" as const, text: response }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: "text" as const, text: `Error listing secrets: ${redactSecretsFromText(errorMessage)}` }], isError: true }; } } ); // In-memory documentation store for search interface DocSection { id: string; title: string; content: string; type: 'endpoint' | 'schema' | 'component' | 'example'; tags: string[]; } let documentationStore: DocSection[] = []; // Simple search function function searchDocumentation(query: string, limit: number = 5): DocSection[] { if (!query.trim()) { return documentationStore.slice(0, limit); } const searchTerms = query.toLowerCase().split(/\s+/); const results = documentationStore .map(doc => { let score = 0; const searchableText = `${doc.title} ${doc.content} ${doc.tags.join(' ')}`.toLowerCase(); // Exact phrase match gets highest score if (searchableText.includes(query.toLowerCase())) { score += 100; } // Title matches get high score searchTerms.forEach(term => { if (doc.title.toLowerCase().includes(term)) { score += 50; } if (doc.tags.some(tag => tag.toLowerCase().includes(term))) { score += 30; } // Count occurrences in content const regex = new RegExp(term, 'gi'); const matches = searchableText.match(regex); if (matches) { score += matches.length * 10; } }); return { doc, score }; }) .filter(result => result.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map(result => result.doc); return results; } // Function to register Swagger documentation tools async function registerSwaggerDocTools(spec: OpenAPISpec) { // Build documentation store documentationStore = []; // Add API overview documentationStore.push({ id: 'api-overview', title: `${spec.info.title} Overview`, content: `${spec.info.title} v${spec.info.version}\n\n${spec.info.description || ''}\n\nBase URL: ${API_BASE_URL || spec.servers?.[0]?.url || 'Not specified'}`, type: 'endpoint', tags: ['api', 'overview', 'info', spec.info.title.toLowerCase()] }); // Add endpoints for (const [path, pathItem] of Object.entries(spec.paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) { continue; } let content = `${method.toUpperCase()} ${path}\n\n`; if (operation.summary) content += `${operation.summary}\n\n`; if (operation.description) content += `${operation.description}\n\n`; // Parameters if (operation.parameters && operation.parameters.length > 0) { content += `Parameters:\n`; for (const param of operation.parameters) { content += `- ${param.name} (${param.in}) - ${param.description || 'No description'}\n`; if (param.required) content += ` Required: Yes\n`; if (param.schema) { content += ` Type: ${param.schema.type || 'object'}\n`; if (param.schema.example) content += ` Example: ${param.schema.example}\n`; } } content += `\n`; } // Request body if (operation.requestBody) { content += `Request Body: ${operation.requestBody.required ? 'Required' : 'Optional'}\n`; if (operation.requestBody.content) { for (const [contentType] of Object.entries(operation.requestBody.content)) { content += `Content-Type: ${contentType}\n`; } } content += `\n`; } const tags = [ method, path.split('/').filter(p => p && !p.startsWith('{')).join(' '), operation.summary?.toLowerCase() || '', ...(operation.tags || []) ].filter(Boolean); documentationStore.push({ id: `${method}-${path.replace(/[^a-zA-Z0-9]/g, '_')}`, title: `${method.toUpperCase()} ${path}`, content, type: 'endpoint', tags }); } } // Add schemas if (spec.components?.schemas) { for (const [schemaName, schema] of Object.entries(spec.components.schemas)) { let content = `${schemaName}\n\n`; if ((schema as any).description) { content += `${(schema as any).description}\n\n`; } content += `Type: ${(schema as any).type || 'object'}\n\n`; if ((schema as any).properties) { content += `Properties:\n`; for (const [propName, prop] of Object.entries((schema as any).properties)) { const isRequired = (schema as any).required?.includes(propName); content += `- ${propName}${isRequired ? ' (required)' : ''} - ${(prop as any).description || 'No description'}\n`; content += ` Type: ${(prop as any).type || 'object'}\n`; if ((prop as any).example !== undefined) { content += ` Example: ${JSON.stringify((prop as any).example)}\n`; } if ((prop as any).enum) { content += ` Allowed values: ${(prop as any).enum.map((v: any) => v).join(', ')}\n`; } } content += `\n`; } if ((schema as any).example) { content += `Example:\n${JSON.stringify((schema as any).example, null, 2)}\n\n`; } documentationStore.push({ id: `schema-${schemaName}`, title: `${schemaName} Schema`, content, type: 'schema', tags: ['schema', 'model', 'data', schemaName.toLowerCase()] }); } } // Add presentation components (specific to your API) if (spec.components?.schemas?.Slide?.properties?.content?.properties?.elements?.items?.properties?.type?.enum) { const slideSchema = spec.components?.schemas?.Slide as any; const elementTypes = slideSchema?.properties?.content?.properties?.elements?.items?.properties?.type?.enum || []; const componentDescriptions = { 'h1': 'Main heading - largest text size', 'h2': 'Secondary heading', 'h3': 'Tertiary heading', 'h4': 'Quaternary heading', 'p': 'Paragraph text', 'ul': 'Unordered (bulleted) list', 'ol': 'Ordered (numbered) list', 'li': 'List item', 'image': 'Image element with src, alt, width properties', 'button': 'Interactive button element', 'company-logo': 'Company logo display', 'stats-section': 'Statistics display section with items array', 'opportunity-box': 'Highlighted opportunity or feature box', 'alert-success': 'Success message alert', 'alert-warning': 'Warning message alert', 'alert-info': 'Information alert', 'risk-warning': 'Risk warning alert', 'key-points': 'Key points section', 'two-column': 'Two-column layout', 'three-column': 'Three-column layout', 'competitive-advantage': 'Competitive advantage showcase', 'info-card': 'Information card display', 'ai-query-demo': 'AI query demonstration', 'product-showcase': 'Product showcase with title, features, and certifications' }; for (const elementType of elementTypes) { let content = `${elementType}\n\n`; if (componentDescriptions[elementType as keyof typeof componentDescriptions]) { content += `${componentDescriptions[elementType as keyof typeof componentDescriptions]}\n\n`; } // Add property info for complex components if (elementType === 'stats-section') { content += `Properties:\n- items: Array of statistics strings\n- style: CSS styling\n\n`; } else if (elementType === 'product-showcase') { content += `Properties:\n- title: Product title\n- features: Array of feature strings\n- certifications: Array of certification strings\n- style: CSS styling\n\n`; } else if (elementType === 'image') { content += `Properties:\n- src: Image URL\n- alt: Alt text\n- width: Width specification\n- style: CSS styling\n\n`; } else if (['ul', 'ol'].includes(elementType)) { content += `Properties:\n- text: HTML list items as text (e.g., "<li>Item 1</li><li>Item 2</li>")\n- style: CSS styling\n\n`; } else { content += `Properties:\n- text: Text content\n- style: CSS styling\n\n`; } documentationStore.push({ id: `component-${elementType}`, title: `${elementType} Component`, content, type: 'component', tags: ['component', 'element', 'presentation', elementType] }); } // Add complete example if (spec.components?.schemas?.PresentationData?.example) { documentationStore.push({ id: 'complete-example', title: 'Complete Presentation Example', content: `Complete example of presentation data structure:\n\n${JSON.stringify((spec.components.schemas.PresentationData as any).example, null, 2)}`, type: 'example', tags: ['example', 'presentation', 'sample', 'template'] }); } } // Register documentation search tool server.registerTool( "docs_search", { title: "Search API Documentation", description: "Search through the API documentation, schemas, components, and examples. Returns relevant matches based on your query.", inputSchema: { query: z.string().describe("Search query (e.g., 'presentation components', 'generate endpoint', 'stats section')"), limit: z.number().optional().default(5).describe("Maximum number of results to return (default: 5)") } }, async ({ query, limit = 5 }: { query: string; limit?: number }) => { try { const results = searchDocumentation(query, limit); if (results.length === 0) { return { content: [{ type: "text" as const, text: `No documentation found for "${query}". Try broader search terms like "components", "schemas", "endpoints", or "examples".` }] }; } let response = `Found ${results.length} result(s) for "${query}":\n\n`; results.forEach((doc, index) => { response += `## ${index + 1}. ${doc.title}\n`; response += `Type: ${doc.type}\n\n`; response += `${doc.content}\n`; response += `---\n\n`; }); return { content: [{ type: "text" as const, text: response }] }; } catch (error) { return { content: [{ type: "text" as const, text: `Error searching documentation: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); // Register specific documentation lookup tools server.registerTool( "docs_components", { title: "List All Presentation Components", description: "Get a complete list of all available presentation components and their descriptions", inputSchema: {} }, async () => { try { const components = documentationStore.filter(doc => doc.type === 'component'); if (components.length === 0) { return { content: [{ type: "text" as const, text: "No presentation components found in the documentation." }] }; } let response = `Available Presentation Components (${components.length} total):\n\n`; components.forEach(comp => { response += `## ${comp.title}\n${comp.content}\n---\n\n`; }); return { content: [{ type: "text" as const, text: response }] }; } catch (error) { return { content: [{ type: "text" as const, text: `Error retrieving components: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); server.registerTool( "docs_schemas", { title: "List All Data Schemas", description: "Get all available data schemas and their structure", inputSchema: {} }, async () => { try { const schemas = documentationStore.filter(doc => doc.type === 'schema'); let response = `Available Data Schemas (${schemas.length} total):\n\n`; schemas.forEach(schema => { response += `## ${schema.title}\n${schema.content}\n---\n\n`; }); return { content: [{ type: "text" as const, text: response }] }; } catch (error) { return { content: [{ type: "text" as const, text: `Error retrieving schemas: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); server.registerTool( "docs_endpoints", { title: "List All API Endpoints", description: "Get all available API endpoints and their documentation", inputSchema: {} }, async () => { try { const endpoints = documentationStore.filter(doc => doc.type === 'endpoint'); let response = `Available API Endpoints (${endpoints.length} total):\n\n`; endpoints.forEach(endpoint => { response += `## ${endpoint.title}\n${endpoint.content}\n---\n\n`; }); return { content: [{ type: "text" as const, text: response }] }; } catch (error) { return { content: [{ type: "text" as const, text: `Error retrieving endpoints: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); server.registerTool( "docs_example", { title: "Get Complete Presentation Example", description: "Get a complete example of how to structure presentation data", inputSchema: {} }, async () => { try { const examples = documentationStore.filter(doc => doc.type === 'example'); if (examples.length === 0) { return { content: [{ type: "text" as const, text: "No examples found in the documentation." }] }; } let response = `Available Examples:\n\n`; examples.forEach(example => { response += `## ${example.title}\n${example.content}\n\n`; }); return { content: [{ type: "text" as const, text: response }] }; } catch (error) { return { content: [{ type: "text" as const, text: `Error retrieving examples: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); } // Function to register Swagger resources async function registerSwaggerResources(spec: OpenAPISpec) { // Register main API documentation resource server.registerResource( "swagger-api-docs", "docs://swagger/api", { title: `${spec.info.title} - API Documentation`, description: `Complete API documentation for ${spec.info.title}`, mimeType: "text/markdown" }, async (uri: { href: string }) => { let content = `# ${spec.info.title} v${spec.info.version}\n\n`; if (spec.info.description) { content += `${spec.info.description}\n\n`; } content += `## Base URL\n${API_BASE_URL || spec.servers?.[0]?.url || 'Not specified'}\n\n`; content += `## Available Endpoints\n\n`; for (const [path, pathItem] of Object.entries(spec.paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) { continue; } content += `### ${method.toUpperCase()} ${path}\n`; if (operation.summary) content += `**${operation.summary}**\n\n`; if (operation.description) content += `${operation.description}\n\n`; // Parameters if (operation.parameters && operation.parameters.length > 0) { content += `**Parameters:**\n`; for (const param of operation.parameters) { content += `- \`${param.name}\` (${param.in}) - ${param.description || 'No description'}\n`; if (param.required) content += ` - Required: Yes\n`; if (param.schema) { content += ` - Type: ${param.schema.type || 'object'}\n`; if (param.schema.example) content += ` - Example: \`${param.schema.example}\`\n`; } } content += `\n`; } // Request body if (operation.requestBody) { content += `**Request Body:**\n`; content += `- Required: ${operation.requestBody.required ? 'Yes' : 'No'}\n`; if (operation.requestBody.content) { for (const [contentType, contentSchema] of Object.entries(operation.requestBody.content)) { content += `- Content-Type: \`${contentType}\`\n`; } } content += `\n`; } content += `---\n\n`; } } return { contents: [{ uri: uri.href, text: content }] }; } ); // Register components/schemas resource if (spec.components?.schemas) { server.registerResource( "swagger-schemas", "docs://swagger/schemas", { title: `${spec.info.title} - Data Schemas`, description: "Data models and schemas used by the API", mimeType: "text/markdown" }, async (uri: { href: string }) => { let content = `# ${spec.info.title} - Data Schemas\n\n`; for (const [schemaName, schema] of Object.entries(spec.components?.schemas || {})) { content += `## ${schemaName}\n\n`; if ((schema as any).description) { content += `${(schema as any).description}\n\n`; } content += `**Type:** ${(schema as any).type || 'object'}\n\n`; if ((schema as any).properties) { content += `**Properties:**\n`; for (const [propName, prop] of Object.entries((schema as any).properties)) { const isRequired = (schema as any).required?.includes(propName); content += `- \`${propName}\`${isRequired ? ' (required)' : ''} - ${(prop as any).description || 'No description'}\n`; content += ` - Type: ${(prop as any).type || 'object'}\n`; if ((prop as any).example !== undefined) { content += ` - Example: \`${JSON.stringify((prop as any).example)}\`\n`; } if ((prop as any).enum) { content += ` - Allowed values: ${(prop as any).enum.map((v: any) => `\`${v}\``).join(', ')}\n`; } } content += `\n`; } if ((schema as any).example) { content += `**Example:**\n\`\`\`json\n${JSON.stringify((schema as any).example, null, 2)}\n\`\`\`\n\n`; } content += `---\n\n`; } return { contents: [{ uri: uri.href, text: content }] }; } ); } // Register presentation components resource (specific to your API) if (spec.components?.schemas?.Slide?.properties?.content?.properties?.elements?.items?.properties?.type?.enum) { server.registerResource( "swagger-components", "docs://swagger/components", { title: `${spec.info.title} - Presentation Components`, description: "Available presentation components and their usage", mimeType: "text/markdown" }, async (uri: { href: string }) => { const slideSchema = spec.components?.schemas?.Slide as any; const elementTypes = slideSchema?.properties?.content?.properties?.elements?.items?.properties?.type?.enum || []; let content = `# ${spec.info.title} - Presentation Components\n\n`; content += `This API supports the following presentation element types:\n\n`; for (const elementType of elementTypes) { content += `## ${elementType}\n`; // Add descriptions for known component types const descriptions = { 'h1': 'Main heading - largest text size', 'h2': 'Secondary heading', 'h3': 'Tertiary heading', 'h4': 'Quaternary heading', 'p': 'Paragraph text', 'ul': 'Unordered (bulleted) list', 'ol': 'Ordered (numbered) list', 'li': 'List item', 'image': 'Image element with src, alt, width properties', 'button': 'Interactive button element', 'company-logo': 'Company logo display', 'stats-section': 'Statistics display section with items array', 'opportunity-box': 'Highlighted opportunity or feature box', 'alert-success': 'Success message alert', 'alert-warning': 'Warning message alert', 'alert-info': 'Information alert', 'risk-warning': 'Risk warning alert', 'key-points': 'Key points section', 'two-column': 'Two-column layout', 'three-column': 'Three-column layout', 'competitive-advantage': 'Competitive advantage showcase', 'info-card': 'Information card display', 'ai-query-demo': 'AI query demonstration', 'product-showcase': 'Product showcase with title, features, and certifications' }; if (descriptions[elementType as keyof typeof descriptions]) { content += `${descriptions[elementType as keyof typeof descriptions]}\n\n`; } // Add special property info for complex components if (elementType === 'stats-section') { content += `**Properties:**\n- \`items\`: Array of statistics strings\n- \`style\`: CSS styling\n\n`; } else if (elementType === 'product-showcase') { content += `**Properties:**\n- \`title\`: Product title\n- \`features\`: Array of feature strings\n- \`certifications\`: Array of certification strings\n- \`style\`: CSS styling\n\n`; } else if (elementType === 'image') { content += `**Properties:**\n- \`src\`: Image URL\n- \`alt\`: Alt text\n- \`width\`: Width specification\n- \`style\`: CSS styling\n\n`; } else if (['ul', 'ol'].includes(elementType)) { content += `**Properties:**\n- \`text\`: HTML list items as text (e.g., "<li>Item 1</li><li>Item 2</li>")\n- \`style\`: CSS styling\n\n`; } else { content += `**Properties:**\n- \`text\`: Text content\n- \`style\`: CSS styling\n\n`; } } // Add example from schema if (spec.components?.schemas?.PresentationData?.example) { content += `## Complete Example\n\n`; content += `\`\`\`json\n${JSON.stringify((spec.components.schemas.PresentationData as any).example, null, 2)}\n\`\`\`\n\n`; } return { contents: [{ uri: uri.href, text: content }] }; } ); } } // Register a resource for API documentation server.registerResource( "api-docs", "docs://hal/api", { title: "HAL API Documentation", description: "Documentation for available HTTP API tools", mimeType: "text/markdown" }, async (uri: { href: string }) => { let docsContent = `# HAL (HTTP API Layer) Documentation ## Features ### Secret Management HAL supports secure secret management through environment variables. You can define secrets using the \`HAL_SECRET_\` prefix and reference them in your HTTP requests using the \`{secrets.key}\` syntax. **Setup:** 1. Set environment variables with the \`HAL_SECRET_\` prefix: - \`HAL_SECRET_TOKEN=your_api_token\` - \`HAL_SECRET_USERNAME=your_username\` - \`HAL_SECRET_PASSWORD=your_password\` 2. Use the secrets in your requests: - URLs: \`https://api.example.com/user?token={secrets.token}\` - Headers: \`{"Authorization": "Bearer {secrets.token}"}\` - Request bodies: \`{"username": "{secrets.username}", "password": "{secrets.password}"}\` **Security Benefits:** - Secret values are never visible to the AI - Secrets are substituted at request time - Environment-based configuration supports different deployment environments ## Available Tools ### Built-in HTTP Tools #### list-secrets Get a list of available secret keys that can be used with {secrets.key} syntax. **Parameters:** None **Example Response:** \`\`\` Available secrets (3 total): You can use these secret keys in your HTTP requests using the {secrets.key} syntax: 1. {secrets.api_key} 2. {secrets.github_token} 3. {secrets.username} \`\`\` #### http-get Make HTTP GET requests to any URL with secret substitution support. **Parameters:** - \`url\` (required): The URL to request (supports {secrets.key} substitution) - \`headers\` (optional): Object of additional headers to send (supports {secrets.key} substitution) **Example:** \`\`\` { "url": "https://api.github.com/user?access_token={secrets.github_token}", "headers": { "Accept": "application/vnd.github.v3+json", "Authorization": "Bearer {secrets.github_token}" } } \`\`\` #### http-post Make HTTP POST requests with optional body and headers with secret substitution support. **Parameters:** - \`url\` (required): The URL to request (supports {secrets.key} substitution) - \`body\` (optional): Request body content (supports {secrets.key} substitution) - \`headers\` (optional): Object of additional headers to send (supports {secrets.key} substitution) - \`contentType\` (optional): Content-Type header (default: application/json) **Example:** \`\`\` { "url": "https://api.example.com/login", "body": "{\\"username\\": \\"{secrets.username}\\", \\"password\\": \\"{secrets.password}\\"}", "headers": { "Authorization": "Bearer {secrets.api_key}" }, "contentType": "application/json" } \`\`\` `; // Add Swagger-generated tools documentation if available if (SWAGGER_FILE_PATH) { try { const spec = await loadSwaggerSpec(); if (spec) { docsContent += ` ### Auto-generated API Tools (from Swagger/OpenAPI) **API:** ${spec.info.title} v${spec.info.version} ${spec.info.description ? `**Description:** ${spec.info.description}` : ''} **Base URL:** ${API_BASE_URL || spec.servers?.[0]?.url || 'Not specified'} #### Documentation Tools - **docs_search** - Search through API documentation with intelligent matching - **docs_components** - List all available presentation components - **docs_schemas** - List all data schemas and models - **docs_endpoints** - List all API endpoints with documentation - **docs_example** - Get complete presentation examples #### API Operation Tools The following tools have been automatically generated from the OpenAPI specification: `; for (const [path, pathItem] of Object.entries(spec.paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) { continue; } const operationId = operation.operationId || `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`; const toolName = `swagger_${operationId}`; docsContent += `#### ${toolName} **${method.toUpperCase()} ${path}** ${operation.summary ? `*${operation.summary}*` : ''} ${operation.description ? `\n${operation.description}` : ''} `; } } } } catch (error) { docsContent += `\n\n**Note:** Error loading Swagger documentation: ${error}`; } } docsContent += ` ## Configuration HAL supports the following environment variables: - \`HAL_SWAGGER_FILE\`: Path to OpenAPI/Swagger specification file (JSON or YAML) - \`HAL_API_BASE_URL\`: Base URL for API requests (overrides servers in spec) - \`HAL_SECRET_*\`: Secret values for substitution (e.g., \`HAL_SECRET_TOKEN=abc123\` allows using \`{secrets.token}\` in requests) ## Usage HAL can be used with any MCP-compatible client to provide HTTP API capabilities to Large Language Models. `; return { contents: [{ uri: uri.href, text: docsContent }] }; } ); // Start the server with stdio transport async function main() { try { // Load secrets from environment variables loadSecrets(); // Load and register Swagger tools if configured if (SWAGGER_FILE_PATH) { console.error(`Loading Swagger specification from: ${SWAGGER_FILE_PATH}`); const spec = await loadSwaggerSpec(); if (spec) { console.error(`Successfully loaded ${spec.info.title} v${spec.info.version}`); await registerSwaggerTools(spec); await registerSwaggerResources(spec); await registerSwaggerDocTools(spec); console.error(`Registered tools for ${Object.keys(spec.paths).length} API paths`); } else { console.error('No valid Swagger specification found, using built-in tools only'); } } else { console.error('No HAL_SWAGGER_FILE specified, using built-in tools only'); } const transport = new StdioServerTransport(); await server.connect(transport); // Keep the process alive process.on('SIGINT', async () => { await server.close(); process.exit(0); }); } catch (error) { console.error('Error during startup:', error); process.exit(1); } } main().catch((error) => { console.error('Failed to start HAL server:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

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/DeanWard/HAL'

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