feat: mcpctl v0.0.1 — first public release
Comprehensive MCP server management with kubectl-style CLI. Key features in this release: - Declarative YAML apply/get round-trip with project cloning support - Gated sessions with prompt intelligence for Claude - Interactive MCP console with traffic inspector - Persistent STDIO connections for containerized servers - RBAC with name-scoped bindings - Shell completions (fish + bash) auto-generated - Rate-limit retry with exponential backoff in apply - Project-scoped prompt management - Credential scrubbing from git history Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ export function createProgram(): Command {
|
||||
.enablePositionalOptions()
|
||||
.option('--daemon-url <url>', 'mcplocal daemon URL')
|
||||
.option('--direct', 'bypass mcplocal and connect directly to mcpd')
|
||||
.option('--project <name>', 'Target project for project commands');
|
||||
.option('-p, --project <name>', 'Target project for project commands');
|
||||
|
||||
program.addCommand(createStatusCommand());
|
||||
program.addCommand(createLoginCommand());
|
||||
@@ -59,17 +59,26 @@ export function createProgram(): Command {
|
||||
const fetchResource = async (resource: string, nameOrId?: string, opts?: { project?: string; all?: boolean }): Promise<unknown[]> => {
|
||||
const projectName = opts?.project ?? program.opts().project as string | undefined;
|
||||
|
||||
// --project scoping for servers and instances
|
||||
if (projectName && !nameOrId && (resource === 'servers' || resource === 'instances')) {
|
||||
const projectId = await resolveNameOrId(client, 'projects', projectName);
|
||||
if (resource === 'servers') {
|
||||
return client.get<unknown[]>(`/api/v1/projects/${projectId}/servers`);
|
||||
// Virtual resource: serverattachments (composed from project data)
|
||||
if (resource === 'serverattachments') {
|
||||
type ProjectWithServers = { name: string; id: string; servers?: Array<{ server: { name: string } }> };
|
||||
let projects: ProjectWithServers[];
|
||||
if (projectName) {
|
||||
const projectId = await resolveNameOrId(client, 'projects', projectName);
|
||||
const project = await client.get<ProjectWithServers>(`/api/v1/projects/${projectId}`);
|
||||
projects = [project];
|
||||
} else {
|
||||
projects = await client.get<ProjectWithServers[]>('/api/v1/projects');
|
||||
}
|
||||
// instances: fetch project servers, then filter instances by serverId
|
||||
const projectServers = await client.get<Array<{ id: string }>>(`/api/v1/projects/${projectId}/servers`);
|
||||
const serverIds = new Set(projectServers.map((s) => s.id));
|
||||
const allInstances = await client.get<Array<{ serverId: string }>>(`/api/v1/instances`);
|
||||
return allInstances.filter((inst) => serverIds.has(inst.serverId));
|
||||
const attachments: Array<{ project: string; server: string }> = [];
|
||||
for (const p of projects) {
|
||||
if (p.servers) {
|
||||
for (const ps of p.servers) {
|
||||
attachments.push({ server: ps.server.name, project: p.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
// --project scoping for prompts and promptrequests
|
||||
@@ -101,6 +110,21 @@ export function createProgram(): Command {
|
||||
};
|
||||
|
||||
const fetchSingleResource = async (resource: string, nameOrId: string): Promise<unknown> => {
|
||||
const projectName = program.opts().project as string | undefined;
|
||||
|
||||
// Prompts: resolve within project scope (or global-only without --project)
|
||||
if (resource === 'prompts' || resource === 'promptrequests') {
|
||||
const scope = projectName
|
||||
? `?project=${encodeURIComponent(projectName)}`
|
||||
: '?scope=global';
|
||||
const items = await client.get<Array<Record<string, unknown>>>(`/api/v1/${resource}${scope}`);
|
||||
const match = items.find((item) => item.name === nameOrId);
|
||||
if (!match) {
|
||||
throw new Error(`${resource.replace(/s$/, '')} '${nameOrId}' not found${projectName ? ` in project '${projectName}'` : ' (global scope). Use --project to specify a project'}`);
|
||||
}
|
||||
return client.get(`/api/v1/${resource}/${match.id as string}`);
|
||||
}
|
||||
|
||||
let id: string;
|
||||
try {
|
||||
id = await resolveNameOrId(client, resource, nameOrId);
|
||||
@@ -113,6 +137,7 @@ export function createProgram(): Command {
|
||||
program.addCommand(createGetCommand({
|
||||
fetchResource,
|
||||
log: (...args) => console.log(...args),
|
||||
getProject: () => program.opts().project as string | undefined,
|
||||
}));
|
||||
|
||||
program.addCommand(createDescribeCommand({
|
||||
|
||||
Reference in New Issue
Block a user