server.ts•13.5 kB
import { McpServer, ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import packageJSON from '../../package.json' with { type: 'json' };
import { ArgoCDClient } from '../argocd/client.js';
import { z, ZodRawShape } from 'zod';
import { V1alpha1Application, V1alpha1ResourceResult } from '../types/argocd-types.js';
import {
ApplicationNamespaceSchema,
ApplicationSchema,
ResourceRefSchema
} from '../shared/models/schema.js';
type ServerInfo = {
argocdBaseUrl: string;
argocdApiToken: string;
};
export class Server extends McpServer {
private argocdClient: ArgoCDClient;
constructor(serverInfo: ServerInfo) {
super({
name: packageJSON.name,
version: packageJSON.version
});
this.argocdClient = new ArgoCDClient(serverInfo.argocdBaseUrl, serverInfo.argocdApiToken);
const isReadOnly =
String(process.env.MCP_READ_ONLY ?? '')
.trim()
.toLowerCase() === 'true';
// Always register read/query tools
this.addJsonOutputTool(
'list_applications',
'list_applications returns list of applications',
{
search: z
.string()
.optional()
.describe(
'Search applications by name. This is a partial match on the application name and does not support glob patterns (e.g. "*"). Optional.'
),
limit: z
.number()
.int()
.positive()
.optional()
.describe(
'Maximum number of applications to return. Use this to reduce token usage when there are many applications. Optional.'
),
offset: z
.number()
.int()
.min(0)
.optional()
.describe(
'Number of applications to skip before returning results. Use with limit for pagination. Optional.'
)
},
async ({ search, limit, offset }) =>
await this.argocdClient.listApplications({
search: search ?? undefined,
limit,
offset
})
);
this.addJsonOutputTool(
'get_application',
'get_application returns application by application name. Optionally specify the application namespace to get applications from non-default namespaces.',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema.optional()
},
async ({ applicationName, applicationNamespace }) =>
await this.argocdClient.getApplication(applicationName, applicationNamespace)
);
this.addJsonOutputTool(
'get_application_resource_tree',
'get_application_resource_tree returns resource tree for application by application name',
{ applicationName: z.string() },
async ({ applicationName }) =>
await this.argocdClient.getApplicationResourceTree(applicationName)
);
this.addJsonOutputTool(
'get_application_managed_resources',
'get_application_managed_resources returns managed resources for application by application name with optional filtering. Use filters to avoid token limits with large applications. Examples: kind="ConfigMap" for config maps only, namespace="production" for specific namespace, or combine multiple filters.',
{
applicationName: z.string(),
kind: z
.string()
.optional()
.describe(
'Filter by Kubernetes resource kind (e.g., "ConfigMap", "Secret", "Deployment")'
),
namespace: z.string().optional().describe('Filter by Kubernetes namespace'),
name: z.string().optional().describe('Filter by resource name'),
version: z.string().optional().describe('Filter by resource API version'),
group: z.string().optional().describe('Filter by API group'),
appNamespace: z.string().optional().describe('Filter by Argo CD application namespace'),
project: z.string().optional().describe('Filter by Argo CD project')
},
async ({ applicationName, kind, namespace, name, version, group, appNamespace, project }) => {
const filters = {
...(kind && { kind }),
...(namespace && { namespace }),
...(name && { name }),
...(version && { version }),
...(group && { group }),
...(appNamespace && { appNamespace }),
...(project && { project })
};
return await this.argocdClient.getApplicationManagedResources(
applicationName,
Object.keys(filters).length > 0 ? filters : undefined
);
}
);
this.addJsonOutputTool(
'get_application_workload_logs',
'get_application_workload_logs returns logs for application workload (Deployment, StatefulSet, Pod, etc.) by application name and resource ref and optionally container name',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema,
resourceRef: ResourceRefSchema,
container: z.string()
},
async ({ applicationName, applicationNamespace, resourceRef, container }) =>
await this.argocdClient.getWorkloadLogs(
applicationName,
applicationNamespace,
resourceRef as V1alpha1ResourceResult,
container
)
);
this.addJsonOutputTool(
'get_application_events',
'get_application_events returns events for application by application name',
{ applicationName: z.string() },
async ({ applicationName }) => await this.argocdClient.getApplicationEvents(applicationName)
);
this.addJsonOutputTool(
'get_resource_events',
'get_resource_events returns events for a resource that is managed by an application',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema,
resourceUID: z.string(),
resourceNamespace: z.string(),
resourceName: z.string()
},
async ({
applicationName,
applicationNamespace,
resourceUID,
resourceNamespace,
resourceName
}) =>
await this.argocdClient.getResourceEvents(
applicationName,
applicationNamespace,
resourceUID,
resourceNamespace,
resourceName
)
);
this.addJsonOutputTool(
'get_resources',
'get_resources return manifests for resources specified by resourceRefs. If resourceRefs is empty or not provided, fetches all resources managed by the application.',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema,
resourceRefs: ResourceRefSchema.array().optional()
},
async ({ applicationName, applicationNamespace, resourceRefs }) => {
let refs = resourceRefs || [];
if (refs.length === 0) {
const tree = await this.argocdClient.getApplicationResourceTree(applicationName);
refs =
tree.nodes?.map((node) => ({
uid: node.uid!,
version: node.version!,
group: node.group!,
kind: node.kind!,
name: node.name!,
namespace: node.namespace!
})) || [];
}
return Promise.all(
refs.map((ref) =>
this.argocdClient.getResource(applicationName, applicationNamespace, ref)
)
);
}
);
this.addJsonOutputTool(
'get_resource_actions',
'get_resource_actions returns actions for a resource that is managed by an application',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema,
resourceRef: ResourceRefSchema
},
async ({ applicationName, applicationNamespace, resourceRef }) =>
await this.argocdClient.getResourceActions(
applicationName,
applicationNamespace,
resourceRef as V1alpha1ResourceResult
)
);
// Only register modification tools if not in read-only mode
if (!isReadOnly) {
this.addJsonOutputTool(
'create_application',
'create_application creates a new ArgoCD application in the specified namespace. The application.metadata.namespace field determines where the Application resource will be created (e.g., "argocd", "argocd-apps", or any custom namespace).',
{ application: ApplicationSchema },
async ({ application }) =>
await this.argocdClient.createApplication(application as V1alpha1Application)
);
this.addJsonOutputTool(
'update_application',
'update_application updates application',
{ applicationName: z.string(), application: ApplicationSchema },
async ({ applicationName, application }) =>
await this.argocdClient.updateApplication(
applicationName,
application as V1alpha1Application
)
);
this.addJsonOutputTool(
'delete_application',
'delete_application deletes application. Specify applicationNamespace if the application is in a non-default namespace to avoid permission errors.',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema.optional().describe(
'The namespace where the application is located. Required if application is not in the default namespace.'
),
cascade: z
.boolean()
.optional()
.describe('Whether to cascade the deletion to child resources'),
propagationPolicy: z
.string()
.optional()
.describe('Deletion propagation policy (e.g., "Foreground", "Background", "Orphan")')
},
async ({ applicationName, applicationNamespace, cascade, propagationPolicy }) => {
const options: Record<string, string | boolean> = {};
if (applicationNamespace) options.appNamespace = applicationNamespace;
if (cascade !== undefined) options.cascade = cascade;
if (propagationPolicy) options.propagationPolicy = propagationPolicy;
return await this.argocdClient.deleteApplication(
applicationName,
Object.keys(options).length > 0 ? options : undefined
);
}
);
this.addJsonOutputTool(
'sync_application',
'sync_application syncs application. Specify applicationNamespace if the application is in a non-default namespace to avoid permission errors.',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema.optional().describe(
'The namespace where the application is located. Required if application is not in the default namespace.'
),
dryRun: z
.boolean()
.optional()
.describe('Perform a dry run sync without applying changes'),
prune: z
.boolean()
.optional()
.describe('Remove resources that are no longer defined in the source'),
revision: z
.string()
.optional()
.describe('Sync to a specific revision instead of the latest'),
syncOptions: z
.array(z.string())
.optional()
.describe(
'Additional sync options (e.g., ["CreateNamespace=true", "PrunePropagationPolicy=foreground"])'
)
},
async ({ applicationName, applicationNamespace, dryRun, prune, revision, syncOptions }) => {
const options: Record<string, string | boolean | string[]> = {};
if (applicationNamespace) options.appNamespace = applicationNamespace;
if (dryRun !== undefined) options.dryRun = dryRun;
if (prune !== undefined) options.prune = prune;
if (revision) options.revision = revision;
if (syncOptions) options.syncOptions = syncOptions;
return await this.argocdClient.syncApplication(
applicationName,
Object.keys(options).length > 0 ? options : undefined
);
}
);
this.addJsonOutputTool(
'run_resource_action',
'run_resource_action runs an action on a resource',
{
applicationName: z.string(),
applicationNamespace: ApplicationNamespaceSchema,
resourceRef: ResourceRefSchema,
action: z.string()
},
async ({ applicationName, applicationNamespace, resourceRef, action }) =>
await this.argocdClient.runResourceAction(
applicationName,
applicationNamespace,
resourceRef as V1alpha1ResourceResult,
action
)
);
}
}
private addJsonOutputTool<Args extends ZodRawShape, T>(
name: string,
description: string,
paramsSchema: Args,
cb: (...cbArgs: Parameters<ToolCallback<Args>>) => T
) {
this.tool(name, description, paramsSchema as ZodRawShape, async (...args) => {
try {
const result = await cb.apply(this, args as Parameters<ToolCallback<Args>>);
return {
isError: false,
content: [{ type: 'text', text: JSON.stringify(result) }]
};
} catch (error) {
return {
isError: true,
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }]
};
}
});
}
}
export const createServer = (serverInfo: ServerInfo) => {
return new Server(serverInfo);
};