feat: replace profiles with kubernetes-style secrets
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled

Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).

- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-22 18:40:58 +00:00
parent 02254f2aac
commit ca02340a4c
77 changed files with 1014 additions and 1931 deletions

View File

@@ -2,4 +2,3 @@ export * from './types/index.js';
export * from './validation/index.js';
export * from './constants/index.js';
export * from './utils/index.js';
export * from './profiles/index.js';

View File

@@ -1,5 +0,0 @@
export type { ProfileTemplate, ProfileCategory, InstantiatedProfile } from './types.js';
export { profileTemplateSchema, envTemplateEntrySchema } from './types.js';
export { ProfileRegistry, defaultRegistry } from './registry.js';
export { validateTemplate, getMissingEnvVars, instantiateProfile, generateMcpJsonEntry } from './utils.js';
export * from './templates/index.js';

View File

@@ -1,67 +0,0 @@
import type { ProfileTemplate, ProfileCategory } from './types.js';
import { filesystemTemplate } from './templates/filesystem.js';
import { githubTemplate } from './templates/github.js';
import { postgresTemplate } from './templates/postgres.js';
import { slackTemplate } from './templates/slack.js';
import { memoryTemplate } from './templates/memory.js';
import { fetchTemplate } from './templates/fetch.js';
const builtinTemplates: ProfileTemplate[] = [
filesystemTemplate,
githubTemplate,
postgresTemplate,
slackTemplate,
memoryTemplate,
fetchTemplate,
];
export class ProfileRegistry {
private templates = new Map<string, ProfileTemplate>();
constructor(templates: ProfileTemplate[] = builtinTemplates) {
for (const t of templates) {
this.templates.set(t.id, t);
}
}
getAll(): ProfileTemplate[] {
return [...this.templates.values()];
}
getById(id: string): ProfileTemplate | undefined {
return this.templates.get(id);
}
getByCategory(category: ProfileCategory): ProfileTemplate[] {
return this.getAll().filter((t) => t.category === category);
}
getCategories(): ProfileCategory[] {
const cats = new Set<ProfileCategory>();
for (const t of this.templates.values()) {
cats.add(t.category);
}
return [...cats];
}
search(query: string): ProfileTemplate[] {
const q = query.toLowerCase();
return this.getAll().filter(
(t) =>
t.id.includes(q) ||
t.name.includes(q) ||
t.displayName.toLowerCase().includes(q) ||
t.description.toLowerCase().includes(q),
);
}
register(template: ProfileTemplate): void {
this.templates.set(template.id, template);
}
has(id: string): boolean {
return this.templates.has(id);
}
}
export const defaultRegistry = new ProfileRegistry();

View File

@@ -1,15 +0,0 @@
import type { ProfileTemplate } from '../types.js';
export const fetchTemplate: ProfileTemplate = {
id: 'fetch',
name: 'fetch',
displayName: 'Fetch',
description: 'Fetch and convert web pages to markdown for reading and analysis',
category: 'utility',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-fetch'],
requiredEnvVars: [],
optionalEnvVars: [],
setupInstructions: 'No configuration required. Fetches web content and converts HTML to markdown.',
documentationUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/fetch',
};

View File

@@ -1,16 +0,0 @@
import type { ProfileTemplate } from '../types.js';
export const filesystemTemplate: ProfileTemplate = {
id: 'filesystem',
name: 'filesystem',
displayName: 'Filesystem',
description: 'Provides read/write access to local filesystem directories',
category: 'filesystem',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem'],
requiredEnvVars: [],
optionalEnvVars: [],
setupInstructions:
'Append allowed directory paths as additional args. Example: npx -y @modelcontextprotocol/server-filesystem /home/user/docs',
documentationUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem',
};

View File

@@ -1,22 +0,0 @@
import type { ProfileTemplate } from '../types.js';
export const githubTemplate: ProfileTemplate = {
id: 'github',
name: 'github',
displayName: 'GitHub',
description: 'Interact with GitHub repositories, issues, pull requests, and more',
category: 'integration',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-github'],
requiredEnvVars: [
{
name: 'GITHUB_PERSONAL_ACCESS_TOKEN',
description: 'GitHub personal access token with repo scope',
isSecret: true,
setupUrl: 'https://github.com/settings/tokens',
},
],
optionalEnvVars: [],
setupInstructions: 'Create a personal access token at GitHub Settings > Developer settings > Personal access tokens.',
documentationUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/github',
};

View File

@@ -1,6 +0,0 @@
export { filesystemTemplate } from './filesystem.js';
export { githubTemplate } from './github.js';
export { postgresTemplate } from './postgres.js';
export { slackTemplate } from './slack.js';
export { memoryTemplate } from './memory.js';
export { fetchTemplate } from './fetch.js';

View File

@@ -1,15 +0,0 @@
import type { ProfileTemplate } from '../types.js';
export const memoryTemplate: ProfileTemplate = {
id: 'memory',
name: 'memory',
displayName: 'Memory',
description: 'Persistent knowledge graph memory for storing and retrieving entities and relations',
category: 'utility',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-memory'],
requiredEnvVars: [],
optionalEnvVars: [],
setupInstructions: 'No configuration required. Memory is stored locally in a JSON knowledge graph file.',
documentationUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/memory',
};

