@freelancercom/phabricator-mcp 1.0.2 → 1.0.3

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.
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Coerce a JSON string into an object before Zod validation.
4
+ *
5
+ * Some MCP clients may send object parameters as JSON strings when they
6
+ * haven't loaded the tool schema. This wrapper gracefully handles that
7
+ * by parsing the string before validation.
8
+ */
9
+ export declare function jsonCoerce<T extends z.ZodTypeAny>(schema: T): z.ZodEffects<T, T["_output"], unknown>;
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Coerce a JSON string into an object before Zod validation.
4
+ *
5
+ * Some MCP clients may send object parameters as JSON strings when they
6
+ * haven't loaded the tool schema. This wrapper gracefully handles that
7
+ * by parsing the string before validation.
8
+ */
9
+ export function jsonCoerce(schema) {
10
+ return z.preprocess((val) => {
11
+ if (typeof val === 'string') {
12
+ try {
13
+ return JSON.parse(val);
14
+ }
15
+ catch {
16
+ return val;
17
+ }
18
+ }
19
+ return val;
20
+ }, schema);
21
+ }
@@ -1,23 +1,24 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerDifferentialTools(server, client) {
3
4
  // Search revisions
4
5
  server.tool('phabricator_revision_search', 'Search Differential revisions (code reviews)', {
5
6
  queryKey: z.string().optional().describe('Built-in query: "all", "active", "authored", "waiting"'),
6
- constraints: z.object({
7
- ids: z.array(z.number()).optional().describe('Revision IDs'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Revision IDs'),
8
9
  phids: z.array(z.string()).optional().describe('Revision PHIDs'),
9
10
  authorPHIDs: z.array(z.string()).optional().describe('Author PHIDs'),
10
11
  reviewerPHIDs: z.array(z.string()).optional().describe('Reviewer PHIDs'),
11
12
  repositoryPHIDs: z.array(z.string()).optional().describe('Repository PHIDs'),
12
13
  statuses: z.array(z.string()).optional().describe('Statuses: needs-review, needs-revision, accepted, published, abandoned, changes-planned'),
13
- }).optional().describe('Search constraints'),
14
- attachments: z.object({
14
+ })).optional().describe('Search constraints'),
15
+ attachments: jsonCoerce(z.object({
15
16
  reviewers: z.boolean().optional().describe('Include reviewers'),
16
17
  subscribers: z.boolean().optional().describe('Include subscribers'),
17
18
  projects: z.boolean().optional().describe('Include projects'),
18
- }).optional().describe('Data attachments'),
19
+ })).optional().describe('Data attachments'),
19
20
  order: z.string().optional().describe('Result order'),
20
- limit: z.number().max(100).optional().describe('Maximum results'),
21
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
21
22
  after: z.string().optional().describe('Pagination cursor'),
22
23
  }, async (params) => {
23
24
  const result = await client.call('differential.revision.search', params);
@@ -71,7 +72,7 @@ export function registerDifferentialTools(server, client) {
71
72
  });
72
73
  // Get raw diff content
