ralph-hero-mcp-server 1.3.2 → 1.3.3

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/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import { registerIssueTools } from "./tools/issue-tools.js";
17
17
  import { registerRelationshipTools } from "./tools/relationship-tools.js";
18
18
  import { registerDashboardTools } from "./tools/dashboard-tools.js";
19
19
  import { registerBatchTools } from "./tools/batch-tools.js";
20
+ import { registerProjectManagementTools } from "./tools/project-management-tools.js";
20
21
  /**
21
22
  * Initialize the GitHub client from environment variables.
22
23
  */
@@ -242,6 +243,8 @@ async function main() {
242
243
  registerDashboardTools(server, client, fieldCache);
243
244
  // Phase 5: Batch operations
244
245
  registerBatchTools(server, client, fieldCache);
246
+ // Project management tools (archive, remove, add, link repo, clear field)
247
+ registerProjectManagementTools(server, client, fieldCache);
245
248
  // Connect via stdio transport
246
249
  const transport = new StdioServerTransport();
247
250
  await server.connect(transport);
@@ -6,6 +6,7 @@
6
6
  * implementations to their original versions.
7
7
  */
8
8
  import { resolveProjectOwner } from "../types.js";
9
+ import { WORKFLOW_STATE_TO_STATUS } from "./workflow-states.js";
9
10
  // ---------------------------------------------------------------------------
10
11
  // Helper: Fetch project data for field cache population
11
12
  // ---------------------------------------------------------------------------
@@ -197,4 +198,28 @@ export function resolveFullConfig(client, args) {
197
198
  }
198
199
  return { owner, repo, projectNumber, projectOwner };
199
200
  }
201
+ // ---------------------------------------------------------------------------
202
+ // Helper: Sync default Status field after Workflow State change
203
+ // ---------------------------------------------------------------------------
204
+ /**
205
+ * Sync the default Status field to match a Workflow State change.
206
+ * Best-effort: logs warning on failure but does not throw.
207
+ */
208
+ export async function syncStatusField(client, fieldCache, projectItemId, workflowState) {
209
+ const targetStatus = WORKFLOW_STATE_TO_STATUS[workflowState];
210
+ if (!targetStatus)
211
+ return;
212
+ const statusFieldId = fieldCache.getFieldId("Status");
213
+ if (!statusFieldId)
214
+ return;
215
+ const statusOptionId = fieldCache.resolveOptionId("Status", targetStatus);
216
+ if (!statusOptionId)
217
+ return;
218
+ try {
219
+ await updateProjectItemField(client, fieldCache, projectItemId, "Status", targetStatus);
220
+ }
221
+ catch {
222
+ // Best-effort sync - don't fail the primary operation
223
+ }
224
+ }
200
225
  //# sourceMappingURL=helpers.js.map
@@ -79,4 +79,26 @@ export function isEarlierState(a, b) {
79
79
  export function isValidState(state) {
80
80
  return VALID_STATES.includes(state);
81
81
  }
82
+ /**
83
+ * Maps Ralph Workflow States to GitHub's default Status field values.
84
+ * Used for one-way sync: Workflow State changes -> Status field updates.
85
+ *
86
+ * Rationale:
87
+ * - Todo = work not yet actively started (queued states)
88
+ * - In Progress = work actively being processed (lock states + review)
89
+ * - Done = terminal/escalated states (no automated progression)
90
+ */
91
+ export const WORKFLOW_STATE_TO_STATUS = {
92
+ "Backlog": "Todo",
93
+ "Research Needed": "Todo",
94
+ "Ready for Plan": "Todo",
95
+ "Plan in Review": "Todo",
96
+ "Research in Progress": "In Progress",
97
+ "Plan in Progress": "In Progress",
98
+ "In Progress": "In Progress",
99
+ "In Review": "In Progress",
100
+ "Done": "Done",
101
+ "Canceled": "Done",
102
+ "Human Needed": "Done",
103
+ };
82
104
  //# sourceMappingURL=workflow-states.js.map
@@ -6,7 +6,7 @@
6
6
  * instead of one per issue).
7
7
  */
8
8
  import { z } from "zod";
9
- import { isEarlierState } from "../lib/workflow-states.js";
9
+ import { isEarlierState, WORKFLOW_STATE_TO_STATUS } from "../lib/workflow-states.js";
10
10
  import { toolSuccess, toolError } from "../types.js";
11
11
  import { ensureFieldCache, resolveFullConfig, } from "../lib/helpers.js";
12
12
  // ---------------------------------------------------------------------------