View File

@@ -1,21 +0,0 @@
import type { ProfileTemplate } from '../types.js';
export const postgresTemplate: ProfileTemplate = {
id: 'postgres',
name: 'postgres',
displayName: 'PostgreSQL',
description: 'Query and inspect PostgreSQL databases with read-only access',
category: 'database',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-postgres'],
requiredEnvVars: [
{
name: 'DATABASE_URL',
description: 'PostgreSQL connection string (e.g., postgresql://user:pass@localhost:5432/dbname)',
isSecret: true,
},
],
optionalEnvVars: [],
setupInstructions: 'Provide a PostgreSQL connection string. The server provides read-only query access by default.',
documentationUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/postgres',
};

View File

@@ -1,28 +0,0 @@
import type { ProfileTemplate } from '../types.js';
export const slackTemplate: ProfileTemplate = {
id: 'slack',
name: 'slack',
displayName: 'Slack',
description: 'Read and send Slack messages, manage channels, and search workspace content',
category: 'integration',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-slack'],
requiredEnvVars: [
{
name: 'SLACK_BOT_TOKEN',
description: 'Slack Bot User OAuth Token (starts with xoxb-)',
isSecret: true,
setupUrl: 'https://api.slack.com/apps',
},
{
name: 'SLACK_TEAM_ID',
description: 'Slack workspace/team ID',
isSecret: false,
},
],
optionalEnvVars: [],
setupInstructions:
'Create a Slack App at api.slack.com/apps, install it to your workspace, and copy the Bot User OAuth Token.',
documentationUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/slack',
};

View File

@@ -1,35 +0,0 @@
import { z } from 'zod';
export const envTemplateEntrySchema = z.object({
name: z.string().min(1),
description: z.string(),
isSecret: z.boolean(),
setupUrl: z.string().url().optional(),
defaultValue: z.string().optional(),
});
export const profileTemplateSchema = z.object({
id: z.string().min(1).regex(/^[a-z0-9-]+$/, 'ID must be lowercase alphanumeric with hyphens'),
name: z.string().min(1),
displayName: z.string().min(1),
description: z.string().min(1),
category: z.enum(['filesystem', 'database', 'integration', 'ai', 'utility', 'development']),
command: z.string().min(1),
args: z.array(z.string()),
requiredEnvVars: z.array(envTemplateEntrySchema).default([]),
optionalEnvVars: z.array(envTemplateEntrySchema).default([]),
setupInstructions: z.string().optional(),
documentationUrl: z.string().url().optional(),
});
export type ProfileTemplate = z.infer<typeof profileTemplateSchema>;
export type ProfileCategory = ProfileTemplate['category'];
export interface InstantiatedProfile {
name: string;
templateId: string;
command: string;
args: string[];
env: Record<string, string>;
}

View File

@@ -1,61 +0,0 @@
import { profileTemplateSchema } from './types.js';
import type { ProfileTemplate, InstantiatedProfile } from './types.js';
export function validateTemplate(template: unknown): { success: true; data: ProfileTemplate } | { success: false; errors: string[] } {
const result = profileTemplateSchema.safeParse(template);
if (result.success) {
return { success: true, data: result.data };
}
return {
success: false,
errors: result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`),
};
}
export function getMissingEnvVars(template: ProfileTemplate, envValues: Record<string, string>): string[] {
return template.requiredEnvVars
.filter((e) => !envValues[e.name] && e.defaultValue === undefined)
.map((e) => e.name);
}
export function instantiateProfile(
template: ProfileTemplate,
envValues: Record<string, string>,
): InstantiatedProfile {
const missing = getMissingEnvVars(template, envValues);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
const env: Record<string, string> = {};
for (const entry of template.requiredEnvVars) {
const value = envValues[entry.name] ?? entry.defaultValue;
if (value !== undefined) {
env[entry.name] = value;
}
}
for (const entry of template.optionalEnvVars) {
const value = envValues[entry.name] ?? entry.defaultValue;
if (value !== undefined) {
env[entry.name] = value;
}
}
return {
name: template.name,
templateId: template.id,
command: template.command,
args: [...template.args],
env,
};
}
export function generateMcpJsonEntry(profile: InstantiatedProfile): Record<string, unknown> {
return {
[profile.name]: {
command: profile.command,
args: profile.args,
env: profile.env,
},
};
}

View File

@@ -6,29 +6,19 @@ export interface McpServerConfig {
type: string;
command: string;
args: string[];
envTemplate: EnvTemplateEntry[];
env: EnvEntry[];
setupGuide?: string;
}
export interface EnvTemplateEntry {
export interface EnvEntry {
name: string;
description: string;
isSecret: boolean;
setupUrl?: string;
defaultValue?: string;
}
export interface McpProfile {
name: string;
serverId: string;
config: Record<string, unknown>;
filterRules?: Record<string, unknown>;
value?: string;
valueFrom?: { secretRef: { name: string; key: string } };
}
export interface McpProject {
name: string;
description?: string;
profileIds: string[];
}
// Service interfaces for dependency injection