feat: add MCP profiles library with builtin templates
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

Pre-configured profile templates for filesystem, GitHub, PostgreSQL,
Slack, memory, and fetch MCP servers. Includes registry, validation,
instantiation utilities, and .mcp.json generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-21 05:25:56 +00:00
parent 6161686441
commit d0a224e839
13 changed files with 499 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
import { describe, it, expect } from 'vitest';
import {
ProfileRegistry,
defaultRegistry,
profileTemplateSchema,
validateTemplate,
getMissingEnvVars,
instantiateProfile,
generateMcpJsonEntry,
filesystemTemplate,
githubTemplate,
postgresTemplate,
slackTemplate,
memoryTemplate,
fetchTemplate,
} from '../src/profiles/index.js';
const allTemplates = [
filesystemTemplate,
githubTemplate,
postgresTemplate,
slackTemplate,
memoryTemplate,
fetchTemplate,
];
describe('ProfileTemplate schema', () => {
it.each(allTemplates)('validates $id template', (template) => {
const result = profileTemplateSchema.safeParse(template);
expect(result.success).toBe(true);
});
it('rejects template with missing required fields', () => {
const result = profileTemplateSchema.safeParse({ id: 'x' });
expect(result.success).toBe(false);
});
it('rejects template with invalid id format', () => {
const result = profileTemplateSchema.safeParse({
...filesystemTemplate,
id: 'Invalid ID!',
});
expect(result.success).toBe(false);
});
it('rejects template with invalid category', () => {
const result = profileTemplateSchema.safeParse({
...filesystemTemplate,
category: 'nonexistent',
});
expect(result.success).toBe(false);
});
});
describe('ProfileRegistry', () => {
it('default registry contains all builtin templates', () => {
const ids = defaultRegistry.getAll().map((t) => t.id);
expect(ids).toContain('filesystem');
expect(ids).toContain('github');
expect(ids).toContain('postgres');
expect(ids).toContain('slack');
expect(ids).toContain('memory');
expect(ids).toContain('fetch');
});
it('has no duplicate IDs', () => {
const ids = defaultRegistry.getAll().map((t) => t.id);
expect(new Set(ids).size).toBe(ids.length);
});
it('getById returns correct template', () => {
expect(defaultRegistry.getById('github')).toBe(githubTemplate);
expect(defaultRegistry.getById('nonexistent')).toBeUndefined();
});
it('getByCategory filters correctly', () => {
const integrations = defaultRegistry.getByCategory('integration');
expect(integrations.map((t) => t.id)).toEqual(
expect.arrayContaining(['github', 'slack']),
);
for (const t of integrations) {
expect(t.category).toBe('integration');
}
});
it('getCategories returns unique categories', () => {
const cats = defaultRegistry.getCategories();
expect(cats.length).toBeGreaterThan(0);
expect(new Set(cats).size).toBe(cats.length);
});
it('search finds by name', () => {
const results = defaultRegistry.search('git');
expect(results.some((t) => t.id === 'github')).toBe(true);
});
it('search finds by description', () => {
const results = defaultRegistry.search('knowledge graph');
expect(results.some((t) => t.id === 'memory')).toBe(true);
});
it('search returns empty for no match', () => {
expect(defaultRegistry.search('zzzznotfound')).toEqual([]);
});
it('register adds a custom template', () => {
const registry = new ProfileRegistry([]);
registry.register(filesystemTemplate);
expect(registry.has('filesystem')).toBe(true);
expect(registry.getAll()).toHaveLength(1);
});
});
describe('validateTemplate', () => {
it('returns success for valid template', () => {
const result = validateTemplate(filesystemTemplate);
expect(result.success).toBe(true);
});
it('returns errors for invalid template', () => {
const result = validateTemplate({ id: '' });
expect(result.success).toBe(false);
if (!result.success) {
expect(result.errors.length).toBeGreaterThan(0);
}
});
});
describe('getMissingEnvVars', () => {
it('returns empty for template with no required env vars', () => {
expect(getMissingEnvVars(filesystemTemplate, {})).toEqual([]);
});
it('returns missing vars for github template', () => {
const missing = getMissingEnvVars(githubTemplate, {});
expect(missing).toContain('GITHUB_PERSONAL_ACCESS_TOKEN');
});
it('returns empty when all vars provided', () => {
const missing = getMissingEnvVars(githubTemplate, {
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_xxx',
});
expect(missing).toEqual([]);
});
});
describe('instantiateProfile', () => {
it('creates profile from template without env vars', () => {
const profile = instantiateProfile(filesystemTemplate, {});
expect(profile.name).toBe('filesystem');
expect(profile.templateId).toBe('filesystem');
expect(profile.command).toBe('npx');
expect(profile.args).toEqual(['-y', '@modelcontextprotocol/server-filesystem']);
expect(profile.env).toEqual({});
});
it('creates profile with env vars', () => {
const profile = instantiateProfile(githubTemplate, {
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_test123',
});
expect(profile.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe('ghp_test123');
});
it('throws on missing required env vars', () => {
expect(() => instantiateProfile(githubTemplate, {})).toThrow(
'Missing required environment variables',
);
});
it('includes optional env vars when provided', () => {
const profile = instantiateProfile(filesystemTemplate, {
SOME_OPTIONAL: 'value',
});
// Optional vars not in template are not included
expect(profile.env).toEqual({});
});
});
describe('generateMcpJsonEntry', () => {
it('generates valid .mcp.json entry', () => {
const profile = instantiateProfile(githubTemplate, {
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_abc',
});
const entry = generateMcpJsonEntry(profile);
expect(entry).toEqual({
github: {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-github'],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_abc',
},
},
});
});
it('generates entry with empty env for no-env template', () => {
const profile = instantiateProfile(memoryTemplate, {});
const entry = generateMcpJsonEntry(profile);
expect(entry).toEqual({
memory: {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-memory'],
env: {},
},
});
});
});