@@ -267,6 +267,27 @@ export function registerBatchTools(server, client, fieldCache) {
267
267
  field: op.field,
268
268
  value: op.value,
269
269
  });
270
+ // For workflow_state operations, also sync the default Status field
271
+ if (op.field === "workflow_state") {
272
+ const targetStatus = WORKFLOW_STATE_TO_STATUS[op.value];
273
+ if (targetStatus) {
274
+ const statusFieldId = fieldCache.getFieldId("Status");
275
+ const statusOptionId = statusFieldId
276
+ ? fieldCache.resolveOptionId("Status", targetStatus)
277
+ : undefined;
278
+ if (statusFieldId && statusOptionId) {
279
+ updates.push({
280
+ alias: `s${num}_${opIdx}`,
281
+ itemId: issue.projectItemId,
282
+ fieldId: statusFieldId,
283
+ optionId: statusOptionId,
284
+ issueNumber: num,
285
+ field: "status_sync",
286
+ value: targetStatus,
287
+ });
288
+ }
289
+ }
290
+ }
270
291
  }
271
292
  }
272
293
  // Chunk mutations if needed
@@ -11,7 +11,7 @@ import { detectPipelinePosition, } from "../lib/pipeline-detection.js";
11
11
  import { isValidState, VALID_STATES, LOCK_STATES, } from "../lib/workflow-states.js";
12
12
  import { resolveState } from "../lib/state-resolution.js";
13
13
  import { toolSuccess, toolError } from "../types.js";
14
- import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, resolveFullConfig, } from "../lib/helpers.js";
14
+ import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, resolveFullConfig, syncStatusField, } from "../lib/helpers.js";
15
15
  // ---------------------------------------------------------------------------
16
16
  // Register issue tools
17
17
  // ---------------------------------------------------------------------------
