For the past year, I’ve deliberately stayed away from learning how to build AI systems. My focus was on being a smart consumer of AI, not an implementer.

Why?

  1. After reading Make Your Own Neural Network, I realized that I didn’t want to spend my time chasing the rapid innovation in AI model design. Instead, I wanted to master using AI effectively.
  2. Over time, the AI hype shifted from “build models” to “integrate AI into tools.” That mindset resonated with me: I’d rather spend energy integrating AI into workflows than re-learning ML algorithms.

Because of this, I also stayed away from the whole agentic AI wave.

But recently, in my homelab, I needed to spin up Proxmox LXCs multiple times for testing. I already had Terraform, but I didn’t want to maintain state files, pipelines, or deal with the overhead. I just wanted to tell Claude what I needed and let it handle it.

That exploration led me to MCP (Model Context Protocol).


Why MCP Clicked for Me

MCP feels a lot like wrtiting Terraform providers. Instead of managing infrastructure or custom config with terraform provider, you build tools that expose APIs to the AI. Claude (or another LLM client) can then call those APIs to interact with your systems.

  • You define an API for your tool.
  • MCP runs your tool as a child process.
  • When you prompt Claude, it can call into your tool through MCP.

The beauty is: you don’t need to over-engineer pipelines or write extra files.


Starting Simple: Obsidian

To learn MCP, I began with something simple — integrating Claude with my local Obsidian notes.

The setup is straightforward:

  1. Install Claude Desktop.
  2. Locate claude.desktop.yaml (this is where you register tools).
  3. Install uvx (a Python package manager).
  4. Install the mcp-obsidian server.
  5. Install the Obsidian “REST API” plugin to expose your vault over HTTP.

Plenty of tutorials already cover this, so I won’t go deep here. Instead, let’s move to something more interesting.


Building a Custom MCP Tool for PingFederate

Now for the fun part. I run a local PingFederate SSO server that manages several signing certificates. I wanted Claude to fetch expiring certificates whenever I asked:

“Get me the PingFederate expiring certificates.”

The plan was simple:

  1. Write an MCP server in Go that calls the PingFederate Admin API.
  2. Package it into a binary.
  3. Register the binary in Claude’s configuration so it can use it.

Here’s how.


Step 1: Write the Tool in Go

Below is a minimal MCP server written with mcp-go. It makes a request to PingFederate’s /keyPairs/signing API and returns the results to Claude.

package main

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// handlePingFederateCerts fetches signing key pairs from PingFederate Admin API
func handlePingFederateCerts(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	url := "https://localhost:9999/pf-admin-api/v1/keyPairs/signing"
	apiToken := "Basic <replace-with-your-token>"

	// Allow self-signed certs for local testing
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{Transport: tr, Timeout: 10 * time.Second}

	// Build request
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", err)), nil
	}
	req.Header.Set("accept", "application/json")
	req.Header.Set("X-XSRF-Header", "PingFederate")
	req.Header.Set("Authorization", apiToken)

	// Send request
	resp, err := client.Do(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
	}
	defer resp.Body.Close()

	// Read body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", err)), nil
	}

	// Pretty-print JSON if possible
	var prettyJSON map[string]interface{}
	if json.Unmarshal(body, &prettyJSON) == nil {
		pretty, _ := json.MarshalIndent(prettyJSON, "", "  ")
		return mcp.NewToolResultText(string(pretty)), nil
	}
	return mcp.NewToolResultText(string(body)), nil
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)

	// Define MCP server
	s := server.NewMCPServer("pingfederate-tool", "1.0.0")

	// Register the PingFederate tool
	pfTool := mcp.NewTool(
		"pingfederate_signing_keys",
		mcp.WithDescription("Fetch signing key pairs from PingFederate Admin API"),
	)
	s.AddTool(pfTool, handlePingFederateCerts)

	log.Println("PingFederate tool registered. Server ready.")

	// Start serving via stdio
	if err := server.ServeStdio(s); err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

Step 2: Build the Binary

Compile the Go code into a binary: go build -o pingfederate-server main.go

Step 3: Register with Claude

Open Claude DesktopSettingsDeveloperEdit Config.

Example image

Add your new MCP server to the JSON config:

{ “mcpServers”: { “filesystem”: { “command”: “npx”, “args”: [ “-y”, “@modelcontextprotocol/server-filesystem”, “/Users/you/Desktop”, “/Users/you/Downloads” ] }, “mcp-obsidian”: { “command”: “/Users/you/.local/bin/uvx”, “args”: [“mcp-obsidian”], “env”: { “OBSIDIAN_API_KEY”: “your-api-key” } }, “pingfederate-server”: { “command”: “/Users/you/dev/prototypes/pingfederate-server”, “args”: [] } } }

Restart Claude, and it will now detect your tool. When you type:

Get me PingFederate expiring certificates

Example image

Claude will call your binary, fetch the data, and return the results.

Wrapping Up

This was my first real dive into MCP tool building, and I loved how simple it was:

You can extend this approach to any API or system you run locally. The same method works for Proxmox, file systems, or any service with an API. I will post that as well in future.