ralph-hero-mcp-server 2.5.3 → 2.5.13

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.
@@ -35,14 +35,19 @@ export function sanitize(obj) {
35
35
  // ---------------------------------------------------------------------------
36
36
  export class DebugLogger {
37
37
  logPath = null;
38
+ logPathPromise = null;
38
39
  logDir;
39
40
  constructor(options) {
40
41
  this.logDir =
41
42
  options?.logDir ?? join(homedir(), ".ralph-hero", "logs");
42
43
  }
43
- async getLogPath() {
44
- if (this.logPath)
45
- return this.logPath;
44
+ getLogPath() {
45
+ if (!this.logPathPromise) {
46
+ this.logPathPromise = this.initLogPath();
47
+ }
48
+ return this.logPathPromise;
49
+ }
50
+ async initLogPath() {
46
51
  await mkdir(this.logDir, { recursive: true });
47
52
  const now = new Date();
48
53
  const ts = now
@@ -26,8 +26,8 @@ const SEED_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
26
26
  number
27
27
  title
28
28
  state
29
- blocking(first: 20) { nodes { number } }
30
- blockedBy(first: 20) { nodes { number } }
29
+ blocking(first: 20) { nodes { number repository { owner { login } name } } }
30
+ blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
31
31
  }
32
32
  }
33
33
  }
@@ -37,15 +37,15 @@ const SEED_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
37
37
  number
38
38
  title
39
39
  state
40
- blocking(first: 20) { nodes { number } }
41
- blockedBy(first: 20) { nodes { number } }
40
+ blocking(first: 20) { nodes { number repository { owner { login } name } } }
41
+ blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
42
42
  }
43
43
  }
44
44
  blocking(first: 20) {
45
- nodes { id number title state }
45
+ nodes { id number title state repository { owner { login } name } }
46
46
  }
47
47
  blockedBy(first: 20) {
48
- nodes { id number title state }
48
+ nodes { id number title state repository { owner { login } name } }
49
49
  }
50
50
  }
51
51
  }
@@ -69,8 +69,8 @@ const EXPAND_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
69
69
  number
70
70
  title
71
71
  state
72
- blocking(first: 20) { nodes { number } }
73
- blockedBy(first: 20) { nodes { number } }
72
+ blocking(first: 20) { nodes { number repository { owner { login } name } } }
73
+ blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
74
74
  }
75
75
  }
76
76
  }
@@ -80,15 +80,15 @@ const EXPAND_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
80
80
  number
81
81
  title
82
82
  state
83
- blocking(first: 20) { nodes { number } }
84
- blockedBy(first: 20) { nodes { number } }
83
+ blocking(first: 20) { nodes { number repository { owner { login } name } } }
84
+ blockedBy(first: 20) { nodes { number repository { owner { login } name } } }
85
85
  }
86
86
  }
87
87
  blocking(first: 20) {
88
- nodes { id number title state }
88
+ nodes { id number title state repository { owner { login } name } }
89
89
  }
90
90
  blockedBy(first: 20) {
91
- nodes { id number title state }
91
+ nodes { id number title state repository { owner { login } name } }
92
92
  }
93
93
  }
94
94
  }
