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?
- 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.
- 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:
- Install Claude Desktop.
- Locate
claude.desktop.yaml
(this is where you register tools). - Install
uvx
(a Python package manager). - Install the
mcp-obsidian
server. - 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:
- Write an MCP server in Go that calls the PingFederate Admin API.
- Package it into a binary.
- 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 Desktop
→ Settings
→ Developer
→ Edit Config
.
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
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.