Skip to content

Global API Key Auth + Claude Skill Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add Cloudflare Global API Key authentication support across cloudflare-mcp (1.0.1) and infrastructure-mcp (1.1.1), create a Claude skill for guided setup, eliminate hardcoded versions across all MCP repos, replace ASCII diagrams with Mermaid, and update Matt's config to use global key auth.

Architecture: The auth change starts in cloudflare-mcp's CloudflareRestClient which currently hardcodes Bearer token auth. We add a sealed CloudflareAuth interface with two implementations (token vs global key), then thread that through infrastructure-mcp's config, TUI, and tools. A Claude skill (markdown file) provides guided setup instructions installable alongside the MCP server. All repos get manifest-driven version resolution and Mermaid diagrams.

Tech Stack: Java 21, Maven, MCP SDK 1.1.1, JUnit 5


File Map

cloudflare-mcp (bump to 1.0.1)

  • Create: src/main/java/com/cloudflare/mcp/CloudflareAuth.java — sealed interface with two record implementations
  • Modify: src/main/java/com/cloudflare/mcp/CloudflareRestClient.java — accept CloudflareAuth instead of String apiToken
  • Modify: src/test/java/com/cloudflare/mcp/CloudflareRestClientTest.java — add auth construction tests
  • Modify: pom.xml — bump version to 1.0.1

infrastructure-mcp (bump to 1.1.1)

  • Modify: src/main/java/com/infrastructure/mcp/ServerConfig.java — add cloudflareApiKey, cloudflareEmail, flexible validation
  • Modify: src/main/java/com/infrastructure/mcp/InfrastructureTools.java — build correct auth type from config
  • Modify: src/main/java/com/infrastructure/mcp/SetupTui.java — add auth type selection on Cloudflare page
  • Modify: src/main/java/com/infrastructure/mcp/InfrastructureMcpServer.java — bump SERVER_VERSION
  • Modify: src/test/java/com/infrastructure/mcp/ServerConfigTest.java — tests for both auth modes
  • Modify: src/test/java/com/infrastructure/mcp/InfrastructureToolsTest.java — update testConfig helper
  • Modify: src/test/java/com/infrastructure/mcp/SetupTuiTest.java — tests for auth type selection
  • Modify: pom.xml — bump version to 1.1.1, depend on cloudflare-mcp 1.0.1
  • Modify: README.md — document both auth methods, add skill installation section
  • Create: skill.md — Claude skill for guided setup (installed to ~/.claude/commands/)

Task 1: CloudflareAuth sealed interface (cloudflare-mcp)

Files: - Create: /home/matt/mcp/cloudflare-mcp/src/main/java/com/cloudflare/mcp/CloudflareAuth.java - Test: /home/matt/mcp/cloudflare-mcp/src/test/java/com/cloudflare/mcp/CloudflareAuthTest.java

  • [ ] Step 1: Write the failing test
package com.cloudflare.mcp;

import org.junit.jupiter.api.Test;
import java.net.http.HttpRequest;
import static org.junit.jupiter.api.Assertions.*;

class CloudflareAuthTest {

    @Test
    void tokenAuthAppliesBearerHeader() {
        var auth = CloudflareAuth.apiToken("my-token-123");
        var builder = HttpRequest.newBuilder().uri(java.net.URI.create("https://example.com"));
        auth.applyHeaders(builder);
        var request = builder.GET().build();
        assertEquals("Bearer my-token-123",
                request.headers().firstValue("Authorization").orElse(null));
    }

    @Test
    void globalKeyAuthAppliesKeyAndEmailHeaders() {
        var auth = CloudflareAuth.globalApiKey("my-key-456", "[email protected]");
        var builder = HttpRequest.newBuilder().uri(java.net.URI.create("https://example.com"));
        auth.applyHeaders(builder);
        var request = builder.GET().build();
        assertEquals("my-key-456",
                request.headers().firstValue("X-Auth-Key").orElse(null));
        assertEquals("[email protected]",
                request.headers().firstValue("X-Auth-Email").orElse(null));
    }

    @Test
    void tokenAuthRejectsNull() {
        assertThrows(IllegalArgumentException.class, () -> CloudflareAuth.apiToken(null));
    }

    @Test
    void tokenAuthRejectsBlank() {
        assertThrows(IllegalArgumentException.class, () -> CloudflareAuth.apiToken("  "));
    }

    @Test
    void globalKeyAuthRejectsNullKey() {
        assertThrows(IllegalArgumentException.class,
                () -> CloudflareAuth.globalApiKey(null, "[email protected]"));
    }

    @Test
    void globalKeyAuthRejectsNullEmail() {
        assertThrows(IllegalArgumentException.class,
                () -> CloudflareAuth.globalApiKey("key", null));
    }
}
  • [ ] Step 2: Run test to verify it fails

Run: cd /home/matt/mcp/cloudflare-mcp && mvn test -pl . -Dtest=CloudflareAuthTest -q Expected: FAIL — CloudflareAuth class does not exist

  • [ ] Step 3: Write implementation
package com.cloudflare.mcp;

import java.net.http.HttpRequest;

/**
 * Authentication strategy for the Cloudflare API.
 * Supports API Token (Bearer) and Global API Key (X-Auth-Key + X-Auth-Email).
 */
public sealed interface CloudflareAuth {

    /** Apply authentication headers to the request builder. */
    void applyHeaders(HttpRequest.Builder builder);

    /** Create auth using a scoped API Token (Bearer authentication). */
    static CloudflareAuth apiToken(String token) {
        if (token == null || token.isBlank()) {
            throw new IllegalArgumentException("API token must not be null or blank");
        }
        return new ApiToken(token);
    }

    /** Create auth using the Global API Key + account email. */
    static CloudflareAuth globalApiKey(String apiKey, String email) {
        if (apiKey == null || apiKey.isBlank()) {
            throw new IllegalArgumentException("API key must not be null or blank");
        }
        if (email == null || email.isBlank()) {
            throw new IllegalArgumentException("Email must not be null or blank");
        }
        return new GlobalApiKey(apiKey, email);
    }

    record ApiToken(String token) implements CloudflareAuth {
        @Override
        public void applyHeaders(HttpRequest.Builder builder) {
            builder.header("Authorization", "Bearer " + token);
        }
    }

