test-mcp

test-mcp

A minimal MCP server that tests Authgear's Dynamic Client Registration and resource-indicator flow, verifying JWTs bound to a specific resource.

Category
Visit Server

README

test-mcp

A minimal MCP resource server for manually testing Authgear's Dynamic Client Registration (DCR) + resource-indicator support (docs/specs/dcr.md, docs/specs/access-token-audience-binding.md in the authgear-server repo).

It does nothing interesting on its own — its only job is to sit behind Authgear-as-authorization-server and let a real MCP client exercise the whole flow: discovery → DCR self-registration → PKCE authorize+consent → token exchange bound to this server's resource → an authenticated MCP tool call.

How the pieces fit together

MCP client  --1. GET /mcp (no token)-->  test-mcp
            <--2. 401 + WWW-Authenticate: Bearer resource_metadata="..."--

MCP client  --3. GET /.well-known/oauth-protected-resource-->  test-mcp
            <--4. { resource, authorization_servers: [Authgear] }--

MCP client  --5. GET /.well-known/oauth-authorization-server-->  Authgear
            <--6. { registration_endpoint, authorization_endpoint, ... }--

MCP client  --7. POST /oauth2/register-->  Authgear   (DCR)
MCP client  --8. /oauth2/authorize + consent, resource=<RESOURCE_URI>--> Authgear
MCP client  --9. POST /oauth2/token, resource=<RESOURCE_URI>-->  Authgear
            <--10. JWT access token, aud=[RESOURCE_URI]--

MCP client  --11. POST /mcp, Authorization: Bearer <token>-->  test-mcp
            <--12. tool result (or 401 if scope/audience don't match)--

Steps 1-2 and 11-12 happen against this server. Everything in between is Authgear, discovered automatically by any spec-compliant MCP client — you don't configure the client with Authgear's URL directly.

Prerequisites

  • A running Authgear instance with DCR enabled, e.g. in authgear.yaml:

    oauth:
      dynamic_client_registration:
        enabled: true
        initial_access_token_required: false # open registration, for easy testing
    
  • A Resource registered in that project matching RESOURCE_URI below, with access_policy.allow_dynamic_third_party_client_access: true on the Resource itself and on every Scope the test tools need — otherwise a DCR client's resource= request gets invalid_target/invalid_scope. Create it via the Admin API GraphQL playground (or admin_api_graphql in an e2e test, if you're doing this from within the authgear-server repo):

    mutation {
      createResource(input: {
        resourceURI: "https://localhost:8090"
        name: "test-mcp"
        accessPolicy: { allowDynamicThirdPartyClientAccess: true }
      }) {
        resource { id }
      }
    }
    
    mutation {
      createScope(input: {
        resourceURI: "https://localhost:8090"
        scope: "read:tools"
        accessPolicy: { allowDynamicThirdPartyClientAccess: true }
      }) {
        scope { id }
      }
    }
    
    mutation {
      createScope(input: {
        resourceURI: "https://localhost:8090"
        scope: "execute:tools"
        accessPolicy: { allowDynamicThirdPartyClientAccess: true }
      }) {
        scope { id }
      }
    }
    

    https://localhost:8090 must match RESOURCE_URI below byte-for-byte, and must be this server's own real origin (scheme + host + port), not an arbitrary placeholder. Two independent constraints pin it down:

    • Authgear requires every Resource URI to be https:// (pkg/lib/resourcescope/formats.go).
    • RFC 9728 protected resource metadata's resource field is expected to match the URL (or origin) the client actually connected to, and strict clients enforce this — MCP Inspector will refuse to connect with an error like Protected resource ... does not match expected ... (or origin) if you point RESOURCE_URI at an unrelated identifier instead of the server's real address.

    That combination is exactly why this server defaults to serving HTTPS (self-signed) rather than plain HTTP: https://localhost:<PORT> is simultaneously a valid Authgear Resource URI and this server's genuine origin. If you change PORT, update the Resource's URI (and RESOURCE_URI below) to match.

Setup

npm install
npm run setup   # generates a self-signed TLS cert for localhost (see below)

Running

npm start

Environment variables (all optional):

Var Default Meaning
PORT 8090 Port this server listens on.
AUTHGEAR_ENDPOINT http://localhost:4000 Base URL of your Authgear instance. Use http://localhost:3000 if you're hitting the make start process directly, or http://localhost:3100 if you're going through the conventional local-dev nginx proxy (docker compose up -d proxy) — either way this must be wherever /.well-known/openid-configuration actually resolves.
RESOURCE_URI https://localhost:<PORT> The RFC 8707 resource identifier — must match the Resource created above, and must be this server's real origin (see above).
USE_HTTP unset Set to 1 to serve plain HTTP instead of HTTPS. Not recommended: with USE_HTTP=1, RESOURCE_URI can no longer equal this server's real origin (it'd have to be http://..., which Authgear rejects as a Resource URI), so a strict MCP client's resource-match check will fail. Only use this against a client you know doesn't enforce that check.

Testing with a real MCP client

MCP Inspector (recommended first step)

npx @modelcontextprotocol/inspector

Open the printed local URL, set the server URL to https://localhost:8090/mcp, and connect — Inspector's "Auth" panel walks through discovery, DCR, and the authorize/token exchange step by step, so you can see exactly what each response contains.

Since the cert is self-signed, you may need to tell Node to trust it for the Inspector's own outgoing requests:

NODE_EXTRA_CA_CERTS=$(pwd)/certs/localhost.crt npx @modelcontextprotocol/inspector

(Only do this for local testing — never disable certificate validation for anything that talks to a real server.)

mcp-remote (for testing against Claude Desktop)

npx mcp-remote https://localhost:8090/mcp

and point Claude Desktop's config at the resulting local stdio bridge per mcp-remote's own docs.

What to look for

  • No resource= requested (a plain OIDC client, or an MCP client that doesn't send resource): Authgear issues an opaque token to a third-party/DCR client by default. This server can't verify an opaque token at all (it isn't a JWT), so every tool call fails with 401 — this is the intended behavior (docs/specs/dcr.md, access-token-audience-binding.md): an unbound third-party token is only usable at Authgear's own /oauth2/userinfo, nowhere else.
  • resource=<RESOURCE_URI> requested: Authgear issues a JWT with aud: [RESOURCE_URI]. whoami should now succeed regardless of granted scopes; list_widgets/run_widget succeed only if the corresponding scope (read:tools/execute:tools) was granted at consent time.
  • A resource-bound token from a different resource, or one whose Resource/Scope lacks allow_dynamic_third_party_client_access: rejected at Authgear itself (invalid_target/invalid_scope) before it ever reaches this server.

Troubleshooting

  • Failed to connect ... Protected resource <X> does not match expected <Y> (or origin) (MCP Inspector, or another RFC-9728-strict client) — RESOURCE_URI is set to something other than this server's real origin. Fix RESOURCE_URI (and the matching Resource in Authgear) to be https://localhost:<PORT>, not an arbitrary placeholder — see "Prerequisites" above.
  • invalid_target at /oauth2/authorize or /oauth2/token — the Resource (and/or the specific Scope) doesn't have access_policy.allow_dynamic_third_party_client_access: true, or the resource= value the client sent doesn't exactly match what's registered.
  • 401 from this server with error_description: "fetch failed" — this server couldn't reach AUTHGEAR_ENDPOINT to fetch discovery metadata; check Authgear is actually running there.
  • 401 with a JWT-verification error — the token is real but either expired, signed by a different issuer, or bound to a different aud than RESOURCE_URI.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured