Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 23 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,50 +86,37 @@ The server provides the following ArgoCD management tools:

### Usage with VSCode

1. Follow the [Use MCP servers in VS Code documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers), and create a `.vscode/mcp.json` file in your project:
```json
{
"servers": {
"argocd-mcp-stdio": {
"type": "stdio",
"command": "npx",
"args": [
"argocd-mcp@latest",
"stdio"
],
"env": {
"ARGOCD_BASE_URL": "<argocd_url>",
"ARGOCD_API_TOKEN": "<argocd_token>"
}
}
}
}
1. Enable the ArgoCD MCP server in VS Code:
```bash
npx argocd-mcp@latest vscode enable --url <argocd_url> --token <argocd_token>
```

Optionally, use the `--workspace` flag to install in the current workspace directory instead of the user configuration directory.

You can also set the `ARGOCD_BASE_URL` and `ARGOCD_API_TOKEN` environment variables instead of using the `--url` and `--token` flags.

2. Start a conversation with an AI assistant in VS Code that supports MCP.

To disable the server, run:
```bash
npx argocd-mcp@latest vscode disable
```

### Usage with Claude Desktop

1. Follow the [MCP in Claude Desktop documentation](https://modelcontextprotocol.io/quickstart/user), and create a `claude_desktop_config.json` configuration file:
```json
{
"mcpServers": {
"argocd-mcp": {
"command": "npx",
"args": [
"argocd-mcp@latest",
"stdio"
],
"env": {
"ARGOCD_BASE_URL": "<argocd_url>",
"ARGOCD_API_TOKEN": "<argocd_token>"
}
}
}
}
1. Enable the ArgoCD MCP server in Claude Desktop:
```bash
npx argocd-mcp@latest claude enable --url <argocd_url> --token <argocd_token>
```

2. Configure Claude Desktop to use this configuration file in settings.
You can also set the `ARGOCD_BASE_URL` and `ARGOCD_API_TOKEN` environment variables instead of using the `--url` and `--token` flags.

2. Restart Claude Desktop to load the configuration.

To disable the server, run:
```bash
npx argocd-mcp@latest claude disable
```

### Self-signed Certificates

Expand Down
154 changes: 153 additions & 1 deletion src/cmd/cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
connectHttpTransport,
connectSSETransport
} from '../server/transport.js';
import { ClaudeConfigManager } from '../platform/claude/config.js';
import { VSCodeConfigManager } from '../platform/vscode/config.js';

export const cmd = () => {
const exe = yargs(hideBin(process.argv));
Expand Down Expand Up @@ -40,5 +42,155 @@ export const cmd = () => {
({ port }) => connectHttpTransport(port)
);

exe.demandCommand().parseSync();
const validateUrl = (baseUrl?: string) => {
// MCP servers do not have access to local env, it must be set in config
// If flag was not set, fallback to env
if (!baseUrl) {
baseUrl = process.env.ARGOCD_BASE_URL;
if (!baseUrl) {
throw new Error(
'Argocd baseurl not provided and not in env, please provide it with the --url flag'
);
}
}

// Validate url
new URL(baseUrl);

return baseUrl;
};

const validateToken = (apiToken?: string) => {
// MCP servers do not have access to local env, it must be set in config
// If flag was not set, fallback to env
if (!apiToken) {
apiToken = process.env.ARGOCD_API_TOKEN;
if (!apiToken) {
throw new Error(
'Argocd token not provided and not in env, please provide it with the --token flag'
);
}
}

return apiToken;
};

exe.command('claude', 'Manage Claude Desktop integration', (yargs) => {
return yargs
.command(
'enable',
'Enable ArgoCD MCP server in Claude Desktop',
(yargs) => {
return yargs
.option('url', {
type: 'string',
description: 'ArgoCD base URL (falls back to ARGOCD_BASE_URL env var)'
})
.option('token', {
type: 'string',
description: 'ArgoCD API token (falls back to ARGOCD_API_TOKEN env var)'
});
},
async ({ url, token }) => {
const manager = new ClaudeConfigManager();
try {
console.log(`Configuration file: ${manager.getConfigPath()}`);
const wasEnabled = await manager.enable(validateUrl(url), validateToken(token));
if (wasEnabled) {
console.log('✓ ArgoCD MCP server configuration updated in Claude Desktop');
} else {
console.log('✓ ArgoCD MCP server enabled in Claude Desktop');
}
} catch (error) {
console.error('Failed to enable ArgoCD MCP server:', (error as Error).message);
process.exit(1);
}
}
)
.command(
'disable',
'Disable ArgoCD MCP server in Claude Desktop',
() => {},
async () => {
const manager = new ClaudeConfigManager();
try {
console.log(`Configuration file: ${manager.getConfigPath()}`);
const wasEnabled = await manager.disable();
if (wasEnabled) {
console.log('✓ ArgoCD MCP server disabled in Claude Desktop');
} else {
console.log('ArgoCD MCP server was not enabled');
}
} catch (error) {
console.error('Failed to disable ArgoCD MCP server:', (error as Error).message);
process.exit(1);
}
}
);
});

exe.command('vscode', 'Manage VS Code integration', (yargs) => {
return yargs
.command(
'enable',
'Enable ArgoCD MCP server in VS Code',
(yargs) => {
return yargs
.option('workspace', {
type: 'boolean',
description: 'Install in current workspace directory'
})
.option('url', {
type: 'string',
description: 'ArgoCD base URL (falls back to ARGOCD_BASE_URL env var)'
})
.option('token', {
type: 'string',
description: 'ArgoCD API token (falls back to ARGOCD_API_TOKEN env var)'
});
},
async ({ workspace, url, token }) => {
const manager = new VSCodeConfigManager(workspace);
try {
console.log(`Configuration file: ${manager.getConfigPath()}`);
const wasEnabled = await manager.enable(validateUrl(url), validateToken(token));
if (wasEnabled) {
console.log('✓ ArgoCD MCP server configuration updated in VS Code');
} else {
console.log('✓ ArgoCD MCP server enabled in VS Code');
}
} catch (error) {
console.error('Failed to enable ArgoCD MCP server:', (error as Error).message);
process.exit(1);
}
}
)
.command(
'disable',
'Disable ArgoCD MCP server in VS Code',
(yargs) => {
return yargs.option('workspace', {
type: 'boolean',
description: 'Install in current workspace directory'
});
},
async ({ workspace }) => {
const manager = new VSCodeConfigManager(workspace);
try {
console.log(`Configuration file: ${manager.getConfigPath()}`);
const wasEnabled = await manager.disable();
if (wasEnabled) {
console.log('✓ ArgoCD MCP server disabled in VS Code');
} else {
console.log('ArgoCD MCP server was not enabled');
}
} catch (error) {
console.error('Failed to disable ArgoCD MCP server:', (error as Error).message);
process.exit(1);
}
}
);
});

exe.demandCommand().strict().parse();
};
145 changes: 145 additions & 0 deletions src/platform/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { readFile, writeFile, mkdir, copyFile, stat } from 'fs/promises';
import { dirname, join } from 'path';

