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:
@@ -2,3 +2,4 @@ export * from './types/index.js';
|
|||||||
export * from './validation/index.js';
|
export * from './validation/index.js';
|
||||||
export * from './constants/index.js';
|
export * from './constants/index.js';
|
||||||
export * from './utils/index.js';
|
export * from './utils/index.js';
|
||||||
|
export * from './profiles/index.js';
|
||||||
|
|||||||
5
src/shared/src/profiles/index.ts
Normal file
5
src/shared/src/profiles/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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';
|
||||||
67
src/shared/src/profiles/registry.ts
Normal file
67
src/shared/src/profiles/registry.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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();
|
||||||
15
src/shared/src/profiles/templates/fetch.ts
Normal file
15
src/shared/src/profiles/templates/fetch.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
16
src/shared/src/profiles/templates/filesystem.ts
Normal file
16
src/shared/src/profiles/templates/filesystem.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
22
src/shared/src/profiles/templates/github.ts
Normal file
22
src/shared/src/profiles/templates/github.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
6
src/shared/src/profiles/templates/index.ts
Normal file
6
src/shared/src/profiles/templates/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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';
|
||||||
15
src/shared/src/profiles/templates/memory.ts
Normal file
15
src/shared/src/profiles/templates/memory.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
21
src/shared/src/profiles/templates/postgres.ts
Normal file
21
src/shared/src/profiles/templates/postgres.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
28
src/shared/src/profiles/templates/slack.ts
Normal file
28
src/shared/src/profiles/templates/slack.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
35
src/shared/src/profiles/types.ts
Normal file
35
src/shared/src/profiles/types.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
61
src/shared/src/profiles/utils.ts
Normal file
61
src/shared/src/profiles/utils.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
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