Skip to main content

From zero to a launched MCP server

This is the single linear walkthrough you need. It takes a deployed MCP server, walks it through the eight tabs of the Applications screen, and ends with you confirming that the right people get a 200, the wrong people get a 403, and unauthenticated calls get a 401 with a usable challenge.

The whole loop is about twenty minutes the first time, mostly because step 4 (mapping tools to scopes) is the only step that has actual decisions in it.

Before you start

You'll need:

  • A deployed MCP server reachable over HTTPS. The running example below uses https://20-106-226-245.sslip.io/mcp from a real screenshot — substitute your own URL anywhere you see it. Localhost works too; HTTPS is only enforced on non-loopback hostnames.
  • Admin access to AuthSec. If you can see the Applications screen, you're good.
  • An AuthSec SDK installed in your MCP server's codebase. The Go SDK and the Python SDK are at full parity — pick whichever matches your stack. The TypeScript SDK ships the legacy decorator API today; a mountMcp-equivalent is on the roadmap.
  • An OAuth client to test with. Any of Claude Desktop, Codex, or plain curl works. If you're testing with curl, you'll also want jq to read JSON responses.

Total moving parts: your MCP server, the AuthSec backend, an OAuth client. The AuthSec backend handles the OAuth dance, scope enforcement, and the admin UI. The SDK lives inside your MCP server and gates each tool call.


Step 1 — Register the application

Open the Applications screen, click Install protection, and paste your MCP server's URL.

That's it for the UI. Behind the scenes, AuthSec creates a resource_servers row with state = 'pending_scan' and gives you back:

{
"id": "525da3b4-4206-4070-ad68-90cc3a6de43b",
"introspection_secret": "9b7c5e...",
"issuer_url": "http://localhost:7468",
"jwks_uri": "http://localhost:7468/oauth/jwks",
"introspection_endpoint": "http://localhost:7468/oauth/introspect",
"resource_url": "https://20-106-226-245.sslip.io/mcp",
"scope_matrix_url": "http://localhost:7468/authsec/resource-servers/525da3b4-.../sdk-policy",
"manifest_url": "http://localhost:7468/authsec/resource-servers/525da3b4-.../sdk-manifest"
}

Keep this response. Every value in it maps directly to a field of the Go SDK's Config struct in step 9. Pasting from the response is the no-drift way to configure the SDK — don't try to hand-write these URLs.

The introspection_secret is only shown once. If you lose it, rotate it from the application detail panel (POST /authsec/resource-servers/:id/rotate-introspection-secret).

More detail: Register an application.


Step 2 — Scan tools

Click the Tools tab → Scan now.

AuthSec hits your /mcp URL with a synthetic JSON-RPC tools/list request, enumerates every tool you expose, and stores them in mcp_tools with inventory_source = "mcp_scan". The example screenshot shows 101 tools discovered from a GitHub MCP server.

If your server requires authentication on tools/list, the scan fails — state flips to scan_failed and you'll see the reason in the UI. Most MCP servers expose tools/list unauthenticated (the AuthSec SDK adds OAuth on top later); if yours doesn't, you have two options:

  1. Loosen tools/list to unauthenticated. The SDK will re-protect it once installed.
  2. Use the ToolInventoryProvider escape hatch in Go SDK config to skip synthetic enumeration entirely.

More detail: Tool inventory and discovery.


Step 3 — Define scopes

Click the Protect tab. AuthSec proposes a starter scope set keyed to your resource server:

  • rs-<id>:tools:read
  • rs-<id>:tools:write
  • rs-<id>:admin

In the running example: 9 scopes already defined. Accept the defaults if you're just starting out — you can always add custom scopes later for tools that don't fit cleanly into read/write.

Scope IDs become the scope claim values on tokens. The naming convention matters because every token your MCP server validates will carry these strings; pick something stable.

For the tradeoffs (per-tool vs per-operation vs catch-all), read Scope design for MCP tools. For the mechanics, Define scopes.


