diff --git a/apps/jscpd-server/package.json b/apps/jscpd-server/package.json index f8153ed5..bcc662ae 100644 --- a/apps/jscpd-server/package.json +++ b/apps/jscpd-server/package.json @@ -71,5 +71,8 @@ "typescript": "^5.9.3", "vitest": "^4.1.9" }, + "optionalDependencies": { + "@blackwell-systems/gcf": "2.2.1" + }, "preferGlobal": true } diff --git a/apps/jscpd-server/src/index.ts b/apps/jscpd-server/src/index.ts index 1bad315b..2b63bbb6 100644 --- a/apps/jscpd-server/src/index.ts +++ b/apps/jscpd-server/src/index.ts @@ -23,6 +23,12 @@ function initServerCli(packageJson: any, argv: string[]): Command { .option( "-H, --host [string]", "host to bind the server to (Default is 0.0.0.0)", + ) + .option( + "--gcf", + // Parsed directly via process.argv in serialize.ts for decoupling: + // the serialize module works independently of the server startup flow. + "Enable GCF output format for MCP tool responses (56% fewer tokens)", ); addCommonOptions(cli); diff --git a/apps/jscpd-server/src/server/mcp-server.ts b/apps/jscpd-server/src/server/mcp-server.ts index d3293de0..20785117 100644 --- a/apps/jscpd-server/src/server/mcp-server.ts +++ b/apps/jscpd-server/src/server/mcp-server.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { JscpdServerService } from "./service"; import { API_INFO } from "./constants"; +import { serialize } from "./serialize"; export const createMcpServer = (service: JscpdServerService) => { const server = new McpServer( @@ -46,7 +47,7 @@ export const createMcpServer = (service: JscpdServerService) => { content: [ { type: "text", - text: JSON.stringify(result, null, 2), + text: serialize(result), }, ], }; @@ -79,7 +80,7 @@ export const createMcpServer = (service: JscpdServerService) => { content: [ { type: "text", - text: JSON.stringify(stats, null, 2), + text: serialize(stats), }, ], }; @@ -113,7 +114,7 @@ export const createMcpServer = (service: JscpdServerService) => { content: [ { type: "text", - text: JSON.stringify(statistics), + text: serialize(statistics), }, ], }; @@ -145,6 +146,7 @@ export const createMcpServer = (service: JscpdServerService) => { contents: [ { uri: uri.href, + // Resource declares mimeType: "application/json", so always use JSON here. text: JSON.stringify(stats, null, 2), }, ], diff --git a/apps/jscpd-server/src/server/serialize.ts b/apps/jscpd-server/src/server/serialize.ts new file mode 100644 index 00000000..8d758b77 --- /dev/null +++ b/apps/jscpd-server/src/server/serialize.ts @@ -0,0 +1,51 @@ +/** + * Optional GCF (Graph Compact Format) serialization for MCP tool responses. + * + * When enabled, structured data is encoded using GCF's tabular encoding + * instead of JSON, saving ~56% of tokens on duplication results. + * + * Enable via CLI flag: jscpd-server --gcf + * Or environment variable: JSCPD_OUTPUT_FORMAT=gcf + * + * Requires the optional @blackwell-systems/gcf package. + */ + +let encodeGeneric: ((data: unknown) => string) | null = null; + +try { + const gcf = await import("@blackwell-systems/gcf"); + encodeGeneric = gcf.encodeGeneric; +} catch { + // GCF is an optional dependency +} + +let gcfEnabled: boolean | null = null; + +/** + * Check whether GCF output is enabled and available. + * Returns true only when the `--gcf` flag or `JSCPD_OUTPUT_FORMAT=gcf` + * env var is set AND the `@blackwell-systems/gcf` package is installed. + */ +export function isGcfEnabled(): boolean { + if (gcfEnabled === null) { + gcfEnabled = + process.env.JSCPD_OUTPUT_FORMAT === "gcf" || + process.argv.includes("--gcf"); + } + return gcfEnabled && encodeGeneric !== null; +} + +/** + * Serialize data as JSON or GCF depending on configuration. + * Drop-in replacement for JSON.stringify(data, null, 2). + */ +export function serialize(data: unknown, indent = 2): string { + if (isGcfEnabled()) { + try { + return encodeGeneric(data); + } catch { + // Fall back to JSON on any encoding error + } + } + return JSON.stringify(data, null, indent); +}