#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import {
searchPackages,
getPackageDetails,
getCategories,
getFeaturedPackages,
type TypstPackage,
type PackageDetails,
} from './scraper.js';
// Define the tools available in this MCP server
const TOOLS: Tool[] = [
{
name: 'search_packages',
description: 'Search for Typst packages in the Typst Universe. You can search by query text, filter by category, and specify the kind (packages or templates).',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query text to find packages (e.g., "math", "diagram", "table")',
},
kind: {
type: 'string',
enum: ['packages', 'templates'],
description: 'Type of items to search for. Defaults to "packages".',
default: 'packages',
},
category: {
type: 'string',
description: 'Filter by category (e.g., "visualization", "math", "text")',
},
limit: {
type: 'number',
description: 'Maximum number of results to return. Defaults to 20.',
default: 20,
},
},
},
},
{
name: 'get_package_details',
description: 'Get detailed information about a specific Typst package, including its description, authors, categories, repository link, import code, and version history.',
inputSchema: {
type: 'object',
properties: {
packageName: {
type: 'string',
description: 'The exact name of the package (e.g., "cetz", "polylux", "fletcher")',
},
},
required: ['packageName'],
},
},
{
name: 'list_categories',
description: 'List all available package categories in Typst Universe.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_featured_packages',
description: 'Get a list of featured/popular packages from Typst Universe.',
inputSchema: {
type: 'object',
properties: {},
},
},
];
// Zod schemas for validation
const SearchPackagesSchema = z.object({
query: z.string().optional(),
kind: z.enum(['packages', 'templates']).optional().default('packages'),
category: z.string().optional(),
limit: z.number().optional().default(20),
});
const GetPackageDetailsSchema = z.object({
packageName: z.string().min(1, 'Package name is required'),
});
/**
* Formats a package for display
*/
function formatPackage(pkg: TypstPackage): string {
return `📦 ${pkg.name} v${pkg.version}\n ${pkg.description}\n URL: ${pkg.url}`;
}
/**
* Formats package details for display
*/
function formatPackageDetails(details: PackageDetails): string {
const lines = [
`📦 ${details.name} v${details.version}`,
``,
`📝 Description: ${details.description}`,
``,
`📥 Import: ${details.importCode}`,
``,
];
if (details.authors.length > 0) {
lines.push(`👤 Authors: ${details.authors.join(', ')}`);
}
if (details.categories.length > 0) {
lines.push(`🏷️ Categories: ${details.categories.join(', ')}`);
}
if (details.repository) {
lines.push(`📂 Repository: ${details.repository}`);
}
if (details.homepage) {
lines.push(`🌐 Homepage: ${details.homepage}`);
}
lines.push(`🔗 URL: ${details.url}`);
if (details.versionHistory.length > 0) {
lines.push(``, `📜 Recent Versions: ${details.versionHistory.slice(0, 5).join(', ')}`);
}
return lines.join('\n');
}
/**
* Main server class
*/
class TypstUniverseServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: 'typst-universe-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
// Handle list tools request
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'search_packages': {
const validatedArgs = SearchPackagesSchema.parse(args);
const packages = await searchPackages({
query: validatedArgs.query,
kind: validatedArgs.kind,
category: validatedArgs.category,
limit: validatedArgs.limit,
});
if (packages.length === 0) {
return {
content: [
{
type: 'text',
text: 'No packages found matching your search criteria.',
},
],
};
}
const formattedResults = packages.map(formatPackage).join('\n\n');
return {
content: [
{
type: 'text',
text: `Found ${packages.length} package(s):\n\n${formattedResults}`,
},
],
};
}
case 'get_package_details': {
const validatedArgs = GetPackageDetailsSchema.parse(args);
const details = await getPackageDetails(validatedArgs.packageName);
if (!details) {
return {
content: [
{
type: 'text',
text: `Package "${validatedArgs.packageName}" not found. Please check the package name and try again.`,
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: formatPackageDetails(details),
},
],
};
}
case 'list_categories': {
const categories = await getCategories();
if (categories.length === 0) {
return {
content: [
{
type: 'text',
text: 'No categories found.',
},
],
};
}
return {
content: [
{
type: 'text',
text: `Available categories (${categories.length}):\n\n${categories.map(c => `• ${c}`).join('\n')}`,
},
],
};
}
case 'get_featured_packages': {
const packages = await getFeaturedPackages();
if (packages.length === 0) {
return {
content: [
{
type: 'text',
text: 'No featured packages found.',
},
],
};
}
const formattedResults = packages.map(formatPackage).join('\n\n');
return {
content: [
{
type: 'text',
text: `Featured packages (${packages.length}):\n\n${formattedResults}`,
},
],
};
}
default:
return {
content: [
{
type: 'text',
text: `Unknown tool: ${name}`,
},
],
isError: true,
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error executing tool "${name}": ${errorMessage}`,
},
],
isError: true,
};
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Typst Universe MCP server running on stdio');
}
}
// Start the server
const server = new TypstUniverseServer();
server.run().catch(console.error);