@freelancercom/phabricator-mcp 1.0.5 → 2.0.0

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/README.md CHANGED
@@ -170,11 +170,20 @@ Add to your `~/.claude/settings.json`:
170
170
  "permissions": {
171
171
  "allow": [
172
172
  "mcp__phabricator__phabricator_task_search",
173
+ "mcp__phabricator__phabricator_task_status_search",
174
+ "mcp__phabricator__phabricator_task_priority_search",
173
175
  "mcp__phabricator__phabricator_revision_search",
174
176
  "mcp__phabricator__phabricator_diff_search",
175
- "mcp__phabricator__phabricator_get_raw_diff",
177
+ "mcp__phabricator__phabricator_diff_raw",
178
+ "mcp__phabricator__phabricator_changeset_search",
176
179
  "mcp__phabricator__phabricator_repository_search",
177
180
  "mcp__phabricator__phabricator_commit_search",
181
+ "mcp__phabricator__phabricator_repository_browse",
182
+ "mcp__phabricator__phabricator_repository_file_content",
183
+ "mcp__phabricator__phabricator_branch_search",
184
+ "mcp__phabricator__phabricator_tag_search",
185
+ "mcp__phabricator__phabricator_repository_file_history",
186
+ "mcp__phabricator__phabricator_repository_code_search",
178
187
  "mcp__phabricator__phabricator_user_whoami",
179
188
  "mcp__phabricator__phabricator_user_search",
180
189
  "mcp__phabricator__phabricator_project_search",
@@ -183,6 +192,18 @@ Add to your `~/.claude/settings.json`:
183
192
  "mcp__phabricator__phabricator_document_search",
184
193
  "mcp__phabricator__phabricator_blog_search",
185
194
  "mcp__phabricator__phabricator_blog_post_search",
195
+ "mcp__phabricator__phabricator_file_search",
196
+ "mcp__phabricator__phabricator_file_info",
197
+ "mcp__phabricator__phabricator_buildable_search",
198
+ "mcp__phabricator__phabricator_build_search",
199
+ "mcp__phabricator__phabricator_build_target_search",
200
+ "mcp__phabricator__phabricator_build_log_search",
201
+ "mcp__phabricator__phabricator_build_plan_search",
202
+ "mcp__phabricator__phabricator_owners_search",
203
+ "mcp__phabricator__phabricator_feed_query",
204
+ "mcp__phabricator__phabricator_conpherence_search",
205
+ "mcp__phabricator__phabricator_conpherence_read",
206
+ "mcp__phabricator__phabricator_audit_query",
186
207
  "mcp__phabricator__phabricator_phid_lookup",
187
208
  "mcp__phabricator__phabricator_phid_query",
188
209
  "mcp__phabricator__phabricator_transaction_search",
@@ -204,15 +225,19 @@ To allowlist all tools including write operations, use `"mcp__phabricator__*"` i
204
225
  | `phabricator_task_create` | Create a new task |
205
226
  | `phabricator_task_edit` | Edit an existing task |
206
227
  | `phabricator_task_add_comment` | Add a comment to a task |
228
+ | `phabricator_task_status_search` | List all available task statuses on the instance |
229
+ | `phabricator_task_priority_search` | List all available task priorities on the instance |
207
230
 
208
231
  ### Code Reviews (Differential)
209
232
 
210
233
  | Tool | Description |
211
234
  |------|-------------|
212
235
  | `phabricator_revision_search` | Search code review revisions |
213
- | `phabricator_revision_edit` | Edit a revision (add reviewers, comment, etc.) |
214
- | `phabricator_get_raw_diff` | Get the raw diff/patch content for a diff by ID |
215
- | `phabricator_diff_search` | Search diffs |
236
+ | `phabricator_revision_edit` | Edit a revision (accept, reject, abandon, add reviewers, comment, etc.) |
237
+ | `phabricator_revision_inline_comment` | Create an inline comment on a specific line of a diff |
238
+ | `phabricator_diff_raw` | Get the raw diff/patch content for a diff by ID |
239
+ | `phabricator_diff_search` | Search diffs (code change snapshots within a revision) |
240
+ | `phabricator_changeset_search` | Search changesets (individual changed files within a diff) |
216
241
 
217
242
  ### Repositories (Diffusion)
218
243
 
@@ -220,6 +245,12 @@ To allowlist all tools including write operations, use `"mcp__phabricator__*"` i
220
245
  |------|-------------|
221
246
  | `phabricator_repository_search` | Search repositories |
222
247
  | `phabricator_commit_search` | Search commits |
248
+ | `phabricator_repository_browse` | Browse a repository directory tree |
249
+ | `phabricator_repository_file_content` | Read file contents from a repository |
250
+ | `phabricator_branch_search` | List branches in a repository |
251
+ | `phabricator_tag_search` | List tags in a repository |
252
+ | `phabricator_repository_file_history` | Get commit history for a file path |
253
+ | `phabricator_repository_code_search` | Search (grep) file contents within a repository |
223
254
 
224
255
  ### Users
225
256
 
@@ -233,7 +264,7 @@ To allowlist all tools including write operations, use `"mcp__phabricator__*"` i
233
264
  | Tool | Description |
234
265
  |------|-------------|
235
266
  | `phabricator_project_search` | Search projects |
236
- | `phabricator_project_edit` | Edit a project |
267
+ | `phabricator_project_edit` | Create or edit a project |
237
268
  | `phabricator_column_search` | Search workboard columns |
238
269
 
239
270
  ### Pastes
@@ -242,13 +273,15 @@ To allowlist all tools including write operations, use `"mcp__phabricator__*"` i
242
273
  |------|-------------|
243
274
  | `phabricator_paste_search` | Search pastes |
244
275
  | `phabricator_paste_create` | Create a paste |
276
+ | `phabricator_paste_edit` | Edit an existing paste |
245
277
 
246
278
  ### Wiki (Phriction)
247
279
 
248
280
  | Tool | Description |
249
281
  |------|-------------|
250
282
  | `phabricator_document_search` | Search wiki documents |
251
- | `phabricator_document_edit` | Edit a wiki document |
283
+ | `phabricator_document_edit` | Create or edit a wiki document |
284
+ | `phabricator_document_add_comment` | Add a comment to a wiki document |
252
285
 
253
286
  ### Blogs (Phame)
254
287
 
@@ -266,6 +299,53 @@ To allowlist all tools including write operations, use `"mcp__phabricator__*"` i
266
299
  |------|-------------|
267
300
  | `phabricator_transaction_search` | Search transactions (comments, status changes, etc.) on any object |
268
301
 
302
+ ### Files
303
+
304
+ | Tool | Description |
305
+ |------|-------------|
306
+ | `phabricator_file_upload` | Upload a file and get an ID for embedding in descriptions/comments via `{F<id>}` |
307
+ | `phabricator_file_search` | Search for files |
308
+ | `phabricator_file_info` | Get file metadata (name, size, MIME type, URI) |
309
+
310
+ ### Builds (Harbormaster)
311
+
312
+ | Tool | Description |
313
+ |------|-------------|
314
+ | `phabricator_buildable_search` | Search buildables (revisions/commits with builds) |
315
+ | `phabricator_build_search` | Search builds (CI/build results) |
316
+ | `phabricator_build_target_search` | Search build targets (individual build steps) |
317
+ | `phabricator_build_log_search` | Search build logs (output from build steps) |
318
+ | `phabricator_build_command` | Report build status to Harbormaster (pass, fail, work) |
319
+ | `phabricator_build_plan_search` | Search build plans (CI pipeline configurations) |
320
+
321
+ ### Code Ownership (Owners)
322
+
323
+ | Tool | Description |
324
+ |------|-------------|
325
+ | `phabricator_owners_search` | Search code ownership packages |
326
+
327
+ ### Activity Feed
328
+
329
+ | Tool | Description |
330
+ |------|-------------|
331
+ | `phabricator_feed_query` | Query the activity feed (recent task updates, revision changes, commits, etc.) |
332
+
333
+ ### Chat (Conpherence)
334
+
335
+ | Tool | Description |
336
+ |------|-------------|
337
+ | `phabricator_conpherence_search` | Search chat rooms/threads |
338
+ | `phabricator_conpherence_create` | Create a new chat room/thread |
339
+ | `phabricator_conpherence_edit` | Edit a chat room (rename, manage participants) |
340
+ | `phabricator_conpherence_read` | Read messages from a chat thread |
341
+ | `phabricator_conpherence_send` | Send a message to a chat thread |
342
+
343
+ ### Audits
344
+
345
+ | Tool | Description |
346
+ |------|-------------|
347
+ | `phabricator_audit_query` | Search commit audit requests |
348
+
269
349
  ### PHID Utilities
270
350
 
271
351
  | Tool | Description |
@@ -288,12 +368,25 @@ Once connected, just ask your AI assistant to perform Phabricator tasks in natur
288
368
  - "Create a task titled 'Fix login bug' in project Backend"
289
369
  - "Add a comment to T12345 saying the fix is ready for review"
290
370
  - "Close task T12345"
371
+ - "What custom fields are available for incident tasks?"
372
+ - "Set the start date and root cause category on T12345"
373
+ - "Make T456 a subtask of T123"
374
+ - "Upload this screenshot and add it to the description of T789"
291
375
 
292
376
  **Code Reviews**
293
377
  - "Show my open diffs"
294
378
  - "What's the status of D6789?"
295
379
  - "Review the code changes in D6789"
296
380
  - "Add @alice as a reviewer to D6789"
381
+ - "Accept D6789"
382
+ - "Leave an inline comment on line 42 of src/index.ts in D6789"
383
+
384
+ **Repositories & Builds**
385
+ - "Show me the contents of src/config.ts in repo Backend"
386
+ - "Browse the /src directory in the main repo"
387
+ - "Is the build passing on D6789?"
388
+ - "Show me the build logs for D6789"
389
+ - "Who owns the code in /src/auth/?"
297
390
 
298
391
  **Search & Lookup**
299
392
  - "Find user john.doe"
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ConduitClient } from '../client/conduit.js';
3
+ export declare function registerAuditTools(server: McpServer, client: ConduitClient): void;
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ export function registerAuditTools(server, client) {
3
+ // Query audits
4
+ server.tool('phabricator_audit_query', 'Search commit audit requests. Find commits needing audit, or audits by a specific user. Uses the audit.query endpoint (no modern replacement available).', {
5
+ auditorPHIDs: z.array(z.string()).optional().describe('Auditor user/project PHIDs'),
6
+ commitPHIDs: z.array(z.string()).optional().describe('Commit PHIDs to check audit status for'),
7
+ status: z.string().optional().describe('Audit status filter: "audit-none", "audit-needs-audit", "audit-accepted", "audit-concern-raised", "audit-requested"'),
8
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
9
+ offset: z.coerce.number().optional().describe('Result offset for pagination'),
10
+ }, async (params) => {
11
+ const result = await client.call('audit.query', params);
12
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
13
+ });
14
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ConduitClient } from '../client/conduit.js';
3
+ export declare function registerConpherenceTools(server: McpServer, client: ConduitClient): void;
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
3
+ export function registerConpherenceTools(server, client) {
4
+ // Search chat rooms
5
+ server.tool('phabricator_conpherence_search', 'Search Conpherence chat rooms/threads', {
6
+ queryKey: z.string().optional().describe('Built-in query: "all", "participant"'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Room IDs'),
9
+ phids: z.array(z.string()).optional().describe('Room PHIDs'),
10
+ participants: z.array(z.string()).optional().describe('Participant user PHIDs'),
11
+ })).optional().describe('Search constraints'),
12
+ attachments: jsonCoerce(z.object({
13
+ participants: z.boolean().optional().describe('Include participant details'),
14
+ })).optional().describe('Data attachments'),
15
+ order: z.string().optional().describe('Result order'),
16
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
17
+ after: z.string().optional().describe('Pagination cursor'),
18
+ }, async (params) => {
19
+ const result = await client.call('conpherence.search', params);
20
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
21
+ });
22
+ // Read messages in a thread
23
+ server.tool('phabricator_conpherence_read', 'Read messages from a Conpherence chat room/thread (returned in reverse chronological order). Uses conpherence.querythread (the only Conduit method that returns message content).', {
24
+ roomID: z.coerce.number().describe('Numeric room ID (use phabricator_conpherence_search to find it)'),
25
+ limit: z.coerce.number().max(100).optional().describe('Maximum messages to return'),
26
+ offset: z.coerce.number().optional().describe('Result offset for pagination'),
27
+ }, async (params) => {
28
+ const result = await client.call('conpherence.querythread', {
29
+ ids: [params.roomID],
30
+ limit: params.limit,
31
+ offset: params.offset,
32
+ });
33
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
34
+ });
35
+ // Create a new thread
36
+ server.tool('phabricator_conpherence_create', 'Create a new Conpherence chat room/thread', {
37
+ title: z.string().describe('Thread title'),
38
+ message: z.string().optional().describe('Initial message (supports Remarkup)'),
39
+ participantPHIDs: z.array(z.string()).optional().describe('Participant user PHIDs to add'),
40
+ }, async (params) => {
41
+ const transactions = [
42
+ { type: 'title', value: params.title },
43
+ ];
44
+ if (params.message !== undefined) {
45
+ transactions.push({ type: 'comment', value: params.message });
46
+ }
47
+ if (params.participantPHIDs !== undefined) {
48
+ transactions.push({ type: 'participants.add', value: params.participantPHIDs });
49
+ }
50
+ const result = await client.call('conpherence.edit', { transactions });
51
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
52
+ });
53
+ // Edit an existing thread
54
+ server.tool('phabricator_conpherence_edit', 'Edit a Conpherence chat room/thread. Rename it or manage participants.', {
55
+ objectIdentifier: z.string().describe('Room ID or PHID'),
56
+ title: z.string().optional().describe('New room title'),
57
+ addParticipantPHIDs: z.array(z.string()).optional().describe('Participant PHIDs to add'),
58
+ removeParticipantPHIDs: z.array(z.string()).optional().describe('Participant PHIDs to remove'),
59
+ }, async (params) => {
60
+ const transactions = [];
61
+ if (params.title !== undefined) {
62
+ transactions.push({ type: 'title', value: params.title });
63
+ }
64
+ if (params.addParticipantPHIDs !== undefined) {
65
+ transactions.push({ type: 'participants.add', value: params.addParticipantPHIDs });
66
+ }
67
+ if (params.removeParticipantPHIDs !== undefined) {
68
+ transactions.push({ type: 'participants.remove', value: params.removeParticipantPHIDs });
69
+ }
70
+ if (transactions.length === 0) {
71
+ return { content: [{ type: 'text', text: 'No changes specified' }] };
72
+ }
73
+ const result = await client.call('conpherence.edit', {
74
+ objectIdentifier: params.objectIdentifier,
75
+ transactions,
76
+ });
77
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
78
+ });
79
+ // Send a message
80
+ server.tool('phabricator_conpherence_send', 'Send a message to a Conpherence chat room/thread', {
81
+ objectIdentifier: z.string().describe('Room ID or PHID'),
82
+ message: z.string().describe('Message text (supports Remarkup)'),
83
+ }, async (params) => {
84
+ const result = await client.call('conpherence.edit', {
85
+ objectIdentifier: params.objectIdentifier,
86
+ transactions: [{ type: 'comment', value: params.message }],
87
+ });
88
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
89
+ });
90
+ }
@@ -3,7 +3,7 @@ import { jsonCoerce } from './coerce.js';
3
3
  export function registerDifferentialTools(server, client) {
4
4
  // Search revisions
5
5
  server.tool('phabricator_revision_search', 'Search Differential revisions (code reviews)', {
6
- queryKey: z.string().optional().describe('Built-in query: "all", "active", "authored", "waiting"'),
6
+ queryKey: z.string().optional().describe('Built-in query: "all", "active", "authored", "waiting", "reviewable"'),
7
7
  constraints: jsonCoerce(z.object({
8
8
  ids: z.array(z.coerce.number()).optional().describe('Revision IDs'),
9
9
  phids: z.array(z.string()).optional().describe('Revision PHIDs'),
@@ -11,21 +11,29 @@ export function registerDifferentialTools(server, client) {
11
11
  reviewerPHIDs: z.array(z.string()).optional().describe('Reviewer PHIDs'),
12
12
  repositoryPHIDs: z.array(z.string()).optional().describe('Repository PHIDs'),
13
13
  statuses: z.array(z.string()).optional().describe('Statuses: needs-review, needs-revision, accepted, published, abandoned, changes-planned'),
14
+ responsiblePHIDs: z.array(z.string()).optional().describe('User PHIDs who are responsible (as author or reviewer)'),
15
+ affectedPaths: z.array(z.string()).optional().describe('File paths affected by the revision'),
16
+ createdStart: z.coerce.number().optional().describe('Created after (epoch timestamp)'),
17
+ createdEnd: z.coerce.number().optional().describe('Created before (epoch timestamp)'),
18
+ modifiedStart: z.coerce.number().optional().describe('Modified after (epoch timestamp)'),
19
+ modifiedEnd: z.coerce.number().optional().describe('Modified before (epoch timestamp)'),
20
+ query: z.string().optional().describe('Full-text search query'),
14
21
  })).optional().describe('Search constraints'),
15
22
  attachments: jsonCoerce(z.object({
16
23
  reviewers: z.boolean().optional().describe('Include reviewers'),
17
24
  subscribers: z.boolean().optional().describe('Include subscribers'),
18
25
  projects: z.boolean().optional().describe('Include projects'),
26
+ 'reviewers-extra': z.boolean().optional().describe('Include detailed reviewer info with status (accepted, rejected, etc.)'),
19
27
  })).optional().describe('Data attachments'),
20
- order: z.string().optional().describe('Result order'),
21
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
28
+ order: z.string().optional().describe('Result order: "newest", "oldest", "updated", "relevance"'),
29
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
22
30
  after: z.string().optional().describe('Pagination cursor'),
23
31
  }, async (params) => {
24
32
  const result = await client.call('differential.revision.search', params);
25
33
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
26
34
  });
27
35
  // Edit revision
28
- server.tool('phabricator_revision_edit', 'Edit a Differential revision', {
36
+ server.tool('phabricator_revision_edit', 'Edit a Differential revision. Supports actions like accept, reject, abandon, request-review, plan-changes, and commandeer. Can also add/remove reviewers, subscribers, and comments.', {
29
37
  objectIdentifier: z.string().describe('Revision PHID or ID (e.g., "D123")'),
30
38
  title: z.string().optional().describe('New title'),
31
39
  summary: z.string().optional().describe('New summary'),
@@ -35,6 +43,10 @@ export function registerDifferentialTools(server, client) {
35
43
  addProjectPHIDs: z.array(z.string()).optional().describe('Add projects'),
36
44
  removeProjectPHIDs: z.array(z.string()).optional().describe('Remove projects'),
37
45
  comment: z.string().optional().describe('Add a comment'),
46
+ action: z.enum(['accept', 'reject', 'abandon', 'reclaim', 'request-review', 'resign', 'commandeer', 'plan-changes']).optional().describe('Revision action to take'),
47
+ addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
48
+ removeSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to remove'),
49
+ repositoryPHID: z.string().optional().describe('Repository PHID to associate with the revision'),
38
50
  }, async (params) => {
39
51
  const transactions = [];
40
52
  if (params.title !== undefined) {
@@ -61,6 +73,18 @@ export function registerDifferentialTools(server, client) {
61
73
  if (params.comment !== undefined) {
62
74
  transactions.push({ type: 'comment', value: params.comment });
63
75
  }
76
+ if (params.action !== undefined) {
77
+ transactions.push({ type: 'action', value: params.action });
78
+ }
79
+ if (params.addSubscriberPHIDs !== undefined) {
80
+ transactions.push({ type: 'subscribers.add', value: params.addSubscriberPHIDs });
81
+ }
82
+ if (params.removeSubscriberPHIDs !== undefined) {
83
+ transactions.push({ type: 'subscribers.remove', value: params.removeSubscriberPHIDs });
84
+ }
85
+ if (params.repositoryPHID !== undefined) {
86
+ transactions.push({ type: 'repositoryPHID', value: params.repositoryPHID });
87
+ }
64
88
  if (transactions.length === 0) {
65
89
  return { content: [{ type: 'text', text: 'No changes specified' }] };
66
90
  }
@@ -71,7 +95,7 @@ export function registerDifferentialTools(server, client) {
71
95
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
72
96
  });
73
97
  // Get raw diff content
74
- server.tool('phabricator_get_raw_diff', 'Get the raw diff/patch content for a Differential diff by diff ID. Use phabricator_diff_search to find the diff ID from a revision PHID first.', {
98
+ server.tool('phabricator_diff_raw', 'Get the raw diff/patch content for a Differential diff by diff ID. Use phabricator_diff_search to find the diff ID from a revision PHID first.', {
75
99
  diffID: z.coerce.number().describe('The diff ID (numeric, e.g., 1392561). Use phabricator_diff_search to find this from a revision.'),
76
100
  }, async (params) => {
77
101
  const result = await client.call('differential.getrawdiff', {
@@ -80,7 +104,8 @@ export function registerDifferentialTools(server, client) {
80
104
  return { content: [{ type: 'text', text: result }] };
81
105
  });
82
106
  // Search diffs
83
- server.tool('phabricator_diff_search', 'Search Differential diffs', {
107
+ server.tool('phabricator_diff_search', 'Search Differential diffs (code change snapshots within a revision). A revision may have multiple diffs as it gets updated.', {
108
+ queryKey: z.string().optional().describe('Built-in query: "all"'),
84
109
  constraints: jsonCoerce(z.object({
85
110
  ids: z.array(z.coerce.number()).optional().describe('Diff IDs'),
86
111
  phids: z.array(z.string()).optional().describe('Diff PHIDs'),
@@ -89,10 +114,48 @@ export function registerDifferentialTools(server, client) {
89
114
  attachments: jsonCoerce(z.object({
90
115
  commits: z.boolean().optional().describe('Include commit info'),
91
116
  })).optional().describe('Data attachments'),
92
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
117
+ order: z.string().optional().describe('Result order: "newest", "oldest"'),
118
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
93
119
  after: z.string().optional().describe('Pagination cursor'),
94
120
  }, async (params) => {
95
121
  const result = await client.call('differential.diff.search', params);
96
122
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
97
123
  });
124
+ // Search changesets (changed files within a diff)
125
+ server.tool('phabricator_changeset_search', 'Search changesets (individual changed files) within a Differential diff. Use phabricator_diff_search to find the diff PHID first.', {
126
+ queryKey: z.string().optional().describe('Built-in query: "all"'),
127
+ constraints: jsonCoerce(z.object({
128
+ diffPHIDs: z.array(z.string()).optional().describe('Diff PHIDs to list changesets for'),
129
+ })).optional().describe('Search constraints'),
130
+ attachments: jsonCoerce(z.object({
131
+ hunks: z.boolean().optional().describe('Include diff hunks (actual changed content)'),
132
+ })).optional().describe('Data attachments'),
133
+ order: z.string().optional().describe('Result order'),
134
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
135
+ after: z.string().optional().describe('Pagination cursor'),
136
+ }, async (params) => {
137
+ const result = await client.call('differential.changeset.search', params);
138
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
139
+ });
140
+ // Create inline comment on a diff
141
+ server.tool('phabricator_revision_inline_comment', 'Create an inline comment on a specific line of a Differential diff. The comment will appear as a draft — publish it by calling phabricator_revision_edit with a comment on the same revision.', {
142
+ revisionID: z.coerce.number().describe('Numeric revision ID (e.g., 123). Do not include the "D" prefix.'),
143
+ diffID: z.coerce.number().describe('Diff ID to comment on. Use phabricator_diff_search to find this.'),
144
+ filePath: z.string().describe('Path to the file being commented on'),
145
+ lineNumber: z.coerce.number().describe('Line number in the file'),
146
+ lineLength: z.coerce.number().optional().describe('Number of lines the comment spans (default: 0 for single line)'),
147
+ content: z.string().describe('Comment text (supports Remarkup)'),
148
+ isNewFile: z.boolean().optional().describe('Whether the line number refers to the new file (true) or old file (false). Default: true'),
149
+ }, async (params) => {
150
+ const result = await client.call('differential.createinline', {
151
+ revisionID: params.revisionID,
152
+ diffID: params.diffID,
153
+ filePath: params.filePath,
154
+ lineNumber: params.lineNumber,
155
+ lineLength: params.lineLength ?? 0,
156
+ content: params.content,
157
+ isNewFile: params.isNewFile ?? true,
158
+ });
159
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
160
+ });
98
161
  }
@@ -19,7 +19,7 @@ export function registerDiffusionTools(server, client) {
19
19
  projects: z.boolean().optional().describe('Include projects'),
20
20
  })).optional().describe('Data attachments'),
21
21
  order: z.string().optional().describe('Result order'),
22
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
22
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
23
23
  after: z.string().optional().describe('Pagination cursor'),
24
24
  }, async (params) => {
25
25
  const result = await client.call('diffusion.repository.search', params);
@@ -27,6 +27,7 @@ export function registerDiffusionTools(server, client) {
27
27
  });
28
28
  // Search commits
29
29
  server.tool('phabricator_commit_search', 'Search Diffusion commits', {
30
+ queryKey: z.string().optional().describe('Built-in query: "all", "authored"'),
30
31
  constraints: jsonCoerce(z.object({
31
32
  ids: z.array(z.coerce.number()).optional().describe('Commit IDs'),
32
33
  phids: z.array(z.string()).optional().describe('Commit PHIDs'),
@@ -38,12 +39,88 @@ export function registerDiffusionTools(server, client) {
38
39
  attachments: jsonCoerce(z.object({
39
40
  projects: z.boolean().optional().describe('Include projects'),
40
41
  subscribers: z.boolean().optional().describe('Include subscribers'),
42
+ auditors: z.boolean().optional().describe('Include auditor info'),
41
43
  })).optional().describe('Data attachments'),
42
44
  order: z.string().optional().describe('Result order'),
43
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
45
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
44
46
  after: z.string().optional().describe('Pagination cursor'),
45
47
  }, async (params) => {
46
48
  const result = await client.call('diffusion.commit.search', params);
47
49
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
48
50
  });
51
+ // Browse repository file tree
52
+ server.tool('phabricator_repository_browse', 'Browse a repository directory tree at a given path and commit/branch', {
53
+ path: z.string().describe('Path to browse (e.g., "/", "/src/")'),
54
+ repository: z.string().optional().describe('Repository callsign, short name, or PHID'),
55
+ commit: z.string().optional().describe('Commit hash or branch name (default: HEAD)'),
56
+ }, async (params) => {
57
+ const result = await client.call('diffusion.browsequery', {
58
+ path: params.path,
59
+ repository: params.repository,
60
+ commit: params.commit,
61
+ });
62
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
63
+ });
64
+ // Read file content from repository
65
+ server.tool('phabricator_repository_file_content', 'Read file contents from a Diffusion repository at a given path and commit/branch', {
66
+ path: z.string().describe('File path in the repository (e.g., "src/index.ts")'),
67
+ repository: z.string().optional().describe('Repository callsign, short name, or PHID'),
68
+ commit: z.string().optional().describe('Commit hash or branch name (default: HEAD)'),
69
+ }, async (params) => {
70
+ const result = await client.call('diffusion.filecontentquery', {
71
+ path: params.path,
72
+ repository: params.repository,
73
+ commit: params.commit,
74
+ });
75
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
76
+ });
77
+ // List branches
78
+ server.tool('phabricator_branch_search', 'List branches in a Diffusion repository', {
79
+ repository: z.string().describe('Repository callsign, short name, or PHID'),
80
+ contains: z.string().optional().describe('Only branches containing this commit'),
81
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
82
+ offset: z.coerce.number().optional().describe('Result offset for pagination'),
83
+ }, async (params) => {
84
+ const result = await client.call('diffusion.branchquery', params);
85
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
86
+ });
87
+ // List tags
88
+ server.tool('phabricator_tag_search', 'List tags in a Diffusion repository', {
89
+ repository: z.string().describe('Repository callsign, short name, or PHID'),
90
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
91
+ offset: z.coerce.number().optional().describe('Result offset for pagination'),
92
+ }, async (params) => {
93
+ const result = await client.call('diffusion.tagsquery', params);
94
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
95
+ });
96
+ // File commit history
97
+ server.tool('phabricator_repository_file_history', 'Get commit history for a file path in a Diffusion repository', {
98
+ path: z.string().describe('File path in the repository'),
99
+ repository: z.string().optional().describe('Repository callsign, short name, or PHID'),
100
+ commit: z.string().optional().describe('Commit hash or branch to start from (default: HEAD)'),
101
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
102
+ offset: z.coerce.number().optional().describe('Result offset for pagination'),
103
+ }, async (params) => {
104
+ const result = await client.call('diffusion.historyquery', params);
105
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
106
+ });
107
+ // Search file contents in repository
108
+ server.tool('phabricator_repository_code_search', 'Search (grep) file contents within a Diffusion repository', {
109
+ path: z.string().optional().describe('Directory path to search within (default: root)'),
110
+ repository: z.string().describe('Repository callsign, short name, or PHID'),
111
+ query: z.string().describe('Search query / pattern'),
112
+ commit: z.string().optional().describe('Commit hash or branch (default: HEAD)'),
113
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
114
+ offset: z.coerce.number().optional().describe('Result offset for pagination'),
115
+ }, async (params) => {
116
+ const result = await client.call('diffusion.searchquery', {
117
+ path: params.path ?? '/',
118
+ repository: params.repository,
119
+ grep: params.query,
120
+ commit: params.commit,
121
+ limit: params.limit,
122
+ offset: params.offset,
123
+ });
124
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
125
+ });
49
126
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ConduitClient } from '../client/conduit.js';
3
+ export declare function registerFeedTools(server: McpServer, client: ConduitClient): void;
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export function registerFeedTools(server, client) {
3
+ // Query activity feed
4
+ server.tool('phabricator_feed_query', 'Query the Phabricator activity feed. Returns recent activity (task updates, revision changes, commits, etc.) as an object keyed by story PHID. Uses feed.query (the only Conduit method for feed data).', {
5
+ filterPHIDs: z.array(z.string()).optional().describe('Only show activity involving these PHIDs (user, project, task, etc.)'),
6
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
7
+ after: z.string().optional().describe('Cursor for pagination (chronological key from previous results)'),
8
+ before: z.string().optional().describe('Cursor for reverse pagination'),
9
+ }, async (params) => {
10
+ const result = await client.call('feed.query', params);
11
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
12
+ });
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ConduitClient } from '../client/conduit.js';
3
+ export declare function registerFileTools(server: McpServer, client: ConduitClient): void;
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
3
+ export function registerFileTools(server, client) {
4
+ // Upload a file
5
+ server.tool('phabricator_file_upload', 'Upload a file to Phabricator. Returns a file PHID that can be used with phabricator_file_info to get the file ID for embedding in Remarkup via {F<id>}.', {
6
+ name: z.string().describe('Filename with extension (e.g. "screenshot.png")'),
7
+ data_base64: z.string().describe('Base64-encoded file content'),
8
+ }, async (params) => {
9
+ const phid = await client.call('file.upload', {
10
+ name: params.name,
11
+ data_base64: params.data_base64,
12
+ });
13
+ return { content: [{ type: 'text', text: phid }] };
14
+ });
15
+ // Search files
16
+ server.tool('phabricator_file_search', 'Search for files in Phabricator', {
17
+ queryKey: z.string().optional().describe('Built-in query: "all", "authored"'),
18
+ constraints: jsonCoerce(z.object({
19
+ ids: z.array(z.coerce.number()).optional().describe('File IDs'),
20
+ phids: z.array(z.string()).optional().describe('File PHIDs'),
21
+ authorPHIDs: z.array(z.string()).optional().describe('Author PHIDs'),
22
+ names: z.array(z.string()).optional().describe('File names'),
23
+ })).optional().describe('Search constraints'),
24
+ order: z.string().optional().describe('Result order'),
25
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
26
+ after: z.string().optional().describe('Pagination cursor'),
27
+ }, async (params) => {
28
+ const result = await client.call('file.search', params);
29
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
30
+ });
31
+ // Get file info
32
+ server.tool('phabricator_file_info', 'Get metadata about a file (name, size, MIME type, URI). Use the returned URI to download. Provide at least one of id or phid. Uses file.info (the only Conduit method that returns download URIs).', {
33
+ id: z.coerce.number().optional().describe('File ID (provide this or phid)'),
34
+ phid: z.string().optional().describe('File PHID (provide this or id)'),
35
+ }, async (params) => {
36
+ const result = await client.call('file.info', params);
37
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
38
+ });
39
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ConduitClient } from '../client/conduit.js';
3
+ export declare function registerHarbormasterTools(server: McpServer, client: ConduitClient): void;
@@ -0,0 +1,104 @@
1
+ import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
3
+ export function registerHarbormasterTools(server, client) {
4
+ // Search buildables
5
+ server.tool('phabricator_buildable_search', 'Search Harbormaster buildables (objects that can be built, like revisions or commits)', {
6
+ queryKey: z.string().optional().describe('Built-in query: "all"'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Buildable IDs'),
9
+ phids: z.array(z.string()).optional().describe('Buildable PHIDs'),
10
+ objectPHIDs: z.array(z.string()).optional().describe('Object PHIDs (revision or commit PHIDs)'),
11
+ containerPHIDs: z.array(z.string()).optional().describe('Container PHIDs'),
12
+ statuses: z.array(z.string()).optional().describe('Buildable statuses'),
13
+ })).optional().describe('Search constraints'),
14
+ attachments: jsonCoerce(z.object({
15
+ builds: z.boolean().optional().describe('Include builds for each buildable'),
16
+ })).optional().describe('Data attachments'),
17
+ order: z.string().optional().describe('Result order'),
18
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
19
+ after: z.string().optional().describe('Pagination cursor'),
20
+ }, async (params) => {
21
+ const result = await client.call('harbormaster.buildable.search', params);
22
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
23
+ });
24
+ // Search builds
25
+ server.tool('phabricator_build_search', 'Search Harbormaster builds (CI/build results)', {
26
+ queryKey: z.string().optional().describe('Built-in query: "all"'),
27
+ constraints: jsonCoerce(z.object({
28
+ ids: z.array(z.coerce.number()).optional().describe('Build IDs'),
29
+ phids: z.array(z.string()).optional().describe('Build PHIDs'),
30
+ buildablePHIDs: z.array(z.string()).optional().describe('Buildable PHIDs'),
31
+ buildPlanPHIDs: z.array(z.string()).optional().describe('Build plan PHIDs'),
32
+ statuses: z.array(z.string()).optional().describe('Build statuses: building, passed, failed, aborted, error, paused, deadlocked'),
33
+ })).optional().describe('Search constraints'),
34
+ attachments: jsonCoerce(z.object({
35
+ targets: z.boolean().optional().describe('Include build targets for each build'),
36
+ })).optional().describe('Data attachments'),
37
+ order: z.string().optional().describe('Result order'),
38
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
39
+ after: z.string().optional().describe('Pagination cursor'),
40
+ }, async (params) => {
41
+ const result = await client.call('harbormaster.build.search', params);
42
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
43
+ });
44
+ // Search build targets
45
+ server.tool('phabricator_build_target_search', 'Search Harbormaster build targets (individual build steps within a build)', {
46
+ queryKey: z.string().optional().describe('Built-in query: "all"'),
47
+ constraints: jsonCoerce(z.object({
48
+ ids: z.array(z.coerce.number()).optional().describe('Target IDs'),
49
+ phids: z.array(z.string()).optional().describe('Target PHIDs'),
50
+ buildPHIDs: z.array(z.string()).optional().describe('Build PHIDs'),
51
+ })).optional().describe('Search constraints'),
52
+ order: z.string().optional().describe('Result order'),
53
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
54
+ after: z.string().optional().describe('Pagination cursor'),
55
+ }, async (params) => {
56
+ const result = await client.call('harbormaster.target.search', params);
57
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
58
+ });
59
+ // Search build logs
60
+ server.tool('phabricator_build_log_search', 'Search Harbormaster build logs (output from build steps). Use phabricator_build_target_search to find target PHIDs first.', {
61
+ queryKey: z.string().optional().describe('Built-in query: "all"'),
62
+ constraints: jsonCoerce(z.object({
63
+ ids: z.array(z.coerce.number()).optional().describe('Log IDs'),
64
+ phids: z.array(z.string()).optional().describe('Log PHIDs'),
65
+ buildTargetPHIDs: z.array(z.string()).optional().describe('Build target PHIDs'),
66
+ })).optional().describe('Search constraints'),
67
+ attachments: jsonCoerce(z.object({
68
+ content: z.boolean().optional().describe('Include actual log text content'),
69
+ })).optional().describe('Data attachments'),
70
+ order: z.string().optional().describe('Result order'),
71
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
72
+ after: z.string().optional().describe('Pagination cursor'),
73
+ }, async (params) => {
74
+ const result = await client.call('harbormaster.log.search', params);
75
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
76
+ });
77
+ // Send build command
78
+ server.tool('phabricator_build_command', 'Report build status to Harbormaster. Used by external build systems to notify Phabricator of build results. Provide the build target PHID (use phabricator_build_target_search to find it).', {
79
+ buildTargetPHID: z.string().describe('Build target PHID to send the message to. Use phabricator_build_target_search to find this.'),
80
+ type: z.enum(['pass', 'fail', 'work']).describe('Message type: "pass" (build succeeded), "fail" (build failed), "work" (build is still running)'),
81
+ }, async (params) => {
82
+ const result = await client.call('harbormaster.sendmessage', {
83
+ buildTargetPHID: params.buildTargetPHID,
84
+ type: params.type,
85
+ });
86
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
87
+ });
88
+ // Search build plans
89
+ server.tool('phabricator_build_plan_search', 'Search Harbormaster build plans (CI pipeline configurations)', {
90
+ queryKey: z.string().optional().describe('Built-in query: "all", "active"'),
91
+ constraints: jsonCoerce(z.object({
92
+ ids: z.array(z.coerce.number()).optional().describe('Build plan IDs'),
93
+ phids: z.array(z.string()).optional().describe('Build plan PHIDs'),
94
+ statuses: z.array(z.string()).optional().describe('Plan statuses'),
95
+ query: z.string().optional().describe('Full-text search query'),
96
+ })).optional().describe('Search constraints'),
97
+ order: z.string().optional().describe('Result order'),
98
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
99
+ after: z.string().optional().describe('Pagination cursor'),
100
+ }, async (params) => {
101
+ const result = await client.call('harbormaster.buildplan.search', params);
102
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
103
+ });
104
+ }
@@ -11,6 +11,12 @@ import { registerPhrictionTools } from './phriction.js';
11
11
  import { registerPhidTools } from './phid.js';
12
12
  import { registerPhameTools } from './phame.js';
13
13
  import { registerTransactionTools } from './transaction.js';
14
+ import { registerFileTools } from './file.js';
15
+ import { registerHarbormasterTools } from './harbormaster.js';
16
+ import { registerOwnersTools } from './owners.js';
17
+ import { registerFeedTools } from './feed.js';
18
+ import { registerConpherenceTools } from './conpherence.js';
19
+ import { registerAuditTools } from './audit.js';
14
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
21
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
16
22
  export function registerAllTools(server, client) {
@@ -27,4 +33,10 @@ export function registerAllTools(server, client) {
27
33
  registerPhidTools(server, client);
28
34
  registerPhameTools(server, client);
29
35
  registerTransactionTools(server, client);
36
+ registerFileTools(server, client);
37
+ registerHarbormasterTools(server, client);
38
+ registerOwnersTools(server, client);
39
+ registerFeedTools(server, client);
40
+ registerConpherenceTools(server, client);
41
+ registerAuditTools(server, client);
30
42
  }
@@ -7,7 +7,7 @@ export function registerManiphestTools(server, client) {
7
7
  constraints: jsonCoerce(z.object({
8
8
  ids: z.array(z.coerce.number()).optional().describe('Task IDs to search for'),
9
9
  phids: z.array(z.string()).optional().describe('Task PHIDs to search for'),
10
- assigned: z.array(z.string()).optional().describe('Assigned user PHIDs'),
10
+ assignedPHIDs: z.array(z.string()).optional().describe('Assigned user PHIDs'),
11
11
  authorPHIDs: z.array(z.string()).optional().describe('Author PHIDs'),
12
12
  statuses: z.array(z.string()).optional().describe('Task statuses: open, resolved, wontfix, invalid, spite, duplicate'),
13
13
  priorities: z.array(z.coerce.number()).optional().describe('Priority levels'),
@@ -15,11 +15,20 @@ export function registerManiphestTools(server, client) {
15
15
  columnPHIDs: z.array(z.string()).optional().describe('Workboard column PHIDs'),
16
16
  projectPHIDs: z.array(z.string()).optional().describe('Project PHIDs (tasks tagged with these projects)'),
17
17
  query: z.string().optional().describe('Full-text search query'),
18
+ createdStart: z.coerce.number().optional().describe('Created after (epoch timestamp)'),
19
+ createdEnd: z.coerce.number().optional().describe('Created before (epoch timestamp)'),
20
+ modifiedStart: z.coerce.number().optional().describe('Modified after (epoch timestamp)'),
21
+ modifiedEnd: z.coerce.number().optional().describe('Modified before (epoch timestamp)'),
22
+ parentIDs: z.array(z.coerce.number()).optional().describe('Parent task IDs'),
23
+ subtaskIDs: z.array(z.coerce.number()).optional().describe('Subtask IDs'),
24
+ hasParents: z.boolean().optional().describe('Filter to tasks that have parent tasks'),
25
+ hasSubtasks: z.boolean().optional().describe('Filter to tasks that have subtasks'),
18
26
  })).optional().describe('Search constraints'),
19
27
  attachments: jsonCoerce(z.object({
20
28
  columns: z.boolean().optional().describe('Include workboard column info'),
21
29
  projects: z.boolean().optional().describe('Include project info'),
22
30
  subscribers: z.boolean().optional().describe('Include subscriber info'),
31
+ 'custom-fields': z.boolean().optional().describe('Include custom field values in results'),
23
32
  })).optional().describe('Data attachments to include'),
24
33
  order: z.string().optional().describe('Result order: "priority", "updated", "newest", "oldest"'),
25
34
  limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
@@ -37,6 +46,11 @@ export function registerManiphestTools(server, client) {
37
46
  projectPHIDs: z.array(z.string()).optional().describe('Project PHIDs to tag'),
38
47
  subscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs'),
39
48
  status: z.string().optional().describe('Initial status'),
49
+ subtype: z.string().optional().describe('Task subtype (e.g. "default", "incident")'),
50
+ parentPHIDs: z.array(z.string()).optional().describe('Parent task PHIDs'),
51
+ subtaskPHIDs: z.array(z.string()).optional().describe('Subtask PHIDs'),
52
+ comment: z.string().optional().describe('Initial comment on the task (supports Remarkup)'),
53
+ customFields: jsonCoerce(z.record(z.string(), z.unknown())).optional().describe('Custom field transactions. Keys are transaction types (e.g. "custom.my-field"), values are the field values. Check your Phabricator Conduit console (conduit/method/maniphest.edit/) for available fields.'),
40
54
  }, async (params) => {
41
55
  const transactions = [
42
56
  { type: 'title', value: params.title },
@@ -59,6 +73,23 @@ export function registerManiphestTools(server, client) {
59
73
  if (params.status !== undefined) {
60
74
  transactions.push({ type: 'status', value: params.status });
61
75
  }
76
+ if (params.subtype !== undefined) {
77
+ transactions.push({ type: 'subtype', value: params.subtype });
78
+ }
79
+ if (params.parentPHIDs !== undefined) {
80
+ transactions.push({ type: 'parents.set', value: params.parentPHIDs });
81
+ }
82
+ if (params.subtaskPHIDs !== undefined) {
83
+ transactions.push({ type: 'subtasks.set', value: params.subtaskPHIDs });
84
+ }
85
+ if (params.comment !== undefined) {
86
+ transactions.push({ type: 'comment', value: params.comment });
87
+ }
88
+ if (params.customFields !== undefined) {
89
+ for (const [key, value] of Object.entries(params.customFields)) {
90
+ transactions.push({ type: key, value });
91
+ }
92
+ }
62
93
  const result = await client.call('maniphest.edit', { transactions });
63
94
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
64
95
  });
@@ -70,11 +101,18 @@ export function registerManiphestTools(server, client) {
70
101
  ownerPHID: z.string().nullable().optional().describe('New owner PHID (null to unassign)'),
71
102
  priority: z.string().optional().describe('New priority'),
72
103
  status: z.string().optional().describe('New status: open, resolved, wontfix, invalid, spite, duplicate'),
104
+ subtype: z.string().optional().describe('Task subtype (e.g. "default", "incident")'),
73
105
  addProjectPHIDs: z.array(z.string()).optional().describe('Project PHIDs to add'),
74
106
  removeProjectPHIDs: z.array(z.string()).optional().describe('Project PHIDs to remove'),
75
107
  addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
76
108
  removeSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to remove'),
109
+ addParentPHIDs: z.array(z.string()).optional().describe('Parent task PHIDs to add'),
110
+ removeParentPHIDs: z.array(z.string()).optional().describe('Parent task PHIDs to remove'),
111
+ addSubtaskPHIDs: z.array(z.string()).optional().describe('Subtask PHIDs to add'),
112
+ removeSubtaskPHIDs: z.array(z.string()).optional().describe('Subtask PHIDs to remove'),
77
113
  columnPHID: z.string().optional().describe('Move to workboard column'),
114
+ comment: z.string().optional().describe('Add a comment alongside the edit (supports Remarkup)'),
115
+ customFields: jsonCoerce(z.record(z.string(), z.unknown())).optional().describe('Custom field transactions. Keys are transaction types (e.g. "custom.my-field"), values are the field values. Check your Phabricator Conduit console (conduit/method/maniphest.edit/) for available fields.'),
78
116
  }, async (params) => {
79
117
  const transactions = [];
80
118
  if (params.title !== undefined) {
@@ -92,6 +130,9 @@ export function registerManiphestTools(server, client) {
92
130
  if (params.status !== undefined) {
93
131
  transactions.push({ type: 'status', value: params.status });
94
132
  }
133
+ if (params.subtype !== undefined) {
134
+ transactions.push({ type: 'subtype', value: params.subtype });
135
+ }
95
136
  if (params.addProjectPHIDs !== undefined) {
96
137
  transactions.push({ type: 'projects.add', value: params.addProjectPHIDs });
97
138
  }
@@ -104,9 +145,29 @@ export function registerManiphestTools(server, client) {
104
145
  if (params.removeSubscriberPHIDs !== undefined) {
105
146
  transactions.push({ type: 'subscribers.remove', value: params.removeSubscriberPHIDs });
106
147
  }
148
+ if (params.addParentPHIDs !== undefined) {
149
+ transactions.push({ type: 'parents.add', value: params.addParentPHIDs });
150
+ }
151
+ if (params.removeParentPHIDs !== undefined) {
152
+ transactions.push({ type: 'parents.remove', value: params.removeParentPHIDs });
153
+ }
154
+ if (params.addSubtaskPHIDs !== undefined) {
155
+ transactions.push({ type: 'subtasks.add', value: params.addSubtaskPHIDs });
156
+ }
157
+ if (params.removeSubtaskPHIDs !== undefined) {
158
+ transactions.push({ type: 'subtasks.remove', value: params.removeSubtaskPHIDs });
159
+ }
107
160
  if (params.columnPHID !== undefined) {
108
161
  transactions.push({ type: 'column', value: [params.columnPHID] });
109
162
  }
163
+ if (params.comment !== undefined) {
164
+ transactions.push({ type: 'comment', value: params.comment });
165
+ }
166
+ if (params.customFields !== undefined) {
167
+ for (const [key, value] of Object.entries(params.customFields)) {
168
+ transactions.push({ type: key, value });
169
+ }
170
+ }
110
171
  if (transactions.length === 0) {
111
172
  return { content: [{ type: 'text', text: 'No changes specified' }] };
112
173
  }
@@ -127,4 +188,14 @@ export function registerManiphestTools(server, client) {
127
188
  });
128
189
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
129
190
  });
191
+ // Search available task statuses
192
+ server.tool('phabricator_task_status_search', 'List all available task statuses configured on this Phabricator instance', {}, async () => {
193
+ const result = await client.call('maniphest.status.search');
194
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
195
+ });
196
+ // Search available task priorities
197
+ server.tool('phabricator_task_priority_search', 'List all available task priorities configured on this Phabricator instance', {}, async () => {
198
+ const result = await client.call('maniphest.priority.search');
199
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
200
+ });
130
201
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ConduitClient } from '../client/conduit.js';
3
+ export declare function registerOwnersTools(server: McpServer, client: ConduitClient): void;
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
3
+ export function registerOwnersTools(server, client) {
4
+ // Search code ownership packages
5
+ server.tool('phabricator_owners_search', 'Search Owners packages (code ownership). Find who owns a code path or list ownership packages.', {
6
+ queryKey: z.string().optional().describe('Built-in query: "all", "active"'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Package IDs'),
9
+ phids: z.array(z.string()).optional().describe('Package PHIDs'),
10
+ owners: z.array(z.string()).optional().describe('Owner user or project PHIDs'),
11
+ repositoryPHIDs: z.array(z.string()).optional().describe('Repository PHIDs'),
12
+ paths: z.array(z.array(z.string())).optional().describe('Code paths as [repositoryPHID, path] pairs (e.g. [["PHID-REPO-xxx", "/src/foo.ts"]])'),
13
+ statuses: z.array(z.string()).optional().describe('Package statuses'),
14
+ query: z.string().optional().describe('Full-text search query'),
15
+ })).optional().describe('Search constraints'),
16
+ attachments: jsonCoerce(z.object({
17
+ owners: z.boolean().optional().describe('Include owner details'),
18
+ paths: z.boolean().optional().describe('Include owned paths'),
19
+ })).optional().describe('Data attachments'),
20
+ order: z.string().optional().describe('Result order'),
21
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
22
+ after: z.string().optional().describe('Pagination cursor'),
23
+ }, async (params) => {
24
+ const result = await client.call('owners.search', params);
25
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
26
+ });
27
+ }
@@ -15,7 +15,7 @@ export function registerPasteTools(server, client) {
15
15
  content: z.boolean().optional().describe('Include paste content'),
16
16
  })).optional().describe('Data attachments'),
17
17
  order: z.string().optional().describe('Result order'),
18
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
18
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
19
19
  after: z.string().optional().describe('Pagination cursor'),
20
20
  }, async (params) => {
21
21
  const result = await client.call('paste.search', params);
@@ -27,6 +27,7 @@ export function registerPasteTools(server, client) {
27
27
  content: z.string().describe('Paste content'),
28
28
  language: z.string().optional().describe('Syntax highlighting language'),
29
29
  status: z.string().optional().describe('Status: active or archived'),
30
+ addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
30
31
  }, async (params) => {
31
32
  const transactions = [
32
33
  { type: 'text', value: params.content },
@@ -40,7 +41,48 @@ export function registerPasteTools(server, client) {
40
41
  if (params.status !== undefined) {
41
42
  transactions.push({ type: 'status', value: params.status });
42
43
  }
44
+ if (params.addSubscriberPHIDs !== undefined) {
45
+ transactions.push({ type: 'subscribers.add', value: params.addSubscriberPHIDs });
46
+ }
43
47
  const result = await client.call('paste.edit', { transactions });
44
48
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
45
49
  });
50
+ // Edit paste
51
+ server.tool('phabricator_paste_edit', 'Edit an existing Phabricator paste', {
52
+ objectIdentifier: z.string().describe('Paste PHID or ID (e.g., "P123")'),
53
+ title: z.string().optional().describe('New title'),
54
+ content: z.string().optional().describe('New content'),
55
+ language: z.string().optional().describe('Syntax highlighting language'),
56
+ status: z.string().optional().describe('Status: active or archived'),
57
+ addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
58
+ removeSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to remove'),
59
+ }, async (params) => {
60
+ const transactions = [];
61
+ if (params.title !== undefined) {
62
+ transactions.push({ type: 'title', value: params.title });
63
+ }
64
+ if (params.content !== undefined) {
65
+ transactions.push({ type: 'text', value: params.content });
66
+ }
67
+ if (params.language !== undefined) {
68
+ transactions.push({ type: 'language', value: params.language });
69
+ }
70
+ if (params.status !== undefined) {
71
+ transactions.push({ type: 'status', value: params.status });
72
+ }
73
+ if (params.addSubscriberPHIDs !== undefined) {
74
+ transactions.push({ type: 'subscribers.add', value: params.addSubscriberPHIDs });
75
+ }
76
+ if (params.removeSubscriberPHIDs !== undefined) {
77
+ transactions.push({ type: 'subscribers.remove', value: params.removeSubscriberPHIDs });
78
+ }
79
+ if (transactions.length === 0) {
80
+ return { content: [{ type: 'text', text: 'No changes specified' }] };
81
+ }
82
+ const result = await client.call('paste.edit', {
83
+ objectIdentifier: params.objectIdentifier,
84
+ transactions,
85
+ });
86
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
87
+ });
46
88
  }
@@ -9,6 +9,9 @@ export function registerPhameTools(server, client) {
9
9
  phids: z.array(z.string()).optional().describe('Blog PHIDs'),
10
10
  query: z.string().optional().describe('Full-text search query'),
11
11
  })).optional().describe('Search constraints'),
12
+ attachments: jsonCoerce(z.object({
13
+ subscribers: z.boolean().optional().describe('Include subscriber details'),
14
+ })).optional().describe('Data attachments'),
12
15
  order: z.string().optional().describe('Result order'),
13
16
  limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
14
17
  after: z.string().optional().describe('Cursor for pagination'),
@@ -23,9 +26,12 @@ export function registerPhameTools(server, client) {
23
26
  ids: z.array(z.coerce.number()).optional().describe('Post IDs'),
24
27
  phids: z.array(z.string()).optional().describe('Post PHIDs'),
25
28
  blogPHIDs: z.array(z.string()).optional().describe('Filter by blog PHIDs'),
26
- visibility: z.array(z.string()).optional().describe('Visibility: "published", "draft", "archived"'),
29
+ visibility: z.array(z.coerce.number()).optional().describe('Visibility: 1 (published), 0 (draft), 2 (archived). Note: use these numeric codes in search; use string names like "published" in create/edit.'),
27
30
  query: z.string().optional().describe('Full-text search query'),
28
31
  })).optional().describe('Search constraints'),
32
+ attachments: jsonCoerce(z.object({
33
+ content: z.boolean().optional().describe('Include blog post body content'),
34
+ })).optional().describe('Data attachments'),
29
35
  order: z.string().optional().describe('Result order'),
30
36
  limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
31
37
  after: z.string().optional().describe('Cursor for pagination'),
@@ -40,6 +46,7 @@ export function registerPhameTools(server, client) {
40
46
  blogPHID: z.string().describe('PHID of the blog to post to'),
41
47
  subtitle: z.string().optional().describe('Post subtitle'),
42
48
  visibility: z.string().optional().describe('Visibility: "published", "draft", "archived" (default: draft)'),
49
+ addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
43
50
  }, async (params) => {
44
51
  const transactions = [
45
52
  { type: 'title', value: params.title },
@@ -52,6 +59,9 @@ export function registerPhameTools(server, client) {
52
59
  if (params.visibility !== undefined) {
53
60
  transactions.push({ type: 'visibility', value: params.visibility });
54
61
  }
62
+ if (params.addSubscriberPHIDs !== undefined) {
63
+ transactions.push({ type: 'subscribers.add', value: params.addSubscriberPHIDs });
64
+ }
55
65
  const result = await client.call('phame.post.edit', { transactions });
56
66
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
57
67
  });
@@ -63,6 +73,8 @@ export function registerPhameTools(server, client) {
63
73
  body: z.string().optional().describe('New post body content (supports Remarkup)'),
64
74
  visibility: z.string().optional().describe('Visibility: "published", "draft", "archived"'),
65
75
  blogPHID: z.string().optional().describe('Move post to a different blog (PHID)'),
76
+ addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
77
+ removeSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to remove'),
66
78
  }, async (params) => {
67
79
  const transactions = [];
68
80
  if (params.title !== undefined) {
@@ -80,6 +92,12 @@ export function registerPhameTools(server, client) {
80
92
  if (params.blogPHID !== undefined) {
81
93
  transactions.push({ type: 'blog', value: params.blogPHID });
82
94
  }
95
+ if (params.addSubscriberPHIDs !== undefined) {
96
+ transactions.push({ type: 'subscribers.add', value: params.addSubscriberPHIDs });
97
+ }
98
+ if (params.removeSubscriberPHIDs !== undefined) {
99
+ transactions.push({ type: 'subscribers.remove', value: params.removeSubscriberPHIDs });
100
+ }
83
101
  if (transactions.length === 0) {
84
102
  return { content: [{ type: 'text', text: 'No changes specified' }] };
85
103
  }
@@ -16,17 +16,19 @@ export function registerPhrictionTools(server, client) {
16
16
  content: z.boolean().optional().describe('Include document content'),
17
17
  })).optional().describe('Data attachments'),
18
18
  order: z.string().optional().describe('Result order'),
19
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
19
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
20
20
  after: z.string().optional().describe('Pagination cursor'),
21
21
  }, async (params) => {
22
22
  const result = await client.call('phriction.document.search', params);
23
23
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
24
24
  });
25
25
  // Edit wiki document
26
- server.tool('phabricator_document_edit', 'Edit a Phriction wiki document', {
27
- slug: z.string().describe('Document path/slug (e.g., "projects/myproject/")'),
26
+ server.tool('phabricator_document_edit', 'Create or edit a Phriction wiki document. To create, provide a new slug with title and content.', {
27
+ objectIdentifier: z.string().describe('Document slug, PHID, or ID (e.g., "projects/myproject/")'),
28
28
  title: z.string().optional().describe('Document title'),
29
29
  content: z.string().optional().describe('Document content (Remarkup)'),
30
+ addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
31
+ removeSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to remove'),
30
32
  }, async (params) => {
31
33
  const transactions = [];
32
34
  if (params.title !== undefined) {
@@ -35,13 +37,30 @@ export function registerPhrictionTools(server, client) {
35
37
  if (params.content !== undefined) {
36
38
  transactions.push({ type: 'content', value: params.content });
37
39
  }
40
+ if (params.addSubscriberPHIDs !== undefined) {
41
+ transactions.push({ type: 'subscribers.add', value: params.addSubscriberPHIDs });
42
+ }
43
+ if (params.removeSubscriberPHIDs !== undefined) {
44
+ transactions.push({ type: 'subscribers.remove', value: params.removeSubscriberPHIDs });
45
+ }
38
46
  if (transactions.length === 0) {
39
47
  return { content: [{ type: 'text', text: 'No changes specified' }] };
40
48
  }
41
49
  const result = await client.call('phriction.document.edit', {
42
- objectIdentifier: params.slug,
50
+ objectIdentifier: params.objectIdentifier,
43
51
  transactions,
44
52
  });
45
53
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
46
54
  });
55
+ // Add comment to document
56
+ server.tool('phabricator_document_add_comment', 'Add a comment to a Phriction wiki document', {
57
+ objectIdentifier: z.string().describe('Document slug, PHID, or ID (e.g., "projects/myproject/")'),
58
+ comment: z.string().describe('Comment text (supports Remarkup)'),
59
+ }, async (params) => {
60
+ const result = await client.call('phriction.document.edit', {
61
+ objectIdentifier: params.objectIdentifier,
62
+ transactions: [{ type: 'comment', value: params.comment }],
63
+ });
64
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
65
+ });
47
66
  }
@@ -22,26 +22,33 @@ export function registerProjectTools(server, client) {
22
22
  ancestors: z.boolean().optional().describe('Include ancestors'),
23
23
  })).optional().describe('Data attachments'),
24
24
  order: z.string().optional().describe('Result order'),
25
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
25
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
26
26
  after: z.string().optional().describe('Pagination cursor'),
27
27
  }, async (params) => {
28
28
  const result = await client.call('project.search', params);
29
29
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
30
30
  });
31
31
  // Edit project
32
- server.tool('phabricator_project_edit', 'Edit a Phabricator project', {
33
- objectIdentifier: z.string().describe('Project PHID or ID'),
32
+ server.tool('phabricator_project_edit', 'Create or edit a Phabricator project. Omit objectIdentifier to create a new project (name is required for creation).', {
33
+ objectIdentifier: z.string().optional().describe('Project PHID or ID. Omit to create a new project.'),
34
34
  name: z.string().optional().describe('New name'),
35
35
  description: z.string().optional().describe('New description'),
36
36
  icon: z.string().optional().describe('New icon'),
37
37
  color: z.string().optional().describe('New color'),
38
38
  addMemberPHIDs: z.array(z.string()).optional().describe('Add members'),
39
39
  removeMemberPHIDs: z.array(z.string()).optional().describe('Remove members'),
40
+ addSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to add'),
41
+ removeSubscriberPHIDs: z.array(z.string()).optional().describe('Subscriber PHIDs to remove'),
42
+ slug: z.string().optional().describe('Project URL slug (replaces ALL existing slugs with this one)'),
43
+ comment: z.string().optional().describe('Add a comment alongside the edit (supports Remarkup)'),
40
44
  }, async (params) => {
41
45
  const transactions = [];
42
46
  if (params.name !== undefined) {
43
47
  transactions.push({ type: 'name', value: params.name });
44
48
  }
49
+ if (params.slug !== undefined) {
50
+ transactions.push({ type: 'slugs', value: [params.slug] });
51
+ }
45
52
  if (params.description !== undefined) {
46
53
  transactions.push({ type: 'description', value: params.description });
47
54
  }
@@ -57,24 +64,38 @@ export function registerProjectTools(server, client) {
57
64
  if (params.removeMemberPHIDs !== undefined) {
58
65
  transactions.push({ type: 'members.remove', value: params.removeMemberPHIDs });
59
66
  }
67
+ if (params.addSubscriberPHIDs !== undefined) {
68
+ transactions.push({ type: 'subscribers.add', value: params.addSubscriberPHIDs });
69
+ }
70
+ if (params.removeSubscriberPHIDs !== undefined) {
71
+ transactions.push({ type: 'subscribers.remove', value: params.removeSubscriberPHIDs });
72
+ }
73
+ if (params.comment !== undefined) {
74
+ transactions.push({ type: 'comment', value: params.comment });
75
+ }
60
76
  if (transactions.length === 0) {
61
77
  return { content: [{ type: 'text', text: 'No changes specified' }] };
62
78
  }
63
- const result = await client.call('project.edit', {
64
- objectIdentifier: params.objectIdentifier,
65
- transactions,
66
- });
79
+ const apiParams = { transactions };
80
+ if (params.objectIdentifier !== undefined) {
81
+ apiParams.objectIdentifier = params.objectIdentifier;
82
+ }
83
+ const result = await client.call('project.edit', apiParams);
67
84
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
68
85
  });
69
86
  // Search workboard columns
70
87
  server.tool('phabricator_column_search', 'Search project workboard columns', {
88
+ queryKey: z.string().optional().describe('Built-in query: "all"'),
71
89
  constraints: jsonCoerce(z.object({
72
90
  ids: z.array(z.coerce.number()).optional().describe('Column IDs'),
73
91
  phids: z.array(z.string()).optional().describe('Column PHIDs'),
74
92
  projects: z.array(z.string()).optional().describe('Project PHIDs'),
75
93
  })).optional().describe('Search constraints'),
94
+ attachments: jsonCoerce(z.object({
95
+ items: z.boolean().optional().describe('Include items (tasks) in each column'),
96
+ })).optional().describe('Data attachments'),
76
97
  order: z.string().optional().describe('Result order'),
77
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
98
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
78
99
  after: z.string().optional().describe('Pagination cursor'),
79
100
  }, async (params) => {
80
101
  const result = await client.call('project.column.search', params);
@@ -24,7 +24,7 @@ export function registerUserTools(server, client) {
24
24
  availability: z.boolean().optional().describe('Include availability info'),
25
25
  })).optional().describe('Data attachments'),
26
26
  order: z.string().optional().describe('Result order'),
27
- limit: z.coerce.number().max(100).optional().describe('Maximum results'),
27
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
28
28
  after: z.string().optional().describe('Pagination cursor'),
29
29
  }, async (params) => {
30
30
  const result = await client.call('user.search', params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freelancercom/phabricator-mcp",
3
- "version": "1.0.5",
3
+ "version": "2.0.0",
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",