Building an MCP Server
Step-by-step guide for services to build a remote MCP server with OAuth 2.1 (PKCE), session management, and Streamable HTTP or SSE transport — aligned with the official MCP authorization and security guidance.
Overview
If your service has an API that you want AI applications (Claude, ChatGPT, Cursor, NIN Connect, etc.) to use via Model Context Protocol, you can expose it as a remote MCP server. Users connect once with OAuth; AI clients discover and call your tools without per-user API key management in the client.
This guide walks through building a remote MCP server that:
- Exposes your API capabilities as MCP tools
- Uses OAuth 2.1 (Authorization Code flow with PKCE; no implicit or password flows)
- Supports Streamable HTTP (recommended) and SSE for compatibility
- Handles sessions, token verification, and token refresh
For a full walkthrough of the authorization flow from the client’s perspective and when to use authorization, see the official Understanding Authorization in MCP tutorial. For building a basic MCP server (including local STDIO), see Build an MCP server.
When to use authorization
Authorization for MCP servers is optional but strongly recommended when your server accesses user-specific data, you need audit trails, or you are building for enterprise or multi-tenant use. For remote HTTP-based servers, OAuth 2.1 is the standard. For local STDIO servers, environment-based or embedded credentials are often sufficient. See Understanding Authorization in MCP for more detail.
OAuth 2.1 flow (high level)
From an unauthenticated tool request to authorized data:
Prerequisites
- MCP SDK (server): e.g. @modelcontextprotocol/sdk (TypeScript) or one of the official MCP SDKs
- OAuth 2.1 (Authorization Code + PKCE): any OAuth library that supports PKCE
- HTTPS in production; secure storage for tokens and client secrets
Language options: The steps below use TypeScript/Node for illustration. The same concepts apply in Python, Go, Rust, C#, etc. — use the official MCP SDKs and any OAuth 2.1 / PKCE–capable library for your stack.
Step 1: 401 challenge and discovery (RFC 9728 + RFC 8414)
When a client connects without a token, your server must respond with 401 Unauthorized and tell the client where to find authorization information. The client uses that to discover your OAuth configuration.
- 401 + WWW-Authenticate — Return
401with aWWW-Authenticateheader that includesresource_metadatapointing to your Protected Resource Metadata (PRM) document:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"- Protected Resource Metadata (RFC 9728) — At that URL, serve JSON with
resource,authorization_servers, andscopes_supported:
{
"resource": "https://your-server.com/mcp",
"authorization_servers": ["https://auth.your-server.com"],
"scopes_supported": ["mcp:tools", "mcp:resources"]
}- Authorization Server Metadata (RFC 8414) — At
{auth_server}/.well-known/oauth-authorization-server, exposeauthorization_endpoint,token_endpoint, optionalregistration_endpoint, and PKCE support:
{
"issuer": "https://auth.your-server.com",
"authorization_endpoint": "https://auth.your-server.com/authorize",
"token_endpoint": "https://auth.your-server.com/token",
"registration_endpoint": "https://auth.your-server.com/register",
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"response_types_supported": ["code"],
"scopes_supported": ["mcp:tools", "mcp:resources"]
}This flow is described step-by-step in Understanding Authorization in MCP.
Step 2: PKCE (Proof Key for Code Exchange)
Require PKCE for all authorization code flows. The client generates a code_verifier and sends the derived code_challenge (S256) when redirecting the user to your authorize endpoint. You store the challenge (and optional state) server-side; on token exchange you verify the code_verifier against the stored challenge.
import { createHash, randomBytes } from "crypto";
function base64URLEncode(buffer: Buffer): string {
return buffer
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
// Client generates these; server validates code_verifier on token exchange
function generateCodeVerifier(): string {
return base64URLEncode(randomBytes(32));
}
function generateCodeChallenge(verifier: string): string {
return base64URLEncode(createHash("sha256").update(verifier).digest());
}On the server, when issuing an authorization code, store the code_challenge and code_challenge_method (and state if used). When the client exchanges the code at the token endpoint, require code_verifier, recompute the challenge with S256, and reject if it does not match.
Step 3: Dynamic client registration (optional, recommended)
If you support RFC 7591 dynamic client registration, expose registration_endpoint in your authorization server metadata. Clients can then obtain a client_id (and optionally client_secret) without pre-sharing credentials.
// POST /register
// Request body: client_name, redirect_uris, grant_types, response_types, etc.
// Response: client_id, client_secret (if issued), token_endpoint_auth_methodIf you do not support registration, document the required redirect URIs and have clients use a pre-registered client_id (and optional secret); show the callback URL clearly in your developer docs. See Understanding Authorization in MCP — Client Registration for details.
Step 4: Authorize and callback flow
- Authorize — Client redirects the user to your
authorization_endpointwithresponse_type=code,client_id,redirect_uri,scope,state,code_challenge, andcode_challenge_method=S256. - User login — Your page authenticates the user (e.g. with your existing auth system or an IdP).
- Callback — After consent, redirect to
redirect_uriwithcodeandstate. Thecodeis short-lived (e.g. 10 minutes). - Token exchange — Client calls your
token_endpointwithgrant_type=authorization_code,code,redirect_uri,client_id, andcode_verifier. You validate the code and PKCE, then returnaccess_tokenand optionallyrefresh_token.
Always validate state on callback to mitigate CSRF. Use HTTPS for redirect URIs in production.
Step 5: MCP server core and tools
Create an MCP server instance and register tools that map to your API. Each tool has a name, description, input schema (e.g. JSON Schema), and a handler that performs the actual API call. The handler can receive the authenticated user context (e.g. from the access token) to enforce per-user authorization.
For the basics of defining tools and running a server, see Build an MCP server.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const mcpServer = new McpServer({
name: "Your Service MCP",
version: "1.0.0",
instructions: "Use these tools to interact with the service.",
});
// Example: list resources (schema and handler depend on your MCP SDK)
mcpServer.tool(
"list_resources",
{ limit: z.number().optional(), cursor: z.string().optional() }, // schema
{ description: "List user's resources with optional pagination" },
async (params, { authInfo }) => {
const userId = await resolveUserFromToken(authInfo.token);
const result = await yourApi.listResources(userId, params);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);Ensure token verification runs before handling MCP requests: validate the Authorization: Bearer <access_token> header, resolve the user (and optional scopes), and attach an authInfo (or equivalent) object so tool handlers can enforce access control. Always validate that the token was issued for your server (e.g. audience/resource) to avoid token passthrough risks.
Step 6: Transport — Streamable HTTP and SSE
Remote MCP servers typically support one or both of:
| Transport | Endpoint(s) | Notes |
|---|---|---|
| Streamable HTTP | POST /mcp (single endpoint) | Recommended; session via Mcp-Session-Id header |
| HTTP + SSE | GET /mcp (SSE stream) + POST /messages?sessionId=... | Legacy; broader client compatibility |
Implement Streamable HTTP first: one POST /mcp that accepts JSON-RPC–style MCP messages, uses Mcp-Session-Id for session affinity, and returns JSON or SSE streamed responses per the MCP specification. If you need to support older clients, add an SSE endpoint that opens a long-lived stream and a separate POST /messages for requests, with session identified by query parameter.
Session management: create a unique session ID on first request (e.g. initialize), store the transport or session state keyed by that ID, and reuse it for subsequent requests. On session end (e.g. DELETE /mcp or connection close), remove the session and clean up. Treat Mcp-Session-Id as untrusted input and do not use it for authorization; see Security Best Practices for session hardening.
Step 7: Token refresh
Access tokens should expire (e.g. 1 hour). Support grant_type=refresh_token at your token endpoint so clients can obtain a new access token (and optionally a new refresh token if you rotate them). Require the same client_id (and client_secret if applicable) used for the original authorization. If refresh fails with invalid_grant, the client should prompt the user to re-authorize.
Store refresh tokens securely; associate them with the user and client. Rotating refresh tokens (issuing a new one and invalidating the old on each use) is recommended for security.
Step 8: Security checklist
For comprehensive attack vectors and mitigations, see the official Security Best Practices. Key points:
- Use HTTPS only in production (except loopback for development).
- Validate
stateon OAuth callback; generate and store it securely and only after user consent. - Enforce PKCE for authorization code flows.
- Store tokens and secrets encrypted at rest; never log or expose them.
- Scope minimization — request and grant only the scopes you need; avoid catch-all scopes.
- Always validate tokens — ensure the token is valid and issued for your server (audience/resource); do not accept token passthrough.
- Rate limiting — apply to token endpoint and MCP endpoint to prevent abuse.
- Audit logging — log auth and tool-call events without storing full tokens.
- Session IDs — use secure, non-deterministic session IDs; do not use sessions for authentication.
- Trust — document that users should only connect to your official MCP URL.
Step 9: Deployment
Deploy your MCP server as a standard web service. Streamable HTTP works well on serverless (e.g. Vercel, Cloud Run) because each request is self-contained. SSE requires long-lived connections, so use a platform that supports persistent connections (e.g. a classic Node server, Railway, Fly.io). Ensure environment variables for secrets and issuer URLs are set correctly and that your .well-known URLs are reachable.
Cloudflare Workers is a strong option for hosting remote MCP servers: Cloudflare provides workers-oauth-provider for OAuth 2.1 authorization and McpAgent in the Agents SDK for remote transport, so you can focus on your tools and auth UI rather than implementing discovery and tokens from scratch. See Build and deploy Remote MCP servers to Cloudflare and the remote MCP server guide for details.
Testing
- Use the same discovery flow as clients: fetch
/.well-known/oauth-protected-resource, then authorization server metadata, and confirm endpoints and PKCE support. - Run through the full OAuth flow (authorize → callback → token exchange → refresh).
- Call your MCP endpoint with a valid
Authorization: Bearer <access_token>andMcp-Session-Id(for non-initialize requests), and verify tool listing and invocation. - Cloudflare AI Playground — Enter your remote MCP server URL and connect; the playground runs the full authorization flow and includes a debug log that helps identify issues during discovery, consent, and tool calls.
- Test with MCP Inspector or a client that supports remote MCP (e.g. VS Code with MCP: Add server → HTTP).
References
- Understanding Authorization in MCP — Official step-by-step authorization flow, 401/PRM discovery, client registration, and Keycloak-based implementation examples (TypeScript, Python, C#).
- Build an MCP server — Official tutorial for building an MCP server (tools, resources, prompts) in multiple languages; includes STDIO and client configuration.
- Security Best Practices — Attack vectors, mitigations, token passthrough, SSRF, session hijacking, scope minimization, and related guidance.
- Simplescraper: How to MCP — Full remote MCP server guide: OAuth 2.1 with PKCE, session management, Streamable HTTP vs HTTP+SSE, tool definitions with
authInfo. - Build and deploy Remote MCP servers to Cloudflare — Cloudflare Workers as a host for remote MCP with workers-oauth-provider and McpAgent; remote MCP server guide and AI Playground for testing.
- MCP Specification — Protocol and transport details.
- RFC 6749 — OAuth 2.0.
- RFC 7636 — PKCE.
- RFC 8414 — OAuth 2.0 Authorization Server Metadata.
- RFC 9728 — Protected Resource Metadata (PRM).
Next steps
- Integrating your MCP client — How to build a client that connects to MCP servers (e.g. NIN Connect).
- Security best practices — Securing MCP usage and defending against prompt injection and tool poisoning.
MCP Integration
Use MCP servers as plugin tool sources with realtime discovery, PKCE OAuth, public-server support, and layered credential management.
Integrating your MCP client
Build an MCP client flow like NIN Connect with discovery fallbacks, PKCE, dynamic registration, public-server support, and secure token refresh.