    record GlobalApiKey(String apiKey, String email) implements CloudflareAuth {
        @Override
        public void applyHeaders(HttpRequest.Builder builder) {
            builder.header("X-Auth-Key", apiKey);
            builder.header("X-Auth-Email", email);
        }
    }
}
  • [ ] Step 4: Run test to verify it passes

Run: cd /home/matt/mcp/cloudflare-mcp && mvn test -pl . -Dtest=CloudflareAuthTest -q Expected: PASS (6 tests)

  • [ ] Step 5: Commit
cd /home/matt/mcp/cloudflare-mcp
git add src/main/java/com/cloudflare/mcp/CloudflareAuth.java src/test/java/com/cloudflare/mcp/CloudflareAuthTest.java
git commit -m "feat: add CloudflareAuth sealed interface for token and global key auth"

Task 2: Update CloudflareRestClient to use CloudflareAuth (cloudflare-mcp)

Files: - Modify: /home/matt/mcp/cloudflare-mcp/src/main/java/com/cloudflare/mcp/CloudflareRestClient.java - Modify: /home/matt/mcp/cloudflare-mcp/src/test/java/com/cloudflare/mcp/CloudflareRestClientTest.java

  • [ ] Step 1: Write test for new constructor

Add to CloudflareRestClientTest.java:

@Test
void constructsWithApiTokenAuth() {
    var auth = CloudflareAuth.apiToken("test-token");
    var limiter = new RateLimiter(240);
    var client = new CloudflareRestClient(auth, "account-id", limiter);
    assertNotNull(client);
}

@Test
void constructsWithGlobalApiKeyAuth() {
    var auth = CloudflareAuth.globalApiKey("test-key", "[email protected]");
    var limiter = new RateLimiter(240);
    var client = new CloudflareRestClient(auth, "account-id", limiter);
    assertNotNull(client);
}

@Test
void legacyConstructorStillWorks() {
    var limiter = new RateLimiter(240);
    var client = new CloudflareRestClient("test-token", "account-id", limiter);
    assertNotNull(client);
}
  • [ ] Step 2: Run test to verify it fails

Run: cd /home/matt/mcp/cloudflare-mcp && mvn test -pl . -Dtest=CloudflareRestClientTest -q Expected: FAIL — no constructor accepting CloudflareAuth

  • [ ] Step 3: Update CloudflareRestClient

In CloudflareRestClient.java, replace the apiToken field and constructors:

Replace:

    private final String apiToken;
    private final String accountId;
    private final RateLimiter rateLimiter;
    private final HttpClient httpClient;

    public CloudflareRestClient(String apiToken, String accountId, RateLimiter rateLimiter) {
        this.apiToken = apiToken;
        this.accountId = accountId;
        this.rateLimiter = rateLimiter;
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .followRedirects(HttpClient.Redirect.NORMAL)
                .build();
    }

With:

    private final CloudflareAuth auth;
    private final String accountId;
    private final RateLimiter rateLimiter;
    private final HttpClient httpClient;

    public CloudflareRestClient(CloudflareAuth auth, String accountId, RateLimiter rateLimiter) {
        this.auth = auth;
        this.accountId = accountId;
        this.rateLimiter = rateLimiter;
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .followRedirects(HttpClient.Redirect.NORMAL)
                .build();
    }

    /** Legacy constructor for backwards compatibility — uses API Token auth. */
    public CloudflareRestClient(String apiToken, String accountId, RateLimiter rateLimiter) {
        this(CloudflareAuth.apiToken(apiToken), accountId, rateLimiter);
    }

In the request() method (line 143-174), replace:

                    .header("Authorization", "Bearer " + apiToken)

With:

                    // Auth headers applied below

And after the builder is created (after .timeout(Duration.ofSeconds(30))), add:

            auth.applyHeaders(builder);

The full request() method should look like:

    private String request(String method, String path, String body) {
        rateLimiter.checkAndRecord();
        try {
            var builder = HttpRequest.newBuilder()
                    .uri(URI.create(BASE_URL + path))
                    .header("Content-Type", "application/json")
                    .timeout(Duration.ofSeconds(30));

            auth.applyHeaders(builder);

            HttpRequest request = switch (method) {
                case "GET" -> builder.GET().build();
                case "POST" -> builder.POST(HttpRequest.BodyPublishers.ofString(body)).build();
                case "PATCH" -> builder.method("PATCH", HttpRequest.BodyPublishers.ofString(body)).build();
                case "PUT" -> builder.PUT(HttpRequest.BodyPublishers.ofString(body)).build();
                case "DELETE" -> builder.DELETE().build();
                default -> throw new IllegalArgumentException("Unknown method: " + method);
            };

            log.debug("CF {} {}", method, path);
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) {
                throw new CloudflareApiException("HTTP " + response.statusCode() + ": " +
                        truncate(response.body(), 500));
            }
            return response.body();
        } catch (CloudflareApiException e) {
            throw e;
        } catch (Exception e) {
            throw new CloudflareApiException("Request failed: " + e.getMessage(), e);
        }
    }

  • [ ] Step 4: Run all tests

Run: cd /home/matt/mcp/cloudflare-mcp && mvn test -q Expected: All tests PASS (existing tests use static parse methods, not HTTP calls)

  • [ ] Step 5: Bump version to 1.0.1

In pom.xml, change:

    <version>1.0.0</version>
To:
    <version>1.0.1</version>

  • [ ] Step 6: Install to local Maven repo

Run: cd /home/matt/mcp/cloudflare-mcp && mvn install -DskipTests -q Expected: BUILD SUCCESS

  • [ ] Step 7: Commit
cd /home/matt/mcp/cloudflare-mcp
git add pom.xml src/main/java/com/cloudflare/mcp/CloudflareRestClient.java src/test/java/com/cloudflare/mcp/CloudflareRestClientTest.java
git commit -m "feat: support CloudflareAuth in CloudflareRestClient, bump to 1.0.1"

Task 3: Update ServerConfig for dual auth (infrastructure-mcp)

Files: - Modify: /home/matt/mcp/infrastructure-mcp/src/main/java/com/infrastructure/mcp/ServerConfig.java - Modify: /home/matt/mcp/infrastructure-mcp/src/test/java/com/infrastructure/mcp/ServerConfigTest.java

  • [ ] Step 1: Write failing tests

Replace the full content of ServerConfigTest.java:

