This document tracks implementation progress and next steps for peppi-mcp.
-
Package Structure
- Created src/, bin/ directories
- Project.toml with core dependencies
- Module structure following spec
-
MCP Server Core
- JSON-RPC 2.0 protocol implementation
- stdio transport (readline from stdin, write to stdout)
- tools/list and tools/call handlers
- Proper error handling and response formatting
-
Tool Definitions
generate_statswith complete input schemasearch_replayswith complete input schema- All parameters properly typed and documented
-
Module Stubs
parsing.jl— directory scanning, GameData structuresstats.jl— statistics structures and calculation outlinesembeddings.jl— feature extraction and similarity functionssearch.jl— query parsing and ranking logic
-
Documentation
- README.md with usage instructions
- Configuration examples for Claude Desktop/Code
- .gitignore for Julia projects
Status: Blocked on peppi-jl availability
What's needed:
-
Add peppi-jl dependency to Project.toml
[deps] # Once peppi-jl is registered or via URL: # peppi_jl = "..."
-
Implement
parse_replay()in parsing.jlusing peppi_jl # or whatever the package is called function parse_replay(path::String)::Union{GameData,Nothing} try game = peppi_jl.read_game(path) # Extract metadata metadata = GameMetadata( stage = game.start.stage, players = [ Dict( "port" => p.port, "character" => p.character_id, "connect_code" => p.connect_code ) for p in game.start.players ], start_time = game.start.timestamp, duration_frames = length(game.frames.id), version = game.start.version, winner = determine_winner(game) ) # frames is already an Arrow.Table from peppi-jl return GameData(path, metadata, game.frames) catch e @error "Failed to parse $path" exception=e return nothing end end
-
Implement frame data access patterns
Need to understand the Arrow schema from peppi-jl:
# What columns exist in game.frames? # - frames.id (frame index) # - frames.ports[0].pre.state (action state) # - frames.ports[0].pre.position_x, position_y # - frames.ports[0].post.percent # - frames.ports[0].post.stocks_remaining # ... etc
-
Map action state constants
Import from ssbm-data or peppi's constants:
const ACTION_STATES = peppi_jl.ActionState # or manually maintain Julia const dict
Priority: High
-
Core Stats (stats.jl)
- Implement
calculate_core_stats()by iterating frames - Track stocks via
post.stocks_remainingchanges - Accumulate damage dealt/taken via
post.percentdeltas - Detect self-destructs (death while opponent stocks unchanged)
- Implement
-
Neutral Game Analysis
- Define "neutral interaction": both players in non-hitstun states
- Opening: transition from neutral to opponent in hitstun
- Opening rate: openings / neutral interactions
- Damage efficiency: damage per opening
-
Punish Detection (stats.jl)
detect_punishes(): scan for hitstun → escape sequences- Track damage dealt during hitstun window
- Conversion: punish that ends in stock loss
- Zero-to-death: opponent at 0% at punish start, dies at end
-
Movement Tech Detection (stats.jl)
- L-cancel:
LandingFallSpecialstate, check next frame for reduced lag- Requires per-character frame data (build lookup table)
- Wavedash:
JumpF/JumpB→ aerial → airdodge → land sequence - Dash dance:
Dashstate with rapid direction reversals - Ledgedash:
CliffCatch→ airdodge → land
- L-cancel:
-
Per-Game Granularity
- Calculate stats for each game individually
- Store in
stats.per_game_statsarray - Return both aggregated and per-game data
Priority: Medium
-
Frame Feature Aggregation (embeddings.jl)
function aggregate_frame_features(frames::Arrow.Table) # Access frames.ports[port].pre.*, frames.ports[port].post.* # Position stats pos_x = frames.ports[port].pre.position_x pos_y = frames.ports[port].pre.position_y mean_x, std_x = mean(pos_x), std(pos_x) mean_y, std_y = mean(pos_y), std(pos_y) # Action state histogram states = frames.ports[port].pre.state state_counts = countmap(states) # requires StatsBase top_states = sort(collect(state_counts), by=x->x[2], rev=true)[1:20] # Damage trajectory percents = frames.ports[port].post.percent mean_pct, std_pct = mean(percents), std(percents) # etc. return [mean_x, std_x, mean_y, std_y, ..., histogram..., mean_pct, std_pct, ...] end
-
Metadata Feature Encoding (embeddings.jl)
- Character one-hot: map
game.metadata.players[i].characterto CHARACTERS dict - Stage one-hot: map
game.metadata.stageto STAGES dict - Identify player port: match connect code or port index convention
- Determine outcome:
game.metadata.winner == player_port
- Character one-hot: map
-
Query Embedding (search.jl v2)
- Current: keyword-based, returns zero vector
- Upgrade: use Anthropic text embeddings API
using HTTP, JSON3 function generate_query_embedding(query::SearchQuery) # Call Anthropic embedding endpoint (if available) # For now, keyword matching is sufficient end
-
Index Persistence (search.jl)
- Cache embeddings to avoid re-parsing
- Use Arrow file:
Arrow.write("index.arrow", embeddings_table) - Check file hashes to detect changed replays
- Store in
PEPPI_MCP_INDEX_DIRenv var location
Priority: Low (post-v0.1.0)
-
Yggdrasil Recipe
- Write
build_tarballs.jlfor peppi (Rust → libpeppi.so/dylib/dll) - Submit PR to JuliaPackaging/Yggdrasil
- Wait for
peppi_jllto be generated
- Write
-
Package Registration
- Register peppi-jl to Julia General (if not already)
- Register peppi-mcp to Julia General
- Update installation instructions to use Pkg.add
Create test/runtests.jl:
using Test
using PeppiMCP
@testset "Parsing" begin
# Test directory scanning
files = scan_directory("test_replays/")
@test length(files) > 0
@test all(endswith(f, ".slp") for f in files)
end
@testset "Statistics" begin
# Test with known replay
# (requires test fixture .slp file)
end
@testset "Embeddings" begin
# Test feature dimension consistency
# Test normalization
end
@testset "Search" begin
# Test query parsing
@test extract_character_from_text("Fox vs Marth") == "Fox"
@test extract_stage_from_text("on FD") == "Final Destination"
end-
MCP Protocol Test
- Send JSON-RPC requests via stdin
- Verify response format
- Check error handling
-
End-to-End Test
- Place test replays in
test_replays/ - Call
generate_statswith dir - Verify stats structure
- Place test replays in
- peppi-jl unavailable: Blocking progress on actual .slp parsing
- ModelContextProtocol.jl: May need to use ClaudeMCPTools.jl instead if MCP.jl not available
- Arrow schema unknown: Need to inspect peppi-jl output to understand column names
- Action state constants: Need to import or vendor ssbm-data mappings
- Streaming stats: Incremental updates as replays are parsed
- Real-time index updates: Watch directory for new replays
- Advanced queries: Boolean operators, date ranges, matchup filters
- Visualizations: Return matplotlib/Makie plots as ImageContent
- Replay comparison: Compare two sets of replays
- Improvement tracking: Time-series analysis of skill progression
- Database backend: SQLite for persistent stats storage
- Is peppi-jl API stable enough to depend on?
- What is the exact Arrow schema from peppi-jl.read_game()?
- Should we vendor peppi-jl source or wait for package registry?
- Is ModelContextProtocol.jl the preferred MCP library, or should we use ClaudeMCPTools.jl?