* Establish a persistent connection to an MCP server. * If the server supports `listChanged`, the connection is kept alive * and notifications are forwarded to subscribers. * * If the server does NOT support `listChanged`, the client is disconnected * immediately — there's nothing to
(
config: McpServerConfig,
userId: string,
workspaceId: string,
resolvedIP?: string | null
)
| 107 | * immediately — there's nothing to listen for. |
| 108 | */ |
| 109 | async connect( |
| 110 | config: McpServerConfig, |
| 111 | userId: string, |
| 112 | workspaceId: string, |
| 113 | resolvedIP?: string | null |
| 114 | ): Promise<{ supportsListChanged: boolean }> { |
| 115 | if (this.disposed) { |
| 116 | logger.warn('Connection manager is disposed, ignoring connect request') |
| 117 | return { supportsListChanged: false } |
| 118 | } |
| 119 | |
| 120 | const key = connectionKey(config) |
| 121 | |
| 122 | if (this.connections.has(key) || this.connectingServers.has(key)) { |
| 123 | logger.info(`[${config.name}] Already has a managed connection or is connecting, skipping`) |
| 124 | const state = this.states.get(key) |
| 125 | return { supportsListChanged: state?.supportsListChanged ?? false } |
| 126 | } |
| 127 | |
| 128 | if (this.connections.size >= MAX_CONNECTIONS) { |
| 129 | logger.warn(`Max connections (${MAX_CONNECTIONS}) reached, cannot connect to ${config.name}`) |
| 130 | return { supportsListChanged: false } |
| 131 | } |
| 132 | |
| 133 | this.connectingServers.add(key) |
| 134 | |
| 135 | try { |
| 136 | const onToolsChanged: McpToolsChangedCallback = () => { |
| 137 | this.handleToolsChanged(key) |
| 138 | } |
| 139 | |
| 140 | let authProvider: McpClientOptions['authProvider'] |
| 141 | if (config.authType === 'oauth') { |
| 142 | const row = await getOrCreateOauthRow({ |
| 143 | mcpServerId: config.id, |
| 144 | userId, |
| 145 | workspaceId, |
| 146 | }) |
| 147 | if (!row.tokens) { |
| 148 | logger.info( |
| 149 | `[${config.name}] OAuth server has no workspace tokens — skipping persistent connection until authorized` |
| 150 | ) |
| 151 | return { supportsListChanged: false } |
| 152 | } |
| 153 | const preregistered = await loadPreregisteredClient(config.id) |
| 154 | authProvider = new SimMcpOauthProvider({ row, preregistered }) |
| 155 | } |
| 156 | |
| 157 | const client = new McpClient({ |
| 158 | config, |
| 159 | securityPolicy: { |
| 160 | requireConsent: false, |
| 161 | auditLevel: 'basic', |
| 162 | maxToolExecutionsPerHour: 1000, |
| 163 | }, |
| 164 | onToolsChanged, |
| 165 | resolvedIP: resolvedIP ?? undefined, |
| 166 | authProvider, |
no test coverage detected