Skip to main contentArrow Right

Table of Contents

This tutorial was written by Kevin Kimani, a passionate developer and technical writer who enjoys explaining complicated concepts in a simple way. You can check out his GitHub or X account to connect and see more of his work!


Model Context Protocol (MCP) servers allow developers to connect large language models (LLMs) to external tools and resources through a standardized protocol. Some MCP servers are designed to run locally, often because they need to access resources that are in the same environment as the AI agent. Others are designed to run remotely, such as when exposing shared APIs, cloud-hosted tools, or team-wide services. Remote access is powerful, but without proper controls, it can easily become a security risk.

An unsecured MCP server exposed to the internet can be discovered by automated scanners, abused by unauthorized users, or even used to extract sensitive data from connected resources. Nevertheless, with proper authentication and authorization in place, remote MCP servers can unlock new collaboration models. Teams can share cloud-hosted tools and services, manage who can access which resources, and even integrate audit logs for compliance and observability. This makes MCP servers safer and more practical in enterprise or team-based environments.

In this guide, you'll learn how to take a local Playwright MCP server and make it remote-ready with authentication powered by Descope. You'll expose your server for secure remote access, add user authentication using Descope Flows, and implement role-based access control (RBAC) to ensure only authorized users can access sensitive tools.

Prerequisites

To complete this tutorial, you need the following:

  • A Descope account

  • Node.js v18+ installed on your local machine.

  • The assets ZIP folder downloaded and extracted. The folder includes a preprepared Descope flow that you will use later in this guide.

  • A code editor and a web browser.

Preparing the local MCP server

The Playwright MCP server exposes a set of tools that let LLMs interact with Playwright to perform actions such as running browser tests, inspecting pages, taking screenshots, and more. For example, you can use it to build an AI agent that automates quality assurance by navigating to a web application, filling out forms, and verifying that the UI behaves correctly. These tasks otherwise require manual testing or complex custom scripts.

When running locally, the Playwright MCP server is accessible only from your own machine. This is ideal for initial development and testing as there's no risk of unauthorized access. However, if you want to expose this server remotely, for example, to integrate it into a cloud-based workflow or share it with your team, you need to make it accessible over the internet. This is where you need to start thinking about security; anyone who invokes the server's URL can invoke its tools and potentially run arbitrary browser automation on your infrastructure.

Let's go ahead and set up the Playwright MCP server locally. Start by creating a new project folder and initializing a Node.js project:

mkdir descope-playwright-mcp-auth
cd descope-playwright-mcp-auth

npm init -y

Install the Playwright MCP server, which is offered as an npm package by Microsoft:

npm install @playwright/mcp

Open the package.json file and replace the scripts section with the following content that provides a command to run the MCP server as a standalone server and adds the port flag to enable HTTP transport:

"scripts": {
   "start:mcp": "mcp-server-playwright --port 3001"
},

You can now run the Playwright MCP server by executing the following command in the terminal:

npm run start:mcp

You should get the following output:

> descope-playwright-mcp-auth@1.0.0 start:mcp
> mcp-server-playwright --port 3001

Listening on http://localhost:3001
Put this in your client config:
{
 "mcpServers": {
   "playwright": {
     "url": "http://localhost:3001/mcp"
   }
 }
}
For legacy SSE transport support, you can use the /sse endpoint instead.

To test that the Playwright MCP server is running as expected, you can use MCP Inspector. The MCP Inspector is a browser-based debugging tool that lets you interact with MCP servers without writing any client code. It's perfect for quickly verifying that your server is exposing the right tools and responding correctly before integrating it with an actual AI agent. On a separate terminal, execute the following command to start the MCP Inspector:

npx @modelcontextprotocol/inspector@0.17.2 --transport http --server-url http://localhost:3001/mcp

