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

@@ -64,7 +64,7 @@ describe('config claude', () => {
});
});
it('merges with existing .mcp.json', async () => {
it('always merges with existing .mcp.json', async () => {
const outPath = join(tmpDir, '.mcp.json');
writeFileSync(outPath, JSON.stringify({
mcpServers: { 'existing--server': { command: 'echo', args: [] } },
@@ -74,7 +74,7 @@ describe('config claude', () => {
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['claude', '--project', 'proj-1', '-o', outPath, '--merge'], { from: 'user' });
await cmd.parseAsync(['claude', '--project', 'proj-1', '-o', outPath], { from: 'user' });
const written = JSON.parse(readFileSync(outPath, 'utf-8'));
expect(written.mcpServers['existing--server']).toBeDefined();
@@ -85,6 +85,36 @@ describe('config claude', () => {
expect(output.join('\n')).toContain('2 server(s)');
});
it('adds inspect MCP server with --inspect', async () => {
const outPath = join(tmpDir, '.mcp.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['claude', '--inspect', '-o', outPath], { from: 'user' });
const written = JSON.parse(readFileSync(outPath, 'utf-8'));
expect(written.mcpServers['mcpctl-inspect']).toEqual({
command: 'mcpctl',
args: ['console', '--inspect', '--stdin-mcp'],
});
expect(output.join('\n')).toContain('1 server(s)');
});
it('adds both project and inspect with --project --inspect', async () => {
const outPath = join(tmpDir, '.mcp.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['claude', '--project', 'ha', '--inspect', '-o', outPath], { from: 'user' });
const written = JSON.parse(readFileSync(outPath, 'utf-8'));
expect(written.mcpServers['ha']).toBeDefined();
expect(written.mcpServers['mcpctl-inspect']).toBeDefined();
expect(output.join('\n')).toContain('2 server(s)');
});
it('backward compat: claude-generate still works', async () => {
const outPath = join(tmpDir, '.mcp.json');
const cmd = createConfigCommand(

View File

@@ -41,27 +41,28 @@ describe('get command', () => {
expect(deps.fetchResource).toHaveBeenCalledWith('servers', 'srv-1', undefined);
});
it('outputs apply-compatible JSON format', async () => {
it('outputs apply-compatible JSON format (multi-doc)', async () => {
const deps = makeDeps([{ id: 'srv-1', name: 'slack', createdAt: '2025-01-01', updatedAt: '2025-01-01', version: 1 }]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'servers', '-o', 'json']);
const parsed = JSON.parse(deps.output[0] ?? '');
// Wrapped in resource key, internal fields stripped
expect(parsed).toHaveProperty('servers');
expect(parsed.servers[0].name).toBe('slack');
expect(parsed.servers[0]).not.toHaveProperty('id');
expect(parsed.servers[0]).not.toHaveProperty('createdAt');
expect(parsed.servers[0]).not.toHaveProperty('updatedAt');
expect(parsed.servers[0]).not.toHaveProperty('version');
// Array of documents with kind field, internal fields stripped
expect(Array.isArray(parsed)).toBe(true);
expect(parsed[0].kind).toBe('server');
expect(parsed[0].name).toBe('slack');
expect(parsed[0]).not.toHaveProperty('id');
expect(parsed[0]).not.toHaveProperty('createdAt');
expect(parsed[0]).not.toHaveProperty('updatedAt');
expect(parsed[0]).not.toHaveProperty('version');
});
it('outputs apply-compatible YAML format', async () => {
it('outputs apply-compatible YAML format (multi-doc)', async () => {
const deps = makeDeps([{ id: 'srv-1', name: 'slack', createdAt: '2025-01-01' }]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'servers', '-o', 'yaml']);
const text = deps.output[0];
expect(text).toContain('servers:');
expect(text).toContain('kind: server');
expect(text).toContain('name: slack');
expect(text).not.toContain('id:');
expect(text).not.toContain('createdAt:');

View File

@@ -76,7 +76,7 @@ describe('status command', () => {
const cmd = createStatusCommand(baseDeps());
await cmd.parseAsync(['-o', 'json'], { from: 'user' });
const parsed = JSON.parse(output[0]) as Record<string, unknown>;
expect(parsed['version']).toBe('0.1.0');
expect(parsed['version']).toBe('0.0.1');
expect(parsed['mcplocalReachable']).toBe(true);
expect(parsed['mcpdReachable']).toBe(true);
});