lilith-platform.live/codebase/@features/admin/mcp-server/src/client.ts
Natalie 6bf26998d6 feat(mcp/quinn-admin): add MCP tools for uploading content (upload_gallery_photo) and making posts (content drops with buy links + published_at for retro platform dates)
Supports the VIP prepaid wallet channel: create posts whose gallery media can be unlocked via balance-purchase or intents (targetRef to drop).

Also synced payment_methods tools for vip_unlock_enabled.
2026-06-22 02:21:19 -05:00

97 lines
3.3 KiB
TypeScript

/**
* HTTP clients for the quinn-admin MCP server.
*
* - apiGet/apiMutate → quinn.my (still used for platform stats / signups / financials)
* - quinnApiGet/Mutate → quinn.api unified backend (everything else: gallery, identity,
* physical, rate cards, rate entries). Bearer SERVICE_TOKEN auth
* against `config.SERVICE_TOKEN` enforced at /admin/* routes.
*/
const QUINN_MY_BASE = process.env['QUINN_MY_BASE_URL'] ?? 'https://my.quinn.apricot.lan';
const QUINN_MY_TOKEN = process.env['QUINN_MY_TOKEN'] ?? '';
const QUINN_API_BASE = process.env['QUINN_API_URL'] ?? 'http://localhost:3030';
const QUINN_API_TOKEN = process.env['QUINN_API_SERVICE_TOKEN'] ?? '';
export interface ApiResponse<T = unknown> {
ok: boolean;
status: number;
data: T;
}
async function request<T>(
base: string,
token: string,
method: string,
path: string,
body?: unknown,
): Promise<ApiResponse<T>> {
try {
const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
};
const init: RequestInit = { method, headers };
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(body);
}
const res = await fetch(`${base}${path}`, init);
// 204 No Content (gallery DELETE, rate-entry DELETE, etc.)
if (res.status === 204) {
return { ok: res.ok, status: res.status, data: null as T };
}
const text = await res.text();
const data = text ? (JSON.parse(text) as T) : (null as T);
return { ok: res.ok, status: res.status, data };
} catch (cause) {
throw new Error(`${method} ${base}${path} failed: ${String(cause)}`);
}
}
export const apiGet = <T>(path: string): Promise<ApiResponse<T>> =>
request<T>(QUINN_MY_BASE, QUINN_MY_TOKEN, 'GET', path);
export const apiMutate = <T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> =>
request<T>(QUINN_MY_BASE, QUINN_MY_TOKEN, method, path, body);
export const quinnApiGet = <T>(path: string): Promise<ApiResponse<T>> =>
request<T>(QUINN_API_BASE, QUINN_API_TOKEN, 'GET', path);
export const quinnApiMutate = <T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> =>
request<T>(QUINN_API_BASE, QUINN_API_TOKEN, method, path, body);
/**
* Multipart upload helper for endpoints that expect form-data (e.g. gallery-items/upload).
* Accepts a pre-built FormData (with Blob for files from base64).
* Does not set Content-Type (fetch sets multipart boundary).
*/
export async function quinnApiUpload<T>(path: string, formData: FormData): Promise<ApiResponse<T>> {
try {
const headers: Record<string, string> = {
'Authorization': `Bearer ${QUINN_API_TOKEN}`,
'Accept': 'application/json',
};
const res = await fetch(`${QUINN_API_BASE}${path}`, {
method: 'POST',
headers,
body: formData,
});
if (res.status === 204) {
return { ok: res.ok, status: res.status, data: null as T };
}
const text = await res.text();
const data = text ? (JSON.parse(text) as T) : (null as T);
return { ok: res.ok, status: res.status, data };
} catch (cause) {
throw new Error(`POST ${QUINN_API_BASE}${path} (upload) failed: ${String(cause)}`);
}
}