View on GitHub →
← Back to Home

Secure Your Go MCP Server

An interactive guide to implementing SchemaPin in a Go application.

The Challenge: Insecure Tool Schemas

The Model Context Protocol (MCP) relies on JSON schemas to define tool capabilities. If a schema is tampered with, an attacker can hijack your AI agent's tools. This is a critical supply chain vulnerability.

SchemaPin solves this by using cryptographic signatures to guarantee that tool schemas are authentic and unmodified.

Advanced Demo: Open In Colab

Step 1: Install CLI Tools

The command-line tools for signing are distributed via Pip and are required for the next steps.

pip install schemapin

Step 2: Create & Sign Your Schema

Create a file named get_issue_schema.json with the tool's definition.

{
  "name": "get_issue",
  "description": "Gets the contents of an issue within a repository",
  "input_schema": {
    "type": "object",
    "properties": {
      "owner": { "type": "string", "description": "Repository owner" },
      "repo": { "type": "string", "description": "Repository name" },
      "issue_number": { "type": "number", "description": "Issue number" }
    },
    "required": ["owner", "repo", "issue_number"]
  }
}

Generate your cryptographic key pair.

schemapin-keygen

Now, use your private key to sign the schema, producing a secure, signed version.

schemapin-sign --schema get_issue_schema.json \
  --key private.key \
  --output get_issue_signed.json

Step 3: Host Public Key for Discovery

For services to verify your signatures, they must be able to find your public key. The standard approach is to host it at a .well-known/schemapin.json URL on your domain. Use the automated well-known server to host your public key.

Run the well-known server, which automatically creates the proper directory structure and serves your public key. Keep this server running in a separate terminal.

python -m schemapin.well_known_server --public-key-file public.key --port 8000

Step 4: Build the Secure Go Server

Setup

go mod init secure-server
go get github.com/thirdkeyai/schemapin/go@latest
touch main.go

Add to main.go

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"github.com/thirdkeyai/schemapin/go/core"
	"github.com/thirdkeyai/schemapin/go/crypto"
	"github.com/thirdkeyai/schemapin/go/discovery"
)

var TOOL_SCHEMAS = make(map[string]interface{})

func loadAndVerifySchema(path, host, keyId string) (map[string]interface{}, error) {
	log.Printf("Verifying %s...", path)
	signedSchemaBytes, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read signed schema: %w", err)
	}

	log.Printf("Discovering key from %s...", host)
	wellKnownURL := fmt.Sprintf("%s/.well-known/schemapin.json", host)
	resp, err := http.Get(wellKnownURL)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch .well-known: %w", err)
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)

	wellKnownConfig, err := discovery.ParseWellKnownConfig(body)
	if err != nil {
		return nil, fmt.Errorf("failed to parse .well-known: %w", err)
	}

	keyStr, err := discovery.FindKeyInConfig(wellKnownConfig, keyId)
	if err != nil {
		return nil, fmt.Errorf("keyId '%s' not found: %w", keyId, err)
	}

	publicKey, err := crypto.ParsePublicKey(keyStr)
	if err != nil {
		return nil, fmt.Errorf("failed to parse public key: %w", err)
	}

	verifiedBytes, err := core.Verify(signedSchemaBytes, publicKey)
	if err != nil {
		return nil, fmt.Errorf("VERIFICATION FAILED: %w", err)
	}

	var schema map[string]interface{}
	json.Unmarshal(verifiedBytes, &schema)
	log.Printf("SUCCESS: Verified '%s'.", schema["name"])
	return schema, nil
}

func main() {
	verifiedTool, err := loadAndVerifySchema("get_issue_signed.json", "http://localhost:8000", "default")
	if err != nil {
		log.Fatalf("Could not start server: %v", err)
	}
	TOOL_SCHEMAS[verifiedTool["name"].(string)] = verifiedTool

	http.HandleFunc("/tools", func(w http.ResponseWriter, r *http.Request) {
		var names []string
		for name := range TOOL_SCHEMAS {
			names = append(names, name)
		}
		json.NewEncoder(w).Encode(names)
	})

	log.Println("Go MCP server running on port 5003")
	http.ListenAndServe(":5003", nil)
}

Run and Test Security

Run the server. Then, stop it, tamper with the signature in get_issue_signed.json, and run it again to see the fatal error.

Valid Schema Tampered
2025/06/08 14:38:00 Verifying get_issue_signed.json...
2025/06/08 14:38:00 Discovering key from http://localhost:8000...
2025/06/08 14:38:00 SUCCESS: Verified 'get_issue'.
2025/06/08 14:38:00 Go MCP server running on port 5003