feat: add MCP profiles library with builtin templates
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:
207
src/shared/tests/profiles.test.ts
Normal file
207
src/shared/tests/profiles.test.ts
Normal 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: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user