Model Context Protocol (MCP) Go Example

This guide will walk you through how to implement MCP authentication in Go using PropelAuth's MCP support. We'll be using Go's official MCP SDK to create a simple MCP server that authenticates users using PropelAuth. We'll also create a tool in the MCP server that returns the logged in user's information.

By the end of this guide you'll have a working MCP server that you can use with the majority of AI services, such as Cursor:

Enabling MCP Authentication

Enabling and Configuring MCP Authentication

Begin by navigating to the MCP page in the PropelAuth Dashboard and enabling MCP authentication. Once enabled globally, you'll be able to enable it for any of your environments, such as test, staging, or prod.

Enabling MCP Authentication

The next step is to add a Redirect URI to the Allowed MCP Clients. This is the URL that your users will be redirected to after successfully logging in. We include several templates for the most common AI tools, including Claude Desktop, ChatGPT, and Cursor. In this guide we'll be using the template for Cursor.

Adding Redirect URI

Creating Scopes

Let's move onto scopes. With MCP auth, scopes define the specific permissions and access levels granted to an AI service, allowing servers to restrict what resources or operations the AI service can access.

For this example we'll create a scope called read:user_data. We can add this scope by navigating to the Scopes section of the MCP page and clicking Add Scope.

Creating MCP Scopes

Creating Request Validation Credentials

The last step before we start coding is to generate credentials for request validation. These credentials are used in the Introspection Endpoint to verify your user's access tokens that are sent by the AI tool to your MCP server. See the documentation here for more information.

Generate a new set of credentials by clicking the Generate Credentials button. Make sure to save the Client ID and Client Secret for the next step.

Creating Introspection Credentials

Creating a Go MCP Server

Let's create a simple MCP server. We'll use the Go's official MCP SDK to create a simple MCP server that authenticates users using PropelAuth. We'll also create a tool in the MCP server that returns the logged in user's information.

Begin by installing the Go SDK by adding it to your go.mod file:

require github.com/modelcontextprotocol/go-sdk 

We'll be using the Go MCP SDK's to do most of the heavy lifting for us. All we have to do is configure it with the necessary information, such as our Auth URL (found in the Backend Integration page of the PropelAuth Dashboard), the Client ID and Client Secret that we generated in the previous step, and the Scopes that we created earlier.

We also need to include the URL of the MCP server. In this example we'll be using http://localhost:8000.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/modelcontextprotocol/go-sdk/auth"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/modelcontextprotocol/go-sdk/oauthex"
)

const requestValidationClientID = "YOUR_CLIENT_ID"
const requestValidationClientSecret = "YOUR_CLIENT_SECRET"
const propelauthAuthUrl = "PROPELAUTH_AUTH_URL"
const mcpServerUrl = "http://localhost:8000"
var scopes = []string{"read:user_data"}

type introspectResponse struct {
	Active    bool     `json:"active"`
	ClientID  string   `json:"client_id"`
	Scope     string   `json:"scope"`
	Username  string   `json:"username"`
	Subject   string   `json:"sub"`
	Resource  string   `json:"aud"`
	ExpiresAt int64    `json:"exp"`
	IssuedAt  int64    `json:"iat"`
}

var httpAddr = flag.String("http", ":8000", "HTTP address to listen on")

// verifyAccessToken verifies the access token and returns TokenInfo for the auth middleware.
// This function implements the TokenVerifier interface required by auth.RequireBearerToken.
func verifyAccessToken(ctx context.Context, token string, _ *http.Request) (*auth.TokenInfo, error) {
	// Make a /oauth/2.1/introspect request to the auth server with the bearer token
	data := url.Values{}
	data.Set("token", token)
	requestBody := strings.NewReader(data.Encode())
	req, err := http.NewRequestWithContext(
		ctx,
		"POST",
		propelauthAuthUrl + "/oauth/2.1/introspect",
		requestBody,
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create /introspect request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(requestValidationClientID, requestValidationClientSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to cal /introspect with token: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("introspect request failed with status %d", resp.StatusCode)
	}

	buf := new(bytes.Buffer)
	_, err = buf.ReadFrom(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("error on reading /introspect response body: %w", err)
	}

	// Parse the introspection response
	introspectResponse := introspectResponse{}
	if err := json.Unmarshal(buf.Bytes(), &introspectResponse); err != nil {
		return nil, fmt.Errorf("failed to decode introspection response: %w", err)
	}

	// Check if the token is active
	if !introspectResponse.Active {
		return nil, fmt.Errorf("token is not active")
	}
	
	// check the resource is correct
	if introspectResponse.Resource != mcpServerUrl + "/mcp" {
		return nil, fmt.Errorf("token is not valid for this resource")
	}

	// the scope on the introspection response is a space-separated string
	scopes := strings.Split(introspectResponse.Scope, " ")

	return &auth.TokenInfo{
		Scopes:     scopes,
		Expiration: time.Unix(introspectResponse.ExpiresAt, 0),
		UserID:     introspectResponse.Subject,
		Extra: 		map[string]any{
			"email": introspectResponse.Username,
		},
	}, nil
}

// createMCPServer creates an MCP server with authentication-aware tools
func createMCPServer() *mcp.Server {
	server := mcp.NewServer(&mcp.Implementation{Name: "authenticated-mcp-server"}, nil)

	return server
}

func main() {
	flag.Parse()

	// Create the MCP server.
	server := createMCPServer()

	// Create authentication middleware.
	externalAuth := auth.RequireBearerToken(verifyAccessToken, &auth.RequireBearerTokenOptions{
		Scopes:              scopes,
		ResourceMetadataURL: mcpServerUrl + "/.well-known/oauth-protected-resource",
	})

	// Create HTTP handler with authentication.
	handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
		return server
	}, nil)

	// Apply authentication middleware to the MCP handler.
	authenticatedHandler := externalAuth(handler)

	// Create router for different authentication methods.
	http.HandleFunc("/mcp", authenticatedHandler.ServeHTTP)

	// Health check endpoint.
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"status": "healthy",
			"time":   time.Now().Format(time.RFC3339),
		})
	})

	// This endpoint provides OAuth configuration information to clients.
	metadata := &oauthex.ProtectedResourceMetadata{
		Resource: mcpServerUrl + "/mcp",
		AuthorizationServers: []string{
			propelauthAuthUrl + "/oauth/2.1",
		},
		ScopesSupported: []string{"read:user_data"},
	}
	http.Handle("/.well-known/oauth-protected-resource",
		auth.ProtectedResourceMetadataHandler(metadata))

	// Start the HTTP server.
	log.Println("Authenticated MCP Server")
	log.Println("========================")
	log.Println("Server starting on", *httpAddr)

	log.Fatal(http.ListenAndServe(*httpAddr, nil))
}