package com.infrastructure.mcp;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class ServerConfigTest {

    @Test
    void parsesApiTokenAuth() {
        ServerConfig config = ServerConfig.fromEnv(
                "cf-token-123", null, null,
                "cf-account-456",
                "nc-user", "nc-key-789", "1.2.3.4",
                "/custom/registry.json", "/usr/local/bin/fleet"
        );

        assertEquals("cf-token-123", config.cloudflareApiToken());
        assertNull(config.cloudflareApiKey());
        assertNull(config.cloudflareEmail());
        assertEquals("cf-account-456", config.cloudflareAccountId());
        assertTrue(config.isApiTokenAuth());
    }

    @Test
    void parsesGlobalApiKeyAuth() {
        ServerConfig config = ServerConfig.fromEnv(
                null, "cf-global-key", "[email protected]",
                "cf-account-456",
                "nc-user", "nc-key-789", "1.2.3.4",
                null, null
        );

        assertNull(config.cloudflareApiToken());
        assertEquals("cf-global-key", config.cloudflareApiKey());
        assertEquals("[email protected]", config.cloudflareEmail());
        assertFalse(config.isApiTokenAuth());
    }

    @Test
    void defaultsForOptionalValues() {
        ServerConfig config = ServerConfig.fromEnv(
                "cf-token", null, null,
                "cf-account",
                "nc-user", "nc-key", "10.0.0.1",
                null, null
        );

        assertEquals("/home/matt/fleet/data/registry.json", config.fleetRegistryPath());
        assertEquals("fleet", config.fleetBinary());
    }

    @Test
    void throwsWhenNoCloudflareAuth() {
        IllegalStateException ex = assertThrows(IllegalStateException.class, () ->
                ServerConfig.fromEnv(null, null, null,
                        "cf-account", "nc-user", "nc-key", "1.2.3.4", null, null)
        );
        assertTrue(ex.getMessage().contains("CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY"));
    }

    @Test
    void throwsWhenGlobalKeyWithoutEmail() {
        IllegalStateException ex = assertThrows(IllegalStateException.class, () ->
                ServerConfig.fromEnv(null, "cf-key", null,
                        "cf-account", "nc-user", "nc-key", "1.2.3.4", null, null)
        );
        assertTrue(ex.getMessage().contains("CLOUDFLARE_EMAIL"));
    }

    @Test
    void throwsOnMissingAccountId() {
        IllegalStateException ex = assertThrows(IllegalStateException.class, () ->
                ServerConfig.fromEnv("cf-token", null, null,
                        null, "nc-user", "nc-key", "1.2.3.4", null, null)
        );
        assertTrue(ex.getMessage().contains("CLOUDFLARE_ACCOUNT_ID"));
    }

    @Test
    void throwsOnMissingNamecheapApiKey() {
        IllegalStateException ex = assertThrows(IllegalStateException.class, () ->
                ServerConfig.fromEnv("cf-token", null, null,
                        "cf-account", "nc-user", null, "1.2.3.4", null, null)
        );
        assertTrue(ex.getMessage().contains("NAMECHEAP_API_KEY"));
    }

    @Test
    void apiTokenTakesPrecedenceOverGlobalKey() {
        ServerConfig config = ServerConfig.fromEnv(
                "cf-token", "cf-key", "[email protected]",
                "cf-account",
                "nc-user", "nc-key", "1.2.3.4",
                null, null
        );

        assertTrue(config.isApiTokenAuth());
    }
}
  • [ ] Step 2: Run test to verify it fails

Run: cd /home/matt/mcp/infrastructure-mcp && mvn test -pl . -Dtest=ServerConfigTest -q Expected: FAIL — fromEnv has wrong signature

  • [ ] Step 3: Rewrite ServerConfig

Replace the full content of ServerConfig.java:

package com.infrastructure.mcp;

/**
 * Holds all server configuration, sourced from environment variables.
 * Supports two Cloudflare auth modes: API Token (Bearer) or Global API Key (X-Auth-Key + X-Auth-Email).
 */
public record ServerConfig(
        String cloudflareApiToken,
        String cloudflareApiKey,
        String cloudflareEmail,
        String cloudflareAccountId,
        String namecheapApiUser,
        String namecheapApiKey,
        String namecheapClientIp,
        String fleetRegistryPath,
        String fleetBinary
) {

    private static final String DEFAULT_FLEET_REGISTRY_PATH = "/home/matt/fleet/data/registry.json";
    private static final String DEFAULT_FLEET_BINARY = "fleet";

    /** True if using scoped API Token auth, false if using Global API Key. */
    public boolean isApiTokenAuth() {
        return cloudflareApiToken != null && !cloudflareApiToken.isBlank();
    }

    /**
     * Factory method for explicit values — primarily for testing.
     */
    public static ServerConfig fromEnv(
            String cloudflareApiToken,
            String cloudflareApiKey,
            String cloudflareEmail,
            String cloudflareAccountId,
            String namecheapApiUser,
            String namecheapApiKey,
            String namecheapClientIp,
            String fleetRegistryPath,
            String fleetBinary
    ) {
        // Cloudflare auth: token OR (key + email) required
        boolean hasToken = cloudflareApiToken != null && !cloudflareApiToken.isBlank();
        boolean hasKey = cloudflareApiKey != null && !cloudflareApiKey.isBlank();

        if (!hasToken && !hasKey) {
            throw new IllegalStateException(
                    "Either CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL must be set");
        }
        if (hasKey && (cloudflareEmail == null || cloudflareEmail.isBlank())) {
            throw new IllegalStateException(
                    "CLOUDFLARE_EMAIL is required when using CLOUDFLARE_API_KEY (Global API Key auth)");
        }

        requirePresent(cloudflareAccountId, "CLOUDFLARE_ACCOUNT_ID");
        requirePresent(namecheapApiUser, "NAMECHEAP_API_USER");
        requirePresent(namecheapApiKey, "NAMECHEAP_API_KEY");
        requirePresent(namecheapClientIp, "NAMECHEAP_CLIENT_IP");

        return new ServerConfig(
                hasToken ? cloudflareApiToken : null,
                hasKey ? cloudflareApiKey : null,
                hasKey ? cloudflareEmail : null,
                cloudflareAccountId,
                namecheapApiUser,
                namecheapApiKey,
                namecheapClientIp,
                fleetRegistryPath != null ? fleetRegistryPath : DEFAULT_FLEET_REGISTRY_PATH,
                fleetBinary != null ? fleetBinary : DEFAULT_FLEET_BINARY
        );
    }

    /**
     * Factory method that reads configuration from {@link System#getenv()}.
     */
    public static ServerConfig fromSystem() {
        return fromEnv(
                System.getenv("CLOUDFLARE_API_TOKEN"),
                System.getenv("CLOUDFLARE_API_KEY"),
                System.getenv("CLOUDFLARE_EMAIL"),
                System.getenv("CLOUDFLARE_ACCOUNT_ID"),
                System.getenv("NAMECHEAP_API_USER"),
                System.getenv("NAMECHEAP_API_KEY"),
                System.getenv("NAMECHEAP_CLIENT_IP"),
                System.getenv("FLEET_REGISTRY_PATH"),
                System.getenv("FLEET_BINARY")
        );
    }

    private static void requirePresent(String value, String envVarName) {
        if (value == null || value.isBlank()) {
            throw new IllegalStateException(
                    "Required environment variable " + envVarName + " is missing or blank");
        }
    }
}
  • [ ] Step 4: Run test to verify it passes

