diff --git a/mcp-gateway/erpnext-mcp/src/services/erpnext-client.ts b/mcp-gateway/erpnext-mcp/src/services/erpnext-client.ts deleted file mode 100644 index 3b68ac5..0000000 --- a/mcp-gateway/erpnext-mcp/src/services/erpnext-client.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { ERPNextConfig, ERPNextListParams, PaginatedResult } from "../types.js"; -import { RESOURCE_PATH, METHOD_PATH, CHARACTER_LIMIT, DEFAULT_PAGE_SIZE } from "../constants.js"; - -export class ERPNextClient { - private baseUrl: string; - private authHeader: string; - - constructor(config: ERPNextConfig) { - this.baseUrl = config.baseUrl.replace(/\/$/, ""); - this.authHeader = `token ${config.apiKey}:${config.apiSecret}`; - } - - private async request( - path: string, - method: string = "GET", - body?: Record, - params?: Record - ): Promise { - let url = `${this.baseUrl}${path}`; - if (params) { - const searchParams = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null && value !== "") { - searchParams.append(key, value); - } - } - const qs = searchParams.toString(); - if (qs) url += `?${qs}`; - } - - const headers: Record = { - Authorization: this.authHeader, - "Content-Type": "application/json", - Accept: "application/json", - }; - - const fetchOptions: RequestInit = { method, headers }; - if (body && (method === "POST" || method === "PUT" || method === "DELETE")) { - fetchOptions.body = JSON.stringify(body); - } - - const response = await fetch(url, fetchOptions); - - if (!response.ok) { - const errorText = await response.text(); - let errorMessage: string; - try { - const errorJson = JSON.parse(errorText); - errorMessage = - errorJson.message || - errorJson.exc || - errorJson._server_messages || - errorText; - if (typeof errorMessage !== "string") { - errorMessage = JSON.stringify(errorMessage); - } - } catch { - errorMessage = errorText; - } - throw new Error( - `ERPNext API error (${response.status} ${response.statusText}): ${errorMessage.slice(0, 500)}` - ); - } - - const json = await response.json(); - return json as T; - } - - // ── Generic CRUD ────────────────────────────────────────── - - async listDocuments( - doctype: string, - listParams?: ERPNextListParams - ): Promise>> { - const params: Record = {}; - - if (listParams?.fields?.length) { - params.fields = JSON.stringify(listParams.fields); - } - if (listParams?.filters?.length) { - params.filters = JSON.stringify(listParams.filters); - } - if (listParams?.or_filters?.length) { - params.or_filters = JSON.stringify(listParams.or_filters); - } - if (listParams?.order_by) { - params.order_by = listParams.order_by; - } - if (listParams?.group_by) { - params.group_by = listParams.group_by; - } - - const offset = listParams?.limit_start ?? 0; - const limit = listParams?.limit_page_length ?? DEFAULT_PAGE_SIZE; - params.limit_start = String(offset); - params.limit_page_length = String(limit); - - const result = await this.request<{ data: Record[] }>( - `${RESOURCE_PATH}/${encodeURIComponent(doctype)}`, - "GET", - undefined, - params - ); - - // Get total count - const countParams: Record = {}; - if (listParams?.filters?.length) { - countParams.filters = JSON.stringify(listParams.filters); - } - if (listParams?.or_filters?.length) { - countParams.or_filters = JSON.stringify(listParams.or_filters); - } - countParams.limit_page_length = "0"; - - let total = result.data.length + offset; - try { - const countResult = await this.request<{ data: number }>( - `${METHOD_PATH}/frappe.client.get_count`, - "GET", - undefined, - { - doctype, - ...(listParams?.filters?.length - ? { filters: JSON.stringify(listParams.filters) } - : {}), - } - ); - total = countResult.data ?? (countResult as unknown as { message: number }).message ?? total; - } catch { - // If count fails, estimate based on returned data - if (result.data.length === limit) { - total = offset + limit + 1; // Signal there might be more - } - } - - return { - total, - count: result.data.length, - offset, - items: result.data, - has_more: offset + result.data.length < total, - ...(offset + result.data.length < total - ? { next_offset: offset + result.data.length } - : {}), - }; - } - - async getDocument( - doctype: string, - name: string, - fields?: string[] - ): Promise> { - const params: Record = {}; - if (fields?.length) { - params.fields = JSON.stringify(fields); - } - const result = await this.request<{ data: Record }>( - `${RESOURCE_PATH}/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`, - "GET", - undefined, - params - ); - return result.data; - } - - async createDocument( - doctype: string, - data: Record - ): Promise> { - const result = await this.request<{ data: Record }>( - `${RESOURCE_PATH}/${encodeURIComponent(doctype)}`, - "POST", - data - ); - return result.data; - } - - async updateDocument( - doctype: string, - name: string, - data: Record - ): Promise> { - const result = await this.request<{ data: Record }>( - `${RESOURCE_PATH}/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`, - "PUT", - data - ); - return result.data; - } - - async deleteDocument( - doctype: string, - name: string - ): Promise<{ message: string }> { - const result = await this.request<{ message: string }>( - `${RESOURCE_PATH}/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`, - "DELETE" - ); - return result; - } - - // ── Workflow / Submit / Cancel / Amend ──────────────────── - - async submitDocument( - doctype: string, - name: string - ): Promise> { - return this.updateDocument(doctype, name, { docstatus: 1 }); - } - - async cancelDocument( - doctype: string, - name: string - ): Promise> { - return this.updateDocument(doctype, name, { docstatus: 2 }); - } - - async amendDocument( - doctype: string, - name: string - ): Promise> { - const result = await this.callMethod("frappe.client.amend", { - doctype, - name, - }); - return result as Record; - } - - // ── Method Calls ────────────────────────────────────────── - - async callMethod( - method: string, - args?: Record - ): Promise { - const result = await this.request<{ message: unknown }>( - `${METHOD_PATH}/${method}`, - "POST", - args - ); - return result.message ?? result; - } - - async callGetMethod( - method: string, - params?: Record - ): Promise { - const result = await this.request<{ message: unknown }>( - `${METHOD_PATH}/${method}`, - "GET", - undefined, - params - ); - return result.message ?? result; - } - - // ── Search / Link ───────────────────────────────────────── - - async searchLink( - doctype: string, - txt: string, - filters?: Record, - pageLength?: number - ): Promise> { - const params: Record = { - doctype, - txt, - page_length: String(pageLength ?? 20), - }; - if (filters) { - params.filters = JSON.stringify(filters); - } - const result = await this.callGetMethod( - "frappe.desk.search.search_link", - params - ); - return result as Array<{ value: string; description: string }>; - } - - // ── Report ──────────────────────────────────────────────── - - async runReport( - reportName: string, - filters?: Record - ): Promise { - return this.callMethod("frappe.desk.query_report.run", { - report_name: reportName, - filters: filters ?? {}, - }); - } - - // ── File upload ─────────────────────────────────────────── - - async getFileUrl(fileName: string): Promise { - return `${this.baseUrl}/api/method/frappe.client.get_file?file_name=${encodeURIComponent(fileName)}`; - } - - // ── Print format ────────────────────────────────────────── - - async getPrintFormat( - doctype: string, - name: string, - printFormat?: string - ): Promise { - const params: Record = { - doctype, - name, - format: printFormat ?? "Standard", - }; - const result = await this.callGetMethod( - "frappe.utils.print_format.download_pdf", - params - ); - return result as string; - } - - // ── Get count ───────────────────────────────────────────── - - async getCount( - doctype: string, - filters?: Array<[string, string, string, string | number | boolean]> - ): Promise { - const params: Record = { doctype }; - if (filters?.length) { - params.filters = JSON.stringify(filters); - } - const result = await this.callGetMethod("frappe.client.get_count", params); - return result as number; - } - - // ── Get list of DocTypes ────────────────────────────────── - - async getDocTypeList(): Promise { - const result = await this.listDocuments("DocType", { - fields: ["name"], - limit_page_length: 500, - order_by: "name asc", - }); - return result.items.map((d) => d.name as string); - } - - // ── Get DocType schema/meta ─────────────────────────────── - - async getDocTypeMeta( - doctype: string - ): Promise> { - const result = await this.callGetMethod("frappe.client.get_list", { - doctype: "DocField", - filters: JSON.stringify([["parent", "=", doctype]]), - fields: JSON.stringify([ - "fieldname", - "fieldtype", - "label", - "options", - "reqd", - "read_only", - "hidden", - ]), - limit_page_length: "500", - order_by: "idx asc", - }); - return { doctype, fields: result }; - } -} - -// ── Response Formatting Helpers ─────────────────────────── - -export function truncateResponse(text: string): string { - if (text.length <= CHARACTER_LIMIT) return text; - return ( - text.slice(0, CHARACTER_LIMIT) + - `\n\n--- Response truncated (${text.length} chars). Use filters, pagination, or narrower field selection to get more targeted results. ---` - ); -} - -export function formatDocumentMarkdown( - doc: Record, - doctype?: string -): string { - const lines: string[] = []; - if (doctype) { - lines.push(`# ${doctype}: ${doc.name ?? "Unknown"}`); - } else { - lines.push(`# ${doc.name ?? "Document"}`); - } - lines.push(""); - - for (const [key, value] of Object.entries(doc)) { - if ( - value === null || - value === undefined || - value === "" || - key.startsWith("_") - ) - continue; - if (Array.isArray(value)) { - lines.push(`**${key}**: [${value.length} items]`); - } else if (typeof value === "object") { - lines.push(`**${key}**: ${JSON.stringify(value)}`); - } else { - lines.push(`**${key}**: ${value}`); - } - } - return lines.join("\n"); -} - -export function formatListMarkdown( - items: Record[], - doctype: string, - pagination: { total: number; offset: number; has_more: boolean } -): string { - const lines: string[] = [ - `# ${doctype} List`, - `Showing ${items.length} of ${pagination.total} (offset: ${pagination.offset})`, - "", - ]; - - for (const item of items) { - const name = item.name ?? "unnamed"; - const summary = Object.entries(item) - .filter(([k]) => k !== "name") - .map(([k, v]) => `${k}: ${v}`) - .join(" | "); - lines.push(`- **${name}**${summary ? ` — ${summary}` : ""}`); - } - - if (pagination.has_more) { - lines.push( - "", - `_More results available. Use offset=${pagination.offset + items.length} to see next page._` - ); - } - - return lines.join("\n"); -}