* fix(mcp): render full row data in tool text output, not just counts MCP clients that surface only the text content block (not structuredContent) saw count/truncated summaries instead of the actual rows. Add a shared table renderer (server/mcp/table.ts) and use it so every row ships in the text block across research_keywords, get_ranked_keywords, get_keyword_metrics, search_local_businesses, get_local_serp_results, get_google_business_questions, find_serp_competitors, get_backlinks_profile, get_backlinks_overview, get_domain_keyword_suggestions, get_rank_tracker, get_serp_results, and get_search_console_performance. * test(mcp): assert tool text output renders full row tables Cover the shared table renderer (table.test.ts) and add per-tool assertions that each tool's text content block contains the actual row data (exact rendered rows, null->em-dash), guarding against a column wired to the wrong field. Adds tool-text-output.test.ts for the service-backed tools and text assertions to the DataForSEO and Search Console tool tests. * release: v0.0.23 * style(mcp): prettier-format tool table columns
96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { formatMcpCell, formatMcpTable, readPath } from "./table";
|
|
|
|
describe("formatMcpCell", () => {
|
|
it("renders nullish and empty values as an em dash", () => {
|
|
expect(formatMcpCell(null)).toBe("—");
|
|
expect(formatMcpCell(undefined)).toBe("—");
|
|
expect(formatMcpCell("")).toBe("—");
|
|
});
|
|
|
|
it("keeps integers exact and gives other numbers two decimals", () => {
|
|
expect(formatMcpCell(0)).toBe("0");
|
|
expect(formatMcpCell(2400)).toBe("2400");
|
|
expect(formatMcpCell(1.5)).toBe("1.50");
|
|
expect(formatMcpCell(0.333333)).toBe("0.33");
|
|
});
|
|
|
|
it("renders non-finite numbers as an em dash", () => {
|
|
expect(formatMcpCell(Number.NaN)).toBe("—");
|
|
expect(formatMcpCell(Number.POSITIVE_INFINITY)).toBe("—");
|
|
});
|
|
|
|
it("renders booleans and bigints", () => {
|
|
expect(formatMcpCell(true)).toBe("yes");
|
|
expect(formatMcpCell(false)).toBe("no");
|
|
expect(formatMcpCell(10n)).toBe("10");
|
|
});
|
|
|
|
it("collapses whitespace so a stray newline can't break the table", () => {
|
|
expect(formatMcpCell("multi\nline value")).toBe("multi line value");
|
|
});
|
|
|
|
it("emits compact JSON for objects instead of [object Object]", () => {
|
|
expect(formatMcpCell({ a: 1 })).toBe('{"a":1}');
|
|
expect(formatMcpCell([1, 2])).toBe("[1,2]");
|
|
});
|
|
});
|
|
|
|
describe("formatMcpTable", () => {
|
|
type Row = { keyword: string; volume: number | null };
|
|
const columns = [
|
|
{ header: "keyword", value: (row: Row) => row.keyword },
|
|
{ header: "volume", value: (row: Row) => row.volume },
|
|
];
|
|
|
|
it("renders a header line plus one line per row", () => {
|
|
const table = formatMcpTable(
|
|
[
|
|
{ keyword: "seo tools", volume: 2400 },
|
|
{ keyword: "seo audit", volume: null },
|
|
],
|
|
columns,
|
|
);
|
|
expect(table).toBe(
|
|
["keyword | volume", "seo tools | 2400", "seo audit | —"].join("\n"),
|
|
);
|
|
});
|
|
|
|
it("renders only the header when there are no rows", () => {
|
|
expect(formatMcpTable([], columns)).toBe("keyword | volume");
|
|
});
|
|
|
|
it("uses a per-column format override when provided", () => {
|
|
const table = formatMcpTable(
|
|
[{ keyword: "x", volume: 0.04 }],
|
|
[
|
|
{ header: "keyword", value: (row: Row) => row.keyword },
|
|
{
|
|
header: "CTR",
|
|
value: (row: Row) => row.volume,
|
|
format: (value) =>
|
|
typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "—",
|
|
},
|
|
],
|
|
);
|
|
expect(table).toBe(["keyword | CTR", "x | 4.0%"].join("\n"));
|
|
});
|
|
});
|
|
|
|
describe("readPath", () => {
|
|
it("walks nested records", () => {
|
|
expect(readPath({ a: { b: { c: 3 } } }, "a", "b", "c")).toBe(3);
|
|
});
|
|
|
|
it("returns undefined when a hop is missing or not an object", () => {
|
|
expect(readPath({ a: null }, "a", "b")).toBeUndefined();
|
|
expect(readPath({ a: 1 }, "a", "b")).toBeUndefined();
|
|
expect(readPath(null, "a")).toBeUndefined();
|
|
expect(readPath(undefined, "a")).toBeUndefined();
|
|
});
|
|
|
|
it("reads a top-level key", () => {
|
|
expect(readPath({ keyword: "seo" }, "keyword")).toBe("seo");
|
|
});
|
|
});
|