@@ -111,18 +111,36 @@ export async function detectGroup(client, owner, repo, seedNumber) {
111
111
  const issueMap = new Map();
112
112
  // Queue of issue numbers to expand
113
113
  const expandQueue = [];
114
+ // Cross-repo info for dependency targets (number -> { owner, repo })
115
+ const depRepoInfo = new Map();
116
+ // Helper to store cross-repo info from dependency nodes
117
+ function trackDepRepoInfo(nodes) {
118
+ for (const dep of nodes) {
119
+ if (dep.repository) {
120
+ depRepoInfo.set(dep.number, {
121
+ owner: dep.repository.owner.login,
122
+ repo: dep.repository.name,
123
+ });
124
+ }
125
+ }
126
+ }
114
127
  // Step 1: Seed query
115
128
  const seedResult = await client.query(SEED_QUERY, { owner, repo, number: seedNumber });
116
129
  const seedIssue = seedResult.repository?.issue;
117
130
  if (!seedIssue) {
118
131
  throw new Error(`Issue #${seedNumber} not found in ${owner}/${repo}`);
119
132
  }
133
+ // Track cross-repo info from seed's direct dependencies
134
+ trackDepRepoInfo(seedIssue.blocking.nodes);
135
+ trackDepRepoInfo(seedIssue.blockedBy.nodes);
120
136
  // Process seed issue
121
137
  addIssueToMap(issueMap, {
122
138
  id: seedIssue.id,
123
139
  number: seedIssue.number,
124
140
  title: seedIssue.title,
125
141
  state: seedIssue.state,
142
+ repoOwner: owner,
143
+ repoName: repo,
126
144
  parentNumber: seedIssue.parent?.number ?? null,
127
145
  subIssueNumbers: seedIssue.subIssues.nodes.map((n) => n.number),
128
146
  blockingNumbers: seedIssue.blocking.nodes.map((n) => n.number),
@@ -136,17 +154,24 @@ export async function detectGroup(client, owner, repo, seedNumber) {
136
154
  number: parent.number,
137
155
  title: parent.title,
138
156
  state: parent.state,
157
+ repoOwner: owner,
158
+ repoName: repo,
139
159
  parentNumber: null,
140
160
  subIssueNumbers: parent.subIssues.nodes.map((n) => n.number),
141
161
  blockingNumbers: [],
142
162
  blockedByNumbers: [],
143
163
  });
144
164
  for (const sibling of parent.subIssues.nodes) {
165
+ // Track cross-repo info from sibling dependencies
166
+ trackDepRepoInfo(sibling.blocking?.nodes ?? []);
167
+ trackDepRepoInfo(sibling.blockedBy?.nodes ?? []);
145
168
  addIssueToMap(issueMap, {
146
169
  id: sibling.id,
147
170
  number: sibling.number,
148
171
  title: sibling.title,
149
172
  state: sibling.state,
173
+ repoOwner: owner,
174
+ repoName: repo,
150
175
  parentNumber: parent.number,
151
176
  subIssueNumbers: [],
152
177
  blockingNumbers: sibling.blocking?.nodes.map((n) => n.number) ?? [],
@@ -156,11 +181,16 @@ export async function detectGroup(client, owner, repo, seedNumber) {
156
181
  }
157
182
  // Process sub-issues of seed
158
183
  for (const child of seedIssue.subIssues.nodes) {
184
+ // Track cross-repo info from child dependencies
185
+ trackDepRepoInfo(child.blocking?.nodes ?? []);
186
+ trackDepRepoInfo(child.blockedBy?.nodes ?? []);
159
187
  addIssueToMap(issueMap, {
160
188
  id: child.id,
161
189
  number: child.number,
162
190
  title: child.title,
163
191
  state: child.state,
192
+ repoOwner: owner,
193
+ repoName: repo,
164
194
  parentNumber: seedIssue.number,
165
195
  subIssueNumbers: [],
166
196
  blockingNumbers: child.blocking?.nodes.map((n) => n.number) ?? [],
@@ -169,12 +199,16 @@ export async function detectGroup(client, owner, repo, seedNumber) {
169
199
  }
170
200
  // Process direct dependencies
171
201
  for (const dep of seedIssue.blocking.nodes) {
202
+ const depOwner = dep.repository?.owner.login ?? owner;
203
+ const depRepo = dep.repository?.name ?? repo;
172
204
  if (!issueMap.has(dep.number)) {
173
205
  addIssueToMap(issueMap, {
174
206
  id: dep.id,
175
207
  number: dep.number,
176
208
  title: dep.title,
177
209
  state: dep.state,
210
+ repoOwner: depOwner,
211
+ repoName: depRepo,
178
212
  parentNumber: null,
179
213
  subIssueNumbers: [],
180
214
  blockingNumbers: [],
@@ -184,12 +218,16 @@ export async function detectGroup(client, owner, repo, seedNumber) {
184
218
  }
185
219
  }
186
220
  for (const dep of seedIssue.blockedBy.nodes) {
221
+ const depOwner = dep.repository?.owner.login ?? owner;
222
+ const depRepo = dep.repository?.name ?? repo;
187
223
  if (!issueMap.has(dep.number)) {
188
224
  addIssueToMap(issueMap, {
189
225
  id: dep.id,
190
226
  number: dep.number,
191
227
  title: dep.title,
192
228
  state: dep.state,
229
+ repoOwner: depOwner,
230
+ repoName: depRepo,
193
231
  parentNumber: null,
194
232
  subIssueNumbers: [],
195
233
  blockingNumbers: [],
@@ -220,16 +258,35 @@ export async function detectGroup(client, owner, repo, seedNumber) {
220
258
  continue;
221
259
  }
222
260
  expanded.add(num);
261
+ // Resolve owner/repo for this issue — check cross-repo info first
262
+ const crossRepoInfo = depRepoInfo.get(num);
263
+ const expandOwner = crossRepoInfo?.owner ?? owner;
264
+ const expandRepo = crossRepoInfo?.repo ?? repo;
223
265
  try {
224
- const expandResult = await client.query(EXPAND_QUERY, { owner, repo, number: num });
266
+ const expandResult = await client.query(EXPAND_QUERY, { owner: expandOwner, repo: expandRepo, number: num });
225
267
  const expandedIssue = expandResult.repository?.issue;
226
268
  if (!expandedIssue)
227
- continue; // Cross-repo or deleted issue, skip
269
+ continue; // Deleted issue, skip
270
+ // Track cross-repo info from expanded issue's dependency nodes
271
+ trackDepRepoInfo(expandedIssue.blocking.nodes);
272
+ trackDepRepoInfo(expandedIssue.blockedBy.nodes);
273
+ for (const child of expandedIssue.subIssues.nodes) {
274
+ trackDepRepoInfo(child.blocking?.nodes ?? []);
275
+ trackDepRepoInfo(child.blockedBy?.nodes ?? []);
276
+ }
277
+ if (expandedIssue.parent) {
278
+ for (const sibling of expandedIssue.parent.subIssues.nodes) {
279
+ trackDepRepoInfo(sibling.blocking?.nodes ?? []);
280
+ trackDepRepoInfo(sibling.blockedBy?.nodes ?? []);
281
+ }
282
+ }
228
283
  addIssueToMap(issueMap, {
229
284
  id: expandedIssue.id,
230
285
  number: expandedIssue.number,
231
286
  title: expandedIssue.title,
232
287
  state: expandedIssue.state,
288
+ repoOwner: expandOwner,
289
+ repoName: expandRepo,
233
290
  parentNumber: expandedIssue.parent?.number ?? null,
234
291
  subIssueNumbers: expandedIssue.subIssues.nodes.map((n) => n.number),
235
292
  blockingNumbers: expandedIssue.blocking.nodes.map((n) => n.number),
@@ -247,7 +304,7 @@ export async function detectGroup(client, owner, repo, seedNumber) {
247
304
  }
248
305
  catch {
249
306
  // Skip issues that can't be fetched (cross-repo, permissions, etc.)
250
- console.error(`[group-detection] Could not fetch issue #${num}, skipping (may be cross-repo)`);
307
+ console.error(`[group-detection] Could not fetch issue #${num} from ${expandOwner}/${expandRepo}, skipping`);
251
308
  }
252
309
  }
253
310
  // Step 3: Topological sort
@@ -258,13 +315,18 @@ export async function detectGroup(client, owner, repo, seedNumber) {
258
315
  // Step 4: Build result
259
316
  const groupTickets = sorted.map((num, index) => {
260
317
  const issue = issueMap.get(num);
261
- return {
318
+ const result = {
262
319
  id: issue.id,
263
320
  number: issue.number,
264
321
  title: issue.title,
265
322
  state: issue.state,
266
323
  order: index + 1,
267
324
  };
325
+ // Include repository only for cross-repo issues
326
+ if (issue.repoOwner !== owner || issue.repoName !== repo) {
327
+ result.repository = `${issue.repoOwner}/${issue.repoName}`;
328
+ }
329
+ return result;
268
330
  });
269
331
  const primary = groupTickets[0] || {
270
332
  id: seedIssue.id,
@@ -301,13 +363,17 @@ function addIssueToMap(map, data) {
301
363
  if (data.parentNumber !== null && existing.parentNumber === null) {
302
364
  existing.parentNumber = data.parentNumber;
303
365
  }
304
- // Prefer non-empty id/title/state
366
+ // Prefer non-empty id/title/state/repo
305
367
  if (!existing.id && data.id)
306
368
  existing.id = data.id;
307
369
  if (!existing.title && data.title)
308
370
  existing.title = data.title;
309
371
  if (!existing.state && data.state)
310
372
  existing.state = data.state;
373
+ if (!existing.repoOwner && data.repoOwner)
374
+ existing.repoOwner = data.repoOwner;
375
+ if (!existing.repoName && data.repoName)
376
+ existing.repoName = data.repoName;
311
377
  }
312
378
  else {
313
379
  map.set(data.number, { ...data });
@@ -259,49 +259,51 @@ export function registerDecomposeTools(server, client, fieldCache) {
259
259
  projectItemId,
260
260
  });
261
261
  }
262
- // Step 5: Wire dependencies (addSubIssue for dependency edges)
263
- // Parse "a -> b" edges from dependency_chain; cross-repo sub-issues
264
- // may not be supported — catch and continue.
262
+ // Step 5: Wire dependencies (addBlockedBy for dependency-flow edges)
263
+ // Parse "a -> b" edges: a blocks b (b is blocked by a)
265
264
  const wiringResults = [];
266
265
  for (const edge of decomposition.dependency_chain) {
267
266
  const match = edge.match(/^\s*(\S+)\s*->\s*(\S+)\s*$/);
268
267
  if (!match) {
269
268
  wiringResults.push({
270
269
  edge,
270
+ type: "blockedBy",
271
271
  status: "skipped",
272
272
  reason: "Unrecognized edge format (expected 'a -> b')",
273
273
  });
274
274
  continue;
275
275
  }
276
- const [, fromRepo, toRepo] = match;
277
- const fromIssue = createdIssues.find((i) => i.repoKey === fromRepo);
278
- const toIssue = createdIssues.find((i) => i.repoKey === toRepo);
279
- if (!fromIssue || !toIssue) {
276
+ const [, blockingRepo, blockedRepo] = match;
277
+ const blockingIssue = createdIssues.find((i) => i.repoKey === blockingRepo);
278
+ const blockedIssue = createdIssues.find((i) => i.repoKey === blockedRepo);
279
+ if (!blockingIssue || !blockedIssue) {
280
280
  wiringResults.push({
281
281
  edge,
282
+ type: "blockedBy",
282
283
  status: "skipped",
283
- reason: `Could not find created issue for repo "${fromRepo}" or "${toRepo}"`,
284
+ reason: `Could not find created issue for repo "${blockingRepo}" or "${blockedRepo}"`,
284
285
  });
285
286
  continue;
286
287
  }
287
288
  try {
288
- await client.mutate(`mutation($parentId: ID!, $childId: ID!) {
289
- addSubIssue(input: {
290
- issueId: $parentId,
291
- subIssueId: $childId
289
+ await client.mutate(`mutation($blockedId: ID!, $blockingId: ID!) {
290
+ addBlockedBy(input: {
291
+ issueId: $blockedId,
292
+ blockingIssueId: $blockingId
292
293
  }) {
293
294
  issue { id }
294
- subIssue { id }
295
+ blockingIssue { id }
295
296
  }
296
- }`, { parentId: fromIssue.id, childId: toIssue.id });
297
- wiringResults.push({ edge, status: "ok" });
297
+ }`, { blockedId: blockedIssue.id, blockingId: blockingIssue.id });
298
+ wiringResults.push({ edge, type: "blockedBy", status: "ok" });
298
299
  }
299
300
  catch (err) {
300
301
  const reason = err instanceof Error ? err.message : String(err);
301
302
  wiringResults.push({
302
303
  edge,
304
+ type: "blockedBy",
303
305
  status: "skipped",
304
- reason: `addSubIssue failed (cross-repo sub-issues may not be supported): ${reason}`,
306
+ reason: `addBlockedBy failed: ${reason}`,
305
307
  });
306
308
  }
307
309
  }
@@ -193,33 +193,55 @@ export function registerRelationshipTools(server, client, fieldCache) {
193
193
  // -------------------------------------------------------------------------
194
194
  // ralph_hero__add_dependency
195
195
  // -------------------------------------------------------------------------
196
- server.tool("ralph_hero__add_dependency", "Create a blocking dependency between two GitHub issues. The 'blockingNumber' issue blocks the 'blockedNumber' issue.", {
196
+ server.tool("ralph_hero__add_dependency", "Create a blocking dependency between two GitHub issues. Supports cross-repo: " +
197
+ "the blocked and blocking issues can be in different repositories. " +
198
+ "The 'blockingNumber' issue blocks the 'blockedNumber' issue.", {
197
199
  owner: z
198
200
  .string()
199
201
  .optional()
200
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
202
+ .describe("Default GitHub owner for both issues. Defaults to GITHUB_OWNER env var"),
201
203
  repo: z
202
204
  .string()
203
205
  .optional()
204
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
206
+ .describe("Default repository for both issues. Defaults to GITHUB_REPO env var"),
205
207
  blockedNumber: z
206
208
  .number()
207
209
  .describe("Issue number that IS blocked (cannot proceed until blocker is done)"),
210
+ blockedOwner: z
211
+ .string()
212
+ .optional()
213
+ .describe("GitHub owner for the blocked issue. Defaults to 'owner' param"),
214
+ blockedRepo: z
215
+ .string()
216
+ .optional()
217
+ .describe("Repository for the blocked issue. Defaults to 'repo' param"),
208
218
  blockingNumber: z
209
219
  .number()
210
220
  .describe("Issue number that IS the blocker (must be completed first)"),
221
+ blockingOwner: z
222
+ .string()
223
+ .optional()
224
+ .describe("GitHub owner for the blocking issue. Defaults to 'owner' param"),
225
+ blockingRepo: z
226
+ .string()
227
+ .optional()
228
+ .describe("Repository for the blocking issue. Defaults to 'repo' param"),
211
229
  }, async (args) => {
212
230
  try {
213
231
  const { owner, repo } = resolveConfig(client, args);
214
- const blockedId = await resolveIssueNodeId(client, owner, repo, args.blockedNumber);
215
- const blockingId = await resolveIssueNodeId(client, owner, repo, args.blockingNumber);
232
+ const bOwner = args.blockedOwner || owner;
233
+ const bRepo = args.blockedRepo || repo;
234
+ const kOwner = args.blockingOwner || owner;
235
+ const kRepo = args.blockingRepo || repo;
236
+ const blockedId = await resolveIssueNodeId(client, bOwner, bRepo, args.blockedNumber);
237
+ const blockingId = await resolveIssueNodeId(client, kOwner, kRepo, args.blockingNumber);
216
238
  const result = await client.mutate(`mutation($blockedId: ID!, $blockingId: ID!) {
217
239
  addBlockedBy(input: {
218
240
  issueId: $blockedId,
219
241
  blockingIssueId: $blockingId
220
242
  }) {
221
- issue { id number title }
222
- blockingIssue { id number title }
243
+ issue { id number title repository { nameWithOwner } }
244
+ blockingIssue { id number title repository { nameWithOwner } }
223
245
  }
224
246
  }`, { blockedId, blockingId });
225
247
  return toolSuccess({
@@ -227,11 +249,13 @@ export function registerRelationshipTools(server, client, fieldCache) {
227
249
  id: result.addBlockedBy.issue.id,
228
250
  number: result.addBlockedBy.issue.number,
229
251
  title: result.addBlockedBy.issue.title,
252
+ repository: result.addBlockedBy.issue.repository.nameWithOwner,
230
253
  },
231
254
  blocking: {
232
255
  id: result.addBlockedBy.blockingIssue.id,
233
256
  number: result.addBlockedBy.blockingIssue.number,
234
257
  title: result.addBlockedBy.blockingIssue.title,
258
+ repository: result.addBlockedBy.blockingIssue.repository.nameWithOwner,
235
259
  },
236
260
  });
237
261
  }
@@ -243,29 +267,50 @@ export function registerRelationshipTools(server, client, fieldCache) {
243
267
  // -------------------------------------------------------------------------
244
268
  // ralph_hero__remove_dependency
245
269
  // -------------------------------------------------------------------------
246
- server.tool("ralph_hero__remove_dependency", "Remove a blocking dependency between two GitHub issues", {
270
+ server.tool("ralph_hero__remove_dependency", "Remove a blocking dependency between two GitHub issues. Supports cross-repo: " +
271
+ "the blocked and blocking issues can be in different repositories.", {
247
272
  owner: z
248
273
  .string()
249
274
  .optional()
250
- .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
275
+ .describe("Default GitHub owner for both issues. Defaults to GITHUB_OWNER env var"),
251
276
  repo: z
252
277
  .string()
253
278
  .optional()
254
- .describe("Repository name. Defaults to GITHUB_REPO env var"),
279
+ .describe("Default repository for both issues. Defaults to GITHUB_REPO env var"),
255
280
  blockedNumber: z.coerce.number().describe("Issue number that was blocked"),
281
+ blockedOwner: z
282
+ .string()
283
+ .optional()
284
+ .describe("GitHub owner for the blocked issue. Defaults to 'owner' param"),
285
+ blockedRepo: z
286
+ .string()
287
+ .optional()
288
+ .describe("Repository for the blocked issue. Defaults to 'repo' param"),
256
289
  blockingNumber: z.coerce.number().describe("Issue number that was the blocker"),
290
+ blockingOwner: z
291
+ .string()
292
+ .optional()
293
+ .describe("GitHub owner for the blocking issue. Defaults to 'owner' param"),
294
+ blockingRepo: z
295
+ .string()
296
+ .optional()
297
+ .describe("Repository for the blocking issue. Defaults to 'repo' param"),
257
298
  }, async (args) => {
258
299
  try {
259
300
  const { owner, repo } = resolveConfig(client, args);
260
- const blockedId = await resolveIssueNodeId(client, owner, repo, args.blockedNumber);
261
- const blockingId = await resolveIssueNodeId(client, owner, repo, args.blockingNumber);
301
+ const bOwner = args.blockedOwner || owner;
302
+ const bRepo = args.blockedRepo || repo;
303
+ const kOwner = args.blockingOwner || owner;
304
+ const kRepo = args.blockingRepo || repo;
305
+ const blockedId = await resolveIssueNodeId(client, bOwner, bRepo, args.blockedNumber);
306
+ const blockingId = await resolveIssueNodeId(client, kOwner, kRepo, args.blockingNumber);
262
307
  const result = await client.mutate(`mutation($blockedId: ID!, $blockingId: ID!) {
263
308
  removeBlockedBy(input: {
264
309
  issueId: $blockedId,
265
310
  blockingIssueId: $blockingId
266
311
  }) {
267
- issue { id number title }
268
- blockingIssue { id number title }
312
+ issue { id number title repository { nameWithOwner } }
313
+ blockingIssue { id number title repository { nameWithOwner } }
269
314
  }
270
315
  }`, { blockedId, blockingId });
271
316
  return toolSuccess({
@@ -273,11 +318,13 @@ export function registerRelationshipTools(server, client, fieldCache) {
273
318
  id: result.removeBlockedBy.issue.id,
274
319
  number: result.removeBlockedBy.issue.number,
275
320
  title: result.removeBlockedBy.issue.title,
321
+ repository: result.removeBlockedBy.issue.repository.nameWithOwner,
276
322
  },
277
323
  blocking: {
278
324
  id: result.removeBlockedBy.blockingIssue.id,
279
325
  number: result.removeBlockedBy.blockingIssue.number,
280
326
  title: result.removeBlockedBy.blockingIssue.title,
327
+ repository: result.removeBlockedBy.blockingIssue.repository.nameWithOwner,
281
328
  },
282
329
  });
283
330
  }
@@ -286,6 +333,85 @@ export function registerRelationshipTools(server, client, fieldCache) {
286
333
  return toolError(`Failed to remove dependency: ${message}`);
287
334
  }
288
335
  });
336
+ // -------------------------------------------------------------------------
337
+ // ralph_hero__list_dependencies
338
+ // -------------------------------------------------------------------------
339
+ server.tool("ralph_hero__list_dependencies", "List all blocking dependencies for a GitHub issue. Returns both 'blocking' " +
340
+ "(issues this issue blocks) and 'blockedBy' (issues blocking this issue) " +
341
+ "with full cross-repo context including repository name.", {
342
+ owner: z
343
+ .string()
344
+ .optional()
345
+ .describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
346
+ repo: z
347
+ .string()
348
+ .optional()
349
+ .describe("Repository name. Defaults to GITHUB_REPO env var"),
350
+ number: z.coerce.number().describe("Issue number to query dependencies for"),
351
+ }, async (args) => {
352
+ try {
353
+ const { owner, repo } = resolveConfig(client, args);
354
+ const result = await client.query(`query($owner: String!, $repo: String!, $number: Int!) {
355
+ repository(owner: $owner, name: $repo) {
356
+ issue(number: $number) {
357
+ id
358
+ number
359
+ title
360
+ state
361
+ blocking(first: 50) {
362
+ nodes {
363
+ id number title state
364
+ repository { nameWithOwner }
365
+ }
366
+ }
367
+ blockedBy(first: 50) {
368
+ nodes {
369
+ id number title state
370
+ repository { nameWithOwner }
371
+ }
372
+ }
373
+ }
374
+ }
375
+ }`, { owner, repo, number: args.number });
376
+ const issue = result.repository?.issue;
377
+ if (!issue) {
378
+ return toolError(`Issue #${args.number} not found in ${owner}/${repo}`);
379
+ }
380
+ return toolSuccess({
381
+ issue: {
382
+ id: issue.id,
383
+ number: issue.number,
384
+ title: issue.title,
385
+ state: issue.state,
386
+ repository: `${owner}/${repo}`,
387
+ },
388
+ blocking: issue.blocking.nodes.map((n) => ({
389
+ id: n.id,
390
+ number: n.number,
391
+ title: n.title,
392
+ state: n.state,
393
+ repository: n.repository.nameWithOwner,
394
+ })),
395
+ blockedBy: issue.blockedBy.nodes.map((n) => ({
396
+ id: n.id,
397
+ number: n.number,
398
+ title: n.title,
399
+ state: n.state,
400
+ repository: n.repository.nameWithOwner,
401
+ })),
402
+ summary: {
403
+ blockingCount: issue.blocking.nodes.length,
404
+ blockedByCount: issue.blockedBy.nodes.length,
405
+ isBlocked: issue.blockedBy.nodes.length > 0,
406
+ isBlocking: issue.blocking.nodes.length > 0,
407
+ },
408
+ });
409
+ }
410
+ catch (error) {
411
+ const message = error instanceof Error ? error.message : String(error);
412
+ return toolError(`Failed to list dependencies: ${message}`);
413
+ }
414
+ });
289
415
  // ralph_hero__advance_issue
290
416
  // -------------------------------------------------------------------------
291
417
  server.tool("ralph_hero__advance_issue", "Advance workflow state for related issues. " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.5.3",
3
+ "version": "2.5.13",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",