feat: mcpctl v0.0.1 — first public release
Some checks are pending
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (push) Waiting to run
CI / build (push) Blocked by required conditions
CI / package (push) Blocked by required conditions

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:
Michal
2026-02-27 17:05:05 +00:00
parent 414a8d3774
commit 69867bd47a
65 changed files with 5710 additions and 695 deletions

View File

@@ -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({