Skip to main content
Glama
ADR-002-MANAGE-INVENTORY-SCHEMA.md15.6 kB
# ADR-002: manage_inventory Tool Schema Design **Status:** PROPOSED **Date:** 2025-12-19 **Deciders:** Orchestrator, Architect **Depends On:** ADR-001 (execute_action needs equipment stats) --- ## Context The RPG-Lite MCP consolidates **8 inventory-related tools** from rpg-mcp into a single `manage_inventory` tool, following the same menu-driven enum pattern established in `src/types.ts`. ### Consolidation Map | Old Tool | Lite Operation | |----------|----------------| | `create_item_template` | `give` with item definition | | `give_item` | `give` | | `remove_item` | `remove` | | `equip_item` | `equip` | | `unequip_item` | `unequip` | | `get_inventory` | `view` | | `transfer_item` | `transfer` | | `use_item` | `use` | ### Integration Requirements (from SYSTEM-INTERSECTIONS.md) Equipment must integrate with the **Effective Stats Model**: - AC bonuses from armor/shields - Stat bonuses from magic items - Speed modifiers from boots - Attack/damage bonuses from weapons **Order of Operations:** ``` Base → Equipment → Conditions → Effects → Effective ``` --- ## Decision: Enum Schemas ### InventoryOperationSchema ```typescript export const InventoryOperationSchema = z.enum([ 'give', // Add item to character (with optional item definition) 'remove', // Remove item from character 'equip', // Move item from inventory to equipment slot 'unequip', // Move item from equipment slot to inventory 'view', // List all inventory and equipped items 'transfer', // Move item between two characters 'use', // Consume or activate an item ]); export type InventoryOperation = z.infer<typeof InventoryOperationSchema>; ``` ### ItemTypeSchema ```typescript export const ItemTypeSchema = z.enum([ 'weapon', // Swords, bows, etc. 'armor', // Plate, leather, shields 'consumable', // Potions, scrolls, food 'wondrous', // Magic items (cloaks, rings) 'tool', // Thieves' tools, artisan's tools 'adventuring_gear', // Rope, torches, etc. 'treasure', // Coins, gems, art 'ammunition', // Arrows, bolts, bullets ]); export type ItemType = z.infer<typeof ItemTypeSchema>; ``` ### EquipmentSlotSchema ```typescript export const EquipmentSlotSchema = z.enum([ 'mainhand', // Primary weapon 'offhand', // Shield or secondary weapon 'armor', // Body armor 'head', // Helmets, circlets 'feet', // Boots, shoes 'hands', // Gloves, gauntlets 'cloak', // Cloaks, capes 'neck', // Amulets, necklaces 'ring1', // First ring slot 'ring2', // Second ring slot 'belt', // Belts (Belt of Giant Strength) 'quiver', // Ammunition container ]); export type EquipmentSlot = z.infer<typeof EquipmentSlotSchema>; ``` ### RaritySchema ```typescript export const RaritySchema = z.enum([ 'common', 'uncommon', 'rare', 'very_rare', 'legendary', 'artifact', ]); export type Rarity = z.infer<typeof RaritySchema>; ``` ### WeaponCategorySchema (extends ADR-001) ```typescript export const WeaponCategorySchema = z.enum([ 'simple_melee', 'simple_ranged', 'martial_melee', 'martial_ranged', ]); export type WeaponCategory = z.infer<typeof WeaponCategorySchema>; ``` ### WeaponPropertySchema ```typescript export const WeaponPropertySchema = z.enum([ 'ammunition', 'finesse', 'heavy', 'light', 'loading', 'range', 'reach', 'special', 'thrown', 'two_handed', 'versatile', ]); export type WeaponProperty = z.infer<typeof WeaponPropertySchema>; ``` ### ArmorCategorySchema ```typescript export const ArmorCategorySchema = z.enum([ 'light', // Leather, padded 'medium', // Chain shirt, breastplate 'heavy', // Plate, splint 'shield', // All shields ]); export type ArmorCategory = z.infer<typeof ArmorCategorySchema>; ``` --- ## Item Interface with MechanicalEffects Following the pattern from SYSTEM-INTERSECTIONS.md Section 10: ```typescript // Weapon-specific stats export const WeaponStatsSchema = z.object({ damageDice: z.string(), // "1d8", "2d6" damageType: DamageTypeSchema, // From types.ts category: WeaponCategorySchema, properties: z.array(WeaponPropertySchema).optional(), range: z.object({ normal: z.number(), // 30ft for shortbow long: z.number().optional(), // 120ft for shortbow }).optional(), versatileDamage: z.string().optional(), // "1d10" for longsword }).optional(); // Armor-specific stats export const ArmorStatsSchema = z.object({ baseAc: z.number(), // 14 for chain shirt category: ArmorCategorySchema, maxDexBonus: z.number().optional(), // 2 for medium armor stealthDisadvantage: z.boolean().optional(), strengthRequirement: z.number().optional(), // 15 for plate }).optional(); // Mechanical effects (stat modifiers) export const MechanicalEffectsSchema = z.object({ // Defensive acBonus: z.number().optional(), // +1 for Ring of Protection saveBonus: z.number().optional(), // +1 to all saves (Cloak of Protection) saveBonusAbility: AbilitySchema.optional(), // Specific save bonus // Offensive attackBonus: z.number().optional(), // +1 weapon damageBonus: z.number().optional(), // +1 weapon bonusDamage: z.string().optional(), // "1d6" fire (Flame Tongue) bonusDamageType: DamageTypeSchema.optional(), // Stats strBonus: z.number().optional(), dexBonus: z.number().optional(), conBonus: z.number().optional(), intBonus: z.number().optional(), wisBonus: z.number().optional(), chaBonus: z.number().optional(), // Set ability to value (Belt of Giant Strength) strSet: z.number().optional(), dexSet: z.number().optional(), conSet: z.number().optional(), // Movement speedBonus: z.number().optional(), // Boots of Speed speedMultiplier: z.number().optional(), // 2x for Boots of Speed (active) // Special advantageOn: z.array(SkillSchema).optional(), // Cloak of Elvenkind resistances: z.array(DamageTypeSchema).optional(), immunities: z.array(DamageTypeSchema).optional(), conditionImmunities: z.array(ConditionSchema).optional(), // Charges charges: z.number().optional(), maxCharges: z.number().optional(), rechargeOn: z.enum(['dawn', 'dusk', 'short_rest', 'long_rest', 'never']).optional(), }).optional(); // Full Item Schema export const ItemSchema = z.object({ // Identity id: z.string().optional(), // Auto-generated if not provided name: z.string(), description: z.string().optional(), // Classification type: ItemTypeSchema, rarity: RaritySchema.default('common'), requiresAttunement: z.boolean().default(false), attuned: z.boolean().default(false), // Equipment slot: EquipmentSlotSchema.optional(), equipped: z.boolean().default(false), // Type-specific stats weaponStats: WeaponStatsSchema.optional(), armorStats: ArmorStatsSchema.optional(), // Mechanical effects (the integration point!) mechanicalEffects: MechanicalEffectsSchema.optional(), // Economy value: z.number().optional(), // Gold pieces weight: z.number().optional(), // Pounds // Consumables quantity: z.number().default(1), usesRemaining: z.number().optional(), effect: z.string().optional(), // "Heal 2d4+2 HP" // Stacking stackable: z.boolean().default(false), }); export type Item = z.infer<typeof ItemSchema>; ``` --- ## ManageInventorySchema ```typescript // Character targeting (either/or pattern from update_character) const CharacterTargetSchema = z.object({ characterId: z.string().optional(), characterName: z.string().optional(), }).refine( data => data.characterId || data.characterName, { message: 'Either characterId or characterName is required' } ); // Operation-specific fields const GiveOperationSchema = z.object({ operation: z.literal('give'), item: ItemSchema, // Full item definition }); const RemoveOperationSchema = z.object({ operation: z.literal('remove'), itemId: z.string().optional(), itemName: z.string().optional(), quantity: z.number().optional().default(1), }); const EquipOperationSchema = z.object({ operation: z.literal('equip'), itemId: z.string().optional(), itemName: z.string().optional(), slot: EquipmentSlotSchema.optional(), // Auto-detect if not specified }); const UnequipOperationSchema = z.object({ operation: z.literal('unequip'), slot: EquipmentSlotSchema.optional(), itemId: z.string().optional(), itemName: z.string().optional(), }); const ViewOperationSchema = z.object({ operation: z.literal('view'), showEquipped: z.boolean().default(true), showInventory: z.boolean().default(true), showStats: z.boolean().default(true), // Show calculated bonuses }); const TransferOperationSchema = z.object({ operation: z.literal('transfer'), itemId: z.string().optional(), itemName: z.string().optional(), quantity: z.number().optional().default(1), toCharacterId: z.string().optional(), toCharacterName: z.string().optional(), }); const UseOperationSchema = z.object({ operation: z.literal('use'), itemId: z.string().optional(), itemName: z.string().optional(), targetId: z.string().optional(), // For healing potions, etc. targetName: z.string().optional(), }); // Unified schema with discriminated union export const ManageInventorySchema = z.intersection( CharacterTargetSchema, z.discriminate [ GiveOperationSchema, RemoveOperationSchema, EquipOperationSchema, UnequipOperationSchema, ViewOperationSchema, TransferOperationSchema, UseOperationSchema, ]) ); // Batch support (matches update_character pattern) export const ManageInventoryBatchSchema = z.union([ ManageInventorySchema, z.object({ batch: z.array(ManageInventorySchema).min(1).max(20), }), ]); ``` --- ## Integration Points ### 1. With update_character When equipment is equipped/unequipped, stats may need updating: ```typescript // After equipping Belt of Giant Strength if (item.mechanicalEffects?.strSet) { // Note: Don't modify base stats, use calculateEffectiveStats } ``` **Key Principle:** Equipment bonuses are calculated at runtime via `calculateEquipmentStats()`, not stored on character. ### 2. With execute_action Attack action needs weapon stats: ```typescript // In execute_action handler const weapon = getEquippedWeapon(actorId, 'mainhand'); if (weapon) { damageExpression = weapon.weaponStats.damageDice; damageType = weapon.weaponStats.damageType; attackBonus += weapon.mechanicalEffects?.attackBonus ?? 0; } ``` ### 3. With get_character Character display should show effective stats: ```typescript export function calculateEffectiveStats(characterId: string): EffectiveStats { const base = getBaseStats(characterId); // Layer 1: Equipment bonuses const equipped = getEquippedItems(characterId); let stats = applyEquipmentBonuses(base, equipped); // Layer 2: Condition effects (already implemented) stats = applyConditionEffects(characterId, stats); return stats; } ``` ### 4. With manage_condition Some equipment grants condition immunities: ```typescript // Ring of Free Action: immune to paralyzed, restrained const immunities = getConditionImmunitiesFromEquipment(characterId); if (immunities.includes(condition)) { return { blocked: true, reason: 'immune via Ring of Free Action' }; } ``` --- ## Attunement Rules Per D&D 5e: - Maximum 3 attuned items per character - Attunement requires short rest - Some items require class/race/alignment ```typescript export function canAttune(characterId: string, item: Item): boolean { if (!item.requiresAttunement) return true; const currentlyAttuned = getAttunedItems(characterId); if (currentlyAttuned.length >= 3) return false; // Check class/race requirements if specified // ... return true; } ``` --- ## Example Usage ### Give a magic sword ```json { "characterName": "Thorin", "operation": "give", "item": { "name": "Flame Tongue Longsword", "type": "weapon", "rarity": "rare", "requiresAttunement": true, "slot": "mainhand", "weaponStats": { "damageDice": "1d8", "damageType": "slashing", "category": "martial_melee", "properties": ["versatile"], "versatileDamage": "1d10" }, "mechanicalEffects": { "bonusDamage": "2d6", "bonusDamageType": "fire" }, "value": 5000 } } ``` ### Equip armor ```json { "characterName": "Thorin", "operation": "equip", "itemName": "Plate Armor" } ``` ### View inventory with stats ```json { "characterName": "Thorin", "operation": "view", "showStats": true } ``` ### Batch: Loot distribution ```json { "batch": [ { "characterName": "Thorin", "operation": "give", "item": { "name": "Gold", "type": "treasure", "quantity": 150 } }, { "characterName": "Elara", "operation": "give", "item": { "name": "Gold", "type": "treasure", "quantity": 150 } }, { "characterName": "Thorin", "operation": "give", "item": { "name": "Healing Potion", "type": "consumable", "effect": "Heal 2d4+2 HP" } } ] } ``` --- ## Implementation Order ### Phase 1: Core CRUD 1. `give` - Add items to inventory 2. `remove` - Remove items from inventory 3. `view` - List inventory (no stats yet) ### Phase 2: Equipment 4. `equip` - Move to slot, validate slot type 5. `unequip` - Move to inventory 6. Update `view` to show equipped items ### Phase 3: Stats Integration 7. Implement `calculateEquipmentStats()` 8. Integrate with `get_character` display 9. Add `showStats` to `view` operation ### Phase 4: Advanced 10. `transfer` - Character-to-character 11. `use` - Consumable activation 12. Attunement enforcement 13. Batch operations --- ## Persistence Strategy ### Option A: Items on Character JSON ```json { "id": "char_123", "name": "Thorin", "inventory": [ { "id": "item_1", "name": "Longsword", "equipped": true, "slot": "mainhand" } ] } ``` **Pros:** Single source of truth **Cons:** Character files get large ### Option B: Separate Items Store ``` data/ characters/thor_1.json items/ thor_1.json ← Thorin's items ``` **Pros:** Cleaner separation **Cons:** More files to manage ### Recommendation: Option A (start simple) Items are stored directly on character JSON. This matches how `update_character` already works and keeps all character data in one place. --- ## Consequences ### Positive - 8 tools → 1 tool (87% reduction) - Menu-driven enums for all item properties - Mechanical effects use same pattern as conditions - Full D&D 5e equipment categories supported ### Negative - Complex discriminated union schema - Equipment stats calculation adds runtime overhead - Attunement rules add validation complexity ### Risks - Item database could grow large (consider item templates) - Two-weapon fighting requires careful slot handling - Magic item identification system not specified --- ## References - [src/types.ts](../src/types.ts) - Existing enum patterns - [SYSTEM-INTERSECTIONS.md](./SYSTEM-INTERSECTIONS.md) - Section 10: Inventory ↔ Equipment - [RPG-MCP-LITE-FEATURES.md](./RPG-MCP-LITE-FEATURES.md) - Consolidation map - [ADR-001](./ADR-001-EXECUTE-ACTION-DEPENDENCIES.md) - execute_action dependencies --- _ADR-002 | manage_inventory Schema | Unified Item Management_ _Last Updated: 2025-12-19_

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/ChatRPG'

If you have feedback or need assistance with the MCP directory API, please join our Discord server