Skip to content

Commit dddbf89

Browse files
authored
Add installation validation; update naming scheme (#18)
1 parent 5e36f1a commit dddbf89

8 files changed

Lines changed: 552 additions & 58 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# @coinbase/payments-mcp
22

3-
A TypeScript-based npx installer for the payments-mcp project, providing seamless integration with stdio-compatible MCP clients for agentic payments via x402 and the x402 Bazaar.
3+
Payments MCP is an MCP server & companion wallet app that combines wallets, onramps, and payments via x402 into a single solution for agentic commerce.
4+
5+
It enables AI agents to autonomously discover and pay for services without API keys, complex seed phrases, or manual intervention.
6+
7+
Read the [documentation](https://docs.cdp.coinbase.com/payments-mcp/welcome)
48

59
## Quick Start
610

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/payments-mcp",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "NPX installer for Payments MCP",
55
"main": "dist/cli.js",
66
"bin": {
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { ProcessUtils } from '../../utils/processUtils';
3+
import { Logger } from '../../utils/logger';
4+
import { exec } from 'child_process';
5+
6+
// Mock child_process
7+
jest.mock('child_process', () => ({
8+
exec: jest.fn(),
9+
}));
10+
11+
describe('ProcessUtils', () => {
12+
let processUtils: ProcessUtils;
13+
let logger: Logger;
14+
let mockExec: jest.MockedFunction<typeof exec>;
15+
16+
beforeEach(() => {
17+
logger = new Logger(false);
18+
processUtils = new ProcessUtils(logger);
19+
mockExec = exec as jest.MockedFunction<typeof exec>;
20+
jest.clearAllMocks();
21+
});
22+
23+
describe('isPaymentsMCPRunning', () => {
24+
it('should return true when process is running on Unix systems', async () => {
25+
// Mock platform to be darwin (macOS)
26+
Object.defineProperty(process, 'platform', {
27+
value: 'darwin',
28+
writable: true,
29+
});
30+
31+
// Mock pgrep returning PIDs
32+
mockExec.mockImplementation((_cmd, callback: any) => {
33+
callback(null, { stdout: '12345\n67890\n', stderr: '' });
34+
return {} as any;
35+
});
36+
37+
const result = await processUtils.isPaymentsMCPRunning();
38+
expect(result).toBe(true);
39+
});
40+
41+
it('should return false when process is not running on Unix systems', async () => {
42+
Object.defineProperty(process, 'platform', {
43+
value: 'darwin',
44+
writable: true,
45+
});
46+
47+
// Mock pgrep returning no results (exit code 1)
48+
mockExec.mockImplementation((_cmd, callback: any) => {
49+
const error = new Error('Command failed') as any;
50+
error.code = 1;
51+
callback(error, { stdout: '', stderr: '' });
52+
return {} as any;
53+
});
54+
55+
const result = await processUtils.isPaymentsMCPRunning();
56+
expect(result).toBe(false);
57+
});
58+
59+
it('should return true when process is running on Windows', async () => {
60+
Object.defineProperty(process, 'platform', {
61+
value: 'win32',
62+
writable: true,
63+
});
64+
65+
// Mock tasklist returning process info
66+
mockExec.mockImplementation((_cmd, callback: any) => {
67+
callback(null, {
68+
stdout: 'payments-mcp-server.exe 12345 Console',
69+
stderr: '',
70+
});
71+
return {} as any;
72+
});
73+
74+
const result = await processUtils.isPaymentsMCPRunning();
75+
expect(result).toBe(true);
76+
});
77+
78+
it('should return false when process is not running on Windows', async () => {
79+
Object.defineProperty(process, 'platform', {
80+
value: 'win32',
81+
writable: true,
82+
});
83+
84+
// Mock tasklist returning "no tasks" message
85+
mockExec.mockImplementation((_cmd, callback: any) => {
86+
callback(null, {
87+
stdout:
88+
'INFO: No tasks are running which match the specified criteria.',
89+
stderr: '',
90+
});
91+
return {} as any;
92+
});
93+
94+
const result = await processUtils.isPaymentsMCPRunning();
95+
expect(result).toBe(false);
96+
});
97+
});
98+
99+
describe('getMCPClientProcessNames', () => {
100+
it('should return macOS process names on darwin', () => {
101+
Object.defineProperty(process, 'platform', {
102+
value: 'darwin',
103+
writable: true,
104+
});
105+
106+
const names = processUtils.getMCPClientProcessNames();
107+
expect(names).toEqual(['Claude', 'Cherry Studio', 'Goose', 'Codex']);
108+
});
109+
110+
it('should return Windows process names on win32', () => {
111+
Object.defineProperty(process, 'platform', {
112+
value: 'win32',
113+
writable: true,
114+
});
115+
116+
const names = processUtils.getMCPClientProcessNames();
117+
expect(names).toEqual([
118+
'Claude.exe',
119+
'Cherry Studio.exe',
120+
'Goose.exe',
121+
'Codex.exe',
122+
]);
123+
});
124+
125+
it('should return Linux process names on linux', () => {
126+
Object.defineProperty(process, 'platform', {
127+
value: 'linux',
128+
writable: true,
129+
});
130+
131+
const names = processUtils.getMCPClientProcessNames();
132+
expect(names).toEqual(['claude', 'cherry-studio', 'goose', 'codex']);
133+
});
134+
});
135+
136+
describe('getRunningMCPClients', () => {
137+
it('should detect running clients on macOS', async () => {
138+
Object.defineProperty(process, 'platform', {
139+
value: 'darwin',
140+
writable: true,
141+
});
142+
143+
let callCount = 0;
144+
mockExec.mockImplementation((cmd: string, callback: any) => {
145+
callCount++;
146+
if (cmd.includes('Claude')) {
147+
// Claude is running
148+
callback(null, { stdout: '12345\n', stderr: '' });
149+
} else {
150+
// Other clients are not running
151+
const error = new Error('Command failed') as any;
152+
error.code = 1;
153+
callback(error, { stdout: '', stderr: '' });
154+
}
155+
return {} as any;
156+
});
157+
158+
const clients = await processUtils.getRunningMCPClients();
159+
expect(clients).toEqual(['Claude']);
160+
expect(callCount).toBeGreaterThan(0);
161+
});
162+
163+
it('should return empty array when no clients are running', async () => {
164+
Object.defineProperty(process, 'platform', {
165+
value: 'darwin',
166+
writable: true,
167+
});
168+
169+
mockExec.mockImplementation((_cmd, callback: any) => {
170+
const error = new Error('Command failed') as any;
171+
error.code = 1;
172+
callback(error, { stdout: '', stderr: '' });
173+
return {} as any;
174+
});
175+
176+
const clients = await processUtils.getRunningMCPClients();
177+
expect(clients).toEqual([]);
178+
});
179+
});
180+
181+
describe('getRunningMCPClientsFormatted', () => {
182+
it('should return formatted string when all clients are running', async () => {
183+
Object.defineProperty(process, 'platform', {
184+
value: 'darwin',
185+
writable: true,
186+
});
187+
188+
mockExec.mockImplementation((_cmd, callback: any) => {
189+
// All clients running
190+
callback(null, { stdout: '12345\n', stderr: '' });
191+
return {} as any;
192+
});
193+
194+
const formatted = await processUtils.getRunningMCPClientsFormatted();
195+
expect(formatted).toBe('Claude, Cherry Studio, Goose, Codex');
196+
});
197+
198+
it('should return null when no clients are running', async () => {
199+
Object.defineProperty(process, 'platform', {
200+
value: 'darwin',
201+
writable: true,
202+
});
203+
204+
mockExec.mockImplementation((_cmd, callback: any) => {
205+
const error = new Error('Command failed') as any;
206+
error.code = 1;
207+
callback(error, { stdout: '', stderr: '' });
208+
return {} as any;
209+
});
210+
211+
const formatted = await processUtils.getRunningMCPClientsFormatted();
212+
expect(formatted).toBeNull();
213+
});
214+
215+
it('should remove .exe extension on Windows', async () => {
216+
Object.defineProperty(process, 'platform', {
217+
value: 'win32',
218+
writable: true,
219+
});
220+
221+
mockExec.mockImplementation((cmd: string, callback: any) => {
222+
// Only Claude and Cherry Studio are running
223+
if (cmd.includes('Claude.exe')) {
224+
callback(null, { stdout: 'Claude.exe 12345 Console', stderr: '' });
225+
} else if (cmd.includes('Cherry Studio.exe')) {
226+
callback(null, {
227+
stdout: 'Cherry Studio.exe 67890 Console',
228+
stderr: '',
229+
});
230+
} else {
231+
callback(null, { stdout: 'INFO: No tasks are running', stderr: '' });
232+
}
233+
return {} as any;
234+
});
235+
236+
const formatted = await processUtils.getRunningMCPClientsFormatted();
237+
expect(formatted).toBe('Claude, Cherry Studio');
238+
});
239+
});
240+
});

src/cli.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,14 @@ program.configureHelp({
110110
program.on('--help', () => {
111111
console.log('');
112112
console.log('Examples:');
113-
console.log(' $ npx install-payments-mcp');
114-
console.log(' $ npx install-payments-mcp install');
115-
console.log(' $ npx install-payments-mcp install --force');
116-
console.log(' $ npx install-payments-mcp install --client <client>');
117-
console.log(' $ npx install-payments-mcp status');
118-
console.log(' $ npx install-payments-mcp status --client <client>');
119-
console.log(' $ npx install-payments-mcp uninstall');
120-
console.log(' $ npx install-payments-mcp --verbose');
113+
console.log(' $ npx @coinbase/payments-mcp');
114+
console.log(' $ npx @coinbase/payments-mcp install');
115+
console.log(' $ npx @coinbase/payments-mcp install --force');
116+
console.log(' $ npx @coinbase/payments-mcp install --client <client>');
117+
console.log(' $ npx @coinbase/payments-mcp status');
118+
console.log(' $ npx @coinbase/payments-mcp status --client <client>');
119+
console.log(' $ npx @coinbase/payments-mcp uninstall');
120+
console.log(' $ npx @coinbase/payments-mcp --verbose');
121121
console.log('');
122122
console.log('Supported MCP clients:');
123123
console.log(' - claude: Claude Desktop application');

0 commit comments

Comments
 (0)