mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
feat: mcp endpoints
This commit is contained in:
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -5,6 +5,7 @@ import { Cluster, Redis } from "ioredis";
|
|||||||
import { TUsers } from "@app/db/schemas";
|
import { TUsers } from "@app/db/schemas";
|
||||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||||
|
import { TAiMcpEndpointServiceFactory } from "@app/ee/services/ai-mcp-endpoint/ai-mcp-endpoint-service";
|
||||||
import { TAiMcpServerServiceFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-service";
|
import { TAiMcpServerServiceFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-service";
|
||||||
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types";
|
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types";
|
||||||
import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
@@ -363,6 +364,7 @@ declare module "fastify" {
|
|||||||
subOrganization: TSubOrgServiceFactory;
|
subOrganization: TSubOrgServiceFactory;
|
||||||
pkiAlertV2: TPkiAlertV2ServiceFactory;
|
pkiAlertV2: TPkiAlertV2ServiceFactory;
|
||||||
aiMcpServer: TAiMcpServerServiceFactory;
|
aiMcpServer: TAiMcpServerServiceFactory;
|
||||||
|
aiMcpEndpoint: TAiMcpEndpointServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
281
backend/src/ee/routes/v1/ai-mcp-endpoint-router.ts
Normal file
281
backend/src/ee/routes/v1/ai-mcp-endpoint-router.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AiMcpEndpointServerToolsSchema } from "@app/db/schemas/ai-mcp-endpoint-server-tools";
|
||||||
|
import { AiMcpEndpointsSchema } from "@app/db/schemas/ai-mcp-endpoints";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerAiMcpEndpointRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim().min(1),
|
||||||
|
name: z.string().trim().min(1).max(64),
|
||||||
|
description: z.string().trim().max(256).optional(),
|
||||||
|
serverIds: z.array(z.string().uuid()).default([])
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
endpoint: AiMcpEndpointsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const endpoint = await server.services.aiMcpEndpoint.createMcpEndpoint({
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
name: req.body.name,
|
||||||
|
description: req.body.description,
|
||||||
|
serverIds: req.body.serverIds
|
||||||
|
});
|
||||||
|
|
||||||
|
return { endpoint };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
endpoints: z.array(
|
||||||
|
AiMcpEndpointsSchema.extend({
|
||||||
|
connectedServers: z.number(),
|
||||||
|
activeTools: z.number()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const endpoints = await server.services.aiMcpEndpoint.listMcpEndpoints({
|
||||||
|
projectId: req.query.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
endpoints,
|
||||||
|
totalCount: endpoints.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:endpointId",
|
||||||
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
endpointId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
endpoint: AiMcpEndpointsSchema.extend({
|
||||||
|
connectedServers: z.number(),
|
||||||
|
activeTools: z.number(),
|
||||||
|
serverIds: z.array(z.string())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const endpoint = await server.services.aiMcpEndpoint.getMcpEndpointById({
|
||||||
|
endpointId: req.params.endpointId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { endpoint };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:endpointId",
|
||||||
|
method: "PATCH",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
endpointId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim().min(1).max(64).optional(),
|
||||||
|
description: z.string().trim().max(256).optional(),
|
||||||
|
serverIds: z.array(z.string().uuid()).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
endpoint: AiMcpEndpointsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const endpoint = await server.services.aiMcpEndpoint.updateMcpEndpoint({
|
||||||
|
endpointId: req.params.endpointId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return { endpoint };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:endpointId",
|
||||||
|
method: "DELETE",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
endpointId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
endpoint: AiMcpEndpointsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const endpoint = await server.services.aiMcpEndpoint.deleteMcpEndpoint({
|
||||||
|
endpointId: req.params.endpointId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { endpoint };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:endpointId/tools",
|
||||||
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
endpointId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
tools: z.array(AiMcpEndpointServerToolsSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const tools = await server.services.aiMcpEndpoint.listEndpointTools({
|
||||||
|
endpointId: req.params.endpointId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tools };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:endpointId/tools/:serverToolId",
|
||||||
|
method: "POST",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
endpointId: z.string().trim().min(1),
|
||||||
|
serverToolId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
tool: AiMcpEndpointServerToolsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const tool = await server.services.aiMcpEndpoint.enableEndpointTool({
|
||||||
|
endpointId: req.params.endpointId,
|
||||||
|
serverToolId: req.params.serverToolId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tool };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:endpointId/tools/:serverToolId",
|
||||||
|
method: "DELETE",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
endpointId: z.string().trim().min(1),
|
||||||
|
serverToolId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.aiMcpEndpoint.disableEndpointTool({
|
||||||
|
endpointId: req.params.endpointId,
|
||||||
|
serverToolId: req.params.serverToolId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Tool disabled" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:endpointId/tools/bulk",
|
||||||
|
method: "PATCH",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
endpointId: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
tools: z.array(
|
||||||
|
z.object({
|
||||||
|
serverToolId: z.string().uuid(),
|
||||||
|
isEnabled: z.boolean()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
tools: z.array(AiMcpEndpointServerToolsSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const tools = await server.services.aiMcpEndpoint.bulkUpdateEndpointTools({
|
||||||
|
endpointId: req.params.endpointId,
|
||||||
|
tools: req.body.tools
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tools };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-templat
|
|||||||
|
|
||||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||||
|
import { registerAiMcpEndpointRouter } from "./ai-mcp-endpoint-router";
|
||||||
import { registerAiMcpServerRouter } from "./ai-mcp-server-router";
|
import { registerAiMcpServerRouter } from "./ai-mcp-server-router";
|
||||||
import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
||||||
import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers";
|
import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers";
|
||||||
@@ -217,6 +218,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(
|
await server.register(
|
||||||
async (aiRouter) => {
|
async (aiRouter) => {
|
||||||
await aiRouter.register(registerAiMcpServerRouter, { prefix: "/mcp-servers" });
|
await aiRouter.register(registerAiMcpServerRouter, { prefix: "/mcp-servers" });
|
||||||
|
await aiRouter.register(registerAiMcpEndpointRouter, { prefix: "/mcp-endpoints" });
|
||||||
},
|
},
|
||||||
{ prefix: "/ai" }
|
{ prefix: "/ai" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TAiMcpEndpointDALFactory } from "./ai-mcp-endpoint-dal";
|
||||||
|
import { TAiMcpEndpointServerDALFactory } from "./ai-mcp-endpoint-server-dal";
|
||||||
|
import { TAiMcpEndpointServerToolDALFactory } from "./ai-mcp-endpoint-server-tool-dal";
|
||||||
|
import {
|
||||||
|
TAiMcpEndpointWithServers,
|
||||||
|
TBulkUpdateEndpointToolsDTO,
|
||||||
|
TCreateAiMcpEndpointDTO,
|
||||||
|
TDeleteAiMcpEndpointDTO,
|
||||||
|
TDisableEndpointToolDTO,
|
||||||
|
TEnableEndpointToolDTO,
|
||||||
|
TGetAiMcpEndpointDTO,
|
||||||
|
TListAiMcpEndpointsDTO,
|
||||||
|
TListEndpointToolsDTO,
|
||||||
|
TUpdateAiMcpEndpointDTO
|
||||||
|
} from "./ai-mcp-endpoint-types";
|
||||||
|
|
||||||
|
type TAiMcpEndpointServiceFactoryDep = {
|
||||||
|
aiMcpEndpointDAL: TAiMcpEndpointDALFactory;
|
||||||
|
aiMcpEndpointServerDAL: TAiMcpEndpointServerDALFactory;
|
||||||
|
aiMcpEndpointServerToolDAL: TAiMcpEndpointServerToolDALFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAiMcpEndpointServiceFactory = ReturnType<typeof aiMcpEndpointServiceFactory>;
|
||||||
|
|
||||||
|
export const aiMcpEndpointServiceFactory = ({
|
||||||
|
aiMcpEndpointDAL,
|
||||||
|
aiMcpEndpointServerDAL,
|
||||||
|
aiMcpEndpointServerToolDAL
|
||||||
|
}: TAiMcpEndpointServiceFactoryDep) => {
|
||||||
|
const createMcpEndpoint = async ({ projectId, name, description, serverIds }: TCreateAiMcpEndpointDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.create({
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
status: "active"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect servers if provided
|
||||||
|
if (serverIds && serverIds.length > 0) {
|
||||||
|
await aiMcpEndpointServerDAL.insertMany(
|
||||||
|
serverIds.map((serverId) => ({
|
||||||
|
aiMcpEndpointId: endpoint.id,
|
||||||
|
aiMcpServerId: serverId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listMcpEndpoints = async ({ projectId }: TListAiMcpEndpointsDTO): Promise<TAiMcpEndpointWithServers[]> => {
|
||||||
|
const endpoints = await aiMcpEndpointDAL.find({ projectId });
|
||||||
|
|
||||||
|
// Get connected servers count and tools count for each endpoint
|
||||||
|
const endpointsWithStats = await Promise.all(
|
||||||
|
endpoints.map(async (endpoint) => {
|
||||||
|
const connectedServers = await aiMcpEndpointServerDAL.find({ aiMcpEndpointId: endpoint.id });
|
||||||
|
const tools = await aiMcpEndpointServerToolDAL.find({ aiMcpEndpointId: endpoint.id });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...endpoint,
|
||||||
|
connectedServers: connectedServers.length,
|
||||||
|
activeTools: tools.length
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return endpointsWithStats;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMcpEndpointById = async ({ endpointId }: TGetAiMcpEndpointDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectedServers = await aiMcpEndpointServerDAL.find({ aiMcpEndpointId: endpointId });
|
||||||
|
const tools = await aiMcpEndpointServerToolDAL.find({ aiMcpEndpointId: endpointId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...endpoint,
|
||||||
|
connectedServers: connectedServers.length,
|
||||||
|
activeTools: tools.length,
|
||||||
|
serverIds: connectedServers.map((s) => s.aiMcpServerId)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMcpEndpoint = async ({ endpointId, name, description, serverIds }: TUpdateAiMcpEndpointDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: { name?: string; description?: string } = {};
|
||||||
|
if (name !== undefined) updateData.name = name;
|
||||||
|
if (description !== undefined) updateData.description = description;
|
||||||
|
|
||||||
|
const updatedEndpoint = await aiMcpEndpointDAL.updateById(endpointId, updateData);
|
||||||
|
|
||||||
|
// Update server connections if provided
|
||||||
|
if (serverIds !== undefined) {
|
||||||
|
// Delete existing connections
|
||||||
|
await aiMcpEndpointServerDAL.delete({ aiMcpEndpointId: endpointId });
|
||||||
|
|
||||||
|
// Add new connections
|
||||||
|
if (serverIds.length > 0) {
|
||||||
|
await aiMcpEndpointServerDAL.insertMany(
|
||||||
|
serverIds.map((serverId) => ({
|
||||||
|
aiMcpEndpointId: endpointId,
|
||||||
|
aiMcpServerId: serverId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedEndpoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMcpEndpoint = async ({ endpointId }: TDeleteAiMcpEndpointDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete endpoint
|
||||||
|
await aiMcpEndpointDAL.deleteById(endpointId);
|
||||||
|
|
||||||
|
return endpoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listEndpointTools = async ({ endpointId }: TListEndpointToolsDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolConfigs = await aiMcpEndpointServerToolDAL.find({ aiMcpEndpointId: endpointId });
|
||||||
|
return toolConfigs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableEndpointTool = async ({ endpointId, serverToolId }: TEnableEndpointToolDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingConfig = await aiMcpEndpointServerToolDAL.findOne({
|
||||||
|
aiMcpEndpointId: endpointId,
|
||||||
|
aiMcpServerToolId: serverToolId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingConfig) {
|
||||||
|
return existingConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
return aiMcpEndpointServerToolDAL.create({
|
||||||
|
aiMcpEndpointId: endpointId,
|
||||||
|
aiMcpServerToolId: serverToolId,
|
||||||
|
isEnabled: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableEndpointTool = async ({ endpointId, serverToolId }: TDisableEndpointToolDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingConfig = await aiMcpEndpointServerToolDAL.findOne({
|
||||||
|
aiMcpEndpointId: endpointId,
|
||||||
|
aiMcpServerToolId: serverToolId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingConfig) {
|
||||||
|
await aiMcpEndpointServerToolDAL.deleteById(existingConfig.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkUpdateEndpointTools = async ({ endpointId, tools }: TBulkUpdateEndpointToolsDTO) => {
|
||||||
|
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate tools to enable and disable
|
||||||
|
const toEnable = tools.filter((t) => t.isEnabled);
|
||||||
|
const toDisable = tools.filter((t) => !t.isEnabled);
|
||||||
|
|
||||||
|
// Delete disabled tools
|
||||||
|
if (toDisable.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
toDisable.map(async ({ serverToolId }) => {
|
||||||
|
const existing = await aiMcpEndpointServerToolDAL.findOne({
|
||||||
|
aiMcpEndpointId: endpointId,
|
||||||
|
aiMcpServerToolId: serverToolId
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
await aiMcpEndpointServerToolDAL.deleteById(existing.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create enabled tools (if not already existing)
|
||||||
|
const results = await Promise.all(
|
||||||
|
toEnable.map(async ({ serverToolId }) => {
|
||||||
|
const existing = await aiMcpEndpointServerToolDAL.findOne({
|
||||||
|
aiMcpEndpointId: endpointId,
|
||||||
|
aiMcpServerToolId: serverToolId
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
return aiMcpEndpointServerToolDAL.create({
|
||||||
|
aiMcpEndpointId: endpointId,
|
||||||
|
aiMcpServerToolId: serverToolId,
|
||||||
|
isEnabled: true
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createMcpEndpoint,
|
||||||
|
listMcpEndpoints,
|
||||||
|
getMcpEndpointById,
|
||||||
|
updateMcpEndpoint,
|
||||||
|
deleteMcpEndpoint,
|
||||||
|
listEndpointTools,
|
||||||
|
enableEndpointTool,
|
||||||
|
disableEndpointTool,
|
||||||
|
bulkUpdateEndpointTools
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
export type TCreateAiMcpEndpointDTO = {
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
serverIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateAiMcpEndpointDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
serverIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteAiMcpEndpointDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetAiMcpEndpointDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListAiMcpEndpointsDTO = {
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAiMcpEndpointWithServers = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
projectId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
connectedServers: number;
|
||||||
|
activeTools: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListEndpointToolsDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEnableEndpointToolDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
serverToolId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDisableEndpointToolDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
serverToolId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBulkUpdateEndpointToolsDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
tools: Array<{
|
||||||
|
serverToolId: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEndpointToolConfig = {
|
||||||
|
id: string;
|
||||||
|
aiMcpEndpointId: string;
|
||||||
|
aiMcpServerToolId: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
};
|
||||||
@@ -17,6 +17,10 @@ import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-appr
|
|||||||
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
||||||
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
|
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
|
||||||
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||||
|
import { aiMcpEndpointDALFactory } from "@app/ee/services/ai-mcp-endpoint/ai-mcp-endpoint-dal";
|
||||||
|
import { aiMcpEndpointServerDALFactory } from "@app/ee/services/ai-mcp-endpoint/ai-mcp-endpoint-server-dal";
|
||||||
|
import { aiMcpEndpointServerToolDALFactory } from "@app/ee/services/ai-mcp-endpoint/ai-mcp-endpoint-server-tool-dal";
|
||||||
|
import { aiMcpEndpointServiceFactory } from "@app/ee/services/ai-mcp-endpoint/ai-mcp-endpoint-service";
|
||||||
import { aiMcpServerDALFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-dal";
|
import { aiMcpServerDALFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-dal";
|
||||||
import { aiMcpServerServiceFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-service";
|
import { aiMcpServerServiceFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-service";
|
||||||
import { aiMcpServerToolDALFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-tool-dal";
|
import { aiMcpServerToolDALFactory } from "@app/ee/services/ai-mcp-server/ai-mcp-server-tool-dal";
|
||||||
@@ -2402,6 +2406,9 @@ export const registerRoutes = async (
|
|||||||
const pamSessionDAL = pamSessionDALFactory(db);
|
const pamSessionDAL = pamSessionDALFactory(db);
|
||||||
const aiMcpServerDAL = aiMcpServerDALFactory(db);
|
const aiMcpServerDAL = aiMcpServerDALFactory(db);
|
||||||
const aiMcpServerToolDAL = aiMcpServerToolDALFactory(db);
|
const aiMcpServerToolDAL = aiMcpServerToolDALFactory(db);
|
||||||
|
const aiMcpEndpointDAL = aiMcpEndpointDALFactory(db);
|
||||||
|
const aiMcpEndpointServerDAL = aiMcpEndpointServerDALFactory(db);
|
||||||
|
const aiMcpEndpointServerToolDAL = aiMcpEndpointServerToolDALFactory(db);
|
||||||
|
|
||||||
const pamFolderService = pamFolderServiceFactory({
|
const pamFolderService = pamFolderServiceFactory({
|
||||||
pamFolderDAL,
|
pamFolderDAL,
|
||||||
@@ -2451,6 +2458,12 @@ export const registerRoutes = async (
|
|||||||
keyStore
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const aiMcpEndpointService = aiMcpEndpointServiceFactory({
|
||||||
|
aiMcpEndpointDAL,
|
||||||
|
aiMcpEndpointServerDAL,
|
||||||
|
aiMcpEndpointServerToolDAL
|
||||||
|
});
|
||||||
|
|
||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
externalMigrationQueue,
|
externalMigrationQueue,
|
||||||
userDAL,
|
userDAL,
|
||||||
@@ -2643,7 +2656,8 @@ export const registerRoutes = async (
|
|||||||
identityProject: identityProjectService,
|
identityProject: identityProjectService,
|
||||||
convertor: convertorService,
|
convertor: convertorService,
|
||||||
pkiAlertV2: pkiAlertV2Service,
|
pkiAlertV2: pkiAlertV2Service,
|
||||||
aiMcpServer: aiMcpServerService
|
aiMcpServer: aiMcpServerService,
|
||||||
|
aiMcpEndpoint: aiMcpEndpointService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
|||||||
15
frontend/src/hooks/api/aiMcpEndpoints/index.ts
Normal file
15
frontend/src/hooks/api/aiMcpEndpoints/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export {
|
||||||
|
useBulkUpdateEndpointTools,
|
||||||
|
useCreateAiMcpEndpoint,
|
||||||
|
useDeleteAiMcpEndpoint,
|
||||||
|
useDisableEndpointTool,
|
||||||
|
useEnableEndpointTool,
|
||||||
|
useUpdateAiMcpEndpoint
|
||||||
|
} from "./mutations";
|
||||||
|
export {
|
||||||
|
aiMcpEndpointKeys,
|
||||||
|
useGetAiMcpEndpointById,
|
||||||
|
useListAiMcpEndpoints,
|
||||||
|
useListEndpointTools
|
||||||
|
} from "./queries";
|
||||||
|
export * from "./types";
|
||||||
126
frontend/src/hooks/api/aiMcpEndpoints/mutations.tsx
Normal file
126
frontend/src/hooks/api/aiMcpEndpoints/mutations.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { aiMcpEndpointKeys } from "./queries";
|
||||||
|
import {
|
||||||
|
TAiMcpEndpoint,
|
||||||
|
TAiMcpEndpointToolConfig,
|
||||||
|
TBulkUpdateEndpointToolsDTO,
|
||||||
|
TCreateAiMcpEndpointDTO,
|
||||||
|
TDeleteAiMcpEndpointDTO,
|
||||||
|
TDisableEndpointToolDTO,
|
||||||
|
TEnableEndpointToolDTO,
|
||||||
|
TUpdateAiMcpEndpointDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const useCreateAiMcpEndpoint = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (dto: TCreateAiMcpEndpointDTO) => {
|
||||||
|
const { data } = await apiRequest.post<{ endpoint: TAiMcpEndpoint }>(
|
||||||
|
"/api/v1/ai/mcp-endpoints",
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
return data.endpoint;
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: aiMcpEndpointKeys.list(variables.projectId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateAiMcpEndpoint = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ endpointId, ...dto }: TUpdateAiMcpEndpointDTO) => {
|
||||||
|
const { data } = await apiRequest.patch<{ endpoint: TAiMcpEndpoint }>(
|
||||||
|
`/api/v1/ai/mcp-endpoints/${endpointId}`,
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
return data.endpoint;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: aiMcpEndpointKeys.list(data.projectId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: aiMcpEndpointKeys.byId(data.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteAiMcpEndpoint = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ endpointId }: TDeleteAiMcpEndpointDTO) => {
|
||||||
|
const { data } = await apiRequest.delete<{ endpoint: TAiMcpEndpoint }>(
|
||||||
|
`/api/v1/ai/mcp-endpoints/${endpointId}`
|
||||||
|
);
|
||||||
|
return data.endpoint;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: aiMcpEndpointKeys.list(data.projectId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEnableEndpointTool = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ endpointId, serverToolId }: TEnableEndpointToolDTO) => {
|
||||||
|
const { data } = await apiRequest.post<{ tool: TAiMcpEndpointToolConfig }>(
|
||||||
|
`/api/v1/ai/mcp-endpoints/${endpointId}/tools/${serverToolId}`
|
||||||
|
);
|
||||||
|
return data.tool;
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: aiMcpEndpointKeys.tools(variables.endpointId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDisableEndpointTool = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ endpointId, serverToolId }: TDisableEndpointToolDTO) => {
|
||||||
|
await apiRequest.delete(`/api/v1/ai/mcp-endpoints/${endpointId}/tools/${serverToolId}`);
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: aiMcpEndpointKeys.tools(variables.endpointId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBulkUpdateEndpointTools = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ endpointId, tools }: TBulkUpdateEndpointToolsDTO) => {
|
||||||
|
const { data } = await apiRequest.patch<{ tools: TAiMcpEndpointToolConfig[] }>(
|
||||||
|
`/api/v1/ai/mcp-endpoints/${endpointId}/tools/bulk`,
|
||||||
|
{ tools }
|
||||||
|
);
|
||||||
|
return data.tools;
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: aiMcpEndpointKeys.tools(variables.endpointId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
59
frontend/src/hooks/api/aiMcpEndpoints/queries.tsx
Normal file
59
frontend/src/hooks/api/aiMcpEndpoints/queries.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TAiMcpEndpoint,
|
||||||
|
TAiMcpEndpointToolConfig,
|
||||||
|
TAiMcpEndpointWithServerIds,
|
||||||
|
TListAiMcpEndpointsDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const aiMcpEndpointKeys = {
|
||||||
|
all: ["aiMcpEndpoints"] as const,
|
||||||
|
list: (projectId: string) => [...aiMcpEndpointKeys.all, "list", projectId] as const,
|
||||||
|
byId: (endpointId: string) => [...aiMcpEndpointKeys.all, "byId", endpointId] as const,
|
||||||
|
tools: (endpointId: string) => [...aiMcpEndpointKeys.all, "tools", endpointId] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListAiMcpEndpoints = ({ projectId }: TListAiMcpEndpointsDTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: aiMcpEndpointKeys.list(projectId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
endpoints: TAiMcpEndpoint[];
|
||||||
|
totalCount: number;
|
||||||
|
}>("/api/v1/ai/mcp-endpoints", {
|
||||||
|
params: { projectId }
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAiMcpEndpointById = ({ endpointId }: { endpointId: string }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: aiMcpEndpointKeys.byId(endpointId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ endpoint: TAiMcpEndpointWithServerIds }>(
|
||||||
|
`/api/v1/ai/mcp-endpoints/${endpointId}`
|
||||||
|
);
|
||||||
|
return data.endpoint;
|
||||||
|
},
|
||||||
|
enabled: Boolean(endpointId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListEndpointTools = ({ endpointId }: { endpointId: string }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: aiMcpEndpointKeys.tools(endpointId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ tools: TAiMcpEndpointToolConfig[] }>(
|
||||||
|
`/api/v1/ai/mcp-endpoints/${endpointId}/tools`
|
||||||
|
);
|
||||||
|
return data.tools;
|
||||||
|
},
|
||||||
|
enabled: Boolean(endpointId)
|
||||||
|
});
|
||||||
|
};
|
||||||
70
frontend/src/hooks/api/aiMcpEndpoints/types.ts
Normal file
70
frontend/src/hooks/api/aiMcpEndpoints/types.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export type TAiMcpEndpoint = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
status: string | null;
|
||||||
|
projectId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
connectedServers: number;
|
||||||
|
activeTools: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAiMcpEndpointWithServerIds = TAiMcpEndpoint & {
|
||||||
|
serverIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateAiMcpEndpointDTO = {
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
serverIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateAiMcpEndpointDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
serverIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteAiMcpEndpointDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListAiMcpEndpointsDTO = {
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetAiMcpEndpointDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAiMcpEndpointToolConfig = {
|
||||||
|
id: string;
|
||||||
|
aiMcpEndpointId: string;
|
||||||
|
aiMcpServerToolId: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListEndpointToolsDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEnableEndpointToolDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
serverToolId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDisableEndpointToolDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
serverToolId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBulkUpdateEndpointToolsDTO = {
|
||||||
|
endpointId: string;
|
||||||
|
tools: Array<{
|
||||||
|
serverToolId: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./accessApproval";
|
export * from "./accessApproval";
|
||||||
export * from "./admin";
|
export * from "./admin";
|
||||||
|
export * from "./aiMcpEndpoints";
|
||||||
export * from "./aiMcpServers";
|
export * from "./aiMcpServers";
|
||||||
export * from "./apiKeys";
|
export * from "./apiKeys";
|
||||||
export * from "./assumePrivileges";
|
export * from "./assumePrivileges";
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import {
|
||||||
|
faBan,
|
||||||
|
faChevronLeft,
|
||||||
|
faEllipsisV,
|
||||||
|
faNetworkWired
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ContentLoader,
|
||||||
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useDeleteAiMcpEndpoint, useGetAiMcpEndpointById } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { EditMCPEndpointModal } from "../MCPPage/components/MCPEndpointsTab/EditMCPEndpointModal";
|
||||||
|
import {
|
||||||
|
MCPEndpointConnectedServersSection,
|
||||||
|
MCPEndpointConnectionSection,
|
||||||
|
MCPEndpointDetailsSection,
|
||||||
|
MCPEndpointToolSelectionSection
|
||||||
|
} from "./components";
|
||||||
|
|
||||||
|
const MCPEndpointStatusBadge = ({ status }: { status: string | null }) => {
|
||||||
|
const statusConfig: Record<string, { color: string; label: string }> = {
|
||||||
|
active: { color: "bg-emerald-500", label: "Active" },
|
||||||
|
inactive: { color: "bg-red-500", label: "Inactive" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status || "inactive"] || statusConfig.inactive;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 rounded-full border border-mineshaft-500 bg-mineshaft-800 px-3 py-1">
|
||||||
|
<div className={`h-2 w-2 rounded-full ${config.color}`} />
|
||||||
|
<span className="text-sm text-mineshaft-200">{config.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageContent = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const params = useParams({
|
||||||
|
strict: false
|
||||||
|
}) as { endpointId?: string; projectId?: string; orgId?: string };
|
||||||
|
|
||||||
|
const { endpointId, projectId, orgId } = params;
|
||||||
|
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data: mcpEndpoint, isPending } = useGetAiMcpEndpointById({
|
||||||
|
endpointId: endpointId!
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteEndpoint = useDeleteAiMcpEndpoint();
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<ContentLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mcpEndpoint) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center px-20">
|
||||||
|
<EmptyState
|
||||||
|
className="max-w-2xl rounded-md text-center"
|
||||||
|
icon={faBan}
|
||||||
|
title={`Could not find MCP Endpoint with ID ${endpointId}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate({
|
||||||
|
to: "/organizations/$orgId/projects/ai/$projectId/overview",
|
||||||
|
params: { orgId: orgId!, projectId: projectId! }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!mcpEndpoint) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteEndpoint.mutateAsync({ endpointId: mcpEndpoint.id });
|
||||||
|
createNotification({
|
||||||
|
text: `MCP endpoint "${mcpEndpoint.name}" deleted successfully`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
handleBack();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete MCP endpoint:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete MCP endpoint",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex max-w-7xl flex-col px-6 py-6 text-mineshaft-50">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBack}
|
||||||
|
className="mb-4 flex items-center gap-1 text-sm text-bunker-300 hover:text-primary-400"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faChevronLeft} className="text-xs" />
|
||||||
|
MCP Endpoints
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-mineshaft-700">
|
||||||
|
<FontAwesomeIcon icon={faNetworkWired} className="text-xl text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-mineshaft-100">{mcpEndpoint.name}</h1>
|
||||||
|
<p className="text-sm text-bunker-300">MCP Endpoint</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<MCPEndpointStatusBadge status={mcpEndpoint.status} />
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline_bg" size="sm">
|
||||||
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setIsEditModalOpen(true)}>
|
||||||
|
Edit Endpoint
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setIsDeleteModalOpen(true)} className="text-red-500">
|
||||||
|
Delete Endpoint
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{/* Left Column - Details, Connection, Connected Servers */}
|
||||||
|
<div className="flex w-96 flex-col gap-4">
|
||||||
|
<MCPEndpointDetailsSection
|
||||||
|
endpoint={mcpEndpoint}
|
||||||
|
onEdit={() => setIsEditModalOpen(true)}
|
||||||
|
/>
|
||||||
|
<MCPEndpointConnectionSection endpoint={mcpEndpoint} />
|
||||||
|
<MCPEndpointConnectedServersSection
|
||||||
|
projectId={mcpEndpoint.projectId}
|
||||||
|
serverIds={mcpEndpoint.serverIds}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - Tool Selection */}
|
||||||
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
|
<MCPEndpointToolSelectionSection
|
||||||
|
endpointId={mcpEndpoint.id}
|
||||||
|
projectId={mcpEndpoint.projectId}
|
||||||
|
serverIds={mcpEndpoint.serverIds}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditMCPEndpointModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
onOpenChange={setIsEditModalOpen}
|
||||||
|
endpoint={mcpEndpoint}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
title={`Delete MCP Endpoint ${mcpEndpoint.name}?`}
|
||||||
|
onChange={(isOpen) => setIsDeleteModalOpen(isOpen)}
|
||||||
|
deleteKey={mcpEndpoint.name}
|
||||||
|
onDeleteApproved={handleDeleteConfirm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MCPEndpointDetailPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>MCP Endpoint | Infisical</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
</Helmet>
|
||||||
|
<PageContent />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useListAiMcpServers } from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectId: string;
|
||||||
|
serverIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MCPEndpointConnectedServersSection = ({ projectId, serverIds }: Props) => {
|
||||||
|
const { data: serversData } = useListAiMcpServers({ projectId });
|
||||||
|
|
||||||
|
const connectedServers =
|
||||||
|
serversData?.servers.filter((server) => serverIds.includes(server.id)) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
|
<h3 className="font-medium text-mineshaft-100">Connected MCP Servers</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{connectedServers.length === 0 ? (
|
||||||
|
<p className="py-2 text-sm text-bunker-400">No servers connected</p>
|
||||||
|
) : (
|
||||||
|
connectedServers.map((server) => (
|
||||||
|
<div
|
||||||
|
key={server.id}
|
||||||
|
className="flex items-center gap-3 rounded-md p-2 transition-colors hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faServer} className="text-sm text-bunker-400" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-mineshaft-200">{server.name}</p>
|
||||||
|
{server.description && (
|
||||||
|
<p className="text-xs text-bunker-400">{server.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`h-2 w-2 rounded-full ${
|
||||||
|
server.status === "active" ? "bg-emerald-500" : "bg-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { GenericFieldLabel } from "@app/components/v2";
|
||||||
|
import { TAiMcpEndpointWithServerIds } from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
endpoint: TAiMcpEndpointWithServerIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MCPEndpointConnectionSection = ({ endpoint }: Props) => {
|
||||||
|
// Generate a mock endpoint URL based on the endpoint name
|
||||||
|
const endpointUrl = `mcp://${endpoint.name.toLowerCase().replace(/\s+/g, "-")}.infisical.com:8080`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
|
<h3 className="font-medium text-mineshaft-100">Connection</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<GenericFieldLabel label="Endpoint">
|
||||||
|
<code className="rounded bg-mineshaft-700 px-2 py-1 font-mono text-sm text-mineshaft-200">
|
||||||
|
{endpointUrl}
|
||||||
|
</code>
|
||||||
|
</GenericFieldLabel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { GenericFieldLabel, IconButton } from "@app/components/v2";
|
||||||
|
import { TAiMcpEndpointWithServerIds } from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
endpoint: TAiMcpEndpointWithServerIds;
|
||||||
|
onEdit: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string | null) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
active: "Active",
|
||||||
|
inactive: "Inactive"
|
||||||
|
};
|
||||||
|
return labels[status || "inactive"] || "Unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string | null) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
active: "bg-emerald-500",
|
||||||
|
inactive: "bg-red-500"
|
||||||
|
};
|
||||||
|
return colors[status || "inactive"] || "bg-red-500";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MCPEndpointDetailsSection = ({ endpoint, onEdit }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
|
<h3 className="font-medium text-mineshaft-100">Details</h3>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
ariaLabel="Edit endpoint details"
|
||||||
|
onClick={onEdit}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<GenericFieldLabel label="Name">{endpoint.name}</GenericFieldLabel>
|
||||||
|
<GenericFieldLabel label="Description">
|
||||||
|
{endpoint.description || <span className="text-bunker-400">No description</span>}
|
||||||
|
</GenericFieldLabel>
|
||||||
|
<GenericFieldLabel label="Status">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`h-2 w-2 rounded-full ${getStatusColor(endpoint.status)}`} />
|
||||||
|
{getStatusLabel(endpoint.status)}
|
||||||
|
</div>
|
||||||
|
</GenericFieldLabel>
|
||||||
|
<GenericFieldLabel label="Created">
|
||||||
|
{format(new Date(endpoint.createdAt), "yyyy-MM-dd, hh:mm aaa")}
|
||||||
|
</GenericFieldLabel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
|
faInfoCircle,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faServer
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Input, Switch, Tooltip } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
TAiMcpEndpointToolConfig,
|
||||||
|
useDisableEndpointTool,
|
||||||
|
useEnableEndpointTool,
|
||||||
|
useListAiMcpServers,
|
||||||
|
useListAiMcpServerTools,
|
||||||
|
useListEndpointTools
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
endpointId: string;
|
||||||
|
projectId: string;
|
||||||
|
serverIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServerToolsSectionProps = {
|
||||||
|
serverId: string;
|
||||||
|
serverName: string;
|
||||||
|
serverStatus: string;
|
||||||
|
searchQuery: string;
|
||||||
|
toolConfigs: TAiMcpEndpointToolConfig[];
|
||||||
|
onToolToggle: (serverToolId: string, isEnabled: boolean) => void;
|
||||||
|
isUpdating: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerToolsSection = ({
|
||||||
|
serverId,
|
||||||
|
serverName,
|
||||||
|
serverStatus,
|
||||||
|
searchQuery,
|
||||||
|
toolConfigs,
|
||||||
|
onToolToggle,
|
||||||
|
isUpdating
|
||||||
|
}: ServerToolsSectionProps) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const { data: toolsData } = useListAiMcpServerTools({ serverId });
|
||||||
|
const tools = toolsData?.tools || [];
|
||||||
|
|
||||||
|
// Create a set of enabled tool IDs for quick lookup
|
||||||
|
// Presence in the list = enabled, absence = disabled
|
||||||
|
const enabledToolIds = useMemo(() => {
|
||||||
|
return new Set(toolConfigs.map((config) => config.aiMcpServerToolId));
|
||||||
|
}, [toolConfigs]);
|
||||||
|
|
||||||
|
// Filter tools based on search query
|
||||||
|
const filteredTools = tools.filter(
|
||||||
|
(tool) =>
|
||||||
|
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
(tool.description && tool.description.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Count enabled tools
|
||||||
|
const enabledCount = tools.filter((tool) => enabledToolIds.has(tool.id)).length;
|
||||||
|
const totalCount = tools.length;
|
||||||
|
|
||||||
|
// Check if tool is enabled
|
||||||
|
const isToolEnabled = (toolId: string) => {
|
||||||
|
return enabledToolIds.has(toolId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filteredTools.length === 0 && searchQuery) {
|
||||||
|
return null; // Hide section if no tools match search
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<FontAwesomeIcon icon={faServer} className="text-sm text-bunker-400" />
|
||||||
|
<span className="text-sm text-mineshaft-200">{serverName}</span>
|
||||||
|
<div
|
||||||
|
className={`h-2 w-2 rounded-full ${
|
||||||
|
serverStatus === "active" ? "bg-emerald-500" : "bg-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="rounded-full bg-primary/20 px-2 py-0.5 text-xs font-medium text-primary">
|
||||||
|
{enabledCount}/{totalCount} Enabled
|
||||||
|
</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={isExpanded ? faChevronUp : faChevronDown}
|
||||||
|
className="text-xs text-bunker-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && filteredTools.length > 0 && (
|
||||||
|
<div className="border-t border-mineshaft-600">
|
||||||
|
<div className="grid grid-cols-[1fr_auto] gap-2 border-b border-mineshaft-600 px-4 py-2 text-xs font-medium tracking-wider text-bunker-300 uppercase">
|
||||||
|
<span>Tool Name</span>
|
||||||
|
<span>Enabled</span>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-mineshaft-600">
|
||||||
|
{filteredTools.map((tool) => (
|
||||||
|
<div key={tool.id} className="grid grid-cols-[1fr_auto] items-center gap-2 px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-sm text-mineshaft-200">{tool.name}</span>
|
||||||
|
{tool.description && (
|
||||||
|
<Tooltip content={tool.description}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faInfoCircle}
|
||||||
|
className="text-xs text-bunker-400 hover:text-bunker-300"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id={`tool-${tool.id}`}
|
||||||
|
isChecked={isToolEnabled(tool.id)}
|
||||||
|
onCheckedChange={(checked) => onToolToggle(tool.id, checked)}
|
||||||
|
isDisabled={isUpdating}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isExpanded && tools.length === 0 && (
|
||||||
|
<div className="border-t border-mineshaft-600 px-4 py-4 text-center text-sm text-bunker-400">
|
||||||
|
No tools available from this server
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MCPEndpointToolSelectionSection = ({ endpointId, projectId, serverIds }: Props) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const { data: serversData } = useListAiMcpServers({ projectId });
|
||||||
|
const { data: toolConfigs = [] } = useListEndpointTools({ endpointId });
|
||||||
|
const enableTool = useEnableEndpointTool();
|
||||||
|
const disableTool = useDisableEndpointTool();
|
||||||
|
|
||||||
|
const connectedServers =
|
||||||
|
serversData?.servers.filter((server) => serverIds.includes(server.id)) || [];
|
||||||
|
|
||||||
|
const handleToolToggle = async (serverToolId: string, isEnabled: boolean) => {
|
||||||
|
try {
|
||||||
|
if (isEnabled) {
|
||||||
|
await enableTool.mutateAsync({ endpointId, serverToolId });
|
||||||
|
} else {
|
||||||
|
await disableTool.mutateAsync({ endpointId, serverToolId });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update tool:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update tool configuration",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-mineshaft-100">Tool Selection</h3>
|
||||||
|
<p className="mt-1 text-sm text-bunker-300">
|
||||||
|
Control which tools from connected MCP servers are available through this endpoint
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faMagnifyingGlass}
|
||||||
|
className="absolute top-1/2 left-3 -translate-y-1/2 text-bunker-400"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search tools..."
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{connectedServers.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 px-4 py-8 text-center">
|
||||||
|
<FontAwesomeIcon icon={faServer} className="mb-2 text-2xl text-bunker-400" />
|
||||||
|
<p className="text-sm text-bunker-400">No MCP servers connected to this endpoint</p>
|
||||||
|
<p className="mt-1 text-xs text-bunker-400">
|
||||||
|
Connect servers to configure available tools
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
connectedServers.map((server) => (
|
||||||
|
<ServerToolsSection
|
||||||
|
key={server.id}
|
||||||
|
serverId={server.id}
|
||||||
|
serverName={server.name}
|
||||||
|
serverStatus={server.status}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
toolConfigs={toolConfigs}
|
||||||
|
onToolToggle={handleToolToggle}
|
||||||
|
isUpdating={enableTool.isPending || disableTool.isPending}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { MCPEndpointConnectedServersSection } from "./MCPEndpointConnectedServersSection";
|
||||||
|
export { MCPEndpointConnectionSection } from "./MCPEndpointConnectionSection";
|
||||||
|
export { MCPEndpointDetailsSection } from "./MCPEndpointDetailsSection";
|
||||||
|
export { MCPEndpointToolSelectionSection } from "./MCPEndpointToolSelectionSection";
|
||||||
1
frontend/src/pages/ai/MCPEndpointDetailPage/index.ts
Normal file
1
frontend/src/pages/ai/MCPEndpointDetailPage/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { MCPEndpointDetailPage } from "./MCPEndpointDetailPage";
|
||||||
26
frontend/src/pages/ai/MCPEndpointDetailPage/route.tsx
Normal file
26
frontend/src/pages/ai/MCPEndpointDetailPage/route.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { MCPEndpointDetailPage } from "./MCPEndpointDetailPage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-endpoints/$endpointId"
|
||||||
|
)({
|
||||||
|
component: MCPEndpointDetailPage,
|
||||||
|
beforeLoad: ({ context, params }) => {
|
||||||
|
return {
|
||||||
|
breadcrumbs: [
|
||||||
|
...context.breadcrumbs,
|
||||||
|
{
|
||||||
|
label: "MCP Endpoints",
|
||||||
|
link: linkOptions({
|
||||||
|
to: "/organizations/$orgId/projects/ai/$projectId/overview",
|
||||||
|
params: { orgId: params.orgId, projectId: params.projectId }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Endpoint Details"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/co
|
|||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
|
|
||||||
|
import { MCPEndpointsTab } from "./components/MCPEndpointsTab";
|
||||||
import { MCPServersTab } from "./components/MCPServersTab";
|
import { MCPServersTab } from "./components/MCPServersTab";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
@@ -48,7 +49,7 @@ export const MCPPage = () => {
|
|||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanel value={TabSections.MCPEndpoints}>
|
<TabPanel value={TabSections.MCPEndpoints}>
|
||||||
<div>MCP Endpoints - Coming soon</div>
|
<MCPEndpointsTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={TabSections.MCPServers}>
|
<TabPanel value={TabSections.MCPServers}>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const AddMCPEndpointFormSchema = z.object({
|
||||||
|
name: z.string().trim().min(1, "Name is required").max(64, "Name cannot exceed 64 characters"),
|
||||||
|
description: z.string().trim().max(256, "Description cannot exceed 256 characters").optional(),
|
||||||
|
serverIds: z.array(z.string()).default([])
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAddMCPEndpointForm = z.infer<typeof AddMCPEndpointFormSchema>;
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import { useCreateAiMcpEndpoint, useListAiMcpServers } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { AddMCPEndpointFormSchema, TAddMCPEndpointForm } from "./AddMCPEndpointForm.schema";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddMCPEndpointModal = ({ isOpen, onOpenChange }: Props) => {
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
const createEndpoint = useCreateAiMcpEndpoint();
|
||||||
|
|
||||||
|
// Fetch available MCP servers for selection
|
||||||
|
const { data: serversData, isLoading: isLoadingServers } = useListAiMcpServers({
|
||||||
|
projectId: currentProject?.id || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const servers = serversData?.servers || [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddMCPEndpointForm>({
|
||||||
|
resolver: zodResolver(AddMCPEndpointFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
serverIds: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedServerIds = watch("serverIds");
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
reset();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServerToggle = (serverId: string) => {
|
||||||
|
const current = selectedServerIds || [];
|
||||||
|
if (current.includes(serverId)) {
|
||||||
|
setValue(
|
||||||
|
"serverIds",
|
||||||
|
current.filter((id) => id !== serverId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setValue("serverIds", [...current, serverId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: TAddMCPEndpointForm) => {
|
||||||
|
if (!currentProject?.id) {
|
||||||
|
createNotification({
|
||||||
|
text: "No project selected",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createEndpoint.mutateAsync({
|
||||||
|
projectId: currentProject.id,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description || undefined,
|
||||||
|
serverIds: data.serverIds
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully created MCP endpoint "${data.name}"`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create MCP endpoint:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create MCP endpoint",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
title="Create MCP Endpoint"
|
||||||
|
subTitle="Configure a unified entrypoint interface with security rules and governance controls"
|
||||||
|
className="max-w-2xl"
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<p className="mb-4 text-sm text-bunker-300">
|
||||||
|
Create an endpoint to aggregate multiple MCP servers into a single entrypoint.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Endpoint Name"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="production-ai-proxy" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Description"
|
||||||
|
isOptional
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter endpoint description"
|
||||||
|
className="resize-none!"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<FormControl label="Connected Servers" isOptional>
|
||||||
|
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-900 p-3">
|
||||||
|
{isLoadingServers && <p className="text-sm text-bunker-400">Loading servers...</p>}
|
||||||
|
{!isLoadingServers && servers.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center py-4 text-center">
|
||||||
|
<FontAwesomeIcon icon={faServer} className="mb-2 text-2xl text-bunker-400" />
|
||||||
|
<p className="text-sm text-bunker-400">No MCP servers available</p>
|
||||||
|
<p className="text-xs text-bunker-500">
|
||||||
|
Add MCP servers first to connect them to this endpoint
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoadingServers && servers.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{servers.map((server) => (
|
||||||
|
<div
|
||||||
|
key={server.id}
|
||||||
|
className="flex items-start gap-3 rounded-md p-2 transition-colors hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`server-${server.id}`}
|
||||||
|
className="mt-0.5"
|
||||||
|
isChecked={selectedServerIds?.includes(server.id)}
|
||||||
|
onCheckedChange={() => handleServerToggle(server.id)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`server-${server.id}`}
|
||||||
|
className="flex flex-1 cursor-pointer items-start gap-2"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faServer}
|
||||||
|
className="mt-0.5 text-sm text-bunker-400"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-mineshaft-200">{server.name}</p>
|
||||||
|
{server.description && (
|
||||||
|
<p className="text-xs text-bunker-400">{server.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<span className="mt-0.5 text-xs text-bunker-400">
|
||||||
|
{server.toolsCount ?? 0} tools
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedServerIds && selectedServerIds.length > 0 && (
|
||||||
|
<p className="mt-2 text-xs text-bunker-400">
|
||||||
|
{selectedServerIds.length} server{selectedServerIds.length !== 1 ? "s" : ""}{" "}
|
||||||
|
selected
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-4">
|
||||||
|
<Button onClick={handleClose} colorSchema="secondary" type="button">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
isLoading={isSubmitting || createEndpoint.isPending}
|
||||||
|
>
|
||||||
|
Create Endpoint
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./AddMCPEndpointModal";
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
TAiMcpEndpoint,
|
||||||
|
useGetAiMcpEndpointById,
|
||||||
|
useListAiMcpServers,
|
||||||
|
useUpdateAiMcpEndpoint
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
const EditMCPEndpointFormSchema = z.object({
|
||||||
|
name: z.string().trim().min(1, "Name is required").max(64, "Name must be 64 characters or less"),
|
||||||
|
description: z.string().trim().max(256, "Description must be 256 characters or less").optional(),
|
||||||
|
serverIds: z.array(z.string()).default([])
|
||||||
|
});
|
||||||
|
|
||||||
|
type TEditMCPEndpointForm = z.infer<typeof EditMCPEndpointFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
endpoint: TAiMcpEndpoint | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditMCPEndpointModal = ({ isOpen, onOpenChange, endpoint }: Props) => {
|
||||||
|
const updateEndpoint = useUpdateAiMcpEndpoint();
|
||||||
|
|
||||||
|
// Fetch full endpoint details including serverIds
|
||||||
|
const { data: endpointDetails } = useGetAiMcpEndpointById({
|
||||||
|
endpointId: endpoint?.id || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch available MCP servers for selection
|
||||||
|
const { data: serversData, isLoading: isLoadingServers } = useListAiMcpServers({
|
||||||
|
projectId: endpoint?.projectId || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const servers = serversData?.servers || [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TEditMCPEndpointForm>({
|
||||||
|
resolver: zodResolver(EditMCPEndpointFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
serverIds: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedServerIds = watch("serverIds");
|
||||||
|
|
||||||
|
// Reset form when endpoint details change
|
||||||
|
useEffect(() => {
|
||||||
|
if (endpointDetails) {
|
||||||
|
reset({
|
||||||
|
name: endpointDetails.name,
|
||||||
|
description: endpointDetails.description || "",
|
||||||
|
serverIds: endpointDetails.serverIds || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [endpointDetails, reset]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
reset();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServerToggle = (serverId: string) => {
|
||||||
|
const current = selectedServerIds || [];
|
||||||
|
if (current.includes(serverId)) {
|
||||||
|
setValue(
|
||||||
|
"serverIds",
|
||||||
|
current.filter((id) => id !== serverId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setValue("serverIds", [...current, serverId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: TEditMCPEndpointForm) => {
|
||||||
|
if (!endpoint) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateEndpoint.mutateAsync({
|
||||||
|
endpointId: endpoint.id,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description || undefined,
|
||||||
|
serverIds: data.serverIds
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully updated MCP endpoint "${data.name}"`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update MCP endpoint:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update MCP endpoint",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
title="Edit MCP Endpoint"
|
||||||
|
subTitle="Update the endpoint details and connected servers"
|
||||||
|
className="max-w-2xl"
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Endpoint Name"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="production-ai-proxy" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
placeholder="Optional description for this endpoint"
|
||||||
|
className="resize-none!"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<FormControl label="Connected Servers" isOptional>
|
||||||
|
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-900 p-3">
|
||||||
|
{isLoadingServers && <p className="text-sm text-bunker-400">Loading servers...</p>}
|
||||||
|
{!isLoadingServers && servers.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center py-4 text-center">
|
||||||
|
<FontAwesomeIcon icon={faServer} className="mb-2 text-2xl text-bunker-400" />
|
||||||
|
<p className="text-sm text-bunker-400">No MCP servers available</p>
|
||||||
|
<p className="text-xs text-bunker-500">
|
||||||
|
Add MCP servers first to connect them to this endpoint
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoadingServers && servers.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{servers.map((server) => (
|
||||||
|
<div
|
||||||
|
key={server.id}
|
||||||
|
className="flex items-start gap-3 rounded-md p-2 transition-colors hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`edit-server-${server.id}`}
|
||||||
|
className="mt-0.5"
|
||||||
|
isChecked={selectedServerIds?.includes(server.id)}
|
||||||
|
onCheckedChange={() => handleServerToggle(server.id)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`edit-server-${server.id}`}
|
||||||
|
className="flex flex-1 cursor-pointer items-start gap-2"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faServer}
|
||||||
|
className="mt-0.5 text-sm text-bunker-400"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-mineshaft-200">{server.name}</p>
|
||||||
|
{server.description && (
|
||||||
|
<p className="text-xs text-bunker-400">{server.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<span className="mt-0.5 text-xs text-bunker-400">
|
||||||
|
{server.toolsCount ?? 0} tools
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedServerIds && selectedServerIds.length > 0 && (
|
||||||
|
<p className="mt-2 text-xs text-bunker-400">
|
||||||
|
{selectedServerIds.length} server{selectedServerIds.length !== 1 ? "s" : ""}{" "}
|
||||||
|
selected
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-4">
|
||||||
|
<Button onClick={handleClose} colorSchema="secondary" type="button">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
isLoading={isSubmitting || updateEndpoint.isPending}
|
||||||
|
isDisabled={isSubmitting || updateEndpoint.isPending}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import { TAiMcpEndpoint, useListAiMcpEndpoints } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { MCPEndpointRow } from "./MCPEndpointRow";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onEditEndpoint: (endpoint: TAiMcpEndpoint) => void;
|
||||||
|
onDeleteEndpoint: (endpoint: TAiMcpEndpoint) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MCPEndpointList = ({ onEditEndpoint, onDeleteEndpoint }: Props) => {
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
|
const { data, isLoading } = useListAiMcpEndpoints({
|
||||||
|
projectId: currentProject?.id || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpoints = data?.endpoints;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th>Connected Servers</Th>
|
||||||
|
<Th>Active Tools</Th>
|
||||||
|
<Th className="w-16" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={5} innerKey="mcp-endpoints" />}
|
||||||
|
{!isLoading && (!endpoints || endpoints.length === 0) && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState title="No MCP Endpoints" />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{!isLoading &&
|
||||||
|
endpoints &&
|
||||||
|
endpoints.length > 0 &&
|
||||||
|
endpoints.map((endpoint) => (
|
||||||
|
<MCPEndpointRow
|
||||||
|
key={endpoint.id}
|
||||||
|
endpoint={endpoint}
|
||||||
|
onEditEndpoint={onEditEndpoint}
|
||||||
|
onDeleteEndpoint={onDeleteEndpoint}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { faCheck, faCopy, faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Td,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TAiMcpEndpoint } from "@app/hooks/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
endpoint: TAiMcpEndpoint;
|
||||||
|
onEditEndpoint: (endpoint: TAiMcpEndpoint) => void;
|
||||||
|
onDeleteEndpoint: (endpoint: TAiMcpEndpoint) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string | null) => {
|
||||||
|
const statusConfig: Record<string, { color: string; label: string }> = {
|
||||||
|
active: {
|
||||||
|
color: "bg-emerald-500",
|
||||||
|
label: "Active"
|
||||||
|
},
|
||||||
|
inactive: {
|
||||||
|
color: "bg-red-500",
|
||||||
|
label: "Inactive"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status || "inactive"] || statusConfig.inactive;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`h-2 w-2 rounded-full ${config.color}`} />
|
||||||
|
<span className="text-sm text-mineshaft-300">{config.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MCPEndpointRow = ({ endpoint, onEditEndpoint, onDeleteEndpoint }: Props) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { orgId, projectId } = useParams({
|
||||||
|
strict: false
|
||||||
|
}) as { orgId?: string; projectId?: string };
|
||||||
|
|
||||||
|
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const handleCopyId = useCallback(() => {
|
||||||
|
setIsIdCopied.on();
|
||||||
|
navigator.clipboard.writeText(endpoint.id);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Endpoint ID copied to clipboard",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => setIsIdCopied.off(), 2000);
|
||||||
|
}, [setIsIdCopied, endpoint.id]);
|
||||||
|
|
||||||
|
const handleRowClick = () => {
|
||||||
|
if (orgId && projectId) {
|
||||||
|
navigate({
|
||||||
|
to: "/organizations/$orgId/projects/ai/$projectId/mcp-endpoints/$endpointId",
|
||||||
|
params: { orgId, projectId, endpointId: endpoint.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
onClick={handleRowClick}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<span className="text-mineshaft-300">{endpoint.name}</span>
|
||||||
|
</Td>
|
||||||
|
<Td>{getStatusBadge(endpoint.status)}</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="text-sm text-mineshaft-300">{endpoint.connectedServers}</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="text-sm text-mineshaft-300">{endpoint.activeTools}</span>
|
||||||
|
</Td>
|
||||||
|
<Td className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer justify-end hover:text-primary-400 data-[state=open]:text-primary-400"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsisV} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="p-1">
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} className="w-3" />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCopyId();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Endpoint ID
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEditEndpoint(endpoint);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
|
||||||
|
>
|
||||||
|
Edit Endpoint
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteEndpoint(endpoint);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
|
||||||
|
className="text-red-500 hover:text-red-400"
|
||||||
|
>
|
||||||
|
Delete Endpoint
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { TAiMcpEndpoint, useDeleteAiMcpEndpoint } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { AddMCPEndpointModal } from "./AddMCPEndpointModal";
|
||||||
|
import { EditMCPEndpointModal } from "./EditMCPEndpointModal";
|
||||||
|
import { MCPEndpointList } from "./MCPEndpointList";
|
||||||
|
|
||||||
|
export const MCPEndpointsTab = () => {
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [selectedEndpoint, setSelectedEndpoint] = useState<TAiMcpEndpoint | null>(null);
|
||||||
|
|
||||||
|
const deleteEndpoint = useDeleteAiMcpEndpoint();
|
||||||
|
|
||||||
|
const handleCreateEndpoint = () => {
|
||||||
|
setIsCreateModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditEndpoint = (endpoint: TAiMcpEndpoint) => {
|
||||||
|
setSelectedEndpoint(endpoint);
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEndpoint = (endpoint: TAiMcpEndpoint) => {
|
||||||
|
setSelectedEndpoint(endpoint);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!selectedEndpoint) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteEndpoint.mutateAsync({ endpointId: selectedEndpoint.id });
|
||||||
|
createNotification({
|
||||||
|
text: `MCP endpoint "${selectedEndpoint.name}" deleted successfully`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete MCP endpoint:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete MCP endpoint",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setSelectedEndpoint(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-mineshaft-100">MCP Endpoints</h2>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Unified entrypoint interfaces with security rules and governance controls
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="button"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={handleCreateEndpoint}
|
||||||
|
>
|
||||||
|
Create Endpoint
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MCPEndpointList
|
||||||
|
onEditEndpoint={handleEditEndpoint}
|
||||||
|
onDeleteEndpoint={handleDeleteEndpoint}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddMCPEndpointModal isOpen={isCreateModalOpen} onOpenChange={setIsCreateModalOpen} />
|
||||||
|
|
||||||
|
<EditMCPEndpointModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
setIsEditModalOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
setSelectedEndpoint(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
endpoint={selectedEndpoint}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedEndpoint && (
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
title={`Delete MCP Endpoint ${selectedEndpoint.name}?`}
|
||||||
|
onChange={(isOpen) => {
|
||||||
|
setIsDeleteModalOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
setSelectedEndpoint(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
deleteKey={selectedEndpoint.name}
|
||||||
|
onDeleteApproved={handleDeleteConfirm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./MCPEndpointsTab";
|
||||||
@@ -28,30 +28,6 @@ export const MCPServerList = ({ onEditServer, onDeleteServer }: Props) => {
|
|||||||
|
|
||||||
const servers = data?.servers;
|
const servers = data?.servers;
|
||||||
|
|
||||||
if (!currentProject?.id) {
|
|
||||||
return (
|
|
||||||
<TableContainer>
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>Status</Th>
|
|
||||||
<Th>Available Tools</Th>
|
|
||||||
<Th className="w-16" />
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={4}>
|
|
||||||
<EmptyState title="No Project Selected" />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -59,7 +35,6 @@ export const MCPServerList = ({ onEditServer, onDeleteServer }: Props) => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Available Tools</Th>
|
|
||||||
<Th className="w-16" />
|
<Th className="w-16" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
|
|||||||
@@ -81,9 +81,6 @@ export const MCPServerRow = ({ server, onEditServer, onDeleteServer }: Props) =>
|
|||||||
<span className="text-mineshaft-300">{server.name}</span>
|
<span className="text-mineshaft-300">{server.name}</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{getStatusBadge(server.status)}</Td>
|
<Td>{getStatusBadge(server.status)}</Td>
|
||||||
<Td>
|
|
||||||
<span className="text-sm text-mineshaft-300">{server.toolsCount ?? 0}</span>
|
|
||||||
</Td>
|
|
||||||
<Td className="text-right">
|
<Td className="text-right">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './p
|
|||||||
import { Route as certManagerPkiSyncDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSyncDetailsByIDPage/route'
|
import { Route as certManagerPkiSyncDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSyncDetailsByIDPage/route'
|
||||||
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
|
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
|
||||||
import { Route as aiMCPServerDetailPageRouteImport } from './pages/ai/MCPServerDetailPage/route'
|
import { Route as aiMCPServerDetailPageRouteImport } from './pages/ai/MCPServerDetailPage/route'
|
||||||
|
import { Route as aiMCPEndpointDetailPageRouteImport } from './pages/ai/MCPEndpointDetailPage/route'
|
||||||
import { Route as secretScanningSecretScanningDataSourcesPageRouteImport } from './pages/secret-scanning/SecretScanningDataSourcesPage/route'
|
import { Route as secretScanningSecretScanningDataSourcesPageRouteImport } from './pages/secret-scanning/SecretScanningDataSourcesPage/route'
|
||||||
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route'
|
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route'
|
||||||
import { Route as pamPamSessionsPageRouteImport } from './pages/pam/PamSessionsPage/route'
|
import { Route as pamPamSessionsPageRouteImport } from './pages/pam/PamSessionsPage/route'
|
||||||
@@ -1611,6 +1612,13 @@ const aiMCPServerDetailPageRouteRoute = aiMCPServerDetailPageRouteImport.update(
|
|||||||
} as any,
|
} as any,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const aiMCPEndpointDetailPageRouteRoute =
|
||||||
|
aiMCPEndpointDetailPageRouteImport.update({
|
||||||
|
id: '/mcp-endpoints/$endpointId',
|
||||||
|
path: '/mcp-endpoints/$endpointId',
|
||||||
|
getParentRoute: () => aiLayoutRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const secretScanningSecretScanningDataSourcesPageRouteRoute =
|
const secretScanningSecretScanningDataSourcesPageRouteRoute =
|
||||||
secretScanningSecretScanningDataSourcesPageRouteImport.update({
|
secretScanningSecretScanningDataSourcesPageRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
@@ -3315,6 +3323,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof secretScanningSecretScanningDataSourcesPageRouteImport
|
preLoaderRoute: typeof secretScanningSecretScanningDataSourcesPageRouteImport
|
||||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationsOrgIdProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesImport
|
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationsOrgIdProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesImport
|
||||||
}
|
}
|
||||||
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-endpoints/$endpointId': {
|
||||||
|
id: '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-endpoints/$endpointId'
|
||||||
|
path: '/mcp-endpoints/$endpointId'
|
||||||
|
fullPath: '/organizations/$orgId/projects/ai/$projectId/mcp-endpoints/$endpointId'
|
||||||
|
preLoaderRoute: typeof aiMCPEndpointDetailPageRouteImport
|
||||||
|
parentRoute: typeof aiLayoutImport
|
||||||
|
}
|
||||||
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId': {
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId': {
|
||||||
id: '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId'
|
id: '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId'
|
||||||
path: '/mcp-servers/$serverId'
|
path: '/mcp-servers/$serverId'
|
||||||
@@ -4204,6 +4219,7 @@ interface aiLayoutRouteChildren {
|
|||||||
aiSettingsPageRouteRoute: typeof aiSettingsPageRouteRoute
|
aiSettingsPageRouteRoute: typeof aiSettingsPageRouteRoute
|
||||||
projectAccessControlPageRouteAiRoute: typeof projectAccessControlPageRouteAiRoute
|
projectAccessControlPageRouteAiRoute: typeof projectAccessControlPageRouteAiRoute
|
||||||
projectAuditLogsPageRouteAiRoute: typeof projectAuditLogsPageRouteAiRoute
|
projectAuditLogsPageRouteAiRoute: typeof projectAuditLogsPageRouteAiRoute
|
||||||
|
aiMCPEndpointDetailPageRouteRoute: typeof aiMCPEndpointDetailPageRouteRoute
|
||||||
aiMCPServerDetailPageRouteRoute: typeof aiMCPServerDetailPageRouteRoute
|
aiMCPServerDetailPageRouteRoute: typeof aiMCPServerDetailPageRouteRoute
|
||||||
projectGroupDetailsByIDPageRouteAiRoute: typeof projectGroupDetailsByIDPageRouteAiRoute
|
projectGroupDetailsByIDPageRouteAiRoute: typeof projectGroupDetailsByIDPageRouteAiRoute
|
||||||
projectIdentityDetailsByIDPageRouteAiRoute: typeof projectIdentityDetailsByIDPageRouteAiRoute
|
projectIdentityDetailsByIDPageRouteAiRoute: typeof projectIdentityDetailsByIDPageRouteAiRoute
|
||||||
@@ -4216,6 +4232,7 @@ const aiLayoutRouteChildren: aiLayoutRouteChildren = {
|
|||||||
aiSettingsPageRouteRoute: aiSettingsPageRouteRoute,
|
aiSettingsPageRouteRoute: aiSettingsPageRouteRoute,
|
||||||
projectAccessControlPageRouteAiRoute: projectAccessControlPageRouteAiRoute,
|
projectAccessControlPageRouteAiRoute: projectAccessControlPageRouteAiRoute,
|
||||||
projectAuditLogsPageRouteAiRoute: projectAuditLogsPageRouteAiRoute,
|
projectAuditLogsPageRouteAiRoute: projectAuditLogsPageRouteAiRoute,
|
||||||
|
aiMCPEndpointDetailPageRouteRoute: aiMCPEndpointDetailPageRouteRoute,
|
||||||
aiMCPServerDetailPageRouteRoute: aiMCPServerDetailPageRouteRoute,
|
aiMCPServerDetailPageRouteRoute: aiMCPServerDetailPageRouteRoute,
|
||||||
projectGroupDetailsByIDPageRouteAiRoute:
|
projectGroupDetailsByIDPageRouteAiRoute:
|
||||||
projectGroupDetailsByIDPageRouteAiRoute,
|
projectGroupDetailsByIDPageRouteAiRoute,
|
||||||
@@ -5421,6 +5438,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/organizations/$orgId/projects/pam/$projectId/sessions/': typeof pamPamSessionsPageRouteRoute
|
'/organizations/$orgId/projects/pam/$projectId/sessions/': typeof pamPamSessionsPageRouteRoute
|
||||||
'/organizations/$orgId/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
'/organizations/$orgId/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||||
'/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
'/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
||||||
|
'/organizations/$orgId/projects/ai/$projectId/mcp-endpoints/$endpointId': typeof aiMCPEndpointDetailPageRouteRoute
|
||||||
'/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId': typeof aiMCPServerDetailPageRouteRoute
|
'/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId': typeof aiMCPServerDetailPageRouteRoute
|
||||||
'/organizations/$orgId/projects/cert-management/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
'/organizations/$orgId/projects/cert-management/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||||
'/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
'/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
||||||
@@ -5661,6 +5679,7 @@ export interface FileRoutesByTo {
|
|||||||
'/organizations/$orgId/projects/pam/$projectId/sessions': typeof pamPamSessionsPageRouteRoute
|
'/organizations/$orgId/projects/pam/$projectId/sessions': typeof pamPamSessionsPageRouteRoute
|
||||||
'/organizations/$orgId/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
|
'/organizations/$orgId/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
|
||||||
'/organizations/$orgId/projects/secret-scanning/$projectId/data-sources': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
'/organizations/$orgId/projects/secret-scanning/$projectId/data-sources': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
||||||
|
'/organizations/$orgId/projects/ai/$projectId/mcp-endpoints/$endpointId': typeof aiMCPEndpointDetailPageRouteRoute
|
||||||
'/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId': typeof aiMCPServerDetailPageRouteRoute
|
'/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId': typeof aiMCPServerDetailPageRouteRoute
|
||||||
'/organizations/$orgId/projects/cert-management/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
'/organizations/$orgId/projects/cert-management/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||||
'/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
'/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
||||||
@@ -5925,6 +5944,7 @@ export interface FileRoutesById {
|
|||||||
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/': typeof pamPamSessionsPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/': typeof pamPamSessionsPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
||||||
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-endpoints/$endpointId': typeof aiMCPEndpointDetailPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId': typeof aiMCPServerDetailPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId': typeof aiMCPServerDetailPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
||||||
@@ -6180,6 +6200,7 @@ export interface FileRouteTypes {
|
|||||||
| '/organizations/$orgId/projects/pam/$projectId/sessions/'
|
| '/organizations/$orgId/projects/pam/$projectId/sessions/'
|
||||||
| '/organizations/$orgId/projects/secret-management/$projectId/integrations/'
|
| '/organizations/$orgId/projects/secret-management/$projectId/integrations/'
|
||||||
| '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/'
|
| '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/'
|
||||||
|
| '/organizations/$orgId/projects/ai/$projectId/mcp-endpoints/$endpointId'
|
||||||
| '/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId'
|
| '/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId'
|
||||||
| '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId'
|
| '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId'
|
||||||
| '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId'
|
| '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId'
|
||||||
@@ -6419,6 +6440,7 @@ export interface FileRouteTypes {
|
|||||||
| '/organizations/$orgId/projects/pam/$projectId/sessions'
|
| '/organizations/$orgId/projects/pam/$projectId/sessions'
|
||||||
| '/organizations/$orgId/projects/secret-management/$projectId/integrations'
|
| '/organizations/$orgId/projects/secret-management/$projectId/integrations'
|
||||||
| '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources'
|
| '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources'
|
||||||
|
| '/organizations/$orgId/projects/ai/$projectId/mcp-endpoints/$endpointId'
|
||||||
| '/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId'
|
| '/organizations/$orgId/projects/ai/$projectId/mcp-servers/$serverId'
|
||||||
| '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId'
|
| '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId'
|
||||||
| '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId'
|
| '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId'
|
||||||
@@ -6681,6 +6703,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/'
|
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/'
|
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/'
|
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/'
|
||||||
|
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-endpoints/$endpointId'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId'
|
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId'
|
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId'
|
| '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId'
|
||||||
@@ -7311,6 +7334,7 @@ export const routeTree = rootRoute
|
|||||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/settings",
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/settings",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/access-management",
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/access-management",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/audit-logs",
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/audit-logs",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-endpoints/$endpointId",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId",
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/groups/$groupId",
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/groups/$groupId",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/identities/$identityId",
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/identities/$identityId",
|
||||||
@@ -7733,6 +7757,10 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "secret-scanning/SecretScanningDataSourcesPage/route.tsx",
|
"filePath": "secret-scanning/SecretScanningDataSourcesPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources"
|
||||||
},
|
},
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-endpoints/$endpointId": {
|
||||||
|
"filePath": "ai/MCPEndpointDetailPage/route.tsx",
|
||||||
|
"parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout"
|
||||||
|
},
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId": {
|
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout/mcp-servers/$serverId": {
|
||||||
"filePath": "ai/MCPServerDetailPage/route.tsx",
|
"filePath": "ai/MCPServerDetailPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ai/$projectId/_ai-layout"
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ const certManagerRoutes = route("/organizations/$orgId/projects/cert-management/
|
|||||||
const aiRoutes = route("/organizations/$orgId/projects/ai/$projectId", [
|
const aiRoutes = route("/organizations/$orgId/projects/ai/$projectId", [
|
||||||
layout("ai-layout", "ai/layout.tsx", [
|
layout("ai-layout", "ai/layout.tsx", [
|
||||||
route("/mcp-servers/$serverId", "ai/MCPServerDetailPage/route.tsx"),
|
route("/mcp-servers/$serverId", "ai/MCPServerDetailPage/route.tsx"),
|
||||||
|
route("/mcp-endpoints/$endpointId", "ai/MCPEndpointDetailPage/route.tsx"),
|
||||||
route("/overview", "ai/MCPPage/route.tsx"),
|
route("/overview", "ai/MCPPage/route.tsx"),
|
||||||
route("/settings", "ai/SettingsPage/route.tsx"),
|
route("/settings", "ai/SettingsPage/route.tsx"),
|
||||||
route("/audit-logs", "project/AuditLogsPage/route-ai.tsx"),
|
route("/audit-logs", "project/AuditLogsPage/route-ai.tsx"),
|
||||||
|
|||||||
Reference in New Issue
Block a user