Step 4 — Map tools to scopes

This is the slow step, because it's the only one with real decisions in it.

Click the Tools tab. For each of the 101 tools, you have three choices:

  • Map to a scope — the most common path. Tool requires that scope on the token to be callable.
  • Mark as Public — no auth required. Use sparingly; only for things truly safe to expose unauthenticated.
  • Leave unmapped — the tool stays in the inventory but cannot be called by anyone (denied at runtime).

The activation preview at the top tracks mapped / public / unmapped counts in real time. The running example screenshot shows:

TotalMappedPublicUnmapped
1015196

96 unmapped is a launch blocker. The Activate button on the Launch tab stays disabled until you address them.

For bulk patterns — e.g. "every tool starting with actions_get_ is read scope" — use the API directly. See the bulk-mapping example in Map tools to scopes.


Step 5 — Default access

Click the Access tab.

Pick a default role (likely viewer, which is auto-created during registration with no scopes). Toggle Enabled so the policy fires on first_successful_login. Every new user who logs in for the first time gets this role automatically.

Then add manual exceptions for yourself as admin — if you don't, you'll lock yourself out the first time you test. The exception table tracks assignment_source (MANUAL_ADMIN for ones you add by hand vs. DEFAULT_POLICY for auto-grants), so admin overrides survive policy changes later.

The Access tab also previews per-tool grants on the right side. Per-scenario simulation — "what would user X see with client Y on tool Z?" — is on the roadmap. See Access policy and default role for what works today.


Step 6 — Pre-register your first client (optional)

Click the Clients tab.

If you're integrating Claude Desktop, Codex, or any client that supports Dynamic Client Registration (DCR), skip this step. The client registers itself when a user first tries to connect, and AuthSec auto-approves DCR registrations. The example screenshot shows 30 such clients, all APPROVED.

If you're testing with curl or a custom CLI that doesn't do DCR, click Manage clientsAdd, paste a redirect URI, and you'll get a client_id to use.

More detail: Pre-register OAuth clients.


Step 7 — Run the protection test

Click the Test tab → Run test.

This calls POST /authsec/resource-servers/:id/test-login and reports back AuthSec's view of the world:

  • OAuth state — is Hydra ready, are credentials provisioned
  • SDK policy state — has the scope matrix been populated
  • Tool count and unmapped count — matches what you saw in step 4

It's a setup signal, not a runtime simulator. It doesn't acquire a token, doesn't call your live MCP server, and doesn't prove that a specific user can call a specific tool. Those are coming — see Run the protection test for the honest delta. For now, this confirms AuthSec is ready to start enforcing; you'll prove the rest yourself in step 10.


Step 8 — Activate

Click the Launch tab.

The setup checklist shows the six steps you just did. If every checkbox is green, the Activate button lights up. Click it.

That calls POST /authsec/resource-servers/:id/activate, which flips state = 'ready', sets setup_completed_at, and starts enforcing the policy preview at runtime. The application card on the Applications screen changes from "Not launched" to launched.

From this moment, your MCP server — once it has the SDK installed in step 9 — will reject calls that don't carry an appropriately-scoped token.

If Activate is greyed out, the checklist tells you which step still needs work. The most common blocker is unmapped tools (step 4); the next most common is a disabled access policy (step 5).

More detail: Launch the application.


Step 9 — Drop in the Go SDK

In your MCP server's main file, replace your HTTP handler registration with the AuthSec wrap. The full executable example is at sdk-authsec/packages/go-sdk/examples/firstrun/main.go — paste from there, not from this page, so you stay in sync with whatever the SDK actually does today.

The minimal shape:

import authsec "github.com/authsec-ai/sdk-authsec/packages/go-sdk"

