Skip to main content
Glama
acl-tools.ts15.3 kB
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod/v4"; import { logger } from "../logger.js"; import { returnToolError, returnToolSuccess } from "../utils.js"; import type { ToolContext, ToolModule } from "./index.js"; // Schemas const ACLSchema = z.object({ operation: z .enum(["get", "update", "validate"]) .describe("ACL operation to perform"), aclConfig: z .object({ acls: z .array( z.object({ action: z.enum(["accept", "drop"]), src: z.array(z.string()), dst: z.array(z.string()), }), ) .optional() .describe("Access control rules"), groups: z .record(z.string(), z.array(z.string())) .optional() .describe("User groups definition"), tagOwners: z .record(z.string(), z.array(z.string())) .optional() .describe("Tag ownership mapping"), }) .optional() .describe("ACL configuration (required for update/validate operations)"), }); const DNSSchema = z.object({ operation: z .enum([ "get_nameservers", "set_nameservers", "get_preferences", "set_preferences", "get_searchpaths", "set_searchpaths", ]) .describe("DNS operation to perform"), nameservers: z .array(z.string()) .optional() .describe("DNS nameservers (for set_nameservers operation)"), magicDNS: z .boolean() .optional() .describe("Enable/disable MagicDNS (for set_preferences operation)"), searchPaths: z .array(z.string()) .optional() .describe("DNS search paths (for set_searchpaths operation)"), }); const KeyManagementSchema = z.object({ operation: z .enum(["list", "create", "delete"]) .describe("Key management operation"), keyConfig: z .object({ description: z.string().optional(), expirySeconds: z.number().optional(), capabilities: z .object({ devices: z .object({ create: z .object({ reusable: z.boolean().optional(), ephemeral: z.boolean().optional(), preauthorized: z.boolean().optional(), tags: z.array(z.string()).optional(), }) .optional(), }) .optional(), }) .optional(), }) .optional() .describe("Key configuration (for create operation)"), keyId: z .string() .optional() .describe("Authentication key ID (for delete operation)"), }); const NetworkLockSchema = z.object({ operation: z .enum(["status", "enable", "disable", "add_key", "remove_key", "list_keys"]) .describe("Network lock operation to perform"), publicKey: z .string() .optional() .describe("Public key for add/remove operations"), keyId: z.string().optional().describe("Key ID for remove operations"), }); const PolicyFileSchema = z.object({ operation: z .enum(["get", "update", "test_access"]) .describe("Policy file operation to perform"), policy: z .string() .optional() .describe("Policy content (HuJSON format) for update operation"), testRequest: z .object({ src: z.string(), dst: z.string(), proto: z.string().optional(), }) .optional() .describe("Access test parameters for test_access operation"), }); // Tool handlers async function manageACL( args: z.infer<typeof ACLSchema>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Managing ACL configuration:", args); switch (args.operation) { case "get": { const result = await context.api.getACL(); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Current ACL configuration:\n\n${ typeof result.data === "string" ? result.data : JSON.stringify(result.data, null, 2) }`, ); } case "update": { if (!args.aclConfig) { return returnToolError( "ACL configuration is required for update operation", ); } const aclString = JSON.stringify(args.aclConfig, null, 2); const result = await context.api.updateACL(aclString); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess("ACL configuration updated successfully"); } case "validate": { if (!args.aclConfig) { return returnToolError( "ACL configuration is required for validation", ); } const aclString = JSON.stringify(args.aclConfig, null, 2); const result = await context.api.validateACL(aclString); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess("ACL configuration is valid"); } default: return returnToolError( "Invalid ACL operation. Use: get, update, or validate", ); } } catch (error) { logger.error("Error managing ACL:", error); return returnToolError(error); } } async function manageDNS( args: z.infer<typeof DNSSchema>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Managing DNS configuration:", args); switch (args.operation) { case "get_nameservers": { const result = await context.api.getDNSNameservers(); if (!result.success) { return returnToolError(result.error); } const nameservers = result.data?.dns || []; return returnToolSuccess( `DNS Nameservers:\n${ nameservers.length > 0 ? nameservers.map((ns) => ` - ${ns}`).join("\n") : " No custom nameservers configured" }`, ); } case "set_nameservers": { if (!args.nameservers) { return returnToolError( "Nameservers array is required for set_nameservers operation", ); } const result = await context.api.setDNSNameservers(args.nameservers); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `DNS nameservers updated to: ${args.nameservers.join(", ")}`, ); } case "get_preferences": { const result = await context.api.getDNSPreferences(); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `DNS Preferences:\n MagicDNS: ${ result.data?.magicDNS ? "Enabled" : "Disabled" }`, ); } case "set_preferences": { if (args.magicDNS === undefined) { return returnToolError( "magicDNS boolean is required for set_preferences operation", ); } const result = await context.api.setDNSPreferences(args.magicDNS); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `MagicDNS ${args.magicDNS ? "enabled" : "disabled"}`, ); } case "get_searchpaths": { const result = await context.api.getDNSSearchPaths(); if (!result.success) { return returnToolError(result.error); } const searchPaths = result.data?.searchPaths || []; return returnToolSuccess( `DNS Search Paths:\n${ searchPaths.length > 0 ? searchPaths.map((path) => ` - ${path}`).join("\n") : " No search paths configured" }`, ); } case "set_searchpaths": { if (!args.searchPaths) { return returnToolError( "searchPaths array is required for set_searchpaths operation", ); } const result = await context.api.setDNSSearchPaths(args.searchPaths); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `DNS search paths updated to: ${args.searchPaths.join(", ")}`, ); } default: return returnToolError( "Invalid DNS operation. Use: get_nameservers, set_nameservers, get_preferences, set_preferences, get_searchpaths, set_searchpaths", ); } } catch (error: unknown) { logger.error("Error managing DNS:", error); return returnToolError(error); } } async function manageKeys( args: z.infer<typeof KeyManagementSchema>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Managing authentication keys:", args); switch (args.operation) { case "list": { const result = await context.api.listAuthKeys(); if (!result.success) { return returnToolError(result.error); } const keys = result.data?.keys || []; if (keys.length === 0) { return returnToolSuccess("No authentication keys found"); } const keyList = keys .map((key, index: number) => { return `**Key ${index + 1}** - ID: ${key.id} - Description: ${key.description || "No description"} - Created: ${key.created} - Expires: ${key.expires} - Revoked: ${key.revoked ? "Yes" : "No"} - Reusable: ${key.capabilities?.devices?.create?.reusable ? "Yes" : "No"} - Preauthorized: ${ key.capabilities?.devices?.create?.preauthorized ? "Yes" : "No" }`; }) .join("\n\n"); return returnToolSuccess( `Found ${keys.length} authentication keys:\n\n${keyList}`, ); } case "create": { if (!args.keyConfig) { return returnToolError( "Key configuration is required for create operation", ); } const keyConfig = { ...args.keyConfig, capabilities: { devices: { create: { ...args.keyConfig.capabilities?.devices?.create, }, }, }, }; const result = await context.api.createAuthKey(keyConfig); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Authentication key created successfully: - ID: ${result.data?.id} - Key: ${result.data?.key} - Description: ${result.data?.description || "No description"}`, ); } case "delete": { if (!args.keyId) { return returnToolError("Key ID is required for delete operation"); } const result = await context.api.deleteAuthKey(args.keyId); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Authentication key ${args.keyId} deleted successfully`, ); } default: return returnToolError( "Invalid key operation. Use: list, create, or delete", ); } } catch (error: unknown) { logger.error("Error managing keys:", error); return returnToolError(error); } } async function manageNetworkLock( args: z.infer<typeof NetworkLockSchema>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Managing network lock:", args); switch (args.operation) { case "status": { const result = await context.api.getNetworkLockStatus(); if (!result.success) { return returnToolError(result.error); } const status = result.data; return returnToolSuccess( `Network Lock Status: - Enabled: ${status?.enabled ? "Yes" : "No"} - Node Key: ${status?.nodeKey || "Not available"} - Trusted Keys: ${status?.trustedKeys?.length || 0}`, ); } case "enable": { const result = await context.api.enableNetworkLock(); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Network lock enabled successfully. Key: ${ result.data?.key || "Generated" }`, ); } case "disable": { const result = await context.api.disableNetworkLock(); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess("Network lock disabled successfully"); } default: return returnToolError( "Invalid network lock operation. Use: status, enable, disable, add_key, remove_key, or list_keys", ); } } catch (error) { logger.error("Error managing network lock:", error); return returnToolError(error); } } async function managePolicyFile( args: z.infer<typeof PolicyFileSchema>, context: ToolContext, ): Promise<CallToolResult> { try { logger.debug("Managing policy file:", args); switch (args.operation) { case "get": { const result = await context.api.getPolicyFile(); if (!result.success) { return returnToolError(result.error); } return returnToolSuccess( `Policy File (HuJSON format):\n\n${result.data}`, ); } case "test_access": { if (!args.testRequest) { return returnToolError( "Test request parameters are required for test_access operation", ); } const { src, dst, proto } = args.testRequest; const result = await context.api.testACLAccess(src, dst, proto); if (!result.success) { return returnToolError(result.error); } const testResult = result.data; return returnToolSuccess( `ACL Access Test Result: - Source: ${src} - Destination: ${dst} - Protocol: ${proto || "any"} - Result: ${testResult?.allowed ? "ALLOWED" : "DENIED"} - Rule: ${testResult?.rule || "No matching rule"} - Match: ${testResult?.match || "N/A"}`, ); } default: return returnToolError( "Invalid policy operation. Use: get or test_access", ); } } catch (error) { logger.error("Error managing policy file:", error); return returnToolError(error); } } // Export the tool module export const aclTools: ToolModule = { tools: [ { name: "manage_acl", description: "Manage Tailscale Access Control Lists (ACLs)", inputSchema: ACLSchema, handler: manageACL, }, { name: "manage_dns", description: "Manage Tailscale DNS configuration", inputSchema: DNSSchema, handler: manageDNS, }, { name: "manage_keys", description: "Manage Tailscale authentication keys", inputSchema: KeyManagementSchema, handler: manageKeys, }, { name: "manage_network_lock", description: "Manage Tailscale network lock (key authority) for enhanced security", inputSchema: NetworkLockSchema, handler: manageNetworkLock, }, { name: "manage_policy_file", description: "Manage policy files and test ACL access rules", inputSchema: PolicyFileSchema, handler: managePolicyFile, }, ], };

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/HexSleeves/tailscale-mcp'

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