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:
committed by
GitHub
parent
059408a70a
commit
f3ac595d06
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
109
packages/mcp-server/src/tools/emails/index.ts
Normal file
109
packages/mcp-server/src/tools/emails/index.ts
Normal 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';
|
||||
289
packages/mcp-server/src/tools/emails/kit-emails.service.ts
Normal file
289
packages/mcp-server/src/tools/emails/kit-emails.service.ts
Normal 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}"`);
|
||||
},
|
||||
};
|
||||
}
|
||||
46
packages/mcp-server/src/tools/emails/schema.ts
Normal file
46
packages/mcp-server/src/tools/emails/schema.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user