Skip to content
Draft
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
30 changes: 30 additions & 0 deletions agents-manage-api/src/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import evalsRoutes from './routes/evals';
import crudRoutes from './routes/index';
import invitationsRoutes from './routes/invitations';
import mcpRoutes from './routes/mcp';
import nangoRoutes from './routes/nango';
import oauthRoutes from './routes/oauth';
import playgroundTokenRoutes from './routes/playgroundToken';
import projectFullRoutes from './routes/projectFull';
Expand Down Expand Up @@ -124,6 +125,29 @@ function createManagementHono(
})
);

// CORS middleware for Nango config routes (must be registered before global CORS)
app.use(
'/tenants/*/nango/*',
cors({
origin: (origin) => {
return isOriginAllowed(origin) ? origin : null;
},
allowHeaders: [
'content-type',
'Content-Type',
'authorization',
'Authorization',
'User-Agent',
'Cookie',
'X-Forwarded-Cookie',
],
allowMethods: ['GET', 'OPTIONS'],
exposeHeaders: ['Content-Length', 'Set-Cookie'],
maxAge: 600,
credentials: true,
})
);

// CORS middleware - handles all other routes
app.use('*', async (c, next) => {
// Skip CORS middleware for routes with their own CORS config
Expand All @@ -136,6 +160,9 @@ function createManagementHono(
if (c.req.path.includes('/signoz/')) {
return next();
}
if (c.req.path.includes('/nango/')) {
return next();
}

return cors(defaultCorsConfig)(c, next);
});
Expand Down Expand Up @@ -243,6 +270,9 @@ function createManagementHono(
// Mount SigNoz proxy routes under tenant (uses requireTenantAccess middleware for authorization)
app.route('/tenants/:tenantId/signoz', signozRoutes);

// Mount Nango config routes under tenant (uses requireTenantAccess middleware for authorization)
app.route('/tenants/:tenantId/nango', nangoRoutes);

// Mount full project routes directly under tenant
app.route('/tenants/:tenantId', projectFullRoutes);

Expand Down
49 changes: 49 additions & 0 deletions agents-manage-api/src/routes/nango.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Hono } from 'hono';
import { env } from '../env';
import { getLogger } from '../logger';
import type { BaseAppVariables } from '../types/app';

const logger = getLogger('nango-config');

const app = new Hono<{ Variables: BaseAppVariables }>();

// GET /health - Check Nango configuration
app.get('/health', async (c) => {
const nangoSecretKey = env.NANGO_SECRET_KEY;

logger.info(
{
hasSecretKey: !!nangoSecretKey,
},
'Checking Nango configuration'
);

// Check if secret key is set
if (!nangoSecretKey) {
logger.warn({}, 'Nango secret key not set');
return c.json({
status: 'not_configured',
configured: false,
error: 'NANGO_SECRET_KEY not set',
});
}

// Validate the key format - Nango secret keys typically start with "sk-"
if (!nangoSecretKey.startsWith('sk-')) {
logger.warn({}, 'Nango secret key has invalid format');
return c.json({
status: 'invalid_format',
configured: false,
error: 'NANGO_SECRET_KEY has invalid format (should start with "sk-")',
});
}

logger.info({}, 'Nango configuration check successful');

return c.json({
status: 'ok',
configured: true,
});
});

export default app;
16 changes: 14 additions & 2 deletions agents-manage-api/src/routes/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,20 @@ app.openapi(
} catch (error) {
logger.error({ toolId, tenantId, projectId, error }, 'OAuth login failed');

const errorMessage =
error instanceof Error ? error.message : 'Failed to initiate OAuth login';
let errorMessage = 'Failed to initiate OAuth login';

if (error instanceof Error) {
// Check for Nango-specific errors
if (error.message.includes('NANGO_SECRET_KEY')) {
errorMessage = 'NANGO_SECRET_KEY environment variable is not configured. Please set it up to enable OAuth authentication.';
} else if (error.message.includes('query') || error.message.includes('undefined')) {
// Catch the "Cannot read properties of undefined (reading 'query')" error
errorMessage = 'OAuth configuration error. This may be caused by a missing NANGO_SECRET_KEY environment variable.';
} else {
errorMessage = error.message;
}
}

return c.text(`OAuth Error: ${errorMessage}`, 500);
}
}
Expand Down
68 changes: 68 additions & 0 deletions agents-manage-ui/src/app/api/nango/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import axios from 'axios';
import { type NextRequest, NextResponse } from 'next/server';
import { getManageApiUrl } from '@/lib/api/api-config';
import { getLogger } from '@/lib/logger';

