mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-08 06:42:39 +02:00
housekeeping
This commit is contained in:
parent
775a64c5a8
commit
a298036b4b
77 changed files with 2 additions and 14090 deletions
|
|
@ -1,62 +1,13 @@
|
|||
'use server';
|
||||
import { WebpageCrawlResponse } from "../lib/types/tool_types";
|
||||
import { webpagesCollection } from "../lib/mongodb";
|
||||
import { z } from 'zod';
|
||||
import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js';
|
||||
import { getAgenticResponseStreamId } from "../lib/utils";
|
||||
import { check_query_limit } from "../lib/rate_limiting";
|
||||
import { QueryLimitError } from "../lib/client_utils";
|
||||
import { projectAuthCheck } from "./project_actions";
|
||||
import { authorizeUserAction } from "./billing_actions";
|
||||
import { Workflow, WorkflowTool } from "../lib/types/workflow_types";
|
||||
import { Workflow } from "../lib/types/workflow_types";
|
||||
import { Message } from "@/app/lib/types/types";
|
||||
|
||||
const crawler = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY || '' });
|
||||
|
||||
export async function scrapeWebpage(url: string): Promise<z.infer<typeof WebpageCrawlResponse>> {
|
||||
const page = await webpagesCollection.findOne({
|
||||
"_id": url,
|
||||
lastUpdatedAt: {
|
||||
'$gte': new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), // 24 hours
|
||||
},
|
||||
});
|
||||
if (page) {
|
||||
// console.log("found webpage in db", url);
|
||||
return {
|
||||
title: page.title,
|
||||
content: page.contentSimple,
|
||||
};
|
||||
}
|
||||
|
||||
// otherwise use firecrawl
|
||||
const scrapeResult = await crawler.scrapeUrl(
|
||||
url,
|
||||
{
|
||||
formats: ['markdown'],
|
||||
onlyMainContent: true
|
||||
}
|
||||
) as ScrapeResponse;
|
||||
|
||||
// save the webpage using upsert
|
||||
await webpagesCollection.updateOne(
|
||||
{ _id: url },
|
||||
{
|
||||
$set: {
|
||||
title: scrapeResult.metadata?.title || '',
|
||||
contentSimple: scrapeResult.markdown || '',
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
}
|
||||
},
|
||||
{ upsert: true }
|
||||
);
|
||||
|
||||
// console.log("crawled webpage", url);
|
||||
return {
|
||||
title: scrapeResult.metadata?.title || '',
|
||||
content: scrapeResult.markdown || '',
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAssistantResponseStreamId(
|
||||
projectId: string,
|
||||
workflow: z.infer<typeof Workflow>,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { WithStringId } from "../lib/types/types";
|
|||
import { DataSourceDoc } from "../lib/types/datasource_types";
|
||||
import { DataSource } from "../lib/types/datasource_types";
|
||||
import { uploadsS3Client } from "../lib/uploads_s3_client";
|
||||
import { USE_RAG_S3_UPLOADS } from "../lib/feature_flags";
|
||||
|
||||
export async function getDataSource(projectId: string, sourceId: string): Promise<WithStringId<z.infer<typeof DataSource>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
|
|
|||
|
|
@ -1,906 +0,0 @@
|
|||
'use server';
|
||||
|
||||
import { projectAuthCheck } from './project_actions';
|
||||
import { z } from 'zod';
|
||||
import { MCPServer, McpTool, McpServerResponse, McpServerTool } from '../lib/types/types';
|
||||
import { projectsCollection } from '../lib/mongodb';
|
||||
import { fetchMcpTools, toggleMcpTool } from './mcp_actions';
|
||||
import { fetchMcpToolsForServer } from './mcp_actions';
|
||||
import { headers } from 'next/headers';
|
||||
import { authorizeUserAction } from './billing_actions';
|
||||
import { redisClient } from '../lib/redis';
|
||||
import { SERVER_URL_PARAMS, SERVER_CLIENT_ID_MAP } from '../lib/constants/klavis';
|
||||
|
||||
type McpServerType = z.infer<typeof MCPServer>;
|
||||
type McpToolType = z.infer<typeof McpTool>;
|
||||
type McpServerResponseType = z.infer<typeof McpServerResponse>;
|
||||
|
||||
// Internal API Response Types
|
||||
interface KlavisServerMetadata {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tools: {
|
||||
name: string;
|
||||
description: string;
|
||||
}[];
|
||||
authNeeded: boolean;
|
||||
}
|
||||
|
||||
interface GetAllServersResponse {
|
||||
servers: KlavisServerMetadata[];
|
||||
}
|
||||
|
||||
interface CreateServerInstanceResponse {
|
||||
serverUrl: string;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
interface DeleteServerInstanceResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface UserInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
tools: {
|
||||
name: string;
|
||||
description: string;
|
||||
authNeeded: boolean;
|
||||
isAuthenticated: boolean;
|
||||
}[] | null;
|
||||
authNeeded: boolean;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
interface GetUserInstancesResponse {
|
||||
instances: UserInstance[];
|
||||
}
|
||||
|
||||
// Add type for raw MCP tool response at the top with other types
|
||||
interface RawMcpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: string | {
|
||||
type: string;
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const KLAVIS_BASE_URL = 'https://api.klavis.ai';
|
||||
|
||||
interface KlavisApiCallOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
body?: Record<string, any>;
|
||||
additionalHeaders?: Record<string, string>;
|
||||
}
|
||||
|
||||
async function klavisApiCall<T>(
|
||||
endpoint: string,
|
||||
options: KlavisApiCallOptions = {}
|
||||
): Promise<T> {
|
||||
const { method = 'GET', body, additionalHeaders = {} } = options;
|
||||
const startTime = performance.now();
|
||||
const url = `${KLAVIS_BASE_URL}${endpoint}`;
|
||||
|
||||
try {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${process.env.KLAVIS_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
...additionalHeaders
|
||||
};
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
...(body ? { body: JSON.stringify(body) } : {})
|
||||
};
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
const endTime = performance.now();
|
||||
|
||||
console.log('[Klavis API] Response time:', {
|
||||
url,
|
||||
method,
|
||||
durationMs: Math.round(endTime - startTime)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return await response.json() as T;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
console.error('[Klavis API] Failed call:', {
|
||||
url,
|
||||
method,
|
||||
durationMs: Math.round(endTime - startTime),
|
||||
error
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Lists all active server instances for a given project
|
||||
export async function listActiveServerInstances(projectId: string): Promise<UserInstance[]> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
user_id: projectId,
|
||||
platform_name: 'Rowboat'
|
||||
});
|
||||
|
||||
console.log('[Klavis API] Fetching active instances:', { projectId, platformName: 'Rowboat' });
|
||||
|
||||
const endpoint = `/user/instances?${queryParams}`;
|
||||
const data = await klavisApiCall<GetUserInstancesResponse>(endpoint);
|
||||
|
||||
// Only show instances that are authenticated or need auth
|
||||
const relevantInstances = data.instances.filter(i => i.isAuthenticated || i.authNeeded);
|
||||
console.log('[Klavis API] Active instances:', {
|
||||
count: relevantInstances.length,
|
||||
authenticated: relevantInstances.filter(i => i.isAuthenticated).map(i => i.name).join(', '),
|
||||
needsAuth: relevantInstances.filter(i => i.authNeeded && !i.isAuthenticated).map(i => i.name).join(', ')
|
||||
});
|
||||
|
||||
return data.instances;
|
||||
} catch (error) {
|
||||
console.error('[Klavis API] Error listing active instances:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function enrichToolsWithParameters(
|
||||
projectId: string,
|
||||
serverName: string,
|
||||
basicTools: { name: string; description: string }[],
|
||||
isNewlyEnabled: boolean = false
|
||||
): Promise<McpToolType[]> {
|
||||
try {
|
||||
console.log(`[Klavis API] Starting tool enrichment for ${serverName}`);
|
||||
const enrichedTools = await fetchMcpToolsForServer(projectId, serverName);
|
||||
|
||||
if (enrichedTools.length === 0) {
|
||||
console.log(`[Klavis API] No tools enriched for ${serverName}`);
|
||||
return basicTools.map(tool => ({
|
||||
id: tool.name,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(`[Klavis API] Processing ${enrichedTools.length} tools for ${serverName}`);
|
||||
|
||||
// Create a map of enriched tools for this server
|
||||
const enrichedToolMap = new Map(
|
||||
enrichedTools.map(tool => [tool.name, {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters?.properties || {},
|
||||
required: tool.parameters?.required || []
|
||||
}
|
||||
}])
|
||||
);
|
||||
|
||||
// Find tools that couldn't be enriched
|
||||
const unenrichedTools = basicTools
|
||||
.filter(tool => !enrichedToolMap.has(tool.name))
|
||||
.map(tool => tool.name);
|
||||
|
||||
if (unenrichedTools.length > 0) {
|
||||
console.log('[Klavis API] Tools that could not be enriched:', {
|
||||
serverName,
|
||||
unenrichedTools: unenrichedTools.join(', ')
|
||||
});
|
||||
}
|
||||
|
||||
// Enrich the basic tools with parameters and descriptions
|
||||
const result = basicTools.map(basicTool => {
|
||||
const enrichedTool = enrichedToolMap.get(basicTool.name);
|
||||
|
||||
const tool: McpToolType = {
|
||||
id: basicTool.name,
|
||||
name: basicTool.name,
|
||||
description: enrichedTool?.description || basicTool.description || '',
|
||||
parameters: enrichedTool?.parameters || {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
};
|
||||
|
||||
return tool;
|
||||
});
|
||||
|
||||
console.log('[Klavis API] Tools processed:', {
|
||||
serverName,
|
||||
toolCount: result.length,
|
||||
tools: result.map(t => ({
|
||||
name: t.name,
|
||||
paramCount: Object.keys(t.parameters?.properties || {}).length,
|
||||
hasParams: t.parameters && Object.keys(t.parameters.properties).length > 0
|
||||
}))
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[Klavis API] Error enriching tools with parameters:', {
|
||||
serverName,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
basicToolCount: basicTools.length
|
||||
});
|
||||
// Return basic tools with empty parameters if enrichment fails
|
||||
return basicTools.map(tool => ({
|
||||
id: tool.name,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Modify listAvailableMcpServers to use enriched tools
|
||||
export async function listAvailableMcpServers(projectId: string): Promise<McpServerResponseType> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
console.log('[Klavis API] Starting server list fetch:', { projectId });
|
||||
|
||||
// Get MongoDB project data first
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
const mongodbServers = project?.mcpServers || [];
|
||||
const mongodbServerMap = new Map(mongodbServers.map(server => [server.name, server]));
|
||||
|
||||
console.log('[Klavis API] Found ', mongodbServers.length, ' MongoDB servers');
|
||||
|
||||
const serversEndpoint = '/mcp-server/servers';
|
||||
const rawData = await klavisApiCall<GetAllServersResponse>(serversEndpoint, {
|
||||
additionalHeaders: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
console.log('[Klavis API] Raw server response:', {
|
||||
serverCount: rawData.servers.length,
|
||||
servers: rawData.servers.map(s => s.name).join(', ')
|
||||
});
|
||||
|
||||
if (!rawData || !rawData.servers || !Array.isArray(rawData.servers)) {
|
||||
console.error('[Klavis API] Invalid response format:', rawData);
|
||||
return { data: null, error: 'Invalid response format from server' };
|
||||
}
|
||||
|
||||
// Get active instances for comparison
|
||||
const queryParams = new URLSearchParams({
|
||||
user_id: projectId,
|
||||
platform_name: 'Rowboat'
|
||||
});
|
||||
|
||||
const instancesEndpoint = `/user/instances?${queryParams}`;
|
||||
let activeInstances: UserInstance[] = [];
|
||||
|
||||
try {
|
||||
const instancesData = await klavisApiCall<GetUserInstancesResponse>(instancesEndpoint);
|
||||
activeInstances = instancesData.instances;
|
||||
console.log('[Klavis API] Active instances:', {
|
||||
count: activeInstances.length,
|
||||
authenticated: activeInstances.filter(i => i.isAuthenticated).map(i => i.name).join(', '),
|
||||
needsAuth: activeInstances.filter(i => i.authNeeded && !i.isAuthenticated).map(i => i.name).join(', ')
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Klavis API] Failed to fetch user instances:', error);
|
||||
}
|
||||
|
||||
const activeInstanceMap = new Map(activeInstances.map(instance => [instance.name, instance]));
|
||||
|
||||
// Transform and enrich the data
|
||||
const transformedServers = [];
|
||||
let eligibleCount = 0;
|
||||
let serversWithToolsCount = 0;
|
||||
|
||||
for (const server of rawData.servers) {
|
||||
const activeInstance = activeInstanceMap.get(server.name);
|
||||
const mongodbServer = mongodbServerMap.get(server.name);
|
||||
|
||||
// Determine server eligibility
|
||||
const isActive = !!activeInstance;
|
||||
const authNeeded = activeInstance ? activeInstance.authNeeded : (server.authNeeded || false);
|
||||
const isAuthenticated = activeInstance ? activeInstance.isAuthenticated : false;
|
||||
const isEligible = isActive && (!authNeeded || isAuthenticated);
|
||||
|
||||
// Get basic tools data first
|
||||
const basicTools = (server.tools || []).map(tool => ({
|
||||
id: tool.name || '',
|
||||
name: tool.name || '',
|
||||
description: tool.description || '',
|
||||
}));
|
||||
|
||||
let availableTools: McpToolType[];
|
||||
let selectedTools: McpToolType[];
|
||||
|
||||
// Only use MongoDB data for eligible servers
|
||||
if (isEligible) {
|
||||
eligibleCount++;
|
||||
console.log('[Klavis API] Processing server:', server.name);
|
||||
|
||||
// Use MongoDB data if available
|
||||
availableTools = mongodbServer?.availableTools || basicTools;
|
||||
selectedTools = mongodbServer?.tools || [];
|
||||
|
||||
if (selectedTools.length > 0) {
|
||||
serversWithToolsCount++;
|
||||
}
|
||||
} else {
|
||||
// For non-eligible servers, just use basic data
|
||||
availableTools = basicTools;
|
||||
selectedTools = [];
|
||||
}
|
||||
|
||||
transformedServers.push({
|
||||
...server,
|
||||
instanceId: activeInstance?.id || server.id,
|
||||
serverName: server.name,
|
||||
tools: selectedTools,
|
||||
availableTools,
|
||||
isActive,
|
||||
authNeeded,
|
||||
isAuthenticated,
|
||||
requiresAuth: server.authNeeded || false,
|
||||
serverUrl: mongodbServer?.serverUrl
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[Klavis API] Server processing complete:', {
|
||||
totalServers: transformedServers.length,
|
||||
eligibleServers: eligibleCount,
|
||||
serversWithTools: serversWithToolsCount
|
||||
});
|
||||
|
||||
return { data: transformedServers, error: null };
|
||||
} catch (error: any) {
|
||||
console.error('[Klavis API] Server list error:', error.message);
|
||||
return { data: null, error: error.message || 'An unexpected error occurred' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMcpServerInstance(
|
||||
serverName: string,
|
||||
projectId: string,
|
||||
platformName: string,
|
||||
): Promise<CreateServerInstanceResponse> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
const requestBody = {
|
||||
serverName,
|
||||
userId: projectId,
|
||||
platformName,
|
||||
connectionType: "StreamableHttp",
|
||||
};
|
||||
console.log('[Klavis API] Creating server instance:', requestBody);
|
||||
|
||||
const endpoint = '/mcp-server/instance/create';
|
||||
const result = await klavisApiCall<CreateServerInstanceResponse>(endpoint, {
|
||||
method: 'POST',
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
console.log('[Klavis API] Created server instance:', result);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error('[Klavis API] Error creating instance:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to filter eligible servers
|
||||
function getEligibleServers(servers: McpServerType[]): McpServerType[] {
|
||||
return servers.filter(server =>
|
||||
server.isActive && (!server.authNeeded || server.isAuthenticated)
|
||||
);
|
||||
}
|
||||
|
||||
async function getServerInstance(instanceId: string): Promise<{
|
||||
instanceId: string;
|
||||
authNeeded: boolean;
|
||||
isAuthenticated: boolean;
|
||||
serverName: string;
|
||||
serverUrl?: string;
|
||||
}> {
|
||||
const endpoint = `/mcp-server/instance/get/${instanceId}`;
|
||||
return await klavisApiCall(endpoint);
|
||||
}
|
||||
|
||||
export async function updateProjectServers(projectId: string, targetServerName?: string): Promise<void> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
console.log('[Auth] Starting server data update:', { projectId, targetServerName });
|
||||
|
||||
// Get current MongoDB data
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project) {
|
||||
console.error('[Auth] Project not found in MongoDB:', { projectId });
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
const mcpServers = project.mcpServers || [];
|
||||
|
||||
// Get active instances to find auth status
|
||||
const instances = await listActiveServerInstances(projectId);
|
||||
|
||||
// If targetServerName is provided, only process that server
|
||||
const instancesToProcess = targetServerName
|
||||
? instances.filter(i => i.name === targetServerName)
|
||||
: instances;
|
||||
|
||||
// For each active instance, get its current status
|
||||
for (const instance of instancesToProcess) {
|
||||
if (!instance.id) continue;
|
||||
|
||||
try {
|
||||
// Get fresh instance data
|
||||
const serverInstance = await getServerInstance(instance.id);
|
||||
|
||||
// Find this server in MongoDB
|
||||
const serverIndex = mcpServers.findIndex(s => s.name === instance.name);
|
||||
if (serverIndex === -1) continue;
|
||||
|
||||
// Update server readiness based on auth status
|
||||
const isReady = !serverInstance.authNeeded || serverInstance.isAuthenticated;
|
||||
|
||||
// Update existing server
|
||||
const updatedServer = {
|
||||
...mcpServers[serverIndex],
|
||||
isAuthenticated: serverInstance.isAuthenticated,
|
||||
isReady
|
||||
};
|
||||
mcpServers[serverIndex] = updatedServer;
|
||||
|
||||
// If server is now ready and has no tools, try to enrich them
|
||||
if (isReady && (!updatedServer.tools || updatedServer.tools.length === 0)) {
|
||||
try {
|
||||
console.log(`[Auth] Enriching tools for ${instance.name}`);
|
||||
const enrichedTools = await enrichToolsWithParameters(
|
||||
projectId,
|
||||
instance.name,
|
||||
updatedServer.availableTools || [],
|
||||
true
|
||||
);
|
||||
|
||||
if (enrichedTools.length > 0) {
|
||||
console.log(`[Auth] Writing ${enrichedTools.length} tools to DB for ${instance.name}`);
|
||||
updatedServer.availableTools = enrichedTools;
|
||||
await batchAddTools(projectId, instance.name, enrichedTools);
|
||||
}
|
||||
} catch (enrichError) {
|
||||
console.error(`[Auth] Tool enrichment failed for ${instance.name}:`, enrichError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Auth] Error updating ${instance.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update MongoDB with new server data
|
||||
await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { mcpServers } }
|
||||
);
|
||||
console.log('[Auth] MongoDB update completed');
|
||||
} catch (error) {
|
||||
console.error('[Auth] Error updating server data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function batchAddTools(projectId: string, serverName: string, tools: McpToolType[]): Promise<void> {
|
||||
console.log(`[Klavis API] Writing ${tools.length} tools to ${serverName}`);
|
||||
|
||||
const toolsToWrite = tools.map(tool => ({
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters || {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
}));
|
||||
|
||||
console.log('[Klavis API] DB Write - batchAddTools:', {
|
||||
serverName,
|
||||
toolCount: tools.length,
|
||||
tools: tools.map(t => t.name).join(', ')
|
||||
});
|
||||
|
||||
// Update MongoDB in a single operation
|
||||
await projectsCollection.updateOne(
|
||||
{ _id: projectId, "mcpServers.name": serverName },
|
||||
{
|
||||
$set: {
|
||||
"mcpServers.$.tools": toolsToWrite
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`[Klavis API] Tools written to ${serverName}`);
|
||||
}
|
||||
|
||||
export async function enableServer(
|
||||
serverName: string,
|
||||
projectId: string,
|
||||
enabled: boolean
|
||||
): Promise<CreateServerInstanceResponse | {} | { billingError: string }> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
console.log('[Klavis API] Toggle server request:', { serverName, projectId, enabled });
|
||||
|
||||
if (enabled) {
|
||||
// get count of enabled hosted mcp servers for this project
|
||||
const existingInstances = await listActiveServerInstances(projectId);
|
||||
// billing limit check
|
||||
const authResponse = await authorizeUserAction({
|
||||
type: 'enable_hosted_tool_server',
|
||||
data: {
|
||||
existingServerCount: existingInstances.length,
|
||||
},
|
||||
});
|
||||
if (!authResponse.success) {
|
||||
return { billingError: authResponse.error || 'Billing error' };
|
||||
}
|
||||
|
||||
// set key in redis to indicate that a server is being enabled on this project
|
||||
// the key set should only succeed if the key does not already exist
|
||||
const setResult = await redisClient.set(`klavis_enabling_server:${projectId}`, 'true', 'EX', 60 * 60, 'NX');
|
||||
console.log('[redis] Set result here:', setResult);
|
||||
if (setResult !== 'OK') {
|
||||
throw new Error("A server is already being enabled on this project");
|
||||
}
|
||||
|
||||
console.log(`[Klavis API] Creating server instance for ${serverName}...`);
|
||||
const result = await createMcpServerInstance(serverName, projectId, "Rowboat");
|
||||
console.log('[Klavis API] Server instance created:', {
|
||||
serverName,
|
||||
instanceId: result.instanceId,
|
||||
serverUrl: result.serverUrl
|
||||
});
|
||||
|
||||
// Get the current server list from MongoDB
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project) throw new Error("Project not found");
|
||||
|
||||
const mcpServers = project.mcpServers || [];
|
||||
|
||||
// Find the server we're enabling
|
||||
const serverIndex = mcpServers.findIndex(s => s.name === serverName);
|
||||
const rawServerData = (await klavisApiCall<GetAllServersResponse>('/mcp-server/servers')).servers
|
||||
.find(s => s.name === serverName);
|
||||
|
||||
if (!rawServerData) throw new Error("Server data not found");
|
||||
|
||||
// Get basic tools data
|
||||
const basicTools = (rawServerData.tools || []).map(tool => ({
|
||||
id: tool.name || '',
|
||||
name: tool.name || '',
|
||||
description: tool.description || '',
|
||||
}));
|
||||
|
||||
// Update server status in MongoDB
|
||||
const updatedServer = {
|
||||
...rawServerData,
|
||||
instanceId: result.instanceId,
|
||||
serverName: serverName,
|
||||
serverUrl: result.serverUrl,
|
||||
tools: basicTools, // Select all tools by default
|
||||
availableTools: basicTools, // Use basic tools initially
|
||||
isActive: true, // Keep isActive true to indicate server is enabled
|
||||
isReady: !rawServerData.authNeeded, // Use isReady to indicate eligibility
|
||||
authNeeded: rawServerData.authNeeded || false,
|
||||
isAuthenticated: false,
|
||||
requiresAuth: rawServerData.authNeeded || false
|
||||
};
|
||||
|
||||
if (serverIndex === -1) {
|
||||
mcpServers.push(updatedServer);
|
||||
} else {
|
||||
mcpServers[serverIndex] = updatedServer;
|
||||
}
|
||||
|
||||
// Update MongoDB with server status
|
||||
await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { mcpServers } }
|
||||
);
|
||||
|
||||
// Wait for server warm-up (increased from 2s to 5s)
|
||||
console.log(`[Klavis API] New server detected, waiting 5s for ${serverName} to initialize...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
console.log(`[Klavis API] Warm-up period complete for ${serverName}`);
|
||||
|
||||
// Try to enrich tools regardless of auth status
|
||||
try {
|
||||
console.log(`[Klavis API] Enriching tools for ${serverName}`);
|
||||
const enrichedTools = await enrichToolsWithParameters(
|
||||
projectId,
|
||||
serverName,
|
||||
basicTools,
|
||||
true
|
||||
);
|
||||
|
||||
if (enrichedTools.length > 0) {
|
||||
console.log(`[Klavis API] Writing ${enrichedTools.length} tools to DB for ${serverName}`);
|
||||
// First update availableTools
|
||||
await projectsCollection.updateOne(
|
||||
{ _id: projectId, "mcpServers.name": serverName },
|
||||
{
|
||||
$set: {
|
||||
"mcpServers.$.availableTools": enrichedTools,
|
||||
"mcpServers.$.isReady": true // Mark server as ready after successful enrichment
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Then write the tools
|
||||
await batchAddTools(projectId, serverName, enrichedTools);
|
||||
console.log(`[Klavis API] Successfully wrote tools for ${serverName}`);
|
||||
}
|
||||
} catch (enrichError) {
|
||||
console.error(`[Klavis API] Tool enrichment failed for ${serverName}:`, enrichError);
|
||||
}
|
||||
|
||||
// remove key from redis
|
||||
await redisClient.del(`klavis_enabling_server:${projectId}`);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
// Get active instances to find the one to delete
|
||||
const instances = await listActiveServerInstances(projectId);
|
||||
const instance = instances.find(i => i.name === serverName);
|
||||
|
||||
if (instance?.id) {
|
||||
// Check if this server uses auth token (authNeeded but no OAuth)
|
||||
const usesAuthToken = instance.authNeeded && !SERVER_URL_PARAMS[serverName];
|
||||
|
||||
if (usesAuthToken) {
|
||||
// Delete auth data first
|
||||
await deleteServerAuthData(instance.id);
|
||||
}
|
||||
|
||||
await deleteMcpServerInstance(instance.id, projectId);
|
||||
console.log('[Klavis API] Disabled server:', { serverName, instanceId: instance.id });
|
||||
|
||||
// Remove from MongoDB
|
||||
await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $pull: { mcpServers: { name: serverName } } }
|
||||
);
|
||||
} else {
|
||||
console.log('[Klavis API] No instance found to disable:', { serverName });
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[Klavis API] Toggle error:', {
|
||||
server: serverName,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMcpServerInstance(
|
||||
instanceId: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
console.log('[Klavis API] Deleting instance:', { instanceId });
|
||||
|
||||
const endpoint = `/mcp-server/instance/delete/${instanceId}`;
|
||||
try {
|
||||
await klavisApiCall<DeleteServerInstanceResponse>(endpoint, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
console.log('[Klavis API] Instance deleted successfully:', { instanceId });
|
||||
|
||||
// Get the server info from MongoDB to find its name
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
const server = project?.mcpServers?.find(s => s.instanceId === instanceId);
|
||||
|
||||
if (server) {
|
||||
// Update just this server's status in MongoDB
|
||||
await projectsCollection.updateOne(
|
||||
{ _id: projectId, "mcpServers.name": server.name },
|
||||
{
|
||||
$set: {
|
||||
"mcpServers.$.isActive": false,
|
||||
"mcpServers.$.serverUrl": null,
|
||||
"mcpServers.$.tools": [],
|
||||
"mcpServers.$.availableTools": [],
|
||||
"mcpServers.$.instanceId": null
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log('[MongoDB] Server status updated:', { serverName: server.name });
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('404')) {
|
||||
console.log('[Klavis API] Instance already deleted:', { instanceId });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[Klavis API] Error deleting instance:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateServerAuthUrl(
|
||||
serverName: string,
|
||||
projectId: string,
|
||||
instanceId: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// Get the origin from request headers
|
||||
const headersList = await headers();
|
||||
const host = headersList.get('host') || '';
|
||||
const protocol = headersList.get('x-forwarded-proto') || 'http';
|
||||
const origin = `${protocol}://${host}`;
|
||||
|
||||
// Get the URL parameter for this server
|
||||
const serverUrlParam = SERVER_URL_PARAMS[serverName] || serverName.toLowerCase();
|
||||
|
||||
// Build base params
|
||||
const params: Record<string, string> = {
|
||||
instance_id: instanceId,
|
||||
redirect_url: `${origin}/projects/${projectId}/tools/oauth/callback`
|
||||
};
|
||||
|
||||
// Add client_id if available for this server
|
||||
const clientId = SERVER_CLIENT_ID_MAP[serverName];
|
||||
if (clientId) {
|
||||
params.client_id = clientId;
|
||||
}
|
||||
|
||||
let authUrl = `${KLAVIS_BASE_URL}/oauth/${serverUrlParam}/authorize?${new URLSearchParams(params).toString()}`
|
||||
console.log('authUrl', authUrl);
|
||||
|
||||
return authUrl;
|
||||
} catch (error) {
|
||||
console.error('[Klavis API] Error generating auth URL:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncServerTools(projectId: string, serverName: string): Promise<void> {
|
||||
try {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
console.log('[Klavis API] Starting server tool sync:', { projectId, serverName });
|
||||
|
||||
// Get enriched tools from MCP
|
||||
const enrichedTools = await fetchMcpToolsForServer(projectId, serverName);
|
||||
console.log('[Klavis API] Received enriched tools:', {
|
||||
serverName,
|
||||
toolCount: enrichedTools.length
|
||||
});
|
||||
|
||||
// Convert enriched tools to the correct format
|
||||
const formattedTools = enrichedTools.map(tool => {
|
||||
return {
|
||||
id: tool.name,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters?.properties || {},
|
||||
required: tool.parameters?.required || []
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// First verify the server exists
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project) {
|
||||
throw new Error(`Project ${projectId} not found`);
|
||||
}
|
||||
const server = project.mcpServers?.find(s => s.name === serverName);
|
||||
if (!server) {
|
||||
throw new Error(`Server ${serverName} not found in project ${projectId}`);
|
||||
}
|
||||
|
||||
// Update MongoDB with enriched tools
|
||||
const updateResult = await projectsCollection.updateOne(
|
||||
{ _id: projectId, "mcpServers.name": serverName },
|
||||
{
|
||||
$set: {
|
||||
"mcpServers.$.availableTools": formattedTools,
|
||||
"mcpServers.$.tools": formattedTools // Also update selected tools to match
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('[Klavis API] Tools synced:', {
|
||||
serverName,
|
||||
toolCount: formattedTools.length,
|
||||
success: updateResult.modifiedCount > 0
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Klavis API] Error syncing server tools:', {
|
||||
serverName,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Auth Token Management Functions
|
||||
export async function setServerAuthToken(
|
||||
instanceId: string,
|
||||
authToken: string
|
||||
): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||
try {
|
||||
const response = await klavisApiCall<{ success: boolean; message: string }>(
|
||||
`/mcp-server/instance/set-auth-token`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { instanceId, authToken }
|
||||
}
|
||||
);
|
||||
|
||||
return { success: true, message: response.message };
|
||||
} catch (error: any) {
|
||||
// Handle 422 validation errors
|
||||
if (error.message.includes('422')) {
|
||||
try {
|
||||
const errorData = JSON.parse(error.message);
|
||||
const validationErrors = errorData.detail?.map((err: any) => err.msg).join(', ');
|
||||
return { success: false, error: validationErrors || 'Invalid auth token' };
|
||||
} catch {
|
||||
return { success: false, error: 'Invalid auth token format' };
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
return { success: false, error: 'Failed to set auth token. Please try again.' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteServerAuthData(instanceId: string): Promise<void> {
|
||||
try {
|
||||
await klavisApiCall<{ success: boolean; message: string }>(
|
||||
`/mcp-server/instance/delete-auth/${instanceId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
console.log('[Klavis API] Auth data deleted for instance:', instanceId);
|
||||
} catch (error: any) {
|
||||
// Log error but don't fail the deletion process
|
||||
console.error('[Klavis API] Failed to delete auth data:', error);
|
||||
// Don't throw - auth cleanup failure shouldn't prevent server deletion
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { WorkflowTool } from "../lib/types/workflow_types";
|
|||
import { projectAuthCheck } from "./project_actions";
|
||||
import { projectsCollection } from "../lib/mongodb";
|
||||
import { Project } from "../lib/types/project_types";
|
||||
import { MCPServer, McpServerTool, convertMcpServerToolToWorkflowTool } from "../lib/types/types";
|
||||
import { McpServerTool, convertMcpServerToolToWorkflowTool } from "../lib/types/types";
|
||||
import { getMcpClient } from "../lib/mcp";
|
||||
|
||||
export async function fetchMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { User, WithStringId } from "../lib/types/types";
|
|||
import { ApiKey } from "../lib/types/project_types";
|
||||
import { Project } from "../lib/types/project_types";
|
||||
import { USE_AUTH } from "../lib/feature_flags";
|
||||
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
|
||||
import { authorizeUserAction } from "./billing_actions";
|
||||
import { Workflow } from "../lib/types/workflow_types";
|
||||
|
||||
|
|
@ -188,53 +187,9 @@ interface McpServerDeletionError {
|
|||
error: string;
|
||||
}
|
||||
|
||||
async function cleanupMcpServers(projectId: string): Promise<McpServerDeletionError[]> {
|
||||
// Get all active instances directly from Klavis
|
||||
const activeInstances = await listActiveServerInstances(projectId);
|
||||
if (activeInstances.length === 0) return [];
|
||||
|
||||
console.log(`[Project Cleanup] Found ${activeInstances.length} active Klavis instances`);
|
||||
|
||||
// Track deletion errors
|
||||
const deletionErrors: McpServerDeletionError[] = [];
|
||||
|
||||
// Delete each instance
|
||||
const deletionPromises = activeInstances.map(async (instance) => {
|
||||
if (!instance.id) return; // Skip if no instance ID
|
||||
|
||||
try {
|
||||
await deleteMcpServerInstance(instance.id, projectId);
|
||||
console.log(`[Project Cleanup] Deleted Klavis instance: ${instance.name} (${instance.id})`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`[Project Cleanup] Failed to delete Klavis instance: ${instance.name}`, error);
|
||||
deletionErrors.push({
|
||||
serverName: instance.name,
|
||||
error: errorMessage
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all deletions to complete
|
||||
await Promise.all(deletionPromises);
|
||||
|
||||
return deletionErrors;
|
||||
}
|
||||
|
||||
export async function deleteProject(projectId: string) {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// First cleanup any Klavis instances
|
||||
if (KLAVIS_API_KEY) {
|
||||
const deletionErrors = await cleanupMcpServers(projectId);
|
||||
|
||||
// If there were any errors deleting instances, throw an error
|
||||
if (deletionErrors.length > 0) {
|
||||
const failedServers = deletionErrors.map(e => `${e.serverName} (${e.error})`).join(', ');
|
||||
throw new Error(`Cannot delete project because the following Klavis instances could not be deleted: ${failedServers}. Please try again or contact support if the issue persists.`);
|
||||
}
|
||||
}
|
||||
|
||||
// delete api keys
|
||||
await apiKeysCollection.deleteMany({
|
||||
projectId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue