Skip to content

Commit 0db7fed

Browse files
committed
feat: add enable/disable commands for Claude and VSCode
Signed-off-by: nick powell <[email protected]>
1 parent a1971c7 commit 0db7fed

File tree

4 files changed

+405
-1
lines changed

4 files changed

+405
-1
lines changed

src/cmd/cmd.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
connectHttpTransport,
66
connectSSETransport
77
} from '../server/transport.js';
8+
import { ClaudeConfigManager } from '../platform/claude/config.js';
9+
import { VSCodeConfigManager } from '../platform/vscode/config.js';
810

911
export const cmd = () => {
1012
const exe = yargs(hideBin(process.argv));
@@ -40,5 +42,122 @@ export const cmd = () => {
4042
({ port }) => connectHttpTransport(port)
4143
);
4244

43-
exe.demandCommand().parseSync();
45+
exe.command('claude', 'Manage Claude Desktop integration', (yargs) => {
46+
return yargs
47+
.command(
48+
'enable',
49+
'Enable ArgoCD MCP server in Claude Desktop',
50+
(yargs) => {
51+
return yargs
52+
.option('url', {
53+
type: 'string',
54+
description: 'ArgoCD base URL (falls back to ARGOCD_BASE_URL env var)'
55+
})
56+
.option('token', {
57+
type: 'string',
58+
description: 'ArgoCD API token (falls back to ARGOCD_API_TOKEN env var)'
59+
});
60+
},
61+
async ({ url, token }) => {
62+
const manager = new ClaudeConfigManager();
63+
try {
64+
console.log(`Configuration file: ${manager.getConfigPath()}`);
65+
const wasEnabled = await manager.enable(url, token);
66+
if (wasEnabled) {
67+
console.log('✓ ArgoCD MCP server configuration updated in Claude Desktop');
68+
} else {
69+
console.log('✓ ArgoCD MCP server enabled in Claude Desktop');
70+
}
71+
} catch (error) {
72+
console.error('Failed to enable ArgoCD MCP server:', (error as Error).message);
73+
process.exit(1);
74+
}
75+
}
76+
)
77+
.command(
78+
'disable',
79+
'Disable ArgoCD MCP server in Claude Desktop',
80+
() => {},
81+
async () => {
82+
const manager = new ClaudeConfigManager();
83+
try {
84+
console.log(`Configuration file: ${manager.getConfigPath()}`);
85+
const wasEnabled = await manager.disable();
86+
if (wasEnabled) {
87+
console.log('✓ ArgoCD MCP server disabled in Claude Desktop');
88+
} else {
89+
console.log('ArgoCD MCP server was not enabled');
90+
}
91+
} catch (error) {
92+
console.error('Failed to disable ArgoCD MCP server:', (error as Error).message);
93+
process.exit(1);
94+
}
95+
}
96+
);
97+
});
98+
99+
exe.command('vscode', 'Manage VS Code integration', (yargs) => {
100+
return yargs
101+
.command(
102+
'enable',
103+
'Enable ArgoCD MCP server in VS Code',
104+
(yargs) => {
105+
return yargs
106+
.option('workspace', {
107+
type: 'boolean',
108+
description: 'Install in current workspace directory'
109+
})
110+
.option('url', {
111+
type: 'string',
112+
description: 'ArgoCD base URL (falls back to ARGOCD_BASE_URL env var)'
113+
})
114+
.option('token', {
115+
type: 'string',
116+
description: 'ArgoCD API token (falls back to ARGOCD_API_TOKEN env var)'
117+
});
118+
},
119+
async ({ workspace, url, token }) => {
120+
const manager = new VSCodeConfigManager(workspace);
121+
try {
122+
console.log(`Configuration file: ${manager.getConfigPath()}`);
123+
const wasEnabled = await manager.enable(url, token);
124+
if (wasEnabled) {
125+
console.log('✓ ArgoCD MCP server configuration updated in VS Code');
126+
} else {
127+
console.log('✓ ArgoCD MCP server enabled in VS Code');
128+
}
129+
} catch (error) {
130+
console.error('Failed to enable ArgoCD MCP server:', (error as Error).message);
131+
process.exit(1);
132+
}
133+
}
134+
)
135+
.command(
136+
'disable',
137+
'Disable ArgoCD MCP server in VS Code',
138+
(yargs) => {
139+
return yargs.option('workspace', {
140+
type: 'boolean',
141+
description: 'Install in current workspace directory'
142+
});
143+
},
144+
async ({ workspace }) => {
145+
const manager = new VSCodeConfigManager(workspace);
146+
try {
147+
console.log(`Configuration file: ${manager.getConfigPath()}`);
148+
const wasEnabled = await manager.disable();
149+
if (wasEnabled) {
150+
console.log('✓ ArgoCD MCP server disabled in VS Code');
151+
} else {
152+
console.log('ArgoCD MCP server was not enabled');
153+
}
154+
} catch (error) {
155+
console.error('Failed to disable ArgoCD MCP server:', (error as Error).message);
156+
process.exit(1);
157+
}
158+
}
159+
);
160+
});
161+
162+
exe.demandCommand().strict().parse();
44163
};

src/platform/base.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { readFile, writeFile, mkdir } from 'fs/promises';
2+
import { dirname } from 'path';
3+
4+
/**
5+
* Base interface that all MCP config formats must implement.
6+
* This ensures type safety when accessing the servers collection.
7+
*/
8+
export interface MCPConfig {
9+
[serversKey: string]: Record<string, unknown>;
10+
}
11+
12+
/**
13+
* Abstract base class for managing MCP server configurations across different platforms.
14+
*
15+
* This implementation preserves all unknown properties in the config file to avoid data loss
16+
* when modifying only the server configuration.
17+
*
18+
* @template T - The specific config type for the platform (must extend MCPConfig)
19+
* @template S - The server configuration type for the platform
20+
*/
21+
export abstract class ConfigManager<T extends MCPConfig, S = unknown> {
22+
protected readonly serverName = 'argocd-mcp-stdio';
23+
24+
protected abstract configPath: string;
25+
protected abstract getDefaultConfig(): T;
26+
protected abstract getServersKey(): Extract<keyof T, string>;
27+
protected abstract createServerConfig(baseUrl: string, apiToken: string): S;
28+
29+
/**
30+
* ReadConfig preserves all existing properties in the config file.
31+
* @returns config casted to type T
32+
*/
33+
async readConfig(): Promise<T> {
34+
try {
35+
const content = await readFile(this.configPath, 'utf-8');
36+
37+
// Parse as unknown first to ensure we preserve all properties
38+
const parsed = JSON.parse(content) as unknown;
39+
40+
// Ensure parsed is an object, not array
41+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
42+
throw new Error(`Config file ${this.configPath} contains invalid data: expected an object`);
43+
}
44+
45+
return parsed as T;
46+
} catch (error) {
47+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
48+
return this.getDefaultConfig();
49+
}
50+
51+
if (error instanceof SyntaxError) {
52+
throw new Error(`Invalid JSON in config file ${this.configPath}: ${error.message}`);
53+
}
54+
55+
throw error;
56+
}
57+
}
58+
59+
async writeConfig(config: T): Promise<void> {
60+
const dir = dirname(this.configPath);
61+
try {
62+
await mkdir(dir, { recursive: true });
63+
await writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
64+
} catch (error) {
65+
throw new Error(`Failed to write config to ${this.configPath}: ${(error as Error).message}`);
66+
}
67+
}
68+
69+
/**
70+
* Enable the server configuration.
71+
* @param baseUrl - Optional ArgoCD base URL
72+
* @param apiToken - Optional ArgoCD API token
73+
* @returns true if the server was already enabled, false if it was newly enabled
74+
*/
75+
async enable(baseUrl?: string, apiToken?: string): Promise<boolean> {
76+
// MCP servers do not have access to local env, it must be set in config
77+
// If flag was not set, fallback to env
78+
if (!baseUrl) {
79+
baseUrl = process.env.ARGOCD_BASE_URL;
80+
if (!baseUrl) {
81+
throw new Error(
82+
'Argocd baseurl not provided and not in env, please provide it with the --url flag'
83+
);
84+
}
85+
}
86+
// Validate url
87+
new URL(baseUrl);
88+
89+
if (!apiToken) {
90+
apiToken = process.env.ARGOCD_API_TOKEN;
91+
if (!apiToken) {
92+
throw new Error(
93+
'Argocd token not provided and not in env, please provide it with the --token flag'
94+
);
95+
}
96+
}
97+
98+
const config = await this.readConfig();
99+
const serversKey = this.getServersKey();
100+
101+
// Ensure servers object exists
102+
const obj = config[serversKey];
103+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
104+
(config[serversKey] as Record<string, S>) = {};
105+
}
106+
107+
const servers = config[serversKey] as Record<string, S>;
108+
const wasEnabled = this.serverName in servers;
109+
const serverConfig = this.createServerConfig(baseUrl, apiToken);
110+
servers[this.serverName] = serverConfig;
111+
await this.writeConfig(config);
112+
return wasEnabled;
113+
}
114+
115+
/**
116+
* Disable the server configuration.
117+
* @returns true if the server was enabled and has been disabled, false if it was not enabled
118+
*/
119+
async disable(): Promise<boolean> {
120+
const config = await this.readConfig();
121+
const serversKey = this.getServersKey();
122+
123+
const obj = config[serversKey];
124+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
125+
// Nothing to disable if servers key doesn't exist
126+
return false;
127+
}
128+
129+
const servers = config[serversKey] as Record<string, S>;
130+
const wasEnabled = this.serverName in servers;
131+
132+
if (wasEnabled) {
133+
delete servers[this.serverName];
134+
await this.writeConfig(config);
135+
}
136+
137+
return wasEnabled;
138+
}
139+
140+
getConfigPath(): string {
141+
return this.configPath;
142+
}
143+
}