Above, we define a verifyAccessToken function that will handle making requests to the Introspection Endpoint to validate user tokens and return the user's information.

And that's it! We have successfully set up our MCP server with authentication. But let's create a client and hook it up to Cursor to test it out.

Creating an MCP OAuth Client

In a previous step we set an available Redirect URI that can be used for MCP clients. Now we have to make the client itself by navigating to the OAuth Clients section of the MCP dashboard and clicking Create Client.

Select the Redirect URI that we added earlier, set Client Type to public, and select Create Client.

Creating MCP Client

You'll then get back a Client ID and Client Secret. We'll use these when installing our MCP client in Cursor.

Installing the MCP Client in Cursor

Now that we have our MCP client set up, we can install it in Cursor. Navigate to the Tools & MCP section of the Cursor app and click Add Custom MCP.

This will open a mcp.json file. We'll use this file to configure our MCP client. Use the following template and replace the placeholders with your actual Client ID and Client Secret.

{
  "mcpServers": {
    "my_mcp_server": {
      "url": "http://localhost:8000/mcp",
      "auth": {
        "CLIENT_ID": "{YOUR_CLIENT_ID}",
        "CLIENT_SECRET": "{YOUR_CLIENT_SECRET}",
        "scopes": ["read:user_data"]
      }
    }
  }
}

Save your changes and navigate back to the Tools & MCP section. A MCP server will now be listed. Click the Connect button and proceed through the authentication process by logging in. You should then see a page to authorize access to the requested scope that we created earlier:

Authorizing scopes

And you're now logged in! Let's now create a tool in our MCP server to return the authenticated user's data.

Getting the Authenticated User's Information

Let's create a tool that can be used to retrieve the authenticated user's information. We'll grab the TokenInfo from the request and return it in a tool called who_am_i in our MCP server.

func WhoAmI(ctx context.Context, req *mcp.CallToolRequest, args struct{}) (*mcp.CallToolResult, any, error) {
	// Extract user information from request (v0.3.0+)
	userInfo := req.Extra.TokenInfo

	// Check if user has read:user_data scope.
	if !slices.Contains(userInfo.Scopes, "read:user_data") {
		return nil, nil, fmt.Errorf("insufficient permissions: read:user_data scope required")
	}

	userDataJSON, err := json.Marshal(userInfo)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to marshal user data: %w", err)
	}

	return &mcp.CallToolResult{
		Content: []mcp.Content{
			&mcp.TextContent{Text: string(userDataJSON)},
		},
	}, nil, nil
}

func createMCPServer() *mcp.Server {
	server := mcp.NewServer(&mcp.Implementation{Name: "authenticated-mcp-server"}, nil)

	// Add tools that require authentication.
	mcp.AddTool(server, &mcp.Tool{
		Name:        "who_am_i",
		Description: "Get user information (requires read:user_data scope)",
	}, WhoAmI)

	return server
}

When we connect to our MCP server in Cursor, we can call the who_am_i tool to retrieve the authenticated user's information.

Enabling MCP Authentication

If you require more information about the user, such as their organization membership, you can use the Fetch User By User ID API by passing the UserID value from the token response as the UserID parameter.

And that's it! You've now added authentication to your MCP server and created a tool that returns the authenticated user's information.

If you have any questions or need further assistance, feel free to reach out to our support team at support@propelauth.com.