You'll be redirected to authenticate with the upstream provider. Once verified, you'll be granted access to this MCP server's resources.
- + Continue to Authentication diff --git a/src/config.ts b/auth-server/src/config.ts similarity index 50% rename from src/config.ts rename to auth-server/src/config.ts index cffc9c9..96ddfe7 100644 --- a/src/config.ts +++ b/auth-server/src/config.ts @@ -23,28 +23,11 @@ if (baseUrl.port && parseInt(baseUrl.port) !== PORT) { export const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; /** - * Authentication mode: - * - 'integrated': MCP server acts as its own OAuth server (default) - * - 'separate': MCP server delegates to external auth server - */ -export const AUTH_MODE = (process.env.AUTH_MODE as 'integrated' | 'separate') || 'integrated'; - -/** - * Port for the standalone auth server (only used in separate mode) - * Used when running the auth-server component + * Port for this authorization server */ export const AUTH_SERVER_PORT = parseInt(process.env.AUTH_SERVER_PORT || '3001'); /** - * URL of the external authorization server (only used when AUTH_MODE='separate') - * This is where the MCP server will redirect clients for authentication + * URL where this authorization server is hosted */ export const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL || `http://localhost:${AUTH_SERVER_PORT}`; - -// Validate AUTH_SERVER configuration -if (AUTH_MODE === 'separate') { - const authUrl = new URL(AUTH_SERVER_URL); - if (authUrl.port && parseInt(authUrl.port) !== AUTH_SERVER_PORT) { - throw new Error(`Configuration error: AUTH_SERVER_URL port (${authUrl.port}) doesn't match AUTH_SERVER_PORT (${AUTH_SERVER_PORT})`); - } -} diff --git a/src/handlers/fakeauth.ts b/auth-server/src/handlers/mock-upstream-idp.ts similarity index 86% rename from src/handlers/fakeauth.ts rename to auth-server/src/handlers/mock-upstream-idp.ts index 0ccc5d3..54137c7 100644 --- a/src/handlers/fakeauth.ts +++ b/auth-server/src/handlers/mock-upstream-idp.ts @@ -3,13 +3,39 @@ import { generateMcpTokens, readPendingAuthorization, saveMcpInstallation, saveR import { McpInstallation } from "../types.js"; import { logger } from "../utils/logger.js"; -// this module has a fake upstream auth server that returns a fake auth code, it also allows you to authorize or fail -// authorization, to test the different flows +/** + * ============================================================================ + * MOCK UPSTREAM IDENTITY PROVIDER - FOR DEMONSTRATION ONLY + * ============================================================================ + * + * This file simulates what happens when an OAuth server delegates user + * authentication to an external identity provider. In production, this would be: + * + * - Google OAuth (accounts.google.com) + * - GitHub OAuth (github.com/login) + * - Corporate SSO (SAML, LDAP, Active Directory) + * - Auth0/Okta user database + * + * The mock implementation: + * - Shows a user selection UI + * - Generates random user IDs for testing + * - Simulates the redirect flow back to the OAuth server + * + * In production, users would see their actual identity provider's login page + * (Google's login, GitHub's login, corporate SSO portal, etc.) + * + * ============================================================================ + */ -// TODO: make an implementation of this using the ProxyAuthProvider - -// This is mocking an upstream auth server. This wouldn't normally be in the same server as the MCP auth server -export async function handleFakeAuthorize(req: Request, res: Response) { +/** + * Mock authorization endpoint - simulates external IDP login page + * In production, this would be replaced by redirecting to: + * - https://accounts.google.com/oauth/authorize (Google) + * - https://github.com/login/oauth/authorize (GitHub) + * - https://login.microsoftonline.com (Azure AD) + * - Your corporate SSO login page + */ +export async function handleMockUpstreamAuthorize(req: Request, res: Response) { // get the redirect_uri and state from the query params const { redirect_uri, state } = req.query; @@ -244,7 +270,7 @@ export async function handleFakeAuthorize(req: Request, res: Response) { const baseUrl = redirectUri.startsWith('http') ? redirectUri : window.location.origin + redirectUri; const url = new URL(baseUrl); url.searchParams.set('state', '${state}'); - url.searchParams.set('code', 'fakecode'); + url.searchParams.set('code', 'mock-auth-code'); url.searchParams.set('userId', userId); window.location.href = url.toString(); } @@ -259,7 +285,7 @@ export async function handleFakeAuthorize(req: Request, res: Response) { // This is the callback URL that the upstream auth server will redirect to after authorization -export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { +export async function handleMockUpstreamCallback(req: Request, res: Response) { const { // The state returned from the upstream auth server is actually the authorization code state: mcpAuthorizationCode, @@ -267,7 +293,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { userId, // User ID from the authorization flow } = req.query; - logger.debug('Fake auth redirect received', { + logger.debug('Mock upstream IDP callback received', { mcpAuthorizationCode: typeof mcpAuthorizationCode === 'string' ? mcpAuthorizationCode.substring(0, 8) + '...' : mcpAuthorizationCode, upstreamAuthorizationCode: typeof upstreamAuthorizationCode === 'string' ? upstreamAuthorizationCode.substring(0, 8) + '...' : upstreamAuthorizationCode, userId @@ -275,7 +301,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { // This is where you'd exchange the upstreamAuthorizationCode for access/refresh tokens // In this case, we're just going to fake it - const upstreamTokens = await fakeUpstreamTokenExchange(upstreamAuthorizationCode as string); + const upstreamTokens = await mockUpstreamTokenExchange(upstreamAuthorizationCode as string); // Validate that it's a string if (typeof mcpAuthorizationCode !== "string") { @@ -300,9 +326,9 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { }); const mcpInstallation: McpInstallation = { - fakeUpstreamInstallation: { - fakeAccessTokenForDemonstration: upstreamTokens.access_token, - fakeRefreshTokenForDemonstration: upstreamTokens.refresh_token, + mockUpstreamInstallation: { + mockUpstreamAccessToken: upstreamTokens.access_token, + mockUpstreamRefreshToken: upstreamTokens.refresh_token, }, mcpTokens, clientId: pendingAuth.clientId, @@ -343,7 +369,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { logger.debug('Redirect completed'); }; -function fakeUpstreamTokenExchange( +function mockUpstreamTokenExchange( authorizationCode: string, ): Promise<{ access_token: string; refresh_token: string }> { // just return the authorization code with a suffix diff --git a/auth-server/index.ts b/auth-server/src/index.ts similarity index 84% rename from auth-server/index.ts rename to auth-server/src/index.ts index 8db58a0..79ff128 100644 --- a/auth-server/index.ts +++ b/auth-server/src/index.ts @@ -2,11 +2,11 @@ import express from 'express'; import cors from 'cors'; import rateLimit from 'express-rate-limit'; import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'; -import { EverythingAuthProvider } from '../src/auth/provider.js'; -import { handleFakeAuthorize, handleFakeAuthorizeRedirect } from '../src/handlers/fakeauth.js'; -import { redisClient } from '../src/redis.js'; -import { logger } from '../src/utils/logger.js'; -import { AUTH_SERVER_PORT, AUTH_SERVER_URL, BASE_URI } from '../src/config.js'; +import { FeatureReferenceAuthProvider } from './auth/provider.js'; +import { handleMockUpstreamAuthorize, handleMockUpstreamCallback } from './handlers/mock-upstream-idp.js'; +import { redisClient } from './redis.js'; +import { logger } from './utils/logger.js'; +import { AUTH_SERVER_PORT, AUTH_SERVER_URL, BASE_URI } from './config.js'; const app = express(); @@ -14,7 +14,7 @@ console.log('====================================='); console.log('MCP Demonstration Authorization Server'); console.log('====================================='); console.log('This standalone server demonstrates OAuth 2.0'); -console.log('authorization separate from the MCP resource server'); +console.log('authorization for the MCP resource server'); console.log(''); console.log('This is for demonstration purposes only.'); console.log('In production, you would use a real OAuth provider'); @@ -46,7 +46,7 @@ app.get('/health', (req, res) => { }); // Create auth provider instance for reuse -const authProvider = new EverythingAuthProvider(); +const authProvider = new FeatureReferenceAuthProvider(); // OAuth endpoints via SDK's mcpAuthRouter app.use(mcpAuthRouter({ @@ -67,7 +67,7 @@ const introspectRateLimit = rateLimit({ message: { error: 'too_many_requests', error_description: 'Token introspection rate limit exceeded' } }); -const fakeAuthRateLimit = rateLimit({ +const mockUpstreamIdpRateLimit = rateLimit({ windowMs: 60 * 1000, // 1 minute limit: 20, // 20 auth attempts per minute message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' } @@ -116,8 +116,8 @@ app.post('/introspect', introspectRateLimit, express.urlencoded({ extended: fals }); // Fake upstream auth endpoints (for user authentication simulation) -app.get('/fakeupstreamauth/authorize', fakeAuthRateLimit, cors(), handleFakeAuthorize); -app.get('/fakeupstreamauth/callback', fakeAuthRateLimit, cors(), handleFakeAuthorizeRedirect); +app.get('/mock-upstream-idp/authorize', mockUpstreamIdpRateLimit, cors(), handleMockUpstreamAuthorize); +app.get('/mock-upstream-idp/callback', mockUpstreamIdpRateLimit, cors(), handleMockUpstreamCallback); // Static assets (for auth page styling) import path from 'path'; @@ -127,8 +127,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); app.get('/mcp-logo.png', staticFileRateLimit, (req, res) => { - // Serve from the main server's static directory - const logoPath = path.join(__dirname, '../src/static/mcp.png'); + const logoPath = path.join(__dirname, 'static/mcp.png'); res.sendFile(logoPath); }); @@ -159,8 +158,8 @@ app.listen(AUTH_SERVER_PORT, () => { console.log(` curl ${AUTH_SERVER_URL}/health`); console.log(` curl ${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`); console.log(''); - console.log('💡 To test separate mode:'); + console.log('💡 To test with MCP server:'); console.log(' 1. Keep this server running'); - console.log(' 2. In another terminal: AUTH_MODE=separate npm run dev'); + console.log(' 2. In another terminal: cd ../mcp-server && npm run dev'); console.log(' 3. Connect Inspector to http://localhost:3232/mcp'); }); \ No newline at end of file diff --git a/src/redis.ts b/auth-server/src/redis.ts similarity index 100% rename from src/redis.ts rename to auth-server/src/redis.ts diff --git a/src/services/auth.test.ts b/auth-server/src/services/auth.test.ts similarity index 96% rename from src/services/auth.test.ts rename to auth-server/src/services/auth.test.ts index 4964031..1695cf1 100644 --- a/src/services/auth.test.ts +++ b/auth-server/src/services/auth.test.ts @@ -182,9 +182,9 @@ describe("auth utils", () => { const accessToken = generateToken(); const mcpInstallation: McpInstallation = { - fakeUpstreamInstallation: { - fakeAccessTokenForDemonstration: "fake-upstream-access-token", - fakeRefreshTokenForDemonstration: "fake-upstream-refresh-token", + mockUpstreamInstallation: { + mockUpstreamAccessToken: "fake-upstream-access-token", + mockUpstreamRefreshToken: "fake-upstream-refresh-token", }, mcpTokens: { access_token: accessToken, @@ -229,9 +229,9 @@ describe("auth utils", () => { // Save it to Redis with actual function await saveMcpInstallation(accessToken, { - fakeUpstreamInstallation: { - fakeAccessTokenForDemonstration: "fake-upstream-access-token", - fakeRefreshTokenForDemonstration: "fake-upstream-refresh-token", + mockUpstreamInstallation: { + mockUpstreamAccessToken: "fake-upstream-access-token", + mockUpstreamRefreshToken: "fake-upstream-refresh-token", }, mcpTokens: { access_token: accessToken, diff --git a/src/services/auth.ts b/auth-server/src/services/auth.ts similarity index 94% rename from src/services/auth.ts rename to auth-server/src/services/auth.ts index 2067de0..5d9b4e3 100644 --- a/src/services/auth.ts +++ b/auth-server/src/services/auth.ts @@ -2,15 +2,15 @@ import { redisClient } from "../redis.js"; import { McpInstallation, PendingAuthorization, TokenExchange } from "../types.js"; import { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js"; -// Re-export from shared modules for backward compatibility +// Re-export from auth-core module export { generatePKCEChallenge, generateToken, decryptString, generateMcpTokens -} from "../../shared/auth-core.js"; +} from "../auth/auth-core.js"; -import * as sharedRedisAuth from "../../shared/redis-auth.js"; +import * as sharedRedisAuth from "./redis-auth.js"; // Wrapper functions that pass redisClient to shared module functions diff --git a/shared/redis-auth.ts b/auth-server/src/services/redis-auth.ts similarity index 97% rename from shared/redis-auth.ts rename to auth-server/src/services/redis-auth.ts index fc4d636..7886bbc 100644 --- a/shared/redis-auth.ts +++ b/auth-server/src/services/redis-auth.ts @@ -1,9 +1,9 @@ import { SetOptions } from "@redis/client"; import { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js"; -import { RedisClient } from "../src/redis.js"; -import { McpInstallation, PendingAuthorization, TokenExchange } from "./types.js"; -import { sha256, encryptString, decryptString } from "./auth-core.js"; -import { logger } from "../src/utils/logger.js"; +import { RedisClient } from "../redis.js"; +import { McpInstallation, PendingAuthorization, TokenExchange } from "../types.js"; +import { sha256, encryptString, decryptString } from "../auth/auth-core.js"; +import { logger } from "../utils/logger.js"; /** * Redis key prefixes for different data types diff --git a/src/static/index.html b/auth-server/src/static/index.html similarity index 100% rename from src/static/index.html rename to auth-server/src/static/index.html diff --git a/src/static/mcp.png b/auth-server/src/static/mcp.png similarity index 100% rename from src/static/mcp.png rename to auth-server/src/static/mcp.png diff --git a/src/static/styles.css b/auth-server/src/static/styles.css similarity index 100% rename from src/static/styles.css rename to auth-server/src/static/styles.css diff --git a/shared/types.ts b/auth-server/src/types.ts similarity index 88% rename from shared/types.ts rename to auth-server/src/types.ts index d87ce13..877b5dc 100644 --- a/shared/types.ts +++ b/auth-server/src/types.ts @@ -29,14 +29,14 @@ export interface TokenExchange { } /** - * Represents fake upstream tokens for demonstration purposes. + * Represents mock upstream identity provider tokens for demonstration purposes. * In production, this would contain real upstream provider tokens. */ -export interface FakeUpstreamInstallation { - /** Simulated access token from the fake upstream provider */ - fakeAccessTokenForDemonstration: string; - /** Simulated refresh token from the fake upstream provider */ - fakeRefreshTokenForDemonstration: string; +export interface MockUpstreamInstallation { + /** Access token from the mock upstream identity provider */ + mockUpstreamAccessToken: string; + /** Refresh token from the mock upstream identity provider */ + mockUpstreamRefreshToken: string; } /** @@ -46,7 +46,7 @@ export interface FakeUpstreamInstallation { */ export interface McpInstallation { /** Information from the upstream authentication provider */ - fakeUpstreamInstallation: FakeUpstreamInstallation; + mockUpstreamInstallation: MockUpstreamInstallation; /** MCP OAuth tokens issued to the client */ mcpTokens: OAuthTokens; /** The OAuth client ID associated with this installation */ diff --git a/src/utils/logger.ts b/auth-server/src/utils/logger.ts similarity index 100% rename from src/utils/logger.ts rename to auth-server/src/utils/logger.ts diff --git a/auth-server/tsconfig.json b/auth-server/tsconfig.json new file mode 100644 index 0000000..fee103a --- /dev/null +++ b/auth-server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "Node16", + "moduleResolution": "Node16", + "sourceMap": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/docs/customization-guide.md b/docs/customization-guide.md new file mode 100644 index 0000000..374e9d7 --- /dev/null +++ b/docs/customization-guide.md @@ -0,0 +1,117 @@ +# Customization Guide + +This reference implementation includes demo functionality to showcase MCP features. Here's how to adapt it for your own use case. + +## Overview: What to Customize vs. Keep + +**Replace with your own:** +- MCP tools, resources, and prompts (your business logic) +- Authentication provider (use commercial OAuth provider) + +**Keep as-is (infrastructure):** +- Redis transport and session management +- HTTP handlers and routing +- Security middleware and rate limiting +- Logging infrastructure + +--- + +## Customizing MCP Functionality + +All MCP feature implementations live in **`mcp-server/src/services/mcp.ts`**. This is where you define what your server can do. + +### Tools + +Replace the 7 demo tools (echo, add, etc.) with your actual tools: + +**Location:** `createMcpServer()` function, look for the `CallToolRequestSchema` handler + +**What to change:** +- Tool definitions: name, description, input schema (using Zod) +- Tool execution logic: what happens when the tool is called +- Return format: text, images, or embedded resources + +**Pattern:** Each tool validates input with a Zod schema and returns content in MCP format. + +### Resources + +Replace the 100 fake resources with your actual data sources: + +**Location:** `createMcpServer()` function, resource-related handlers: +- `ListResourcesRequestSchema` - List available resources +- `ReadResourceRequestSchema` - Read specific resources +- `SubscribeRequestSchema` / `UnsubscribeRequestSchema` - Resource updates + +**What to change:** +- Resource URIs and names +- Data fetching logic +- Pagination if needed +- Update notifications for subscribed resources + +**Pattern:** Resources use URIs (e.g., `db://users/123`) and return content as text, JSON, or binary. + +### Prompts + +Replace the 3 demo prompts with useful prompts for your domain: + +**Location:** `createMcpServer()` function, prompt-related handlers: +- `ListPromptsRequestSchema` - List available prompts +- `GetPromptRequestSchema` - Return prompt content + +**What to change:** +- Prompt names and descriptions +- Prompt arguments and validation +- Prompt content and embedded resources + +**Pattern:** Prompts can include dynamic arguments and reference resources. + +--- + +## Customizing Authentication + +**Replace the demo auth server** with a commercial OAuth provider (Auth0, Okta, Azure AD, AWS Cognito, Google, GitHub, etc.). + +See [OAuth Architecture Patterns](oauth-architecture-patterns.md#using-a-commercial-auth-provider) for detailed integration steps. + +**Do not repurpose the demo auth server** - it's designed for development/testing only. Commercial providers offer better security, reliability, and user management. + +--- + +## Configuration + +Update environment variables for your deployment: + +**MCP Server** (`mcp-server/.env`): +```bash +BASE_URI=https://your-mcp-server.com +PORT=443 +AUTH_SERVER_URL=https://your-tenant.auth0.com +REDIS_URL=redis://your-redis-server:6379 +``` + +**Auth Server** (only if using the demo server for development): +```bash +AUTH_SERVER_URL=http://localhost:3001 +BASE_URI=https://your-mcp-server.com +``` + +--- + +## Testing Your Customizations + +1. **Unit tests:** Add tests for your tools/resources in `mcp-server/src/services/` +2. **Integration tests:** Test with MCP Inspector or client.js +3. **Build and lint:** Run `npm run build && npm run lint` +4. **End-to-end:** Use the examples to verify OAuth and MCP flows + +--- + +## Next Steps + +1. Fork/clone this repository +2. Replace tools, resources, and prompts in `mcp-server/src/services/mcp.ts` +3. Integrate with your OAuth provider (see OAuth Architecture Patterns doc) +4. Update environment variables for your deployment +5. Test thoroughly before deploying + +For questions about the MCP protocol itself, see the [MCP specification](https://modelcontextprotocol.io/specification). diff --git a/docs/endpoints.md b/docs/endpoints.md new file mode 100644 index 0000000..4332605 --- /dev/null +++ b/docs/endpoints.md @@ -0,0 +1,129 @@ +# Endpoint Reference + +Complete listing of all endpoints provided by each server in the architecture. + +## Auth Server + +Standalone OAuth 2.0 authorization server that handles authentication and token management. + +### OAuth Authorization Endpoints +Provided by `mcpAuthRouter` from MCP SDK: + +- `GET /.well-known/oauth-authorization-server` - OAuth metadata discovery +- `POST /register` - Dynamic client registration +- `GET /authorize` - Authorization request (starts OAuth flow) +- `POST /token` - Token exchange (authorization code → tokens) and token refresh +- `POST /revoke` - Token revocation + +### Token Introspection +Custom implementation for resource server token validation: + +- `POST /introspect` - Token introspection ([RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)) + - Called by MCP server to validate tokens + - Returns token status, scopes, expiry, user info + - Protected endpoint (not public) + +### Mock Upstream Identity Provider Endpoints +Local simulation of upstream IDP (would be external in production): + +- `GET /mock-upstream-idp/authorize` - Mock user authentication page +- `GET /mock-upstream-idp/callback` - IDP callback handler (returns userId) + +**Note**: In production, this would redirect to external providers like Auth0, Okta, Google, GitHub, etc. These endpoints simulate that functionality for demonstration purposes. + +### Utility Endpoints +- `GET /health` - Health check (returns server status) +- `GET /mcp-logo.png` - Logo asset for auth pages + +--- + +## MCP Server + +MCP resource server that implements the Model Context Protocol with delegated authentication. + +### OAuth Metadata (Read-Only) + +- `GET /.well-known/oauth-authorization-server` - Returns metadata pointing to external auth server + - Tells clients to use auth server at :3001 + - Returns 503 if auth server is unavailable (degraded mode) + - Read-only - no token issuance happens here + +### MCP Resource Endpoints + +#### Streamable HTTP Transport (Recommended) +- `GET /mcp` - Establish SSE stream for session +- `POST /mcp` - Initialize session or send messages +- `DELETE /mcp` - Terminate session + +#### SSE Transport (Legacy) +- `GET /sse` - Establish SSE connection +- `POST /message` - Send messages to session + +All MCP endpoints require `Authorization: Bearer+ A comprehensive reference implementation of the Model Context Protocol (MCP) server + demonstrating all protocol features with full authentication support and horizontal scalability. +
+ +All MCP features including tools, resources, prompts, sampling, completions, and logging with full protocol compliance.
+Streamable HTTP (SHTTP) and Server-Sent Events (SSE) transports for flexible client integration.
+Complete OAuth flow with PKCE support and a built-in fake provider for testing and development.
+Redis-backed session management enables multi-instance deployments with automatic load distribution.
+Echo, add, long-running operations, LLM sampling, image handling, annotations, and resource references.
+Example resources with pagination, templates, subscriptions, and real-time update notifications.
+