src/platform/claude/config.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { join } from 'path';
2+
import { homedir } from 'os';
3+
import { ConfigManager, MCPConfig } from '../base.js';
4+
5+
interface ClaudeServerConfig {
6+
command: string;
7+
args: string[];
8+
env?: Record<string, string>;
9+
}
10+
11+
// Claude-specific key
12+
const serversKey = 'mcpServers';
13+
14+
interface ClaudeConfig extends MCPConfig {
15+
[serversKey]: Record<string, ClaudeServerConfig>;
16+
}
17+
18+
export class ClaudeConfigManager extends ConfigManager<ClaudeConfig, ClaudeServerConfig> {
19+
protected configPath: string;
20+
21+
constructor() {
22+
super();
23+
const home = homedir();
24+
const platform = process.platform;
25+
26+
switch (platform) {
27+
case 'darwin':
28+
this.configPath = join(
29+
home,
30+
'Library',
31+
'Application Support',
32+
'Claude',
33+
'claude_desktop_config.json'
34+
);
35+
break;
36+
case 'win32':
37+
this.configPath = join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
38+
break;
39+
case 'linux':
40+
this.configPath = join(home, '.config', 'Claude', 'claude_desktop_config.json');
41+
break;
42+
default:
43+
throw new Error(`platform not supported: ${platform}`);
44+
}
45+
}
46+
47+
protected getDefaultConfig(): ClaudeConfig {
48+
return { [serversKey]: {} };
49+
}
50+
51+
protected getServersKey() {
52+
return serversKey;
53+
}
54+
55+
protected createServerConfig(baseUrl: string, apiToken: string): ClaudeServerConfig {
56+
const serverConfig: ClaudeServerConfig = {
57+
command: 'npx',
58+
args: ['argocd-mcp@latest', 'stdio'],
59+
env: {
60+
ARGOCD_BASE_URL: baseUrl,
61+
ARGOCD_API_TOKEN: apiToken
62+
}
63+
};
64+
65+
return serverConfig;
66+
}
67+
}

0 commit comments

Comments
 (0)