@@ -607,6 +607,8 @@ export function registerIssueTools(server, client, fieldCache) {
607
607
  const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
608
608
  // Update the field with the resolved state
609
609
  await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", resolvedState);
610
+ // Sync default Status field (best-effort, one-way)
611
+ await syncStatusField(client, fieldCache, projectItemId, resolvedState);
610
612
  const result = {
611
613
  number: args.number,
612
614
  previousState: previousState || "(unknown)",
@@ -0,0 +1,248 @@
1
+ /**
2
+ * MCP tools for GitHub Projects V2 management operations.
3
+ *
4
+ * Provides tools for archiving/unarchiving items, removing items from projects,
5
+ * adding existing issues to projects, linking repositories, and clearing field values.
6
+ */
7
+ import { z } from "zod";
8
+ import { toolSuccess, toolError } from "../types.js";
9
+ import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, resolveFullConfig, } from "../lib/helpers.js";
10
+ // ---------------------------------------------------------------------------
11
+ // Register project management tools
12
+ // ---------------------------------------------------------------------------
13
+ export function registerProjectManagementTools(server, client, fieldCache) {
14
+ // -------------------------------------------------------------------------
15
+ // ralph_hero__archive_item
16
+ // -------------------------------------------------------------------------
17
+ server.tool("ralph_hero__archive_item", "Archive or unarchive a project item. Archived items are hidden from default views but not deleted. Returns: number, archived, projectItemId.", {
18
+ owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
19
+ repo: z.string().optional().describe("Repository name. Defaults to env var"),
20
+ number: z.number().describe("Issue number"),
21
+ unarchive: z.boolean().optional().default(false)
22
+ .describe("If true, unarchive instead of archive (default: false)"),
23
+ }, async (args) => {
24
+ try {
25
+ const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
26
+ await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
27
+ const projectId = fieldCache.getProjectId();
28
+ if (!projectId) {
29
+ return toolError("Could not resolve project ID");
30
+ }
31
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
32
+ if (args.unarchive) {
33
+ await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
34
+ unarchiveProjectV2Item(input: {
35
+ projectId: $projectId,
36
+ itemId: $itemId
37
+ }) {
38
+ item { id }
39
+ }
40
+ }`, { projectId, itemId: projectItemId });
41
+ }
42
+ else {
43
+ await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
44
+ archiveProjectV2Item(input: {
45
+ projectId: $projectId,
46
+ itemId: $itemId
47
+ }) {
48
+ item { id }
49
+ }
50
+ }`, { projectId, itemId: projectItemId });
51
+ }
52
+ return toolSuccess({
53
+ number: args.number,
54
+ archived: !args.unarchive,
55
+ projectItemId,
56
+ });
57
+ }
58
+ catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ return toolError(`Failed to ${args.unarchive ? "unarchive" : "archive"} item: ${message}`);
61
+ }
62
+ });
63
+ // -------------------------------------------------------------------------
64
+ // ralph_hero__remove_from_project
65
+ // -------------------------------------------------------------------------
66
+ server.tool("ralph_hero__remove_from_project", "Remove an issue from the project. This deletes the project item (not the issue itself). Returns: number, removed.", {
67
+ owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
68
+ repo: z.string().optional().describe("Repository name. Defaults to env var"),
69
+ number: z.number().describe("Issue number"),
70
+ }, async (args) => {
71
+ try {
72
+ const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
73
+ await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
74
+ const projectId = fieldCache.getProjectId();
75
+ if (!projectId) {
76
+ return toolError("Could not resolve project ID");
77
+ }
78
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
79
+ await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!) {
80
+ deleteProjectV2Item(input: {
81
+ projectId: $projectId,
82
+ itemId: $itemId
83
+ }) {
84
+ deletedItemId
85
+ }
86
+ }`, { projectId, itemId: projectItemId });
87
+ // Invalidate cached project item ID since it no longer exists
88
+ client.getCache().invalidate(`project-item-id:${owner}/${repo}#${args.number}`);
89
+ return toolSuccess({
90
+ number: args.number,
91
+ removed: true,
92
+ });
93
+ }
94
+ catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ return toolError(`Failed to remove from project: ${message}`);
97
+ }
98
+ });
99
+ // -------------------------------------------------------------------------
100
+ // ralph_hero__add_to_project
101
+ // -------------------------------------------------------------------------
102
+ server.tool("ralph_hero__add_to_project", "Add an existing issue to the project. The issue must already exist in the repository. Returns: number, projectItemId, added.", {
103
+ owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
104
+ repo: z.string().optional().describe("Repository name. Defaults to env var"),
105
+ number: z.number().describe("Issue number"),
106
+ }, async (args) => {
107
+ try {
108
+ const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
109
+ await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
110
+ const projectId = fieldCache.getProjectId();
111
+ if (!projectId) {
112
+ return toolError("Could not resolve project ID");
113
+ }
114
+ const issueNodeId = await resolveIssueNodeId(client, owner, repo, args.number);
115
+ const result = await client.projectMutate(`mutation($projectId: ID!, $contentId: ID!) {
116
+ addProjectV2ItemById(input: {
117
+ projectId: $projectId,
118
+ contentId: $contentId
119
+ }) {
120
+ item { id }
121
+ }
122
+ }`, { projectId, contentId: issueNodeId });
123
+ const projectItemId = result.addProjectV2ItemById.item.id;
124
+ // Cache the new project item ID
125
+ client.getCache().set(`project-item-id:${owner}/${repo}#${args.number}`, projectItemId, 30 * 60 * 1000);
126
+ return toolSuccess({
127
+ number: args.number,
128
+ projectItemId,
129
+ added: true,
130
+ });
131
+ }
132
+ catch (error) {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ return toolError(`Failed to add to project: ${message}`);
135
+ }
136
+ });
137
+ // -------------------------------------------------------------------------
138
+ // ralph_hero__link_repository
139
+ // -------------------------------------------------------------------------
140
+ server.tool("ralph_hero__link_repository", "Link or unlink a repository to/from the project. Linked repositories enable auto-add workflows and issue filtering. Returns: repository, linked.", {
141
+ owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
142
+ repoToLink: z.string()
143
+ .describe("Repository to link, as 'owner/name' or just 'name' (uses default owner)"),
144
+ unlink: z.boolean().optional().default(false)
145
+ .describe("If true, unlink instead of link (default: false)"),
146
+ }, async (args) => {
147
+ try {
148
+ const { projectNumber, projectOwner } = resolveFullConfig(client, args);
149
+ await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
150
+ const projectId = fieldCache.getProjectId();
151
+ if (!projectId) {
152
+ return toolError("Could not resolve project ID");
153
+ }
154
+ // Parse repoToLink: "owner/name" or just "name" (using default owner)
155
+ let repoOwner;
156
+ let repoName;
157
+ if (args.repoToLink.includes("/")) {
158
+ const parts = args.repoToLink.split("/");
159
+ repoOwner = parts[0];
160
+ repoName = parts[1];
161
+ }
162
+ else {
163
+ repoOwner = client.config.owner || projectOwner;
164
+ repoName = args.repoToLink;
165
+ }
166
+ // Resolve repository node ID
167
+ const repoResult = await client.query(`query($repoOwner: String!, $repoName: String!) {
168
+ repository(owner: $repoOwner, name: $repoName) { id }
169
+ }`, { repoOwner, repoName }, { cache: true, cacheTtlMs: 60 * 60 * 1000 });
170
+ const repoId = repoResult.repository?.id;
171
+ if (!repoId) {
172
+ return toolError(`Repository ${repoOwner}/${repoName} not found`);
173
+ }
174
+ if (args.unlink) {
175
+ await client.projectMutate(`mutation($projectId: ID!, $repositoryId: ID!) {
176
+ unlinkProjectV2FromRepository(input: {
177
+ projectId: $projectId,
178
+ repositoryId: $repositoryId
179
+ }) {
180
+ repository { id }
181
+ }
182
+ }`, { projectId, repositoryId: repoId });
183
+ }
184
+ else {
185
+ await client.projectMutate(`mutation($projectId: ID!, $repositoryId: ID!) {
186
+ linkProjectV2ToRepository(input: {
187
+ projectId: $projectId,
188
+ repositoryId: $repositoryId
189
+ }) {
190
+ repository { id }
191
+ }
192
+ }`, { projectId, repositoryId: repoId });
193
+ }
194
+ return toolSuccess({
195
+ repository: `${repoOwner}/${repoName}`,
196
+ linked: !args.unlink,
197
+ });
198
+ }
199
+ catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ return toolError(`Failed to ${args.unlink ? "unlink" : "link"} repository: ${message}`);
202
+ }
203
+ });
204
+ // -------------------------------------------------------------------------
205
+ // ralph_hero__clear_field
206
+ // -------------------------------------------------------------------------
207
+ server.tool("ralph_hero__clear_field", "Clear a field value on a project item. Works for any single-select field (Workflow State, Estimate, Priority, Status, etc.). Returns: number, field, cleared.", {
208
+ owner: z.string().optional().describe("GitHub owner. Defaults to env var"),
209
+ repo: z.string().optional().describe("Repository name. Defaults to env var"),
210
+ number: z.number().describe("Issue number"),
211
+ field: z.string().describe("Field name to clear (e.g., 'Estimate', 'Priority', 'Workflow State')"),
212
+ }, async (args) => {
213
+ try {
214
+ const { owner, repo, projectNumber, projectOwner } = resolveFullConfig(client, args);
215
+ await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
216
+ const projectId = fieldCache.getProjectId();
217
+ if (!projectId) {
218
+ return toolError("Could not resolve project ID");
219
+ }
220
+ const fieldId = fieldCache.getFieldId(args.field);
221
+ if (!fieldId) {
222
+ const validFields = fieldCache.getFieldNames();
223
+ return toolError(`Field "${args.field}" not found in project. ` +
224
+ `Valid fields: ${validFields.join(", ")}`);
225
+ }
226
+ const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, args.number);
227
+ await client.projectMutate(`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!) {
228
+ clearProjectV2ItemFieldValue(input: {
229
+ projectId: $projectId,
230
+ itemId: $itemId,
231
+ fieldId: $fieldId
232
+ }) {
233
+ projectV2Item { id }
234
+ }
235
+ }`, { projectId, itemId: projectItemId, fieldId });
236
+ return toolSuccess({
237
+ number: args.number,
238
+ field: args.field,
239
+ cleared: true,
240
+ });
241
+ }
242
+ catch (error) {
243
+ const message = error instanceof Error ? error.message : String(error);
244
+ return toolError(`Failed to clear field: ${message}`);
245
+ }
246
+ });
247
+ }
248
+ //# sourceMappingURL=project-management-tools.js.map
@@ -11,7 +11,7 @@ import { z } from "zod";
11
11
  import { detectGroup } from "../lib/group-detection.js";
12
12
  import { isValidState, isEarlierState, VALID_STATES, } from "../lib/workflow-states.js";
13
13
  import { toolSuccess, toolError, resolveProjectOwner } from "../types.js";
14
- import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, } from "../lib/helpers.js";
14
+ import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, syncStatusField, } from "../lib/helpers.js";
15
15
  // ---------------------------------------------------------------------------
16
16
  // Register relationship tools
17
17
  // ---------------------------------------------------------------------------
@@ -411,6 +411,8 @@ export function registerRelationshipTools(server, client, fieldCache) {
411
411
  // Advance the child
412
412
  const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, child.number);
413
413
  await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.targetState);
414
+ // Sync default Status field (best-effort, one-way)
415
+ await syncStatusField(client, fieldCache, projectItemId, args.targetState);
414
416
  advanced.push({
415
417
  number: child.number,
416
418
  fromState: currentState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",