Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ MCP_TIMEOUT=180000
LOG_LEVEL=info

# CORS Configuration
<<<<<<< HEAD
# Specifies the allowed origin for CORS. Use '*' for all origins or a specific URL (e.g., https://example.com).
=======
>>>>>>> origin/main
CORS_ALLOW_ORIGIN=*

# Authentication
API_KEY=your-secret-api-key

# Multiplexing SSE Transport Configuration
# Set to 'true' to enable multiplexing SSE transport (handles multiple clients with a single transport)
# Set to 'false' to use individual SSE transport for each client (legacy behavior)
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ A Model Context Protocol (MCP) server that provides GitHub API integration throu
- Configurable timeouts, CORS settings, and logging levels
- Robust error handling and detailed logging

## Authentication

This server uses API key authentication to protect its endpoints. All requests to `/mcp` and `/messages` must include an `Authorization` header with a valid bearer token.

Example: `Authorization: Bearer your-secret-api-key`

To set up authentication, add the following variable to your `.env` file:

`API_KEY=your-secret-api-key`

## Project Structure

The project follows a modular, feature-based architecture. All source code is located in the `src` directory.
Expand Down Expand Up @@ -66,6 +76,9 @@ The project follows a modular, feature-based architecture. All source code is lo
# Generate a token at https://github.com/settings/tokens
GITHUB_TOKEN=your_github_token_here

# Authentication
API_KEY=your-secret-api-key

# Server Port Configuration
MCP_SSE_PORT=3200

Expand Down
1 change: 1 addition & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const config = {
sseTimeout: process.env.SSE_TIMEOUT ? parseInt(process.env.SSE_TIMEOUT, 10) : 1800000,
corsAllowOrigin: process.env.CORS_ALLOW_ORIGIN ?? '',
useMultiplexing: process.env.USE_MULTIPLEXING_SSE === 'true',
apiKey: process.env.API_KEY ?? '',
// Rate Limiting Configuration
rateLimitWindowMs: process.env.RATE_LIMIT_WINDOW_MS ? parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) : 15 * 60 * 1000, // 15 minutes
rateLimitMaxRequests: process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) : 100, // 100 requests
Expand Down
29 changes: 29 additions & 0 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Request, Response, NextFunction } from 'express';
import { config } from '#config/index';
import { logger } from '#core/logger';

export const authenticate = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];

if (!authHeader) {
logger.warn('Authentication failed: No Authorization header');
return res.status(401).json({ error: 'Authorization header required' });
}

const tokenParts = authHeader.split(' ');

if (tokenParts.length !== 2 || tokenParts[0].toLowerCase() !== 'bearer') {
logger.warn('Authentication failed: Invalid token format');
return res.status(401).json({ error: 'Invalid token format. Expected "Bearer <token>"' });
}

const token = tokenParts[1];

if (token !== config.apiKey) {
logger.warn('Authentication failed: Invalid API key');
return res.status(401).json({ error: 'Invalid API key' });
}

logger.info('Authentication successful');
next();
};
5 changes: 3 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { config } from '#config/index';
import { logger } from '#core/logger';
import rateLimit from 'express-rate-limit';
import express, { Request, Response, NextFunction } from 'express';
import { authenticate } from './middleware/auth.js';

const generalLimiter = rateLimit({
windowMs: config.rateLimitWindowMs,
Expand Down Expand Up @@ -183,7 +184,7 @@ export function createServer(mcpServer: McpServer, port: number): http.Server {
});
}

app.all('/mcp', createUserLimiter(), (req: express.Request, res: express.Response, next: express.NextFunction) => {
app.all('/mcp', authenticate, createUserLimiter(), (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug(`New Streamable HTTP request: ${req.method} ${req.url}`);
if (req.method === 'OPTIONS') {
res.status(200).end();
Expand Down Expand Up @@ -312,7 +313,7 @@ export function createServer(mcpServer: McpServer, port: number): http.Server {
}
});

app.post('/messages', messageLimiter, (req: express.Request, res: express.Response) => {
app.post('/messages', authenticate, messageLimiter, (req: express.Request, res: express.Response) => {
try {
const sessionIdSchema = z.string().uuid();
const sessionId = sessionIdSchema.parse(req.query.sessionId);
Expand Down