Sensei MCP
Official
by dojoengine
- sensei-mcp
- src
import fs from 'fs/promises';
import path from 'path';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import Logger from './logger.js';
import { loadFile } from './resources.js';
import {
CallToolResult,
GetPromptResult,
} from '@modelcontextprotocol/sdk/types.js';
import { fileURLToPath } from 'url';
// Get the directory of the current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Configuration
// Use the package directory if it exists, otherwise use the current working directory
export const PROMPTS_DIR = path.join(process.cwd(), 'prompts');
// Try to use the package directory for prompts if it exists
export async function getPromptsDir(): Promise<string> {
const packagePromptsDir = path.join(__dirname, '../../prompts');
try {
await fs.access(packagePromptsDir);
return packagePromptsDir;
} catch (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_error
) {
return PROMPTS_DIR;
}
}
// Resource reference regex pattern (e.g., {{resource:path/to/resource}})
export const RESOURCE_REF_PATTERN = /\{\{resource:(.*?)\}\}/g;
// Variable pattern for prompt files (e.g., {{variable_name}})
export const VARIABLE_PATTERN = /\{\{([a-zA-Z0-9_]+)\}\}/g;
// Input variable pattern (special case for default input)
export const INPUT_VARIABLE_PATTERN = /\{\{input\}\}/g;
// Metadata pattern for prompt files
// Format: ---\nkey: value\n---
export const METADATA_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n/;
/**
* Interface for prompt metadata
*/
export interface PromptMetadata {
description?: string;
registerAsTool?: boolean;
toolName?: string;
name?: string;
role?: string;
registerAsPrompt?: boolean;
}
/**
* Parse metadata from prompt content
*/
export function parseMetadata(content: string): {
metadata: PromptMetadata;
content: string;
} {
const metadata: PromptMetadata = {};
const metadataMatch = content.match(METADATA_PATTERN);
if (metadataMatch) {
const metadataBlock = metadataMatch[1];
// Only process metadata if there's actual content in the block
if (metadataBlock.trim()) {
const lines = metadataBlock.split('\n');
for (const line of lines) {
// Split only on the first colon to preserve colons in values
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim().toLowerCase();
const value = line.substring(colonIndex + 1).trim();
if (key && value) {
switch (key) {
case 'description':
metadata.description = value;
break;
case 'name':
metadata.name = value;
break;
case 'role':
metadata.role = value;
break;
case 'register_as_prompt':
case 'registerasprompt':
metadata.registerAsPrompt = value.toLowerCase() === 'true';
break;
case 'register_as_tool':
case 'registerastool':
metadata.registerAsTool = value.toLowerCase() === 'true';
break;
case 'tool_name':
case 'toolname':
metadata.toolName = value;
break;
}
}
}
}
}
// Remove metadata block from content
return {
metadata,
content: content.substring(metadataMatch[0].length),
};
}
return { metadata, content };
}
/**
* Extract variables from prompt content
*/
export function extractVariables(content: string): string[] {
const variables = new Set<string>();
// Find all variable references (excluding resource references)
const matches = content.matchAll(VARIABLE_PATTERN);
for (const match of matches) {
const variable = match[1];
// Skip resource variables as they're handled separately
if (!variable.startsWith('resource:')) {
variables.add(variable);
}
}
return Array.from(variables);
}
/**
* Generate input schema based on variables in the prompt
*/
export function generateInputSchema(
variables: string[],
): z.ZodObject<Record<string, z.ZodTypeAny>> {
const schemaObj: Record<string, z.ZodTypeAny> = {};
// Add all other variables
for (const variable of variables) {
schemaObj[variable] = z.string().optional();
}
return z.object(schemaObj);
}
/**
* Process prompt content to embed referenced resources
*/
export async function processPromptContent(
content: string,
resourceMap: Map<string, string>,
): Promise<string> {
const span = Logger.span('processPromptContent', {
contentLength: content.length,
});
let processedContent = content;
// Find all resource references
const resourceRefs = [...content.matchAll(RESOURCE_REF_PATTERN)];
Logger.debug(`Found resource references`, { count: resourceRefs.length });
// Process each reference
for (const match of resourceRefs) {
const [fullMatch, resourcePath] = match;
const resourceUri = `file://${resourcePath}`;
const resourceContent = resourceMap.get(resourceUri);
if (!resourceContent) {
Logger.warn(`Resource not found`, {
uri: resourceUri,
path: resourcePath,
});
processedContent = processedContent.replace(
fullMatch,
`[Resource not found: ${resourcePath}]`,
);
} else {
// Directly embed the resource content
Logger.debug(`Embedding resource`, {
uri: resourceUri,
contentLength: resourceContent.length,
});
processedContent = processedContent.replace(fullMatch, resourceContent);
}
}
span.end('success');
return processedContent;
}
/**
* Register a prompt with the server
*/
export function registerPrompt(
server: McpServer,
promptName: string,
processedContent: string,
metadata: PromptMetadata,
): void {
// Create a prompt handler that accepts the required arguments
const promptHandler = (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_args: undefined,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_extra: Record<string, unknown>,
): GetPromptResult => ({
messages: [
{
role: 'user' as const,
content: {
type: 'text' as const,
text: processedContent,
},
},
],
});
// Register as a regular prompt
if (metadata.description) {
// With description
server.prompt(promptName, metadata.description, promptHandler);
} else {
// Without description
server.prompt(promptName, promptHandler);
}
// If registerAsTool is true, also register as a tool
if (metadata.registerAsTool) {
const toolName = metadata.toolName || promptName;
// Extract variables from the prompt content for the tool
const variables = extractVariables(processedContent);
Logger.debug(`Found variables in tool`, {
name: toolName,
variables,
});
// Create a schema object directly
const schemaObj: Record<string, z.ZodTypeAny> = {};
for (const variable of variables) {
schemaObj[variable] = z.string().optional();
}
// Create tool handler that replaces variables and returns the result
const toolHandler = async (
inputs: Record<string, string>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_extra: Record<string, unknown>,
): Promise<CallToolResult> => {
const span = Logger.span('toolExecution', { tool: toolName });
try {
let finalContent = processedContent;
// Replace each variable with its value
for (const variable of variables) {
const value = inputs[variable] || '';
const pattern = new RegExp(`\\{\\{${variable}\\}\\}`, 'g');
finalContent = finalContent.replace(pattern, value);
}
// If there's a generic input and no specific {{input}} variable,
// append it to the end
if (inputs.input && !variables.includes('input')) {
finalContent = `${finalContent}\n\n${inputs.input}`;
}
span.end('success');
return {
content: [
{
type: 'text' as const,
text: finalContent,
},
],
};
} catch (error) {
Logger.error(`Tool execution failed: ${toolName}`, error);
span.end('error');
throw error;
}
};
// Register the tool
if (metadata.description) {
// With description
server.tool(toolName, metadata.description, schemaObj, toolHandler);
} else {
// Without description
server.tool(toolName, schemaObj, toolHandler);
}
Logger.info(`Registered prompt as tool`, {
name: toolName,
variables: variables.length > 0 ? variables : ['input'],
});
}
}
/**
* Load all prompts from the prompts directory
*/
export async function loadPrompts(
server: McpServer,
resourceMap: Map<string, string>,
): Promise<void> {
const span = Logger.span('loadPrompts');
try {
// Get the prompts directory
const promptsDir = await getPromptsDir();
await fs.mkdir(promptsDir, { recursive: true });
Logger.debug(`Ensuring prompts directory exists`, { path: promptsDir });
const files = await fs.readdir(promptsDir);
Logger.info(`Found ${files.length} potential prompt files`, {
directory: promptsDir,
});
let loadedCount = 0;
let toolCount = 0;
for (const file of files) {
const promptSpan = Logger.span('processPromptFile', { file });
if (!file.endsWith('.txt')) {
Logger.trace(`Skipping non-txt file`, { file });
promptSpan.end('skipped_non_txt');
continue;
}
const filePath = path.join(promptsDir, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
Logger.trace(`Skipping directory`, { path: filePath });
promptSpan.end('skipped_directory');
continue;
}
// Parse the prompt name from the filename
const promptName = path.basename(file, '.txt');
try {
// Load the raw content
const rawContent = await loadFile(filePath);
// Parse metadata
const { metadata, content } = parseMetadata(rawContent);
Logger.debug(`Processing prompt content`, {
name: promptName,
rawLength: content.length,
hasDescription: !!metadata.description,
registerAsTool: !!metadata.registerAsTool,
});
// Process content to embed resources
const processedContent = await processPromptContent(
content,
resourceMap,
);
// Register the prompt (and optionally as a tool)
registerPrompt(server, promptName, processedContent, metadata);
loadedCount++;
if (metadata.registerAsTool) {
toolCount++;
}
// Extract variables for logging
const variables = extractVariables(processedContent);
Logger.info(`Registered prompt`, {
name: promptName,
rawLength: content.length,
processedLength: processedContent.length,
description: metadata.description
? metadata.description.substring(0, 50) + '...'
: 'None',
isTool: metadata.registerAsTool,
variables: variables.length > 0 ? variables : ['input'],
});
promptSpan.end('success');
} catch (error) {
Logger.error(`Failed to process prompt file`, error, {
path: filePath,
});
promptSpan.end('error');
}
}
Logger.info(`Successfully loaded prompts`, {
total: loadedCount,
tools: toolCount,
});
span.end('success');
} catch (error) {
Logger.error('Failed to load prompts directory', error);
span.end('error');
}
}