Skip to content

Commit e99c151

Browse files
feat(miner): scaffold the gittensory-miner MCP stdio server (#5254)
Add a second bin entry, gittensory-miner-mcp, mirroring the packages/gittensory-mcp harness (MCP SDK server + stdio transport). It ships exactly one trivial health-check tool, gittensory_miner_ping, returning a static {status,tool} object -- no AMS state read, no arguments -- so future AMS-state-reading tools (status/doctor, portfolio dashboard, claim-ledger listing) have a real server to be added to. - new bin/gittensory-miner-mcp.js (executable; exported factory for in-process tests) - register the bin + @modelcontextprotocol/sdk dependency in package.json (+ lockfile) - smoke test drives the server over an in-memory transport, asserting the ping response, the single-tool listing, and the static invariant with no on-disk AMS state - README pointer for the new entry point and its current single-tool scope Closes #5153 Co-authored-by: jaytbarimbao-collab <300663773+jaytbarimbao-collab@users.noreply.github.com>
1 parent 73a0912 commit e99c151

8 files changed

Lines changed: 142 additions & 7 deletions

File tree

package-lock.json

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/gittensory-miner/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ gittensory-miner manage status [--json]
123123
gittensory-miner manage poll <owner/repo> <pr#> [--branch <name>] [--json]
124124
```
125125

126+
## MCP server
127+
128+
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:
129+
130+
```sh
131+
gittensory-miner-mcp
132+
```
133+
134+
It currently exposes a single tool, `gittensory_miner_ping` — a health check that returns a static
135+
`{ "status": "ok", "tool": "gittensory_miner_ping" }` object, reads no AMS state, and takes no arguments. This is a
136+
scaffold (#5153): real AMS-state-reading tools (status/doctor diagnostics, portfolio dashboard, claim-ledger
137+
listing) land as follow-up PRs on top of it.
138+
126139
## Version check
127140

128141
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`.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
3+
/** The static, non-secret payload the gittensory_miner_ping tool always returns, independent of input. */
4+
export const MINER_PING_STATUS: { status: "ok"; tool: "gittensory_miner_ping" };
5+
6+
/**
7+
* Build the miner MCP server with its single gittensory_miner_ping health-check tool registered. No I/O
8+
* and no AMS-state reads, so a test can drive it over an in-memory transport.
9+
*/
10+
export function createMinerMcpServer(): McpServer;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env node
2+
import { readFileSync, realpathSync } from "node:fs";
3+
import { fileURLToPath } from "node:url";
4+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6+
7+
// Minimal MCP stdio-server scaffold for @jsonbored/gittensory-miner (#5153). Mirrors the
8+
// packages/gittensory-mcp harness (MCP SDK server + stdio transport) but ships exactly ONE trivial
9+
// health-check tool -- gittensory_miner_ping -- returning a static status object. It reads NO AMS
10+
// state and takes no arguments. Future AMS-state-reading tools (status/doctor, portfolio dashboard,
11+
// claim-ledger listing) land as follow-up PRs on top of this scaffold.
12+
13+
// Read the version from this package's own package.json (always shipped) rather than a hand-synced
14+
// literal, so a release bump never has a second place to forget -- same approach as the mcp harness.
15+
const ownPackageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
16+
17+
/** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */
18+
export const MINER_PING_STATUS = { status: "ok", tool: "gittensory_miner_ping" };
19+
20+
/**
21+
* Build the miner MCP server with its single health-check tool registered. No I/O and no AMS-state
22+
* reads, so a test can drive it over an in-memory transport without spawning a process or requiring
23+
* any on-disk state to exist.
24+
*/
25+
export function createMinerMcpServer() {
26+
const server = new McpServer({ name: "gittensory-miner", version: ownPackageJson.version });
27+
server.registerTool(
28+
"gittensory_miner_ping",
29+
{
30+
description:
31+
"Health check for the gittensory-miner MCP server. Returns a static status object confirming the " +
32+
"server is reachable. Reads no AMS state and takes no arguments.",
33+
inputSchema: {},
34+
},
35+
async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] }),
36+
);
37+
return server;
38+
}
39+
40+
// Start the stdio transport only when executed directly as the bin, not when imported by a test.
41+
// realpathSync on both sides resolves the npm bin symlink so a global/npx install still matches.
42+
const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : "";
43+
if (invokedPath && invokedPath === realpathSync(fileURLToPath(import.meta.url))) {
44+
createMinerMcpServer()
45+
.connect(new StdioServerTransport())
46+
.catch((error) => {
47+
console.error(error);
48+
process.exit(1);
49+
});
50+
}