The --transport flag specifies how the client communicates with the MCP server. MCP supports three transport types: http (streamable HTTP), sse (server-sent events (SSE) for streaming), and stdio (standard input/output for local processes). Since the Playwright MCP server is running as an HTTP server on port 3001, you're using the http transport to connect to it.

This opens a browser tab where you can see the MCP Inspector interface. On the interface, select the Connect button to connect to your MCP server:

Fig: MCP Inspector interface
Fig: MCP Inspector interface

Once you're connected, you can perform actions such as listing the tools or invoking them:

Fig: Connected to MCP server
Fig: Connected to MCP server

Making the MCP server remote-capable

So far, the Playwright MCP server is listening on localhost:3001, which means it's accessible only from your own machine. No external connections can reach it because it's bound to localhost (127.0.0.1). This isolation makes it safe for local testing, but it also means teammates can't connect or share the same tools. To enable collaboration, you need to expose it over the network, but doing so requires proper authentication and authorization to prevent unauthorized access.

There are several approaches you can use to make the MCP server remotely accessible:

  1. Binding to all interfaces (--host 0.0.0.0) exposes your server directly to your network but offers no authentication.

  2. Using an authentication proxy offers programmatic control over authentication and authorization.

For this guide, you will use the authentication proxy approach with Express. This gives you full control over authentication logic and RBAC. It also integrates well with the Descope MCP Express SDK, which is designed to allow you to easily add MCP specification-compliant authorization to your MCP server. The authentication proxy sits between clients and the MCP server, and validates every request before forwarding it.

Start by installing the required dependencies for the authentication proxy:

npm i express cors http-proxy @descope/mcp-express dotenv
npm i -D typescript @types/node @types/express @types/cors @types/http-proxy ts-node

Create a new file named .env to store your configuration and add the following:

AUTH_PROXY_PORT=3000
MCP_SERVER_PORT=3001

This defines the ports assigned to the Playwright MCP server and the authentication proxy.

Define your TypeScript configuration by creating a new file named tsconfig.json and add the following:

{
   "compilerOptions": {
       "target": "ES2022",
       "module": "commonjs",
       "outDir": "./dist",
       "rootDir": "./src",
       "strict": true,
       "esModuleInterop": true,
       "skipLibCheck": true
   }
}

To implement the proxy logic, create a new file named src/auth-proxy.ts and add the following:

import "dotenv/config";
import express from "express";
import cors from "cors";
import httpProxy from "http-proxy";



const app = express();
app.use(cors());



const AUTH_PROXY_PORT = process.env.AUTH_PROXY_PORT || 3000;
const MCP_SERVER_PORT = process.env.MCP_SERVER_PORT || 3001;



// Create proxy instance targeting the localhost-bound MCP server
const proxy = httpProxy.createProxyServer({
   target: `http://localhost:${MCP_SERVER_PORT}`,
   changeOrigin: true,
   ws: false,
});



// Handle proxy errors
proxy.on("error", (err, req, res) => {
   console.error("Proxy error:", err);
   try {
       (res as any).writeHead?.(500, { "Content-Type": "text/plain" });
       (res as any).end?.("Proxy error");
   } catch (e) {
       // Ignore if already closed
   }
});



// Proxy all /mcp requests
app.use("/mcp", (req: any, res: any) => {
   proxy.web(req, res);
});



app.listen(AUTH_PROXY_PORT, () => {
   console.log(`Server running on port ${AUTH_PROXY_PORT}`);
});



process.on("SIGINT", () => {
   console.log("Shutting down server...");
   process.exit();
});

This code sets up a simple Express proxy server that listens on port 3000 and forwards all /mcp requests to the local Playwright MCP server running on port 3001. It uses http-proxy for request forwarding, applies cors to allow cross-origin access, and includes basic error handling.

Note: Please note that this setup supports HTTP transport only. Clients attempting to use SSE via the /sse endpoint or WebSocket connections do not function with this configuration. Supporting SSE requires additional setup to handle streaming responses, and WebSocket support requires the ws proxy option enabled. For most remote MCP deployments, HTTP transport is sufficient and easier to secure.

