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
7 changes: 5 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions packages/gittensory-miner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,19 @@ gittensory-miner manage status [--json]
gittensory-miner manage poll <owner/repo> <pr#> [--branch <name>] [--json]
```

## MCP server

The package ships a second bin entry, `gittensory-miner-mcp`, a minimal [Model Context Protocol](https://modelcontextprotocol.io) stdio server that any MCP-compatible client can connect to:

```sh
gittensory-miner-mcp
```

It currently exposes a single tool, `gittensory_miner_ping` — a health check that returns a static
`{ "status": "ok", "tool": "gittensory_miner_ping" }` object, reads no AMS state, and takes no arguments. This is a
scaffold (#5153): real AMS-state-reading tools (status/doctor diagnostics, portfolio dashboard, claim-ledger
listing) land as follow-up PRs on top of it.

## Version check

On every invocation the CLI starts an async npm registry lookup (5s timeout). When the installed package is behind `@jsonbored/gittensory-miner@latest`, it prints a one-line upgrade command to stderr without blocking or failing the requested command. Set `GITTENSORY_NPM_REGISTRY_URL` to point at a mirror, same as `@jsonbored/gittensory-mcp`.
10 changes: 10 additions & 0 deletions packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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" };

/**
* Build the miner MCP server with its single gittensory_miner_ping health-check tool registered. No I/O
* and no AMS-state reads, so a test can drive it over an in-memory transport.
*/
export function createMinerMcpServer(): McpServer;
50 changes: 50 additions & 0 deletions packages/gittensory-miner/bin/gittensory-miner-mcp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env node
import { readFileSync, realpathSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// Minimal MCP stdio-server scaffold for @jsonbored/gittensory-miner (#5153). Mirrors the
// packages/gittensory-mcp harness (MCP SDK server + stdio transport) but ships exactly ONE trivial
// health-check tool -- gittensory_miner_ping -- returning a static status object. It reads NO AMS
// state and takes no arguments. Future AMS-state-reading tools (status/doctor, portfolio dashboard,
// claim-ledger listing) land as follow-up PRs on top of this scaffold.

// 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"));

/** 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 single health-check tool registered. No I/O and no AMS-state
* reads, so a test can drive it over an in-memory transport without spawning a process or requiring
* any on-disk state to exist.
*/
export function createMinerMcpServer() {
const server = new McpServer({ name: "gittensory-miner", version: ownPackageJson.version });
server.registerTool(
"gittensory_miner_ping",
{
description:
"Health check for the gittensory-miner MCP server. Returns a static status object confirming the " +
"server is reachable. Reads no AMS state and takes no arguments.",
inputSchema: {},
},
async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] }),
);
return server;
}

// Start the stdio transport only when executed directly as the bin, not when imported by a test.
// realpathSync on both sides resolves the npm bin symlink so a global/npx install still matches.
const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : "";
if (invokedPath && invokedPath === realpathSync(fileURLToPath(import.meta.url))) {
createMinerMcpServer()
.connect(new StdioServerTransport())
.catch((error) => {
console.error(error);
process.exit(1);
});
}
8 changes: 5 additions & 3 deletions packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@
"access": "public"
},
"bin": {
"gittensory-miner": "bin/gittensory-miner.js"
"gittensory-miner": "bin/gittensory-miner.js",
"gittensory-miner-mcp": "bin/gittensory-miner-mcp.js"
},
"files": [
"bin",
"lib",
"expected-engine.version"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/ams-policy.js && node --check lib/attempt-cli.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/coding-task-spec.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/execute-local-write.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint-persisted.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/live-issue-snapshot.js && node --check lib/local-store.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-expiry.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-signal.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-generation.js && node --check lib/repo-clone.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/self-review-context.js && node --check lib/slop-assessment.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
"build": "node --check bin/gittensory-miner.js && node --check bin/gittensory-miner-mcp.js && node --check lib/ams-policy.js && node --check lib/attempt-cli.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/coding-task-spec.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/execute-local-write.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint-persisted.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/live-issue-snapshot.js && node --check lib/local-store.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-expiry.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-signal.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-generation.js && node --check lib/repo-clone.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/self-review-context.js && node --check lib/slop-assessment.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
},
"dependencies": {
"@jsonbored/gittensory-engine": "*"
"@jsonbored/gittensory-engine": "*",
"@modelcontextprotocol/sdk": "1.29.0"
},
"engines": {
"node": ">=22.13.0"
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-miner-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const ALLOWED = [
/^bin\/gittensory-miner\.js$/,
/^bin\/gittensory-miner(-[a-z0-9-]+)?\.(js|d\.ts)$/,
/^lib\/[a-z0-9-]+\.(js|d\.ts)$/,
/^package\.json$/,
/^README\.md$/,
Expand Down
54 changes: 54 additions & 0 deletions test/unit/miner-mcp-scaffold.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { describe, expect, it } from "vitest";
import {
createMinerMcpServer,
MINER_PING_STATUS,
} from "../../packages/gittensory-miner/bin/gittensory-miner-mcp.js";

// Smoke test for the gittensory-miner MCP scaffold (#5153). Drives the real server over an in-memory
// transport (no child process, no AMS state on disk) and exercises the single gittensory_miner_ping tool.

async function connectedClient(): Promise<Client> {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "miner-mcp-test", version: "0.0.0" });
await Promise.all([createMinerMcpServer().connect(serverTransport), client.connect(clientTransport)]);
return client;
}

function pingText(result: { content: Array<{ type: string; text?: string }> }): string {
const first = result.content[0];
if (!first || first.type !== "text" || typeof first.text !== "string") {
throw new Error("expected a single text content block");
}
return first.text;
}

describe("gittensory-miner MCP scaffold (#5153)", () => {
it("exposes exactly the gittensory_miner_ping tool", async () => {
const client = await connectedClient();
const { tools } = await client.listTools();
expect(tools.map((tool) => tool.name)).toEqual(["gittensory_miner_ping"]);
});

it("gittensory_miner_ping returns the static, non-secret status object", async () => {
const client = await connectedClient();
const result = (await client.callTool({ name: "gittensory_miner_ping", arguments: {} })) as {
content: Array<{ type: string; text?: string }>;
};
expect(JSON.parse(pingText(result))).toEqual({ status: "ok", tool: "gittensory_miner_ping" });
expect(JSON.parse(pingText(result))).toEqual(MINER_PING_STATUS);
});

it("returns the same object on every call, with no AMS state required on disk (invariant)", async () => {
const client = await connectedClient();
const first = (await client.callTool({ name: "gittensory_miner_ping", arguments: {} })) as {
content: Array<{ type: string; text?: string }>;
};
const second = (await client.callTool({ name: "gittensory_miner_ping", arguments: {} })) as {
content: Array<{ type: string; text?: string }>;
};
expect(pingText(first)).toBe(pingText(second));
expect(JSON.parse(pingText(first))).toEqual(MINER_PING_STATUS);
});
});
5 changes: 4 additions & 1 deletion test/unit/miner-package-skeleton.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ describe("gittensory-miner package skeleton (#2287)", () => {
expect(miner.name).toBe("@jsonbored/gittensory-miner");
expect(miner.license).toBe("AGPL-3.0-only");
expect(miner.type).toBe("module");
expect(miner.bin).toEqual({ "gittensory-miner": "bin/gittensory-miner.js" });
expect(miner.bin).toEqual({
"gittensory-miner": "bin/gittensory-miner.js",
"gittensory-miner-mcp": "bin/gittensory-miner-mcp.js",
});
expect(miner.publishConfig).toEqual(mcp.publishConfig);
expect(miner.dependencies["@jsonbored/gittensory-engine"]).toBeDefined();
expect(miner.engines.node).toMatch(/^>=22(?:\.\d+){0,2}$/);
Expand Down
Loading