mcp-ai-agent-guidelines
Version:
A comprehensive Model Context Protocol server providing advanced tools, resources, and prompts for implementing AI agent best practices
284 lines • 10.2 kB
JavaScript
import { REFERENCE_DISCLAIMER } from "./constants.js";
import { exportAsCSV, exportAsLaTeX } from "./export-utils.js";
/**
* Escape a string value for safe use in YAML frontmatter.
* Handles special YAML characters, multiline strings, and YAML terminators.
*/
export function escapeYamlValue(value) {
// Handle empty strings
if (!value)
return "''";
// Check if the value contains characters that need escaping
const needsEscaping = value.includes("'") ||
value.includes('"') ||
value.includes("\n") ||
value.includes("\r") ||
value.includes("---") ||
value.includes(":") ||
value.includes("[") ||
value.includes("]") ||
value.includes("{") ||
value.includes("}") ||
value.includes("#") ||
value.includes("&") ||
value.includes("*") ||
value.includes("!") ||
value.includes("|") ||
value.includes(">") ||
value.includes("@") ||
value.includes("`") ||
value.trim() !== value; // Leading/trailing whitespace
if (!needsEscaping) {
return `'${value}'`;
}
// For multiline strings or strings with YAML terminators, use literal block scalar
if (value.includes("\n") || value.includes("---")) {
// Use literal block scalar (|) for multiline strings
// Indent each line by 2 spaces
const lines = value.split("\n");
return `|\n ${lines.join("\n ")}`;
}
// For single-line strings with quotes, escape single quotes by doubling them
return `'${value.replace(/'/g, "''")}'`;
}
export function slugify(text, maxLength = 80) {
const slug = text
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
// Truncate to maxLength if necessary
return slug.length > maxLength ? slug.slice(0, maxLength) : slug;
}
export function buildFrontmatter({ mode, model, tools, description, }) {
const lines = ["---"];
if (mode)
lines.push(`mode: ${escapeYamlValue(mode)}`);
if (model)
lines.push(`model: ${model}`);
if (tools?.length)
lines.push(`tools: [${tools.map((t) => escapeYamlValue(t)).join(", ")}]`);
// Escape and add description
lines.push(`description: ${escapeYamlValue(description)}`);
lines.push("---");
return lines.join("\n");
}
// Import MODEL_ALIASES from generated types
import { MODEL_ALIASES } from "../config/generated/index.js";
// Policy: enforce allowed modes/models/tools and normalize casing
const ALLOWED_MODES = new Set(["agent"]);
const ALLOWED_TOOLS = new Set(["githubRepo", "codebase", "editFiles"]);
export function validateAndNormalizeFrontmatter(opts) {
const comments = [];
// Mode
let mode = opts.mode;
if (mode && !ALLOWED_MODES.has(mode)) {
comments.push(`# Note: Unrecognized mode '${mode}', defaulting to 'agent'`);
mode = "agent";
}
// Model normalization
let model = opts.model;
if (model) {
const key = model.toLowerCase();
model = MODEL_ALIASES[key] || model;
if (!MODEL_ALIASES[key] && !Object.values(MODEL_ALIASES).includes(model)) {
comments.push(`# Note: Unrecognized model '${opts.model}'.`);
}
}
// Tools filtering
let tools = opts.tools;
if (tools?.length) {
const unknown = tools.filter((t) => !ALLOWED_TOOLS.has(t));
tools = tools.filter((t) => ALLOWED_TOOLS.has(t));
if (unknown.length) {
comments.push(`# Note: Dropped unknown tools: ${unknown.join(", ")}`);
}
}
return {
...opts,
mode,
model,
tools,
comments: comments.length ? comments : undefined,
};
}
export function buildFrontmatterWithPolicy(opts) {
const normalized = validateAndNormalizeFrontmatter(opts);
const fm = buildFrontmatter(normalized);
if (!normalized.comments?.length)
return fm;
// Insert comments after the starting '---' for visibility
const lines = fm.split("\n");
lines.splice(1, 0, ...normalized.comments);
return lines.join("\n");
}
export function buildMetadataSection(opts) {
const updated = (opts.updatedDate || new Date()).toISOString().slice(0, 10);
const lines = [
"### Metadata",
`- Updated: ${updated}`,
`- Source tool: ${opts.sourceTool}`,
];
if (opts.inputFile)
lines.push(`- Input file: ${opts.inputFile}`);
if (opts.filenameHint)
lines.push(`- Suggested filename: ${opts.filenameHint}`);
lines.push("");
return lines.join("\n");
}
/**
* Builds a "Further Reading" section with a disclaimer about external references.
*
* The disclaimer clarifies that:
* - References are provided for informational purposes only
* - No endorsement or affiliation is implied
* - Information may change over time
* - Users should verify with official sources
*
* This approach follows open-source best practices for referencing external resources
* without creating legal liability or implying endorsement.
*/
export function buildFurtherReadingSection(refs) {
if (!refs || refs.length === 0)
return "";
const lines = [
"## Further Reading",
"",
REFERENCE_DISCLAIMER,
"",
...refs.map((r) => {
if (typeof r === "string") {
// Legacy format: "Title: URL"
return `- ${r}`;
}
// New format: object with title, url, and optional description
const link = `**[${r.title}](${r.url})**`;
return r.description ? `- ${link}: ${r.description}` : `- ${link}`;
}),
"",
"",
];
return lines.join("\n");
}
/**
* Apply export format to content based on output options
*
* @param content - The markdown content to export
* @param options - Output options including format and headers
* @returns Formatted content according to the specified export format
*/
export function applyExportFormat(content, options) {
// Default to markdown with headers
if (!options) {
return content;
}
const { exportFormat = "markdown", includeHeaders = true } = options;
// Remove headers if requested (for chat outputs)
let processedContent = content;
if (!includeHeaders) {
// Remove YAML frontmatter block (lines between --- ... ---)
// Handle different line endings and spacing variations
processedContent = processedContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/m, "");
// Remove markdown headers (lines starting with #)
processedContent = processedContent
.split("\n")
.filter((line) => !line.match(/^#{1,6}\s+/))
.join("\n")
.trim();
}
// Apply format-specific transformations
switch (exportFormat) {
case "latex":
return exportAsLaTeX({
title: options.documentTitle || "Document",
author: options.documentAuthor,
date: options.documentDate,
content: processedContent,
});
case "csv":
// For CSV export, we need structured data
// This is a basic implementation that converts simple markdown tables
// More complex data should use objectsToCSV from export-utils
return convertMarkdownTableToCSV(processedContent);
case "json":
// Return content as JSON string
return JSON.stringify({
content: processedContent,
metadata: {
title: options.documentTitle,
author: options.documentAuthor,
date: options.documentDate,
format: "json",
},
}, null, 2);
default:
return processedContent;
}
}
/**
* Convert a simple markdown table to CSV format
* This is a basic implementation for simple use cases
*/
function convertMarkdownTableToCSV(markdown) {
const lines = markdown.split("\n");
const tableLines = [];
// Find table lines (lines containing |)
for (const line of lines) {
if (line.includes("|")) {
// Skip separator lines (e.g., |---|---|)
if (!line.match(/^\|[\s-:|]+\|$/)) {
tableLines.push(line);
}
}
}
if (tableLines.length === 0) {
// No table found, return simple CSV with content as single cell
return `"${markdown.replace(/"/g, '""')}"`;
}
// Parse table rows
const rows = tableLines.map((line) => line
.split("|")
.map((cell) => cell.trim())
.filter((cell) => cell !== ""));
return exportAsCSV({
headers: rows[0] || [],
rows: rows.slice(1),
includeHeaders: true,
});
}
/**
* Build optional sections based on config flags.
* Reduces repetitive conditional logic like:
* const metadata = config.includeMetadata ? buildMetadata(config) : "";
*
* @example
* const sections = buildOptionalSections(config, [
* { key: 'includeMetadata', builder: (c) => buildMetadata(c) },
* { key: 'includeReferences', builder: (c) => buildReferences(c) }
* ]);
*/
export function buildOptionalSections(config, sectionMap) {
return sectionMap
.filter(({ key }) => config[key])
.map(({ builder }) => builder(config));
}
/**
* Build optional sections as an object with named keys.
* Similar to buildOptionalSections but returns an object instead of array,
* making it safer for destructuring with specific section names.
*
* @example
* const { metadata, references } = buildOptionalSectionsMap(config, {
* metadata: { key: 'includeMetadata', builder: (c) => buildMetadata(c) },
* references: { key: 'includeReferences', builder: (c) => buildReferences(c) }
* });
*/
export function buildOptionalSectionsMap(config, sectionMap) {
const result = {};
for (const [name, builder] of Object.entries(sectionMap)) {
result[name] = config[builder.key] ? builder.builder(config) : "";
}
return result;
}
//# sourceMappingURL=prompt-utils.js.map