NIN Connect Docs
Platform (Self-Host)MCP

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.

  1. 401 + WWW-Authenticate — Return 401 with a WWW-Authenticate header that includes resource_metadata pointing 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"
  1. Protected Resource Metadata (RFC 9728) — At that URL, serve JSON with resource, authorization_servers, and scopes_supported:
{
  "resource": "https://your-server.com/mcp",
  "authorization_servers": ["https://auth.your-server.com"],
  "scopes_supported": ["mcp:tools", "mcp:resources"]
}
  1. Authorization Server Metadata (RFC 8414) — At {auth_server}/.well-known/oauth-authorization-server, expose authorization_endpoint, token_endpoint, optional registration_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.

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_method

If 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

  1. Authorize — Client redirects the user to your authorization_endpoint with response_type=code, client_id, redirect_uri, scope, state, code_challenge, and code_challenge_method=S256.
  2. User login — Your page authenticates the user (e.g. with your existing auth system or an IdP).
  3. Callback — After consent, redirect to redirect_uri with code and state. The code is short-lived (e.g. 10 minutes).
  4. Token exchange — Client calls your token_endpoint with grant_type=authorization_code, code, redirect_uri, client_id, and code_verifier. You validate the code and PKCE, then return access_token and optionally refresh_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:

TransportEndpoint(s)Notes
Streamable HTTPPOST /mcp (single endpoint)Recommended; session via Mcp-Session-Id header
HTTP + SSEGET /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 state on 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> and Mcp-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

Next steps

On this page