0,
- tools: server.toolInfo.map((tool) => ({
- name: tool.name,
- checked: allowedTools.includes(tool.name),
- description: tool.description,
- })),
+ tools:
+ server.toolInfo?.map((tool) => ({
+ name: tool.name,
+ checked: allowedTools.includes(tool.name),
+ description: tool.description,
+ })) ?? [],
error: server.error,
status: server.status,
+ perUserAuth: server.perUserAuth,
+ isAuthorized: server.isAuthorized,
};
});
}, [mcpServerList, allowedMcpServers]);
@@ -653,7 +656,10 @@ function McpServerSelector() {
className="flex items-center gap-2 font-semibold cursor-pointer"
icon={
- {server.status === "authorizing" ? (
+ {server.status === "authorizing" ||
+ (server.perUserAuth &&
+ !server.isAuthorized &&
+ server.status === "disconnected") ? (
@@ -696,7 +702,12 @@ function McpServerSelector() {
{
@@ -818,8 +829,12 @@ function McpServerToolSelector({
{filteredTools.length === 0 ? (
-
- {t("noResults")}
+
+ {isAuthorizing ? (
+
Authorize to see available tools.
+ ) : (
+
{t("noResults")}
+ )}
) : (
filteredTools.map((tool) => (
diff --git a/src/components/workflow/node-config/tool-node-config.tsx b/src/components/workflow/node-config/tool-node-config.tsx
index b083b928e..bd8a3baee 100644
--- a/src/components/workflow/node-config/tool-node-config.tsx
+++ b/src/components/workflow/node-config/tool-node-config.tsx
@@ -51,7 +51,7 @@ export const ToolNodeDataConfig = memo(function ({
const toolList = useMemo
(() => {
const mcpTools: WorkflowToolKey[] = (mcpList || []).flatMap((mcp) => {
- return mcp.toolInfo.map((tool) => {
+ return (mcp.toolInfo || []).map((tool) => {
return {
type: "mcp-tool",
serverId: mcp.id,
diff --git a/src/lib/ai/mcp/create-mcp-client.ts b/src/lib/ai/mcp/create-mcp-client.ts
index c537efbba..692f597a0 100644
--- a/src/lib/ai/mcp/create-mcp-client.ts
+++ b/src/lib/ai/mcp/create-mcp-client.ts
@@ -31,6 +31,9 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
type ClientOptions = {
autoDisconnectSeconds?: number;
+ userId?: string;
+ perUserAuth?: boolean;
+ onToolInfoUpdate?: (toolInfo: MCPToolInfo[]) => void;
};
const CONNET_TIMEOUT = IS_VERCEL_ENV ? 30000 : 120000;
@@ -68,6 +71,9 @@ export class MCPClient {
`[${this.id.slice(0, 4)}] MCP Client ${this.name}: `,
),
});
+ if (this.options.perUserAuth) {
+ this.needOauthProvider = true;
+ }
}
get status() {
@@ -119,6 +125,7 @@ export class MCPClient {
toolInfo: this.toolInfo,
visibility: "private" as const,
enabled: true,
+ perUserAuth: this.options.perUserAuth ?? false,
userId: "", // This will be filled by the manager
};
}
@@ -135,6 +142,7 @@ export class MCPClient {
this.oauthProvider = new PgOAuthClientProvider({
name: this.name,
mcpServerId: this.id,
+ userId: this.options.userId,
serverUrl: this.serverConfig.url,
state: oauthState,
_clientMetadata: {
@@ -349,6 +357,7 @@ export class MCPClient {
inputSchema: tool.inputSchema,
}) as MCPToolInfo,
);
+ this.options.onToolInfoUpdate?.(this.toolInfo);
}
}
@@ -356,14 +365,28 @@ export class MCPClient {
const id = generateUUID();
this.inProgressToolCallIds.push(id);
const execute = async () => {
- const client = await this.connect();
- if (this.status === "authorizing") {
- throw new Error("OAuth authorization required. Try Refresh MCP Client");
+ try {
+ const client = await this.connect();
+ return await client?.callTool({
+ name: toolName,
+ arguments: input as Record,
+ });
+ } catch (err) {
+ if (this.status === "authorizing") {
+ return {
+ isError: true,
+ content: [
+ {
+ type: "text",
+ text: "OAuth authorization required",
+ },
+ ],
+ _mcpAuthRequired: true,
+ _mcpServerId: this.id,
+ };
+ }
+ throw err;
}
- return client?.callTool({
- name: toolName,
- arguments: input as Record,
- });
};
return safe(() => this.logger.info("tool call", toolName))
.ifOk(() => this.scheduleAutoDisconnect()) // disconnect if autoDisconnectSeconds is set
diff --git a/src/lib/ai/mcp/create-mcp-clients-manager.ts b/src/lib/ai/mcp/create-mcp-clients-manager.ts
index a15667240..cee83fbff 100644
--- a/src/lib/ai/mcp/create-mcp-clients-manager.ts
+++ b/src/lib/ai/mcp/create-mcp-clients-manager.ts
@@ -14,7 +14,6 @@ import {
toAny,
} from "lib/utils";
import { safe } from "ts-safe";
-import { McpServerTable } from "lib/db/pg/schema.pg";
import { createMCPToolId } from "./mcp-tool-id";
import globalLogger from "logger";
import { jsonSchema, ToolCallOptions } from "ai";
@@ -109,64 +108,89 @@ export class MCPClientsManager {
/**
* Returns all tools from all clients as a flat object
*/
- async tools(): Promise> {
+ async tools(userId?: string): Promise> {
await this.waitInitialized();
- return Array.from(this.clients.entries()).reduce(
- (acc, [id, client]) => {
- if (!client.client?.toolInfo?.length) return acc;
- const clientName = client.name;
- return {
- ...acc,
- ...client.client.toolInfo.reduce(
- (bcc, tool) => {
- return {
- ...bcc,
- [createMCPToolId(clientName, tool.name)]:
- VercelAIMcpToolTag.create({
- description: tool.description,
- inputSchema: jsonSchema(
- toAny({
- ...tool.inputSchema,
- properties: tool.inputSchema?.properties ?? {},
- additionalProperties: false,
- }),
- ),
- _originToolName: tool.name,
- _mcpServerName: clientName,
- _mcpServerId: id,
- execute: (params, options: ToolCallOptions) => {
- options?.abortSignal?.throwIfAborted();
- return this.toolCall(id, tool.name, params);
- },
- }),
- };
+ const configs = await this.storage.loadAll();
+
+ const tools: Record = {};
+
+ for (const config of configs) {
+ const { id, name, toolInfo: storedToolInfo, perUserAuth } = config;
+ const clientId = perUserAuth && userId ? `${id}:${userId}` : id;
+ const client = this.clients.get(clientId);
+
+ const toolInfo =
+ client?.client?.toolInfo && client.client.toolInfo.length > 0
+ ? client.client.toolInfo
+ : storedToolInfo || [];
+
+ if (!toolInfo.length) continue;
+
+ const clientName = name;
+ for (const tool of toolInfo) {
+ tools[createMCPToolId(clientName, tool.name)] =
+ VercelAIMcpToolTag.create({
+ description: tool.description,
+ inputSchema: jsonSchema(
+ toAny({
+ ...tool.inputSchema,
+ properties: tool.inputSchema?.properties ?? {},
+ additionalProperties: false,
+ }),
+ ),
+ _originToolName: tool.name,
+ _mcpServerName: clientName,
+ _mcpServerId: id,
+ execute: (params, options: ToolCallOptions) => {
+ options?.abortSignal?.throwIfAborted();
+ return this.toolCall(id, tool.name, params, userId);
},
- {} as Record,
- ),
- };
- },
- {} as Record,
- );
+ });
+ }
+ }
+
+ return tools;
}
/**
* Creates and adds a new client instance to memory only (no storage persistence)
*/
- async addClient(id: string, name: string, serverConfig: MCPServerConfig) {
- if (this.clients.has(id)) {
- const prevClient = this.clients.get(id)!;
+ async addClient(
+ id: string,
+ name: string,
+ serverConfig: MCPServerConfig,
+ userId?: string,
+ ) {
+ const server = await this.storage.get(id);
+ const clientId = await this.getClientId(id, userId);
+ if (this.clients.has(clientId)) {
+ const prevClient = this.clients.get(clientId)!;
void prevClient.client.disconnect();
}
const client = createMCPClient(id, name, serverConfig, {
autoDisconnectSeconds: this.autoDisconnectSeconds,
+ userId,
+ perUserAuth: server?.perUserAuth ?? false,
+ onToolInfoUpdate: (toolInfo) => {
+ // Only update storage if it's the main client (not per-user)
+ // or if we want to share tool info across users
+ void this.storage.get(id).then((server) => {
+ if (server) {
+ void this.storage.save({
+ ...server,
+ toolInfo,
+ });
+ }
+ });
+ },
});
- this.clients.set(id, { client, name });
+ this.clients.set(clientId, { client, name });
return client.connect();
}
/**
* Persists a new client configuration to storage and adds the client instance to memory
*/
- async persistClient(server: typeof McpServerTable.$inferInsert) {
+ async persistClient(server: McpServerInsert) {
let id = server.name;
if (this.storage) {
const entity = await this.storage.save(server);
@@ -205,15 +229,18 @@ export class MCPClientsManager {
/**
* Refreshes an existing client with a new configuration or its existing config
*/
- async refreshClient(id: string) {
+ async refreshClient(id: string, userId?: string) {
await this.waitInitialized();
const server = await this.storage.get(id);
if (!server) {
throw new Error(`Client ${id} not found`);
}
- this.logger.info(`Refreshing client ${server.name}`);
- await this.addClient(id, server.name, server.config);
- return this.clients.get(id)!;
+ this.logger.info(
+ `Refreshing client ${server.name}${userId ? ` for user ${userId}` : ""}`,
+ );
+ await this.addClient(id, server.name, server.config, userId);
+ const clientId = await this.getClientId(id, userId);
+ return this.clients.get(clientId)!;
}
async cleanup() {
@@ -222,43 +249,77 @@ export class MCPClientsManager {
await Promise.allSettled(clients.map(({ client }) => client.disconnect()));
}
- async getClients() {
+ async getClients(userId?: string) {
await this.waitInitialized();
- return Array.from(this.clients.entries()).map(([id, { client }]) => ({
- id,
- client: client,
- }));
+ const configs = await this.storage.loadAll();
+ const result: {
+ id: string;
+ clientId: string;
+ client: MCPClient;
+ name: string;
+ }[] = [];
+
+ for (const config of configs) {
+ const clientId =
+ config.perUserAuth && userId ? `${config.id}:${userId}` : config.id;
+ const client = this.clients.get(clientId);
+ if (client) {
+ result.push({
+ id: config.id,
+ clientId,
+ client: client.client,
+ name: client.name,
+ });
+ }
+ }
+
+ return result;
}
- async getClient(id: string) {
+
+ private async getClientId(id: string, userId?: string) {
+ const server = await this.storage.get(id);
+ if (!server) {
+ return id;
+ }
+ return server.perUserAuth && userId ? `${id}:${userId}` : id;
+ }
+
+ async getClient(id: string, userId?: string) {
await this.waitInitialized();
- const client = this.clients.get(id);
+ const server = await this.storage.get(id);
+ if (!server) {
+ throw new Error(`Client ${id} not found`);
+ }
+
+ const clientId = await this.getClientId(id, userId);
+
+ const client = this.clients.get(clientId);
if (!client) {
- await this.refreshClient(id);
+ await this.addClient(id, server.name, server.config, userId);
}
- return this.clients.get(id);
+ return this.clients.get(clientId);
}
async toolCallByServerName(
serverName: string,
toolName: string,
input: unknown,
+ userId?: string,
) {
- const clients = await this.getClients();
- const client = clients.find((c) => c.client.getInfo().name === serverName);
- if (!client) {
- if (this.storage) {
- const servers = await this.storage.loadAll();
- const server = servers.find((s) => s.name === serverName);
- if (server) {
- return this.toolCall(server.id, toolName, input);
- }
- }
+ const configs = await this.storage.loadAll();
+ const server = configs.find((s) => s.name === serverName);
+ if (!server) {
throw new Error(`Client ${serverName} not found`);
}
- return this.toolCall(client.id, toolName, input);
+ return this.toolCall(server.id, toolName, input, userId);
}
- async toolCall(id: string, toolName: string, input: unknown) {
- return safe(() => this.getClient(id))
+ async toolCall(
+ id: string,
+ toolName: string,
+ input: unknown,
+ userId?: string,
+ ) {
+ return safe(() => this.getClient(id, userId))
.map((client) => {
if (!client) throw new Error(`Client ${id} not found`);
return client.client;
diff --git a/src/lib/ai/mcp/fb-mcp-config-storage.ts b/src/lib/ai/mcp/fb-mcp-config-storage.ts
index 5b1d19372..541f34885 100644
--- a/src/lib/ai/mcp/fb-mcp-config-storage.ts
+++ b/src/lib/ai/mcp/fb-mcp-config-storage.ts
@@ -197,6 +197,8 @@ function fillMcpServerTable(
userId: server.userId || "file-based-user",
visibility: server.visibility || "private",
enabled: true,
+ perUserAuth: server.perUserAuth ?? false,
+ toolInfo: server.toolInfo ?? [],
createdAt: new Date(),
updatedAt: new Date(),
};
diff --git a/src/lib/ai/mcp/memory-mcp-config-storage.ts b/src/lib/ai/mcp/memory-mcp-config-storage.ts
index fb9ff2b97..f156f0c40 100644
--- a/src/lib/ai/mcp/memory-mcp-config-storage.ts
+++ b/src/lib/ai/mcp/memory-mcp-config-storage.ts
@@ -29,6 +29,8 @@ export class MemoryMCPConfigStorage implements MCPConfigStorage {
config: server.config,
userId: server.userId || "test-user",
visibility: server.visibility || "private",
+ perUserAuth: server.perUserAuth || false,
+ toolInfo: server.toolInfo || [],
};
this.configs.set(id, savedServer);
return savedServer;
diff --git a/src/lib/ai/mcp/pg-oauth-provider.ts b/src/lib/ai/mcp/pg-oauth-provider.ts
index 1a851c85e..66e07bba4 100644
--- a/src/lib/ai/mcp/pg-oauth-provider.ts
+++ b/src/lib/ai/mcp/pg-oauth-provider.ts
@@ -32,6 +32,7 @@ export class PgOAuthClientProvider implements OAuthClientProvider {
private config: {
name: string;
mcpServerId: string;
+ userId?: string;
serverUrl: string;
_clientMetadata: OAuthClientMetadata;
onRedirectToAuthorization: (authUrl: URL) => Promise;
@@ -62,24 +63,31 @@ export class PgOAuthClientProvider implements OAuthClientProvider {
}
}
// 1. Check for authenticated session first
+ this.logger.info(
+ `Checking for authenticated session: server=${this.config.mcpServerId}, user=${this.config.userId}`,
+ );
const authenticated = await pgMcpOAuthRepository.getAuthenticatedSession(
this.config.mcpServerId,
+ this.config.userId,
);
if (authenticated) {
this.currentOAuthState = authenticated.state || "";
this.cachedAuthData = authenticated;
this.initialized = true;
- this.logger.info("Using existing authenticated session");
+ this.logger.info(
+ `Using existing authenticated session: state=${this.currentOAuthState}`,
+ );
return;
}
- // 2. Always create a new in-progress session when not authenticated
+ this.logger.info("No authenticated session found, creating new one");
this.currentOAuthState = generateUUID();
this.cachedAuthData = await pgMcpOAuthRepository.createSession(
this.config.mcpServerId,
{
state: this.currentOAuthState,
serverUrl: this.config.serverUrl,
+ userId: this.config.userId,
},
);
this.initialized = true;
diff --git a/src/lib/db/migrations/pg/0015_abnormal_titania.sql b/src/lib/db/migrations/pg/0015_abnormal_titania.sql
new file mode 100644
index 000000000..48ad1b742
--- /dev/null
+++ b/src/lib/db/migrations/pg/0015_abnormal_titania.sql
@@ -0,0 +1,7 @@
+DROP INDEX "mcp_oauth_session_tokens_idx";--> statement-breakpoint
+ALTER TABLE "mcp_oauth_session" ADD COLUMN "user_id" uuid;--> statement-breakpoint
+ALTER TABLE "mcp_server" ADD COLUMN "per_user_auth" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "mcp_server" ADD COLUMN "tool_info" json DEFAULT '[]'::json;--> statement-breakpoint
+ALTER TABLE "mcp_oauth_session" ADD CONSTRAINT "mcp_oauth_session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "mcp_oauth_session_user_id_idx" ON "mcp_oauth_session" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "mcp_oauth_session_tokens_idx" ON "mcp_oauth_session" USING btree ("mcp_server_id","user_id") WHERE "mcp_oauth_session"."tokens" is not null;
\ No newline at end of file
diff --git a/src/lib/db/migrations/pg/meta/0015_snapshot.json b/src/lib/db/migrations/pg/meta/0015_snapshot.json
new file mode 100644
index 000000000..7b59edd73
--- /dev/null
+++ b/src/lib/db/migrations/pg/meta/0015_snapshot.json
@@ -0,0 +1,1645 @@
+{
+ "id": "59e0e6d7-7019-440c-9e56-5583b590fe62",
+ "prevId": "38d89506-17d0-44ef-89dd-625725e3bbfd",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent": {
+ "name": "agent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "instructions": {
+ "name": "instructions",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'private'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "agent_user_id_user_id_fk": {
+ "name": "agent_user_id_user_id_fk",
+ "tableFrom": "agent",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.archive_item": {
+ "name": "archive_item",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "archive_id": {
+ "name": "archive_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "archive_item_item_id_idx": {
+ "name": "archive_item_item_id_idx",
+ "columns": [
+ {
+ "expression": "item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "archive_item_archive_id_archive_id_fk": {
+ "name": "archive_item_archive_id_archive_id_fk",
+ "tableFrom": "archive_item",
+ "tableTo": "archive",
+ "columnsFrom": ["archive_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "archive_item_user_id_user_id_fk": {
+ "name": "archive_item_user_id_user_id_fk",
+ "tableFrom": "archive_item",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.archive": {
+ "name": "archive",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "archive_user_id_user_id_fk": {
+ "name": "archive_user_id_user_id_fk",
+ "tableFrom": "archive",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.bookmark": {
+ "name": "bookmark",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_type": {
+ "name": "item_type",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "bookmark_user_id_idx": {
+ "name": "bookmark_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmark_item_idx": {
+ "name": "bookmark_item_idx",
+ "columns": [
+ {
+ "expression": "item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "item_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "bookmark_user_id_user_id_fk": {
+ "name": "bookmark_user_id_user_id_fk",
+ "tableFrom": "bookmark",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "bookmark_user_id_item_id_item_type_unique": {
+ "name": "bookmark_user_id_item_id_item_type_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "item_id", "item_type"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat_export_comment": {
+ "name": "chat_export_comment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "export_id": {
+ "name": "export_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "chat_export_comment_export_id_chat_export_id_fk": {
+ "name": "chat_export_comment_export_id_chat_export_id_fk",
+ "tableFrom": "chat_export_comment",
+ "tableTo": "chat_export",
+ "columnsFrom": ["export_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_export_comment_author_id_user_id_fk": {
+ "name": "chat_export_comment_author_id_user_id_fk",
+ "tableFrom": "chat_export_comment",
+ "tableTo": "user",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_export_comment_parent_id_chat_export_comment_id_fk": {
+ "name": "chat_export_comment_parent_id_chat_export_comment_id_fk",
+ "tableFrom": "chat_export_comment",
+ "tableTo": "chat_export_comment",
+ "columnsFrom": ["parent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat_export": {
+ "name": "chat_export",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "exporter_id": {
+ "name": "exporter_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_thread_id": {
+ "name": "original_thread_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "messages": {
+ "name": "messages",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "exported_at": {
+ "name": "exported_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "chat_export_exporter_id_user_id_fk": {
+ "name": "chat_export_exporter_id_user_id_fk",
+ "tableFrom": "chat_export",
+ "tableTo": "user",
+ "columnsFrom": ["exporter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat_message": {
+ "name": "chat_message",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parts": {
+ "name": "parts",
+ "type": "json[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "chat_message_thread_id_chat_thread_id_fk": {
+ "name": "chat_message_thread_id_chat_thread_id_fk",
+ "tableFrom": "chat_message",
+ "tableTo": "chat_thread",
+ "columnsFrom": ["thread_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat_thread": {
+ "name": "chat_thread",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "chat_thread_user_id_user_id_fk": {
+ "name": "chat_thread_user_id_user_id_fk",
+ "tableFrom": "chat_thread",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_oauth_session": {
+ "name": "mcp_oauth_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "mcp_server_id": {
+ "name": "mcp_server_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "server_url": {
+ "name": "server_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_info": {
+ "name": "client_info",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokens": {
+ "name": "tokens",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "code_verifier": {
+ "name": "code_verifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "mcp_oauth_session_server_id_idx": {
+ "name": "mcp_oauth_session_server_id_idx",
+ "columns": [
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_oauth_session_user_id_idx": {
+ "name": "mcp_oauth_session_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_oauth_session_state_idx": {
+ "name": "mcp_oauth_session_state_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_oauth_session_tokens_idx": {
+ "name": "mcp_oauth_session_tokens_idx",
+ "columns": [
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"mcp_oauth_session\".\"tokens\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_oauth_session_mcp_server_id_mcp_server_id_fk": {
+ "name": "mcp_oauth_session_mcp_server_id_mcp_server_id_fk",
+ "tableFrom": "mcp_oauth_session",
+ "tableTo": "mcp_server",
+ "columnsFrom": ["mcp_server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_oauth_session_user_id_user_id_fk": {
+ "name": "mcp_oauth_session_user_id_user_id_fk",
+ "tableFrom": "mcp_oauth_session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mcp_oauth_session_state_unique": {
+ "name": "mcp_oauth_session_state_unique",
+ "nullsNotDistinct": false,
+ "columns": ["state"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_server_custom_instructions": {
+ "name": "mcp_server_custom_instructions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mcp_server_id": {
+ "name": "mcp_server_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mcp_server_custom_instructions_user_id_user_id_fk": {
+ "name": "mcp_server_custom_instructions_user_id_user_id_fk",
+ "tableFrom": "mcp_server_custom_instructions",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_server_custom_instructions_mcp_server_id_mcp_server_id_fk": {
+ "name": "mcp_server_custom_instructions_mcp_server_id_mcp_server_id_fk",
+ "tableFrom": "mcp_server_custom_instructions",
+ "tableTo": "mcp_server",
+ "columnsFrom": ["mcp_server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mcp_server_custom_instructions_user_id_mcp_server_id_unique": {
+ "name": "mcp_server_custom_instructions_user_id_mcp_server_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "mcp_server_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_server": {
+ "name": "mcp_server",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "per_user_auth": {
+ "name": "per_user_auth",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'private'"
+ },
+ "tool_info": {
+ "name": "tool_info",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'::json"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mcp_server_user_id_user_id_fk": {
+ "name": "mcp_server_user_id_user_id_fk",
+ "tableFrom": "mcp_server",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_server_tool_custom_instructions": {
+ "name": "mcp_server_tool_custom_instructions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_name": {
+ "name": "tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mcp_server_id": {
+ "name": "mcp_server_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mcp_server_tool_custom_instructions_user_id_user_id_fk": {
+ "name": "mcp_server_tool_custom_instructions_user_id_user_id_fk",
+ "tableFrom": "mcp_server_tool_custom_instructions",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_server_tool_custom_instructions_mcp_server_id_mcp_server_id_fk": {
+ "name": "mcp_server_tool_custom_instructions_mcp_server_id_mcp_server_id_fk",
+ "tableFrom": "mcp_server_tool_custom_instructions",
+ "tableTo": "mcp_server",
+ "columnsFrom": ["mcp_server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mcp_server_tool_custom_instructions_user_id_tool_name_mcp_server_id_unique": {
+ "name": "mcp_server_tool_custom_instructions_user_id_tool_name_mcp_server_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "tool_name", "mcp_server_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preferences": {
+ "name": "preferences",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::json"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_edge": {
+ "name": "workflow_edge",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0.1.0'"
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target": {
+ "name": "target",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ui_config": {
+ "name": "ui_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::json"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workflow_edge_workflow_id_workflow_id_fk": {
+ "name": "workflow_edge_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_edge",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edge_source_workflow_node_id_fk": {
+ "name": "workflow_edge_source_workflow_node_id_fk",
+ "tableFrom": "workflow_edge",
+ "tableTo": "workflow_node",
+ "columnsFrom": ["source"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edge_target_workflow_node_id_fk": {
+ "name": "workflow_edge_target_workflow_node_id_fk",
+ "tableFrom": "workflow_edge",
+ "tableTo": "workflow_node",
+ "columnsFrom": ["target"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_node": {
+ "name": "workflow_node",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0.1.0'"
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ui_config": {
+ "name": "ui_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::json"
+ },
+ "node_config": {
+ "name": "node_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::json"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "workflow_node_kind_idx": {
+ "name": "workflow_node_kind_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_node_workflow_id_workflow_id_fk": {
+ "name": "workflow_node_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_node",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow": {
+ "name": "workflow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0.1.0'"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_published": {
+ "name": "is_published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'private'"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workflow_user_id_user_id_fk": {
+ "name": "workflow_user_id_user_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/src/lib/db/migrations/pg/meta/_journal.json b/src/lib/db/migrations/pg/meta/_journal.json
index 3a5bc3a85..3c936b806 100644
--- a/src/lib/db/migrations/pg/meta/_journal.json
+++ b/src/lib/db/migrations/pg/meta/_journal.json
@@ -106,6 +106,13 @@
"when": 1759110840795,
"tag": "0014_faulty_gateway",
"breakpoints": true
+ },
+ {
+ "idx": 15,
+ "version": "7",
+ "when": 1766468535006,
+ "tag": "0015_abnormal_titania",
+ "breakpoints": true
}
]
}
diff --git a/src/lib/db/pg/repositories/mcp-oauth-repository.pg.ts b/src/lib/db/pg/repositories/mcp-oauth-repository.pg.ts
index 930573a44..d76ae3c4d 100644
--- a/src/lib/db/pg/repositories/mcp-oauth-repository.pg.ts
+++ b/src/lib/db/pg/repositories/mcp-oauth-repository.pg.ts
@@ -8,13 +8,16 @@ export const pgMcpOAuthRepository: McpOAuthRepository = {
// 1. Query methods
// Get session with valid tokens (authenticated)
- getAuthenticatedSession: async (mcpServerId) => {
+ getAuthenticatedSession: async (mcpServerId, userId) => {
const [session] = await db
.select()
.from(McpOAuthSessionTable)
.where(
and(
eq(McpOAuthSessionTable.mcpServerId, mcpServerId),
+ userId
+ ? eq(McpOAuthSessionTable.userId, userId)
+ : isNull(McpOAuthSessionTable.userId),
isNotNull(McpOAuthSessionTable.tokens),
),
)
@@ -85,15 +88,20 @@ export const pgMcpOAuthRepository: McpOAuthRepository = {
.where(eq(McpOAuthSessionTable.state, state))
.returning();
- await db
- .delete(McpOAuthSessionTable)
- .where(
- and(
- eq(McpOAuthSessionTable.mcpServerId, mcpServerId),
- isNull(McpOAuthSessionTable.tokens),
- ne(McpOAuthSessionTable.state, state),
- ),
- );
+ if (session) {
+ await db
+ .delete(McpOAuthSessionTable)
+ .where(
+ and(
+ eq(McpOAuthSessionTable.mcpServerId, mcpServerId),
+ session.userId
+ ? eq(McpOAuthSessionTable.userId, session.userId)
+ : isNull(McpOAuthSessionTable.userId),
+ isNull(McpOAuthSessionTable.tokens),
+ ne(McpOAuthSessionTable.state, state),
+ ),
+ );
+ }
return session as McpOAuthSession;
},
diff --git a/src/lib/db/pg/repositories/mcp-repository.pg.ts b/src/lib/db/pg/repositories/mcp-repository.pg.ts
index 8bcba97e3..d0d303db1 100644
--- a/src/lib/db/pg/repositories/mcp-repository.pg.ts
+++ b/src/lib/db/pg/repositories/mcp-repository.pg.ts
@@ -14,6 +14,8 @@ export const pgMcpRepository: MCPRepository = {
config: server.config,
userId: server.userId,
visibility: server.visibility ?? "private",
+ perUserAuth: server.perUserAuth ?? false,
+ toolInfo: server.toolInfo ?? [],
enabled: true,
createdAt: new Date(),
updatedAt: new Date(),
@@ -22,6 +24,8 @@ export const pgMcpRepository: MCPRepository = {
target: [McpServerTable.id],
set: {
config: server.config,
+ perUserAuth: server.perUserAuth,
+ toolInfo: server.toolInfo,
updatedAt: new Date(),
},
})
@@ -51,6 +55,8 @@ export const pgMcpRepository: MCPRepository = {
name: McpServerTable.name,
config: McpServerTable.config,
enabled: McpServerTable.enabled,
+ perUserAuth: McpServerTable.perUserAuth,
+ toolInfo: McpServerTable.toolInfo,
userId: McpServerTable.userId,
visibility: McpServerTable.visibility,
createdAt: McpServerTable.createdAt,
@@ -77,6 +83,13 @@ export const pgMcpRepository: MCPRepository = {
.where(eq(McpServerTable.id, id));
},
+ async updatePerUserAuth(id, perUserAuth) {
+ await db
+ .update(McpServerTable)
+ .set({ perUserAuth, updatedAt: new Date() })
+ .where(eq(McpServerTable.id, id));
+ },
+
async deleteById(id) {
await db.delete(McpServerTable).where(eq(McpServerTable.id, id));
},
diff --git a/src/lib/db/pg/schema.pg.ts b/src/lib/db/pg/schema.pg.ts
index 5c2e753b9..bcce1db90 100644
--- a/src/lib/db/pg/schema.pg.ts
+++ b/src/lib/db/pg/schema.pg.ts
@@ -1,6 +1,6 @@
import { Agent } from "app-types/agent";
import { UserPreferences } from "app-types/user";
-import { MCPServerConfig } from "app-types/mcp";
+import { MCPServerConfig, MCPToolInfo } from "app-types/mcp";
import { sql } from "drizzle-orm";
import {
pgTable,
@@ -84,6 +84,7 @@ export const McpServerTable = pgTable("mcp_server", {
name: text("name").notNull(),
config: json("config").notNull().$type(),
enabled: boolean("enabled").notNull().default(true),
+ perUserAuth: boolean("per_user_auth").notNull().default(false),
userId: uuid("user_id")
.notNull()
.references(() => UserTable.id, { onDelete: "cascade" }),
@@ -92,6 +93,7 @@ export const McpServerTable = pgTable("mcp_server", {
})
.notNull()
.default("private"),
+ toolInfo: json("tool_info").$type().default([]),
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
@@ -299,6 +301,9 @@ export const McpOAuthSessionTable = pgTable(
mcpServerId: uuid("mcp_server_id")
.notNull()
.references(() => McpServerTable.id, { onDelete: "cascade" }),
+ userId: uuid("user_id").references(() => UserTable.id, {
+ onDelete: "cascade",
+ }),
serverUrl: text("server_url").notNull(),
clientInfo: json("client_info"),
tokens: json("tokens"),
@@ -313,10 +318,11 @@ export const McpOAuthSessionTable = pgTable(
},
(t) => [
index("mcp_oauth_session_server_id_idx").on(t.mcpServerId),
+ index("mcp_oauth_session_user_id_idx").on(t.userId),
index("mcp_oauth_session_state_idx").on(t.state),
// Partial index for sessions with tokens for better performance
index("mcp_oauth_session_tokens_idx")
- .on(t.mcpServerId)
+ .on(t.mcpServerId, t.userId)
.where(isNotNull(t.tokens)),
],
);
diff --git a/src/types/mcp.ts b/src/types/mcp.ts
index 4b0bb6502..2cfa4e223 100644
--- a/src/types/mcp.ts
+++ b/src/types/mcp.ts
@@ -46,9 +46,11 @@ export type MCPServerInfo = {
visibility: "public" | "private";
error?: unknown;
enabled: boolean;
+ perUserAuth: boolean;
+ isAuthorized?: boolean;
userId: string;
status: "connected" | "disconnected" | "loading" | "authorizing";
- toolInfo: MCPToolInfo[];
+ toolInfo?: MCPToolInfo[];
createdAt?: Date | string;
updatedAt?: Date | string;
userName?: string | null;
@@ -68,6 +70,8 @@ export type McpServerInsert = {
id?: string;
userId: string;
visibility?: "public" | "private";
+ perUserAuth?: boolean;
+ toolInfo?: MCPToolInfo[];
};
export type McpServerSelect = {
name: string;
@@ -75,6 +79,8 @@ export type McpServerSelect = {
id: string;
userId: string;
visibility: "public" | "private";
+ perUserAuth: boolean;
+ toolInfo?: MCPToolInfo[] | null;
};
export type VercelAIMcpTool = Tool & {
@@ -94,6 +100,7 @@ export interface MCPRepository {
deleteById(id: string): Promise;
existsByServerName(name: string): Promise;
updateVisibility(id: string, visibility: "public" | "private"): Promise;
+ updatePerUserAuth(id: string, perUserAuth: boolean): Promise;
}
export const McpToolCustomizationZodSchema = z.object({
@@ -234,6 +241,8 @@ export const CallToolResultSchema = z.object({
content: z.array(ContentUnion).default([]),
structuredContent: z.object({}).passthrough().optional(),
isError: z.boolean().optional(),
+ _mcpAuthRequired: z.boolean().optional(),
+ _mcpServerId: z.string().optional(),
});
export type CallToolResult = z.infer;
@@ -241,6 +250,7 @@ export type CallToolResult = z.infer;
export type McpOAuthSession = {
id: string;
mcpServerId: string;
+ userId?: string | null;
serverUrl: string;
clientInfo?: OAuthClientInformationFull;
tokens?: OAuthTokens;
@@ -256,6 +266,7 @@ export type McpOAuthRepository = {
// Get session with valid tokens (authenticated)
getAuthenticatedSession(
mcpServerId: string,
+ userId?: string,
): Promise;
// Get session by OAuth state (for callback handling)