Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/gittensory-miner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ It exposes these read-only tools:
- `gittensory_miner_ping` (#5153) — a health check returning a static `{ "status": "ok", "tool": "gittensory_miner_ping" }` object. Reads no AMS state, takes no arguments.
- `gittensory_miner_get_portfolio_dashboard` (#5155) — the per-repo portfolio-queue backlog dashboard: status counts (queued / in_progress / done), totals, and the oldest-queued age. Wraps `collectPortfolioDashboard()` (no new logic) — the same data `gittensory-miner queue dashboard --json` prints locally. Read-only, takes no arguments.
- `gittensory_miner_list_claims` (#5156) — lists the local claim ledger (repo, issue number, status, claimed-at, note) via `listClaims()`. Optional `repoFullName` / `status` filters pass through to the query. Read-only — exposes no claim/release mutation.
- `gittensory_miner_get_audit_feed` (#5158) — read-only, metadata-only event-ledger audit feed (`eventType`, `repoFullName`, `outcome`, `actor`, `detail`, `createdAt`). Wraps `collectEventLedgerAuditFeed()` with the same filters as `gittensory-miner ledger list` (`--repo`, `--since`, `--type`). Never returns `payload_json` or other raw ledger columns.

Further AMS-state-reading tools (status/doctor diagnostics, run-state, event/governor ledgers) land as follow-up PRs on top of this server.
Further AMS-state-reading tools (status/doctor diagnostics, run-state, governor ledgers) land as follow-up PRs on top of this server.

## Version check

Expand Down
6 changes: 5 additions & 1 deletion packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { EventLedger } from "../lib/event-ledger.js";

/** The static, non-secret payload the gittensory_miner_ping tool always returns, independent of input. */
export const MINER_PING_STATUS: { status: "ok"; tool: "gittensory_miner_ping" };
Expand All @@ -19,10 +20,13 @@ export interface MinerMcpServerOptions {
};
/** Override the clock used for the oldest-queued age (defaults to Date.now()); injection seam for tests. */
nowMs?: number;
/** Override the event-ledger opener (defaults to initEventLedger); injection seam for tests. */
initEventLedger?: () => EventLedger;
}

/**
* Build the miner MCP server with its tools registered (gittensory_miner_ping, gittensory_miner_get_portfolio_dashboard).
* Build the miner MCP server with its tools registered (gittensory_miner_ping,
* gittensory_miner_get_portfolio_dashboard, gittensory_miner_list_claims, gittensory_miner_get_audit_feed).
* `options` supplies test injection seams; production callers pass nothing.
*/
export function createMinerMcpServer(options?: MinerMcpServerOptions): McpServer;
57 changes: 53 additions & 4 deletions packages/gittensory-miner/bin/gittensory-miner-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import { fileURLToPath } from "node:url";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js";
import {
collectEventLedgerAuditFeed,
normalizeAuditFeedMcpFilter,
} from "../lib/event-ledger-cli.js";
import { initEventLedger } from "../lib/event-ledger.js";
import { collectPortfolioDashboard } from "../lib/portfolio-dashboard.js";
import { initPortfolioQueueStore } from "../lib/portfolio-queue.js";
import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js";

// MCP stdio server for @jsonbored/gittensory-miner (scaffold #5153). Mirrors the packages/gittensory-mcp
// harness (MCP SDK server + stdio transport). Tools:
Expand All @@ -15,19 +20,29 @@ import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js";
// existing collectPortfolioDashboard aggregator (no new logic; same data as `queue dashboard --json`).
// - gittensory_miner_list_claims (#5156): read-only listing of the local claim ledger (optional repo/status
// filter passed through to listClaims); exposes no claim/release mutation.
// Remaining AMS-state-reading tools (status/doctor, run-state, event/governor ledgers, etc.) land as follow-ups.
// - gittensory_miner_get_audit_feed (#5158): read-only metadata-only event-ledger audit feed via
// collectEventLedgerAuditFeed() (same filters as `ledger list`; never returns payload_json).
// Remaining AMS-state-reading tools (status/doctor, run-state, governor ledgers, etc.) land as follow-ups.

// Read the version from this package's own package.json (always shipped) rather than a hand-synced
// literal, so a release bump never has a second place to forget -- same approach as the mcp harness.
const ownPackageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));

/** Optional filters accepted by gittensory_miner_get_audit_feed (#5158). */
const auditFeedInputSchema = {
repoFullName: z.string().min(1).optional(),
since: z.number().int().nonnegative().optional(),
type: z.string().min(1).optional(),
};

/** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */
export const MINER_PING_STATUS = { status: "ok", tool: "gittensory_miner_ping" };

/**
* Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`,
* and `options.nowMs` are injection seams for tests (default to the real stores and the wall clock); the ping tool
* needs none. Each store-backed tool opens its store only when invoked and closes any store it opened.
* `options.initEventLedger`, and `options.nowMs` are injection seams for tests (default to the real stores and the
* wall clock); the ping tool needs none. Each store-backed tool opens its store only when invoked and closes any
* store it opened.
*/
export function createMinerMcpServer(options = {}) {
const server = new McpServer({ name: "gittensory-miner", version: ownPackageJson.version });
Expand Down Expand Up @@ -86,6 +101,40 @@ export function createMinerMcpServer(options = {}) {
}
},
);
server.registerTool(
"gittensory_miner_get_audit_feed",
{
description:
"Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, " +
"outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) — " +
"the same read filters as `gittensory-miner ledger list` (--repo, --since, --type). Never returns " +
"payload_json or other raw ledger columns; never writes to the ledger.",
inputSchema: auditFeedInputSchema,
},
async (input) => {
const ownsLedger = options.initEventLedger === undefined;
const eventLedger = (options.initEventLedger ?? initEventLedger)();
try {
const filter = normalizeAuditFeedMcpFilter(input ?? {});
const feed = collectEventLedgerAuditFeed(eventLedger, filter);
return { content: [{ type: "text", text: JSON.stringify(feed) }] };
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}),
},
],
isError: true,
};
} finally {
if (ownsLedger) eventLedger.close();
}
},
);
return server;
}