To make sure that the proxy is working as expected, run the MCP server using the command npm run start:mcp. In a separate terminal, run the proxy server:

npx nodemon --exec 'ts-node' src/auth-proxy.ts

Note: The --exec ts-node flag explicitly tells nodemon to use ts-node to execute the TypeScript file.

Run the MCP Inspector:

npx @modelcontextprotocol/inspector@0.17.2 --transport http --server-url http://localhost:3000/mcp

Note: Note that the --server-url flag is now pointing to the proxy server.

Once the MCP Inspector interface launches, you should be able to connect to the MCP server, list tools, and invoke them.

At this stage, your MCP server is accessible to anyone who can reach the exposed host and port. There's no authentication, no encryption (TLS), and no request validation. This means that anyone can potentially connect to your MCP server and invoke any tool.

Introducing authentication with Descope

As you saw in the previous section, anyone who discovers your proxy server's URL can connect to your MCP server and invoke any available tool. In production or shared development environments, this opens doors to unauthorized access and abuse.

For a Playwright MCP server, this can mean unauthorized users running browser automation on your infrastructure, executing scripts that capture sensitive data from your environment, or triggering resource-intensive operations that consume server resources. Without authentication, you have no control over who has access to your tools and no way to audit usage or attribute usage to specific users.

Descope simplifies securing your MCP server with its visual, low-code flow editor. Instead of writing authentication logic from scratch, you can build complete authentication flows through a drag-and-drop interface. These flows define the entire authentication journey, from the user sign-in method, multifactor authentication (MFA), consent screens for third-party client access, and the process when authentication fails or succeeds. This approach lets you implement enterprise-grade security in minutes rather than days, while maintaining full control over the authentication experience.

The primary step to implementing authentication with Descope is to create a project. On your Descope console, click the project drop-down on the top navigation pane, and select + Project:

Fig: Creating a new project
Fig: Creating a new project

On the Create project form, provide "descope-playwright-mcp-auth" as the name of the project and select Create:

Fig: Providing project details
Fig: Providing project details

Next, navigate to the Flows page from the sidebar and click the Import flow button:

Fig: Importing a flow
Fig: Importing a flow

Upload the mcp-auth-consent.json file from the assets you downloaded earlier. This opens the flow in the Flow editor:

Fig: Flow editor
Fig: Flow editor

This flow has been created using the Descope flow editor by dragging and dropping authentication components, such as the user.loggedIn condition, authentication screens, verification actions, and consent conditions, onto the canvas and connecting them to define authentication logic. You can build similar flows either from scratch or from a template and use the available components. This tutorial uses a preprepared flow that includes the auth logic and consent flow to save time, but feel free to explore the editor and customize it later.

The flow starts by checking if the user is logged in. If they're not, they are redirected to a screen where they can sign in using either an emailed one-time password (OTP) or Google single sign-on (SSO). Once logged in, the flow checks if the third-party application (the MCP client) has been authorized. If not, they're redirected to a screen where the scopes being requested by the app are displayed, and the user can choose whether to authorize the app or not.

With the auth and consent flow in place, you need to set up Dynamic Client Registration (DCR). This protocol allows third-party client applications to register themselves automatically with your Descope project, instead of being manually preconfigured. When an MCP client, like the MCP connector, connects to your MCP server, it automatically initiates the DCR process by sending a registration request to the proxy's /oauth/register endpoint (provided by the Descope MCP Express SDK).

The proxy handles the client registration, creates OAuth credentials for the client, and returns them so the client can proceed with authentication. All this happens behind the scenes. The MCP Inspector takes care of the DCR flow automatically once you point it to your proxy's URL, which is why you don't see explicit DCR code in the client implementation.

To set up DCR, navigate to the Inbound Apps page on your Descope console and select DCR Settings:

