@freelancercom/phabricator-mcp 2.0.17 → 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', {
@@ -161,25 +214,22 @@ export function registerManiphestTools(server, client) {
161
214
  if (params.comment !== undefined) {
162
215
  transactions.push({ type: 'comment', value: params.comment });
163
216
  }
164
- if (params.customFields !== undefined) {
165
- for (const [key, value] of Object.entries(params.customFields)) {
166
- transactions.push({ type: key, value });
167
- }
168
- }
169
217
  const result = await client.call('maniphest.edit', { transactions });
218
+ const extras = {};
219
+ // Custom fields are applied in a second call because Phabricator validates
220
+ // transaction types against the default subtype during creation. Subtype-specific
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);
224
+ }
170
225
  // Link revisions to the newly created task via differential.revision.edit
171
226
  if (params.revisionIDs !== undefined && params.revisionIDs.length > 0) {
172
227
  const revPHIDs = await resolveRevisionPHIDs(client, params.revisionIDs);
173
228
  const taskId = `T${result.object.id}`;
174
- const linkResults = await linkRevisionsToTask(client, taskId, revPHIDs, 'add');
175
- return {
176
- content: [{
177
- type: 'text',
178
- text: JSON.stringify({ ...result, linkedRevisions: linkResults }, null, 2),
179
- }],
180
- };
229
+ extras.linkedRevisions = await linkRevisionsToTask(client, taskId, revPHIDs, 'add');
181
230
  }
182
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
231
+ const output = Object.keys(extras).length > 0 ? { ...result, ...extras } : result;
232
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
183
233
  });
184
234
  // Edit task
185
235
  server.tool('phabricator_task_edit', 'Edit an existing Maniphest task', {
@@ -269,16 +319,13 @@ export function registerManiphestTools(server, client) {
269
319
  if (params.comment !== undefined) {
270
320
  transactions.push({ type: 'comment', value: params.comment });
271
321
  }
272
- if (params.customFields !== undefined) {
273
- for (const [key, value] of Object.entries(params.customFields)) {
274
- transactions.push({ type: key, value });
275
- }
276
- }
322
+ const hasCustomFields = params.customFields !== undefined && Object.keys(params.customFields).length > 0;
277
323
  const hasRevisionChanges = (params.addRevisionIDs !== undefined && params.addRevisionIDs.length > 0) ||
278
324
  (params.removeRevisionIDs !== undefined && params.removeRevisionIDs.length > 0);
279
- if (transactions.length === 0 && !hasRevisionChanges) {
325
+ if (transactions.length === 0 && !hasCustomFields && !hasRevisionChanges) {
280
326
  return { content: [{ type: 'text', text: 'No changes specified' }] };
281
327
  }
328
+ const extras = {};
282
329
  let result = undefined;
283
330
  if (transactions.length > 0) {
284
331
  result = await client.call('maniphest.edit', {
@@ -286,18 +333,22 @@ export function registerManiphestTools(server, client) {
286
333
  transactions,
287
334
  });
288
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
+ }
289
341
  // Link/unlink revisions via differential.revision.edit
290
- const revisionResults = {};
291
342
  if (params.addRevisionIDs !== undefined && params.addRevisionIDs.length > 0) {
292
343
  const revPHIDs = await resolveRevisionPHIDs(client, params.addRevisionIDs);
293
- revisionResults.added = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'add');
344
+ extras.addedRevisions = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'add');
294
345
  }
295
346
  if (params.removeRevisionIDs !== undefined && params.removeRevisionIDs.length > 0) {
296
347
  const revPHIDs = await resolveRevisionPHIDs(client, params.removeRevisionIDs);
297
- revisionResults.removed = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'remove');
348
+ extras.removedRevisions = await linkRevisionsToTask(client, params.objectIdentifier, revPHIDs, 'remove');
298
349
  }
299
- const output = hasRevisionChanges
300
- ? { ...(result !== undefined ? { task: result } : {}), linkedRevisions: revisionResults }
350
+ const output = Object.keys(extras).length > 0
351
+ ? { ...(result !== undefined ? { task: result } : {}), ...extras }
301
352
  : result;
302
353
  return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
303
354
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freelancercom/phabricator-mcp",
3
- "version": "2.0.17",
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",