export async function GET(request: NextRequest) {
const logger = getLogger('nango-config-check');

try {
// Extract tenantId from query params
const url = new URL(request.url);
const tenantId = url.searchParams.get('tenantId') || 'default';

// Forward cookies for authentication
const cookieHeader = request.headers.get('cookie');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (cookieHeader) {
headers.Cookie = cookieHeader;
}

// Forward to manage-api health endpoint
const manageApiUrl = getManageApiUrl();
const endpoint = `${manageApiUrl}/tenants/${tenantId}/nango/health`;

logger.info({ endpoint }, 'Checking Nango configuration via manage-api');

const response = await axios.get(endpoint, {
headers,
timeout: 5000,
withCredentials: true,
});

logger.info({ status: response.status }, 'Nango health check successful');

return NextResponse.json(response.data);
} catch (error) {
logger.error(
{
error,
message: error instanceof Error ? error.message : 'Unknown error',
code: axios.isAxiosError(error) ? error.code : undefined,
status: axios.isAxiosError(error) ? error.response?.status : undefined,
},
'Nango health check failed'
);

let errorMessage = 'Failed to check Nango configuration';
const configured = false;

if (axios.isAxiosError(error)) {
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
errorMessage = 'Management API not reachable';
} else if (error.response?.status === 401 || error.response?.status === 403) {
errorMessage = 'Authentication required';
} else if (error.response?.data?.error) {
errorMessage = error.response.data.error;
}
}

return NextResponse.json({
status: 'connection_failed',
configured,
error: errorMessage,
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { AlertTriangle } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'sonner';
Expand All @@ -9,8 +10,12 @@ import {
type CredentialScope,
CredentialScopeEnum,
} from '@/components/mcp-servers/form/validation';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { ExternalLink } from '@/components/ui/external-link';
import { Input } from '@/components/ui/input';
import { DOCS_BASE_URL } from '@/constants/page-descriptions';
import { useNangoConfig } from '@/hooks/use-nango-config';
import { useOAuthLogin } from '@/hooks/use-oauth-login';
import { useScopeSelection } from '@/hooks/use-scope-selection';
import type { Credential } from '@/lib/api/credentials';
Expand Down Expand Up @@ -50,6 +55,9 @@ export function MCPServerSelection({ credentials, tenantId, projectId }: MCPServ
const [searchQuery, setSearchQuery] = useState('');
const router = useRouter();

// Check if Nango is configured
const { isLoading: isNangoConfigLoading, configError: nangoConfigError } = useNangoConfig();

const { handleOAuthLogin } = useOAuthLogin({
tenantId,
projectId,
Expand Down Expand Up @@ -148,6 +156,27 @@ export function MCPServerSelection({ credentials, tenantId, projectId }: MCPServ

return (
<>
{/* Nango Configuration Banner */}
{!isNangoConfigLoading && nangoConfigError && (
<Alert variant="warning" className="mb-6">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Nango Configuration Error</AlertTitle>
<AlertDescription>
<p>
{nangoConfigError} Please follow the instructions in the{' '}
<ExternalLink
className="text-amber-700 dark:text-amber-300 dark:hover:text-amber-200"
iconClassName="text-amber-700 dark:text-amber-300 dark:group-hover/link:text-amber-200"
href={`${DOCS_BASE_URL}/typescript-sdk/credentials/nango`}
>
Nango setup guide
</ExternalLink>
.
</p>
</AlertDescription>
</Alert>
)}

<PageHeader
className="gap-2 items-start"
title={selectedMode === 'popular' ? 'Popular MCP Servers' : 'Custom MCP Server'}
Expand Down
40 changes: 40 additions & 0 deletions agents-manage-ui/src/hooks/use-nango-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';

interface NangoConfigStatus {
status: string;
configured: boolean;
error?: string;
}

export function useNangoConfig() {
const { tenantId } = useParams();
const [isLoading, setIsLoading] = useState(true);
const [configError, setConfigError] = useState<string | null>(null);

useEffect(() => {
const checkConfig = async () => {
try {
setIsLoading(true);
// Call Next.js route which forwards to manage-api
const response = await fetch(`/api/nango?tenantId=${tenantId}`);
if (!response.ok) {
throw new Error('Failed to check Nango configuration');
}
const data: NangoConfigStatus = await response.json();
setConfigError(data.error || null);
} catch (err) {
console.error('Error checking Nango configuration:', err);
setConfigError(err instanceof Error ? err.message : 'Failed to check Nango configuration');
} finally {
setIsLoading(false);
}
};

if (tenantId) {
checkConfig();
}
}, [tenantId]);

return { isLoading, configError };
}
Loading