ralph-hero-mcp-server 2.5.190 → 2.5.192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/index.js +0 -3
- package/dist/lib/dashboard-fetch.js +2 -2
- package/dist/lib/directions.js +6 -0
- package/dist/lib/repo-registry.js +1 -1
- package/package.json +1 -1
- package/dist/lib/delegation-log.js +0 -199
- package/dist/tools/delegation-tools.js +0 -44
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ralph-hero-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for GitHub Projects V2 — the workflow-automation engine behind the [Ralph](https://github.com/cdubiel08/ralph-hero) Claude Code plugin.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
`ralph-hero-mcp-server` exposes [GitHub Projects V2](https://docs.github.com/en/issues/planning-and-tracking-with-projects) as a set of [Model Context Protocol](https://modelcontextprotocol.io/) tools, so an agent (Claude Code) can read and drive an issue through a workflow state machine — `Backlog → Research Needed → … → In Review → Done` — entirely through typed tool calls instead of shelling out to `gh`.
|
|
8
|
+
|
|
9
|
+
It is bundled and consumed by the `ralph` Claude Code plugin (the skills call these tools), but it is a standalone stdio MCP server and can be wired into any MCP client.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
The server is published to npm and is normally run via `npx` from an MCP client config (`.mcp.json`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"ralph-github": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "ralph-hero-mcp-server"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
All tools are namespaced with the `ralph_hero__` prefix (e.g. `ralph_hero__get_issue`, `ralph_hero__save_issue`, `ralph_hero__next_actions`).
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Configuration flows through the parent process's environment (the `.mcp.json` has **no** `env` block — do not put tokens there).
|
|
31
|
+
|
|
32
|
+
| Variable | Required | Description |
|
|
33
|
+
|----------|----------|-------------|
|
|
34
|
+
| `RALPH_GH_OWNER` | Yes | GitHub owner (user or org). |
|
|
35
|
+
| `RALPH_GH_PROJECT_NUMBER` | Yes | GitHub Projects V2 number. |
|
|
36
|
+
| `RALPH_GH_REPO` | No | Repository name (inferred from the project if omitted). |
|
|
37
|
+
| `RALPH_HERO_GITHUB_TOKEN` | No | GitHub PAT with `repo` + `project` scopes. **Falls back to `gh auth token`** when unset — so with `gh auth login -s repo,project,read:org` you usually need no token in any config. |
|
|
38
|
+
| `RALPH_GH_PROJECT_OWNER` | No | Project owner, if different from the repo owner (split-owner setups). |
|
|
39
|
+
|
|
40
|
+
## Tool architecture
|
|
41
|
+
|
|
42
|
+
Each tool module exports a `registerXyzTools()` function that registers tools onto the MCP server. All tools use the `ralph_hero__` prefix and return via `toolSuccess()` / `toolError()`. Modules cover issues (`get_issue`, `save_issue`, `list_issues`), projects, relationships (`add_sub_issue`, `add_dependency`, `advance_issue`), dashboards (`pipeline_dashboard`, `next_actions`), trends, and more.
|
|
43
|
+
|
|
44
|
+
For the full module/tool inventory and internals (GitHub client dual-endpoint design, caching, the workflow state machine), see [the repo's CLAUDE.md § "MCP Server Internals"](https://github.com/cdubiel08/ralph-hero/blob/main/CLAUDE.md#mcp-server-internals).
|
|
45
|
+
|
|
46
|
+
## Build & test
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install
|
|
50
|
+
npm run build # TypeScript -> dist/ (tsc)
|
|
51
|
+
npm test # vitest
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The server is ESM (`"type": "module"`, `"module": "NodeNext"`) — internal imports use `.js` extensions. TypeScript strict mode is the primary quality gate (no linter).
|
|
55
|
+
|
|
56
|
+
## Links
|
|
57
|
+
|
|
58
|
+
- Repository: <https://github.com/cdubiel08/ralph-hero>
|
|
59
|
+
- Plugin + workflow docs: <https://github.com/cdubiel08/ralph-hero/blob/main/README.md>
|
|
60
|
+
- Issues: <https://github.com/cdubiel08/ralph-hero/issues>
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
See the [repository](https://github.com/cdubiel08/ralph-hero).
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,6 @@ import { registerDecomposeTools } from "./tools/decompose-tools.js";
|
|
|
31
31
|
import { registerViewTools } from "./tools/view-tools.js";
|
|
32
32
|
import { registerPlanGraphTools } from "./tools/plan-graph-tools.js";
|
|
33
33
|
import { registerActivityTools } from "./tools/activity-tools.js";
|
|
34
|
-
import { registerDelegationTools } from "./tools/delegation-tools.js";
|
|
35
34
|
import { registerTrendsTools } from "./tools/trends-tools.js";
|
|
36
35
|
import { registerSreTools } from "./tools/sre-tools.js";
|
|
37
36
|
/**
|
|
@@ -447,8 +446,6 @@ async function main() {
|
|
|
447
446
|
registerPlanGraphTools(server, client);
|
|
448
447
|
// Activity log reader (recent_activity tool — pure filesystem, no GitHub client)
|
|
449
448
|
registerActivityTools(server);
|
|
450
|
-
// Delegation telemetry reader (delegation_stats tool — pure filesystem, no GitHub client)
|
|
451
|
-
registerDelegationTools(server);
|
|
452
449
|
// Trends tools (capture_snapshot — JSONL persistence under ~/.ralph-hero/snapshots/)
|
|
453
450
|
registerTrendsTools(server, client, fieldCache);
|
|
454
451
|
// SRE operation tools (kubectl autoremediation — typed argv, no-shell invariant)
|
|
@@ -51,7 +51,7 @@ export function toDashboardItems(raw, projectNumber, projectTitle) {
|
|
|
51
51
|
estimate: getFieldValue(r, "Estimate"),
|
|
52
52
|
assignees: r.content.assignees?.nodes?.map((a) => a.login) ?? [],
|
|
53
53
|
subIssueCount: r.content.subIssues?.totalCount ?? 0,
|
|
54
|
-
blockedBy: r.content.
|
|
54
|
+
blockedBy: r.content.blockedBy?.nodes?.map((n) => ({
|
|
55
55
|
number: n.number,
|
|
56
56
|
workflowState: n.state === "CLOSED" ? "Done" : null,
|
|
57
57
|
})) ?? [],
|
|
@@ -93,7 +93,7 @@ export const DASHBOARD_ITEMS_QUERY = `query($projectId: ID!, $cursor: String, $f
|
|
|
93
93
|
assignees(first: 5) { nodes { login } }
|
|
94
94
|
repository { nameWithOwner name }
|
|
95
95
|
subIssues { totalCount }
|
|
96
|
-
|
|
96
|
+
blockedBy(first: 20) { nodes { number state } }
|
|
97
97
|
trackedInIssues(first: 3) { nodes { number state closedAt } }
|
|
98
98
|
}
|
|
99
99
|
... on PullRequest {
|
package/dist/lib/directions.js
CHANGED
|
@@ -541,6 +541,12 @@ export function rankDirections(items, openPRs, config) {
|
|
|
541
541
|
if (item.workflowState !== "Backlog" && item.workflowState !== null) {
|
|
542
542
|
continue;
|
|
543
543
|
}
|
|
544
|
+
// Defense-in-depth: skip blocked items in the fallback loop so a
|
|
545
|
+
// dependency-blocked Backlog issue never enters scored even when the
|
|
546
|
+
// primary filter (step 2 below) would catch it anyway.
|
|
547
|
+
if (hasOpenBlockers(item)) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
544
550
|
const { score, kind, tags, signals } = scoreIssue(item, items, config);
|
|
545
551
|
scored.push({
|
|
546
552
|
item,
|
package/package.json
CHANGED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure read library for the local ralph-delegate JSONL audit log.
|
|
3
|
-
*
|
|
4
|
-
* The log lives at `~/.ralph-hero/delegate.log` (overridable via
|
|
5
|
-
* `RALPH_DELEGATE_LOG_PATH`). One JSON object per line, append-only,
|
|
6
|
-
* written by `plugin/ralph-hero/scripts/ralph-delegate.sh`. This library
|
|
7
|
-
* only reads — it never writes.
|
|
8
|
-
*
|
|
9
|
-
* Schema versioning: F1 (issue #1185) does NOT emit an explicit
|
|
10
|
-
* `schemaVersion` field on each line. F5 treats lines containing the
|
|
11
|
-
* required fields `{ts, task, status, ms}` as **implicit v1**. A future
|
|
12
|
-
* issue MAY add an explicit `schemaVersion >= 2` — when that happens, the
|
|
13
|
-
* shape-check should be expanded to honor it. TODO: revisit when the
|
|
14
|
-
* producer side emits `schemaVersion`.
|
|
15
|
-
*
|
|
16
|
-
* Determinism: pure functions. Filesystem reads are the only side effect.
|
|
17
|
-
* Missing log file resolves to a zero-state result with no throw, matching
|
|
18
|
-
* the activity.ts precedent (the steady state for opt-out users).
|
|
19
|
-
*/
|
|
20
|
-
import * as fs from "node:fs/promises";
|
|
21
|
-
import * as os from "node:os";
|
|
22
|
-
import * as path from "node:path";
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Test hook + default path resolution
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
/**
|
|
27
|
-
* Optional override used only by tests. Production code never sets this.
|
|
28
|
-
* Mirrors `__setSnapshotRoot` in `snapshots.ts`.
|
|
29
|
-
*/
|
|
30
|
-
let delegateLogPathOverride = null;
|
|
31
|
-
/** Test hook: override the default log path. Pass `null` to restore. */
|
|
32
|
-
export function __setDelegateLogPath(path) {
|
|
33
|
-
delegateLogPathOverride = path;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Resolve the default log path: env var override, then `~/.ralph-hero/delegate.log`.
|
|
37
|
-
* Expands a leading `~/` (mirrors `ralph-delegate.sh:208-211`).
|
|
38
|
-
*/
|
|
39
|
-
export function defaultDelegationLogPath() {
|
|
40
|
-
if (delegateLogPathOverride !== null)
|
|
41
|
-
return delegateLogPathOverride;
|
|
42
|
-
const fromEnv = process.env.RALPH_DELEGATE_LOG_PATH;
|
|
43
|
-
if (fromEnv && fromEnv.length > 0) {
|
|
44
|
-
return expandHome(fromEnv);
|
|
45
|
-
}
|
|
46
|
-
return path.join(os.homedir(), ".ralph-hero", "delegate.log");
|
|
47
|
-
}
|
|
48
|
-
function expandHome(p) {
|
|
49
|
-
if (p.startsWith("~/")) {
|
|
50
|
-
return path.join(os.homedir(), p.slice(2));
|
|
51
|
-
}
|
|
52
|
-
return p;
|
|
53
|
-
}
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Reader
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
/**
|
|
58
|
-
* Read the delegation audit log. Returns a zero-state result with
|
|
59
|
-
* `fileExists: false` when the file is missing — never throws on ENOENT.
|
|
60
|
-
* Lines that fail JSON.parse OR lack required fields are skipped and
|
|
61
|
-
* counted in `skippedLines`; each skip emits a `console.warn` with a
|
|
62
|
-
* truncated prefix of the offending line.
|
|
63
|
-
*/
|
|
64
|
-
export async function readDelegationLog(config) {
|
|
65
|
-
const logPath = config.logPath;
|
|
66
|
-
let content;
|
|
67
|
-
try {
|
|
68
|
-
content = await fs.readFile(logPath, "utf8");
|
|
69
|
-
}
|
|
70
|
-
catch (e) {
|
|
71
|
-
if (e &&
|
|
72
|
-
typeof e === "object" &&
|
|
73
|
-
"code" in e &&
|
|
74
|
-
e.code === "ENOENT") {
|
|
75
|
-
return { events: [], skippedLines: 0, fileExists: false, logPath };
|
|
76
|
-
}
|
|
77
|
-
throw e;
|
|
78
|
-
}
|
|
79
|
-
const events = [];
|
|
80
|
-
let skipped = 0;
|
|
81
|
-
for (const raw of content.split("\n")) {
|
|
82
|
-
const line = raw.trim();
|
|
83
|
-
if (line.length === 0)
|
|
84
|
-
continue;
|
|
85
|
-
let parsed;
|
|
86
|
-
try {
|
|
87
|
-
parsed = JSON.parse(line);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
skipped++;
|
|
91
|
-
console.warn(`[delegation-log] Skipping malformed line in ${logPath}: ${line.slice(0, 80)}`);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (!isDelegationEventShape(parsed)) {
|
|
95
|
-
skipped++;
|
|
96
|
-
console.warn(`[delegation-log] Skipping line with missing required fields in ${logPath}: ${line.slice(0, 80)}`);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
events.push(parsed);
|
|
100
|
-
}
|
|
101
|
-
return { events, skippedLines: skipped, fileExists: true, logPath };
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Implicit-v1 shape check: presence of `{ts, task, status, ms}` with
|
|
105
|
-
* correct primitive types. A future explicit `schemaVersion >= 2` may
|
|
106
|
-
* extend this gate.
|
|
107
|
-
*/
|
|
108
|
-
function isDelegationEventShape(v) {
|
|
109
|
-
if (!v || typeof v !== "object")
|
|
110
|
-
return false;
|
|
111
|
-
const o = v;
|
|
112
|
-
return (typeof o.ts === "string" &&
|
|
113
|
-
typeof o.task === "string" &&
|
|
114
|
-
typeof o.status === "string" &&
|
|
115
|
-
typeof o.ms === "number");
|
|
116
|
-
}
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Aggregator
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
/**
|
|
121
|
-
* Aggregate parsed events into per-task + totals. Pure function — does
|
|
122
|
-
* no I/O. Percentiles use the nearest-rank method against successful
|
|
123
|
-
* (status=ok) calls only.
|
|
124
|
-
*/
|
|
125
|
-
export function aggregateDelegationStats(events) {
|
|
126
|
-
const byTask = {};
|
|
127
|
-
const okMsByTask = {};
|
|
128
|
-
let totalCalls = 0;
|
|
129
|
-
let totalFallbacks = 0;
|
|
130
|
-
let totalBytesIn = 0;
|
|
131
|
-
let totalBytesOut = 0;
|
|
132
|
-
for (const ev of events) {
|
|
133
|
-
const task = ev.task;
|
|
134
|
-
if (!byTask[task]) {
|
|
135
|
-
byTask[task] = {
|
|
136
|
-
calls: 0,
|
|
137
|
-
fallbacks: 0,
|
|
138
|
-
p50Ms: null,
|
|
139
|
-
p99Ms: null,
|
|
140
|
-
bytesIn: 0,
|
|
141
|
-
bytesOut: 0,
|
|
142
|
-
tokens: null,
|
|
143
|
-
};
|
|
144
|
-
okMsByTask[task] = [];
|
|
145
|
-
}
|
|
146
|
-
byTask[task].calls += 1;
|
|
147
|
-
totalCalls += 1;
|
|
148
|
-
if (isFallbackStatus(ev.status)) {
|
|
149
|
-
byTask[task].fallbacks += 1;
|
|
150
|
-
totalFallbacks += 1;
|
|
151
|
-
}
|
|
152
|
-
const bIn = typeof ev.bytes_in === "number" ? ev.bytes_in : 0;
|
|
153
|
-
const bOut = typeof ev.bytes_out === "number" ? ev.bytes_out : 0;
|
|
154
|
-
byTask[task].bytesIn += bIn;
|
|
155
|
-
byTask[task].bytesOut += bOut;
|
|
156
|
-
totalBytesIn += bIn;
|
|
157
|
-
totalBytesOut += bOut;
|
|
158
|
-
if (ev.status === "ok") {
|
|
159
|
-
okMsByTask[task].push(ev.ms);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Compute percentiles
|
|
163
|
-
for (const task of Object.keys(byTask)) {
|
|
164
|
-
const samples = okMsByTask[task];
|
|
165
|
-
byTask[task].p50Ms = percentile(samples, 0.5);
|
|
166
|
-
byTask[task].p99Ms = percentile(samples, 0.99);
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
totals: {
|
|
170
|
-
calls: totalCalls,
|
|
171
|
-
fallbacks: totalFallbacks,
|
|
172
|
-
bytesIn: totalBytesIn,
|
|
173
|
-
bytesOut: totalBytesOut,
|
|
174
|
-
},
|
|
175
|
-
byTask,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* A fallback is any non-`ok`, non-`dry_run` status: timeout, unreachable,
|
|
180
|
-
* parse_error, http_*. `dry_run` is operator-driven and not a real
|
|
181
|
-
* delegation failure, so it does not count.
|
|
182
|
-
*/
|
|
183
|
-
function isFallbackStatus(status) {
|
|
184
|
-
return status !== "ok" && status !== "dry_run";
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Nearest-rank percentile: `sorted[ceil(q * n) - 1]`. Returns `null`
|
|
188
|
-
* when the sample is empty.
|
|
189
|
-
*/
|
|
190
|
-
function percentile(samples, q) {
|
|
191
|
-
if (samples.length === 0)
|
|
192
|
-
return null;
|
|
193
|
-
const sorted = [...samples].sort((a, b) => a - b);
|
|
194
|
-
const rank = Math.ceil(q * sorted.length);
|
|
195
|
-
// Guard the edge case q=0 (would index -1)
|
|
196
|
-
const idx = Math.max(0, rank - 1);
|
|
197
|
-
return sorted[idx];
|
|
198
|
-
}
|
|
199
|
-
//# sourceMappingURL=delegation-log.js.map
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registers the `ralph_hero__delegation_stats` MCP tool. Pure read-only
|
|
3
|
-
* surface over the JSONL audit log written by `ralph-delegate.sh` (F1).
|
|
4
|
-
*
|
|
5
|
-
* Follows the same registration convention as `activity-tools.ts`: no
|
|
6
|
-
* GitHub client argument, defaults pulled from env var with a homedir
|
|
7
|
-
* fallback, returns `toolSuccess` on missing-file (zero-state) so callers
|
|
8
|
-
* can render a dashboard without an error path.
|
|
9
|
-
*/
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
import { readDelegationLog, aggregateDelegationStats, defaultDelegationLogPath, } from "../lib/delegation-log.js";
|
|
12
|
-
import { toolSuccess, toolError } from "../types.js";
|
|
13
|
-
const TOKENS_REASON = "F1 audit-log does not capture token usage; bytes used as a proxy";
|
|
14
|
-
export function registerDelegationTools(server) {
|
|
15
|
-
server.tool("ralph_hero__delegation_stats", "Read-only telemetry over the local ralph-delegate JSONL audit log. Returns per-task call counts, fallback counts (non-ok/non-dry_run), p50/p99 latency from successful calls, and bytes_in/bytes_out aggregates. Reads RALPH_DELEGATE_LOG_PATH (default ~/.ralph-hero/delegate.log). Missing log file returns a zero-state result, never errors.", {
|
|
16
|
-
logPath: z
|
|
17
|
-
.string()
|
|
18
|
-
.optional()
|
|
19
|
-
.describe("Optional override for the JSONL log path. Defaults to RALPH_DELEGATE_LOG_PATH or ~/.ralph-hero/delegate.log."),
|
|
20
|
-
}, async (params) => {
|
|
21
|
-
try {
|
|
22
|
-
const resolvedPath = params.logPath ?? defaultDelegationLogPath();
|
|
23
|
-
const read = await readDelegationLog({ logPath: resolvedPath });
|
|
24
|
-
const stats = aggregateDelegationStats(read.events);
|
|
25
|
-
return toolSuccess({
|
|
26
|
-
logPath: read.logPath,
|
|
27
|
-
fileExists: read.fileExists,
|
|
28
|
-
totals: {
|
|
29
|
-
calls: stats.totals.calls,
|
|
30
|
-
fallbacks: stats.totals.fallbacks,
|
|
31
|
-
bytesIn: stats.totals.bytesIn,
|
|
32
|
-
bytesOut: stats.totals.bytesOut,
|
|
33
|
-
skippedLines: read.skippedLines,
|
|
34
|
-
},
|
|
35
|
-
byTask: stats.byTask,
|
|
36
|
-
tokensReason: TOKENS_REASON,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
//# sourceMappingURL=delegation-tools.js.map
|