feat: client registration against MCP endpoint

This commit is contained in:
Sheen Capadngan
2025-12-06 04:57:07 +08:00
parent ff64a83dc2
commit 2e8b354f56
18 changed files with 1215 additions and 24 deletions

View File

@@ -0,0 +1,97 @@
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { removeTrailingSlash } from "@app/lib/fn";
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`;
// 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 tokenEndpointUrl = `${apiBaseUrl}/oauth/token`;
const authorizeEndpointUrl = `${apiBaseUrl}/oauth/authorize`;
const registrationEndpointUrl = `${apiBaseUrl}/oauth/register`;
return {
resourceUrl,
authServerIssuer,
tokenEndpointUrl,
authorizeEndpointUrl,
registrationEndpointUrl
};
};
export const registerMcpEndpointMetadataRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const siteUrl = removeTrailingSlash(appCfg.SITE_URL || "");
const siteHost = new URL(siteUrl).host;
const scopeAccess = `https://${siteHost}/mcp:access`;
// OAuth 2.1: Protected Resource metadata
// GET /mcp-endpoints/:endpointId/.well-known/oauth-protected-resource
server.route({
method: "GET",
url: "/:endpointId/.well-known/oauth-protected-resource",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
})
},
handler: async (req, reply) => {
const { resourceUrl, authServerIssuer } = getMcpUrls(siteUrl, req.params.endpointId);
return reply.send({
resource: resourceUrl,
authorization_servers: [authServerIssuer],
scopes_supported: ["openid", scopeAccess],
bearer_methods_supported: ["header"]
});
}
});
};
// RFC 8414 compliant OAuth Authorization Server metadata
// GET /.well-known/oauth-authorization-server/mcp-endpoints/:endpointId
export const registerMcpEndpointAuthServerMetadataRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const siteUrl = removeTrailingSlash(appCfg.SITE_URL || "");
const siteHost = new URL(siteUrl).host;
const scopeAccess = `https://${siteHost}/mcp:access`;
server.route({
method: "GET",
url: "/mcp-endpoints/:endpointId",
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
})
},
config: {
rateLimit: readLimit
},
handler: async (req, reply) => {
const { authServerIssuer, authorizeEndpointUrl, tokenEndpointUrl, registrationEndpointUrl } = getMcpUrls(
siteUrl,
req.params.endpointId
);
return reply.send({
issuer: authServerIssuer,
authorization_endpoint: authorizeEndpointUrl,
token_endpoint: tokenEndpointUrl,
registration_endpoint: registrationEndpointUrl,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
token_endpoint_auth_methods_supported: ["none"],
scopes_supported: ["openid", scopeAccess]
});
}
});
};

View File

