@freelancercom/phabricator-mcp 2.0.18 → 2.0.19

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.
@@ -58,6 +58,59 @@ async function linkRevisionsToTask(client, taskIdentifier, revisionPHIDs, action
58
58
  }
59
59
  return results;
60
60
  }
61
+ /**
62
+ * Phabricator custom fields are only editable when the task's subtype matches
63
+ * the form that exposes them. For example, `custom.postmortem.*` fields require
64
+ * subtype "postmortem", while `custom.incident.*` fields require "incident".
65
+ *
66
+ * This function groups custom field transactions by required subtype, temporarily
67
+ * switches the task's subtype to set each group, then restores the original subtype.
68
+ */
69
+ const SUBTYPE_FIELD_PREFIXES = {
70
+ 'custom.postmortem.': 'postmortem',
71
+ 'custom.incident.': 'incident',
72
+ };
73
+ async function applyCustomFields(client, objectIdentifier, customFields, currentSubtype) {
74
+ // Group fields by required subtype
75
+ const groups = new Map();
76
+ for (const [key, value] of Object.entries(customFields)) {
77
+ let requiredSubtype = null;
78
+ for (const [prefix, subtype] of Object.entries(SUBTYPE_FIELD_PREFIXES)) {
79
+ if (key.startsWith(prefix)) {
80
+ requiredSubtype = subtype;
81
+ break;
82
+ }
83
+ }
84
+ const group = groups.get(requiredSubtype) ?? [];
85
+ group.push({ type: key, value });
86
+ groups.set(requiredSubtype, group);
87
+ }
88
+ const results = {};
89
+ let lastSubtype = currentSubtype;
90
+ for (const [requiredSubtype, transactions] of groups) {
91
+ // Switch subtype if needed
92
+ if (requiredSubtype !== null && requiredSubtype !== lastSubtype) {
93
+ await client.call('maniphest.edit', {
94
+ objectIdentifier,
95
+ transactions: [{ type: 'subtype', value: requiredSubtype }],
96
+ });
97
+ lastSubtype = requiredSubtype;
98
+ }
99
+ const result = await client.call('maniphest.edit', {
100
+ objectIdentifier,
101
+ transactions,
102
+ });
103
+ results[requiredSubtype ?? 'default'] = result;
104
+ }
105
+ // Restore original subtype if we changed it
106
+ if (lastSubtype !== currentSubtype && currentSubtype !== undefined) {
107
+ await client.call('maniphest.edit', {
108
+ objectIdentifier,
109
+ transactions: [{ type: 'subtype', value: currentSubtype }],
110
+ });
111
+ }
112
+ return results;
113
+ }
61
114
  export function registerManiphestTools(server, client) {
62
115
  // Search tasks
63
116
  server.tool('phabricator_task_search', 'Search Maniphest tasks with optional filters', {
@@ -165,19 +218,9 @@ export function registerManiphestTools(server, client) {
165
218
  const extras = {};
166
219
  // Custom fields are applied in a second call because Phabricator validates
167
220
  // 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.
170
- if (params.customFields !== undefined) {
171
- const customTransactions = [];
172
- for (const [key, value] of Object.entries(params.customFields)) {
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
- });
180
- }
221
+ // custom fields (e.g. postmortem fields) require temporarily switching the subtype.
222
+ if (params.customFields !== undefined && Object.keys(params.customFields).length > 0) {
223
+ extras.customFields = await applyCustomFields(client, result.object.phid, params.customFields, params.subtype);
181
224
  }
182
225
  // Link revisions to the newly created task via differential.revision.edit
183
226
  if (params.revisionIDs !== undefined && params.revisionIDs.length > 0) {
@@ -276,16 +319,13 @@ export function registerManiphestTools(server, client) {
276
319
  if (params.comment !== undefined) {
277
320
  transactions.push({ type: 'comment', value: params.comment });
278
321
  }
279
- if (params.customFields !== undefined) {
280
- for (const [key, value] of Object.entries(params.customFields)) {
281
- transactions.push({ type: key, value });
282
- }
283
- }
322
+ const hasCustomFields = params.customFields !== undefined && Object.keys(params.customFields).length > 0;
284
323
  const hasRevisionChanges = (params.addRevisionIDs !== undefined && params.addRevisionIDs.length > 0) ||
285
324
  (params.removeRevisionIDs !== undefined && params.removeRevisionIDs.length > 0);
286
- if (transactions.length === 0 && !hasRevisionChanges) {
325
+ if (transactions.length === 0 && !hasCustomFields && !hasRevisionChanges) {
287
326
  return { content: [{ type: 'text', text: 'No changes specified' }] };
288
327
  }
328
+ const extras = {};
289
329
  let result = undefined;
290
330
  if (transactions.length > 0) {
291
331
  result = await client.call('maniphest.edit', {
@@ -293,18 +333,22 @@ export function registerManiphestTools(server, client) {
293
333
  transactions,
294
334
  });
295
335
  }
336
+ // Custom fields may require subtype switching (e.g. postmortem fields
337
+ // need subtype "postmortem"). applyCustomFields handles this automatically.
338
+ if (hasCustomFields) {
339
+ extras.customFields = await applyCustomFields(client, params.objectIdentifier, params.customFields, params.subtype);
340
+ }
296
341
  // Link/unlink revisions via differential.revision.edit
297
- const revisionResults = {};
298
342
  if (params.addRevisionIDs !== undefined && params.addRevisionIDs.length > 0) {
299
343
  const revPHIDs = await resolveRevisionPHIDs(client, params.addRevisionIDs);
300
- revisionResults.added = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'add');
344
+ extras.addedRevisions = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'add');
301
345
  }
302
346
  if (params.removeRevisionIDs !== undefined && params.removeRevisionIDs.length > 0) {
303
347
  const revPHIDs = await resolveRevisionPHIDs(client, params.removeRevisionIDs);
304
- revisionResults.removed = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'remove');
348
+ extras.removedRevisions = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'remove');
305
349
  }
306
- const output = hasRevisionChanges
307
- ? { ...(result !== undefined ? { task: result } : {}), linkedRevisions: revisionResults }
350
+ const output = Object.keys(extras).length > 0
351
+ ? { ...(result !== undefined ? { task: result } : {}), ...extras }
308
352
  : result;
309
353
  return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
310
354
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freelancercom/phabricator-mcp",
3
- "version": "2.0.18",
3
+ "version": "2.0.19",
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",