import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Define configuration schema
export const configSchema = z.object({
debug: z.boolean().default(false).describe("Enable debug logging"),
});
// Define spell-related types
const SpellSchool = z.enum([
"abjuration",
"conjuration",
"divination",
"enchantment",
"evocation",
"illusion",
"necromancy",
"transmutation",
]);
const SpellClass = z.enum([
"barbarian",
"bard",
"cleric",
"druid",
"fighter",
"monk",
"paladin",
"ranger",
"rogue",
"sorcerer",
"warlock",
"wizard",
"artificer",
]);
export default function createStatelessServer({
config,
}: {
config: z.infer<typeof configSchema>;
}) {
const server = new McpServer({
name: "Grimoire",
version: "1.0.0",
});
// Tool: Search spells with filtering
server.tool(
"search_spells",
"Search for D&D 5E spells with various filters",
{
level: z.number().min(0).max(9).optional().describe("Spell level (0-9)"),
school: SpellSchool.optional().describe("Spell school"),
class: SpellClass.optional().describe(
"Character class that can cast this spell"
),
name: z.string().optional().describe("Partial spell name to search for"),
ritual: z.boolean().optional().describe("Filter for ritual spells only"),
concentration: z
.boolean()
.optional()
.describe("Filter for concentration spells only"),
},
async ({
level,
school,
class: characterClass,
name,
ritual,
concentration,
}) => {
try {
// Build query parameters
const params = new URLSearchParams();
if (level !== undefined) params.append("level", level.toString());
if (school) params.append("school", school);
if (characterClass) params.append("class", characterClass);
if (name) params.append("name", name);
if (ritual !== undefined) params.append("ritual", ritual.toString());
if (concentration !== undefined)
params.append("concentration", concentration.toString());
// Use the correct D&D 5e API endpoint
const url = `https://www.dnd5eapi.co/api/2014/spells?${params.toString()}`;
if (config.debug) {
console.log(`Fetching spells from: ${url}`);
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
const data = await response.json();
const spells = data.results || [];
if (spells.length === 0) {
return {
content: [
{
type: "text",
text: "No spells found matching your criteria. Try adjusting your filters.",
},
],
};
}
const spellList = spells
.map(
(spell: any) =>
`• **${spell.name || "Unknown"}** (Level ${
spell.level || "Unknown"
}) - ${spell.school?.name || "Unknown School"}`
)
.join("\n");
return {
content: [
{
type: "text",
text: `Found ${spells.length} spell(s):\n\n${spellList}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error searching spells: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
// Tool: Get detailed spell information
server.tool(
"get_spell",
"Get detailed information about a specific D&D 5E spell",
{
spell_name: z.string().describe("Name of the spell to look up"),
},
async ({ spell_name }) => {
try {
// Use the correct D&D 5e API endpoint for a spell by index
const spellIndex = spell_name.toLowerCase().replace(/\s+/g, "-");
const url = `https://www.dnd5eapi.co/api/2014/spells/${encodeURIComponent(
spellIndex
)}`;
if (config.debug) {
console.log(`Fetching spell details from: ${url}`);
}
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
return {
content: [
{
type: "text",
text: `Spell \"${spell_name}\" not found. Please check the spelling and try again.`,
},
],
};
}
throw new Error(`API request failed: ${response.status}`);
}
const spell = await response.json();
// Format the spell details with safe property access
const formatComponents = (components: any) => {
if (Array.isArray(components)) {
return components.join(", ");
} else if (typeof components === "string") {
return components;
} else {
return "Unknown";
}
};
const formatDescription = (desc: any) => {
if (Array.isArray(desc)) {
return desc.join("\n\n");
} else if (typeof desc === "string") {
return desc;
} else {
return "No description available";
}
};
const details = [
`**${spell.name || "Unknown Spell"}**`,
`Level: ${spell.level || "Unknown"}`,
spell.school && `School: ${spell.school}`,
spell.casting_time && `Casting Time: ${spell.casting_time}`,
spell.range && `Range: ${spell.range}`,
spell.components &&
`Components: ${formatComponents(spell.components)}`,
spell.material && `Material: ${spell.material}`,
spell.duration && `Duration: ${spell.duration}`,
spell.concentration && "**Requires Concentration**",
spell.ritual && "**Can be cast as a ritual**",
"",
"**Description:**",
formatDescription(spell.desc),
"",
spell.higher_level && `**At Higher Levels:** ${spell.higher_level}`,
"",
spell.classes &&
Array.isArray(spell.classes) &&
spell.classes.length > 0 &&
`**Classes:** ${spell.classes.map((c: any) => c.name).join(", ")}`,
spell.subclasses &&
Array.isArray(spell.subclasses) &&
spell.subclasses.length > 0 &&
`**Subclasses:** ${spell.subclasses
.map((sc: any) => sc.name)
.join(", ")}`,
]
.filter(Boolean)
.join("\n");
return {
content: [
{
type: "text",
text: details,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error retrieving spell: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
// Tool: List available spell schools
server.tool(
"list_schools",
"List all available D&D 5E spell schools",
{},
async () => {
const schools = [
"Abjuration - Protective and defensive magic",
"Conjuration - Summoning and teleportation magic",
"Divination - Information-gathering magic",
"Enchantment - Mind-affecting magic",
"Evocation - Energy and elemental magic",
"Illusion - Deceptive and illusory magic",
"Necromancy - Death and life-force magic",
"Transmutation - Transformation and alteration magic",
];
return {
content: [
{
type: "text",
text: `**D&D 5E Spell Schools:**\n\n${schools
.map((school) => `• ${school}`)
.join("\n")}`,
},
],
};
}
);
// Tool: List available character classes
server.tool(
"list_classes",
"List all available D&D 5E character classes that can cast spells",
{},
async () => {
const classes = [
"Artificer - Magical inventors and crafters",
"Bard - Musical spellcasters",
"Cleric - Divine spellcasters",
"Druid - Nature spellcasters",
"Fighter - Martial warriors (some subclasses)",
"Monk - Martial artists (some subclasses)",
"Paladin - Divine warriors",
"Ranger - Wilderness warriors",
"Rogue - Skilled specialists (some subclasses)",
"Sorcerer - Innate spellcasters",
"Warlock - Pact spellcasters",
"Wizard - Scholarly spellcasters",
];
return {
content: [
{
type: "text",
text: `**D&D 5E Spellcasting Classes:**\n\n${classes
.map((cls) => `• ${cls}`)
.join("\n")}`,
},
],
};
}
);
// Tool: Get spells by level for a specific class
server.tool(
"get_class_spells",
"Get all spells available to a specific class at a given level",
{
class: SpellClass.describe("Character class to search spells for"),
level: z.number().min(0).max(9).describe("Spell level to search for"),
},
async ({ class: characterClass, level }) => {
try {
const params = new URLSearchParams({
class: characterClass,
level: level.toString(),
});
const url = `https://api.open5e.com/spells/?${params.toString()}`;
if (config.debug) {
console.log(
`Fetching ${characterClass} spells of level ${level}: ${url}`
);
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
const data = await response.json();
const spells = data.results || [];
if (spells.length === 0) {
return {
content: [
{
type: "text",
text: `No level ${level} spells found for ${characterClass}.`,
},
],
};
}
const spellList = spells
.map(
(spell: any) =>
`• **${spell.name || "Unknown"}** - ${
spell.school?.name || "Unknown School"
}`
)
.join("\n");
return {
content: [
{
type: "text",
text: `**${characterClass} Spells (Level ${level}):**\n\n${spellList}\n\nTotal: ${spells.length} spell(s)`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error retrieving class spells: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
return server.server;
}