diff --git a/crates/torii/server/src/handlers/mcp.rs b/crates/torii/server/src/handlers/mcp.rs
index 310e8ed254..22c9e4a64b 100644
--- a/crates/torii/server/src/handlers/mcp.rs
+++ b/crates/torii/server/src/handlers/mcp.rs
@@ -1,14 +1,13 @@
use std::sync::Arc;
-use base64::engine::general_purpose::STANDARD;
-use base64::Engine;
use futures_util::{SinkExt, StreamExt};
use hyper::{Body, Request, Response, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
-use sqlx::{Column, Row, SqlitePool, TypeInfo};
+use sqlx::{Row, SqlitePool};
use tokio_tungstenite::tungstenite::Message;
+use super::sql::map_row_to_json;
use super::Handler;
const JSONRPC_VERSION: &str = "2.0";
@@ -88,179 +87,90 @@ impl McpHandler {
async fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse {
if request.jsonrpc != JSONRPC_VERSION {
- return JsonRpcResponse {
- jsonrpc: JSONRPC_VERSION.to_string(),
- id: request.id,
- result: None,
- error: Some(JsonRpcError {
- code: -32600,
- message: "Invalid Request".to_string(),
- data: None,
- }),
- };
+ return JsonRpcResponse::invalid_request(request.id);
}
match request.method.as_str() {
- "initialize" => JsonRpcResponse {
- jsonrpc: JSONRPC_VERSION.to_string(),
- id: request.id,
- result: Some(json!({
- "protocolVersion": MCP_VERSION,
- "serverInfo": Implementation {
- name: "torii-mcp".to_string(),
- version: env!("CARGO_PKG_VERSION").to_string(),
+ "initialize" => self.handle_initialize(request.id),
+ "tools/list" => self.handle_tools_list(request.id),
+ "tools/call" => self.handle_tools_call(request).await,
+ _ => JsonRpcResponse::method_not_found(request.id),
+ }
+ }
+
+ fn handle_initialize(&self, id: Value) -> JsonRpcResponse {
+ JsonRpcResponse::ok(
+ id,
+ json!({
+ "protocolVersion": MCP_VERSION,
+ "serverInfo": Implementation {
+ name: "torii-mcp".to_string(),
+ version: env!("CARGO_PKG_VERSION").to_string(),
+ },
+ "capabilities": ServerCapabilities {
+ tools: ToolCapabilities {
+ list_changed: true,
},
- "capabilities": ServerCapabilities {
- tools: ToolCapabilities {
- list_changed: true,
- },
- resources: ResourceCapabilities {
- subscribe: true,
- list_changed: true,
- },
+ resources: ResourceCapabilities {
+ subscribe: true,
+ list_changed: true,
},
- "instructions": r#"
-Torii - Dojo Game Indexer for Starknet
-
-Torii is a specialized indexer designed for Dojo games running on Starknet. It indexes and tracks Entity Component System (ECS) data, providing a comprehensive view of game state and history.
-
-Database Structure:
-- entities: Tracks all game entities and their current state
-- components: Stores component data associated with entities
-- models: Contains model definitions from the game
-- events: Records all game events and state changes
-- transactions: Stores all blockchain transactions affecting the game
-
-Key Features:
-1. Entity Tracking
- - Query entities by type, component, or state
- - Track entity history and state changes
- - Aggregate entity statistics
-
-2. Component Analysis
- - Retrieve component data for specific entities
- - Query entities with specific component combinations
- - Track component value changes over time
-
-3. Event History
- - Access chronological game events
- - Filter events by type, entity, or time range
- - Analyze event patterns and frequencies
-
-4. Transaction Records
- - Query game-related transactions
- - Track transaction status and effects
- - Link transactions to entity changes
-
-Available Tools:
-1. 'query': Execute custom SQL queries for complex data analysis
-2. 'schema': Retrieve database schema information to understand table structures
-
-Common Query Patterns:
-1. Entity Lookup:
- SELECT * FROM entities WHERE entity_id = X
-
-2. Component State:
- SELECT e.*, c.*
- FROM entities e
- JOIN components c ON e.id = c.entity_id
- WHERE c.name = 'position'
-
-3. Event History:
- SELECT * FROM events
- WHERE entity_id = X
- ORDER BY block_number DESC
-
-4. State Changes:
- SELECT * FROM transactions
- WHERE affected_entity_id = X
- ORDER BY block_number DESC
-
-The database is optimized for querying game state and history, allowing clients to:
-- Retrieve current game state
-- Track entity lifecycle
-- Analyze game events
-- Monitor state changes
-- Generate game statistics
-"#
- })),
- error: None,
- },
- "tools/list" => JsonRpcResponse {
- jsonrpc: JSONRPC_VERSION.to_string(),
- id: request.id,
- result: Some(json!({
- "tools": [
- {
- "name": "query",
- "description": "Execute a SQL query on the database",
- "inputSchema": {
- "type": "object",
- "properties": {
- "query": {
- "type": "string",
- "description": "SQL query to execute"
- }
- },
- "required": ["query"]
- }
- },
- {
- "name": "schema",
- "description": "Retrieve the database schema including tables, columns, and their types",
- "inputSchema": {
- "type": "object",
- "properties": {
- "table": {
- "type": "string",
- "description": "Optional table name to get schema for. If omitted, returns schema for all tables."
- }
+ },
+ "instructions": include_str!("../../static/mcp-instructions.txt")
+ }),
+ )
+ }
+
+ fn handle_tools_list(&self, id: Value) -> JsonRpcResponse {
+ JsonRpcResponse::ok(
+ id,
+ json!({
+ "tools": [
+ {
+ "name": "query",
+ "description": "Execute a SQL query on the database",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "SQL query to execute"
+ }
+ },
+ "required": ["query"]
+ }
+ },
+ {
+ "name": "schema",
+ "description": "Retrieve the database schema including tables, columns, and their types",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "table": {
+ "type": "string",
+ "description": "Optional table name to get schema for. If omitted, returns schema for all tables."
}
}
}
- ]
- })),
- error: None,
- },
- "tools/call" => {
- if let Some(params) = &request.params {
- match params.get("name").and_then(Value::as_str) {
- Some("query") => self.handle_query_tool(request).await,
- Some("schema") => self.handle_schema_tool(request).await,
- _ => JsonRpcResponse {
- jsonrpc: JSONRPC_VERSION.to_string(),
- id: request.id,
- result: None,
- error: Some(JsonRpcError {
- code: -32601,
- message: "Tool not found".to_string(),
- data: None,
- }),
- },
}
- } else {
- JsonRpcResponse {
- jsonrpc: JSONRPC_VERSION.to_string(),
- id: request.id,
- result: None,
- error: Some(JsonRpcError {
- code: -32602,
- message: "Invalid params".to_string(),
- data: None,
- }),
- }
- }
- }
- _ => JsonRpcResponse {
- jsonrpc: JSONRPC_VERSION.to_string(),
- id: request.id,
- result: None,
- error: Some(JsonRpcError {
- code: -32601,
- message: "Method not found".to_string(),
- data: None,
- }),
- },
+ ]
+ }),
+ )
+ }
+
+ async fn handle_tools_call(&self, request: JsonRpcRequest) -> JsonRpcResponse {
+ let Some(params) = &request.params else {
+ return JsonRpcResponse::invalid_params(request.id, "Missing params");
+ };
+
+ let Some(tool_name) = params.get("name").and_then(Value::as_str) else {
+ return JsonRpcResponse::invalid_params(request.id, "Missing tool name");
+ };
+
+ match tool_name {
+ "query" => self.handle_query_tool(request).await,
+ "schema" => self.handle_schema_tool(request).await,
+ _ => JsonRpcResponse::method_not_found(request.id),
}
}
@@ -278,16 +188,7 @@ The database is optimized for querying game state and history, allowing clients
// Handle notifications if needed
continue;
}
- Err(e) => JsonRpcResponse {
- jsonrpc: JSONRPC_VERSION.to_string(),
- id: Value::Null,
- result: None,
- error: Some(JsonRpcError {
- code: -32700,
- message: "Parse error".to_string(),
- data: Some(json!({ "details": e.to_string() })),
- }),
- },
+ Err(e) => JsonRpcResponse::parse_error(Value::Null, &e.to_string()),
};
if let Err(e) =
@@ -393,67 +294,11 @@ The database is optimized for querying game state and history, allowing clients
async fn handle_query_tool(&self, request: JsonRpcRequest) -> JsonRpcResponse {
if let Some(params) = request.params {
- if let Some(query) = params.get("query").and_then(Value::as_str) {
+ if let Some(query) = params.get("arguments").and_then(Value::as_str) {
match sqlx::query(query).fetch_all(&*self.pool).await {
Ok(rows) => {
- // Convert rows to JSON using the same logic as SqlHandler
- let result = rows
- .iter()
- .map(|row| {
- let mut obj = serde_json::Map::new();
- for (i, column) in row.columns().iter().enumerate() {
- let value: serde_json::Value = match column.type_info().name() {
- "TEXT" => row.get::