cfg := authsec.Config{
// Paste from the POST /authsec/resource-servers response in step 1:
Issuer: "http://localhost:7468", // issuer_url
AuthorizationServer: "http://localhost:7468", // also issuer_url
JWKSURL: "http://localhost:7468/oauth/jwks", // jwks_uri
IntrospectionURL: "http://localhost:7468/oauth/introspect", // introspection_endpoint
ResourceServerID: "525da3b4-4206-4070-ad68-90cc3a6de43b", // id
IntrospectionClientID: "525da3b4-4206-4070-ad68-90cc3a6de43b", // same as id
IntrospectionClientSecret: os.Getenv("AUTHSEC_INTROSPECTION_SECRET"), // introspection_secret
ResourceURI: "https://20-106-226-245.sslip.io/mcp", // resource_url
PublishManifest: true,
}

mux := http.NewServeMux()
if err := authsec.MountMCP(mux, "/mcp", yourMCPHandler, cfg); err != nil {
log.Fatal(err)
}
http.ListenAndServe(":8000", mux)

MountMCP registers two things on the mux:

  1. Your MCP handler at /mcp, wrapped with token validation and scope checking.
  2. The RFC 9728 protected-resource metadata endpoint that the 401 challenge points at.

Restart your server.

More detail: Go SDK reference.


Step 10 — Reproduce 401, 403, 200

Three curls. If all three behave as described, the integration is live.

Unauthenticated → 401 with a working challenge:

curl -sv -X POST https://your-mcp-server/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Expected response: status 401, header WWW-Authenticate: Bearer resource_metadata="https://your-mcp-server/.well-known/oauth-protected-resource/mcp".

Hit the metadata URL — it should return 200 with a JSON document listing the authorization server and supported scopes. If it 404s instead, the SDK isn't mounted correctly; check step 9.

Read-scoped token calling a write tool → 403 insufficient_scope:

Get a token via the Hydra authorization-code flow (any OAuth client works; Claude Desktop is the easiest path if you don't want to script curl). Ask for mcp:tools:read only. Then:

curl -sv -X POST https://your-mcp-server/mcp \
-H "Authorization: Bearer $READ_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"actions_create_pr"}}'

Expected: 403, body {"error":"insufficient_scope","required_scopes":["rs-...:tools:write"]}.

Properly-scoped token → 200:

Acquire a token with mcp:tools:write in scope. Call the same write tool. Expected: 200 with the tool's response.

If all three behave correctly, you've completed the loop.

If any one is off, see Troubleshoot OAuth tokens on MCP servers — there's a four-claim checklist (aud, iss, exp, scope) that resolves the vast majority of cases.


Step 11 — Watch it run

Click the Monitor tab.

Today this surfaces drift events — anything that changes the active policy after launch: scopes deleted, tools unmapped, default role disabled, introspection secrets rotated. Every event has a payload telling you what changed, when, and by which admin.

Runtime metrics — denied-call counts, insufficient_scope error rates, policy health — aren't in the Monitor tab yet. They're a roadmap item. For now, your MCP server's own logs are the best source of operational signal; the Go SDK logs every authorization decision via the standard slog interface. Wire that into whatever observability stack you already have.

See Monitor drift and runtime for the full drift event catalog and the workaround for the runtime-metrics gap.


You're done

At this point:

  • Your MCP server validates every tool call's token.
  • The right people see the right tools (tools/list is filtered by granted scopes).
  • The wrong people get 403 insufficient_scope, not silent allows.
  • Drift between what admins configure and what's actually enforced shows up in the Monitor tab.

What's next:

  • Add more scopes as your tool surface grows. The Tools tab makes scope updates a one-click affair.
  • If you have a Python MCP server, the Python SDK is at full Go parity — mount_mcp(app, "/mcp", handler, cfg) does the same job as authsec.MountMCP above.
  • If you have a TypeScript MCP server, see the TypeScript SDK. A mountMcp-equivalent is on the roadmap; the legacy decorator path works today.
  • If something didn't fit your situation, the two pages most likely to help are Token introspection vs JWT for latency tuning and Preventing confused-deputy attacks for multi-server token flow.