73
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.', {
74
- diffID: z.number().describe('The diff ID (numeric, e.g., 1392561). Use phabricator_diff_search to find this from a revision.'),
75
+ diffID: z.coerce.number().describe('The diff ID (numeric, e.g., 1392561). Use phabricator_diff_search to find this from a revision.'),
75
76
  }, async (params) => {
76
77
  const result = await client.call('differential.getrawdiff', {
77
78
  diffID: params.diffID,
@@ -80,15 +81,15 @@ export function registerDifferentialTools(server, client) {
80
81
  });
81
82
  // Search diffs
82
83
  server.tool('phabricator_diff_search', 'Search Differential diffs', {
83
- constraints: z.object({
84
- ids: z.array(z.number()).optional().describe('Diff IDs'),
84
+ constraints: jsonCoerce(z.object({
85
+ ids: z.array(z.coerce.number()).optional().describe('Diff IDs'),
85
86
  phids: z.array(z.string()).optional().describe('Diff PHIDs'),
86
87
  revisionPHIDs: z.array(z.string()).optional().describe('Revision PHIDs'),
87
- }).optional().describe('Search constraints'),
88
- attachments: z.object({
88
+ })).optional().describe('Search constraints'),
89
+ attachments: jsonCoerce(z.object({
89
90
  commits: z.boolean().optional().describe('Include commit info'),
90
- }).optional().describe('Data attachments'),
91
- limit: z.number().max(100).optional().describe('Maximum results'),
91
+ })).optional().describe('Data attachments'),
92
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
92
93
  after: z.string().optional().describe('Pagination cursor'),
93
94
  }, async (params) => {
94
95
  const result = await client.call('differential.diff.search', params);
@@ -1,24 +1,25 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerDiffusionTools(server, client) {
3
4
  // Search repositories
4
5
  server.tool('phabricator_repository_search', 'Search Diffusion repositories', {
5
6
  queryKey: z.string().optional().describe('Built-in query: "all", "active"'),
6
- constraints: z.object({
7
- ids: z.array(z.number()).optional().describe('Repository IDs'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Repository IDs'),
8
9
  phids: z.array(z.string()).optional().describe('Repository PHIDs'),
9
10
  callsigns: z.array(z.string()).optional().describe('Repository callsigns'),
10
11
  shortNames: z.array(z.string()).optional().describe('Repository short names'),
11
12
  types: z.array(z.string()).optional().describe('VCS types: git, hg, svn'),
12
13
  uris: z.array(z.string()).optional().describe('Repository URIs'),
13
14
  query: z.string().optional().describe('Full-text search query'),
14
- }).optional().describe('Search constraints'),
15
- attachments: z.object({
15
+ })).optional().describe('Search constraints'),
16
+ attachments: jsonCoerce(z.object({
16
17
  uris: z.boolean().optional().describe('Include repository URIs'),
17
18
  metrics: z.boolean().optional().describe('Include metrics'),
18
19
  projects: z.boolean().optional().describe('Include projects'),
19
- }).optional().describe('Data attachments'),
20
+ })).optional().describe('Data attachments'),
20
21
  order: z.string().optional().describe('Result order'),
21
- limit: z.number().max(100).optional().describe('Maximum results'),
22
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
22
23
  after: z.string().optional().describe('Pagination cursor'),
23
24
  }, async (params) => {
24
25
  const result = await client.call('diffusion.repository.search', params);
@@ -26,20 +27,20 @@ export function registerDiffusionTools(server, client) {
26
27
  });
27
28
  // Search commits
28
29
  server.tool('phabricator_commit_search', 'Search Diffusion commits', {
29
- constraints: z.object({
30
- ids: z.array(z.number()).optional().describe('Commit IDs'),
30
+ constraints: jsonCoerce(z.object({
31
+ ids: z.array(z.coerce.number()).optional().describe('Commit IDs'),
31
32
  phids: z.array(z.string()).optional().describe('Commit PHIDs'),
32
33
  repositoryPHIDs: z.array(z.string()).optional().describe('Repository PHIDs'),
33
34
  identifiers: z.array(z.string()).optional().describe('Commit identifiers (hashes)'),
34
35
  authorPHIDs: z.array(z.string()).optional().describe('Author PHIDs'),
35
36
  query: z.string().optional().describe('Full-text search query'),
36
- }).optional().describe('Search constraints'),
37
- attachments: z.object({
37
+ })).optional().describe('Search constraints'),
38
+ attachments: jsonCoerce(z.object({
38
39
  projects: z.boolean().optional().describe('Include projects'),
39
40
  subscribers: z.boolean().optional().describe('Include subscribers'),
40
- }).optional().describe('Data attachments'),
41
+ })).optional().describe('Data attachments'),
41
42
  order: z.string().optional().describe('Result order'),
42
- limit: z.number().max(100).optional().describe('Maximum results'),
43
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
43
44
  after: z.string().optional().describe('Pagination cursor'),
44
45
  }, async (params) => {
45
46
  const result = await client.call('diffusion.commit.search', params);
@@ -1,27 +1,28 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerManiphestTools(server, client) {
3
4
  // Search tasks
4
5
  server.tool('phabricator_task_search', 'Search Maniphest tasks with optional filters', {
5
6
  queryKey: z.string().optional().describe('Built-in query: "all", "open", "authored", "assigned"'),
6
- constraints: z.object({
7
- ids: z.array(z.number()).optional().describe('Task IDs to search for'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Task IDs to search for'),
8
9
  phids: z.array(z.string()).optional().describe('Task PHIDs to search for'),
9
10
  assigned: z.array(z.string()).optional().describe('Assigned user PHIDs'),
10
11
  authorPHIDs: z.array(z.string()).optional().describe('Author PHIDs'),
11
12
  statuses: z.array(z.string()).optional().describe('Task statuses: open, resolved, wontfix, invalid, spite, duplicate'),
12
- priorities: z.array(z.number()).optional().describe('Priority levels'),
13
+ priorities: z.array(z.coerce.number()).optional().describe('Priority levels'),
13
14
  subtypes: z.array(z.string()).optional().describe('Task subtypes'),
14
15
  columnPHIDs: z.array(z.string()).optional().describe('Workboard column PHIDs'),
15
16
  projectPHIDs: z.array(z.string()).optional().describe('Project PHIDs (tasks tagged with these projects)'),
16
17
  query: z.string().optional().describe('Full-text search query'),
17
- }).optional().describe('Search constraints'),
18
- attachments: z.object({
18
+ })).optional().describe('Search constraints'),
19
+ attachments: jsonCoerce(z.object({
19
20
  columns: z.boolean().optional().describe('Include workboard column info'),
20
21
  projects: z.boolean().optional().describe('Include project info'),
21
22
  subscribers: z.boolean().optional().describe('Include subscriber info'),
22
- }).optional().describe('Data attachments to include'),
23
+ })).optional().describe('Data attachments to include'),
23
24
  order: z.string().optional().describe('Result order: "priority", "updated", "newest", "oldest"'),
24
- limit: z.number().max(100).optional().describe('Maximum results (max 100)'),
25
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
25
26
  after: z.string().optional().describe('Cursor for pagination'),
26
27
  }, async (params) => {
27
28
  const result = await client.call('maniphest.search', params);
@@ -1,20 +1,21 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerPasteTools(server, client) {
3
4
  // Search pastes
4
5
  server.tool('phabricator_paste_search', 'Search Phabricator pastes', {
5
6
  queryKey: z.string().optional().describe('Built-in query: "all", "authored"'),
6
- constraints: z.object({
7
- ids: z.array(z.number()).optional().describe('Paste IDs'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Paste IDs'),
8
9
  phids: z.array(z.string()).optional().describe('Paste PHIDs'),
9
10
  authorPHIDs: z.array(z.string()).optional().describe('Author PHIDs'),
10
11
  languages: z.array(z.string()).optional().describe('Languages'),
11
12
  query: z.string().optional().describe('Full-text search query'),
12
- }).optional().describe('Search constraints'),
13
- attachments: z.object({
13
+ })).optional().describe('Search constraints'),
14
+ attachments: jsonCoerce(z.object({
14
15
  content: z.boolean().optional().describe('Include paste content'),
15
- }).optional().describe('Data attachments'),
16
+ })).optional().describe('Data attachments'),
16
17
  order: z.string().optional().describe('Result order'),
17
- limit: z.number().max(100).optional().describe('Maximum results'),
18
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
18
19
  after: z.string().optional().describe('Pagination cursor'),
19
20
  }, async (params) => {
20
21
  const result = await client.call('paste.search', params);
@@ -1,15 +1,16 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerPhameTools(server, client) {
3
4
  // Search blogs
4
5
  server.tool('phabricator_blog_search', 'Search Phame blogs', {
5
6
  queryKey: z.string().optional().describe('Built-in query: "all", "active"'),
6
- constraints: z.object({
7
- ids: z.array(z.number()).optional().describe('Blog IDs'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Blog IDs'),
8
9
  phids: z.array(z.string()).optional().describe('Blog PHIDs'),
9
10
  query: z.string().optional().describe('Full-text search query'),
10
- }).optional().describe('Search constraints'),
11
+ })).optional().describe('Search constraints'),
11
12
  order: z.string().optional().describe('Result order'),
12
- limit: z.number().max(100).optional().describe('Maximum results (max 100)'),
13
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
13
14
  after: z.string().optional().describe('Cursor for pagination'),
14
15
  }, async (params) => {
15
16
  const result = await client.call('phame.blog.search', params);
@@ -18,15 +19,15 @@ export function registerPhameTools(server, client) {
18
19
  // Search blog posts
19
20
  server.tool('phabricator_blog_post_search', 'Search Phame blog posts', {
20
21
  queryKey: z.string().optional().describe('Built-in query: "all", "live"'),
21
- constraints: z.object({
22
- ids: z.array(z.number()).optional().describe('Post IDs'),
22
+ constraints: jsonCoerce(z.object({
23
+ ids: z.array(z.coerce.number()).optional().describe('Post IDs'),
23
24
  phids: z.array(z.string()).optional().describe('Post PHIDs'),
24
25
  blogPHIDs: z.array(z.string()).optional().describe('Filter by blog PHIDs'),
25
26
  visibility: z.array(z.string()).optional().describe('Visibility: "published", "draft", "archived"'),
26
27
  query: z.string().optional().describe('Full-text search query'),
27
- }).optional().describe('Search constraints'),
28
+ })).optional().describe('Search constraints'),
28
29
  order: z.string().optional().describe('Result order'),
29
- limit: z.number().max(100).optional().describe('Maximum results (max 100)'),
30
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
30
31
  after: z.string().optional().describe('Cursor for pagination'),
31
32
  }, async (params) => {
32
33
  const result = await client.call('phame.post.search', params);
@@ -1,21 +1,22 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerPhrictionTools(server, client) {
3
4
  // Search wiki documents
4
5
  server.tool('phabricator_document_search', 'Search Phriction wiki documents', {
5
6
  queryKey: z.string().optional().describe('Built-in query: "all", "active"'),
6
- constraints: z.object({
7
- ids: z.array(z.number()).optional().describe('Document IDs'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Document IDs'),
8
9
  phids: z.array(z.string()).optional().describe('Document PHIDs'),
9
10
  paths: z.array(z.string()).optional().describe('Document paths'),
10
11
  ancestorPaths: z.array(z.string()).optional().describe('Ancestor paths to search under'),
11
12
  statuses: z.array(z.string()).optional().describe('Document statuses'),
12
13
  query: z.string().optional().describe('Full-text search query'),
13
- }).optional().describe('Search constraints'),
14
- attachments: z.object({
14
+ })).optional().describe('Search constraints'),
15
+ attachments: jsonCoerce(z.object({
15
16
  content: z.boolean().optional().describe('Include document content'),
16
- }).optional().describe('Data attachments'),
17
+ })).optional().describe('Data attachments'),
17
18
  order: z.string().optional().describe('Result order'),
18
- limit: z.number().max(100).optional().describe('Maximum results'),
19
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
19
20
  after: z.string().optional().describe('Pagination cursor'),
20
21
  }, async (params) => {
21
22
  const result = await client.call('phriction.document.search', params);
@@ -1,10 +1,11 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerProjectTools(server, client) {
3
4
  // Search projects
4
5
  server.tool('phabricator_project_search', 'Search Phabricator projects', {
5
6
  queryKey: z.string().optional().describe('Built-in query: "all", "active", "joined"'),
6
- constraints: z.object({
7
- ids: z.array(z.number()).optional().describe('Project IDs'),
7
+ constraints: jsonCoerce(z.object({
8
+ ids: z.array(z.coerce.number()).optional().describe('Project IDs'),
8
9
  phids: z.array(z.string()).optional().describe('Project PHIDs'),
9
10
  slugs: z.array(z.string()).optional().describe('Project slugs'),
10
11
  name: z.string().optional().describe('Exact name match'),
@@ -14,14 +15,14 @@ export function registerProjectTools(server, client) {
14
15
  isMilestone: z.boolean().optional().describe('Filter milestones'),
15
16
  isRoot: z.boolean().optional().describe('Filter root projects'),
16
17
  query: z.string().optional().describe('Full-text search query'),
17
- }).optional().describe('Search constraints'),
18
- attachments: z.object({
18
+ })).optional().describe('Search constraints'),
19
+ attachments: jsonCoerce(z.object({
19
20
  members: z.boolean().optional().describe('Include members'),
20
21
  watchers: z.boolean().optional().describe('Include watchers'),
21
22
  ancestors: z.boolean().optional().describe('Include ancestors'),
22
- }).optional().describe('Data attachments'),
23
+ })).optional().describe('Data attachments'),
23
24
  order: z.string().optional().describe('Result order'),
24
- limit: z.number().max(100).optional().describe('Maximum results'),
25
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
25
26
  after: z.string().optional().describe('Pagination cursor'),
26
27
  }, async (params) => {
27
28
  const result = await client.call('project.search', params);
@@ -67,13 +68,13 @@ export function registerProjectTools(server, client) {
67
68
  });
68
69
  // Search workboard columns
69
70
  server.tool('phabricator_column_search', 'Search project workboard columns', {
70
- constraints: z.object({
71
- ids: z.array(z.number()).optional().describe('Column IDs'),
71
+ constraints: jsonCoerce(z.object({
72
+ ids: z.array(z.coerce.number()).optional().describe('Column IDs'),
72
73
  phids: z.array(z.string()).optional().describe('Column PHIDs'),
73
74
  projects: z.array(z.string()).optional().describe('Project PHIDs'),
74
- }).optional().describe('Search constraints'),
75
+ })).optional().describe('Search constraints'),
75
76
  order: z.string().optional().describe('Result order'),
76
- limit: z.number().max(100).optional().describe('Maximum results'),
77
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
77
78
  after: z.string().optional().describe('Pagination cursor'),
78
79
  }, async (params) => {
79
80
  const result = await client.call('project.column.search', params);
@@ -1,12 +1,13 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerTransactionTools(server, client) {
3
4
  server.tool('phabricator_transaction_search', 'Search transactions (comments, status changes, etc.) on any Phabricator object (e.g., "D123", "T456")', {
4
5
  objectIdentifier: z.string().describe('Object ID (e.g., "D123", "T456") or PHID'),
5
- constraints: z.object({
6
+ constraints: jsonCoerce(z.object({
6
7
  phids: z.array(z.string()).optional().describe('Transaction PHIDs'),
7
8
  authorPHIDs: z.array(z.string()).optional().describe('Author PHIDs'),
8
- }).optional().describe('Search constraints'),
9
- limit: z.number().max(100).optional().describe('Maximum results (max 100)'),
9
+ })).optional().describe('Search constraints'),
10
+ limit: z.coerce.number().max(100).optional().describe('Maximum results (max 100)'),
10
11
  after: z.string().optional().describe('Pagination cursor'),
11
12
  }, async (params) => {
12
13
  const { objectIdentifier, ...searchParams } = params;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { jsonCoerce } from './coerce.js';
2
3
  export function registerUserTools(server, client) {
3
4
  // Get current user
4
5
  server.tool('phabricator_user_whoami', 'Get information about the current authenticated user', {}, async () => {
@@ -8,8 +9,8 @@ export function registerUserTools(server, client) {
8
9
  // Search users
9
10
  server.tool('phabricator_user_search', 'Search Phabricator users', {
10
11
  queryKey: z.string().optional().describe('Built-in query: "all", "active", "approval"'),
11
- constraints: z.object({
12
- ids: z.array(z.number()).optional().describe('User IDs'),
12
+ constraints: jsonCoerce(z.object({
13
+ ids: z.array(z.coerce.number()).optional().describe('User IDs'),
13
14
  phids: z.array(z.string()).optional().describe('User PHIDs'),
14
15
  usernames: z.array(z.string()).optional().describe('Usernames'),
15
16
  nameLike: z.string().optional().describe('Name prefix search'),
@@ -18,12 +19,12 @@ export function registerUserTools(server, client) {
18
19
  isBot: z.boolean().optional().describe('Filter by bot status'),
19
20
  isMailingList: z.boolean().optional().describe('Filter by mailing list status'),
20
21
  query: z.string().optional().describe('Full-text search query'),
21
- }).optional().describe('Search constraints'),
22
- attachments: z.object({
22
+ })).optional().describe('Search constraints'),
23
+ attachments: jsonCoerce(z.object({
23
24
  availability: z.boolean().optional().describe('Include availability info'),
24
- }).optional().describe('Data attachments'),
25
+ })).optional().describe('Data attachments'),
25
26
  order: z.string().optional().describe('Result order'),
26
- limit: z.number().max(100).optional().describe('Maximum results'),
27
+ limit: z.coerce.number().max(100).optional().describe('Maximum results'),
27
28
  after: z.string().optional().describe('Pagination cursor'),
28
29
  }, async (params) => {
29
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.2",
3
+ "version": "1.0.3",
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",
@@ -15,8 +15,7 @@
15
15
  "prepare": "npm run build",
16
16
  "start": "node dist/index.js",
17
17
  "dev": "tsx --watch src/index.ts",
18
- "typecheck": "tsc --noEmit",
19
- "test": "node --test --import tsx 'src/**/*.test.ts'"
18
+ "typecheck": "tsc --noEmit"
20
19
  },
21
20
  "keywords": [
22
21
  "mcp",