@freelancercom/phabricator-mcp 1.0.0 → 1.0.2
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 +33 -7
- package/package.json +1 -1
- package/dist/client/conduit.test.d.ts +0 -1
- package/dist/client/conduit.test.js +0 -97
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -54
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
|
|
|
11
11
|
### Claude Code (CLI)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
claude mcp add --scope user phabricator -- npx
|
|
14
|
+
claude mcp add --scope user phabricator -- npx @freelancercom/phabricator-mcp@latest
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Or with environment variables (if not using `~/.arcrc`):
|
|
@@ -20,7 +20,7 @@ Or with environment variables (if not using `~/.arcrc`):
|
|
|
20
20
|
claude mcp add --scope user phabricator \
|
|
21
21
|
-e PHABRICATOR_URL=https://phabricator.example.com \
|
|
22
22
|
-e PHABRICATOR_API_TOKEN=api-xxxxx \
|
|
23
|
-
-- npx
|
|
23
|
+
-- npx @freelancercom/phabricator-mcp@latest
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
The `--scope user` flag installs the server globally, making it available in all projects.
|
|
@@ -34,7 +34,7 @@ Add to your Codex config (`~/.codex/config.json`):
|
|
|
34
34
|
"mcpServers": {
|
|
35
35
|
"phabricator": {
|
|
36
36
|
"command": "npx",
|
|
37
|
-
"args": ["
|
|
37
|
+
"args": ["@freelancercom/phabricator-mcp@latest"],
|
|
38
38
|
"env": {
|
|
39
39
|
"PHABRICATOR_URL": "https://phabricator.example.com",
|
|
40
40
|
"PHABRICATOR_API_TOKEN": "api-xxxxxxxxxxxxx"
|
|
@@ -54,7 +54,7 @@ Add to your opencode config (`~/.config/opencode/config.json`):
|
|
|
54
54
|
"servers": {
|
|
55
55
|
"phabricator": {
|
|
56
56
|
"command": "npx",
|
|
57
|
-
"args": ["
|
|
57
|
+
"args": ["@freelancercom/phabricator-mcp@latest"],
|
|
58
58
|
"env": {
|
|
59
59
|
"PHABRICATOR_URL": "https://phabricator.example.com",
|
|
60
60
|
"PHABRICATOR_API_TOKEN": "api-xxxxxxxxxxxxx"
|
|
@@ -74,7 +74,7 @@ Add to your VS Code `settings.json`:
|
|
|
74
74
|
"claude.mcpServers": {
|
|
75
75
|
"phabricator": {
|
|
76
76
|
"command": "npx",
|
|
77
|
-
"args": ["
|
|
77
|
+
"args": ["@freelancercom/phabricator-mcp@latest"],
|
|
78
78
|
"env": {
|
|
79
79
|
"PHABRICATOR_URL": "https://phabricator.example.com",
|
|
80
80
|
"PHABRICATOR_API_TOKEN": "api-xxxxxxxxxxxxx"
|
|
@@ -93,7 +93,7 @@ Add to your Cursor MCP config (`~/.cursor/mcp.json`):
|
|
|
93
93
|
"mcpServers": {
|
|
94
94
|
"phabricator": {
|
|
95
95
|
"command": "npx",
|
|
96
|
-
"args": ["
|
|
96
|
+
"args": ["@freelancercom/phabricator-mcp@latest"],
|
|
97
97
|
"env": {
|
|
98
98
|
"PHABRICATOR_URL": "https://phabricator.example.com",
|
|
99
99
|
"PHABRICATOR_API_TOKEN": "api-xxxxxxxxxxxxx"
|
|
@@ -112,7 +112,7 @@ Add to your VS Code `settings.json`:
|
|
|
112
112
|
"github.copilot.chat.mcp.servers": {
|
|
113
113
|
"phabricator": {
|
|
114
114
|
"command": "npx",
|
|
115
|
-
"args": ["
|
|
115
|
+
"args": ["@freelancercom/phabricator-mcp@latest"],
|
|
116
116
|
"env": {
|
|
117
117
|
"PHABRICATOR_URL": "https://phabricator.example.com",
|
|
118
118
|
"PHABRICATOR_API_TOKEN": "api-xxxxxxxxxxxxx"
|
|
@@ -122,6 +122,32 @@ Add to your VS Code `settings.json`:
|
|
|
122
122
|
}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
## Upgrading
|
|
126
|
+
|
|
127
|
+
The default install uses `@freelancercom/phabricator-mcp@latest`, which tells npx to check for updates on each run. No action needed.
|
|
128
|
+
|
|
129
|
+
If you pinned a specific version (e.g. `@freelancercom/phabricator-mcp@1.0.0`) or omitted the version suffix, npx caches the package and won't pick up new versions. To upgrade:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx clear-npx-cache
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then restart your MCP client.
|
|
136
|
+
|
|
137
|
+
### Migrating from `github:freelancer/phabricator-mcp`
|
|
138
|
+
|
|
139
|
+
If you previously installed using the GitHub URL, update your config to use the npm package instead:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Remove old server
|
|
143
|
+
claude mcp remove phabricator -s user
|
|
144
|
+
|
|
145
|
+
# Add new one
|
|
146
|
+
claude mcp add --scope user phabricator -- npx @freelancercom/phabricator-mcp@latest
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For JSON configs, replace `["github:freelancer/phabricator-mcp"]` with `["@freelancercom/phabricator-mcp@latest"]` in your args.
|
|
150
|
+
|
|
125
151
|
## Configuration
|
|
126
152
|
|
|
127
153
|
The server automatically reads configuration from `~/.arcrc` (created by [Arcanist](https://secure.phabricator.com/book/phabricator/article/arcanist/)). No additional configuration is needed if you've already set up `arc`.
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import { ConduitClient, ConduitError } from './conduit.js';
|
|
4
|
-
describe('ConduitClient', () => {
|
|
5
|
-
const mockConfig = {
|
|
6
|
-
phabricatorUrl: 'https://phabricator.example.com',
|
|
7
|
-
apiToken: 'api-test-token',
|
|
8
|
-
};
|
|
9
|
-
let originalFetch;
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
originalFetch = global.fetch;
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
global.fetch = originalFetch;
|
|
15
|
-
});
|
|
16
|
-
it('should construct correct API URL', async () => {
|
|
17
|
-
let capturedUrl;
|
|
18
|
-
global.fetch = mock.fn(async (url) => {
|
|
19
|
-
capturedUrl = url;
|
|
20
|
-
return new Response(JSON.stringify({ result: {}, error_code: null, error_info: null }));
|
|
21
|
-
});
|
|
22
|
-
const client = new ConduitClient(mockConfig);
|
|
23
|
-
await client.call('user.whoami');
|
|
24
|
-
assert.strictEqual(capturedUrl, 'https://phabricator.example.com/api/user.whoami');
|
|
25
|
-
});
|
|
26
|
-
it('should include API token in request body', async () => {
|
|
27
|
-
let capturedBody;
|
|
28
|
-
global.fetch = mock.fn(async (_url, init) => {
|
|
29
|
-
capturedBody = init?.body;
|
|
30
|
-
return new Response(JSON.stringify({ result: {}, error_code: null, error_info: null }));
|
|
31
|
-
});
|
|
32
|
-
const client = new ConduitClient(mockConfig);
|
|
33
|
-
await client.call('user.whoami');
|
|
34
|
-
assert.ok(capturedBody);
|
|
35
|
-
const params = new URLSearchParams(capturedBody);
|
|
36
|
-
const paramsJson = JSON.parse(params.get('params'));
|
|
37
|
-
assert.strictEqual(paramsJson.__conduit__.token, 'api-test-token');
|
|
38
|
-
});
|
|
39
|
-
it('should pass parameters to the API', async () => {
|
|
40
|
-
let capturedBody;
|
|
41
|
-
global.fetch = mock.fn(async (_url, init) => {
|
|
42
|
-
capturedBody = init?.body;
|
|
43
|
-
return new Response(JSON.stringify({ result: {}, error_code: null, error_info: null }));
|
|
44
|
-
});
|
|
45
|
-
const client = new ConduitClient(mockConfig);
|
|
46
|
-
await client.call('maniphest.search', { queryKey: 'assigned', limit: 10 });
|
|
47
|
-
const params = new URLSearchParams(capturedBody);
|
|
48
|
-
const paramsJson = JSON.parse(params.get('params'));
|
|
49
|
-
assert.strictEqual(paramsJson.queryKey, 'assigned');
|
|
50
|
-
assert.strictEqual(paramsJson.limit, 10);
|
|
51
|
-
});
|
|
52
|
-
it('should return result on success', async () => {
|
|
53
|
-
const expectedResult = { userName: 'testuser', realName: 'Test User' };
|
|
54
|
-
global.fetch = mock.fn(async () => {
|
|
55
|
-
return new Response(JSON.stringify({ result: expectedResult, error_code: null, error_info: null }));
|
|
56
|
-
});
|
|
57
|
-
const client = new ConduitClient(mockConfig);
|
|
58
|
-
const result = await client.call('user.whoami');
|
|
59
|
-
assert.deepStrictEqual(result, expectedResult);
|
|
60
|
-
});
|
|
61
|
-
it('should throw ConduitError on API error', async () => {
|
|
62
|
-
global.fetch = mock.fn(async () => {
|
|
63
|
-
return new Response(JSON.stringify({
|
|
64
|
-
result: null,
|
|
65
|
-
error_code: 'ERR-CONDUIT-CORE',
|
|
66
|
-
error_info: 'Invalid token',
|
|
67
|
-
}));
|
|
68
|
-
});
|
|
69
|
-
const client = new ConduitClient(mockConfig);
|
|
70
|
-
await assert.rejects(() => client.call('user.whoami'), (err) => {
|
|
71
|
-
assert.ok(err instanceof ConduitError);
|
|
72
|
-
assert.strictEqual(err.code, 'ERR-CONDUIT-CORE');
|
|
73
|
-
assert.strictEqual(err.message, 'Invalid token');
|
|
74
|
-
return true;
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
it('should throw ConduitError on HTTP error', async () => {
|
|
78
|
-
global.fetch = mock.fn(async () => {
|
|
79
|
-
return new Response('Not Found', { status: 404, statusText: 'Not Found' });
|
|
80
|
-
});
|
|
81
|
-
const client = new ConduitClient(mockConfig);
|
|
82
|
-
await assert.rejects(() => client.call('user.whoami'), (err) => {
|
|
83
|
-
assert.ok(err instanceof ConduitError);
|
|
84
|
-
assert.strictEqual(err.code, 'HTTP_ERROR');
|
|
85
|
-
assert.ok(err.message.includes('404'));
|
|
86
|
-
return true;
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
describe('ConduitError', () => {
|
|
91
|
-
it('should have correct name and properties', () => {
|
|
92
|
-
const error = new ConduitError('TEST_CODE', 'Test message');
|
|
93
|
-
assert.strictEqual(error.name, 'ConduitError');
|
|
94
|
-
assert.strictEqual(error.code, 'TEST_CODE');
|
|
95
|
-
assert.strictEqual(error.message, 'Test message');
|
|
96
|
-
});
|
|
97
|
-
});
|
package/dist/config.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/config.test.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
describe('loadConfig', () => {
|
|
4
|
-
const originalEnv = { ...process.env };
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
// Clear relevant env vars
|
|
7
|
-
delete process.env.PHABRICATOR_URL;
|
|
8
|
-
delete process.env.PHABRICATOR_API_TOKEN;
|
|
9
|
-
});
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
process.env = { ...originalEnv };
|
|
12
|
-
});
|
|
13
|
-
it('should load config from environment variables', async () => {
|
|
14
|
-
process.env.PHABRICATOR_URL = 'https://phabricator.example.com';
|
|
15
|
-
process.env.PHABRICATOR_API_TOKEN = 'api-test-token';
|
|
16
|
-
// Re-import to get fresh module
|
|
17
|
-
const { loadConfig } = await import('./config.js');
|
|
18
|
-
const config = loadConfig();
|
|
19
|
-
assert.strictEqual(config.phabricatorUrl, 'https://phabricator.example.com');
|
|
20
|
-
assert.strictEqual(config.apiToken, 'api-test-token');
|
|
21
|
-
});
|
|
22
|
-
it('should strip trailing slash from URL', async () => {
|
|
23
|
-
process.env.PHABRICATOR_URL = 'https://phabricator.example.com/';
|
|
24
|
-
process.env.PHABRICATOR_API_TOKEN = 'api-test-token';
|
|
25
|
-
const { loadConfig } = await import('./config.js');
|
|
26
|
-
const config = loadConfig();
|
|
27
|
-
assert.strictEqual(config.phabricatorUrl, 'https://phabricator.example.com');
|
|
28
|
-
});
|
|
29
|
-
it('should throw error for invalid URL', async () => {
|
|
30
|
-
process.env.PHABRICATOR_URL = 'not-a-valid-url';
|
|
31
|
-
process.env.PHABRICATOR_API_TOKEN = 'api-test-token';
|
|
32
|
-
const { loadConfig } = await import('./config.js?v=1');
|
|
33
|
-
assert.throws(() => loadConfig(), /Invalid url/);
|
|
34
|
-
});
|
|
35
|
-
it('should throw error for empty token', async () => {
|
|
36
|
-
process.env.PHABRICATOR_URL = 'https://phabricator.example.com';
|
|
37
|
-
process.env.PHABRICATOR_API_TOKEN = '';
|
|
38
|
-
// When token is empty string, it should either throw or fall back to arcrc
|
|
39
|
-
// Since arcrc exists on this machine, it will use that - so we skip this test
|
|
40
|
-
// if arcrc is present. The important thing is it doesn't accept empty string.
|
|
41
|
-
const { loadConfig } = await import('./config.js?v=2');
|
|
42
|
-
// This test verifies the schema validation - empty string should fail zod validation
|
|
43
|
-
// but it may fall back to arcrc first, so we just verify it doesn't crash
|
|
44
|
-
try {
|
|
45
|
-
const config = loadConfig();
|
|
46
|
-
// If it succeeds, it used arcrc fallback which is fine
|
|
47
|
-
assert.ok(config.apiToken.length > 0);
|
|
48
|
-
}
|
|
49
|
-
catch (e) {
|
|
50
|
-
// If it throws, that's also fine - means it correctly rejected empty token
|
|
51
|
-
assert.ok(e.message.includes('too_small') || e.message.includes('API_TOKEN'));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|