/**
* Councly API Client
*
* REST client for communicating with Councly's MCP API.
* Handles authentication, requests, and error handling.
*/
export interface CounclyConfig {
apiKey: string;
baseUrl?: string;
}
export interface CreateHearingRequest {
subject: string;
preset?: 'balanced' | 'fast' | 'coding' | 'coding_plus';
workflow?: 'auto' | 'discussion' | 'review' | 'brainstorming';
idempotencyKey?: string;
}
export interface CreateHearingResponse {
hearingId: string;
status: string;
preset: string;
workflow: string;
cost: {
credits: number;
usd: number;
};
createdAt: string;
}
export interface HearingStatusResponse {
hearingId: string;
status: 'pending' | 'streaming' | 'completed' | 'failed' | 'early_stopped';
phase: string | null;
createdAt: string;
completedAt?: string | null;
verdict?: string | null;
trustScore?: number | null;
totalCost?: number;
totalTokens?: number;
counselSummaries?: Array<{
seat: string;
model: string;
lastTurn: string;
}>;
progress?: number;
error?: string;
}
export interface PresetInfo {
id: string;
name: string;
description: string;
counselCount: number;
cost: {
credits: number;
usd: number;
};
models: Array<{
seat: string;
provider: string;
model: string;
}>;
}
export interface PresetsResponse {
presets: PresetInfo[];
workflows: Array<{
id: string;
name: string;
description: string;
}>;
defaults: {
preset: string;
workflow: string;
};
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}
export class CounclyApiError extends Error {
constructor(
public code: string,
message: string,
public status: number,
public details?: Record<string, unknown>
) {
super(message);
this.name = 'CounclyApiError';
}
}
export class CounclyClient {
private apiKey: string;
private baseUrl: string;
constructor(config: CounclyConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://councly.ai';
}
private async request<T>(
method: 'GET' | 'POST' | 'DELETE',
path: string,
body?: unknown,
headers?: Record<string, string>
): Promise<T> {
const url = `${this.baseUrl}/api/v1/mcp${path}`;
const response = await fetch(url, {
method,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': '@councly/mcp',
...headers,
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await response.json();
if (!response.ok) {
const error = data as ApiError;
throw new CounclyApiError(
error.code || 'UNKNOWN_ERROR',
error.message || 'An unknown error occurred',
response.status,
error.details
);
}
return data as T;
}
/**
* Create a new council hearing.
*/
async createHearing(request: CreateHearingRequest): Promise<CreateHearingResponse> {
const headers: Record<string, string> = {};
if (request.idempotencyKey) {
headers['Idempotency-Key'] = request.idempotencyKey;
}
return this.request<CreateHearingResponse>('POST', '/council', request, headers);
}
/**
* Get hearing status.
*/
async getHearingStatus(hearingId: string): Promise<HearingStatusResponse> {
return this.request<HearingStatusResponse>('GET', `/council/${hearingId}`);
}
/**
* Cancel a hearing.
*/
async cancelHearing(hearingId: string): Promise<{ success: boolean; message: string }> {
return this.request<{ success: boolean; message: string }>('DELETE', `/council/${hearingId}`);
}
/**
* List available presets.
*/
async listPresets(): Promise<PresetsResponse> {
return this.request<PresetsResponse>('GET', '/presets');
}
/**
* Health check.
*/
async healthCheck(): Promise<{ status: string }> {
return this.request<{ status: string }>('GET', '/health');
}
/**
* Poll for hearing completion with timeout.
* Returns when hearing reaches terminal state or timeout.
*/
async waitForCompletion(
hearingId: string,
options: {
pollIntervalMs?: number;
timeoutMs?: number;
onProgress?: (status: HearingStatusResponse) => void;
} = {}
): Promise<HearingStatusResponse> {
const {
pollIntervalMs = 5000,
timeoutMs = 300000, // 5 minutes default
onProgress,
} = options;
const startTime = Date.now();
while (true) {
const status = await this.getHearingStatus(hearingId);
if (onProgress) {
onProgress(status);
}
// Terminal states
if (['completed', 'failed', 'early_stopped'].includes(status.status)) {
return status;
}
// Timeout check
if (Date.now() - startTime > timeoutMs) {
throw new CounclyApiError(
'TIMEOUT',
`Hearing did not complete within ${timeoutMs / 1000} seconds`,
408
);
}
// Wait before next poll
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
}
}