MCP Server 2.0 (#452)

* MCP Server 2.0

- Updated application version from 2.23.14 to 2.24.0 in package.json.
- MCP Server improved with new features
- Migrated functionality from Dev Tools to MCP Server
- Improved getMonitoringProvider not to crash application when misconfigured
This commit is contained in:
Giancarlo Buomprisco
2026-02-11 20:42:01 +01:00
committed by GitHub
parent 059408a70a
commit f3ac595d06
123 changed files with 17803 additions and 5265 deletions

View File

@@ -0,0 +1,292 @@
import { describe, expect, it } from 'vitest';
import {
type KitEmailsDeps,
createKitEmailsService,
} from '../kit-emails.service';
function createDeps(
files: Record<string, string>,
directories: string[],
): KitEmailsDeps {
const store = { ...files };
const dirSet = new Set(directories);
return {
rootPath: '/repo',
async readFile(filePath: string) {
if (!(filePath in store)) {
const error = new Error(
`ENOENT: no such file: ${filePath}`,
) as NodeJS.ErrnoException;
error.code = 'ENOENT';
throw error;
}
return store[filePath]!;
},
async readdir(dirPath: string) {
if (!dirSet.has(dirPath)) {
return [];
}
return Object.keys(store)
.filter((p) => {
const parent = p.substring(0, p.lastIndexOf('/'));
return parent === dirPath;
})
.map((p) => p.substring(p.lastIndexOf('/') + 1));
},
async fileExists(filePath: string) {
return filePath in store || dirSet.has(filePath);
},
async renderReactEmail() {
return null;
},
};
}
const REACT_DIR = '/repo/packages/email-templates/src/emails';
const SUPABASE_DIR = '/repo/apps/web/supabase/templates';
describe('KitEmailsService.list', () => {
it('discovers React Email templates with -email suffix in id', async () => {
const deps = createDeps(
{
[`${REACT_DIR}/invite.email.tsx`]:
'export function renderInviteEmail() {}',
[`${REACT_DIR}/otp.email.tsx`]: 'export function renderOtpEmail() {}',
},
[REACT_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.list();
expect(result.templates).toHaveLength(2);
expect(result.categories).toEqual(['transactional']);
expect(result.total).toBe(2);
const invite = result.templates.find((t) => t.id === 'invite-email');
expect(invite).toBeDefined();
expect(invite!.name).toBe('Invite');
expect(invite!.category).toBe('transactional');
expect(invite!.file).toBe(
'packages/email-templates/src/emails/invite.email.tsx',
);
const otp = result.templates.find((t) => t.id === 'otp-email');
expect(otp).toBeDefined();
expect(otp!.name).toBe('Otp');
});
it('discovers Supabase Auth HTML templates', async () => {
const deps = createDeps(
{
[`${SUPABASE_DIR}/magic-link.html`]: '<html>magic</html>',
[`${SUPABASE_DIR}/reset-password.html`]: '<html>reset</html>',
},
[SUPABASE_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.list();
expect(result.templates).toHaveLength(2);
expect(result.categories).toEqual(['supabase-auth']);
const magicLink = result.templates.find((t) => t.id === 'magic-link');
expect(magicLink).toBeDefined();
expect(magicLink!.name).toBe('Magic Link');
expect(magicLink!.category).toBe('supabase-auth');
expect(magicLink!.file).toBe('apps/web/supabase/templates/magic-link.html');
});
it('discovers both types and returns sorted categories', async () => {
const deps = createDeps(
{
[`${REACT_DIR}/invite.email.tsx`]:
'export function renderInviteEmail() {}',
[`${SUPABASE_DIR}/confirm-email.html`]: '<html>confirm</html>',
},
[REACT_DIR, SUPABASE_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.list();
expect(result.templates).toHaveLength(2);
expect(result.categories).toEqual(['supabase-auth', 'transactional']);
expect(result.total).toBe(2);
});
it('handles empty directories gracefully', async () => {
const deps = createDeps({}, []);
const service = createKitEmailsService(deps);
const result = await service.list();
expect(result.templates).toEqual([]);
expect(result.categories).toEqual([]);
expect(result.total).toBe(0);
});
it('ignores non-email files in the directories', async () => {
const deps = createDeps(
{
[`${REACT_DIR}/invite.email.tsx`]:
'export function renderInviteEmail() {}',
[`${REACT_DIR}/utils.ts`]: 'export const helper = true;',
[`${REACT_DIR}/README.md`]: '# readme',
[`${SUPABASE_DIR}/magic-link.html`]: '<html>magic</html>',
[`${SUPABASE_DIR}/config.json`]: '{}',
},
[REACT_DIR, SUPABASE_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.list();
expect(result.templates).toHaveLength(2);
});
it('avoids id collision between React otp-email and Supabase otp', async () => {
const deps = createDeps(
{
[`${REACT_DIR}/otp.email.tsx`]: 'export function renderOtpEmail() {}',
[`${SUPABASE_DIR}/otp.html`]: '<html>otp</html>',
},
[REACT_DIR, SUPABASE_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.list();
const ids = result.templates.map((t) => t.id);
expect(ids).toContain('otp-email');
expect(ids).toContain('otp');
expect(new Set(ids).size).toBe(ids.length);
});
});
describe('KitEmailsService.read', () => {
it('reads a React Email template and extracts props', async () => {
const source = `
interface Props {
teamName: string;
teamLogo?: string;
inviter: string | undefined;
invitedUserEmail: string;
link: string;
productName: string;
language?: string;
}
export async function renderInviteEmail(props: Props) {}
`;
const deps = createDeps(
{
[`${REACT_DIR}/invite.email.tsx`]: source,
},
[REACT_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.read({ id: 'invite-email' });
expect(result.id).toBe('invite-email');
expect(result.name).toBe('Invite');
expect(result.category).toBe('transactional');
expect(result.source).toBe(source);
expect(result.props).toEqual([
{ name: 'teamName', type: 'string', required: true },
{ name: 'teamLogo', type: 'string', required: false },
{ name: 'inviter', type: 'string | undefined', required: true },
{ name: 'invitedUserEmail', type: 'string', required: true },
{ name: 'link', type: 'string', required: true },
{ name: 'productName', type: 'string', required: true },
{ name: 'language', type: 'string', required: false },
]);
});
it('reads a Supabase HTML template with empty props', async () => {
const html = '<html><body>Magic Link</body></html>';
const deps = createDeps(
{
[`${SUPABASE_DIR}/magic-link.html`]: html,
},
[SUPABASE_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.read({ id: 'magic-link' });
expect(result.id).toBe('magic-link');
expect(result.source).toBe(html);
expect(result.props).toEqual([]);
});
it('throws for unknown template id', async () => {
const deps = createDeps({}, []);
const service = createKitEmailsService(deps);
await expect(service.read({ id: 'nonexistent' })).rejects.toThrow(
'Email template not found: "nonexistent"',
);
});
it('handles templates without Props interface', async () => {
const source =
'export async function renderSimpleEmail() { return { html: "" }; }';
const deps = createDeps(
{
[`${REACT_DIR}/simple.email.tsx`]: source,
},
[REACT_DIR],
);
const service = createKitEmailsService(deps);
const result = await service.read({ id: 'simple-email' });
expect(result.props).toEqual([]);
});
});
describe('Path safety', () => {
it('rejects ids with path traversal', async () => {
const deps = createDeps({}, []);
const service = createKitEmailsService(deps);
await expect(service.read({ id: '../etc/passwd' })).rejects.toThrow(
'Template id must not contain ".."',
);
});
it('rejects ids with forward slashes', async () => {
const deps = createDeps({}, []);
const service = createKitEmailsService(deps);
await expect(service.read({ id: 'foo/bar' })).rejects.toThrow(
'Template id must not include path separators',
);
});
it('rejects ids with backslashes', async () => {
const deps = createDeps({}, []);
const service = createKitEmailsService(deps);
await expect(service.read({ id: 'foo\\bar' })).rejects.toThrow(
'Template id must not include path separators',
);
});
});

View File

@@ -0,0 +1,109 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
type KitEmailsDeps,
createKitEmailsDeps,
createKitEmailsService,
} from './kit-emails.service';
import {
KitEmailsListInputSchema,
KitEmailsListOutputSchema,
KitEmailsReadInputSchema,
KitEmailsReadOutputSchema,
} from './schema';
type TextContent = {
type: 'text';
text: string;
};
export function registerKitEmailTemplatesTools(server: McpServer) {
const service = createKitEmailsService(createKitEmailsDeps());
server.registerTool(
'kit_email_templates_list',
{
description:
'List project email template files (React Email + Supabase auth templates), not received inbox messages',
inputSchema: KitEmailsListInputSchema,
outputSchema: KitEmailsListOutputSchema,
},
async () => {
try {
const result = await service.list();
return {
structuredContent: result,
content: buildTextContent(JSON.stringify(result)),
};
} catch (error) {
return buildErrorResponse('kit_email_templates_list', error);
}
},
);
server.registerTool(
'kit_email_templates_read',
{
description:
'Read a project email template source file by template id, with extracted props and optional rendered HTML sample',
inputSchema: KitEmailsReadInputSchema,
outputSchema: KitEmailsReadOutputSchema,
},
async (input) => {
try {
const { id } = KitEmailsReadInputSchema.parse(input);
const result = await service.read({ id });
const content: TextContent[] = [];
// Return source, props, and metadata
const { renderedHtml, ...metadata } = result;
content.push({ type: 'text', text: JSON.stringify(metadata) });
// Include rendered HTML as a separate content block
if (renderedHtml) {
content.push({
type: 'text',
text: `\n\n--- Rendered HTML ---\n\n${renderedHtml}`,
});
}
return {
structuredContent: result,
content,
};
} catch (error) {
return buildErrorResponse('kit_email_templates_read', error);
}
},
);
}
export const registerKitEmailsTools = registerKitEmailTemplatesTools;
function buildErrorResponse(tool: string, error: unknown) {
const message = `${tool} failed: ${toErrorMessage(error)}`;
return {
isError: true,
content: buildTextContent(message),
};
}
function toErrorMessage(error: unknown) {
if (error instanceof Error) {
return error.message;
}
return 'Unknown error';
}
function buildTextContent(text: string): TextContent[] {
return [{ type: 'text', text }];
}
export { createKitEmailsService, createKitEmailsDeps };
export type { KitEmailsDeps };
export type { KitEmailsListOutput, KitEmailsReadOutput } from './schema';

View File

@@ -0,0 +1,289 @@
import path from 'node:path';
import { EMAIL_TEMPLATE_RENDERERS } from '@kit/email-templates/registry';
import type { KitEmailsListOutput, KitEmailsReadOutput } from './schema';
export interface KitEmailsDeps {
rootPath: string;
readFile(filePath: string): Promise<string>;
readdir(dirPath: string): Promise<string[]>;
fileExists(filePath: string): Promise<boolean>;
renderReactEmail(
sampleProps: Record<string, string>,
templateId: string,
): Promise<string | null>;
}
interface EmailTemplate {
id: string;
name: string;
category: string;
file: string;
description: string;
}
export function createKitEmailsService(deps: KitEmailsDeps) {
return new KitEmailsService(deps);
}
export class KitEmailsService {
constructor(private readonly deps: KitEmailsDeps) {}
async list(): Promise<KitEmailsListOutput> {
const templates: EmailTemplate[] = [];
const reactTemplates = await this.discoverReactEmailTemplates();
const supabaseTemplates = await this.discoverSupabaseAuthTemplates();
templates.push(...reactTemplates, ...supabaseTemplates);
const categories = [...new Set(templates.map((t) => t.category))].sort();
return {
templates,
categories,
total: templates.length,
};
}
async read(input: { id: string }): Promise<KitEmailsReadOutput> {
assertSafeId(input.id);
const { templates } = await this.list();
const template = templates.find((t) => t.id === input.id);
if (!template) {
throw new Error(`Email template not found: "${input.id}"`);
}
const absolutePath = path.resolve(this.deps.rootPath, template.file);
ensureInsideRoot(absolutePath, this.deps.rootPath, input.id);
const source = await this.deps.readFile(absolutePath);
const isReactEmail = absolutePath.includes('packages/email-templates');
const props = isReactEmail ? extractPropsFromSource(source) : [];
let renderedHtml: string | null = null;
if (isReactEmail) {
const sampleProps = buildSampleProps(props);
renderedHtml = await this.deps.renderReactEmail(sampleProps, template.id);
}
return {
id: template.id,
name: template.name,
category: template.category,
file: template.file,
source,
props,
renderedHtml,
};
}
private async discoverReactEmailTemplates(): Promise<EmailTemplate[]> {
const dir = path.join('packages', 'email-templates', 'src', 'emails');
const absoluteDir = path.resolve(this.deps.rootPath, dir);
if (!(await this.deps.fileExists(absoluteDir))) {
return [];
}
const files = await this.deps.readdir(absoluteDir);
const templates: EmailTemplate[] = [];
for (const file of files) {
if (!file.endsWith('.email.tsx')) {
continue;
}
const stem = file.replace(/\.email\.tsx$/, '');
const id = `${stem}-email`;
const name = humanize(stem);
templates.push({
id,
name,
category: 'transactional',
file: path.join(dir, file),
description: `${name} transactional email template`,
});
}
return templates.sort((a, b) => a.id.localeCompare(b.id));
}
private async discoverSupabaseAuthTemplates(): Promise<EmailTemplate[]> {
const dir = path.join('apps', 'web', 'supabase', 'templates');
const absoluteDir = path.resolve(this.deps.rootPath, dir);
if (!(await this.deps.fileExists(absoluteDir))) {
return [];
}
const files = await this.deps.readdir(absoluteDir);
const templates: EmailTemplate[] = [];
for (const file of files) {
if (!file.endsWith('.html')) {
continue;
}
const id = file.replace(/\.html$/, '');
const name = humanize(id);
templates.push({
id,
name,
category: 'supabase-auth',
file: path.join(dir, file),
description: `${name} Supabase auth email template`,
});
}
return templates.sort((a, b) => a.id.localeCompare(b.id));
}
}
function humanize(kebab: string): string {
return kebab
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
function extractPropsFromSource(
source: string,
): Array<{ name: string; type: string; required: boolean }> {
const interfaceMatch = source.match(/interface\s+Props\s*\{([^}]*)\}/);
if (!interfaceMatch?.[1]) {
return [];
}
const body = interfaceMatch[1];
const props: Array<{ name: string; type: string; required: boolean }> = [];
const propRegex = /(\w+)(\??):\s*([^;\n]+)/g;
let match: RegExpExecArray | null;
while ((match = propRegex.exec(body)) !== null) {
const name = match[1]!;
const optional = match[2] === '?';
const type = match[3]!.trim();
props.push({
name,
type,
required: !optional,
});
}
return props;
}
function ensureInsideRoot(resolved: string, root: string, input: string) {
const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
if (!resolved.startsWith(normalizedRoot) && resolved !== root) {
throw new Error(
`Invalid path: "${input}" resolves outside the project root`,
);
}
return resolved;
}
function buildSampleProps(
props: Array<{ name: string; type: string; required: boolean }>,
): Record<string, string> {
const sample: Record<string, string> = {};
for (const prop of props) {
if (prop.name === 'language') continue;
sample[prop.name] = SAMPLE_PROP_VALUES[prop.name] ?? `Sample ${prop.name}`;
}
return sample;
}
const SAMPLE_PROP_VALUES: Record<string, string> = {
productName: 'Makerkit',
teamName: 'Acme Team',
inviter: 'John Doe',
invitedUserEmail: 'user@example.com',
link: 'https://example.com/action',
otp: '123456',
email: 'user@example.com',
name: 'Jane Doe',
userName: 'Jane Doe',
};
function assertSafeId(id: string) {
if (id.includes('..')) {
throw new Error('Template id must not contain ".."');
}
if (id.includes('/') || id.includes('\\')) {
throw new Error('Template id must not include path separators');
}
}
export function createKitEmailsDeps(rootPath = process.cwd()): KitEmailsDeps {
return {
rootPath,
async readFile(filePath: string) {
const fs = await import('node:fs/promises');
return fs.readFile(filePath, 'utf8');
},
async readdir(dirPath: string) {
const fs = await import('node:fs/promises');
return fs.readdir(dirPath);
},
async fileExists(filePath: string) {
const fs = await import('node:fs/promises');
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
},
async renderReactEmail(
sampleProps: Record<string, string>,
templateId?: string,
) {
const renderFromRegistry =
typeof templateId === 'string'
? EMAIL_TEMPLATE_RENDERERS?.[templateId]
: undefined;
if (typeof renderFromRegistry === 'function') {
const result = await renderFromRegistry(sampleProps);
if (typeof result === 'string') {
return result;
}
if (
typeof result === 'object' &&
result !== null &&
'html' in result &&
typeof (result as { html: unknown }).html === 'string'
) {
return (result as { html: string }).html;
}
return null;
}
throw new Error(`Email template renderer not found: "${templateId}"`);
},
};
}

View File

@@ -0,0 +1,46 @@
import { z } from 'zod/v3';
export const KitEmailsListInputSchema = z.object({});
const EmailTemplateSchema = z.object({
id: z.string(),
name: z.string(),
category: z.string(),
file: z.string(),
description: z.string(),
});
const KitEmailsListSuccessOutputSchema = z.object({
templates: z.array(EmailTemplateSchema),
categories: z.array(z.string()),
total: z.number(),
});
export const KitEmailsListOutputSchema = KitEmailsListSuccessOutputSchema;
export const KitEmailsReadInputSchema = z.object({
id: z.string().min(1),
});
const PropSchema = z.object({
name: z.string(),
type: z.string(),
required: z.boolean(),
});
const KitEmailsReadSuccessOutputSchema = z.object({
id: z.string(),
name: z.string(),
category: z.string(),
file: z.string(),
source: z.string(),
props: z.array(PropSchema),
renderedHtml: z.string().nullable(),
});
export const KitEmailsReadOutputSchema = KitEmailsReadSuccessOutputSchema;
export type KitEmailsListInput = z.infer<typeof KitEmailsListInputSchema>;
export type KitEmailsListOutput = z.infer<typeof KitEmailsListOutputSchema>;
export type KitEmailsReadInput = z.infer<typeof KitEmailsReadInputSchema>;
export type KitEmailsReadOutput = z.infer<typeof KitEmailsReadOutputSchema>;