Skip to main content
Glama
nl-intent.tsβ€’14.7 kB
/** * Natural language intent parser for ARC MCP server operations. * Lightweight (regex + heuristics) to avoid model round-trips. * * Responsibilities: * - Classify user utterance into a known ARC intent * - Extract parameters (namespace, runner-scale-set, image, replicas, etc.) * - Return structured object suitable for invoking existing MCP tools */ // Primitive value types we expect to extract from utterances type ParamValue = string | number | boolean | undefined; type Params = Record<string, ParamValue>; export interface ParsedIntent<P extends Params = Params> { intent: string; // canonical tool name or meta action confidence: number; // 0..1 heuristic params: P; // extracted parameters missing?: string[]; // required params not found notes?: string; // explanation for the mapping alternatives?: string[]; // suggested alternative intents when ambiguous } interface IntentPattern<P extends Params = Params> { name: string; tool: string; // underlying tool name description: string; required?: string[]; // required param keys regexes: RegExp[]; // matching patterns paramExtractors?: ((utterance: string) => Partial<P>)[]; confidenceBoost?: number; } // Basic helpers const toNumber = (s: string | undefined) => { if (!s) return undefined; const n = parseInt(s, 10); return isNaN(n) ? undefined : n; }; // ARC-specific patterns definition const arcPatterns: IntentPattern[] = [ { name: 'arc_deploy_runners_hybrid', tool: 'arc_deploy_runners_hybrid', description: 'Deploy ARC runners with hybrid GitOps approach', regexes: [ /install.*arc.*(runner|with|using)/i, // "Install ARC with/using runners" or "Install ARC runners" /(setup|deploy|create).*runners?/i, /add.*runners?.*(repo|repository|org|organization)/i, /install.*arc.*(container.*mode|novolume|ephemeral|storage)/i // "Install ARC with container mode" ], paramExtractors: [utter => { const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; const org = utter.match(/(org|organization)[:\s]+([a-z0-9-]+)/i)?.[2]; const minReplicas = toNumber(utter.match(/min(imum)? (\d+)/i)?.[2]); const maxReplicas = toNumber(utter.match(/max(imum)? (\d+)/i)?.[2]); // Extract container mode let containerMode: string | undefined; if (/dind|docker.?in.?docker|container.*mode.*dind|eliminate.*storage/i.test(utter)) { containerMode = 'dind'; } else if (/novolume|no.?volume|ephemeral.*storage|kubernetes-novolume/i.test(utter)) { containerMode = 'kubernetes-novolume'; } else if (/with.*volume|persistent.*storage|kubernetes.*mode/i.test(utter)) { containerMode = 'kubernetes'; } return { namespace: ns, organization: org, minRunners: minReplicas, maxRunners: maxReplicas, containerMode, apply: true // Default to applying when user says "install" }; }] }, { name: 'arc_install_controller', tool: 'arc_install_controller', description: 'Install ARC controller only (without runners)', regexes: [ /install.*arc.*controller/i, /^(setup|deploy).*arc$/i, // Only match "setup ARC" or "deploy ARC" without additional context /install.*github.*actions.*runner.*controller/i, /install.*arc.*only/i // "Install ARC only" (no runners) ], paramExtractors: [utter => { const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2] || 'arc-systems'; const version = utter.match(/version ([v]?[\d.]+)/i)?.[1]; return { namespace: ns, version }; }] }, { name: 'arc_create_runner_scale_set', tool: 'arc_create_runner_scale_set', description: 'Create ARC runner scale set (legacy direct approach)', required: ['name', 'namespace', 'githubConfigUrl'], regexes: [/create.*runner.*scale.*set/i, /add.*runner.*scale.*set/i], paramExtractors: [utter => { const name = utter.match(/named? ([a-z0-9-]+)/i)?.[1]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; const repo = utter.match(/(repo|repository) ([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)/i)?.[2]; const minReplicas = toNumber(utter.match(/min(imum)? (\d+) replica/i)?.[2]); const maxReplicas = toNumber(utter.match(/max(imum)? (\d+) replica/i)?.[2]); // Extract container mode let containerMode: string | undefined; if (/dind|docker.?in.?docker|container.*mode.*dind|eliminate.*storage/i.test(utter)) { containerMode = 'dind'; } else if (/novolume|no.?volume|ephemeral.*storage|kubernetes-novolume/i.test(utter)) { containerMode = 'kubernetes-novolume'; } else if (/with.*volume|persistent.*storage|kubernetes.*mode/i.test(utter)) { containerMode = 'kubernetes'; } return { name, namespace: ns, githubConfigUrl: repo ? `https://github.com/${repo}` : undefined, minReplicas, maxReplicas, containerMode }; }] }, { name: 'arc_list_runner_scale_sets', tool: 'arc_list_runner_scale_sets', description: 'List ARC runner scale sets', regexes: [/list.*runner.*scale.*sets?/i, /(show|what).*runners?/i, /get.*arc.*runners?/i], paramExtractors: [utter => { const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; return { namespace: ns }; }] }, { name: 'arc_get_runner_scale_set_status', tool: 'arc_get_runner_scale_set_status', description: 'Get runner scale set status', required: ['name', 'namespace'], regexes: [/(status|health|state).*(runner.*scale.*set|runners?)/i, /how.*(runners?).*(doing|health)/i], paramExtractors: [utter => { const name = utter.match(/(runner.*scale.*set|runners?) ([a-z0-9-]+)/i)?.[2]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; return { name, namespace: ns }; }] }, { name: 'arc_scale_runner_scale_set', tool: 'arc_scale_runner_scale_set', description: 'Scale ARC runner scale set', required: ['name', 'namespace', 'replicas'], regexes: [/(scale|set).*(runner.*scale.*set|runners?)/i, /(increase|decrease).*(runner|replicas)/i], paramExtractors: [utter => { const name = utter.match(/(runner.*scale.*set|runners?) ([a-z0-9-]+)/i)?.[2]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; const reps = toNumber(utter.match(/to (\d{1,3}) ?replica/i)?.[1] || utter.match(/to (\d{1,3})$/)?.[1]); return { name, namespace: ns, replicas: reps }; }] }, { name: 'arc_update_runner_image', tool: 'arc_update_runner_image', description: 'Update runner image', required: ['name', 'namespace', 'image'], regexes: [/update.*runner.*image/i, /(set|change).*image.*runner/i], paramExtractors: [utter => { const name = utter.match(/(runner.*scale.*set|runners?) ([a-z0-9-]+)/i)?.[2]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; const image = utter.match(/image ([\w./:-]+:[\w.-]+)/i)?.[1] || utter.match(/to ([\w./:-]+:[\w.-]+)/i)?.[1]; return { name, namespace: ns, image }; }] }, { name: 'arc_delete_runner_scale_set', tool: 'arc_delete_runner_scale_set', description: 'Delete runner scale set', required: ['name', 'namespace'], regexes: [/delete.*runner.*scale.*set/i, /remove.*runners?/i, /(cleanup|destroy).*arc.*runners?/i], paramExtractors: [utter => { const name = utter.match(/(runner.*scale.*set|runners?) ([a-z0-9-]+)/i)?.[2]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; return { name, namespace: ns }; }] }, { name: 'arc_get_runner_logs', tool: 'arc_get_runner_logs', description: 'Get runner logs', required: ['runnerScaleSetName', 'namespace'], regexes: [/(logs|log).*(runner|arc)/i, /(show|get).*runner.*logs?/i], paramExtractors: [utter => { const name = utter.match(/(runner.*scale.*set|runners?) ([a-z0-9-]+)/i)?.[2]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; const lines = toNumber(utter.match(/last (\d{1,4}) lines?/i)?.[1]); return { runnerScaleSetName: name, namespace: ns, lines }; }] }, { name: 'arc_evaluate_policies', tool: 'arc_evaluate_policies', description: 'Evaluate ARC policies', required: ['runnerScaleSetName', 'namespace'], regexes: [/evaluate.*arc.*polic/i, /(policy|policies).*arc.*(check|evaluate)/i, /check.*arc.*compliance/i], paramExtractors: [utter => { const name = utter.match(/(runner.*scale.*set|runners?) ([a-z0-9-]+)/i)?.[2]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; return { runnerScaleSetName: name, namespace: ns }; }] }, { name: 'arc_generate_compliance_report', tool: 'arc_generate_compliance_report', description: 'Generate ARC compliance report', regexes: [/(generate|create).*arc.*compliance/i, /arc.*compliance.*report/i, /compliance.*report.*arc/i], paramExtractors: [utter => { const ns = utter.match(/namespace ([a-z0-9-]+)/i)?.[1]; return { namespace: ns }; }] }, { name: 'arc_auto_fix_violations', tool: 'arc_auto_fix_violations', description: 'Auto fix ARC policy violations', required: ['runnerScaleSetName', 'namespace'], regexes: [/auto.*fix.*arc/i, /(apply|run).*arc.*auto[- ]?fix/i, /fix.*arc.*(violations|policies)/i], paramExtractors: [utter => { const name = utter.match(/(runner.*scale.*set|runners?) ([a-z0-9-]+)/i)?.[2]; const ns = utter.match(/in (namespace |ns )?([a-z0-9-]+)/i)?.[2]; const dryRun = /dry run/i.test(utter) ? true : undefined; return { runnerScaleSetName: name, namespace: ns, dryRun }; }] }, { name: 'arc_check_github_connection', tool: 'arc_check_github_connection', description: 'Check GitHub connection', regexes: [/check.*github.*connection/i, /(test|verify).*github.*connect/i, /github.*status/i], paramExtractors: [utter => { const token = utter.match(/token ([a-zA-Z0-9_]+)/i)?.[1]; return { githubToken: token }; }] }, { name: 'arc_get_cluster_info', tool: 'arc_get_cluster_info', description: 'Get Kubernetes cluster info', regexes: [/^(what'?s|show|get).*(cluster|k8s).*info/i, /(cluster|kubernetes).*status/i] }, { name: 'arc_backup_configuration', tool: 'arc_backup_configuration', description: 'Backup ARC configuration', regexes: [/backup.*arc.*config/i, /(save|export).*arc.*configuration/i], paramExtractors: [utter => { const outputPath = utter.match(/to (.+\.(yaml|yml|json))/i)?.[1]; return { outputPath }; }] }, { name: 'arc_restore_configuration', tool: 'arc_restore_configuration', description: 'Restore ARC configuration', required: ['configPath'], regexes: [/restore.*arc.*config/i, /(import|load).*arc.*configuration/i], paramExtractors: [utter => { const configPath = utter.match(/from (.+\.(yaml|yml|json))/i)?.[1]; return { configPath }; }] }, { name: 'arc_monitor_webhooks', tool: 'arc_monitor_webhooks', description: 'Monitor GitHub webhooks', regexes: [/monitor.*webhooks?/i, /(check|watch).*webhook.*status/i, /webhook.*health/i], paramExtractors: [utter => { const duration = toNumber(utter.match(/for (\d+) (minutes?|mins?)/i)?.[1]); return { durationMinutes: duration }; }] } ]; export function parseArcIntent(utterance: string): ParsedIntent { const cleaned = utterance.trim(); const matches: ParsedIntent[] = []; for (const p of arcPatterns) { if (p.regexes.some(r => r.test(cleaned))) { let params: Params = {}; if (p.paramExtractors) { for (const ex of p.paramExtractors) { params = { ...params, ...ex(cleaned) }; } } const missing = (p.required || []).filter(req => params[req] === undefined || params[req] === null || params[req] === ''); const confidenceBase = 0.6 + (p.confidenceBoost || 0); const completeness = p.required && p.required.length > 0 ? (((p.required.length - missing.length) / p.required.length) * 0.4) : 0.2; matches.push({ intent: p.tool, confidence: Math.min(1, confidenceBase + completeness), params, missing: missing.length ? missing : undefined, notes: p.description }); } } if (matches.length === 0) { return { intent: 'unknown', confidence: 0, params: {}, notes: 'No matching ARC intent patterns', alternatives: ['arc_list_tools', 'arc_get_cluster_info', 'arc_list_runner_scale_sets'] }; } // Select highest confidence; if close, mark alternatives matches.sort((a, b) => b.confidence - a.confidence); const top = matches[0]; const alt = matches.slice(1).filter(m => top.confidence - m.confidence < 0.15).map(m => m.intent); if (alt.length) top.alternatives = alt; return top; } // Export the pattern list for external use (e.g., help generation) export const getArcIntentPatterns = () => arcPatterns.map(p => ({ name: p.name, tool: p.tool, description: p.description, required: p.required || [], examples: p.regexes.map(r => r.source) }));

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/tsviz/arc-config-mcp'

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