Skip to content
Open
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
16 changes: 12 additions & 4 deletions daemon/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ const app = express();
const PORT = Number(process.env.MCP_DAEMON_PORT ?? 8006);
const HOST = process.env.MCP_DAEMON_HOST ?? '127.0.0.1';

// MCP clients (Claude Desktop, etc.) are external — CORS must be permissive.
// The daemon is already protected by requiring a DreamFactory session token.
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Internal API key for PHP proxy -> daemon communication
const INTERNAL_API_KEY = process.env.MCP_INTERNAL_KEY ?? '';

/**
* Send 401 Unauthorized response
*/
Expand All @@ -46,25 +51,28 @@ function sendUnauthorized(res: Response): void {
const sessionManager = new SessionService();
const sessions = new Map<string, SessionEntry>();

// Health check endpoints
// Health check endpoints — do not expose session IDs
app.get('/health', (_req, res) => {
res.json({
status: 'ok',
timestamp: Math.floor(Date.now() / 1000),
sessions: Array.from(sessions.keys())
active_sessions: sessions.size,
});
});

app.get('/ping', (_req, res) => {
res.json({
status: 'ok',
timestamp: Math.floor(Date.now() / 1000),
sessions: Array.from(sessions.keys())
active_sessions: sessions.size,
});
});

// Cache management endpoint
// Cache management endpoint — requires internal API key from PHP proxy
app.post('/mcp/cache/clear', (req, res) => {
if (INTERNAL_API_KEY && req.headers['x-mcp-internal-key'] !== INTERNAL_API_KEY) {
return res.status(403).json({ error: 'Forbidden: invalid internal key' });
}
const service = typeof req.body === 'object' ? req.body?.service : undefined;
if (service) {
for (const [sessionId, entry] of sessions.entries()) {
Expand Down
29 changes: 19 additions & 10 deletions src/Http/Controllers/McpOAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,21 @@ public function authorizeGet(Request $request, string $mcpService)
]);
}

// Validate redirect URI
// Validate redirect URI is present and registered with the client
if (empty($redirectUri)) {
return $this->errorResponse('invalid_request', 'Missing redirect_uri');
}

$registeredUris = $client->redirect_uris ?? [];
if (!empty($registeredUris) && !in_array($redirectUri, $registeredUris, true)) {
Log::warning('MCP OAuth: redirect_uri not registered', [
'provided' => $redirectUri,
'registered' => $registeredUris,
'service' => $mcpService,
]);
return $this->errorResponse('invalid_request', 'redirect_uri is not registered for this client');
}

// ============================================================
// SSO CHECK: Try to find existing DreamFactory session
// If user is already logged in, skip the login page entirely
Expand Down Expand Up @@ -931,17 +941,16 @@ private function getExistingDfSession(Request $request): ?array
*/
private function getBaseUrl(Request $request): string
{
$scheme = $request->header('X-Forwarded-Proto', $request->getScheme());
$host = $request->header('X-Forwarded-Host', $request->getHost());

// For proxied requests, don't include port (proxy handles it)
// Only include port for direct requests with non-standard ports
if ($request->header('X-Forwarded-Proto')) {
// Proxied request - don't add port
return "{$scheme}://{$host}";
// Prefer configured APP_URL to prevent host header injection attacks.
// Only fall back to request headers for local/dev environments.
$configuredUrl = rtrim(config('app.url', ''), '/');
if (!empty($configuredUrl) && $configuredUrl !== 'http://localhost') {
return $configuredUrl;
}

// Direct request - check if we need to include port
$scheme = $request->getScheme();
$host = $request->getHost();

$port = $request->getPort();
$url = "{$scheme}://{$host}";
if (($scheme === 'http' && $port != 80) || ($scheme === 'https' && $port != 443)) {
Expand Down
25 changes: 15 additions & 10 deletions src/Http/Middleware/McpStreamMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,17 @@ class McpStreamMiddleware
/**
* CORS headers for MCP endpoints
*/
private const CORS_HEADERS = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, mcp-session-id',
'Access-Control-Expose-Headers' => 'WWW-Authenticate',
];
// MCP clients (Claude Desktop, etc.) are external — CORS must be permissive.
// Endpoints are protected by requiring a DreamFactory session/Bearer token.
private static function corsHeaders(): array
{
return [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, mcp-session-id',
'Access-Control-Expose-Headers' => 'WWW-Authenticate',
];
}

/**
* Handle an incoming request.
Expand All @@ -76,7 +81,7 @@ public function handle(Request $request, Closure $next)

// Handle OPTIONS preflight for any MCP path
if ($method === 'OPTIONS') {
return response('', 200)->withHeaders(self::CORS_HEADERS);
return response('', 200)->withHeaders(self::corsHeaders());
}

$mcpService = $matches[1];
Expand All @@ -100,7 +105,7 @@ public function handle(Request $request, Closure $next)

// Add CORS headers to response
if ($response) {
foreach (self::CORS_HEADERS as $key => $value) {
foreach (self::corsHeaders() as $key => $value) {
$response->headers->set($key, $value);
}
return $response;
Expand All @@ -116,7 +121,7 @@ public function handle(Request $request, Closure $next)
private function handleRfc8414WellKnown(Request $request, Closure $next, string $wellKnownType, string $mcpService, string $method)
{
if ($method === 'OPTIONS') {
return response('', 200)->withHeaders(self::CORS_HEADERS);
return response('', 200)->withHeaders(self::corsHeaders());
}

$controllerMethod = self::RFC8414_WELL_KNOWN[$wellKnownType] ?? null;
Expand All @@ -129,7 +134,7 @@ private function handleRfc8414WellKnown(Request $request, Closure $next, string
$response = $controller->$controllerMethod($request, $mcpService);

if ($response) {
foreach (self::CORS_HEADERS as $key => $value) {
foreach (self::corsHeaders() as $key => $value) {
$response->headers->set($key, $value);
}
return $response;
Expand Down