Run: cd /home/matt/mcp/infrastructure-mcp && mvn test -pl . -Dtest=ServerConfigTest -q Expected: PASS (8 tests)

  • [ ] Step 5: Commit
cd /home/matt/mcp/infrastructure-mcp
git add src/main/java/com/infrastructure/mcp/ServerConfig.java src/test/java/com/infrastructure/mcp/ServerConfigTest.java
git commit -m "feat: support Global API Key auth in ServerConfig"

Task 4: Update InfrastructureTools to build correct auth (infrastructure-mcp)

Files: - Modify: /home/matt/mcp/infrastructure-mcp/src/main/java/com/infrastructure/mcp/InfrastructureTools.java - Modify: /home/matt/mcp/infrastructure-mcp/src/test/java/com/infrastructure/mcp/InfrastructureToolsTest.java

  • [ ] Step 1: Update testConfig helper in InfrastructureToolsTest

Replace the testConfig() method:

    private static ServerConfig testConfig() {
        return ServerConfig.fromEnv(
                "test-cf-token", null, null,
                "test-cf-account",
                "test-nc-user", "test-nc-key", "127.0.0.1",
                "/tmp/nonexistent-registry.json", "/usr/bin/false");
    }
  • [ ] Step 2: Run existing tests to verify they still pass

Run: cd /home/matt/mcp/infrastructure-mcp && mvn test -pl . -Dtest=InfrastructureToolsTest -q Expected: PASS (tests should work with updated testConfig() once constructor is updated)

  • [ ] Step 3: Update InfrastructureTools constructor

In InfrastructureTools.java, replace the InfrastructureTools(ServerConfig config) constructor:

    InfrastructureTools(ServerConfig config) {
        var cfLimiter = new com.cloudflare.mcp.RateLimiter(240);

        com.cloudflare.mcp.CloudflareAuth cfAuth;
        if (config.isApiTokenAuth()) {
            cfAuth = com.cloudflare.mcp.CloudflareAuth.apiToken(config.cloudflareApiToken());
        } else {
            cfAuth = com.cloudflare.mcp.CloudflareAuth.globalApiKey(
                    config.cloudflareApiKey(), config.cloudflareEmail());
        }
        this.cloudflare = new CloudflareRestClient(cfAuth, config.cloudflareAccountId(), cfLimiter);

        var ncLimiter = new com.namecheap.mcp.RateLimiter(20, 700);
        this.namecheap = new NamecheapClient(
                config.namecheapApiUser(), config.namecheapApiKey(),
                config.namecheapClientIp(), ncLimiter);

        this.fleet = new FleetClient(config.fleetRegistryPath(), config.fleetBinary());
    }
  • [ ] Step 4: Run all tests

Run: cd /home/matt/mcp/infrastructure-mcp && mvn test -pl . -Dtest=InfrastructureToolsTest -q Expected: PASS

  • [ ] Step 5: Commit
cd /home/matt/mcp/infrastructure-mcp
git add src/main/java/com/infrastructure/mcp/InfrastructureTools.java src/test/java/com/infrastructure/mcp/InfrastructureToolsTest.java
git commit -m "feat: build CloudflareAuth from ServerConfig in InfrastructureTools"

Task 5: Update SetupTui for auth type selection (infrastructure-mcp)

Files: - Modify: /home/matt/mcp/infrastructure-mcp/src/main/java/com/infrastructure/mcp/SetupTui.java - Modify: /home/matt/mcp/infrastructure-mcp/src/test/java/com/infrastructure/mcp/SetupTuiTest.java

  • [ ] Step 1: Write failing tests for auth type

Add to SetupTuiTest.java inside a new @Nested class AuthType:

    @Nested
    class AuthType {

        @Test
        void defaultsToGlobalApiKey() {
            var tui = createTui("");
            assertEquals("global", tui.getCloudflareAuthType());
        }

        @Test
        void validWithGlobalApiKey() {
            var tui = createTui("");
            tui.setCloudflareAuthType("global");
            tui.setCloudflareApiKey("key");
            tui.setCloudflareEmail("[email protected]");
            tui.setCloudflareAccountId("id");
            tui.setNamecheapApiUser("user");
            tui.setNamecheapApiKey("key");
            tui.setNamecheapClientIp("1.2.3.4");
            assertTrue(tui.isConfigValid());
        }

        @Test
        void validWithApiToken() {
            var tui = createTui("");
            tui.setCloudflareAuthType("token");
            tui.setCloudflareApiToken("token");
            tui.setCloudflareAccountId("id");
            tui.setNamecheapApiUser("user");
            tui.setNamecheapApiKey("key");
            tui.setNamecheapClientIp("1.2.3.4");
            assertTrue(tui.isConfigValid());
        }

        @Test
        void invalidGlobalKeyWithoutEmail() {
            var tui = createTui("");
            tui.setCloudflareAuthType("global");
            tui.setCloudflareApiKey("key");
            tui.setCloudflareEmail("");
            tui.setCloudflareAccountId("id");
            tui.setNamecheapApiUser("user");
            tui.setNamecheapApiKey("key");
            tui.setNamecheapClientIp("1.2.3.4");
            assertFalse(tui.isConfigValid());
        }
    }

