mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-08 23:18:05 -05:00
misc: addressed comments
This commit is contained in:
@@ -9,7 +9,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.string("url").notNullable();
|
||||
t.string("description");
|
||||
t.text("description");
|
||||
t.string("status");
|
||||
t.string("credentialMode");
|
||||
t.string("authMethod");
|
||||
@@ -38,7 +38,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable(TableName.AiMcpEndpoint, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.string("description");
|
||||
t.text("description");
|
||||
t.string("status");
|
||||
t.boolean("piiFiltering").defaultTo(false).notNullable();
|
||||
t.string("projectId").notNullable();
|
||||
@@ -104,7 +104,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// await dropOnUpdateTrigger(knex, TableName.AiMcpServerUserCredential);
|
||||
await dropOnUpdateTrigger(knex, TableName.AiMcpServerUserCredential);
|
||||
await knex.schema.dropTableIfExists(TableName.AiMcpServerUserCredential);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.AiMcpEndpointServerTool);
|
||||
|
||||
@@ -6,12 +6,12 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
||||
|
||||
const getMcpUrls = (siteUrl: string, endpointId: string) => {
|
||||
// The MCP resource/connect URL
|
||||
const resourceUrl = `${siteUrl}/api/v1/ai/mcp-endpoints/${endpointId}/connect`;
|
||||
const resourceUrl = `${siteUrl}/api/v1/ai/mcp/endpoints/${endpointId}/connect`;
|
||||
// The authorization server issuer (RFC 8414: metadata at /.well-known/oauth-authorization-server/{path})
|
||||
const authServerIssuer = `${siteUrl}/mcp-endpoints/${endpointId}`;
|
||||
|
||||
// OAuth endpoint URLs
|
||||
const apiBaseUrl = `${siteUrl}/api/v1/ai/mcp-endpoints/${endpointId}`;
|
||||
const apiBaseUrl = `${siteUrl}/api/v1/ai/mcp/endpoints/${endpointId}`;
|
||||
const tokenEndpointUrl = `${apiBaseUrl}/oauth/token`;
|
||||
const authorizeEndpointUrl = `${apiBaseUrl}/oauth/authorize`;
|
||||
const registrationEndpointUrl = `${apiBaseUrl}/oauth/register`;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { z } from "zod";
|
||||
|
||||
import { AiMcpServerToolsSchema } from "@app/db/schemas/ai-mcp-server-tools";
|
||||
import { AiMcpServersSchema } from "@app/db/schemas/ai-mcp-servers";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AiMcpServerAuthMethod, AiMcpServerCredentialMode } from "@app/ee/services/ai-mcp-server/ai-mcp-server-enum";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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";
|
||||
|
||||
@@ -229,9 +229,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.register(
|
||||
async (aiRouter) => {
|
||||
await aiRouter.register(registerAiMcpServerRouter, { prefix: "/mcp-servers" });
|
||||
await aiRouter.register(registerAiMcpEndpointRouter, { prefix: "/mcp-endpoints" });
|
||||
await aiRouter.register(registerAiMcpActivityLogRouter, { prefix: "/mcp-activity-logs" });
|
||||
await aiRouter.register(registerAiMcpServerRouter, { prefix: "/mcp/servers" });
|
||||
await aiRouter.register(registerAiMcpEndpointRouter, { prefix: "/mcp/endpoints" });
|
||||
await aiRouter.register(registerAiMcpActivityLogRouter, { prefix: "/mcp/activity-logs" });
|
||||
},
|
||||
{ prefix: "/ai" }
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { AiMcpServerCredentialMode } from "../ai-mcp-server/ai-mcp-server-enum";
|
||||
import { TAiMcpServerServiceFactory } from "../ai-mcp-server/ai-mcp-server-service";
|
||||
import { TAiMcpServerToolDALFactory } from "../ai-mcp-server/ai-mcp-server-tool-dal";
|
||||
import { TAiMcpServerUserCredentialDALFactory } from "../ai-mcp-server/ai-mcp-server-user-credential-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { ProjectPermissionMcpEndpointActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TAiMcpEndpointDALFactory } from "./ai-mcp-endpoint-dal";
|
||||
@@ -72,6 +73,7 @@ type TAiMcpEndpointServiceFactoryDep = {
|
||||
authTokenService: Pick<TAuthTokenServiceFactory, "getUserTokenSessionById">;
|
||||
userDAL: TUserDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
// OAuth schemas for parsing cached data
|
||||
@@ -129,57 +131,9 @@ export const aiMcpEndpointServiceFactory = ({
|
||||
keyStore,
|
||||
authTokenService,
|
||||
userDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TAiMcpEndpointServiceFactoryDep) => {
|
||||
// PII filtering utility - redacts sensitive information
|
||||
const applyPiiFiltering = (data: unknown): unknown => {
|
||||
if (typeof data === "string") {
|
||||
let filtered = data;
|
||||
|
||||
// Redact SSN (matches formats: 123-45-6789, 123456789)
|
||||
filtered = filtered.replace(/\b\d{3}-?\d{2}-?\d{4}\b/g, "<REDACTED>");
|
||||
|
||||
// Redact Phone Numbers (matches various formats)
|
||||
// Matches: (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 123 456 7890
|
||||
filtered = filtered.replace(/(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, "<REDACTED>");
|
||||
|
||||
// Redact Email Addresses
|
||||
filtered = filtered.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "<REDACTED>");
|
||||
|
||||
// Redact Passport Numbers (matches common formats)
|
||||
// Matches: US (9 digits), international alphanumeric (e.g., AB1234567, P12345678)
|
||||
filtered = filtered.replace(/\b[A-Z]{1,2}\d{6,9}\b/g, "<REDACTED>");
|
||||
filtered = filtered.replace(/\bP\d{8}\b/g, "<REDACTED>");
|
||||
|
||||
// Redact Driver's License Numbers (matches common US state formats)
|
||||
// Matches various state formats: alphanumeric combinations typically 6-20 characters
|
||||
// Examples: CA: A1234567, TX: 12345678, NY: 123456789, FL: A123-456-78-901-0
|
||||
filtered = filtered.replace(/\b[A-Z]{1,2}[-\s]?\d{6,8}\b/g, "<REDACTED>");
|
||||
filtered = filtered.replace(/\b\d{7,9}\b/g, "<REDACTED>");
|
||||
filtered = filtered.replace(/\b[A-Z]\d{3}-\d{3}-\d{2}-\d{3}-\d\b/g, "<REDACTED>");
|
||||
|
||||
// Redact State ID Numbers (similar patterns to driver's licenses)
|
||||
// Matches alphanumeric state ID formats
|
||||
filtered = filtered.replace(/\b[A-Z]{1,3}\d{5,12}\b/g, "<REDACTED>");
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) => applyPiiFiltering(item));
|
||||
}
|
||||
|
||||
if (data && typeof data === "object") {
|
||||
const filtered: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
filtered[key] = applyPiiFiltering(value);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const interactWithMcp = async ({
|
||||
endpointId,
|
||||
userId,
|
||||
@@ -349,34 +303,58 @@ export const aiMcpEndpointServiceFactory = ({
|
||||
}
|
||||
|
||||
try {
|
||||
// Apply PII filtering to arguments if enabled
|
||||
const filteredArgs = (endpoint.piiFiltering ? applyPiiFiltering(args) : args) as Record<string, unknown>;
|
||||
|
||||
const result = await selectedMcpClient.client.callTool({
|
||||
name,
|
||||
arguments: filteredArgs
|
||||
arguments: args
|
||||
});
|
||||
|
||||
// Apply PII filtering to result if enabled
|
||||
const filteredResult = endpoint.piiFiltering ? applyPiiFiltering(result) : result;
|
||||
|
||||
await aiMcpActivityLogService.createActivityLog({
|
||||
endpointName: endpoint.name,
|
||||
serverName: selectedMcpClient.server.name,
|
||||
toolName: name,
|
||||
actor: user.email || "",
|
||||
request: filteredArgs, // Log filtered args
|
||||
response: filteredResult, // Log filtered response
|
||||
request: args,
|
||||
response: result,
|
||||
projectId: endpoint.projectId
|
||||
});
|
||||
|
||||
return filteredResult as Record<string, unknown>;
|
||||
return result as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
// Log the full error internally for system administrators
|
||||
logger.error(
|
||||
{
|
||||
error,
|
||||
endpointName: endpoint.name,
|
||||
serverName: selectedMcpClient.server.name,
|
||||
toolName: name,
|
||||
actor: user.email || "",
|
||||
projectId: endpoint.projectId
|
||||
},
|
||||
"Tool call failed"
|
||||
);
|
||||
|
||||
// Log failed activity with full error details for user visibility in activity logs
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
await aiMcpActivityLogService
|
||||
.createActivityLog({
|
||||
endpointName: endpoint.name,
|
||||
serverName: selectedMcpClient.server.name,
|
||||
toolName: name,
|
||||
actor: user.email || "",
|
||||
request: args,
|
||||
response: { error: errorMessage },
|
||||
projectId: endpoint.projectId
|
||||
})
|
||||
.catch((logError) => {
|
||||
logger.error({ error: logError }, "Failed to log tool call error activity");
|
||||
});
|
||||
|
||||
// Return generic error to client to avoid information leakage
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
text: "Tool execution failed"
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
@@ -402,6 +380,13 @@ export const aiMcpEndpointServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateAiMcpEndpointDTO) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actorOrgId);
|
||||
if (!orgLicensePlan.ai) {
|
||||
throw new BadRequestError({
|
||||
message: "AI operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
||||
import axios, { AxiosError } from "axios";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
@@ -19,6 +20,7 @@ import { ActorType, AuthMethod } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TAiMcpServerDALFactory } from "./ai-mcp-server-dal";
|
||||
@@ -51,6 +53,7 @@ type TAiMcpServerServiceFactoryDep = {
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TAiMcpServerServiceFactory = ReturnType<typeof aiMcpServerServiceFactory>;
|
||||
@@ -137,7 +140,8 @@ export const aiMcpServerServiceFactory = ({
|
||||
aiMcpServerUserCredentialDAL,
|
||||
kmsService,
|
||||
keyStore,
|
||||
permissionService
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TAiMcpServerServiceFactoryDep) => {
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-redundant-type-constituents */
|
||||
const fetchMcpTools = async (serverUrl: string, accessToken: string): Promise<TMcpTool[]> => {
|
||||
@@ -200,6 +204,9 @@ export const aiMcpServerServiceFactory = ({
|
||||
}> => {
|
||||
let resourceMetadataUrl: string | null = null;
|
||||
|
||||
const url = new URL(mcpUrl);
|
||||
await verifyHostInputValidity(url.hostname, true);
|
||||
|
||||
// 1. Try to access the MCP server to get WWW-Authenticate header
|
||||
try {
|
||||
await request.get(mcpUrl);
|
||||
@@ -306,6 +313,9 @@ export const aiMcpServerServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.McpServers);
|
||||
}
|
||||
|
||||
const urlObj = new URL(url);
|
||||
await verifyHostInputValidity(urlObj.hostname, true);
|
||||
|
||||
// 1. Discover OAuth metadata following RFC 9728 flow
|
||||
const { protectedResource, authServer } = await discoverOAuthMetadata(url);
|
||||
|
||||
@@ -313,7 +323,7 @@ export const aiMcpServerServiceFactory = ({
|
||||
const sessionId = crypto.randomUUID();
|
||||
|
||||
// 3. Build redirect URI
|
||||
const redirectUri = `${appCfg.SITE_URL}/api/v1/ai/mcp-servers/oauth/callback`;
|
||||
const redirectUri = `${appCfg.SITE_URL}/api/v1/ai/mcp/servers/oauth/callback`;
|
||||
|
||||
// 4. Get client credentials - either from DCR or hardcoded
|
||||
let resolvedClientId: string;
|
||||
@@ -517,6 +527,13 @@ export const aiMcpServerServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateAiMcpServerDTO) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actorOrgId);
|
||||
if (!orgLicensePlan.ai) {
|
||||
throw new BadRequestError({
|
||||
message: "AI operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
|
||||
@@ -113,7 +113,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
eventSubscriptions: false,
|
||||
machineIdentityAuthTemplates: false,
|
||||
pkiLegacyTemplates: false,
|
||||
pam: false
|
||||
pam: false,
|
||||
ai: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (
|
||||
|
||||
@@ -93,6 +93,7 @@ export type TFeatureSet = {
|
||||
fips: false;
|
||||
eventSubscriptions: false;
|
||||
pam: false;
|
||||
ai: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
||||
@@ -141,7 +141,7 @@ export const injectIdentity = fp(
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === "/api/v1/ai/mcp-servers/oauth/callback") {
|
||||
if (req.url === "/api/v1/ai/mcp/servers/oauth/callback") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2499,7 +2499,8 @@ export const registerRoutes = async (
|
||||
aiMcpServerUserCredentialDAL,
|
||||
kmsService,
|
||||
keyStore,
|
||||
permissionService
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const aiMcpActivityLogService = aiMcpActivityLogServiceFactory({
|
||||
@@ -2520,7 +2521,8 @@ export const registerRoutes = async (
|
||||
authTokenService: tokenService,
|
||||
aiMcpActivityLogService,
|
||||
userDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const migrationService = externalMigrationServiceFactory({
|
||||
|
||||
@@ -22,7 +22,7 @@ export const useListAiMcpActivityLogs = (
|
||||
queryFn: async ({ pageParam }) => {
|
||||
try {
|
||||
const { data } = await apiRequest.get<{ activityLogs: TAiMcpActivityLog[] }>(
|
||||
"/api/v1/ai/mcp-activity-logs",
|
||||
"/api/v1/ai/mcp/activity-logs",
|
||||
{
|
||||
params: {
|
||||
projectId: filters.projectId,
|
||||
|
||||
@@ -23,7 +23,7 @@ export const useCreateAiMcpEndpoint = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (dto: TCreateAiMcpEndpointDTO) => {
|
||||
const { data } = await apiRequest.post<{ endpoint: TAiMcpEndpoint }>(
|
||||
"/api/v1/ai/mcp-endpoints",
|
||||
"/api/v1/ai/mcp/endpoints",
|
||||
dto
|
||||
);
|
||||
return data.endpoint;
|
||||
@@ -42,7 +42,7 @@ export const useUpdateAiMcpEndpoint = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId, ...dto }: TUpdateAiMcpEndpointDTO) => {
|
||||
const { data } = await apiRequest.patch<{ endpoint: TAiMcpEndpoint }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}`,
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}`,
|
||||
dto
|
||||
);
|
||||
return data.endpoint;
|
||||
@@ -64,7 +64,7 @@ export const useDeleteAiMcpEndpoint = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId }: TDeleteAiMcpEndpointDTO) => {
|
||||
const { data } = await apiRequest.delete<{ endpoint: TAiMcpEndpoint }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}`
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}`
|
||||
);
|
||||
return data.endpoint;
|
||||
},
|
||||
@@ -82,7 +82,7 @@ export const useEnableEndpointTool = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId, serverToolId }: TEnableEndpointToolDTO) => {
|
||||
const { data } = await apiRequest.post<{ tool: TAiMcpEndpointToolConfig }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}/tools/${serverToolId}`
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}/tools/${serverToolId}`
|
||||
);
|
||||
return data.tool;
|
||||
},
|
||||
@@ -99,7 +99,7 @@ export const useDisableEndpointTool = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId, serverToolId }: TDisableEndpointToolDTO) => {
|
||||
await apiRequest.delete(`/api/v1/ai/mcp-endpoints/${endpointId}/tools/${serverToolId}`);
|
||||
await apiRequest.delete(`/api/v1/ai/mcp/endpoints/${endpointId}/tools/${serverToolId}`);
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -115,7 +115,7 @@ export const useBulkUpdateEndpointTools = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId, tools }: TBulkUpdateEndpointToolsDTO) => {
|
||||
const { data } = await apiRequest.patch<{ tools: TAiMcpEndpointToolConfig[] }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}/tools/bulk`,
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}/tools/bulk`,
|
||||
{ tools }
|
||||
);
|
||||
return data.tools;
|
||||
@@ -132,7 +132,7 @@ export const useFinalizeMcpEndpointOAuth = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId, ...body }: TFinalizeMcpEndpointOAuthDTO) => {
|
||||
const { data } = await apiRequest.post<{ callbackUrl: string }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}/oauth/finalize`,
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}/oauth/finalize`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
@@ -144,7 +144,7 @@ export const useInitiateServerOAuth = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId, serverId }: TInitiateServerOAuthDTO) => {
|
||||
const { data } = await apiRequest.post<{ authUrl: string; sessionId: string }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}/servers/${serverId}/oauth/initiate`
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}/servers/${serverId}/oauth/initiate`
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ export const useSaveUserServerCredential = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ endpointId, serverId, ...body }: TSaveUserServerCredentialDTO) => {
|
||||
const { data } = await apiRequest.post<{ success: boolean }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}/servers/${serverId}/credentials`,
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}/servers/${serverId}/credentials`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useListAiMcpEndpoints = ({ projectId }: TListAiMcpEndpointsDTO) =>
|
||||
const { data } = await apiRequest.get<{
|
||||
endpoints: TAiMcpEndpoint[];
|
||||
totalCount: number;
|
||||
}>("/api/v1/ai/mcp-endpoints", {
|
||||
}>("/api/v1/ai/mcp/endpoints", {
|
||||
params: { projectId }
|
||||
});
|
||||
return data;
|
||||
@@ -40,7 +40,7 @@ export const useGetAiMcpEndpointById = ({ endpointId }: { endpointId: string })
|
||||
queryKey: aiMcpEndpointKeys.byId(endpointId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ endpoint: TAiMcpEndpointWithServerIds }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}`
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}`
|
||||
);
|
||||
return data.endpoint;
|
||||
},
|
||||
@@ -53,7 +53,7 @@ export const useListEndpointTools = ({ endpointId }: { endpointId: string }) =>
|
||||
queryKey: aiMcpEndpointKeys.tools(endpointId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ tools: TAiMcpEndpointToolConfig[] }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}/tools`
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}/tools`
|
||||
);
|
||||
return data.tools;
|
||||
},
|
||||
@@ -66,7 +66,7 @@ export const useGetServersRequiringAuth = ({ endpointId }: { endpointId: string
|
||||
queryKey: aiMcpEndpointKeys.serversRequiringAuth(endpointId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ servers: TServerAuthStatus[] }>(
|
||||
`/api/v1/ai/mcp-endpoints/${endpointId}/servers-requiring-auth`
|
||||
`/api/v1/ai/mcp/endpoints/${endpointId}/servers-requiring-auth`
|
||||
);
|
||||
return data.servers;
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ export const useCreateAiMcpServer = () => {
|
||||
mutationFn: async (data) => {
|
||||
const { data: response } = await apiRequest.post<{
|
||||
server: TAiMcpServer;
|
||||
}>("/api/v1/ai/mcp-servers", data);
|
||||
}>("/api/v1/ai/mcp/servers", data);
|
||||
return response.server;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
@@ -39,7 +39,7 @@ export const useUpdateAiMcpServer = () => {
|
||||
mutationFn: async ({ serverId, ...data }) => {
|
||||
const { data: response } = await apiRequest.patch<{
|
||||
server: TAiMcpServer;
|
||||
}>(`/api/v1/ai/mcp-servers/${serverId}`, data);
|
||||
}>(`/api/v1/ai/mcp/servers/${serverId}`, data);
|
||||
return response.server;
|
||||
},
|
||||
onSuccess: (server, { serverId }) => {
|
||||
@@ -60,7 +60,7 @@ export const useDeleteAiMcpServer = () => {
|
||||
mutationFn: async ({ serverId }) => {
|
||||
const { data: response } = await apiRequest.delete<{
|
||||
server: TAiMcpServer;
|
||||
}>(`/api/v1/ai/mcp-servers/${serverId}`);
|
||||
}>(`/api/v1/ai/mcp/servers/${serverId}`);
|
||||
return response.server;
|
||||
},
|
||||
onSuccess: (server, { serverId }) => {
|
||||
@@ -78,7 +78,7 @@ export const useInitiateOAuth = () => {
|
||||
return useMutation<TInitiateOAuthResponse, object, TInitiateOAuthDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: response } = await apiRequest.post<TInitiateOAuthResponse>(
|
||||
"/api/v1/ai/mcp-servers/oauth/initiate",
|
||||
"/api/v1/ai/mcp/servers/oauth/initiate",
|
||||
data
|
||||
);
|
||||
return response;
|
||||
@@ -93,7 +93,7 @@ export const useSyncAiMcpServerTools = () => {
|
||||
mutationFn: async ({ serverId }) => {
|
||||
const { data: response } = await apiRequest.post<{
|
||||
tools: TAiMcpServerTool[];
|
||||
}>(`/api/v1/ai/mcp-servers/${serverId}/tools/sync`);
|
||||
}>(`/api/v1/ai/mcp/servers/${serverId}/tools/sync`);
|
||||
return response;
|
||||
},
|
||||
onSuccess: (_, { serverId }) => {
|
||||
|
||||
@@ -30,7 +30,7 @@ export const useListAiMcpServers = ({
|
||||
const { data } = await apiRequest.get<{
|
||||
servers: TAiMcpServer[];
|
||||
totalCount: number;
|
||||
}>("/api/v1/ai/mcp-servers", {
|
||||
}>("/api/v1/ai/mcp/servers", {
|
||||
params: { projectId, limit, offset }
|
||||
});
|
||||
return data;
|
||||
@@ -48,7 +48,7 @@ export const useGetAiMcpServerById = (
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
server: TAiMcpServer;
|
||||
}>(`/api/v1/ai/mcp-servers/${serverId}`);
|
||||
}>(`/api/v1/ai/mcp/servers/${serverId}`);
|
||||
return data.server;
|
||||
},
|
||||
enabled: Boolean(serverId),
|
||||
@@ -62,7 +62,7 @@ export const useListAiMcpServerTools = ({ serverId }: TListAiMcpServerToolsDTO)
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
tools: TAiMcpServerTool[];
|
||||
}>(`/api/v1/ai/mcp-servers/${serverId}/tools`);
|
||||
}>(`/api/v1/ai/mcp/servers/${serverId}/tools`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(serverId)
|
||||
@@ -77,7 +77,7 @@ export const useGetOAuthStatus = (
|
||||
queryKey: aiMcpServerKeys.oauthStatus(sessionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TOAuthStatusResponse>(
|
||||
`/api/v1/ai/mcp-servers/oauth/status/${sessionId}`
|
||||
`/api/v1/ai/mcp/servers/oauth/status/${sessionId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -63,4 +63,5 @@ export type SubscriptionPlan = {
|
||||
cardDeclinedDays?: number;
|
||||
machineIdentityAuthTemplates: boolean;
|
||||
pam: boolean;
|
||||
ai: boolean;
|
||||
};
|
||||
|
||||
@@ -1,87 +1,112 @@
|
||||
import { useEffect } from "react";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { useOrganization, useProject, useProjectPermission } from "@app/context";
|
||||
import { useOrganization, useProject, useProjectPermission, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
|
||||
|
||||
export const AILayout = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
|
||||
const location = useLocation();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.ai) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"Your current plan does not provide access to Infisical AI. To unlock this feature, please upgrade to Infisical Enterprise plan.",
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
return (
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/overview"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>MCP</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/access-management"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/audit-logs"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/settings"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<>
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/overview"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>MCP</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/access-management"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/audit-logs"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/ai/$projectId/settings"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text={popUp.upgradePlan.data?.description}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const MCPEndpointConnectionSection = ({ endpoint }: Props) => {
|
||||
const [copiedTokenId, setCopiedTokenId] = useState<string | null>(null);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
|
||||
const endpointUrl = `${window.location.origin}/api/v1/ai/mcp-endpoints/${endpoint.id}/connect`;
|
||||
const endpointUrl = `${window.location.origin}/api/v1/ai/mcp/endpoints/${endpoint.id}/connect`;
|
||||
const storageKey = `${STORAGE_KEY_PREFIX}${endpoint.id}`;
|
||||
|
||||
// Load tokens from localStorage on mount
|
||||
|
||||
@@ -3,7 +3,7 @@ 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([])
|
||||
serverIds: z.array(z.string().uuid()).default([])
|
||||
});
|
||||
|
||||
export type TAddMCPEndpointForm = z.infer<typeof AddMCPEndpointFormSchema>;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
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([])
|
||||
serverIds: z.array(z.string().uuid()).default([])
|
||||
});
|
||||
|
||||
type TEditMCPEndpointForm = z.infer<typeof EditMCPEndpointFormSchema>;
|
||||
|
||||
@@ -2,10 +2,16 @@ import { useState } from "react";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionMcpEndpointActions, ProjectPermissionSub } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionMcpEndpointActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TAiMcpEndpoint, useDeleteAiMcpEndpoint } from "@app/hooks/api";
|
||||
|
||||
import { AddMCPEndpointModal } from "./AddMCPEndpointModal";
|
||||
@@ -18,9 +24,18 @@ export const MCPEndpointsTab = () => {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedEndpoint, setSelectedEndpoint] = useState<TAiMcpEndpoint | null>(null);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
|
||||
const deleteEndpoint = useDeleteAiMcpEndpoint();
|
||||
|
||||
const handleCreateEndpoint = () => {
|
||||
if (subscription && !subscription.ai) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
text: "Your current plan does not include access to Infisical AI. To unlock this feature, please upgrade to Infisical Enterprise plan.",
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsCreateModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -115,6 +130,13 @@ export const MCPEndpointsTab = () => {
|
||||
onDeleteApproved={handleDeleteConfirm}
|
||||
/>
|
||||
)}
|
||||
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={popUp.upgradePlan.data?.text}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,12 @@ import { useState } from "react";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TAiMcpServer, useDeleteAiMcpServer } from "@app/hooks/api";
|
||||
|
||||
import { AddMCPServerModal } from "./AddMCPServerModal";
|
||||
@@ -18,9 +20,18 @@ export const MCPServersTab = () => {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedServer, setSelectedServer] = useState<TAiMcpServer | null>(null);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
|
||||
const deleteServer = useDeleteAiMcpServer();
|
||||
|
||||
const handleAddServer = () => {
|
||||
if (subscription && !subscription.ai) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
text: "Your current plan does not include access to Infisical AI. To unlock this feature, please upgrade to Infisical Enterprise plan.",
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsAddModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -112,6 +123,13 @@ export const MCPServersTab = () => {
|
||||
onDeleteApproved={handleDeleteConfirm}
|
||||
/>
|
||||
)}
|
||||
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={popUp.upgradePlan.data?.text}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user