Fig: Inbound Apps page
Fig: Inbound Apps page

On the DCR Settings page, toggle the switch to enable DCR; and in the Approved scopes list section, provide the following:

  • Name: "openid"

  • Description: "Allows this app to confirm your identity and access basic profile information, such as your assigned roles."

  • Mandatory: Toggle to make sure this scope is mandatory

Fig: Approved scopes
Fig: Approved scopes

Next, scroll down to the User consent flow section, and in the Consent flow drop-down, select the flow you imported in the previous steps and save the settings:

Fig: User consent flow
Fig: User consent flow

Now that the DCR is configured, you need to obtain credentials that your proxy server uses to communicate with the Descope APIs. The proxy needs these credentials to validate tokens, manage client registrations, and enforce authentication policies.

Start by obtaining the project ID, which identifies your Descope project and tells the proxy which project's authentication rules to enforce. You can get this by navigating to the Project page:

Fig: Project page
Fig: Project page

You also need a management key to give your proxy server permission to perform administrative operations, like managing direct client registrations. Navigate to the Management Keys page, select + Management Key, and provide the key details by specifying the name and the roles:

Fig: Generating a management key
Fig: Generating a management key

Integrating Descope into the Playwright MCP server

At this point, you have completed setting up Descope. You enabled DCR so MCP clients can register automatically, configured the authentication and consent flow that users go through, and obtained credentials your proxy server needs to enforce authentication. The next step is to use these credentials to build the authentication proxy.

With your Descope project set up, you can now integrate authentication into the proxy. The goal is to make sure that every request is verified before it's proxied to the MCP server.

Before diving into the implementation, it's important to understand how the authentication flow works:

  1. The MCP client, in this case, the MCP Inspector, registers with your proxy via the DCR.

  2. You're redirected to the authentication/consent flow you set up earlier in Descope.

  3. You log in and consent to the request scopes.

  4. Descope issues an access token to the MCP client.

  5. The MCP client includes this token in its requests to the proxy server.

  6. The proxy server validates the token before forwarding the request to the MCP server.

To handle auth in the proxy, use the Descope MCP Express SDK you installed earlier. Start by adding the following to your .env file:

DESCOPE_PROJECT_ID=<YOUR-DESCOPE-PROJECT-ID>
DESCOPE_MANAGEMENT_KEY=<YOUR-DESCOPE-MANAGEMENT-KEY>
SERVER_URL=http://localhost:3000

The SERVER_URL ENV variable refers to the URL that exposes your MCP server; in this case, it's the proxy URL. Make sure to replace the placeholders for DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY with the values you obtained from your Descope console.

Next, open the src/auth-proxy.ts file and add the following imports:

import {
   descopeMcpAuthRouter,
   descopeMcpBearerAuth,
   DescopeMcpProvider,
} from "@descope/mcp-express";
Just below the MCP_SERVER_PORT definition, add the Descope MCP provider configuration:
const descopeProvider = new DescopeMcpProvider({
   projectId: process.env.DESCOPE_PROJECT_ID!,
   managementKey: process.env.DESCOPE_MANAGEMENT_KEY!,
   serverUrl: process.env.SERVER_URL!,



   dynamicClientRegistrationOptions: {
       authPageUrl: `https://api.descope.com/login/${process.env
           .DESCOPE_PROJECT_ID!}?flow=mcp-auth-consent`,
   },
});



// Add OAuth metadata and DCR endpoints
app.use(descopeMcpAuthRouter(undefined, descopeProvider));



// Protect your MCP endpoints with bearer authentication
app.use(["/mcp"], descopeMcpBearerAuth(descopeProvider));

This code initializes DescopeMcpProvider with your Descope project credentials and the proxy server URL. It also configures the DCR by specifying the URL to the auth and consent flow you set up earlier in the Descope console. The descopeMcpAuthRouter() function adds the required OAuth metadata endpoints and the DCR /register endpoint. The descopeMcpBearerAuth() function protects the specified endpoints by checking the incoming request for a bearer auth token and attaches the user information to req.auth.