/**
* Base interface that all MCP config formats must implement.
* This ensures type safety when accessing the servers collection.
*/
export interface MCPConfig {
[serversKey: string]: Record<string, unknown>;
}

const isObject = (obj: unknown): boolean => {
return !!obj && typeof obj === 'object' && !Array.isArray(obj);
};

/**
* Abstract base class for managing MCP server configurations across different platforms.
*
* This implementation preserves all unknown properties in the config file to avoid data loss
* when modifying only the server configuration.
*
* @template T - The specific config type for the platform (must extend MCPConfig)
* @template S - The server configuration type for the platform
*/
export abstract class ConfigManager<T extends MCPConfig, S = unknown> {
protected readonly serverName = 'argocd-mcp-stdio';
protected abstract configPath: string;
protected abstract getServersKey(): Extract<keyof T, string>;
protected abstract createServerConfig(baseUrl: string, apiToken: string): S;

/**
* ReadConfig preserves all existing properties in the config file.
* @returns config casted to type T
*/
async readConfig(): Promise<T> {
try {
const content = await readFile(this.configPath, 'utf-8');

// Parse as unknown first to ensure we preserve all properties
const parsed = JSON.parse(content) as unknown;

if (!isObject(parsed)) {
// Overwrite with object
return {} as T;
}

return parsed as T;
} catch (error) {
// File does not exist
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return {} as T;
}

// Invalid JSON
if (error instanceof SyntaxError) {
// Overwrite with object
return {} as T;
}

throw error;
}
}

async writeConfig(config: T): Promise<void> {
const dir = dirname(this.configPath);
try {
await mkdir(dir, { recursive: true });
await writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
} catch (error) {
throw new Error(`Failed to write config to ${this.configPath}: ${(error as Error).message}`);
}
}

/**
* Enable the server configuration.
* @param baseUrl - Optional ArgoCD base URL
* @param apiToken - Optional ArgoCD API token
* @returns true if the server was already enabled, false if it was newly enabled
*/
async enable(baseUrl: string, apiToken: string): Promise<boolean> {
const config = await this.readConfig();
const serversKey = this.getServersKey();

// Ensure servers object exists
const obj = config[serversKey];
if (!isObject(obj)) {
// Overwrite with object
(config[serversKey] as Record<string, S>) = {};
}

const servers = config[serversKey] as Record<string, S>;
const wasEnabled = this.serverName in servers;
const serverConfig = this.createServerConfig(baseUrl, apiToken);
servers[this.serverName] = serverConfig;
await this.createBackup();
await this.writeConfig(config);
return wasEnabled;
}

async createBackup(): Promise<void> {
try {
await stat(this.configPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return;
}
throw error;
}

const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const dir = dirname(this.configPath);
const backupPath = join(dir, `mcp.backup.${timestamp}.json`);

await copyFile(this.configPath, backupPath);
}

/**
* Disable the server configuration.
* @returns true if the server was enabled and has been disabled, false if it was not enabled
*/
async disable(): Promise<boolean> {
const config = await this.readConfig();
const serversKey = this.getServersKey();

const obj = config[serversKey];
if (!isObject(obj)) {
// Nothing to disable if servers object doesn't exist
return false;
}

const servers = config[serversKey] as Record<string, S>;
const wasEnabled = this.serverName in servers;

if (wasEnabled) {
delete servers[this.serverName];
await this.writeConfig(config);
}

return wasEnabled;
}

getConfigPath(): string {
return this.configPath;
}
}
Loading