Expand Down
45 changes: 45 additions & 0 deletions packages/gittensory-miner/lib/event-ledger-cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,51 @@ export function filterLedgerEvents(
options?: { type?: string | null },
): LedgerEntry[];

export const AUDIT_FEED_ENTRY_FIELDS: readonly [
"eventType",
"repoFullName",
"outcome",
"actor",
"detail",
"createdAt",
];

export function projectLedgerEventToAuditFeedEntry(entry: LedgerEntry): {
eventType: string;
repoFullName: string | null;
outcome: string | null;
actor: string | null;
detail: string | null;
createdAt: string;
};

export type AuditFeedMcpFilterInput = {
repoFullName?: string | null;
since?: number | null;
type?: string | null;
};

export function normalizeAuditFeedMcpFilter(input?: AuditFeedMcpFilterInput): {
repoFullName: string | null;
since: number | null;
type: string | null;
};

export function collectEventLedgerAuditFeed(
eventLedger: EventLedger,
filter?: { repoFullName?: string | null; since?: number | null; type?: string | null },
): {
repoFullName?: string;
events: Array<{
eventType: string;
repoFullName: string | null;
outcome: string | null;
actor: string | null;
detail: string | null;
createdAt: string;
}>;
};

export function renderLedgerTable(events: LedgerEntry[]): string;

export function runLedgerList(
Expand Down
69 changes: 69 additions & 0 deletions packages/gittensory-miner/lib/event-ledger-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,75 @@ export function filterLedgerEvents(events, options = {}) {
return events.filter((entry) => entry.type === type);
}

/** Metadata-only audit-feed columns exposed by the MCP tool (#5158). */
export const AUDIT_FEED_ENTRY_FIELDS = Object.freeze([
"eventType",
"repoFullName",
"outcome",
"actor",
"detail",
"createdAt",
]);

function optionalMetadataString(value) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed || null;
}

/** Project one ledger row to the public, metadata-only audit-feed shape — never returns payload_json. */
export function projectLedgerEventToAuditFeedEntry(entry) {
const payload =
entry?.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) ? entry.payload : {};
return {
eventType: entry.type,
repoFullName: entry.repoFullName,
outcome: optionalMetadataString(payload.outcome),
actor: optionalMetadataString(payload.actor),
detail: optionalMetadataString(payload.detail),
createdAt: entry.createdAt,
};
}

/** Normalize optional MCP/JSON filter args into the shape `ledger list` already uses (#5158). */
export function normalizeAuditFeedMcpFilter(input = {}) {
if (input === null || typeof input !== "object" || Array.isArray(input)) {
throw new Error("filter must be an object");
}
const filter = { repoFullName: null, since: null, type: null };
if (input.repoFullName !== undefined && input.repoFullName !== null) {
const repo = parseRepoArg(String(input.repoFullName), "repoFullName must be in owner/repo form.");
if ("error" in repo) throw new Error(repo.error);
filter.repoFullName = repo.repoFullName;
}
if (input.since !== undefined && input.since !== null) {
const parsedSince = parseSinceArg(String(input.since));
if ("error" in parsedSince) throw new Error(parsedSince.error);
filter.since = parsedSince.since;
}
if (input.type !== undefined && input.type !== null) {
const trimmed = String(input.type).trim();
if (!trimmed) throw new Error("type must be a non-empty string.");
filter.type = trimmed;
}
return filter;
}

/** Read-only audit feed shared by the MCP audit-feed tool (#5158). */
export function collectEventLedgerAuditFeed(eventLedger, filter = {}) {
const events = filterLedgerEvents(
eventLedger.readEvents({
repoFullName: filter.repoFullName,
since: filter.since,
}),
{ type: filter.type },
);
return {
...(filter.repoFullName ? { repoFullName: filter.repoFullName } : {}),
events: events.map(projectLedgerEventToAuditFeedEntry),
};
}

function display(value) {
if (value === null || value === undefined) return "-";
return String(value);
Expand Down
Loading