Adding authorization to the Playwright MCP server

At this point, your proxy requires authentication but doesn't enforce RBAC, which restricts access to tools based on user roles. Authorization determines what authenticated users are allowed to do. For example, you may want to prevent regular users from installing browser binaries (which can consume significant resources or introduce security risks) while allowing admins full access to all tools.

For this guide, you'll use the already defined Tenant Admin role in Descope to represent a user with full access to all Playwright MCP tools. Only a user with this role can invoke the browser_install tool. In a production environment, you define custom roles and permissions that map to specific MCP tools or categories of tools. You maintain a configuration file or database that maps tool names to required roles and then check user roles against this mapping before allowing tool invocation. This tutorial uses a simple hard-coded check with a built-in Tenant Admin role to demonstrate the authorization pattern.

Start by adding the following import statement to the src/auth-proxy.ts file:

import { Readable } from "stream";
Next, add the following code before the line where you proxy /mcp requests:
app.use("/mcp", express.json(), async (req: any, res: any, next: any) => {
   // Only check authorization for POST requests with a body
   if (req.method == "POST" && req.body) {
       const mcpReq = req.body;



       // Check if the MCP method is a tools call
       if (mcpReq.method === "tools/call") {
           const toolName = mcpReq.params?.name;
           console.log(`Accessing tool: ${toolName}`);



           // If the tool is an admin tool, verify user has admin role
           if (toolName === "browser_install") {
               const authInfo = await descopeProvider.descope.validateJwt(
                   req?.auth?.token!
               );
               const isAdmin = descopeProvider.descope.validateRoles(
                   authInfo,
                   ["Tenant Admin"]
               );



               if (!isAdmin) {
                   console.log(
                       `Unauthorized access attempt to admin tool: ${toolName}`
                   );
                   return res.status(403).json({
                       jsonrpc: "2.0",
                       error: {
                           code: -32000,
                           message: `Access denied: Tool '${toolName}' requires Tenant Admin role.`,
                       },
                       id: mcpReq.id || null,
                   });
               }
           }
       }



       // Convert object back to string for the proxy
       req.body = JSON.stringify(mcpReq);
       req.headers["content-length"] = Buffer.byteLength(req.body).toString();
   }



   next();
});

This middleware intercepts all requests to the /mcp endpoint and uses express.json() to parse incoming JSON bodies so they can be inspected. The middleware specifically looks for tool invocation requests (identified by tools/call method) and checks the tool name against your authorization rules.

For the restricted browser_install tool, it validates the user's JWT using the Descope SDK and ensures that they have the Tenant Admin role. Unauthorized requests are blocked with a 403 error, while valid requests continue through to the MCP server. The middleware reconstructs the request body and updates its headers so it can be properly proxied to the MCP server.

Finally, update the proxy handler to properly forward the reconstructed body:

app.use("/mcp", (req: any, res: any) => {
   proxy.web(req, res, {
       buffer: Readable.from([req.body ?? ""]),
   });
});

This code creates a readable stream from the reconstructed request body because the proxy library expects streaming data, not a plain string. After the middleware parses and potentially modifies the JSON body, you need to convert it back into a stream format that the proxy can forward to the upstream MCP server.

Your proxy now enforces both authentication and RBAC before forwarding requests to the MCP server.

Testing the integrations

You've set up authentication and authorization, so let's test that authenticated users can connect to your MCP server, unauthorized users are blocked, and RBAC properly restricts admin-only tools.

Start by running your local MCP server:

npm run start:mcp

In a separate terminal, run the auth proxy:

npx nodemon --exec 'ts-node' src/auth-proxy.ts

In another terminal, run the MCP Inspector:

npx @modelcontextprotocol/inspector@0.17.2 --transport http --server-url http://localhost:3000/mcp

