SurfSense/surfsense_web/lib/apis/connectors-api.service.ts
CREDO23 1f1b6c5425 hitl/generic-approval: drop client-side MCP gate, dispatch approve_always
The 'Always Allow' button is now driven entirely by the server-supplied
allowed_decisions palette. The card no longer peeks at
context.mcp_connector_id to decide whether to render the button, and no
longer fires a separate trust-tool HTTP call on click - one
{type: 'approve_always'} dispatch is enough; the agent middleware
handles the in-memory promotion and (for MCP tools) the database save
via its trusted_tool_saver callback.

Drops the dead trustMCPTool / untrustMCPTool service helpers - they had
no remaining callers after this rework. The backing HTTP routes are
kept on the server as a programmatic surface.
2026-05-15 14:59:45 +02:00

424 lines
12 KiB
TypeScript

import {
type CreateConnectorRequest,
createConnectorRequest,
createConnectorResponse,
type DeleteConnectorRequest,
type DiscordChannel,
deleteConnectorRequest,
deleteConnectorResponse,
type GetConnectorRequest,
type GetConnectorsRequest,
getConnectorRequest,
getConnectorResponse,
getConnectorsRequest,
getConnectorsResponse,
type IndexConnectorRequest,
indexConnectorRequest,
indexConnectorResponse,
type ListGitHubRepositoriesRequest,
type ListGoogleDriveFoldersRequest,
listDiscordChannelsResponse,
listGitHubRepositoriesRequest,
listGitHubRepositoriesResponse,
listGoogleDriveFoldersRequest,
listGoogleDriveFoldersResponse,
listSlackChannelsResponse,
type SlackChannel,
type UpdateConnectorRequest,
updateConnectorRequest,
updateConnectorResponse,
} from "@/contracts/types/connector.types";
import type {
CreateMCPConnectorRequest,
GetMCPConnectorsRequest,
MCPConnectorRead,
MCPServerConfig,
MCPTestConnectionResponse,
UpdateMCPConnectorRequest,
} from "@/contracts/types/mcp.types";
import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service";
class ConnectorsApiService {
/**
* Get all connectors for a search space
*/
getConnectors = async (request: GetConnectorsRequest) => {
const parsedRequest = getConnectorsRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
// Transform query params to be string values, filtering out undefined/null
const transformedQueryParams = parsedRequest.data.queryParams
? Object.fromEntries(
Object.entries(parsedRequest.data.queryParams)
.filter(([_, v]) => v !== undefined && v !== null)
.map(([k, v]) => {
return [k, String(v)];
})
)
: undefined;
const queryParams = transformedQueryParams
? new URLSearchParams(transformedQueryParams).toString()
: "";
return baseApiService.get(
`/api/v1/search-source-connectors?${queryParams}`,
getConnectorsResponse
);
};
/**
* Get a single connector by ID
*/
getConnector = async (request: GetConnectorRequest) => {
const parsedRequest = getConnectorRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.get(
`/api/v1/search-source-connectors/${request.id}`,
getConnectorResponse
);
};
/**
* Create a new connector
*/
createConnector = async (request: CreateConnectorRequest) => {
const parsedRequest = createConnectorRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { data, queryParams } = parsedRequest.data;
// Transform query params to be string values, filtering out undefined/null
const transformedQueryParams = Object.fromEntries(
Object.entries(queryParams)
.filter(([_, v]) => v !== undefined && v !== null)
.map(([k, v]) => {
return [k, String(v)];
})
);
const queryString = new URLSearchParams(transformedQueryParams).toString();
return baseApiService.post(
`/api/v1/search-source-connectors?${queryString}`,
createConnectorResponse,
{
body: data,
}
);
};
/**
* Update an existing connector
*/
updateConnector = async (request: UpdateConnectorRequest) => {
const parsedRequest = updateConnectorRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { id, data } = parsedRequest.data;
return baseApiService.put(`/api/v1/search-source-connectors/${id}`, updateConnectorResponse, {
body: data,
});
};
/**
* Delete a connector
*/
deleteConnector = async (request: DeleteConnectorRequest) => {
const parsedRequest = deleteConnectorRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.delete(
`/api/v1/search-source-connectors/${request.id}`,
deleteConnectorResponse
);
};
/**
* Index connector content
*/
indexConnector = async (request: IndexConnectorRequest) => {
const parsedRequest = indexConnectorRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { connector_id, queryParams, body } = parsedRequest.data;
// Transform query params to be string values, filtering out undefined/null
const transformedQueryParams = Object.fromEntries(
Object.entries(queryParams)
.filter(([_, v]) => v !== undefined && v !== null)
.map(([k, v]) => {
return [k, String(v)];
})
);
const queryString = new URLSearchParams(transformedQueryParams).toString();
return baseApiService.post(
`/api/v1/search-source-connectors/${connector_id}/index?${queryString}`,
indexConnectorResponse,
{
body: body || {},
}
);
};
/**
* List GitHub repositories using a Personal Access Token
*/
listGitHubRepositories = async (request: ListGitHubRepositoriesRequest) => {
const parsedRequest = listGitHubRepositoriesRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.post(`/api/v1/github/repositories`, listGitHubRepositoriesResponse, {
body: parsedRequest.data,
});
};
/**
* List Google Drive folders and files
*/
listGoogleDriveFolders = async (request: ListGoogleDriveFoldersRequest) => {
const parsedRequest = listGoogleDriveFoldersRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { connector_id, parent_id } = parsedRequest.data;
const queryParams = parent_id ? `?parent_id=${encodeURIComponent(parent_id)}` : "";
return baseApiService.get(
`/api/v1/connectors/${connector_id}/google-drive/folders${queryParams}`,
listGoogleDriveFoldersResponse
);
};
/**
* List Composio Google Drive folders and files
*/
listComposioDriveFolders = async (request: ListGoogleDriveFoldersRequest) => {
const parsedRequest = listGoogleDriveFoldersRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { connector_id, parent_id } = parsedRequest.data;
const queryParams = parent_id ? `?parent_id=${encodeURIComponent(parent_id)}` : "";
return baseApiService.get(
`/api/v1/connectors/${connector_id}/composio-drive/folders${queryParams}`,
listGoogleDriveFoldersResponse
);
};
/**
* Get Google Picker token (access_token + client_id + picker_api_key) for a Drive connector
*/
getDrivePickerToken = async (connectorId: number) => {
return baseApiService.get<{
access_token: string;
client_id: string;
picker_api_key: string | null;
}>(`/api/v1/connectors/${connectorId}/drive-picker-token`);
};
/**
* List OneDrive folders and files
*/
listOneDriveFolders = async (request: { connector_id: number; parent_id?: string }) => {
const queryParams = request.parent_id
? `?parent_id=${encodeURIComponent(request.parent_id)}`
: "";
return baseApiService.get(
`/api/v1/connectors/${request.connector_id}/onedrive/folders${queryParams}`,
listGoogleDriveFoldersResponse
);
};
/**
* List Dropbox folders and files
*/
listDropboxFolders = async (request: { connector_id: number; parent_path?: string }) => {
const queryParams = request.parent_path
? `?parent_path=${encodeURIComponent(request.parent_path)}`
: "";
return baseApiService.get(
`/api/v1/connectors/${request.connector_id}/dropbox/folders${queryParams}`,
listGoogleDriveFoldersResponse
);
};
// =============================================================================
// MCP Connector Methods
// =============================================================================
/**
* Get all MCP connectors for a search space
*/
getMCPConnectors = async (request: GetMCPConnectorsRequest) => {
const { search_space_id } = request.queryParams;
const queryString = new URLSearchParams({
search_space_id: String(search_space_id),
}).toString();
return baseApiService.get<MCPConnectorRead[]>(`/api/v1/connectors/mcp?${queryString}`);
};
/**
* Get a single MCP connector by ID
*/
getMCPConnector = async (connectorId: number) => {
return baseApiService.get<MCPConnectorRead>(`/api/v1/connectors/mcp/${connectorId}`);
};
/**
* Create a new MCP connector
*/
createMCPConnector = async (request: CreateMCPConnectorRequest) => {
const { data, queryParams } = request;
const queryString = new URLSearchParams({
search_space_id: String(queryParams.search_space_id),
}).toString();
return baseApiService.post<MCPConnectorRead>(
`/api/v1/connectors/mcp?${queryString}`,
undefined,
{
body: data,
}
);
};
/**
* Update an existing MCP connector
*/
updateMCPConnector = async (request: UpdateMCPConnectorRequest) => {
const { id, data } = request;
return baseApiService.put<MCPConnectorRead>(`/api/v1/connectors/mcp/${id}`, undefined, {
body: data,
});
};
/**
* Delete an MCP connector
*/
deleteMCPConnector = async (connectorId: number) => {
return baseApiService.delete<void>(`/api/v1/connectors/mcp/${connectorId}`);
};
/**
* Test MCP server connection and retrieve available tools
*/
testMCPConnection = async (serverConfig: MCPServerConfig) => {
return baseApiService.post<MCPTestConnectionResponse>(
"/api/v1/connectors/mcp/test",
undefined,
{
body: serverConfig,
}
);
};
// =============================================================================
// Slack Connector Methods
// =============================================================================
/**
* Get Slack channels with bot membership status
*/
getSlackChannels = async (connectorId: number) => {
return baseApiService.get(
`/api/v1/slack/connector/${connectorId}/channels`,
listSlackChannelsResponse
);
};
// =============================================================================
// Discord Connector Methods
// =============================================================================
/**
* Get Discord text channels for a connector
*/
getDiscordChannels = async (connectorId: number) => {
return baseApiService.get(
`/api/v1/discord/connector/${connectorId}/channels`,
listDiscordChannelsResponse
);
};
/** Live stats for the Obsidian connector tile. */
getObsidianStats = async (vaultId: string): Promise<ObsidianStats> => {
return baseApiService.get<ObsidianStats>(
`/api/v1/obsidian/stats?vault_id=${encodeURIComponent(vaultId)}`
);
};
}
export interface ObsidianStats {
vault_id: string;
files_synced: number;
last_sync_at: string | null;
}
export type { SlackChannel, DiscordChannel };
export const connectorsApiService = new ConnectorsApiService();