@freelancercom/phabricator-mcp 2.0.16 → 2.0.18

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.
@@ -1,5 +1,63 @@
1
1
  import { z } from 'zod';
2
2
  import { jsonCoerce } from './coerce.js';
3
+ /**
4
+ * Resolve revision identifiers (e.g. "D223125", "PHID-DREV-xxx") to PHIDs.
5
+ * Accepts a mix of D-prefixed IDs and raw PHIDs.
6
+ */
7
+ async function resolveRevisionPHIDs(client, ids) {
8
+ const phids = [];
9
+ const namesToLookup = [];
10
+ for (const id of ids) {
11
+ if (id.startsWith('PHID-')) {
12
+ phids.push(id);
13
+ }
14
+ else {
15
+ // Normalize to D-prefixed format if just a number
16
+ const name = /^\d+$/.test(id) ? `D${id}` : id;
17
+ namesToLookup.push(name);
18
+ }
19
+ }
20
+ if (namesToLookup.length > 0) {
21
+ const lookupResult = await client.call('phid.lookup', { names: namesToLookup });
22
+ for (const name of namesToLookup) {
23
+ const entry = lookupResult[name];
24
+ if (entry) {
25
+ phids.push(entry.phid);
26
+ }
27
+ else {
28
+ throw new Error(`Could not resolve revision identifier: ${name}`);
29
+ }
30
+ }
31
+ }
32
+ return phids;
33
+ }
34
+ /**
35
+ * Link or unlink revisions to/from a task by calling differential.revision.edit
36
+ * with tasks.add / tasks.remove transactions.
37
+ */
38
+ async function linkRevisionsToTask(client, taskIdentifier, revisionPHIDs, action) {
39
+ // Resolve the task identifier to a PHID if it's a T-prefixed ID
40
+ let taskPHID = taskIdentifier;
41
+ if (!taskIdentifier.startsWith('PHID-')) {
42
+ const lookupResult = await client.call('phid.lookup', { names: [taskIdentifier] });
43
+ const entry = lookupResult[taskIdentifier];
44
+ if (entry) {
45
+ taskPHID = entry.phid;
46
+ }
47
+ else {
48
+ throw new Error(`Could not resolve task identifier: ${taskIdentifier}`);
49
+ }
50
+ }
51
+ const results = [];
52
+ for (const revPHID of revisionPHIDs) {
53
+ const result = await client.call('differential.revision.edit', {
54
+ objectIdentifier: revPHID,
55
+ transactions: [{ type: `tasks.${action}`, value: [taskPHID] }],
56
+ });
57
+ results.push(result);
58
+ }
59
+ return results;
60
+ }
3
61
  export function registerManiphestTools(server, client) {
4
62
  // Search tasks
5
63
  server.tool('phabricator_task_search', 'Search Maniphest tasks with optional filters', {
@@ -54,7 +112,8 @@ export function registerManiphestTools(server, client) {
54
112
  subtype: z.string().optional().describe('Task subtype (e.g. "default", "incident")'),
55
113
  parentPHIDs: z.array(z.string()).optional().describe('Parent task PHIDs'),
56
114
  subtaskPHIDs: z.array(z.string()).optional().describe('Subtask PHIDs'),
57
- commitPHIDs: z.array(z.string()).optional().describe('Commit PHIDs to associate'),
115
+ commitPHIDs: z.array(z.string()).optional().describe('Commit PHIDs to associate (for actual commits only, not revisions)'),
116
+ revisionIDs: z.array(z.string()).optional().describe('Differential revision IDs to link (e.g. ["D223125"] or ["PHID-DREV-xxx"]). Creates a bidirectional link between the revision and the task.'),
58
117
  points: z.coerce.number().nullable().optional().describe('Story points value (if points are enabled on this instance)'),
59
118
  space: z.string().optional().describe('Space PHID to place the task in (for multi-space installations)'),
60
119
  comment: z.string().optional().describe('Initial comment on the task (supports Remarkup)'),
@@ -102,13 +161,32 @@ export function registerManiphestTools(server, client) {
102
161
  if (params.comment !== undefined) {
103
162
  transactions.push({ type: 'comment', value: params.comment });
104
163
  }
164
+ const result = await client.call('maniphest.edit', { transactions });
165
+ const extras = {};
166
+ // Custom fields are applied in a second call because Phabricator validates
167
+ // transaction types against the default subtype during creation. Subtype-specific
168
+ // custom fields (e.g. incident fields) are only available after the task exists
169
+ // with the correct subtype.
105
170
  if (params.customFields !== undefined) {
171
+ const customTransactions = [];
106
172
  for (const [key, value] of Object.entries(params.customFields)) {
107
- transactions.push({ type: key, value });
173
+ customTransactions.push({ type: key, value });
174
+ }
175
+ if (customTransactions.length > 0) {
176
+ extras.customFields = await client.call('maniphest.edit', {
177
+ objectIdentifier: result.object.phid,
178
+ transactions: customTransactions,
179
+ });
108
180
  }
109
181
  }
110
- const result = await client.call('maniphest.edit', { transactions });
111
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
182
+ // Link revisions to the newly created task via differential.revision.edit
183
+ if (params.revisionIDs !== undefined && params.revisionIDs.length > 0) {
184
+ const revPHIDs = await resolveRevisionPHIDs(client, params.revisionIDs);
185
+ const taskId = `T${result.object.id}`;
186
+ extras.linkedRevisions = await linkRevisionsToTask(client, taskId, revPHIDs, 'add');
187
+ }
188
+ const output = Object.keys(extras).length > 0 ? { ...result, ...extras } : result;
189
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
112
190
  });
113
191
  // Edit task
114
192
  server.tool('phabricator_task_edit', 'Edit an existing Maniphest task', {
@@ -127,8 +205,10 @@ export function registerManiphestTools(server, client) {
127
205
  removeParentPHIDs: z.array(z.string()).optional().describe('Parent task PHIDs to remove'),
128
206
  addSubtaskPHIDs: z.array(z.string()).optional().describe('Subtask PHIDs to add'),
129
207
  removeSubtaskPHIDs: z.array(z.string()).optional().describe('Subtask PHIDs to remove'),
130
- addCommitPHIDs: z.array(z.string()).optional().describe('Commit PHIDs to associate'),
131
- removeCommitPHIDs: z.array(z.string()).optional().describe('Commit PHIDs to disassociate'),
208
+ addCommitPHIDs: z.array(z.string()).optional().describe('Commit PHIDs to associate (for actual commits only, not revisions)'),
209
+ removeCommitPHIDs: z.array(z.string()).optional().describe('Commit PHIDs to disassociate (for actual commits only, not revisions)'),
210
+ addRevisionIDs: z.array(z.string()).optional().describe('Differential revision IDs to link (e.g. ["D223125"] or ["PHID-DREV-xxx"]). Creates a bidirectional link between the revision and the task.'),
211
+ removeRevisionIDs: z.array(z.string()).optional().describe('Differential revision IDs to unlink (e.g. ["D223125"] or ["PHID-DREV-xxx"]).'),
132
212
  points: z.coerce.number().nullable().optional().describe('Story points value (null to clear)'),
133
213
  columnPHID: z.string().optional().describe('Move to workboard column'),
134
214
  space: z.string().optional().describe('Space PHID to move the task to (for multi-space installations)'),
@@ -201,14 +281,32 @@ export function registerManiphestTools(server, client) {
201
281
  transactions.push({ type: key, value });
202
282
  }
203
283
  }
204
- if (transactions.length === 0) {
284
+ const hasRevisionChanges = (params.addRevisionIDs !== undefined && params.addRevisionIDs.length > 0) ||
285
+ (params.removeRevisionIDs !== undefined && params.removeRevisionIDs.length > 0);
286
+ if (transactions.length === 0 && !hasRevisionChanges) {
205
287
  return { content: [{ type: 'text', text: 'No changes specified' }] };
206
288
  }
207
- const result = await client.call('maniphest.edit', {
208
- objectIdentifier: params.objectIdentifier,
209
- transactions,
210
- });
211
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
289
+ let result = undefined;
290
+ if (transactions.length > 0) {
291
+ result = await client.call('maniphest.edit', {
292
+ objectIdentifier: params.objectIdentifier,
293
+ transactions,
294
+ });
295
+ }
296
+ // Link/unlink revisions via differential.revision.edit
297
+ const revisionResults = {};
298
+ if (params.addRevisionIDs !== undefined && params.addRevisionIDs.length > 0) {
299
+ const revPHIDs = await resolveRevisionPHIDs(client, params.addRevisionIDs);
300
+ revisionResults.added = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'add');
301
+ }
302
+ if (params.removeRevisionIDs !== undefined && params.removeRevisionIDs.length > 0) {
303
+ const revPHIDs = await resolveRevisionPHIDs(client, params.removeRevisionIDs);
304
+ revisionResults.removed = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'remove');
305
+ }
306
+ const output = hasRevisionChanges
307
+ ? { ...(result !== undefined ? { task: result } : {}), linkedRevisions: revisionResults }
308
+ : result;
309
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
212
310
  });
213
311
  // Add comment to task
214
312
  server.tool('phabricator_task_add_comment', 'Add a comment to a Maniphest task', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freelancercom/phabricator-mcp",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
4
4
  "description": "MCP server for Phabricator Conduit API - manage tasks, code reviews, repositories, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",