Also update the existing Validation.validWhenAllRequired test:

        @Test
        void validWhenAllRequired() {
            var tui = createTui("");
            tui.setCloudflareAuthType("token");
            tui.setCloudflareApiToken("token");
            tui.setCloudflareAccountId("id");
            tui.setNamecheapApiUser("user");
            tui.setNamecheapApiKey("key");
            tui.setNamecheapClientIp("1.2.3.4");
            assertTrue(tui.isConfigValid());
        }

And update Validation.invalidWhenPartial:

        @Test
        void invalidWhenPartial() {
            var tui = createTui("");
            tui.setCloudflareAuthType("token");
            tui.setCloudflareApiToken("token");
            tui.setCloudflareAccountId("id");
            // Missing namecheap fields
            assertFalse(tui.isConfigValid());
        }

And update Validation.invalidWithEmptyToken:

        @Test
        void invalidWithEmptyToken() {
            var tui = createTui("");
            tui.setCloudflareAuthType("token");
            tui.setCloudflareApiToken("");
            tui.setCloudflareAccountId("id");
            tui.setNamecheapApiUser("user");
            tui.setNamecheapApiKey("key");
            tui.setNamecheapClientIp("1.2.3.4");
            assertFalse(tui.isConfigValid());
        }
  • [ ] Step 2: Run test to verify it fails

Run: cd /home/matt/mcp/infrastructure-mcp && mvn test -pl . -Dtest=SetupTuiTest -q Expected: FAIL — getCloudflareAuthType() method does not exist

  • [ ] Step 3: Update SetupTui

Add new fields after line 35 (after private String cloudflareApiToken = ""):

    private String cloudflareAuthType = "global"; // "global" or "token"
    private String cloudflareApiKey = "";
    private String cloudflareEmail = "";

Replace showCloudflarePage():

    private void showCloudflarePage() {
        printHeader("Cloudflare", 2, 5);
        out.println();
        out.println(CYAN + "  Cloudflare API Configuration" + RESET);
        out.println();

        out.println("  Auth type: " + YELLOW + "[1]" + RESET + " Global API Key  "
                + YELLOW + "[2]" + RESET + " Scoped API Token");
        out.println("  " + DIM + "Current: " + (cloudflareAuthType.equals("global") ? "Global API Key" : "Scoped API Token") + RESET);
        out.print("  " + DIM + "Choose (1/2, enter to keep): " + RESET);
        String choice = scanner.nextLine().trim();
        if (choice.equals("1")) cloudflareAuthType = "global";
        else if (choice.equals("2")) cloudflareAuthType = "token";

        out.println();

        if (cloudflareAuthType.equals("global")) {
            out.println("  Using " + GREEN + "Global API Key" + RESET + " authentication");
            out.println("  " + DIM + "Find your key at: https://dash.cloudflare.com/profile/api-tokens" + RESET);
            out.println("  " + DIM + "(scroll down to 'Global API Key' and click 'View')" + RESET);
            out.println();
            cloudflareApiKey = promptField("Global API Key", cloudflareApiKey, true, true);
            cloudflareEmail = promptField("Account Email", cloudflareEmail, true, false);
        } else {
            out.println("  Using " + GREEN + "Scoped API Token" + RESET + " authentication");
            out.println("  " + DIM + "Create a token at: https://dash.cloudflare.com/profile/api-tokens" + RESET);
            out.println("  Permissions: " + YELLOW + "Zone:Read+Edit, DNS:Edit, Zone Settings:Edit" + RESET);
            out.println();
            cloudflareApiToken = promptField("API Token", cloudflareApiToken, true, true);
        }

        cloudflareAccountId = promptField("Account ID", cloudflareAccountId, true, false);

        out.println();
        boolean valid;
        if (cloudflareAuthType.equals("global")) {
            valid = !cloudflareApiKey.isEmpty() && !cloudflareEmail.isEmpty() && !cloudflareAccountId.isEmpty();
        } else {
            valid = !cloudflareApiToken.isEmpty() && !cloudflareAccountId.isEmpty();
        }
        if (valid) {
            out.println("  " + GREEN + "  Cloudflare credentials set" + RESET);
        } else {
            out.println("  " + RED + "  Required fields missing" + RESET);
        }

        printDivider();
    }

Update showSummaryPage() — replace the two Cloudflare summary rows:

        if (cloudflareAuthType.equals("global")) {
            printSummaryRow("CF Auth Type", "Global API Key", true);
            printSummaryRow("CF API Key", maskSecret(cloudflareApiKey), !cloudflareApiKey.isEmpty());
            printSummaryRow("CF Email", cloudflareEmail, !cloudflareEmail.isEmpty());
        } else {
            printSummaryRow("CF Auth Type", "Scoped API Token", true);
            printSummaryRow("CF API Token", maskSecret(cloudflareApiToken), !cloudflareApiToken.isEmpty());
        }
        printSummaryRow("CF Account ID", cloudflareAccountId, !cloudflareAccountId.isEmpty());

Update isConfigValid():

    boolean isConfigValid() {
        boolean cfValid;
        if (cloudflareAuthType.equals("global")) {
            cfValid = !cloudflareApiKey.isEmpty() && !cloudflareEmail.isEmpty();
        } else {
            cfValid = !cloudflareApiToken.isEmpty();
        }
        return cfValid
                && !cloudflareAccountId.isEmpty()
                && !namecheapApiUser.isEmpty()
                && !namecheapApiKey.isEmpty()
                && !namecheapClientIp.isEmpty();
    }

Update doInstall() — replace the env map building section:

        var env = new LinkedHashMap<String, String>();
        if (cloudflareAuthType.equals("global")) {
            env.put("CLOUDFLARE_API_KEY", cloudflareApiKey);
            env.put("CLOUDFLARE_EMAIL", cloudflareEmail);
        } else {
            env.put("CLOUDFLARE_API_TOKEN", cloudflareApiToken);
        }
        env.put("CLOUDFLARE_ACCOUNT_ID", cloudflareAccountId);
        env.put("NAMECHEAP_API_USER", namecheapApiUser);
        env.put("NAMECHEAP_API_KEY", namecheapApiKey);
        env.put("NAMECHEAP_CLIENT_IP", namecheapClientIp);

Add getters/setters at the end:

    String getCloudflareAuthType() { return cloudflareAuthType; }
    String getCloudflareApiKey() { return cloudflareApiKey; }
    String getCloudflareEmail() { return cloudflareEmail; }
    void setCloudflareAuthType(String v) { cloudflareAuthType = v; }
    void setCloudflareApiKey(String v) { cloudflareApiKey = v; }
    void setCloudflareEmail(String v) { cloudflareEmail = v; }

  • [ ] Step 4: Run tests

