Jake Shore f3c4cd817b Add all MCP servers + factory infra to MCPEngine — 2026-02-06
=== 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.
2026-02-06 06:32:29 -05:00

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,
};
}