Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.work
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
go 1.25.0
go 1.25.5

use (
./core
./http
./mcp
./mark3labs
)
57 changes: 57 additions & 0 deletions mark3labs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Authplane Go SDK — mark3labs

[![Go Reference](https://pkg.go.dev/badge/github.com/authplane/go-sdk/mark3labs.svg)](https://pkg.go.dev/github.com/authplane/go-sdk/mark3labs)

Adapter between the [Authplane core SDK](../core/README.md) and [`github.com/mark3labs/mcp-go`](https://github.com/mark3labs/mcp-go). Validates OAuth 2.1 JWT access tokens, serves RFC 9728 Protected Resource Metadata, bridges verified claims into per-tool-call contexts, and maps RFC 8693 token-exchange consent errors to MCP URL elicitation (JSON-RPC -32042).

If you're using the [official MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk) instead, see [`github.com/authplane/go-sdk/mcp`](../mcp/README.md).

## Install

```bash
go get github.com/authplane/go-sdk/mark3labs
```

## Quickstart

```go
package main

import (
"context"
"net/http"

"github.com/authplane/go-sdk/mark3labs/pkg/authplanemark3labs"
"github.com/mark3labs/mcp-go/server"
)

func main() {
ctx := context.Background()

adapter, err := authplanemark3labs.NewAdapter(ctx, authplanemark3labs.Options{
Issuer: "https://auth.example.com",
Resource: "https://mcp.example.com/mcp",
Scopes: []string{"tools/query", "tools/write"},
})
if err != nil {
panic(err)
}
defer adapter.Close() // stops background refresh goroutines, closes the client

mcpServer := server.NewMCPServer("My Server", "1.0.0",
server.WithToolCapabilities(false),
server.WithRecovery(),
)

streamable := server.NewStreamableHTTPServer(mcpServer,
server.WithHTTPContextFunc(adapter.HTTPContextFunc()),
)

http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler())
http.Handle("/mcp", adapter.AuthMiddleware(streamable))

http.ListenAndServe(":8080", nil)
}
```

See the **[User Guide](docs/user-guide.md)** for the full API, per-tool scope enforcement, revocation checking, token exchange, the URL elicitation propagation caveat, dev mode, and lifecycle details.
63 changes: 63 additions & 0 deletions mark3labs/demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Calculator Service Example — mark3labs/mcp-go

A minimal MCP server built with [`github.com/mark3labs/mcp-go`](https://github.com/mark3labs/mcp-go) and the Authplane adapter, demonstrating JWT authentication with per-tool scope enforcement.

The server exposes two tools:

| Tool | Required scope |
|------|----------------|
| `add` | `tools/add` |
| `multiply` | `tools/multiply` |

A token with only `tools/add` can call `add` but not `multiply`.

## Prerequisites

- Go 1.25.5+
- The **Authplane authserver** running locally — from a checkout of the `authserver` repo, run:

```bash
bash demo/mcp-demo-server-start.sh
```

This starts the auth server on `http://localhost:9000`, registers the calculator client and scopes, and creates a demo user.

## Run

```bash
cd go-sdk/mark3labs
./demo/run.sh
```

All demo credentials are pre-configured — no additional setup needed. The server starts on port `8080`.

## How it works

```
MCP Client ──Bearer JWT──► demo/main.go (port 8080)
├─ authplanemark3labs.NewAdapter()
│ • Discovers AS metadata + JWKS (RFC 8414)
│ • Validates JWT signature, aud, exp
│ • Introspects token for revocation (RFC 7662)
│ • Injects VerifiedClaims into HTTP request context
├─ server.WithHTTPContextFunc(adapter.HTTPContextFunc())
│ • Forwards claims into the per-tool-call MCP context
└─ claims.RequireScope("tools/add")
• Tool handler reads claims via ClaimsFromContext()
• Returns isError=true CallToolResult on missing scope
```

## Key patterns shown

**`ClientOptions` with `authplane.WithClientCredentials`** — providing credentials automatically wires RFC 7662 token introspection (revocation checking). No separate `WithIntrospection` option needed.

**`adapter.AuthMiddleware(streamable)`** — wraps the `*server.StreamableHTTPServer` (which is an `http.Handler`) with bearer-token verification.

**`server.WithHTTPContextFunc(adapter.HTTPContextFunc())`** — the bridge. mark3labs/mcp-go invokes this once per HTTP request to derive the context that tool handlers see. Without it, tool handlers receive a fresh context with no claims.

**`authplanemark3labs.ClaimsFromContext(ctx)`** — retrieves the `VerifiedClaims` from inside a tool handler.

**`claims.RequireScope(scope)` → `mcp.NewToolResultError(...)`** — mark3labs/mcp-go coerces any error returned from a tool handler to JSON-RPC `-32603` (INTERNAL_ERROR), so scope failures are surfaced as `IsError: true` tool results instead. The MCP client sees a structured failure with the message intact.
137 changes: 137 additions & 0 deletions mark3labs/demo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Calculator Service — Authplane mark3labs/mcp-go adapter demo.
//
// Mirrors the official-SDK demo at go-sdk/mcp/demo/, demonstrating:
// - Token introspection for revocation checking (RFC 7662)
// - Per-tool scope enforcement via ClaimsFromContext
// - Bridging verified claims from AuthMiddleware into the per-tool-call
// MCP context via server.WithHTTPContextFunc(adapter.HTTPContextFunc())
//
// Run:
//
// go run ./demo/
package main

import (
"context"
"fmt"
"log/slog"
"net/http"
"os"

"github.com/authplane/go-sdk/core/authplane"
"github.com/authplane/go-sdk/core/resource/verifier"
"github.com/authplane/go-sdk/mark3labs/pkg/authplanemark3labs"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func main() {
if err := run(); err != nil {
slog.Error("fatal", "error", err)
os.Exit(1)
}
}

func run() error {
issuer := env("ISSUER_URL", "http://localhost:9000")
resource := env("RESOURCE_URL", "http://localhost:8080/mcp")
clientID := env("CLIENT_ID", resource)
clientSecret := env("CLIENT_SECRET", "")
port := env("PORT", "8080")

ctx := context.Background()

// WithClientCredentials wires introspection (revocation) and enables token exchange.
var clientOpts []authplane.Option
if clientSecret != "" {
clientOpts = append(clientOpts, authplane.WithClientCredentials(clientID, clientSecret))
}

adapter, err := authplanemark3labs.NewAdapter(ctx, authplanemark3labs.Options{
Issuer: issuer,
Resource: resource,
Scopes: []string{"tools/add", "tools/multiply"},
DevMode: true, // relaxes SSRF for local dev; remove before deploying to production
ClientOptions: clientOpts,
})
if err != nil {
return fmt.Errorf("initialize adapter: %w", err)
}
defer func() { _ = adapter.Close() }()

mcpServer := server.NewMCPServer(
"Calculator Service",
"0.1.0",
server.WithToolCapabilities(false),
server.WithRecovery(),
)

// add — requires scope "tools/add"
addTool := mcp.NewTool("add",
mcp.WithDescription("Add two numbers"),
mcp.WithNumber("a", mcp.Required(), mcp.Description("First addend")),
mcp.WithNumber("b", mcp.Required(), mcp.Description("Second addend")),
)
mcpServer.AddTool(addTool, scopedHandler("tools/add", func(a, b float64) float64 {
return a + b
}))

// multiply — requires scope "tools/multiply"
multiplyTool := mcp.NewTool("multiply",
mcp.WithDescription("Multiply two numbers"),
mcp.WithNumber("a", mcp.Required(), mcp.Description("First factor")),
mcp.WithNumber("b", mcp.Required(), mcp.Description("Second factor")),
)
mcpServer.AddTool(multiplyTool, scopedHandler("tools/multiply", func(a, b float64) float64 {
return a * b
}))

// Wire HTTPContextFunc so AuthMiddleware's per-request claims/token reach
// each tool handler's context.
streamable := server.NewStreamableHTTPServer(mcpServer,
server.WithHTTPContextFunc(adapter.HTTPContextFunc()),
)

http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler())
http.Handle("/mcp", adapter.AuthMiddleware(streamable))

slog.Info("Calculator MCP server listening", "url", "http://localhost:"+port)

return http.ListenAndServe(":"+port, nil)
}

// scopedHandler returns a mark3labs/mcp-go tool handler that first enforces
// the required OAuth scope via ClaimsFromContext, then performs the operation.
//
// Scope failure is surfaced as a CallToolResult with IsError=true rather than
// a returned error: mark3labs/mcp-go coerces handler-returned errors to
// JSON-RPC -32603 (INTERNAL_ERROR), so an isError result is the only way to
// give the client a structured failure with details.
func scopedHandler(scope string, op func(a, b float64) float64) server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
claims := authplanemark3labs.ClaimsFromContext(ctx)
if claims == nil {
return mcp.NewToolResultError(verifier.ErrTokenMissing.Error()), nil
}
if err := claims.RequireScope(scope); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

a, err := request.RequireFloat("a")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
b, err := request.RequireFloat("b")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(fmt.Sprintf("%g", op(a, b))), nil
}
}

func env(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
20 changes: 20 additions & 0 deletions mark3labs/demo/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ADAPTER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# Demo defaults — override by setting the variable in your shell before running.
export ISSUER_URL="${ISSUER_URL:-http://localhost:9000}"
export RESOURCE_URL="${RESOURCE_URL:-http://localhost:8080/mcp}"
export PORT="${PORT:-8080}"
if [[ -z "${CLIENT_ID:-}" && -f /tmp/authserver-demo.client-id ]]; then
export CLIENT_ID="$(cat /tmp/authserver-demo.client-id)"
fi
if [[ -z "${CLIENT_SECRET:-}" && -f /tmp/authserver-demo.key ]]; then
export CLIENT_SECRET="$(cat /tmp/authserver-demo.key)"
fi

echo "==> Starting Calculator MCP server (mark3labs/mcp-go) on http://localhost:${PORT}/mcp ..."
cd "$ADAPTER_DIR"
go run ./demo/
Loading
Loading