@@ -1,12 +1,120 @@
import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
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 { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms";
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";
const sendWwwAuthenticate = (reply: FastifyReply, endpointId: string, description?: string) => {
const appCfg = getConfig();
const protectedResourceMetadataUrl = `${appCfg.SITE_URL}/mcp-endpoints/${endpointId}/.well-known/oauth-protected-resource`;
let header = `Bearer resource_metadata="${protectedResourceMetadataUrl}", scope="openid"`;
if (description) header = `${header}, error_description="${description}"`;
void reply.header("WWW-Authenticate", header);
};
// Custom onRequest hook to enforce auth while returning proper WWW-Authenticate hint for MCP clients
const requireMcpAuthHook = (
req: FastifyRequest,
reply: FastifyReply,
done: HookHandlerDoneFunction,
endpointId: string
) => {
const { auth } = req;
if (!auth) {
sendWwwAuthenticate(reply, endpointId, "Missing authorization header");
void reply.status(401).send();
return;
}
const allowed = auth.authMode === AuthMode.MCP_JWT;
if (!allowed) {
void reply.status(403).send();
return;
}
if (auth.authMode === AuthMode.MCP_JWT && !req.permission.orgId) {
void reply.status(401).send({ message: "Unauthorized: organization context required" });
return;
}
done();
};
export const registerAiMcpEndpointRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
})
},
url: "/:endpointId/connect",
onRequest: (req, reply, done) => requireMcpAuthHook(req, reply, done, req.params.endpointId),
handler: async (req, res) => {
await res.hijack(); // allow manual control of the underlying res
if (req.auth.authMode !== AuthMode.MCP_JWT) {
throw new UnauthorizedError({ message: "Unauthorized" });
}
if (req.params.endpointId !== req.auth.token.mcp?.endpointId) {
throw new UnauthorizedError({ message: "Unauthorized" });
}
const { server: mcpServer, transport } = await server.services.aiMcpEndpoint.interactWithMcp({
endpointId: req.params.endpointId
});
// Close transport when client disconnects
res.raw.on("close", () => {
void transport.close().catch((err) => {
logger.error(err, "Failed to close transport for mcp endpoint");
});
});
await mcpServer.connect(transport);
await transport.handleRequest(req.raw, res.raw, req.body);
}
});
server.route({
method: ["GET", "DELETE"],
config: {
rateLimit: writeLimit
},
url: "/:endpointId/connect",
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
})
},
onRequest: (req, reply, done) => requireMcpAuthHook(req, reply, done, req.params.endpointId),
handler: async (_req, res) => {
void res
.status(405)
.header("Allow", "POST")
.send({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed"
},
id: null
});
}
});
server.route({
url: "/",
method: "POST",
@@ -278,4 +386,166 @@ export const registerAiMcpEndpointRouter = async (server: FastifyZodProvider) =>
return { tools };
}
});
// OAUTH 2.0
server.route({
method: "POST",
url: "/:endpointId/oauth/register",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
}),
body: z.object({
redirect_uris: z.array(z.string()),
token_endpoint_auth_method: z.string(),
grant_types: z.array(z.string()),
response_types: z.array(z.string()),
client_name: z.string(),
client_uri: z.string().optional()
}),
response: {
200: z.object({
client_id: z.string(),
redirect_uris: z.array(z.string()),
client_name: z.string(),
client_uri: z.string().optional(),
grant_types: z.array(z.string()),
response_types: z.array(z.string()),
token_endpoint_auth_method: z.string(),
client_id_issued_at: z.number()
})
}
},
handler: async (req) => {
const payload = await server.services.aiMcpEndpoint.oauthRegisterClient({
endpointId: req.params.endpointId,
...req.body
});
return payload;
}
});
// OAuth authorize - redirect to scope selection page
server.route({
method: "GET",
url: "/:endpointId/oauth/authorize",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
}),
querystring: z.object({
response_type: z.string(),
client_id: z.string(),
code_challenge: z.string(),
code_challenge_method: z.enum(["S256"]),
redirect_uri: z.string(),
resource: z.string(),
state: z.string().optional()
})
},
handler: async (req, res) => {
await server.services.aiMcpEndpoint.oauthAuthorizeClient({
clientId: req.query.client_id,
state: req.query.state
});
const query = new URLSearchParams({
...req.query,
endpointId: req.params.endpointId
}).toString();
void res.redirect(`/organization/mcp-endpoint-finalize?${query}`);
}
});
server.route({
method: "POST",
url: "/:endpointId/oauth/finalize",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
}),
body: z.object({
response_type: z.string(),
client_id: z.string(),
code_challenge: z.string(),
code_challenge_method: z.enum(["S256"]),
redirect_uri: z.string(),
resource: z.string(),
expireIn: z.string().refine((val) => ms(val) > 0, "Max TTL must be a positive number")
}),
response: {
200: z.object({
callbackUrl: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const userInfo = req.auth.authMode === AuthMode.JWT ? req.auth.user : null;
if (!userInfo) throw new BadRequestError({ message: "User info not found" });
const redirectUri = await server.services.aiMcpEndpoint.oauthFinalize({
endpointId: req.params.endpointId,
clientId: req.body.client_id,
codeChallenge: req.body.code_challenge,
codeChallengeMethod: req.body.code_challenge_method,
redirectUri: req.body.redirect_uri,
resource: req.body.resource,
responseType: req.body.response_type,
projectId: "",
tokenId: req.auth.authMode === AuthMode.JWT ? req.auth.tokenVersionId : "",
userInfo,
expiry: req.body.expireIn,
permission: req.permission,
userAgent: req.auditLogInfo.userAgent || "",
userIp: req.auditLogInfo.ipAddress || ""
});
return { callbackUrl: redirectUri.toString() };
}
});
server.route({
method: "POST",
url: "/:endpointId/oauth/token",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
endpointId: z.string().trim().min(1)
}),
body: z.object({
grant_type: z.literal("authorization_code"),
code: z.string(),
redirect_uri: z.string().url(),
code_verifier: z.string(),
client_id: z.string()
}),
response: {
200: z.object({
access_token: z.string(),
token_type: z.string(),
expires_in: z.number(),
scope: z.string()
})
}
},
handler: async (req) => {
const payload = await server.services.aiMcpEndpoint.oauthTokenExchange({
endpointId: req.params.endpointId,
...req.body
});
return payload;
}
});
};

View File

