// progression-chain-read — full state-of-chain for one deal, by chainId or // address. Links are priority-ordered; a priority gap or duplicate is flagged // at read time, not silently passed. Read-only. import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { lifelineTool } from "../../../../../../../platform/lib/mcp-lifeline/dist/index.js"; import { getSession } from "../lib/neo4j.js"; import { readChain, DEFAULT_STALLED_THRESHOLD_DAYS } from "../lib/chain-graph.js"; export function registerChainRead(server: McpServer, accountId: string): void { lifelineTool( server, "progression-chain-read", "Read the full state of one sale-agreed chain — the Sale, the Chain, every link ordered by priority with its ten milestone dates and party roles, plus a stall flag and any structural warnings (priority gap or duplicate). Select by chainId or by a link address. Read-only.", { chainId: z.string().optional().describe("The chain to read."), address: z.string().optional().describe("A link address to resolve the chain from (when chainId is unknown)."), stalledThresholdDays: z .number() .int() .positive() .optional() .describe(`Days with no milestone movement before 'stalled' (default ${DEFAULT_STALLED_THRESHOLD_DAYS}).`), }, async (params: Record) => { const { chainId, address, stalledThresholdDays } = params as { chainId?: string; address?: string; stalledThresholdDays?: number; }; if (!chainId && !address) { return { content: [{ type: "text" as const, text: "Error: one of chainId or address is required." }], isError: true, }; } const session = getSession(); try { const state = await readChain( session, accountId, { chainId, address }, Date.now(), stalledThresholdDays ?? DEFAULT_STALLED_THRESHOLD_DAYS, ); if (!state) { console.error(`[progression] op=chain-read chainId=${chainId ?? address} links=0 stalled=false cause=not-found`); return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, found: false }, null, 2) }] }; } console.error( `[progression] op=chain-read chainId=${state.chainId} links=${state.links.length} stalled=${state.stalled} cause=${state.structuralFlags.length ? state.structuralFlags.join(";") : state.cause}`, ); return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, found: true, ...state }, null, 2) }] }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; } finally { await session.close(); } }, ); }