=== NEW SERVERS ADDED (7) === - servers/closebot — 119 tools, 14 modules, 4,656 lines TS (Stage 7) - servers/google-console — Google Search Console MCP (Stage 7) - servers/meta-ads — Meta/Facebook Ads MCP (Stage 8) - servers/twilio — Twilio communications MCP (Stage 8) - servers/competitor-research — Competitive intel MCP (Stage 6) - servers/n8n-apps — n8n workflow MCP apps (Stage 6) - servers/reonomy — Commercial real estate MCP (Stage 1) === FACTORY INFRASTRUCTURE ADDED === - infra/factory-tools — mcp-jest, mcp-validator, mcp-add, MCP Inspector - 60 test configs, 702 auto-generated test cases - All 30 servers score 100/100 protocol compliance - infra/command-center — Pipeline state, operator playbook, dashboard config - infra/factory-reviews — Automated eval reports === DOCS ADDED === - docs/MCP-FACTORY.md — Factory overview - docs/reports/ — 5 pipeline evaluation reports - docs/research/ — Browser MCP research === RULES ESTABLISHED === - CONTRIBUTING.md — All MCP work MUST go in this repo - README.md — Full inventory of 37 servers + infra docs - .gitignore — Updated for Python venvs TOTAL: 37 MCP servers + full factory pipeline in one repo. This is now the single source of truth for all MCP work.
211 lines
5.3 KiB
TypeScript
211 lines
5.3 KiB
TypeScript
// ============================================================================
|
|
// CloseBot API HTTP Client
|
|
// ============================================================================
|
|
|
|
const BASE_URL = "https://api.closebot.com";
|
|
|
|
export class CloseBotClient {
|
|
private apiKey: string;
|
|
private baseUrl: string;
|
|
|
|
constructor(apiKey?: string, baseUrl?: string) {
|
|
this.apiKey = apiKey || process.env.CLOSEBOT_API_KEY || "";
|
|
this.baseUrl = baseUrl || process.env.CLOSEBOT_BASE_URL || BASE_URL;
|
|
|
|
if (!this.apiKey) {
|
|
throw new Error(
|
|
"CloseBot API key is required. Set CLOSEBOT_API_KEY environment variable."
|
|
);
|
|
}
|
|
}
|
|
|
|
private buildUrl(path: string, query?: Record<string, unknown>): string {
|
|
const url = new URL(path, this.baseUrl);
|
|
if (query) {
|
|
for (const [key, value] of Object.entries(query)) {
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
url.searchParams.set(key, String(value));
|
|
}
|
|
}
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
private get headers(): Record<string, string> {
|
|
return {
|
|
"X-CB-KEY": this.apiKey,
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
};
|
|
}
|
|
|
|
async get<T = unknown>(
|
|
path: string,
|
|
query?: Record<string, unknown>
|
|
): Promise<T> {
|
|
const url = this.buildUrl(path, query);
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: this.headers,
|
|
});
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
async post<T = unknown>(
|
|
path: string,
|
|
body?: unknown,
|
|
query?: Record<string, unknown>
|
|
): Promise<T> {
|
|
const url = this.buildUrl(path, query);
|
|
const options: RequestInit = {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
};
|
|
if (body !== undefined) {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
const response = await fetch(url, options);
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
async put<T = unknown>(
|
|
path: string,
|
|
body?: unknown,
|
|
query?: Record<string, unknown>
|
|
): Promise<T> {
|
|
const url = this.buildUrl(path, query);
|
|
const options: RequestInit = {
|
|
method: "PUT",
|
|
headers: this.headers,
|
|
};
|
|
if (body !== undefined) {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
const response = await fetch(url, options);
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
async delete<T = unknown>(
|
|
path: string,
|
|
query?: Record<string, unknown>
|
|
): Promise<T> {
|
|
const url = this.buildUrl(path, query);
|
|
const response = await fetch(url, {
|
|
method: "DELETE",
|
|
headers: this.headers,
|
|
});
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
async postFormData<T = unknown>(
|
|
path: string,
|
|
formData: FormData,
|
|
query?: Record<string, unknown>
|
|
): Promise<T> {
|
|
const url = this.buildUrl(path, query);
|
|
const headers: Record<string, string> = {
|
|
"X-CB-KEY": this.apiKey,
|
|
Accept: "application/json",
|
|
};
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers,
|
|
body: formData,
|
|
});
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
async putFormData<T = unknown>(
|
|
path: string,
|
|
formData: FormData,
|
|
query?: Record<string, unknown>
|
|
): Promise<T> {
|
|
const url = this.buildUrl(path, query);
|
|
const headers: Record<string, string> = {
|
|
"X-CB-KEY": this.apiKey,
|
|
Accept: "application/json",
|
|
};
|
|
const response = await fetch(url, {
|
|
method: "PUT",
|
|
headers,
|
|
body: formData,
|
|
});
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
private async handleResponse<T>(response: Response): Promise<T> {
|
|
if (!response.ok) {
|
|
let errorBody: string;
|
|
try {
|
|
errorBody = await response.text();
|
|
} catch {
|
|
errorBody = "Unable to read error body";
|
|
}
|
|
throw new ApiError(
|
|
`CloseBot API error ${response.status}: ${response.statusText}`,
|
|
response.status,
|
|
errorBody
|
|
);
|
|
}
|
|
|
|
const contentType = response.headers.get("content-type");
|
|
if (!contentType || response.status === 204) {
|
|
return {} as T;
|
|
}
|
|
|
|
if (contentType.includes("application/json") || contentType.includes("text/json")) {
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
const text = await response.text();
|
|
try {
|
|
return JSON.parse(text) as T;
|
|
} catch {
|
|
return text as unknown as T;
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
public statusCode: number;
|
|
public responseBody: string;
|
|
|
|
constructor(message: string, statusCode: number, responseBody: string) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
this.statusCode = statusCode;
|
|
this.responseBody = responseBody;
|
|
}
|
|
}
|
|
|
|
/** Format API result as MCP text content */
|
|
export function ok(data: unknown): {
|
|
content: Array<{ type: "text"; text: string }>;
|
|
} {
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text" as const,
|
|
text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/** Format error as MCP error content */
|
|
export function err(error: unknown): {
|
|
content: Array<{ type: "text"; text: string }>;
|
|
isError: boolean;
|
|
} {
|
|
const message =
|
|
error instanceof ApiError
|
|
? `API Error ${error.statusCode}: ${error.message}\n${error.responseBody}`
|
|
: error instanceof Error
|
|
? error.message
|
|
: String(error);
|
|
return {
|
|
content: [{ type: "text" as const, text: message }],
|
|
isError: true,
|
|
};
|
|
}
|