ralph-hero-mcp-server 2.5.191 → 2.5.193
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/lib/dashboard-fetch.js +2 -2
- package/dist/lib/directions.js +6 -0
- package/dist/lib/workflow-states.js +18 -0
- package/dist/tools/issue-tools.js +18 -2
- package/package.json +1 -1
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).
|
|
@@ -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,
|
|
@@ -131,4 +131,22 @@ export const WORKFLOW_STATE_TO_STATUS = {
|
|
|
131
131
|
"Canceled": "Done",
|
|
132
132
|
"Human Needed": "Todo",
|
|
133
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* Reverse-inference map (GH-1471): when a save_issue call closes the GitHub
|
|
136
|
+
* issue but provides no explicit workflowState, infer the matching terminal
|
|
137
|
+
* board state. Keyed by `CLOSED:${stateReason ?? ""}` to mirror the lookup in
|
|
138
|
+
* issue-tools.ts. This is the symmetric inverse of the forward auto-close path.
|
|
139
|
+
*
|
|
140
|
+
* - CLOSED:COMPLETED → Done (issue closed as completed)
|
|
141
|
+
* - CLOSED:NOT_PLANNED → Canceled (issue closed as not planned)
|
|
142
|
+
* - CLOSED: → Done (close with no stateReason defaults to Done)
|
|
143
|
+
*
|
|
144
|
+
* An explicit workflowState always wins — the caller only consults this map
|
|
145
|
+
* when args.workflowState is undefined.
|
|
146
|
+
*/
|
|
147
|
+
export const ISSUE_STATE_TO_TERMINAL_WORKFLOW = {
|
|
148
|
+
"CLOSED:COMPLETED": "Done",
|
|
149
|
+
"CLOSED:NOT_PLANNED": "Canceled",
|
|
150
|
+
"CLOSED:": "Done",
|
|
151
|
+
};
|
|
134
152
|
//# sourceMappingURL=workflow-states.js.map
|
|
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
import { paginateConnection } from "../lib/pagination.js";
|
|
9
9
|
import { detectGroup } from "../lib/group-detection.js";
|
|
10
10
|
import { detectPipelinePosition, OVERSIZED_ESTIMATES, } from "../lib/pipeline-detection.js";
|
|
11
|
-
import { isValidState, isParentGateState, VALID_STATES, LOCK_STATES, TERMINAL_STATES, WORKFLOW_STATE_TO_STATUS, } from "../lib/workflow-states.js";
|
|
11
|
+
import { isValidState, isParentGateState, VALID_STATES, LOCK_STATES, TERMINAL_STATES, WORKFLOW_STATE_TO_STATUS, ISSUE_STATE_TO_TERMINAL_WORKFLOW, } from "../lib/workflow-states.js";
|
|
12
12
|
import { buildBatchMutationQuery } from "./batch-tools.js";
|
|
13
13
|
import { resolveState } from "../lib/state-resolution.js";
|
|
14
14
|
import { parseDateMath } from "../lib/date-math.js";
|
|
@@ -808,6 +808,7 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
808
808
|
"and project field values (workflow state, estimate, priority, iteration) in a single call. " +
|
|
809
809
|
"Supports semantic intents (__LOCK__, __COMPLETE__, etc.) for workflowState. " +
|
|
810
810
|
"Auto-closes the GitHub issue when workflowState resolves to a terminal state (Done, Canceled) unless issueState is explicitly set. " +
|
|
811
|
+
"Reverse inference: when issueState closes the issue (CLOSED→Done, CLOSED_NOT_PLANNED→Canceled) and no workflowState is provided, the board is advanced to the matching terminal state automatically. Explicit workflowState always wins. " +
|
|
811
812
|
"Set estimate, priority, or iteration to null to clear the field. Use @current/@next tokens for iteration. " +
|
|
812
813
|
"Returns: number, url, changes.", {
|
|
813
814
|
owner: z.string().optional().describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
@@ -887,6 +888,20 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
887
888
|
stateReason = resolvedWorkflowState === "Canceled" ? "NOT_PLANNED" : "COMPLETED";
|
|
888
889
|
changes.autoClose = true;
|
|
889
890
|
}
|
|
891
|
+
// Reverse inference: if issueState closes the issue and no explicit workflowState,
|
|
892
|
+
// default the board to the matching terminal workflow state (Done or Canceled).
|
|
893
|
+
// This is the symmetric inverse of the forward auto-close path above.
|
|
894
|
+
// Explicit workflowState always wins — this only fires when args.workflowState is absent.
|
|
895
|
+
let inferredFromClose = false;
|
|
896
|
+
if (args.workflowState === undefined && targetState === "CLOSED") {
|
|
897
|
+
const key = `CLOSED:${stateReason ?? ""}`;
|
|
898
|
+
const inferred = ISSUE_STATE_TO_TERMINAL_WORKFLOW[key];
|
|
899
|
+
if (inferred) {
|
|
900
|
+
resolvedWorkflowState = inferred;
|
|
901
|
+
inferredFromClose = true;
|
|
902
|
+
changes.workflowStateInferred = inferred;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
890
905
|
// 3. Issue state mutations (close/reopen) - use dedicated mutations
|
|
891
906
|
const hasMetadataFields = args.title !== undefined || args.body !== undefined ||
|
|
892
907
|
args.labels !== undefined || args.assignees !== undefined;
|
|
@@ -982,7 +997,8 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
982
997
|
}
|
|
983
998
|
}
|
|
984
999
|
// 4. Project-field mutations (aliased batch for workflow state + status sync + estimate + priority)
|
|
985
|
-
|
|
1000
|
+
// Also fires when workflowState was inferred from issueState (reverse-close inference).
|
|
1001
|
+
if (hasProjectFields || inferredFromClose) {
|
|
986
1002
|
const { projectNumber, projectOwner } = resolveFullConfig(client, args);
|
|
987
1003
|
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
988
1004
|
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number, projectNumber);
|