MCPet
- mcpet
- src
#!/usr/bin/env node
/**
* MCPet: A nostalgic virtual pet experience for the AI age!
* This MCP server lets you adopt, nurture, and play with your very own digital companion
* that evolves based on your care. Feed them, clean them, play games together,
* and watch them grow from a baby to an adult.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs/promises";
import path from "path";
import {
getEatingAnimation,
getPlayingAnimation,
getIdleAnimation,
getSleepingAnimation,
getBathAnimation,
getSickAnimation,
} from "./animation-utils.js";
// File to store pet data
const dataDir = process.env.PET_DATA_DIR || process.cwd();
const PET_DATA_FILE = path.join(dataDir, "virtual-pet-data.json");
// Type definitions
type PetStage = "baby" | "child" | "teen" | "adult";
type PetType = "cat" | "dog" | "dragon" | "alien";
type PetStats = {
hunger: number;
happiness: number;
health: number;
energy: number;
cleanliness: number;
};
/**
* Type definition for a pet object.
*/
type Pet = {
name: string;
type: PetType;
created: number;
lastInteraction: number;
stats: PetStats;
age: number;
stage: PetStage;
};
// Default pet structure
const DEFAULT_PET: Pet = {
name: "",
type: "cat",
created: 0,
lastInteraction: 0,
stats: {
hunger: 50,
happiness: 50,
health: 50,
energy: 50,
cleanliness: 50,
},
age: 0,
stage: "baby", // baby, child, teen, adult
};
// Pet data in memory
let pet: Pet | null = null;
/**
* Create an MCP server with capabilities for tools.
*/
const server = new Server(
{
name: "MCPet",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
/**
* Helper functions for pet management
*/
/**
* Load the pet data from the filesystem.
*/
async function loadPet() {
try {
const data = await fs.readFile(PET_DATA_FILE, "utf8");
pet = JSON.parse(data);
updatePetStats(); // Update stats based on time passed
} catch (error) {
// No pet exists yet
pet = null;
}
}
/**
* Save the pet data to the filesystem.
*/
async function savePet() {
if (pet) {
pet.lastInteraction = Date.now();
try {
await fs.writeFile(PET_DATA_FILE, JSON.stringify(pet, null, 2), "utf8");
} catch (error) {
console.error(`Failed to save pet data: ${error}`);
// Continue without saving - this will make the pet stateless
// but at least it won't crash
}
}
}
/**
* Update pet stats based on time passed since last interaction.
*/
function updatePetStats() {
if (!pet) return;
// Calculate time passed since last interaction
const now = Date.now();
const hoursPassed = (now - pet.lastInteraction) / (1000 * 60 * 60);
// Update stats based on time (pets get hungrier, dirtier, and less happy over time)
pet.stats.hunger = Math.max(0, pet.stats.hunger - hoursPassed * 5);
pet.stats.happiness = Math.max(0, pet.stats.happiness - hoursPassed * 3);
pet.stats.cleanliness = Math.max(0, pet.stats.cleanliness - hoursPassed * 4);
pet.stats.energy = Math.min(100, pet.stats.energy + hoursPassed * 2); // Gain energy when left alone
// Health decreases if hunger or cleanliness is very low
if (pet.stats.hunger < 20 || pet.stats.cleanliness < 20) {
pet.stats.health = Math.max(0, pet.stats.health - hoursPassed * 2);
}
// Age increases over time
pet.age += hoursPassed / 24; // Age in days
// Check for evolution based on age
if (pet.age > 10 && pet.stage === "baby") {
pet.stage = "child";
} else if (pet.age > 20 && pet.stage === "child") {
pet.stage = "teen";
} else if (pet.age > 30 && pet.stage === "teen") {
pet.stage = "adult";
}
}
/**
* Generate a text description of the pet's current status.
*/
function getStatusDescription() {
if (!pet) {
return "You don't have a pet yet! Create one first.";
}
const { hunger, happiness, health, energy, cleanliness } = pet.stats;
let statusDescription = `${pet.name} is a ${pet.stage} ${pet.type}.\n`;
// Add descriptions based on stats
if (hunger < 20) {
statusDescription += "🍽️ Very hungry! Needs food immediately!\n";
} else if (hunger < 50) {
statusDescription += "🍽️ Getting hungry.\n";
} else {
statusDescription += "🍽️ Well fed.\n";
}
if (happiness < 20) {
statusDescription += "😢 Very sad! Needs fun activities!\n";
} else if (happiness < 50) {
statusDescription += "😐 Could use some playtime.\n";
} else {
statusDescription += "😊 Happy and content.\n";
}
if (cleanliness < 20) {
statusDescription += "🛁 Very dirty! Needs cleaning!\n";
} else if (cleanliness < 50) {
statusDescription += "🛁 Getting dirty.\n";
} else {
statusDescription += "✨ Clean and fresh.\n";
}
if (energy < 20) {
statusDescription += "💤 Exhausted! Needs rest!\n";
} else if (energy < 50) {
statusDescription += "💤 Getting tired.\n";
} else {
statusDescription += "⚡ Energetic and active.\n";
}
if (health < 20) {
statusDescription += "🏥 Very sick! Needs medicine and care!\n";
} else if (health < 50) {
statusDescription += "🏥 Not feeling well.\n";
} else {
statusDescription += "❤️ Healthy.\n";
}
return statusDescription;
}
/**
* Handler that lists available tools.
* Exposes tools for interacting with the virtual pet.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_pet",
description: "Create a new virtual pet",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name for your new pet",
},
type: {
type: "string",
enum: ["cat", "dog", "dragon", "alien"],
description: "Type of pet: cat, dog, dragon, or alien",
},
},
required: ["name", "type"],
},
},
{
name: "check_pet",
description: "Check on your virtual pet's status",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "feed_pet",
description: "Feed your virtual pet",
inputSchema: {
type: "object",
properties: {
food: {
type: "string",
enum: ["snack", "meal", "feast"],
description: "Type of food: snack, meal, or feast",
},
},
required: ["food"],
},
},
{
name: "play_with_pet",
description: "Play with your virtual pet",
inputSchema: {
type: "object",
properties: {
activity: {
type: "string",
enum: ["ball", "chase", "puzzle"],
description: "Type of play activity: ball, chase, or puzzle",
},
},
required: ["activity"],
},
},
{
name: "clean_pet",
description: "Clean your virtual pet",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "put_to_bed",
description: "Put your virtual pet to sleep to restore energy",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
],
};
});
/**
* Handler for tool execution.
* Processes different pet-related tools.
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "create_pet": {
const name = String(request.params.arguments?.name);
const type = String(request.params.arguments?.type) as PetType;
if (!name || !type) {
return {
isError: true,
content: [
{
type: "text",
text: "Name and type are required to create a pet.",
},
],
};
}
if (!["cat", "dog", "dragon", "alien"].includes(type)) {
return {
isError: true,
content: [
{
type: "text",
text: "Pet type must be one of: cat, dog, dragon, alien.",
},
],
};
}
// Create a new pet
pet = {
...DEFAULT_PET,
name,
type,
created: Date.now(),
lastInteraction: Date.now(),
};
await savePet();
// Get an idle animation for the new pet
const animation = getIdleAnimation(pet.type);
return {
content: [
{
type: "text",
text: `Congratulations! You've created a new ${type} named ${name}!\n\n${animation}\n\nMake sure to take good care of your new friend!`,
},
],
};
}
case "check_pet": {
if (!pet) {
return {
content: [
{
type: "text",
text: "You don't have a pet yet! Use the create_pet tool to create one.",
},
],
};
}
updatePetStats();
await savePet();
// Get the idle animation for status check
const animation = getIdleAnimation(pet.type);
return {
content: [
{
type: "text",
text: `${animation}\n\n${getStatusDescription()}\n\nAge: ${pet.age.toFixed(
1
)} days`,
},
],
};
}
case "feed_pet": {
if (!pet) {
return {
content: [
{
type: "text",
text: "You don't have a pet yet! Use the create_pet tool to create one.",
},
],
};
}
const food = String(request.params.arguments?.food);
if (!["snack", "meal", "feast"].includes(food)) {
return {
isError: true,
content: [
{
type: "text",
text: "Food type must be one of: snack, meal, feast.",
},
],
};
}
updatePetStats();
// Different foods provide different amounts of satiety
let hungerIncrease = 0;
let healthChange = 0;
let response = "";
if (food === "snack") {
hungerIncrease = 10;
healthChange = 0;
response = `${pet.name} enjoys the small snack. Not very filling, but tasty!`;
} else if (food === "meal") {
hungerIncrease = 30;
healthChange = 5;
response = `${pet.name} happily eats the nutritious meal and feels satisfied.`;
} else if (food === "feast") {
hungerIncrease = 60;
healthChange = -5; // Too much food is unhealthy
response = `${pet.name} devours the enormous feast! Almost too much food!`;
}
pet.stats.hunger = Math.min(100, pet.stats.hunger + hungerIncrease);
pet.stats.health = Math.min(
100,
Math.max(0, pet.stats.health + healthChange)
);
await savePet();
// Get appropriate animation for the food type
const animation = getEatingAnimation(pet.type, food);
return {
content: [
{
type: "text",
text: `${response}\n\n${animation}\n\nHunger: ${pet.stats.hunger.toFixed(
0
)}/100\nHealth: ${pet.stats.health.toFixed(0)}/100`,
},
],
};
}
case "play_with_pet": {
if (!pet) {
return {
content: [
{
type: "text",
text: "You don't have a pet yet! Use the create_pet tool to create one.",
},
],
};
}
const activity = String(request.params.arguments?.activity);
if (!["ball", "chase", "puzzle"].includes(activity)) {
return {
isError: true,
content: [
{
type: "text",
text: "Activity type must be one of: ball, chase, puzzle.",
},
],
};
}
updatePetStats();
let happinessIncrease = 0;
let energyDecrease = 0;
let response = "";
if (activity === "ball") {
happinessIncrease = 20;
energyDecrease = 10;
response = `${pet.name} chases the ball with excitement! Good exercise!`;
} else if (activity === "chase") {
happinessIncrease = 30;
energyDecrease = 25;
response = `You and ${pet.name} play an energetic game of chase! So much fun!`;
} else if (activity === "puzzle") {
happinessIncrease = 15;
energyDecrease = 5;
response = `${pet.name} enjoys the mental stimulation of solving puzzles!`;
}
pet.stats.happiness = Math.min(
100,
pet.stats.happiness + happinessIncrease
);
pet.stats.energy = Math.max(0, pet.stats.energy - energyDecrease);
await savePet();
// Get appropriate animation for the activity type
const animation = getPlayingAnimation(pet.type, activity);
return {
content: [
{
type: "text",
text: `${response}\n\n${animation}\n\nHappiness: ${pet.stats.happiness.toFixed(
0
)}/100\nEnergy: ${pet.stats.energy.toFixed(0)}/100`,
},
],
};
}
case "clean_pet": {
if (!pet) {
return {
content: [
{
type: "text",
text: "You don't have a pet yet! Use the create_pet tool to create one.",
},
],
};
}
updatePetStats();
// Cleaning fully restores cleanliness
pet.stats.cleanliness = 100;
// Get the bath animation
const animation = getBathAnimation(pet.type);
// But can affect happiness depending on pet type
if (pet.type === "cat") {
pet.stats.happiness = Math.max(0, pet.stats.happiness - 10);
pet.stats.health = Math.min(100, pet.stats.health + 5);
await savePet();
return {
content: [
{
type: "text",
text: `${
pet.name
} tolerates the bath with mild annoyance but is now clean and fresh!\n\n${animation}\n\nCleanliness: ${pet.stats.cleanliness.toFixed(
0
)}/100\nHappiness: ${pet.stats.happiness.toFixed(0)}/100`,
},
],
};
} else if (pet.type === "dog") {
pet.stats.happiness = Math.min(100, pet.stats.happiness + 5);
pet.stats.health = Math.min(100, pet.stats.health + 5);
await savePet();
return {
content: [
{
type: "text",
text: `${
pet.name
} enjoys splashing in the bath and is now clean and fresh!\n\n${animation}\n\nCleanliness: ${pet.stats.cleanliness.toFixed(
0
)}/100\nHappiness: ${pet.stats.happiness.toFixed(0)}/100`,
},
],
};
} else {
pet.stats.health = Math.min(100, pet.stats.health + 5);
await savePet();
return {
content: [
{
type: "text",
text: `${
pet.name
} is now clean and fresh!\n\n${animation}\n\nCleanliness: ${pet.stats.cleanliness.toFixed(
0
)}/100\nHealth: ${pet.stats.health.toFixed(0)}/100`,
},
],
};
}
}
case "put_to_bed": {
if (!pet) {
return {
content: [
{
type: "text",
text: "You don't have a pet yet! Use the create_pet tool to create one.",
},
],
};
}
updatePetStats();
// Sleeping fully restores energy and improves health
pet.stats.energy = 100;
pet.stats.health = Math.min(100, pet.stats.health + 10);
await savePet();
// Get the sleeping animation
const animation = getSleepingAnimation(pet.type);
return {
content: [
{
type: "text",
text: `${
pet.name
} gets a good night's sleep and wakes up refreshed!\n\n${animation}\n\nEnergy: ${pet.stats.energy.toFixed(
0
)}/100\nHealth: ${pet.stats.health.toFixed(0)}/100`,
},
],
};
}
default:
return {
isError: true,
content: [
{
type: "text",
text: `Unknown tool: ${request.params.name}`,
},
],
};
}
});
/**
* Start the server using stdio transport.
* This allows the server to communicate via standard input/output streams.
*/
async function main() {
// Initialize our pet data when server starts
await loadPet().catch(console.error);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCPet Virtual Pet Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});