@@ -1,5 +1,25 @@
import { NotFoundError } from "@app/lib/errors";
import crypto from "node:crypto";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Server as RawMcpServer } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { crypto as cryptoModule } from "@app/lib/crypto";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TAiMcpServerDALFactory } from "../ai-mcp-server/ai-mcp-server-dal";
import { TAiMcpServerToolDALFactory } from "../ai-mcp-server/ai-mcp-server-tool-dal";
import { TAiMcpServerCredentials } from "../ai-mcp-server/ai-mcp-server-types";
import { TAiMcpEndpointDALFactory } from "./ai-mcp-endpoint-dal";
import { TAiMcpEndpointServerDALFactory } from "./ai-mcp-endpoint-server-dal";
import { TAiMcpEndpointServerToolDALFactory } from "./ai-mcp-endpoint-server-tool-dal";
@@ -11,8 +31,13 @@ import {
TDisableEndpointToolDTO,
TEnableEndpointToolDTO,
TGetAiMcpEndpointDTO,
TInteractWithMcpDTO,
TListAiMcpEndpointsDTO,
TListEndpointToolsDTO,
TOAuthAuthorizeClientDTO,
TOAuthFinalizeDTO,
TOAuthRegisterClientDTO,
TOAuthTokenExchangeDTO,
TUpdateAiMcpEndpointDTO
} from "./ai-mcp-endpoint-types";
@@ -20,15 +45,243 @@ type TAiMcpEndpointServiceFactoryDep = {
aiMcpEndpointDAL: TAiMcpEndpointDALFactory;
aiMcpEndpointServerDAL: TAiMcpEndpointServerDALFactory;
aiMcpEndpointServerToolDAL: TAiMcpEndpointServerToolDALFactory;
aiMcpServerDAL: TAiMcpServerDALFactory;
aiMcpServerToolDAL: TAiMcpServerToolDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
authTokenService: Pick<TAuthTokenServiceFactory, "getUserTokenSessionById">;
};
// OAuth schemas for parsing cached data
const DynamicClientInfoSchema = z.object({
client_id: z.string(),
redirect_uris: z.array(z.string()),
client_name: z.string(),
client_uri: z.string().optional(),
grant_types: z.array(z.string()),
response_types: z.array(z.string()),
token_endpoint_auth_method: z.string(),
client_id_issued_at: z.number(),
state: z.string().optional()
});
const OauthChallengeCodeSchema = z.object({
codeChallenge: z.string(),
codeChallengeMethod: z.string(),
userId: z.string(),
endpointId: z.string(),
expiry: z.string(),
redirectUri: z.string(),
userInfo: z.object({
tokenId: z.string(),
orgId: z.string(),
authMethod: z.string().nullable(),
email: z.string(),
actorIp: z.string(),
actorName: z.string(),
actorUserAgent: z.string()
})
});
const OAUTH_FLOW_EXPIRY_IN_SECS = 5 * 60;
// PKCE challenge computation
const computePkceChallenge = (codeVerifier: string) => {
const sha256 = crypto.createHash("sha256").update(codeVerifier).digest();
return Buffer.from(sha256).toString("base64url");
};
export type TAiMcpEndpointServiceFactory = ReturnType<typeof aiMcpEndpointServiceFactory>;
/* 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-unsafe-argument */
export const aiMcpEndpointServiceFactory = ({
aiMcpEndpointDAL,
aiMcpEndpointServerDAL,
aiMcpEndpointServerToolDAL
aiMcpEndpointServerToolDAL,
aiMcpServerDAL,
aiMcpServerToolDAL,
kmsService,
keyStore,
authTokenService
}: TAiMcpEndpointServiceFactoryDep) => {
const interactWithMcp = async ({ endpointId }: TInteractWithMcpDTO) => {
const appCfg = getConfig();
// Get the endpoint
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
if (!endpoint) {
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
}
// Get connected servers for this endpoint
const connectedServerLinks = await aiMcpEndpointServerDAL.find({ aiMcpEndpointId: endpointId });
// Get enabled tools for this endpoint
const enabledToolConfigs = await aiMcpEndpointServerToolDAL.find({ aiMcpEndpointId: endpointId });
const enabledToolIds = new Set(enabledToolConfigs.map((t) => t.aiMcpServerToolId));
// Get the actual server details
const serverIds = connectedServerLinks.map((link) => link.aiMcpServerId);
const servers = await Promise.all(serverIds.map((id) => aiMcpServerDAL.findById(id)));
const validServers = servers.filter((s) => s !== null && s !== undefined);
if (validServers.length === 0) {
// Return an empty MCP server if no servers are connected
const emptyServer = new RawMcpServer(
{
name: "infisical-mcp-endpoint",
version: appCfg.INFISICAL_PLATFORM_VERSION || "0.0.1"
},
{
capabilities: {
tools: {}
}
}
);
emptyServer.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [] }));
emptyServer.setRequestHandler(CallToolRequestSchema, () => {
throw new Error("No MCP servers connected to this endpoint");
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
return { server: emptyServer, transport };
}
// Create cipher pair for decryption
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: endpoint.projectId
});
// Connect to each server and get their tools
const mcpClientTools = await Promise.all(
validServers.map(async (mcpServer) => {
// Get the database tool records for this server (to map tool names to IDs)
const dbServerTools = await aiMcpServerToolDAL.find({ aiMcpServerId: mcpServer.id });
// Create a map from tool name to database tool ID for this specific server
const toolNameToDbId = new Map(dbServerTools.map((t) => [t.name, t.id]));
if (!mcpServer.encryptedCredentials) {
return { client: null, server: mcpServer, tools: [], toolNameToDbId };
}
const decryptedCredentials = JSON.parse(
decryptor({ cipherTextBlob: mcpServer.encryptedCredentials }).toString()
) as TAiMcpServerCredentials;
// Get access token from credentials
let accessToken: string | undefined;
if ("accessToken" in decryptedCredentials) {
accessToken = decryptedCredentials.accessToken;
} else if ("token" in decryptedCredentials) {
accessToken = decryptedCredentials.token;
}
const headers: Record<string, string> = {};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
try {
const client = new Client({
name: `infisical-mcp-client-${mcpServer.name}`,
version: "1.0.0"
});
const clientTransport = new StreamableHTTPClientTransport(new URL(mcpServer.url), {
requestInit: { headers }
});
await client.connect(clientTransport);
// Get tools from this server
const { tools } = await client.listTools();
return {
client,
server: mcpServer,
tools: tools as Array<{ name: string; description?: string; inputSchema?: Record<string, unknown> }>,
toolNameToDbId
};
} catch {
// If connection fails, return empty tools for this server
return { client: null, server: mcpServer, tools: [], toolNameToDbId };
}
})
);
// Filter tools to only include explicitly enabled ones (least privilege principle)
// If no tools are explicitly enabled, no tools will be available
const enabledMcpClientTools = mcpClientTools.map((clientTool) => ({
...clientTool,
tools: clientTool.tools.filter((tool) => {
// Get the database ID for this tool (specific to this server)
const dbToolId = clientTool.toolNameToDbId.get(tool.name);
// Only include if the database tool ID is explicitly enabled
return dbToolId !== undefined && enabledToolIds.has(dbToolId);
})
}));
// Create the aggregating MCP server
const server = new RawMcpServer(
{
name: "infisical-mcp-endpoint",
version: appCfg.INFISICAL_PLATFORM_VERSION || "0.0.1"
},
{
capabilities: {
tools: {}
}
}
);
// Handle ListTools request - aggregate tools from all connected servers
server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: enabledMcpClientTools.flatMap((el) => el.tools)
}));
// Handle CallTool request - route to the appropriate server
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Find the server that has this tool
const selectedMcpClient = enabledMcpClientTools.find((el) => el.tools.find((t) => t.name === name));
if (!selectedMcpClient || !selectedMcpClient.client) {
throw new Error(`Unknown tool: ${name}`);
}
try {
const result = await selectedMcpClient.client.callTool({
name,
arguments: args
});
return result;
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
}
],
isError: true
};
}
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
return { server, transport };
};
const createMcpEndpoint = async ({ projectId, name, description, serverIds }: TCreateAiMcpEndpointDTO) => {
const endpoint = await aiMcpEndpointDAL.create({
projectId,
@@ -224,7 +477,183 @@ export const aiMcpEndpointServiceFactory = ({
return results;
};
// OAuth 2.0 Methods
const oauthRegisterClient = async ({
endpointId,
client_name,
client_uri,
grant_types,
redirect_uris,
response_types,
token_endpoint_auth_method
}: TOAuthRegisterClientDTO) => {
// Verify the endpoint exists
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
if (!endpoint) {
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
}
const clientId = `mcp_client_${crypto.randomBytes(32).toString("hex")}`;
const now = Math.floor(Date.now() / 1000);
const payload = {
client_id: clientId,
client_name,
client_uri,
grant_types,
redirect_uris,
response_types,
token_endpoint_auth_method,
client_id_issued_at: now
};
await keyStore.setItemWithExpiry(
KeyStorePrefixes.AiMcpEndpointOAuthClient(clientId),
OAUTH_FLOW_EXPIRY_IN_SECS,
JSON.stringify(payload)
);
return payload;
};
const oauthAuthorizeClient = async ({ clientId, state }: TOAuthAuthorizeClientDTO) => {
const oauthClientCache = await keyStore.getItem(KeyStorePrefixes.AiMcpEndpointOAuthClient(clientId));
if (!oauthClientCache) {
throw new UnauthorizedError({ message: `MCP OAuth client with id ${clientId} not found` });
}
// Update with state
await keyStore.setItemWithExpiry(
KeyStorePrefixes.AiMcpEndpointOAuthClient(clientId),
OAUTH_FLOW_EXPIRY_IN_SECS,
JSON.stringify({ ...JSON.parse(oauthClientCache), state })
);
};
const oauthFinalize = async ({
endpointId,
clientId,
codeChallenge,
codeChallengeMethod,
redirectUri,
expiry,
tokenId,
userInfo,
permission,
userAgent,
userIp
}: TOAuthFinalizeDTO) => {
const oauthClientCache = await keyStore.getItem(KeyStorePrefixes.AiMcpEndpointOAuthClient(clientId));
if (!oauthClientCache) {
throw new UnauthorizedError({ message: `MCP OAuth client with id ${clientId} not found` });
}
const oauthClient = await DynamicClientInfoSchema.parseAsync(JSON.parse(oauthClientCache));
const isValidRedirectUri = oauthClient.redirect_uris.some((el) => new URL(el).toString() === redirectUri);
if (!isValidRedirectUri) throw new BadRequestError({ message: "Redirect URI mismatch" });
// Verify endpoint exists
const endpoint = await aiMcpEndpointDAL.findById(endpointId);
if (!endpoint) {
throw new NotFoundError({ message: `MCP endpoint with ID '${endpointId}' not found` });
}
const code = crypto.randomBytes(32).toString("hex");
await keyStore.setItemWithExpiry(
KeyStorePrefixes.AiMcpEndpointOAuthCode(clientId, code),
OAUTH_FLOW_EXPIRY_IN_SECS,
JSON.stringify({
codeChallenge,
codeChallengeMethod,
userId: permission.id,
endpointId,
expiry,
redirectUri,
userInfo: {
tokenId,
orgId: permission.orgId,
authMethod: permission.authMethod,
email: userInfo.email || "",
actorIp: userIp,
actorName: `${userInfo.firstName || ""} ${userInfo.lastName || ""}`.trim(),
actorUserAgent: userAgent
}
})
);
const url = new URL(redirectUri);
url.searchParams.set("code", code);
if (oauthClient.state) url.searchParams.set("state", String(oauthClient.state));
return url;
};
const oauthTokenExchange = async (dto: TOAuthTokenExchangeDTO) => {
const appCfg = getConfig();
if (dto.grant_type !== "authorization_code") {
throw new BadRequestError({ message: "Only authorization_code grant type is supported" });
}
const oauthClientCache = await keyStore.getItem(KeyStorePrefixes.AiMcpEndpointOAuthClient(dto.client_id));
if (!oauthClientCache) {
throw new UnauthorizedError({ message: `MCP OAuth client with id ${dto.client_id} not found` });
}
const oauthAuthorizeSessionCache = await keyStore.getItem(
KeyStorePrefixes.AiMcpEndpointOAuthCode(dto.client_id, dto.code)
);
if (!oauthAuthorizeSessionCache) {
throw new UnauthorizedError({ message: "MCP OAuth session not found" });
}
const oauthAuthorizeInfo = await OauthChallengeCodeSchema.parseAsync(JSON.parse(oauthAuthorizeSessionCache));
const isInvalidRedirectUri = dto.redirect_uri !== oauthAuthorizeInfo.redirectUri;
if (isInvalidRedirectUri) throw new BadRequestError({ message: "Redirect URI mismatch" });
// Delete the code (one-time use)
await keyStore.deleteItem(KeyStorePrefixes.AiMcpEndpointOAuthCode(dto.client_id, dto.code));
// Verify PKCE challenge
const challenge = computePkceChallenge(dto.code_verifier);
if (challenge !== oauthAuthorizeInfo.codeChallenge) {
throw new BadRequestError({ message: "PKCE challenge mismatch" });
}
// Verify user session is still valid
const tokenSession = await authTokenService.getUserTokenSessionById(
oauthAuthorizeInfo.userInfo.tokenId,
oauthAuthorizeInfo.userId
);
if (!tokenSession) throw new UnauthorizedError({ message: "User session not found" });
// Generate MCP access token
const accessToken = cryptoModule.jwt().sign(
{
authMethod: oauthAuthorizeInfo.userInfo.authMethod,
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: oauthAuthorizeInfo.userId,
tokenVersionId: tokenSession.id,
accessVersion: tokenSession.accessVersion,
organizationId: oauthAuthorizeInfo.userInfo.orgId,
isMfaVerified: true,
mcp: {
endpointId: oauthAuthorizeInfo.endpointId
}
},
appCfg.AUTH_SECRET,
{ expiresIn: oauthAuthorizeInfo.expiry }
);
return {
access_token: accessToken,
token_type: "Bearer",
expires_in: Math.floor(ms(oauthAuthorizeInfo.expiry) / 1000),
scope: "openid"
};
};
return {
interactWithMcp,
createMcpEndpoint,
listMcpEndpoints,
getMcpEndpointById,
@@ -233,6 +662,10 @@ export const aiMcpEndpointServiceFactory = ({
listEndpointTools,
enableEndpointTool,
disableEndpointTool,
bulkUpdateEndpointTools
bulkUpdateEndpointTools,
oauthRegisterClient,
oauthAuthorizeClient,
oauthFinalize,
oauthTokenExchange
};
};

View File

@@ -20,6 +20,10 @@ export type TGetAiMcpEndpointDTO = {
endpointId: string;
};
export type TInteractWithMcpDTO = {
endpointId: string;
};
export type TListAiMcpEndpointsDTO = {
projectId: string;
};
@@ -64,3 +68,56 @@ export type TEndpointToolConfig = {
aiMcpServerToolId: string;
isEnabled: boolean;
};
// OAuth 2.0 Types
export type TOAuthRegisterClientDTO = {
endpointId: string;
redirect_uris: string[];
token_endpoint_auth_method: string;
grant_types: string[];
response_types: string[];
client_name: string;
client_uri?: string;
};
export type TOAuthAuthorizeClientDTO = {
clientId: string;
state?: string;
};
export type TOAuthFinalizeDTO = {
endpointId: string;
clientId: string;
codeChallenge: string;
codeChallengeMethod: string;
redirectUri: string;
resource: string;
responseType: string;
projectId: string;
path?: string;
expiry: string;
tokenId: string;
userInfo: {
id: string;
email?: string | null;
firstName?: string | null;
lastName?: string | null;
};
permission: {
type: string;
id: string;
orgId: string;
authMethod: string | null;
};
userAgent: string;
userIp: string;
};
export type TOAuthTokenExchangeDTO = {
endpointId: string;
grant_type: "authorization_code";
code: string;
redirect_uri: string;
code_verifier: string;
client_id: string;
};

View File

@@ -81,7 +81,11 @@ export const KeyStorePrefixes = {
PkiAcmeNonce: (nonce: string) => `pki-acme-nonce:${nonce}` as const,
AiMcpServerOAuth: (sessionId: string) => `ai-mcp-server-oauth:${sessionId}` as const
AiMcpServerOAuth: (sessionId: string) => `ai-mcp-server-oauth:${sessionId}` as const,
// AI MCP Endpoint OAuth
AiMcpEndpointOAuthClient: (clientId: string) => `ai-mcp-endpoint-oauth-client:${clientId}` as const,
AiMcpEndpointOAuthCode: (clientId: string, code: string) => `ai-mcp-endpoint-oauth-code:${clientId}:${code}` as const
};
export const KeyStoreTtls = {

View File

@@ -15,7 +15,7 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
export type TAuthMode =
| {
authMode: AuthMode.JWT;
authMode: AuthMode.JWT | AuthMode.MCP_JWT;
actor: ActorType.USER;
userId: string;
tokenVersionId: string; // the session id of token used
@@ -91,12 +91,21 @@ export const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
const decodedToken = crypto.jwt().verify(authTokenValue, jwtSecret) as JwtPayload;
switch (decodedToken.authTokenType) {
case AuthTokenType.ACCESS_TOKEN:
case AuthTokenType.ACCESS_TOKEN: {
if (decodedToken?.mcp) {
return {
authMode: AuthMode.MCP_JWT,
token: decodedToken as AuthModeJwtTokenPayload,
actor: ActorType.USER
} as const;
}
return {
authMode: AuthMode.JWT,
token: decodedToken as AuthModeJwtTokenPayload,
actor: ActorType.USER
} as const;
}
case AuthTokenType.API_KEY:
// throw new Error("API Key auth is no longer supported.");
return { authMode: AuthMode.API_KEY, token: decodedToken, actor: ActorType.USER } as const;
@@ -183,6 +192,27 @@ export const injectIdentity = fp(
};
break;
}
case AuthMode.MCP_JWT: {
const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
requestContext.set("orgId", orgId);
requestContext.set("orgName", orgName);
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
req.auth = {
authMode: AuthMode.MCP_JWT,
user,
userId: user.id,
tokenVersionId,
actor,
orgId,
rootOrgId,
parentOrgId,
authMethod: token.authMethod,
isMfaVerified: token.isMfaVerified,
token
};
break;
}
case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(
token,

View File

@@ -4,6 +4,10 @@ import { Knex } from "knex";
import { monitorEventLoopDelay } from "perf_hooks";
import { z } from "zod";
import {
registerMcpEndpointAuthServerMetadataRouter,
registerMcpEndpointMetadataRouter
} from "@app/ee/routes/ai/mcp-endpoint-metadata-router";
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
import { registerV1EERoutes } from "@app/ee/routes/v1";
import { registerV2EERoutes } from "@app/ee/routes/v2";
@@ -2461,7 +2465,12 @@ export const registerRoutes = async (
const aiMcpEndpointService = aiMcpEndpointServiceFactory({
aiMcpEndpointDAL,
aiMcpEndpointServerDAL,
aiMcpEndpointServerToolDAL
aiMcpEndpointServerToolDAL,
aiMcpServerDAL,
aiMcpServerToolDAL,
kmsService,
keyStore,
authTokenService: tokenService
});
const migrationService = externalMigrationServiceFactory({
@@ -2760,6 +2769,10 @@ export const registerRoutes = async (
// register special routes
await server.register(registerCertificateEstRouter, { prefix: "/.well-known/est" });
await server.register(registerMcpEndpointMetadataRouter, { prefix: "/mcp-endpoints" });
await server.register(registerMcpEndpointAuthServerMetadataRouter, {
prefix: "/.well-known/oauth-authorization-server"
});
// register routes for v1
await server.register(

View File

@@ -30,7 +30,8 @@ export enum AuthMode {
SERVICE_TOKEN = "serviceToken",
API_KEY = "apiKey",
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
SCIM_TOKEN = "scimToken"
SCIM_TOKEN = "scimToken",
MCP_JWT = "mcpJwt"
}
export enum ActorType { // would extend to AWS, Azure, ...
@@ -57,6 +58,9 @@ export type AuthModeJwtTokenPayload = {
organizationId?: string;
isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
mcp?: {
endpointId: string;
};
};
export type AuthModeMfaJwtTokenPayload = {

View File

@@ -4,6 +4,7 @@ export {
useDeleteAiMcpEndpoint,
useDisableEndpointTool,
useEnableEndpointTool,
useFinalizeMcpEndpointOAuth,
useUpdateAiMcpEndpoint
} from "./mutations";
export {

View File

@@ -11,6 +11,7 @@ import {
TDeleteAiMcpEndpointDTO,
TDisableEndpointToolDTO,
TEnableEndpointToolDTO,
TFinalizeMcpEndpointOAuthDTO,
TUpdateAiMcpEndpointDTO
} from "./types";
@@ -124,3 +125,15 @@ export const useBulkUpdateEndpointTools = () => {
}
});
};
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`,
body
);
return data;
}
});
};

View File

@@ -68,3 +68,14 @@ export type TBulkUpdateEndpointToolsDTO = {
isEnabled: boolean;
}>;
};
export type TFinalizeMcpEndpointOAuthDTO = {
endpointId: string;
response_type: string;
client_id: string;
code_challenge: string;
code_challenge_method: string;
redirect_uri: string;
resource: string;
expireIn: string;
};

View File

@@ -1,4 +1,9 @@
import { GenericFieldLabel } from "@app/components/v2";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { GenericFieldLabel, IconButton, Tooltip } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TAiMcpEndpointWithServerIds } from "@app/hooks/api";
type Props = {
@@ -6,8 +11,18 @@ type Props = {
};
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`;
const [isCopied, setIsCopied] = useToggle(false);
const endpointUrl = `${window.location.origin}/api/v1/ai/mcp-endpoints/${endpoint.id}/connect`;
const handleCopy = () => {
navigator.clipboard.writeText(endpointUrl);
setIsCopied.on();
createNotification({
text: "Endpoint URL copied to clipboard",
type: "info"
});
setTimeout(() => setIsCopied.off(), 2000);
};
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
@@ -15,10 +30,24 @@ export const MCPEndpointConnectionSection = ({ endpoint }: Props) => {
<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 label="Endpoint URL">
<div className="flex items-center gap-2">
<div className="flex-1 overflow-hidden rounded border border-mineshaft-500 bg-mineshaft-700">
<code className="block overflow-x-auto px-3 py-2 font-mono text-sm whitespace-nowrap text-mineshaft-200">
{endpointUrl}
</code>
</div>
<Tooltip content={isCopied ? "Copied!" : "Copy URL"}>
<IconButton
ariaLabel="Copy endpoint URL"
variant="outline_bg"
size="sm"
onClick={handleCopy}
>
<FontAwesomeIcon icon={isCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</GenericFieldLabel>
</div>
</div>

View File

@@ -39,10 +39,10 @@ export const MCPServerList = ({ onEditServer, onDeleteServer }: Props) => {
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="mcp-servers" />}
{isLoading && <TableSkeleton columns={3} innerKey="mcp-servers" />}
{!isLoading && (!servers || servers.length === 0) && (
<Tr>
<Td colSpan={4}>
<Td colSpan={3}>
<EmptyState title="No MCP Servers" />
</Td>
</Tr>

View File

@@ -0,0 +1,177 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle, faPlug, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link, useSearch } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useFinalizeMcpEndpointOAuth, useGetAiMcpEndpointById } from "@app/hooks/api";
const FinalizeFormSchema = z.object({
expireIn: z.string().min(1, "Expiration is required")
});
type FormData = z.infer<typeof FinalizeFormSchema>;
export const McpEndpointFinalizePage = () => {
const [isRedirecting, setIsRedirecting] = useState(false);
const search = useSearch({
from: "/_authenticate/organization/mcp-endpoint-finalize"
});
const { data: endpoint, isLoading: isEndpointLoading } = useGetAiMcpEndpointById({
endpointId: search.endpointId
});
const { mutateAsync: finalizeOAuth, isPending } = useFinalizeMcpEndpointOAuth();
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(FinalizeFormSchema),
defaultValues: {
expireIn: "30d"
}
});
const onSubmit = async ({ expireIn }: FormData) => {
try {
const { callbackUrl } = await finalizeOAuth({
endpointId: search.endpointId,
response_type: search.response_type,
client_id: search.client_id,
code_challenge: search.code_challenge,
code_challenge_method: search.code_challenge_method,
redirect_uri: search.redirect_uri,
resource: search.resource,
expireIn
});
setIsRedirecting(true);
window.location.href = callbackUrl;
// Fallback: try to close the window after 3 seconds if redirect doesn't navigate away
setTimeout(() => {
window.close();
}, 3000);
} catch (error) {
console.error("Failed to authorize:", error);
createNotification({
text: "Failed to authorize MCP endpoint access",
type: "error"
});
}
};
if (isRedirecting) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-bunker-800">
<div className="w-full max-w-md rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8 text-center shadow-lg">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-500/10">
<FontAwesomeIcon icon={faCheckCircle} className="text-3xl text-green-500" />
</div>
<h1 className="text-xl font-semibold text-mineshaft-100">Authorization Successful</h1>
<p className="mt-2 text-sm text-bunker-300">
<FontAwesomeIcon icon={faSpinner} className="mr-2 animate-spin" />
Redirecting back to the application...
</p>
</div>
</div>
);
}
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-bunker-800">
<Helmet>
<title>Authorize MCP Endpoint</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="w-full max-w-md rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8 shadow-lg">
<Link to="/" className="mb-6 block">
<img src="/images/gradientLogo.svg" className="mx-auto h-16" alt="Infisical logo" />
</Link>
<div className="mb-6 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
<FontAwesomeIcon icon={faPlug} className="text-2xl text-primary" />
</div>
<h1 className="text-xl font-semibold text-mineshaft-100">Authorize MCP Access</h1>
<p className="mt-2 text-sm text-bunker-300">
An external application is requesting access to your MCP endpoint
</p>
</div>
{isEndpointLoading && (
<div className="mb-6 animate-pulse rounded-lg border border-mineshaft-600 bg-mineshaft-700 p-4">
<div className="h-4 w-1/3 rounded bg-mineshaft-600" />
<div className="mt-2 h-3 w-2/3 rounded bg-mineshaft-600" />
</div>
)}
{!isEndpointLoading && endpoint && (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-700 p-4">
<p className="text-xs font-medium tracking-wide text-bunker-400 uppercase">Endpoint</p>
<p className="mt-1 font-medium text-mineshaft-100">{endpoint.name}</p>
{endpoint.description && (
<p className="mt-1 text-sm text-bunker-300">{endpoint.description}</p>
)}
<div className="mt-3 flex items-center gap-4 text-xs text-bunker-400">
<span>{endpoint.connectedServers} server(s)</span>
<span>{endpoint.activeTools} tool(s)</span>
</div>
</div>
)}
{!isEndpointLoading && !endpoint && (
<div className="mb-6 rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-center">
<p className="text-sm text-red-400">Endpoint not found</p>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="expireIn"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Duration"
isError={Boolean(error)}
errorText={error?.message}
helperText="How long the access token should be valid (e.g., 1h, 7d, 30d)"
>
<Input {...field} placeholder="30d" />
</FormControl>
)}
/>
<div className="mt-6 flex gap-3">
<Button
type="submit"
className="flex-1"
isLoading={isSubmitting || isPending}
isDisabled={isSubmitting || isPending || !endpoint}
>
Authorize
</Button>
<Link to="/">
<Button variant="outline_bg" colorSchema="secondary">
Cancel
</Button>
</Link>
</div>
</form>
<p className="mt-6 text-center text-xs text-bunker-400">
By authorizing, you grant the external application access to interact with the tools
available through this MCP endpoint.
</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,22 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { McpEndpointFinalizePage } from "./McpEndpointFinalizePage";
const McpEndpointFinalizePageQuerySchema = z.object({
response_type: z.string(),
client_id: z.string(),
code_challenge: z.string(),
code_challenge_method: z.string(),
redirect_uri: z.string(),
resource: z.string(),
state: z.string().optional(),
scope: z.string().optional(),
endpointId: z.string()
});
export const Route = createFileRoute("/_authenticate/organization/mcp-endpoint-finalize")({
component: McpEndpointFinalizePage,
validateSearch: zodValidator(McpEndpointFinalizePageQuerySchema)
});

View File

@@ -37,6 +37,7 @@ import { Route as authLoginLdapPageRouteImport } from './pages/auth/LoginLdapPag
import { Route as authAdminLoginPageRouteImport } from './pages/auth/AdminLoginPage/route'
import { Route as adminSignUpPageRouteImport } from './pages/admin/SignUpPage/route'
import { Route as organizationNoOrgPageRouteImport } from './pages/organization/NoOrgPage/route'
import { Route as organizationMcpEndpointFinalizePageRouteImport } from './pages/organization/McpEndpointFinalizePage/route'
import { Route as authSignUpPageRouteImport } from './pages/auth/SignUpPage/route'
import { Route as authLoginPageRouteImport } from './pages/auth/LoginPage/route'
import { Route as redirectsProjectRedirectImport } from './pages/redirects/project-redirect'
@@ -539,6 +540,13 @@ const organizationNoOrgPageRouteRoute = organizationNoOrgPageRouteImport.update(
} as any,
)
const organizationMcpEndpointFinalizePageRouteRoute =
organizationMcpEndpointFinalizePageRouteImport.update({
id: '/organization/mcp-endpoint-finalize',
path: '/organization/mcp-endpoint-finalize',
getParentRoute: () => middlewaresAuthenticateRoute,
} as any)
const authSignUpPageRouteRoute = authSignUpPageRouteImport.update({
id: '/',
path: '/',
@@ -2455,6 +2463,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authSignUpPageRouteImport
parentRoute: typeof RestrictLoginSignupSignupImport
}
'/_authenticate/organization/mcp-endpoint-finalize': {
id: '/_authenticate/organization/mcp-endpoint-finalize'
path: '/organization/mcp-endpoint-finalize'
fullPath: '/organization/mcp-endpoint-finalize'
preLoaderRoute: typeof organizationMcpEndpointFinalizePageRouteImport
parentRoute: typeof middlewaresAuthenticateImport
}
'/_authenticate/organizations/none': {
id: '/_authenticate/organizations/none'
path: '/organizations/none'
@@ -5219,6 +5234,7 @@ interface middlewaresAuthenticateRouteChildren {
authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
organizationMcpEndpointFinalizePageRouteRoute: typeof organizationMcpEndpointFinalizePageRouteRoute
organizationNoOrgPageRouteRoute: typeof organizationNoOrgPageRouteRoute
}
@@ -5229,6 +5245,8 @@ const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren
middlewaresInjectOrgDetailsRouteWithChildren,
AuthenticatePersonalSettingsRoute:
AuthenticatePersonalSettingsRouteWithChildren,
organizationMcpEndpointFinalizePageRouteRoute:
organizationMcpEndpointFinalizePageRouteRoute,
organizationNoOrgPageRouteRoute: organizationNoOrgPageRouteRoute,
}
@@ -5324,6 +5342,7 @@ export interface FileRoutesByFullPath {
'/signup': typeof RestrictLoginSignupSignupRouteWithChildren
'/login/': typeof authLoginPageRouteRoute
'/signup/': typeof authSignUpPageRouteRoute
'/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute
'/organizations/none': typeof organizationNoOrgPageRouteRoute
'/admin/signup': typeof adminSignUpPageRouteRoute
'/login/admin': typeof authAdminLoginPageRouteRoute
@@ -5576,6 +5595,7 @@ export interface FileRoutesByTo {
'/personal-settings': typeof userPersonalSettingsPageRouteRoute
'/login': typeof authLoginPageRouteRoute
'/signup': typeof authSignUpPageRouteRoute
'/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute
'/organizations/none': typeof organizationNoOrgPageRouteRoute
'/admin/signup': typeof adminSignUpPageRouteRoute
'/login/admin': typeof authAdminLoginPageRouteRoute
@@ -5820,6 +5840,7 @@ export interface FileRoutesById {
'/_restrict-login-signup/signup': typeof RestrictLoginSignupSignupRouteWithChildren
'/_restrict-login-signup/login/': typeof authLoginPageRouteRoute
'/_restrict-login-signup/signup/': typeof authSignUpPageRouteRoute
'/_authenticate/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute
'/_authenticate/organizations/none': typeof organizationNoOrgPageRouteRoute
'/_restrict-login-signup/admin/signup': typeof adminSignUpPageRouteRoute
'/_restrict-login-signup/login/admin': typeof authAdminLoginPageRouteRoute
@@ -6086,6 +6107,7 @@ export interface FileRouteTypes {
| '/signup'
| '/login/'
| '/signup/'
| '/organization/mcp-endpoint-finalize'
| '/organizations/none'
| '/admin/signup'
| '/login/admin'
@@ -6337,6 +6359,7 @@ export interface FileRouteTypes {
| '/personal-settings'
| '/login'
| '/signup'
| '/organization/mcp-endpoint-finalize'
| '/organizations/none'
| '/admin/signup'
| '/login/admin'
@@ -6579,6 +6602,7 @@ export interface FileRouteTypes {
| '/_restrict-login-signup/signup'
| '/_restrict-login-signup/login/'
| '/_restrict-login-signup/signup/'
| '/_authenticate/organization/mcp-endpoint-finalize'
| '/_authenticate/organizations/none'
| '/_restrict-login-signup/admin/signup'
| '/_restrict-login-signup/login/admin'
@@ -6890,6 +6914,7 @@ export const routeTree = rootRoute
"/_authenticate/password-setup",
"/_authenticate/_inject-org-details",
"/_authenticate/personal-settings",
"/_authenticate/organization/mcp-endpoint-finalize",
"/_authenticate/organizations/none"
]
},
@@ -6976,6 +7001,10 @@ export const routeTree = rootRoute
"filePath": "auth/SignUpPage/route.tsx",
"parent": "/_restrict-login-signup/signup"
},
"/_authenticate/organization/mcp-endpoint-finalize": {
"filePath": "organization/McpEndpointFinalizePage/route.tsx",
"parent": "/_authenticate"
},
"/_authenticate/organizations/none": {
"filePath": "organization/NoOrgPage/route.tsx",
"parent": "/_authenticate"

View File

@@ -445,6 +445,7 @@ export const routes = rootRoute("root.tsx", [
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
]),
route("/organizations/none", "organization/NoOrgPage/route.tsx"),
route("/organization/mcp-endpoint-finalize", "organization/McpEndpointFinalizePage/route.tsx"),
middleware("inject-org-details.tsx", [
route("/organization/$", "redirects/organization-redirect.tsx"),
route("/projects/$", "redirects/project-redirect.tsx"),

View File

@@ -11,7 +11,7 @@ server {
location ~ ^/(api|secret-scanning/webhooks) {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
@@ -24,7 +24,7 @@ server {
location /runtime-ui-env.js {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
@@ -39,7 +39,7 @@ server {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
@@ -49,7 +49,7 @@ server {
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /.well-known/est {
location ~ /\.well-known {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -70,13 +70,13 @@ server {
location / {
include /etc/nginx/mime.types;
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";