Run: cd /home/matt/mcp/infrastructure-mcp && mvn test -pl . -Dtest=SetupTuiTest -q Expected: PASS

  • [ ] Step 5: Commit
cd /home/matt/mcp/infrastructure-mcp
git add src/main/java/com/infrastructure/mcp/SetupTui.java src/test/java/com/infrastructure/mcp/SetupTuiTest.java
git commit -m "feat: add auth type selection to SetupTui (global key vs token)"

Task 6: Version bumps and full test run (infrastructure-mcp)

Files: - Modify: /home/matt/mcp/infrastructure-mcp/pom.xml - Modify: /home/matt/mcp/infrastructure-mcp/src/main/java/com/infrastructure/mcp/InfrastructureMcpServer.java

  • [ ] Step 1: Bump pom.xml version to 1.1.1 and cloudflare-mcp dependency to 1.0.1

In pom.xml, change:

    <version>1.0.0</version>
To:
    <version>1.1.1</version>

And change the cloudflare-mcp dependency:

            <version>1.0.0</version>
To:
            <version>1.0.1</version>

  • [ ] Step 2: Bump SERVER_VERSION

In InfrastructureMcpServer.java, change:

    private static final String SERVER_VERSION = "1.0.0";
To:
    private static final String SERVER_VERSION = "1.1.1";

  • [ ] Step 3: Run full test suite

Run: cd /home/matt/mcp/infrastructure-mcp && mvn clean test -q Expected: All tests PASS

  • [ ] Step 4: Build shaded JAR

Run: cd /home/matt/mcp/infrastructure-mcp && mvn clean package -q Expected: BUILD SUCCESS, produces target/infrastructure-mcp-1.1.1.jar

  • [ ] Step 5: Commit
cd /home/matt/mcp/infrastructure-mcp
git add pom.xml src/main/java/com/infrastructure/mcp/InfrastructureMcpServer.java
git commit -m "chore: bump to v1.1.1, depend on cloudflare-mcp 1.0.1"

Task 7: Create Claude skill for setup (infrastructure-mcp)

Files: - Create: /home/matt/mcp/infrastructure-mcp/skill.md

  • [ ] Step 1: Create skill.md
---
name: infrastructure-setup
description: Guided setup for the Infrastructure MCP server — configures Cloudflare, Namecheap, and Fleet credentials
---

# Infrastructure MCP Server — Setup Guide

You are helping the user configure the Infrastructure MCP server. Walk them through each step below. Ask for confirmation before modifying any files.

## Prerequisites

- Java 21 installed (`java -version`)
- The JAR built at the expected path (usually `target/infrastructure-mcp-1.1.1.jar`)
- Cloudflare account
- Namecheap API access enabled

## Step 1: Cloudflare Authentication

Ask the user which auth method they prefer:

**Option A: Global API Key** (simpler, full account access)
- Find it at: https://dash.cloudflare.com/profile/api-tokens → scroll to "Global API Key" → View
- Needs: the key + the email address used to sign in to Cloudflare
- Env vars: `CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL`

**Option B: Scoped API Token** (recommended for shared/CI environments)
- Create at: https://dash.cloudflare.com/profile/api-tokens → Create Token
- Permissions needed: Zone:Read+Edit, DNS:Read+Edit, Zone Settings:Read+Edit
- Zone Resources: All zones (or specific zones)
- Env var: `CLOUDFLARE_API_TOKEN`

Both options also need: `CLOUDFLARE_ACCOUNT_ID`
- Find it at: https://dash.cloudflare.com → pick any domain → right sidebar under "API" section

## Step 2: Namecheap API

- Enable API at: https://ap.www.namecheap.com/settings/tools/apiaccess
- Whitelist the server's public IP address in the Namecheap dashboard
- Env vars needed: `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY`, `NAMECHEAP_CLIENT_IP`

## Step 3: Fleet (Optional)

Only needed if you use Fleet for app deployment.
- `FLEET_REGISTRY_PATH` — path to `registry.json` (default: `/home/matt/fleet/data/registry.json`)
- `FLEET_BINARY` — path to fleet CLI (default: `fleet`)

## Step 4: Register with Claude Code

Once you have the credentials, write the config to `~/.claude.json` under `mcpServers`:

For Global API Key auth:
```json
{
  "infrastructure-mcp": {
    "type": "stdio",
    "command": "java",
    "args": ["-jar", "/path/to/infrastructure-mcp-1.1.1.jar"],
    "env": {
      "CLOUDFLARE_API_KEY": "<your-global-api-key>",
      "CLOUDFLARE_EMAIL": "<your-cloudflare-email>",
      "CLOUDFLARE_ACCOUNT_ID": "<your-account-id>",
      "NAMECHEAP_API_USER": "<your-username>",
      "NAMECHEAP_API_KEY": "<your-api-key>",
      "NAMECHEAP_CLIENT_IP": "<your-whitelisted-ip>"
    }
  }
}

For API Token auth:

{
  "infrastructure-mcp": {
    "type": "stdio",
    "command": "java",
    "args": ["-jar", "/path/to/infrastructure-mcp-1.1.1.jar"],
    "env": {
      "CLOUDFLARE_API_TOKEN": "<your-api-token>",
      "CLOUDFLARE_ACCOUNT_ID": "<your-account-id>",
      "NAMECHEAP_API_USER": "<your-username>",
      "NAMECHEAP_API_KEY": "<your-api-key>",
      "NAMECHEAP_CLIENT_IP": "<your-whitelisted-ip>"
    }
  }
}

Step 5: Verify

After restarting Claude Code, test with: - "List my Cloudflare zones" → should return zone data - "List my Namecheap domains" → should return domain list - "List Fleet apps" → should return app data (or empty if no Fleet)

If Cloudflare returns a 403/auth error, double-check the API key/email and account ID.

- [ ] **Step 2: Commit**

```bash
cd /home/matt/mcp/infrastructure-mcp
git add skill.md
git commit -m "feat: add Claude skill for guided setup"


Task 8: Update README (infrastructure-mcp)

Files: - Modify: /home/matt/mcp/infrastructure-mcp/README.md

  • [ ] Step 1: Update README