packages/gittensory-miner/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@
2424
"access": "public"
2525
},
2626
"bin": {
27-
"gittensory-miner": "bin/gittensory-miner.js"
27+
"gittensory-miner": "bin/gittensory-miner.js",
28+
"gittensory-miner-mcp": "bin/gittensory-miner-mcp.js"
2829
},
2930
"files": [
3031
"bin",
3132
"lib",
3233
"expected-engine.version"
3334
],
3435
"scripts": {
35-
"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"
36+
"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"
3637
},
3738
"dependencies": {
38-
"@jsonbored/gittensory-engine": "*"
39+
"@jsonbored/gittensory-engine": "*",
40+
"@modelcontextprotocol/sdk": "1.29.0"
3941
},
4042
"engines": {
4143
"node": ">=22.13.0"

scripts/check-miner-package.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { spawnSync } from "node:child_process";
55
import { fileURLToPath } from "node:url";
66

77
const ALLOWED = [
8-
/^bin\/gittensory-miner\.js$/,
8+
/^bin\/gittensory-miner(-[a-z0-9-]+)?\.(js|d\.ts)$/,
99
/^lib\/[a-z0-9-]+\.(js|d\.ts)$/,
1010
/^package\.json$/,
1111
/^README\.md$/,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3+
import { describe, expect, it } from "vitest";
4+
import {
5+
createMinerMcpServer,
6+
MINER_PING_STATUS,
7+
} from "../../packages/gittensory-miner/bin/gittensory-miner-mcp.js";
8+
9+
// Smoke test for the gittensory-miner MCP scaffold (#5153). Drives the real server over an in-memory
10+
// transport (no child process, no AMS state on disk) and exercises the single gittensory_miner_ping tool.
11+
12+
async function connectedClient(): Promise<Client> {
13+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
14+
const client = new Client({ name: "miner-mcp-test", version: "0.0.0" });
15+
await Promise.all([createMinerMcpServer().connect(serverTransport), client.connect(clientTransport)]);
16+
return client;
17+
}
18+
19+
function pingText(result: { content: Array<{ type: string; text?: string }> }): string {
20+
const first = result.content[0];
21+
if (!first || first.type !== "text" || typeof first.text !== "string") {
22+
throw new Error("expected a single text content block");
23+
}
24+
return first.text;
25+
}
26+
27+
describe("gittensory-miner MCP scaffold (#5153)", () => {
28+
it("exposes exactly the gittensory_miner_ping tool", async () => {
29+
const client = await connectedClient();
30+
const { tools } = await client.listTools();
31+
expect(tools.map((tool) => tool.name)).toEqual(["gittensory_miner_ping"]);
32+
});
33+
34+
it("gittensory_miner_ping returns the static, non-secret status object", async () => {
35+
const client = await connectedClient();
36+
const result = (await client.callTool({ name: "gittensory_miner_ping", arguments: {} })) as {
37+
content: Array<{ type: string; text?: string }>;
38+
};
39+
expect(JSON.parse(pingText(result))).toEqual({ status: "ok", tool: "gittensory_miner_ping" });
40+
expect(JSON.parse(pingText(result))).toEqual(MINER_PING_STATUS);
41+
});
42+
43+
it("returns the same object on every call, with no AMS state required on disk (invariant)", async () => {
44+
const client = await connectedClient();
45+
const first = (await client.callTool({ name: "gittensory_miner_ping", arguments: {} })) as {
46+
content: Array<{ type: string; text?: string }>;
47+
};
48+
const second = (await client.callTool({ name: "gittensory_miner_ping", arguments: {} })) as {
49+
content: Array<{ type: string; text?: string }>;
50+
};
51+
expect(pingText(first)).toBe(pingText(second));
52+
expect(JSON.parse(pingText(first))).toEqual(MINER_PING_STATUS);
53+
});
54+
});

test/unit/miner-package-skeleton.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ describe("gittensory-miner package skeleton (#2287)", () => {
3333
expect(miner.name).toBe("@jsonbored/gittensory-miner");
3434
expect(miner.license).toBe("AGPL-3.0-only");
3535
expect(miner.type).toBe("module");
36-
expect(miner.bin).toEqual({ "gittensory-miner": "bin/gittensory-miner.js" });
36+
expect(miner.bin).toEqual({
37+
"gittensory-miner": "bin/gittensory-miner.js",
38+
"gittensory-miner-mcp": "bin/gittensory-miner-mcp.js",
39+
});
3740
expect(miner.publishConfig).toEqual(mcp.publishConfig);
3841
expect(miner.dependencies["@jsonbored/gittensory-engine"]).toBeDefined();
3942
expect(miner.engines.node).toMatch(/^>=22(?:\.\d+){0,2}$/);

0 commit comments

Comments
 (0)