Note: If you have another machine running on the network, you can run the MCP Inspector on it with the command npx @modelcontextprotocol/inspector@0.17.2 --transport http --server-url http://<AUTH-PROXY-MACHINE-IP>:3000/mcp, where <AUTH-PROXY-MACHINE-IP> is the IP address of the machine that is running both the MCP server and the auth proxy.

Once the MCP Inspector UI launches, select Connect to connect to your MCP server. You are then redirected to Descope to sign in:

Fig: Descope sign-in
Fig: Descope sign-in

Consent to the requested scopes:

Fig: Consent to requested scopes
Fig: Consent to requested scopes

Once you authorize the MCP client, you'll be connected to the MCP server:

Fig: Connected to MCP server
Fig: Connected to MCP server

At this point, if you try to invoke the browser_install tool, you will get an error that confirms RBAC is working as expected:

Fig: RBAC error
Fig: RBAC error

You now need to assign the user the Tenant Admin role to confirm that they can invoke the browser_install command if they have the appropriate roles. To do this, navigate to the Descope Users page and edit your user to assign them the Tenant Admin role:

Fig: Assigning the Tenant Admin role to the user
Fig: Assigning the Tenant Admin role to the user

Go back to the MCP Inspector UI, disconnect from the MCP server, and connect again. You are then prompted to sign in and consent to the requested scopes. Once you've done this, you can successfully invoke the browser_install command:

Fig: Invoking admin tool calls successfully
Fig: Invoking admin tool calls successfully

You can access the full code on GitHub.

Audit logging for compliance

For production environments, logging user activity is important for security monitoring and regulatory compliance. The current setup logs basic activity (such as tool access and authorization results) to the console. This is useful for local testing but not sufficient for production.

In a production environment, you should enhance this by doing the following:

  • Application-level logging: Capture detailed information about each request, including the timestamps, user IDs, roles, tools accessed, params used, and status of whether the request was granted. Send these logs to a centralized logging service, such as Amazon CloudWatch or Datadog, for long-term retention and analysis.

  • Descope audit logs: Descope automatically tracks all authentication events, login attempts, token generation, and role assignments. You can view these on the Audit page and export them for compliance reporting.

  • Compliance considerations: Different regulations (System and Organization Controls (SOC) 2 type 2, Health Insurance Portability and Accountability Act (HIPAA), General Data Protection Regulation (GDPR)) have specific requirements for log retention periods, tamper-proof storage, and access controls. Ensure your logging strategy aligns with your organization's compliance needs.

With proper audit logging, you gain full visibility into who is accessing your MCP server and what actions they're performing. As a result, you can quickly detect suspicious activity or unauthorized access attempts.

Conclusion

In this guide, you transformed a basic, local-only Playwright MCP server into a secure, remote-capable service. You learned how to upgrade from the default stdio transport to a remote-capable transport, expose it to remote connections through a proxy, protect it behind an authentication layer, and enforce fine-grained authorization using Descope.

Leaving an MCP server exposed without protection is a serious security risk. By adding authentication and authorization layers, you prevent unauthorized access while enabling collaborative, enterprise-grade workflows. This approach makes it possible for teams to connect to shared MCP servers with confidence that only approved users and roles can perform sensitive operations.

Descope provides purpose-built identity infrastructure for AI agents and MCP servers

  • Teams building external-facing MCP servers can add OAuth 2.1, PKCE, and secure DCR in three lines of code

  • Teams building AI agents can offload token management and storage for third-party tool connections

  • Teams building internal-facing MCP servers can implement policy-based AI agent access to corporate tools

Whether you're building your first MCP server or scaling agentic workflows to production, Descope eliminates auth complexity so you can focus on building AI experiences. Explore Descope's AI-focused demos, check out the MCP documentation, or get started with a Free Forever account.

Agentic Identity Control Plane
Fig: Agentic Identity Control Plane