Make these changes to README.md:

  1. In the Prerequisites section, replace the Cloudflare line:

    - **Cloudflare API token** — [create one here](https://dash.cloudflare.com/profile/api-tokens)
    
    With:
    - **Cloudflare credentials** — either a [Global API Key](https://dash.cloudflare.com/profile/api-tokens) or a [scoped API Token](https://dash.cloudflare.com/profile/api-tokens)
    

  2. Replace the "2b. Manual configuration" section env vars:

    export CLOUDFLARE_API_TOKEN='your-cloudflare-api-token'
    export CLOUDFLARE_ACCOUNT_ID='your-cloudflare-account-id'
    
    With:
    # Option A: Global API Key (simpler)
    export CLOUDFLARE_API_KEY='your-global-api-key'
    export CLOUDFLARE_EMAIL='your-cloudflare-email'
    export CLOUDFLARE_ACCOUNT_ID='your-cloudflare-account-id'
    
    # Option B: Scoped API Token
    # export CLOUDFLARE_API_TOKEN='your-scoped-api-token'
    # export CLOUDFLARE_ACCOUNT_ID='your-cloudflare-account-id'
    

  3. Replace the env vars in the ~/.claude/settings.json example:

            "CLOUDFLARE_API_TOKEN": "your-token",
            "CLOUDFLARE_ACCOUNT_ID": "your-account-id",
    
    With:
            "CLOUDFLARE_API_KEY": "your-global-api-key",
            "CLOUDFLARE_EMAIL": "your-cloudflare-email",
            "CLOUDFLARE_ACCOUNT_ID": "your-account-id",
    

  4. Replace the Configuration env vars table:

Variable Description Required Default
CLOUDFLARE_API_TOKEN Cloudflare scoped API token (Bearer auth) *
CLOUDFLARE_API_KEY Cloudflare Global API Key *
CLOUDFLARE_EMAIL Cloudflare account email (required with API_KEY) *
CLOUDFLARE_ACCOUNT_ID Cloudflare account ID Yes
NAMECHEAP_API_USER Namecheap API username Yes
NAMECHEAP_API_KEY Namecheap API key Yes
NAMECHEAP_CLIENT_IP Whitelisted IP for Namecheap API Yes
FLEET_REGISTRY_PATH Path to Fleet's registry.json No /home/matt/fleet/data/registry.json
FLEET_BINARY Path to fleet CLI binary No fleet

* Provide either CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL. If both are set, the API token takes precedence.

  1. Update the JAR filename references from infrastructure-mcp-1.0.0.jar to infrastructure-mcp-1.1.1.jar.

  2. Add a "Claude Skill" section after "Quick start" section 4:

### 5. Claude Skill (optional)

Install the included skill for guided setup help:

```bash
cp skill.md ~/.claude/commands/infrastructure-setup.md

Then use /infrastructure-setup in Claude Code for step-by-step configuration guidance.

7. Update the test count if it changed (check after full test run).

- [ ] **Step 2: Commit**

```bash
cd /home/matt/mcp/infrastructure-mcp
git add README.md
git commit -m "docs: update README for dual auth, skill installation, v1.1.1"


Task 9: Update Matt's config and verify (infrastructure-mcp)

Files: - Modify: /root/.claude.json (the mcpServers.infrastructure-mcp entry)

  • [ ] Step 1: Read current config to get existing values

Read /root/.claude.json and note the current NAMECHEAP_* values and CLOUDFLARE_ACCOUNT_ID.

  • [ ] Step 2: Update config to use Global API Key auth

In the mcpServers.infrastructure-mcp.env section: - Remove CLOUDFLARE_API_TOKEN - Add CLOUDFLARE_API_KEY with the value from the current CLOUDFLARE_API_TOKEN (Matt said he gave the global API key — it was just in the wrong field) - Add CLOUDFLARE_EMAIL with value [email protected] - Keep all other env vars unchanged

Update args to point to new JAR: ["-jar", "/home/matt/mcp/infrastructure-mcp/target/infrastructure-mcp-1.1.1.jar"]

  • [ ] Step 3: Install skill
cp /home/matt/mcp/infrastructure-mcp/skill.md ~/.claude/commands/infrastructure-setup.md
  • [ ] Step 4: Verify build works

Run: cd /home/matt/mcp/infrastructure-mcp && mvn clean package -q && echo "JAR size: $(du -h target/infrastructure-mcp-1.1.1.jar | cut -f1)" Expected: BUILD SUCCESS with JAR file

  • [ ] Step 5: Note for user

Tell Matt to restart Claude Code for the new config to take effect, then test with cloudflare_list_zones.


Task 10: Runtime version from JAR manifest (all repos)

Files: - Modify: /home/matt/mcp/infrastructure-mcp/pom.xml — add manifest entries to shade plugin - Modify: /home/matt/mcp/infrastructure-mcp/src/main/java/com/infrastructure/mcp/InfrastructureMcpServer.java — read version from manifest - Modify: /home/matt/mcp/infrastructure-mcp/src/main/java/com/infrastructure/mcp/SetupTui.java — remove hardcoded JAR name fallback - Modify: /home/matt/mcp/cloudflare-mcp/pom.xml — add manifest entries - Modify: /home/matt/mcp/cloudflare-mcp/src/main/java/com/cloudflare/mcp/CloudflareMcpServer.java — read version from manifest - Modify: /home/matt/mcp/cloudflare-mcp/src/main/java/com/cloudflare/mcp/Install.java — remove hardcoded JAR name - Modify: /home/matt/mcp/google-workspace/pom.xml — add manifest entries - Modify: /home/matt/mcp/google-workspace/src/main/java/com/google/workspace/mcp/GoogleWorkspaceMcpServer.java — read version from manifest - Modify: /home/matt/mcp/reddit/src/main/java/com/reddit/mcp/RedditMcpServer.java — read version from manifest

  • [ ] Step 1: Add manifest entries to infrastructure-mcp pom.xml

Inside the ManifestResourceTransformer in the shade plugin config, add:

<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
    <mainClass>com.infrastructure.mcp.InfrastructureMcpServer</mainClass>
    <manifestEntries>
        <Implementation-Version>${project.version}</Implementation-Version>
        <Implementation-Title>${project.artifactId}</Implementation-Title>
    </manifestEntries>
</transformer>
  • [ ] Step 2: Replace hardcoded SERVER_VERSION in InfrastructureMcpServer.java

Replace:

    private static final String SERVER_VERSION = "1.1.1";
With:
    private static final String SERVER_VERSION = resolveVersion();

    private static String resolveVersion() {
        String v = InfrastructureMcpServer.class.getPackage().getImplementationVersion();
        return v != null ? v : "dev";
    }

  • [ ] Step 3: Fix hardcoded JAR name in SetupTui.java

Replace:

            jarPath = "infrastructure-mcp-1.0.0.jar";
With:
            String version = InfrastructureMcpServer.class.getPackage().getImplementationVersion();
            jarPath = "infrastructure-mcp-" + (version != null ? version : "1.1.1") + ".jar";

Add import at top of SetupTui.java if not present (it's in same package, no import needed).

  • [ ] Step 4: Apply same pattern to cloudflare-mcp

Add manifest entries to cloudflare-mcp pom.xml shade plugin:

    <manifestEntries>
        <Implementation-Version>${project.version}</Implementation-Version>
        <Implementation-Title>${project.artifactId}</Implementation-Title>
    </manifestEntries>

In CloudflareMcpServer.java, replace:

    private static final String SERVER_VERSION = "1.0.0";
With:
    private static final String SERVER_VERSION = resolveVersion();

    private static String resolveVersion() {
        String v = CloudflareMcpServer.class.getPackage().getImplementationVersion();
        return v != null ? v : "dev";
    }

In Install.java, replace hardcoded JAR references:

            System.out.println("Then re-run: java -jar cloudflare-mcp-1.0.0.jar --install");
With:
            String v = Install.class.getPackage().getImplementationVersion();
            String jar = "cloudflare-mcp-" + (v != null ? v : "1.0.1") + ".jar";
            System.out.println("Then re-run: java -jar " + jar + " --install");

And similarly for the other Install.java reference on line 107.

  • [ ] Step 5: Apply same pattern to google-workspace-mcp

Add manifest entries to google-workspace pom.xml shade plugin.

In GoogleWorkspaceMcpServer.java, replace:

    static final String SERVER_VERSION = "1.0.0";
With:
    static final String SERVER_VERSION = resolveVersion();

    private static String resolveVersion() {
        String v = GoogleWorkspaceMcpServer.class.getPackage().getImplementationVersion();
        return v != null ? v : "dev";
    }

Note: the test assertEquals("1.0.0", GoogleWorkspaceMcpServer.SERVER_VERSION) in GoogleWorkspaceMcpServerTest.java needs updating:

    // Version comes from manifest at runtime, "dev" when running tests without packaged JAR
    assertEquals("dev", GoogleWorkspaceMcpServer.SERVER_VERSION);

  • [ ] Step 6: Apply same pattern to reddit-mcp

In RedditMcpServer.java, replace the inline version:

                .serverInfo("reddit", "1.0.0")
With:
                .serverInfo("reddit", resolveVersion())

And add the helper method:

    private static String resolveVersion() {
        String v = RedditMcpServer.class.getPackage().getImplementationVersion();
        return v != null ? v : "dev";
    }

Add manifest entries to reddit pom.xml shade plugin if not present.

  • [ ] Step 7: Build and test all repos

Run:

cd /home/matt/mcp/cloudflare-mcp && mvn clean install -q
cd /home/matt/mcp/infrastructure-mcp && mvn clean test -q
cd /home/matt/mcp/google-workspace && mvn clean test -q
cd /home/matt/mcp/reddit && mvn clean test -q

Expected: All PASS

  • [ ] Step 8: Commit each repo
cd /home/matt/mcp/cloudflare-mcp
git add pom.xml src/main/java/com/cloudflare/mcp/CloudflareMcpServer.java src/main/java/com/cloudflare/mcp/Install.java
git commit -m "fix: read version from JAR manifest instead of hardcoding"

cd /home/matt/mcp/infrastructure-mcp
git add pom.xml src/main/java/com/infrastructure/mcp/InfrastructureMcpServer.java src/main/java/com/infrastructure/mcp/SetupTui.java
git commit -m "fix: read version from JAR manifest instead of hardcoding"

cd /home/matt/mcp/google-workspace
git add pom.xml src/main/java/com/google/workspace/mcp/GoogleWorkspaceMcpServer.java src/test/java/com/google/workspace/mcp/GoogleWorkspaceMcpServerTest.java
git commit -m "fix: read version from JAR manifest instead of hardcoding"

cd /home/matt/mcp/reddit
git add pom.xml src/main/java/com/reddit/mcp/RedditMcpServer.java
git commit -m "fix: read version from JAR manifest instead of hardcoding"

Task 11: Replace ASCII diagrams with Mermaid in infrastructure-mcp README

Files: - Modify: /home/matt/mcp/infrastructure-mcp/README.md

  • [ ] Step 1: Replace ASCII diagram with Mermaid

Replace the ASCII architecture diagram block:

```
┌─────────────────────────────────────────────────────────────────┐
│                     Claude Code / LLM Client                     │
│                          (MCP Client)                            │
└──────────────────────────────┬──────────────────────────────────┘
                               │ stdio
┌─────────────────────────────────────────────────────────────────┐
│                   Infrastructure MCP Server                      │
│                        12 MCP tools                              │
├──────────────────┬──────────────────┬───────────────────────────┤
│   Fleet Client   │ Namecheap Client │   Cloudflare REST Client  │
│  (registry.json  │   (XML API)      │      (API v4 JSON)        │
│   + CLI shell)   │                  │                           │
└────────┬─────────┴────────┬─────────┴─────────────┬─────────────┘
         ▼                  ▼                       ▼
   Fleet Registry    Namecheap API          Cloudflare API v4
   + fleet CLI       api.namecheap.com      api.cloudflare.com
```

With:

```mermaid
graph TD
    Client[Claude Code / LLM Client<br><i>MCP Client</i>]
    Client -->|stdio| Server

    subgraph Server[Infrastructure MCP Server]
        Fleet[Fleet Client<br><small>registry.json + CLI</small>]
        Namecheap[Namecheap Client<br><small>XML API</small>]
        Cloudflare[Cloudflare REST Client<br><small>API v4 JSON</small>]
    end

    Fleet --> FleetAPI[Fleet Registry + CLI]
    Namecheap --> NCAPI[api.namecheap.com]
    Cloudflare --> CFAPI[api.cloudflare.com]
```
  • [ ] Step 2: Commit
cd /home/matt/mcp/infrastructure-mcp
git add README.md
git commit -m "docs: replace ASCII diagram with Mermaid"